import React, { useState, useEffect } from 'react'; import { Link } from 'react-router-dom'; 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' }, }; const PAGE_SIZE = 25; 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 }); useEffect(() => { loadAssets(); assetService.getTypes().then(setAssetTypes).catch(() => {}); }, []); useEffect(() => { if (openMenuId === null) return; const close = () => setOpenMenuId(null); window.addEventListener('click', close); return () => window.removeEventListener('click', close); }, [openMenuId]); useEffect(() => { filterAssets(); setCurrentPage(1); }, [assets, searchTerm, statusFilter, typeFilter, deptFilter, maintenanceFilter]); const canModifyAssets = () => { return isSuperAdmin() || isAdmin() || isTechniker(); }; const handleSort = (field) => { if (sortField === field) { setSortDir(d => d === 'asc' ? 'desc' : 'asc'); } else { setSortField(field); setSortDir('asc'); } setCurrentPage(1); }; const SortIcon = ({ field }) => { if (sortField !== field) return ; return {sortDir === 'asc' ? '↑' : '↓'}; }; const loadAssets = async () => { try { const data = await assetService.getAll(); setAssets(data); } catch (error) { toast.error('Fehler beim Laden der Assets'); } finally { setLoading(false); } }; const filterAssets = () => { let filtered = [...assets]; // Search filter if (searchTerm) { filtered = filtered.filter( (asset) => asset.name.toLowerCase().includes(searchTerm.toLowerCase()) || asset.serial_number?.toLowerCase().includes(searchTerm.toLowerCase()) || asset.inventory_number?.toLowerCase().includes(searchTerm.toLowerCase()) || asset.model?.toLowerCase().includes(searchTerm.toLowerCase()) ); } // Status filter if (statusFilter !== 'all') { filtered = filtered.filter((asset) => asset.status === statusFilter); } // Type filter if (typeFilter !== 'all') { filtered = filtered.filter((asset) => asset.type === typeFilter); } // Department filter if (deptFilter !== 'all') { filtered = filtered.filter((asset) => (asset.department || 'IT') === deptFilter); } // Maintenance filter if (maintenanceFilter) { const today = new Date().toISOString().slice(0, 10); filtered = filtered.filter( (asset) => asset.next_maintenance_date && asset.next_maintenance_date <= today ); } setFilteredAssets(filtered); }; const handleCreate = () => { setEditingAsset(null); setShowAssetModal(true); }; const handleEdit = (asset) => { setEditingAsset(asset); setShowAssetModal(true); }; const handleAssetModalClose = () => { setShowAssetModal(false); setEditingAsset(null); }; const handleAssetSaved = () => { loadAssets(); handleAssetModalClose(); }; const handleAssign = (asset) => { setAssigningAsset(asset); setShowAssignModal(true); }; const handleUnassign = async (asset) => { if (!window.confirm('Möchten Sie die Zuweisung dieses Assets wirklich aufheben?')) { return; } try { await assetService.unassign(asset.id, 'verfuegbar'); toast.success('Zuweisung erfolgreich aufgehoben'); loadAssets(); } catch (error) { toast.error(error.message || 'Fehler beim Aufheben der Zuweisung'); } }; const handleAssignModalClose = () => { setShowAssignModal(false); setAssigningAsset(null); }; const handleAssigned = () => { loadAssets(); handleAssignModalClose(); }; const handlePrintLabel = async () => { setLabelLoading(true); try { await assetService.printLabel(labelModal.id, labelCopies, labelSize); setLabelModal(null); } catch (error) { toast.error('Fehler beim Erstellen des Labels'); } finally { setLabelLoading(false); } }; const handleIntuneImport = async () => { setIntuneLoading(true); try { const result = await assetService.importFromIntune(); toast.success( `Intune-Import abgeschlossen: ${result.imported} importiert, ${result.skipped} übersprungen` + (result.errors.length > 0 ? `, ${result.errors.length} Fehler` : '') ); if (result.imported > 0) loadAssets(); } catch (error) { toast.error(error.message || 'Fehler beim Intune-Import'); } finally { setIntuneLoading(false); } }; const handleExportCsv = () => { const headers = ['Name', 'Typ', 'Seriennummer', 'Modell', 'Status', 'Zugewiesen an', 'Kaufdatum', 'Nächste Prüfung', 'TeamViewer-ID']; const rows = filteredAssets.map(a => [ a.name, a.type, a.serial_number, a.model || '', a.status, a.assigned_to_username || '', a.purchase_date || '', a.next_maintenance_date || '', a.teamviewer_id || '', ].map(v => `"${String(v).replace(/"/g, '""')}"`).join(';')); const csv = [headers.join(';'), ...rows].join('\n'); const blob = new Blob(['\uFEFF' + csv], { type: 'text/csv;charset=utf-8;' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `assets_${new Date().toISOString().slice(0,10)}.csv`; a.click(); URL.revokeObjectURL(url); toast.success(`${filteredAssets.length} Assets exportiert`); }; const handleCsvFileChange = (e) => { const file = e.target.files[0]; if (!file) return; setCsvFile(file); const reader = new FileReader(); reader.onload = (ev) => { const text = ev.target.result.replace(/^\uFEFF/, ''); const lines = text.split(/\r?\n/).filter(l => l.trim()); const sep = lines[0].includes(';') ? ';' : ','; const headers = lines[0].split(sep).map(h => h.replace(/^"|"$/g, '').trim().toLowerCase()); const rows = lines.slice(1).map(line => { const vals = line.split(sep).map(v => v.replace(/^"|"$/g, '').trim()); const obj = {}; headers.forEach((h, i) => { obj[h] = vals[i] || ''; }); return obj; }).filter(r => r.name || r['name']); setCsvPreview({ headers, rows: rows.slice(0, 5), total: rows.length, allRows: rows }); }; reader.readAsText(file, 'UTF-8'); }; const handleCsvImport = async () => { if (!csvPreview) return; setCsvImporting(true); let imported = 0, errors = 0; for (const row of csvPreview.allRows) { try { await assetService.create({ name: row.name || row['name'] || '', type: row.typ || row.type || 'Other', serial_number: row.seriennummer || row['serial_number'] || row['serialnumber'] || '', model: row.modell || row.model || '', status: row.status || 'verfuegbar', purchase_date: row.kaufdatum || row['purchase_date'] || '', teamviewer_id: row['teamviewer-id'] || row.teamviewer_id || '', }); imported++; } catch { errors++; } } toast.success(`CSV-Import: ${imported} importiert${errors > 0 ? `, ${errors} Fehler` : ''}`); setShowCsvImport(false); setCsvFile(null); setCsvPreview(null); loadAssets(); setCsvImporting(false); }; const handleDelete = async (id) => { if (!window.confirm('Möchten Sie dieses Asset wirklich löschen?')) { return; } try { await assetService.delete(id); toast.success('Asset erfolgreich gelöscht'); loadAssets(); } catch (error) { toast.error(error.message || 'Fehler beim Löschen'); } }; if (loading) { return (
); } // Sort + paginate in render const sorted = [...filteredAssets].sort((a, b) => { let va = (a[sortField] || '').toString().toLowerCase(); let vb = (b[sortField] || '').toString().toLowerCase(); return sortDir === 'asc' ? va.localeCompare(vb, 'de') : vb.localeCompare(va, 'de'); }); const totalPages = Math.max(1, Math.ceil(sorted.length / PAGE_SIZE)); const pagedAssets = sorted.slice((currentPage - 1) * PAGE_SIZE, currentPage * PAGE_SIZE); const hasAnyMaintenanceDate = assets.some(a => a.next_maintenance_date); return (

Assets

{filteredAssets.length} von {assets.length}
{canModifyAssets() && ( <> {!isTechniker() && ( )} )}
{/* Search and Filter — kompakte einzeilige Leiste */}
setSearchTerm(e.target.value)} style={{ flex: '1 1 220px', minWidth: 180, maxWidth: 340 }} /> {(isAdmin() || isSuperAdmin()) && ( )}
{[ { key: 'all', label: 'Alle', cls: statusFilter === 'all' ? 'btn-primary' : 'btn-secondary' }, { key: 'verfuegbar', label: 'Verfügbar', cls: statusFilter === 'verfuegbar' ? 'btn-success' : 'btn-secondary' }, { key: 'zugewiesen', label: 'Zugewiesen', cls: statusFilter === 'zugewiesen' ? 'btn-primary' : 'btn-secondary' }, { key: 'inaktiv', label: 'Inaktiv', cls: statusFilter === 'inaktiv' ? 'btn-secondary active' : 'btn-secondary' }, { key: 'beschaedigt', label: 'Beschädigt', cls: statusFilter === 'beschaedigt' ? 'btn-danger' : 'btn-secondary' }, ].map(s => ( ))}
{/* Table */}
{hasAnyMaintenanceDate && } {[ { field: 'name', label: 'Name' }, { field: 'type', label: 'Typ' }, { field: 'department', label: 'Bereich' }, { field: 'inventory_number', label: 'Inventarnummer' }, { field: 'model', label: 'Modell' }, { field: 'status', label: 'Status' }, { field: 'assigned_to_username', label: 'Zugewiesen an' }, ].map(col => ( ))} {hasAnyMaintenanceDate && ( )} {pagedAssets.length === 0 ? ( ) : ( pagedAssets.map((asset) => { const today = new Date().toISOString().slice(0, 10); const maintenanceOverdue = asset.next_maintenance_date && asset.next_maintenance_date <= today; const dept = asset.department || 'IT'; const dc = DEPT_COLORS[dept] || DEPT_COLORS['IT']; return ( {hasAnyMaintenanceDate && ( )} ); }) )}
handleSort(col.field)} style={{ cursor: 'pointer', userSelect: 'none', whiteSpace: 'nowrap' }}> {col.label} handleSort('next_maintenance_date')} style={{ cursor: 'pointer', userSelect: 'none', whiteSpace: 'nowrap' }} title="Nächste Prüfung"> Prüfung Aktionen
Keine Assets gefunden
{asset.name} {asset.type} {dept} {asset.inventory_number || '—'} {asset.model || '—'} {{ verfuegbar: 'Verfügbar', zugewiesen: 'Zugewiesen', inaktiv: 'Inaktiv', beschaedigt: 'Beschädigt' }[asset.status] || asset.status} {asset.assigned_to_username || '—'} {asset.next_maintenance_date ? ( {maintenanceOverdue && '⚠️ '} {new Date(asset.next_maintenance_date).toLocaleDateString('de-DE')} ) : '—'}
{/* Pagination */} {totalPages > 1 && (
{((currentPage - 1) * PAGE_SIZE) + 1}–{Math.min(currentPage * PAGE_SIZE, sorted.length)} von {sorted.length} Assets
{Array.from({ length: totalPages }, (_, i) => i + 1) .filter(p => p === 1 || p === totalPages || Math.abs(p - currentPage) <= 1) .reduce((acc, p, idx, arr) => { if (idx > 0 && p - arr[idx - 1] > 1) acc.push('...'); acc.push(p); return acc; }, []) .map((p, i) => p === '...' ? : ) }
)} {/* Asset Modal */} {showAssetModal && ( )} {/* Assign Modal */} {showAssignModal && ( )} {/* Label Modal */} {labelModal && (
setLabelModal(null)}>
e.stopPropagation()}>

Label drucken

{labelModal.name} — {labelModal.serial_number}

{labelCopies}
)} {/* CSV Import Modal */} {showCsvImport && (
{ setShowCsvImport(false); setCsvFile(null); setCsvPreview(null); }}>
e.stopPropagation()}>

⬆️ Assets per CSV importieren

CSV-Spalten (Semikolon oder Komma getrennt):
Name;Typ;Seriennummer;Modell;Status;Kaufdatum;TeamViewer-ID

{csvPreview && (

Vorschau ({csvPreview.total} Assets erkannt, erste 5):

{csvPreview.headers.map(h => )} {csvPreview.rows.map((row, i) => ( {csvPreview.headers.map(h => )} ))}
{h}
{row[h]}
)}
)} {/* Actions Dropdown (fixed position to escape overflow) */} {openMenuId !== null && (() => { const asset = filteredAssets.find(a => a.id === openMenuId); if (!asset) return null; return (
e.stopPropagation()} > {[ { label: '🔍 Prüfprotokoll', action: () => { setInspectionAsset(asset); setOpenMenuId(null); } }, { label: '🏷️ Label', action: () => { setLabelModal(asset); setLabelCopies(1); setLabelSize('medium'); setOpenMenuId(null); } }, canModifyAssets() && { label: '✏️ Bearbeiten', action: () => { handleEdit(asset); setOpenMenuId(null); } }, canModifyAssets() && asset.status === 'verfuegbar' && { label: '👤 Zuweisen', action: () => { handleAssign(asset); setOpenMenuId(null); } }, canModifyAssets() && asset.status === 'zugewiesen' && { label: '🔓 Freigeben', action: () => { handleUnassign(asset); setOpenMenuId(null); } }, canModifyAssets() && asset.status === 'zugewiesen' && { label: '📄 Übergabeprotokoll', action: () => { assetService.printHandoverProtocol(asset.id).catch(() => toast.error('Fehler')); setOpenMenuId(null); } }, canModifyAssets() && { label: '🗑️ Löschen', action: () => { handleDelete(asset.id); setOpenMenuId(null); }, danger: true }, ].filter(Boolean).map((item, i) => ( ))}
); })()} {/* Inspection Modal */} {inspectionAsset && (
setInspectionAsset(null)}>
e.stopPropagation()} style={{ maxWidth: 640 }}>

Prüfprotokoll – {inspectionAsset.name}

)}
); }; export default AssetsPage;