main page

This commit is contained in:
2025-12-17 17:55:13 +01:00
commit 7fb446c53a
8943 changed files with 1209030 additions and 0 deletions

View File

@@ -0,0 +1,333 @@
import { useState } from 'react'
import { FaTimes } from 'react-icons/fa'
const TICKET_TYPES = [
'Home Office', 'Holidays', 'Trip', 'Supportrequest', 'Change Request',
'Maintenance', 'Project', 'Controlling', 'Development', 'Documentation',
'Meeting/Conference', 'IT Management', 'IT Security', 'Procurement',
'Rollout', 'Emergency Call', 'Other Services'
]
const SYSTEMS = [
'Account View', 'Client', 'Cofano', 'Credentials', 'Diamant', 'Docuware',
'EDI', 'eMail', 'Employee', 'Invoice', 'LBase', 'Medical Office', 'Network',
'O365', 'PDF Viewer', 'Printer', 'Reports', 'Server', 'Time Tracking',
'TK', 'TOS', 'Vivendi NG', 'VGM', '(W)LAN', '(W)WAN', 'WOMS', 'n/a'
]
const RESPONSE_LEVELS = [
'USER', 'KEY USER', 'Helpdesk', 'Support', 'Admin', 'FS/FE', '24/7',
'TECH MGMT', 'Backoffice', 'BUSI MGMT', 'n/a'
]
const SERVICE_TYPES = ['Remote', 'On Site', 'Off Site']
const PRIORITIES = [
{ value: 0, label: 'None' },
{ value: 1, label: 'Low' },
{ value: 2, label: 'Medium' },
{ value: 3, label: 'High' },
{ value: 4, label: 'Critical' }
]
const today = new Date().toLocaleDateString('de-DE')
export default function CreateTicketModal({ isOpen, onClose, onCreate, customers = [] }) {
const [formData, setFormData] = useState({
customerId: '',
type: 'Supportrequest',
systemType: '',
responseLevel: '',
serviceType: 'Remote',
priority: 1,
topic: '',
requestedBy: '',
requestedFor: '',
startDate: today,
startTime: '',
deadline: today,
endTime: '',
estimate: '30',
mailCopyTo: '',
sendNotification: false,
details: ''
})
const [loading, setLoading] = useState(false)
const handleChange = (field, value) => {
setFormData(prev => ({ ...prev, [field]: value }))
}
const handleSubmit = async (e) => {
e.preventDefault()
setLoading(true)
try {
await onCreate(formData)
onClose()
setFormData({
customerId: '',
type: 'Supportrequest',
systemType: '',
responseLevel: '',
serviceType: 'Remote',
priority: 1,
topic: '',
requestedBy: '',
requestedFor: '',
startDate: today,
startTime: '',
deadline: today,
endTime: '',
estimate: '30',
mailCopyTo: '',
sendNotification: false,
details: ''
})
} catch (error) {
console.error('Error creating ticket:', error)
} finally {
setLoading(false)
}
}
if (!isOpen) return null
return (
<div className="overlay">
<span className="overlay-close" onClick={onClose}>
<FaTimes />
</span>
<div className="overlay-content">
<h2 className="mb-2">Create New Ticket</h2>
<form onSubmit={handleSubmit}>
<div className="row">
<div className="col col-6">
<div className="form-group">
<label className="form-label">Customer ID</label>
<select
className="form-control"
value={formData.customerId}
onChange={(e) => handleChange('customerId', e.target.value)}
required
>
<option value="">Affected Customer</option>
{customers.map(c => (
<option key={c.id} value={c.id}>({c.code}) {c.name}</option>
))}
</select>
</div>
<div className="form-group">
<label className="form-label">Work Order Type</label>
<select
className="form-control"
value={formData.type}
onChange={(e) => handleChange('type', e.target.value)}
>
{TICKET_TYPES.map(type => (
<option key={type} value={type}>{type}</option>
))}
</select>
</div>
<div className="form-group">
<label className="form-label">Affected System</label>
<select
className="form-control"
value={formData.systemType}
onChange={(e) => handleChange('systemType', e.target.value)}
required
>
<option value="">Affected System</option>
{SYSTEMS.map(sys => (
<option key={sys} value={sys}>{sys}</option>
))}
</select>
</div>
<div className="form-group">
<label className="form-label">Response Level</label>
<select
className="form-control"
value={formData.responseLevel}
onChange={(e) => handleChange('responseLevel', e.target.value)}
>
<option value="">Response Level</option>
{RESPONSE_LEVELS.map(level => (
<option key={level} value={level}>{level}</option>
))}
</select>
</div>
<div className="form-group">
<label className="form-label">Service Type</label>
<select
className="form-control"
value={formData.serviceType}
onChange={(e) => handleChange('serviceType', e.target.value)}
>
{SERVICE_TYPES.map(type => (
<option key={type} value={type}>{type}</option>
))}
</select>
</div>
<div className="form-group">
<label className="form-label">Priority</label>
<select
className="form-control"
value={formData.priority}
onChange={(e) => handleChange('priority', parseInt(e.target.value))}
required
>
<option value="">Priority Level</option>
{PRIORITIES.map(p => (
<option key={p.value} value={p.value}>{p.label}</option>
))}
</select>
</div>
</div>
<div className="col col-6">
<div className="form-group">
<label className="form-label">Topic</label>
<input
type="text"
className="form-control"
placeholder="Topic"
value={formData.topic}
onChange={(e) => handleChange('topic', e.target.value)}
required
/>
</div>
<div className="form-group">
<label className="form-label">Requested by</label>
<input
type="text"
className="form-control"
placeholder="Name"
value={formData.requestedBy}
onChange={(e) => handleChange('requestedBy', e.target.value)}
required
/>
</div>
<div className="form-group">
<label className="form-label">Requested for</label>
<input
type="text"
className="form-control"
placeholder="Name"
value={formData.requestedFor}
onChange={(e) => handleChange('requestedFor', e.target.value)}
/>
</div>
<div className="form-group">
<label className="form-label">Start Date & Time</label>
<div style={{ display: 'flex', gap: '8px' }}>
<input
type="text"
className="form-control"
placeholder="dd.mm.yyyy"
value={formData.startDate}
onChange={(e) => handleChange('startDate', e.target.value)}
style={{ flex: 1 }}
/>
<input
type="text"
className="form-control"
placeholder="hhmm"
value={formData.startTime}
onChange={(e) => handleChange('startTime', e.target.value)}
style={{ flex: 1 }}
/>
</div>
</div>
<div className="form-group">
<label className="form-label">End Date & Time (Deadline)</label>
<div style={{ display: 'flex', gap: '8px' }}>
<input
type="text"
className="form-control"
placeholder="dd.mm.yyyy"
value={formData.deadline}
onChange={(e) => handleChange('deadline', e.target.value)}
style={{ flex: 1 }}
/>
<input
type="text"
className="form-control"
placeholder="hhmm"
value={formData.endTime}
onChange={(e) => handleChange('endTime', e.target.value)}
style={{ flex: 1 }}
/>
</div>
</div>
<div className="form-group">
<label className="form-label">Workload Estimate</label>
<input
type="text"
className="form-control"
placeholder="xx minutes"
value={formData.estimate}
onChange={(e) => handleChange('estimate', e.target.value)}
/>
</div>
<div className="form-group">
<label className="form-label">Notification Recipient(s)</label>
<input
type="text"
className="form-control"
placeholder="name@domain.xx"
value={formData.mailCopyTo}
onChange={(e) => handleChange('mailCopyTo', e.target.value)}
/>
</div>
<div className="form-group">
<label style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
<input
type="checkbox"
checked={formData.sendNotification}
onChange={(e) => handleChange('sendNotification', e.target.checked)}
/>
Send Notification?
</label>
</div>
</div>
</div>
<div className="form-group">
<label className="form-label">Details</label>
<textarea
className="form-control"
rows={5}
placeholder="Details"
value={formData.details}
onChange={(e) => handleChange('details', e.target.value)}
/>
</div>
<div className="text-center mt-2">
<button
type="submit"
className="btn btn-dark"
disabled={loading}
>
{loading ? 'Creating...' : 'CREATE NOW'}
</button>
</div>
</form>
</div>
</div>
)
}

View File

@@ -0,0 +1,47 @@
import { FaUserPlus } from 'react-icons/fa6'
// Diese Liste sollte aus der Datenbank kommen
const EDITORS = [
{ id: 'CHLE', name: 'Christian Lehmann' },
{ id: 'DIBR', name: 'Dietmar Bruckauf' },
{ id: 'DOAR', name: 'Dominik Armata' },
{ id: 'GRVO', name: 'Gregor Vowinkel' },
{ id: 'HADW', name: 'Hasan Dwiko' },
{ id: 'JEDI', name: 'Jessica Diaz' },
{ id: 'KNSO', name: 'Kenso Grimm' },
{ id: 'LUPL', name: 'Lukas Placzek' },
{ id: 'NIKI', name: 'Nikita Gaidach' },
{ id: 'MARK', name: 'Marco Kobza' },
{ id: 'MABA', name: 'Markus Bauer' },
{ id: 'MATS', name: 'Maksim Tschetschjotkin' },
{ id: 'PASI', name: 'Pascal Siegfried' },
{ id: 'NICT', name: 'Nico Stegmann' },
{ id: 'MEQU', name: 'Melissa Quednau' },
{ id: 'SASC', name: 'Saskia Schmahl' },
{ id: 'CHPA', name: 'Christin Paulus' },
{ id: 'SOSC', name: 'Sonja Schulze' },
{ id: 'WAWA', name: 'Walter Wawer' },
{ id: 'YAFO', name: 'Yannick Föller' },
{ id: 'TODE', name: 'Tobias Decker' }
]
export default function EditorDropdown({ value, onChange }) {
return (
<div className="dropdown">
<button className="btn" style={{ background: 'inherit', color: 'inherit' }}>
{value || <FaUserPlus size={20} />}
</button>
<div className="dropdown-content">
{EDITORS.map(editor => (
<span
key={editor.id}
className="dropdown-item"
onClick={() => onChange(editor.id)}
>
{editor.name}
</span>
))}
</div>
</div>
)
}

View File

@@ -0,0 +1,96 @@
import { useState, useRef } from 'react'
import { FaTimes, FaSpinner } from 'react-icons/fa'
import { storage, BUCKET_ID, ID } from '../lib/appwrite'
export default function FileUploadModal({ isOpen, onClose, workorderId, onUploadComplete }) {
const [uploading, setUploading] = useState(false)
const [dragOver, setDragOver] = useState(false)
const fileInputRef = useRef(null)
const handleDrop = async (e) => {
e.preventDefault()
setDragOver(false)
const file = e.dataTransfer.files[0]
if (file) {
await uploadFile(file)
}
}
const handleFileSelect = async (e) => {
const file = e.target.files[0]
if (file) {
await uploadFile(file)
}
}
const uploadFile = async (file) => {
// Validate file type
const allowedTypes = ['application/pdf', 'image/gif', 'image/png', 'image/jpeg']
if (!allowedTypes.includes(file.type)) {
alert('Only PDF, GIF, PNG, and JPEG files are allowed.')
return
}
setUploading(true)
try {
const response = await storage.createFile(
BUCKET_ID,
ID.unique(),
file
)
onUploadComplete?.(response)
onClose()
} catch (error) {
console.error('Upload error:', error)
alert('Error uploading file: ' + error.message)
} finally {
setUploading(false)
}
}
if (!isOpen) return null
return (
<div className="overlay">
<span className="overlay-close" onClick={onClose}>
<FaTimes />
</span>
<div className="overlay-content">
<h2 className="text-center mb-2">UPLOAD AREA</h2>
<p className="mb-2">Upload Attachments for WOID {workorderId}</p>
<div
className={`drop-zone ${dragOver ? 'border-green' : ''}`}
onDrop={handleDrop}
onDragOver={(e) => { e.preventDefault(); setDragOver(true) }}
onDragLeave={() => setDragOver(false)}
onClick={() => fileInputRef.current?.click()}
>
{uploading ? (
<div>
<FaSpinner className="spinner" size={32} />
<p className="mt-1">Uploading file...</p>
</div>
) : (
<>
<p>Drop file here (.pdf, .gif, .png, .jpg, .jpeg)</p>
<p className="mt-1">or</p>
<button className="btn btn-dark mt-1">Select File</button>
</>
)}
</div>
<input
ref={fileInputRef}
type="file"
accept=".pdf,.gif,.png,.jpg,.jpeg"
style={{ display: 'none' }}
onChange={handleFileSelect}
/>
</div>
</div>
)
}

13
src/components/Footer.jsx Normal file
View File

@@ -0,0 +1,13 @@
export default function Footer() {
return (
<footer className="footer">
<p>
WOMS 2.0 - Work Order Management System |
Powered by <a href="https://appwrite.io" target="_blank" rel="noopener noreferrer">Appwrite</a> & React
</p>
<p className="text-small text-grey">
© {new Date().getFullYear()} - All rights reserved
</p>
</footer>
)
}

49
src/components/Navbar.jsx Normal file
View File

@@ -0,0 +1,49 @@
import { Link, useNavigate } from 'react-router-dom'
import { useAuth } from '../context/AuthContext'
import { FaUser } from 'react-icons/fa'
export default function Navbar() {
const { user, logout } = useAuth()
const navigate = useNavigate()
const handleLogout = async () => {
await logout()
navigate('/login')
}
return (
<nav className="navbar">
<div className="navbar-brand">
<span>NetWEB</span>
<img src="/logo.png" alt="" height="18" width="18" />
<span>Systems</span>
</div>
<ul className="navbar-nav">
<li><Link to="/tickets" className="nav-link">Tickets</Link></li>
<li><Link to="/assets" className="nav-link">Assets</Link></li>
<li><Link to="/dashboard" className="nav-link">Dashboard</Link></li>
<li><Link to="/planboard" className="nav-link">Planboard</Link></li>
<li><Link to="/projects" className="nav-link">Projects</Link></li>
<li><Link to="/reports" className="nav-link">Reports</Link></li>
<li><Link to="/docs" className="nav-link">Docs</Link></li>
</ul>
<div className="nav-right">
{user ? (
<button
onClick={handleLogout}
className="nav-link"
style={{ background: 'none', border: 'none', cursor: 'pointer' }}
>
<FaUser /> Logout, {user.name || user.email}
</button>
) : (
<Link to="/login" className="nav-link">
<FaUser /> Login
</Link>
)}
</div>
</nav>
)
}

View File

@@ -0,0 +1,36 @@
const PRIORITIES = [
{ value: 0, label: 'None' },
{ value: 1, label: 'Low' },
{ value: 2, label: 'Medium' },
{ value: 3, label: 'High' },
{ value: 4, label: 'Critical' }
]
const PRIORITY_LABELS = {
0: 'NONE',
1: 'LOW',
2: 'MEDIUM',
3: 'HIGH',
4: 'CRITICAL'
}
export default function PriorityDropdown({ value, onChange }) {
return (
<div className="dropdown">
<button className="btn" style={{ background: 'inherit', color: 'inherit' }}>
{PRIORITY_LABELS[value] || 'LOW'}
</button>
<div className="dropdown-content">
{PRIORITIES.map(priority => (
<span
key={priority.value}
className="dropdown-item"
onClick={() => onChange(priority.value)}
>
{priority.label}
</span>
))}
</div>
</div>
)
}

View File

@@ -0,0 +1,89 @@
import { FaTimes } from 'react-icons/fa'
import { format } from 'date-fns'
const STATUS_CLASSES = {
'Open': 'bg-red',
'Occupied': 'bg-blue-grey',
'Assigned': 'bg-teal',
'Awaiting': 'bg-amber',
'Closed': 'bg-grey'
}
const PRIORITY_CLASSES = {
0: 'bg-blue',
1: 'bg-green',
2: 'bg-amber',
3: 'bg-orange',
4: 'bg-red'
}
const PRIORITY_LABELS = {
0: 'NONE',
1: 'LOW',
2: 'MEDIUM',
3: 'HIGH',
4: 'CRITICAL'
}
export default function QuickOverviewModal({ isOpen, onClose, workorders = [] }) {
if (!isOpen) return null
return (
<div className="overlay">
<span className="overlay-close" onClick={onClose}>
<FaTimes />
</span>
<div className="overlay-content">
<h3 className="mb-2">WOMS Ticket Quick Overview</h3>
<table className="table" style={{ background: '#fff', color: '#000' }}>
<thead>
<tr className="bg-dark-grey text-white">
<th>WOID</th>
<th>Created</th>
<th>Type</th>
<th>System</th>
<th>CID & CLO</th>
<th>Requested by</th>
<th>Topic</th>
<th className="text-center">Priority</th>
<th className="text-right">Status</th>
<th>Editor</th>
</tr>
</thead>
<tbody>
{workorders.map(wo => (
<tr key={wo.$id} className="bg-white">
<td className="bg-dark-grey text-white text-small">
{wo.woid || wo.$id?.slice(-5)}
</td>
<td className="bg-light-grey text-small">
{format(new Date(wo.$createdAt || wo.createdAt), 'dd.MM.yyyy, HH:mm')}h
</td>
<td className="bg-light-grey text-small">{wo.type}</td>
<td className="bg-light-grey text-small">{wo.systemType || 'n/a'}</td>
<td className="text-small">{wo.customerCode} {wo.customerLocation}</td>
<td className="text-small">{wo.requestedBy}</td>
<td className="text-small">{wo.topic}</td>
<td className={`text-small text-center ${PRIORITY_CLASSES[wo.priority]}`}>
{PRIORITY_LABELS[wo.priority]}
</td>
<td className={`text-small text-right ${STATUS_CLASSES[wo.status]}`}>
{wo.status?.toUpperCase()}
</td>
<td className={`text-small ${STATUS_CLASSES[wo.status]}`}>
{wo.assignedTo || '-'}
</td>
</tr>
))}
</tbody>
</table>
<p className="mt-2 text-center">
Summary: Listed a total of {workorders.length} Workorders!
<br />EOL =)
</p>
</div>
</div>
)
}

View File

@@ -0,0 +1,32 @@
const RESPONSE_LEVELS = [
{ value: 'KEY USER', label: 'Key User' },
{ value: 'Helpdesk', label: '1st Level' },
{ value: 'Support', label: '2nd Level' },
{ value: 'Admin', label: '3rd Level' },
{ value: 'FS/FE', label: 'FS/FE' },
{ value: '24/7', label: '24/7' },
{ value: 'TECH MGMT', label: 'Tech Mgmt' },
{ value: 'Backoffice', label: 'Backoffice' },
{ value: 'BUSI MGMT', label: 'Busi Mgmt' }
]
export default function ResponseDropdown({ value, onChange }) {
return (
<div className="dropdown">
<button className="btn" style={{ background: 'inherit', color: 'inherit' }}>
{value?.toUpperCase() || 'BACKOFFICE'}
</button>
<div className="dropdown-content">
{RESPONSE_LEVELS.map(level => (
<span
key={level.value}
className="dropdown-item"
onClick={() => onChange(level.value)}
>
{level.label}
</span>
))}
</div>
</div>
)
}

View File

@@ -0,0 +1,33 @@
const STATUSES = [
'Open',
'Occupied',
'Assigned',
'Awaiting',
'Added Info',
'In Test',
'Halted',
'Aborted',
'Cancelled',
'Closed'
]
export default function StatusDropdown({ value, onChange }) {
return (
<div className="dropdown">
<button className="btn" style={{ background: 'inherit', color: 'inherit' }}>
{value?.toUpperCase() || 'OPEN'}
</button>
<div className="dropdown-content">
{STATUSES.map(status => (
<span
key={status}
className="dropdown-item"
onClick={() => onChange(status)}
>
{status}
</span>
))}
</div>
</div>
)
}

View File

@@ -0,0 +1,177 @@
import { useState } from 'react'
import { FaCaretDown } from 'react-icons/fa6'
const TICKET_TYPES = [
'Home Office', 'Holidays', 'Trip', 'Supportrequest', 'Change Request',
'Maintenance', 'Project', 'Controlling', 'Development', 'Documentation',
'Meeting/Conference', 'IT Management', 'IT Security', 'Procurement',
'Rollout', 'Emergency Call', 'Other Services'
]
const SYSTEMS = [
'Account View', 'Client', 'Cofano', 'Credentials', 'Diamant', 'Docuware',
'EDI', 'eMail', 'Employee', 'Invoice', 'LBase', 'Medical Office', 'Network',
'O365', 'PDF Viewer', 'Printer', 'Reports', 'Server', 'Time Tracking',
'TK', 'TOS', 'Vivendi NG', 'VGM', '(W)LAN', '(W)WAN', 'WOMS', 'n/a'
]
const STATUSES = [
'Open', 'Occupied', 'Assigned', 'Awaiting', 'Added Info',
'In Test', 'Halted', 'Aborted', 'Cancelled', 'Closed'
]
const PRIORITIES = [
{ value: 0, label: 'None' },
{ value: 1, label: 'Low' },
{ value: 2, label: 'Medium' },
{ value: 3, label: 'High' },
{ value: 4, label: 'Critical' }
]
export default function TicketFilters({ filters, onChange, onApply }) {
const [localFilters, setLocalFilters] = useState(filters)
const handleCheckboxChange = (category, value) => {
const current = localFilters[category] || []
const updated = current.includes(value)
? current.filter(v => v !== value)
: [...current, value]
setLocalFilters({ ...localFilters, [category]: updated })
}
const handleInputChange = (field, value) => {
setLocalFilters({ ...localFilters, [field]: value })
}
const handleApply = () => {
onChange(localFilters)
onApply()
}
return (
<tr className="bg-dark-grey text-white">
<td className="text-center">
<input
type="text"
placeholder="WOID"
className="form-control"
style={{ width: '80px', background: '#616161', color: '#fff', border: 'none' }}
value={localFilters.woid || ''}
onChange={(e) => handleInputChange('woid', e.target.value)}
/>
</td>
<td>
<input
type="text"
placeholder="Created"
className="form-control"
style={{ width: '100px', background: '#616161', color: '#fff', border: 'none' }}
value={localFilters.createdDate || ''}
onChange={(e) => handleInputChange('createdDate', e.target.value)}
/>
</td>
<td>
<div className="dropdown">
<button className="btn btn-dark">
Type / Location <FaCaretDown />
</button>
<div className="dropdown-content" style={{ maxHeight: '300px', overflowY: 'auto' }}>
{TICKET_TYPES.map(type => (
<label key={type} className="dropdown-item" style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
<input
type="checkbox"
checked={(localFilters.type || []).includes(type)}
onChange={() => handleCheckboxChange('type', type)}
/>
{type}
</label>
))}
</div>
</div>
</td>
<td>
<div className="dropdown">
<button className="btn btn-dark">
System <FaCaretDown />
</button>
<div className="dropdown-content" style={{ maxHeight: '300px', overflowY: 'auto' }}>
{SYSTEMS.map(system => (
<label key={system} className="dropdown-item" style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
<input
type="checkbox"
checked={(localFilters.system || []).includes(system)}
onChange={() => handleCheckboxChange('system', system)}
/>
{system}
</label>
))}
</div>
</div>
</td>
<td>
<input
type="text"
placeholder="Customer or Location"
className="form-control"
style={{ width: '150px', background: '#616161', color: '#fff', border: 'none' }}
value={localFilters.customer || ''}
onChange={(e) => handleInputChange('customer', e.target.value)}
/>
</td>
<td>
<input
type="text"
placeholder="User or Topic"
className="form-control"
style={{ width: '150px', background: '#616161', color: '#fff', border: 'none' }}
value={localFilters.userTopic || ''}
onChange={(e) => handleInputChange('userTopic', e.target.value)}
/>
</td>
<td>
<div className="dropdown">
<button className="btn btn-dark">
Status <FaCaretDown />
</button>
<div className="dropdown-content">
{STATUSES.map(status => (
<label key={status} className="dropdown-item" style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
<input
type="checkbox"
checked={(localFilters.status || []).includes(status)}
onChange={() => handleCheckboxChange('status', status)}
/>
{status}
</label>
))}
</div>
</div>
</td>
<td>
<div className="dropdown">
<button className="btn btn-dark">
Priority <FaCaretDown />
</button>
<div className="dropdown-content">
{PRIORITIES.map(p => (
<label key={p.value} className="dropdown-item" style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
<input
type="checkbox"
checked={(localFilters.priority || []).includes(p.value)}
onChange={() => handleCheckboxChange('priority', p.value)}
/>
{p.label}
</label>
))}
</div>
</div>
</td>
<td className="text-center">Approval</td>
<td className="text-center">
<button className="btn btn-green" onClick={handleApply}>
Apply!
</button>
</td>
</tr>
)
}

View File

@@ -0,0 +1,171 @@
import { useState } from 'react'
import { FaLock, FaLockOpen, FaPlay, FaStop, FaTruck, FaSackDollar, FaUserGear } from 'react-icons/fa6'
import { formatDistanceToNow, format } from 'date-fns'
import { de } from 'date-fns/locale'
import StatusDropdown from './StatusDropdown'
import PriorityDropdown from './PriorityDropdown'
import EditorDropdown from './EditorDropdown'
import ResponseDropdown from './ResponseDropdown'
const PRIORITY_CLASSES = {
0: 'priority-none',
1: 'priority-low',
2: 'priority-medium',
3: 'priority-high',
4: 'priority-critical'
}
const PRIORITY_LABELS = {
0: 'NONE',
1: 'LOW',
2: 'MEDIUM',
3: 'HIGH',
4: 'CRITICAL'
}
const STATUS_CLASSES = {
'Open': 'status-open',
'Occupied': 'status-occupied',
'Assigned': 'status-assigned',
'Awaiting': 'status-awaiting',
'Closed': 'status-closed'
}
const APPROVAL_ICONS = {
'pending': FaUserGear,
'approved': FaSackDollar,
'quote': FaSackDollar,
'shipping': FaTruck
}
export default function TicketRow({ ticket, onUpdate, onExpand }) {
const [expanded, setExpanded] = useState(false)
const [locked, setLocked] = useState(true)
const createdAt = new Date(ticket.$createdAt || ticket.createdAt)
const elapsed = formatDistanceToNow(createdAt, { locale: de })
const handleStatusChange = (newStatus) => {
onUpdate(ticket.$id, { status: newStatus })
}
const handlePriorityChange = (newPriority) => {
onUpdate(ticket.$id, { priority: newPriority })
}
const handleEditorChange = (newEditor) => {
onUpdate(ticket.$id, { assignedTo: newEditor })
}
const handleResponseChange = (newResponse) => {
onUpdate(ticket.$id, { responseLevel: newResponse })
}
const toggleLock = () => {
if (locked) {
setExpanded(true)
onExpand?.(ticket.$id)
} else {
setExpanded(false)
}
setLocked(!locked)
}
const ApprovalIcon = APPROVAL_ICONS[ticket.approvalStatus] || FaUserGear
return (
<>
<tr className="ticket-row">
<td className="ticket-id" rowSpan={2}>
<div>{ticket.woid || ticket.$id?.slice(-5)}</div>
<div className="ticket-time">{elapsed}</div>
</td>
<td className="ticket-info" rowSpan={2}>
<strong>{format(createdAt, 'dd.MM.yyyy')}</strong>
</td>
<td className="ticket-info" rowSpan={2}>
<strong>{ticket.type}</strong>
<br />
{ticket.serviceType || 'Remote'}
<br />
<FaPlay className="text-green" /> {ticket.startDate || format(createdAt, 'dd.MM.yyyy')}
<br />
<FaStop className="text-red" /> {ticket.deadline || '-'}
</td>
<td className="ticket-info" rowSpan={2}>
<strong>{ticket.systemType || 'n/a'}</strong>
</td>
<td rowSpan={2}>
<strong>{ticket.customerName || 'Unknown'}</strong>
<br />
{ticket.customerLocation || ''}
</td>
<td rowSpan={2}>
<strong>{ticket.requestedBy || '-'}</strong>
<br />
{ticket.requestedFor && <>Requested for: {ticket.requestedFor}<br /></>}
{ticket.topic}
</td>
<td className={`text-center ${STATUS_CLASSES[ticket.status] || 'status-open'}`}>
<StatusDropdown
value={ticket.status}
onChange={handleStatusChange}
/>
</td>
<td className={`text-center ${PRIORITY_CLASSES[ticket.priority] || 'priority-low'}`}>
<PriorityDropdown
value={ticket.priority}
onChange={handlePriorityChange}
/>
</td>
<td
className={`text-center ${ticket.approvalStatus === 'approved' ? 'bg-green' : 'bg-yellow'}`}
rowSpan={2}
style={{ verticalAlign: 'middle' }}
>
<ApprovalIcon size={24} />
</td>
<td
className="bg-dark-grey text-center text-white"
rowSpan={2}
style={{ verticalAlign: 'middle', cursor: 'pointer' }}
onClick={toggleLock}
>
{locked ? <FaLock size={24} /> : <FaLockOpen size={24} />}
</td>
</tr>
<tr className="ticket-row">
<td className={`text-center ${STATUS_CLASSES[ticket.status] || 'status-open'}`}>
<EditorDropdown
value={ticket.assignedTo}
onChange={handleEditorChange}
/>
</td>
<td className={`text-center ${PRIORITY_CLASSES[ticket.priority] || 'priority-low'}`}>
<ResponseDropdown
value={ticket.responseLevel}
onChange={handleResponseChange}
/>
</td>
</tr>
{expanded && (
<>
<tr>
<td colSpan={10} className="p-2">
<div className="card">
<div className="card-header">Details - WOID {ticket.woid || ticket.$id}</div>
<div className="card-body">
<p><strong>Beschreibung:</strong></p>
<p>{ticket.details || 'Keine Details vorhanden.'}</p>
</div>
</div>
</td>
</tr>
</>
)}
<tr className="spacer">
<td colSpan={10} style={{ height: '8px', background: '#fff' }}></td>
</tr>
</>
)
}