diff --git a/.env.example b/.env.example index 5d14b7f..fd5c5bf 100644 --- a/.env.example +++ b/.env.example @@ -6,6 +6,13 @@ VITE_APPWRITE_ENDPOINT=https://cloud.appwrite.io/v1 VITE_APPWRITE_PROJECT_ID=dein-projekt-id VITE_APPWRITE_DATABASE_ID=defekttrack_db +# Optional: Storage-Bucket-ID für Kommentar-Anhänge (Standard: defekttrack_anhaenge) +# VITE_APPWRITE_ATTACHMENTS_BUCKET_ID=defekttrack_anhaenge + +# Nur Dev: Wenn VITE_APPWRITE_ENDPOINT=http://localhost:5173/v1 — wohin Vite /v1 weiterleitet (Origin ohne /v1) +# Standard im Repo: https://appwrite.webklar.com — bei eigenem Server hier die Basis-URL eintragen. +# VITE_APPWRITE_PROXY_TARGET=https://appwrite.webklar.com + # Nur für das Setup-Skript (npm run setup) und API-Server – nicht im Frontend APPWRITE_ENDPOINT=https://cloud.appwrite.io/v1 APPWRITE_API_KEY=dein-api-key-hier diff --git a/package-lock.json b/package-lock.json index a1523e8..d7b9606 100644 --- a/package-lock.json +++ b/package-lock.json @@ -100,6 +100,7 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", @@ -1617,6 +1618,7 @@ "resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-1.3.0.tgz", "integrity": "sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw==", "license": "MIT", + "peer": true, "engines": { "node": "^14.21.3 || >=16" }, @@ -2362,6 +2364,7 @@ "integrity": "sha512-JuRQ9KXLEjaUNjTWpzuR231Z2WpIwczOkBEIvbHNCzQefFIT0L8IqE6NV6ULLyC1SI/i234JnDoMkfg+RjQj2g==", "devOptional": true, "license": "MIT", + "peer": true, "dependencies": { "csstype": "^3.0.2" } @@ -2427,6 +2430,7 @@ "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -2825,6 +2829,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -3943,6 +3948,7 @@ "integrity": "sha512-m1mM33o6dBUjxl2qb6wv6nGNwCAsns1eKtaQ4l/NPHeTvhiUPbtdfMyktxN4B3fgHIgsYh1VT3V9txblpQHq+g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", @@ -4226,6 +4232,7 @@ "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", "license": "MIT", + "peer": true, "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", @@ -4914,6 +4921,7 @@ "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.5.tgz", "integrity": "sha512-3qq+FUBtlTHhtYxbxheZgY8NIFnkkC/MR8u5TTsr7YZ3wixryQ3cCwn3iZbg8p8B88iDBBAYSfZDS75t8MN7Vg==", "license": "MIT", + "peer": true, "engines": { "node": ">=16.9.0" } @@ -6845,6 +6853,7 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -7085,6 +7094,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.0.0.tgz", "integrity": "sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -7094,6 +7104,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.0.0.tgz", "integrity": "sha512-4GV5sHFG0e/0AD4X+ySy6UJd3jVl1iNsNHdpad0qhABJ11twS3TTBnseqsKurKcsNqCEFeGL3uLpVChpIO3QfQ==", "license": "MIT", + "peer": true, "dependencies": { "scheduler": "^0.25.0" }, @@ -7217,6 +7228,7 @@ "version": "2.15.4", "resolved": "https://registry.npmjs.org/recharts/-/recharts-2.15.4.tgz", "integrity": "sha512-UT/q6fwS3c1dHbXv2uFgYJ9BMFHu3fwnd7AYZaEQhXuYQ4hgsxLvsUXzGdKeZrW5xopzDCvuA2N41WJ88I7zIw==", + "license": "MIT", "dependencies": { "clsx": "^2.0.0", "eventemitter3": "^4.0.1", @@ -8500,6 +8512,7 @@ "resolved": "https://registry.npmjs.org/vite/-/vite-6.1.1.tgz", "integrity": "sha512-4GgM54XrwRfrOp297aIYspIti66k56v16ZnqHvrIM7mG+HjDlAwS7p+Srr7J6fGvEdOJ5JcQ/D9T7HhtdXDTzA==", "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.24.2", "postcss": "^8.5.2", @@ -8890,6 +8903,7 @@ "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", "license": "MIT", + "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/package.json b/package.json index 2020e5e..7f71d80 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ "lint": "eslint .", "preview": "vite preview", "setup": "node scripts/setup-appwrite.js", + "setup:storage": "node scripts/create-attachments-bucket.js", "seed:dummy": "node scripts/seed-dummy-data.js" }, "dependencies": { diff --git a/scripts/create-attachments-bucket.js b/scripts/create-attachments-bucket.js new file mode 100644 index 0000000..d46a835 --- /dev/null +++ b/scripts/create-attachments-bucket.js @@ -0,0 +1,63 @@ +/** + * Legt nur den Storage-Bucket für Kommentar-Anhänge an (ohne komplettes Setup). + * Benötigt .env mit APPWRITE_ENDPOINT, VITE_APPWRITE_PROJECT_ID, APPWRITE_API_KEY. + */ +import { Client, Storage, Permission, Role } from 'node-appwrite'; +import { readFileSync } from 'fs'; +import { resolve, dirname } from 'path'; +import { fileURLToPath } from 'url'; + +const __dirname = dirname(fileURLToPath(import.meta.url)); + +function loadEnv() { + const envPath = resolve(__dirname, '..', '.env'); + const lines = readFileSync(envPath, 'utf-8').split('\n'); + for (const line of lines) { + const trimmed = line.trim(); + if (!trimmed || trimmed.startsWith('#')) continue; + const eqIdx = trimmed.indexOf('='); + if (eqIdx === -1) continue; + const key = trimmed.slice(0, eqIdx).trim(); + const value = trimmed.slice(eqIdx + 1).trim(); + process.env[key] = value; + } +} + +loadEnv(); + +const ENDPOINT = process.env.APPWRITE_ENDPOINT; +const PROJECT_ID = process.env.VITE_APPWRITE_PROJECT_ID; +const API_KEY = process.env.APPWRITE_API_KEY; +const BUCKET_ID = process.env.VITE_APPWRITE_ATTACHMENTS_BUCKET_ID || 'defekttrack_anhaenge'; + +if (!ENDPOINT || !PROJECT_ID || !API_KEY) { + console.error('Bitte APPWRITE_ENDPOINT, VITE_APPWRITE_PROJECT_ID und APPWRITE_API_KEY in .env setzen.'); + process.exit(1); +} + +const client = new Client().setEndpoint(ENDPOINT).setProject(PROJECT_ID).setKey(API_KEY); +const storageService = new Storage(client); + +async function main() { + try { + await storageService.createBucket({ + bucketId: BUCKET_ID, + name: 'DefektTrack Kommentar-Anhänge', + permissions: [Permission.read(Role.users()), Permission.create(Role.users())], + fileSecurity: false, + enabled: true, + maximumFileSize: 15 * 1024 * 1024, + allowedFileExtensions: ['jpg', 'jpeg', 'png', 'gif', 'webp', 'pdf'], + }); + console.log(`Bucket erstellt: ${BUCKET_ID}`); + } catch (err) { + if (err.code === 409) { + console.log(`Bucket existiert bereits: ${BUCKET_ID}`); + } else { + console.error('Fehler:', err.message || err); + process.exit(1); + } + } +} + +main(); diff --git a/scripts/setup-appwrite.js b/scripts/setup-appwrite.js index 4267c49..f525eb1 100644 --- a/scripts/setup-appwrite.js +++ b/scripts/setup-appwrite.js @@ -1,4 +1,4 @@ -import { Client, Databases, Teams, Users, ID, Permission, Role, Query } from 'node-appwrite'; +import { Client, Databases, Teams, Users, ID, Permission, Role, Query, Storage } from 'node-appwrite'; import { readFileSync } from 'fs'; import { resolve, dirname } from 'path'; import { fileURLToPath } from 'url'; @@ -39,6 +39,9 @@ const client = new Client() const databases = new Databases(client); const teamsService = new Teams(client); const users = new Users(client); +const storageService = new Storage(client); + +const ATTACHMENTS_BUCKET_ID = 'defekttrack_anhaenge'; const TEAM_ROLES = ['admin', 'firmenleiter', 'filialleiter', 'service', 'lager']; @@ -180,7 +183,7 @@ async function createAssetsCollection() { await databases.createStringAttribute(DATABASE_ID, COLLECTION_ID, 'zustaendig', 128, true); await databases.createStringAttribute(DATABASE_ID, COLLECTION_ID, 'status', 32, true); await databases.createStringAttribute(DATABASE_ID, COLLECTION_ID, 'prio', 16, true); - await databases.createStringAttribute(DATABASE_ID, COLLECTION_ID, 'kommentar', 2048, false, ''); + await databases.createStringAttribute(DATABASE_ID, COLLECTION_ID, 'kommentar', 8192, false, ''); await databases.createStringAttribute(DATABASE_ID, COLLECTION_ID, 'createdBy', 128, false, ''); await databases.createStringAttribute(DATABASE_ID, COLLECTION_ID, 'lastEditedBy', 128, false, ''); console.log(' Attribute fuer assets erstellt (erlNummer, seriennummer, artikelNr, bezeichnung, defekt, lagerstandortId, zustaendig, status, prio, kommentar, createdBy, lastEditedBy)'); @@ -234,6 +237,27 @@ async function createAuditLogsCollection() { console.log(' Attribute fuer audit_logs erstellt (assetId, action, details, userId, userName)'); } +async function createAttachmentsBucket() { + try { + await storageService.createBucket({ + bucketId: ATTACHMENTS_BUCKET_ID, + name: 'DefektTrack Kommentar-Anhänge', + permissions: [Permission.read(Role.users()), Permission.create(Role.users())], + fileSecurity: false, + enabled: true, + maximumFileSize: 15 * 1024 * 1024, + allowedFileExtensions: ['jpg', 'jpeg', 'png', 'gif', 'webp', 'pdf'], + }); + console.log(`Storage-Bucket erstellt: ${ATTACHMENTS_BUCKET_ID}`); + } catch (err) { + if (err.code === 409) { + console.log(`Storage-Bucket existiert bereits: ${ATTACHMENTS_BUCKET_ID}`); + } else { + throw err; + } + } +} + async function createTeams() { for (const role of TEAM_ROLES) { try { @@ -330,6 +354,9 @@ async function main() { await createDatabase(); console.log(''); + await createAttachmentsBucket(); + console.log(''); + await createLocationsCollection(); console.log(''); @@ -361,6 +388,9 @@ async function main() { console.log(' Passwort: Admin1234!'); console.log(''); console.log('Vergiss nicht, den API-Key aus .env zu entfernen oder sicher aufzubewahren.'); + console.log(''); + console.log(`Kommentar-Anhänge: Bucket „${ATTACHMENTS_BUCKET_ID}“ — optional in .env:`); + console.log(` VITE_APPWRITE_ATTACHMENTS_BUCKET_ID=${ATTACHMENTS_BUCKET_ID}`); } main().catch((err) => { diff --git a/src/components/AssetDetail.jsx b/src/components/AssetDetail.jsx index d0da64b..f2dda16 100644 --- a/src/components/AssetDetail.jsx +++ b/src/components/AssetDetail.jsx @@ -21,6 +21,8 @@ import { SelectValue, } from '@/components/ui/select'; import { ArrowLeft, Pencil, Save, X } from 'lucide-react'; +import { parseKommentarForDisplay } from '@/lib/kommentarAnhaenge'; +import KommentarAnhaengeList from '@/components/KommentarAnhaengeList'; const STATUS_LABEL = { offen: 'Offen', in_bearbeitung: 'In Bearbeitung', entsorgt: 'Entsorgt' }; const PRIO_LABELS = { kritisch: 'Kritisch', hoch: 'Hoch', mittel: 'Mittel', niedrig: 'Niedrig' }; @@ -47,6 +49,25 @@ function PrioBadge({ prio }) { return {PRIO_LABELS[prio]}; } +function KommentarReadonly({ value }) { + const { subject, body, attachments } = parseKommentarForDisplay(value); + const empty = !subject && !(body && body.trim()) && attachments.length === 0; + if (empty) return

; + return ( +
+ {subject && ( +
+ {subject} +
+ )} + {body && body.trim() ? ( +

{body}

+ ) : null} + +
+ ); +} + export default function AssetDetail() { const { id } = useParams(); const navigate = useNavigate(); @@ -348,7 +369,18 @@ export default function AssetDetail() { )} - setForm(f => ({ ...f, kommentar: v }))} textarea className="sm:col-span-2" /> +
+ + {editing ? ( +