diff --git a/app/controllers/categories_controller.rb b/app/controllers/categories_controller.rb index fb16c0d..a319965 100644 --- a/app/controllers/categories_controller.rb +++ b/app/controllers/categories_controller.rb @@ -9,7 +9,17 @@ class CategoriesController < ApplicationController # GET /categories/1 or /categories/1.json def show @category = Category.find(params[:id]) - @items = @category.items.includes(:user, :room).order(:name) + + # Basis-Abfrage: Alle Items dieser Kategorie laden + @items = @category.items.includes(:user, :room).order(created_at: :desc) + + 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 /categories/new diff --git a/app/controllers/items_controller.rb b/app/controllers/items_controller.rb index 98241e8..77bfea8 100644 --- a/app/controllers/items_controller.rb +++ b/app/controllers/items_controller.rb @@ -5,6 +5,15 @@ class ItemsController < ApplicationController def index @items = Item.all.includes(:category, :user, :room).order(created_at: :desc) + if params[:query].present? + query_str = "%#{params[:query]}%" + # Durchsucht Name, SKU, Seriennummer und deine Sticker-ID gleichzeitig + @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 + respond_to do |format| format.html # Rendert ganz normal deine Bestandsliste im Browser format.csv do diff --git a/app/javascript/controllers/assignment_controller.js b/app/javascript/controllers/assignment_controller.js index 5410755..dd02d15 100644 --- a/app/javascript/controllers/assignment_controller.js +++ b/app/javascript/controllers/assignment_controller.js @@ -1,27 +1,29 @@ -import { Controller } from "@hotwire/stimulus" +// Wird der controller überhaupt gebraucht? Funktioniert auch ohne.. ;) + +import { Controller } from "@hotwired/stimulus"; export default class extends Controller { - static targets = [ "userSection", "roomSection" ] + static targets = ["userSection", "roomSection"]; toggle(event) { - const value = event.target.value - const userDropdown = this.userSectionTarget.querySelector('select') - const roomDropdown = this.roomSectionTarget.querySelector('select') + 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 + 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 + 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 = "" + 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 index 69bffd3..69102ae 100644 --- a/app/javascript/controllers/scanner_controller.js +++ b/app/javascript/controllers/scanner_controller.js @@ -1,49 +1,58 @@ -import { Controller } from "@hotwire/stimulus" +import { Controller } from "@hotwired/stimulus"; export default class extends Controller { - static targets = [ "input", "preview", "modal" ] + static targets = ["input", "preview", "modal"]; connect() { - this.html5QrCode = null + this.html5QrCode = null; } // Öffnet das Modal und startet den Kamera-Stream startCamera(event) { - event.preventDefault() - + event.preventDefault(); + // Modal anzeigen - this.modalTarget.classList.remove("hidden") + this.modalTarget.classList.remove("hidden"); // Neue Instanz auf dem Preview-Div mit der ID des Elements erzeugen - this.html5QrCode = new Html5Qrcode(this.previewTarget.id) + this.html5QrCode = new Html5Qrcode(this.previewTarget.id); // FPS-Rate und Scan-Rahmen (250x250px) festlegen - const config = { fps: 10, qrbox: { width: 250, height: 250 } } + 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) - }) + 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 + + // NEU: Simuliert das Tippen, damit dein search-form Controller die Live-Suche sofort startet! + this.inputTarget.dispatchEvent(new Event("input", { bubbles: true })); + + 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)) + 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") + this.modalTarget.classList.add("hidden"); } } } diff --git a/app/javascript/controllers/search_form_controller.js b/app/javascript/controllers/search_form_controller.js new file mode 100644 index 0000000..d706c80 --- /dev/null +++ b/app/javascript/controllers/search_form_controller.js @@ -0,0 +1,11 @@ +import { Controller } from "@hotwired/stimulus"; + +export default class extends Controller { + submit() { + clearTimeout(this.timeout); + // Wartet 200ms nach dem letzten Tastendruck, bevor gesucht wird + this.timeout = setTimeout(() => { + this.element.requestSubmit(); + }, 200); + } +} diff --git a/app/views/categories/show.html.erb b/app/views/categories/show.html.erb index f5a9917..549c86c 100644 --- a/app/views/categories/show.html.erb +++ b/app/views/categories/show.html.erb @@ -8,28 +8,25 @@ <% end %>
- +

Beschreibung

<%= @category.description.presence || "Keine Beschreibung hinterlegt." %>

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

Bisher sind keine Inventargegenstände erfasst.

-

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

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

Keine passenden Artikel in dieser Kategorie gefunden.

+
+ <% end %> <% end %>
- diff --git a/app/views/items/_list.html.erb b/app/views/items/_list.html.erb index fbbf919..eea04c1 100644 --- a/app/views/items/_list.html.erb +++ b/app/views/items/_list.html.erb @@ -1,19 +1,19 @@
- +
<% items.each do |item| %>
- +

<%= item.name %>

<%= item.category.name %>

- +
@@ -58,10 +58,17 @@
- <%= link_to item_path(item), 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 %> + + <%= 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), 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 %> + + + <%= 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 %>
@@ -103,23 +110,33 @@ 📦 Im Hauptlager <% end %> - + #<%= item.sticker_id %> - + <%= number_to_currency(item.price, unit: "€", separator: ",", delimiter: ".", format: "%n %u") %> +
- <%= link_to item_path(item), 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", title: "Details anzeigen" do %> + + <%= 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", + title: "Details anzeigen" do %> <% end %> - <%= link_to edit_item_path(item), 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", title: "Artikel bearbeiten" do %> + + + <%= 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", + title: "Artikel bearbeiten" do %> <% end %>
diff --git a/app/views/items/_search_bar.html.erb b/app/views/items/_search_bar.html.erb index ac960cd..708e7a5 100644 --- a/app/views/items/_search_bar.html.erb +++ b/app/views/items/_search_bar.html.erb @@ -1,11 +1,48 @@ -
- - -
- +<%# Wenn dem Partial keine feste URL übergeben wurde, nutzt es automatisch die aktuelle Browser-URL %> +<% form_url = local_assigns[:url] || request.fullpath %> + +<%= form_with(url: form_url, method: :get, + data: { controller: "search-form", turbo_frame: "items_list_frame" }, + class: "w-full bg-white border border-gray-200 rounded-xl shadow-sm p-4 bg-gray-50 flex flex-col sm:flex-row gap-3 justify-between items-center mb-6") do |form| %> + + +
+ <%= form.text_field :query, + value: params[:query], + placeholder: "Artikel, SKU, SN oder ID...", + data: { action: "input->search-form#submit", scanner_target: "input" }, + class: "py-2 px-3 pl-9 pr-10 block w-full border border-gray-300 rounded-lg text-sm bg-white focus:border-blue-500 focus:ring-blue-500" %> + +
- - + + + +
+ + + + + +
@@ -19,7 +56,7 @@ CSV <% end %> <% end %> - +
-
+<% end %> diff --git a/app/views/items/index.html.erb b/app/views/items/index.html.erb index 142099f..dd7ca99 100644 --- a/app/views/items/index.html.erb +++ b/app/views/items/index.html.erb @@ -1,35 +1,6 @@ - - <% 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 %> @@ -38,23 +9,17 @@ <% end %>
- + <%= render "items/search_bar", show_csv: true %> - - <% if @items.any? %> - <%= render "items/list", items: @items %> - <% else %> -
- - - -

Bisher sind keine Inventargegenstände erfasst.

-

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

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

Keine passenden Inventargegenstände gefunden.

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