Dokumente und Paperless aus WOMS entfernt (Paket C)
Paperless enthält 0 Dokumente, der Upload wurde nie benutzt. Weg sind: die Seite Dokumente (alte Links führen zu den Tickets), der Menüpunkt, der Reiter Dokumente in Ticketkarte und Kundenseite, der Upload-Dialog (legte Dateien zusätzlich ungelesen im Appwrite-Bucket ab), das Feld "Paperless-Korrespondent" und das Abzeichen "Docs", auf dem Dashboard die Kachel Dokumente und "Dokument hochladen", im API-Client die Paperless-Aufrufe. Gespeicherte Daten bleiben unverändert; die Paperless-Routen im Backend ticket-integrations sind nicht angefasst. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
062d992b63
commit
34c4db0228
11 files changed
+6
-437
No files matched your search
@@ -14,7 +14,6 @@ import CustomersPage from './pages/CustomersPage'
|
||||
import LeadsPage from './pages/LeadsPage'
|
||||
import CustomerDetailPage from './pages/CustomerDetailPage'
|
||||
import FinancePage from './pages/FinancePage'
|
||||
import DocumentsPage from './pages/DocumentsPage'
|
||||
|
||||
function ProtectedRoute({ children }) {
|
||||
const { user, loading } = useAuth()
|
||||
@@ -47,7 +46,6 @@ function AppRoutes() {
|
||||
<Route path="/customers/:id" element={<ProtectedRoute><CustomerDetailPage /></ProtectedRoute>} />
|
||||
<Route path="/leads" element={<ProtectedRoute><LeadsPage /></ProtectedRoute>} />
|
||||
<Route path="/finance" element={<ProtectedRoute><FinancePage /></ProtectedRoute>} />
|
||||
<Route path="/documents" element={<ProtectedRoute><DocumentsPage /></ProtectedRoute>} />
|
||||
<Route path="/dashboard" element={<ProtectedRoute><DashboardPage /></ProtectedRoute>} />
|
||||
<Route path="/planboard" element={<ProtectedRoute><PlanboardPage /></ProtectedRoute>} />
|
||||
<Route path="/projects" element={<ProtectedRoute><ProjectsPage /></ProtectedRoute>} />
|
||||
|
||||
@@ -1,158 +0,0 @@
|
||||
import { useState, useEffect, useCallback } from 'react'
|
||||
import { FaFileLines, FaDownload, FaEye, FaSpinner, FaRotate, FaUpload, FaMagnifyingGlass } from 'react-icons/fa6'
|
||||
import { integrationsApi } from '../lib/integrationsApi'
|
||||
import FileUploadModal from './FileUploadModal'
|
||||
|
||||
const card = {
|
||||
background: 'rgba(45, 55, 72, 0.5)',
|
||||
borderRadius: '12px',
|
||||
padding: '16px',
|
||||
border: '1px solid rgba(16, 185, 129, 0.2)',
|
||||
}
|
||||
|
||||
export default function DocumentsPanel({ ticket, initialScope = 'ticket' }) {
|
||||
const [docs, setDocs] = useState([])
|
||||
const [count, setCount] = useState(0)
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [error, setError] = useState(null)
|
||||
const [query, setQuery] = useState('')
|
||||
const [scope, setScope] = useState(initialScope) // 'ticket' | 'customer'
|
||||
const [showUpload, setShowUpload] = useState(false)
|
||||
const [busyId, setBusyId] = useState(null)
|
||||
|
||||
const correspondent = ticket.customerName || ''
|
||||
const woid = ticket.woid || ''
|
||||
|
||||
const load = useCallback(async () => {
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
try {
|
||||
const params = { page_size: 30 }
|
||||
if (query) params.query = query
|
||||
if (scope === 'customer' && correspondent) params.correspondent = correspondent
|
||||
if (scope === 'ticket' && woid) params.tag = `WOID-${woid}`
|
||||
const data = await integrationsApi.listDocuments(params)
|
||||
setDocs(data.documents || [])
|
||||
setCount(data.count || 0)
|
||||
} catch (err) {
|
||||
setError(err.message)
|
||||
setDocs([])
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}, [query, scope, correspondent, woid])
|
||||
|
||||
useEffect(() => {
|
||||
load()
|
||||
}, [load])
|
||||
|
||||
const open = async (id, kind) => {
|
||||
setBusyId(id + kind)
|
||||
try {
|
||||
await integrationsApi.openDocument(id, kind)
|
||||
} catch (err) {
|
||||
alert('Dokument konnte nicht geoeffnet werden: ' + err.message)
|
||||
} finally {
|
||||
setBusyId(null)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={card}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '12px', flexWrap: 'wrap', gap: '8px' }}>
|
||||
<h5 style={{ color: 'var(--dark-text)', fontWeight: 'bold', margin: 0, fontSize: '16px' }}>
|
||||
Dokumente (Paperless)
|
||||
</h5>
|
||||
<div style={{ display: 'flex', gap: '8px' }}>
|
||||
<button className="btn btn-green" style={{ margin: 0 }} onClick={() => setShowUpload(true)}>
|
||||
<FaUpload /> Hochladen
|
||||
</button>
|
||||
<button className="btn" onClick={load} title="Aktualisieren"
|
||||
style={{ background: '#374151', color: 'white', border: 'none', padding: '6px 10px', borderRadius: '6px', cursor: 'pointer' }}>
|
||||
<FaRotate />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', gap: '8px', marginBottom: '12px', flexWrap: 'wrap' }}>
|
||||
<div style={{ display: 'flex', gap: '4px' }}>
|
||||
<button
|
||||
className={`btn ${scope === 'ticket' ? 'btn-green' : ''}`}
|
||||
style={{ margin: 0, background: scope === 'ticket' ? undefined : '#374151', color: 'white' }}
|
||||
onClick={() => setScope('ticket')}
|
||||
>
|
||||
Dieses Ticket
|
||||
</button>
|
||||
<button
|
||||
className={`btn ${scope === 'customer' ? 'btn-green' : ''}`}
|
||||
style={{ margin: 0, background: scope === 'customer' ? undefined : '#374151', color: 'white' }}
|
||||
onClick={() => setScope('customer')}
|
||||
disabled={!correspondent}
|
||||
>
|
||||
Ganzer Kunde
|
||||
</button>
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: '6px', flex: 1, minWidth: '180px' }}>
|
||||
<input
|
||||
className="form-control"
|
||||
style={{ margin: 0 }}
|
||||
placeholder="Volltextsuche..."
|
||||
value={query}
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
onKeyDown={(e) => e.key === 'Enter' && load()}
|
||||
/>
|
||||
<button className="btn btn-green" style={{ margin: 0 }} onClick={load}>
|
||||
<FaMagnifyingGlass />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{error && <div className="text-red" style={{ marginBottom: 8 }}>{error}</div>}
|
||||
|
||||
{loading ? (
|
||||
<p style={{ color: '#a0aec0' }}><FaSpinner className="spinner" /> Dokumente werden geladen...</p>
|
||||
) : docs.length === 0 ? (
|
||||
<p style={{ color: '#a0aec0', fontSize: '13px' }}>
|
||||
Keine Dokumente gefunden{scope === 'ticket' ? ' fuer dieses Ticket' : correspondent ? ` fuer ${correspondent}` : ''}.
|
||||
</p>
|
||||
) : (
|
||||
<>
|
||||
<p style={{ color: '#718096', fontSize: '12px', marginTop: 0 }}>{count} Dokument(e)</p>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
|
||||
{docs.map((d) => (
|
||||
<div key={d.id} style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '10px', background: 'rgba(0,0,0,0.15)', borderRadius: '8px' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '10px', minWidth: 0 }}>
|
||||
<FaFileLines style={{ color: '#10b981', flexShrink: 0 }} />
|
||||
<div style={{ minWidth: 0 }}>
|
||||
<div style={{ color: 'var(--dark-text)', fontWeight: 600, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{d.title}</div>
|
||||
<div style={{ color: '#718096', fontSize: '12px' }}>{d.created ? new Date(d.created).toLocaleDateString('de-DE') : ''}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: '6px', flexShrink: 0 }}>
|
||||
<button className="btn" title="Vorschau" disabled={busyId === d.id + 'preview'}
|
||||
style={{ background: '#3b82f6', color: 'white', border: 'none', padding: '6px 10px', borderRadius: '6px', cursor: 'pointer' }}
|
||||
onClick={() => open(d.id, 'preview')}>
|
||||
{busyId === d.id + 'preview' ? <FaSpinner className="spinner" /> : <FaEye />}
|
||||
</button>
|
||||
<button className="btn" title="Download" disabled={busyId === d.id + 'download'}
|
||||
style={{ background: '#374151', color: 'white', border: 'none', padding: '6px 10px', borderRadius: '6px', cursor: 'pointer' }}
|
||||
onClick={() => open(d.id, 'download')}>
|
||||
{busyId === d.id + 'download' ? <FaSpinner className="spinner" /> : <FaDownload />}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
<FileUploadModal
|
||||
isOpen={showUpload}
|
||||
onClose={() => setShowUpload(false)}
|
||||
workorderId={woid}
|
||||
ticket={ticket}
|
||||
onUploadComplete={() => setTimeout(load, 1500)}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,124 +0,0 @@
|
||||
import { useState, useRef } from 'react'
|
||||
import { FaTimes, FaSpinner } from 'react-icons/fa'
|
||||
import { storage, BUCKET_ID, ID } from '../lib/appwrite'
|
||||
import { integrationsApi } from '../lib/integrationsApi'
|
||||
|
||||
export default function FileUploadModal({ isOpen, onClose, workorderId, ticket, onUploadComplete }) {
|
||||
const [uploading, setUploading] = useState(false)
|
||||
const [dragOver, setDragOver] = useState(false)
|
||||
const [status, setStatus] = useState('')
|
||||
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)
|
||||
setStatus('Lade in Ablage hoch...')
|
||||
|
||||
try {
|
||||
const response = await storage.createFile(
|
||||
BUCKET_ID,
|
||||
ID.unique(),
|
||||
file
|
||||
)
|
||||
|
||||
// Automatisch nach Paperless-ngx schieben (wichtige Kundendokumente)
|
||||
const woid = ticket?.woid || workorderId
|
||||
const correspondent = ticket?.customerName || undefined
|
||||
if (correspondent || woid) {
|
||||
setStatus('Synchronisiere mit Paperless...')
|
||||
const tags = []
|
||||
if (correspondent) tags.push(correspondent)
|
||||
if (woid) tags.push(`WOID-${woid}`)
|
||||
tags.push('Ticket')
|
||||
try {
|
||||
await integrationsApi.uploadToPaperless({
|
||||
bucketId: BUCKET_ID,
|
||||
fileId: response.$id,
|
||||
filename: file.name,
|
||||
title: `${woid ? `[${woid}] ` : ''}${file.name}`,
|
||||
correspondent,
|
||||
tags,
|
||||
})
|
||||
} catch (e) {
|
||||
console.warn('Paperless-Sync fehlgeschlagen:', e.message)
|
||||
// Upload in Appwrite ist trotzdem erfolgt - nicht hart abbrechen
|
||||
}
|
||||
}
|
||||
|
||||
onUploadComplete?.(response)
|
||||
onClose()
|
||||
} catch (error) {
|
||||
console.error('Upload error:', error)
|
||||
alert('Error uploading file: ' + error.message)
|
||||
} finally {
|
||||
setUploading(false)
|
||||
setStatus('')
|
||||
}
|
||||
}
|
||||
|
||||
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">{status || '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>
|
||||
)
|
||||
}
|
||||
@@ -6,7 +6,6 @@ import {
|
||||
IconUsers,
|
||||
IconTargetArrow,
|
||||
IconReportMoney,
|
||||
IconFiles,
|
||||
IconFolders,
|
||||
IconCalendar,
|
||||
IconRoute,
|
||||
@@ -49,10 +48,6 @@ export default function Sidebar() {
|
||||
label: 'Finanzen',
|
||||
links: [{ label: 'Finanzen', href: '/finance', icon: <IconReportMoney /> }],
|
||||
},
|
||||
{
|
||||
label: 'Dokumente',
|
||||
links: [{ label: 'Dokumente', href: '/documents', icon: <IconFiles /> }],
|
||||
},
|
||||
{
|
||||
label: 'Auswertung',
|
||||
links: [
|
||||
|
||||
@@ -14,7 +14,6 @@ import TicketProjectsModal from './TicketProjectsModal'
|
||||
import TicketRoadmapPanel from './TicketRoadmapPanel'
|
||||
import ProjectContextPanel from './ProjectContextPanel'
|
||||
import InvoicePanel from './InvoicePanel'
|
||||
import DocumentsPanel from './DocumentsPanel'
|
||||
import Tabs from './ui/Tabs'
|
||||
import Badge from './ui/Badge'
|
||||
import { StatusPill, UrgentPill } from './ui/StatusPill'
|
||||
@@ -29,7 +28,6 @@ const TABS = [
|
||||
{ id: 'details', label: 'Details' },
|
||||
{ id: 'worksheets', label: 'Arbeitsblätter' },
|
||||
{ id: 'invoices', label: 'Rechnungen' },
|
||||
{ id: 'documents', label: 'Dokumente' },
|
||||
]
|
||||
|
||||
export default function TicketCard({ ticket, onUpdate }) {
|
||||
@@ -154,7 +152,6 @@ export default function TicketCard({ ticket, onUpdate }) {
|
||||
)}
|
||||
|
||||
{activeTab === 'invoices' && <InvoicePanel ticket={ticket} />}
|
||||
{activeTab === 'documents' && <DocumentsPanel ticket={ticket} />}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
+1
-4
@@ -1,4 +1,4 @@
|
||||
import { Client, Account, Databases, Storage, ID, Query } from 'appwrite'
|
||||
import { Client, Account, Databases, ID, Query } from 'appwrite'
|
||||
|
||||
/** Ticket-System Appwrite-Projekt (Fallback wenn keine .env beim Build) */
|
||||
const DEFAULT_PROJECT_ID = '6a1058610003c5a13a05'
|
||||
@@ -23,7 +23,6 @@ const client = new Client().setEndpoint(endpoint).setProject(projectId)
|
||||
|
||||
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'
|
||||
|
||||
@@ -43,8 +42,6 @@ export const COLLECTIONS = {
|
||||
|
||||
export const PROJECT_TICKET_TYPE = 'Project'
|
||||
|
||||
export const BUCKET_ID = import.meta.env.VITE_APPWRITE_BUCKET_ID || 'woms-attachments'
|
||||
|
||||
/** Prüft ob vermutlich eine Session existiert (cookieFallback oder Same-Origin-Cookie). */
|
||||
export function hasAppwriteSession() {
|
||||
if (typeof window === 'undefined' || !projectId) return false
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { account } from './appwrite'
|
||||
|
||||
/**
|
||||
* Client fuer das Integrations-Backend (InvoiceNinja + Paperless).
|
||||
* Client fuer das Integrations-Backend (InvoiceNinja).
|
||||
* Same-Origin unter /api/integrations, authentifiziert per Appwrite-JWT.
|
||||
*/
|
||||
const BASE = '/api/integrations'
|
||||
@@ -64,16 +64,6 @@ export const integrationsApi = {
|
||||
const blob = await callBlob(`/invoices/${encodeURIComponent(id)}/pdf`)
|
||||
await openBlobInNewTab(blob)
|
||||
},
|
||||
|
||||
// ---- Paperless ----
|
||||
listDocuments: (params = {}) => call(`/paperless/documents${qs(params)}`),
|
||||
ensureCorrespondent: (name) => call('/paperless/ensure-correspondent', { method: 'POST', body: { name } }),
|
||||
ensureTag: (name) => call('/paperless/ensure-tag', { method: 'POST', body: { name } }),
|
||||
uploadToPaperless: (payload) => call('/paperless/upload', { method: 'POST', body: payload }),
|
||||
async openDocument(id, kind = 'preview') {
|
||||
const blob = await callBlob(`/paperless/documents/${encodeURIComponent(id)}/${kind}`)
|
||||
await openBlobInNewTab(blob)
|
||||
},
|
||||
}
|
||||
|
||||
// Oeffentliche InvoiceNinja-URL (fuer Deeplinks ins Portal)
|
||||
|
||||
@@ -5,7 +5,6 @@ import { databases, DATABASE_ID, COLLECTIONS, Query } from '../lib/appwrite'
|
||||
import { PageHeader, Card, Button, Badge, Tabs, EmptyState, Field } from '../components/ui'
|
||||
import { StatusPill, UrgentPill } from '../components/ui/StatusPill'
|
||||
import InvoicePanel from '../components/InvoicePanel'
|
||||
import DocumentsPanel from '../components/DocumentsPanel'
|
||||
import AssignedProjectCard from '../components/AssignedProjectCard'
|
||||
import CopyableCredential, { getCustomerPortalPassword } from '../components/CopyableCredential'
|
||||
import { updateCustomerWithPortalAccess } from '../lib/customerAdminApi'
|
||||
@@ -18,7 +17,6 @@ const TABS = [
|
||||
{ id: 'overview', label: 'Uebersicht' },
|
||||
{ id: 'tickets', label: 'Tickets' },
|
||||
{ id: 'invoices', label: 'Rechnungen' },
|
||||
{ id: 'documents', label: 'Dokumente' },
|
||||
{ id: 'projects', label: 'Projekte' },
|
||||
]
|
||||
|
||||
@@ -92,7 +90,6 @@ export default function CustomerDetailPage() {
|
||||
<InvoicePanel ticket={syntheticTicket} />
|
||||
)
|
||||
)}
|
||||
{tab === 'documents' && <DocumentsPanel ticket={syntheticTicket} initialScope="customer" />}
|
||||
{tab === 'projects' && <CustomerProjects customerId={customer.$id} />}
|
||||
</div>
|
||||
)
|
||||
@@ -114,7 +111,6 @@ function OverviewTab({ customer, stage, onSaved }) {
|
||||
location: customer.location || '',
|
||||
notes: customer.notes || '',
|
||||
importantCustomer: !!customer.importantCustomer,
|
||||
paperlessCorrespondent: customer.paperlessCorrespondent || '',
|
||||
})
|
||||
const [busy, setBusy] = useState(false)
|
||||
const [msg, setMsg] = useState(null)
|
||||
@@ -186,9 +182,6 @@ function OverviewTab({ customer, stage, onSaved }) {
|
||||
<span className="muted">InvoiceNinja</span>
|
||||
{customer.invoiceNinjaClientId ? <Badge tone="ok" dot>verknuepft</Badge> : <Badge tone="muted">nicht verknuepft</Badge>}
|
||||
</div>
|
||||
<Field label="Paperless-Korrespondent" hint="Leer = Kundenname wird verwendet">
|
||||
<input className="form-control" value={form.paperlessCorrespondent} onChange={(e) => setForm({ ...form, paperlessCorrespondent: e.target.value })} placeholder={customer.name} />
|
||||
</Field>
|
||||
</Card>
|
||||
|
||||
<PortalAccessCard customer={customer} onSaved={onSaved} />
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useState, useMemo } from 'react'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { FaSpinner, FaPlus, FaStar, FaFileInvoiceDollar, FaFileLines } from 'react-icons/fa6'
|
||||
import { FaSpinner, FaPlus, FaStar, FaFileInvoiceDollar } from 'react-icons/fa6'
|
||||
import { useCustomers } from '../hooks/useCustomers'
|
||||
import { useAuth } from '../context/AuthContext'
|
||||
import { createLead, customerStage, isArchived } from '../lib/leads'
|
||||
@@ -92,7 +92,6 @@ export default function CustomersPage() {
|
||||
{st === 'customer' && <Badge tone="ok" dot>Fester Kunde</Badge>}
|
||||
{st === 'lost' && <Badge tone="muted">Abgesagt</Badge>}
|
||||
{c.invoiceNinjaClientId && <Badge tone="info"><FaFileInvoiceDollar /> Ninja</Badge>}
|
||||
{c.paperlessCorrespondent && <Badge tone="info"><FaFileLines /> Docs</Badge>}
|
||||
</div>
|
||||
</Link>
|
||||
)
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { databases, DATABASE_ID, COLLECTIONS, Query } from '../lib/appwrite'
|
||||
import { FaTicket, FaClipboardList, FaClock, FaCircleCheck, FaPlus, FaFileInvoiceDollar, FaUpload } from 'react-icons/fa6'
|
||||
import { integrationsApi, formatMoney } from '../lib/integrationsApi'
|
||||
import { FaTicket, FaClipboardList, FaClock, FaCircleCheck, FaPlus, FaFileInvoiceDollar } from 'react-icons/fa6'
|
||||
import { formatMoney } from '../lib/integrationsApi'
|
||||
import { useFinanceSummary } from '../hooks/useFinanceSummary'
|
||||
import { PageHeader, Card, Button, Kpi, EmptyState } from '../components/ui'
|
||||
import { StatusPill, UrgentPill } from '../components/ui/StatusPill'
|
||||
@@ -11,7 +11,6 @@ export default function DashboardPage() {
|
||||
const { summary, loading: finLoading } = useFinanceSummary()
|
||||
const [stats, setStats] = useState({ total: 0, open: 0, awaiting: 0, closed: 0 })
|
||||
const [recent, setRecent] = useState([])
|
||||
const [docCount, setDocCount] = useState(null)
|
||||
const [loading, setLoading] = useState(true)
|
||||
|
||||
useEffect(() => {
|
||||
@@ -33,10 +32,6 @@ export default function DashboardPage() {
|
||||
} finally {
|
||||
if (active) setLoading(false)
|
||||
}
|
||||
try {
|
||||
const d = await integrationsApi.listDocuments({ page_size: 1 })
|
||||
if (active) setDocCount(d.count)
|
||||
} catch { /* ignore */ }
|
||||
})()
|
||||
return () => { active = false }
|
||||
}, [])
|
||||
@@ -45,7 +40,7 @@ export default function DashboardPage() {
|
||||
<div className="page">
|
||||
<PageHeader
|
||||
title="Dashboard"
|
||||
subtitle="Ueberblick ueber Tickets, Finanzen und Dokumente"
|
||||
subtitle="Überblick über Tickets und Finanzen"
|
||||
actions={
|
||||
<>
|
||||
<Button variant="ghost" as={Link} to="/finance"><FaFileInvoiceDollar /> Finanzen</Button>
|
||||
@@ -65,7 +60,6 @@ export default function DashboardPage() {
|
||||
<Kpi label="Offener Betrag" value={finLoading ? '...' : formatMoney(summary?.outstanding)} sub={`${summary?.openCount ?? 0} offene Rechnungen`} accent="var(--warn)" />
|
||||
<Kpi label="Ueberfaellig" value={finLoading ? '...' : formatMoney(summary?.overdue)} sub={`${summary?.overdueCount ?? 0} ueberfaellig`} accent="var(--danger)" />
|
||||
<Kpi label="Bezahlt (Monat)" value={finLoading ? '...' : formatMoney(summary?.paidThisMonth)} accent="var(--ok)" />
|
||||
<Kpi label="Dokumente" value={docCount ?? '...'} accent="var(--info)" />
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(300px, 1fr))', gap: 16 }}>
|
||||
@@ -95,7 +89,6 @@ export default function DashboardPage() {
|
||||
<div className="flex flex-col gap-2">
|
||||
<Button as={Link} to="/tickets"><FaPlus /> Neues Ticket erstellen</Button>
|
||||
<Button as={Link} to="/finance"><FaFileInvoiceDollar /> Rechnung erstellen</Button>
|
||||
<Button as={Link} to="/documents"><FaUpload /> Dokument hochladen</Button>
|
||||
<Button as={Link} to="/customers" variant="ghost">Kunden verwalten</Button>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
@@ -1,111 +0,0 @@
|
||||
import { useState, useEffect, useCallback } from 'react'
|
||||
import { FaFileLines, FaEye, FaDownload, FaSpinner, FaMagnifyingGlass, FaRotate, FaUpload } from 'react-icons/fa6'
|
||||
import { integrationsApi } from '../lib/integrationsApi'
|
||||
import { useCustomers } from '../hooks/useCustomers'
|
||||
import { PageHeader, Card, Button, EmptyState } from '../components/ui'
|
||||
import FileUploadModal from '../components/FileUploadModal'
|
||||
|
||||
export default function DocumentsPage() {
|
||||
const { customers } = useCustomers()
|
||||
const [docs, setDocs] = useState([])
|
||||
const [count, setCount] = useState(0)
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [error, setError] = useState(null)
|
||||
const [query, setQuery] = useState('')
|
||||
const [correspondent, setCorrespondent] = useState('')
|
||||
const [busyId, setBusyId] = useState(null)
|
||||
const [showUpload, setShowUpload] = useState(false)
|
||||
|
||||
const load = useCallback(async () => {
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
try {
|
||||
const params = { page_size: 50 }
|
||||
if (query) params.query = query
|
||||
if (correspondent) params.correspondent = correspondent
|
||||
const data = await integrationsApi.listDocuments(params)
|
||||
setDocs(data.documents || [])
|
||||
setCount(data.count || 0)
|
||||
} catch (err) {
|
||||
setError(err.message)
|
||||
setDocs([])
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}, [query, correspondent])
|
||||
|
||||
useEffect(() => { load() }, [load])
|
||||
|
||||
const open = async (id, kind) => {
|
||||
setBusyId(id + kind)
|
||||
try { await integrationsApi.openDocument(id, kind) }
|
||||
catch (err) { alert('Fehler: ' + err.message) }
|
||||
finally { setBusyId(null) }
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="page">
|
||||
<PageHeader
|
||||
title="Dokumente"
|
||||
subtitle="Alle Belege und Kundendokumente aus Paperless-ngx"
|
||||
actions={
|
||||
<>
|
||||
<Button variant="ghost" onClick={load}><FaRotate /> Aktualisieren</Button>
|
||||
<Button variant="primary" onClick={() => setShowUpload(true)}><FaUpload /> Hochladen</Button>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
|
||||
<Card pad={false}>
|
||||
<div className="ui-card-head" style={{ gap: 12, flexWrap: 'wrap' }}>
|
||||
<select className="form-control" style={{ maxWidth: 240 }} value={correspondent} onChange={(e) => setCorrespondent(e.target.value)}>
|
||||
<option value="">Alle Kunden</option>
|
||||
{customers.map((c) => <option key={c.$id} value={c.name}>{c.name}</option>)}
|
||||
</select>
|
||||
<div className="flex gap-2 grow" style={{ minWidth: 220 }}>
|
||||
<input className="form-control" placeholder="Volltextsuche..." value={query} onChange={(e) => setQuery(e.target.value)} onKeyDown={(e) => e.key === 'Enter' && load()} />
|
||||
<Button variant="primary" onClick={load}><FaMagnifyingGlass /></Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="ui-card-body" style={{ padding: 0 }}>
|
||||
{loading ? (
|
||||
<div className="empty"><FaSpinner className="spinner" /></div>
|
||||
) : error ? (
|
||||
<div className="empty text-red">{error}</div>
|
||||
) : docs.length === 0 ? (
|
||||
<EmptyState icon={<FaFileLines />} title="Keine Dokumente" hint="Keine Dokumente gefunden." />
|
||||
) : (
|
||||
<>
|
||||
<div className="faint" style={{ padding: '8px 16px', fontSize: 12 }}>{count} Dokument(e)</div>
|
||||
<div className="list">
|
||||
{docs.map((d) => (
|
||||
<div key={d.id} className="list-row">
|
||||
<FaFileLines style={{ color: 'var(--accent)' }} />
|
||||
<div className="grow">
|
||||
<div className="text-ellipsis" style={{ fontWeight: 600 }}>{d.title}</div>
|
||||
<div className="muted">{d.created ? new Date(d.created).toLocaleDateString('de-DE') : ''}</div>
|
||||
</div>
|
||||
<Button size="sm" variant="ghost" disabled={busyId === d.id + 'preview'} onClick={() => open(d.id, 'preview')} title="Vorschau">
|
||||
{busyId === d.id + 'preview' ? <FaSpinner className="spinner" /> : <FaEye />}
|
||||
</Button>
|
||||
<Button size="sm" variant="ghost" disabled={busyId === d.id + 'download'} onClick={() => open(d.id, 'download')} title="Download">
|
||||
{busyId === d.id + 'download' ? <FaSpinner className="spinner" /> : <FaDownload />}
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<FileUploadModal
|
||||
isOpen={showUpload}
|
||||
onClose={() => setShowUpload(false)}
|
||||
workorderId=""
|
||||
ticket={correspondent ? { customerName: correspondent, woid: '' } : { customerName: '', woid: '' }}
|
||||
onUploadComplete={() => setTimeout(load, 1500)}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in new issue
Block a user