From d9d5e4f6180ab159a886bda6356418e3f1bc05ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Gr=C3=BCssing?= Date: Mon, 1 Jun 2026 22:02:21 +0200 Subject: [PATCH] Add: AssetsPage Master-Detail Redesign + API-Response Fix --- frontend/src/pages/AssetsPage.jsx | 1805 +++++++++++++++++++---------- frontend/src/styles/App.css | 847 ++++++++++++++ 2 files changed, 2032 insertions(+), 620 deletions(-) diff --git a/frontend/src/pages/AssetsPage.jsx b/frontend/src/pages/AssetsPage.jsx index 9d3db12..6e740b9 100644 --- a/frontend/src/pages/AssetsPage.jsx +++ b/frontend/src/pages/AssetsPage.jsx @@ -1,671 +1,1236 @@ -import React, { useState, useEffect } from 'react'; -import { Link } from 'react-router-dom'; +import React, { useState, useEffect, useCallback, useRef } from 'react'; import { useAuth } from '../context/AuthContext'; -import assetService from '../services/assetService'; -import LoadingSpinner from '../components/common/LoadingSpinner'; -import AssetModal from '../components/assets/AssetModal'; -import AssetAssignModal from '../components/assets/AssetAssignModal'; -import InspectionSection from '../components/assets/InspectionSection'; import { toast } from 'react-toastify'; -const DEPT_COLORS = { - 'IT': { bg: 'rgba(59,130,246,0.12)', text: '#3b82f6', border: '#3b82f6' }, - 'Produktion': { bg: 'rgba(245,158,11,0.12)', text: '#f59e0b', border: '#f59e0b' }, - 'Techniker': { bg: 'rgba(34,197,94,0.12)', text: '#22c55e', border: '#22c55e' }, +/* โ”€โ”€ helpers โ”€โ”€ */ +const authFetch = (url, opts = {}) => { + const token = localStorage.getItem('token'); + return fetch(url, { + ...opts, + headers: { + ...(opts.headers || {}), + Authorization: `Bearer ${token}`, + 'Content-Type': 'application/json', + }, + }); +}; +const API = process.env.REACT_APP_API_URL || '/api'; + +const TYPE_ICONS = { + notebook: '๐Ÿ’ป', + monitor: '๐Ÿ–ฅ๏ธ', + headset: '๐ŸŽง', + drucker: '๐Ÿ–จ๏ธ', + sonstiges: '๐Ÿ“ฆ', }; -const PAGE_SIZE = 25; +const getTypeIcon = (type) => { + if (!type) return '๐Ÿ“ฆ'; + const t = type.toLowerCase(); + return TYPE_ICONS[t] || '๐Ÿ“ฆ'; +}; -const AssetsPage = () => { - const { isAdmin, isSuperAdmin, isTechniker } = useAuth(); - const [assets, setAssets] = useState([]); - const [filteredAssets, setFilteredAssets] = useState([]); - const [loading, setLoading] = useState(true); - const [searchTerm, setSearchTerm] = useState(''); - const [statusFilter, setStatusFilter] = useState('all'); - const [typeFilter, setTypeFilter] = useState('all'); - const [deptFilter, setDeptFilter] = useState('all'); - const [maintenanceFilter, setMaintenanceFilter] = useState(false); - const [sortField, setSortField] = useState('name'); - const [sortDir, setSortDir] = useState('asc'); - const [currentPage, setCurrentPage] = useState(1); - const [assetTypes, setAssetTypes] = useState([]); - const [showAssetModal, setShowAssetModal] = useState(false); - const [showAssignModal, setShowAssignModal] = useState(false); - const [editingAsset, setEditingAsset] = useState(null); - const [assigningAsset, setAssigningAsset] = useState(null); - const [labelModal, setLabelModal] = useState(null); - const [labelCopies, setLabelCopies] = useState(1); - const [labelSize, setLabelSize] = useState('medium'); - const [labelLoading, setLabelLoading] = useState(false); - const [intuneLoading, setIntuneLoading] = useState(false); - const [showCsvImport, setShowCsvImport] = useState(false); - const [csvFile, setCsvFile] = useState(null); - const [csvPreview, setCsvPreview] = useState(null); - const [csvImporting, setCsvImporting] = useState(false); - const [inspectionAsset, setInspectionAsset] = useState(null); - const [openMenuId, setOpenMenuId] = useState(null); - const [menuPos, setMenuPos] = useState({ top: 0, left: 0 }); +const STATUS_LABELS = { + verfuegbar: 'Verfรผgbar', + zugewiesen: 'Zugewiesen', + inaktiv: 'Inaktiv', + beschaedigt: 'Beschรคdigt', +}; + +const STATUS_CSS = { + verfuegbar: 'ap-badge-available', + zugewiesen: 'ap-badge-assigned', + inaktiv: 'ap-badge-inactive', + beschaedigt: 'ap-badge-damaged', +}; + +const fmtDate = (d) => { + if (!d) return 'โ€”'; + try { return new Date(d).toLocaleDateString('de-DE'); } catch { return d; } +}; + +const fmtCurrency = (n) => { + if (n === null || n === undefined || n === '') return 'โ€”'; + return parseFloat(n).toLocaleString('de-DE', { minimumFractionDigits: 2, maximumFractionDigits: 2 }) + ' โ‚ฌ'; +}; + +const calcBookValue = (asset) => { + if (!asset.purchase_price || !asset.purchase_date || !asset.useful_life_years) return null; + const price = parseFloat(asset.purchase_price); + const residual = parseFloat(asset.residual_value || 0); + const lifeMonths = parseFloat(asset.useful_life_years) * 12; + const depPerMonth = (price - residual) / lifeMonths; + const months = Math.max(0, (Date.now() - new Date(asset.purchase_date).getTime()) / (1000 * 60 * 60 * 24 * 30.44)); + const depreciated = Math.min(price - residual, depPerMonth * months); + return Math.max(residual, price - depreciated); +}; + +const calcDepPercent = (asset) => { + if (!asset.purchase_price || !asset.purchase_date || !asset.useful_life_years) return 0; + const price = parseFloat(asset.purchase_price); + const residual = parseFloat(asset.residual_value || 0); + const lifeMonths = parseFloat(asset.useful_life_years) * 12; + const depPerMonth = (price - residual) / lifeMonths; + const months = Math.max(0, (Date.now() - new Date(asset.purchase_date).getTime()) / (1000 * 60 * 60 * 24 * 30.44)); + const depreciated = Math.min(price - residual, depPerMonth * months); + return Math.round((depreciated / (price - residual || 1)) * 100); +}; + +const calcMonthlyDep = (asset) => { + if (!asset.purchase_price || !asset.useful_life_years) return null; + const price = parseFloat(asset.purchase_price); + const residual = parseFloat(asset.residual_value || 0); + const lifeMonths = parseFloat(asset.useful_life_years) * 12; + return (price - residual) / lifeMonths; +}; + +const relativeTime = (d) => { + if (!d) return 'โ€”'; + const diff = Date.now() - new Date(d).getTime(); + const mins = Math.floor(diff / 60000); + if (mins < 2) return 'gerade eben'; + if (mins < 60) return `vor ${mins} Min`; + const h = Math.floor(mins / 60); + if (h < 24) return `vor ${h} Std`; + return `vor ${Math.floor(h / 24)} Tag(en)`; +}; + +const isOnline = (agent) => { + if (!agent || !agent.last_seen) return false; + const diff = Date.now() - new Date(agent.last_seen).getTime(); + return diff < 5 * 60 * 1000; +}; + +const initials = (name) => { + if (!name) return '??'; + const parts = name.trim().split(' '); + if (parts.length >= 2) return (parts[0][0] + parts[parts.length - 1][0]).toUpperCase(); + return name.slice(0, 2).toUpperCase(); +}; + +/* โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• + EDIT MODAL +โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• */ +const EditModal = ({ asset, onClose, onSaved }) => { + const [form, setForm] = useState({ + name: asset.name || '', + type: asset.type || '', + model: asset.model || '', + serial_number: asset.serial_number || '', + manufacturer: asset.manufacturer || '', + os: asset.os || '', + ip_address: asset.ip_address || '', + inventory_number: asset.inventory_number || '', + department: asset.department || '', + purchase_date: asset.purchase_date ? asset.purchase_date.slice(0, 10) : '', + purchase_price: asset.purchase_price || '', + useful_life_years: asset.useful_life_years || '', + residual_value: asset.residual_value || '', + next_maintenance_date: asset.next_maintenance_date ? asset.next_maintenance_date.slice(0, 10) : '', + maintenance_interval_months: asset.maintenance_interval_months || '', + maintenance_notes: asset.maintenance_notes || '', + teamviewer_id: asset.teamviewer_id || '', + description: asset.description || '', + }); + const [saving, setSaving] = useState(false); + + const set = (k, v) => setForm(f => ({ ...f, [k]: v })); + + const save = async () => { + setSaving(true); + try { + const res = await authFetch(`${API}/assets/${asset.id}`, { + method: 'PUT', + body: JSON.stringify(form), + }); + if (!res.ok) throw new Error(await res.text()); + toast.success('Asset gespeichert'); + onSaved(); + } catch (e) { + toast.error('Fehler beim Speichern: ' + e.message); + } finally { + setSaving(false); + } + }; + + const Field = ({ label, k, type = 'text', placeholder }) => ( +
+ + set(k, e.target.value)} + /> +
+ ); + + return ( +
+
e.stopPropagation()}> +
+ โœ๏ธ Asset bearbeiten โ€” {asset.name} + +
+
+
Gerรคtedaten
+
+ +
+ + +
+ + + + + + + + +
+
Finanzen & Wartung
+
+ + + + + + +
+
+ +