main page
This commit is contained in:
333
src/components/CreateTicketModal.jsx
Normal file
333
src/components/CreateTicketModal.jsx
Normal 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>
|
||||
)
|
||||
}
|
||||
47
src/components/EditorDropdown.jsx
Normal file
47
src/components/EditorDropdown.jsx
Normal 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>
|
||||
)
|
||||
}
|
||||
96
src/components/FileUploadModal.jsx
Normal file
96
src/components/FileUploadModal.jsx
Normal 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
13
src/components/Footer.jsx
Normal 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
49
src/components/Navbar.jsx
Normal 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>
|
||||
)
|
||||
}
|
||||
36
src/components/PriorityDropdown.jsx
Normal file
36
src/components/PriorityDropdown.jsx
Normal 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>
|
||||
)
|
||||
}
|
||||
89
src/components/QuickOverviewModal.jsx
Normal file
89
src/components/QuickOverviewModal.jsx
Normal 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>
|
||||
)
|
||||
}
|
||||
32
src/components/ResponseDropdown.jsx
Normal file
32
src/components/ResponseDropdown.jsx
Normal 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>
|
||||
)
|
||||
}
|
||||
33
src/components/StatusDropdown.jsx
Normal file
33
src/components/StatusDropdown.jsx
Normal 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>
|
||||
)
|
||||
}
|
||||
177
src/components/TicketFilters.jsx
Normal file
177
src/components/TicketFilters.jsx
Normal 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>
|
||||
)
|
||||
}
|
||||
171
src/components/TicketRow.jsx
Normal file
171
src/components/TicketRow.jsx
Normal 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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user