From af10dca2891f1bec7c8e7c1ed8c5109c50eeb7a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20B=C3=B6hm?= Date: Thu, 28 May 2026 01:04:19 +0200 Subject: [PATCH] Added Room and some little design updates --- app/controllers/rooms_controller.rb | 97 +++++++ app/helpers/rooms_helper.rb | 2 + app/views/dashboard/index.html.erb | 295 ++++++++++++++-------- app/views/items/_list.html.erb | 190 ++++++++------ app/views/layouts/_sidebar.html.erb | 39 +++ app/views/layouts/application.html.erb | 23 +- app/views/rooms/_form.html.erb | 51 ++++ app/views/rooms/_room.html.erb | 2 + app/views/rooms/_room.json.jbuilder | 2 + app/views/rooms/edit.html.erb | 27 ++ app/views/rooms/index.html.erb | 138 ++++++++++ app/views/rooms/index.json.jbuilder | 1 + app/views/rooms/new.html.erb | 12 + app/views/rooms/show.html.erb | 38 +++ app/views/rooms/show.json.jbuilder | 1 + config/routes.rb | 1 + test/controllers/rooms_controller_test.rb | 48 ++++ 17 files changed, 750 insertions(+), 217 deletions(-) create mode 100644 app/controllers/rooms_controller.rb create mode 100644 app/helpers/rooms_helper.rb create mode 100644 app/views/layouts/_sidebar.html.erb create mode 100644 app/views/rooms/_form.html.erb create mode 100644 app/views/rooms/_room.html.erb create mode 100644 app/views/rooms/_room.json.jbuilder create mode 100644 app/views/rooms/edit.html.erb create mode 100644 app/views/rooms/index.html.erb create mode 100644 app/views/rooms/index.json.jbuilder create mode 100644 app/views/rooms/new.html.erb create mode 100644 app/views/rooms/show.html.erb create mode 100644 app/views/rooms/show.json.jbuilder create mode 100644 test/controllers/rooms_controller_test.rb diff --git a/app/controllers/rooms_controller.rb b/app/controllers/rooms_controller.rb new file mode 100644 index 0000000..a0b18a9 --- /dev/null +++ b/app/controllers/rooms_controller.rb @@ -0,0 +1,97 @@ +class RoomsController < ApplicationController + before_action :set_room, only: %i[ show edit update destroy ] + + # GET /rooms or /rooms.json + # 1. Übersicht aller Räume (Kacheldesign) + def index + @rooms = Room.all.order( + Room.arel_table[:building].asc.nulls_last, + Room.arel_table[:floor].asc.nulls_last, + :name + # :building, :floor, :name + ) + + # NEU: Filtert die Räume live nach Name, Gebäude oder Etage + if params[:query].present? + query_str = "%#{params[:query]}%" + @rooms = @rooms.where( + "rooms.name LIKE :q OR rooms.building LIKE :q OR rooms.floor LIKE :q", + q: query_str + ) + end + end + + # GET /rooms/1 or /rooms/1.json + # 2. Detailansicht eines Raums mit integrierter Live-Suche für dessen Inventar + def show + # Basis-Abfrage: Alle Items dieses Raums laden + @items = @room.items.includes(:category, :user).order(created_at: :desc) + + # Wenn in der Suchleiste getippt wird, filtert der Controller live + if params[:query].present? + query_str = "%#{params[:query]}%" + @items = @items.where( + "items.name LIKE :q OR items.sku LIKE :q OR items.serial_number LIKE :q OR items.sticker_id LIKE :q", + q: query_str + ) + end + end + + # GET /rooms/new + def new + @room = Room.new + end + + # GET /rooms/1/edit + def edit + end + + # POST /rooms or /rooms.json + def create + @room = Room.new(room_params) + + respond_to do |format| + if @room.save + format.html { redirect_to @room, notice: "Room was successfully created." } + format.json { render :show, status: :created, location: @room } + else + format.html { render :new, status: :unprocessable_content } + format.json { render json: @room.errors, status: :unprocessable_content } + end + end + end + + # PATCH/PUT /rooms/1 or /rooms/1.json + def update + respond_to do |format| + if @room.update(room_params) + format.html { redirect_to @room, notice: "Room was successfully updated.", status: :see_other } + format.json { render :show, status: :ok, location: @room } + else + format.html { render :edit, status: :unprocessable_content } + format.json { render json: @room.errors, status: :unprocessable_content } + end + end + end + + # DELETE /rooms/1 or /rooms/1.json + def destroy + @room.destroy! + + respond_to do |format| + format.html { redirect_to rooms_path, notice: "Room was successfully destroyed.", status: :see_other } + format.json { head :no_content } + end + end + + private + # Use callbacks to share common setup or constraints between actions. + def set_room + @room = Room.find(params.expect(:id)) + end + + def room_params + # :description wurde hier restlos entfernt + params.require(:room).permit(:name, :building, :floor) + end +end diff --git a/app/helpers/rooms_helper.rb b/app/helpers/rooms_helper.rb new file mode 100644 index 0000000..1d0f4c7 --- /dev/null +++ b/app/helpers/rooms_helper.rb @@ -0,0 +1,2 @@ +module RoomsHelper +end diff --git a/app/views/dashboard/index.html.erb b/app/views/dashboard/index.html.erb index 89bbe90..0341a98 100644 --- a/app/views/dashboard/index.html.erb +++ b/app/views/dashboard/index.html.erb @@ -1,134 +1,205 @@ -<% content_for :title, "Dashboard Übersicht" %> +<% content_for :title, "Dashboard" %>
- - -
- - -
-
- - -
-
-

Objekte im System

-

<%= @total_items %>

+ + +
+ + +
+
+ +
+ + + +
+
+

Gesamtbestand

+

Registrierte Unikate

+
+ +

<%= Item.count %>

- -
-
- - -
-
-

Aktuell im Lager

-

<%= @items_in_storage %>

+ +
+
+ +
+ + + +
+
+

Kategorien

+

Geräte-Typen

+
+ +

<%= Category.count %>

- -
-
- - -
-
-

Gesamtwert Inventar

-

- <%= number_to_currency(@total_value, unit: "€", separator: ",", delimiter: ".", format: "%n %u") %> -

+ +
+
+ +
+ + + + +
+
+

Räume & Büros

+

Erfasste Standorte

+
+ +

<%= Room.count %>

+
- -
-

- - - Zuletzt registrierte Artikel -

- - <% if @recent_items.any? %> -
- <% @recent_items.each do |item| %> -
-
- #<%= item.sticker_id %> -
-

<%= item.name %>

-

+ +

+ + +
+
+ + + +

Zuletzt hinzugefügt

+
+ +
+ + + <% @recent_items.each.each do |item| %> + + + + + + + <% end %> + +
+
+ <%= link_to item_path(item), class: "shrink-0 transition hover:scale-105 active:scale-95 block" do %> + + #<%= item.sticker_id %> + + <% end %> +
+ <%= link_to item_path(item), class: "font-bold text-gray-900 hover:text-blue-600 hover:underline inline leading-tight" do %> + <%= item.name %> + <% end %> + <%= item.category.name %> +
+
+
<% if item.user.present? %> - 👤 <%= item.user.name %> + + + + + <%= item.user.name %> + <% elsif item.room.present? %> - 📍 <%= item.room.name_with_building %> + <%= link_to room_path item.room do %> + + + + + <%= item.room.name %> + + <% end %> <% else %> - 📦 Im Hauptlager + + + + + Hauptlager + <% end %> -

- - -
- <%= time_ago_in_words(item.created_at) %> -
- - <% end %> +
+ <%= time_ago_in_words(item.created_at) %> +
- <% else %> -
- Bisher wurden keine Artikel im System erfasst. -
- <% end %> -
-
-

- - - - - Letzte Artikel-Zuordnungen & Bewegungen -

+
- <% if @recent_assignments.any? %> -
- <% @recent_assignments.each do |log| %> -
-
- - - #<%= log.item.sticker_id %> - - -
-

- <%= link_to log.item.name, item_path(log.item), class: "hover:text-blue-600 transition" %> -

-

- <% if log.user.present? %> - 👤 <%= log.user.name %> - <% elsif log.room.present? %> - 📍 <%= log.room.name_with_building %> - <% else %> - 📦 Ins Hauptlager gelegt - <% end %> -

-
-
- -
- <%= time_ago_in_words(log.created_at) %> -
-
- <% end %> + +
+
+ + + +

Letzte Aktivitäten

- <% else %> -
- Es wurden noch keine Artikel-Bewegungen im System registriert. + +
+ + + <% @recent_assignments.each do |log| %> + + + + + + <% end %> + +
+
+ <%= link_to item_path(log.item), class: "shrink-0 transition hover:scale-105 active:scale-95 block" do %> + + #<%= log.item.sticker_id %> + + <% end %> +
+ <% if log.item.present? %> + <%= link_to item_path(log.item), class: "font-bold text-gray-900 hover:text-blue-600 hover:underline inline leading-tight" do %> + <%= log.item.name %> + <% end %> + <% else %> + Gelöschter Artikel + <% end %> + +
+ <% if log.user.present? %> + + + + + <%= log.user.name %> zugewiesen + + <% elsif log.room.present? %> + <%= link_to room_path log.room do %> + + + + + In <%= log.room.name %> platziert + + <% end %> + <% else %> + + + + + Hauptlager zurückgebracht + + <% end %> +
+
+
+
+ <%= time_ago_in_words(log.assigned_at) %> +
- <% end %> +
+
-
diff --git a/app/views/items/_list.html.erb b/app/views/items/_list.html.erb index eea04c1..b4f7cd0 100644 --- a/app/views/items/_list.html.erb +++ b/app/views/items/_list.html.erb @@ -1,82 +1,75 @@
- - - -
- <% items.each do |item| %> -
- - -
+ + + +
+ <% items.each do |item| %> +
+ + +
+ + + <%= link_to item_path(item), data: { turbo_frame: "_top" }, class: "shrink-0 transition hover:scale-105 active:scale-95 block mt-0.5" do %> + + #<%= item.sticker_id %> + + <% end %> + + +
+ +
-

<%= item.name %>

-

<%= item.category.name %>

+ <%= link_to item_path(item), data: { turbo_frame: "_top" }, class: "font-black text-gray-900 hover:text-blue-600 text-base leading-tight block truncate" do %> + <%= item.name %> + <% end %>
- -
- - #<%= item.sticker_id %> - - - <%= number_to_currency(item.price, unit: "€", separator: ",", delimiter: ".", format: "%n %u") %> - + +
+ <%= item.category.name %> + + SKU: <%= item.sku %>
-
- -
-
- SKU - <%= item.sku %> -
-
- Seriennummer - <%= item.serial_number %> -
-
- - -
- -
+ +
<% if item.user.present? %> - - 👤 <%= item.user.name %> + + + <%= item.user.name %> <% elsif item.room.present? %> - - 📍 <%= item.room.name %> + + + <%= item.room.name %> <% else %> - - 📦 Hauptlager + + + Hauptlager <% end %>
- -
- - <%= link_to item_path(item), - data: { turbo_frame: "_top" }, - class: "p-2 text-gray-500 hover:text-blue-600 hover:bg-blue-50 rounded-lg border border-gray-200 bg-white shadow-sm" do %> - - <% end %> - - - <%= link_to edit_item_path(item), - data: { turbo_frame: "_top" }, - class: "p-2 text-gray-500 hover:text-amber-600 hover:bg-amber-50 rounded-lg border border-gray-200 bg-white shadow-sm" do %> - - <% end %> -
-
- <% end %> -
+ + +
+ <%= link_to item_path(item), data: { turbo_frame: "_top" }, class: "p-2 text-gray-500 hover:text-blue-600 hover:bg-blue-50 rounded-lg border border-gray-200 bg-white shadow-sm flex items-center justify-center", title: "Details" do %> + + <% end %> + <%= link_to edit_item_path(item), data: { turbo_frame: "_top" }, class: "p-2 text-gray-500 hover:text-amber-600 hover:bg-amber-50 rounded-lg border border-gray-200 bg-white shadow-sm flex items-center justify-center", title: "Bearbeiten" do %> + + <% end %> +
+ +
+ <% end %> +
@@ -86,8 +79,8 @@ Artikel / Details - Seriennummer (SN) - Aktueller Standort / Inhaber + SN + Standort / Inhaber Sticker-ID Wert Aktionen @@ -101,46 +94,77 @@
SKU: <%= item.sku %> • <%= item.category.name %>
<%= item.serial_number %> - + + <% if item.user.present? %> - 👤 <%= item.user.name %> + + + + + + <%= item.user.name %> + <% elsif item.room.present? %> - 📍 <%= item.room.name_with_building %> + + + + + + + <%= item.room.name_with_building %> + <% else %> - 📦 Im Hauptlager + + + + + + Hauptlager + <% end %> + - - #<%= item.sticker_id %> - - + <%= link_to item_path(item), data: { turbo_frame: "_top" }, class: "inline-block transition hover:scale-105" do %> + + #<%= item.sticker_id %> + + <% end %> <%= number_to_currency(item.price, unit: "€", separator: ",", delimiter: ".", format: "%n %u") %> - - + + +
- - <%= link_to item_path(item), - data: { turbo_frame: "_top" }, - class: "p-1.5 text-gray-500 hover:text-blue-600 hover:bg-blue-50 rounded-lg transition-colors border border-transparent hover:border-blue-100", + + + <%= link_to item_path(item), + data: { turbo_frame: "_top" }, + class: "p-2 text-gray-500 hover:text-blue-600 hover:bg-blue-50 rounded-lg border border-gray-200 bg-white shadow-sm flex items-center justify-center transition", title: "Details anzeigen" do %> - + + + + <% end %> - - <%= link_to edit_item_path(item), - data: { turbo_frame: "_top" }, - class: "p-1.5 text-gray-500 hover:text-amber-600 hover:bg-amber-50 rounded-lg transition-colors border border-transparent hover:border-amber-100", + + <%= link_to edit_item_path(item), + data: { turbo_frame: "_top" }, + class: "p-2 text-gray-500 hover:text-amber-600 hover:bg-amber-50 rounded-lg border border-gray-200 bg-white shadow-sm flex items-center justify-center transition", title: "Artikel bearbeiten" do %> - + + + <% end %> +
+ <% end %> diff --git a/app/views/layouts/_sidebar.html.erb b/app/views/layouts/_sidebar.html.erb new file mode 100644 index 0000000..5c87612 --- /dev/null +++ b/app/views/layouts/_sidebar.html.erb @@ -0,0 +1,39 @@ + + diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb index c9a97ec..8319a06 100644 --- a/app/views/layouts/application.html.erb +++ b/app/views/layouts/application.html.erb @@ -60,28 +60,7 @@
- + <%= render "layouts/sidebar" %>
diff --git a/app/views/rooms/_form.html.erb b/app/views/rooms/_form.html.erb new file mode 100644 index 0000000..6d15a09 --- /dev/null +++ b/app/views/rooms/_form.html.erb @@ -0,0 +1,51 @@ +<%= form_with(model: room, class: "space-y-6 max-w-xl mx-auto bg-white border border-gray-200 rounded-xl shadow-sm p-6 md:p-8") do |form| %> + + <% if room.errors.any? %> + + <% end %> + +
+

+ <%= room.new_record? ? "Raum anlegen" : "Raum bearbeiten" %> +

+

+ <%= room.new_record? ? "Definiere einen neuen physischen Standort für deine Inventargegenstände." : "Aktualisiere die Standortbezeichnung oder die Etage." %> +

+
+ +
+ + +
+ <%= form.label :name, "Raumbezeichnung / Raumnummer", class: "block text-sm font-medium mb-1.5 text-gray-700" %> + <%= form.text_field :name, placeholder: "z.B. Raum 101, Serverraum, Werkstatt", class: "py-2 px-3 block w-full border border-gray-300 rounded-lg text-sm bg-gray-50/50 focus:border-blue-500 focus:ring-blue-500" %> +
+ + +
+
+ <%= form.label :building, "Gebäude / Gebäudeteil", class: "block text-sm font-medium mb-1.5 text-gray-700" %> + <%= form.text_field :building, placeholder: "z.B. Hauptgebäude, Bau B", class: "py-2 px-3 block w-full border border-gray-300 rounded-lg text-sm bg-gray-50/50 focus:border-blue-500 focus:ring-blue-500" %> +
+
+ <%= form.label :floor, "Etage / Stockwerk", class: "block text-sm font-medium mb-1.5 text-gray-700" %> + <%= form.text_field :floor, placeholder: "z.B. EG, 1. OG, Keller", class: "py-2 px-3 block w-full border border-gray-300 rounded-lg text-sm bg-gray-50/50 focus:border-blue-500 focus:ring-blue-500" %> +
+
+ +
+ + +
+ <%= link_to "Abbrechen", rooms_path, class: "py-2.5 px-4 inline-flex items-center text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-lg hover:bg-gray-50 transition shadow-sm" %> + <%= form.submit room.new_record? ? "Raum erstellen" : "Änderungen speichern", class: "py-2.5 px-4 inline-flex items-center text-sm font-semibold rounded-lg bg-blue-600 text-white hover:bg-blue-700 shadow-sm transition cursor-pointer" %> +
+ +<% end %> diff --git a/app/views/rooms/_room.html.erb b/app/views/rooms/_room.html.erb new file mode 100644 index 0000000..198d981 --- /dev/null +++ b/app/views/rooms/_room.html.erb @@ -0,0 +1,2 @@ +
+
diff --git a/app/views/rooms/_room.json.jbuilder b/app/views/rooms/_room.json.jbuilder new file mode 100644 index 0000000..2331046 --- /dev/null +++ b/app/views/rooms/_room.json.jbuilder @@ -0,0 +1,2 @@ +json.extract! room, :id, :created_at, :updated_at +json.url room_url(room, format: :json) diff --git a/app/views/rooms/edit.html.erb b/app/views/rooms/edit.html.erb new file mode 100644 index 0000000..4521ef2 --- /dev/null +++ b/app/views/rooms/edit.html.erb @@ -0,0 +1,27 @@ +<% content_for :title, "Raum bearbeiten: #{@room.name}" %> + +<% content_for :top_bar_actions do %> + <%= link_to rooms_path, class: "py-2 px-3 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-lg hover:bg-gray-50 flex items-center gap-1.5 shadow-sm transition" do %> + + Zurück zur Übersicht + <% end %> +<% end %> + +
+ <%= render "form", room: @room %> + + +
+
+
+ +
+
+

Raum löschen

+

In diesem Raum gelistete Artikel verlieren ihren Standort und werden automatisch ins Hauptlager umgebucht.

+
+
+ + <%= link_to "Raum löschen", room_path(@room), data: { turbo_method: :delete, turbo_confirm: "Möchtest du diesen Standort wirklich löschen?" }, class: "py-2 px-4 text-sm font-semibold text-white bg-red-600 hover:bg-red-700 rounded-lg shadow-sm transition" %> +
+
diff --git a/app/views/rooms/index.html.erb b/app/views/rooms/index.html.erb new file mode 100644 index 0000000..44d60a5 --- /dev/null +++ b/app/views/rooms/index.html.erb @@ -0,0 +1,138 @@ +
+ + <% content_for :title, "Räume" %> + + <% content_for :top_bar_actions do %> + <%= link_to new_room_path, data: { turbo_frame: "_top" }, class: "py-2 px-4 text-sm font-semibold rounded-lg bg-blue-600 text-white hover:bg-blue-700 flex items-center gap-1.5 shadow-sm transition" do %> + + + + + Raum anlegen + <% end %> +<% end %> + + + <%= render "items/search_bar", show_csv: false %> + + + <%= turbo_frame_tag "items_list_frame" do %> + <% if @rooms.any? %> +
+ + + + +
+ <% @rooms.each do |room| %> +
+ + +
+
+ + + + +
+

<%= room.name %>

+
+ <%= room.building %> + + Etage <%= room.floor %> +
+
+
+ +
+ + <%= room.items.count %> <%= room.items.count == 1 ? "Gerät" : "Geräte" %> + +
+
+ + +
+ + + <%= link_to room_path(room), data: { turbo_frame: "_top" }, class: "w-8 h-8 text-gray-500 hover:text-blue-600 hover:bg-blue-50 rounded-lg border border-gray-200 bg-white shadow-sm flex items-center justify-center transition flex-initial shrink-0", title: "Inventar anzeigen" do %> + + <% end %> + + + <%= link_to edit_room_path(room), data: { turbo_frame: "_top" }, class: "w-8 h-8 text-gray-500 hover:text-amber-600 hover:bg-amber-50 rounded-lg border border-gray-200 bg-white shadow-sm flex items-center justify-center transition flex-initial shrink-0", title: "Bearbeiten" do %> + + <% end %> + +
+ +
+ <% end %> +
+ + + + + + +
+ <% else %> +
+

Keine passenden Gebäude oder Büros gefunden.

+
+ <% end %> + <% end %> + +
diff --git a/app/views/rooms/index.json.jbuilder b/app/views/rooms/index.json.jbuilder new file mode 100644 index 0000000..2a654bb --- /dev/null +++ b/app/views/rooms/index.json.jbuilder @@ -0,0 +1 @@ +json.array! @rooms, partial: "rooms/room", as: :room diff --git a/app/views/rooms/new.html.erb b/app/views/rooms/new.html.erb new file mode 100644 index 0000000..0cc5c8b --- /dev/null +++ b/app/views/rooms/new.html.erb @@ -0,0 +1,12 @@ +<% content_for :title, "Neuen Raum hinzufügen" %> + +<% content_for :top_bar_actions do %> + <%= link_to rooms_path, class: "py-2 px-3 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-lg hover:bg-gray-50 flex items-center gap-1.5 shadow-sm transition" do %> + + Zurück zur Übersicht + <% end %> +<% end %> + +
+ <%= render "form", room: @room %> +
diff --git a/app/views/rooms/show.html.erb b/app/views/rooms/show.html.erb new file mode 100644 index 0000000..4710615 --- /dev/null +++ b/app/views/rooms/show.html.erb @@ -0,0 +1,38 @@ +<% content_for :title, "Standort: #{@room.name}" %> + +<% content_for :top_bar_actions do %> + <%= link_to rooms_path, class: "py-2 px-3 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-lg hover:bg-gray-50 flex items-center gap-1.5 shadow-sm transition" do %> + + Alle Räume + <% end %> +<% end %> + +
+ +
+
+

Gebäude

+

<%= @room.building %>

+
+
+

Stockwerk / Etage

+

<%= @room.floor %>

+
+
+ +
+ + <%= render "items/search_bar", show_csv: false %> + + + <%= turbo_frame_tag "items_list_frame" do %> + <% if @items.any? %> + <%= render "items/list", items: @items %> + <% else %> +
+

Keine passenden Artikel an diesem Standort gefunden.

+
+ <% end %> + <% end %> +
+
diff --git a/app/views/rooms/show.json.jbuilder b/app/views/rooms/show.json.jbuilder new file mode 100644 index 0000000..c3628ad --- /dev/null +++ b/app/views/rooms/show.json.jbuilder @@ -0,0 +1 @@ +json.partial! "rooms/room", room: @room diff --git a/config/routes.rb b/config/routes.rb index ca623c1..3c7f5c7 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,4 +1,5 @@ Rails.application.routes.draw do + resources :rooms resources :items resources :categories namespace :authentications do diff --git a/test/controllers/rooms_controller_test.rb b/test/controllers/rooms_controller_test.rb new file mode 100644 index 0000000..909ab08 --- /dev/null +++ b/test/controllers/rooms_controller_test.rb @@ -0,0 +1,48 @@ +require "test_helper" + +class RoomsControllerTest < ActionDispatch::IntegrationTest + setup do + @room = rooms(:one) + end + + test "should get index" do + get rooms_url + assert_response :success + end + + test "should get new" do + get new_room_url + assert_response :success + end + + test "should create room" do + assert_difference("Room.count") do + post rooms_url, params: { room: {} } + end + + assert_redirected_to room_url(Room.last) + end + + test "should show room" do + get room_url(@room) + assert_response :success + end + + test "should get edit" do + get edit_room_url(@room) + assert_response :success + end + + test "should update room" do + patch room_url(@room), params: { room: {} } + assert_redirected_to room_url(@room) + end + + test "should destroy room" do + assert_difference("Room.count", -1) do + delete room_url(@room) + end + + assert_redirected_to rooms_url + end +end