Initialize clean CSR inventory repository
This commit is contained in:
1
inventory/templates/403.html
Normal file
1
inventory/templates/403.html
Normal file
@@ -0,0 +1 @@
|
||||
{% extends 'inventory/base.html' %}{% block title %}Keine Berechtigung{% endblock %}{% block content %}<section class="narrow-page"><div class="form-card"><h1>Keine Berechtigung</h1><p>Ihr Konto darf diese Aktion nicht ausführen. Wenden Sie sich bei Bedarf an einen Administrator.</p><a class="btn primary" href="{% url 'inventory:device-list' %}">Zur Geräteliste</a></div></section>{% endblock %}
|
||||
1
inventory/templates/404.html
Normal file
1
inventory/templates/404.html
Normal file
@@ -0,0 +1 @@
|
||||
{% extends 'inventory/base.html' %}{% block title %}Nicht gefunden{% endblock %}{% block content %}<section class="narrow-page"><div class="form-card"><h1>Seite nicht gefunden</h1><p>Die angeforderte Seite oder das Gerät existiert nicht mehr.</p><a class="btn primary" href="{% url 'inventory:device-list' %}">Zur Geräteliste</a></div></section>{% endblock %}
|
||||
13
inventory/templates/500.html
Normal file
13
inventory/templates/500.html
Normal file
@@ -0,0 +1,13 @@
|
||||
{% load static %}
|
||||
<!doctype html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title>Technischer Fehler · CS-R</title>
|
||||
<link rel="icon" type="image/png" href="{% static 'inventory/img/csr-logo.png' %}">
|
||||
<script src="{% static 'inventory/js/theme.js' %}?v=20260928-3"></script>
|
||||
<style>body{font:16px system-ui;background:#f6f8fc;color:#111827;display:grid;place-items:center;min-height:100vh;margin:0}main{background:white;border:1px solid #e5e7eb;border-radius:10px;padding:28px;max-width:500px}a{display:inline-block;background:#0066ff;color:white;text-decoration:none;padding:9px 14px;border-radius:7px}.theme-toggle{display:block;margin-top:20px;padding:7px 10px;border:1px solid #cbd5e1;border-radius:7px;background:transparent;color:inherit;cursor:pointer}html[data-theme=dark] body{background:#101827;color:#e9eef7}html[data-theme=dark] main{background:#1a2637;border-color:#344154}html[data-theme=dark] .theme-toggle{border-color:#56677f}</style>
|
||||
</head>
|
||||
<body><main><h1>Das hat nicht funktioniert</h1><p>Es ist ein technischer Fehler aufgetreten. Bitte versuchen Sie es erneut. Falls der Fehler bleibt, informieren Sie einen Administrator.</p><a href="/">Erneut versuchen</a><button class="theme-toggle" type="button" data-theme-toggle aria-pressed="false"><span data-theme-icon aria-hidden="true">☾</span> <span data-theme-label>Dunkelmodus</span></button></main></body>
|
||||
</html>
|
||||
61
inventory/templates/inventory/_device_table.html
Normal file
61
inventory/templates/inventory/_device_table.html
Normal file
@@ -0,0 +1,61 @@
|
||||
{% load inventory_tags %}
|
||||
<div class="table-shell" data-table-total="{{ total_count }}">
|
||||
<table class="device-table" data-device-table>
|
||||
<thead><tr>
|
||||
{% if can_edit_devices %}<th class="selection-column"><input type="checkbox" data-select-all aria-label="Alle sichtbaren Geräte auswählen" title="Alle sichtbaren auswählen"></th>{% endif %}
|
||||
<th data-column="number"><a href="?{% sort_query 'number' %}">Nr.</a></th>
|
||||
<th data-column="shelf"><a href="?{% sort_query 'shelf' %}">Regal</a></th>
|
||||
<th data-column="stage"><a href="?{% sort_query 'stage' %}">Arbeitsschritt</a></th>
|
||||
<th data-column="status"><a href="?{% sort_query 'status' %}">Status</a></th>
|
||||
<th data-column="date"><a href="?{% sort_query 'date' %}">Datum</a></th>
|
||||
<th data-column="recipient"><a href="?{% sort_query 'recipient' %}">Empfänger</a></th>
|
||||
<th data-column="location">Ort</th>
|
||||
<th data-column="ticket"><a href="?{% sort_query 'ticket' %}">Ticket</a></th>
|
||||
<th data-column="device_type"><a href="?{% sort_query 'device_type' %}">Typ</a></th>
|
||||
<th data-column="price"><a href="?{% sort_query 'price' %}">Preis</a></th>
|
||||
{% if "battery" in visible_hardware_fields %} <th data-column="battery">Akku</th>{% endif %}
|
||||
{% if "wifi" in visible_hardware_fields %} <th data-column="wifi">WLAN</th>{% endif %}
|
||||
{% if "lan" in visible_hardware_fields %} <th data-column="lan">LAN</th>{% endif %}
|
||||
{% if "display_inches" in visible_hardware_fields %} <th data-column="display_inches"><a href="?{% sort_query 'display_inches' %}">Zoll</a></th>{% endif %}
|
||||
{% if "ram_gb" in visible_hardware_fields %} <th data-column="ram_gb"><a href="?{% sort_query 'ram_gb' %}">RAM</a></th>{% endif %}
|
||||
{% if "ssd_gb" in visible_hardware_fields %} <th data-column="ssd_gb"><a href="?{% sort_query 'ssd_gb' %}">SSD</a></th>{% endif %}
|
||||
{% if "hdd_gb" in visible_hardware_fields %} <th data-column="hdd_gb"><a href="?{% sort_query 'hdd_gb' %}">HDD</a></th>{% endif %}
|
||||
{% if "cpu" in visible_hardware_fields %} <th data-column="cpu"><a href="?{% sort_query 'cpu' %}">CPU</a></th>{% endif %}
|
||||
<th data-column="model"><a href="?{% sort_query 'model' %}">Modell</a></th>
|
||||
<th data-column="employee"><a href="?{% sort_query 'employee' %}">Bearbeiter</a></th>
|
||||
<th data-column="donor">Spender</th>
|
||||
<th data-column="comment">Kommentar</th>
|
||||
{% for field in custom_columns %}<th data-column="custom_{{ field.key }}">{{ field.name }}</th>{% endfor %}
|
||||
</tr></thead>
|
||||
<tbody>
|
||||
{% for device in page_obj %}
|
||||
<tr data-device-id="{{ device.pk }}" data-revision="{{ device.revision }}" data-updated-at="{{ device.updated_at.isoformat }}" data-detail-url="{% url 'inventory:device-detail' device.pk %}{% if navigation_query %}?{{ navigation_query }}{% endif %}">
|
||||
{% if can_edit_devices %}<td class="selection-column"><input type="checkbox" value="{{ device.pk }}" data-device-select aria-label="Gerät {{ device.number }} auswählen"></td>{% endif %}
|
||||
<td data-column="number" class="mono strong"><a class="device-link" href="{% url 'inventory:device-detail' device.pk %}{% if navigation_query %}?{{ navigation_query }}{% endif %}">{{ device.number }}</a></td>
|
||||
<td data-column="shelf" data-field="shelf" data-value="{{ device.shelf }}">{{ device.shelf|default:'—' }}</td>
|
||||
<td data-column="stage">{{ device.get_stage_display }}</td>
|
||||
<td data-column="status" data-field="status" data-input="select" data-value="{{ device.status_id }}"><span class="status status-colored" style="--status-color:{{ device.status.color|default:'#64748b' }}">{{ device.status.label }}</span></td>
|
||||
<td data-column="date" data-field="date" data-input="date" data-value="{{ device.date|date:'Y-m-d' }}">{{ device.date|date:'d.m.Y' }}</td>
|
||||
<td data-column="recipient" data-field="recipient" data-input="select" data-value="{{ device.recipient_id|default:'' }}">{{ device.recipient.label|default:'—' }}</td>
|
||||
<td data-column="location" data-field="location" data-input="select" data-value="{{ device.location_id|default:'' }}">{{ device.location.label|default:'—' }}</td>
|
||||
<td data-column="ticket" data-field="ticket" data-value="{{ device.ticket }}">{{ device.ticket|default:'—' }}</td>
|
||||
<td data-column="device_type" data-field="device_type" data-input="select" data-value="{{ device.device_type_id|default:'' }}">{{ device.device_type.label|default:'—' }}</td>
|
||||
<td data-column="price" data-field="price" data-input="number" data-step="0.01" data-value="{{ device.price|default:'' }}">{% if device.price != None %}{{ device.price|floatformat:2 }} €{% else %}—{% endif %}</td>
|
||||
{% if "battery" in visible_hardware_fields %} {% if "battery" in device.hardware_fields %}<td data-column="battery" data-field="battery" data-input="tristate" data-value="{{ device.battery }}">{{ device.get_battery_display }}</td>{% else %}<td data-column="battery">—</td>{% endif %}{% endif %}
|
||||
{% if "wifi" in visible_hardware_fields %} {% if "wifi" in device.hardware_fields %}<td data-column="wifi" data-field="wifi" data-input="tristate" data-value="{{ device.wifi }}">{{ device.get_wifi_display }}</td>{% else %}<td data-column="wifi">—</td>{% endif %}{% endif %}
|
||||
{% if "lan" in visible_hardware_fields %} {% if "lan" in device.hardware_fields %}<td data-column="lan" data-field="lan" data-input="tristate" data-value="{{ device.lan }}">{{ device.get_lan_display }}</td>{% else %}<td data-column="lan">—</td>{% endif %}{% endif %}
|
||||
{% if "display_inches" in visible_hardware_fields %} {% if "display_inches" in device.hardware_fields %}<td data-column="display_inches" data-field="display_inches" data-input="number" data-step="0.1" data-value="{{ device.display_inches|default:'' }}">{% if device.display_inches != None %}{{ device.display_inches|floatformat:-1 }} Zoll{% else %}—{% endif %}</td>{% else %}<td data-column="display_inches">—</td>{% endif %}{% endif %}
|
||||
{% if "ram_gb" in visible_hardware_fields %} {% if "ram_gb" in device.hardware_fields %}<td data-column="ram_gb" data-field="ram_gb" data-input="number" data-value="{{ device.ram_gb|default:'' }}">{% if device.ram_gb %}{{ device.ram_gb }} GB{% else %}—{% endif %}</td>{% else %}<td data-column="ram_gb">—</td>{% endif %}{% endif %}
|
||||
{% if "ssd_gb" in visible_hardware_fields %} {% if "ssd_gb" in device.hardware_fields %}<td data-column="ssd_gb" data-field="ssd_gb" data-input="number" data-value="{{ device.ssd_gb|default:'' }}">{% if device.ssd_gb != None %}{{ device.ssd_gb }} GB{% else %}—{% endif %}</td>{% else %}<td data-column="ssd_gb">—</td>{% endif %}{% endif %}
|
||||
{% if "hdd_gb" in visible_hardware_fields %} {% if "hdd_gb" in device.hardware_fields %}<td data-column="hdd_gb" data-field="hdd_gb" data-input="number" data-value="{{ device.hdd_gb|default:'' }}">{% if device.hdd_gb != None %}{{ device.hdd_gb }} GB{% else %}—{% endif %}</td>{% else %}<td data-column="hdd_gb">—</td>{% endif %}{% endif %}
|
||||
{% if "cpu" in visible_hardware_fields %} {% if "cpu" in device.hardware_fields %}<td data-column="cpu" data-field="cpu" data-value="{{ device.cpu }}">{{ device.cpu|default:'—' }}</td>{% else %}<td data-column="cpu">—</td>{% endif %}{% endif %}
|
||||
<td data-column="model" data-field="model" data-value="{{ device.model }}">{{ device.model|default:'—' }}</td>
|
||||
<td data-column="employee" data-field="employee" data-input="select" data-value="{{ device.employee_id|default:'' }}">{{ device.employee.code|default:'—' }}</td>
|
||||
<td data-column="donor" data-field="donor" data-value="{{ device.donor }}">{{ device.donor|default:'—' }}</td>
|
||||
<td data-column="comment" data-field="comment" data-value="{{ device.comment }}">{{ device.comment|default:'—' }}</td>
|
||||
{% for field in custom_columns %}<td data-column="custom_{{ field.key }}">{{ device.custom_values|get_item:field.key|display_custom|default:'—' }}</td>{% endfor %}
|
||||
</tr>
|
||||
{% empty %}<tr><td colspan="30" class="empty"><strong>Keine Geräte gefunden</strong><span>Filter anpassen oder ein neues Gerät anlegen.</span></td></tr>{% endfor %}
|
||||
</tbody>
|
||||
</table></div>
|
||||
<div class="pagination"><span>Seite {{ page_obj.number }} von {{ page_obj.paginator.num_pages }}</span><div>{% if page_obj.has_previous %}<a class="btn" href="?page={{ page_obj.previous_page_number }}&{{ query_string }}">← Zurück</a>{% endif %}{% if page_obj.has_next %}<a class="btn" href="?page={{ page_obj.next_page_number }}&{{ query_string }}">Weiter →</a>{% endif %}</div></div>
|
||||
1
inventory/templates/inventory/archive_list.html
Normal file
1
inventory/templates/inventory/archive_list.html
Normal file
@@ -0,0 +1 @@
|
||||
{% extends 'inventory/base.html' %}{% block title %}Archiv{% endblock %}{% block content %}<section class="page-head"><div><h1>Archiv</h1><p>{{ total_count }} archivierte Geräte</p></div></section><div class="table-shell"><table class="device-table"><thead><tr><th>Nr.</th><th>Status</th><th>Typ</th><th>Modell</th><th>Archiviert</th><th></th></tr></thead><tbody>{% for device in page_obj %}<tr><td class="mono strong">{{ device.number }}</td><td><span class="status status-colored" style="--status-color:{{ device.status.color|default:'#64748b' }}">{{ device.status }}</span></td><td>{{ device.device_type|default:'—' }}</td><td>{{ device.model|default:'—' }}</td><td>{{ device.archived_at|date:'d.m.Y H:i' }}</td><td><form method="post" action="{% url 'inventory:restore-device' device.pk %}">{% csrf_token %}<button class="btn small">Wiederherstellen</button></form></td></tr>{% empty %}<tr><td colspan="6" class="empty">Das Archiv ist leer.</td></tr>{% endfor %}</tbody></table></div>{% endblock %}
|
||||
5
inventory/templates/inventory/audit_list.html
Normal file
5
inventory/templates/inventory/audit_list.html
Normal file
@@ -0,0 +1,5 @@
|
||||
{% extends 'inventory/base.html' %}{% block title %}Änderungshistorie{% endblock %}{% block content %}
|
||||
<section class="page-head"><div><h1>Änderungshistorie</h1><p>Nachvollziehbare Änderungen an allen Geräten</p></div></section>
|
||||
<form class="toolbar compact" method="get"><div class="search"><span>⌕</span><input name="q" value="{{ request.GET.q }}" placeholder="Änderungen durchsuchen …"></div></form>
|
||||
<div class="history-list">{% for log in page_obj %}<article><div class="history-icon">{% if log.action == 'CREATE' %}+{% elif log.action == 'ARCHIVE' %}□{% elif log.action == 'RESTORE' %}↺{% else %}↻{% endif %}</div><div class="history-content"><header><strong>Gerät #{{ log.device.number }}</strong><span>{{ log.created_at|date:'d.m.Y H:i' }}</span></header><p><b>{{ log.field|default:log.get_action_display }}</b>{% if log.old_value or log.new_value %}: <del>{{ log.old_value|default:'leer' }}</del> → <ins>{{ log.new_value|default:'leer' }}</ins>{% endif %}</p><footer><small>{{ log.user.get_full_name|default:log.user.username|default:'System' }}{% if log.api_client %} · API: {{ log.api_client }}{% endif %}</small>{% if log.reverted_at %}<span class="reverted-badge">Zurückgesetzt am {{ log.reverted_at|date:'d.m.Y H:i' }}</span>{% elif can_revert_audits and log.action == 'UPDATE' %}<form method="post" action="{% url 'inventory:audit-revert' log.pk %}" data-confirm="Soll diese einzelne Änderung wirklich auf den vorherigen Wert zurückgesetzt werden?" data-confirm-action="Änderung zurücksetzen">{% csrf_token %}<button class="btn small">↶ Zurücksetzen</button></form>{% endif %}</footer></div></article>{% empty %}<div class="empty">Noch keine Änderungen protokolliert.</div>{% endfor %}</div>
|
||||
{% endblock %}
|
||||
49
inventory/templates/inventory/base.html
Normal file
49
inventory/templates/inventory/base.html
Normal file
@@ -0,0 +1,49 @@
|
||||
{% load static %}
|
||||
<!doctype html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="description" content="Interne CSR Geräteverwaltung">
|
||||
<title>{% block title %}Geräteverwaltung{% endblock %} · CS-R</title>
|
||||
<link rel="icon" type="image/png" href="{% static 'inventory/img/csr-logo.png' %}">
|
||||
<script src="{% static 'inventory/js/theme.js' %}?v=20260928-3"></script>
|
||||
<link rel="stylesheet" href="{% static 'inventory/css/app.css' %}?v=20260928-3">
|
||||
<script defer src="{% static 'inventory/js/app.js' %}?v=20260928-2"></script>
|
||||
</head>
|
||||
<body class="{% block body_class %}{% endblock %}" data-csrf="{{ csrf_token }}" data-user-id="{{ request.user.pk|default:'' }}">
|
||||
<aside class="sidebar" id="sidebar">
|
||||
<a class="brand" href="{% url 'inventory:overview' %}">
|
||||
<img src="{% static 'inventory/img/csr-logo.png' %}" alt="CS-R">
|
||||
<span><strong>CS-R</strong><small>Geräteverwaltung</small></span>
|
||||
</a>
|
||||
<nav aria-label="Hauptnavigation">
|
||||
<a href="{% url 'inventory:overview' %}" class="{% if request.resolver_match.url_name == 'overview' %}active{% endif %}"><span>◫</span> Übersicht</a>
|
||||
<a href="{% url 'inventory:device-list' %}" class="{% if request.resolver_match.url_name == 'device-list' %}active{% endif %}"><span>▦</span> Geräte</a>
|
||||
<a href="{% url 'inventory:workflow-board' %}" class="{% if request.resolver_match.url_name == 'workflow-board' %}active{% endif %}"><span>?</span> Ablauf</a>
|
||||
<a href="{% url 'inventory:task-list' %}" class="{% if request.resolver_match.url_name == 'task-list' %}active{% endif %}"><span>☑</span> Aufgaben</a>
|
||||
{% if current_role != 'VIEWER' %}<a href="{% url 'inventory:quick-add' %}" class="{% if request.resolver_match.url_name == 'quick-add' %}active{% endif %}"><span>+</span> Neues Gerät</a>{% endif %}
|
||||
<a href="{% url 'inventory:audit-list' %}" class="{% if request.resolver_match.url_name == 'audit-list' %}active{% endif %}"><span>◷</span> Änderungshistorie</a>
|
||||
{% if current_role == 'ADMIN' %}
|
||||
<div class="nav-label">Verwaltung</div>
|
||||
<a href="{% url 'inventory:configuration' %}" class="{% if 'configuration' in request.path %}active{% endif %}"><span>⚙</span> Werte & Felder</a>
|
||||
<a href="{% url 'inventory:import-excel' %}" class="{% if 'import' in request.path %}active{% endif %}"><span>⇅</span> Import / Export</a>
|
||||
<a href="{% url 'inventory:archive-list' %}" class="{% if request.resolver_match.url_name == 'archive-list' %}active{% endif %}"><span>□</span> Archiv</a>
|
||||
<a href="{% url 'inventory:user-list' %}" class="{% if 'users' in request.path %}active{% endif %}"><span>♙</span> Benutzer</a>
|
||||
{% endif %}
|
||||
</nav>
|
||||
<button class="theme-toggle sidebar-theme" type="button" data-theme-toggle aria-pressed="false"><span data-theme-icon aria-hidden="true">☾</span><span data-theme-label>Dunkelmodus</span></button>
|
||||
<div class="sidebar-user">
|
||||
<span class="avatar">{{ request.user.username|slice:':1'|upper }}</span>
|
||||
<span><strong>{{ request.user.get_full_name|default:request.user.username }}</strong><small>{{ current_role_label }}</small></span>
|
||||
<form action="{% url 'logout' %}" method="post">{% csrf_token %}<button title="Abmelden" aria-label="Abmelden">↪</button></form>
|
||||
</div>
|
||||
</aside>
|
||||
<main class="main">
|
||||
<header class="mobile-header"><button data-sidebar-toggle aria-label="Navigation öffnen">☰</button><strong>CS-R Geräte</strong><button class="theme-toggle mobile-theme" type="button" data-theme-toggle aria-pressed="false"><span data-theme-icon aria-hidden="true">☾</span><span class="sr-only" data-theme-label>Dunkelmodus</span></button></header>
|
||||
{% if messages %}<div class="toast-stack" aria-live="polite">{% for message in messages %}<div class="toast {{ message.tags }}">{{ message }}</div>{% endfor %}</div>{% endif %}
|
||||
{% block content %}{% endblock %}
|
||||
</main>
|
||||
<div id="toast-root" class="toast-stack" aria-live="polite"></div>
|
||||
</body>
|
||||
</html>
|
||||
21
inventory/templates/inventory/configuration.html
Normal file
21
inventory/templates/inventory/configuration.html
Normal file
@@ -0,0 +1,21 @@
|
||||
{% extends 'inventory/base.html' %}
|
||||
{% block title %}Werte & Felder{% endblock %}
|
||||
{% block content %}
|
||||
<section class="page-head"><div><h1>Werte & Felder</h1><p>Bearbeiter, Auswahllisten und Zusatzfelder verwalten</p></div></section>
|
||||
<div class="config-grid config-grid-three">
|
||||
<section class="panel">
|
||||
<header><div><h2>Bearbeiter</h2><p>Namen und Kürzel für die Geräteerfassung.</p></div><a class="btn primary small" href="{% url 'inventory:employee-new' %}">+ Bearbeiter</a></header>
|
||||
<div class="config-list employee-list">
|
||||
{% for employee in employees %}<div class="config-row"><a href="{% url 'inventory:employee-edit' employee.pk %}" class="config-item-main"><span><strong>{{ employee.name }}</strong><small>{{ employee.code }}</small></span><em class="{% if employee.active %}active{% endif %}">{% if employee.active %}Aktiv{% else %}Inaktiv{% endif %}</em></a><form method="post" action="{% url 'inventory:employee-remove' employee.pk %}" data-confirm="Bearbeiter {{ employee.name }} wirklich aus der Auswahl entfernen?" data-confirm-action="Entfernen">{% csrf_token %}<button class="btn small danger" aria-label="{{ employee.name }} entfernen">Entfernen</button></form></div>{% empty %}<p class="empty">Noch keine Bearbeiter vorhanden.</p>{% endfor %}
|
||||
</div>
|
||||
</section>
|
||||
<section class="panel">
|
||||
<header><div><h2>Auswahlwerte</h2><p>Deaktivieren statt löschen – bestehende Verknüpfungen bleiben erhalten.</p></div><a class="btn primary small" href="{% url 'inventory:choice-new' %}">+ Wert</a></header>
|
||||
<div class="config-list">{% regroup choices by get_kind_display as grouped %}{% for group in grouped %}<h3>{{ group.grouper }}</h3>{% for item in group.list %}<a href="{% url 'inventory:choice-edit' item.pk %}"><span class="config-value-label">{% if item.kind == 'status' %}<i class="status-color-dot" style="--status-color:{{ item.color|default:'#64748b' }}"></i>{% endif %}{{ item.label }}{% if item.numeric_value != None %} · {{ item.numeric_value }}{% endif %}</span><em class="{% if item.active %}active{% endif %}">{% if item.active %}Aktiv{% else %}Inaktiv{% endif %}</em></a>{% endfor %}{% endfor %}</div>
|
||||
</section>
|
||||
<section class="panel">
|
||||
<header><div><h2>Benutzerdefinierte Felder</h2><p>Neue Felder ohne Datenbankmigration hinzufügen.</p></div><a class="btn primary small" href="{% url 'inventory:field-new' %}">+ Feld</a></header>
|
||||
<div class="config-list">{% for field in definitions %}<a href="{% url 'inventory:field-edit' field.pk %}"><span><strong>{{ field.name }}</strong><small>{{ field.get_field_type_display }} · {{ field.key }}</small></span><em class="{% if field.active %}active{% endif %}">{% if field.active %}Aktiv{% else %}Inaktiv{% endif %}</em></a>{% empty %}<p class="empty">Noch keine Zusatzfelder.</p>{% endfor %}</div>
|
||||
</section>
|
||||
</div>
|
||||
{% endblock %}
|
||||
37
inventory/templates/inventory/device_detail.html
Normal file
37
inventory/templates/inventory/device_detail.html
Normal file
@@ -0,0 +1,37 @@
|
||||
{% extends 'inventory/base.html' %}{% load inventory_tags %}{% block title %}Gerät #{{ device.number }}{% endblock %}{% block content %}
|
||||
<section class="narrow-page wide-form"><div class="page-head device-detail-head"><div><a class="back" href="{{ list_url }}">← Zur Geräteliste</a><h1>Gerät #{{ device.number }}</h1><p>Zuletzt geändert {{ device.updated_at|date:'d.m.Y H:i' }}</p></div><nav class="record-navigation" aria-label="Zwischen Geräten wechseln"><a class="btn icon{% if not previous_device_id %} disabled{% endif %}" {% if previous_device_id %}href="{% url 'inventory:device-detail' previous_device_id %}{% if navigation_query %}?{{ navigation_query }}{% endif %}"{% else %}aria-disabled="true"{% endif %} title="Vorheriges Gerät" aria-label="Vorheriges Gerät">←</a><span>{% if navigation_position %}{{ navigation_position }} / {{ navigation_total }}{% endif %}</span><a class="btn icon{% if not next_device_id %} disabled{% endif %}" {% if next_device_id %}href="{% url 'inventory:device-detail' next_device_id %}{% if navigation_query %}?{{ navigation_query }}{% endif %}"{% else %}aria-disabled="true"{% endif %} title="Nächstes Gerät" aria-label="Nächstes Gerät">→</a></nav></div>
|
||||
<div class="device-workbench">
|
||||
<section><h2>Ablauf: {{ device.get_stage_display }}</h2>
|
||||
<p>Eingang → Inspektion → bei Bedarf Reparatur → Lagerung → Ausgabe</p>
|
||||
{% if can_edit_devices and not device.archived_at and next_stages %}
|
||||
<form method="post" action="{% url 'inventory:device-advance' device.pk %}" class="app-form">
|
||||
{% csrf_token %}<input type="hidden" name="expected_revision" value="{{ device.revision }}">
|
||||
<label>Nächster Schritt<select name="stage" required>{% for value, label in next_stages %}<option value="{{ value }}">{{ label }}</option>{% endfor %}</select></label>
|
||||
<label>Notiz<input name="note" maxlength="500"></label><button class="btn primary">Schritt übernehmen</button>
|
||||
</form>{% endif %}
|
||||
<div class="mini-history">{% for event in workflow_events %}<div><span>{{ event.created_at|date:'d.m.Y H:i' }}</span><strong>{{ event.get_from_stage_display }} → {{ event.get_to_stage_display }}</strong><small>{{ event.note }}{% if event.user %} · {{ event.user.username }}{% endif %}</small></div>{% empty %}<p>Noch keine Schritte erfasst.</p>{% endfor %}</div>
|
||||
{% if inspections %}<h3>Inspektionsbefunde</h3><div class="mini-history">{% for record in inspections %}<div><span>{{ record.created_at|date:'d.m.Y H:i' }}</span><strong>{{ record.defects|linebreaksbr }}</strong><small>{% if record.recorded_by %}{{ record.recorded_by.username }}{% endif %}</small></div>{% endfor %}</div>{% endif %}
|
||||
{% if repairs %}<h3>Reparaturen</h3><div class="mini-history">{% for record in repairs %}<div><span>{{ record.created_at|date:'d.m.Y H:i' }}</span><strong>{{ record.work_done|linebreaksbr }}</strong><small>{% if record.recorded_by %}{{ record.recorded_by.username }}{% endif %}</small></div>{% endfor %}</div>{% endif %}
|
||||
</section>
|
||||
<section><h2>Aufgaben</h2>
|
||||
{% for task in tasks %}<div class="form-card"><strong>{{ task.title }}</strong> · {{ task.get_state_display }}{% if task.due_date %} · fällig {{ task.due_date|date:'d.m.Y' }}{% endif %}{% if task.assigned_to %} · {{ task.assigned_to.username }}{% endif %}{% if task.description %}<p>{{ task.description|linebreaksbr }}</p>{% endif %}
|
||||
{% if can_edit_devices and task.state == 'OPEN' %}<form method="post" action="{% url 'inventory:device-task-complete' device.pk task.pk %}">{% csrf_token %}<button class="btn">Erledigt</button></form>{% endif %}</div>{% empty %}<p>Keine Aufgaben vorhanden.</p>{% endfor %}
|
||||
{% if can_edit_devices and not device.archived_at and device.stage != 'ISSUED' %}<form method="post" action="{% url 'inventory:device-task-add' device.pk %}" class="app-form">
|
||||
{% csrf_token %}<label>Neue Aufgabe<input name="title" maxlength="200" required></label>
|
||||
<label>Beschreibung<textarea name="description" rows="2"></textarea></label>
|
||||
<label>Zuständig<select name="assigned_to"><option value="">Noch offen</option>{% for assignee in task_assignees %}<option value="{{ assignee.pk }}">{{ assignee.username }}</option>{% endfor %}</select></label>
|
||||
<label>Fällig am<input name="due_date" type="date"></label><button class="btn primary">Aufgabe anlegen</button>
|
||||
</form>{% endif %}
|
||||
</section>
|
||||
</div>
|
||||
<form method="post" class="detail-form" data-device-type-form data-readonly="{{ can_edit_devices|yesno:'false,true' }}">{% csrf_token %}<input type="hidden" name="expected_revision" value="{{ device.revision }}">{{ form.non_field_errors }}
|
||||
<datalist id="suggestions-ram_gb">{% for item in ram_suggestions %}<option value="{{ item.numeric_value|floatformat:'-2' }}">{% endfor %}</datalist><datalist id="suggestions-ssd_gb">{% for item in ssd_suggestions %}<option value="{{ item.numeric_value|floatformat:'-2' }}">{% endfor %}</datalist><datalist id="suggestions-hdd_gb">{% for item in hdd_suggestions %}<option value="{{ item.numeric_value|floatformat:'-2' }}">{% endfor %}</datalist><datalist id="suggestions-display_inches">{% for item in display_suggestions %}<option value="{{ item.numeric_value|floatformat:'-2' }}">{% endfor %}</datalist>
|
||||
<section><h2>Allgemein</h2><div class="form-grid">{% for field in form %}{% if field.name == 'shelf' or field.name == 'status' or field.name == 'date' or field.name == 'device_type' or field.name == 'model' %}<label>{{ field.label }}{{ field }}{{ field.errors }}</label>{% endif %}{% endfor %}</div></section>
|
||||
<section data-hardware-section><h2>Hardware <small data-selected-type-label></small></h2><div class="form-grid">{% for field in form %}{% if field.name in all_hardware_fields %}<label data-hardware-field="{{ field.name }}"{% if field.name not in hardware_fields %} hidden{% endif %}>{{ field.label }}{{ field }}{{ field.errors }}</label>{% endif %}{% endfor %}</div></section>
|
||||
<section><h2>Verwaltung</h2><div class="form-grid">{% for field in form %}{% if field.name == 'ticket' or field.name == 'employee' or field.name == 'recipient' or field.name == 'location' or field.name == 'donor' or field.name == 'price' %}<label>{{ field.label }}{{ field }}{{ field.errors }}</label>{% endif %}{% endfor %}</div></section>
|
||||
<section><h2>Notizen</h2>{% for field in form %}{% if field.name == 'comment' %}<label>{{ field.label }}{{ field }}{{ field.errors }}</label>{% endif %}{% endfor %}</section>
|
||||
{% if definitions %}<section><h2>Zusatzfelder</h2><div class="form-grid">{% for field in definitions %}<label>{{ field.name }}{% if field.field_type == 'select' %}<select name="custom_{{ field.key }}"><option value="">—</option>{% for option in field.options.all %}{% if option.active %}<option {% if device.custom_values|get_item:field.key == option.label %}selected{% endif %}>{{ option.label }}</option>{% endif %}{% endfor %}</select>{% elif field.field_type == 'multi_select' %}<select name="custom_{{ field.key }}" multiple>{% for option in field.options.all %}{% if option.active %}<option {% if device.custom_values|get_item:field.key|contains_value:option.label %}selected{% endif %}>{{ option.label }}</option>{% endif %}{% endfor %}</select>{% elif field.field_type == 'long_text' %}<textarea name="custom_{{ field.key }}">{{ device.custom_values|get_item:field.key }}</textarea>{% elif field.field_type == 'boolean' %}<select name="custom_{{ field.key }}"><option value="">—</option><option value="true">JA</option><option value="false">NEIN</option></select>{% elif field.field_type == 'date' %}<input name="custom_{{ field.key }}" value="{{ device.custom_values|get_item:field.key|german_date }}" type="text" placeholder="TT.MM.JJJJ" inputmode="numeric" data-german-date>{% else %}<input name="custom_{{ field.key }}" value="{{ device.custom_values|get_item:field.key }}" type="{% if field.field_type == 'integer' or field.field_type == 'decimal' %}number{% else %}text{% endif %}">{% endif %}{% if field.unit %}<small>{{ field.unit }}</small>{% endif %}</label>{% endfor %}</div></section>{% endif %}
|
||||
<div class="sticky-actions"><a class="btn" href="{{ list_url }}">Abbrechen</a>{% if can_edit_devices %}<button class="btn primary">Änderungen speichern</button>{% endif %}</div>
|
||||
</form>
|
||||
{{ type_field_map|json_script:'device-type-fields' }}
|
||||
</section>{% endblock %}
|
||||
113
inventory/templates/inventory/device_list.html
Normal file
113
inventory/templates/inventory/device_list.html
Normal file
@@ -0,0 +1,113 @@
|
||||
{% extends 'inventory/base.html' %}
|
||||
{% load inventory_tags %}
|
||||
{% block body_class %}device-list-page{% endblock %}
|
||||
{% block title %}Geräte{% endblock %}
|
||||
{% block content %}
|
||||
<section class="page-head" data-live-refresh>
|
||||
<div><h1>Geräte</h1><p><span data-total-count>{{ total_count }}</span> Einträge</p></div>
|
||||
<div class="actions">
|
||||
<details class="menu export-menu"><summary class="btn">⇩ Exportieren</summary><div>
|
||||
<strong>Aktuelle Auswahl</strong><small>{{ total_count }} gefundene Geräte</small>
|
||||
<a href="{% url 'inventory:export-devices' 'csv' %}?scope=filtered{% if request.GET.urlencode %}&{{ request.GET.urlencode }}{% endif %}">Als CSV exportieren</a>
|
||||
<a href="{% url 'inventory:export-devices' 'xlsx' %}?scope=filtered{% if request.GET.urlencode %}&{{ request.GET.urlencode }}{% endif %}">Als XLSX exportieren</a>
|
||||
<a href="{% url 'inventory:export-devices' 'ods' %}?scope=filtered{% if request.GET.urlencode %}&{{ request.GET.urlencode }}{% endif %}">Als ODS für LibreOffice</a>
|
||||
<hr><a href="{% url 'inventory:export-devices' 'xlsx' %}">Alle Geräte als XLSX</a><a href="{% url 'inventory:export-devices' 'ods' %}">Alle Geräte als ODS</a>
|
||||
</div></details>
|
||||
{% if can_edit_devices %}<a class="btn primary" href="{% url 'inventory:quick-add' %}">+ Neues Gerät</a>{% endif %}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<nav aria-label="Gerätetypen" class="form-actions"><a class="btn" href="{% url 'inventory:device-list' %}">Alle Typen</a>{% for option in types %}<a class="btn" href="?type={{ option.pk }}">{{ option.label }}</a>{% endfor %}</nav>
|
||||
<form class="toolbar" method="get" id="filter-form">
|
||||
<div class="search"><span>⌕</span><input name="q" value="{{ request.GET.q }}" placeholder="Gerätenummer suchen …" inputmode="numeric" pattern="[0-9]*" autocomplete="off" aria-label="Nach Gerätenummer suchen" data-search-input></div>
|
||||
<button type="button" class="btn filter-toggle {% if has_filters %}active{% endif %}" data-filter-toggle aria-expanded="{% if has_filters %}true{% else %}false{% endif %}">☷ Filter <span class="filter-count" {% if not active_filter_count %}hidden{% endif %}>{{ active_filter_count }}</span></button>
|
||||
<label class="page-size">Zeilen <select name="page_size" data-auto-submit><option {% if page_size == 25 %}selected{% endif %}>25</option><option {% if page_size == 50 %}selected{% endif %}>50</option><option {% if page_size == 100 %}selected{% endif %}>100</option></select></label>
|
||||
<button type="button" class="btn icon" data-columns-toggle title="Spalten auswählen" aria-label="Spalten auswählen">☷</button>
|
||||
<div class="column-picker" hidden data-columns-panel></div>
|
||||
|
||||
<div class="filter-panel" {% if not has_filters %}hidden{% endif %} data-filter-panel>
|
||||
<header class="filter-panel-head"><div><h2>Geräte filtern</h2><p>Mehrere Angaben lassen sich miteinander kombinieren.</p></div><button type="button" class="icon-close" data-filter-close aria-label="Filter schließen">×</button></header>
|
||||
<div class="filter-panel-body">
|
||||
<section class="filter-section">
|
||||
<header><h3>Einordnung</h3><p>Status, Geräteart und Zuständigkeit</p></header>
|
||||
<div class="filter-grid filter-grid-four">
|
||||
<label>Arbeitsschritt<select name="stage"><option value="">Alle</option>{% for value, label in stages %}<option value="{{ value }}" {% if request.GET.stage == value %}selected{% endif %}>{{ label }}</option>{% endfor %}</select></label>
|
||||
<details class="multi-filter" data-multi-filter><summary><span>Status</span><b data-multi-value>Alle</b></summary><div>{% for option in statuses %}<label><input type="checkbox" name="status" value="{{ option.pk }}" {% option_checked request 'status' option.pk %}><span>{{ option.label }}</span></label>{% endfor %}</div></details>
|
||||
<details class="multi-filter" data-multi-filter><summary><span>Typ</span><b data-multi-value>Alle</b></summary><div>{% for option in types %}<label><input type="checkbox" name="type" value="{{ option.pk }}" {% option_checked request 'type' option.pk %}><span>{{ option.label }}</span></label>{% endfor %}</div></details>
|
||||
<details class="multi-filter" data-multi-filter><summary><span>Bearbeiter</span><b data-multi-value>Alle</b></summary><div>{% for option in employees %}<label><input type="checkbox" name="employee" value="{{ option.pk }}" {% option_checked request 'employee' option.pk %}><span>{{ option.code }} – {{ option.name }}</span></label>{% endfor %}</div></details>
|
||||
<details class="multi-filter" data-multi-filter><summary><span>Empfänger</span><b data-multi-value>Alle</b></summary><div>{% for option in recipients %}<label><input type="checkbox" name="recipient" value="{{ option.pk }}" {% option_checked request 'recipient' option.pk %}><span>{{ option.label }}</span></label>{% endfor %}</div></details>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="filter-section">
|
||||
<header><h3>Zeitraum und Preis</h3><p>Eingangsdatum und erfasster Gerätepreis</p></header>
|
||||
<div class="filter-grid filter-grid-four">
|
||||
<label>Datum von<input type="text" name="date_from" value="{{ request.GET.date_from }}" placeholder="TT.MM.JJJJ" inputmode="numeric" data-german-date></label>
|
||||
<label>Datum bis<input type="text" name="date_to" value="{{ request.GET.date_to }}" placeholder="TT.MM.JJJJ" inputmode="numeric" data-german-date></label>
|
||||
<label>Preis von<input type="number" step="0.01" min="0" name="price_min" value="{{ request.GET.price_min }}" placeholder="0,00 €"></label>
|
||||
<label>Preis bis<input type="number" step="0.01" min="0" name="price_max" value="{{ request.GET.price_max }}" placeholder="0,00 €"></label>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="filter-section">
|
||||
<header><h3>Technische Daten</h3><p>Speicher und Bildschirmgröße eingrenzen</p></header>
|
||||
<div class="filter-grid filter-grid-hardware">
|
||||
<fieldset><legend>RAM in GB</legend><div><label>Genau<input type="number" min="0" name="ram_exact" value="{{ request.GET.ram_exact }}"></label><label>Mindestens<input type="number" min="0" name="ram_min" value="{{ request.GET.ram_min }}"></label><label>Höchstens<input type="number" min="0" name="ram_max" value="{{ request.GET.ram_max }}"></label></div></fieldset>
|
||||
<fieldset><legend>SSD in GB</legend><div><label>Genau<input type="number" min="0" name="ssd_exact" value="{{ request.GET.ssd_exact }}"></label><label>Mindestens<input type="number" min="0" name="ssd_min" value="{{ request.GET.ssd_min }}"></label><label>Höchstens<input type="number" min="0" name="ssd_max" value="{{ request.GET.ssd_max }}"></label></div></fieldset>
|
||||
<fieldset><legend>HDD in GB</legend><div><label>Genau<input type="number" min="0" name="hdd_exact" value="{{ request.GET.hdd_exact }}"></label><label>Mindestens<input type="number" min="0" name="hdd_min" value="{{ request.GET.hdd_min }}"></label><label>Höchstens<input type="number" min="0" name="hdd_max" value="{{ request.GET.hdd_max }}"></label></div></fieldset>
|
||||
<fieldset><legend>Bildschirm in Zoll</legend><div><label>Mindestens<input type="number" step="0.1" min="0" name="display_min" value="{{ request.GET.display_min }}"></label><label>Höchstens<input type="number" step="0.1" min="0" name="display_max" value="{{ request.GET.display_max }}"></label></div></fieldset>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="filter-section">
|
||||
<header><h3>Prozessor</h3><p>Nach Hersteller, Leistungsklasse oder konkretem Modell filtern</p></header>
|
||||
<div class="filter-grid cpu-filter-grid">
|
||||
<details class="multi-filter" data-multi-filter><summary><span>Hersteller</span><b data-multi-value>Alle</b></summary><div>{% for value, label in cpu_vendors %}<label><input type="checkbox" name="cpu_vendor" value="{{ value }}" {% option_checked request 'cpu_vendor' value %}><span>{{ label }}</span></label>{% endfor %}</div></details>
|
||||
<details class="multi-filter" data-multi-filter><summary><span>CPU-Klasse</span><b data-multi-value>Alle</b></summary><div>{% for value, label in cpu_families %}<label><input type="checkbox" name="cpu_family" value="{{ value }}" {% option_checked request 'cpu_family' value %}><span>{{ label }}</span></label>{% endfor %}</div></details>
|
||||
<label class="cpu-search-field">CPU-Modell<input name="cpu_contains" value="{{ request.GET.cpu_contains }}" placeholder="z. B. i5 8500 oder Ryzen 5" autocomplete="off" data-cpu-autocomplete></label>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="filter-section">
|
||||
<header><h3>Ausstattung und Stichwörter</h3><p>Verbindungen und Textangaben durchsuchen</p></header>
|
||||
<div class="filter-grid filter-grid-six">
|
||||
<label>WLAN<select name="wifi"><option value="">Alle</option><option value="YES" {% option_selected request 'wifi' 'YES' %}>JA</option><option value="NO" {% option_selected request 'wifi' 'NO' %}>NEIN</option><option value="UNKNOWN" {% option_selected request 'wifi' 'UNKNOWN' %}>UNBEKANNT</option></select></label>
|
||||
<label>Akku<select name="battery"><option value="">Alle</option><option value="YES" {% option_selected request 'battery' 'YES' %}>JA</option><option value="NO" {% option_selected request 'battery' 'NO' %}>NEIN</option><option value="UNKNOWN" {% option_selected request 'battery' 'UNKNOWN' %}>UNBEKANNT</option></select></label>
|
||||
<label>LAN<select name="lan"><option value="">Alle</option><option value="YES" {% option_selected request 'lan' 'YES' %}>JA</option><option value="NO" {% option_selected request 'lan' 'NO' %}>NEIN</option><option value="UNKNOWN" {% option_selected request 'lan' 'UNKNOWN' %}>UNBEKANNT</option></select></label>
|
||||
<label>Modell enthält<input name="model_contains" value="{{ request.GET.model_contains }}" placeholder="z. B. ThinkPad"></label>
|
||||
<label>Ticket enthält<input name="ticket_contains" value="{{ request.GET.ticket_contains }}"></label>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{% if custom_filters %}<section class="filter-section"><header><h3>Zusatzfelder</h3></header><div class="filter-grid filter-grid-four">{% for field in custom_filters %}{% with param='custom_'|add:field.key %}<label>{{ field.name }}{% if field.field_type == 'select' %}<select name="{{ param }}"><option value="">Alle</option>{% for option in field.options.all %}{% if option.active %}<option value="{{ option.label }}" {% option_selected request param option.label %}>{{ option.label }}</option>{% endif %}{% endfor %}</select>{% else %}<input name="{{ param }}" value="{{ request.GET|get_item:param }}">{% endif %}</label>{% endwith %}{% endfor %}</div></section>{% endif %}
|
||||
</div>
|
||||
<footer class="filter-actions"><span>Die aktuelle Filterung wird auch für den Export verwendet.</span><a class="btn" href="{% url 'inventory:device-list' %}">Alles zurücksetzen</a><button class="btn primary">Filter anwenden</button></footer>
|
||||
</div>
|
||||
</form>
|
||||
{% if can_edit_devices %}
|
||||
<form class="bulk-bar" method="post" action="{% url 'inventory:bulk-update-devices' %}" data-bulk-form hidden data-confirm="Die ausgewählten Geräte wirklich gemeinsam ändern?" data-confirm-action="Änderungen übernehmen">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="return_query" value="{{ navigation_query }}">
|
||||
<strong><span data-bulk-count>0</span> ausgewählt</strong>
|
||||
<label>Status
|
||||
<select name="status" data-bulk-field>
|
||||
<option value="">Nicht ändern</option>
|
||||
{% for option in statuses %}<option value="{{ option.pk }}">{{ option.label }}</option>{% endfor %}
|
||||
</select>
|
||||
</label>
|
||||
<label>Empfänger
|
||||
<select name="recipient" data-bulk-field>
|
||||
<option value="">Nicht ändern</option>
|
||||
<option value="__clear__">Empfänger entfernen</option>
|
||||
{% for option in recipients %}<option value="{{ option.pk }}">{{ option.label }}</option>{% endfor %}
|
||||
</select>
|
||||
</label>
|
||||
<span data-bulk-inputs></span>
|
||||
<button type="button" class="btn small" data-bulk-clear>Auswahl aufheben</button>
|
||||
<button class="btn primary small" data-bulk-submit disabled>Änderungen übernehmen</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
<div id="device-table-container" data-initial-device="{{ request.GET.open|default:'' }}">{% include 'inventory/_device_table.html' %}</div>
|
||||
{{ preference.visible_columns|json_script:"server-visible-columns" }}
|
||||
{{ preference.column_order|json_script:"server-column-order" }}
|
||||
{% endblock %}
|
||||
20
inventory/templates/inventory/import_map.html
Normal file
20
inventory/templates/inventory/import_map.html
Normal file
@@ -0,0 +1,20 @@
|
||||
{% extends 'inventory/base.html' %}
|
||||
{% load inventory_tags %}
|
||||
{% block title %}Spalten zuordnen{% endblock %}
|
||||
{% block content %}
|
||||
<section class="page-head">
|
||||
<div><a class="back" href="{% url 'inventory:import-excel' %}">← Andere Datei</a><h1>Spalten zuordnen</h1><p>{{ job.original_name }}</p></div>
|
||||
</section>
|
||||
<form method="post">{% csrf_token %}
|
||||
<div class="mapping-grid">
|
||||
{% for header in job.headers %}
|
||||
<label><span>{{ header|default:'Unbenannte Spalte' }}</span><select name="column_{{ forloop.counter0 }}">
|
||||
{% for value,label in fields %}<option value="{{ value }}" {% if suggestions|get_item:forloop.parentloop.counter0 == value %}selected{% endif %}>{{ label }}</option>{% endfor %}
|
||||
</select></label>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<h2 class="section-title">Vorschau</h2>
|
||||
<div class="table-shell"><table class="device-table"><thead><tr>{% for header in job.headers %}<th>{{ header }}</th>{% endfor %}</tr></thead><tbody>{% for row in job.preview_rows %}<tr>{% for cell in row %}<td>{{ cell|default:'—' }}</td>{% endfor %}</tr>{% endfor %}</tbody></table></div>
|
||||
<div class="sticky-actions"><a class="btn" href="{% url 'inventory:import-excel' %}">Abbrechen</a><button class="btn primary">Import bestätigen</button></div>
|
||||
</form>
|
||||
{% endblock %}
|
||||
1
inventory/templates/inventory/import_upload.html
Normal file
1
inventory/templates/inventory/import_upload.html
Normal file
@@ -0,0 +1 @@
|
||||
{% extends 'inventory/base.html' %}{% block title %}Import / Export{% endblock %}{% block content %}<section class="narrow-page"><div class="page-head"><div><h1>Import / Export</h1><p>Excel-Dateien kontrolliert und transaktional übernehmen.</p></div></div><div class="form-card"><h2>Excel importieren</h2><ol class="steps"><li class="active">Datei hochladen</li><li>Spalten zuordnen</li><li>Import bestätigen</li></ol><form method="post" enctype="multipart/form-data" class="app-form">{% csrf_token %}{{ form.non_field_errors }}{% for field in form %}<label class="file-drop">{{ field }}<strong>.xlsx-Datei auswählen</strong><small>Maximal 10 MB</small>{{ field.errors }}</label>{% endfor %}<div class="form-actions"><button class="btn primary">Datei analysieren</button></div></form></div><div class="form-card export-card"><h2>Direkt exportieren</h2><p>Die vollständige Geräteliste als weiterverwendbare Datei laden.</p><div class="actions"><a class="btn" href="{% url 'inventory:export-devices' 'csv' %}">CSV herunterladen</a><a class="btn" href="{% url 'inventory:export-devices' 'xlsx' %}">XLSX herunterladen</a><a class="btn" href="{% url 'inventory:export-devices' 'ods' %}">ODS für LibreOffice</a></div></div></section>{% endblock %}
|
||||
59
inventory/templates/inventory/overview.html
Normal file
59
inventory/templates/inventory/overview.html
Normal file
@@ -0,0 +1,59 @@
|
||||
{% extends 'inventory/base.html' %}
|
||||
{% block title %}Übersicht{% endblock %}
|
||||
{% block content %}
|
||||
<section class="page-head overview-head" data-live-refresh>
|
||||
<div><h1>Übersicht</h1><p>Bestand, Eingänge und Datenqualität auf einen Blick</p></div>
|
||||
<nav class="period-tabs" aria-label="Zeitraum auswählen">
|
||||
<a href="?months=3" class="{% if period_months == 3 %}active{% endif %}">3 Monate</a>
|
||||
<a href="?months=6" class="{% if period_months == 6 %}active{% endif %}">6 Monate</a>
|
||||
<a href="?months=12" class="{% if period_months == 12 %}active{% endif %}">12 Monate</a>
|
||||
</nav>
|
||||
</section>
|
||||
|
||||
<section class="metric-grid" aria-label="Wichtige Kennzahlen">
|
||||
<article class="metric-card"><span>Aktive Geräte</span><strong>{{ active_count }}</strong><small>derzeit im Bestand</small></article>
|
||||
<article class="metric-card accent"><span>Neueingänge</span><strong>{{ period_count }}</strong><small>{{ period_start|date:'d.m.Y' }} bis {{ today|date:'d.m.Y' }}</small><em class="{% if difference > 0 %}up{% elif difference < 0 %}down{% endif %}">{% if difference > 0 %}+{% endif %}{{ difference }} zum vorherigen Zeitraum</em></article>
|
||||
<article class="metric-card"><span>Davon Laptops</span><strong>{{ period_laptops }}</strong><small>in den letzten {{ period_months }} Monaten</small></article>
|
||||
<article class="metric-card"><span>Davon fertig</span><strong>{{ period_finished }}</strong><small>aktuell mit Status „Fertig“</small></article>
|
||||
<article class="metric-card"><span>Im Archiv</span><strong>{{ archived_count }}</strong><small>historisch abgelegte Geräte</small></article>
|
||||
</section>
|
||||
|
||||
<section class="dashboard-grid">
|
||||
<article class="chart-panel monthly-panel">
|
||||
<header><div><h2>Geräteeingänge</h2><p>Erfasste Geräte pro Monat · letzte 12 Monate</p></div><strong class="chart-total">{{ maximum_month }}<small>Höchstwert</small></strong></header>
|
||||
<div class="line-chart-wrap">
|
||||
<svg class="line-chart" viewBox="0 0 900 230" role="img" aria-labelledby="arrivals-title">
|
||||
<title id="arrivals-title">Geräteeingänge der letzten zwölf Monate</title>
|
||||
<line x1="40" y1="35" x2="860" y2="35" class="chart-grid-line"/><line x1="40" y1="120" x2="860" y2="120" class="chart-grid-line"/><line x1="40" y1="205" x2="860" y2="205" class="chart-grid-line"/>
|
||||
<text x="31" y="39" text-anchor="end">{{ maximum_month }}</text><text x="31" y="124" text-anchor="end">{{ half_month }}</text><text x="31" y="209" text-anchor="end">0</text>
|
||||
<polygon points="{{ area_points }}" class="chart-area"/><polyline points="{{ line_points }}" class="chart-line"/>
|
||||
{% for item in month_series %}<circle cx="{{ item.x }}" cy="{{ item.y }}" r="4" class="chart-point"><title>{{ item.label }}: {{ item.count }} Geräte</title></circle>{% endfor %}
|
||||
</svg>
|
||||
<div class="month-labels">{% for item in month_series %}<span>{{ item.label }}</span>{% endfor %}</div>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<article class="chart-panel">
|
||||
<header><div><h2>Gerätearten</h2><p>Neueingänge im gewählten Zeitraum</p></div></header>
|
||||
<div class="bar-chart">{% for item in type_rows %}<div class="bar-row"><div><span>{{ item.label }}</span><strong>{{ item.count }}</strong></div><span class="bar-track"><i style="width:{{ item.percentage }}%"></i></span></div>{% empty %}<p class="chart-empty">In diesem Zeitraum gibt es keine Eingänge.</p>{% endfor %}</div>
|
||||
</article>
|
||||
|
||||
<article class="chart-panel">
|
||||
<header><div><h2>Statusverteilung</h2><p>Alle aktiven Geräte</p></div></header>
|
||||
<div class="donut-layout">
|
||||
<div class="donut" style="background:conic-gradient({{ status_gradient }})"><div><strong>{{ status_total }}</strong><span>Geräte</span></div></div>
|
||||
<div class="legend">{% for item in status_rows %}<div><i style="background:{{ item.color }}"></i><span>{{ item.label }}</span><strong>{{ item.count }} <small>{{ item.percentage }} %</small></strong></div>{% empty %}<p class="chart-empty">Keine aktiven Geräte vorhanden.</p>{% endfor %}</div>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<article class="chart-panel">
|
||||
<header><div><h2>Häufigste Empfänger</h2><p>Neueingänge im gewählten Zeitraum</p></div></header>
|
||||
<div class="bar-chart recipient-chart">{% for item in recipient_rows %}<div class="bar-row"><div><span>{{ item.label }}</span><strong>{{ item.count }}</strong></div><span class="bar-track"><i style="width:{{ item.percentage }}%"></i></span></div>{% empty %}<p class="chart-empty">Noch keine Empfänger im Zeitraum erfasst.</p>{% endfor %}</div>
|
||||
</article>
|
||||
|
||||
<article class="chart-panel">
|
||||
<header><div><h2>Datenvollständigkeit</h2><p>Anteil ausgefüllter Angaben bei aktiven Geräten</p></div></header>
|
||||
<div class="completion-chart">{% for item in completeness %}<div><span>{{ item.label }}</span><span class="completion-track"><i style="width:{{ item.percentage }}%"></i></span><strong>{{ item.percentage }} %</strong></div>{% endfor %}</div>
|
||||
</article>
|
||||
</section>
|
||||
{% endblock %}
|
||||
13
inventory/templates/inventory/quick_add.html
Normal file
13
inventory/templates/inventory/quick_add.html
Normal file
@@ -0,0 +1,13 @@
|
||||
{% extends 'inventory/base.html' %}{% block title %}Neues Gerät{% endblock %}{% block content %}
|
||||
<section class="narrow-page"><div class="page-head"><div><h1>Neues Gerät</h1><p>Typ auswählen und die passenden Daten direkt erfassen.</p></div></div>
|
||||
<div class="form-card"><form method="post" class="app-form" data-device-type-form data-hide-hardware-without-type>
|
||||
{% csrf_token %}{{ form.non_field_errors }}
|
||||
{% for field in form %}{% if field.name not in all_hardware_fields %}<label>{{ field.label }}{{ field }}{% if field.help_text %}<small>{{ field.help_text }}</small>{% endif %}{{ field.errors }}</label>{% endif %}{% endfor %}
|
||||
<section class="quick-hardware-section" data-hardware-section hidden>
|
||||
<h2>Technische Daten <small data-selected-type-label></small></h2>
|
||||
<div class="quick-hardware-grid">{% for field in form.hardware_fields %}<label data-hardware-field="{{ field.name }}" hidden>{{ field.label }}{{ field }}{{ field.errors }}</label>{% endfor %}</div>
|
||||
</section>
|
||||
<div class="form-actions"><a class="btn" href="{% url 'inventory:device-list' %}">Abbrechen</a><button class="btn primary">Gerät anlegen</button></div>
|
||||
</form></div></section>
|
||||
{{ type_field_map|json_script:'device-type-fields' }}
|
||||
{% endblock %}
|
||||
17
inventory/templates/inventory/simple_form.html
Normal file
17
inventory/templates/inventory/simple_form.html
Normal file
@@ -0,0 +1,17 @@
|
||||
{% extends 'inventory/base.html' %}
|
||||
{% block title %}{{ title }}{% endblock %}
|
||||
{% block content %}
|
||||
<section class="narrow-page">
|
||||
<div class="page-head"><div><a class="back" href="{{ cancel_url|default:'/configuration/' }}">← Zurück</a><h1>{{ title }}</h1></div></div>
|
||||
<div class="form-card"><form method="post" class="app-form">{% csrf_token %}{{ form.non_field_errors }}
|
||||
{% for field in form %}
|
||||
{% if field.name == 'color' %}
|
||||
<fieldset class="color-field"><legend>{{ field.label }}</legend><div class="color-options">{% for option in field %}<label class="color-option" style="--choice-color:{{ option.data.value }}">{{ option.tag }}<i></i><span>{{ option.choice_label }}</span></label>{% endfor %}</div>{% if field.help_text %}<small>{{ field.help_text }}</small>{% endif %}{{ field.errors }}</fieldset>
|
||||
{% else %}
|
||||
<label{% if field.field.widget.input_type == 'checkbox' %} class="check-label"{% endif %}>{{ field.label }}{{ field }}{% if field.help_text %}<small>{{ field.help_text }}</small>{% endif %}{{ field.errors }}</label>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
<div class="form-actions"><a class="btn" href="{{ cancel_url|default:'/configuration/' }}">Abbrechen</a><button class="btn primary">Speichern</button></div>
|
||||
</form></div>
|
||||
</section>
|
||||
{% endblock %}
|
||||
8
inventory/templates/inventory/task_list.html
Normal file
8
inventory/templates/inventory/task_list.html
Normal file
@@ -0,0 +1,8 @@
|
||||
{% extends 'inventory/base.html' %}{% block title %}Aufgaben{% endblock %}{% block content %}
|
||||
<section class="narrow-page" data-live-refresh><div class="page-head"><div><h1>Aufgaben</h1><p>Offene Arbeiten an Geräten im Überblick.</p></div></div>
|
||||
<p><a class="btn{% if state == 'OPEN' %} primary{% endif %}" href="?state=OPEN">Offen</a> <a class="btn{% if state == 'DONE' %} primary{% endif %}" href="?state=DONE">Erledigt</a></p>
|
||||
{% for task in tasks %}<div class="form-card"><h2><a href="{% url 'inventory:device-detail' task.device.pk %}">Gerät #{{ task.device.number }}</a> · {{ task.title }}</h2>
|
||||
<p>{{ task.device.get_stage_display }}{% if task.assigned_to %} · {{ task.assigned_to.username }}{% endif %}{% if task.due_date %} · fällig {{ task.due_date|date:'d.m.Y' }}{% endif %}</p>
|
||||
{% if task.description %}<p>{{ task.description|linebreaksbr }}</p>{% endif %}
|
||||
{% if can_edit_devices and task.state == 'OPEN' %}<form method="post" action="{% url 'inventory:device-task-complete' task.device.pk task.pk %}">{% csrf_token %}<button class="btn">Erledigt</button></form>{% endif %}
|
||||
</div>{% empty %}<p>Keine Aufgaben vorhanden.</p>{% endfor %}</section>{% endblock %}
|
||||
4
inventory/templates/inventory/user_list.html
Normal file
4
inventory/templates/inventory/user_list.html
Normal file
@@ -0,0 +1,4 @@
|
||||
{% extends 'inventory/base.html' %}{% block title %}Benutzer{% endblock %}{% block content %}
|
||||
<section class="page-head"><div><h1>Benutzer</h1><p>Konten und Rollen der Geräteverwaltung</p></div><a class="btn primary" href="{% url 'inventory:user-new' %}">+ Benutzer</a></section>
|
||||
<div class="table-shell"><table class="device-table"><thead><tr><th>Benutzername</th><th>Name</th><th>E-Mail</th><th>Rolle</th><th>Status</th><th></th></tr></thead><tbody>{% for user in users %}<tr><td class="strong">{{ user.username }}</td><td>{{ user.get_full_name|default:'—' }}</td><td>{{ user.email|default:'—' }}</td><td>{{ user.profile.get_role_display }}</td><td>{% if user.is_active %}<span class="status status-fertig">Aktiv</span>{% else %}<span class="status status-defekt">Inaktiv</span>{% endif %}</td><td><a class="btn small" href="{% url 'inventory:user-edit' user.pk %}">Bearbeiten</a></td></tr>{% empty %}<tr><td colspan="6" class="empty">Keine Benutzer vorhanden.</td></tr>{% endfor %}</tbody></table></div>
|
||||
{% endblock %}
|
||||
42
inventory/templates/inventory/workflow_board.html
Normal file
42
inventory/templates/inventory/workflow_board.html
Normal file
@@ -0,0 +1,42 @@
|
||||
{% extends 'inventory/base.html' %}{% block title %}Ablauf{% endblock %}{% block content %}
|
||||
<section class="page-head" data-live-refresh><div><h1>Ablauf</h1><p>Geräte prüfen, reparieren, lagern und ausgeben.</p></div><a class="btn" href="{% url 'inventory:task-list' %}">Alle Aufgaben</a></section>
|
||||
<div class="workflow-board">
|
||||
{% for column in columns %}<section class="workflow-column" aria-labelledby="stage-{{ column.stage }}">
|
||||
<header><h2 id="stage-{{ column.stage }}">{{ column.label }}</h2><span>{{ column.total }}</span></header>
|
||||
<div class="workflow-cards">
|
||||
{% for device in column.devices %}<article class="workflow-card">
|
||||
<div class="workflow-card-head"><a href="{% url 'inventory:device-detail' device.pk %}">#{{ device.number }} · {{ device.model|default:device.device_type.label|default:'Gerät' }}</a><small>{{ device.device_type.label|default:'Ohne Typ' }}</small></div>
|
||||
<p>{{ device.status.label }}{% if device.employee %} · {{ device.employee.code }}{% endif %}{% if device.shelf %} · Regal {{ device.shelf }}{% endif %}</p>
|
||||
{% if device.stage == 'INSPECTION' %}
|
||||
{% with latest=device.inspection_history.0 %}{% if latest %}<p class="workflow-record"><strong>Letzter Befund:</strong> {{ latest.defects|truncatechars:180 }}<small>{{ latest.created_at|date:'d.m.Y H:i' }}{% if latest.recorded_by %} · {{ latest.recorded_by.username }}{% endif %}</small></p>{% endif %}{% endwith %}
|
||||
{% if can_edit_devices %}<form method="post" action="{% url 'inventory:device-inspection-save' device.pk %}" class="workflow-stage-form">
|
||||
{% csrf_token %}<input type="hidden" name="return_to" value="workflow"><input type="hidden" name="expected_revision" value="{{ device.revision }}">
|
||||
<h3>Inspektion erfassen</h3><label>{{ device.stage_form.defects.label }}{{ device.stage_form.defects }}</label>
|
||||
<div class="workflow-spec-grid">{% for field in device.stage_form.specification_fields %}<label>{{ field.label }}{{ field }}</label>{% endfor %}</div>
|
||||
<button class="btn small primary">Befund und Daten speichern</button>
|
||||
</form>{% endif %}
|
||||
{% elif device.stage == 'REPAIR' %}
|
||||
{% with latest=device.repair_history.0 %}{% if latest %}<p class="workflow-record"><strong>Letzte Reparatur:</strong> {{ latest.work_done|truncatechars:180 }}<small>{{ latest.created_at|date:'d.m.Y H:i' }}{% if latest.recorded_by %} · {{ latest.recorded_by.username }}{% endif %}</small></p>{% endif %}{% endwith %}
|
||||
{% if can_edit_devices %}<form method="post" action="{% url 'inventory:device-repair-add' device.pk %}" class="workflow-stage-form">
|
||||
{% csrf_token %}<input type="hidden" name="return_to" value="workflow"><input type="hidden" name="expected_revision" value="{{ device.revision }}">
|
||||
<h3>Reparatur dokumentieren</h3><label>{{ device.stage_form.work_done.label }}{{ device.stage_form.work_done }}</label>
|
||||
<button class="btn small primary">Arbeiten speichern</button>
|
||||
</form>{% endif %}
|
||||
{% elif device.stage == 'STORAGE' %}
|
||||
{% if can_edit_devices %}<form method="post" action="{% url 'inventory:device-storage-assign' device.pk %}" class="workflow-stage-form">
|
||||
{% csrf_token %}<input type="hidden" name="return_to" value="workflow"><input type="hidden" name="expected_revision" value="{{ device.revision }}">
|
||||
<h3>Ausgabe vorbereiten</h3><label>{{ device.stage_form.recipient.label }}{{ device.stage_form.recipient }}</label>
|
||||
<button class="btn small primary">Empfänger speichern</button>
|
||||
</form>{% elif device.recipient %}<p class="workflow-record"><strong>Empfänger:</strong> {{ device.recipient.label }}</p>{% endif %}
|
||||
{% endif %}
|
||||
<div class="workflow-task-line"><strong>{{ device.open_task_count }} offen</strong>{% for task in device.open_tasks|slice:':2' %}<span>{{ task.title }}</span>{% endfor %}</div>
|
||||
{% if can_edit_devices %}
|
||||
{% if device.stage != 'ISSUED' %}<form method="post" action="{% url 'inventory:device-task-add' device.pk %}" class="workflow-quick-form">{% csrf_token %}<input type="hidden" name="return_to" value="workflow"><input name="title" maxlength="200" required placeholder="Neue Aufgabe" aria-label="Neue Aufgabe für Gerät {{ device.number }}"><button class="btn small" title="Aufgabe anlegen">+</button></form>{% endif %}
|
||||
{% if device.next_stages %}<form method="post" action="{% url 'inventory:device-advance' device.pk %}" class="workflow-quick-form">{% csrf_token %}<input type="hidden" name="return_to" value="workflow"><input type="hidden" name="expected_revision" value="{{ device.revision }}"><select name="stage" aria-label="Nächster Schritt für Gerät {{ device.number }}">{% for value, label in device.next_stages %}<option value="{{ value }}">{{ label }}</option>{% endfor %}</select><button class="btn small primary">Weiter</button></form>{% endif %}
|
||||
{% endif %}
|
||||
</article>{% empty %}<p class="workflow-empty">Keine Geräte</p>{% endfor %}
|
||||
{% if column.total > 30 %}<p class="workflow-empty">Die neuesten 30 von {{ column.total }} Geräten. <a href="{% url 'inventory:device-list' %}?stage={{ column.stage }}">Alle anzeigen</a></p>{% endif %}
|
||||
</div>
|
||||
</section>{% endfor %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
5
inventory/templates/registration/login.html
Normal file
5
inventory/templates/registration/login.html
Normal file
@@ -0,0 +1,5 @@
|
||||
{% load static %}
|
||||
<!doctype html><html lang="de"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Anmelden · CS-R</title><link rel="icon" type="image/png" href="{% static 'inventory/img/csr-logo.png' %}"><script src="{% static 'inventory/js/theme.js' %}?v=20260928-3"></script><link rel="stylesheet" href="{% static 'inventory/css/app.css' %}?v=20260928-3"></head>
|
||||
<body class="login-page"><main class="login-card"><img src="{% static 'inventory/img/csr-logo.png' %}" alt="CS-R"><h1>Geräteverwaltung</h1><p>Melden Sie sich mit Ihrem internen Konto an.</p>
|
||||
{% if form.errors %}<div class="form-error">Benutzername oder Passwort ist nicht korrekt.</div>{% endif %}
|
||||
<form method="post">{% csrf_token %}<label>Benutzername{{ form.username }}</label><label>Passwort{{ form.password }}</label><input type="hidden" name="next" value="{{ next }}"><button class="btn primary full">Anmelden</button></form><button class="theme-toggle login-theme" type="button" data-theme-toggle aria-pressed="false"><span data-theme-icon aria-hidden="true">☾</span><span data-theme-label>Dunkelmodus</span></button></main></body></html>
|
||||
Reference in New Issue
Block a user