diff --git a/init-db-temp.mjs b/init-db-temp.mjs
new file mode 100644
index 0000000..1908bc5
--- /dev/null
+++ b/init-db-temp.mjs
@@ -0,0 +1,5 @@
+import initializeDatabase from './src/lib/init-db.js';
+
+console.log('Initializing database...');
+initializeDatabase();
+console.log('Database initialized successfully!');
diff --git a/migrate-to-username.js b/migrate-to-username.js
new file mode 100644
index 0000000..67e8ad8
--- /dev/null
+++ b/migrate-to-username.js
@@ -0,0 +1,60 @@
+import Database from "better-sqlite3";
+
+const db = new Database("./data/database.sqlite");
+
+console.log("š Migrating database to username-based authentication...\n");
+
+try {
+ // Check current table structure
+ const tableInfo = db.prepare("PRAGMA table_info(users)").all();
+ console.log("Current users table columns:");
+ tableInfo.forEach(col => console.log(` - ${col.name}: ${col.type}`));
+
+ const hasUsername = tableInfo.some(col => col.name === 'username');
+ const hasEmail = tableInfo.some(col => col.name === 'email');
+
+ if (hasUsername) {
+ console.log("ā
Username column already exists!");
+ } else if (hasEmail) {
+ console.log("\nš Adding username column...");
+
+ // Add username column
+ db.exec(`ALTER TABLE users ADD COLUMN username TEXT;`);
+ console.log("ā
Username column added");
+
+ // Copy email data to username for existing users
+ console.log("š Migrating existing email data to username...");
+ const result = db.exec(`UPDATE users SET username = email WHERE username IS NULL;`);
+ console.log("ā
Data migrated");
+
+ // Create unique index on username
+ console.log("š Creating unique index on username...");
+ try {
+ db.exec(`CREATE UNIQUE INDEX idx_users_username_unique ON users(username);`);
+ console.log("ā
Unique index created");
+ } catch (e) {
+ console.log("ā¹ļø Index already exists or couldn't be created:", e.message);
+ }
+
+ // Verify migration
+ console.log("\nš Verifying migration...");
+ const users = db.prepare("SELECT id, name, username, email FROM users LIMIT 3").all();
+ console.log("Sample users after migration:");
+ users.forEach(user => {
+ console.log(` - ${user.name}: username="${user.username}", email="${user.email || 'NULL'}"`);
+ });
+
+ console.log("\nā
Migration completed successfully!");
+ console.log("ā¹ļø You can now log in using usernames instead of emails");
+
+ } else {
+ console.log("ā Neither username nor email column found. Database may be corrupted.");
+ process.exit(1);
+ }
+
+} catch (error) {
+ console.error("ā Migration failed:", error.message);
+ process.exit(1);
+} finally {
+ db.close();
+}
diff --git a/src/app/api/files/[fileId]/route.js b/src/app/api/files/[fileId]/route.js
new file mode 100644
index 0000000..f5ebbea
--- /dev/null
+++ b/src/app/api/files/[fileId]/route.js
@@ -0,0 +1,79 @@
+import { NextResponse } from "next/server";
+import { unlink } from "fs/promises";
+import path from "path";
+import db from "@/lib/db";
+
+export async function DELETE(request, { params }) {
+ try {
+ const fileId = params.fileId;
+
+ // Get file info from database
+ const file = db.prepare(`
+ SELECT * FROM file_attachments WHERE file_id = ?
+ `).get(parseInt(fileId));
+
+ if (!file) {
+ return NextResponse.json(
+ { error: "File not found" },
+ { status: 404 }
+ );
+ }
+
+ // Delete physical file
+ try {
+ const fullPath = path.join(process.cwd(), "public", file.file_path);
+ await unlink(fullPath);
+ } catch (fileError) {
+ console.warn("Could not delete physical file:", fileError.message);
+ // Continue with database deletion even if file doesn't exist
+ }
+
+ // Delete from database
+ const result = db.prepare(`
+ DELETE FROM file_attachments WHERE file_id = ?
+ `).run(parseInt(fileId));
+
+ if (result.changes === 0) {
+ return NextResponse.json(
+ { error: "File not found" },
+ { status: 404 }
+ );
+ }
+
+ return NextResponse.json({ success: true });
+
+ } catch (error) {
+ console.error("Error deleting file:", error);
+ return NextResponse.json(
+ { error: "Failed to delete file" },
+ { status: 500 }
+ );
+ }
+}
+
+export async function GET(request, { params }) {
+ try {
+ const fileId = params.fileId;
+
+ // Get file info from database
+ const file = db.prepare(`
+ SELECT * FROM file_attachments WHERE file_id = ?
+ `).get(parseInt(fileId));
+
+ if (!file) {
+ return NextResponse.json(
+ { error: "File not found" },
+ { status: 404 }
+ );
+ }
+
+ return NextResponse.json(file);
+
+ } catch (error) {
+ console.error("Error fetching file:", error);
+ return NextResponse.json(
+ { error: "Failed to fetch file" },
+ { status: 500 }
+ );
+ }
+}
diff --git a/src/app/api/files/route.js b/src/app/api/files/route.js
new file mode 100644
index 0000000..edc98a2
--- /dev/null
+++ b/src/app/api/files/route.js
@@ -0,0 +1,162 @@
+import { NextRequest, NextResponse } from "next/server";
+import { writeFile, mkdir } from "fs/promises";
+import { existsSync } from "fs";
+import path from "path";
+import db from "@/lib/db";
+import { auditLog } from "@/lib/middleware/auditLog";
+
+const UPLOAD_DIR = path.join(process.cwd(), "public", "uploads");
+const MAX_FILE_SIZE = 10 * 1024 * 1024; // 10MB
+const ALLOWED_TYPES = [
+ "application/pdf",
+ "application/msword",
+ "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
+ "application/vnd.ms-excel",
+ "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
+ "image/jpeg",
+ "image/png",
+ "image/gif",
+ "text/plain"
+];
+
+export async function POST(request) {
+ try {
+ const formData = await request.formData();
+ const file = formData.get("file");
+ const entityType = formData.get("entityType");
+ const entityId = formData.get("entityId");
+ const description = formData.get("description") || "";
+
+ if (!file || !entityType || !entityId) {
+ return NextResponse.json(
+ { error: "File, entityType, and entityId are required" },
+ { status: 400 }
+ );
+ }
+
+ // Validate entity type
+ if (!["contract", "project", "task"].includes(entityType)) {
+ return NextResponse.json(
+ { error: "Invalid entity type" },
+ { status: 400 }
+ );
+ }
+
+ // Validate file
+ if (file.size > MAX_FILE_SIZE) {
+ return NextResponse.json(
+ { error: "File size too large (max 10MB)" },
+ { status: 400 }
+ );
+ }
+
+ if (!ALLOWED_TYPES.includes(file.type)) {
+ return NextResponse.json(
+ { error: "File type not allowed" },
+ { status: 400 }
+ );
+ }
+
+ // Create upload directory structure
+ const entityDir = path.join(UPLOAD_DIR, entityType + "s", entityId);
+ if (!existsSync(entityDir)) {
+ await mkdir(entityDir, { recursive: true });
+ }
+
+ // Generate unique filename
+ const timestamp = Date.now();
+ const sanitizedOriginalName = file.name.replace(/[^a-zA-Z0-9.-]/g, "_");
+ const storedFilename = `${timestamp}_${sanitizedOriginalName}`;
+ const filePath = path.join(entityDir, storedFilename);
+ const relativePath = `/uploads/${entityType}s/${entityId}/${storedFilename}`;
+
+ // Save file
+ const bytes = await file.arrayBuffer();
+ const buffer = Buffer.from(bytes);
+ await writeFile(filePath, buffer);
+
+ // Save to database
+ const stmt = db.prepare(`
+ INSERT INTO file_attachments (
+ entity_type, entity_id, original_filename, stored_filename,
+ file_path, file_size, mime_type, description, uploaded_by
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
+ `);
+
+ const result = stmt.run(
+ entityType,
+ parseInt(entityId),
+ file.name,
+ storedFilename,
+ relativePath,
+ file.size,
+ file.type,
+ description,
+ null // TODO: Get from session when auth is implemented
+ );
+
+ const newFile = {
+ file_id: result.lastInsertRowid,
+ entity_type: entityType,
+ entity_id: parseInt(entityId),
+ original_filename: file.name,
+ stored_filename: storedFilename,
+ file_path: relativePath,
+ file_size: file.size,
+ mime_type: file.type,
+ description: description,
+ upload_date: new Date().toISOString()
+ };
+
+ return NextResponse.json(newFile, { status: 201 });
+
+ } catch (error) {
+ console.error("File upload error:", error);
+ return NextResponse.json(
+ { error: "Failed to upload file" },
+ { status: 500 }
+ );
+ }
+}
+
+export async function GET(request) {
+ try {
+ const { searchParams } = new URL(request.url);
+ const entityType = searchParams.get("entityType");
+ const entityId = searchParams.get("entityId");
+
+ if (!entityType || !entityId) {
+ return NextResponse.json(
+ { error: "entityType and entityId are required" },
+ { status: 400 }
+ );
+ }
+
+ const files = db.prepare(`
+ SELECT
+ file_id,
+ entity_type,
+ entity_id,
+ original_filename,
+ stored_filename,
+ file_path,
+ file_size,
+ mime_type,
+ description,
+ upload_date,
+ uploaded_by
+ FROM file_attachments
+ WHERE entity_type = ? AND entity_id = ?
+ ORDER BY upload_date DESC
+ `).all(entityType, parseInt(entityId));
+
+ return NextResponse.json(files);
+
+ } catch (error) {
+ console.error("Error fetching files:", error);
+ return NextResponse.json(
+ { error: "Failed to fetch files" },
+ { status: 500 }
+ );
+ }
+}
diff --git a/src/app/contracts/[id]/page.js b/src/app/contracts/[id]/page.js
index 03189ea..58f33c5 100644
--- a/src/app/contracts/[id]/page.js
+++ b/src/app/contracts/[id]/page.js
@@ -10,6 +10,8 @@ import PageContainer from "@/components/ui/PageContainer";
import PageHeader from "@/components/ui/PageHeader";
import { LoadingState } from "@/components/ui/States";
import { formatDate } from "@/lib/utils";
+import FileUploadModal from "@/components/FileUploadModal";
+import FileAttachmentsList from "@/components/FileAttachmentsList";
export default function ContractDetailsPage() {
const params = useParams();
@@ -17,6 +19,8 @@ export default function ContractDetailsPage() {
const [contract, setContract] = useState(null);
const [projects, setProjects] = useState([]);
const [loading, setLoading] = useState(true);
+ const [showUploadModal, setShowUploadModal] = useState(false);
+ const [attachments, setAttachments] = useState([]);
useEffect(() => {
async function fetchContractDetails() {
@@ -52,6 +56,14 @@ export default function ContractDetailsPage() {
fetchContractDetails();
}
}, [contractId]);
+ const handleFileUploaded = (newFile) => {
+ setAttachments(prev => [newFile, ...prev]);
+ };
+
+ const handleFilesChange = (files) => {
+ setAttachments(files);
+ };
+
if (loading) {
return (
+ Contract Documents ({attachments.length})
+
+
+
No documents uploaded yet
+