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

71
src/App.jsx Normal file
View File

@@ -0,0 +1,71 @@
import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom'
import { AuthProvider, useAuth } from './context/AuthContext'
import Navbar from './components/Navbar'
import Footer from './components/Footer'
import LoginPage from './pages/LoginPage'
import TicketsPage from './pages/TicketsPage'
import DashboardPage from './pages/DashboardPage'
import AssetsPage from './pages/AssetsPage'
import PlanboardPage from './pages/PlanboardPage'
import ProjectsPage from './pages/ProjectsPage'
import ReportsPage from './pages/ReportsPage'
import DocsPage from './pages/DocsPage'
function ProtectedRoute({ children }) {
const { user, loading } = useAuth()
if (loading) {
return (
<div className="text-center p-4">
<div className="spinner"></div>
<p>Loading...</p>
</div>
)
}
if (!user) {
return <Navigate to="/login" replace />
}
return children
}
function AppRoutes() {
const { user } = useAuth()
return (
<Routes>
<Route path="/login" element={user ? <Navigate to="/tickets" replace /> : <LoginPage />} />
<Route path="/" element={<Navigate to="/tickets" replace />} />
<Route path="/tickets" element={<ProtectedRoute><TicketsPage /></ProtectedRoute>} />
<Route path="/dashboard" element={<ProtectedRoute><DashboardPage /></ProtectedRoute>} />
<Route path="/assets" element={<ProtectedRoute><AssetsPage /></ProtectedRoute>} />
<Route path="/planboard" element={<ProtectedRoute><PlanboardPage /></ProtectedRoute>} />
<Route path="/projects" element={<ProtectedRoute><ProjectsPage /></ProtectedRoute>} />
<Route path="/reports" element={<ProtectedRoute><ReportsPage /></ProtectedRoute>} />
<Route path="/docs" element={<ProtectedRoute><DocsPage /></ProtectedRoute>} />
</Routes>
)
}
function AppContent() {
const { user } = useAuth()
return (
<>
{user && <Navbar />}
<AppRoutes />
{user && <Footer />}
</>
)
}
export default function App() {
return (
<BrowserRouter>
<AuthProvider>
<AppContent />
</AuthProvider>
</BrowserRouter>
)
}

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>
</>
)
}

102
src/context/AuthContext.jsx Normal file
View File

@@ -0,0 +1,102 @@
import { createContext, useContext, useState, useEffect } from 'react'
import { account } from '../lib/appwrite'
const AuthContext = createContext()
// Demo mode when Appwrite is not configured
const DEMO_MODE = !import.meta.env.VITE_APPWRITE_PROJECT_ID
export function AuthProvider({ children }) {
const [user, setUser] = useState(null)
const [loading, setLoading] = useState(true)
useEffect(() => {
checkUser()
}, [])
async function checkUser() {
if (DEMO_MODE) {
// Check localStorage for demo session
const demoUser = localStorage.getItem('demo_user')
if (demoUser) {
setUser(JSON.parse(demoUser))
}
setLoading(false)
return
}
try {
const session = await account.get()
setUser(session)
} catch (error) {
setUser(null)
} finally {
setLoading(false)
}
}
async function login(email, password) {
if (DEMO_MODE) {
// Demo login - accept any credentials
const demoUser = { $id: 'demo', email, name: email.split('@')[0] }
localStorage.setItem('demo_user', JSON.stringify(demoUser))
setUser(demoUser)
return { success: true }
}
try {
await account.createEmailPasswordSession(email, password)
await checkUser()
return { success: true }
} catch (error) {
return { success: false, error: error.message }
}
}
async function logout() {
if (DEMO_MODE) {
localStorage.removeItem('demo_user')
setUser(null)
return
}
try {
await account.deleteSession('current')
setUser(null)
} catch (error) {
console.error('Logout error:', error)
}
}
async function register(email, password, name) {
if (DEMO_MODE) {
return login(email, password)
}
try {
await account.create('unique()', email, password, name)
await login(email, password)
return { success: true }
} catch (error) {
return { success: false, error: error.message }
}
}
const value = {
user,
loading,
login,
logout,
register
}
return (
<AuthContext.Provider value={value}>
{children}
</AuthContext.Provider>
)
}
export function useAuth() {
return useContext(AuthContext)
}

162
src/hooks/useWorkorders.js Normal file
View File

@@ -0,0 +1,162 @@
import { useState, useEffect, useCallback } from 'react'
import { databases, DATABASE_ID, COLLECTIONS, Query, ID } from '../lib/appwrite'
const DEMO_MODE = !import.meta.env.VITE_APPWRITE_PROJECT_ID
// Demo data for testing without Appwrite
const DEMO_WORKORDERS = [
{ $id: '1', title: 'Server Wartung', description: 'Monatliche Wartung', status: 'Open', priority: 2, type: 'Maintenance', customerName: 'Kunde A', assignedName: 'Max M.', response: 'Onsite', $createdAt: new Date().toISOString() },
{ $id: '2', title: 'Netzwerk Problem', description: 'WLAN funktioniert nicht', status: 'Occupied', priority: 3, type: 'Support', customerName: 'Kunde B', assignedName: 'Lisa S.', response: 'Remote', $createdAt: new Date().toISOString() },
{ $id: '3', title: 'Software Installation', description: 'Office 365 Setup', status: 'Assigned', priority: 1, type: 'Installation', customerName: 'Kunde C', assignedName: 'Tom K.', response: 'Onsite', $createdAt: new Date().toISOString() },
{ $id: '4', title: 'Drucker defekt', description: 'Papierstau', status: 'Awaiting', priority: 2, type: 'Hardware', customerName: 'Kunde D', assignedName: '', response: 'Pickup', $createdAt: new Date().toISOString() },
{ $id: '5', title: 'Kritischer Serverausfall', description: 'Produktionsserver down', status: 'Open', priority: 4, type: 'Emergency', customerName: 'Kunde E', assignedName: 'Max M.', response: 'Onsite', $createdAt: new Date().toISOString() },
]
export function useWorkorders(filters = {}) {
const [workorders, setWorkorders] = useState([])
const [loading, setLoading] = useState(true)
const [error, setError] = useState(null)
const fetchWorkorders = useCallback(async () => {
setLoading(true)
if (DEMO_MODE) {
// Filter demo data
let filtered = [...DEMO_WORKORDERS]
if (filters.status?.length > 0) {
filtered = filtered.filter(wo => filters.status.includes(wo.status))
}
if (filters.priority?.length > 0) {
filtered = filtered.filter(wo => filters.priority.includes(wo.priority))
}
if (filters.limit) {
filtered = filtered.slice(0, filters.limit)
}
setWorkorders(filtered)
setLoading(false)
return
}
try {
const queries = [Query.orderDesc('$createdAt')]
if (filters.limit) {
queries.push(Query.limit(filters.limit))
}
if (filters.status && filters.status.length > 0) {
queries.push(Query.equal('status', filters.status))
}
if (filters.type && filters.type.length > 0) {
queries.push(Query.equal('type', filters.type))
}
if (filters.priority && filters.priority.length > 0) {
queries.push(Query.equal('priority', filters.priority))
}
if (filters.customerId) {
queries.push(Query.equal('customerId', filters.customerId))
}
if (filters.assignedTo) {
queries.push(Query.equal('assignedTo', filters.assignedTo))
}
const response = await databases.listDocuments(
DATABASE_ID,
COLLECTIONS.WORKORDERS,
queries
)
setWorkorders(response.documents)
setError(null)
} catch (err) {
setError(err.message)
console.error('Error fetching workorders:', err)
} finally {
setLoading(false)
}
}, [filters])
useEffect(() => {
fetchWorkorders()
}, [fetchWorkorders])
const createWorkorder = async (data) => {
if (DEMO_MODE) {
const newWo = { ...data, $id: Date.now().toString(), status: 'Open', $createdAt: new Date().toISOString() }
setWorkorders(prev => [newWo, ...prev])
return { success: true, data: newWo }
}
try {
const response = await databases.createDocument(
DATABASE_ID,
COLLECTIONS.WORKORDERS,
ID.unique(),
{
...data,
status: 'Open',
createdAt: new Date().toISOString()
}
)
setWorkorders(prev => [response, ...prev])
return { success: true, data: response }
} catch (err) {
return { success: false, error: err.message }
}
}
const updateWorkorder = async (id, data) => {
if (DEMO_MODE) {
setWorkorders(prev => prev.map(wo => wo.$id === id ? { ...wo, ...data } : wo))
return { success: true }
}
try {
const response = await databases.updateDocument(
DATABASE_ID,
COLLECTIONS.WORKORDERS,
id,
data
)
setWorkorders(prev =>
prev.map(wo => wo.$id === id ? response : wo)
)
return { success: true, data: response }
} catch (err) {
return { success: false, error: err.message }
}
}
const deleteWorkorder = async (id) => {
if (DEMO_MODE) {
setWorkorders(prev => prev.filter(wo => wo.$id !== id))
return { success: true }
}
try {
await databases.deleteDocument(
DATABASE_ID,
COLLECTIONS.WORKORDERS,
id
)
setWorkorders(prev => prev.filter(wo => wo.$id !== id))
return { success: true }
} catch (err) {
return { success: false, error: err.message }
}
}
return {
workorders,
loading,
error,
refresh: fetchWorkorders,
createWorkorder,
updateWorkorder,
deleteWorkorder
}
}

25
src/lib/appwrite.js Normal file
View File

@@ -0,0 +1,25 @@
import { Client, Account, Databases, Storage, ID, Query } from 'appwrite'
const client = new Client()
.setEndpoint(import.meta.env.VITE_APPWRITE_ENDPOINT || 'https://cloud.appwrite.io/v1')
.setProject(import.meta.env.VITE_APPWRITE_PROJECT_ID || '')
export const account = new Account(client)
export const databases = new Databases(client)
export const storage = new Storage(client)
export const DATABASE_ID = import.meta.env.VITE_APPWRITE_DATABASE_ID || 'woms-database'
// Collection IDs
export const COLLECTIONS = {
WORKORDERS: 'workorders',
WORKSHEETS: 'worksheets',
CUSTOMERS: 'customers',
USERS: 'users',
ATTACHMENTS: 'attachments'
}
export const BUCKET_ID = 'woms-attachments'
// Helper functions
export { ID, Query }

10
src/main.jsx Normal file
View File

@@ -0,0 +1,10 @@
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App'
import './styles/global.css'
ReactDOM.createRoot(document.getElementById('root')).render(
<React.StrictMode>
<App />
</React.StrictMode>
)

47
src/pages/AssetsPage.jsx Normal file
View File

@@ -0,0 +1,47 @@
import { useState } from 'react'
import { FaServer, FaDesktop, FaPrint, FaNetworkWired } from 'react-icons/fa6'
export default function AssetsPage() {
const [searchTerm, setSearchTerm] = useState('')
// Placeholder data - would come from Appwrite in production
const assetCategories = [
{ icon: FaServer, name: 'Servers', count: 0 },
{ icon: FaDesktop, name: 'Workstations', count: 0 },
{ icon: FaPrint, name: 'Printers', count: 0 },
{ icon: FaNetworkWired, name: 'Network Devices', count: 0 }
]
return (
<div className="main-content">
<header className="text-center mb-2">
<h2>Assets Management</h2>
</header>
<div className="search-bar mb-2">
<input
type="text"
placeholder="Search assets..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="input"
style={{ width: '100%', maxWidth: '400px' }}
/>
</div>
<div className="assets-grid">
{assetCategories.map(({ icon: Icon, name, count }) => (
<div key={name} className="asset-card">
<Icon size={48} className="text-teal" />
<h4>{name}</h4>
<span className="asset-count">{count}</span>
</div>
))}
</div>
<div className="text-center mt-4 text-grey">
<p>Asset management module - Connect to Appwrite to manage your IT assets.</p>
</div>
</div>
)
}

View File

@@ -0,0 +1,73 @@
import { useState, useEffect } from 'react'
import { databases, DATABASE_ID, COLLECTIONS, Query } from '../lib/appwrite'
import { FaTicket, FaClipboardList, FaClock, FaCircleCheck } from 'react-icons/fa6'
export default function DashboardPage() {
const [stats, setStats] = useState({
totalTickets: 0,
openTickets: 0,
closedTickets: 0,
pendingTickets: 0
})
const [loading, setLoading] = useState(true)
useEffect(() => {
async function fetchStats() {
try {
const [total, open, closed, pending] = await Promise.all([
databases.listDocuments(DATABASE_ID, COLLECTIONS.WORKORDERS, [Query.limit(1)]),
databases.listDocuments(DATABASE_ID, COLLECTIONS.WORKORDERS, [Query.equal('status', 'Open'), Query.limit(1)]),
databases.listDocuments(DATABASE_ID, COLLECTIONS.WORKORDERS, [Query.equal('status', 'Closed'), Query.limit(1)]),
databases.listDocuments(DATABASE_ID, COLLECTIONS.WORKORDERS, [Query.equal('status', 'Awaiting'), Query.limit(1)])
])
setStats({
totalTickets: total.total,
openTickets: open.total,
closedTickets: closed.total,
pendingTickets: pending.total
})
} catch (err) {
console.error('Error fetching stats:', err)
} finally {
setLoading(false)
}
}
fetchStats()
}, [])
const StatCard = ({ icon: Icon, label, value, color }) => (
<div className="stat-card" style={{ borderLeft: `4px solid ${color}` }}>
<div className="stat-icon" style={{ color }}>
<Icon size={32} />
</div>
<div className="stat-info">
<span className="stat-value">{loading ? '...' : value}</span>
<span className="stat-label">{label}</span>
</div>
</div>
)
return (
<div className="main-content">
<header className="text-center mb-2">
<h2>Dashboard</h2>
</header>
<div className="stats-grid">
<StatCard icon={FaTicket} label="Total Tickets" value={stats.totalTickets} color="#2196F3" />
<StatCard icon={FaClipboardList} label="Open Tickets" value={stats.openTickets} color="#4CAF50" />
<StatCard icon={FaClock} label="Pending" value={stats.pendingTickets} color="#FF9800" />
<StatCard icon={FaCircleCheck} label="Closed" value={stats.closedTickets} color="#9E9E9E" />
</div>
<div className="dashboard-section mt-4">
<h3>Quick Actions</h3>
<div className="quick-actions">
<a href="/tickets" className="btn btn-teal">View All Tickets</a>
<a href="/reports" className="btn btn-dark">Generate Report</a>
</div>
</div>
</div>
)
}

66
src/pages/DocsPage.jsx Normal file
View File

@@ -0,0 +1,66 @@
import { FaBook, FaCircleQuestion, FaKeyboard } from 'react-icons/fa6'
export default function DocsPage() {
const sections = [
{
icon: FaBook,
title: 'Getting Started',
content: 'Learn how to create and manage work orders in WOMS 2.0.'
},
{
icon: FaCircleQuestion,
title: 'FAQ',
content: 'Frequently asked questions about the system.'
},
{
icon: FaKeyboard,
title: 'Keyboard Shortcuts',
content: 'Speed up your workflow with keyboard shortcuts.'
}
]
const shortcuts = [
{ key: 'N', action: 'New Ticket' },
{ key: 'F', action: 'Focus Search' },
{ key: 'R', action: 'Refresh List' },
{ key: 'Esc', action: 'Close Modal' }
]
return (
<div className="main-content">
<header className="text-center mb-2">
<h2>Documentation</h2>
</header>
<div className="docs-grid">
{sections.map(({ icon: Icon, title, content }) => (
<div key={title} className="doc-card">
<Icon size={32} className="text-teal" />
<h4>{title}</h4>
<p className="text-grey">{content}</p>
</div>
))}
</div>
<div className="shortcuts-section mt-4">
<h3 className="text-center">Keyboard Shortcuts</h3>
<table className="table" style={{ maxWidth: '400px', margin: '0 auto' }}>
<thead>
<tr>
<th>Key</th>
<th>Action</th>
</tr>
</thead>
<tbody>
{shortcuts.map(({ key, action }) => (
<tr key={key}>
<td><kbd>{key}</kbd></td>
<td>{action}</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
)
}

85
src/pages/LoginPage.jsx Normal file
View File

@@ -0,0 +1,85 @@
import { useState } from 'react'
import { useNavigate } from 'react-router-dom'
import { useAuth } from '../context/AuthContext'
export default function LoginPage() {
const [email, setEmail] = useState('')
const [password, setPassword] = useState('')
const [error, setError] = useState('')
const [loading, setLoading] = useState(false)
const { login } = useAuth()
const navigate = useNavigate()
const handleSubmit = async (e) => {
e.preventDefault()
setError('')
setLoading(true)
const result = await login(email, password)
if (result.success) {
navigate('/tickets')
} else {
setError(result.error || 'Login failed')
}
setLoading(false)
}
return (
<div style={{
minHeight: '100vh',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
background: '#f1f1f1'
}}>
<div className="card" style={{ width: '400px' }}>
<div className="card-header text-center">
<h2>NetWEB Systems WOMS 2.0</h2>
</div>
<div className="card-body">
<form onSubmit={handleSubmit}>
{error && (
<div className="bg-red text-white p-1 mb-2" style={{ borderRadius: '4px' }}>
{error}
</div>
)}
<div className="form-group">
<label className="form-label">Email</label>
<input
type="email"
className="form-control"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
/>
</div>
<div className="form-group">
<label className="form-label">Password</label>
<input
type="password"
className="form-control"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
/>
</div>
<button
type="submit"
className="btn btn-green"
style={{ width: '100%' }}
disabled={loading}
>
{loading ? 'Logging in...' : 'Login'}
</button>
</form>
</div>
</div>
</div>
)
}

View File

@@ -0,0 +1,46 @@
import { useState } from 'react'
import { format, addDays, startOfWeek } from 'date-fns'
export default function PlanboardPage() {
const [currentWeek, setCurrentWeek] = useState(startOfWeek(new Date(), { weekStartsOn: 1 }))
const weekDays = Array.from({ length: 7 }, (_, i) => addDays(currentWeek, i))
const navigateWeek = (direction) => {
setCurrentWeek(prev => addDays(prev, direction * 7))
}
return (
<div className="main-content">
<header className="text-center mb-2">
<h2>Planboard</h2>
</header>
<div className="planboard-nav text-center mb-2">
<button className="btn btn-dark" onClick={() => navigateWeek(-1)}> Previous Week</button>
<span className="mx-2">
{format(currentWeek, 'dd.MM.yyyy')} - {format(addDays(currentWeek, 6), 'dd.MM.yyyy')}
</span>
<button className="btn btn-dark" onClick={() => navigateWeek(1)}>Next Week </button>
</div>
<div className="planboard-grid">
{weekDays.map(day => (
<div key={day.toISOString()} className="planboard-day">
<div className="day-header">
<strong>{format(day, 'EEEE')}</strong>
<span>{format(day, 'dd.MM')}</span>
</div>
<div className="day-content">
<p className="text-grey text-small">No tasks scheduled</p>
</div>
</div>
))}
</div>
<div className="text-center mt-4 text-grey">
<p>Drag and drop tickets to schedule them on the planboard.</p>
</div>
</div>
)
}

View File

@@ -0,0 +1,39 @@
import { useState } from 'react'
import { FaFolder, FaPlus } from 'react-icons/fa6'
export default function ProjectsPage() {
const [projects] = useState([])
return (
<div className="main-content">
<header className="text-center mb-2">
<h2>Projects</h2>
</header>
<div className="text-center mb-2">
<button className="btn btn-teal">
<FaPlus /> New Project
</button>
</div>
{projects.length === 0 ? (
<div className="text-center p-4">
<FaFolder size={64} className="text-grey" />
<p className="text-grey mt-2">No projects yet. Create your first project to get started.</p>
</div>
) : (
<div className="projects-grid">
{projects.map(project => (
<div key={project.$id} className="project-card">
<h4>{project.name}</h4>
<p className="text-grey">{project.description}</p>
<div className="project-meta">
<span>{project.ticketCount || 0} tickets</span>
</div>
</div>
))}
</div>
)}
</div>
)
}

82
src/pages/ReportsPage.jsx Normal file
View File

@@ -0,0 +1,82 @@
import { useState } from 'react'
import { format, subDays } from 'date-fns'
import { FaFileExport, FaChartBar } from 'react-icons/fa6'
export default function ReportsPage() {
const [dateRange, setDateRange] = useState({
from: format(subDays(new Date(), 30), 'yyyy-MM-dd'),
to: format(new Date(), 'yyyy-MM-dd')
})
const [reportType, setReportType] = useState('tickets')
const reportTypes = [
{ value: 'tickets', label: 'Ticket Summary' },
{ value: 'performance', label: 'Performance Report' },
{ value: 'customer', label: 'Customer Report' },
{ value: 'technician', label: 'Technician Report' }
]
const handleGenerateReport = () => {
// Would generate report from Appwrite data
alert(`Generating ${reportType} report from ${dateRange.from} to ${dateRange.to}`)
}
return (
<div className="main-content">
<header className="text-center mb-2">
<h2>Reports</h2>
</header>
<div className="report-filters card p-2">
<div className="filter-row">
<label>
Report Type:
<select
value={reportType}
onChange={(e) => setReportType(e.target.value)}
className="input ml-1"
>
{reportTypes.map(type => (
<option key={type.value} value={type.value}>{type.label}</option>
))}
</select>
</label>
</div>
<div className="filter-row mt-2">
<label>
From:
<input
type="date"
value={dateRange.from}
onChange={(e) => setDateRange(prev => ({ ...prev, from: e.target.value }))}
className="input ml-1"
/>
</label>
<label className="ml-2">
To:
<input
type="date"
value={dateRange.to}
onChange={(e) => setDateRange(prev => ({ ...prev, to: e.target.value }))}
className="input ml-1"
/>
</label>
</div>
<div className="text-center mt-2">
<button className="btn btn-teal" onClick={handleGenerateReport}>
<FaChartBar /> Generate Report
</button>
<button className="btn btn-dark ml-1">
<FaFileExport /> Export PDF
</button>
</div>
</div>
<div className="text-center mt-4 text-grey">
<p>Select report parameters and click Generate to view your report.</p>
</div>
</div>
)
}

216
src/pages/TicketsPage.jsx Normal file
View File

@@ -0,0 +1,216 @@
import { useState } from 'react'
import { format } from 'date-fns'
import { FaAngleDown, FaSpinner } from 'react-icons/fa6'
import { useWorkorders } from '../hooks/useWorkorders'
import TicketRow from '../components/TicketRow'
import TicketFilters from '../components/TicketFilters'
import CreateTicketModal from '../components/CreateTicketModal'
import QuickOverviewModal from '../components/QuickOverviewModal'
export default function TicketsPage() {
const [limit, setLimit] = useState(10)
const [filters, setFilters] = useState({
status: ['Open', 'Occupied', 'Assigned', 'Awaiting', 'Added Info'],
type: [],
priority: [],
limit: 10
})
const { workorders, loading, error, refresh, updateWorkorder, createWorkorder } = useWorkorders(filters)
const [showCreateModal, setShowCreateModal] = useState(false)
const [showOverviewModal, setShowOverviewModal] = useState(false)
const handleFilterChange = (newFilters) => {
setFilters({ ...newFilters, limit })
}
const handleApplyFilters = () => {
refresh()
}
const handleLimitChange = (e) => {
const newLimit = parseInt(e.target.value)
setLimit(newLimit)
setFilters(prev => ({ ...prev, limit: newLimit }))
}
const handleUpdate = async (id, data) => {
await updateWorkorder(id, data)
}
const handleCreate = async (data) => {
const result = await createWorkorder(data)
if (result.success) {
setShowCreateModal(false)
}
return result
}
const handleLoadMore = () => {
setLimit(prev => prev + 10)
setFilters(prev => ({ ...prev, limit: prev.limit + 10 }))
}
return (
<div className="main-content">
<header className="text-center mb-2">
<h2>Active Tickets Overview</h2>
</header>
<p className="text-center text-grey">
Work Order loading limit is set to <span className="text-xlarge">{limit}</span>.
Reduce value to increase reload speed.
</p>
<p className="text-center text-grey text-small">
Last page reload: {format(new Date(), 'dd.MM.yyyy, HH:mm:ss')}
</p>
<hr className="mb-2" />
<table className="table table-hover">
<tbody>
<TicketFilters
filters={filters}
onChange={handleFilterChange}
onApply={handleApplyFilters}
/>
<tr>
<td colSpan={10} className="bg-dark-grey">
<div className="slider-container text-center">
<input
type="range"
min="5"
max="50"
value={limit}
className="slider"
onChange={handleLimitChange}
/>
</div>
</td>
</tr>
<tr>
<td colSpan={10} className="text-center p-1">
<button
className="btn btn-teal"
onClick={() => setFilters(prev => ({ ...prev, type: ['Procurement'] }))}
>
Procurements
</button>
{' '}
<button
className="btn btn-teal"
onClick={() => setFilters(prev => ({ ...prev, priority: [4] }))}
>
Criticals
</button>
{' '}
<button
className="btn btn-teal"
onClick={() => setFilters(prev => ({ ...prev, priority: [3] }))}
>
Highs
</button>
{' '}
<button
className="btn btn-teal"
onClick={() => { setLimit(10); setFilters(prev => ({ ...prev, limit: 10 })) }}
>
10
</button>
{' '}
<button
className="btn btn-teal"
onClick={() => { setLimit(25); setFilters(prev => ({ ...prev, limit: 25 })) }}
>
25
</button>
</td>
</tr>
<tr>
<td colSpan={10} className="text-center p-1">
<button
className="btn btn-dark"
onClick={() => setShowCreateModal(true)}
>
CREATE NEW TICKET
</button>
{' '}
<button
className="btn btn-dark"
onClick={() => setShowOverviewModal(true)}
>
QUICK OVERVIEW
</button>
</td>
</tr>
<tr className="spacer">
<td colSpan={10} style={{ height: '16px' }}></td>
</tr>
{loading ? (
<tr>
<td colSpan={10} className="text-center p-2">
<FaSpinner className="spinner" size={32} />
<p>Loading...</p>
</td>
</tr>
) : error ? (
<tr>
<td colSpan={10} className="text-center p-2 text-red">
Error: {error}
</td>
</tr>
) : workorders.length === 0 ? (
<tr>
<td colSpan={10} className="text-center p-2">
No tickets found matching your filters.
</td>
</tr>
) : (
workorders.map(ticket => (
<TicketRow
key={ticket.$id}
ticket={ticket}
onUpdate={handleUpdate}
/>
))
)}
</tbody>
</table>
{workorders.length > 0 && workorders.length >= limit && (
<div className="text-center mt-2">
<button
className="btn"
style={{ background: 'none', border: 'none' }}
onClick={handleLoadMore}
>
<FaAngleDown size={48} />
</button>
</div>
)}
<p className="text-center text-grey mt-2">
Summary: Listed a total of {workorders.length} Workorders.
<br />EOL =)
</p>
<CreateTicketModal
isOpen={showCreateModal}
onClose={() => setShowCreateModal(false)}
onCreate={handleCreate}
/>
<QuickOverviewModal
isOpen={showOverviewModal}
onClose={() => setShowOverviewModal(false)}
workorders={workorders}
/>
</div>
)
}

372
src/styles/global.css Normal file
View File

@@ -0,0 +1,372 @@
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Lato', sans-serif;
background-color: #fff;
color: #000;
}
/* Color Classes */
.bg-dark-grey { background-color: #616161; }
.bg-light-grey { background-color: #f1f1f1; }
.bg-white { background-color: #fff; }
.bg-black { background-color: #000; }
.bg-green { background-color: #4CAF50; }
.bg-teal { background-color: #009688; }
.bg-blue { background-color: #2196F3; }
.bg-blue-grey { background-color: #607D8B; }
.bg-red { background-color: #f44336; }
.bg-yellow { background-color: #ffeb3b; }
.bg-amber { background-color: #ffc107; }
.bg-orange { background-color: #ff9800; }
.text-white { color: #fff; }
.text-black { color: #000; }
.text-grey { color: #9e9e9e; }
.text-green { color: #4CAF50; }
.text-red { color: #f44336; }
/* Layout */
.container {
max-width: 1400px;
margin: 0 auto;
padding: 0 16px;
}
.main-content {
margin-top: 64px;
padding: 16px;
}
/* Navbar */
.navbar {
position: fixed;
top: 0;
left: 0;
right: 0;
background: #000;
z-index: 199;
display: flex;
align-items: center;
padding: 0 8px;
}
.navbar-brand {
padding: 12px 16px;
color: #fff;
display: flex;
align-items: center;
gap: 8px;
}
.navbar-nav {
display: flex;
list-style: none;
}
.nav-link {
padding: 16px 24px;
color: #fff;
text-decoration: none;
transition: background 0.3s;
}
.nav-link:hover {
background: #4CAF50;
}
.nav-right {
margin-left: auto;
}
/* Buttons */
.btn {
padding: 8px 16px;
border: none;
cursor: pointer;
font-size: 14px;
transition: all 0.3s;
}
.btn-dark {
background: #616161;
color: #fff;
}
.btn-dark:hover {
background: #4CAF50;
}
.btn-green {
background: #4CAF50;
color: #fff;
}
.btn-teal {
background: #009688;
color: #fff;
}
/* Table */
.table {
width: 100%;
border-collapse: collapse;
}
.table th,
.table td {
padding: 8px 12px;
text-align: left;
border: 1px solid #ddd;
}
.table-hover tbody tr:hover {
background: #f5f5f5;
}
/* Cards */
.card {
background: #fff;
border: 1px solid #ddd;
border-radius: 4px;
margin-bottom: 16px;
}
.card-header {
padding: 12px 16px;
background: #616161;
color: #fff;
font-weight: bold;
}
.card-body {
padding: 16px;
}
/* Forms */
.form-control {
width: 100%;
padding: 8px 12px;
border: 1px solid #ccc;
border-radius: 4px;
font-size: 14px;
}
.form-control:focus {
outline: none;
border-color: #4CAF50;
}
.form-group {
margin-bottom: 16px;
}
.form-label {
display: block;
margin-bottom: 4px;
font-weight: 500;
}
select.form-control {
background: #616161;
color: #fff;
}
/* Grid */
.row {
display: flex;
flex-wrap: wrap;
margin: 0 -8px;
}
.col {
padding: 0 8px;
}
.col-6 { width: 50%; }
.col-12 { width: 100%; }
/* Utilities */
.text-center { text-align: center; }
.text-right { text-align: right; }
.text-small { font-size: 12px; }
.text-large { font-size: 18px; }
.text-xlarge { font-size: 24px; }
.mt-1 { margin-top: 8px; }
.mt-2 { margin-top: 16px; }
.mt-3 { margin-top: 24px; }
.mb-1 { margin-bottom: 8px; }
.mb-2 { margin-bottom: 16px; }
.p-1 { padding: 8px; }
.p-2 { padding: 16px; }
.hidden { display: none; }
/* Ticket Row */
.ticket-row {
border: 1px solid #ddd;
}
.ticket-id {
background: #616161;
color: #fff;
text-align: center;
vertical-align: middle;
font-size: 18px;
font-weight: bold;
padding: 16px;
}
.ticket-time {
font-size: 11px;
color: #ccc;
}
.ticket-info {
background: #f1f1f1;
font-size: 13px;
}
/* Status Colors */
.status-open { background: #4CAF50; color: #fff; }
.status-occupied { background: #607D8B; color: #fff; }
.status-assigned { background: #009688; color: #fff; }
.status-awaiting { background: #ff9800; color: #fff; }
.status-closed { background: #9e9e9e; color: #fff; }
/* Priority Colors */
.priority-none { background: #2196F3; color: #fff; }
.priority-low { background: #4CAF50; color: #fff; }
.priority-medium { background: #ffc107; color: #000; }
.priority-high { background: #ff9800; color: #fff; }
.priority-critical { background: #f44336; color: #fff; }
/* Dropdown */
.dropdown {
position: relative;
display: inline-block;
}
.dropdown-content {
display: none;
position: absolute;
background: #616161;
min-width: 160px;
box-shadow: 0 8px 16px rgba(0,0,0,0.2);
z-index: 100;
}
.dropdown:hover .dropdown-content {
display: block;
}
.dropdown-item {
padding: 8px 16px;
color: #fff;
cursor: pointer;
display: block;
}
.dropdown-item:hover {
background: #4CAF50;
}
/* Modal/Overlay */
.overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0,0,0,0.9);
z-index: 200;
overflow-y: auto;
padding: 60px 20px;
}
.overlay-close {
position: absolute;
top: 20px;
right: 45px;
font-size: 48px;
color: #fff;
cursor: pointer;
}
.overlay-content {
max-width: 1000px;
margin: 0 auto;
color: #fff;
}
/* Slider */
.slider-container {
width: 100%;
padding: 16px 0;
}
.slider {
width: 100%;
height: 10px;
background: #d3d3d3;
outline: none;
opacity: 0.7;
transition: opacity 0.2s;
-webkit-appearance: none;
}
.slider:hover {
opacity: 1;
}
.slider::-webkit-slider-thumb {
-webkit-appearance: none;
width: 10px;
height: 25px;
background: #4CAF50;
cursor: pointer;
}
/* File Upload */
.drop-zone {
background: #616161;
border: 5px dashed #999;
padding: 40px;
text-align: center;
color: #fff;
cursor: pointer;
}
.drop-zone:hover {
border-color: #4CAF50;
}
/* Spinner */
.spinner {
animation: spin 1s linear infinite;
}
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
/* Footer */
.footer {
background: #000;
color: #fff;
text-align: center;
padding: 24px;
margin-top: 32px;
}
/* Responsive */
@media (max-width: 768px) {
.col-6 { width: 100%; }
.navbar-nav { display: none; }
}