diff --git a/src/app/api/admin/users/[id]/route.js b/src/app/api/admin/users/[id]/route.js index 49a407c..28e1269 100644 --- a/src/app/api/admin/users/[id]/route.js +++ b/src/app/api/admin/users/[id]/route.js @@ -4,8 +4,9 @@ import { withAdminAuth } from "@/lib/middleware/auth"; // GET: Get user by ID (admin only) async function getUserHandler(req, { params }) { + const { id } = await params; try { - const user = getUserById(params.id); + const user = getUserById(id); if (!user) { return NextResponse.json( @@ -29,9 +30,10 @@ async function getUserHandler(req, { params }) { // PUT: Update user (admin only) async function updateUserHandler(req, { params }) { + const { id } = await params; try { const data = await req.json(); - const userId = params.id; + const userId = id; // Prevent admin from deactivating themselves if (data.is_active === false && userId === req.user.id) { @@ -92,8 +94,9 @@ async function updateUserHandler(req, { params }) { // DELETE: Delete user (admin only) async function deleteUserHandler(req, { params }) { + const { id } = await params; try { - const userId = params.id; + const userId = id; // Prevent admin from deleting themselves if (userId === req.user.id) { diff --git a/src/app/api/files/[fileId]/route.js b/src/app/api/files/[fileId]/route.js index 32d1a0b..29fab25 100644 --- a/src/app/api/files/[fileId]/route.js +++ b/src/app/api/files/[fileId]/route.js @@ -6,9 +6,9 @@ import path from "path"; import db from "@/lib/db"; export async function GET(request, { params }) { - try { - const fileId = params.fileId; + const { fileId } = await params; + try { // Get file info from database const file = db.prepare(` SELECT * FROM file_attachments WHERE file_id = ? @@ -53,10 +53,94 @@ export async function GET(request, { params }) { } } -export async function DELETE(request, { params }) { +export async function PUT(request, { params }) { + const { fileId } = await params; try { - const fileId = params.fileId; + const body = await request.json(); + const { description, original_filename } = body; + // Validate input + if (description !== undefined && typeof description !== 'string') { + return NextResponse.json( + { error: "Description must be a string" }, + { status: 400 } + ); + } + + if (original_filename !== undefined && typeof original_filename !== 'string') { + return NextResponse.json( + { error: "Original filename must be a string" }, + { status: 400 } + ); + } + + // Check if file exists + const existingFile = db.prepare(` + SELECT * FROM file_attachments WHERE file_id = ? + `).get(parseInt(fileId)); + + if (!existingFile) { + return NextResponse.json( + { error: "File not found" }, + { status: 404 } + ); + } + + // Build update query + const updates = []; + const values = []; + + if (description !== undefined) { + updates.push('description = ?'); + values.push(description); + } + + if (original_filename !== undefined) { + updates.push('original_filename = ?'); + values.push(original_filename); + } + + if (updates.length === 0) { + return NextResponse.json( + { error: "No valid fields to update" }, + { status: 400 } + ); + } + + values.push(parseInt(fileId)); + + const result = db.prepare(` + UPDATE file_attachments + SET ${updates.join(', ')} + WHERE file_id = ? + `).run(...values); + + if (result.changes === 0) { + return NextResponse.json( + { error: "File not found" }, + { status: 404 } + ); + } + + // Get updated file + const updatedFile = db.prepare(` + SELECT * FROM file_attachments WHERE file_id = ? + `).get(parseInt(fileId)); + + return NextResponse.json(updatedFile); + + } catch (error) { + console.error("Error updating file:", error); + return NextResponse.json( + { error: "Failed to update file" }, + { status: 500 } + ); + } +} + +export async function DELETE(request, { params }) { + const { fileId } = await params; + try { // Get file info from database const file = db.prepare(` SELECT * FROM file_attachments WHERE file_id = ? diff --git a/src/app/api/notes/route.js b/src/app/api/notes/route.js index 0a0b6ae..048ac92 100644 --- a/src/app/api/notes/route.js +++ b/src/app/api/notes/route.js @@ -110,7 +110,7 @@ async function createNoteHandler(req) { } async function deleteNoteHandler(req, { params }) { - const { id } = params; + const { id } = await params; // Get note data before deletion for audit log const note = db.prepare("SELECT * FROM notes WHERE note_id = ?").get(id); @@ -137,7 +137,8 @@ async function deleteNoteHandler(req, { params }) { } async function updateNoteHandler(req, { params }) { - const noteId = params.id; + const { id } = await params; + const noteId = id; const { note } = await req.json(); if (!note || !noteId) { diff --git a/src/app/api/tasks/[id]/route.js b/src/app/api/tasks/[id]/route.js index 5e792af..92009e4 100644 --- a/src/app/api/tasks/[id]/route.js +++ b/src/app/api/tasks/[id]/route.js @@ -4,10 +4,11 @@ import { withReadAuth, withUserAuth } from "@/lib/middleware/auth"; // GET: Get a specific task template async function getTaskHandler(req, { params }) { + const { id } = await params; try { const template = db .prepare("SELECT * FROM tasks WHERE task_id = ? AND is_standard = 1") - .get(params.id); + .get(id); if (!template) { return NextResponse.json( @@ -27,6 +28,7 @@ async function getTaskHandler(req, { params }) { // PUT: Update a task template async function updateTaskHandler(req, { params }) { + const { id } = await params; try { const { name, max_wait_days, description } = await req.json(); @@ -40,7 +42,7 @@ async function updateTaskHandler(req, { params }) { SET name = ?, max_wait_days = ?, description = ? WHERE task_id = ? AND is_standard = 1` ) - .run(name, max_wait_days || 0, description || null, params.id); + .run(name, max_wait_days || 0, description || null, id); if (result.changes === 0) { return NextResponse.json( @@ -60,10 +62,11 @@ async function updateTaskHandler(req, { params }) { // DELETE: Delete a task template async function deleteTaskHandler(req, { params }) { + const { id } = await params; try { const result = db .prepare("DELETE FROM tasks WHERE task_id = ? AND is_standard = 1") - .run(params.id); + .run(id); if (result.changes === 0) { return NextResponse.json( diff --git a/src/app/projects/[id]/page.js b/src/app/projects/[id]/page.js index 8484baf..4206c2e 100644 --- a/src/app/projects/[id]/page.js +++ b/src/app/projects/[id]/page.js @@ -16,6 +16,8 @@ import PageContainer from "@/components/ui/PageContainer"; import PageHeader from "@/components/ui/PageHeader"; import ProjectStatusDropdown from "@/components/ProjectStatusDropdown"; import ClientProjectMap from "@/components/ui/ClientProjectMap"; +import FileUploadBox from "@/components/FileUploadBox"; +import FileItem from "@/components/FileItem"; export default function ProjectViewPage() { const params = useParams(); @@ -23,6 +25,7 @@ export default function ProjectViewPage() { const [project, setProject] = useState(null); const [notes, setNotes] = useState([]); const [loading, setLoading] = useState(true); + const [uploadedFiles, setUploadedFiles] = useState([]); // Helper function to add a new note to the list const addNote = (newNote) => { @@ -40,6 +43,39 @@ export default function ProjectViewPage() { return note.created_by === session.user.id; }; + // Helper function to handle file upload + const handleFileUploaded = (newFile) => { + setUploadedFiles(prevFiles => [newFile, ...prevFiles]); + }; + + // Helper function to handle file deletion + const handleFileDelete = async (fileId) => { + if (confirm('Czy na pewno chcesz usunąć ten plik?')) { + try { + const res = await fetch(`/api/files/${fileId}`, { + method: 'DELETE', + }); + if (res.ok) { + setUploadedFiles(prevFiles => prevFiles.filter(file => file.file_id !== fileId)); + } else { + alert('Błąd podczas usuwania pliku'); + } + } catch (error) { + console.error('Error deleting file:', error); + alert('Błąd podczas usuwania pliku'); + } + } + }; + + // Helper function to handle file update (edit) + const handleFileUpdate = async (updatedFile) => { + setUploadedFiles(prevFiles => + prevFiles.map(file => + file.file_id === updatedFile.file_id ? updatedFile : file + ) + ); + }; + useEffect(() => { const fetchData = async () => { if (!params.id) return; @@ -56,8 +92,13 @@ export default function ProjectViewPage() { const notesRes = await fetch(`/api/notes?project_id=${params.id}`); const notesData = notesRes.ok ? await notesRes.json() : []; + // Fetch files data + const filesRes = await fetch(`/api/files?entityType=project&entityId=${params.id}`); + const filesData = filesRes.ok ? await filesRes.json() : []; + setProject(projectData); setNotes(notesData); + setUploadedFiles(filesData); } catch (error) { console.error('Error fetching data:', error); setProject(null); @@ -168,7 +209,7 @@ export default function ProjectViewPage() { + + + + + ); + } + + return ( +
+
+
+ {getFileIcon(file.original_filename)} +
+
+
+ {file.original_filename} +
+
+ {formatFileSize(file.file_size)} + + {new Date(file.upload_date).toLocaleDateString('pl-PL')} +
+ {file.description && ( +
+ {file.description} +
+ )} +
+
+
+ + + + + + + +
+
+ ); +} diff --git a/src/components/FileUploadBox.js b/src/components/FileUploadBox.js new file mode 100644 index 0000000..36d2aca --- /dev/null +++ b/src/components/FileUploadBox.js @@ -0,0 +1,137 @@ +"use client"; + +import { useState, useRef, useCallback } from "react"; +import Button from "@/components/ui/Button"; + +export default function FileUploadBox({ projectId, onFileUploaded }) { + const [uploading, setUploading] = useState(false); + const [dragOver, setDragOver] = useState(false); + const fileInputRef = useRef(null); + + const acceptedTypes = ".pdf,.doc,.docx,.xls,.xlsx,.jpg,.jpeg,.png,.gif,.txt,.dwg,.zip"; + + const uploadFiles = async (files) => { + const validFiles = Array.from(files).filter(file => { + const isValidType = acceptedTypes.split(',').some(type => + file.name.toLowerCase().endsWith(type.replace('*', '')) + ); + const isValidSize = file.size <= 10 * 1024 * 1024; // 10MB limit + return isValidType && isValidSize; + }); + + if (validFiles.length === 0) { + alert('No valid files selected (invalid type or size > 10MB)'); + return; + } + + if (validFiles.length !== files.length) { + alert('Some files were skipped due to invalid type or size (max 10MB)'); + } + + setUploading(true); + const uploadPromises = validFiles.map(async (file) => { + const formData = new FormData(); + formData.append("file", file); + formData.append("entityType", "project"); + formData.append("entityId", projectId.toString()); + + try { + const response = await fetch("/api/files", { + method: "POST", + body: formData, + }); + + if (response.ok) { + const uploadedFile = await response.json(); + onFileUploaded?.(uploadedFile); + return { success: true }; + } else { + const error = await response.json(); + return { success: false, error: error.error || "Upload failed" }; + } + } catch (error) { + return { success: false, error: "Network error" }; + } + }); + + const results = await Promise.all(uploadPromises); + const failed = results.filter(r => !r.success); + + if (failed.length > 0) { + alert(`Failed to upload ${failed.length} file(s)`); + } + + setUploading(false); + }; + + const handleInputChange = (e) => { + if (e.target.files.length > 0) { + uploadFiles(e.target.files); + } + e.target.value = ''; // Reset input + }; + + const handleDrop = useCallback((e) => { + e.preventDefault(); + setDragOver(false); + if (e.dataTransfer.files.length > 0) { + uploadFiles(e.dataTransfer.files); + } + }, []); + + const handleDragOver = useCallback((e) => { + e.preventDefault(); + setDragOver(true); + }, []); + + const handleDragLeave = useCallback((e) => { + e.preventDefault(); + setDragOver(false); + }, []); + + const handleClick = () => { + fileInputRef.current?.click(); + }; + + return ( +
+ + + {uploading ? ( +
+ + + + + Przesyłanie plików... +
+ ) : ( +
+ + + + + {dragOver ? 'Upuść pliki tutaj' : 'Przeciągnij pliki lub kliknij'} + + + PDF, DOC, XLS, obrazki, DWG, ZIP + +
+ )} +
+ ); +}