Browse Source

add timer

main
Christoph Marzell 4 weeks ago
parent
commit
a2d38543ec
  1. 9
      Gemfile
  2. 22
      Gemfile.lock
  3. 4
      app/assets/javascript/application.js
  4. 3
      app/assets/javascript/jquery_ui.js
  5. 3
      app/assets/stylesheets/application.css
  6. 32
      app/controllers/entries_controller.rb
  7. 8
      app/javascript/application.js
  8. 5
      app/javascript/jquery_ui.js
  9. 25
      app/models/entry.rb
  10. 4
      app/views/entries/_form.html.erb
  11. 83
      app/views/entries/index.html.erb
  12. 17
      app/views/layouts/application.html.erb
  13. 3
      config/application.rb
  14. 3
      config/importmap.rb
  15. 2
      config/initializers/assets.rb
  16. 42
      config/routes.rb
  17. 5
      db/migrate/20251113045021_add_start_time_to_entries.rb
  18. 5
      db/migrate/20251113045025_add_end_time_to_entries.rb
  19. 5
      db/migrate/20251113045029_add_lunch_break_minutes_to_entries.rb
  20. 5
      db/schema.rb
  21. 26
      docker-entrypoint.sh

9
Gemfile

@ -17,6 +17,15 @@ gem 'turbolinks', '~> 5'
# Build JSON APIs with ease [https://github.com/rails/jbuilder]
gem "jbuilder"
gem 'tzinfo-data'
gem 'jquery-rails'
# Use jquery-ui for pretty UI
gem 'jquery-ui-rails'
# Use Sass to process CSS
gem "sassc-rails"
# Use Redis adapter to run Action Cable in production
# gem "redis", ">= 4.0.1"

22
Gemfile.lock

@ -110,6 +110,8 @@ GEM
erubi (1.13.1)
et-orbi (1.4.0)
tzinfo
ffi (1.17.2-x64-mingw32)
ffi (1.17.2-x86_64-linux-gnu)
fugit (1.12.1)
et-orbi (~> 1.4)
raabro (~> 1.4)
@ -125,6 +127,12 @@ GEM
jbuilder (2.14.1)
actionview (>= 7.0.0)
activesupport (>= 7.0.0)
jquery-rails (4.6.1)
rails-dom-testing (>= 1, < 3)
railties (>= 4.2.0)
thor (>= 0.14, < 2.0)
jquery-ui-rails (8.0.0)
railties (>= 3.2.16)
kaminari (1.2.2)
activesupport (>= 4.1.0)
kaminari-actionview (= 1.2.2)
@ -230,6 +238,16 @@ GEM
railties (>= 7.0)
rufus-scheduler (3.9.2)
fugit (~> 1.1, >= 1.11.1)
sassc (2.4.0)
ffi (~> 1.9)
sassc (2.4.0-x64-mingw32)
ffi (~> 1.9)
sassc-rails (2.1.2)
railties (>= 4.0.0)
sassc (>= 2.0)
sprockets (> 3.0)
sprockets-rails
tilt
securerandom (0.3.2)
sprockets (4.2.2)
concurrent-ruby (~> 1.0)
@ -241,6 +259,7 @@ GEM
sprockets (>= 3.0.0)
stringio (3.1.7)
thor (1.4.0)
tilt (2.6.1)
timeout (0.4.4)
tsort (0.2.0)
turbolinks (5.2.1)
@ -272,10 +291,13 @@ DEPENDENCIES
bootsnap
devise
jbuilder
jquery-rails
jquery-ui-rails
pg (~> 1.1)
puma (>= 5.0)
rails (~> 7.1.5, >= 7.1.5.2)
rufus-scheduler
sassc-rails
sprockets-rails
turbolinks (~> 5)
tzinfo-data

4
app/assets/javascript/application.js

@ -4,4 +4,6 @@ import "@hotwired/turbo-rails"
import * as ActiveStorage from "@rails/activestorage"
import "channels"
console.log("HALLO RAILS JS")
import "jquery"
import "jquery_ujs"
import "./jquery_ui"

3
app/assets/javascript/jquery_ui.js

@ -0,0 +1,3 @@
import "jquery"
import "jquery_ujs"
import "./jquery_ui"

3
app/assets/stylesheets/application.css

@ -1,3 +1,6 @@
@import "jquery-ui.css";
/*
* This is a manifest file that'll be compiled into application.css, which will include all the files
* listed below.

32
app/controllers/entries_controller.rb

@ -1,10 +1,10 @@
class EntriesController < ApplicationController
before_action :authenticate_user!
before_action :set_entry, only: %i[edit update destroy]
before_action :set_entry, only: %i[edit update destroy stop_timer]
def index
@entries = current_user.entries.order(date: :desc)
@running_entry = current_user.entries.find_by(end_time: nil, beschreibung: "Timer")
# Gesamtzeit in Minuten
@total_minutes = @entries.sum { |e| e.hours.to_i * 60 + e.minutes.to_i }
@ -60,6 +60,34 @@ class EntriesController < ApplicationController
@entry = current_user.entries.build
end
def start_timer
typ = params[:typ]
art = params[:art]
@entry = current_user.entries.create!(start_time: Time.current, beschreibung: "Timer" , praktikums_typ:typ, entry_art: art)
redirect_to entries_path, notice: "Timer gestartet"
end
# neue Aktion: Timer stoppen
def stop_timer
#@entry = current_user.entries.where(end_time: nil).order(start_time: :desc).first
if @entry
@entry.end_time = Time.current
@entry.lunch_break_minutes = params[:lunch_break_minutes].to_i
total_minutes = @entry.total_minutes_including_break
@entry.hours = total_minutes / 60
@entry.minutes = total_minutes % 60
@entry.save!
notice = "Eintrag gestoppt – Gesamtzeit: #{@entry.hours} h #{@entry.minutes} min"
else
notice = "Kein laufender Eintrag gefunden"
end
redirect_to entries_path, notice: notice
end
def create
@entry = current_user.entries.build(entry_params)
if @entry.save

8
app/javascript/application.js

@ -0,0 +1,8 @@
import "@hotwired/turbo-rails"
import * as ActiveStorage from "@rails/activestorage"
import "channels"
import "jquery"
import "jquery_ujs"
import "./jquery_ui"

5
app/javascript/jquery_ui.js

@ -0,0 +1,5 @@
import jquery from "jquery";
window.jQuery = jquery;
window.$ = jquery;

25
app/models/entry.rb

@ -4,9 +4,9 @@ class Entry < ApplicationRecord
belongs_to :user
validates :date, :hours, :minutes, presence: true
validates :hours, numericality: { only_integer: true, greater_than_or_equal_to: 0 }
validates :minutes, numericality: { only_integer: true, greater_than_or_equal_to: 0, less_than: 60 }
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? }
before_save :normalize_time
@ -112,9 +112,28 @@ class Entry < ApplicationRecord
end
end
def total_minutes_including_break
return nil unless start_time && end_time
minutes = ((end_time - start_time) / 60).to_i
minutes -= lunch_break_minutes.to_i
minutes = 0 if minutes < 0
minutes
end
def hours_and_minutes
mins = total_minutes_including_break
return [0, 0] unless mins
[mins / 60, mins % 60]
end
private
def normalize_time
self.minutes ||= 0
self.hours ||= 0
self.hours += minutes / 60
self.minutes = minutes % 60
end

4
app/views/entries/_form.html.erb

@ -43,12 +43,12 @@
<div class="col-12 col-md-4">
<%= form.label :start_time, 'Beginn (Uhrzeit)', class: 'form-label' %>
<%= form.time_field :start_time, class: 'form-control', step: 60 %>
<%= form.time_field :start_time, class: 'form-control', step: 60 , discard_month: true, discard_day: true, discard_seconds: true, value: @entry.start_time&.strftime("%H:%M") %>
</div>
<div class="col-12 col-md-4">
<%= form.label :end_time, 'Ende (Uhrzeit)', class: 'form-label' %>
<%= form.time_field :end_time, class: 'form-control', step: 60 %>
<%= form.time_field :end_time, class: 'form-control', step: 60 , discard_month: true, discard_day: true, discard_seconds: true, value: @entry.end_time&.strftime("%H:%M") %>
</div>

83
app/views/entries/index.html.erb

@ -1,4 +1,4 @@
<div class="container my-4">
<h1 class="mb-4">Meine Einträge</h1>
<!-- 🔢 Zusammenfassung -->
@ -99,17 +99,67 @@
</table>
</div>
<%= link_to '➕ Neuer Eintrag', new_entry_path, class: 'btn btn-light mt-3 w-100 w-md-auto' %>
</div>
<div class="mb-3">
<input type="text" id="entryFilter" class="form-control" placeholder="🔍 Filter nach Datum, Typ, Art …">
<div class="container my-4 rounded border shadow-sm p-3">
<h4 class="mb-4">⏱ Timer</h4>
<% if @running_entry.present? %>
<div class="alert alert-info d-flex justify-content-between align-items-center">
<div>
<strong>Laufender Eintrag:</strong>
<%= @running_entry.praktikums_typ %> – <%= @running_entry.entry_art %><br>
Gestartet: <%= l(@running_entry.start_time, format: :short) %><br>
Dauer: <span id="live-timer">Berechne …</span>
</div>
<div>
<%= button_to '⏹️ Stoppen', stop_timer_entry_path(@running_entry), method: :post, class: "btn btn-danger" %>
</div>
</div>
<% end %>
<div class="mb-4 row" >
<%= form_with url: start_timer_entries_path, method: :post, local: true do %>
<div class="row g-2 align-items-end">
<div class="col-md-4">
<%= label_tag :typ, 'Typ' %>
<%= select_tag :typ, options_for_select(Entry::PRAKTIKUMSTYPEN), class: "form-select" %>
</div>
<div class="col-md-4">
<%= label_tag :art, 'Art' %>
<%= select_tag :art, options_for_select(Entry::ENTRY_ARTEN), class: "form-select" %>
</div>
<div class="col-md-4">
<%= submit_tag '▶️ Start', class: "btn btn-success w-100", disabled: @running_entry.present? %>
</div>
</div>
<% end %>
</div>
</div>
<%= link_to '➕ Neuer Eintrag', new_entry_path, class: 'btn btn-info mt-3 w-100 w-md-auto mb-3' %>
<!-- 📋 Tabelle aller Einträge -->
<div class="m-1">
<h4 class="mb-3">📋 Einträge</h4>
<div class="mb-3">
<input type="text" id="entryFilter" class="form-control" placeholder="🔍 Filter nach Datum, Typ, Art …">
</div>
<div class="table-responsive">
<table class="table table-striped table-hover table-bordered" id="entriesTable">
<thead>
@ -150,8 +200,6 @@
<%= number_to_currency(entry.kosten, unit: "€", separator: ",", delimiter: ".") %>
</td>
<td class="text-end">
<div class="d-flex justify-content-between">
<%= link_to 'Bearbeiten', edit_entry_path(entry), class: 'btn btn-sm btn-outline-primary' %>
@ -240,4 +288,25 @@
});
});
document.addEventListener('DOMContentLoaded', function () {
const timerElement = document.getElementById('live-timer');
if (!timerElement) return;
<% if @running_entry&.start_time.present? %>
const startedAt = new Date("<%= @running_entry.start_time.iso8601 %>");
<% end %>
if (!startedAt) return;
const updateTimer = () => {
const now = new Date();
const diffMs = now - startedAt;
const totalMinutes = Math.floor(diffMs / 60000);
const hours = Math.floor(totalMinutes / 60);
const minutes = totalMinutes % 60;
timerElement.textContent = `${hours}h ${minutes}min (${totalMinutes}min)`;
};
updateTimer();
setInterval(updateTimer, 60000); // alle Minute aktualisieren
});
</script>

17
app/views/layouts/application.html.erb

@ -3,7 +3,8 @@
<head>
<title>Praktikumsuhr</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<script src="//ajax.googleapis.com/ajax/libs/jquery/1.6.2/jquery.min.js"></script>
<script>window.jQuery || document.write('<script src="js/libs/jquery-1.6.2.min.js"><\/script>')</script>
<%= csrf_meta_tags %>
<%= csp_meta_tag %>
<!-- Bootstrap CSS -->
@ -45,8 +46,20 @@
});
function clearNotice(){
$("#notice").animate({opacity:'0'}, 1500);
}
$(document).ready(ready);
$(document).on('page:load', ready);
var ready = function() {
setTimeout(clearNotice, 1000); //Flash fade
};
</script>
<%= javascript_include_tag "application", "data-turbo-track": "reload", defer: true %>
<style>
@ -134,7 +147,7 @@
</div>
</nav>
<div class="container">
<div class="container" id="notice">
<% if notice %>
<div class="alert alert-success"><%= notice %></div>
<% end %>

3
config/application.rb

@ -22,7 +22,8 @@ module Praktikum
class Application < Rails::Application
# Initialize configuration defaults for originally generated Rails version.
config.load_defaults 7.1
config.time_zone = 'Europe/Vienna'
config.active_record.default_timezone = :utc
# Please, add to the `ignore` list any other `lib` subdirectories that do
# not contain `.rb` files, or that should not be reloaded or eager loaded.
# Common ones are `templates`, `generators`, or `middleware`, for example.

3
config/importmap.rb

@ -0,0 +1,3 @@
pin "jquery", to: "jquery.min.js", preload: true
pin "jquery_ujs", to: "jquery_ujs.js", preload: true
pin "jquery-ui", to: "jquery-ui.min.js", preload: true

2
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 )
Rails.application.config.assets.precompile += %w( application.js jquery.min.js jquery_ujs.js jquery-ui.min.js )
# Add additional assets to the asset load path.
# Rails.application.config.assets.paths << Emoji.images_path

42
config/routes.rb

@ -1,28 +1,32 @@
Rails.application.routes.draw do
namespace :admin do
resources :entries
resources :users
root to: "entries#index"
end
resources :entries do
collection do
get :export_csv
end
end
namespace :admin do
resources :entries
resources :users
root to: "entries#index"
end
resources :entries do
post :start_timer, on: :collection
member do
post :stop_timer
end
collection do
get :export_csv
end
end
resource :user_goal, only: [:update]
root 'entries#index'
devise_for :users, controllers: {
registrations: 'users/registrations'
}
get '/impressum', to: 'static_pages#impressum'
post "/db_dump/restore", to: "db_dump#restore"
get "/db_dump/dump", to: "db_dump#dump"
get "/db_dump", to: "db_dump#index"
get "/rechner", to: "calculations#new", as: :rechner
# Define your application routes per the DSL in https://guides.rubyonrails.org/routing.html
get '/impressum', to: 'static_pages#impressum'
post "/db_dump/restore", to: "db_dump#restore"
get "/db_dump/dump", to: "db_dump#dump"
get "/db_dump", to: "db_dump#index"
get "/rechner", to: "calculations#new", as: :rechner
# Define your application routes per the DSL in https://guides.rubyonrails.org/routing.html
# Reveal health status on /up that returns 200 if the app boots with no exceptions, otherwise 500.
# Can be used by load balancers and uptime monitors to verify that the app is live.

5
db/migrate/20251113045021_add_start_time_to_entries.rb

@ -0,0 +1,5 @@
class AddStartTimeToEntries < ActiveRecord::Migration[7.1]
def change
add_column :entries, :start_time, :datetime
end
end

5
db/migrate/20251113045025_add_end_time_to_entries.rb

@ -0,0 +1,5 @@
class AddEndTimeToEntries < ActiveRecord::Migration[7.1]
def change
add_column :entries, :end_time, :datetime
end
end

5
db/migrate/20251113045029_add_lunch_break_minutes_to_entries.rb

@ -0,0 +1,5 @@
class AddLunchBreakMinutesToEntries < ActiveRecord::Migration[7.1]
def change
add_column :entries, :lunch_break_minutes, :integer, default: 0
end
end

5
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_09_044216) do
ActiveRecord::Schema[7.1].define(version: 2025_11_13_045029) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@ -26,6 +26,9 @@ ActiveRecord::Schema[7.1].define(version: 2025_11_09_044216) do
t.integer "distance_km", default: 0, null: false
t.string "beschreibung"
t.decimal "kosten"
t.datetime "start_time"
t.datetime "end_time"
t.integer "lunch_break_minutes", default: 0
t.index ["user_id"], name: "index_entries_on_user_id"
end

26
docker-entrypoint.sh

@ -0,0 +1,26 @@
#!/bin/sh
set -e
bundle config set without 'development test'
bundle check || bundle install
echo ===========================================
echo ${RAILS_ENV}
echo ===========================================
echo Create Database
echo ${RAILS_ENV}
echo ===========================================
bundle exec rails db:create RAILS_ENV=${RAILS_ENV} || true
echo ===========================================
echo ${RAILS_ENV}
echo ===========================================
bundle exec rails db:migrate RAILS_ENV=${RAILS_ENV} || true
echo ===========================================
echo ${RAILS_ENV}
echo ===========================================
bundle exec rails db:seed RAILS_ENV=${RAILS_ENV} || true
echo ===========================================
echo ${PORT}
echo ===========================================
bundle exec puma -C config/puma.rb
Loading…
Cancel
Save