From deb4e693eda37d7eff0b761af4dc5fe3b8f50f68 Mon Sep 17 00:00:00 2001
From: Christoph Marzell
Date: Thu, 20 Aug 2026 06:01:34 +0200
Subject: [PATCH] add filters
---
.../api/v1/dashboard_controller.rb | 4 +-
app/controllers/api/v1/entries_controller.rb | 11 ++
app/controllers/api/v1/settings_controller.rb | 36 ++++-
app/models/entry.rb | 32 +---
app/models/user.rb | 137 ++++++++++++++----
config/initializers/build_version.rb | 4 +-
...0_add_enabled_praktikums_typen_to_users.rb | 15 ++
db/schema.rb | 5 +-
frontend/src/app/core/models.ts | 3 +
frontend/src/app/pages/calendar.component.ts | 40 ++++-
frontend/src/app/pages/dashboard.component.ts | 39 +++--
frontend/src/app/pages/entries.component.ts | 69 +++++++--
.../src/app/pages/entry-form.component.ts | 94 ++++++------
frontend/src/app/pages/reports.component.ts | 67 ++++++++-
frontend/src/app/pages/settings.component.ts | 60 +++++++-
15 files changed, 468 insertions(+), 148 deletions(-)
create mode 100644 db/migrate/20260820000000_add_enabled_praktikums_typen_to_users.rb
diff --git a/app/controllers/api/v1/dashboard_controller.rb b/app/controllers/api/v1/dashboard_controller.rb
index 2feaeaf..c0af570 100644
--- a/app/controllers/api/v1/dashboard_controller.rb
+++ b/app/controllers/api/v1/dashboard_controller.rb
@@ -4,7 +4,8 @@ module Api
def show
current_user.update_required_matrices!
entries = current_user.entries.where("date <= ?", Date.current)
- progress = User::PRAKTIKUMSTYPEN.flat_map do |typ|
+ enabled_types = current_user.enabled_praktikums_typen_list
+ progress = enabled_types.flat_map do |typ|
User.entry_arten_for(typ).map do |art|
spent = entries.where(praktikums_typ: typ, entry_art: art).sum("COALESCE(hours, 0) * 60 + COALESCE(minutes, 0)").to_i
target = current_user.required_hours_for(typ, art).to_f * 60
@@ -28,6 +29,7 @@ module Api
completed_percent: total_target.positive? ? (total_spent.to_f / total_target * 100).round(1) : 0,
total_distance_km: current_user.entries.sum(:distance_km), costs_by_year: costs_by_year,
running_entry: current_user.entries.find_by(end_time: nil, beschreibung: "Timer"),
+ enabled_praktikums_typen: enabled_types,
mediation_presence_days: current_user.mediation_praesenzmodule_completed,
mediation_presence_days_required: current_user.mediation_praesenzmodule_required,
last_entry: current_user.entries.where("date <= ?", Date.current).order(date: :desc).first, progress: }
diff --git a/app/controllers/api/v1/entries_controller.rb b/app/controllers/api/v1/entries_controller.rb
index a811c94..84f6fb1 100644
--- a/app/controllers/api/v1/entries_controller.rb
+++ b/app/controllers/api/v1/entries_controller.rb
@@ -20,11 +20,16 @@ module Api
def create
entry = current_user.entries.new(entry_params)
return render json: { error: "Das Propädeutikum ist bereits abgeschlossen" }, status: :unprocessable_entity if blocked?(entry)
+ return render_disabled_type(entry.praktikums_typ) unless current_user.praktikums_typ_enabled?(entry.praktikums_typ)
entry.save ? render(json: payload(entry), status: :created) : render_validation(entry)
end
def update
return render json: { error: "Das Propädeutikum ist bereits abgeschlossen" }, status: :unprocessable_entity if params.dig(:entry, :praktikums_typ) == "propädeutikum" && current_user.praepedeutikum_abgeschlossen?
+ requested_type = params.dig(:entry, :praktikums_typ).presence || @entry.praktikums_typ
+ if requested_type != @entry.praktikums_typ && !current_user.praktikums_typ_enabled?(requested_type)
+ return render_disabled_type(requested_type)
+ end
@entry.update(entry_params) ? render(json: payload(@entry)) : render_validation(@entry)
end
@@ -37,6 +42,7 @@ module Api
return render json: { error: "Es läuft bereits ein Timer" }, status: :unprocessable_entity if current_user.entries.exists?(end_time: nil, beschreibung: "Timer")
entry = current_user.entries.new(date: Date.current, start_time: Time.current, beschreibung: "Timer", praktikums_typ: params[:typ], entry_art: params[:art])
return render json: { error: "Das Propädeutikum ist bereits abgeschlossen" }, status: :unprocessable_entity if blocked?(entry)
+ return render_disabled_type(entry.praktikums_typ) unless current_user.praktikums_typ_enabled?(entry.praktikums_typ)
entry.save ? render(json: payload(entry), status: :created) : render_validation(entry)
end
@@ -69,6 +75,11 @@ module Api
current_user.praepedeutikum_abgeschlossen? && entry.praktikums_typ == "propädeutikum"
end
+ def render_disabled_type(type)
+ label = User::PRAKTIKUMSTYP_LABELS[type.to_s] || type.to_s
+ render json: { error: "#{label} ist in den Einstellungen nicht aktiviert" }, status: :unprocessable_entity
+ end
+
def payload(entry)
entry.as_json(only: %i[id date hours minutes praktikums_typ entry_art distance_km beschreibung kosten start_time end_time lunch_break_minutes zaehlt_als_fortbildung created_at updated_at]).merge(kilometer_pauschale: entry.kilometer_pauschale)
end
diff --git a/app/controllers/api/v1/settings_controller.rb b/app/controllers/api/v1/settings_controller.rb
index c9594ba..b599e78 100644
--- a/app/controllers/api/v1/settings_controller.rb
+++ b/app/controllers/api/v1/settings_controller.rb
@@ -6,13 +6,41 @@ module Api
render json: payload
end
def update
- attributes = params.require(:settings).permit(:email, :total_required_hours, :weekly_target_hours, :praepedeutikum_done, required_hours_matrix: {}, weekly_target_matrix: {})
- current_user.update(attributes) ? render(json: payload) : render_validation(current_user)
+ attributes = params.require(:settings).permit(
+ :email,
+ :total_required_hours,
+ :weekly_target_hours,
+ :praepedeutikum_done,
+ enabled_praktikums_typen: [],
+ required_hours_matrix: {},
+ weekly_target_matrix: {}
+ )
+
+ if current_user.update(attributes)
+ current_user.update_required_matrices!
+ render json: payload
+ else
+ render_validation(current_user)
+ end
end
private
def payload
- current_user.as_json(only: %i[email total_required_hours weekly_target_hours required_hours_matrix weekly_target_matrix praepedeutikum_done]).merge(
- praktikums_typen: User::PRAKTIKUMSTYPEN, entry_arten: User::ENTRY_ARTEN, entry_arten_by_typ: User::ENTRY_ARTEN_BY_TYP)
+ current_user.as_json(
+ only: %i[
+ email
+ total_required_hours
+ weekly_target_hours
+ required_hours_matrix
+ weekly_target_matrix
+ praepedeutikum_done
+ ]
+ ).merge(
+ praktikums_typen: User::PRAKTIKUMSTYPEN,
+ enabled_praktikums_typen: current_user.enabled_praktikums_typen_list,
+ praktikums_typ_labels: User::PRAKTIKUMSTYP_LABELS,
+ entry_arten: User::ENTRY_ARTEN,
+ entry_arten_by_typ: User::ENTRY_ARTEN_BY_TYP
+ )
end
end
end
diff --git a/app/models/entry.rb b/app/models/entry.rb
index 15d6275..f3dc16c 100644
--- a/app/models/entry.rb
+++ b/app/models/entry.rb
@@ -7,34 +7,8 @@ class Entry < ApplicationRecord
# 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
+ PRAKTIKUMSTYPEN = User::PRAKTIKUMSTYPEN
+ ENTRY_ARTEN = User::ENTRY_ARTEN
LEGACY_ENTRY_ART_ALIASES = {
"Peergroup" => "Peergruppenarbeit",
@@ -428,4 +402,4 @@ class Entry < ApplicationRecord
total % 60
end
end
-end
\ No newline at end of file
+end
diff --git a/app/models/user.rb b/app/models/user.rb
index 00a63f8..bd51e20 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -13,34 +13,30 @@ class User < ApplicationRecord
PRAKTIKUMSTYPEN = %w[
propädeutikum
fachspezifikum
+ psychotherapie
+ lsb
mediation
+ coaching
+ supervision
+ sonstige
].freeze
-
- # ------------------------------------------------------------
- # Alle möglichen Entry-Arten
- # ------------------------------------------------------------
- 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
+
+ DEFAULT_ENABLED_PRAKTIKUMSTYPEN = %w[
+ propädeutikum
+ fachspezifikum
+ mediation
+ ].freeze
+
+ PRAKTIKUMSTYP_LABELS = {
+ "propädeutikum" => "Propädeutikum",
+ "fachspezifikum" => "Fachspezifikum",
+ "psychotherapie" => "Psychotherapie",
+ "lsb" => "Lebens- und Sozialberatung (LSB)",
+ "mediation" => "Mediation",
+ "coaching" => "Coaching",
+ "supervision" => "Supervision",
+ "sonstige" => "Sonstige Leistungen"
+ }.freeze
# ------------------------------------------------------------
# Arten pro Ausbildung
@@ -69,6 +65,29 @@ class User < ApplicationRecord
"Einzel-/Kleingruppensupervision",
"Eigenständige Tätigkeit"
],
+
+ "psychotherapie" => [
+ "Praxis/Fälle",
+ "Supervision",
+ "Intervision",
+ "Selbsterfahrung",
+ "Fortbildung",
+ "Literatur- und Selbststudium",
+ "Kosten",
+ "Sonstige Tätigkeit"
+ ],
+
+ "lsb" => [
+ "Theorie/Methodikseminare",
+ "Praktikum",
+ "Praktikumssupervision",
+ "Gruppenselbsterfahrung",
+ "Einzelselbsterfahrung",
+ "Peergruppenarbeit",
+ "Fortbildung",
+ "Literatur- und Selbststudium",
+ "Kosten"
+ ],
"mediation" => [
"Präsenzmodul",
@@ -80,8 +99,44 @@ class User < ApplicationRecord
"Literatur- und Selbststudium",
"Fortbildung",
"Semesterkosten"
+ ],
+
+ "coaching" => [
+ "Ausbildung/Lehrgang",
+ "Praxis/Fälle",
+ "Supervision",
+ "Intervision",
+ "Selbsterfahrung",
+ "Peergruppenarbeit",
+ "Fortbildung",
+ "Literatur- und Selbststudium",
+ "Kosten"
+ ],
+
+ "supervision" => [
+ "Ausbildung/Lehrgang",
+ "Lehrsupervision",
+ "Praxis/Fälle",
+ "Intervision",
+ "Selbsterfahrung",
+ "Fortbildung",
+ "Literatur- und Selbststudium",
+ "Kosten"
+ ],
+
+ "sonstige" => [
+ "Ausbildung/Lehrgang",
+ "Praxis/Fälle",
+ "Supervision",
+ "Selbsterfahrung",
+ "Fortbildung",
+ "Literatur- und Selbststudium",
+ "Kosten",
+ "Sonstige Tätigkeit"
]
}.freeze
+
+ ENTRY_ARTEN = ENTRY_ARTEN_BY_TYP.values.flatten.uniq.freeze
# ------------------------------------------------------------
# Mediation – zusätzliche Anforderungen
@@ -96,6 +151,8 @@ class User < ApplicationRecord
MEDIATION_EINZELSUPERVISION_MINDESTENS = 3
after_initialize :set_default, if: :new_record?
+ before_validation :normalize_enabled_praktikums_typen
+ validate :at_least_one_praktikums_typ_enabled
# ------------------------------------------------------------
# Allgemein
@@ -112,6 +169,15 @@ class User < ApplicationRecord
def self.entry_arten_for(typ)
ENTRY_ARTEN_BY_TYP[typ.to_s] || ENTRY_ARTEN
end
+
+ def enabled_praktikums_typen_list
+ values = Array(enabled_praktikums_typen).map(&:to_s)
+ PRAKTIKUMSTYPEN.select { |typ| values.include?(typ) }
+ end
+
+ def praktikums_typ_enabled?(typ)
+ enabled_praktikums_typen_list.include?(typ.to_s)
+ end
# ------------------------------------------------------------
# Matrizen aktualisieren
@@ -135,7 +201,7 @@ class User < ApplicationRecord
required[typ] ||= {}
weekly[typ] ||= {}
- ENTRY_ARTEN.each do |art|
+ self.class.entry_arten_for(typ).each do |art|
unless required[typ].key?(art)
required[typ][art] = default_hours_for(typ, art)
end
@@ -346,10 +412,11 @@ class User < ApplicationRecord
# ------------------------------------------------------------
def set_default
+ self.enabled_praktikums_typen ||= DEFAULT_ENABLED_PRAKTIKUMSTYPEN.dup
self.required_hours_matrix ||= PRAKTIKUMSTYPEN.to_h do |typ|
[
typ,
- ENTRY_ARTEN.to_h do |art|
+ self.class.entry_arten_for(typ).to_h do |art|
[art, default_hours_for(typ, art)]
end
]
@@ -358,7 +425,7 @@ class User < ApplicationRecord
self.weekly_target_matrix ||= PRAKTIKUMSTYPEN.to_h do |typ|
[
typ,
- ENTRY_ARTEN.to_h do |art|
+ self.class.entry_arten_for(typ).to_h do |art|
[art, default_weekly_target_for(typ, art)]
end
]
@@ -368,6 +435,18 @@ class User < ApplicationRecord
# ------------------------------------------------------------
# Alte Bezeichnungen normalisieren
# ------------------------------------------------------------
+
+ def normalize_enabled_praktikums_typen
+ self.enabled_praktikums_typen = PRAKTIKUMSTYPEN.select do |typ|
+ Array(enabled_praktikums_typen).map(&:to_s).include?(typ)
+ end
+ end
+
+ def at_least_one_praktikums_typ_enabled
+ return if enabled_praktikums_typen_list.any?
+
+ errors.add(:enabled_praktikums_typen, "muss mindestens einen Bereich enthalten")
+ end
def canonical_entry_art(art)
case art.to_s
diff --git a/config/initializers/build_version.rb b/config/initializers/build_version.rb
index 0b1e326..a93c30d 100644
--- a/config/initializers/build_version.rb
+++ b/config/initializers/build_version.rb
@@ -1,2 +1,2 @@
-Rails.application.config.x.build_version = "2026.08.18.1"
-Rails.logger.info("Praktikum API build 2026.08.18.1 loaded")
+Rails.application.config.x.build_version = "2026.08.20.1"
+Rails.logger.info("Praktikum API build 2026.08.20.1 loaded")
diff --git a/db/migrate/20260820000000_add_enabled_praktikums_typen_to_users.rb b/db/migrate/20260820000000_add_enabled_praktikums_typen_to_users.rb
new file mode 100644
index 0000000..52e39d3
--- /dev/null
+++ b/db/migrate/20260820000000_add_enabled_praktikums_typen_to_users.rb
@@ -0,0 +1,15 @@
+class AddEnabledPraktikumsTypenToUsers < ActiveRecord::Migration[7.1]
+ def up
+ return if column_exists?(:users, :enabled_praktikums_typen)
+
+ add_column :users,
+ :enabled_praktikums_typen,
+ :jsonb,
+ default: %w[propädeutikum fachspezifikum mediation],
+ null: false
+ end
+
+ def down
+ remove_column :users, :enabled_praktikums_typen if column_exists?(:users, :enabled_praktikums_typen)
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index db3dc2b..186f7f6 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema[7.1].define(version: 2025_12_09_043625) do
+ActiveRecord::Schema[7.1].define(version: 2026_08_20_000000) do
# These are extensions that must be enabled in order to support this database
enable_extension "pg_stat_statements"
enable_extension "plpgsql"
@@ -188,6 +188,9 @@ ActiveRecord::Schema[7.1].define(version: 2025_12_09_043625) do
t.datetime "confirmation_sent_at"
t.string "unconfirmed_email"
t.boolean "praepedeutikum_done", default: false, null: false
+ t.string "api_token_digest"
+ t.jsonb "enabled_praktikums_typen", default: ["propädeutikum", "fachspezifikum", "mediation"], null: false
+ t.index ["api_token_digest"], name: "index_users_on_api_token_digest", unique: true
t.index ["confirmation_token"], name: "index_users_on_confirmation_token"
t.index ["email"], name: "index_users_on_email", unique: true
t.index ["praepedeutikum_done"], name: "index_users_on_praepedeutikum_done"
diff --git a/frontend/src/app/core/models.ts b/frontend/src/app/core/models.ts
index 12f4bc3..d2e5234 100644
--- a/frontend/src/app/core/models.ts
+++ b/frontend/src/app/core/models.ts
@@ -49,6 +49,7 @@ export interface Dashboard {
}[];
last_entry: Entry | null;
running_entry: Entry | null;
+ enabled_praktikums_typen: string[];
mediation_presence_days: number;
mediation_presence_days_required: number;
progress: Progress[];
@@ -61,6 +62,8 @@ export interface Settings {
required_hours_matrix: Record>;
weekly_target_matrix: Record>;
praktikums_typen: string[];
+ enabled_praktikums_typen: string[];
+ praktikums_typ_labels: Record;
entry_arten: string[];
entry_arten_by_typ: Record;
}
diff --git a/frontend/src/app/pages/calendar.component.ts b/frontend/src/app/pages/calendar.component.ts
index ab751e9..a6be45b 100644
--- a/frontend/src/app/pages/calendar.component.ts
+++ b/frontend/src/app/pages/calendar.component.ts
@@ -3,7 +3,7 @@ import { RouterLink } from "@angular/router";
import { FormsModule } from "@angular/forms";
import { MATERIAL } from "../shared/material";
import { ApiService } from "../core/api.service";
-import { Entry } from "../core/models";
+import { Entry, Settings } from "../core/models";
@Component({
standalone: true,
imports: [RouterLink, FormsModule, ...MATERIAL],
@@ -27,11 +27,11 @@ import { Entry } from "../core/models";
Ausbildung
Alle
- @for (type of types; track type) { {{ type }} }
+ @for (type of types(); track type) { {{ typeLabel(type) }} }
Art
Alle
- @for (art of arts; track art) { {{ art }} }
+ @for (art of arts(); track art) { {{ art }} }
@@ -124,6 +124,24 @@ import { Entry } from "../core/models";
display: grid;
place-items: center;
}
+ :host-context(body.dark) .today {
+ background: #202b3a;
+ box-shadow: inset 0 0 0 2px #4f8ff7;
+ }
+ :host-context(body.dark) .today .day-head > span {
+ background: #4f8ff7;
+ color: #07111f;
+ font-weight: 800;
+ }
+ :host-context(body.dark) .today .day-head a {
+ color: #a9c9ff;
+ }
+ :host-context(body.dark) .today .event {
+ color: #f7f9fc;
+ }
+ :host-context(body.dark) .today .event span {
+ opacity: 0.82;
+ }
.event {
display: flex;
flex-direction: column;
@@ -155,6 +173,11 @@ import { Entry } from "../core/models";
.event.type-propaedeutikum { background:rgba(13,110,253,.14);border-left-color:#0d6efd; }
.event.type-fachspezifikum { background:rgba(25,135,84,.14);border-left-color:#198754; }
.event.type-mediation { background:rgba(253,126,20,.16);border-left-color:#fd7e14; }
+ .event.type-psychotherapie { background:rgba(124,58,237,.14);border-left-color:#7c3aed; }
+ .event.type-lsb { background:rgba(219,39,119,.14);border-left-color:#db2777; }
+ .event.type-coaching { background:rgba(8,145,178,.14);border-left-color:#0891b2; }
+ .event.type-supervision { background:rgba(202,138,4,.14);border-left-color:#ca8a04; }
+ .event.type-sonstige { background:rgba(100,116,139,.14);border-left-color:#64748b; }
@media (max-width: 700px) {
.calendar-filters mat-card-content {
gap: 8px;
@@ -243,13 +266,19 @@ export class CalendarComponent implements OnInit {
readonly entries = signal([]);
readonly loading = signal(false);
readonly weekdays = ["Mo", "Di", "Mi", "Do", "Fr", "Sa", "So"];
- readonly types = ["propädeutikum", "fachspezifikum", "mediation"];
- readonly arts = ["Praktikum", "Selbsterfahrung", "Supervision", "Fortbildung", "Semesterkosten", "Gruppenselbsterfahrung", "Theorie/Methodikseminare", "Peergruppensupervision", "Einzel-/Kleingruppensupervision", "Eigenständige Tätigkeit", "Präsenzmodul", "Peergruppenarbeit", "Fallarbeit", "Praxisseminare", "Literatur- und Selbststudium"];
+ readonly types = signal([]);
+ readonly arts = signal([]);
+ readonly typeLabels = signal>({});
typeFilter = "";
artFilter = "";
readonly todayKey = this.key(new Date());
constructor(private api: ApiService) {}
ngOnInit() {
+ this.api.settings().subscribe((settings: Settings) => {
+ this.types.set(settings.praktikums_typen);
+ this.arts.set(settings.entry_arten);
+ this.typeLabels.set(settings.praktikums_typ_labels);
+ });
this.load();
}
days() {
@@ -276,6 +305,7 @@ export class CalendarComponent implements OnInit {
return this.entries().filter((e) => e.date === key && (!this.typeFilter || e.praktikums_typ === this.typeFilter) && (!this.artFilter || e.entry_art === this.artFilter));
}
clearFilters(){this.typeFilter="";this.artFilter="";}
+ typeLabel(type:string){return this.typeLabels()[type]||type.charAt(0).toUpperCase()+type.slice(1);}
typeClass(value:string){
const normalized=value?.toLocaleLowerCase("de-AT").normalize("NFD").replace(/[\u0300-\u036f]/g,"").replace(/[^a-z0-9]+/g,"-").replace(/^-|-$/g,"");
return `type-${normalized === "propadeutikum" ? "propaedeutikum" : normalized || "unknown"}`;
diff --git a/frontend/src/app/pages/dashboard.component.ts b/frontend/src/app/pages/dashboard.component.ts
index 5bb6cc7..9937d41 100644
--- a/frontend/src/app/pages/dashboard.component.ts
+++ b/frontend/src/app/pages/dashboard.component.ts
@@ -111,18 +111,20 @@ import { Dashboard } from "../core/models";
- Mediation – Präsenztage{{ d.mediation_presence_days }} von
- {{ d.mediation_presence_days_required }} Tagen erledigt
+ @if (d.enabled_praktikums_typen.includes("mediation")) {
+ Mediation – Präsenztage{{ d.mediation_presence_days }} von
+ {{ d.mediation_presence_days_required }} Tagen erledigt
+ }
}
`,
styles: [
@@ -165,6 +167,11 @@ import { Dashboard } from "../core/models";
.progress-row.type-propaedeutikum { background:rgba(13,110,253,.08);border-left-color:#0d6efd; }
.progress-row.type-fachspezifikum { background:rgba(25,135,84,.08);border-left-color:#198754; }
.progress-row.type-mediation { background:rgba(253,126,20,.09);border-left-color:#fd7e14; }
+ .progress-row.type-psychotherapie { background:rgba(124,58,237,.08);border-left-color:#7c3aed; }
+ .progress-row.type-lsb { background:rgba(219,39,119,.08);border-left-color:#db2777; }
+ .progress-row.type-coaching { background:rgba(8,145,178,.08);border-left-color:#0891b2; }
+ .progress-row.type-supervision { background:rgba(202,138,4,.08);border-left-color:#ca8a04; }
+ .progress-row.type-sonstige { background:rgba(100,116,139,.08);border-left-color:#64748b; }
.progress-row div {
display: flex;
flex-direction: column;
@@ -223,10 +230,14 @@ export class DashboardComponent implements OnInit {
return `${Math.floor(m / 60)} h ${m % 60} min`;
}
label(v: string) {
- return v.charAt(0).toUpperCase() + v.slice(1);
+ const labels:Record={lsb:"Lebens- und Sozialberatung (LSB)",sonstige:"Sonstige Leistungen"};
+ return labels[v] || v.charAt(0).toUpperCase() + v.slice(1);
}
active(d: Dashboard) {
return d.progress.filter((r) => r.target_minutes > 0 && (this.showZero || r.percent > 0) && (this.showCompleted || r.percent < 100));
}
- typeClass(value:string){return value === "propädeutikum" ? "type-propaedeutikum" : `type-${value}`;}
+ typeClass(value:string){
+ const type=value.toLocaleLowerCase("de-AT").normalize("NFD").replace(/[\u0300-\u036f]/g,"").replace(/[^a-z0-9]+/g,"-").replace(/^-|-$/g,"");
+ return `type-${type === "propadeutikum" ? "propaedeutikum" : type || "unknown"}`;
+ }
}
diff --git a/frontend/src/app/pages/entries.component.ts b/frontend/src/app/pages/entries.component.ts
index 8a366fd..840329a 100644
--- a/frontend/src/app/pages/entries.component.ts
+++ b/frontend/src/app/pages/entries.component.ts
@@ -4,7 +4,7 @@ import { RouterLink } from "@angular/router";
import { FormsModule } from "@angular/forms";
import { MATERIAL } from "../shared/material";
import { ApiService } from "../core/api.service";
-import { Dashboard, Entry } from "../core/models";
+import { Dashboard, Entry, Settings } from "../core/models";
@Component({
standalone: true,
imports: [RouterLink, FormsModule, DatePipe, DecimalPipe, ...MATERIAL],
@@ -30,9 +30,9 @@ import { Dashboard, Entry } from "../core/models";
} @else {
timerZeiterfassungTimer starten
- Ausbildung@for(t of types;track t){{{t}}}
+ Ausbildung@for(t of types();track t){{{typeLabel(t)}}}
Art@for(a of timerArts();track a){{{a}}}
-
+
}
@@ -49,8 +49,8 @@ import { Dashboard, Entry } from "../core/models";
>AusbildungAlle
- @for (t of types; track t) {
- {{ t }}
+ @for (t of filterTypes(); track t) {
+ {{ typeLabel(t) }}
}
Von JahrAlle@for(y of years();track y){{{y}}}Ausbildung |
- {{ e.praktikums_typ }}
+ {{ typeLabel(e.praktikums_typ) }}
| Art | {{e.entry_art}} | td {
background-color: rgba(253, 126, 20, 0.135);
}
+ tr.mat-mdc-row.type-psychotherapie:nth-of-type(odd) > td { background-color:rgba(124,58,237,.055); }
+ tr.mat-mdc-row.type-psychotherapie:nth-of-type(even) > td { background-color:rgba(124,58,237,.115); }
+ tr.mat-mdc-row.type-lsb:nth-of-type(odd) > td { background-color:rgba(219,39,119,.055); }
+ tr.mat-mdc-row.type-lsb:nth-of-type(even) > td { background-color:rgba(219,39,119,.115); }
+ tr.mat-mdc-row.type-coaching:nth-of-type(odd) > td { background-color:rgba(8,145,178,.055); }
+ tr.mat-mdc-row.type-coaching:nth-of-type(even) > td { background-color:rgba(8,145,178,.115); }
+ tr.mat-mdc-row.type-supervision:nth-of-type(odd) > td { background-color:rgba(202,138,4,.055); }
+ tr.mat-mdc-row.type-supervision:nth-of-type(even) > td { background-color:rgba(202,138,4,.115); }
+ tr.mat-mdc-row.type-sonstige:nth-of-type(odd) > td { background-color:rgba(100,116,139,.055); }
+ tr.mat-mdc-row.type-sonstige:nth-of-type(even) > td { background-color:rgba(100,116,139,.115); }
tr.mat-mdc-row.type-propaedeutikum:hover > td {
background-color: rgba(13, 110, 253, 0.17);
}
@@ -215,9 +225,19 @@ import { Dashboard, Entry } from "../core/models";
tr.mat-mdc-row.type-mediation:hover > td {
background-color: rgba(253, 126, 20, 0.19);
}
+ tr.mat-mdc-row.type-psychotherapie:hover > td { background-color:rgba(124,58,237,.17); }
+ tr.mat-mdc-row.type-lsb:hover > td { background-color:rgba(219,39,119,.17); }
+ tr.mat-mdc-row.type-coaching:hover > td { background-color:rgba(8,145,178,.17); }
+ tr.mat-mdc-row.type-supervision:hover > td { background-color:rgba(202,138,4,.17); }
+ tr.mat-mdc-row.type-sonstige:hover > td { background-color:rgba(100,116,139,.17); }
tr.mat-mdc-row.type-propaedeutikum > td:first-child { border-left-color: #0d6efd; }
tr.mat-mdc-row.type-fachspezifikum > td:first-child { border-left-color: #198754; }
tr.mat-mdc-row.type-mediation > td:first-child { border-left-color: #fd7e14; }
+ tr.mat-mdc-row.type-psychotherapie > td:first-child { border-left-color:#7c3aed; }
+ tr.mat-mdc-row.type-lsb > td:first-child { border-left-color:#db2777; }
+ tr.mat-mdc-row.type-coaching > td:first-child { border-left-color:#0891b2; }
+ tr.mat-mdc-row.type-supervision > td:first-child { border-left-color:#ca8a04; }
+ tr.mat-mdc-row.type-sonstige > td:first-child { border-left-color:#64748b; }
tr.mat-mdc-row.entry-today {
font-weight: 700;
}
@@ -236,9 +256,24 @@ import { Dashboard, Entry } from "../core/models";
:host-context(body.dark) tr.mat-mdc-row.type-fachspezifikum:nth-of-type(even) > td { background-color: rgba(53, 184, 116, 0.18); }
:host-context(body.dark) tr.mat-mdc-row.type-mediation:nth-of-type(odd) > td { background-color: rgba(255, 143, 51, 0.13); }
:host-context(body.dark) tr.mat-mdc-row.type-mediation:nth-of-type(even) > td { background-color: rgba(255, 143, 51, 0.2); }
+ :host-context(body.dark) tr.mat-mdc-row.type-psychotherapie:nth-of-type(odd) > td { background-color:rgba(167,139,250,.12); }
+ :host-context(body.dark) tr.mat-mdc-row.type-psychotherapie:nth-of-type(even) > td { background-color:rgba(167,139,250,.18); }
+ :host-context(body.dark) tr.mat-mdc-row.type-lsb:nth-of-type(odd) > td { background-color:rgba(244,114,182,.12); }
+ :host-context(body.dark) tr.mat-mdc-row.type-lsb:nth-of-type(even) > td { background-color:rgba(244,114,182,.18); }
+ :host-context(body.dark) tr.mat-mdc-row.type-coaching:nth-of-type(odd) > td { background-color:rgba(34,211,238,.12); }
+ :host-context(body.dark) tr.mat-mdc-row.type-coaching:nth-of-type(even) > td { background-color:rgba(34,211,238,.18); }
+ :host-context(body.dark) tr.mat-mdc-row.type-supervision:nth-of-type(odd) > td { background-color:rgba(250,204,21,.11); }
+ :host-context(body.dark) tr.mat-mdc-row.type-supervision:nth-of-type(even) > td { background-color:rgba(250,204,21,.17); }
+ :host-context(body.dark) tr.mat-mdc-row.type-sonstige:nth-of-type(odd) > td { background-color:rgba(148,163,184,.11); }
+ :host-context(body.dark) tr.mat-mdc-row.type-sonstige:nth-of-type(even) > td { background-color:rgba(148,163,184,.17); }
:host-context(body.dark) tr.mat-mdc-row.type-propaedeutikum:hover > td { background-color: rgba(62, 139, 255, 0.25); }
:host-context(body.dark) tr.mat-mdc-row.type-fachspezifikum:hover > td { background-color: rgba(53, 184, 116, 0.25); }
:host-context(body.dark) tr.mat-mdc-row.type-mediation:hover > td { background-color: rgba(255, 143, 51, 0.27); }
+ :host-context(body.dark) tr.mat-mdc-row.type-psychotherapie:hover > td { background-color:rgba(167,139,250,.25); }
+ :host-context(body.dark) tr.mat-mdc-row.type-lsb:hover > td { background-color:rgba(244,114,182,.25); }
+ :host-context(body.dark) tr.mat-mdc-row.type-coaching:hover > td { background-color:rgba(34,211,238,.25); }
+ :host-context(body.dark) tr.mat-mdc-row.type-supervision:hover > td { background-color:rgba(250,204,21,.24); }
+ :host-context(body.dark) tr.mat-mdc-row.type-sonstige:hover > td { background-color:rgba(148,163,184,.24); }
:host-context(body.dark) tr.mat-mdc-row.entry-today > td {
box-shadow: inset 0 3px 0 #b794f4, inset 0 -3px 0 #b794f4;
}
@@ -261,17 +296,20 @@ export class EntriesComponent implements OnInit {
readonly entries = signal([]);
readonly loading = signal(false);
readonly columns = ["date", "time", "type", "art", "details", "distance", "allowance", "cost", "training", "actions"];
- readonly types = ["propädeutikum", "fachspezifikum", "mediation"];
- readonly arts: Record = {propädeutikum:["Praktikum","Selbsterfahrung","Supervision","Fortbildung","Semesterkosten"],fachspezifikum:["Praktikum","Selbsterfahrung","Supervision","Fortbildung","Semesterkosten","Gruppenselbsterfahrung","Theorie/Methodikseminare","Peergruppensupervision","Einzel-/Kleingruppensupervision","Eigenständige Tätigkeit"],mediation:["Präsenzmodul","Selbsterfahrung","Supervision","Peergruppenarbeit","Fallarbeit","Praxisseminare","Literatur- und Selbststudium","Fortbildung","Semesterkosten"]};
+ readonly types = signal([]);
+ readonly filterTypes = signal([]);
+ readonly arts = signal>({});
+ readonly typeLabels = signal>({});
readonly running=signal(null);
readonly clock=signal(Date.now());
- timerType="propädeutikum"; timerArt="Praktikum"; lunchBreak=false;
+ timerType=""; timerArt=""; lunchBreak=false;
search = "";
typ = "";
minYear: number | "" = "";
maxYear: number | "" = "";
constructor(private api: ApiService) {}
ngOnInit() {
+ this.loadSettings();
this.load();
setInterval(()=>this.clock.set(Date.now()),30_000);
}
@@ -286,7 +324,8 @@ export class EntriesComponent implements OnInit {
});
this.api.dashboard().subscribe((d:Dashboard)=>this.running.set(d.running_entry));
}
- timerArts(){const available=this.arts[this.timerType]||[];if(!available.includes(this.timerArt))this.timerArt=available[0];return available;}
+ timerArts(){const available=this.arts()[this.timerType]||[];if(!available.includes(this.timerArt))this.timerArt=available[0]||"";return available;}
+ typeLabel(type:string){return this.typeLabels()[type]||type.charAt(0).toUpperCase()+type.slice(1);}
startTimer(){this.api.startTimer(this.timerType,this.timerArt).subscribe(e=>{this.running.set(e);this.load();});}
stopTimer(entry:Entry){this.api.stopTimer(entry.id,this.lunchBreak).subscribe(()=>{this.running.set(null);this.lunchBreak=false;this.load();});}
elapsed(entry:Entry){this.clock();if(!entry.start_time)return "0 h 0 min";const minutes=Math.max(0,Math.floor((Date.now()-new Date(entry.start_time).getTime())/60000));return `${Math.floor(minutes/60)} h ${minutes%60} min`;}
@@ -328,4 +367,14 @@ export class EntriesComponent implements OnInit {
URL.revokeObjectURL(a.href);
});
}
+ private loadSettings() {
+ this.api.settings().subscribe((settings: Settings) => {
+ this.types.set(settings.enabled_praktikums_typen);
+ this.filterTypes.set(settings.praktikums_typen);
+ this.arts.set(settings.entry_arten_by_typ);
+ this.typeLabels.set(settings.praktikums_typ_labels);
+ this.timerType = settings.enabled_praktikums_typen[0] || "";
+ this.timerArt = settings.entry_arten_by_typ[this.timerType]?.[0] || "";
+ });
+ }
}
diff --git a/frontend/src/app/pages/entry-form.component.ts b/frontend/src/app/pages/entry-form.component.ts
index cbd4005..ff42c0d 100644
--- a/frontend/src/app/pages/entry-form.component.ts
+++ b/frontend/src/app/pages/entry-form.component.ts
@@ -10,6 +10,7 @@ import {
import { MatTimepickerModule } from "@angular/material/timepicker";
import { MATERIAL } from "../shared/material";
import { ApiService } from "../core/api.service";
+import { Settings } from "../core/models";
import {
AUSTRIAN_DATE_FORMATS,
AustrianDateAdapter,
@@ -100,8 +101,8 @@ import {
formControlName="praktikums_typ"
(selectionChange)="typeChanged()"
>
- @for (t of types; track t) {
- {{ t }}
+ @for (t of types(); track t) {
+ {{ typeLabel(t) }}
}
@@ -183,39 +184,11 @@ export class EntryFormComponent implements OnInit {
private readonly fb = inject(FormBuilder);
id?: number;
readonly saving = signal(false);
- readonly types = ["propädeutikum", "fachspezifikum", "mediation"];
- readonly arts: Record = {
- propädeutikum: [
- "Praktikum",
- "Selbsterfahrung",
- "Supervision",
- "Fortbildung",
- "Semesterkosten",
- ],
- fachspezifikum: [
- "Praktikum",
- "Selbsterfahrung",
- "Supervision",
- "Fortbildung",
- "Semesterkosten",
- "Gruppenselbsterfahrung",
- "Theorie/Methodikseminare",
- "Peergruppensupervision",
- "Einzel-/Kleingruppensupervision",
- "Eigenständige Tätigkeit",
- ],
- mediation: [
- "Präsenzmodul",
- "Selbsterfahrung",
- "Supervision",
- "Peergruppenarbeit",
- "Fallarbeit",
- "Praxisseminare",
- "Literatur- und Selbststudium",
- "Fortbildung",
- "Semesterkosten",
- ],
- };
+ readonly configLoading = signal(true);
+ readonly types = signal([]);
+ readonly arts = signal>({});
+ readonly typeLabels = signal>({});
+ private currentEntryType?: string;
readonly form = this.fb.nonNullable.group({
date: [this.today(), Validators.required],
start_time: [this.currentQuarter() as Date | null],
@@ -223,8 +196,8 @@ export class EntryFormComponent implements OnInit {
hours: [0, [Validators.required, Validators.min(0)]],
minutes: [0, [Validators.required, Validators.min(0), Validators.max(59)]],
break: [false],
- praktikums_typ: ["propädeutikum", Validators.required],
- entry_art: ["Praktikum", Validators.required],
+ praktikums_typ: ["", Validators.required],
+ entry_art: ["", Validators.required],
distance_km: [0, Validators.min(0)],
beschreibung: [""],
kosten: [null as number | null],
@@ -247,7 +220,9 @@ export class EntryFormComponent implements OnInit {
this.id = Number(raw);
this.api
.entry(this.id)
- .subscribe((e) =>
+ .subscribe((e) => {
+ this.currentEntryType = e.praktikums_typ;
+ this.ensureTypeAvailable(e.praktikums_typ);
this.form.patchValue({
...e,
date: this.parseDate(e.date) ?? this.today(),
@@ -255,17 +230,22 @@ export class EntryFormComponent implements OnInit {
start_time: this.parseTime(e.start_time),
end_time: this.parseTime(e.end_time),
break: e.lunch_break_minutes === 30,
- }),
- );
+ });
+ });
}
+ this.loadSettings();
}
availableArts() {
- return this.arts[this.form.controls.praktikums_typ.value] || [];
+ return this.arts()[this.form.controls.praktikums_typ.value] || [];
}
typeChanged() {
const arts = this.availableArts();
if (!arts.includes(this.form.controls.entry_art.value))
- this.form.controls.entry_art.setValue(arts[0]);
+ this.form.controls.entry_art.setValue(arts[0] || "");
+ }
+ typeLabel(type: string) {
+ return this.typeLabels()[type] ||
+ type.charAt(0).toUpperCase() + type.slice(1);
}
calculateTime() {
const { start_time: s, end_time: e, break: b } = this.form.getRawValue();
@@ -343,4 +323,32 @@ export class EntryFormComponent implements OnInit {
? `${String(value.getHours()).padStart(2, "0")}:${String(value.getMinutes()).padStart(2, "0")}`
: null;
}
+ private loadSettings() {
+ this.api.settings().subscribe({
+ next: (settings: Settings) => {
+ this.arts.set(settings.entry_arten_by_typ);
+ this.typeLabels.set(settings.praktikums_typ_labels);
+ this.types.set([...settings.enabled_praktikums_typen]);
+
+ if (this.currentEntryType) {
+ this.ensureTypeAvailable(this.currentEntryType);
+ } else {
+ const type = this.types()[0] || "";
+ this.form.patchValue({
+ praktikums_typ: type,
+ entry_art: settings.entry_arten_by_typ[type]?.[0] || "",
+ });
+ }
+ this.configLoading.set(false);
+ },
+ error: () => {
+ this.configLoading.set(false);
+ this.snack.open("Ausbildungsbereiche konnten nicht geladen werden", "OK");
+ },
+ });
+ }
+ private ensureTypeAvailable(type: string) {
+ if (!type || this.types().includes(type)) return;
+ this.types.update((types) => [...types, type]);
+ }
}
diff --git a/frontend/src/app/pages/reports.component.ts b/frontend/src/app/pages/reports.component.ts
index 82e06e0..0788dd8 100644
--- a/frontend/src/app/pages/reports.component.ts
+++ b/frontend/src/app/pages/reports.component.ts
@@ -38,7 +38,7 @@ interface Row {
@for (row of group.rows; track row.typ + row.art) {
-
{{ row.art }}{{ row.typ }}
+
{{ row.art }}{{ typeLabel(row.typ) }}
{{ duration(row.total_minutes) }}
}
@@ -58,9 +58,19 @@ interface Row {
.row.type-propaedeutikum{background:rgba(13,110,253,.08);border-left-color:#0d6efd}
.row.type-fachspezifikum{background:rgba(25,135,84,.08);border-left-color:#198754}
.row.type-mediation{background:rgba(253,126,20,.10);border-left-color:#fd7e14}
+ .row.type-psychotherapie{background:rgba(124,58,237,.09);border-left-color:#7c3aed}
+ .row.type-lsb{background:rgba(219,39,119,.09);border-left-color:#db2777}
+ .row.type-coaching{background:rgba(8,145,178,.09);border-left-color:#0891b2}
+ .row.type-supervision{background:rgba(202,138,4,.09);border-left-color:#ca8a04}
+ .row.type-sonstige{background:rgba(100,116,139,.09);border-left-color:#64748b}
:host-context(body.dark) .row.type-propaedeutikum{background:rgba(62,139,255,.15)}
:host-context(body.dark) .row.type-fachspezifikum{background:rgba(53,184,116,.15)}
:host-context(body.dark) .row.type-mediation{background:rgba(255,143,51,.17)}
+ :host-context(body.dark) .row.type-psychotherapie{background:rgba(167,139,250,.16)}
+ :host-context(body.dark) .row.type-lsb{background:rgba(244,114,182,.16)}
+ :host-context(body.dark) .row.type-coaching{background:rgba(34,211,238,.16)}
+ :host-context(body.dark) .row.type-supervision{background:rgba(250,204,21,.15)}
+ :host-context(body.dark) .row.type-sonstige{background:rgba(148,163,184,.15)}
.row:last-child{border-bottom:0}.row div{display:flex;flex-direction:column}.row small{margin-top:3px}mat-card-content{padding-top:12px!important}
@media(max-width:500px){.report{grid-template-columns:1fr}.month-heading{flex-direction:column}.current-badge{align-self:flex-start}}
`],
@@ -68,6 +78,26 @@ interface Row {
export class ReportsComponent implements OnInit {
readonly rows = signal([]);
readonly loading = signal(true);
+ private readonly typeOrder: Record = {
+ propadeutikum: 0,
+ fachspezifikum: 1,
+ psychotherapie: 2,
+ lsb: 3,
+ mediation: 4,
+ coaching: 5,
+ supervision: 6,
+ sonstige: 7,
+ };
+ private readonly typeLabels: Record = {
+ propädeutikum: "Propädeutikum",
+ fachspezifikum: "Fachspezifikum",
+ psychotherapie: "Psychotherapie",
+ lsb: "Lebens- und Sozialberatung (LSB)",
+ mediation: "Mediation",
+ coaching: "Coaching",
+ supervision: "Supervision",
+ sonstige: "Sonstige Leistungen",
+ };
constructor(private api: ApiService) {}
@@ -83,7 +113,7 @@ export class ReportsComponent implements OnInit {
for (const row of this.rows()) map.set(row.month, [...(map.get(row.month) || []), row]);
return [...map].map(([month, rows]) => ({
month,
- rows,
+ rows: this.sortRows(rows),
total: rows.reduce((sum, row) => sum + row.total_minutes, 0),
}));
}
@@ -94,13 +124,40 @@ export class ReportsComponent implements OnInit {
}
typeClass(type: string) {
- const normalized = type
- ?.toLocaleLowerCase("de-AT")
+ const normalized = this.normalizedType(type);
+ return `type-${normalized === "propadeutikum" ? "propaedeutikum" : normalized || "unknown"}`;
+ }
+
+ typeLabel(type: string) {
+ return this.typeLabels[type] || type.charAt(0).toUpperCase() + type.slice(1);
+ }
+
+ private sortRows(rows: Row[]) {
+ return [...rows].sort((a, b) => {
+ const typeA = this.normalizedType(a.typ);
+ const typeB = this.normalizedType(b.typ);
+ const rankDifference =
+ (this.typeOrder[typeA] ?? Number.MAX_SAFE_INTEGER) -
+ (this.typeOrder[typeB] ?? Number.MAX_SAFE_INTEGER);
+
+ if (rankDifference !== 0) return rankDifference;
+
+ const typeDifference = a.typ.localeCompare(b.typ, "de-AT", {
+ sensitivity: "base",
+ });
+ if (typeDifference !== 0) return typeDifference;
+
+ return a.art.localeCompare(b.art, "de-AT", { sensitivity: "base" });
+ });
+ }
+
+ private normalizedType(type: string): string {
+ return (type || "")
+ .toLocaleLowerCase("de-AT")
.normalize("NFD")
.replace(/[\u0300-\u036f]/g, "")
.replace(/[^a-z0-9]+/g, "-")
.replace(/^-|-$/g, "");
- return `type-${normalized === "propadeutikum" ? "propaedeutikum" : normalized || "unknown"}`;
}
duration(minutes: number) {
diff --git a/frontend/src/app/pages/settings.component.ts b/frontend/src/app/pages/settings.component.ts
index 1ebc829..0cf6c7b 100644
--- a/frontend/src/app/pages/settings.component.ts
+++ b/frontend/src/app/pages/settings.component.ts
@@ -30,6 +30,30 @@ import { Settings } from "../core/models";
+ Verwendete BereicheNur aktivierte Bereiche stehen bei neuen Einträgen und beim Timer zur Auswahl.
+
+ @for (typ of s.praktikums_typen; track typ) {
+ {{ label(s, typ) }}
+ }
+
+ @if (!s.enabled_praktikums_typen.length) {
+ Mindestens ein Bereich muss aktiviert sein.
+ }
+
+ Bereits vorhandene Einträge deaktivierter Bereiche bleiben in Tabellen,
+ Kalender und Berichten erhalten.
+
+
Profil
- @for (typ of s.praktikums_typen; track typ) {
+ @for (typ of enabledTypes(s); track typ) {
{{ label(typ) }}{{ label(s, typ) }}Sollstunden und durchschnittliches Wochenziel
@@ -133,6 +157,16 @@ import { Settings } from "../core/models";
.targets mat-form-field {
margin-top: 8px;
}
+ .type-options {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
+ gap: 10px 18px;
+ }
+ .selection-error {
+ color: var(--mat-sys-error);
+ font-weight: 600;
+ margin: 14px 0 0;
+ }
.footer {
justify-content: flex-end;
position: sticky;
@@ -179,8 +213,24 @@ export class SettingsComponent implements OnInit {
error: () => this.loading.set(false),
});
}
- label(v: string) {
- return v.charAt(0).toUpperCase() + v.slice(1);
+ label(settings: Settings, value: string) {
+ return settings.praktikums_typ_labels[value] ||
+ value.charAt(0).toUpperCase() + value.slice(1);
+ }
+ isTypeEnabled(settings: Settings, type: string) {
+ return settings.enabled_praktikums_typen.includes(type);
+ }
+ setTypeEnabled(settings: Settings, type: string, enabled: boolean) {
+ settings.enabled_praktikums_typen = enabled
+ ? settings.praktikums_typen.filter((value) =>
+ value === type || settings.enabled_praktikums_typen.includes(value),
+ )
+ : settings.enabled_praktikums_typen.filter((value) => value !== type);
+ }
+ enabledTypes(settings: Settings) {
+ return settings.praktikums_typen.filter((type) =>
+ settings.enabled_praktikums_typen.includes(type),
+ );
}
save() {
const s = this.settings();