- InvoiceNinja-Rechnungen pro Kunde/Ticket anzeigen, verknuepfen und aus Ticket heraus erstellen (integrationsApi, useInvoices, InvoicePanel) - Paperless-ngx: Ticket-Uploads automatisch synchronisieren und Kunden-/ Ticket-Dokumente durchsuchen/anzeigen (DocumentsPanel, FileUploadModal) - Projekt-/Preview-Karte mit Status, Provisioning und letztem Commit - Einheitliches Ticket-Detail mit Tabs (Details/Arbeitsblaetter/Rechnungen/ Dokumente/Projekt) + zugehoerige Styles Aufruf des Integrations-Backends same-origin per Appwrite-JWT. Co-authored-by: Cursor <cursoragent@cursor.com>
125 lines
3.6 KiB
React
125 lines
3.6 KiB
React
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>
|
|
)
|
|
}
|