Compare commits

...

30 Commits

Author SHA1 Message Date
c139ae6aa8 Hide text in search buttons in items#index
Some checks failed
CI / scan_ruby (push) Has been cancelled
CI / scan_js (push) Has been cancelled
CI / lint (push) Has been cancelled
CI / test (push) Has been cancelled
CI / system-test (push) Has been cancelled
2026-06-01 10:29:55 +02:00
f7375d29a6 Added htmlbeautifier to gemfile
Some checks failed
CI / scan_ruby (push) Has been cancelled
CI / scan_js (push) Has been cancelled
CI / lint (push) Has been cancelled
CI / test (push) Has been cancelled
CI / system-test (push) Has been cancelled
2026-06-01 10:02:21 +02:00
8c7482c1d7 Changed item#_form input user/room
Some checks failed
CI / scan_ruby (push) Has been cancelled
CI / scan_js (push) Has been cancelled
CI / lint (push) Has been cancelled
CI / test (push) Has been cancelled
CI / system-test (push) Has been cancelled
2026-05-31 23:34:57 +02:00
68d31090f7 Updated icons in sidebar
Some checks failed
CI / scan_ruby (push) Has been cancelled
CI / scan_js (push) Has been cancelled
CI / lint (push) Has been cancelled
CI / test (push) Has been cancelled
CI / system-test (push) Has been cancelled
2026-05-31 22:58:41 +02:00
8c5e862eb3 Fixed flash in mobile view
Some checks failed
CI / scan_ruby (push) Has been cancelled
CI / scan_js (push) Has been cancelled
CI / lint (push) Has been cancelled
CI / test (push) Has been cancelled
CI / system-test (push) Has been cancelled
2026-05-31 01:52:01 +02:00
ff9f8b7523 Moved flash overlay away from buttons in top nav
Some checks failed
CI / scan_ruby (push) Has been cancelled
CI / scan_js (push) Has been cancelled
CI / lint (push) Has been cancelled
CI / test (push) Has been cancelled
CI / system-test (push) Has been cancelled
2026-05-31 01:40:37 +02:00
92786161b4 Removed category badge in item#show
Some checks failed
CI / scan_ruby (push) Has been cancelled
CI / scan_js (push) Has been cancelled
CI / lint (push) Has been cancelled
CI / test (push) Has been cancelled
CI / system-test (push) Has been cancelled
2026-05-31 01:27:44 +02:00
c1fc589883 Reduced infos in item#show log
Some checks failed
CI / scan_ruby (push) Has been cancelled
CI / scan_js (push) Has been cancelled
CI / lint (push) Has been cancelled
CI / test (push) Has been cancelled
CI / system-test (push) Has been cancelled
2026-05-31 01:22:53 +02:00
59c462506d Fixed dashboard#index mobile view
Some checks failed
CI / scan_ruby (push) Has been cancelled
CI / scan_js (push) Has been cancelled
CI / lint (push) Has been cancelled
CI / test (push) Has been cancelled
CI / system-test (push) Has been cancelled
2026-05-31 01:09:53 +02:00
c12eab7f98 And again really small layout adjustments
Some checks failed
CI / scan_ruby (push) Has been cancelled
CI / scan_js (push) Has been cancelled
CI / lint (push) Has been cancelled
CI / test (push) Has been cancelled
CI / system-test (push) Has been cancelled
2026-05-30 03:21:16 +02:00
1f18b7dd7d Some more layout adjustments
Some checks failed
CI / scan_ruby (push) Has been cancelled
CI / scan_js (push) Has been cancelled
CI / lint (push) Has been cancelled
CI / test (push) Has been cancelled
CI / system-test (push) Has been cancelled
2026-05-30 03:17:48 +02:00
2931314cd1 More layout adjustments and more dry refactor
Some checks failed
CI / scan_ruby (push) Has been cancelled
CI / scan_js (push) Has been cancelled
CI / lint (push) Has been cancelled
CI / test (push) Has been cancelled
CI / system-test (push) Has been cancelled
2026-05-30 03:02:31 +02:00
d8c6a7ab82 Small layout adjustments and form errors in partial outsourced
Some checks failed
CI / scan_ruby (push) Has been cancelled
CI / scan_js (push) Has been cancelled
CI / lint (push) Has been cancelled
CI / test (push) Has been cancelled
CI / system-test (push) Has been cancelled
2026-05-30 02:11:51 +02:00
fd3149b13f Added condition badge in show and removed in_use
Some checks failed
CI / scan_ruby (push) Has been cancelled
CI / scan_js (push) Has been cancelled
CI / lint (push) Has been cancelled
CI / test (push) Has been cancelled
CI / system-test (push) Has been cancelled
2026-05-30 01:45:23 +02:00
5c9e6a34b4 Added QR Scanner to more input fields
Some checks failed
CI / scan_ruby (push) Has been cancelled
CI / scan_js (push) Has been cancelled
CI / lint (push) Has been cancelled
CI / test (push) Has been cancelled
CI / system-test (push) Has been cancelled
2026-05-30 00:02:02 +02:00
00be2bd4d3 Added essential de translations
Some checks failed
CI / scan_ruby (push) Has been cancelled
CI / scan_js (push) Has been cancelled
CI / lint (push) Has been cancelled
CI / test (push) Has been cancelled
CI / system-test (push) Has been cancelled
2026-05-28 23:16:16 +02:00
f7ef41459e Removed unused files
Some checks failed
CI / scan_ruby (push) Has been cancelled
CI / scan_js (push) Has been cancelled
CI / lint (push) Has been cancelled
CI / test (push) Has been cancelled
CI / system-test (push) Has been cancelled
2026-05-28 22:51:15 +02:00
bdcdcbd681 Fixed typo..
Some checks failed
CI / scan_ruby (push) Has been cancelled
CI / scan_js (push) Has been cancelled
CI / lint (push) Has been cancelled
CI / test (push) Has been cancelled
CI / system-test (push) Has been cancelled
2026-05-28 22:30:28 +02:00
9e18b233d9 Added condition for items and started with localize
Some checks failed
CI / scan_ruby (push) Has been cancelled
CI / scan_js (push) Has been cancelled
CI / lint (push) Has been cancelled
CI / test (push) Has been cancelled
CI / system-test (push) Has been cancelled
2026-05-28 22:28:13 +02:00
edf3886b94 Fixed bad layout error (no display of main content)
Some checks failed
CI / scan_ruby (push) Has been cancelled
CI / scan_js (push) Has been cancelled
CI / lint (push) Has been cancelled
CI / test (push) Has been cancelled
CI / system-test (push) Has been cancelled
2026-05-28 19:50:57 +02:00
a3352bc1eb Fixed branch display when deployed
Some checks failed
CI / scan_ruby (push) Has been cancelled
CI / scan_js (push) Has been cancelled
CI / lint (push) Has been cancelled
CI / test (push) Has been cancelled
CI / system-test (push) Has been cancelled
2026-05-28 19:48:30 +02:00
0ede18f6f0 Added link to gitea commit to sidebar
Some checks failed
CI / scan_ruby (push) Has been cancelled
CI / scan_js (push) Has been cancelled
CI / lint (push) Has been cancelled
CI / test (push) Has been cancelled
CI / system-test (push) Has been cancelled
2026-05-28 19:43:56 +02:00
b4c57b6f14 Added more infos in category#index cards
Some checks failed
CI / scan_ruby (push) Has been cancelled
CI / scan_js (push) Has been cancelled
CI / lint (push) Has been cancelled
CI / test (push) Has been cancelled
CI / system-test (push) Has been cancelled
2026-05-28 19:24:37 +02:00
af10dca289 Added Room and some little design updates
Some checks failed
CI / scan_ruby (push) Has been cancelled
CI / scan_js (push) Has been cancelled
CI / lint (push) Has been cancelled
CI / test (push) Has been cancelled
CI / system-test (push) Has been cancelled
2026-05-28 01:04:19 +02:00
e161582c4a Import qr-scanner the rails way.
Some checks failed
CI / scan_ruby (push) Has been cancelled
CI / scan_js (push) Has been cancelled
CI / lint (push) Has been cancelled
CI / test (push) Has been cancelled
CI / system-test (push) Has been cancelled
2026-05-27 02:18:24 +02:00
a0e7272b6f Fixed qr-scanner (needs to be deployed (https))
Some checks failed
CI / scan_ruby (push) Has been cancelled
CI / scan_js (push) Has been cancelled
CI / lint (push) Has been cancelled
CI / test (push) Has been cancelled
CI / system-test (push) Has been cancelled
2026-05-27 01:56:45 +02:00
b66f59eedc Added qr scanner
Some checks failed
CI / scan_ruby (push) Has been cancelled
CI / scan_js (push) Has been cancelled
CI / lint (push) Has been cancelled
CI / test (push) Has been cancelled
CI / system-test (push) Has been cancelled
2026-05-27 00:46:52 +02:00
623b7f0256 Small layout fixes Dasboard#index
Some checks failed
CI / scan_ruby (push) Has been cancelled
CI / scan_js (push) Has been cancelled
CI / lint (push) Has been cancelled
CI / test (push) Has been cancelled
CI / system-test (push) Has been cancelled
2026-05-26 03:27:29 +02:00
a0409e1cf8 Expand items search field for user and room
Some checks failed
CI / scan_ruby (push) Has been cancelled
CI / scan_js (push) Has been cancelled
CI / lint (push) Has been cancelled
CI / test (push) Has been cancelled
CI / system-test (push) Has been cancelled
2026-05-26 03:15:25 +02:00
204a6c05dc Added Search function for items and fixed javascript controller
Some checks failed
CI / scan_ruby (push) Has been cancelled
CI / scan_js (push) Has been cancelled
CI / lint (push) Has been cancelled
CI / test (push) Has been cancelled
CI / system-test (push) Has been cancelled
2026-05-25 04:23:08 +02:00
56 changed files with 1714 additions and 662 deletions

View File

@@ -63,6 +63,8 @@ group :development do
gem "letter_opener", "~> 1.10"
gem "hotwire-spark"
gem "htmlbeautifier"
end
group :test do

View File

@@ -134,6 +134,7 @@ GEM
listen
rails (>= 7.0.0)
zeitwerk
htmlbeautifier (1.4.3)
i18n (1.14.8)
concurrent-ruby (~> 1.0)
image_processing (1.14.0)
@@ -427,6 +428,7 @@ DEPENDENCIES
csv (~> 3.3)
debug
hotwire-spark
htmlbeautifier
image_processing (~> 1.2)
importmap-rails
jbuilder
@@ -499,6 +501,7 @@ CHECKSUMS
fugit (1.12.1) sha256=5898f478ede9b415f0804e42b8f3fd53f814bd85eebffceebdbc34e1107aaf68
globalid (1.3.0) sha256=05c639ad6eb4594522a0b07983022f04aa7254626ab69445a0e493aa3786ff11
hotwire-spark (0.1.13) sha256=0a24799b0942fc9b7ea3e560a5aba3f4dd9a6086958a25929894340f920fb499
htmlbeautifier (1.4.3) sha256=b43d08f7e2aa6ae1b5a6f0607b4ed8954c8d4a8e85fd2336f975dda1e4db385b
i18n (1.14.8) sha256=285778639134865c5e0f6269e0b818256017e8cde89993fdfcbfb64d088824a5
image_processing (1.14.0) sha256=754cc169c9c262980889bec6bfd325ed1dafad34f85242b5a07b60af004742fb
importmap-rails (2.2.3) sha256=7101be2a4dc97cf1558fb8f573a718404c5f6bcfe94f304bf1f39e444feeb16a

View File

@@ -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.left_outer_joins(:user).left_outer_joins(:room).where(
"items.name LIKE :q OR items.sku LIKE :q OR items.serial_number LIKE :q OR items.sticker_id LIKE :q OR users.first_name LIKE :q OR users.last_name LIKE :q OR rooms.name LIKE :q",
q: query_str
).order("items.name ASC")
end
end
# GET /categories/new

View File

@@ -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 und firstname und lastname gleichzeitig
@items = @items.left_outer_joins(:user).left_outer_joins(:room).where(
"items.name LIKE :q OR items.sku LIKE :q OR items.serial_number LIKE :q OR items.sticker_id LIKE :q OR users.first_name LIKE :q OR users.last_name LIKE :q OR rooms.name LIKE :q",
q: query_str
).order("items.name ASC")
end
respond_to do |format|
format.html # Rendert ganz normal deine Bestandsliste im Browser
format.csv do
@@ -116,6 +125,6 @@ class ItemsController < ApplicationController
def item_params
# 'user_name' und 'room_name' müssen in die Strong Parameters aufgenommen werden!
params.require(:item).permit(:name, :sku, :sticker_id, :serial_number, :price, :notes, :category_id, :user_id, :room_id, :user_name, :room_name)
params.require(:item).permit(:name, :sku, :sticker_id, :serial_number, :price, :notes, :category_id, :user_id, :room_id, :user_name, :room_name, :condition)
end
end

View File

@@ -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

View File

@@ -0,0 +1,2 @@
module RoomsHelper
end

View File

@@ -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 = "";
}
}
}

View File

@@ -0,0 +1,65 @@
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
static targets = ["input", "results", "item"]
connect() {
// Schließt die Liste, wenn man irgendwo außerhalb hinklickt
this.closeHandler = (e) => {
if (!this.element.contains(e.target)) {
this.hideResults()
}
}
document.addEventListener("click", this.closeHandler)
}
disconnect() {
document.removeEventListener("click", this.closeHandler)
}
// Wird aufgerufen, wenn der Nutzer tippt (input-Event)
filter() {
const filterValue = this.inputTarget.value.toLowerCase().trim()
if (filterValue === "") {
this.showAll()
return
}
this.showResults()
let hasMatches = false
this.itemTargets.forEach(item => {
const text = item.textContent.toLowerCase()
if (text.includes(filterValue)) {
item.classList.remove("hidden")
hasMatches = true
} else {
item.classList.add("hidden")
}
})
// Falls gar nichts gefunden wird, blenden wir die Liste aus
if (!hasMatches) this.hideResults()
}
// Ein Klick auf einen Eintrag wählt ihn aus
select(event) {
const value = event.currentTarget.dataset.value
this.inputTarget.value = value
this.hideResults()
}
showResults() {
this.resultsTarget.classList.remove("hidden")
}
hideResults() {
this.resultsTarget.classList.add("hidden")
}
showAll() {
this.showResults()
this.itemTargets.forEach(item => item.classList.remove("hidden"))
}
}

View File

@@ -1,49 +1,59 @@
import { Controller } from "@hotwire/stimulus"
import { Controller } from "@hotwired/stimulus";
//import QrScanner from "qr-scanner"; // Rails findet jetzt über die Importmap deine Datei unter vendor/javascript/
// KORREKTUR: Lädt das Modul ohne Namens-Zuweisung, da die Legacy-Datei keinen Default-Export besitzt
import "qr-scanner";
export default class extends Controller {
static targets = [ "input", "preview", "modal" ]
static targets = ["input", "preview", "modal"];
connect() {
this.html5QrCode = null
this.qrScanner = null;
}
// Öffnet das Modal und startet den Kamera-Stream
startCamera(event) {
event.preventDefault()
event.preventDefault();
this.modalTarget.classList.remove("hidden");
// Modal anzeigen
this.modalTarget.classList.remove("hidden")
const videoElement = this.previewTarget;
// 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)
// Scanner initialisieren (Direkt über die importierte Klasse ohne 'window.')
//this.qrScanner = new QrScanner(
// Greift jetzt absolut fehlerfrei auf die geladene Klasse zu
this.qrScanner = new window.QrScanner(
videoElement,
(result) => { this.handleScanSuccess(result.data); },
{
onDecodeError: (error) => { /* Fehler ignorieren */ },
highlightScanRegion: true,
highlightCodeOutline: true,
maxScansPerSecond: 10
}
).catch((err) => {
console.error("Kamera-Zugriff verweigert oder blockiert:", err)
})
);
this.qrScanner.start().catch((err) => {
alert("Kamera-Zugriff blockiert! Bitte prüfe die Browser-Berechtigungen.");
console.error("Kamera-Fehler:", err);
this.modalTarget.classList.add("hidden");
});
}
handleScanSuccess(decodedText) {
this.inputTarget.value = decodedText;
this.inputTarget.dispatchEvent(new Event("input", { bubbles: true }));
this.stopCamera();
}
// 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")
}
if (this.qrScanner) {
this.qrScanner.stop();
this.qrScanner.destroy();
this.qrScanner = null;
}
this.modalTarget.classList.add("hidden");
}
disconnect() {
this.stopCamera();
}
}

View File

@@ -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);
}
}

View File

@@ -1,5 +0,0 @@
class AssignmentLog2 < ApplicationRecord
belongs_to :item
belongs_to :user
belongs_to :room
end

View File

@@ -2,4 +2,19 @@ class Category < ApplicationRecord
has_many :items, dependent: :restrict_with_error
validates :name, presence: true, uniqueness: true
# 1. Zählt Artikel im Hauptlager (weder User noch Raum zugewiesen)
def items_in_storage_count
items.where(user_id: nil, room_id: nil).count
end
# 2. Zählt Artikel, die bei Mitarbeitern im Umlauf sind
def items_with_users_count
items.where.not(user_id: nil).count
end
# 3. Zählt Artikel, die fest in Räumen verbaut sind
def items_in_rooms_count
items.where.not(room_id: nil).count
end
end

View File

@@ -6,6 +6,15 @@ class Item < ApplicationRecord
# ohne dass dafür Spalten in der Datenbank existieren müssen.
attr_accessor :user_name, :room_name
enum :condition, {
unknown: "unknown",
new_item: "new_item",
as_new: "as_new",
used: "used",
heavily_used: "heavily_used",
defective: "defective"
}
belongs_to :category
belongs_to :user, optional: true # Optional, falls im Raum oder Lager
belongs_to :room, optional: true # Optional, falls beim User oder Lager
@@ -71,6 +80,42 @@ class Item < ApplicationRecord
end
end
# Holt die saubere Übersetzung vollautomatisch über die Rails-Konvention
# In app/models/item.rb
def human_condition
# Holt den nackten String-Wert direkt aus der Spalte
current_condition = self[:condition]
return nil if current_condition.blank?
Item.human_attribute_name("conditions.#{current_condition}")
end
# 1. Ermittelt den abstrakten Standort-Typen für das Badge
def location_badge_type
if user_id.present?
"user"
elsif room_id.present?
"room"
else
"storage"
end
end
# 2. Liefert den passenden Text für das Standort-Badge
def location_badge_label(short_room: false)
if user.present?
user.name
elsif room.present?
short_room ? room.name : room.name_with_building
else
"Hauptlager"
end
end
def condition_badge_type
condition
end
private
def either_user_or_room

View File

@@ -1,41 +1,32 @@
<%= form_with(model: category, 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| %>
<%= form_with(model: category, class: "space-y-6 max-w-4xl mx-auto bg-white border border-gray-200 rounded-xl shadow-sm p-6 md:p-8") do |form| %>
<% if category.errors.any? %>
<div class="p-4 mb-4 text-sm text-red-800 rounded-lg bg-red-50 border border-red-200" role="alert">
<h2 class="font-bold mb-1"><%= pluralize(category.errors.count, "Fehler") %> verhinderten das Speichern:</h2>
<ul class="list-disc list-inside text-xs">
<% category.errors.full_messages.each do |message| %>
<li><%= message %></li>
<% end %>
</ul>
</div>
<% end %>
<div>
<h2 class="text-xl font-bold text-gray-800">Kategorie-Details</h2>
<p class="text-sm text-gray-500 mt-1">Definiere eine übergeordnete Gruppe für dein Inventar (z.B. Laptops oder Bürostühle).</p>
</div>
<%= render "layouts/form_header",
model: @category,
new_title: "Kategorie anlegen",
new_description: "Definiere einen neuen Hardware-Typ für deine Bestandsliste.",
edit_title: "Kategorie bearbeiten",
edit_description: "Aktualisiere die Bezeichnung oder die Notizen dieser Kategorie." %>
<hr class="border-gray-200">
<!-- Name -->
<div>
<%= form.label :name, "Name der Kategorie", class: "block text-sm font-medium mb-2 text-gray-700" %>
<%= form.text_field :name, required: true, class: "py-2.5 px-4 block w-full border border-gray-300 rounded-lg text-sm focus:border-blue-500 focus:ring-blue-500 bg-gray-50/50", placeholder: "z.B. IT-Infrastruktur" %>
<%= render "layouts/form_errors", model: category %>
<!-- KATEGORIENAME INPUT -->
<div class="space-y-1.5">
<%= form.label :name, "Kategoriebezeichnung", class: "block text-sm font-semibold text-gray-700" %>
<%= form.text_field :name, placeholder: "z.B. Laptops, Monitore, Server...", class: "py-2.5 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" %>
</div>
<!-- Beschreibung -->
<div>
<%= form.label :description, "Beschreibung / Notizen", class: "block text-sm font-medium mb-2 text-gray-700" %>
<div class="space-y-1.5">
<%= form.label :description, "Beschreibung / Notizen", class: "block text-sm font-semibold text-gray-700" %>
<%= form.text_area :description, rows: 4, class: "py-2.5 px-4 block w-full border border-gray-300 rounded-lg text-sm focus:border-blue-500 focus:ring-blue-500 bg-gray-50/50", placeholder: "Welche Art von Gegenständen fällt in diese Kategorie?..." %>
</div>
<hr class="border-gray-200">
<!-- Buttons -->
<div class="flex justify-end gap-x-3">
<%= link_to "Abbrechen", categories_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 "Kategorie 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" %>
<!-- AKTIONEN (Speichern & Abbrechen) -->
<div class="flex items-center justify-end gap-3 pt-4 border-t border-gray-100">
<%= link_to "Abbrechen", categories_path, class: "py-2 px-4 text-sm font-semibold rounded-lg border border-gray-200 bg-white text-gray-700 hover:bg-gray-50 transition" %>
<%= form.submit "Kategorie speichern", class: "py-2 px-4 text-sm font-semibold rounded-lg bg-blue-600 text-white hover:bg-blue-700 shadow-sm transition cursor-pointer" %>
</div>
<% end %>

View File

@@ -1,14 +1,21 @@
<% content_for :title, "Kategorie bearbeiten" %>
<% content_for :title, @category.name %>
<div class="p-4 md:p-6 space-y-6">
<% content_for :top_bar_actions do %>
<%= link_to "javascript:history.back()", 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 %>
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M10.5 19.5L3 12m0 0l7.5-7.5M3 12h18" /></svg>
<div class="hidden md:inline">
Zurück
</div>
<% end %>
<% end %>
<div class="space-y-6 p-4 md:p-6">
<%= render "form", category: @category %>
<!-- Gefahrenbereich: Kategorie löschen (Nur wenn keine Items drin sind) -->
<div class="max-w-2xl mx-auto bg-red-50/50 border border-red-200 rounded-xl p-6 flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4">
<div>
<h3 class="text-sm font-bold text-red-800">Kategorie löschen</h3>
<p class="text-xs text-red-600 mt-0.5">Dies kann nicht rückgängig gemacht werden. Nur möglich, wenn die Kategorie komplett leer ist.</p>
</div>
<%= link_to "Löschen", @category, data: { turbo_method: :delete, turbo_confirm: "Möchtest du diese Kategorie wirklich unwiderruflich löschen?" }, class: "py-2 px-3 text-xs font-semibold text-white bg-red-600 hover:bg-red-700 rounded-lg shadow-sm transition" %>
</div>
<%= render "layouts/danger_zone",
title: "Kategorie löschen",
description: "Das Löschen einer Kategorie entfernt diesen Typ dauerhaft. Alle zugeordneten Artikel müssen danach neu kategorisiert werden.",
button_text: "Kategorie löschen",
confirm_message: "Möchtest du diese Kategorie wirklich löschen? Alle Artikel dieses Typs verlieren ihre Kategorie.",
path: category_path(@category) %>
</div>

View File

@@ -1,44 +1,92 @@
<!-- Ersetze den alten Button im Top-Bar-Yield durch diesen echten Link -->
<% content_for :title, "Kategorien" %>
<div class="space-y-6">
<!-- Header-Aktionen (Nutzt das Yield aus deinem Layout) -->
<!-- NEU: Fügt den Hinzufügen-Button oben rechts in die Hauptleiste ein -->
<% content_for :top_bar_actions do %>
<%= link_to new_category_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 %>
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15" /></svg>
Kategorie erstellen
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15" />
</svg>
<div class="hidden md:inline">
Kategorie anlegen
</div>
<% end %>
<% end %>
<!-- Das Kachel-Grid -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<div class="space-y-6">
<% if @categories.any? %>
<!-- RASTER: Symmetrische Kacheln für Desktop und Mobilgeräte -->
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
<% @categories.each do |category| %>
<div class="bg-white border border-gray-200 rounded-xl p-6 shadow-sm hover:shadow-md transition flex flex-col justify-between">
<div class="bg-white border border-gray-200 rounded-xl p-5 shadow-sm hover:shadow-md transition flex flex-col justify-between gap-4">
<!-- OBERER TEIL: Kategorie-Name -->
<div>
<div class="flex items-center justify-between mb-4">
<div class="p-2.5 bg-blue-50 text-blue-600 rounded-lg">
<!-- Heroicon: folder -->
<svg class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M2.25 12.75V12A2.25 2.25 0 0 1 4.5 9.75h15A2.25 2.25 0 0 1 21.75 12v.75m-19.5 0A2.25 2.25 0 0 0 2.25 15v2.25m0-4.5A2.25 2.25 0 0 1 4.5 12h15a2.25 2.25 0 0 1 2.25 2.25m-19.5 0v4.5A2.25 2.25 0 0 0 4.5 21h15a2.25 2.25 0 0 0 2.25-2.25V15M2.25 12V6a2.25 2.25 0 0 1 2.25-2.25h2.25c.59 0 1.157.234 1.576.652L9.75 5.85a2.25 2.25 0 0 0 1.576.652h6.924a2.25 2.25 0 0 1 2.25 2.25v3.25" /></svg>
<div class="flex items-center gap-3.5">
<!-- Großes Ordner-Icon -->
<div class="p-2.5 bg-blue-50 text-blue-600 rounded-xl shrink-0 shadow-sm border border-blue-100">
<svg class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M2.25 12.75V12A2.25 2.25 0 0 1 4.5 9.75h15A2.25 2.25 0 0 1 21.75 12v.75m-19.5 0A2.25 2.25 0 0 0 2.25 15v2.25m0-4.5A2.25 2.25 0 0 1 4.5 12h15a2.25 2.25 0 0 1 2.25 2.25m-19.5 0v4.5A2.25 2.25 0 0 0 4.5 21h15a2.25 2.25 0 0 0 2.25-2.25V15M2.25 12V6a2.25 2.25 0 0 1 2.25-2.25h2.25c.59 0 1.157.234 1.576.652L9.75 5.85a2.25 2.25 0 0 0 1.576.652h6.924a2.25 2.25 0 0 1 2.25 2.25v3.25" />
</svg>
</div>
<!-- Dynamische Anzahl der Objekte im System -->
<span class="inline-flex items-center py-1 px-2.5 rounded-full text-xs font-semibold bg-gray-100 text-gray-800 border border-gray-200">
<%= category.items.count %> <%= category.items.count == 1 ? "Objekt" : "Objekte" %>
</span>
<div class="min-w-0">
<h3 class="text-base font-black text-gray-900 truncate leading-tight"><%= category.name %></h3>
<span class="text-[11px] font-bold text-gray-400 font-mono block mt-0.5 uppercase tracking-wider">Gesamt: <%= category.items.count %></span>
</div>
<h3 class="text-base font-bold text-gray-800"><%= category.name %></h3>
<p class="text-xs text-gray-500 mt-2 leading-relaxed"><%= category.description.presence || "Keine Beschreibung hinterlegt." %></p>
</div>
<div class="mt-6 pt-4 border-t border-gray-100 flex justify-end gap-3 text-xs font-semibold">
<!-- MITTLERER TEIL: Die reinen Bestands-Kacheln (Größere Zahlen & Icons, ohne Text) -->
<div class="mt-4 grid grid-cols-3 gap-2 border-t border-b border-gray-100 py-3 bg-gray-50/50 -mx-5 px-5">
<%= link_to "Bearbeiten", edit_category_path(category), class: "text-gray-500 hover:text-gray-700 transition" %>
<!-- 1. IM HAUPTLAGER (Amber/Orange) -->
<div class="flex flex-col items-center justify-center py-2.5 bg-white border border-amber-100 rounded-lg shadow-sm">
<!-- Icon vergrößert auf h-5 w-5 -->
<svg class="h-5 w-5 text-amber-500 shrink-0" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M21 7.5l-9-5.25L3 7.5m18 0l-9 5.25m9-5.25v9l-9 5.25M3 7.5l9 5.25M3 7.5v9l5.25 3.03M12 12.75v9" />
</svg>
<!-- Zahl vergrößert auf text-base -->
<span class="text-base font-black text-gray-900 mt-1.5 font-mono leading-none"><%= category.items_in_storage_count %></span>
</div>
<%= link_to category_path(category), class: "text-blue-600 hover:text-blue-800 flex items-center gap-0.5 transition" do %>
Artikel ansehen &rarr;
<!-- 2. BEI USERN / MITARBEITERN (Grün) -->
<div class="flex flex-col items-center justify-center py-2.5 bg-white border border-green-100 rounded-lg shadow-sm">
<!-- Icon vergrößert auf h-5 w-5 -->
<svg class="h-5 w-5 text-green-500 shrink-0" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M15.75 6a3.75 3.75 0 1 1-7.5 0 3.75 3.75 0 0 1 7.5 0ZM4.501 20.118a7.5 7.5 0 0 1 14.998 0A17.933 17.933 0 0 1 12 21.75c-2.676 0-5.216-.584-7.499-1.632Z" />
</svg>
<!-- Zahl vergrößert auf text-base -->
<span class="text-base font-black text-gray-900 mt-1.5 font-mono leading-none"><%= category.items_with_users_count %></span>
</div>
<!-- 3. IN RÄUMEN / BÜROS (Blau) -->
<div class="flex flex-col items-center justify-center py-2.5 bg-white border border-blue-100 rounded-lg shadow-sm">
<!-- Icon vergrößert auf h-5 w-5 -->
<svg class="h-5 w-5 text-blue-500 shrink-0" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M15 10.5a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z" />
<path stroke-linecap="round" stroke-linejoin="round" d="M19.5 10.5c0 7.142-7.5 11.25-7.5 11.25s-7.5-4.108-7.5-11.25a7.5 7.5 0 1 1 15 0Z" />
</svg>
<!-- Zahl vergrößert auf text-base -->
<span class="text-base font-black text-gray-900 mt-1.5 font-mono leading-none"><%= category.items_in_rooms_count %></span>
</div>
</div>
</div>
<!-- UNTERER TEIL: REST-Aktionen -->
<div class="pt-1 flex items-center justify-between text-xs font-semibold">
<%= link_to category_path(category), class: "text-blue-600 hover:text-blue-700 transition" do %>
Liste öffnen →
<% end %>
<%= link_to "Bearbeiten", edit_category_path(category), class: "text-gray-400 hover:text-gray-600 transition" %>
</div>
</div>
<% end %>
</div>
<% else %>
<div class="text-center py-16 text-gray-400 bg-white border border-gray-200 rounded-xl shadow-sm">
<p class="text-sm font-medium">Bisher sind keine Kategorien hinterlegt.</p>
</div>
<% end %>
</div>

View File

@@ -1,5 +1,15 @@
<% content_for :title, "Neue Kategorie erstellen" %>
<% content_for :title, "Kategorien anlegen" %>
<% content_for :top_bar_actions do %>
<%= link_to "javascript:history.back()", 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 %>
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M10.5 19.5L3 12m0 0l7.5-7.5M3 12h18" /></svg>
<div class="hidden md:inline">
Zurück
</div>
<% end %>
<% end %>
<div class="p-4 md:p-6">
<%= render "form", category: @category %>
</div>

View File

@@ -1,35 +1,34 @@
<% content_for :title, "Kategorie: #{@category.name}" %>
<% content_for :top_bar_actions do %>
<%= link_to categories_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 %>
<%= link_to "javascript:history.back()", 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 %>
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M10.5 19.5L3 12m0 0l7.5-7.5M3 12h18" /></svg>
Alle Kategorien
<div class="hidden md:inline">
Zurück
</div>
<% end %>
<% end %>
<div class="w-full space-y-6">
<!-- Beschreibungskarte -->
<!-- Beschreibungskarte der Kategorie -->
<div class="bg-white border border-gray-200 rounded-xl p-6 shadow-sm">
<h2 class="text-sm font-bold text-gray-400 uppercase tracking-wider">Beschreibung</h2>
<p class="text-sm text-gray-600 mt-1 leading-relaxed"><%= @category.description.presence || "Keine Beschreibung hinterlegt." %></p>
</div>
<div>
<!-- 1. Gleiche Suchleiste laden (CSV-Export bleibt hier deaktiviert!) -->
<!-- 1. Suchleiste steht außerhalb des Frames (CSV-Export hier auf 'false') -->
<%= render "items/search_bar", show_csv: false %>
<!-- 2. Artikelliste laden -->
<!-- 2. NUR DAS LIST-PARTIAL WIRD IN DEN FRAME GEPAKT -->
<%= turbo_frame_tag "items_list_frame" do %>
<% if @items.any? %>
<%= render "items/list", items: @items %>
<% else %>
<div class="text-center py-16 text-gray-400 bg-white border border-gray-200 rounded-xl shadow-sm">
<svg class="mx-auto h-12 w-12 text-gray-300" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M21 7.5l-9-5.25L3 7.5m18 0l-9 5.25m9-5.25v9l-9 5.25M3 7.5l9 5.25M3 7.5v9l5.25 3.03M12 12.75v9" />
</svg>
<p class="text-sm mt-3 font-medium">Bisher sind keine Inventargegenstände erfasst.</p>
<p class="text-xs text-gray-400 mt-1">Klicke oben rechts auf "Artikel hinzufügen", um das erste Gerät einzubuchen.</p>
<div class="text-center py-12 text-gray-400 bg-white border border-gray-200 rounded-xl shadow-sm">
<p class="text-sm">Keine passenden Artikel in dieser Kategorie gefunden.</p>
</div>
<% end %>
<% end %>
</div>
</div>

View File

@@ -1,134 +1,157 @@
<% content_for :title, "Dashboard Übersicht" %>
<% content_for :title, "Dashboard" %>
<div class="space-y-6">
<!-- KENNZAHLEN-GRID -->
<div class="grid grid-cols-1 sm:grid-cols-3 gap-4">
<!-- DYNAMISCHES STATISTIK-RASTER (3 Spalten auf Desktop, 1 auf Mobile) -->
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
<!-- Karte 1: Gesamtartikel -->
<div class="bg-white border border-gray-200 rounded-xl p-5 shadow-sm flex items-center gap-4">
<div class="p-3 bg-blue-50 text-blue-600 rounded-lg">
<!-- Heroicon: cube -->
<svg class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M21 7.5l-9-5.25L3 7.5m18 0l-9 5.25m9-5.25v9l-9 5.25M3 7.5l9 5.25M3 7.5v9l5.25 3.03M12 12.75v9" /></svg>
</div>
<div>
<p class="text-xs font-semibold uppercase text-gray-400">Objekte im System</p>
<h3 class="text-2xl font-bold text-gray-800"><%= @total_items %></h3>
</div>
</div>
<!-- Karte 2: Im Lager -->
<div class="bg-white border border-gray-200 rounded-xl p-5 shadow-sm flex items-center gap-4">
<div class="p-3 bg-amber-50 text-amber-600 rounded-lg">
<!-- Heroicon: archive-box -->
<svg class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M20.25 7.5l-.625 10.632a2.25 2.25 0 01-2.247 2.118H6.622a2.25 2.25 0 01-2.247-2.118L3.75 7.5M10 11.25h4M3.375 7.5h17.25c.621 0 1.125-.504 1.125-1.125v-1.5c0-.621-.504-1.125-1.125-1.125H3.375c-.621 0-1.125.504-1.125 1.125v1.5c0 .621.504 1.125 1.125 1.125z" /></svg>
</div>
<div>
<p class="text-xs font-semibold uppercase text-gray-400">Aktuell im Lager</p>
<h3 class="text-2xl font-bold text-amber-600"><%= @items_in_storage %></h3>
</div>
</div>
<!-- Karte 3: Gesamtwert (Wird formatiert in Euro ausgegeben) -->
<div class="bg-white border border-gray-200 rounded-xl p-5 shadow-sm flex items-center gap-4">
<div class="p-3 bg-green-50 text-green-600 rounded-lg">
<!-- Heroicon: banknotes -->
<svg class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M2.25 18.75a60.07 60.07 0 0115.797 2.101c.727.198 1.453-.342 1.453-1.096V18.75M3.75 4.5v.75m0 .75h-.75m0 0v-.75m0 .75h.75m0 0V18m0-13.5h16.5M1.5 6h21M3.75 18v.75m0-.75h-.75m0 0v.75m0-.75h.75m0 0V6M10.5 9a2.25 2.25 0 114.5 0 2.25 2.25 0 01-4.5 0z" /></svg>
</div>
<div>
<p class="text-xs font-semibold uppercase text-gray-400">Gesamtwert Inventar</p>
<h3 class="text-2xl font-bold text-gray-800">
<%= number_to_currency(@total_value, unit: "€", separator: ",", delimiter: ".", format: "%n %u") %>
</h3>
</div>
</div>
</div>
<!-- NEUESTE SYSTEMZUGÄNGE -->
<div class="bg-white border border-gray-200 rounded-xl shadow-sm p-6">
<h2 class="text-base font-bold text-gray-800 mb-4 flex items-center gap-2">
<!-- Heroicon: clock -->
<svg class="h-5 w-5 text-gray-500" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M12 6v6h4.5m4.5 0a9 9 0 11-18 0 9 9 0 0118 0z" /></svg>
Zuletzt registrierte Artikel
</h2>
<% if @recent_items.any? %>
<div class="space-y-3">
<% @recent_items.each do |item| %>
<div class="flex items-start sm:items-center justify-between p-3 bg-gray-50 rounded-lg text-sm border border-gray-100 gap-4">
<div class="flex items-center gap-3 min-w-0">
<span class="p-1.5 bg-blue-100 text-blue-700 rounded-md font-mono text-[10px] font-bold">#<%= item.sticker_id %></span>
<div class="min-w-0">
<p class="font-semibold text-gray-800 truncate"><%= item.name %></p>
<p class="text-xs text-gray-500">
<% if item.user.present? %>
Zugewiesen an: 👤 <%= item.user.name %>
<% elsif item.room.present? %>
Standort: 📍 <%= item.room.name_with_building %>
<% else %>
📦 Im Hauptlager
<% end %>
</p>
</div>
</div>
<div class="text-xs text-gray-400 text-right shrink-0">
<%= time_ago_in_words(item.created_at) %> vor
</div>
</div>
<% end %>
</div>
<% else %>
<div class="text-center py-6 text-gray-400 text-sm">
Bisher wurden keine Artikel im System erfasst.
</div>
<% end %>
</div>
<div class="bg-white border border-gray-200 rounded-xl shadow-sm p-6 mt-6">
<h2 class="text-base font-bold text-gray-800 mb-4 flex items-center gap-2">
<!-- Heroicon: arrows-right-left -->
<svg class="h-5 w-5 text-gray-500" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M7.5 21L3 16.5m0 0L7.5 12M3 16.5h13.5m0-13.5L21 7.5m0 0L16.5 12M21 7.5H7.5" />
<!-- KACHEL 1: GESAMTZEIT INVENTAR -->
<div class="bg-white border border-gray-200 rounded-xl p-6 shadow-sm flex items-center justify-between gap-4">
<div class="flex items-center gap-4 min-w-0">
<!-- Icon jetzt links -->
<div class="p-3 bg-blue-50 text-blue-600 rounded-xl shrink-0 shadow-sm border border-blue-100">
<svg class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M21 7.5l-9-5.25L3 7.5m18 0l-9 5.25m9-5.25v9l-9 5.25M3 7.5l9 5.25M3 7.5v9l5.25 3.03M12 12.75v9" />
</svg>
Letzte Artikel-Zuordnungen & Bewegungen
</h2>
</div>
<div class="space-y-0.5 min-w-0">
<p class="text-xs font-bold text-gray-400 uppercase tracking-wider truncate">Gesamtbestand</p>
<p class="text-xs text-gray-500 truncate">Registrierte Unikate</p>
</div>
</div>
<!-- Große Zahl jetzt rechtsbündig -->
<h3 class="text-3xl font-black text-gray-900 text-end shrink-0"><%= Item.count %></h3>
</div>
<% if @recent_assignments.any? %>
<div class="space-y-3">
<!-- KACHEL 2: KATEGORIEN -->
<div class="bg-white border border-gray-200 rounded-xl p-6 shadow-sm flex items-center justify-between gap-4">
<div class="flex items-center gap-4 min-w-0">
<!-- Icon jetzt links -->
<div class="p-3 bg-emerald-50 text-emerald-600 rounded-xl shrink-0 shadow-sm border border-emerald-100">
<svg class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M2.25 12.75V12A2.25 2.25 0 0 1 4.5 9.75h15A2.25 2.25 0 0 1 21.75 12v.75m-19.5 0A2.25 2.25 0 0 0 2.25 15v2.25m0-4.5A2.25 2.25 0 0 1 4.5 12h15a2.25 2.25 0 0 1 2.25 2.25m-19.5 0v4.5A2.25 2.25 0 0 0 4.5 21h15a2.25 2.25 0 0 0 2.25-2.25V15M2.25 12V6a2.25 2.25 0 0 1 2.25-2.25h2.25c.59 0 1.157.234 1.576.652L9.75 5.85a2.25 2.25 0 0 0 1.576.652h6.924a2.25 2.25 0 0 1 2.25 2.25v3.25" />
</svg>
</div>
<div class="space-y-0.5 min-w-0">
<p class="text-xs font-bold text-gray-400 uppercase tracking-wider truncate">Kategorien</p>
<p class="text-xs text-gray-500 truncate">Geräte-Typen</p>
</div>
</div>
<!-- Große Zahl jetzt rechtsbündig -->
<h3 class="text-3xl font-black text-gray-900 text-end shrink-0"><%= Category.count %></h3>
</div>
<!-- KACHEL 3: RÄUME & STANDORTE -->
<div class="bg-white border border-gray-200 rounded-xl p-6 shadow-sm flex items-center justify-between gap-4 sm:col-span-2 lg:col-span-1">
<div class="flex items-center gap-4 min-w-0">
<!-- Icon jetzt links -->
<div class="p-3 bg-amber-50 text-amber-600 rounded-xl shrink-0 shadow-sm border border-amber-100">
<svg class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M15 10.5a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z" />
<path stroke-linecap="round" stroke-linejoin="round" d="M19.5 10.5c0 7.142-7.5 11.25-7.5 11.25s-7.5-4.108-7.5-11.25a7.5 7.5 0 1 1 15 0Z" />
</svg>
</div>
<div class="space-y-0.5 min-w-0">
<p class="text-xs font-bold text-gray-400 uppercase tracking-wider truncate">Räume & Büros</p>
<p class="text-xs text-gray-500 truncate">Erfasste Standorte</p>
</div>
</div>
<!-- Große Zahl jetzt rechtsbündig -->
<h3 class="text-3xl font-black text-gray-900 text-end shrink-0"><%= Room.count %></h3>
</div>
</div>
<!-- UNTERER BEREICH: BREITEN-OPTIMIERTE LISTEN (items-start verhindert vertikale Streckung) -->
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mt-6 items-start">
<div class="bg-white border border-gray-200 rounded-xl shadow-sm overflow-hidden flex flex-col h-fit">
<div class="px-5 py-4 border-b border-gray-200 bg-gray-50 flex items-center gap-2 shrink-0">
<svg class="h-4 w-4 text-gray-500" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15" />
</svg>
<h3 class="font-bold text-gray-700 text-sm">Zuletzt hinzugefügt</h3>
</div>
<div class="overflow-x-auto">
<table class="min-w-full divide-y divide-gray-200 text-xs table-fixed">
<tbody class="divide-y divide-gray-200 bg-white">
<% @recent_items.each do |item| %>
<tr class="hover:bg-gray-50/50 transition">
<!-- KORREKTUR: max-w begrenzt die Breite, whitespace-normal erlaubt Zeilenumbruch -->
<td class="px-5 py-3.5 text-start align-middle max-w-[200px] sm:max-w-[250px]">
<div class="flex items-start gap-2.5">
<%= link_to item_path(item), class: "shrink-0 transition hover:scale-105 active:scale-95 block" do %>
<%= render "layouts/badge", type: :sticker, label: "##{item.sticker_id}" %>
<% end %>
<div class="min-w-0 whitespace-normal break-words">
<% if item.present? %>
<%= link_to item_path(item), class: "font-bold text-gray-900 hover:text-blue-600 hover:underline inline leading-tight" do %>
<%= item.name %>
<span class="block text-[10px] text-gray-400 mt-0.5"><%= item.category.name %></span>
<% end %>
<% else %>
<span class="font-bold text-gray-400">Gelöschter Artikel</span>
<% end %>
<div class="mt-1">
<%= render "layouts/badge", type: item.location_badge_type, label: item.location_badge_label(short_room: true) %>
</div>
</div>
</div>
</td>
<td class="px-5 py-3.5 whitespace-nowrap text-end text-gray-400 font-medium align-middle shrink-0 w-24">
<%= time_ago_in_words(item.created_at) %>
</td>
</tr>
<% end %>
</tbody>
</table>
</div>
</div>
<!-- SPALTE 2: LETZTE LOGBUCH-AKTIVITÄTEN (HISTORIE) -->
<div class="bg-white border border-gray-200 rounded-xl shadow-sm overflow-hidden flex flex-col h-fit">
<div class="px-5 py-4 border-b border-gray-200 bg-gray-50 flex items-center gap-2 shrink-0">
<svg class="h-4 w-4 text-gray-500" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 6v6h4.5m4.5 0a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z" />
</svg>
<h3 class="font-bold text-gray-700 text-sm">Letzte Aktivitäten</h3>
</div>
<div class="overflow-x-auto">
<table class="min-w-full divide-y divide-gray-200 text-xs table-fixed">
<tbody class="divide-y divide-gray-200 bg-white">
<% @recent_assignments.each do |log| %>
<div class="flex items-start sm:items-center justify-between p-3 bg-gray-50 rounded-lg text-sm border border-gray-100 gap-4">
<div class="flex items-center gap-3 min-w-0">
<!-- Markantes blaues Badge für die Sticker-ID des bewegten Artikels -->
<span class="p-1.5 bg-blue-100 text-blue-700 rounded-md font-mono text-[10px] font-bold shrink-0">
#<%= log.item.sticker_id %>
</span>
<div class="min-w-0">
<p class="font-semibold text-gray-800 truncate">
<%= link_to log.item.name, item_path(log.item), class: "hover:text-blue-600 transition" %>
</p>
<p class="text-xs text-gray-500 truncate">
<% if log.user.present? %>
Ausgegeben an: 👤 <%= log.user.name %>
<% elsif log.room.present? %>
Standort: 📍 <%= log.room.name_with_building %>
<tr class="hover:bg-gray-50/50 transition">
<!-- KORREKTUR: max-w begrenzt die Breite, whitespace-normal erlaubt Zeilenumbruch -->
<td class="px-5 py-3.5 text-start align-middle max-w-[200px] sm:max-w-[250px]">
<div class="flex items-start gap-2.5">
<%= link_to item_path(log.item), class: "shrink-0 transition hover:scale-105 active:scale-95 block" do %>
<%= render "layouts/badge", type: :sticker, label: "##{log.item.sticker_id}" %>
<% end %>
<div class="min-w-0 whitespace-normal break-words">
<% 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 %>
<span class="block text-[10px] text-gray-400 mt-0.5"><%= log.item.category.name %></span>
<% end %>
<% else %>
📦 Ins Hauptlager gelegt
<span class="font-bold text-gray-400">Gelöschter Artikel</span>
<% end %>
</p>
<div class="mt-1">
<%= render "layouts/badge", type: log.item.location_badge_type, label: log.item.location_badge_label(short_room: true) %>
</div>
</div>
</div>
</td>
<td class="px-5 py-3.5 whitespace-nowrap text-end text-gray-400 font-medium align-middle shrink-0 w-24">
<%= time_ago_in_words(log.assigned_at) %>
</td>
</tr>
<% end %>
</tbody>
</table>
</div>
</div>
<div class="text-xs text-gray-400 text-right shrink-0">
<%= time_ago_in_words(log.created_at) %> vor
</div>
</div>
<% end %>
</div>
<% else %>
<div class="text-center py-6 text-gray-400 text-sm">
Es wurden noch keine Artikel-Bewegungen im System registriert.
</div>
<% end %>
</div>
</div>

View File

@@ -1,45 +1,57 @@
<%= 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| %>
<%= form_with(model: item, class: "space-y-6 max-w-4xl mx-auto bg-white border border-gray-200 rounded-xl shadow-sm p-6 md:p-8") do |form| %>
<% if item.errors.any? %>
<div class="p-4 mb-4 text-sm text-red-800 rounded-lg bg-red-50 border border-red-200" role="alert">
<h2 class="font-bold mb-1"><%= pluralize(item.errors.count, "Fehler") %> verhinderten das Speichern:</h2>
<ul class="list-disc list-inside text-xs">
<% item.errors.each do |error| %>
<li><%= error.full_message %></li>
<% end %>
</ul>
</div>
<% end %>
<div>
<h2 class="text-xl font-bold text-gray-800">
<%= item.new_record? ? "Artikel registrieren" : "Artikel bearbeiten" %>
</h2>
<p class="text-sm text-gray-500 mt-1">
<%= 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." %>
</p>
</div>
<%= render "layouts/form_header",
model: @item,
new_title: "Artikel anlegen",
new_description: "Erfasse ein neues Gerät mitsamt SKU, Seriennummer und Sticker-ID.",
edit_title: "Artikel bearbeiten",
edit_description: "Aktualisiere die Gerätedaten, den Anschaffungspreis oder den Zustand." %>
<hr class="border-gray-200">
<%= render "layouts/form_errors", model: item %>
<!-- Stammdaten (Name und SKU) -->
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div>
<%= 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 focus:border-blue-500 focus:ring-blue-500" %>
</div>
<div>
<div data-controller="scanner" >
<%= 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 focus:border-blue-500 focus:ring-blue-500" %>
<div class="relative flex rounded-lg shadow-sm">
<%= form.text_field :sku, 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 focus:border-blue-500 focus:ring-blue-500" %>
<button type="button" data-action="click->scanner#startCamera" class="py-2 px-4 border border-l-0 border-gray-300 bg-gray-50 text-gray-700 rounded-r-lg text-sm flex items-center gap-1.5 hover:bg-gray-100 transition">
<svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M3.75 4.875c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125v2.25c0 .621-.504 1.125-1.125 1.125h-2.25A1.125 1.125 0 0 1 3.75 7.125v-2.25ZM3.75 14.625c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125v2.25c0 .621-.504 1.125-1.125 1.125h-2.25a1.125 1.125 0 0 1-1.125-1.125v-2.25ZM14.625 4.875c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125v2.25c0 .621-.504 1.125-1.125 1.125h-2.25c-.621 0-1.125-.504-1.125-1.125v-2.25ZM11.25 3.75a.75.75 0 0 1 .75.75v2.25a.75.75 0 0 1-1.5 0V4.5a.75.75 0 0 1 .75-.75ZM4.5 11.25a.75.75 0 0 1 .75-.75h2.25a.75.75 0 0 1 0 1.5H5.25a.75.75 0 0 1-.75-.75ZM11.25 10.5a.75.75 0 0 1 .75.75v2.25a.75.75 0 0 1-1.5 0v-2.25a.75.75 0 0 1 .75-.75ZM10.5 18.75a.75.75 0 0 1 .75-.75h2.25a.75.75 0 0 1 0 1.5h-2.25a.75.75 0 0 1-.75-.75ZM18.75 10.5a.75.75 0 0 1 .75.75v2.25a.75.75 0 0 1-1.5 0v-2.25a.75.75 0 0 1 .75-.75ZM14.625 14.625c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125v2.25c0 .621-.504 1.125-1.125 1.125h-2.25a1.125 1.125 0 0 1-1.125-1.125v-2.25ZM18.75 4.5a.75.75 0 0 1 .75-.75h.75c.621 0 1.125.504 1.125 1.125V5.25a.75.75 0 0 1-1.5 0V4.5h-.375a.75.75 0 0 1-.75-.75ZM19.5 18.75a.75.75 0 0 1 .75-.75h.375V17.25a.75.75 0 0 1 1.5 0v1.5c0 .621-.504 1.125-1.125 1.125h-1.5a.75.75 0 0 1-.75-.75ZM4.5 19.5v-.375a.75.75 0 0 1 1.5 0v.375h.375a.75.75 0 0 1 0 1.5h-1.5A1.125 1.125 0 0 1 3.75 19.5Z" />
</svg>
</button>
</div>
<%= render "layouts/scanner" %>
</div>
</div>
<!-- Herstelldaten & Preise -->
<div class="grid grid-cols-1 sm:grid-cols-3 gap-4">
<div class="sm:col-span-2">
<div data-controller="scanner" >
<%= 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 focus:border-blue-500 focus:ring-blue-500" %>
<div class="relative flex rounded-lg shadow-sm">
<%= form.text_field :serial_number, data: { scanner_target: "input" }, class: "py-2 px-3 blck w-full border border-gray-300 rounded-l-lg text-sm bg-gray-50/50 focus:border-blue-500 focus:ring-blue-500" %>
<button type="button" data-action="click->scanner#startCamera" class="py-2 px-4 border border-l-0 border-gray-300 bg-gray-50 text-gray-700 rounded-r-lg text-sm flex items-center gap-1.5 hover:bg-gray-100 transition">
<svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M3.75 4.875c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125v2.25c0 .621-.504 1.125-1.125 1.125h-2.25A1.125 1.125 0 0 1 3.75 7.125v-2.25ZM3.75 14.625c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125v2.25c0 .621-.504 1.125-1.125 1.125h-2.25a1.125 1.125 0 0 1-1.125-1.125v-2.25ZM14.625 4.875c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125v2.25c0 .621-.504 1.125-1.125 1.125h-2.25c-.621 0-1.125-.504-1.125-1.125v-2.25ZM11.25 3.75a.75.75 0 0 1 .75.75v2.25a.75.75 0 0 1-1.5 0V4.5a.75.75 0 0 1 .75-.75ZM4.5 11.25a.75.75 0 0 1 .75-.75h2.25a.75.75 0 0 1 0 1.5H5.25a.75.75 0 0 1-.75-.75ZM11.25 10.5a.75.75 0 0 1 .75.75v2.25a.75.75 0 0 1-1.5 0v-2.25a.75.75 0 0 1 .75-.75ZM10.5 18.75a.75.75 0 0 1 .75-.75h2.25a.75.75 0 0 1 0 1.5h-2.25a.75.75 0 0 1-.75-.75ZM18.75 10.5a.75.75 0 0 1 .75.75v2.25a.75.75 0 0 1-1.5 0v-2.25a.75.75 0 0 1 .75-.75ZM14.625 14.625c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125v2.25c0 .621-.504 1.125-1.125 1.125h-2.25a1.125 1.125 0 0 1-1.125-1.125v-2.25ZM18.75 4.5a.75.75 0 0 1 .75-.75h.75c.621 0 1.125.504 1.125 1.125V5.25a.75.75 0 0 1-1.5 0V4.5h-.375a.75.75 0 0 1-.75-.75ZM19.5 18.75a.75.75 0 0 1 .75-.75h.375V17.25a.75.75 0 0 1 1.5 0v1.5c0 .621-.504 1.125-1.125 1.125h-1.5a.75.75 0 0 1-.75-.75ZM4.5 19.5v-.375a.75.75 0 0 1 1.5 0v.375h.375a.75.75 0 0 1 0 1.5h-1.5A1.125 1.125 0 0 1 3.75 19.5Z" />
</svg>
</button>
</div>
<%= render "layouts/scanner" %>
</div>
</div>
<div>
<%= 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 focus:border-blue-500 focus:ring-blue-500" %>
@@ -58,9 +70,30 @@
<div class="relative flex rounded-lg shadow-sm">
<%= 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 focus:border-blue-500 focus:ring-blue-500" %>
<button type="button" data-action="click->scanner#startCamera" class="py-2 px-4 border border-l-0 border-gray-300 bg-gray-50 text-gray-700 rounded-r-lg text-sm flex items-center gap-1.5 hover:bg-gray-100 transition">
Scannen
<svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M3.75 4.875c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125v2.25c0 .621-.504 1.125-1.125 1.125h-2.25A1.125 1.125 0 0 1 3.75 7.125v-2.25ZM3.75 14.625c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125v2.25c0 .621-.504 1.125-1.125 1.125h-2.25a1.125 1.125 0 0 1-1.125-1.125v-2.25ZM14.625 4.875c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125v2.25c0 .621-.504 1.125-1.125 1.125h-2.25c-.621 0-1.125-.504-1.125-1.125v-2.25ZM11.25 3.75a.75.75 0 0 1 .75.75v2.25a.75.75 0 0 1-1.5 0V4.5a.75.75 0 0 1 .75-.75ZM4.5 11.25a.75.75 0 0 1 .75-.75h2.25a.75.75 0 0 1 0 1.5H5.25a.75.75 0 0 1-.75-.75ZM11.25 10.5a.75.75 0 0 1 .75.75v2.25a.75.75 0 0 1-1.5 0v-2.25a.75.75 0 0 1 .75-.75ZM10.5 18.75a.75.75 0 0 1 .75-.75h2.25a.75.75 0 0 1 0 1.5h-2.25a.75.75 0 0 1-.75-.75ZM18.75 10.5a.75.75 0 0 1 .75.75v2.25a.75.75 0 0 1-1.5 0v-2.25a.75.75 0 0 1 .75-.75ZM14.625 14.625c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125v2.25c0 .621-.504 1.125-1.125 1.125h-2.25a1.125 1.125 0 0 1-1.125-1.125v-2.25ZM18.75 4.5a.75.75 0 0 1 .75-.75h.75c.621 0 1.125.504 1.125 1.125V5.25a.75.75 0 0 1-1.5 0V4.5h-.375a.75.75 0 0 1-.75-.75ZM19.5 18.75a.75.75 0 0 1 .75-.75h.375V17.25a.75.75 0 0 1 1.5 0v1.5c0 .621-.504 1.125-1.125 1.125h-1.5a.75.75 0 0 1-.75-.75ZM4.5 19.5v-.375a.75.75 0 0 1 1.5 0v.375h.375a.75.75 0 0 1 0 1.5h-1.5A1.125 1.125 0 0 1 3.75 19.5Z" />
</svg>
</button>
</div>
<%= render "layouts/scanner" %>
</div>
<div class="space-y-1.5">
<!-- Rails schreibt das Label automatisch über die de.yml -->
<%= form.label :condition, Item.human_attribute_name(:condition), class: "block text-sm font-semibold text-gray-700" %>
<div class="relative">
<!-- PURE RAILS KONVENTION: Sicher, verständlich und stabil -->
<%= form.select :condition,
Item.conditions.keys.map { |cond| [Item.human_attribute_name("conditions.#{cond}"), cond] },
{},
class: "py-2.5 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 appearance-none pr-10" %>
<!-- Kleiner Custom-Pfeil rechts -->
<div class="absolute inset-y-0 right-0 flex items-center pr-3 pointer-events-none text-gray-400">
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M19.5 8.25l-7.5 7.5-7.5-7.5" /></svg>
</div>
</div>
</div>
<hr class="border-gray-200">
@@ -101,55 +134,73 @@
<% end %>
</div>
<!-- Live-Suche (Reiner Text, keine versteckten IDs) -->
<!-- Live-Suche (Mobil-optimierter Autocomplete-Ersatz für Datalist) -->
<div class="mt-4">
<% if type == "user" %>
<!-- Signalisiert Rails, dass die room_id gelöscht werden soll -->
<input type="hidden" name="item[room_id]" value="">
<label for="item_user_name" class="block text-sm font-medium mb-1.5 text-gray-700">Mitarbeiter suchen (Vorname, Nachname oder E-Mail)...</label>
<div class="relative">
<!-- Wir senden den Klartext-Namen an ein virtuelles Feld 'user_name' -->
<label for="item_user_name" class="block text-sm font-medium mb-1.5 text-gray-700">Mitarbeiter suchen (Vorname, Nachname)...</label>
<!-- STIMULUS CONTAINER FÜR USER -->
<div class="relative" data-controller="autocomplete">
<input type="text"
id="item_user_name"
name="item[user_name]"
list="users_datalist"
value="<%= item.user&.name %>"
data-autocomplete-target="input"
data-action="input->autocomplete#filter focus->autocomplete#showAll"
class="py-2.5 px-3 block w-full border border-gray-300 rounded-lg text-sm bg-white focus:border-blue-500 focus:ring-blue-500"
placeholder="Tippe den Namen ein...">
placeholder="Tippe den Namen ein..."
autocomplete="off">
<datalist id="users_datalist">
<!-- DIE NEUE MOBILE ERGEBNIS-LISTE -->
<div data-autocomplete-target="results" class="hidden absolute z-50 left-0 right-0 mt-1 max-h-60 overflow-y-auto bg-white border border-gray-200 rounded-lg shadow-lg divide-y divide-gray-100">
<% User.all.order(:first_name, :last_name).each do |user| %>
<!-- Hier nutzen wir den reinen Vor- und Nachnamen als Value -->
<option value="<%= user.name %>"></option>
<div data-autocomplete-target="item"
data-action="click->autocomplete#select"
data-value="<%= user.name %>"
class="px-4 py-2.5 text-sm text-gray-800 hover:bg-blue-50 cursor-pointer transition flex items-center gap-2">
<!-- Kleines Symbol für visuelles Feedback -->
<svg class="h-4 w-4 text-gray-400" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M15.75 6a3.75 3.75 0 1 1-7.5 0 3.75 3.75 0 0 1 7.5 0ZM4.501 20.118a7.5 7.5 0 0 1 14.998 0A17.933 17.933 0 0 1 12 21.75c-2.676 0-5.216-.584-7.499-1.632Z" /></svg>
<%= user.name %>
</div>
<% end %>
</datalist>
</div>
</div>
<% elsif type == "room" %>
<!-- Signalisiert Rails, dass die user_id gelöscht werden soll -->
<input type="hidden" name="item[user_id]" value="">
<label for="item_room_name" class="block text-sm font-medium mb-1.5 text-gray-700">Raum suchen (Raumnummer)...</label>
<div class="relative">
<!-- Wir senden den Klartext-Namen an ein virtuelles Feld 'room_name' -->
<!-- STIMULUS CONTAINER FÜR RÄUME -->
<div class="relative" data-controller="autocomplete">
<input type="text"
id="item_room_name"
name="item[room_name]"
list="rooms_datalist"
value="<%= item.room&.name %>"
data-autocomplete-target="input"
data-action="input->autocomplete#filter focus->autocomplete#showAll"
class="py-2.5 px-3 block w-full border border-gray-300 rounded-lg text-sm bg-white focus:border-blue-500 focus:ring-blue-500"
placeholder="Tippe die Raumnummer ein...">
placeholder="Tippe die Raumnummer ein..."
autocomplete="off">
<datalist id="rooms_datalist">
<!-- DIE NEUE MOBILE ERGEBNIS-LISTE -->
<div data-autocomplete-target="results" class="hidden absolute z-50 left-0 right-0 mt-1 max-h-60 overflow-y-auto bg-white border border-gray-200 rounded-xl shadow-lg divide-y divide-gray-100">
<% Room.all.order(:name).each do |room| %>
<option value="<%= room.name %>"></option>
<div data-autocomplete-target="item"
data-action="click->autocomplete#select"
data-value="<%= room.name %>"
class="px-4 py-2.5 text-sm text-gray-800 hover:bg-blue-50 cursor-pointer transition flex items-center gap-2">
<svg class="h-4 w-4 text-gray-400" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M15 10.5a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z" /><path stroke-linecap="round" stroke-linejoin="round" d="M19.5 10.5c0 7.142-7.5 11.25-7.5 11.25s-7.5-4.108-7.5-11.25a7.5 7.5 0 1 1 15 0Z" /></svg>
<div class="font-medium"><%= room.name %></div>
<div class="text-xs text-gray-400 font-normal ml-auto"><%= room.building %></div>
</div>
<% end %>
</datalist>
</div>
</div>
<% else %>
<!-- Hauptlager gewählt -> Beide IDs nullen -->
<input type="hidden" name="item[user_id]" value="">
<input type="hidden" name="item[room_id]" value="">
<div class="p-4 rounded-lg border border-dashed border-gray-200 bg-gray-50/50 text-center text-xs text-gray-500">
@@ -157,6 +208,7 @@
</div>
<% end %>
</div>
<% end %>
</div>

View File

@@ -1,132 +1,137 @@
<div class="bg-white border border-gray-200 rounded-xl shadow-sm overflow-hidden">
<!-- ========================================================================= -->
<!-- 1. HANDY-ANSICHT: OPTIMIERTE INVENTAR-CARDS (md:hidden) -->
<!-- 1. MOBIL-ANSICHT: OPTIMIERT MIT REINEM CODESYMBOL & ZUSTANDS-BADGES -->
<!-- ========================================================================= -->
<div class="block md:hidden divide-y divide-gray-200 bg-white">
<% items.each do |item| %>
<div class="p-4 space-y-3 hover:bg-gray-50/50 transition">
<div class="p-4 hover:bg-gray-50/50 transition flex items-center justify-between gap-4">
<!-- Zeile 1: Name, Kategorie & die markante Sticker-ID Plakette -->
<div class="flex justify-between items-start gap-3">
<div class="min-w-0">
<h4 class="font-bold text-gray-900 text-sm leading-snug truncate"><%= item.name %></h4>
<p class="text-xs text-gray-500 font-medium mt-0.5"><%= item.category.name %></p>
</div>
<!-- LINKER BEREICH: TEXTE BÜNDIG ZUM ARTIKELNAMEN FLUCHTEND -->
<div class="flex-1 min-w-0 flex items-start gap-3">
<!-- Rechte Box für ID und den optimal platzierten Preis direkt darunter -->
<div class="flex flex-col items-end gap-1.5 shrink-0">
<span class="inline-flex items-center font-mono font-black text-xs text-white bg-blue-600 px-2.5 py-1 rounded-md shadow-sm tracking-wider">
<!-- Das ID-Badge steht als sauberer, fester Anker ganz links -->
<%= link_to item_path(item), data: { turbo_frame: "_top" }, class: "shrink-0 transition hover:scale-105 active:scale-95 block mt-0.5" do %>
<span class="inline-flex items-center font-mono font-black text-xs text-white bg-blue-600 px-2.5 py-0.5 rounded shadow-sm tracking-widest">
#<%= item.sticker_id %>
</span>
<span class="text-xs font-bold text-gray-600 font-mono bg-gray-100 px-1.5 py-0.5 rounded">
<%= number_to_currency(item.price, unit: "€", separator: ",", delimiter: ".", format: "%n %u") %>
</span>
</div>
</div>
<% end %>
<!-- Zeile 2: Technische Gerätedaten (SKU & Seriennummer) -->
<div class="grid grid-cols-2 gap-3 bg-gray-50 p-2.5 rounded-lg text-xs font-mono text-gray-600">
<div class="truncate">
<span class="text-gray-400 font-sans block text-[10px] uppercase font-bold tracking-wider mb-0.5">SKU</span>
<%= item.sku %>
</div>
<div class="truncate">
<span class="text-gray-400 font-sans block text-[10px] uppercase font-bold tracking-wider mb-0.5">Seriennummer</span>
<%= item.serial_number %>
</div>
</div>
<!-- TEXT-CONTAINER: ALLES FLUCHTET PERFEKT LINKSBÜNDIG UNTER DEM NAMEN -->
<div class="flex-1 min-w-0 space-y-2">
<!-- Zeile 3: Standort & Quick-Actions -->
<div class="flex justify-between items-center pt-1">
<!-- Standort-Badge -->
<div class="text-xs">
<% if item.user.present? %>
<span class="inline-flex items-center gap-1 text-gray-900 font-semibold bg-green-50 text-green-800 px-2 py-1 rounded-md border border-green-200">
👤 <%= item.user.name %>
</span>
<% elsif item.room.present? %>
<span class="inline-flex items-center gap-1 text-gray-700 bg-blue-50 text-blue-800 px-2 py-1 rounded-md border border-blue-200">
📍 <%= item.room.name %>
</span>
<% else %>
<span class="inline-flex items-center gap-1 text-amber-800 bg-amber-50 px-2 py-1 rounded-md border border-amber-200 font-medium">
📦 Hauptlager
</span>
<!-- Zeile 1: Fetter Artikelname -->
<div class="min-w-0">
<%= 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 %>
</div>
<!-- Aktions-Icons -->
<div class="flex items-center gap-1 shrink-0">
<%= 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 %>
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M2.036 12.322a1.012 1.012 0 0 1 0-.639C3.423 7.51 7.36 4.5 12 4.5c4.638 0 8.573 3.007 9.963 7.178.07.207.07.431 0 .639C20.577 16.49 16.64 19.5 12 19.5c-4.638 0-8.573-3.007-9.963-7.178Z" /><path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z" /></svg>
<% 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 %>
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="m16.862 4.487 1.687-1.688a1.875 1.875 0 1 1 2.652 2.652L10.582 16.07a4.5 4.5 0 0 1-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 0 1 1.13-1.897l8.932-8.931Zm0 0L19.5 7.125M18 14v4.75A2.25 2.25 0 0 1 15.75 21H5.25A2.25 2.25 0 0 1 3 18.75V8.25A2.25 2.25 0 0 1 5.25 6H10" /></svg>
<% end %>
<!-- Zeile 2: Kategorie, SKU & Seriennummer (SN) als kompakte Kette -->
<div class="flex items-center flex-wrap gap-x-2 gap-y-0.5 text-xs font-medium text-gray-500 font-mono">
<span class="font-sans font-bold text-gray-400"><%= item.category.name %></span>
<span class="text-gray-300">•</span>
<span>SKU: <span class="text-gray-700 font-bold"><%= item.sku.presence || "—" %></span></span>
<span class="text-gray-300">•</span>
<span>SN: <span class="text-gray-700 font-bold"><%= item.serial_number.presence || "—" %></span></span>
</div>
<!-- Zeile 3: Das ultimativ schlanke, automatisierte Badge-Duo im Mobil-Layout -->
<div class="flex items-center flex-wrap gap-2 pt-0.5">
<!-- A: Standort-Badge (Label wird manuell übergeben, da dynamischer Personen-/Raumname) -->
<%= render "layouts/badge", type: item.location_badge_type, label: item.location_badge_label(short_room: true) %>
<!-- B: Zustands-Badge (Vollautomatisch! Text und Icon kommen direkt aus dem Typen) -->
<%= render "layouts/badge", type: item.condition_badge_type %>
</div>
</div>
</div>
<!-- RECHTER BEREICH: VERTIKAL GESTAPELTE QUICK-ACTIONS -->
<div class="flex flex-col gap-2 shrink-0">
<%= 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 %>
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke-width="2.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M2.036 12.322a1.012 1.012 0 0 1 0-.639C3.423 7.51 7.36 4.5 12 4.5c4.638 0 8.573 3.007 9.963 7.178.07.207.07.431 0 .639C20.577 16.49 16.64 19.5 12 19.5c-4.638 0-8.573-3.007-9.963-7.178Z" /><path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z" /></svg>
<% 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 %>
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke-width="2.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="m16.862 4.487 1.687-1.688a1.875 1.875 0 1 1 2.652 2.652L10.582 16.07a4.5 4.5 0 0 1-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 0 1 1.13-1.897l8.932-8.931Zm0 0L19.5 7.125M18 14v4.75A2.25 2.25 0 0 1 15.75 21H5.25A2.25 2.25 0 0 1 3 18.75V8.25A2.25 2.25 0 0 1 5.25 6H10" /></svg>
<% end %>
</div>
</div>
<% end %>
</div>
<!-- ========================================================================= -->
<!-- 2. DESKTOP-ANSICHT: STICKY-TABELLE (hidden md:block) -->
<!-- 2. DESKTOP-ANSICHT: STICKY-TABELLE (Ab md:) -->
<!-- ========================================================================= -->
<div class="hidden md:block overflow-x-auto max-h-[calc(100vh-12rem)]">
<table class="min-w-full divide-y divide-gray-200 text-sm">
<thead class="bg-gray-50 sticky top-0 z-20 shadow-[0_1px_0_0_rgba(229,231,235,1)]">
<tr>
<th scope="col" class="px-6 py-3 text-start font-semibold text-gray-500 uppercase tracking-wider">Artikel / Details</th>
<th scope="col" class="px-6 py-3 text-start font-semibold text-gray-500 uppercase tracking-wider">Seriennummer (SN)</th>
<th scope="col" class="px-6 py-3 text-start font-semibold text-gray-500 uppercase tracking-wider">Aktueller Standort / Inhaber</th>
<th scope="col" class="px-6 py-3 text-start font-semibold text-gray-500 uppercase tracking-wider">Sticker-ID</th>
<th scope="col" class="px-6 py-3 text-end font-semibold text-gray-500 uppercase tracking-wider">Wert</th>
<th scope="col" class="px-6 py-3 text-end font-semibold text-gray-500 uppercase tracking-wider">Aktionen</th>
<th scope="col" class="px-6 py-3 text-start font-semibold text-gray-500 uppercase tracking-wider w-24">Sticker-ID</th>
<th scope="col" class="px-6 py-3 text-start font-semibold text-gray-500 uppercase tracking-wider">Artikeldetails</th>
<th scope="col" class="px-6 py-3 text-start font-semibold text-gray-500 uppercase tracking-wider">Aktueller Standort</th>
<th scope="col" class="px-6 py-3 text-center font-semibold text-gray-500 uppercase tracking-wider w-36">Zustand</th>
<th scope="col" class="px-6 py-3 text-end font-semibold text-gray-500 uppercase tracking-wider w-24">Aktionen</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-200 bg-white">
<% items.each do |item| %>
<tr class="hover:bg-gray-50/50 transition">
<td class="px-6 py-4 whitespace-nowrap">
<div class="font-semibold text-gray-900"><%= item.name %></div>
<div class="text-xs text-gray-400">SKU: <%= item.sku %> • <%= item.category.name %></div>
</td>
<td class="px-6 py-4 whitespace-nowrap font-mono text-xs text-gray-600"><%= item.serial_number %></td>
<td class="px-6 py-4 whitespace-nowrap text-gray-700">
<% if item.user.present? %>
<span class="inline-flex items-center gap-1.5 text-gray-900 font-medium">👤 <%= item.user.name %></span>
<% elsif item.room.present? %>
<span class="inline-flex items-center gap-1.5 text-gray-600">📍 <%= item.room.name_with_building %></span>
<% else %>
<span class="inline-flex items-center py-0.5 px-2 rounded-full text-xs font-medium bg-amber-50 text-amber-800 border border-amber-200">📦 Im Hauptlager</span>
<% end %>
</td>
<!-- STICKER-ID ALS BADGE (Jetzt auch in der Desktop-Tabelle markant) -->
<!-- Spalte 1: Verlinktes Sticker-ID-Badge -->
<td class="px-6 py-4 whitespace-nowrap">
<span class="inline-flex items-center font-mono font-bold text-xs text-white bg-blue-600 px-2 py-0.5 rounded shadow-sm tracking-wider">
<%= link_to item_path(item), data: { turbo_frame: "_top" }, class: "inline-block transition hover:scale-105" do %>
<span class="inline-flex items-center font-mono font-black text-xs text-white bg-blue-600 px-2.5 py-1 rounded-md shadow-sm tracking-widest">
#<%= item.sticker_id %>
</span>
<% end %>
</td>
<td class="px-6 py-4 whitespace-nowrap text-end font-medium text-gray-900">
<%= number_to_currency(item.price, unit: "€", separator: ",", delimiter: ".", format: "%n %u") %>
</td>
<td class="px-6 py-4 whitespace-nowrap text-end font-medium text-xs">
<div class="flex items-center justify-end gap-2">
<%= 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 %>
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M2.036 12.322a1.012 1.012 0 0 1 0-.639C3.423 7.51 7.36 4.5 12 4.5c4.638 0 8.573 3.007 9.963 7.178.07.207.07.431 0 .639C20.577 16.49 16.64 19.5 12 19.5c-4.638 0-8.573-3.007-9.963-7.178Z" /><path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z" /></svg>
<!-- Spalte 2: Artikelname, Kategorie, SKU & SN direkt darunter im Block -->
<td class="px-6 py-4 max-w-[280px] sm:max-w-[350px]">
<div class="min-w-0 whitespace-normal break-words leading-tight space-y-1">
<%= link_to item_path(item), data: { turbo_frame: "_top" }, class: "font-bold text-gray-900 hover:text-blue-600 hover:underline inline" do %>
<%= item.name %>
<% 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 %>
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="m16.862 4.487 1.687-1.688a1.875 1.875 0 1 1 2.652 2.652L10.582 16.07a4.5 4.5 0 0 1-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 0 1 1.13-1.897l8.932-8.931Zm0 0L19.5 7.125M18 14v4.75A2.25 2.25 0 0 1 15.75 21H5.25A2.25 2.25 0 0 1 3 18.75V8.25A2.25 2.25 0 0 1 5.25 6H10" /></svg>
<span class="block text-[10px] text-gray-400 font-medium"><%= item.category.name %></span>
<div class="flex items-center gap-2 text-[10px] font-mono text-gray-500 pt-0.5">
<span>SKU: <span class="text-gray-700 font-semibold"><%= item.sku.presence || "—" %></span></span>
<span class="text-gray-300">•</span>
<span>SN: <span class="text-gray-700 font-semibold"><%= item.serial_number.presence || "—" %></span></span>
</div>
</div>
</td>
<!-- Spalte 3: Aktueller Standort (Desktop Tabelle - Voller Name übergeben) -->
<td class="px-6 py-4 whitespace-nowrap">
<%= render "layouts/badge", type: item.location_badge_type, label: item.location_badge_label(short_room: false) %>
</td>
<!-- Spalte 4: Zustand (Desktop Tabelle - VOLLAUTOMATISCH!) -->
<td class="px-6 py-4 whitespace-nowrap text-center">
<%= render "layouts/badge", type: item.condition_badge_type %>
</td>
<!-- Spalte 5: Aktionen -->
<td class="px-6 py-4 whitespace-nowrap text-end font-medium text-xs w-24 shrink-0">
<div class="flex items-center justify-end gap-2">
<%= 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" do %>
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M2.036 12.322a1.012 1.012 0 0 1 0-.639C3.423 7.51 7.36 4.5 12 4.5c4.638 0 8.573 3.007 9.963 7.178.07.207.07.431 0 .639C20.577 16.49 16.64 19.5 12 19.5c-4.638 0-8.573-3.007-9.963-7.178Z" /><path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z" /></svg>
<% 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 transition" do %>
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="m16.862 4.487 1.687-1.688a1.875 1.875 0 1 1 2.652 2.652L10.582 16.07a4.5 4.5 0 0 1-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 0 1 1.13-1.897l8.932-8.931Zm0 0L19.5 7.125M18 14v4.75A2.25 2.25 0 0 1 15.75 21H5.25A2.25 2.25 0 0 1 3 18.75V8.25A2.25 2.25 0 0 1 5.25 6H10" /></svg>
<% end %>
</div>
</td>
</tr>
<% end %>
</tbody>
</table>
</div>
</div>

View File

@@ -1,12 +1,38 @@
<div class="bg-white border border-gray-200 rounded-xl shadow-sm p-4 bg-gray-200 flex flex-col sm:flex-row gap-3 justify-between items-center mb-6">
<%# Wenn dem Partial keine feste URL übergeben wurde, nutzt es automatisch die aktuelle Browser-URL %>
<% form_url = local_assigns[:url] || request.fullpath %>
<!-- Das universelle Suchfeld -->
<div class="relative w-full sm:max-w-xs">
<input type="text" placeholder="Artikel, SKU oder Besitzer..." class="py-2 px-3 pl-9 block w-full border border-gray-300 rounded-lg text-sm bg-white focus:border-blue-500 focus:ring-blue-500">
<%= 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| %>
<!-- DAS SUCHFELD (Jetzt zusätzlich an den Scanner-Controller gekoppelt) -->
<div data-controller="scanner" class="relative w-full sm:flex-1">
<%= 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" %>
<!-- Linkes Icon: Lupe -->
<div class="absolute inset-y-0 left-0 flex items-center pl-3 pointer-events-none">
<!-- Heroicon: magnifying-glass -->
<svg class="h-4 w-4 text-gray-400" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M21 21l-5.197-5.197m0 0A7.5 7.5 0 105.196 5.196a7.5 7.5 0 0010.637 10.637z" /></svg>
<svg class="h-4 w-4 text-gray-400" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M21 21l-5.197-5.197m0 0A7.5 7.5 0 105.196 5.196a7.5 7.5 0 0010.637 10.637z" />
</svg>
</div>
<!-- RECHTES ICON IM FELD: Der neue QR-Code-Scanner Button -->
<button type="button"
data-action="click->scanner#startCamera"
class="absolute inset-y-0 right-0 flex items-center pr-3 text-gray-400 hover:text-blue-600 transition-colors"
title="QR-Code scannen">
<!-- Heroicon: qr-code -->
<svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M3.75 4.875c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125v2.25c0 .621-.504 1.125-1.125 1.125h-2.25A1.125 1.125 0 0 1 3.75 7.125v-2.25ZM3.75 14.625c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125v2.25c0 .621-.504 1.125-1.125 1.125h-2.25a1.125 1.125 0 0 1-1.125-1.125v-2.25ZM14.625 4.875c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125v2.25c0 .621-.504 1.125-1.125 1.125h-2.25c-.621 0-1.125-.504-1.125-1.125v-2.25ZM11.25 3.75a.75.75 0 0 1 .75.75v2.25a.75.75 0 0 1-1.5 0V4.5a.75.75 0 0 1 .75-.75ZM4.5 11.25a.75.75 0 0 1 .75-.75h2.25a.75.75 0 0 1 0 1.5H5.25a.75.75 0 0 1-.75-.75ZM11.25 10.5a.75.75 0 0 1 .75.75v2.25a.75.75 0 0 1-1.5 0v-2.25a.75.75 0 0 1 .75-.75ZM10.5 18.75a.75.75 0 0 1 .75-.75h2.25a.75.75 0 0 1 0 1.5h-2.25a.75.75 0 0 1-.75-.75ZM18.75 10.5a.75.75 0 0 1 .75.75v2.25a.75.75 0 0 1-1.5 0v-2.25a.75.75 0 0 1 .75-.75ZM14.625 14.625c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125v2.25c0 .621-.504 1.125-1.125 1.125h-2.25a1.125 1.125 0 0 1-1.125-1.125v-2.25ZM18.75 4.5a.75.75 0 0 1 .75-.75h.75c.621 0 1.125.504 1.125 1.125V5.25a.75.75 0 0 1-1.5 0V4.5h-.375a.75.75 0 0 1-.75-.75ZM19.5 18.75a.75.75 0 0 1 .75-.75h.375V17.25a.75.75 0 0 1 1.5 0v1.5c0 .621-.504 1.125-1.125 1.125h-1.5a.75.75 0 0 1-.75-.75ZM4.5 19.5v-.375a.75.75 0 0 1 1.5 0v.375h.375a.75.75 0 0 1 0 1.5h-1.5A1.125 1.125 0 0 1 3.75 19.5Z" />
</svg>
</button>
<%= render "layouts/scanner" %>
</div>
<!-- Aktions-Bereich rechts -->
@@ -16,7 +42,7 @@
<%= 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 %>
<!-- Heroicon: arrow-down-tray -->
<svg class="h-4 w-4 text-gray-500" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M3 16.5v2.25A2.25 2.25 0 0 0 5.25 21h13.5A2.25 2.25 0 0 0 21 18.75V16.5M16.5 12 12 16.5m0 0L7.5 12m4.5 4.5V3" /></svg>
<span>CSV</span>
<span class="hidden md:inline">CSV</span>
<% end %>
<% end %>
@@ -24,8 +50,8 @@
<button 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 shadow-sm transition">
<!-- Heroicon: funnel -->
<svg class="h-4 w-4 text-gray-500" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M12 3c2.755 0 5.455.232 8.083.678.533.09.917.556.917 1.096v1.044a2.25 2.25 0 0 1-.659 1.591l-5.432 5.432a2.25 2.25 0 0 0-.659 1.591v2.927a2.25 2.25 0 0 1-1.244 2.013L9.75 21v-6.568a2.25 2.25 0 0 0-.659-1.591L3.659 7.409A2.25 2.25 0 0 1 3 5.818V4.774c0-.54.384-1.006.917-1.096A48.32 48.32 0 0 1 12 3Z" /></svg>
<span>Filter</span>
<span class="hidden md:inline">Filter</span>
</button>
</div>
</div>
<% end %>

View File

@@ -1,50 +1,26 @@
<!--<%# content_for :title, "Editing item" %>
<div class="md:w-2/3 w-full">
<h1 class="font-bold text-4xl">Editing item</h1>
<%#= render "form", item: @item %>
<%= link_to "Show this item", @item, class: "w-full sm:w-auto text-center mt-2 sm:mt-0 sm:ml-2 rounded-md px-3.5 py-2.5 bg-gray-100 hover:bg-gray-50 inline-block font-medium" %>
<%= link_to "Back to items", items_path, class: "w-full sm:w-auto text-center mt-2 sm:mt-0 sm:ml-2 rounded-md px-3.5 py-2.5 bg-gray-100 hover:bg-gray-50 inline-block font-medium" %>
</div>
-->
<% content_for :title, "Artikel bearbeiten: #{@item.name}" %>
<% content_for :title, @item.name %>
<!-- OBERE AKTIONSLISTE (Zurück-Button in der Top-Bar) -->
<% 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 %>
<%= link_to "javascript:history.back()", 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 %>
<!-- Heroicon: arrow-left -->
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M10.5 19.5L3 12m0 0l7.5-7.5M3 12h18" /></svg>
Zurück zu den Details
<div class="hidden md:inline">
Zurück
</div>
<% end %>
<% end %>
<div class="space-y-6">
<!-- Das zentrale Formular mit dem aktuellen Artikel-Objekt laden -->
<%= render "form", item: @item %>
<!-- GEFAHRENBEREICH: Artikel löschen -->
<div class="max-w-2xl mx-auto bg-red-50/50 border border-red-200 rounded-xl p-6 flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4 shadow-sm">
<div class="flex items-start gap-3">
<div class="p-2 bg-red-100 text-red-700 rounded-lg shrink-0 mt-0.5">
<!-- Heroicon: trash -->
<svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="m14.74 9-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 0 1-2.244 2.077H8.084a2.25 2.25 0 0 1-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 0 0-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 0 1 3.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 0 0-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 0 0-7.5 0" /></svg>
</div>
<div>
<h3 class="text-sm font-bold text-red-800">Artikel dauerhaft ausbuchen</h3>
<p class="text-xs text-red-600 mt-0.5">Dies entfernt das Gerät unwiderruflich aus dem Bestand. Die bisherige Verlaufshistorie wird dabei ebenfalls gelöscht.</p>
</div>
</div>
<!-- Löschen-Link gekoppelt an Rails Turbo-Method-Delete -->
<%= 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" %>
</div>
<%= render "layouts/danger_zone",
title: "Artikel restlos löschen",
description: "Dieser Artikel wird dauerhaft und unwiderruflich aus der Bestandsliste entfernt. Auch die QR-Code-Zuordnung erlischt.",
button_text: "Artikel löschen",
confirm_message: "Möchtest du diesen Artikel wirklich permanent aus dem System entfernen?",
path: item_path(@item) %>
</div>

View File

@@ -1,60 +1,27 @@
<!-- <%# content_for :title, "Items" %>
<div class="w-full">
<% if notice.present? %>
<p class="py-2 px-3 bg-green-50 mb-5 text-green-500 font-medium rounded-md inline-block" id="notice"><%= notice %></p>
<% end %>
<div class="flex justify-between items-center">
<h1 class="font-bold text-4xl">Items</h1>
<%= link_to "New item", new_item_path, class: "rounded-md px-3.5 py-2.5 bg-blue-600 hover:bg-blue-500 text-white block font-medium" %>
</div>
<div id="items" class="min-w-full divide-y divide-gray-200 space-y-5">
<% if @items.any? %>
<% @items.each do |item| %>
<div class="flex flex-col sm:flex-row justify-between items-center pb-5 sm:pb-0">
<%#= render item %>
<div class="w-full sm:w-auto flex flex-col sm:flex-row space-x-2 space-y-2">
<%= link_to "Show", item, class: "w-full sm:w-auto text-center rounded-md px-3.5 py-2.5 bg-gray-100 hover:bg-gray-50 inline-block font-medium" %>
<%= link_to "Edit", edit_item_path(item), class: "w-full sm:w-auto text-center rounded-md px-3.5 py-2.5 bg-gray-100 hover:bg-gray-50 inline-block font-medium" %>
<%= button_to "Destroy", item, method: :delete, class: "w-full sm:w-auto rounded-md px-3.5 py-2.5 text-white bg-red-600 hover:bg-red-500 font-medium cursor-pointer", data: { turbo_confirm: "Are you sure?" } %>
</div>
</div>
<% end %>
<% else %>
<p class="text-center my-10">No items found.</p>
<% end %>
</div>
</div>-->
<% content_for :title, "Gesamtbestand" %>
<!-- OBERE AKTIONSLISTE -->
<% 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 %>
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15" /></svg>
Artikel hinzufügen
<div class="hidden md:inline">
Artikel anlegen
</div>
<% end %>
<% end %>
<div class="w-full space-y-4">
<!-- 1. Suchleiste laden (mit aktiviertem CSV-Export) -->
<!-- 1. Die Suchleiste steht AUSSERHALB des Frames. Sie wird beim Tippen nicht neu geladen! -->
<%= render "items/search_bar", show_csv: true %>
<!-- 2. Artikelliste laden -->
<!-- 2. NUR DIE LISTE WIRD IN DEN TURBO-FRAME GEPAKT -->
<%= turbo_frame_tag "items_list_frame" do %>
<% if @items.any? %>
<%= render "items/list", items: @items %>
<% else %>
<div class="text-center py-16 text-gray-400 bg-white border border-gray-200 rounded-xl shadow-sm">
<svg class="mx-auto h-12 w-12 text-gray-300" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M21 7.5l-9-5.25L3 7.5m18 0l-9 5.25m9-5.25v9l-9 5.25M3 7.5l9 5.25M3 7.5v9l5.25 3.03M12 12.75v9" />
</svg>
<p class="text-sm mt-3 font-medium">Bisher sind keine Inventargegenstände erfasst.</p>
<p class="text-xs text-gray-400 mt-1">Klicke oben rechts auf "Artikel hinzufügen", um das erste Gerät einzubuchen.</p>
<p class="text-sm font-medium">Keine passenden Inventargegenstände gefunden.</p>
</div>
<% end %>
</div>
<% end %>
</div>

View File

@@ -1,27 +1,17 @@
<!-- <%# content_for :title, "New item" %>
<div class="md:w-2/3 w-full">
<h1 class="font-bold text-4xl">New item</h1>
<%#= render "form", item: @item %>
<%= link_to "Back to items", items_path, class: "w-full sm:w-auto text-center mt-2 sm:mt-0 sm:ml-2 rounded-md px-3.5 py-2.5 bg-gray-100 hover:bg-gray-50 inline-block font-medium" %>
</div>
-->
<% content_for :title, "Neuen Artikel hinzufügen" %>
<% content_for :title, "Artikel anlegen" %>
<!-- OBERE LEISTE (Zurück-Button in der Top-Bar via Layout-Yield) -->
<% 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 %>
<%= link_to "javascript:history.back()", 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 %>
<!-- Heroicon: arrow-left -->
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M10.5 19.5L3 12m0 0l7.5-7.5M3 12h18" /></svg>
Zurück zur Übersicht
<div class="hidden md:inline">
Zurück
</div>
<% end %>
<% end %>
<div class="p-4 md:p-6">
<!-- Lädt das Formular-Partial und übergibt das leere Artikel-Objekt -->
<%= render "form", item: @item %>
</div>

View File

@@ -1,32 +1,19 @@
<!--<%# content_for :title, "Showing item" %>
<div class="md:w-2/3 w-full">
<% if notice.present? %>
<p class="py-2 px-3 bg-green-50 mb-5 text-green-500 font-medium rounded-md inline-block" id="notice"><%= notice %></p>
<% end %>
<h1 class="font-bold text-4xl">Showing item</h1>
<%#= render @item %>
<%= link_to "Edit this item", edit_item_path(@item), class: "w-full sm:w-auto text-center rounded-md px-3.5 py-2.5 bg-gray-100 hover:bg-gray-50 inline-block font-medium" %>
<%= link_to "Back to items", items_path, class: "w-full sm:w-auto text-center mt-2 sm:mt-0 sm:ml-2 rounded-md px-3.5 py-2.5 bg-gray-100 hover:bg-gray-50 inline-block font-medium" %>
<%= button_to "Destroy this item", @item, method: :delete, form_class: "sm:inline-block mt-2 sm:mt-0 sm:ml-2", class: "w-full rounded-md px-3.5 py-2.5 text-white bg-red-600 hover:bg-red-500 font-medium cursor-pointer", data: { turbo_confirm: "Are you sure?" } %>
</div>
-->
<% content_for :title, "Artikel-Details: #{@item.name}" %>
<% content_for :title, "Artikel-Details" %>
<!-- OBERE AKTIONSLISTE (Yield im Top-Bar deines Hauptlayouts) -->
<% content_for :top_bar_actions do %>
<div class="flex items-center gap-2">
<%= 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 %>
<%= link_to "javascript:history.back()", 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 %>
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M10.5 19.5L3 12m0 0l7.5-7.5M3 12h18" /></svg>
<div class="hidden md:inline">
Zurück
</div>
<% 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 %>
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="m16.862 4.487 1.687-1.688a1.875 1.875 0 1 1 2.652 2.652L6.832 19.82a4.5 4.5 0 0 1-1.897 1.13l-2.685.8.8-2.685a4.5 4.5 0 0 1 1.13-1.897L16.863 4.487Zm0 0L19.5 7.125" /></svg>
<div class="hidden md:inline">
Bearbeiten
</div>
<% end %>
</div>
<% end %>
@@ -41,10 +28,12 @@
<!-- STAMMDATEN-BOX -->
<div class="bg-white border border-gray-200 rounded-xl shadow-sm p-4 md:p-6 space-y-4">
<div class="flex items-start justify-between flex-wrap gap-2">
<h2 class="text-lg md:text-xl font-bold text-gray-900 leading-tight"><%= @item.name %></h2>
<span class="inline-flex items-center py-0.5 px-2.5 rounded-full text-xs font-semibold bg-blue-50 text-blue-800 border border-blue-200 shrink-0">
<%= @item.category.name %>
</span>
<h2 class="text-lg md:text-xl font-bold text-gray-900 leading-tight"><%= @item.name %>
<span class="block text-[12px] text-gray-400 mt-0.5"><%= @item.category.name %></span>
</h2>
<div class="flex items-center gap-2 shrink-0">
<%= render "layouts/badge", type: @item.condition_badge_type %>
</div>
</div>
<hr class="border-gray-200">
@@ -193,11 +182,11 @@
<div>
<h4 class="font-bold text-gray-800">
<% if log.user.present? %>
Zuweisung an Mitarbeiter: <span class="text-gray-900 font-extrabold"><%= log.user.name %></span>
<%= log.user.name %>
<% elsif log.room.present? %>
Standortwechsel in Raum: <span class="text-gray-900 font-extrabold"><%= log.room.name_with_building %></span>
<%= log.room.name_with_building %>
<% else %>
Ins Hauptlager übergeben
Hauptlager
<% end %>
</h4>
@@ -212,11 +201,6 @@
<div class="text-right text-xs whitespace-nowrap text-gray-400 pt-0.5 shrink-0 font-medium">
<time class="text-gray-600">
<%= l(log.assigned_at, format: "%d. %b %Y") %>
<% if log.returned_at.present? %>
bis <%= l(log.returned_at, format: "%d. %b %Y") %>
<% else %>
(Laufend)
<% end %>
</time>
</div>
</div>

View File

@@ -0,0 +1,81 @@
<%
# options: class = override css class
# label = override label
# icon_type = choose one of the given icons.
# icon_svg = custom svg icon
# 1. Standard-Design festlegen (Fallback)
css = local_assigns[:class] || ""
type = local_assigns[:type] || ""
# 2. Icons als Strings definieren
icon_user = '<svg class="h-3.5 w-3.5 text-green-600 shrink-0" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M15.75 6a3.75 3.75 0 1 1-7.5 0 3.75 3.75 0 0 1 7.5 0ZM4.501 20.118a7.5 7.5 0 1 1 14.998 0A17.933 17.933 0 0 1 12 21.75c-2.676 0-5.216-.584-7.499-1.632Z" /></svg>'
icon_room = '<svg class="h-3.5 w-3.5 text-blue-600 shrink-0" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M15 10.5a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z" /><path stroke-linecap="round" stroke-linejoin="round" d="M19.5 10.5c0 7.142-7.5 11.25-7.5 11.25s-7.5-4.108-7.5-11.25a7.5 7.5 0 1 1 15 0Z" /></svg>'
icon_storage = '<svg class="h-3.5 w-3.5 text-amber-600 shrink-0" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M21 7.5l-9-5.25L3 7.5m18 0l-9 5.25m9-5.25v9l-9 5.25M3 7.5l9 5.25M3 7.5v9l5.25 3.03M12 12.75v9" /></svg>'
icon_in_use = '<svg class="h-3.5 w-3.5 text-gray-400 shrink-0" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M7.5 21 3 16.5m0 0L7.5 12M3 16.5h13.5m0-13.5L21 7.5m0 0L16.5 12M21 7.5H7.5" /></svg>'
icon_new = '<svg class="h-3.5 w-3.5 text-green-600 shrink-0" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M9.813 15.904 9 21l-.813-5.096L3.096 15 8 14.187 8.813 9l.813 5.187L15 15l-5.187.904ZM18 5.25l-.45 1.8-.45-1.8-1.8-.45 1.8-.45.45-1.8.45 1.8 1.8.45-1.8.45ZM21.75 9.75l-.3.9-.3-.9-.9-.3.9-.3.3-.9.3.9.9.3-.9.3Z" /></svg>'
icon_as_new = '<svg class="h-3.5 w-3.5 text-emerald-600 shrink-0" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M6.633 10.25c.896 0 1.7-.393 2.287-1.011l3.34-3.52c.536-.565 1.433-.317 1.62.428l1.042 4.161h4.582a2.25 2.25 0 0 1 2.247 2.478l-.997 8.475A2.25 2.25 0 0 1 18.522 23H6.633a2.25 2.25 0 0 1-2.228-1.889L2.83 11.913A2.25 2.25 0 0 1 5.058 9.5h1.575c-.21 0-.422.02-.63.06l-.002.008ZM6.633 10.25V23" /></svg>'
icon_used = '<svg class="h-3.5 w-3.5 text-yellow-600 shrink-0" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="m4.5 12.75 6 6 9-13.5" /></svg>'
icon_heavy = '<svg class="h-3.5 w-3.5 text-orange-600 shrink-0" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M11.42 15.17 17.25 21A2.65 2.65 0 1 0 21 17.25l-5.83-5.83m0 0a2.65 2.65 0 1 1-3.75-3.75 2.65 2.65 0 0 1 3.75 3.75Zm-5.83 5.83a2.65 2.65 0 1 1-3.75-3.75 2.65 2.65 0 0 1 3.75 3.75M21 4.75a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0ZM18.75 7a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0ZM16.5 4.75a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0Z" /></svg>'
icon_defective = '<svg class="h-3.5 w-3.5 text-red-600 shrink-0" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126ZM12 15.75h.007v.008H12v-.008Z" /></svg>'
icon_unknown = '<svg class="h-3.5 w-3.5 text-gray-300 shrink-0" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M9.879 7.519c1.171-1.025 3.071-1.025 4.242 0 1.172 1.025 1.172 2.687 0 3.712-.203.179-.43.326-.67.442-.745.361-1.45.999-1.45 1.827v.75M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Zm-9 5.25h.008v.008H12v-.008Z" /></svg>'
icon_category = '<svg class="h-3.5 w-3.5 text-blue-600 shrink-0" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M2.25 12.75V12A2.25 2.25 0 0 1 4.5 9.75h15A2.25 2.25 0 0 1 21.75 12v.75m-19.5 0A2.25 2.25 0 0 0 2.25 15v2.25m0-4.5A2.25 2.25 0 0 1 4.5 12h15a2.25 2.25 0 0 1 2.25 2.25m-19.5 0v4.5A2.25 2.25 0 0 0 4.5 21h15a2.25 2.25 0 0 0 2.25-2.25V15M2.25 12V6a2.25 2.25 0 0 1 2.25-2.25h2.25c.59 0 1.157.234 1.576.652L9.75 5.85a2.25 2.25 0 0 0 1.576.652h6.924a2.25 2.25 0 0 1 2.25 2.25v3.25" /></svg>'
# 3. Logik-Weiche für Farben, Standard-Icons und Standard-Labels
case type.to_s
when "storage"
css = "bg-amber-50 text-amber-800 border-amber-200" if css.blank?
computed_icon, computed_label = icon_storage, "Hauptlager"
when "user"
css = "bg-green-50 text-green-800 border-green-200" if css.blank?
computed_icon, computed_label = icon_user, ""
when "room"
css = "bg-blue-50 text-blue-800 border-blue-200" if css.blank?
computed_icon, computed_label = icon_room, ""
when "in_use"
css = "bg-gray-100 text-gray-600 border-gray-200" if css.blank?
computed_icon, computed_label = icon_in_use, "In Benutzung"
when "new_item"
css = "bg-green-50 text-green-700 border-green-200" if css.blank?
computed_icon, computed_label = icon_new, Item.human_attribute_name("conditions.new_item")
when "as_new"
css = "bg-emerald-50 text-emerald-700 border-emerald-200" if css.blank?
computed_icon, computed_label = icon_as_new, Item.human_attribute_name("conditions.as_new")
when "used"
css = "bg-yellow-50 text-yellow-700 border-yellow-200" if css.blank?
computed_icon, computed_label = icon_used, Item.human_attribute_name("conditions.used")
when "heavily_used"
css = "bg-orange-50 text-orange-700 border-orange-200" if css.blank?
computed_icon, computed_label = icon_heavy, Item.human_attribute_name("conditions.heavily_used")
when "defective"
css = "bg-red-50 text-red-700 border-red-200 animate-pulse font-bold" if css.blank?
computed_icon, computed_label = icon_defective, Item.human_attribute_name("conditions.defective")
when "unknown"
css = "bg-gray-50 text-gray-400 border-gray-100" if css.blank?
computed_icon, computed_label = icon_unknown, Item.human_attribute_name("conditions.unknown")
when "category"
css = "bg-blue-50 text-blue-800 border-blue-200" if css.blank?
computed_icon, computed_label = icon_category, ""
when "sticker"
# Wenn von außen kein custom CSS kommt, brennen wir den sportlichen Sticker-Look direkt hier ein
css = "bg-blue-600 text-white border-blue-600 font-mono font-black tracking-wider uppercase py-0.5 px-1.5" if css.blank?
computed_icon, computed_label = "", ""
end
# 4. Optionale Variablen-Überlagerung von außen (Falls manuell übergeben)
final_label = local_assigns[:label].presence || computed_label
final_icon = if local_assigns[:icon_svg].present?
local_assigns[:icon_svg]
elsif local_assigns[:icon_type].present?
local_assigns[:icon_type]
else
computed_icon.to_s.html_safe
end
%>
<!-- Der Haupt-Container bleibt extrem fokussiert und sauber -->
<span class="inline-flex items-center gap-1.5 rounded-md text-xs font-semibold shadow-sm border py-1 px-2.5 <%= css %>">
<%= final_icon if final_icon.present? %>
<%= final_label %>
</span>

View File

@@ -0,0 +1,20 @@
<div class="max-w-4xl mx-auto bg-red-50/50 border border-red-200 rounded-xl p-6 flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4 shadow-sm mt-8">
<div class="flex items-start gap-3">
<!-- Mülleimer-Icon -->
<div class="p-2 bg-red-100 text-red-700 rounded-lg shrink-0 mt-0.5">
<svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="m14.74 9-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 0 1-2.244 2.077H8.084a2.25 2.25 0 0 1-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 0 0-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 0 1 3.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 0 0-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 0 0-7.5 0" />
</svg>
</div>
<div class="min-w-0">
<h3 class="text-sm font-bold text-red-800"><%= title %></h3>
<p class="text-xs text-red-600 mt-0.5 leading-relaxed"><%= description %></p>
</div>
</div>
<!-- Der dynamische Turbo-Delete-Button -->
<%= link_to button_text,
path,
data: { turbo_method: :delete, turbo_confirm: confirm_message },
class: "py-2 px-4 text-sm font-semibold text-white bg-red-600 hover:bg-red-700 rounded-lg shadow-sm transition whitespace-nowrap shrink-0" %>
</div>

View File

@@ -6,7 +6,7 @@
bar_color = type == "notice" ? "bg-green-500" : "bg-red-500"
%>
<div data-controller="flash" class="fixed top-5 right-5 z-50 max-w-sm w-full">
<div data-controller="flash" class="fixed top-20 right sm:right-5 z-50 max-w-sm sm:w-full">
<!-- Das relative Attribut und overflow-hidden sind wichtig für den Balken -->
<div data-flash-target="notification"

View File

@@ -0,0 +1,18 @@
<% if model.errors.any? %>
<div class="p-4 bg-red-50 border border-red-200 rounded-xl flex gap-3 text-sm text-red-800 shadow-sm animate-fade-in">
<!-- Heroicon: exclamation-triangle -->
<svg class="h-5 w-5 text-red-600 shrink-0 mt-0.5" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m9-.75a9 9 0 1 1-18 0 9 9 0 0 1 18 0Zm-9 3.75h.008v.008H12v-.008Z" />
</svg>
<div>
<h4 class="font-bold">
<%= model.class.model_name.human %> konnte nicht gespeichert werden
</h4>
<ul class="list-disc list-inside mt-1 space-y-0.5 text-xs text-red-700">
<% model.errors.full_messages.each do |message| %>
<li><%= message %></li>
<% end %>
</ul>
</div>
</div>
<% end %>

View File

@@ -0,0 +1,26 @@
<%
# 1. Wir prüfen automatisch, ob es ein neuer oder bestehender Eintrag ist
is_new = model.new_record?
# 2. Farb- und Icon-Weiche (Blau für Neu, Amber für Bearbeiten)
bg_css = is_new ? "bg-blue-50 border-blue-100 text-blue-600" : "bg-amber-50 border-amber-100 text-amber-600"
title_text = is_new ? new_title : edit_title
desc_text = is_new ? new_description : edit_description
# 3. Die beiden SVGs (Plus vs. Stift) mit identischer Konturschärfe (stroke-width="2")
icon_plus = '<svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15" /></svg>'
icon_edit = '<svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="m16.862 4.487 1.687-1.688a1.875 1.875 0 1 1 2.652 2.652L10.582 16.07a4.5 4.5 0 0 1-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 0 1 1.13-1.897l8.932-8.931Zm0 0L19.5 7.125M18 14v4.75A2.25 2.25 0 0 1 15.75 21H5.25A2.25 2.25 0 0 1 3 18.75V8.25A2.25 2.25 0 0 1 5.25 6H10" /></svg>'
final_icon = is_new ? icon_plus.html_safe : icon_edit.html_safe
%>
<div class="flex items-center gap-3">
<!-- Das Icon-Quadrat wechselt dynamisch seine Farbe und Grafik -->
<div class="p-2 rounded-lg border shrink-0 <%= bg_css %>">
<%= final_icon %>
</div>
<div class="min-w-0">
<h2 class="text-xl font-bold text-gray-800 leading-tight"><%= title_text %></h2>
<p class="text-sm text-gray-500 mt-1 leading-normal"><%= desc_text %></p>
</div>
</div>

View File

@@ -0,0 +1,17 @@
<!-- (HINWEIS: Hier im Untergrund muss noch dein HTML-Modal für die Kamera-Vorschau liegen, falls das nicht global eingebunden ist) -->
<div data-scanner-target="modal" class="hidden fixed inset-0 bg-gray-900/60 z-50 flex items-center justify-center p-4 backdrop-blur-sm">
<div class="bg-white rounded-2xl max-w-md w-full p-6 space-y-4 shadow-xl border border-gray-100">
<div class="flex justify-between items-center">
<h3 class="text-sm font-bold text-gray-800 uppercase tracking-wide">QR-Code scannen</h3>
<button type="button" data-action="click->scanner#stopCamera" class="p-1 text-gray-400 hover:text-gray-600 rounded-lg">
<svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M6 18 18 6M6 6l12 12" /></svg>
</button>
</div>
<!-- DAS VORSCHAUFENSTER FÜR DIE KAMERA IN DEINER _search_bar.html.erb -->
<!-- KORREKTUR: Das Video-Tag steht jetzt fest im HTML und dient direkt als Target -->
<div class="w-full aspect-square rounded-xl overflow-hidden border border-gray-200 bg-black flex items-center justify-center">
<video data-scanner-target="preview" class="w-full h-full object-cover rounded-xl" playsinline muted></video>
</div>
<p class="text-xs text-gray-500 text-center">Halte den QR-Code des Geräts ruhig in den Scan-Rahmen.</p>
</div>
</div>

View File

@@ -0,0 +1,46 @@
<!-- Navigations-Links mit dynamischen Helper-Klassen (Ausgelagertes Partial) -->
<nav class="flex-1 p-3 space-y-1 overflow-hidden">
<!-- 1. DASHBOARD -->
<%= link_to root_path, class: nav_link_class("dashboard") do %>
<svg class="h-5 w-5 shrink-0 ml-0.5" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M3 13.125C3 12.504 3.504 12 4.125 12h2.25c.621 0 1.125.504 1.125 1.125v6.75C7.5 20.496 6.996 21 6.375 21h-2.25A1.125 1.125 0 0 1 3 19.875v-6.75ZM9.75 8.625c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125v11.25c0 .621-.504 1.125-1.125 1.125h-2.25a1.125 1.125 0 0 1-1.125-1.125V8.625ZM16.5 4.125c0-.621.504-1.125 1.125-1.125h2.25C20.496 3 21 3.504 21 4.125v15.75c0 .621-.504 1.125-1.125 1.125h-2.25a1.125 1.125 0 0 1-1.125-1.125V4.125Z" />
</svg>
<span class="collapse-text ml-3 transition-all duration-300 ease-in-out max-w-[180px] opacity-100 overflow-hidden whitespace-nowrap">Dashboard</span>
<% end %>
<!-- 3. Scannen -->
<%= link_to "", class: nav_link_class("action") do %>
<svg class="h-5 w-5 shrink-0 ml-0.5" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M3.75 4.875c0-.621.504-1.125 1.125-1.125h4.5c.621 0 1.125.504 1.125 1.125v4.5c0 .621-.504 1.125-1.125 1.125h-4.5A1.125 1.125 0 0 1 3.75 9.375v-4.5ZM3.75 14.625c0-.621.504-1.125 1.125-1.125h4.5c.621 0 1.125.504 1.125 1.125v4.5c0 .621-.504 1.125-1.125 1.125h-4.5a1.125 1.125 0 0 1-1.125-1.125v-4.5ZM13.5 4.875c0-.621.504-1.125 1.125-1.125h4.5c.621 0 1.125.504 1.125 1.125v4.5c0 .621-.504 1.125-1.125 1.125h-4.5A1.125 1.125 0 0 1 13.5 9.375v-4.5Z" />
<path stroke-linecap="round" stroke-linejoin="round" d="M6.75 6.75h.75v.75h-.75v-.75ZM6.75 16.5h.75v.75h-.75v-.75ZM16.5 6.75h.75v.75h-.75v-.75ZM13.5 13.5h.75v.75h-.75v-.75ZM13.5 19.5h.75v.75h-.75v-.75ZM19.5 13.5h.75v.75h-.75v-.75ZM19.5 19.5h.75v.75h-.75v-.75ZM16.5 16.5h.75v.75h-.75v-.75Z" />
</svg>
<span class="collapse-text ml-3 transition-all duration-300 ease-in-out max-w-[180px] opacity-100 overflow-hidden whitespace-nowrap">Aktionen</span>
<% end %>
<!-- 2. BESTANDSLISTE -->
<%= link_to items_path, class: nav_link_class("items") do %>
<svg class="h-5 w-5 shrink-0 ml-0.5" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M8.25 6.75h12M8.25 12h12m-12 5.25h12M3.75 6.75h.007v.008H3.75V6.75Zm.375 0a.375.375 0 1 1-.75 0 .375.375 0 0 1 .75 0ZM3.75 12h.007v.008H3.75V12Zm.375 0a.375.375 0 1 1-.75 0 .375.375 0 0 1 .75 0Zm-.375 5.25h.007v.008H3.75v-.008Zm.375 0a.375.375 0 1 1-.75 0 .375.375 0 0 1 .75 0Z" />
</svg>
<span class="collapse-text ml-3 transition-all duration-300 ease-in-out max-w-[180px] opacity-100 overflow-hidden whitespace-nowrap">Bestandsliste</span>
<% end %>
<!-- 4. KATEGORIEN -->
<%= link_to categories_path, class: nav_link_class("categories") do %>
<svg class="h-5 w-5 shrink-0 ml-0.5" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M2.25 12.75V12A2.25 2.25 0 0 1 4.5 9.75h15A2.25 2.25 0 0 1 21.75 12v.75m-8.69-6.44-2.12-2.12a1.5 1.5 0 0 0-1.061-.44H4.5A2.25 2.25 0 0 0 2.25 6v12a2.25 2.25 0 0 0 2.25 2.25h15A2.25 2.25 0 0 0 21.75 18V9a2.25 2.25 0 0 0-2.25-2.25h-5.379a1.5 1.5 0 0 1-1.06-.44Z" />
</svg>
<span class="collapse-text ml-3 transition-all duration-300 ease-in-out max-w-[180px] opacity-100 overflow-hidden whitespace-nowrap">Kategorien</span>
<% end %>
<!-- 5. RÄUME & STANDORTE -->
<%= link_to rooms_path, class: nav_link_class("rooms") do %>
<svg class="h-5 w-5 shrink-0 ml-0.5" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M15 10.5a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z" />
<path stroke-linecap="round" stroke-linejoin="round" d="M19.5 10.5c0 7.142-7.5 11.25-7.5 11.25S4.5 17.642 4.5 10.5a7.5 7.5 0 1 1 15 0Z" />
</svg>
<span class="collapse-text ml-3 transition-all duration-300 ease-in-out max-w-[180px] opacity-100 overflow-hidden whitespace-nowrap">Räume</span>
<% end %>
</nav>

View File

@@ -4,7 +4,7 @@
<title><%= content_for(:title) || "Vault171" %></title>
<meta name="viewport" content="width=device-width,initial-scale=1">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="application-name" content="Vault17">
<meta name="application-name" content="Vault171">
<meta name="mobile-web-app-capable" content="yes">
<%= csrf_meta_tags %>
<%= csp_meta_tag %>
@@ -60,27 +60,7 @@
</div>
<!-- Navigations-Links mit dynamischen Helper-Klassen -->
<nav class="flex-1 p-3 space-y-1 overflow-hidden">
<%= link_to root_path, class: nav_link_class("dashboard") do %>
<svg class="h-5 w-5 shrink-0 ml-0.5" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M3 13.125C3 12.504 3.504 12 4.125 12h2.25c.621 0 1.125.504 1.125 1.125v6.75C7.5 20.496 6.996 21 6.375 21h2.25A1.125 1.125 0 0 1 7.5 19.875v-6.75ZM9.75 8.625c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125v11.25c0 .621-.504 1.125-1.125 1.125h-2.25a1.125 1.125 0 0 1-1.125-1.125V8.625ZM16.5 4.125c0-.621.504-1.125 1.125-1.125h2.25C20.496 3 21 3.504 21 4.125v15.75c0 .621-.504 1.125-1.125 1.125h-2.25a1.125 1.125 0 0 1-1.125-1.125V4.125Z" /></svg>
<span class="collapse-text ml-3 transition-all duration-300 ease-in-out max-w-[180px] opacity-100 overflow-hidden whitespace-nowrap">Dashboard</span>
<% end %>
<%= link_to items_path, class: nav_link_class("items") do %>
<svg class="h-5 w-5 shrink-0 ml-0.5" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M8.25 6.75h12M8.25 12h12m-12 5.25h12M3.75 6.75h.007v.008H3.75V6.75Zm.007 5.25H3.75v.008h.008V12Zm0 5.25H3.75v.008h.008v-.008Z" /></svg>
<span class="collapse-text ml-3 transition-all duration-300 ease-in-out max-w-[180px] opacity-100 overflow-hidden whitespace-nowrap">Bestandsliste</span>
<% end %>
<%= link_to "", class: nav_link_class("inbound_shipments") do %>
<svg class="h-5 w-5 shrink-0 ml-0.5" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M3 16.5v2.25A2.25 2.25 0 0 0 5.25 21h13.5A2.25 2.25 0 0 0 21 18.75V16.5M16.5 12 12 16.5m0 0L7.5 12m4.5 4.5V3" /></svg>
<span class="collapse-text ml-3 transition-all duration-300 ease-in-out max-w-[180px] opacity-100 overflow-hidden whitespace-nowrap">Wareneingang</span>
<% end %>
<%= link_to categories_path, class: nav_link_class("categories") do %>
<svg class="h-5 w-5 shrink-0 ml-0.5" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M19.5 14.25v-2.625a3.375 3.375 0 0 0-3.375-3.375h-1.5A1.125 1.125 0 0 1 13.5 7.125v-1.5a3.375 3.375 0 0 0-3.375-3.375H8.25m0 12.75h7.5m-7.5 3H12M10.5 2.25H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621 Clyde5.04 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 0 0-9-9Z" /></svg>
<span class="collapse-text ml-3 transition-all duration-300 ease-in-out max-w-[180px] opacity-100 overflow-hidden whitespace-nowrap">Kategorien</span>
<% end %>
</nav>
<%= render "layouts/sidebar" %>
<!-- DER NEUE, AUFGERÄUMTE SIDEBAR-FOOTER -->
<div class="border-t border-gray-200 bg-gray-50 flex flex-col p-3 space-y-1 shrink-0">
@@ -106,15 +86,38 @@
Ausloggen
</span>
<% end %>
<%
repo_url = "https://gitea.daboh.ipv64.de/daboh/vault171/"
commit_url = CURRENT_COMMIT.present? ? "#{repo_url}/commit/#{CURRENT_COMMIT}" : repo_url
%>
<%= link_to commit_url, target: "_blank",
class: "group flex items-center justify-center px-3 py-1 text-[11px] rounded-md text-gray-400 hover:text-gray-700 hover:bg-gray-50/50 transition w-full",
title: "Aktuelle Git-Revision anzeigen" do %>
<!-- Das Icon bleibt klein und zentriert -->
<svg class="h-3.5 w-3.5 shrink-0 text-gray-400 group-hover:text-gray-700 transition-colors" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M17.25 6.75 22.5 12l-5.25 5.25m-10.5 0L1.5 12l5.25-5.25m7.5-3-4.5 16.5" />
</svg>
<!-- Durch 'w-0 group-data-[collapsed]:w-0' oder ähnliche Selektoren blockiert der Text beim Einklappen 0 Pixel Platz -->
<span class="collapse-text ml-1.5 transition-all duration-300 ease-in-out max-w-[180px] opacity-100 overflow-hidden whitespace-nowrap font-mono tracking-wider origin-left">
<% if CURRENT_COMMIT.present? %>
<%= CURRENT_COMMIT %>
<% else %>
repository
<% end %>
</span>
<% end %>
</div>
</aside>
<!-- HAUPTBEREICH (Inhaltsfläche rückt bei verkleinerter Sidebar nach) -->
<div class="main-content flex-1 flex flex-col min-h-screen w-full transition-all duration-300 ease-in-out pl-0 md:pl-64">
<!-- HAUPTBEREICH (Fixiert die Gesamthöhe auf den Bildschirm und steuert das Scrollen intern) -->
<div class="main-content flex-1 flex flex-col h-screen w-full transition-all duration-300 ease-in-out pl-0 md:pl-64 overflow-hidden">
<!-- OBERE LEISTE (Top Bar) -->
<header class="h-16 bg-white border-b border-gray-200 flex items-center justify-between px-4 md:px-6 sticky top-0 z-30">
<!-- OBERE LEISTE (Bleibt durch 'sticky' oben, da der umschließende Container nicht mehr mitwächst) -->
<header class="h-16 bg-white border-b border-gray-200 flex items-center justify-between px-4 md:px-6 sticky top-0 z-30 shrink-0">
<div class="flex items-center gap-3">
<!-- BURGER BUTTON (Nur mobil sichtbar) -->
<label for="mobile-sidebar-toggle" class="md:hidden p-2 text-gray-500 hover:text-gray-700 hover:bg-gray-100 rounded-lg transition cursor-pointer">
@@ -127,17 +130,10 @@
</div>
</header>
<!-- INHALT DER JEWEILIGEN VIEW -->
<main class="p-4 md:p-6 flex-1 w-full mx-auto px-4 md:px-8">
<!-- Platzhalter für Flash-Meldungen (z.B. erfolgreiches Speichern)
<% flash.each do |type, message| %>
<div class="mb-4 p-4 text-sm rounded-lg border <%= type == 'notice' ? 'bg-green-50 text-green-800 border-green-200' : 'bg-red-50 text-red-800 border-red-200' %>">
<%= message %>
</div>
<% end %>
-->
<%= render "layouts/flash" %>
<!-- INHALTSFLÄCHE (Nur dieser Bereich scrollt jetzt flexibel nach unten weg!) -->
<main class="p-4 md:p-6 flex-1 w-full mx-auto px-4 md:px-8 overflow-y-auto bg-gray-50/30">
<%= yield %>
</main>
</div>

View File

@@ -0,0 +1,41 @@
<%= form_with(model: room, class: "space-y-6 max-w-4xl mx-auto bg-white border border-gray-200 rounded-xl shadow-sm p-6 md:p-8") do |form| %>
<%= render "layouts/form_header",
model: @room,
new_title: "Raum anlegen",
new_description: "Definiere einen neuen physischen Standort für deine Inventargegenstände.",
edit_title: "Raum bearbeiten",
edit_description: "Aktualisiere die Raumbezeichnung, das Gebäude oder die Etage." %>
<hr class="border-gray-200">
<%= render "layouts/form_errors", model: room %>
<!-- Raumnummer / Name -->
<div>
<%= 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" %>
</div>
<!-- Gebäude & Etage im Grid nebeneinander -->
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div>
<%= 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" %>
</div>
<div>
<%= 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" %>
</div>
</div>
<hr class="border-gray-200">
<!-- Aktions-Buttons -->
<div class="flex justify-end gap-x-3">
<%= 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" %>
</div>
<% end %>

View File

@@ -0,0 +1,2 @@
<div id="<%= dom_id room %>" class="w-full sm:w-auto my-5 space-y-5">
</div>

View File

@@ -0,0 +1,2 @@
json.extract! room, :id, :created_at, :updated_at
json.url room_url(room, format: :json)

View File

@@ -0,0 +1,21 @@
<% content_for :title, "#{@room.name}" %>
<% content_for :top_bar_actions do %>
<%= link_to "javascript:history.back()", 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 %>
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M10.5 19.5L3 12m0 0l7.5-7.5M3 12h18" /></svg>
<div class="hidden md:inline">
Zurück
</div>
<% end %>
<% end %>
<div class="space-y-6 p-4 md:p-6">
<%= render "form", room: @room %>
<%= render "layouts/danger_zone",
title: "Raum löschen",
description: "In diesem Raum gelistete Artikel verlieren ihren Standort und werden automatisch ins Hauptlager umgebucht.",
button_text: "Raum löschen",
confirm_message: "Möchtest du diesen Standort wirklich löschen?",
path: room_path(@room) %>
</div>

View File

@@ -0,0 +1,140 @@
<div class="w-full space-y-4">
<% 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 %>
<!-- Heroicon: plus -->
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15" />
</svg>
<div class="hidden md:inline">
Raum anlegen
</div>
<% end %>
<% end %>
<!-- 1. Suchleiste steht außerhalb des Frames (CSV-Export bleibt hier aus) -->
<%= render "items/search_bar", show_csv: false %>
<!-- 2. NUR DIE TABELLE WIRD IN DEN TURBO-FRAME GEPAKT -->
<%= turbo_frame_tag "items_list_frame" do %>
<% if @rooms.any? %>
<div class="bg-white border border-gray-200 rounded-xl shadow-sm overflow-hidden">
<!-- ========================================================================= -->
<!-- 1. HANDY-ANSICHT: KOMPAKTE CARDS MIT KLEINEN BUTTONS (Bis md:) -->
<!-- ========================================================================= -->
<div class="block md:hidden divide-y divide-gray-200 bg-white">
<% @rooms.each do |room| %>
<div class="p-3 hover:bg-gray-50/50 transition space-y-2">
<!-- OBERE ZEILE: Icon + Name & Geräte-Badge -->
<div class="flex items-start justify-between gap-3 w-full">
<div class="flex items-start gap-2 text-gray-500 min-w-0 flex-1">
<svg class="h-5 w-5 shrink-0 mt-0.5" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M15 10.5a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z" />
<path stroke-linecap="round" stroke-linejoin="round" d="M19.5 10.5c0 7.142-7.5 11.25-7.5 11.25s-7.5-4.108-7.5-11.25a7.5 7.5 0 1 1 15 0Z" />
</svg>
<div class="min-w-0">
<h4 class="font-black text-gray-900 text-base leading-tight truncate"><%= room.name %></h4>
<div class="flex items-center gap-1.5 text-xs font-semibold text-gray-400 font-sans mt-0.5">
<span><%= room.building %></span>
<span class="text-gray-300">•</span>
<span>Etage <%= room.floor %></span>
</div>
</div>
</div>
<div class="shrink-0">
<span class="inline-flex items-center py-0.5 px-2 rounded-full text-xs font-bold <%= room.items.count > 0 ? 'bg-blue-50 text-blue-800 border border-blue-200' : 'bg-gray-100 text-gray-500 border border-gray-200' %>">
<%= room.items.count %> <%= room.items.count == 1 ? "Gerät" : "Geräte" %>
</span>
</div>
</div>
<!-- UNTERE ZEILE: RECHTSBÜNDIGE, QUADRATISCHE BUTTONS (WICHTIG: 'w-auto' erzwingt Kompaktheit) -->
<div class="flex items-center justify-end gap-1.5 w-full">
<!-- DETAILS BUTTON (Durch w-8 h-8 flex-initial garantiert quadratisch und kompakt) -->
<%= 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 %>
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke-width="2.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M2.036 12.322a1.012 1.012 0 0 1 0-.639C3.423 7.51 7.36 4.5 12 4.5c4.638 0 8.573 3.007 9.963 7.178.07.207.07.431 0 .639C20.577 16.49 16.64 19.5 12 19.5c-4.638 0-8.573-3.007-9.963-7.178Z" /><path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z" /></svg>
<% end %>
<!-- BEARBEITEN BUTTON (Durch w-8 h-8 flex-initial garantiert quadratisch und kompakt) -->
<%= 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 %>
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke-width="2.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="m16.862 4.487 1.687-1.688a1.875 1.875 0 1 1 2.652 2.652L10.582 16.07a4.5 4.5 0 0 1-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 0 1 1.13-1.897l8.932-8.931Zm0 0L19.5 7.125M18 14v4.75A2.25 2.25 0 0 1 15.75 21H5.25A2.25 2.25 0 0 1 3 18.75V8.25A2.25 2.25 0 0 1 5.25 6H10" /></svg>
<% end %>
</div>
</div>
<% end %>
</div>
<!-- ========================================================================= -->
<!-- 2. DESKTOP-ANSICHT: STICKY-RÄUME-TABELLE -->
<!-- ========================================================================= -->
<div class="hidden md:block overflow-x-auto max-h-[calc(100vh-12rem)]">
<table class="min-w-full divide-y divide-gray-200 text-sm">
<thead class="bg-gray-50 sticky top-0 z-20 shadow-[0_1px_0_0_rgba(229,231,235,1)]">
<tr>
<th scope="col" class="px-6 py-3 text-start font-semibold text-gray-500 uppercase tracking-wider">Raum</th>
<th scope="col" class="px-6 py-3 text-start font-semibold text-gray-500 uppercase tracking-wider">Gebäudeteil</th>
<th scope="col" class="px-6 py-3 text-start font-semibold text-gray-500 uppercase tracking-wider">Etage</th>
<th scope="col" class="px-6 py-3 text-center font-semibold text-gray-500 uppercase tracking-wider">Aktiver Bestand</th>
<th scope="col" class="px-6 py-3 text-end font-semibold text-gray-500 uppercase tracking-wider">Aktionen</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-200 bg-white">
<% @rooms.each do |room| %>
<tr class="hover:bg-gray-50/50 transition">
<td class="px-6 py-4 whitespace-nowrap">
<div class="flex items-center gap-3 text-gray-400">
<svg class="h-5 w-5 shrink-0 ml-0.5" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M15 10.5a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z" />
<path stroke-linecap="round" stroke-linejoin="round" d="M19.5 10.5c0 7.142-7.5 11.25-7.5 11.25s-7.5-4.108-7.5-11.25a7.5 7.5 0 1 1 15 0Z" />
</svg>
<div class="font-semibold text-gray-900"><%= room.name %></div>
</div>
</td>
<td class="px-6 py-4 whitespace-nowrap text-gray-700 font-medium"><%= room.building %></td>
<td class="px-6 py-4 whitespace-nowrap text-gray-500 font-mono text-xs"><%= room.floor %></td>
<td class="px-6 py-4 whitespace-nowrap text-center">
<span class="inline-flex items-center py-0.5 px-2.5 rounded-full text-xs font-semibold <%= room.items.count > 0 ? 'bg-blue-50 text-blue-800 border border-blue-200' : 'bg-gray-100 text-gray-500 border border-gray-200' %>">
<%= room.items.count %> <%= room.items.count == 1 ? "Gerät" : "Geräte" %>
</span>
</td>
<td class="px-6 py-4 whitespace-nowrap text-end font-medium text-xs">
<!-- KORREKTUR FÜR DIE RÄUME-ÜBERSICHT (Mobil-Bereich) -->
<div class="flex items-center justify-end gap-2 shrink-0">
<!-- DETAILS BUTTON im edlen weißen Design -->
<%= link_to room_path(room), data: { turbo_frame: "_top" },
class: "p-2.5 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: "Inventar öffnen" do %>
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke-width="2.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M2.036 12.322a1.012 1.012 0 0 1 0-.639C3.423 7.51 7.36 4.5 12 4.5c4.638 0 8.573 3.007 9.963 7.178.07.207.07.431 0 .639C20.577 16.49 16.64 19.5 12 19.5c-4.638 0-8.573-3.007-9.963-7.178Z" /><path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z" /></svg>
<% end %>
<!-- BEARBEITEN BUTTON im edlen weißen Design -->
<%= link_to edit_room_path(room), data: { turbo_frame: "_top" },
class: "p-2.5 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 %>
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke-width="2.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="m16.862 4.487 1.687-1.688a1.875 1.875 0 1 1 2.652 2.652L10.582 16.07a4.5 4.5 0 0 1-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 0 1 1.13-1.897l8.932-8.931Zm0 0L19.5 7.125M18 14v4.75A2.25 2.25 0 0 1 15.75 21H5.25A2.25 2.25 0 0 1 3 18.75V8.25A2.25 2.25 0 0 1 5.25 6H10" /></svg>
<% end %>
</div>
</td>
</tr>
<% end %>
</tbody>
</table>
</div>
</div>
<% else %>
<div class="text-center py-16 text-gray-400 bg-white border border-gray-200 rounded-xl shadow-sm">
<p class="text-sm font-medium">Keine passenden Gebäude oder Büros gefunden.</p>
</div>
<% end %>
<% end %>
</div>

View File

@@ -0,0 +1 @@
json.array! @rooms, partial: "rooms/room", as: :room

View File

@@ -0,0 +1,14 @@
<% content_for :title, "Raum anlegen" %>
<% content_for :top_bar_actions do %>
<%= link_to "javascript:history.back()", 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 %>
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M10.5 19.5L3 12m0 0l7.5-7.5M3 12h18" /></svg>
<div class="hidden md:inline">
Zurück
</div>
<% end %>
<% end %>
<div class="p-4 md:p-6">
<%= render "form", room: @room %>
</div>

View File

@@ -0,0 +1,40 @@
<% content_for :title, "Standort: #{@room.name}" %>
<% content_for :top_bar_actions do %>
<%= link_to "javascript:history.back()", 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 %>
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M10.5 19.5L3 12m0 0l7.5-7.5M3 12h18" /></svg>
<div class="hidden md:inline">
Zurück
</div>
<% end %>
<% end %>
<div class="w-full space-y-6">
<!-- Metadaten des Raums -->
<div class="bg-white border border-gray-200 rounded-xl p-6 shadow-sm grid grid-cols-1 sm:grid-cols-2 gap-4 text-sm max-w-xl">
<div>
<p class="text-[10px] font-bold text-gray-400 uppercase tracking-wider">Gebäude</p>
<p class="text-gray-800 font-semibold mt-0.5"><%= @room.building %></p>
</div>
<div>
<p class="text-[10px] font-bold text-gray-400 uppercase tracking-wider">Stockwerk / Etage</p>
<p class="text-gray-800 font-semibold mt-0.5"><%= @room.floor %></p>
</div>
</div>
<div>
<!-- 1. Suchleiste steht außerhalb des Frames (CSV-Export hier deaktiviert) -->
<%= render "items/search_bar", show_csv: false %>
<!-- 2. NUR DAS REUSE-LIST-PARTIAL IN DEN FRAME GEPAKT -->
<%= turbo_frame_tag "items_list_frame" do %>
<% if @items.any? %>
<%= render "items/list", items: @items %>
<% else %>
<div class="text-center py-12 text-gray-400 bg-white border border-gray-200 rounded-xl shadow-sm">
<p class="text-sm">Keine passenden Artikel an diesem Standort gefunden.</p>
</div>
<% end %>
<% end %>
</div>
</div>

View File

@@ -0,0 +1 @@
json.partial! "rooms/room", room: @room

View File

@@ -16,6 +16,15 @@ module Vault171
# Common ones are `templates`, `generators`, or `middleware`, for example.
config.autoload_lib(ignore: %w[assets tasks])
# Setzt die Standard-Sprache der App dauerhaft auf Deutsch
config.i18n.default_locale = :de
# Erlaubt Rails, auch Unterordner in locales zu durchsuchen
config.i18n.available_locales = [ :de, :en ]
# Deaktiviert das umschließende field_with_errors Div bei Validierungsfehlern
config.action_view.field_error_proc = Proc.new { |html_tag, instance| html_tag.html_safe }
# Configuration for the application, engines, and railties goes here.
#
# These settings can be overridden in specific environments using the files

View File

@@ -50,6 +50,10 @@ env:
# When you start using multiple servers, you should split out job processing to a dedicated machine.
SOLID_QUEUE_IN_PUMA: true
# Hier liest Kamal beim Deployen lokal deine Git-SHA aus
# und brennt sie als Umgebungsvariable fest in den Server-Container ein!
GIT_COMMIT_SHA: <%= `git rev-parse --short HEAD`.strip %>
# Set number of processes dedicated to Solid Queue (default: 1)
# JOB_CONCURRENCY: 3

View File

@@ -5,3 +5,4 @@ pin "@hotwired/turbo-rails", to: "turbo.min.js"
pin "@hotwired/stimulus", to: "stimulus.min.js"
pin "@hotwired/stimulus-loading", to: "stimulus-loading.js"
pin_all_from "app/javascript/controllers", under: "controllers"
pin "qr-scanner", to: "qr-scanner.js"

View File

@@ -0,0 +1,9 @@
# 1. Versuch: Schaut nach der von Kamal eingebrannten Umgebungsvariable (Produktiv-Server)
CURRENT_COMMIT = ENV["GIT_COMMIT_SHA"].presence ||
ENV["RENDER_GIT_COMMIT"].presence ||
# 2. Versuch: Lokal auf deinem PC direkt über Git auslesen (Development)
begin
`git rev-parse --short HEAD`.strip
rescue
nil
end

119
config/locales/de.yml Normal file
View File

@@ -0,0 +1,119 @@
de:
# =========================================================================
# 1. GENERISCHE RAILS-FORMATIERUNGEN (Datum & Zeit)
# =========================================================================
date:
formats:
default: "%d.%m.%Y"
short: "%d. %b"
long: "%d. %B %Y"
month_names: [~, Januar, Februar, März, April, Mai, Juni, Juli, August, September, Oktober, November, Dezember]
abbr_month_names: [~, Jan, Feb, Mär, Apr, Mai, Jun, Jul, Aug, Sep, Okt, Nov, Dez]
day_names: [Sonntag, Montag, Dienstag, Mittwoch, Donnerstag, Freitag, Samstag]
abbr_day_names: [So, Mo, Di, Mi, Do, Fr, Sa]
time:
formats:
default: "%d.%m.%Y, %H:%M Uhr"
short: "%d. %b, %H:%M"
long: "%d. %B %Y, %H:%M"
am: "vormittags"
pm: "nachmittags"
# Fix für time_ago_in_words
datetime:
distance_in_words:
half_a_minute: "vor weniger als einer Minute"
less_than_x_minutes:
one: "vor weniger als einer Minute"
other: "vor weniger als %{count} Minuten"
x_minutes:
one: "vor einer Minute"
other: "vor %{count} Minuten"
about_x_hours:
one: "vor etwa einer Stunde"
other: "vor etwa %{count} Stunden"
x_days:
one: "gestern"
other: "vor %{count} Tagen"
about_x_months:
one: "vor etwa einem Monat"
other: "vor etwa %{count} Monaten"
x_months:
one: "vor einem Monat"
other: "vor %{count} Monaten"
about_x_years:
one: "vor etwa einem Jahr"
other: "vor etwa %{count} Jahren"
over_x_years:
one: "vor über einem Jahr"
other: "vor über %{count} Jahren"
almost_x_years:
one: "vor fast einem Jahr"
other: "vor fast %{count} Jahren"
# =========================================================================
# 2. GENERISCHE MODELL-VALIDIERUNGEN (Fehlermeldungen für presence, etc.)
# =========================================================================
errors:
messages:
blank: "darf nicht leer sein"
taken: "wird bereits verwendet"
invalid: "ist ungültig"
inclusion: "ist kein gültiger Wert"
required: "muss ausgefüllt werden"
record_invalid: "Validierung fehlgeschlagen: %{errors}"
# =========================================================================
# 3. INTERNE MODELL-ATTRIBUTE (ActiveRecord-Struktur)
# =========================================================================
activerecord:
errors:
messages:
record_invalid: "Das Objekt konnte nicht gespeichert werden, da Eingaben fehlerhaft sind."
models:
item: "Artikel"
room: "Raum"
category: "Kategorie"
assignment_log: "Standort-Protokoll"
condition_log: "Zustands-Protokoll"
attributes:
# --- Attribute für Artikel ---
item:
name: "Artikelname"
sku: "SKU-Nummer"
serial_number: "Seriennummer"
sticker_id: "Sticker-ID"
price: "Kaufpreis"
notes: "Notizen"
condition: "Zustand bei Einlagerung"
category: "Kategorie"
user: "Benutzer"
room: "Raum"
# Deine unzerstörbare Enum-Struktur:
item/conditions:
unknown: "Unbekannt"
new_item: "Neu"
as_new: "Neuwertig"
used: "Gebraucht"
heavily_used: "Stark Gebraucht"
defective: "Defekt"
# --- Attribute für die Räume ---
room:
name: "RaumNr"
building: "Gebäude"
floor: "Etage"
# --- Attribute für das Standort-Logbuch (AssignmentLog) ---
assignment_log:
assigned_at: "Zuweisungs-Zeitpunkt"
returned_at: "Rückgabe-Zeitpunkt"
# --- Attribute für das neue Zustands-Logbuch (ConditionLog) ---
condition_log:
condition: "Zustands-Änderung"
created_at: "Protokoll-Zeitpunkt"

View File

@@ -28,4 +28,14 @@
# enabled: "ON"
en:
hello: "Hello world"
activerecord:
attributes:
item:
condition: "Condition upon storage"
conditions:
unknown: "Unknown"
new_item: "New"
as_new: "Like New"
used: "Used"
heavily_used: "Heavily Used"
defective: "Defective"

View File

@@ -1,4 +1,5 @@
Rails.application.routes.draw do
resources :rooms
resources :items
resources :categories
namespace :authentications do

View File

@@ -0,0 +1,5 @@
class AddConditionToItems < ActiveRecord::Migration[8.1]
def change
add_column :items, :condition, :string, default: "unknown", null: false
end
end

3
db/schema.rb generated
View File

@@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema[8.1].define(version: 2026_05_21_125254) do
ActiveRecord::Schema[8.1].define(version: 2026_05_28_181506) do
create_table "assignment_logs", force: :cascade do |t|
t.datetime "assigned_at"
t.datetime "created_at", null: false
@@ -50,6 +50,7 @@ ActiveRecord::Schema[8.1].define(version: 2026_05_21_125254) do
create_table "items", force: :cascade do |t|
t.integer "category_id"
t.string "condition", default: "unknown", null: false
t.datetime "created_at", null: false
t.string "name"
t.text "notes"

View File

@@ -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

View File

@@ -1,15 +0,0 @@
# Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html
one:
item: one
user: one
room: one
assigned_at: 2026-05-21 15:29:43
returned_at: 2026-05-21 15:29:43
two:
item: two
user: two
room: two
assigned_at: 2026-05-21 15:29:43
returned_at: 2026-05-21 15:29:43

View File

@@ -1,7 +0,0 @@
require "test_helper"
class AssignmentLog2Test < ActiveSupport::TestCase
# test "the truth" do
# assert true
# end
end

57
vendor/javascript/qr-scanner.js vendored Normal file

File diff suppressed because one or more lines are too long