From ca29316774a1260afb399ab3c6bf7aca3155e63b Mon Sep 17 00:00:00 2001 From: Christoph Marzell Date: Fri, 7 Nov 2025 07:06:02 +0100 Subject: [PATCH] add fixes --- .idea/.gitignore | 8 -- .idea/material_theme_project_new.xml | 5 +- .idea/misc.xml | 4 - .idea/modules.xml | 8 -- .idea/praktikum.iml | 28 ----- .idea/vcs.xml | 2 +- Gemfile | 4 +- Gemfile.lock | 9 ++ app/assets/javascript/application.js | 9 ++ app/controllers/application_controller.rb | 7 ++ app/controllers/entries_controller.rb | 63 ++++++++-- app/models/entry.rb | 16 +++ app/models/user.rb | 34 +++++ app/views/devise/registrations/edit.html.erb | 45 +++++-- app/views/entries/_form.html.erb | 21 +++- app/views/entries/index.html.erb | 117 +++++++++++++++--- app/views/entries/show.html.erb | 8 +- app/views/layouts/application.html.erb | 11 +- config/initializers/assets.rb | 2 +- config/initializers/devise.rb | 2 +- .../20251107034228_add_fields_to_entries.rb | 7 ++ ...4403_add_required_hours_matrix_to_users.rb | 5 + ...54255_add_weekly_target_matrix_to_users.rb | 5 + db/schema.rb | 7 +- 24 files changed, 331 insertions(+), 96 deletions(-) delete mode 100644 .idea/.gitignore delete mode 100644 .idea/misc.xml delete mode 100644 .idea/modules.xml delete mode 100644 .idea/praktikum.iml create mode 100644 app/assets/javascript/application.js create mode 100644 db/migrate/20251107034228_add_fields_to_entries.rb create mode 100644 db/migrate/20251107034403_add_required_hours_matrix_to_users.rb create mode 100644 db/migrate/20251107054255_add_weekly_target_matrix_to_users.rb diff --git a/.idea/.gitignore b/.idea/.gitignore deleted file mode 100644 index 13566b8..0000000 --- a/.idea/.gitignore +++ /dev/null @@ -1,8 +0,0 @@ -# Default ignored files -/shelf/ -/workspace.xml -# Editor-based HTTP Client requests -/httpRequests/ -# Datasource local storage ignored files -/dataSources/ -/dataSources.local.xml diff --git a/.idea/material_theme_project_new.xml b/.idea/material_theme_project_new.xml index 070e376..13e0429 100644 --- a/.idea/material_theme_project_new.xml +++ b/.idea/material_theme_project_new.xml @@ -3,10 +3,7 @@ diff --git a/.idea/misc.xml b/.idea/misc.xml deleted file mode 100644 index de4910b..0000000 --- a/.idea/misc.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - - \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml deleted file mode 100644 index 6336dc1..0000000 --- a/.idea/modules.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/.idea/praktikum.iml b/.idea/praktikum.iml deleted file mode 100644 index 1bf2fc5..0000000 --- a/.idea/praktikum.iml +++ /dev/null @@ -1,28 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml index 35eb1dd..94a25f7 100644 --- a/.idea/vcs.xml +++ b/.idea/vcs.xml @@ -1,6 +1,6 @@ - + \ No newline at end of file diff --git a/Gemfile b/Gemfile index c572ebb..66dfb77 100644 --- a/Gemfile +++ b/Gemfile @@ -13,10 +13,10 @@ gem "pg", "~> 1.1" # Use the Puma web server [https://github.com/puma/puma] gem "puma", ">= 5.0" - +gem 'turbolinks', '~> 5' # Build JSON APIs with ease [https://github.com/rails/jbuilder] gem "jbuilder" - +gem 'tzinfo-data' # Use Redis adapter to run Action Cable in production # gem "redis", ">= 4.0.1" diff --git a/Gemfile.lock b/Gemfile.lock index 9689fad..53d54be 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -218,8 +218,13 @@ GEM thor (1.4.0) timeout (0.4.4) tsort (0.2.0) + turbolinks (5.2.1) + turbolinks-source (~> 5.2) + turbolinks-source (5.2.0) tzinfo (2.0.6) concurrent-ruby (~> 1.0) + tzinfo-data (1.2025.2) + tzinfo (>= 1.0.0) warden (1.2.9) rack (>= 2.0.9) web-console (4.2.1) @@ -245,8 +250,12 @@ DEPENDENCIES puma (>= 5.0) rails (~> 7.1.5, >= 7.1.5.2) sprockets-rails + turbolinks (~> 5) + tzinfo-data web-console RUBY VERSION ruby 3.0.7p220 +BUNDLED WITH + 2.5.23 diff --git a/app/assets/javascript/application.js b/app/assets/javascript/application.js new file mode 100644 index 0000000..ad20229 --- /dev/null +++ b/app/assets/javascript/application.js @@ -0,0 +1,9 @@ +//= require rails-ujs +//= require turbolinks +//= require_tree . + +import "@hotwired/turbo-rails" +import * as ActiveStorage from "@rails/activestorage" +import "channels" + +console.log("HALLO RAILS JS") \ No newline at end of file diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 09705d1..a1d23fe 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -1,2 +1,9 @@ class ApplicationController < ActionController::Base + before_action :configure_permitted_parameters, if: :devise_controller? + + + def configure_permitted_parameters + devise_parameter_sanitizer.permit(:account_update, keys: [:total_required_hours, :weekly_target_hours, required_hours_matrix: {}, required_hours_matrix: {}]) + end + end diff --git a/app/controllers/entries_controller.rb b/app/controllers/entries_controller.rb index f37e51e..029df8a 100644 --- a/app/controllers/entries_controller.rb +++ b/app/controllers/entries_controller.rb @@ -4,17 +4,50 @@ class EntriesController < ApplicationController def index @entries = current_user.entries.order(date: :desc) - @total_minutes = @entries.sum(&:total_minutes) - @remaining_minutes = [current_user.total_required_hours * 60 - @total_minutes, 0].max - if current_user.weekly_target_hours.positive? - remaining_hours = @remaining_minutes / 60.0 - weeks_remaining = (remaining_hours / current_user.weekly_target_hours).ceil - @estimated_end_date = Date.today + (weeks_remaining * 7) - else - @estimated_end_date = nil + # Gesamtzeit in Minuten + @total_minutes = @entries.sum { |e| e.hours.to_i * 60 + e.minutes.to_i } + + # Gesamtbetrag der Kilometerpauschale + @total_kilometer_pauschale = @entries.sum(&:kilometer_pauschale) + + # Zeitverbrauch je Kombination (typ + art) + @time_by_typ_art = @entries.group_by(&:praktikums_typ).transform_values do |group| + group.group_by(&:entry_art).transform_values do |entries| + entries.sum { |e| e.hours.to_i * 60 + e.minutes.to_i } + end + end + + # Verbleibende Minuten je Kombination + @remaining_minutes_matrix = {} + @time_by_typ_art.each do |typ, arts| + @remaining_minutes_matrix[typ] ||= {} + + arts.each do |art, spent_minutes| + target = current_user.required_hours_matrix.dig(typ, art).to_i * 60 + remaining = [target - spent_minutes, 0].max + @remaining_minutes_matrix[typ][art] = remaining + end + end + + # Voraussichtliches Ende je Kombination basierend auf weekly_target_matrix + @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_matrix.dig(typ, art).to_f + if weekly_hours > 0 + weeks_remaining = (hours_remaining / weekly_hours).ceil + @estimated_end_by_typ_art[typ][art] = Date.today + weeks_remaining.weeks + else + @estimated_end_by_typ_art[typ][art] = nil + end + end end end + def new @entry = current_user.entries.build @@ -29,7 +62,9 @@ class EntriesController < ApplicationController end end - def edit; end + def edit; + @entry + end def update if @entry.update(entry_params) @@ -51,6 +86,14 @@ class EntriesController < ApplicationController end def entry_params - params.require(:entry).permit(:date, :hours, :minutes) + params.require(:entry).permit( + :date, + :hours, + :minutes, + :praktikums_typ, + :entry_art, + :distance_km + ) end + end diff --git a/app/models/entry.rb b/app/models/entry.rb index 0a60029..8965c63 100644 --- a/app/models/entry.rb +++ b/app/models/entry.rb @@ -7,6 +7,22 @@ class Entry < ApplicationRecord before_save :normalize_time + PRAKTIKUMSTYPEN = %w[propädeutikum fachspezifikum] + ENTRY_ARTEN = %w[Praktikum Selbsterfahrung Supervision] + + 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 } + + def kilometer_pauschale + return 0 unless distance_km.present? + distance_km * 0.42 + end + + def jahr + date.year + end + def total_minutes hours * 60 + minutes end diff --git a/app/models/user.rb b/app/models/user.rb index c3924af..837a9c7 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -5,4 +5,38 @@ class User < ApplicationRecord :recoverable, :rememberable, :validatable has_many :entries, dependent: :destroy + + PRAKTIKUMSTYPEN = %w[propädeutikum fachspezifikum] + ENTRY_ARTEN = %w[Praktikum Selbsterfahrung Supervision] + + after_initialize :set_default, if: :new_record? + + + def set_default + self.required_hours_matrix = PRAKTIKUMSTYPEN.to_h do |typ| + [typ, ENTRY_ARTEN.to_h { |art| [art, default_hours_for(typ, art)] }] + end + self.weekly_target_matrix = { + "propädeutikum" => { "Praktikum" => 12, "Selbsterfahrung" => 1, "Supervision" => 1 }, + "fachspezifikum" => { "Praktikum" => 10, "Selbsterfahrung" => 2, "Supervision" => 2 } + } + end + + + def default_hours_for(typ, art) + case [typ, art] + when ["propädeutikum", "Praktikum"] then 480 + when ["propädeutikum", "Selbsterfahrung"] then 50 + when ["propädeutikum", "Supervision"] then 20 + when ["fachspezifikum", "Praktikum"] then 600 + when ["fachspezifikum", "Selbsterfahrung"] then 80 + when ["fachspezifikum", "Supervision"] then 40 + else 0 + end + end + + def required_hours_for(typ, art) + required_hours_matrix.dig(typ, art) || 0 + end + end diff --git a/app/views/devise/registrations/edit.html.erb b/app/views/devise/registrations/edit.html.erb index ea51f34..b63826c 100644 --- a/app/views/devise/registrations/edit.html.erb +++ b/app/views/devise/registrations/edit.html.erb @@ -8,17 +8,44 @@
-

🔧 Praktikumsziele

+

🎯 Zielstunden individuell

-
- <%= f.label :total_required_hours, "Zielstunden insgesamt", class: "form-label" %> - <%= f.number_field :total_required_hours, class: "form-control", min: 1 %> -
+ + + + + + + + + + <% User::PRAKTIKUMSTYPEN.product(User::ENTRY_ARTEN).each do |(typ, art)| %> + + + + + + <% end %> + +
TypArtZielstunden
<%= typ.capitalize %><%= art %> + <%= number_field_tag( + "user[required_hours_matrix][#{typ}][#{art}]", + current_user.required_hours_matrix.dig(typ, art), + class: "form-control", min: 0 + ) %> +
-
- <%= f.label :weekly_target_hours, "Stunden pro Woche", class: "form-label" %> - <%= f.number_field :weekly_target_hours, class: "form-control", min: 1 %> -
+
Wöchentliche Zielstunden (weekly_target_matrix)
+ <% ["propädeutikum", "fachspezifikum"].each do |typ| %> + <% ["Praktikum", "Selbsterfahrung", "Supervision"].each do |art| %> +
+ <%= label_tag "user[weekly_target_matrix][#{typ}][#{art}]", "#{typ.capitalize} – #{art}" %> + <%= number_field_tag "user[weekly_target_matrix][#{typ}][#{art}]", + current_user.weekly_target_matrix.dig(typ, art), + class: "form-control", step: 1 %> +
+ <% end %> + <% end %>
diff --git a/app/views/entries/_form.html.erb b/app/views/entries/_form.html.erb index 797fb17..569b801 100644 --- a/app/views/entries/_form.html.erb +++ b/app/views/entries/_form.html.erb @@ -12,17 +12,32 @@
<%= form.label :date, 'Datum', class: 'form-label' %> - <%= form.text_field :date, class: 'form-control flatpickr', data: { enable_time: false } %> + <%= form.text_field :date, class: 'form-control flatpickr', data: { enable_time: false } , value: (form.object.date || Date.today)%>
<%= form.label :hours, 'Stunden', class: 'form-label' %> - <%= form.number_field :hours, class: 'form-control', min: 0 %> + <%= form.number_field :hours, class: 'form-control', min: 0 , value: form.object.minutes || 2%>
<%= form.label :minutes, 'Minuten', class: 'form-label' %> - <%= form.number_field :minutes, class: 'form-control', min: 0, max: 59 %> + <%= form.number_field :minutes, class: 'form-control', min: 0, max: 59, value: form.object.minutes || 0 %> +
+ +
+ <%= form.label :praktikums_typ, 'Praktikumstyp', class: 'form-label' %> + <%= form.select :praktikums_typ, Entry::PRAKTIKUMSTYPEN, {}, class: 'form-control' %> +
+ +
+ <%= form.label :entry_art, 'Art', class: 'form-label' %> + <%= form.select :entry_art, Entry::ENTRY_ARTEN, {}, class: 'form-control' %> +
+ +
+ <%= form.label :distance_km, 'Entfernung (km) Gesamt', class: 'form-label' %> + <%= form.number_field :distance_km, class: 'form-control', min: 0 %>
diff --git a/app/views/entries/index.html.erb b/app/views/entries/index.html.erb index b3a0894..f9d5289 100644 --- a/app/views/entries/index.html.erb +++ b/app/views/entries/index.html.erb @@ -1,35 +1,124 @@ -

🕒 Praktikumszeit Tracker

+

Meine Einträge

+<% if notice %> +

<%= notice %>

+<% end %> + +

Gesamtzeit: <%= @total_minutes / 60 %> h <%= @total_minutes % 60 %> min

-

Fehlend: <%= @remaining_minutes / 60 %> h <%= @remaining_minutes % 60 %> min

-

Geplante Stunden/Woche: <%= current_user.weekly_target_hours %> h

- <% if @estimated_end_date.present? %> -

Voraussichtliches Ende: <%= @estimated_end_date.strftime("%d.%m.%Y") %>

- <% end %> - <%= link_to '➕ Neuer Eintrag', new_entry_path, class: 'btn btn-primary' %> +

Fahrtkosten gesamt: + <%= number_to_currency(@total_kilometer_pauschale, unit: "€", separator: ",", delimiter: ".") %> +

+ +
+ +

📊 Übersicht je Kombination

+ + + + + + + + + + + + + + <% ["propädeutikum", "fachspezifikum"].each do |typ| %> + <% ["Praktikum", "Selbsterfahrung", "Supervision"].each do |art| %> + <% remaining = @remaining_minutes_matrix.dig(typ, art) %> + <% soll = current_user.required_hours_matrix.dig(typ, art) %> + <% weekly = current_user.weekly_target_matrix.dig(typ, art) %> + <% ende = @estimated_end_by_typ_art.dig(typ, art) %> + + <% if remaining.present? || soll.present? || weekly.present? %> + + + + + + + + + <% end %> + <% end %> + <% end %> + +
TypArtVerbleibendSoll (h)WöchentlichVorauss. Ende
<%= typ.capitalize %><%= art %><%= remaining ? "#{remaining / 60} h #{remaining % 60} min" : "—" %><%= soll || "—" %> h<%= weekly || "—" %> h/Woche<%= ende.present? ? ende.strftime("%d.%m.%Y") : "—" %>
+ + + + <%= link_to '➕ Neuer Eintrag', new_entry_path, class: 'btn btn-light mt-3' %>
- + +
- - + + + + + <% @entries.each do |entry| %> - - - + + + + + + <% end %>
DatumStundenMinutenZeitTypArtKilometerPauschale Aktionen
<%= entry.date %><%= entry.hours %><%= entry.minutes %><%= entry.date.strftime('%d.%m.%Y') %><%= entry.hours.to_i %>h <%= entry.minutes.to_i %>min<%= entry.praktikums_typ %><%= entry.entry_art %><%= entry.distance_km.to_f %> km<%= number_to_currency(entry.kilometer_pauschale, unit: "€", separator: ",", delimiter: ".") %> <%= link_to '✏️ Bearbeiten', edit_entry_path(entry), class: 'btn btn-sm btn-primary' %> - <%= link_to '🗑️ Löschen', entry, method: :delete, data: { confirm: 'Sicher?' }, class: 'btn btn-sm btn-danger' %> + <%= link_to '🗑️ Löschen', entry_path(entry), class: 'btn btn-sm btn-danger open-delete-modal' %>
+ + + + + + diff --git a/app/views/entries/show.html.erb b/app/views/entries/show.html.erb index a1260ae..a3fd38f 100644 --- a/app/views/entries/show.html.erb +++ b/app/views/entries/show.html.erb @@ -1,9 +1,15 @@

<%= notice %>

-<%= render @entry %> +<% if @entry.present? %> + <%= render @entry %> +<% else %> +

⚠️ Kein Eintrag gefunden.

+<% end %>
+ <% if @entry.present? %> <%= link_to "Edit this entry", edit_entry_path(@entry) %> | + <% end %> <%= link_to "Back to entries", entries_path %> <%= button_to "Destroy this entry", @entry, method: :delete %> diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb index 94127ed..cfac387 100644 --- a/app/views/layouts/application.html.erb +++ b/app/views/layouts/application.html.erb @@ -1,5 +1,14 @@ + + + + + + +<%= javascript_include_tag "application.js", "data-turbo-track": "reload", defer: true %> + + <% if notice %>
<%= notice %>
<% end %> <% if alert %>
<%= alert %>
<% end %> @@ -10,7 +19,7 @@

Eingeloggt als <%= current_user.email %> | <%= link_to "Profil", edit_user_registration_path %> | - <%= link_to "Logout", destroy_user_session_path, method: :delete %> + <%= link_to "Logout", destroy_user_session_path, method: :delete, data: { turbo: false } %>

<% end %> diff --git a/config/initializers/assets.rb b/config/initializers/assets.rb index 2eeef96..26649d2 100644 --- a/config/initializers/assets.rb +++ b/config/initializers/assets.rb @@ -2,7 +2,7 @@ # Version of your assets, change this if you want to expire all your assets. Rails.application.config.assets.version = "1.0" - +Rails.application.config.assets.precompile += %w( application.js ) # Add additional assets to the asset load path. # Rails.application.config.assets.paths << Emoji.images_path diff --git a/config/initializers/devise.rb b/config/initializers/devise.rb index b5b172b..7ede34c 100644 --- a/config/initializers/devise.rb +++ b/config/initializers/devise.rb @@ -266,7 +266,7 @@ Devise.setup do |config| # config.navigational_formats = ['*/*', :html, :turbo_stream] # The default HTTP method used to sign out a resource. Default is :delete. - config.sign_out_via = :delete + config.sign_out_via = :get # ==> OmniAuth # Add a new OmniAuth provider. Check the wiki for more information on setting diff --git a/db/migrate/20251107034228_add_fields_to_entries.rb b/db/migrate/20251107034228_add_fields_to_entries.rb new file mode 100644 index 0000000..7bb853f --- /dev/null +++ b/db/migrate/20251107034228_add_fields_to_entries.rb @@ -0,0 +1,7 @@ +class AddFieldsToEntries < ActiveRecord::Migration[7.1] + def change + add_column :entries, :praktikums_typ, :string, null: false, default: "propädeutikum" + add_column :entries, :entry_art, :string, null: false, default: "Praktikum" + add_column :entries, :distance_km, :integer, null: false, default: 0 + end +end diff --git a/db/migrate/20251107034403_add_required_hours_matrix_to_users.rb b/db/migrate/20251107034403_add_required_hours_matrix_to_users.rb new file mode 100644 index 0000000..0b8674f --- /dev/null +++ b/db/migrate/20251107034403_add_required_hours_matrix_to_users.rb @@ -0,0 +1,5 @@ +class AddRequiredHoursMatrixToUsers < ActiveRecord::Migration[7.1] + def change + add_column :users, :required_hours_matrix, :jsonb, default: {}, null: false + end +end diff --git a/db/migrate/20251107054255_add_weekly_target_matrix_to_users.rb b/db/migrate/20251107054255_add_weekly_target_matrix_to_users.rb new file mode 100644 index 0000000..8adc4ea --- /dev/null +++ b/db/migrate/20251107054255_add_weekly_target_matrix_to_users.rb @@ -0,0 +1,5 @@ +class AddWeeklyTargetMatrixToUsers < ActiveRecord::Migration[7.1] + def change + add_column :users, :weekly_target_matrix, :jsonb, default: {}, null: false + end +end diff --git a/db/schema.rb b/db/schema.rb index 1c43b69..16d6e49 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_11_06_161110) do +ActiveRecord::Schema[7.1].define(version: 2025_11_07_054255) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -21,6 +21,9 @@ ActiveRecord::Schema[7.1].define(version: 2025_11_06_161110) do t.bigint "user_id", null: false t.datetime "created_at", null: false t.datetime "updated_at", null: false + t.string "praktikums_typ", default: "propädeutikum", null: false + t.string "entry_art", default: "Praktikum", null: false + t.integer "distance_km", default: 0, null: false t.index ["user_id"], name: "index_entries_on_user_id" end @@ -34,6 +37,8 @@ ActiveRecord::Schema[7.1].define(version: 2025_11_06_161110) do t.datetime "updated_at", null: false t.integer "total_required_hours", default: 480, null: false t.integer "weekly_target_hours", default: 12, null: false + t.jsonb "required_hours_matrix", default: {}, null: false + t.jsonb "weekly_target_matrix", default: {}, null: false t.index ["email"], name: "index_users_on_email", unique: true t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true end