Compare commits
30 Commits
650b83bdf4
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| c139ae6aa8 | |||
| f7375d29a6 | |||
| 8c7482c1d7 | |||
| 68d31090f7 | |||
| 8c5e862eb3 | |||
| ff9f8b7523 | |||
| 92786161b4 | |||
| c1fc589883 | |||
| 59c462506d | |||
| c12eab7f98 | |||
| 1f18b7dd7d | |||
| 2931314cd1 | |||
| d8c6a7ab82 | |||
| fd3149b13f | |||
| 5c9e6a34b4 | |||
| 00be2bd4d3 | |||
| f7ef41459e | |||
| bdcdcbd681 | |||
| 9e18b233d9 | |||
| edf3886b94 | |||
| a3352bc1eb | |||
| 0ede18f6f0 | |||
| b4c57b6f14 | |||
| af10dca289 | |||
| e161582c4a | |||
| a0e7272b6f | |||
| b66f59eedc | |||
| 623b7f0256 | |||
| a0409e1cf8 | |||
| 204a6c05dc |
2
Gemfile
2
Gemfile
@@ -63,6 +63,8 @@ group :development do
|
|||||||
gem "letter_opener", "~> 1.10"
|
gem "letter_opener", "~> 1.10"
|
||||||
|
|
||||||
gem "hotwire-spark"
|
gem "hotwire-spark"
|
||||||
|
|
||||||
|
gem "htmlbeautifier"
|
||||||
end
|
end
|
||||||
|
|
||||||
group :test do
|
group :test do
|
||||||
|
|||||||
@@ -134,6 +134,7 @@ GEM
|
|||||||
listen
|
listen
|
||||||
rails (>= 7.0.0)
|
rails (>= 7.0.0)
|
||||||
zeitwerk
|
zeitwerk
|
||||||
|
htmlbeautifier (1.4.3)
|
||||||
i18n (1.14.8)
|
i18n (1.14.8)
|
||||||
concurrent-ruby (~> 1.0)
|
concurrent-ruby (~> 1.0)
|
||||||
image_processing (1.14.0)
|
image_processing (1.14.0)
|
||||||
@@ -427,6 +428,7 @@ DEPENDENCIES
|
|||||||
csv (~> 3.3)
|
csv (~> 3.3)
|
||||||
debug
|
debug
|
||||||
hotwire-spark
|
hotwire-spark
|
||||||
|
htmlbeautifier
|
||||||
image_processing (~> 1.2)
|
image_processing (~> 1.2)
|
||||||
importmap-rails
|
importmap-rails
|
||||||
jbuilder
|
jbuilder
|
||||||
@@ -499,6 +501,7 @@ CHECKSUMS
|
|||||||
fugit (1.12.1) sha256=5898f478ede9b415f0804e42b8f3fd53f814bd85eebffceebdbc34e1107aaf68
|
fugit (1.12.1) sha256=5898f478ede9b415f0804e42b8f3fd53f814bd85eebffceebdbc34e1107aaf68
|
||||||
globalid (1.3.0) sha256=05c639ad6eb4594522a0b07983022f04aa7254626ab69445a0e493aa3786ff11
|
globalid (1.3.0) sha256=05c639ad6eb4594522a0b07983022f04aa7254626ab69445a0e493aa3786ff11
|
||||||
hotwire-spark (0.1.13) sha256=0a24799b0942fc9b7ea3e560a5aba3f4dd9a6086958a25929894340f920fb499
|
hotwire-spark (0.1.13) sha256=0a24799b0942fc9b7ea3e560a5aba3f4dd9a6086958a25929894340f920fb499
|
||||||
|
htmlbeautifier (1.4.3) sha256=b43d08f7e2aa6ae1b5a6f0607b4ed8954c8d4a8e85fd2336f975dda1e4db385b
|
||||||
i18n (1.14.8) sha256=285778639134865c5e0f6269e0b818256017e8cde89993fdfcbfb64d088824a5
|
i18n (1.14.8) sha256=285778639134865c5e0f6269e0b818256017e8cde89993fdfcbfb64d088824a5
|
||||||
image_processing (1.14.0) sha256=754cc169c9c262980889bec6bfd325ed1dafad34f85242b5a07b60af004742fb
|
image_processing (1.14.0) sha256=754cc169c9c262980889bec6bfd325ed1dafad34f85242b5a07b60af004742fb
|
||||||
importmap-rails (2.2.3) sha256=7101be2a4dc97cf1558fb8f573a718404c5f6bcfe94f304bf1f39e444feeb16a
|
importmap-rails (2.2.3) sha256=7101be2a4dc97cf1558fb8f573a718404c5f6bcfe94f304bf1f39e444feeb16a
|
||||||
|
|||||||
@@ -9,7 +9,17 @@ class CategoriesController < ApplicationController
|
|||||||
# GET /categories/1 or /categories/1.json
|
# GET /categories/1 or /categories/1.json
|
||||||
def show
|
def show
|
||||||
@category = Category.find(params[:id])
|
@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
|
end
|
||||||
|
|
||||||
# GET /categories/new
|
# GET /categories/new
|
||||||
|
|||||||
@@ -5,6 +5,15 @@ class ItemsController < ApplicationController
|
|||||||
def index
|
def index
|
||||||
@items = Item.all.includes(:category, :user, :room).order(created_at: :desc)
|
@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|
|
respond_to do |format|
|
||||||
format.html # Rendert ganz normal deine Bestandsliste im Browser
|
format.html # Rendert ganz normal deine Bestandsliste im Browser
|
||||||
format.csv do
|
format.csv do
|
||||||
@@ -116,6 +125,6 @@ class ItemsController < ApplicationController
|
|||||||
|
|
||||||
def item_params
|
def item_params
|
||||||
# 'user_name' und 'room_name' müssen in die Strong Parameters aufgenommen werden!
|
# '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
|
||||||
end
|
end
|
||||||
|
|||||||
97
app/controllers/rooms_controller.rb
Normal file
97
app/controllers/rooms_controller.rb
Normal 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
|
||||||
2
app/helpers/rooms_helper.rb
Normal file
2
app/helpers/rooms_helper.rb
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
module RoomsHelper
|
||||||
|
end
|
||||||
@@ -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 {
|
export default class extends Controller {
|
||||||
static targets = [ "userSection", "roomSection" ]
|
static targets = ["userSection", "roomSection"];
|
||||||
|
|
||||||
toggle(event) {
|
toggle(event) {
|
||||||
const value = event.target.value
|
const value = event.target.value;
|
||||||
const userDropdown = this.userSectionTarget.querySelector('select')
|
const userDropdown = this.userSectionTarget.querySelector("select");
|
||||||
const roomDropdown = this.roomSectionTarget.querySelector('select')
|
const roomDropdown = this.roomSectionTarget.querySelector("select");
|
||||||
|
|
||||||
if (value === "user") {
|
if (value === "user") {
|
||||||
this.userSectionTarget.classList.remove("hidden")
|
this.userSectionTarget.classList.remove("hidden");
|
||||||
this.roomSectionTarget.classList.add("hidden")
|
this.roomSectionTarget.classList.add("hidden");
|
||||||
roomDropdown.value = "" // Raum-ID löschen, da ein Artikel nur einen Inhaber haben kann
|
roomDropdown.value = ""; // Raum-ID löschen, da ein Artikel nur einen Inhaber haben kann
|
||||||
} else if (value === "room") {
|
} else if (value === "room") {
|
||||||
this.roomSectionTarget.classList.remove("hidden")
|
this.roomSectionTarget.classList.remove("hidden");
|
||||||
this.userSectionTarget.classList.add("hidden")
|
this.userSectionTarget.classList.add("hidden");
|
||||||
userDropdown.value = "" // User-ID löschen
|
userDropdown.value = ""; // User-ID löschen
|
||||||
} else {
|
} else {
|
||||||
// Hauptlager ausgewählt -> Beide ausblenden und Werte in der DB nullen
|
// Hauptlager ausgewählt -> Beide ausblenden und Werte in der DB nullen
|
||||||
this.userSectionTarget.classList.add("hidden")
|
this.userSectionTarget.classList.add("hidden");
|
||||||
this.roomSectionTarget.classList.add("hidden")
|
this.roomSectionTarget.classList.add("hidden");
|
||||||
userDropdown.value = ""
|
userDropdown.value = "";
|
||||||
roomDropdown.value = ""
|
roomDropdown.value = "";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
65
app/javascript/controllers/autocomplete_controller.js
Normal file
65
app/javascript/controllers/autocomplete_controller.js
Normal 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"))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 {
|
export default class extends Controller {
|
||||||
static targets = [ "input", "preview", "modal" ]
|
static targets = ["input", "preview", "modal"];
|
||||||
|
|
||||||
connect() {
|
connect() {
|
||||||
this.html5QrCode = null
|
this.qrScanner = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Öffnet das Modal und startet den Kamera-Stream
|
|
||||||
startCamera(event) {
|
startCamera(event) {
|
||||||
event.preventDefault()
|
event.preventDefault();
|
||||||
|
this.modalTarget.classList.remove("hidden");
|
||||||
// Modal anzeigen
|
|
||||||
this.modalTarget.classList.remove("hidden")
|
|
||||||
|
|
||||||
// Neue Instanz auf dem Preview-Div mit der ID des Elements erzeugen
|
const videoElement = this.previewTarget;
|
||||||
this.html5QrCode = new Html5Qrcode(this.previewTarget.id)
|
|
||||||
|
|
||||||
// FPS-Rate und Scan-Rahmen (250x250px) festlegen
|
// Scanner initialisieren (Direkt über die importierte Klasse ohne 'window.')
|
||||||
const config = { fps: 10, qrbox: { width: 250, height: 250 } }
|
//this.qrScanner = new QrScanner(
|
||||||
|
// Greift jetzt absolut fehlerfrei auf die geladene Klasse zu
|
||||||
this.html5QrCode.start(
|
this.qrScanner = new window.QrScanner(
|
||||||
{ facingMode: "environment" }, // Erzwingt die rückseitige Hauptkamera bei Handys
|
videoElement,
|
||||||
config,
|
(result) => { this.handleScanSuccess(result.data); },
|
||||||
(decodedText, decodedResult) => {
|
{
|
||||||
// SUCCESS: Code erkannt!
|
onDecodeError: (error) => { /* Fehler ignorieren */ },
|
||||||
this.inputTarget.value = decodedText // Trägt die ID (z.B. 10024) ins Textfeld ein
|
highlightScanRegion: true,
|
||||||
this.stopCamera() // Stoppt die Kamera und schließt das Fenster
|
highlightCodeOutline: true,
|
||||||
},
|
maxScansPerSecond: 10
|
||||||
(errorMessage) => {
|
|
||||||
// Kontinuierlicher Scan-Loop (Fehler ignorieren, wenn kein QR-Code im Bild ist)
|
|
||||||
}
|
}
|
||||||
).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() {
|
stopCamera() {
|
||||||
if (this.html5QrCode && this.html5QrCode.isScanning) {
|
if (this.qrScanner) {
|
||||||
this.html5QrCode.stop().then(() => {
|
this.qrScanner.stop();
|
||||||
this.modalTarget.classList.add("hidden")
|
this.qrScanner.destroy();
|
||||||
}).catch((err) => console.error("Fehler beim Beenden des Streams:", err))
|
this.qrScanner = null;
|
||||||
} else {
|
|
||||||
this.modalTarget.classList.add("hidden")
|
|
||||||
}
|
}
|
||||||
|
this.modalTarget.classList.add("hidden");
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnect() {
|
||||||
|
this.stopCamera();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
11
app/javascript/controllers/search_form_controller.js
Normal file
11
app/javascript/controllers/search_form_controller.js
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
class AssignmentLog2 < ApplicationRecord
|
|
||||||
belongs_to :item
|
|
||||||
belongs_to :user
|
|
||||||
belongs_to :room
|
|
||||||
end
|
|
||||||
@@ -2,4 +2,19 @@ class Category < ApplicationRecord
|
|||||||
has_many :items, dependent: :restrict_with_error
|
has_many :items, dependent: :restrict_with_error
|
||||||
|
|
||||||
validates :name, presence: true, uniqueness: true
|
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
|
end
|
||||||
|
|||||||
@@ -6,6 +6,15 @@ class Item < ApplicationRecord
|
|||||||
# ohne dass dafür Spalten in der Datenbank existieren müssen.
|
# ohne dass dafür Spalten in der Datenbank existieren müssen.
|
||||||
attr_accessor :user_name, :room_name
|
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 :category
|
||||||
belongs_to :user, optional: true # Optional, falls im Raum oder Lager
|
belongs_to :user, optional: true # Optional, falls im Raum oder Lager
|
||||||
belongs_to :room, optional: true # Optional, falls beim User oder Lager
|
belongs_to :room, optional: true # Optional, falls beim User oder Lager
|
||||||
@@ -71,6 +80,42 @@ class Item < ApplicationRecord
|
|||||||
end
|
end
|
||||||
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
|
private
|
||||||
|
|
||||||
def either_user_or_room
|
def either_user_or_room
|
||||||
|
|||||||
@@ -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? %>
|
<%= render "layouts/form_header",
|
||||||
<div class="p-4 mb-4 text-sm text-red-800 rounded-lg bg-red-50 border border-red-200" role="alert">
|
model: @category,
|
||||||
<h2 class="font-bold mb-1"><%= pluralize(category.errors.count, "Fehler") %> verhinderten das Speichern:</h2>
|
new_title: "Kategorie anlegen",
|
||||||
<ul class="list-disc list-inside text-xs">
|
new_description: "Definiere einen neuen Hardware-Typ für deine Bestandsliste.",
|
||||||
<% category.errors.full_messages.each do |message| %>
|
edit_title: "Kategorie bearbeiten",
|
||||||
<li><%= message %></li>
|
edit_description: "Aktualisiere die Bezeichnung oder die Notizen dieser Kategorie." %>
|
||||||
<% 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>
|
|
||||||
|
|
||||||
<hr class="border-gray-200">
|
<hr class="border-gray-200">
|
||||||
|
|
||||||
<!-- Name -->
|
<%= render "layouts/form_errors", model: category %>
|
||||||
<div>
|
|
||||||
<%= form.label :name, "Name der Kategorie", class: "block text-sm font-medium mb-2 text-gray-700" %>
|
<!-- KATEGORIENAME INPUT -->
|
||||||
<%= 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" %>
|
<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>
|
</div>
|
||||||
|
|
||||||
<!-- Beschreibung -->
|
<!-- Beschreibung -->
|
||||||
<div>
|
<div class="space-y-1.5">
|
||||||
<%= form.label :description, "Beschreibung / Notizen", class: "block text-sm font-medium mb-2 text-gray-700" %>
|
<%= 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?..." %>
|
<%= 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>
|
</div>
|
||||||
|
|
||||||
<hr class="border-gray-200">
|
<!-- AKTIONEN (Speichern & Abbrechen) -->
|
||||||
|
<div class="flex items-center justify-end gap-3 pt-4 border-t border-gray-100">
|
||||||
<!-- Buttons -->
|
<%= 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" %>
|
||||||
<div class="flex justify-end gap-x-3">
|
<%= 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" %>
|
||||||
<%= 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" %>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|||||||
@@ -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 %>
|
<%= render "form", category: @category %>
|
||||||
|
|
||||||
<!-- Gefahrenbereich: Kategorie löschen (Nur wenn keine Items drin sind) -->
|
<%= render "layouts/danger_zone",
|
||||||
<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">
|
title: "Kategorie löschen",
|
||||||
<div>
|
description: "Das Löschen einer Kategorie entfernt diesen Typ dauerhaft. Alle zugeordneten Artikel müssen danach neu kategorisiert werden.",
|
||||||
<h3 class="text-sm font-bold text-red-800">Kategorie löschen</h3>
|
button_text: "Kategorie löschen",
|
||||||
<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>
|
confirm_message: "Möchtest du diese Kategorie wirklich löschen? Alle Artikel dieses Typs verlieren ihre Kategorie.",
|
||||||
</div>
|
path: category_path(@category) %>
|
||||||
<%= 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>
|
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,44 +1,92 @@
|
|||||||
<!-- Ersetze den alten Button im Top-Bar-Yield durch diesen echten Link -->
|
|
||||||
|
|
||||||
<% content_for :title, "Kategorien" %>
|
<% content_for :title, "Kategorien" %>
|
||||||
|
|
||||||
|
<!-- 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>
|
||||||
|
<div class="hidden md:inline">
|
||||||
|
Kategorie anlegen
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
<div class="space-y-6">
|
<div class="space-y-6">
|
||||||
<!-- Header-Aktionen (Nutzt das Yield aus deinem Layout) -->
|
|
||||||
<% content_for :top_bar_actions do %>
|
<% if @categories.any? %>
|
||||||
<%= 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 %>
|
<!-- RASTER: Symmetrische Kacheln für Desktop und Mobilgeräte -->
|
||||||
<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="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||||
Kategorie erstellen
|
<% @categories.each do |category| %>
|
||||||
<% end %>
|
<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 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>
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 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">
|
||||||
|
|
||||||
|
<!-- 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>
|
||||||
|
|
||||||
|
<!-- 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 %>
|
<% end %>
|
||||||
|
|
||||||
<!-- Das Kachel-Grid -->
|
|
||||||
<div class="grid grid-cols-1 md: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>
|
|
||||||
<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>
|
|
||||||
<!-- 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>
|
|
||||||
<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">
|
|
||||||
|
|
||||||
<%= link_to "Bearbeiten", edit_category_path(category), class: "text-gray-500 hover:text-gray-700 transition" %>
|
|
||||||
|
|
||||||
<%= link_to category_path(category), class: "text-blue-600 hover:text-blue-800 flex items-center gap-0.5 transition" do %>
|
|
||||||
Artikel ansehen →
|
|
||||||
<% end %>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<% end %>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -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">
|
<div class="p-4 md:p-6">
|
||||||
<%= render "form", category: @category %>
|
<%= render "form", category: @category %>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -1,35 +1,34 @@
|
|||||||
<% content_for :title, "Kategorie: #{@category.name}" %>
|
<% content_for :title, "Kategorie: #{@category.name}" %>
|
||||||
|
|
||||||
<% content_for :top_bar_actions do %>
|
<% 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>
|
<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 %>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
<div class="w-full space-y-6">
|
<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">
|
<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>
|
<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>
|
<p class="text-sm text-gray-600 mt-1 leading-relaxed"><%= @category.description.presence || "Keine Beschreibung hinterlegt." %></p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<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 %>
|
<%= render "items/search_bar", show_csv: false %>
|
||||||
|
|
||||||
<!-- 2. Artikelliste laden -->
|
<!-- 2. NUR DAS LIST-PARTIAL WIRD IN DEN FRAME GEPAKT -->
|
||||||
<% if @items.any? %>
|
<%= turbo_frame_tag "items_list_frame" do %>
|
||||||
<%= render "items/list", items: @items %>
|
<% if @items.any? %>
|
||||||
<% else %>
|
<%= render "items/list", items: @items %>
|
||||||
<div class="text-center py-16 text-gray-400 bg-white border border-gray-200 rounded-xl shadow-sm">
|
<% else %>
|
||||||
<svg class="mx-auto h-12 w-12 text-gray-300" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
<div class="text-center py-12 text-gray-400 bg-white border border-gray-200 rounded-xl shadow-sm">
|
||||||
<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" />
|
<p class="text-sm">Keine passenden Artikel in dieser Kategorie gefunden.</p>
|
||||||
</svg>
|
</div>
|
||||||
<p class="text-sm mt-3 font-medium">Bisher sind keine Inventargegenstände erfasst.</p>
|
<% end %>
|
||||||
<p class="text-xs text-gray-400 mt-1">Klicke oben rechts auf "Artikel hinzufügen", um das erste Gerät einzubuchen.</p>
|
|
||||||
</div>
|
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -1,134 +1,157 @@
|
|||||||
<% content_for :title, "Dashboard Übersicht" %>
|
<% content_for :title, "Dashboard" %>
|
||||||
|
|
||||||
<div class="space-y-6">
|
<div class="space-y-6">
|
||||||
|
|
||||||
<!-- KENNZAHLEN-GRID -->
|
<!-- DYNAMISCHES STATISTIK-RASTER (3 Spalten auf Desktop, 1 auf Mobile) -->
|
||||||
<div class="grid grid-cols-1 sm:grid-cols-3 gap-4">
|
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||||
|
|
||||||
<!-- Karte 1: Gesamtartikel -->
|
<!-- KACHEL 1: GESAMTZEIT INVENTAR -->
|
||||||
<div class="bg-white border border-gray-200 rounded-xl p-5 shadow-sm flex items-center gap-4">
|
<div class="bg-white border border-gray-200 rounded-xl p-6 shadow-sm flex items-center justify-between gap-4">
|
||||||
<div class="p-3 bg-blue-50 text-blue-600 rounded-lg">
|
<div class="flex items-center gap-4 min-w-0">
|
||||||
<!-- Heroicon: cube -->
|
<!-- Icon jetzt links -->
|
||||||
<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 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>
|
||||||
|
</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>
|
</div>
|
||||||
<div>
|
<!-- Große Zahl jetzt rechtsbündig -->
|
||||||
<p class="text-xs font-semibold uppercase text-gray-400">Objekte im System</p>
|
<h3 class="text-3xl font-black text-gray-900 text-end shrink-0"><%= Item.count %></h3>
|
||||||
<h3 class="text-2xl font-bold text-gray-800"><%= @total_items %></h3>
|
</div>
|
||||||
|
|
||||||
|
<!-- 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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Karte 2: Im Lager -->
|
<!-- SPALTE 2: LETZTE LOGBUCH-AKTIVITÄTEN (HISTORIE) -->
|
||||||
<div class="bg-white border border-gray-200 rounded-xl p-5 shadow-sm flex items-center gap-4">
|
<div class="bg-white border border-gray-200 rounded-xl shadow-sm overflow-hidden flex flex-col h-fit">
|
||||||
<div class="p-3 bg-amber-50 text-amber-600 rounded-lg">
|
<div class="px-5 py-4 border-b border-gray-200 bg-gray-50 flex items-center gap-2 shrink-0">
|
||||||
<!-- Heroicon: archive-box -->
|
<svg class="h-4 w-4 text-gray-500" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
||||||
<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>
|
<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>
|
||||||
<div>
|
|
||||||
<p class="text-xs font-semibold uppercase text-gray-400">Aktuell im Lager</p>
|
<div class="overflow-x-auto">
|
||||||
<h3 class="text-2xl font-bold text-amber-600"><%= @items_in_storage %></h3>
|
<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| %>
|
||||||
|
<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 %>
|
||||||
|
<span class="font-bold text-gray-400">Gelöschter Artikel</span>
|
||||||
|
<% end %>
|
||||||
|
<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>
|
</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>
|
</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" />
|
|
||||||
</svg>
|
|
||||||
Letzte Artikel-Zuordnungen & Bewegungen
|
|
||||||
</h2>
|
|
||||||
|
|
||||||
<% if @recent_assignments.any? %>
|
|
||||||
<div class="space-y-3">
|
|
||||||
<% @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 %>
|
|
||||||
<% else %>
|
|
||||||
📦 Ins Hauptlager gelegt
|
|
||||||
<% end %>
|
|
||||||
</p>
|
|
||||||
</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>
|
|
||||||
|
|||||||
@@ -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? %>
|
<%= render "layouts/form_header",
|
||||||
<div class="p-4 mb-4 text-sm text-red-800 rounded-lg bg-red-50 border border-red-200" role="alert">
|
model: @item,
|
||||||
<h2 class="font-bold mb-1"><%= pluralize(item.errors.count, "Fehler") %> verhinderten das Speichern:</h2>
|
new_title: "Artikel anlegen",
|
||||||
<ul class="list-disc list-inside text-xs">
|
new_description: "Erfasse ein neues Gerät mitsamt SKU, Seriennummer und Sticker-ID.",
|
||||||
<% item.errors.each do |error| %>
|
edit_title: "Artikel bearbeiten",
|
||||||
<li><%= error.full_message %></li>
|
edit_description: "Aktualisiere die Gerätedaten, den Anschaffungspreis oder den Zustand." %>
|
||||||
<% 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>
|
|
||||||
|
|
||||||
<hr class="border-gray-200">
|
<hr class="border-gray-200">
|
||||||
|
|
||||||
|
<%= render "layouts/form_errors", model: item %>
|
||||||
|
|
||||||
<!-- Stammdaten (Name und SKU) -->
|
<!-- Stammdaten (Name und SKU) -->
|
||||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||||
<div>
|
<div>
|
||||||
<%= form.label :name, "Artikelname / Modell", class: "block text-sm font-medium mb-1.5 text-gray-700" %>
|
<%= 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" %>
|
<%= 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>
|
|
||||||
|
<div data-controller="scanner" >
|
||||||
<%= form.label :sku, "SKU (Artikelnummer)", class: "block text-sm font-medium mb-1.5 text-gray-700" %>
|
<%= 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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Herstelldaten & Preise -->
|
<!-- Herstelldaten & Preise -->
|
||||||
<div class="grid grid-cols-1 sm:grid-cols-3 gap-4">
|
<div class="grid grid-cols-1 sm:grid-cols-3 gap-4">
|
||||||
<div class="sm:col-span-2">
|
<div class="sm:col-span-2">
|
||||||
<%= 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 data-controller="scanner" >
|
||||||
|
<%= form.label :serial_number, "Seriennummer (Hersteller)", class: "block text-sm font-medium mb-1.5 text-gray-700" %>
|
||||||
|
<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>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<%= form.label :price, "Einkaufspreis (€)", class: "block text-sm font-medium mb-1.5 text-gray-700" %>
|
<%= 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" %>
|
<%= 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" %>
|
||||||
@@ -53,14 +65,35 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- QR STICKER-ID (Mit deinem funktionierenden Scanner) -->
|
<!-- QR STICKER-ID (Mit deinem funktionierenden Scanner) -->
|
||||||
<div data-controller="scanner">
|
<div data-controller="scanner" >
|
||||||
<%= form.label :sticker_id, "Vorgedruckte Sticker-ID / QR-Nummer", class: "block text-sm font-medium mb-1.5 text-gray-700" %>
|
<%= form.label :sticker_id, "Vorgedruckte Sticker-ID / QR-Nummer", class: "block text-sm font-medium mb-1.5 text-gray-700" %>
|
||||||
<div class="relative flex rounded-lg shadow-sm">
|
<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" %>
|
<%= 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">
|
<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>
|
</button>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
|
|
||||||
<hr class="border-gray-200">
|
<hr class="border-gray-200">
|
||||||
@@ -70,16 +103,16 @@
|
|||||||
<!-- ========================================================================= -->
|
<!-- ========================================================================= -->
|
||||||
<div class="space-y-4">
|
<div class="space-y-4">
|
||||||
<label class="block text-sm font-semibold text-gray-800">Standort / Zuweisung festlegen</label>
|
<label class="block text-sm font-semibold text-gray-800">Standort / Zuweisung festlegen</label>
|
||||||
|
|
||||||
<%= turbo_frame_tag "item_assignment_frame" do %>
|
<%= turbo_frame_tag "item_assignment_frame" do %>
|
||||||
<%
|
<%
|
||||||
current_type = if item.user_id.present?
|
current_type = if item.user_id.present?
|
||||||
"user"
|
"user"
|
||||||
elsif item.room_id.present?
|
elsif item.room_id.present?
|
||||||
"room"
|
"room"
|
||||||
else
|
else
|
||||||
"storage"
|
"storage"
|
||||||
end
|
end
|
||||||
type = params[:assignment_type] || current_type
|
type = params[:assignment_type] || current_type
|
||||||
%>
|
%>
|
||||||
|
|
||||||
@@ -89,67 +122,85 @@
|
|||||||
class: "py-2 px-3 text-xs font-medium rounded-md text-center transition flex items-center justify-center gap-1 #{type == 'storage' ? 'bg-white text-blue-600 shadow-sm font-semibold' : 'text-gray-600 hover:text-gray-900'}" do %>
|
class: "py-2 px-3 text-xs font-medium rounded-md text-center transition flex items-center justify-center gap-1 #{type == 'storage' ? 'bg-white text-blue-600 shadow-sm font-semibold' : 'text-gray-600 hover:text-gray-900'}" do %>
|
||||||
📦 Lager
|
📦 Lager
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
<%= link_to item.new_record? ? new_item_path(assignment_type: "user") : edit_item_path(item, assignment_type: "user"),
|
<%= link_to item.new_record? ? new_item_path(assignment_type: "user") : edit_item_path(item, assignment_type: "user"),
|
||||||
class: "py-2 px-3 text-xs font-medium rounded-md text-center transition flex items-center justify-center gap-1 #{type == 'user' ? 'bg-white text-blue-600 shadow-sm font-semibold' : 'text-gray-600 hover:text-gray-900'}" do %>
|
class: "py-2 px-3 text-xs font-medium rounded-md text-center transition flex items-center justify-center gap-1 #{type == 'user' ? 'bg-white text-blue-600 shadow-sm font-semibold' : 'text-gray-600 hover:text-gray-900'}" do %>
|
||||||
👤 Mitarbeiter
|
👤 Mitarbeiter
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
<%= link_to item.new_record? ? new_item_path(assignment_type: "room") : edit_item_path(item, assignment_type: "room"),
|
<%= link_to item.new_record? ? new_item_path(assignment_type: "room") : edit_item_path(item, assignment_type: "room"),
|
||||||
class: "py-2 px-3 text-xs font-medium rounded-md text-center transition flex items-center justify-center gap-1 #{type == 'room' ? 'bg-white text-blue-600 shadow-sm font-semibold' : 'text-gray-600 hover:text-gray-900'}" do %>
|
class: "py-2 px-3 text-xs font-medium rounded-md text-center transition flex items-center justify-center gap-1 #{type == 'room' ? 'bg-white text-blue-600 shadow-sm font-semibold' : 'text-gray-600 hover:text-gray-900'}" do %>
|
||||||
📍 Raum
|
📍 Raum
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Live-Suche (Reiner Text, keine versteckten IDs) -->
|
<!-- Live-Suche (Mobil-optimierter Autocomplete-Ersatz für Datalist) -->
|
||||||
<div class="mt-4">
|
<div class="mt-4">
|
||||||
<% if type == "user" %>
|
<% if type == "user" %>
|
||||||
<!-- Signalisiert Rails, dass die room_id gelöscht werden soll -->
|
|
||||||
<input type="hidden" name="item[room_id]" value="">
|
<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>
|
<label for="item_user_name" class="block text-sm font-medium mb-1.5 text-gray-700">Mitarbeiter suchen (Vorname, Nachname)...</label>
|
||||||
<div class="relative">
|
|
||||||
<!-- Wir senden den Klartext-Namen an ein virtuelles Feld 'user_name' -->
|
<!-- STIMULUS CONTAINER FÜR USER -->
|
||||||
|
<div class="relative" data-controller="autocomplete">
|
||||||
<input type="text"
|
<input type="text"
|
||||||
id="item_user_name"
|
id="item_user_name"
|
||||||
name="item[user_name]"
|
name="item[user_name]"
|
||||||
list="users_datalist"
|
value="<%= item.user&.name %>"
|
||||||
value="<%= item.user&.name %>"
|
data-autocomplete-target="input"
|
||||||
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"
|
data-action="input->autocomplete#filter focus->autocomplete#showAll"
|
||||||
placeholder="Tippe den Namen ein...">
|
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..."
|
||||||
<datalist id="users_datalist">
|
autocomplete="off">
|
||||||
|
|
||||||
|
<!-- 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| %>
|
<% User.all.order(:first_name, :last_name).each do |user| %>
|
||||||
<!-- Hier nutzen wir den reinen Vor- und Nachnamen als Value -->
|
<div data-autocomplete-target="item"
|
||||||
<option value="<%= user.name %>"></option>
|
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 %>
|
<% end %>
|
||||||
</datalist>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<% elsif type == "room" %>
|
<% elsif type == "room" %>
|
||||||
<!-- Signalisiert Rails, dass die user_id gelöscht werden soll -->
|
|
||||||
<input type="hidden" name="item[user_id]" value="">
|
<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>
|
<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"
|
<input type="text"
|
||||||
id="item_room_name"
|
id="item_room_name"
|
||||||
name="item[room_name]"
|
name="item[room_name]"
|
||||||
list="rooms_datalist"
|
value="<%= item.room&.name %>"
|
||||||
value="<%= item.room&.name %>"
|
data-autocomplete-target="input"
|
||||||
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"
|
data-action="input->autocomplete#filter focus->autocomplete#showAll"
|
||||||
placeholder="Tippe die Raumnummer ein...">
|
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..."
|
||||||
<datalist id="rooms_datalist">
|
autocomplete="off">
|
||||||
|
|
||||||
|
<!-- 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| %>
|
<% 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 %>
|
<% end %>
|
||||||
</datalist>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<% else %>
|
<% else %>
|
||||||
<!-- Hauptlager gewählt -> Beide IDs nullen -->
|
|
||||||
<input type="hidden" name="item[user_id]" value="">
|
<input type="hidden" name="item[user_id]" value="">
|
||||||
<input type="hidden" name="item[room_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">
|
<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>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -1,132 +1,137 @@
|
|||||||
<div class="bg-white border border-gray-200 rounded-xl shadow-sm overflow-hidden">
|
<div class="bg-white border border-gray-200 rounded-xl shadow-sm overflow-hidden">
|
||||||
|
<!-- ========================================================================= -->
|
||||||
<!-- ========================================================================= -->
|
<!-- 1. MOBIL-ANSICHT: OPTIMIERT MIT REINEM CODESYMBOL & ZUSTANDS-BADGES -->
|
||||||
<!-- 1. HANDY-ANSICHT: OPTIMIERTE INVENTAR-CARDS (md:hidden) -->
|
<!-- ========================================================================= -->
|
||||||
<!-- ========================================================================= -->
|
<div class="block md:hidden divide-y divide-gray-200 bg-white">
|
||||||
<div class="block md:hidden divide-y divide-gray-200 bg-white">
|
<% items.each do |item| %>
|
||||||
<% items.each do |item| %>
|
<div class="p-4 hover:bg-gray-50/50 transition flex items-center justify-between gap-4">
|
||||||
<div class="p-4 space-y-3 hover:bg-gray-50/50 transition">
|
|
||||||
|
<!-- LINKER BEREICH: TEXTE BÜNDIG ZUM ARTIKELNAMEN FLUCHTEND -->
|
||||||
|
<div class="flex-1 min-w-0 flex items-start gap-3">
|
||||||
|
|
||||||
<!-- Zeile 1: Name, Kategorie & die markante Sticker-ID Plakette -->
|
<!-- Das ID-Badge steht als sauberer, fester Anker ganz links -->
|
||||||
<div class="flex justify-between items-start gap-3">
|
<%= link_to item_path(item), data: { turbo_frame: "_top" }, class: "shrink-0 transition hover:scale-105 active:scale-95 block mt-0.5" do %>
|
||||||
<div class="min-w-0">
|
<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">
|
||||||
<h4 class="font-bold text-gray-900 text-sm leading-snug truncate"><%= item.name %></h4>
|
#<%= item.sticker_id %>
|
||||||
<p class="text-xs text-gray-500 font-medium mt-0.5"><%= item.category.name %></p>
|
</span>
|
||||||
</div>
|
<% end %>
|
||||||
|
|
||||||
|
<!-- TEXT-CONTAINER: ALLES FLUCHTET PERFEKT LINKSBÜNDIG UNTER DEM NAMEN -->
|
||||||
|
<div class="flex-1 min-w-0 space-y-2">
|
||||||
|
|
||||||
<!-- Rechte Box für ID und den optimal platzierten Preis direkt darunter -->
|
<!-- Zeile 1: Fetter Artikelname -->
|
||||||
<div class="flex flex-col items-end gap-1.5 shrink-0">
|
<div class="min-w-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">
|
<%= 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.sticker_id %>
|
<%= item.name %>
|
||||||
</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>
|
|
||||||
|
|
||||||
<!-- 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>
|
|
||||||
|
|
||||||
<!-- 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>
|
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Aktions-Icons -->
|
<!-- Zeile 2: Kategorie, SKU & Seriennummer (SN) als kompakte Kette -->
|
||||||
<div class="flex items-center gap-1 shrink-0">
|
<div class="flex items-center flex-wrap gap-x-2 gap-y-0.5 text-xs font-medium text-gray-500 font-mono">
|
||||||
<%= 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 %>
|
<span class="font-sans font-bold text-gray-400"><%= item.category.name %></span>
|
||||||
<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>
|
<span class="text-gray-300">•</span>
|
||||||
<% end %>
|
<span>SKU: <span class="text-gray-700 font-bold"><%= item.sku.presence || "—" %></span></span>
|
||||||
<%= 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 %>
|
<span class="text-gray-300">•</span>
|
||||||
<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>
|
<span>SN: <span class="text-gray-700 font-bold"><%= item.serial_number.presence || "—" %></span></span>
|
||||||
<% end %>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
<!-- Zeile 3: Das ultimativ schlanke, automatisierte Badge-Duo im Mobil-Layout -->
|
||||||
<% end %>
|
<div class="flex items-center flex-wrap gap-2 pt-0.5">
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- ========================================================================= -->
|
|
||||||
<!-- 2. DESKTOP-ANSICHT: STICKY-TABELLE (hidden md:block) -->
|
|
||||||
<!-- ========================================================================= -->
|
|
||||||
<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>
|
|
||||||
</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) -->
|
<!-- A: Standort-Badge (Label wird manuell übergeben, da dynamischer Personen-/Raumname) -->
|
||||||
<td class="px-6 py-4 whitespace-nowrap">
|
<%= render "layouts/badge", type: item.location_badge_type, label: item.location_badge_label(short_room: true) %>
|
||||||
<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">
|
|
||||||
|
<!-- 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 (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 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">
|
||||||
|
|
||||||
|
<!-- Spalte 1: Verlinktes Sticker-ID-Badge -->
|
||||||
|
<td class="px-6 py-4 whitespace-nowrap">
|
||||||
|
<%= 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 %>
|
#<%= item.sticker_id %>
|
||||||
</span>
|
</span>
|
||||||
</td>
|
<% 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") %>
|
<!-- Spalte 2: Artikelname, Kategorie, SKU & SN direkt darunter im Block -->
|
||||||
</td>
|
<td class="px-6 py-4 max-w-[280px] sm:max-w-[350px]">
|
||||||
<td class="px-6 py-4 whitespace-nowrap text-end font-medium text-xs">
|
<div class="min-w-0 whitespace-normal break-words leading-tight space-y-1">
|
||||||
<div class="flex items-center justify-end gap-2">
|
<%= link_to item_path(item), data: { turbo_frame: "_top" }, class: "font-bold text-gray-900 hover:text-blue-600 hover:underline inline" do %>
|
||||||
<%= 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 %>
|
<%= item.name %>
|
||||||
<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>
|
<% end %>
|
||||||
<% end %>
|
<span class="block text-[10px] text-gray-400 font-medium"><%= item.category.name %></span>
|
||||||
<%= 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>
|
<div class="flex items-center gap-2 text-[10px] font-mono text-gray-500 pt-0.5">
|
||||||
<% end %>
|
<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>
|
</div>
|
||||||
</tr>
|
</td>
|
||||||
<% end %>
|
|
||||||
</tbody>
|
<!-- Spalte 3: Aktueller Standort (Desktop Tabelle - Voller Name übergeben) -->
|
||||||
</table>
|
<td class="px-6 py-4 whitespace-nowrap">
|
||||||
</div>
|
<%= 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>
|
||||||
|
|||||||
@@ -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">
|
<%= form_with(url: form_url, method: :get,
|
||||||
<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">
|
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">
|
<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">
|
||||||
<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>
|
<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>
|
</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>
|
</div>
|
||||||
|
|
||||||
<!-- Aktions-Bereich rechts -->
|
<!-- Aktions-Bereich rechts -->
|
||||||
@@ -16,16 +42,16 @@
|
|||||||
<%= 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 %>
|
<%= 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 -->
|
<!-- 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>
|
<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 %>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
<!-- Universeller Filter-Button -->
|
<!-- Universeller Filter-Button -->
|
||||||
<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">
|
<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 -->
|
<!-- 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>
|
<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>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
<% end %>
|
||||||
|
|||||||
@@ -1,50 +1,26 @@
|
|||||||
<!--<%# content_for :title, "Editing item" %>
|
<% content_for :title, @item.name %>
|
||||||
|
|
||||||
<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}" %>
|
|
||||||
|
|
||||||
<!-- OBERE AKTIONSLISTE (Zurück-Button in der Top-Bar) -->
|
<!-- OBERE AKTIONSLISTE (Zurück-Button in der Top-Bar) -->
|
||||||
<% content_for :top_bar_actions do %>
|
<% 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 -->
|
<!-- 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>
|
<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 %>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
<div class="space-y-6">
|
<div class="space-y-6">
|
||||||
|
|
||||||
<!-- Das zentrale Formular mit dem aktuellen Artikel-Objekt laden -->
|
|
||||||
<%= render "form", item: @item %>
|
<%= render "form", item: @item %>
|
||||||
|
|
||||||
<!-- GEFAHRENBEREICH: Artikel löschen -->
|
<%= render "layouts/danger_zone",
|
||||||
<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">
|
title: "Artikel restlos löschen",
|
||||||
<div class="flex items-start gap-3">
|
description: "Dieser Artikel wird dauerhaft und unwiderruflich aus der Bestandsliste entfernt. Auch die QR-Code-Zuordnung erlischt.",
|
||||||
<div class="p-2 bg-red-100 text-red-700 rounded-lg shrink-0 mt-0.5">
|
button_text: "Artikel löschen",
|
||||||
<!-- Heroicon: trash -->
|
confirm_message: "Möchtest du diesen Artikel wirklich permanent aus dem System entfernen?",
|
||||||
<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>
|
path: item_path(@item) %>
|
||||||
</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>
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -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" %>
|
<% content_for :title, "Gesamtbestand" %>
|
||||||
|
|
||||||
|
<!-- OBERE AKTIONSLISTE -->
|
||||||
<% content_for :top_bar_actions do %>
|
<% 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 %>
|
<%= 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>
|
<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 %>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
<div class="w-full space-y-4">
|
<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 %>
|
<%= render "items/search_bar", show_csv: true %>
|
||||||
|
|
||||||
<!-- 2. Artikelliste laden -->
|
<!-- 2. NUR DIE LISTE WIRD IN DEN TURBO-FRAME GEPAKT -->
|
||||||
<% if @items.any? %>
|
<%= turbo_frame_tag "items_list_frame" do %>
|
||||||
<%= render "items/list", items: @items %>
|
<% if @items.any? %>
|
||||||
<% else %>
|
<%= render "items/list", items: @items %>
|
||||||
<div class="text-center py-16 text-gray-400 bg-white border border-gray-200 rounded-xl shadow-sm">
|
<% else %>
|
||||||
<svg class="mx-auto h-12 w-12 text-gray-300" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
<div class="text-center py-16 text-gray-400 bg-white border border-gray-200 rounded-xl shadow-sm">
|
||||||
<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" />
|
<p class="text-sm font-medium">Keine passenden Inventargegenstände gefunden.</p>
|
||||||
</svg>
|
</div>
|
||||||
<p class="text-sm mt-3 font-medium">Bisher sind keine Inventargegenstände erfasst.</p>
|
<% end %>
|
||||||
<p class="text-xs text-gray-400 mt-1">Klicke oben rechts auf "Artikel hinzufügen", um das erste Gerät einzubuchen.</p>
|
|
||||||
</div>
|
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|||||||
@@ -1,27 +1,17 @@
|
|||||||
<!-- <%# content_for :title, "New item" %>
|
<% content_for :title, "Artikel anlegen" %>
|
||||||
|
|
||||||
<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" %>
|
|
||||||
|
|
||||||
<!-- OBERE LEISTE (Zurück-Button in der Top-Bar via Layout-Yield) -->
|
<!-- OBERE LEISTE (Zurück-Button in der Top-Bar via Layout-Yield) -->
|
||||||
<% content_for :top_bar_actions do %>
|
<% 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 -->
|
<!-- 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>
|
<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 %>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
<div class="p-4 md:p-6">
|
<div class="p-4 md:p-6">
|
||||||
<!-- Lädt das Formular-Partial und übergibt das leere Artikel-Objekt -->
|
|
||||||
<%= render "form", item: @item %>
|
<%= render "form", item: @item %>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -1,32 +1,19 @@
|
|||||||
<!--<%# content_for :title, "Showing item" %>
|
<% content_for :title, "Artikel-Details" %>
|
||||||
|
|
||||||
<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}" %>
|
|
||||||
|
|
||||||
<!-- OBERE AKTIONSLISTE (Yield im Top-Bar deines Hauptlayouts) -->
|
<!-- OBERE AKTIONSLISTE (Yield im Top-Bar deines Hauptlayouts) -->
|
||||||
<% content_for :top_bar_actions do %>
|
<% content_for :top_bar_actions do %>
|
||||||
<div class="flex items-center gap-2">
|
<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>
|
<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
|
<div class="hidden md:inline">
|
||||||
|
Zurück
|
||||||
|
</div>
|
||||||
<% end %>
|
<% 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 %>
|
<%= 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>
|
<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>
|
||||||
Bearbeiten
|
<div class="hidden md:inline">
|
||||||
|
Bearbeiten
|
||||||
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
@@ -41,10 +28,12 @@
|
|||||||
<!-- STAMMDATEN-BOX -->
|
<!-- STAMMDATEN-BOX -->
|
||||||
<div class="bg-white border border-gray-200 rounded-xl shadow-sm p-4 md:p-6 space-y-4">
|
<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">
|
<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>
|
<h2 class="text-lg md:text-xl font-bold text-gray-900 leading-tight"><%= @item.name %>
|
||||||
<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">
|
<span class="block text-[12px] text-gray-400 mt-0.5"><%= @item.category.name %></span>
|
||||||
<%= @item.category.name %>
|
</h2>
|
||||||
</span>
|
<div class="flex items-center gap-2 shrink-0">
|
||||||
|
<%= render "layouts/badge", type: @item.condition_badge_type %>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<hr class="border-gray-200">
|
<hr class="border-gray-200">
|
||||||
@@ -193,11 +182,11 @@
|
|||||||
<div>
|
<div>
|
||||||
<h4 class="font-bold text-gray-800">
|
<h4 class="font-bold text-gray-800">
|
||||||
<% if log.user.present? %>
|
<% 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? %>
|
<% 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 %>
|
<% else %>
|
||||||
Ins Hauptlager übergeben
|
Hauptlager
|
||||||
<% end %>
|
<% end %>
|
||||||
</h4>
|
</h4>
|
||||||
|
|
||||||
@@ -212,11 +201,6 @@
|
|||||||
<div class="text-right text-xs whitespace-nowrap text-gray-400 pt-0.5 shrink-0 font-medium">
|
<div class="text-right text-xs whitespace-nowrap text-gray-400 pt-0.5 shrink-0 font-medium">
|
||||||
<time class="text-gray-600">
|
<time class="text-gray-600">
|
||||||
<%= l(log.assigned_at, format: "%d. %b %Y") %>
|
<%= 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>
|
</time>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
81
app/views/layouts/_badge.html.erb
Normal file
81
app/views/layouts/_badge.html.erb
Normal 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>
|
||||||
20
app/views/layouts/_danger_zone.html.erb
Normal file
20
app/views/layouts/_danger_zone.html.erb
Normal 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>
|
||||||
@@ -6,7 +6,7 @@
|
|||||||
bar_color = type == "notice" ? "bg-green-500" : "bg-red-500"
|
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 -->
|
<!-- Das relative Attribut und overflow-hidden sind wichtig für den Balken -->
|
||||||
<div data-flash-target="notification"
|
<div data-flash-target="notification"
|
||||||
|
|||||||
18
app/views/layouts/_form_errors.html.erb
Normal file
18
app/views/layouts/_form_errors.html.erb
Normal 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 %>
|
||||||
26
app/views/layouts/_form_header.html.erb
Normal file
26
app/views/layouts/_form_header.html.erb
Normal 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>
|
||||||
17
app/views/layouts/_scanner.html.erb
Normal file
17
app/views/layouts/_scanner.html.erb
Normal 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>
|
||||||
46
app/views/layouts/_sidebar.html.erb
Normal file
46
app/views/layouts/_sidebar.html.erb
Normal 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>
|
||||||
@@ -4,7 +4,7 @@
|
|||||||
<title><%= content_for(:title) || "Vault171" %></title>
|
<title><%= content_for(:title) || "Vault171" %></title>
|
||||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
<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">
|
<meta name="mobile-web-app-capable" content="yes">
|
||||||
<%= csrf_meta_tags %>
|
<%= csrf_meta_tags %>
|
||||||
<%= csp_meta_tag %>
|
<%= csp_meta_tag %>
|
||||||
@@ -60,27 +60,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Navigations-Links mit dynamischen Helper-Klassen -->
|
<!-- Navigations-Links mit dynamischen Helper-Klassen -->
|
||||||
<nav class="flex-1 p-3 space-y-1 overflow-hidden">
|
<%= render "layouts/sidebar" %>
|
||||||
<%= 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>
|
|
||||||
|
|
||||||
<!-- DER NEUE, AUFGERÄUMTE SIDEBAR-FOOTER -->
|
<!-- 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">
|
<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
|
Ausloggen
|
||||||
</span>
|
</span>
|
||||||
<% end %>
|
<% 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>
|
</div>
|
||||||
|
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
<!-- HAUPTBEREICH (Inhaltsfläche rückt bei verkleinerter Sidebar nach) -->
|
<!-- HAUPTBEREICH (Fixiert die Gesamthöhe auf den Bildschirm und steuert das Scrollen intern) -->
|
||||||
<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">
|
<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) -->
|
<!-- 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">
|
<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">
|
<div class="flex items-center gap-3">
|
||||||
<!-- BURGER BUTTON (Nur mobil sichtbar) -->
|
<!-- 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">
|
<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>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<!-- INHALT DER JEWEILIGEN VIEW -->
|
<%= render "layouts/flash" %>
|
||||||
<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 %>
|
<%= yield %>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
41
app/views/rooms/_form.html.erb
Normal file
41
app/views/rooms/_form.html.erb
Normal 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 %>
|
||||||
2
app/views/rooms/_room.html.erb
Normal file
2
app/views/rooms/_room.html.erb
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
<div id="<%= dom_id room %>" class="w-full sm:w-auto my-5 space-y-5">
|
||||||
|
</div>
|
||||||
2
app/views/rooms/_room.json.jbuilder
Normal file
2
app/views/rooms/_room.json.jbuilder
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
json.extract! room, :id, :created_at, :updated_at
|
||||||
|
json.url room_url(room, format: :json)
|
||||||
21
app/views/rooms/edit.html.erb
Normal file
21
app/views/rooms/edit.html.erb
Normal 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>
|
||||||
140
app/views/rooms/index.html.erb
Normal file
140
app/views/rooms/index.html.erb
Normal 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>
|
||||||
1
app/views/rooms/index.json.jbuilder
Normal file
1
app/views/rooms/index.json.jbuilder
Normal file
@@ -0,0 +1 @@
|
|||||||
|
json.array! @rooms, partial: "rooms/room", as: :room
|
||||||
14
app/views/rooms/new.html.erb
Normal file
14
app/views/rooms/new.html.erb
Normal 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>
|
||||||
40
app/views/rooms/show.html.erb
Normal file
40
app/views/rooms/show.html.erb
Normal 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>
|
||||||
1
app/views/rooms/show.json.jbuilder
Normal file
1
app/views/rooms/show.json.jbuilder
Normal file
@@ -0,0 +1 @@
|
|||||||
|
json.partial! "rooms/room", room: @room
|
||||||
@@ -16,6 +16,15 @@ module Vault171
|
|||||||
# Common ones are `templates`, `generators`, or `middleware`, for example.
|
# Common ones are `templates`, `generators`, or `middleware`, for example.
|
||||||
config.autoload_lib(ignore: %w[assets tasks])
|
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.
|
# Configuration for the application, engines, and railties goes here.
|
||||||
#
|
#
|
||||||
# These settings can be overridden in specific environments using the files
|
# These settings can be overridden in specific environments using the files
|
||||||
|
|||||||
@@ -50,6 +50,10 @@ env:
|
|||||||
# When you start using multiple servers, you should split out job processing to a dedicated machine.
|
# When you start using multiple servers, you should split out job processing to a dedicated machine.
|
||||||
SOLID_QUEUE_IN_PUMA: true
|
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)
|
# Set number of processes dedicated to Solid Queue (default: 1)
|
||||||
# JOB_CONCURRENCY: 3
|
# JOB_CONCURRENCY: 3
|
||||||
|
|
||||||
|
|||||||
@@ -5,3 +5,4 @@ pin "@hotwired/turbo-rails", to: "turbo.min.js"
|
|||||||
pin "@hotwired/stimulus", to: "stimulus.min.js"
|
pin "@hotwired/stimulus", to: "stimulus.min.js"
|
||||||
pin "@hotwired/stimulus-loading", to: "stimulus-loading.js"
|
pin "@hotwired/stimulus-loading", to: "stimulus-loading.js"
|
||||||
pin_all_from "app/javascript/controllers", under: "controllers"
|
pin_all_from "app/javascript/controllers", under: "controllers"
|
||||||
|
pin "qr-scanner", to: "qr-scanner.js"
|
||||||
|
|||||||
9
config/initializers/git_revision.rb
Normal file
9
config/initializers/git_revision.rb
Normal 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
119
config/locales/de.yml
Normal 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"
|
||||||
@@ -28,4 +28,14 @@
|
|||||||
# enabled: "ON"
|
# enabled: "ON"
|
||||||
|
|
||||||
en:
|
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"
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
Rails.application.routes.draw do
|
Rails.application.routes.draw do
|
||||||
|
resources :rooms
|
||||||
resources :items
|
resources :items
|
||||||
resources :categories
|
resources :categories
|
||||||
namespace :authentications do
|
namespace :authentications do
|
||||||
|
|||||||
5
db/migrate/20260528181506_add_condition_to_items.rb
Normal file
5
db/migrate/20260528181506_add_condition_to_items.rb
Normal 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
3
db/schema.rb
generated
@@ -10,7 +10,7 @@
|
|||||||
#
|
#
|
||||||
# It's strongly recommended that you check this file into your version control system.
|
# 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|
|
create_table "assignment_logs", force: :cascade do |t|
|
||||||
t.datetime "assigned_at"
|
t.datetime "assigned_at"
|
||||||
t.datetime "created_at", null: false
|
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|
|
create_table "items", force: :cascade do |t|
|
||||||
t.integer "category_id"
|
t.integer "category_id"
|
||||||
|
t.string "condition", default: "unknown", null: false
|
||||||
t.datetime "created_at", null: false
|
t.datetime "created_at", null: false
|
||||||
t.string "name"
|
t.string "name"
|
||||||
t.text "notes"
|
t.text "notes"
|
||||||
|
|||||||
48
test/controllers/rooms_controller_test.rb
Normal file
48
test/controllers/rooms_controller_test.rb
Normal 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
|
||||||
15
test/fixtures/assignment_log2s.yml
vendored
15
test/fixtures/assignment_log2s.yml
vendored
@@ -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
|
|
||||||
@@ -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
57
vendor/javascript/qr-scanner.js
vendored
Normal file
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user