From 44d019b4b5b1117d26ba202125408c2284135bb6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20B=C3=B6hm?= Date: Fri, 22 May 2026 03:52:54 +0200 Subject: [PATCH] Added Items and Dashboard --- Gemfile | 2 + Gemfile.lock | 9 + app/controllers/dashboard_controller.rb | 15 ++ app/controllers/items_controller.rb | 81 ++++++ app/helpers/dashboard_helper.rb | 2 + app/helpers/items_helper.rb | 2 + .../controllers/assignment_controller.js | 27 ++ .../controllers/scanner_controller.js | 49 ++++ app/models/item.rb | 18 ++ app/views/categories/index.html.erb | 2 +- app/views/dashboard/index.html.erb | 86 +++++++ app/views/items/_form.html.erb | 137 ++++++++++ app/views/items/_item.html.erb | 2 + app/views/items/_item.json.jbuilder | 2 + app/views/items/edit.html.erb | 50 ++++ app/views/items/index.html.erb | 154 +++++++++++ app/views/items/index.json.jbuilder | 1 + app/views/items/new.html.erb | 27 ++ app/views/items/show.html.erb | 241 ++++++++++++++++++ app/views/items/show.json.jbuilder | 1 + app/views/layouts/application.html.erb | 4 +- config/routes.rb | 3 +- test/controllers/dashboard_controller_test.rb | 7 + test/controllers/items_controller_test.rb | 48 ++++ 24 files changed, 966 insertions(+), 4 deletions(-) create mode 100644 app/controllers/dashboard_controller.rb create mode 100644 app/controllers/items_controller.rb create mode 100644 app/helpers/dashboard_helper.rb create mode 100644 app/helpers/items_helper.rb create mode 100644 app/javascript/controllers/assignment_controller.js create mode 100644 app/javascript/controllers/scanner_controller.js create mode 100644 app/views/dashboard/index.html.erb create mode 100644 app/views/items/_form.html.erb create mode 100644 app/views/items/_item.html.erb create mode 100644 app/views/items/_item.json.jbuilder create mode 100644 app/views/items/edit.html.erb create mode 100644 app/views/items/index.html.erb create mode 100644 app/views/items/index.json.jbuilder create mode 100644 app/views/items/new.html.erb create mode 100644 app/views/items/show.html.erb create mode 100644 app/views/items/show.json.jbuilder create mode 100644 test/controllers/dashboard_controller_test.rb create mode 100644 test/controllers/items_controller_test.rb diff --git a/Gemfile b/Gemfile index c84db59..4080fe1 100644 --- a/Gemfile +++ b/Gemfile @@ -74,3 +74,5 @@ end gem "authentication-zero", "~> 4.0" + +gem "rqrcode", "~> 3.2" diff --git a/Gemfile.lock b/Gemfile.lock index 538cea6..4ca31e4 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -103,6 +103,7 @@ GEM xpath (~> 3.2) childprocess (5.1.0) logger (~> 1.5) + chunky_png (1.4.0) concurrent-ruby (1.3.6) connection_pool (3.0.2) crass (1.0.6) @@ -292,6 +293,10 @@ GEM reline (0.6.3) io-console (~> 0.5) rexml (3.4.4) + rqrcode (3.2.0) + chunky_png (~> 1.0) + rqrcode_core (~> 2.0) + rqrcode_core (2.1.0) rubocop (1.86.2) json (~> 2.3) language_server-protocol (~> 3.17.0.2) @@ -428,6 +433,7 @@ DEPENDENCIES propshaft puma (>= 5.0) rails (~> 8.1.3) + rqrcode (~> 3.2) rubocop-rails-omakase selenium-webdriver solid_cable @@ -469,6 +475,7 @@ CHECKSUMS bundler-audit (0.9.3) sha256=81c8766c71e47d0d28a0f98c7eed028539f21a6ea3cd8f685eb6f42333c9b4e9 capybara (3.40.0) sha256=42dba720578ea1ca65fd7a41d163dd368502c191804558f6e0f71b391054aeef childprocess (5.1.0) sha256=9a8d484be2fd4096a0e90a0cd3e449a05bc3aa33f8ac9e4d6dcef6ac1455b6ec + chunky_png (1.4.0) sha256=89d5b31b55c0cf4da3cf89a2b4ebc3178d8abe8cbaf116a1dba95668502fdcfe concurrent-ruby (1.3.6) sha256=6b56837e1e7e5292f9864f34b69c5a2cbc75c0cf5338f1ce9903d10fa762d5ab connection_pool (3.0.2) sha256=33fff5ba71a12d2aa26cb72b1db8bba2a1a01823559fb01d29eb74c286e62e0a crass (1.0.6) sha256=dc516022a56e7b3b156099abc81b6d2b08ea1ed12676ac7a5657617f012bd45d @@ -553,6 +560,8 @@ CHECKSUMS regexp_parser (2.12.0) sha256=35a916a1d63190ab5c9009457136ae5f3c0c7512d60291d0d1378ba18ce08ebb reline (0.6.3) sha256=1198b04973565b36ec0f11542ab3f5cfeeec34823f4e54cebde90968092b1835 rexml (3.4.4) sha256=19e0a2c3425dfbf2d4fc1189747bdb2f849b6c5e74180401b15734bc97b5d142 + rqrcode (3.2.0) sha256=64c1494ca6bb67d731330f38b50e3fd09eeab4f5dcd04b608e21218d1d0b9542 + rqrcode_core (2.1.0) sha256=f303b85df89c1b8fc5ee8dc19808c9dc4330e6329b660d99d4a8cbb36ca13051 rubocop (1.86.2) sha256=bb2e97f635eda42c448f2588f4a6ff78f221b8bdfdf65b1e9b07fbd57521b45d rubocop-ast (1.49.1) sha256=4412f3ee70f6fe4546cc489548e0f6fcf76cafcfa80fa03af67098ffed755035 rubocop-performance (1.26.1) sha256=cd19b936ff196df85829d264b522fd4f98b6c89ad271fa52744a8c11b8f71834 diff --git a/app/controllers/dashboard_controller.rb b/app/controllers/dashboard_controller.rb new file mode 100644 index 0000000..51e937f --- /dev/null +++ b/app/controllers/dashboard_controller.rb @@ -0,0 +1,15 @@ +class DashboardController < ApplicationController + def index + # 1. Zählt alle registrierten Hardware-Unikate + @total_items = Item.count + + # 2. Berechnet den Gesamtwert aller Geräte (summiert das Feld :price) + @total_value = Item.sum(:price) + + # 3. Zählt die Artikel, die weder einem User noch einem Raum gehören (= im Lager liegen) + @items_in_storage = Item.where(user_id: nil, room_id: nil).count + + # 4. Holt die letzten 10 registrierten Artikel für die Aktivitätenanzeige + @recent_items = Item.order(created_at: :desc).limit(10).includes(:category, :user, :room) + end +end diff --git a/app/controllers/items_controller.rb b/app/controllers/items_controller.rb new file mode 100644 index 0000000..97a922b --- /dev/null +++ b/app/controllers/items_controller.rb @@ -0,0 +1,81 @@ +class ItemsController < ApplicationController + before_action :set_item, only: %i[ show edit update destroy ] + + # GET /items or /items.json + def index + @items = Item.all.includes(:category, :user, :room).order(created_at: :desc) + end + + # GET /items/1 or /items/1.json + def show + @assignment_logs = @item.assignment_logs.includes(:user, :room).order(assigned_at: :desc) + end + + # GET /items/new + def new + @item = Item.new + end + + # GET /items/1/edit + def edit + end + + # POST /items or /items.json + def create + @item = Item.new(item_params) + + respond_to do |format| + if @item.save + format.html { redirect_to @item, notice: "Artikel '#{@item.name}' wurde erfolgreich im System registriert." } + format.json { render :show, status: :created, location: @item } + else + format.html { render :new, status: :unprocessable_content } + format.json { render json: @item.errors, status: :unprocessable_content } + end + end + end + + # PATCH/PUT /items/1 or /items/1.json + def update + respond_to do |format| + if @item.update(item_params) + format.html { redirect_to @item, notice: "Artikel '#{@item.name} wurde erfolgreich aktualisiert.", status: :see_other } + format.json { render :show, status: :ok, location: @item } + else + format.html { render :edit, status: :unprocessable_content } + format.json { render json: @item.errors, status: :unprocessable_content } + end + end + end + + # DELETE /items/1 or /items/1.json + def destroy + @item.destroy! + + respond_to do |format| + format.html { redirect_to items_path, notice: "Artikel wurde erfolgreich aus dem System gelöscht", status: :see_other } + format.json { head :no_content } + end + end + + private + + def set_item + @item = Item.find(params.expect(:id)) + end + + # Strong Parameters: Schützt vor Mass-Assignment-Injections + def item_params + params.require(:item).permit( + :name, + :sku, + :sticker_id, + :serial_number, + :price, + :notes, + :category_id, + :user_id, # Für die flexible Zuweisung an Mitarbeiter + :room_id # Für die flexible Zuweisung an Räume + ) + end +end diff --git a/app/helpers/dashboard_helper.rb b/app/helpers/dashboard_helper.rb new file mode 100644 index 0000000..a94ddfc --- /dev/null +++ b/app/helpers/dashboard_helper.rb @@ -0,0 +1,2 @@ +module DashboardHelper +end diff --git a/app/helpers/items_helper.rb b/app/helpers/items_helper.rb new file mode 100644 index 0000000..cff0c9f --- /dev/null +++ b/app/helpers/items_helper.rb @@ -0,0 +1,2 @@ +module ItemsHelper +end diff --git a/app/javascript/controllers/assignment_controller.js b/app/javascript/controllers/assignment_controller.js new file mode 100644 index 0000000..5410755 --- /dev/null +++ b/app/javascript/controllers/assignment_controller.js @@ -0,0 +1,27 @@ +import { Controller } from "@hotwire/stimulus" + +export default class extends Controller { + static targets = [ "userSection", "roomSection" ] + + toggle(event) { + const value = event.target.value + const userDropdown = this.userSectionTarget.querySelector('select') + const roomDropdown = this.roomSectionTarget.querySelector('select') + + if (value === "user") { + this.userSectionTarget.classList.remove("hidden") + this.roomSectionTarget.classList.add("hidden") + roomDropdown.value = "" // Raum-ID löschen, da ein Artikel nur einen Inhaber haben kann + } else if (value === "room") { + this.roomSectionTarget.classList.remove("hidden") + this.userSectionTarget.classList.add("hidden") + userDropdown.value = "" // User-ID löschen + } else { + // Hauptlager ausgewählt -> Beide ausblenden und Werte in der DB nullen + this.userSectionTarget.classList.add("hidden") + this.roomSectionTarget.classList.add("hidden") + userDropdown.value = "" + roomDropdown.value = "" + } + } +} diff --git a/app/javascript/controllers/scanner_controller.js b/app/javascript/controllers/scanner_controller.js new file mode 100644 index 0000000..69bffd3 --- /dev/null +++ b/app/javascript/controllers/scanner_controller.js @@ -0,0 +1,49 @@ +import { Controller } from "@hotwire/stimulus" + +export default class extends Controller { + static targets = [ "input", "preview", "modal" ] + + connect() { + this.html5QrCode = null + } + + // Öffnet das Modal und startet den Kamera-Stream + startCamera(event) { + event.preventDefault() + + // Modal anzeigen + this.modalTarget.classList.remove("hidden") + + // Neue Instanz auf dem Preview-Div mit der ID des Elements erzeugen + this.html5QrCode = new Html5Qrcode(this.previewTarget.id) + + // FPS-Rate und Scan-Rahmen (250x250px) festlegen + const config = { fps: 10, qrbox: { width: 250, height: 250 } } + + this.html5QrCode.start( + { facingMode: "environment" }, // Erzwingt die rückseitige Hauptkamera bei Handys + config, + (decodedText, decodedResult) => { + // SUCCESS: Code erkannt! + this.inputTarget.value = decodedText // Trägt die ID (z.B. 10024) ins Textfeld ein + this.stopCamera() // Stoppt die Kamera und schließt das Fenster + }, + (errorMessage) => { + // Kontinuierlicher Scan-Loop (Fehler ignorieren, wenn kein QR-Code im Bild ist) + } + ).catch((err) => { + console.error("Kamera-Zugriff verweigert oder blockiert:", err) + }) + } + + // Schließt das Modal und beendet den Stream sauber + stopCamera() { + if (this.html5QrCode && this.html5QrCode.isScanning) { + this.html5QrCode.stop().then(() => { + this.modalTarget.classList.add("hidden") + }).catch((err) => console.error("Fehler beim Beenden des Streams:", err)) + } else { + this.modalTarget.classList.add("hidden") + } + } +} diff --git a/app/models/item.rb b/app/models/item.rb index ccbeca3..faf1108 100644 --- a/app/models/item.rb +++ b/app/models/item.rb @@ -1,3 +1,5 @@ +require "rqrcode" + class Item < ApplicationRecord belongs_to :category belongs_to :user, optional: true # Optional, falls im Raum oder Lager @@ -13,6 +15,22 @@ class Item < ApplicationRecord # Überwacht Besitzer- oder Raumwechsel für die Historie before_save :track_assignment_changes, if: -> { will_save_change_to_user_id? || will_save_change_to_room_id? } + def generate_qr_code + return if sticker_id.blank? + + # Erzeugt das QR-Code-Objekt basierend auf deiner vorgedruckten Sticker-ID + qrcode = RQRCode::QRCode.new(sticker_id.to_s) + + # Rendert den QR-Code als SVG-Vektorgrafik (perfekt scharf für Bildschirme) + qrcode.as_svg( + color: "000", # Farbe: Schwarz + shape_rendering: "crispEdges", # Erzwingt scharfe Kanten im Browser + module_size: 4, # Kompakte Größe + standalone: true, + use_path: true + ).html_safe # Sagt Rails, dass das HTML unbedenklich ausgegeben werden darf + end + private def either_user_or_room diff --git a/app/views/categories/index.html.erb b/app/views/categories/index.html.erb index 77aee76..2cfbbb3 100644 --- a/app/views/categories/index.html.erb +++ b/app/views/categories/index.html.erb @@ -1,6 +1,6 @@ -<% content_for :title, "Inventar-Kategorien" %> +<% content_for :title, "Kategorien" %>
diff --git a/app/views/dashboard/index.html.erb b/app/views/dashboard/index.html.erb new file mode 100644 index 0000000..f0fac74 --- /dev/null +++ b/app/views/dashboard/index.html.erb @@ -0,0 +1,86 @@ +<% content_for :title, "Dashboard Übersicht" %> + +
+ + +
+ + +
+
+ + +
+
+

Objekte im System

+

<%= @total_items %>

+
+
+ + +
+
+ + +
+
+

Aktuell im Lager

+

<%= @items_in_storage %>

+
+
+ + +
+
+ + +
+
+

Gesamtwert Inventar

+

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

+
+
+
+ + +
+

+ + + Zuletzt registrierte Artikel +

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

<%= item.name %>

+

+ <% if item.user.present? %> + Zugewiesen an: 👤 <%= item.user.name %> + <% elsif item.room.present? %> + Standort: 📍 <%= item.room.name_with_building %> + <% else %> + 📦 Im Hauptlager + <% end %> +

+
+
+
+ <%= time_ago_in_words(item.created_at) %> vor +
+
+ <% end %> +
+ <% else %> +
+ Bisher wurden keine Artikel im System erfasst. +
+ <% end %> +
+
diff --git a/app/views/items/_form.html.erb b/app/views/items/_form.html.erb new file mode 100644 index 0000000..8968858 --- /dev/null +++ b/app/views/items/_form.html.erb @@ -0,0 +1,137 @@ + + +<%= form_with(model: item, class: "space-y-6 max-w-2xl mx-auto bg-white border border-gray-200 rounded-xl shadow-sm p-6 md:p-8") do |form| %> + + <% if item.errors.any? %> + + <% end %> + +
+ +

+ <%= item.new_record? ? "Artikel registrieren" : "Artikel bearbeiten" %> +

+ +

+ <%= item.new_record? ? "Trage hier die Unikat-Daten wie Seriennummer und Sticker-ID ein." : "Aktualisiere die Daten oder verändere den aktuellen Standort des Geräts." %> +

+
+ +
+ + +
+
+ <%= form.label :name, "Artikelname / Modell", class: "block text-sm font-medium mb-1.5 text-gray-700" %> + <%= form.text_field :name, class: "py-2 px-3 block w-full border border-gray-300 rounded-lg text-sm bg-gray-50/50" %> +
+
+ <%= form.label :sku, "SKU (Artikelnummer)", class: "block text-sm font-medium mb-1.5 text-gray-700" %> + <%= form.text_field :sku, class: "py-2 px-3 block w-full border border-gray-300 rounded-lg text-sm bg-gray-50/50" %> +
+
+ + +
+
+ <%= form.label :serial_number, "Seriennummer (Hersteller)", class: "block text-sm font-medium mb-1.5 text-gray-700" %> + <%= form.text_field :serial_number, class: "py-2 px-3 block w-full border border-gray-300 rounded-lg text-sm bg-gray-50/50" %> +
+
+ <%= form.label :price, "Einkaufspreis (€)", class: "block text-sm font-medium mb-1.5 text-gray-700" %> + <%= form.text_field :price, placeholder: "0.00", class: "py-2 px-3 block w-full border border-gray-300 rounded-lg text-sm bg-gray-50/50" %> +
+
+ + +
+ <%= form.label :category_id, "Kategorie", class: "block text-sm font-medium mb-1.5 text-gray-700" %> + <%= form.collection_select :category_id, Category.all, :id, :name, { prompt: "Bitte wählen..." }, { class: "py-2 px-3 block w-full border border-gray-300 rounded-lg text-sm bg-white" } %> +
+ + +
+ <%= form.label :sticker_id, "Vorgedruckte Sticker-ID / QR-Nummer", class: "block text-sm font-medium mb-1.5 text-gray-700" %> +
+ <%= form.text_field :sticker_id, data: { scanner_target: "input" }, class: "py-2 px-3 block w-full border border-gray-300 rounded-l-lg text-sm bg-gray-50/50" %> + +
+
+ +
+ + +
+ + +
+ + + +
+ + + + + + +
+ +
+ + +
+ <%= form.label :notes, "Zusätzliche Notizen / Details", class: "block text-sm font-medium mb-1.5 text-gray-700" %> + <%= form.text_area :notes, rows: 3, class: "py-2 px-3 block w-full border border-gray-300 rounded-lg text-sm bg-gray-50/50" %> +
+ +
+ + +
+ <%= link_to "Abbrechen", items_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 "Artikel 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/items/_item.html.erb b/app/views/items/_item.html.erb new file mode 100644 index 0000000..a159ba5 --- /dev/null +++ b/app/views/items/_item.html.erb @@ -0,0 +1,2 @@ +
+
diff --git a/app/views/items/_item.json.jbuilder b/app/views/items/_item.json.jbuilder new file mode 100644 index 0000000..41c207a --- /dev/null +++ b/app/views/items/_item.json.jbuilder @@ -0,0 +1,2 @@ +json.extract! item, :id, :created_at, :updated_at +json.url item_url(item, format: :json) diff --git a/app/views/items/edit.html.erb b/app/views/items/edit.html.erb new file mode 100644 index 0000000..943371b --- /dev/null +++ b/app/views/items/edit.html.erb @@ -0,0 +1,50 @@ + + +<% content_for :title, "Artikel bearbeiten: #{@item.name}" %> + + +<% content_for :top_bar_actions do %> + <%= link_to item_path(@item), 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 zu den Details + <% end %> +<% end %> + +
+ + + <%= render "form", item: @item %> + + +
+
+
+ + +
+
+

Artikel dauerhaft ausbuchen

+

Dies entfernt das Gerät unwiderruflich aus dem Bestand. Die bisherige Verlaufshistorie wird dabei ebenfalls gelöscht.

+
+
+ + + <%= link_to "Artikel löschen", + item_path(@item), + data: { turbo_method: :delete, turbo_confirm: "Möchtest du diesen Artikel wirklich unwiderruflich aus dem System entfernen?" }, + 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/items/index.html.erb b/app/views/items/index.html.erb new file mode 100644 index 0000000..a900964 --- /dev/null +++ b/app/views/items/index.html.erb @@ -0,0 +1,154 @@ + + +<% content_for :title, "Gesamtbestand" %> + + +<% content_for :top_bar_actions do %> + <%= link_to new_item_path, 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 %> + + + + + Artikel hinzufügen + <% end %> +<% end %> + +
+ + +
+
+ + +
+ +
+ + +
+
+ + +
+ <%= link_to items_path(format: :csv), class: "py-2 px-3 border border-gray-300 rounded-lg text-sm font-medium bg-white text-gray-700 hover:bg-gray-50 flex items-center gap-1.5 transition shadow-sm", title: "Liste als Excel/CSV exportieren" do %> + + + Daten exportieren + <% end %> +
+ +
+ + + +<% if @items.any? %> + +
+ + + + + + + + + + + + + + + <% @items.each do |item| %> + + + + + + + + + + + + + + + + + + + + + + <% end %> + +
Artikel / DetailsSeriennummer (SN)Aktueller Standort / InhaberSticker-IDWertAktionen
+
<%= item.name %>
+
SKU: <%= item.sku %> • <%= item.category.name %>
+
+ <%= item.serial_number %> + + <% if item.user.present? %> + + 👤 <%= item.user.name %> + + <% elsif item.room.present? %> + + 📍 <%= item.room.name_with_building %> + + <% else %> + + 📦 Im Hauptlager + + <% end %> + + #<%= item.sticker_id %> + + <%= number_to_currency(item.price, unit: "€", separator: ",", delimiter: ".", format: "%n %u") %> + + <%= link_to "Details", item_path(item), class: "text-blue-600 hover:text-blue-900 transition" %> + <%= link_to "Bearbeiten", edit_item_path(item), class: "text-gray-500 hover:text-gray-700 transition" %> +
+
+ <% else %> + +
+ + + +

Bisher sind keine Inventargegenstände erfasst.

+

Klicke oben rechts auf "Artikel hinzufügen", um das erste Gerät einzubuchen.

+
+ <% end %> + +
+
+ diff --git a/app/views/items/index.json.jbuilder b/app/views/items/index.json.jbuilder new file mode 100644 index 0000000..1bb9d93 --- /dev/null +++ b/app/views/items/index.json.jbuilder @@ -0,0 +1 @@ +json.array! @items, partial: "items/item", as: :item diff --git a/app/views/items/new.html.erb b/app/views/items/new.html.erb new file mode 100644 index 0000000..344e2f9 --- /dev/null +++ b/app/views/items/new.html.erb @@ -0,0 +1,27 @@ + + +<% content_for :title, "Neuen Artikel hinzufügen" %> + + +<% content_for :top_bar_actions do %> + <%= link_to items_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", item: @item %> +
+ diff --git a/app/views/items/show.html.erb b/app/views/items/show.html.erb new file mode 100644 index 0000000..008cb24 --- /dev/null +++ b/app/views/items/show.html.erb @@ -0,0 +1,241 @@ + + +<% content_for :title, "Artikel-Details: #{@item.name}" %> + + +<% content_for :top_bar_actions do %> +
+ <%= link_to items_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 + <% end %> + <%= link_to edit_item_path(@item), class: "py-2 px-3 text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 rounded-lg flex items-center gap-1.5 shadow-sm transition" do %> + + Bearbeiten + <% end %> +
+<% end %> + +
+ + +
+ + +
+ + +
+
+ + <%= @item.category.name %> + +

<%= @item.name %>

+
+ +
+ +
+
+

Artikelnummer (SKU)

+

<%= @item.sku %>

+
+
+

Seriennummer (Hersteller)

+

<%= @item.serial_number %>

+
+
+

Anschaffungspreis

+

+ <%= number_to_currency(@item.price, unit: "€", separator: ",", delimiter: ".", format: "%n %u") %> +

+
+
+

Registriert am

+

<%= l(@item.created_at, format: :short) %>

+
+
+ + <% if @item.notes.present? %> +
+
+

Beschreibung / Notizen

+

<%= @item.notes %>

+
+ <% end %> +
+ + +
+

Aktueller Status & Aufenthaltsort

+ + <% if @item.user.present? %> +
+
+ +
+
+

In Benutzung (Mitarbeiter)

+

<%= @item.user.name %>

+

<%= @item.user.department&.name || "Keine Abteilung hinterlegt" %>

+
+
+ <% elsif @item.room.present? %> +
+
+ +
+
+

Fest verbaut / Zugewiesener Raum

+

<%= @item.room.name %>

+

<%= @item.room.building %> • <%= @item.room.floor %>

+
+
+ <% else %> +
+
+ +
+
+

Verfügbar

+

Im Hauptlager

+

Bereit zur Zuweisung an Mitarbeiter oder Räume.

+
+
+ <% end %> +
+ +
+ + +
+
+

Interne Kennzeichnung

+ + +
+ <%= @item.generate_qr_code %> +
+ +
+

Inventarnummer

+

#<%= @item.sticker_id %>

+
+ +

Vorgedrucktes Etikett vom A4-Bogen. Scan im System führt direkt hierher.

+
+
+ +
+ + +
+
+ + +

Besitzer- & Standortverlauf

+
+ +
+ +
+ <% if @assignment_logs.any? %> +
    + + <% @assignment_logs.each_with_index do |log, index| %> + <% is_last = (index == @assignment_logs.size - 1) %> + +
  • +
    + + <% unless is_last %> + + <% end %> + +
    +
    + + <% if log.user.present? %> + + + + + + <% elsif log.room.present? %> + + + + + + <% else %> + + + + + + <% end %> +
    + + +
    +
    +

    + <% if log.user.present? %> + Zuweisung an Mitarbeiter: <%= log.user.name %> + <% elsif log.room.present? %> + Standortwechsel in Raum: <%= log.room.name_with_building %> + <% else %> + Ins Hauptlager übergeben + <% end %> +

    + + <% if log.returned_at.nil? %> + + Aktueller Standort + + <% end %> +
    + + +
    + +
    +
    + +
    +
    +
  • + <% end %> + +
+ <% else %> +
+ Bisher wurden keine historischen Übergaben für dieses Gerät verzeichnet. +
+ <% end %> +
+
+ +
diff --git a/app/views/items/show.json.jbuilder b/app/views/items/show.json.jbuilder new file mode 100644 index 0000000..7cfe06e --- /dev/null +++ b/app/views/items/show.json.jbuilder @@ -0,0 +1 @@ +json.partial! "items/item", item: @item diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb index a307620..a67c479 100644 --- a/app/views/layouts/application.html.erb +++ b/app/views/layouts/application.html.erb @@ -66,7 +66,7 @@ Dashboard <% end %> - <%= link_to "", class: nav_link_class("items") do %> + <%= link_to items_path, class: nav_link_class("items") do %> Bestandsliste <% end %> @@ -128,7 +128,7 @@ -
+
<% flash.each do |type, message| %>
diff --git a/config/routes.rb b/config/routes.rb index cfc9516..ca623c1 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,4 +1,5 @@ Rails.application.routes.draw do + resources :items resources :categories namespace :authentications do resources :events, only: :index @@ -26,5 +27,5 @@ Rails.application.routes.draw do # get "service-worker" => "rails/pwa#service_worker", as: :pwa_service_worker # Defines the root path route ("/") - root "home#index" + root "dashboard#index" end diff --git a/test/controllers/dashboard_controller_test.rb b/test/controllers/dashboard_controller_test.rb new file mode 100644 index 0000000..8e67afa --- /dev/null +++ b/test/controllers/dashboard_controller_test.rb @@ -0,0 +1,7 @@ +require "test_helper" + +class DashboardControllerTest < ActionDispatch::IntegrationTest + # test "the truth" do + # assert true + # end +end diff --git a/test/controllers/items_controller_test.rb b/test/controllers/items_controller_test.rb new file mode 100644 index 0000000..243596a --- /dev/null +++ b/test/controllers/items_controller_test.rb @@ -0,0 +1,48 @@ +require "test_helper" + +class ItemsControllerTest < ActionDispatch::IntegrationTest + setup do + @item = items(:one) + end + + test "should get index" do + get items_url + assert_response :success + end + + test "should get new" do + get new_item_url + assert_response :success + end + + test "should create item" do + assert_difference("Item.count") do + post items_url, params: { item: {} } + end + + assert_redirected_to item_url(Item.last) + end + + test "should show item" do + get item_url(@item) + assert_response :success + end + + test "should get edit" do + get edit_item_url(@item) + assert_response :success + end + + test "should update item" do + patch item_url(@item), params: { item: {} } + assert_redirected_to item_url(@item) + end + + test "should destroy item" do + assert_difference("Item.count", -1) do + delete item_url(@item) + end + + assert_redirected_to items_url + end +end