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 %>
-
+
+
+
+ | Typ |
+ Art |
+ Zielstunden |
+
+
+
+ <% User::PRAKTIKUMSTYPEN.product(User::ENTRY_ARTEN).each do |(typ, art)| %>
+
+ | <%= 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
+ ) %>
+ |
+
+ <% end %>
+
+
-
- <%= 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
+
+
+
+
+ | Typ |
+ Art |
+ Verbleibend |
+ Soll (h) |
+ Wöchentlich |
+ Vorauss. Ende |
+
+
+
+ <% ["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? %>
+
+ | <%= typ.capitalize %> |
+ <%= art %> |
+ <%= remaining ? "#{remaining / 60} h #{remaining % 60} min" : "—" %> |
+ <%= soll || "—" %> h |
+ <%= weekly || "—" %> h/Woche |
+ <%= ende.present? ? ende.strftime("%d.%m.%Y") : "—" %> |
+
+ <% end %>
+ <% end %>
+ <% end %>
+
+
+
+
+
+ <%= link_to '➕ Neuer Eintrag', new_entry_path, class: 'btn btn-light mt-3' %>
-
+
+
| Datum |
- Stunden |
- Minuten |
+ Zeit |
+ Typ |
+ Art |
+ Kilometer |
+ Pauschale |
Aktionen |
<% @entries.each do |entry| %>
- | <%= 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' %>
|
<% end %>
+
+
+
+
+
+
+
+ Bist du sicher, dass du diesen Eintrag löschen möchtest?
+
+
+
+
+
+
+
+
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