Browse Source

initial release

svalavuo 4 ngày trước cách đây
mục cha
commit
d7282bb47e
49 tập tin đã thay đổi với 7906 bổ sung1 xóa
  1. 1 1
      README.md
  2. 52 0
      backend/SessionManager.php
  3. 159 0
      backend/api/account_transactions.php
  4. 131 0
      backend/api/attachments.php
  5. 112 0
      backend/api/auth.php
  6. 162 0
      backend/api/chart_of_accounts.php
  7. 213 0
      backend/api/clients.php
  8. 160 0
      backend/api/contact_persons.php
  9. 208 0
      backend/api/invoices.php
  10. 152 0
      backend/api/items.php
  11. 190 0
      backend/api/journal_entries.php
  12. 160 0
      backend/api/payments.php
  13. 200 0
      backend/api/projects.php
  14. 152 0
      backend/api/rental_prices.php
  15. 195 0
      backend/api/subprojects.php
  16. 68 0
      backend/api/upload.php
  17. 23 0
      backend/config/database.php
  18. 25 0
      backend/middleware/auth_middleware.php
  19. 54 0
      backend/migrate_accounting.sql
  20. 5 0
      backend/migrate_add_fields.sql
  21. 19 0
      backend/migrate_auth.sql
  22. 47 0
      backend/migrate_bookkeeping.sql
  23. 20 0
      backend/migrate_clients.sql
  24. 254 0
      backend/migrate_complete.sql
  25. 27 0
      backend/migrate_new_tables.sql
  26. 40 0
      backend/migrate_projects.sql
  27. 5 0
      backend/migrate_rental_client.sql
  28. 21 0
      backend/migrate_y_tunnus.sql
  29. 161 0
      backend/models/AccountTransaction.php
  30. 154 0
      backend/models/Attachment.php
  31. 173 0
      backend/models/ChartOfAccounts.php
  32. 188 0
      backend/models/Client.php
  33. 142 0
      backend/models/ContactPerson.php
  34. 223 0
      backend/models/Customer.php
  35. 213 0
      backend/models/Invoice.php
  36. 121 0
      backend/models/InvoiceItem.php
  37. 120 0
      backend/models/Item.php
  38. 159 0
      backend/models/JournalEntry.php
  39. 161 0
      backend/models/Payment.php
  40. 201 0
      backend/models/Project.php
  41. 131 0
      backend/models/RentalPrice.php
  42. 172 0
      backend/models/Subproject.php
  43. 193 0
      backend/models/User.php
  44. 236 0
      backend/setup_database.sql
  45. 201 0
      frontend/index.html
  46. 18 0
      frontend/package.json
  47. 2064 0
      frontend/src/App.vue
  48. 4 0
      frontend/src/main.js
  49. 16 0
      frontend/vite.config.js

+ 1 - 1
README.md

@@ -116,7 +116,7 @@ The system uses a normalized database structure with proper relationships:
    ```bash
    cd frontend
    npm install
-   npm run dev
+   npm run dev -- --host 0.0.0.0 --port 3000
    ```
 
 5. **Access Application:**

+ 52 - 0
backend/SessionManager.php

@@ -0,0 +1,52 @@
+<?php
+class SessionManager {
+    public static function startSession() {
+        if (session_status() === PHP_SESSION_NONE) {
+            session_start();
+        }
+    }
+
+    public static function destroySession() {
+        session_destroy();
+    }
+
+    public static function isLoggedIn() {
+        return isset($_SESSION['user_id']);
+    }
+
+    public static function getCurrentUser() {
+        if (self::isLoggedIn()) {
+            return array(
+                'id' => $_SESSION['user_id'],
+                'username' => $_SESSION['username'],
+                'email' => $_SESSION['email'] ?? '',
+                'first_name' => $_SESSION['first_name'],
+                'last_name' => $_SESSION['last_name'],
+                'role' => $_SESSION['role']
+            );
+        }
+        return null;
+    }
+
+    public static function requireAuth() {
+        if (!self::isLoggedIn()) {
+            http_response_code(401);
+            echo json_encode(array('message' => 'Unauthorized'));
+            exit();
+        }
+    }
+
+    public static function requireRole($required_role) {
+        $user = self::getCurrentUser();
+        if (!$user || $user['role'] !== $required_role) {
+            http_response_code(403);
+            echo json_encode(array('message' => 'Insufficient permissions'));
+            exit();
+        }
+    }
+
+    public static function requireAdmin() {
+        self::requireRole('admin');
+    }
+}
+?>

+ 159 - 0
backend/api/account_transactions.php

@@ -0,0 +1,159 @@
+<?php
+header("Access-Control-Allow-Origin: *");
+header("Content-Type: application/json; charset=UTF-8");
+header("Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS");
+header("Access-Control-Allow-Headers: Content-Type, Access-Control-Allow-Headers, Authorization, X-Requested-With");
+
+if ($_SERVER['REQUEST_METHOD'] == 'OPTIONS') {
+    exit(0);
+}
+
+require_once '../config/database.php';
+require_once '../models/AccountTransaction.php';
+
+$database = new Database();
+$db = $database->getConnection();
+
+$accountTransaction = new AccountTransaction($db);
+
+$request_method = $_SERVER['REQUEST_METHOD'];
+
+switch($request_method) {
+    case 'GET':
+        if(isset($_GET['id'])) {
+            $accountTransaction->id = $_GET['id'];
+            $accountTransaction->readOne();
+            
+            if($accountTransaction->journal_entry_id != null) {
+                $transaction_arr = array(
+                    "id" => $accountTransaction->id,
+                    "journal_entry_id" => $accountTransaction->journal_entry_id,
+                    "account_id" => $accountTransaction->account_id,
+                    "debit_amount" => $accountTransaction->debit_amount,
+                    "credit_amount" => $accountTransaction->credit_amount,
+                    "description" => $accountTransaction->description,
+                    "created_at" => $accountTransaction->created_at,
+                    "updated_at" => $accountTransaction->updated_at,
+                    "transaction_type" => $accountTransaction->getTransactionType(),
+                    "amount" => $accountTransaction->getAmount()
+                );
+                
+                http_response_code(200);
+                echo json_encode($transaction_arr);
+            } else {
+                http_response_code(404);
+                echo json_encode(array("message" => "Account transaction not found."));
+            }
+        } elseif(isset($_GET['journal_entry_id'])) {
+            $stmt = $accountTransaction->read();
+            $num = $stmt->rowCount();
+            
+            if($num > 0) {
+                $transactions_arr = array();
+                $transactions_arr["records"] = array();
+                
+                while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
+                    extract($row);
+                    
+                    $transaction_item = array(
+                        "id" => $id,
+                        "journal_entry_id" => $journal_entry_id,
+                        "account_id" => $account_id,
+                        "debit_amount" => $debit_amount,
+                        "credit_amount" => $credit_amount,
+                        "description" => $description,
+                        "created_at" => $created_at,
+                        "updated_at" => $updated_at,
+                        "transaction_type" => $accountTransaction->getTransactionType(),
+                        "amount" => $accountTransaction->getAmount(),
+                        "account_name" => $account_name,
+                        "account_type" => $account_type,
+                        "entry_number" => $entry_number,
+                        "entry_date" => $entry_date
+                    );
+                    
+                    array_push($transactions_arr["records"], $transaction_item);
+                }
+                
+                http_response_code(200);
+                echo json_encode($transactions_arr);
+            } else {
+                http_response_code(200);
+                echo json_encode(array("records" => array()));
+            }
+        } else {
+            http_response_code(400);
+            echo json_encode(array("message" => "Missing journal_entry_id parameter."));
+        }
+        break;
+        
+    case 'POST':
+        $data = json_decode(file_get_contents("php://input"));
+        
+        if(!empty($data->journal_entry_id) && !empty($data->account_id)) {
+            $accountTransaction->journal_entry_id = $data->journal_entry_id;
+            $accountTransaction->account_id = $data->account_id;
+            $accountTransaction->debit_amount = $data->debit_amount ?? 0;
+            $accountTransaction->credit_amount = $data->credit_amount ?? 0;
+            $accountTransaction->description = $data->description ?? '';
+            
+            if($accountTransaction->create()) {
+                http_response_code(201);
+                echo json_encode(array("message" => "Account transaction was created."));
+            } else {
+                http_response_code(503);
+                echo json_encode(array("message" => "Unable to create account transaction."));
+            }
+        } else {
+            http_response_code(400);
+            echo json_encode(array("message" => "Unable to create account transaction. Data is incomplete."));
+        }
+        break;
+        
+    case 'PUT':
+        $data = json_decode(file_get_contents("php://input"));
+        
+        if(!empty($data->id) && !empty($data->journal_entry_id) && !empty($data->account_id)) {
+            $accountTransaction->id = $data->id;
+            $accountTransaction->journal_entry_id = $data->journal_entry_id;
+            $accountTransaction->account_id = $data->account_id;
+            $accountTransaction->debit_amount = $data->debit_amount ?? 0;
+            $accountTransaction->credit_amount = $data->credit_amount ?? 0;
+            $accountTransaction->description = $data->description ?? '';
+            
+            if($accountTransaction->update()) {
+                http_response_code(200);
+                echo json_encode(array("message" => "Account transaction was updated."));
+            } else {
+                http_response_code(503);
+                echo json_encode(array("message" => "Unable to update account transaction."));
+            }
+        } else {
+            http_response_code(400);
+            echo json_encode(array("message" => "Unable to update account transaction. Data is incomplete."));
+        }
+        break;
+        
+    case 'DELETE':
+        if(isset($_GET['id'])) {
+            $accountTransaction->id = $_GET['id'];
+            
+            if($accountTransaction->delete()) {
+                http_response_code(200);
+                echo json_encode(array("message" => "Account transaction was deleted."));
+            } else {
+                http_response_code(503);
+                echo json_encode(array("message" => "Unable to delete account transaction."));
+            }
+        } else {
+            http_response_code(400);
+            echo json_encode(array("message" => "Unable to delete account transaction. ID is missing."));
+        }
+        break;
+        
+    default:
+        http_response_code(405);
+        echo json_encode(array("message" => "Method not allowed."));
+        break;
+}
+?>

+ 131 - 0
backend/api/attachments.php

@@ -0,0 +1,131 @@
+<?php
+header("Access-Control-Allow-Origin: *");
+header("Content-Type: application/json; charset=UTF-8");
+header("Access-Control-Allow-Methods: GET, POST, DELETE, OPTIONS");
+header("Access-Control-Allow-Headers: Content-Type, Access-Control-Allow-Headers, Authorization, X-Requested-With");
+
+if ($_SERVER['REQUEST_METHOD'] == 'OPTIONS') {
+    exit(0);
+}
+
+require_once '../config/database.php';
+require_once '../models/Attachment.php';
+
+$database = new Database();
+$db = $database->getConnection();
+
+$attachment = new Attachment($db);
+
+$request_method = $_SERVER['REQUEST_METHOD'];
+
+switch($request_method) {
+    case 'GET':
+        if(isset($_GET['id'])) {
+            $attachment->id = $_GET['id'];
+            $attachment->readOne();
+            
+            if($attachment->item_id != null) {
+                $attachment_arr = array(
+                    "id" => $attachment->id,
+                    "item_id" => $attachment->item_id,
+                    "filename" => $attachment->filename,
+                    "original_name" => $attachment->original_name,
+                    "file_type" => $attachment->file_type,
+                    "file_path" => $attachment->file_path,
+                    "file_size" => $attachment->file_size,
+                    "mime_type" => $attachment->mime_type,
+                    "created_at" => $attachment->created_at
+                );
+                
+                http_response_code(200);
+                echo json_encode($attachment_arr);
+            } else {
+                http_response_code(404);
+                echo json_encode(array("message" => "Attachment not found."));
+            }
+        } elseif(isset($_GET['item_id'])) {
+            $attachment->item_id = $_GET['item_id'];
+            $stmt = $attachment->read();
+            $num = $stmt->rowCount();
+            
+            if($num > 0) {
+                $attachments_arr = array();
+                $attachments_arr["records"] = array();
+                
+                while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
+                    extract($row);
+                    
+                    $attachment_item = array(
+                        "id" => $id,
+                        "item_id" => $item_id,
+                        "filename" => $filename,
+                        "original_name" => $original_name,
+                        "file_type" => $file_type,
+                        "file_path" => $file_path,
+                        "file_size" => $file_size,
+                        "mime_type" => $mime_type,
+                        "created_at" => $created_at
+                    );
+                    
+                    array_push($attachments_arr["records"], $attachment_item);
+                }
+                
+                http_response_code(200);
+                echo json_encode($attachments_arr);
+            } else {
+                http_response_code(200);
+                echo json_encode(array("records" => array()));
+            }
+        } else {
+            http_response_code(400);
+            echo json_encode(array("message" => "Missing item_id parameter."));
+        }
+        break;
+        
+    case 'POST':
+        if(isset($_FILES['attachment']) && isset($_POST['item_id']) && isset($_POST['file_type'])) {
+            $item_id = $_POST['item_id'];
+            $file_type = $_POST['file_type'];
+            
+            $result = $attachment->uploadFile($_FILES['attachment'], $item_id, $file_type);
+            
+            if($result['success']) {
+                http_response_code(201);
+                echo json_encode(array(
+                    "message" => "Attachment uploaded successfully.",
+                    "id" => $result['id'],
+                    "url" => $result['url']
+                ));
+            } else {
+                http_response_code(400);
+                echo json_encode(array("message" => $result['message']));
+            }
+        } else {
+            http_response_code(400);
+            echo json_encode(array("message" => "Missing required parameters."));
+        }
+        break;
+        
+    case 'DELETE':
+        if(isset($_GET['id'])) {
+            $attachment->id = $_GET['id'];
+            
+            if($attachment->delete()) {
+                http_response_code(200);
+                echo json_encode(array("message" => "Attachment was deleted."));
+            } else {
+                http_response_code(503);
+                echo json_encode(array("message" => "Unable to delete attachment."));
+            }
+        } else {
+            http_response_code(400);
+            echo json_encode(array("message" => "Unable to delete attachment. ID is missing."));
+        }
+        break;
+        
+    default:
+        http_response_code(405);
+        echo json_encode(array("message" => "Method not allowed."));
+        break;
+}
+?>

+ 112 - 0
backend/api/auth.php

@@ -0,0 +1,112 @@
+<?php
+header("Access-Control-Allow-Origin: *");
+header("Content-Type: application/json; charset=UTF-8");
+header("Access-Control-Allow-Methods: POST, OPTIONS");
+header("Access-Control-Allow-Headers: Content-Type, Access-Control-Allow-Headers, Authorization, X-Requested-With");
+
+if ($_SERVER['REQUEST_METHOD'] == 'OPTIONS') {
+    exit(0);
+}
+
+require_once '../config/database.php';
+require_once '../models/User.php';
+
+$database = new Database();
+$db = $database->getConnection();
+
+$user = new User($db);
+
+// Start session for authenticated requests
+session_start();
+
+$request_method = $_SERVER['REQUEST_METHOD'];
+
+switch($request_method) {
+    case 'POST':
+        $data = json_decode(file_get_contents("php://input"));
+        
+        if($data->action === 'login') {
+            if(!empty($data->username) && !empty($data->password)) {
+                $authenticated_user = $user->authenticate($data->username, $data->password);
+                
+                if($authenticated_user) {
+                    $_SESSION['user_id'] = $authenticated_user['id'];
+                    $_SESSION['username'] = $authenticated_user['username'];
+                    $_SESSION['role'] = $authenticated_user['role'];
+                    $_SESSION['first_name'] = $authenticated_user['first_name'];
+                    $_SESSION['last_name'] = $authenticated_user['last_name'];
+                    
+                    http_response_code(200);
+                    echo json_encode(array(
+                        "message" => "Login successful",
+                        "user" => array(
+                            "id" => $authenticated_user['id'],
+                            "username" => $authenticated_user['username'],
+                            "email" => $authenticated_user['email'],
+                            "first_name" => $authenticated_user['first_name'],
+                            "last_name" => $authenticated_user['last_name'],
+                            "role" => $authenticated_user['role']
+                        )
+                    ));
+                } else {
+                    http_response_code(401);
+                    echo json_encode(array("message" => "Invalid credentials"));
+                }
+            } else {
+                http_response_code(400);
+                echo json_encode(array("message" => "Username and password are required"));
+            }
+        } elseif($data->action === 'register') {
+            if(!empty($data->username) && !empty($data->email) && !empty($data->password) && !empty($data->first_name) && !empty($data->last_name)) {
+                
+                // Check if username or email already exists
+                $existing_user = $user->findByEmail($data->email);
+                if($existing_user) {
+                    http_response_code(409);
+                    echo json_encode(array("message" => "Email already exists"));
+                    break;
+                }
+                
+                $existing_user = $user->read();
+                while($row = $existing_user->fetch(PDO::FETCH_ASSOC)) {
+                    if($row['username'] === $data->username) {
+                        http_response_code(409);
+                        echo json_encode(array("message" => "Username already exists"));
+                        break 2;
+                    }
+                }
+                
+                $user->username = $data->username;
+                $user->email = $data->email;
+                $user->password_hash = password_hash($data->password, PASSWORD_DEFAULT);
+                $user->first_name = $data->first_name;
+                $user->last_name = $data->last_name;
+                $user->role = $data->role ?? 'user';
+                
+                if($user->create()) {
+                    http_response_code(201);
+                    echo json_encode(array("message" => "User registered successfully"));
+                } else {
+                    http_response_code(503);
+                    echo json_encode(array("message" => "Unable to register user"));
+                }
+            } else {
+                http_response_code(400);
+                echo json_encode(array("message" => "Required fields are missing"));
+            }
+        } elseif($data->action === 'logout') {
+            session_destroy();
+            http_response_code(200);
+            echo json_encode(array("message" => "Logged out successfully"));
+        } else {
+            http_response_code(400);
+            echo json_encode(array("message" => "Invalid action"));
+        }
+        break;
+        
+    default:
+        http_response_code(405);
+        echo json_encode(array("message" => "Method not allowed."));
+        break;
+}
+?>

+ 162 - 0
backend/api/chart_of_accounts.php

@@ -0,0 +1,162 @@
+<?php
+header("Access-Control-Allow-Origin: *");
+header("Content-Type: application/json; charset=UTF-8");
+header("Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS");
+header("Access-Control-Allow-Headers: Content-Type, Access-Control-Allow-Headers, Authorization, X-Requested-With");
+
+if ($_SERVER['REQUEST_METHOD'] == 'OPTIONS') {
+    exit(0);
+}
+
+require_once '../config/database.php';
+require_once '../models/ChartOfAccounts.php';
+
+$database = new Database();
+$db = $database->getConnection();
+
+$account = new ChartOfAccounts($db);
+
+$request_method = $_SERVER['REQUEST_METHOD'];
+
+switch($request_method) {
+    case 'GET':
+        if(isset($_GET['id'])) {
+            $account->id = $_GET['id'];
+            $account->readOne();
+            
+            if($account->account_number != null) {
+                $account_arr = array(
+                    "id" => $account->id,
+                    "account_number" => $account->account_number,
+                    "account_name" => $account->account_name,
+                    "account_type" => $account->account_type,
+                    "parent_id" => $account->parent_id,
+                    "description" => $account->description,
+                    "opening_balance" => $account->opening_balance,
+                    "current_balance" => $account->current_balance,
+                    "is_active" => $account->is_active,
+                    "created_at" => $account->created_at,
+                    "updated_at" => $account->updated_at,
+                    "account_type_name" => $account->getAccountTypeName()
+                );
+                
+                http_response_code(200);
+                echo json_encode($account_arr);
+            } else {
+                http_response_code(404);
+                echo json_encode(array("message" => "Account not found."));
+            }
+        } else {
+            $stmt = $account->read();
+            $num = $stmt->rowCount();
+            
+            if($num > 0) {
+                $accounts_arr = array();
+                $accounts_arr["records"] = array();
+                
+                while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
+                    extract($row);
+                    
+                    $account_item = array(
+                        "id" => $id,
+                        "account_number" => $account_number,
+                        "account_name" => $account_name,
+                        "account_type" => $account_type,
+                        "parent_id" => $parent_id,
+                        "description" => $description,
+                        "opening_balance" => $opening_balance,
+                        "current_balance" => $current_balance,
+                        "is_active" => $is_active,
+                        "created_at" => $created_at,
+                        "updated_at" => $updated_at,
+                        "account_type_name" => $account->getAccountTypeName()
+                    );
+                    
+                    array_push($accounts_arr["records"], $account_item);
+                }
+                
+                http_response_code(200);
+                echo json_encode($accounts_arr);
+            } else {
+                http_response_code(200);
+                echo json_encode(array("records" => array()));
+            }
+        }
+        break;
+        
+    case 'POST':
+        $data = json_decode(file_get_contents("php://input"));
+        
+        if(!empty($data->account_name)) {
+            $account->account_number = $data->account_number ?? '';
+            $account->account_name = $data->account_name;
+            $account->account_type = $data->account_type ?? 'asset';
+            $account->parent_id = $data->parent_id ?? null;
+            $account->description = $data->description ?? '';
+            $account->opening_balance = $data->opening_balance ?? 0;
+            $account->current_balance = $data->current_balance ?? 0;
+            $account->is_active = $data->is_active ?? true;
+            
+            if($account->create()) {
+                http_response_code(201);
+                echo json_encode(array("message" => "Account was created."));
+            } else {
+                http_response_code(503);
+                echo json_encode(array("message" => "Unable to create account."));
+            }
+        } else {
+            http_response_code(400);
+            echo json_encode(array("message" => "Unable to create account. Account name is required."));
+        }
+        break;
+        
+    case 'PUT':
+        $data = json_decode(file_get_contents("php://input"));
+        
+        if(!empty($data->id) && !empty($data->account_name)) {
+            $account->id = $data->id;
+            $account->account_number = $data->account_number ?? '';
+            $account->account_name = $data->account_name;
+            $account->account_type = $data->account_type ?? 'asset';
+            $account->parent_id = $data->parent_id ?? null;
+            $account->description = $data->description ?? '';
+            $account->opening_balance = $data->opening_balance ?? 0;
+            $account->current_balance = $data->current_balance ?? 0;
+            $account->is_active = $data->is_active ?? true;
+            
+            if($account->update()) {
+                http_response_code(200);
+                echo json_encode(array("message" => "Account was updated."));
+            } else {
+                http_response_code(503);
+                echo json_encode(array("message" => "Unable to update account."));
+            }
+        } else {
+            http_response_code(400);
+            echo json_encode(array("message" => "Unable to update account. Data is incomplete."));
+        }
+        break;
+        
+    case 'DELETE':
+        if(isset($_GET['id'])) {
+            $account->id = $_GET['id'];
+            
+            if($account->delete()) {
+                http_response_code(200);
+                echo json_encode(array("message" => "Account was deleted."));
+            } else {
+                http_response_code(503);
+                echo json_encode(array("message" => "Unable to delete account."));
+            }
+        } else {
+            http_response_code(400);
+            echo json_encode(array("message" => "Unable to delete account. ID is missing."));
+        }
+        break;
+        
+    default:
+        http_response_code(405);
+        echo json_encode(array("message" => "Method not allowed."));
+        break;
+}
+?>

+ 213 - 0
backend/api/clients.php

@@ -0,0 +1,213 @@
+<?php
+header("Access-Control-Allow-Origin: *");
+header("Content-Type: application/json; charset=UTF-8");
+header("Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS");
+header("Access-Control-Allow-Headers: Content-Type, Access-Control-Allow-Headers, Authorization, X-Requested-With");
+
+if ($_SERVER['REQUEST_METHOD'] == 'OPTIONS') {
+    exit(0);
+}
+
+require_once '../config/database.php';
+require_once '../models/Client.php';
+
+$database = new Database();
+$db = $database->getConnection();
+
+$client = new Client($db);
+
+$request_method = $_SERVER['REQUEST_METHOD'];
+
+switch($request_method) {
+    case 'GET':
+        if(isset($_GET['id'])) {
+            $client->id = $_GET['id'];
+            $client->readOne();
+            
+            if($client->email != null) {
+                $client_arr = array(
+                    "id" => $client->id,
+                    "company_name" => $client->company_name,
+                    "y_tunnus" => $client->y_tunnus,
+                    "first_name" => $client->first_name,
+                    "last_name" => $client->last_name,
+                    "email" => $client->email,
+                    "phone" => $client->phone,
+                    "address" => $client->address,
+                    "city" => $client->city,
+                    "state" => $client->state,
+                    "postal_code" => $client->postal_code,
+                    "country" => $client->country,
+                    "notes" => $client->notes,
+                    "created_at" => $client->created_at,
+                    "updated_at" => $client->updated_at
+                );
+                
+                http_response_code(200);
+                echo json_encode($client_arr);
+            } else {
+                http_response_code(404);
+                echo json_encode(array("message" => "Client not found."));
+            }
+        } elseif(isset($_GET['search'])) {
+            $search_term = $_GET['search'];
+            $stmt = $client->search($search_term);
+            $num = $stmt->rowCount();
+            
+            if($num > 0) {
+                $clients_arr = array();
+                $clients_arr["records"] = array();
+                
+                while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
+                    extract($row);
+                    
+                    $client_item = array(
+                        "id" => $id,
+                        "company_name" => $company_name,
+                        "first_name" => $first_name,
+                        "last_name" => $last_name,
+                        "email" => $email,
+                        "phone" => $phone,
+                        "address" => $address,
+                        "city" => $city,
+                        "state" => $state,
+                        "postal_code" => $postal_code,
+                        "country" => $country,
+                        "notes" => $notes,
+                        "created_at" => $created_at,
+                        "updated_at" => $updated_at
+                    );
+                    
+                    array_push($clients_arr["records"], $client_item);
+                }
+                
+                http_response_code(200);
+                echo json_encode($clients_arr);
+            } else {
+                http_response_code(200);
+                echo json_encode(array("records" => array()));
+            }
+        } else {
+            $stmt = $client->read();
+            $num = $stmt->rowCount();
+            
+            if($num > 0) {
+                $clients_arr = array();
+                $clients_arr["records"] = array();
+                
+                while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
+                    extract($row);
+                    
+                    $client_item = array(
+                        "id" => $id,
+                        "company_name" => $company_name,
+                        "first_name" => $first_name,
+                        "last_name" => $last_name,
+                        "email" => $email,
+                        "phone" => $phone,
+                        "address" => $address,
+                        "city" => $city,
+                        "state" => $state,
+                        "postal_code" => $postal_code,
+                        "country" => $country,
+                        "notes" => $notes,
+                        "created_at" => $created_at,
+                        "updated_at" => $updated_at
+                    );
+                    
+                    array_push($clients_arr["records"], $client_item);
+                }
+                
+                http_response_code(200);
+                echo json_encode($clients_arr);
+            } else {
+                http_response_code(200);
+                echo json_encode(array("records" => array()));
+            }
+        }
+        break;
+        
+    case 'POST':
+        $data = json_decode(file_get_contents("php://input"));
+        
+        if(!empty($data->first_name) && !empty($data->last_name) && !empty($data->email)) {
+            $client->company_name = $data->company_name ?? '';
+            $client->y_tunnus = $data->y_tunnus ?? '';
+            $client->first_name = $data->first_name;
+            $client->last_name = $data->last_name;
+            $client->email = $data->email;
+            $client->phone = $data->phone ?? '';
+            $client->address = $data->address ?? '';
+            $client->city = $data->city ?? '';
+            $client->state = $data->state ?? '';
+            $client->postal_code = $data->postal_code ?? '';
+            $client->country = $data->country ?? '';
+            $client->notes = $data->notes ?? '';
+            
+            if($client->create()) {
+                http_response_code(201);
+                echo json_encode(array("message" => "Client was created."));
+            } else {
+                http_response_code(503);
+                echo json_encode(array("message" => "Unable to create client."));
+            }
+        } else {
+            http_response_code(400);
+            echo json_encode(array("message" => "Unable to create client. Data is incomplete."));
+        }
+        break;
+        
+    case 'PUT':
+        $data = json_decode(file_get_contents("php://input"));
+        
+        if(!empty($data->id) && !empty($data->first_name) && !empty($data->last_name) && !empty($data->email)) {
+            $client->id = $data->id;
+            $client->company_name = $data->company_name ?? '';
+            $client->y_tunnus = $data->y_tunnus ?? '';
+            $client->first_name = $data->first_name;
+            $client->last_name = $data->last_name;
+            $client->email = $data->email;
+            $client->phone = $data->phone ?? '';
+            $client->address = $data->address ?? '';
+            $client->city = $data->city ?? '';
+            $client->state = $data->state ?? '';
+            $client->postal_code = $data->postal_code ?? '';
+            $client->country = $data->country ?? '';
+            $client->notes = $data->notes ?? '';
+            
+            if($client->update()) {
+                http_response_code(200);
+                echo json_encode(array("message" => "Client was updated."));
+            } else {
+                http_response_code(503);
+                echo json_encode(array("message" => "Unable to update client."));
+            }
+        } else {
+            http_response_code(400);
+            echo json_encode(array("message" => "Unable to update client. Data is incomplete."));
+        }
+        break;
+        
+    case 'DELETE':
+        if(isset($_GET['id'])) {
+            $client->id = $_GET['id'];
+            
+            if($client->delete()) {
+                http_response_code(200);
+                echo json_encode(array("message" => "Client was deleted."));
+            } else {
+                http_response_code(503);
+                echo json_encode(array("message" => "Unable to delete client."));
+            }
+        } else {
+            http_response_code(400);
+            echo json_encode(array("message" => "Unable to delete client. ID is missing."));
+        }
+        break;
+        
+    default:
+        http_response_code(405);
+        echo json_encode(array("message" => "Method not allowed."));
+        break;
+}
+?>

+ 160 - 0
backend/api/contact_persons.php

@@ -0,0 +1,160 @@
+<?php
+header("Access-Control-Allow-Origin: *");
+header("Content-Type: application/json; charset=UTF-8");
+header("Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS");
+header("Access-Control-Allow-Headers: Content-Type, Access-Control-Allow-Headers, Authorization, X-Requested-With");
+
+if ($_SERVER['REQUEST_METHOD'] == 'OPTIONS') {
+    exit(0);
+}
+
+require_once '../config/database.php';
+require_once '../models/ContactPerson.php';
+
+$database = new Database();
+$db = $database->getConnection();
+
+$contactPerson = new ContactPerson($db);
+
+$request_method = $_SERVER['REQUEST_METHOD'];
+
+switch($request_method) {
+    case 'GET':
+        if(isset($_GET['id'])) {
+            $contactPerson->id = $_GET['id'];
+            $contactPerson->readOne();
+            
+            if($contactPerson->first_name != null) {
+                $contact_arr = array(
+                    "id" => $contactPerson->id,
+                    "client_id" => $contactPerson->client_id,
+                    "first_name" => $contactPerson->first_name,
+                    "last_name" => $contactPerson->last_name,
+                    "email" => $contactPerson->email,
+                    "phone" => $contactPerson->phone,
+                    "position" => $contactPerson->position,
+                    "is_primary" => $contactPerson->is_primary,
+                    "created_at" => $contactPerson->created_at,
+                    "updated_at" => $contactPerson->updated_at
+                );
+                
+                http_response_code(200);
+                echo json_encode($contact_arr);
+            } else {
+                http_response_code(404);
+                echo json_encode(array("message" => "Contact person not found."));
+            }
+        } elseif(isset($_GET['client_id'])) {
+            $contactPerson->client_id = $_GET['client_id'];
+            $stmt = $contactPerson->read();
+            $num = $stmt->rowCount();
+            
+            if($num > 0) {
+                $contacts_arr = array();
+                $contacts_arr["records"] = array();
+                
+                while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
+                    extract($row);
+                    
+                    $contact_item = array(
+                        "id" => $id,
+                        "client_id" => $client_id,
+                        "first_name" => $first_name,
+                        "last_name" => $last_name,
+                        "email" => $email,
+                        "phone" => $phone,
+                        "position" => $position,
+                        "is_primary" => $is_primary,
+                        "created_at" => $created_at,
+                        "updated_at" => $updated_at
+                    );
+                    
+                    array_push($contacts_arr["records"], $contact_item);
+                }
+                
+                http_response_code(200);
+                echo json_encode($contacts_arr);
+            } else {
+                http_response_code(200);
+                echo json_encode(array("records" => array()));
+            }
+        } else {
+            http_response_code(400);
+            echo json_encode(array("message" => "Missing client_id parameter."));
+        }
+        break;
+        
+    case 'POST':
+        $data = json_decode(file_get_contents("php://input"));
+        
+        if(!empty($data->client_id) && !empty($data->first_name) && !empty($data->last_name)) {
+            $contactPerson->client_id = $data->client_id;
+            $contactPerson->first_name = $data->first_name;
+            $contactPerson->last_name = $data->last_name;
+            $contactPerson->email = $data->email ?? '';
+            $contactPerson->phone = $data->phone ?? '';
+            $contactPerson->position = $data->position ?? '';
+            $contactPerson->is_primary = $data->is_primary ?? false;
+            
+            if($contactPerson->create()) {
+                http_response_code(201);
+                echo json_encode(array("message" => "Contact person was created."));
+            } else {
+                http_response_code(503);
+                echo json_encode(array("message" => "Unable to create contact person."));
+            }
+        } else {
+            http_response_code(400);
+            echo json_encode(array("message" => "Unable to create contact person. Data is incomplete."));
+        }
+        break;
+        
+    case 'PUT':
+        $data = json_decode(file_get_contents("php://input"));
+        
+        if(!empty($data->id) && !empty($data->client_id) && !empty($data->first_name) && !empty($data->last_name)) {
+            $contactPerson->id = $data->id;
+            $contactPerson->client_id = $data->client_id;
+            $contactPerson->first_name = $data->first_name;
+            $contactPerson->last_name = $data->last_name;
+            $contactPerson->email = $data->email ?? '';
+            $contactPerson->phone = $data->phone ?? '';
+            $contactPerson->position = $data->position ?? '';
+            $contactPerson->is_primary = $data->is_primary ?? false;
+            
+            if($contactPerson->update()) {
+                http_response_code(200);
+                echo json_encode(array("message" => "Contact person was updated."));
+            } else {
+                http_response_code(503);
+                echo json_encode(array("message" => "Unable to update contact person."));
+            }
+        } else {
+            http_response_code(400);
+            echo json_encode(array("message" => "Unable to update contact person. Data is incomplete."));
+        }
+        break;
+        
+    case 'DELETE':
+        if(isset($_GET['id'])) {
+            $contactPerson->id = $_GET['id'];
+            
+            if($contactPerson->delete()) {
+                http_response_code(200);
+                echo json_encode(array("message" => "Contact person was deleted."));
+            } else {
+                http_response_code(503);
+                echo json_encode(array("message" => "Unable to delete contact person."));
+            }
+        } else {
+            http_response_code(400);
+            echo json_encode(array("message" => "Unable to delete contact person. ID is missing."));
+        }
+        break;
+        
+    default:
+        http_response_code(405);
+        echo json_encode(array("message" => "Method not allowed."));
+        break;
+}
+?>

+ 208 - 0
backend/api/invoices.php

@@ -0,0 +1,208 @@
+<?php
+header("Access-Control-Allow-Origin: *");
+header("Content-Type: application/json; charset=UTF-8");
+header("Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS");
+header("Access-Control-Allow-Headers: Content-Type, Access-Control-Allow-Headers, Authorization, X-Requested-With");
+
+if ($_SERVER['REQUEST_METHOD'] == 'OPTIONS') {
+    exit(0);
+}
+
+require_once '../config/database.php';
+require_once '../models/Invoice.php';
+require_once '../models/InvoiceItem.php';
+
+$database = new Database();
+$db = $database->getConnection();
+
+$invoice = new Invoice($db);
+$invoiceItem = new InvoiceItem($db);
+
+$request_method = $_SERVER['REQUEST_METHOD'];
+
+switch($request_method) {
+    case 'GET':
+        if(isset($_GET['id'])) {
+            $invoice->id = $_GET['id'];
+            $invoice->readOne();
+            
+            if($invoice->invoice_number != null) {
+                $invoice_arr = array(
+                    "id" => $invoice->id,
+                    "client_id" => $invoice->client_id,
+                    "invoice_number" => $invoice->invoice_number,
+                    "issue_date" => $invoice->issue_date,
+                    "due_date" => $invoice->due_date,
+                    "status" => $invoice->status,
+                    "subtotal" => $invoice->subtotal,
+                    "tax_amount" => $invoice->tax_amount,
+                    "total_amount" => $invoice->total_amount,
+                    "notes" => $invoice->notes,
+                    "created_at" => $invoice->created_at,
+                    "updated_at" => $invoice->updated_at,
+                    "client_name" => $invoice->getClientName()
+                );
+                
+                $invoice_arr['items'] = $invoice->getInvoiceItems($invoice->id);
+                $invoice_arr['payments'] = $invoice->getPayments($invoice->id);
+                
+                http_response_code(200);
+                echo json_encode($invoice_arr);
+            } else {
+                http_response_code(404);
+                echo json_encode(array("message" => "Invoice not found."));
+            }
+        } elseif(isset($_GET['client_id'])) {
+            $invoice->client_id = $_GET['client_id'];
+            $stmt = $invoice->read();
+            $num = $stmt->rowCount();
+            
+            if($num > 0) {
+                $invoices_arr = array();
+                $invoices_arr["records"] = array();
+                
+                while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
+                    extract($row);
+                    
+                    $invoice_item = array(
+                        "id" => $id,
+                        "client_id" => $client_id,
+                        "invoice_number" => $invoice_number,
+                        "issue_date" => $issue_date,
+                        "due_date" => $due_date,
+                        "status" => $status,
+                        "subtotal" => $subtotal,
+                        "tax_amount" => $tax_amount,
+                        "total_amount" => $total_amount,
+                        "notes" => $notes,
+                        "created_at" => $created_at,
+                        "updated_at" => $updated_at,
+                        "client_name" => $client_name
+                    );
+                    
+                    array_push($invoices_arr["records"], $invoice_item);
+                }
+                
+                http_response_code(200);
+                echo json_encode($invoices_arr);
+            } else {
+                http_response_code(200);
+                echo json_encode(array("records" => array()));
+            }
+        } else {
+            $stmt = $invoice->read();
+            $num = $stmt->rowCount();
+            
+            if($num > 0) {
+                $invoices_arr = array();
+                $invoices_arr["records"] = array();
+                
+                while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
+                    extract($row);
+                    
+                    $invoice_item = array(
+                        "id" => $id,
+                        "client_id" => $client_id,
+                        "invoice_number" => $invoice_number,
+                        "issue_date" => $issue_date,
+                        "due_date" => $due_date,
+                        "status" => $status,
+                        "subtotal" => $subtotal,
+                        "tax_amount" => $tax_amount,
+                        "total_amount" => $total_amount,
+                        "notes" => $notes,
+                        "created_at" => $created_at,
+                        "updated_at" => $updated_at,
+                        "client_name" => $client_name
+                    );
+                    
+                    array_push($invoices_arr["records"], $invoice_item);
+                }
+                
+                http_response_code(200);
+                echo json_encode($invoices_arr);
+            } else {
+                http_response_code(200);
+                echo json_encode(array("records" => array()));
+            }
+        }
+        break;
+        
+    case 'POST':
+        $data = json_decode(file_get_contents("php://input"));
+        
+        if(!empty($data->client_id) && !empty($data->invoice_number) && !empty($data->issue_date) && !empty($data->due_date)) {
+            $invoice->client_id = $data->client_id;
+            $invoice->invoice_number = $data->invoice_number;
+            $invoice->issue_date = $data->issue_date;
+            $invoice->due_date = $data->due_date;
+            $invoice->status = $data->status ?? 'draft';
+            $invoice->subtotal = $data->subtotal ?? 0;
+            $invoice->tax_amount = $data->tax_amount ?? 0;
+            $invoice->total_amount = $data->total_amount ?? 0;
+            $invoice->notes = $data->notes ?? '';
+            
+            if($invoice->create()) {
+                http_response_code(201);
+                echo json_encode(array("message" => "Invoice was created."));
+            } else {
+                http_response_code(503);
+                echo json_encode(array("message" => "Unable to create invoice."));
+            }
+        } else {
+            http_response_code(400);
+            echo json_encode(array("message" => "Unable to create invoice. Data is incomplete."));
+        }
+        break;
+        
+    case 'PUT':
+        $data = json_decode(file_get_contents("php://input"));
+        
+        if(!empty($data->id) && !empty($data->client_id) && !empty($data->invoice_number) && !empty($data->issue_date) && !empty($data->due_date)) {
+            $invoice->id = $data->id;
+            $invoice->client_id = $data->client_id;
+            $invoice->invoice_number = $data->invoice_number;
+            $invoice->issue_date = $data->issue_date;
+            $invoice->due_date = $data->due_date;
+            $invoice->status = $data->status;
+            $invoice->subtotal = $data->subtotal ?? 0;
+            $invoice->tax_amount = $data->tax_amount ?? 0;
+            $invoice->total_amount = $data->total_amount ?? 0;
+            $invoice->notes = $data->notes ?? '';
+            
+            if($invoice->update()) {
+                http_response_code(200);
+                echo json_encode(array("message" => "Invoice was updated."));
+            } else {
+                http_response_code(503);
+                echo json_encode(array("message" => "Unable to update invoice."));
+            }
+        } else {
+            http_response_code(400);
+            echo json_encode(array("message" => "Unable to update invoice. Data is incomplete."));
+        }
+        break;
+        
+    case 'DELETE':
+        if(isset($_GET['id'])) {
+            $invoice->id = $_GET['id'];
+            
+            if($invoice->delete()) {
+                http_response_code(200);
+                echo json_encode(array("message" => "Invoice was deleted."));
+            } else {
+                http_response_code(503);
+                echo json_encode(array("message" => "Unable to delete invoice."));
+            }
+        } else {
+            http_response_code(400);
+            echo json_encode(array("message" => "Unable to delete invoice. ID is missing."));
+        }
+        break;
+        
+    default:
+        http_response_code(405);
+        echo json_encode(array("message" => "Method not allowed."));
+        break;
+}
+?>

+ 152 - 0
backend/api/items.php

@@ -0,0 +1,152 @@
+<?php
+header("Access-Control-Allow-Origin: *");
+header("Content-Type: application/json; charset=UTF-8");
+header("Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS");
+header("Access-Control-Allow-Headers: Content-Type, Access-Control-Allow-Headers, Authorization, X-Requested-With");
+
+if ($_SERVER['REQUEST_METHOD'] == 'OPTIONS') {
+    exit(0);
+}
+
+require_once '../config/database.php';
+require_once '../models/Item.php';
+
+$database = new Database();
+$db = $database->getConnection();
+
+$item = new Item($db);
+
+$request_method = $_SERVER['REQUEST_METHOD'];
+
+switch($request_method) {
+    case 'GET':
+        if(isset($_GET['id'])) {
+            $item->id = $_GET['id'];
+            $item->readOne();
+            
+            if($item->name != null) {
+                $item_arr = array(
+                    "id" => $item->id,
+                    "name" => $item->name,
+                    "description" => $item->description,
+                    "serial_number" => $item->serial_number,
+                    "picture" => $item->picture,
+                    "quantity" => $item->quantity,
+                    "price" => $item->price,
+                    "created_at" => $item->created_at,
+                    "updated_at" => $item->updated_at
+                );
+                
+                http_response_code(200);
+                echo json_encode($item_arr);
+            } else {
+                http_response_code(404);
+                echo json_encode(array("message" => "Item not found."));
+            }
+        } else {
+            $stmt = $item->read();
+            $num = $stmt->rowCount();
+            
+            if($num > 0) {
+                $items_arr = array();
+                $items_arr["records"] = array();
+                
+                while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
+                    extract($row);
+                    
+                    $item_item = array(
+                        "id" => $id,
+                        "name" => $name,
+                        "description" => $description,
+                        "serial_number" => $serial_number,
+                        "picture" => $picture,
+                        "quantity" => $quantity,
+                        "price" => $price,
+                        "created_at" => $created_at,
+                        "updated_at" => $updated_at
+                    );
+                    
+                    array_push($items_arr["records"], $item_item);
+                }
+                
+                http_response_code(200);
+                echo json_encode($items_arr);
+            } else {
+                http_response_code(200);
+                echo json_encode(array("records" => array()));
+            }
+        }
+        break;
+        
+    case 'POST':
+        $data = json_decode(file_get_contents("php://input"));
+        
+        if(!empty($data->name) && !empty($data->quantity) && !empty($data->price)) {
+            $item->name = $data->name;
+            $item->description = $data->description ?? '';
+            $item->serial_number = $data->serial_number ?? '';
+            $item->picture = $data->picture ?? '';
+            $item->quantity = $data->quantity;
+            $item->price = $data->price;
+            
+            if($item->create()) {
+                http_response_code(201);
+                echo json_encode(array("message" => "Item was created."));
+            } else {
+                http_response_code(503);
+                echo json_encode(array("message" => "Unable to create item."));
+            }
+        } else {
+            http_response_code(400);
+            echo json_encode(array("message" => "Unable to create item. Data is incomplete."));
+        }
+        break;
+        
+    case 'PUT':
+        $data = json_decode(file_get_contents("php://input"));
+        
+        if(!empty($data->id) && !empty($data->name) && !empty($data->quantity) && !empty($data->price)) {
+            $item->id = $data->id;
+            $item->name = $data->name;
+            $item->description = $data->description ?? '';
+            $item->serial_number = $data->serial_number ?? '';
+            $item->picture = $data->picture ?? '';
+            $item->quantity = $data->quantity;
+            $item->price = $data->price;
+            
+            if($item->update()) {
+                http_response_code(200);
+                echo json_encode(array("message" => "Item was updated."));
+            } else {
+                http_response_code(503);
+                echo json_encode(array("message" => "Unable to update item."));
+            }
+        } else {
+            http_response_code(400);
+            echo json_encode(array("message" => "Unable to update item. Data is incomplete."));
+        }
+        break;
+        
+    case 'DELETE':
+        if(isset($_GET['id'])) {
+            $item->id = $_GET['id'];
+            
+            if($item->delete()) {
+                http_response_code(200);
+                echo json_encode(array("message" => "Item was deleted."));
+            } else {
+                http_response_code(503);
+                echo json_encode(array("message" => "Unable to delete item."));
+            }
+        } else {
+            http_response_code(400);
+            echo json_encode(array("message" => "Unable to delete item. ID is missing."));
+        }
+        break;
+        
+    default:
+        http_response_code(405);
+        echo json_encode(array("message" => "Method not allowed."));
+        break;
+}
+?>

+ 190 - 0
backend/api/journal_entries.php

@@ -0,0 +1,190 @@
+<?php
+header("Access-Control-Allow-Origin: *");
+header("Content-Type: application/json; charset=UTF-8");
+header("Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS");
+header("Access-Control-Allow-Headers: Content-Type, Access-Control-Allow-Headers, Authorization, X-Requested-With");
+
+if ($_SERVER['REQUEST_METHOD'] == 'OPTIONS') {
+    exit(0);
+}
+
+require_once '../config/database.php';
+require_once '../models/JournalEntry.php';
+require_once '../models/AccountTransaction.php';
+
+$database = new Database();
+$db = $database->getConnection();
+
+$journalEntry = new JournalEntry($db);
+$accountTransaction = new AccountTransaction($db);
+
+$request_method = $_SERVER['REQUEST_METHOD'];
+
+switch($request_method) {
+    case 'GET':
+        if(isset($_GET['id'])) {
+            $journalEntry->id = $_GET['id'];
+            $journalEntry->readOne();
+            
+            if($journalEntry->entry_number != null) {
+                $entry_arr = array(
+                    "id" => $journalEntry->id,
+                    "entry_number" => $journalEntry->entry_number,
+                    "entry_date" => $journalEntry->entry_date,
+                    "description" => $journalEntry->description,
+                    "reference_number" => $journalEntry->reference_number,
+                    "created_at" => $journalEntry->created_at,
+                    "updated_at" => $journalEntry->updated_at,
+                    "transactions" => $journalEntry->getTransactions($journalEntry->id)
+                );
+                
+                http_response_code(200);
+                echo json_encode($entry_arr);
+            } else {
+                http_response_code(404);
+                echo json_encode(array("message" => "Journal entry not found."));
+            }
+        } else {
+            $stmt = $journalEntry->read();
+            $num = $stmt->rowCount();
+            
+            if($num > 0) {
+                $entries_arr = array();
+                $entries_arr["records"] = array();
+                
+                while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
+                    extract($row);
+                    
+                    $entry_item = array(
+                        "id" => $id,
+                        "entry_number" => $entry_number,
+                        "entry_date" => $entry_date,
+                        "description" => $description,
+                        "reference_number" => $reference_number,
+                        "created_at" => $created_at,
+                        "updated_at" => $updated_at
+                    );
+                    
+                    array_push($entries_arr["records"], $entry_item);
+                }
+                
+                http_response_code(200);
+                echo json_encode($entries_arr);
+            } else {
+                http_response_code(200);
+                echo json_encode(array("records" => array()));
+            }
+        }
+        break;
+        
+    case 'POST':
+        $data = json_decode(file_get_contents("php://input"));
+        
+        if(!empty($data->entry_date)) {
+            $journalEntry->entry_number = $data->entry_number ?? $journalEntry->generateEntryNumber();
+            $journalEntry->entry_date = $data->entry_date;
+            $journalEntry->description = $data->description ?? '';
+            $journalEntry->reference_number = $data->reference_number ?? '';
+            
+            if($journalEntry->create()) {
+                $entry_id = $this->conn->lastInsertId();
+                
+                // Create account transactions if provided
+                if(isset($data->transactions) && is_array($data->transactions)) {
+                    foreach($data->transactions as $transaction) {
+                        $accountTransaction->journal_entry_id = $entry_id;
+                        $accountTransaction->account_id = $transaction->account_id;
+                        $accountTransaction->debit_amount = $transaction->debit_amount ?? 0;
+                        $accountTransaction->credit_amount = $transaction->credit_amount ?? 0;
+                        $accountTransaction->description = $transaction->description ?? '';
+                        
+                        if(!$accountTransaction->create()) {
+                            http_response_code(503);
+                            echo json_encode(array("message" => "Unable to create account transaction."));
+                            return;
+                        }
+                    }
+                }
+                
+                http_response_code(201);
+                echo json_encode(array("message" => "Journal entry was created."));
+            } else {
+                http_response_code(503);
+                echo json_encode(array("message" => "Unable to create journal entry."));
+            }
+        } else {
+            http_response_code(400);
+            echo json_encode(array("message" => "Unable to create journal entry. Entry date is required."));
+        }
+        break;
+        
+    case 'PUT':
+        $data = json_decode(file_get_contents("php://input"));
+        
+        if(!empty($data->id) && !empty($data->entry_date)) {
+            $journalEntry->id = $data->id;
+            $journalEntry->entry_number = $data->entry_number;
+            $journalEntry->entry_date = $data->entry_date;
+            $journalEntry->description = $data->description ?? '';
+            $journalEntry->reference_number = $data->reference_number ?? '';
+            
+            if($journalEntry->update()) {
+                // Update account transactions if provided
+                if(isset($data->transactions) && is_array($data->transactions)) {
+                    // Delete existing transactions for this entry
+                    $delete_query = "DELETE FROM account_transactions WHERE journal_entry_id = ?";
+                    $delete_stmt = $this->conn->prepare($delete_query);
+                    $delete_stmt->bindParam(1, $data->id);
+                    $delete_stmt->execute();
+                    
+                    // Create new transactions
+                    foreach($data->transactions as $transaction) {
+                        $accountTransaction->journal_entry_id = $data->id;
+                        $accountTransaction->account_id = $transaction->account_id;
+                        $accountTransaction->debit_amount = $transaction->debit_amount ?? 0;
+                        $accountTransaction->credit_amount = $transaction->credit_amount ?? 0;
+                        $accountTransaction->description = $transaction->description ?? '';
+                        
+                        if(!$accountTransaction->create()) {
+                            http_response_code(503);
+                            echo json_encode(array("message" => "Unable to create account transaction."));
+                            return;
+                        }
+                    }
+                }
+                
+                http_response_code(200);
+                echo json_encode(array("message" => "Journal entry was updated."));
+            } else {
+                http_response_code(503);
+                echo json_encode(array("message" => "Unable to update journal entry."));
+            }
+        } else {
+            http_response_code(400);
+            echo json_encode(array("message" => "Unable to update journal entry. Data is incomplete."));
+        }
+        break;
+        
+    case 'DELETE':
+        if(isset($_GET['id'])) {
+            $journalEntry->id = $_GET['id'];
+            
+            if($journalEntry->delete()) {
+                http_response_code(200);
+                echo json_encode(array("message" => "Journal entry was deleted."));
+            } else {
+                http_response_code(503);
+                echo json_encode(array("message" => "Unable to delete journal entry."));
+            }
+        } else {
+            http_response_code(400);
+            echo json_encode(array("message" => "Unable to delete journal entry. ID is missing."));
+        }
+        break;
+        
+    default:
+        http_response_code(405);
+        echo json_encode(array("message" => "Method not allowed."));
+        break;
+}
+?>

+ 160 - 0
backend/api/payments.php

@@ -0,0 +1,160 @@
+<?php
+header("Access-Control-Allow-Origin: *");
+header("Content-Type: application/json; charset=UTF-8");
+header("Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS");
+header("Access-Control-Allow-Headers: Content-Type, Access-Control-Allow-Headers, Authorization, X-Requested-With");
+
+if ($_SERVER['REQUEST_METHOD'] == 'OPTIONS') {
+    exit(0);
+}
+
+require_once '../config/database.php';
+require_once '../models/Payment.php';
+
+$database = new Database();
+$db = $database->getConnection();
+
+$payment = new Payment($db);
+
+$request_method = $_SERVER['REQUEST_METHOD'];
+
+switch($request_method) {
+    case 'GET':
+        if(isset($_GET['id'])) {
+            $payment->id = $_GET['id'];
+            $payment->readOne();
+            
+            if($payment->payment_date != null) {
+                $payment_arr = array(
+                    "id" => $payment->id,
+                    "invoice_id" => $payment->invoice_id,
+                    "client_id" => $payment->client_id,
+                    "payment_date" => $payment->payment_date,
+                    "amount" => $payment->amount,
+                    "payment_method" => $payment->payment_method,
+                    "reference_number" => $payment->reference_number,
+                    "notes" => $payment->notes,
+                    "created_at" => $payment->created_at,
+                    "updated_at" => $payment->updated_at
+                );
+                
+                http_response_code(200);
+                echo json_encode($payment_arr);
+            } else {
+                http_response_code(404);
+                echo json_encode(array("message" => "Payment not found."));
+            }
+        } elseif(isset($_GET['invoice_id'])) {
+            $payment->invoice_id = $_GET['invoice_id'];
+            $stmt = $payment->read();
+            $num = $stmt->rowCount();
+            
+            if($num > 0) {
+                $payments_arr = array();
+                $payments_arr["records"] = array();
+                
+                while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
+                    extract($row);
+                    
+                    $payment_item = array(
+                        "id" => $id,
+                        "invoice_id" => $invoice_id,
+                        "client_id" => $client_id,
+                        "payment_date" => $payment_date,
+                        "amount" => $amount,
+                        "payment_method" => $payment_method,
+                        "reference_number" => $reference_number,
+                        "notes" => $notes,
+                        "created_at" => $created_at,
+                        "updated_at" => $updated_at
+                    );
+                    
+                    array_push($payments_arr["records"], $payment_item);
+                }
+                
+                http_response_code(200);
+                echo json_encode($payments_arr);
+            } else {
+                http_response_code(200);
+                echo json_encode(array("records" => array()));
+            }
+        } else {
+            http_response_code(400);
+            echo json_encode(array("message" => "Missing invoice_id parameter."));
+        }
+        break;
+        
+    case 'POST':
+        $data = json_decode(file_get_contents("php://input"));
+        
+        if(!empty($data->client_id) && !empty($data->payment_date) && !empty($data->amount)) {
+            $payment->client_id = $data->client_id;
+            $payment->invoice_id = $data->invoice_id ?? null;
+            $payment->payment_date = $data->payment_date;
+            $payment->amount = $data->amount;
+            $payment->payment_method = $data->payment_method ?? 'cash';
+            $payment->reference_number = $data->reference_number ?? '';
+            $payment->notes = $data->notes ?? '';
+            
+            if($payment->create()) {
+                http_response_code(201);
+                echo json_encode(array("message" => "Payment was created."));
+            } else {
+                http_response_code(503);
+                echo json_encode(array("message" => "Unable to create payment."));
+            }
+        } else {
+            http_response_code(400);
+            echo json_encode(array("message" => "Unable to create payment. Data is incomplete."));
+        }
+        break;
+        
+    case 'PUT':
+        $data = json_decode(file_get_contents("php://input"));
+        
+        if(!empty($data->id) && !empty($data->client_id) && !empty($data->payment_date) && !empty($data->amount)) {
+            $payment->id = $data->id;
+            $payment->client_id = $data->client_id;
+            $payment->invoice_id = $data->invoice_id ?? null;
+            $payment->payment_date = $data->payment_date;
+            $payment->amount = $data->amount;
+            $payment->payment_method = $data->payment_method ?? 'cash';
+            $payment->reference_number = $data->reference_number ?? '';
+            $payment->notes = $data->notes ?? '';
+            
+            if($payment->update()) {
+                http_response_code(200);
+                echo json_encode(array("message" => "Payment was updated."));
+            } else {
+                http_response_code(503);
+                echo json_encode(array("message" => "Unable to update payment."));
+            }
+        } else {
+            http_response_code(400);
+            echo json_encode(array("message" => "Unable to update payment. Data is incomplete."));
+        }
+        break;
+        
+    case 'DELETE':
+        if(isset($_GET['id'])) {
+            $payment->id = $_GET['id'];
+            
+            if($payment->delete()) {
+                http_response_code(200);
+                echo json_encode(array("message" => "Payment was deleted."));
+            } else {
+                http_response_code(503);
+                echo json_encode(array("message" => "Unable to delete payment."));
+            }
+        } else {
+            http_response_code(400);
+            echo json_encode(array("message" => "Unable to delete payment. ID is missing."));
+        }
+        break;
+        
+    default:
+        http_response_code(405);
+        echo json_encode(array("message" => "Method not allowed."));
+        break;
+}
+?>

+ 200 - 0
backend/api/projects.php

@@ -0,0 +1,200 @@
+<?php
+header("Access-Control-Allow-Origin: *");
+header("Content-Type: application/json; charset=UTF-8");
+header("Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS");
+header("Access-Control-Allow-Headers: Content-Type, Access-Control-Allow-Headers, Authorization, X-Requested-With");
+
+if ($_SERVER['REQUEST_METHOD'] == 'OPTIONS') {
+    exit(0);
+}
+
+require_once '../config/database.php';
+require_once '../models/Project.php';
+require_once '../models/Subproject.php';
+
+$database = new Database();
+$db = $database->getConnection();
+
+$project = new Project($db);
+$subproject = new Subproject($db);
+
+$request_method = $_SERVER['REQUEST_METHOD'];
+
+switch($request_method) {
+    case 'GET':
+        if(isset($_GET['id'])) {
+            $project->id = $_GET['id'];
+            $project->readOne();
+            
+            if($project->project_name != null) {
+                $project_arr = array(
+                    "id" => $project->id,
+                    "customer_id" => $project->customer_id,
+                    "project_name" => $project->project_name,
+                    "description" => $project->description,
+                    "status" => $project->status,
+                    "start_date" => $project->start_date,
+                    "end_date" => $project->end_date,
+                    "budget" => $project->budget,
+                    "created_at" => $project->created_at,
+                    "updated_at" => $project->updated_at,
+                    "customer_name" => $project->getCustomerName(),
+                    "status_badge" => $project->getStatusBadge(),
+                    "progress" => $project->getProgress()
+                );
+                
+                http_response_code(200);
+                echo json_encode($project_arr);
+            } else {
+                http_response_code(404);
+                echo json_encode(array("message" => "Project not found."));
+            }
+        } elseif(isset($_GET['customer_id'])) {
+            $stmt = $project->read();
+            $num = $stmt->rowCount();
+            
+            if($num > 0) {
+                $projects_arr = array();
+                $projects_arr["records"] = array();
+                
+                while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
+                    extract($row);
+                    
+                    $project_item = array(
+                        "id" => $id,
+                        "customer_id" => $customer_id,
+                        "project_name" => $project_name,
+                        "description" => $description,
+                        "status" => $status,
+                        "start_date" => $start_date,
+                        "end_date" => $end_date,
+                        "budget" => $budget,
+                        "created_at" => $created_at,
+                        "updated_at" => $updated_at,
+                        "customer_name" => $project->getCustomerName(),
+                        "status_badge" => $project->getStatusBadge(),
+                        "progress" => $project->getProgress()
+                    );
+                    
+                    array_push($projects_arr["records"], $project_item);
+                }
+                
+                http_response_code(200);
+                echo json_encode($projects_arr);
+            } else {
+                http_response_code(200);
+                echo json_encode(array("records" => array()));
+            }
+        } else {
+            $stmt = $project->read();
+            $num = $stmt->rowCount();
+            
+            if($num > 0) {
+                $projects_arr = array();
+                $projects_arr["records"] = array();
+                
+                while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
+                    extract($row);
+                    
+                    $project_item = array(
+                        "id" => $id,
+                        "customer_id" => $customer_id,
+                        "project_name" => $project_name,
+                        "description" => $description,
+                        "status" => $status,
+                        "start_date" => $start_date,
+                        "end_date" => $end_date,
+                        "budget" => $budget,
+                        "created_at" => $created_at,
+                        "updated_at" => $updated_at,
+                        "customer_name" => $project->getCustomerName(),
+                        "status_badge" => $project->getStatusBadge(),
+                        "progress" => $project->getProgress()
+                    );
+                    
+                    array_push($projects_arr["records"], $project_item);
+                }
+                
+                http_response_code(200);
+                echo json_encode($projects_arr);
+            } else {
+                http_response_code(200);
+                echo json_encode(array("records" => array()));
+            }
+        }
+        break;
+        
+    case 'POST':
+        $data = json_decode(file_get_contents("php://input"));
+        
+        if(!empty($data->customer_id) && !empty($data->project_name)) {
+            $project->customer_id = $data->customer_id;
+            $project->project_name = $data->project_name;
+            $project->description = $data->description ?? '';
+            $project->status = $data->status ?? 'planning';
+            $project->start_date = $data->start_date ?? null;
+            $project->end_date = $data->end_date ?? null;
+            $project->budget = $data->budget ?? null;
+            
+            if($project->create()) {
+                http_response_code(201);
+                echo json_encode(array("message" => "Project was created."));
+            } else {
+                http_response_code(503);
+                echo json_encode(array("message" => "Unable to create project."));
+            }
+        } else {
+            http_response_code(400);
+            echo json_encode(array("message" => "Unable to create project. Customer ID and project name are required."));
+        }
+        break;
+        
+    case 'PUT':
+        $data = json_decode(file_get_contents("php://input"));
+        
+        if(!empty($data->id) && !empty($data->customer_id) && !empty($data->project_name)) {
+            $project->id = $data->id;
+            $project->customer_id = $data->customer_id;
+            $project->project_name = $data->project_name;
+            $project->description = $data->description ?? '';
+            $project->status = $data->status ?? 'planning';
+            $project->start_date = $data->start_date ?? null;
+            $project->end_date = $data->end_date ?? null;
+            $project->budget = $data->budget ?? null;
+            
+            if($project->update()) {
+                http_response_code(200);
+                echo json_encode(array("message" => "Project was updated."));
+            } else {
+                http_response_code(503);
+                echo json_encode(array("message" => "Unable to update project."));
+            }
+        } else {
+            http_response_code(400);
+            echo json_encode(array("message" => "Unable to update project. Data is incomplete."));
+        }
+        break;
+        
+    case 'DELETE':
+        if(isset($_GET['id'])) {
+            $project->id = $_GET['id'];
+            
+            if($project->delete()) {
+                http_response_code(200);
+                echo json_encode(array("message" => "Project was deleted."));
+            } else {
+                http_response_code(503);
+                echo json_encode(array("message" => "Unable to delete project."));
+            }
+        } else {
+            http_response_code(400);
+            echo json_encode(array("message" => "Unable to delete project. ID is missing."));
+        }
+        break;
+        
+    default:
+        http_response_code(405);
+        echo json_encode(array("message" => "Method not allowed."));
+        break;
+}
+?>

+ 152 - 0
backend/api/rental_prices.php

@@ -0,0 +1,152 @@
+<?php
+header("Access-Control-Allow-Origin: *");
+header("Content-Type: application/json; charset=UTF-8");
+header("Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS");
+header("Access-Control-Allow-Headers: Content-Type, Access-Control-Allow-Headers, Authorization, X-Requested-With");
+
+if ($_SERVER['REQUEST_METHOD'] == 'OPTIONS') {
+    exit(0);
+}
+
+require_once '../config/database.php';
+require_once '../models/RentalPrice.php';
+
+$database = new Database();
+$db = $database->getConnection();
+
+$rentalPrice = new RentalPrice($db);
+
+$request_method = $_SERVER['REQUEST_METHOD'];
+
+switch($request_method) {
+    case 'GET':
+        if(isset($_GET['id'])) {
+            $rentalPrice->id = $_GET['id'];
+            $rentalPrice->readOne();
+            
+            if($rentalPrice->item_id != null) {
+                $rental_arr = array(
+                    "id" => $rentalPrice->id,
+                    "item_id" => $rentalPrice->item_id,
+                    "client_id" => $rentalPrice->client_id,
+                    "start_date" => $rentalPrice->start_date,
+                    "end_date" => $rentalPrice->end_date,
+                    "daily_price" => $rentalPrice->daily_price,
+                    "created_at" => $rentalPrice->created_at,
+                    "updated_at" => $rentalPrice->updated_at
+                );
+                
+                http_response_code(200);
+                echo json_encode($rental_arr);
+            } else {
+                http_response_code(404);
+                echo json_encode(array("message" => "Rental price not found."));
+            }
+        } elseif(isset($_GET['item_id'])) {
+            $rentalPrice->item_id = $_GET['item_id'];
+            $stmt = $rentalPrice->read();
+            $num = $stmt->rowCount();
+            
+            if($num > 0) {
+                $rentals_arr = array();
+                $rentals_arr["records"] = array();
+                
+                while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
+                    extract($row);
+                    
+                    $rental_item = array(
+                        "id" => $id,
+                        "item_id" => $item_id,
+                        "client_id" => $client_id,
+                        "start_date" => $start_date,
+                        "end_date" => $end_date,
+                        "daily_price" => $daily_price,
+                        "created_at" => $created_at,
+                        "updated_at" => $updated_at
+                    );
+                    
+                    array_push($rentals_arr["records"], $rental_item);
+                }
+                
+                http_response_code(200);
+                echo json_encode($rentals_arr);
+            } else {
+                http_response_code(200);
+                echo json_encode(array("records" => array()));
+            }
+        } else {
+            http_response_code(400);
+            echo json_encode(array("message" => "Missing item_id parameter."));
+        }
+        break;
+        
+    case 'POST':
+        $data = json_decode(file_get_contents("php://input"));
+        
+        if(!empty($data->item_id) && !empty($data->start_date) && !empty($data->end_date) && !empty($data->daily_price)) {
+            $rentalPrice->item_id = $data->item_id;
+            $rentalPrice->client_id = $data->client_id ?? null;
+            $rentalPrice->start_date = $data->start_date;
+            $rentalPrice->end_date = $data->end_date;
+            $rentalPrice->daily_price = $data->daily_price;
+            
+            if($rentalPrice->create()) {
+                http_response_code(201);
+                echo json_encode(array("message" => "Rental price was created."));
+            } else {
+                http_response_code(503);
+                echo json_encode(array("message" => "Unable to create rental price."));
+            }
+        } else {
+            http_response_code(400);
+            echo json_encode(array("message" => "Unable to create rental price. Data is incomplete."));
+        }
+        break;
+        
+    case 'PUT':
+        $data = json_decode(file_get_contents("php://input"));
+        
+        if(!empty($data->id) && !empty($data->item_id) && !empty($data->start_date) && !empty($data->end_date) && !empty($data->daily_price)) {
+            $rentalPrice->id = $data->id;
+            $rentalPrice->item_id = $data->item_id;
+            $rentalPrice->client_id = $data->client_id ?? null;
+            $rentalPrice->start_date = $data->start_date;
+            $rentalPrice->end_date = $data->end_date;
+            $rentalPrice->daily_price = $data->daily_price;
+            
+            if($rentalPrice->update()) {
+                http_response_code(200);
+                echo json_encode(array("message" => "Rental price was updated."));
+            } else {
+                http_response_code(503);
+                echo json_encode(array("message" => "Unable to update rental price."));
+            }
+        } else {
+            http_response_code(400);
+            echo json_encode(array("message" => "Unable to update rental price. Data is incomplete."));
+        }
+        break;
+        
+    case 'DELETE':
+        if(isset($_GET['id'])) {
+            $rentalPrice->id = $_GET['id'];
+            
+            if($rentalPrice->delete()) {
+                http_response_code(200);
+                echo json_encode(array("message" => "Rental price was deleted."));
+            } else {
+                http_response_code(503);
+                echo json_encode(array("message" => "Unable to delete rental price."));
+            }
+        } else {
+            http_response_code(400);
+            echo json_encode(array("message" => "Unable to delete rental price. ID is missing."));
+        }
+        break;
+        
+    default:
+        http_response_code(405);
+        echo json_encode(array("message" => "Method not allowed."));
+        break;
+}
+?>

+ 195 - 0
backend/api/subprojects.php

@@ -0,0 +1,195 @@
+<?php
+header("Access-Control-Allow-Origin: *");
+header("Content-Type: application/json; charset=UTF-8");
+header("Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS");
+header("Access-Control-Allow-Headers: Content-Type, Access-Control-Allow-Headers, Authorization, X-Requested-With");
+
+if ($_SERVER['REQUEST_METHOD'] == 'OPTIONS') {
+    exit(0);
+}
+
+require_once '../config/database.php';
+require_once '../models/Subproject.php';
+
+$database = new Database();
+$db = $database->getConnection();
+
+$subproject = new Subproject($db);
+
+$request_method = $_SERVER['REQUEST_METHOD'];
+
+switch($request_method) {
+    case 'GET':
+        if(isset($_GET['id'])) {
+            $subproject->id = $_GET['id'];
+            $subproject->readOne();
+            
+            if($subproject->subproject_name != null) {
+                $subproject_arr = array(
+                    "id" => $subproject->id,
+                    "project_id" => $subproject->project_id,
+                    "subproject_name" => $subproject->subproject_name,
+                    "description" => $subproject->description,
+                    "status" => $subproject->status,
+                    "start_date" => $subproject->start_date,
+                    "end_date" => $subproject->end_date,
+                    "budget" => $subproject->budget,
+                    "created_at" => $subproject->created_at,
+                    "updated_at" => $subproject->updated_at,
+                    "status_badge" => $subproject->getStatusBadge(),
+                    "progress" => $subproject->getProgress()
+                );
+                
+                http_response_code(200);
+                echo json_encode($subproject_arr);
+            } else {
+                http_response_code(404);
+                echo json_encode(array("message" => "Subproject not found."));
+            }
+        } elseif(isset($_GET['project_id'])) {
+            $stmt = $subproject->read();
+            $num = $stmt->rowCount();
+            
+            if($num > 0) {
+                $subprojects_arr = array();
+                $subprojects_arr["records"] = array();
+                
+                while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
+                    extract($row);
+                    
+                    $subproject_item = array(
+                        "id" => $id,
+                        "project_id" => $project_id,
+                        "subproject_name" => $subproject_name,
+                        "description" => $description,
+                        "status" => $status,
+                        "start_date" => $start_date,
+                        "end_date" => $end_date,
+                        "budget" => $budget,
+                        "created_at" => $created_at,
+                        "updated_at" => $updated_at,
+                        "status_badge" => $subproject->getStatusBadge(),
+                        "progress" => $subproject->getProgress()
+                    );
+                    
+                    array_push($subprojects_arr["records"], $subproject_item);
+                }
+                
+                http_response_code(200);
+                echo json_encode($subprojects_arr);
+            } else {
+                http_response_code(200);
+                echo json_encode(array("records" => array()));
+            }
+        } else {
+            $stmt = $subproject->read();
+            $num = $stmt->rowCount();
+            
+            if($num > 0) {
+                $subprojects_arr = array();
+                $subprojects_arr["records"] = array();
+                
+                while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
+                    extract($row);
+                    
+                    $subproject_item = array(
+                        "id" => $id,
+                        "project_id" => $project_id,
+                        "subproject_name" => $subproject_name,
+                        "description" => $description,
+                        "status" => $status,
+                        "start_date" => $start_date,
+                        "end_date" => $end_date,
+                        "budget" => $budget,
+                        "created_at" => $created_at,
+                        "updated_at" => $updated_at,
+                        "status_badge" => $subproject->getStatusBadge(),
+                        "progress" => $subproject->getProgress()
+                    );
+                    
+                    array_push($subprojects_arr["records"], $subproject_item);
+                }
+                
+                http_response_code(200);
+                echo json_encode($subprojects_arr);
+            } else {
+                http_response_code(200);
+                echo json_encode(array("records" => array()));
+            }
+        }
+        break;
+        
+    case 'POST':
+        $data = json_decode(file_get_contents("php://input"));
+        
+        if(!empty($data->project_id) && !empty($data->subproject_name)) {
+            $subproject->project_id = $data->project_id;
+            $subproject->subproject_name = $data->subproject_name;
+            $subproject->description = $data->description ?? '';
+            $subproject->status = $data->status ?? 'planning';
+            $subproject->start_date = $data->start_date ?? null;
+            $subproject->end_date = $data->end_date ?? null;
+            $subproject->budget = $data->budget ?? null;
+            
+            if($subproject->create()) {
+                http_response_code(201);
+                echo json_encode(array("message" => "Subproject was created."));
+            } else {
+                http_response_code(503);
+                echo json_encode(array("message" => "Unable to create subproject."));
+            }
+        } else {
+            http_response_code(400);
+            echo json_encode(array("message" => "Unable to create subproject. Project ID and subproject name are required."));
+        }
+        break;
+        
+    case 'PUT':
+        $data = json_decode(file_get_contents("php://input"));
+        
+        if(!empty($data->id) && !empty($data->project_id) && !empty($data->subproject_name)) {
+            $subproject->id = $data->id;
+            $subproject->project_id = $data->project_id;
+            $subproject->subproject_name = $data->subproject_name;
+            $subproject->description = $data->description ?? '';
+            $subproject->status = $data->status ?? 'planning';
+            $subproject->start_date = $data->start_date ?? null;
+            $subproject->end_date = $data->end_date ?? null;
+            $subproject->budget = $data->budget ?? null;
+            
+            if($subproject->update()) {
+                http_response_code(200);
+                echo json_encode(array("message" => "Subproject was updated."));
+            } else {
+                http_response_code(503);
+                echo json_encode(array("message" => "Unable to update subproject."));
+            }
+        } else {
+            http_response_code(400);
+            echo json_encode(array("message" => "Unable to update subproject. Data is incomplete."));
+        }
+        break;
+        
+    case 'DELETE':
+        if(isset($_GET['id'])) {
+            $subproject->id = $_GET['id'];
+            
+            if($subproject->delete()) {
+                http_response_code(200);
+                echo json_encode(array("message" => "Subproject was deleted."));
+            } else {
+                http_response_code(503);
+                echo json_encode(array("message" => "Unable to delete subproject."));
+            }
+        } else {
+            http_response_code(400);
+            echo json_encode(array("message" => "Unable to delete subproject. ID is missing."));
+        }
+        break;
+        
+    default:
+        http_response_code(405);
+        echo json_encode(array("message" => "Method not allowed."));
+        break;
+}
+?>

+ 68 - 0
backend/api/upload.php

@@ -0,0 +1,68 @@
+<?php
+header("Access-Control-Allow-Origin: *");
+header("Content-Type: application/json; charset=UTF-8");
+header("Access-Control-Allow-Methods: POST, OPTIONS");
+header("Access-Control-Allow-Headers: Content-Type, Access-Control-Allow-Headers, Authorization, X-Requested-With");
+
+if ($_SERVER['REQUEST_METHOD'] == 'OPTIONS') {
+    exit(0);
+}
+
+if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
+    http_response_code(405);
+    echo json_encode(array("message" => "Method not allowed."));
+    exit;
+}
+
+if (!isset($_FILES['picture']) || $_FILES['picture']['error'] !== UPLOAD_ERR_OK) {
+    http_response_code(400);
+    echo json_encode(array("message" => "No file uploaded or upload error."));
+    exit;
+}
+
+$uploadDir = 'uploads/';
+$allowedTypes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
+$maxFileSize = 5 * 1024 * 1024; // 5MB
+
+if (!file_exists($uploadDir)) {
+    mkdir($uploadDir, 0755, true);
+}
+
+$file = $_FILES['picture'];
+$fileType = $file['type'];
+$fileSize = $file['size'];
+$fileName = $file['name'];
+$tmpName = $file['tmp_name'];
+
+if (!in_array($fileType, $allowedTypes)) {
+    http_response_code(400);
+    echo json_encode(array("message" => "Invalid file type. Only JPEG, PNG, GIF, and WebP are allowed."));
+    exit;
+}
+
+if ($fileSize > $maxFileSize) {
+    http_response_code(400);
+    echo json_encode(array("message" => "File too large. Maximum size is 5MB."));
+    exit;
+}
+
+$fileExtension = pathinfo($fileName, PATHINFO_EXTENSION);
+$uniqueFileName = uniqid() . '.' . $fileExtension;
+$uploadPath = $uploadDir . $uniqueFileName;
+
+if (move_uploaded_file($tmpName, $uploadPath)) {
+    $baseUrl = (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on' ? "https" : "http") . "://$_SERVER[HTTP_HOST]";
+    $apiPath = dirname($_SERVER['PHP_SELF']);
+    $fullUrl = $baseUrl . $apiPath . '/' . $uploadPath;
+    
+    http_response_code(200);
+    echo json_encode(array(
+        "message" => "File uploaded successfully.",
+        "filename" => $uniqueFileName,
+        "url" => $fullUrl
+    ));
+} else {
+    http_response_code(500);
+    echo json_encode(array("message" => "Failed to upload file."));
+}
+?>

+ 23 - 0
backend/config/database.php

@@ -0,0 +1,23 @@
+<?php
+class Database {
+    private $host = 'localhost';
+    private $db_name = 'inventory_db';
+    private $username = 'root';
+    private $password = '';
+    public $conn;
+
+    public function getConnection() {
+        $this->conn = null;
+
+        try {
+            $this->conn = new PDO("mysql:host=" . $this->host . ";dbname=" . $this->db_name, $this->username, $this->password);
+            $this->conn->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
+            $this->conn->exec("set names utf8");
+        } catch(PDOException $exception) {
+            echo "Connection error: " . $exception->getMessage();
+        }
+
+        return $this->conn;
+    }
+}
+?>

+ 25 - 0
backend/middleware/auth_middleware.php

@@ -0,0 +1,25 @@
+<?php
+require_once '../SessionManager.php';
+
+class AuthMiddleware {
+    public static function authenticate() {
+        SessionManager::startSession();
+        
+        if (!SessionManager::isLoggedIn()) {
+            http_response_code(401);
+            echo json_encode(array('message' => 'Unauthorized'));
+            exit();
+        }
+        
+        return SessionManager::getCurrentUser();
+    }
+
+    public static function requireRole($required_role) {
+        SessionManager::requireRole($required_role);
+    }
+
+    public static function requireAdmin() {
+        SessionManager::requireAdmin();
+    }
+}
+?>

+ 54 - 0
backend/migrate_accounting.sql

@@ -0,0 +1,54 @@
+USE inventory_db;
+
+CREATE TABLE IF NOT EXISTS invoices (
+    id INT(11) AUTO_INCREMENT PRIMARY KEY,
+    client_id INT(11) NOT NULL,
+    invoice_number VARCHAR(50) NOT NULL UNIQUE,
+    issue_date DATE NOT NULL,
+    due_date DATE NOT NULL,
+    status ENUM('draft', 'sent', 'paid', 'overdue', 'cancelled') DEFAULT 'draft',
+    subtotal DECIMAL(10,2) NOT NULL DEFAULT 0.00,
+    tax_amount DECIMAL(10,2) NOT NULL DEFAULT 0.00,
+    total_amount DECIMAL(10,2) NOT NULL DEFAULT 0.00,
+    notes TEXT NULL,
+    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+    FOREIGN KEY (client_id) REFERENCES clients(id) ON DELETE RESTRICT,
+    INDEX idx_client_status (client_id, status),
+    INDEX idx_invoice_number (invoice_number),
+    INDEX idx_due_date (due_date)
+);
+
+CREATE TABLE IF NOT EXISTS invoice_items (
+    id INT(11) AUTO_INCREMENT PRIMARY KEY,
+    invoice_id INT(11) NOT NULL,
+    item_id INT(11) NOT NULL,
+    description VARCHAR(255) NOT NULL,
+    quantity DECIMAL(10,2) NOT NULL DEFAULT 1.00,
+    unit_price DECIMAL(10,2) NOT NULL DEFAULT 0.00,
+    line_total DECIMAL(10,2) NOT NULL DEFAULT 0.00,
+    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+    FOREIGN KEY (invoice_id) REFERENCES invoices(id) ON DELETE CASCADE,
+    FOREIGN KEY (item_id) REFERENCES items(id) ON DELETE RESTRICT,
+    INDEX idx_invoice (invoice_id),
+    INDEX idx_item (item_id)
+);
+
+CREATE TABLE IF NOT EXISTS payments (
+    id INT(11) AUTO_INCREMENT PRIMARY KEY,
+    invoice_id INT(11) NULL,
+    client_id INT(11) NOT NULL,
+    payment_date DATE NOT NULL,
+    amount DECIMAL(10,2) NOT NULL DEFAULT 0.00,
+    payment_method ENUM('cash', 'check', 'credit_card', 'bank_transfer', 'other') DEFAULT 'cash',
+    reference_number VARCHAR(50) NULL,
+    notes TEXT NULL,
+    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+    FOREIGN KEY (invoice_id) REFERENCES invoices(id) ON DELETE SET NULL,
+    FOREIGN KEY (client_id) REFERENCES clients(id) ON DELETE RESTRICT,
+    INDEX idx_invoice (invoice_id),
+    INDEX idx_client (client_id),
+    INDEX idx_payment_date (payment_date)
+);

+ 5 - 0
backend/migrate_add_fields.sql

@@ -0,0 +1,5 @@
+USE inventory_db;
+
+ALTER TABLE items 
+ADD COLUMN serial_number VARCHAR(100) NULL AFTER description,
+ADD COLUMN picture VARCHAR(255) NULL AFTER serial_number;

+ 19 - 0
backend/migrate_auth.sql

@@ -0,0 +1,19 @@
+USE inventory_db;
+
+CREATE TABLE IF NOT EXISTS users (
+    id INT(11) AUTO_INCREMENT PRIMARY KEY,
+    username VARCHAR(50) NOT NULL UNIQUE,
+    email VARCHAR(255) NOT NULL UNIQUE,
+    password_hash VARCHAR(255) NOT NULL,
+    first_name VARCHAR(100) NOT NULL,
+    last_name VARCHAR(100) NOT NULL,
+    role ENUM('admin', 'manager', 'user') DEFAULT 'user',
+    is_active BOOLEAN DEFAULT TRUE,
+    last_login TIMESTAMP NULL,
+    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+    INDEX idx_username (username),
+    INDEX idx_email (email),
+    INDEX idx_role (role),
+    INDEX idx_active (is_active)
+);

+ 47 - 0
backend/migrate_bookkeeping.sql

@@ -0,0 +1,47 @@
+USE inventory_db;
+
+CREATE TABLE IF NOT EXISTS chart_of_accounts (
+    id INT(11) AUTO_INCREMENT PRIMARY KEY,
+    account_number VARCHAR(20) NOT NULL UNIQUE,
+    account_name VARCHAR(255) NOT NULL,
+    account_type ENUM('asset', 'liability', 'equity', 'revenue', 'expense') NOT NULL,
+    parent_id INT(11) NULL,
+    description TEXT NULL,
+    opening_balance DECIMAL(10,2) DEFAULT 0.00,
+    current_balance DECIMAL(10,2) DEFAULT 0.00,
+    is_active BOOLEAN DEFAULT TRUE,
+    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+    FOREIGN KEY (parent_id) REFERENCES chart_of_accounts(id) ON DELETE SET NULL,
+    INDEX idx_account_type (account_type),
+    INDEX idx_parent (parent_id),
+    INDEX idx_account_number (account_number)
+);
+
+CREATE TABLE IF NOT EXISTS journal_entries (
+    id INT(11) AUTO_INCREMENT PRIMARY KEY,
+    entry_number VARCHAR(50) NOT NULL UNIQUE,
+    entry_date DATE NOT NULL,
+    description TEXT NULL,
+    reference_number VARCHAR(50) NULL,
+    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+    INDEX idx_entry_date (entry_date),
+    INDEX idx_entry_number (entry_number)
+);
+
+CREATE TABLE IF NOT EXISTS account_transactions (
+    id INT(11) AUTO_INCREMENT PRIMARY KEY,
+    journal_entry_id INT(11) NOT NULL,
+    account_id INT(11) NOT NULL,
+    debit_amount DECIMAL(10,2) DEFAULT 0.00,
+    credit_amount DECIMAL(10,2) DEFAULT 0.00,
+    description TEXT NULL,
+    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+    FOREIGN KEY (journal_entry_id) REFERENCES journal_entries(id) ON DELETE CASCADE,
+    FOREIGN KEY (account_id) REFERENCES chart_of_accounts(id) ON DELETE RESTRICT,
+    INDEX idx_journal_entry (journal_entry_id),
+    INDEX idx_account (account_id),
+    INDEX idx_debit_credit (debit_amount, credit_amount)
+);

+ 20 - 0
backend/migrate_clients.sql

@@ -0,0 +1,20 @@
+USE inventory_db;
+
+CREATE TABLE IF NOT EXISTS clients (
+    id INT(11) AUTO_INCREMENT PRIMARY KEY,
+    company_name VARCHAR(255) NULL,
+    first_name VARCHAR(100) NOT NULL,
+    last_name VARCHAR(100) NOT NULL,
+    email VARCHAR(255) NOT NULL UNIQUE,
+    phone VARCHAR(20) NULL,
+    address VARCHAR(255) NULL,
+    city VARCHAR(100) NULL,
+    state VARCHAR(100) NULL,
+    postal_code VARCHAR(20) NULL,
+    country VARCHAR(100) NULL,
+    notes TEXT NULL,
+    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+    INDEX idx_email (email),
+    INDEX idx_name (last_name, first_name)
+);

+ 254 - 0
backend/migrate_complete.sql

@@ -0,0 +1,254 @@
+-- Complete Database Migration Script
+-- This script combines all migration files for a complete database setup
+
+USE inventory_db;
+
+-- Items table with serial numbers and pictures
+CREATE TABLE IF NOT EXISTS items (
+    id INT(11) AUTO_INCREMENT PRIMARY KEY,
+    name VARCHAR(255) NOT NULL,
+    description TEXT NULL,
+    quantity INT(11) NOT NULL DEFAULT 0,
+    price DECIMAL(10,2) NOT NULL DEFAULT 0.00,
+    serial_number VARCHAR(100) NULL,
+    picture VARCHAR(255) NULL,
+    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+    INDEX idx_name (name)
+);
+
+-- Clients table with y-tunnus and hourly rates
+CREATE TABLE IF NOT EXISTS clients (
+    id INT(11) AUTO_INCREMENT PRIMARY KEY,
+    y_tunnus VARCHAR(255) NULL,
+    company_name VARCHAR(255) NULL,
+    first_name VARCHAR(100) NOT NULL,
+    last_name VARCHAR(100) NOT NULL,
+    email VARCHAR(255) NOT NULL UNIQUE,
+    phone VARCHAR(20) NULL,
+    address VARCHAR(255) NULL,
+    city VARCHAR(100) NULL,
+    state VARCHAR(100) NULL,
+    postal_code VARCHAR(20) NULL,
+    country VARCHAR(100) NULL,
+    notes TEXT NULL,
+    hour_price DECIMAL(10,2) DEFAULT 0.00,
+    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+    INDEX idx_email (email),
+    INDEX idx_name (last_name, first_name)
+);
+
+-- Contact persons table for individual contacts
+CREATE TABLE IF NOT EXISTS contact_persons (
+    id INT(11) AUTO_INCREMENT PRIMARY KEY,
+    client_id INT(11) NOT NULL,
+    first_name VARCHAR(100) NOT NULL,
+    last_name VARCHAR(100) NOT NULL,
+    email VARCHAR(255) NULL,
+    phone VARCHAR(20) NULL,
+    position VARCHAR(100) NULL,
+    is_primary BOOLEAN DEFAULT FALSE,
+    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+    FOREIGN KEY (client_id) REFERENCES clients(id) ON DELETE CASCADE,
+    INDEX idx_client_primary (client_id, is_primary),
+    INDEX idx_name (last_name, first_name)
+);
+
+-- Rental prices table for item pricing
+CREATE TABLE IF NOT EXISTS rental_prices (
+    id INT(11) AUTO_INCREMENT PRIMARY KEY,
+    item_id INT(11) NOT NULL,
+    client_id INT(11) NULL,
+    start_date DATE NOT NULL,
+    end_date DATE NOT NULL,
+    daily_price DECIMAL(10,2) NOT NULL DEFAULT 0.00,
+    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+    FOREIGN KEY (item_id) REFERENCES items(id) ON DELETE CASCADE,
+    FOREIGN KEY (client_id) REFERENCES clients(id) ON DELETE SET NULL,
+    INDEX idx_item_dates (item_id, start_date, end_date)
+);
+
+-- Attachments table for document management
+CREATE TABLE IF NOT EXISTS attachments (
+    id INT(11) AUTO_INCREMENT PRIMARY KEY,
+    item_id INT(11) NOT NULL,
+    filename VARCHAR(255) NOT NULL,
+    original_name VARCHAR(255) NOT NULL,
+    file_type ENUM('receipt', 'warranty', 'other') NOT NULL DEFAULT 'other',
+    file_path VARCHAR(255) NOT NULL,
+    file_size INT(11) NOT NULL,
+    mime_type VARCHAR(100) NOT NULL,
+    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+    FOREIGN KEY (item_id) REFERENCES items(id) ON DELETE CASCADE,
+    INDEX idx_item_type (item_id, file_type)
+);
+
+-- Users table for authentication
+CREATE TABLE IF NOT EXISTS users (
+    id INT(11) AUTO_INCREMENT PRIMARY KEY,
+    username VARCHAR(50) NOT NULL UNIQUE,
+    email VARCHAR(255) NOT NULL UNIQUE,
+    password_hash VARCHAR(255) NOT NULL,
+    first_name VARCHAR(100) NOT NULL,
+    last_name VARCHAR(100) NOT NULL,
+    role ENUM('admin', 'manager', 'user') DEFAULT 'user',
+    is_active BOOLEAN DEFAULT TRUE,
+    last_login TIMESTAMP NULL,
+    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+    INDEX idx_username (username),
+    INDEX idx_email (email),
+    INDEX idx_role (role),
+    INDEX idx_active (is_active)
+);
+
+-- Projects table for customer project management
+CREATE TABLE IF NOT EXISTS projects (
+    id INT(11) AUTO_INCREMENT PRIMARY KEY,
+    customer_id INT(11) NOT NULL,
+    project_name VARCHAR(255) NOT NULL,
+    description TEXT NULL,
+    status ENUM('planning', 'in_progress', 'completed', 'on_hold', 'cancelled') DEFAULT 'planning',
+    start_date DATE NULL,
+    end_date DATE NULL,
+    budget DECIMAL(10,2) NULL,
+    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+    FOREIGN KEY (customer_id) REFERENCES clients(id) ON DELETE CASCADE,
+    INDEX idx_customer (customer_id),
+    INDEX idx_status (status),
+    INDEX idx_dates (start_date, end_date)
+);
+
+-- Subprojects table for detailed project tracking
+CREATE TABLE IF NOT EXISTS subprojects (
+    id INT(11) AUTO_INCREMENT PRIMARY KEY,
+    project_id INT(11) NOT NULL,
+    subproject_name VARCHAR(255) NOT NULL,
+    description TEXT NULL,
+    status ENUM('planning', 'in_progress', 'completed', 'on_hold', 'cancelled') DEFAULT 'planning',
+    start_date DATE NULL,
+    end_date DATE NULL,
+    budget DECIMAL(10,2) NULL,
+    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+    FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE,
+    INDEX idx_project (project_id),
+    INDEX idx_status (status),
+    INDEX idx_dates (start_date, end_date)
+);
+
+-- Invoices table for billing management
+CREATE TABLE IF NOT EXISTS invoices (
+    id INT(11) AUTO_INCREMENT PRIMARY KEY,
+    client_id INT(11) NOT NULL,
+    invoice_number VARCHAR(50) NOT NULL UNIQUE,
+    issue_date DATE NOT NULL,
+    due_date DATE NOT NULL,
+    status ENUM('draft', 'sent', 'paid', 'overdue', 'cancelled') DEFAULT 'draft',
+    subtotal DECIMAL(10,2) NOT NULL DEFAULT 0.00,
+    tax_amount DECIMAL(10,2) NOT NULL DEFAULT 0.00,
+    total_amount DECIMAL(10,2) NOT NULL DEFAULT 0.00,
+    notes TEXT NULL,
+    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+    FOREIGN KEY (client_id) REFERENCES clients(id) ON DELETE RESTRICT,
+    INDEX idx_client_status (client_id, status),
+    INDEX idx_invoice_number (invoice_number),
+    INDEX idx_due_date (due_date)
+);
+
+-- Invoice items table for line item management
+CREATE TABLE IF NOT EXISTS invoice_items (
+    id INT(11) AUTO_INCREMENT PRIMARY KEY,
+    invoice_id INT(11) NOT NULL,
+    item_id INT(11) NOT NULL,
+    description VARCHAR(255) NOT NULL,
+    quantity DECIMAL(10,2) NOT NULL DEFAULT 1.00,
+    unit_price DECIMAL(10,2) NOT NULL DEFAULT 0.00,
+    line_total DECIMAL(10,2) NOT NULL DEFAULT 0.00,
+    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+    FOREIGN KEY (invoice_id) REFERENCES invoices(id) ON DELETE CASCADE,
+    FOREIGN KEY (item_id) REFERENCES items(id) ON DELETE RESTRICT,
+    INDEX idx_invoice (invoice_id),
+    INDEX idx_item (item_id)
+);
+
+-- Payments table for transaction tracking
+CREATE TABLE IF NOT EXISTS payments (
+    id INT(11) AUTO_INCREMENT PRIMARY KEY,
+    invoice_id INT(11) NULL,
+    client_id INT(11) NOT NULL,
+    payment_date DATE NOT NULL,
+    amount DECIMAL(10,2) NOT NULL DEFAULT 0.00,
+    payment_method ENUM('cash', 'check', 'credit_card', 'bank_transfer', 'other') DEFAULT 'cash',
+    reference_number VARCHAR(50) NULL,
+    notes TEXT NULL,
+    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+    FOREIGN KEY (invoice_id) REFERENCES invoices(id) ON DELETE SET NULL,
+    FOREIGN KEY (client_id) REFERENCES clients(id) ON DELETE RESTRICT,
+    INDEX idx_invoice (invoice_id),
+    INDEX idx_client (client_id),
+    INDEX idx_payment_date (payment_date)
+);
+
+-- Chart of accounts for bookkeeping
+CREATE TABLE IF NOT EXISTS chart_of_accounts (
+    id INT(11) AUTO_INCREMENT PRIMARY KEY,
+    account_number VARCHAR(20) NOT NULL UNIQUE,
+    account_name VARCHAR(255) NOT NULL,
+    account_type ENUM('asset', 'liability', 'equity', 'revenue', 'expense') NOT NULL,
+    parent_id INT(11) NULL,
+    description TEXT NULL,
+    opening_balance DECIMAL(10,2) DEFAULT 0.00,
+    current_balance DECIMAL(10,2) DEFAULT 0.00,
+    is_active BOOLEAN DEFAULT TRUE,
+    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+    FOREIGN KEY (parent_id) REFERENCES chart_of_accounts(id) ON DELETE SET NULL,
+    INDEX idx_account_type (account_type),
+    INDEX idx_parent (parent_id),
+    INDEX idx_account_number (account_number)
+);
+
+-- Journal entries table for double-entry bookkeeping
+CREATE TABLE IF NOT EXISTS journal_entries (
+    id INT(11) AUTO_INCREMENT PRIMARY KEY,
+    entry_number VARCHAR(50) NOT NULL UNIQUE,
+    entry_date DATE NOT NULL,
+    description TEXT NULL,
+    reference_number VARCHAR(50) NULL,
+    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+    INDEX idx_entry_date (entry_date),
+    INDEX idx_entry_number (entry_number)
+);
+
+-- Account transactions table for detailed bookkeeping
+CREATE TABLE IF NOT EXISTS account_transactions (
+    id INT(11) AUTO_INCREMENT PRIMARY KEY,
+    journal_entry_id INT(11) NOT NULL,
+    account_id INT(11) NOT NULL,
+    debit_amount DECIMAL(10,2) DEFAULT 0.00,
+    credit_amount DECIMAL(10,2) DEFAULT 0.00,
+    description TEXT NULL,
+    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+    FOREIGN KEY (journal_entry_id) REFERENCES journal_entries(id) ON DELETE CASCADE,
+    FOREIGN KEY (account_id) REFERENCES chart_of_accounts(id) ON DELETE RESTRICT,
+    INDEX idx_journal_entry (journal_entry_id),
+    INDEX idx_account (account_id),
+    INDEX idx_debit_credit (debit_amount, credit_amount)
+);
+
+-- Insert sample data
+INSERT INTO items (name, description, quantity, price) VALUES 
+('Laptop', 'Dell XPS 15 laptop', 5, 1299.99),
+('Mouse', 'Wireless optical mouse', 20, 25.50),
+('Keyboard', 'Mechanical keyboard', 15, 89.99);

+ 27 - 0
backend/migrate_new_tables.sql

@@ -0,0 +1,27 @@
+USE inventory_db;
+
+CREATE TABLE IF NOT EXISTS rental_prices (
+    id INT(11) AUTO_INCREMENT PRIMARY KEY,
+    item_id INT(11) NOT NULL,
+    start_date DATE NOT NULL,
+    end_date DATE NOT NULL,
+    daily_price DECIMAL(10,2) NOT NULL DEFAULT 0.00,
+    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+    FOREIGN KEY (item_id) REFERENCES items(id) ON DELETE CASCADE,
+    INDEX idx_item_dates (item_id, start_date, end_date)
+);
+
+CREATE TABLE IF NOT EXISTS attachments (
+    id INT(11) AUTO_INCREMENT PRIMARY KEY,
+    item_id INT(11) NOT NULL,
+    filename VARCHAR(255) NOT NULL,
+    original_name VARCHAR(255) NOT NULL,
+    file_type ENUM('receipt', 'warranty', 'other') NOT NULL DEFAULT 'other',
+    file_path VARCHAR(255) NOT NULL,
+    file_size INT(11) NOT NULL,
+    mime_type VARCHAR(100) NOT NULL,
+    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+    FOREIGN KEY (item_id) REFERENCES items(id) ON DELETE CASCADE,
+    INDEX idx_item_type (item_id, file_type)
+);

+ 40 - 0
backend/migrate_projects.sql

@@ -0,0 +1,40 @@
+USE inventory_db;
+
+-- Add hour_price field to clients table
+ALTER TABLE clients ADD COLUMN hour_price DECIMAL(10,2) DEFAULT 0.00 AFTER country;
+
+-- Create projects table
+CREATE TABLE IF NOT EXISTS projects (
+    id INT(11) AUTO_INCREMENT PRIMARY KEY,
+    customer_id INT(11) NOT NULL,
+    project_name VARCHAR(255) NOT NULL,
+    description TEXT NULL,
+    status ENUM('planning', 'in_progress', 'completed', 'on_hold', 'cancelled') DEFAULT 'planning',
+    start_date DATE NULL,
+    end_date DATE NULL,
+    budget DECIMAL(10,2) NULL,
+    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+    FOREIGN KEY (customer_id) REFERENCES clients(id) ON DELETE CASCADE,
+    INDEX idx_customer (customer_id),
+    INDEX idx_status (status),
+    INDEX idx_dates (start_date, end_date)
+);
+
+-- Create subprojects table
+CREATE TABLE IF NOT EXISTS subprojects (
+    id INT(11) AUTO_INCREMENT PRIMARY KEY,
+    project_id INT(11) NOT NULL,
+    subproject_name VARCHAR(255) NOT NULL,
+    description TEXT NULL,
+    status ENUM('planning', 'in_progress', 'completed', 'on_hold', 'cancelled') DEFAULT 'planning',
+    start_date DATE NULL,
+    end_date DATE NULL,
+    budget DECIMAL(10,2) NULL,
+    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+    FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE,
+    INDEX idx_project (project_id),
+    INDEX idx_status (status),
+    INDEX idx_dates (start_date, end_date)
+);

+ 5 - 0
backend/migrate_rental_client.sql

@@ -0,0 +1,5 @@
+USE inventory_db;
+
+ALTER TABLE rental_prices 
+ADD COLUMN client_id INT(11) NULL AFTER item_id,
+ADD FOREIGN KEY (client_id) REFERENCES clients(id) ON DELETE SET NULL;

+ 21 - 0
backend/migrate_y_tunnus.sql

@@ -0,0 +1,21 @@
+USE inventory_db;
+
+ALTER TABLE clients 
+ADD COLUMN y_tunnus VARCHAR(255) NULL AFTER company_name,
+ADD INDEX idx_y_tunnus (y_tunnus);
+
+CREATE TABLE IF NOT EXISTS contact_persons (
+    id INT(11) AUTO_INCREMENT PRIMARY KEY,
+    client_id INT(11) NOT NULL,
+    first_name VARCHAR(100) NOT NULL,
+    last_name VARCHAR(100) NOT NULL,
+    email VARCHAR(255) NULL,
+    phone VARCHAR(20) NULL,
+    position VARCHAR(100) NULL,
+    is_primary BOOLEAN DEFAULT FALSE,
+    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+    FOREIGN KEY (client_id) REFERENCES clients(id) ON DELETE CASCADE,
+    INDEX idx_client_primary (client_id, is_primary),
+    INDEX idx_name (last_name, first_name)
+);

+ 161 - 0
backend/models/AccountTransaction.php

@@ -0,0 +1,161 @@
+<?php
+class AccountTransaction {
+    private $conn;
+    private $table_name = "account_transactions";
+
+    public $id;
+    public $journal_entry_id;
+    public $account_id;
+    public $debit_amount;
+    public $credit_amount;
+    public $description;
+    public $created_at;
+    public $updated_at;
+
+    public function __construct($db) {
+        $this->conn = $db;
+    }
+
+    public function create() {
+        $query = "INSERT INTO " . $this->table_name . " SET journal_entry_id=:journal_entry_id, account_id=:account_id, debit_amount=:debit_amount, credit_amount=:credit_amount, description=:description, created_at=:created_at, updated_at=:updated_at";
+
+        $stmt = $this->conn->prepare($query);
+
+        $this->journal_entry_id = htmlspecialchars(strip_tags($this->journal_entry_id));
+        $this->account_id = htmlspecialchars(strip_tags($this->account_id));
+        $this->debit_amount = htmlspecialchars(strip_tags($this->debit_amount));
+        $this->credit_amount = htmlspecialchars(strip_tags($this->credit_amount));
+        $this->description = htmlspecialchars(strip_tags($this->description));
+        $this->created_at = date('Y-m-d H:i:s');
+        $this->updated_at = date('Y-m-d H:i:s');
+
+        $stmt->bindParam(":journal_entry_id", $this->journal_entry_id);
+        $stmt->bindParam(":account_id", $this->account_id);
+        $stmt->bindParam(":debit_amount", $this->debit_amount);
+        $stmt->bindParam(":credit_amount", $this->credit_amount);
+        $stmt->bindParam(":description", $this->description);
+        $stmt->bindParam(":created_at", $this->created_at);
+        $stmt->bindParam(":updated_at", $this->updated_at);
+
+        if($stmt->execute()) {
+            return true;
+        }
+
+        return false;
+    }
+
+    public function read() {
+        $query = "SELECT at.*, coa.account_name, coa.account_type, je.entry_number, je.entry_date FROM " . $this->table_name . " at LEFT JOIN chart_of_accounts coa ON at.account_id = coa.id LEFT JOIN journal_entries je ON at.journal_entry_id = je.id ORDER BY je.entry_date DESC, je.created_at DESC, at.id DESC";
+
+        $stmt = $this->conn->prepare($query);
+        $stmt->execute();
+
+        return $stmt;
+    }
+
+    public function readOne() {
+        $query = "SELECT at.*, coa.account_name, coa.account_type, je.entry_number, je.entry_date FROM " . $this->table_name . " at LEFT JOIN chart_of_accounts coa ON at.account_id = coa.id LEFT JOIN journal_entries je ON at.journal_entry_id = je.id WHERE at.id = ? LIMIT 0,1";
+
+        $stmt = $this->conn->prepare($query);
+        $stmt->bindParam(1, $this->id);
+        $stmt->execute();
+
+        $row = $stmt->fetch(PDO::FETCH_ASSOC);
+
+        $this->journal_entry_id = $row['journal_entry_id'];
+        $this->account_id = $row['account_id'];
+        $this->debit_amount = $row['debit_amount'];
+        $this->credit_amount = $row['credit_amount'];
+        $this->description = $row['description'];
+        $this->created_at = $row['created_at'];
+        $this->updated_at = $row['updated_at'];
+    }
+
+    public function update() {
+        $query = "UPDATE " . $this->table_name . " SET journal_entry_id=:journal_entry_id, account_id=:account_id, debit_amount=:debit_amount, credit_amount=:credit_amount, description=:description, updated_at=:updated_at WHERE id=:id";
+
+        $stmt = $this->conn->prepare($query);
+
+        $this->journal_entry_id = htmlspecialchars(strip_tags($this->journal_entry_id));
+        $this->account_id = htmlspecialchars(strip_tags($this->account_id));
+        $this->debit_amount = htmlspecialchars(strip_tags($this->debit_amount));
+        $this->credit_amount = htmlspecialchars(strip_tags($this->credit_amount));
+        $this->description = htmlspecialchars(strip_tags($this->description));
+        $this->updated_at = date('Y-m-d H:i:s');
+
+        $stmt->bindParam(":journal_entry_id", $this->journal_entry_id);
+        $stmt->bindParam(":account_id", $this->account_id);
+        $stmt->bindParam(":debit_amount", $this->debit_amount);
+        $stmt->bindParam(":credit_amount", $this->credit_amount);
+        $stmt->bindParam(":description", $this->description);
+        $stmt->bindParam(":updated_at", $this->updated_at);
+        $stmt->bindParam(":id", $this->id);
+
+        if($stmt->execute()) {
+            return true;
+        }
+
+        return false;
+    }
+
+    public function delete() {
+        $query = "DELETE FROM " . $this->table_name . " WHERE id = ?";
+
+        $stmt = $this->conn->prepare($query);
+        $stmt->bindParam(1, $this->id);
+
+        if($stmt->execute()) {
+            return true;
+        }
+
+        return false;
+    }
+
+    public function search($search_term) {
+        $query = "SELECT at.*, coa.account_name, coa.account_type, je.entry_number, je.entry_date FROM " . $this->table_name . " at LEFT JOIN chart_of_accounts coa ON at.account_id = coa.id LEFT JOIN journal_entries je ON at.journal_entry_id = je.id WHERE 
+                  at.description LIKE ? OR 
+                  coa.account_name LIKE ? OR 
+                  je.entry_number LIKE ? OR 
+                  je.entry_date LIKE ?
+                  ORDER BY je.entry_date DESC, je.created_at DESC, at.id DESC";
+
+        $stmt = $this->conn->prepare($query);
+        $search_term = "%{$search_term}%";
+        $stmt->bindParam(1, $search_term);
+        $stmt->bindParam(2, $search_term);
+        $stmt->bindParam(3, $search_term);
+        $stmt->bindParam(4, $search_term);
+        $stmt->execute();
+
+        return $stmt;
+    }
+
+    public function getBalance($account_id) {
+        $query = "SELECT SUM(debit_amount) - SUM(credit_amount) as balance FROM " . $this->table_name . " WHERE account_id = ?";
+        $stmt = $this->conn->prepare($query);
+        $stmt->bindParam(1, $account_id);
+        $stmt->execute();
+        
+        $row = $stmt->fetch(PDO::FETCH_ASSOC);
+        return $row['balance'] ?? 0;
+    }
+
+    public function getTransactionType() {
+        if ($this->debit_amount > 0 && $this->credit_amount == 0) {
+            return '<span style="background-color: #dc3545; color: white; padding: 2px 6px; border-radius: 4px; font-size: 12px;">Debit</span>';
+        } elseif ($this->debit_amount == 0 && $this->credit_amount > 0) {
+            return '<span style="background-color: #28a745; color: white; padding: 2px 6px; border-radius: 4px; font-size: 12px;">Credit</span>';
+        } else {
+            return '<span style="background-color: #ffc107; color: black; padding: 2px 6px; border-radius: 4px; font-size: 12px;">Mixed</span>';
+        }
+    }
+
+    public function getAmount() {
+        if ($this->debit_amount > 0) {
+            return $this->debit_amount;
+        } else {
+            return $this->credit_amount;
+        }
+    }
+}
+?>

+ 154 - 0
backend/models/Attachment.php

@@ -0,0 +1,154 @@
+<?php
+class Attachment {
+    private $conn;
+    private $table_name = "attachments";
+
+    public $id;
+    public $item_id;
+    public $filename;
+    public $original_name;
+    public $file_type;
+    public $file_path;
+    public $file_size;
+    public $mime_type;
+    public $created_at;
+
+    public function __construct($db) {
+        $this->conn = $db;
+    }
+
+    public function create() {
+        $query = "INSERT INTO " . $this->table_name . " SET item_id=:item_id, filename=:filename, original_name=:original_name, file_type=:file_type, file_path=:file_path, file_size=:file_size, mime_type=:mime_type, created_at=:created_at";
+
+        $stmt = $this->conn->prepare($query);
+
+        $this->item_id = htmlspecialchars(strip_tags($this->item_id));
+        $this->filename = htmlspecialchars(strip_tags($this->filename));
+        $this->original_name = htmlspecialchars(strip_tags($this->original_name));
+        $this->file_type = htmlspecialchars(strip_tags($this->file_type));
+        $this->file_path = htmlspecialchars(strip_tags($this->file_path));
+        $this->file_size = htmlspecialchars(strip_tags($this->file_size));
+        $this->mime_type = htmlspecialchars(strip_tags($this->mime_type));
+        $this->created_at = date('Y-m-d H:i:s');
+
+        $stmt->bindParam(":item_id", $this->item_id);
+        $stmt->bindParam(":filename", $this->filename);
+        $stmt->bindParam(":original_name", $this->original_name);
+        $stmt->bindParam(":file_type", $this->file_type);
+        $stmt->bindParam(":file_path", $this->file_path);
+        $stmt->bindParam(":file_size", $this->file_size);
+        $stmt->bindParam(":mime_type", $this->mime_type);
+        $stmt->bindParam(":created_at", $this->created_at);
+
+        if($stmt->execute()) {
+            return true;
+        }
+
+        return false;
+    }
+
+    public function read() {
+        $query = "SELECT * FROM " . $this->table_name . " WHERE item_id = ? ORDER BY created_at DESC";
+
+        $stmt = $this->conn->prepare($query);
+        $stmt->bindParam(1, $this->item_id);
+        $stmt->execute();
+
+        return $stmt;
+    }
+
+    public function readOne() {
+        $query = "SELECT * FROM " . $this->table_name . " WHERE id = ? LIMIT 0,1";
+
+        $stmt = $this->conn->prepare($query);
+        $stmt->bindParam(1, $this->id);
+        $stmt->execute();
+
+        $row = $stmt->fetch(PDO::FETCH_ASSOC);
+
+        $this->item_id = $row['item_id'];
+        $this->filename = $row['filename'];
+        $this->original_name = $row['original_name'];
+        $this->file_type = $row['file_type'];
+        $this->file_path = $row['file_path'];
+        $this->file_size = $row['file_size'];
+        $this->mime_type = $row['mime_type'];
+        $this->created_at = $row['created_at'];
+    }
+
+    public function delete() {
+        $query = "SELECT file_path FROM " . $this->table_name . " WHERE id = ?";
+        $stmt = $this->conn->prepare($query);
+        $stmt->bindParam(1, $this->id);
+        $stmt->execute();
+        $row = $stmt->fetch(PDO::FETCH_ASSOC);
+        
+        if($row && file_exists($row['file_path'])) {
+            unlink($row['file_path']);
+        }
+
+        $query = "DELETE FROM " . $this->table_name . " WHERE id = ?";
+        $stmt = $this->conn->prepare($query);
+        $stmt->bindParam(1, $this->id);
+
+        if($stmt->execute()) {
+            return true;
+        }
+
+        return false;
+    }
+
+    public function uploadFile($file, $item_id, $file_type) {
+        $uploadDir = 'attachments/';
+        $allowedTypes = [
+            'application/pdf',
+            'image/jpeg',
+            'image/png',
+            'image/gif',
+            'text/plain',
+            'application/msword',
+            'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
+        ];
+        $maxFileSize = 10 * 1024 * 1024; // 10MB
+
+        if (!file_exists($uploadDir)) {
+            mkdir($uploadDir, 0755, true);
+        }
+
+        if (!in_array($file['type'], $allowedTypes)) {
+            return ['success' => false, 'message' => 'Invalid file type.'];
+        }
+
+        if ($file['size'] > $maxFileSize) {
+            return ['success' => false, 'message' => 'File too large. Maximum size is 10MB.'];
+        }
+
+        $fileExtension = pathinfo($file['name'], PATHINFO_EXTENSION);
+        $uniqueFileName = uniqid() . '.' . $fileExtension;
+        $uploadPath = $uploadDir . $uniqueFileName;
+
+        if (move_uploaded_file($file['tmp_name'], $uploadPath)) {
+            $this->item_id = $item_id;
+            $this->filename = $uniqueFileName;
+            $this->original_name = $file['name'];
+            $this->file_type = $file_type;
+            $this->file_path = $uploadPath;
+            $this->file_size = $file['size'];
+            $this->mime_type = $file['type'];
+
+            if ($this->create()) {
+                $baseUrl = (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on' ? "https" : "http") . "://$_SERVER[HTTP_HOST]";
+                $apiPath = dirname($_SERVER['PHP_SELF']);
+                $fullUrl = $baseUrl . $apiPath . '/' . $uploadPath;
+                
+                return ['success' => true, 'url' => $fullUrl, 'id' => $this->conn->lastInsertId()];
+            } else {
+                unlink($uploadPath);
+                return ['success' => false, 'message' => 'Failed to save attachment record.'];
+            }
+        } else {
+            return ['success' => false, 'message' => 'Failed to upload file.'];
+        }
+    }
+}
+?>

+ 173 - 0
backend/models/ChartOfAccounts.php

@@ -0,0 +1,173 @@
+<?php
+class ChartOfAccounts {
+    private $conn;
+    private $table_name = "chart_of_accounts";
+
+    public $id;
+    public $account_number;
+    public $account_name;
+    public $account_type;
+    public $parent_id;
+    public $description;
+    public $opening_balance;
+    public $current_balance;
+    public $is_active;
+    public $created_at;
+    public $updated_at;
+
+    public function __construct($db) {
+        $this->conn = $db;
+    }
+
+    public function create() {
+        $query = "INSERT INTO " . $this->table_name . " SET account_number=:account_number, account_name=:account_name, account_type=:account_type, parent_id=:parent_id, description=:description, opening_balance=:opening_balance, current_balance=:current_balance, is_active=:is_active, created_at=:created_at, updated_at=:updated_at";
+
+        $stmt = $this->conn->prepare($query);
+
+        $this->account_number = htmlspecialchars(strip_tags($this->account_number));
+        $this->account_name = htmlspecialchars(strip_tags($this->account_name));
+        $this->account_type = htmlspecialchars(strip_tags($this->account_type));
+        $this->parent_id = htmlspecialchars(strip_tags($this->parent_id));
+        $this->description = htmlspecialchars(strip_tags($this->description));
+        $this->opening_balance = htmlspecialchars(strip_tags($this->opening_balance));
+        $this->current_balance = htmlspecialchars(strip_tags($this->current_balance));
+        $this->is_active = $this->is_active ? 1 : 0;
+        $this->created_at = date('Y-m-d H:i:s');
+        $this->updated_at = date('Y-m-d H:i:s');
+
+        $stmt->bindParam(":account_number", $this->account_number);
+        $stmt->bindParam(":account_name", $this->account_name);
+        $stmt->bindParam(":account_type", $this->account_type);
+        $stmt->bindParam(":parent_id", $this->parent_id);
+        $stmt->bindParam(":description", $this->description);
+        $stmt->bindParam(":opening_balance", $this->opening_balance);
+        $stmt->bindParam(":current_balance", $this->current_balance);
+        $stmt->bindParam(":is_active", $this->is_active);
+        $stmt->bindParam(":created_at", $this->created_at);
+        $stmt->bindParam(":updated_at", $this->updated_at);
+
+        if($stmt->execute()) {
+            return true;
+        }
+
+        return false;
+    }
+
+    public function read() {
+        $query = "SELECT * FROM " . $this->table_name . " WHERE is_active = TRUE ORDER BY account_type, account_number";
+
+        $stmt = $this->conn->prepare($query);
+        $stmt->execute();
+
+        return $stmt;
+    }
+
+    public function readOne() {
+        $query = "SELECT * FROM " . $this->table_name . " WHERE id = ? LIMIT 0,1";
+
+        $stmt = $this->conn->prepare($query);
+        $stmt->bindParam(1, $this->id);
+        $stmt->execute();
+
+        $row = $stmt->fetch(PDO::FETCH_ASSOC);
+
+        $this->account_number = $row['account_number'];
+        $this->account_name = $row['account_name'];
+        $this->account_type = $row['account_type'];
+        $this->parent_id = $row['parent_id'];
+        $this->description = $row['description'];
+        $this->opening_balance = $row['opening_balance'];
+        $this->current_balance = $row['current_balance'];
+        $this->is_active = $row['is_active'];
+        $this->created_at = $row['created_at'];
+        $this->updated_at = $row['updated_at'];
+    }
+
+    public function update() {
+        $query = "UPDATE " . $this->table_name . " SET account_number=:account_number, account_name=:account_name, account_type=:account_type, parent_id=:parent_id, description=:description, opening_balance=:opening_balance, current_balance=:current_balance, is_active=:is_active, updated_at=:updated_at WHERE id=:id";
+
+        $stmt = $this->conn->prepare($query);
+
+        $this->account_number = htmlspecialchars(strip_tags($this->account_number));
+        $this->account_name = htmlspecialchars(strip_tags($this->account_name));
+        $this->account_type = htmlspecialchars(strip_tags($this->account_type));
+        $this->parent_id = htmlspecialchars(strip_tags($this->parent_id));
+        $this->description = htmlspecialchars(strip_tags($this->description));
+        $this->opening_balance = htmlspecialchars(strip_tags($this->opening_balance));
+        $this->current_balance = htmlspecialchars(strip_tags($this->current_balance));
+        $this->is_active = $this->is_active ? 1 : 0;
+        $this->updated_at = date('Y-m-d H:i:s');
+
+        $stmt->bindParam(":account_number", $this->account_number);
+        $stmt->bindParam(":account_name", $this->account_name);
+        $stmt->bindParam(":account_type", $this->account_type);
+        $stmt->bindParam(":parent_id", $this->parent_id);
+        $stmt->bindParam(":description", $this->description);
+        $stmt->bindParam(":opening_balance", $this->opening_balance);
+        $stmt->bindParam(":current_balance", $this->current_balance);
+        $stmt->bindParam(":is_active", $this->is_active);
+        $stmt->bindParam(":updated_at", $this->updated_at);
+        $stmt->bindParam(":id", $this->id);
+
+        if($stmt->execute()) {
+            return true;
+        }
+
+        return false;
+    }
+
+    public function delete() {
+        $query = "DELETE FROM " . $this->table_name . " WHERE id = ?";
+
+        $stmt = $this->conn->prepare($query);
+        $stmt->bindParam(1, $this->id);
+
+        if($stmt->execute()) {
+            return true;
+        }
+
+        return false;
+    }
+
+    public function search($search_term) {
+        $query = "SELECT * FROM " . $this->table_name . " WHERE 
+                  account_number LIKE ? OR 
+                  account_name LIKE ? OR 
+                  description LIKE ?
+                  ORDER BY account_type, account_number";
+
+        $stmt = $this->conn->prepare($query);
+        $search_term = "%{$search_term}%";
+        $stmt->bindParam(1, $search_term);
+        $stmt->bindParam(2, $search_term);
+        $stmt->bindParam(3, $search_term);
+        $stmt->execute();
+
+        return $stmt;
+    }
+
+    public function getAccountTypeBadge() {
+        $badges = [
+            'asset' => '<span style="background-color: #007bff; color: white; padding: 2px 6px; border-radius: 4px; font-size: 12px;">Asset</span>',
+            'liability' => '<span style="background-color: #dc3545; color: white; padding: 2px 6px; border-radius: 4px; font-size: 12px;">Liability</span>',
+            'equity' => '<span style="background-color: #28a745; color: white; padding: 2px 6px; border-radius: 4px; font-size: 12px;">Equity</span>',
+            'revenue' => '<span style="background-color: #17a2b8; color: white; padding: 2px 6px; border-radius: 4px; font-size: 12px;">Revenue</span>',
+            'expense' => '<span style="background-color: #ffc107; color: black; padding: 2px 6px; border-radius: 4px; font-size: 12px;">Expense</span>'
+        ];
+        
+        return $badges[$this->account_type] ?? $this->account_type;
+    }
+
+    public function getAccountTypeName() {
+        $types = [
+            'asset' => 'Asset',
+            'liability' => 'Liability',
+            'equity' => 'Equity',
+            'revenue' => 'Revenue',
+            'expense' => 'Expense'
+        ];
+        
+        return $types[$this->account_type] ?? $this->account_type;
+    }
+}
+?>

+ 188 - 0
backend/models/Client.php

@@ -0,0 +1,188 @@
+<?php
+class Client {
+    private $conn;
+    private $table_name = "clients";
+
+    public $id;
+    public $company_name;
+    public $y_tunnus;
+    public $first_name;
+    public $last_name;
+    public $email;
+    public $phone;
+    public $address;
+    public $city;
+    public $state;
+    public $postal_code;
+    public $country;
+    public $notes;
+    public $created_at;
+    public $updated_at;
+
+    public function __construct($db) {
+        $this->conn = $db;
+    }
+
+    public function create() {
+        $query = "INSERT INTO " . $this->table_name . " SET company_name=:company_name, y_tunnus=:y_tunnus, first_name=:first_name, last_name=:last_name, email=:email, phone=:phone, address=:address, city=:city, state=:state, postal_code=:postal_code, country=:country, notes=:notes, created_at=:created_at, updated_at=:updated_at";
+
+        $stmt = $this->conn->prepare($query);
+
+        $this->company_name = htmlspecialchars(strip_tags($this->company_name));
+        $this->y_tunnus = htmlspecialchars(strip_tags($this->y_tunnus));
+        $this->first_name = htmlspecialchars(strip_tags($this->first_name));
+        $this->last_name = htmlspecialchars(strip_tags($this->last_name));
+        $this->email = htmlspecialchars(strip_tags($this->email));
+        $this->phone = htmlspecialchars(strip_tags($this->phone));
+        $this->address = htmlspecialchars(strip_tags($this->address));
+        $this->city = htmlspecialchars(strip_tags($this->city));
+        $this->state = htmlspecialchars(strip_tags($this->state));
+        $this->postal_code = htmlspecialchars(strip_tags($this->postal_code));
+        $this->country = htmlspecialchars(strip_tags($this->country));
+        $this->notes = htmlspecialchars(strip_tags($this->notes));
+        $this->created_at = date('Y-m-d H:i:s');
+        $this->updated_at = date('Y-m-d H:i:s');
+
+        $stmt->bindParam(":company_name", $this->company_name);
+        $stmt->bindParam(":y_tunnus", $this->y_tunnus);
+        $stmt->bindParam(":first_name", $this->first_name);
+        $stmt->bindParam(":last_name", $this->last_name);
+        $stmt->bindParam(":email", $this->email);
+        $stmt->bindParam(":phone", $this->phone);
+        $stmt->bindParam(":address", $this->address);
+        $stmt->bindParam(":city", $this->city);
+        $stmt->bindParam(":state", $this->state);
+        $stmt->bindParam(":postal_code", $this->postal_code);
+        $stmt->bindParam(":country", $this->country);
+        $stmt->bindParam(":notes", $this->notes);
+        $stmt->bindParam(":created_at", $this->created_at);
+        $stmt->bindParam(":updated_at", $this->updated_at);
+
+        if($stmt->execute()) {
+            return true;
+        }
+
+        return false;
+    }
+
+    public function read() {
+        $query = "SELECT * FROM " . $this->table_name . " ORDER BY last_name ASC, first_name ASC";
+
+        $stmt = $this->conn->prepare($query);
+        $stmt->execute();
+
+        return $stmt;
+    }
+
+    public function readOne() {
+        $query = "SELECT * FROM " . $this->table_name . " WHERE id = ? LIMIT 0,1";
+
+        $stmt = $this->conn->prepare($query);
+        $stmt->bindParam(1, $this->id);
+        $stmt->execute();
+
+        $row = $stmt->fetch(PDO::FETCH_ASSOC);
+
+        $this->company_name = $row['company_name'];
+        $this->y_tunnus = $row['y_tunnus'];
+        $this->first_name = $row['first_name'];
+        $this->last_name = $row['last_name'];
+        $this->email = $row['email'];
+        $this->phone = $row['phone'];
+        $this->address = $row['address'];
+        $this->city = $row['city'];
+        $this->state = $row['state'];
+        $this->postal_code = $row['postal_code'];
+        $this->country = $row['country'];
+        $this->notes = $row['notes'];
+        $this->created_at = $row['created_at'];
+        $this->updated_at = $row['updated_at'];
+    }
+
+    public function update() {
+        $query = "UPDATE " . $this->table_name . " SET company_name=:company_name, y_tunnus=:y_tunnus, first_name=:first_name, last_name=:last_name, email=:email, phone=:phone, address=:address, city=:city, state=:state, postal_code=:postal_code, country=:country, notes=:notes, updated_at=:updated_at WHERE id=:id";
+
+        $stmt = $this->conn->prepare($query);
+
+        $this->company_name = htmlspecialchars(strip_tags($this->company_name));
+        $this->y_tunnus = htmlspecialchars(strip_tags($this->y_tunnus));
+        $this->first_name = htmlspecialchars(strip_tags($this->first_name));
+        $this->last_name = htmlspecialchars(strip_tags($this->last_name));
+        $this->email = htmlspecialchars(strip_tags($this->email));
+        $this->phone = htmlspecialchars(strip_tags($this->phone));
+        $this->address = htmlspecialchars(strip_tags($this->address));
+        $this->city = htmlspecialchars(strip_tags($this->city));
+        $this->state = htmlspecialchars(strip_tags($this->state));
+        $this->postal_code = htmlspecialchars(strip_tags($this->postal_code));
+        $this->country = htmlspecialchars(strip_tags($this->country));
+        $this->notes = htmlspecialchars(strip_tags($this->notes));
+        $this->updated_at = date('Y-m-d H:i:s');
+
+        $stmt->bindParam(":company_name", $this->company_name);
+        $stmt->bindParam(":y_tunnus", $this->y_tunnus);
+        $stmt->bindParam(":first_name", $this->first_name);
+        $stmt->bindParam(":last_name", $this->last_name);
+        $stmt->bindParam(":email", $this->email);
+        $stmt->bindParam(":phone", $this->phone);
+        $stmt->bindParam(":address", $this->address);
+        $stmt->bindParam(":city", $this->city);
+        $stmt->bindParam(":state", $this->state);
+        $stmt->bindParam(":postal_code", $this->postal_code);
+        $stmt->bindParam(":country", $this->country);
+        $stmt->bindParam(":notes", $this->notes);
+        $stmt->bindParam(":updated_at", $this->updated_at);
+        $stmt->bindParam(":id", $this->id);
+
+        if($stmt->execute()) {
+            return true;
+        }
+
+        return false;
+    }
+
+    public function delete() {
+        $query = "DELETE FROM " . $this->table_name . " WHERE id = ?";
+
+        $stmt = $this->conn->prepare($query);
+        $stmt->bindParam(1, $this->id);
+
+        if($stmt->execute()) {
+            return true;
+        }
+
+        return false;
+    }
+
+    public function search($search_term) {
+        $query = "SELECT * FROM " . $this->table_name . " WHERE 
+                  first_name LIKE ? OR 
+                  last_name LIKE ? OR 
+                  company_name LIKE ? OR 
+                  email LIKE ? OR 
+                  phone LIKE ? 
+                  ORDER BY last_name ASC, first_name ASC";
+
+        $stmt = $this->conn->prepare($query);
+        $search_term = "%{$search_term}%";
+        $stmt->bindParam(1, $search_term);
+        $stmt->bindParam(2, $search_term);
+        $stmt->bindParam(3, $search_term);
+        $stmt->bindParam(4, $search_term);
+        $stmt->bindParam(5, $search_term);
+        $stmt->execute();
+
+        return $stmt;
+    }
+
+    public function getFullName() {
+        return trim($this->first_name . ' ' . $this->last_name);
+    }
+
+    public function getDisplayName() {
+        if (!empty($this->company_name)) {
+            return $this->company_name . ' (' . $this->getFullName() . ')';
+        }
+        return $this->getFullName();
+    }
+}
+?>

+ 142 - 0
backend/models/ContactPerson.php

@@ -0,0 +1,142 @@
+<?php
+class ContactPerson {
+    private $conn;
+    private $table_name = "contact_persons";
+
+    public $id;
+    public $client_id;
+    public $first_name;
+    public $last_name;
+    public $email;
+    public $phone;
+    public $position;
+    public $is_primary;
+    public $created_at;
+    public $updated_at;
+
+    public function __construct($db) {
+        $this->conn = $db;
+    }
+
+    public function create() {
+        $query = "INSERT INTO " . $this->table_name . " SET client_id=:client_id, first_name=:first_name, last_name=:last_name, email=:email, phone=:phone, position=:position, is_primary=:is_primary, created_at=:created_at, updated_at=:updated_at";
+
+        $stmt = $this->conn->prepare($query);
+
+        $this->client_id = htmlspecialchars(strip_tags($this->client_id));
+        $this->first_name = htmlspecialchars(strip_tags($this->first_name));
+        $this->last_name = htmlspecialchars(strip_tags($this->last_name));
+        $this->email = htmlspecialchars(strip_tags($this->email));
+        $this->phone = htmlspecialchars(strip_tags($this->phone));
+        $this->position = htmlspecialchars(strip_tags($this->position));
+        $this->is_primary = $this->is_primary ? 1 : 0;
+        $this->created_at = date('Y-m-d H:i:s');
+        $this->updated_at = date('Y-m-d H:i:s');
+
+        $stmt->bindParam(":client_id", $this->client_id);
+        $stmt->bindParam(":first_name", $this->first_name);
+        $stmt->bindParam(":last_name", $this->last_name);
+        $stmt->bindParam(":email", $this->email);
+        $stmt->bindParam(":phone", $this->phone);
+        $stmt->bindParam(":position", $this->position);
+        $stmt->bindParam(":is_primary", $this->is_primary);
+        $stmt->bindParam(":created_at", $this->created_at);
+        $stmt->bindParam(":updated_at", $this->updated_at);
+
+        if($stmt->execute()) {
+            return true;
+        }
+
+        return false;
+    }
+
+    public function read() {
+        $query = "SELECT * FROM " . $this->table_name . " WHERE client_id = ? ORDER BY is_primary DESC, last_name ASC, first_name ASC";
+
+        $stmt = $this->conn->prepare($query);
+        $stmt->bindParam(1, $this->client_id);
+        $stmt->execute();
+
+        return $stmt;
+    }
+
+    public function readOne() {
+        $query = "SELECT * FROM " . $this->table_name . " WHERE id = ? LIMIT 0,1";
+
+        $stmt = $this->conn->prepare($query);
+        $stmt->bindParam(1, $this->id);
+        $stmt->execute();
+
+        $row = $stmt->fetch(PDO::FETCH_ASSOC);
+
+        $this->client_id = $row['client_id'];
+        $this->first_name = $row['first_name'];
+        $this->last_name = $row['last_name'];
+        $this->email = $row['email'];
+        $this->phone = $row['phone'];
+        $this->position = $row['position'];
+        $this->is_primary = $row['is_primary'];
+        $this->created_at = $row['created_at'];
+        $this->updated_at = $row['updated_at'];
+    }
+
+    public function update() {
+        $query = "UPDATE " . $this->table_name . " SET client_id=:client_id, first_name=:first_name, last_name=:last_name, email=:email, phone=:phone, position=:position, is_primary=:is_primary, updated_at=:updated_at WHERE id=:id";
+
+        $stmt = $this->conn->prepare($query);
+
+        $this->client_id = htmlspecialchars(strip_tags($this->client_id));
+        $this->first_name = htmlspecialchars(strip_tags($this->first_name));
+        $this->last_name = htmlspecialchars(strip_tags($this->last_name));
+        $this->email = htmlspecialchars(strip_tags($this->email));
+        $this->phone = htmlspecialchars(strip_tags($this->phone));
+        $this->position = htmlspecialchars(strip_tags($this->position));
+        $this->is_primary = $this->is_primary ? 1 : 0;
+        $this->updated_at = date('Y-m-d H:i:s');
+
+        $stmt->bindParam(":client_id", $this->client_id);
+        $stmt->bindParam(":first_name", $this->first_name);
+        $stmt->bindParam(":last_name", $this->last_name);
+        $stmt->bindParam(":email", $this->email);
+        $stmt->bindParam(":phone", $this->phone);
+        $stmt->bindParam(":position", $this->position);
+        $stmt->bindParam(":is_primary", $this->is_primary);
+        $stmt->bindParam(":updated_at", $this->updated_at);
+        $stmt->bindParam(":id", $this->id);
+
+        if($stmt->execute()) {
+            return true;
+        }
+
+        return false;
+    }
+
+    public function delete() {
+        $query = "DELETE FROM " . $this->table_name . " WHERE id = ?";
+
+        $stmt = $this->conn->prepare($query);
+        $stmt->bindParam(1, $this->id);
+
+        if($stmt->execute()) {
+            return true;
+        }
+
+        return false;
+    }
+
+    public function getFullName() {
+        return trim($this->first_name . ' ' . $this->last_name);
+    }
+
+    public function getDisplayName() {
+        $name = $this->getFullName();
+        if (!empty($this->position)) {
+            $name .= ' - ' . $this->position;
+        }
+        if ($this->is_primary) {
+            $name .= ' (Primary)';
+        }
+        return $name;
+    }
+}
+?>

+ 223 - 0
backend/models/Customer.php

@@ -0,0 +1,223 @@
+<?php
+class Customer {
+    private $conn;
+    private $table_name = "clients";
+
+    public $id;
+    public $y_tunnus;
+    public $company_name;
+    public $first_name;
+    public $last_name;
+    public $email;
+    public $phone;
+    public $address;
+    public $city;
+    public $state;
+    public $postal_code;
+    public $country;
+    public $notes;
+    public $hour_price;
+    public $created_at;
+    public $updated_at;
+
+    public function __construct($db) {
+        $this->conn = $db;
+    }
+
+    public function create() {
+        $query = "INSERT INTO " . $this->table_name . " SET y_tunnus=:y_tunnus, company_name=:company_name, first_name=:first_name, last_name=:last_name, email=:email, phone=:phone, address=:address, city=:city, state=:state, postal_code=:postal_code, country=:country, notes=:notes, hour_price=:hour_price, created_at=:created_at, updated_at=:updated_at";
+
+        $stmt = $this->conn->prepare($query);
+
+        $this->y_tunnus = htmlspecialchars(strip_tags($this->y_tunnus));
+        $this->company_name = htmlspecialchars(strip_tags($this->company_name));
+        $this->first_name = htmlspecialchars(strip_tags($this->first_name));
+        $this->last_name = htmlspecialchars(strip_tags($this->last_name));
+        $this->email = htmlspecialchars(strip_tags($this->email));
+        $this->phone = htmlspecialchars(strip_tags($this->phone));
+        $this->address = htmlspecialchars(strip_tags($this->address));
+        $this->city = htmlspecialchars(strip_tags($this->city));
+        $this->state = htmlspecialchars(strip_tags($this->state));
+        $this->postal_code = htmlspecialchars(strip_tags($this->postal_code));
+        $this->country = htmlspecialchars(strip_tags($this->country));
+        $this->notes = htmlspecialchars(strip_tags($this->notes));
+        $this->hour_price = htmlspecialchars(strip_tags($this->hour_price));
+        $this->created_at = date('Y-m-d H:i:s');
+        $this->updated_at = date('Y-m-d H:i:s');
+
+        $stmt->bindParam(":y_tunnus", $this->y_tunnus);
+        $stmt->bindParam(":company_name", $this->company_name);
+        $stmt->bindParam(":first_name", $this->first_name);
+        $stmt->bindParam(":last_name", $this->last_name);
+        $stmt->bindParam(":email", $this->email);
+        $stmt->bindParam(":phone", $this->phone);
+        $stmt->bindParam(":address", $this->address);
+        $stmt->bindParam(":city", $this->city);
+        $stmt->bindParam(":state", $this->state);
+        $stmt->bindParam(":postal_code", $this->postal_code);
+        $stmt->bindParam(":country", $this->country);
+        $stmt->bindParam(":notes", $this->notes);
+        $stmt->bindParam(":hour_price", $this->hour_price);
+        $stmt->bindParam(":created_at", $this->created_at);
+        $stmt->bindParam(":updated_at", $this->updated_at);
+
+        if($stmt->execute()) {
+            return true;
+        }
+
+        return false;
+    }
+
+    public function read() {
+        $query = "SELECT * FROM " . $this->table_name . " ORDER BY company_name, last_name, first_name";
+
+        $stmt = $this->conn->prepare($query);
+        $stmt->execute();
+
+        return $stmt;
+    }
+
+    public function readOne() {
+        $query = "SELECT * FROM " . $this->table_name . " WHERE id = ? LIMIT 0,1";
+
+        $stmt = $this->conn->prepare($query);
+        $stmt->bindParam(1, $this->id);
+        $stmt->execute();
+
+        $row = $stmt->fetch(PDO::FETCH_ASSOC);
+
+        $this->y_tunnus = $row['y_tunnus'];
+        $this->company_name = $row['company_name'];
+        $this->first_name = $row['first_name'];
+        $this->last_name = $row['last_name'];
+        $this->email = $row['email'];
+        $this->phone = $row['phone'];
+        $this->address = $row['address'];
+        $this->city = $row['city'];
+        $this->state = $row['state'];
+        $this->postal_code = $row['postal_code'];
+        $this->country = $row['country'];
+        $this->notes = $row['notes'];
+        $this->hour_price = $row['hour_price'];
+        $this->created_at = $row['created_at'];
+        $this->updated_at = $row['updated_at'];
+    }
+
+    public function update() {
+        $query = "UPDATE " . $this->table_name . " SET y_tunnus=:y_tunnus, company_name=:company_name, first_name=:first_name, last_name=:last_name, email=:email, phone=:phone, address=:address, city=:city, state=:state, postal_code=:postal_code, country=:country, notes=:notes, hour_price=:hour_price, updated_at=:updated_at WHERE id=:id";
+
+        $stmt = $this->conn->prepare($query);
+
+        $this->y_tunnus = htmlspecialchars(strip_tags($this->y_tunnus));
+        $this->company_name = htmlspecialchars(strip_tags($this->company_name));
+        $this->first_name = htmlspecialchars(strip_tags($this->first_name));
+        $this->last_name = htmlspecialchars(strip_tags($this->last_name));
+        $this->email = htmlspecialchars(strip_tags($this->email));
+        $this->phone = htmlspecialchars(strip_tags($this->phone));
+        $this->address = htmlspecialchars(strip_tags($this->address));
+        $this->city = htmlspecialchars(strip_tags($this->city));
+        $this->state = htmlspecialchars(strip_tags($this->state));
+        $this->postal_code = htmlspecialchars(strip_tags($this->postal_code));
+        $this->country = htmlspecialchars(strip_tags($this->country));
+        $this->notes = htmlspecialchars(strip_tags($this->notes));
+        $this->hour_price = htmlspecialchars(strip_tags($this->hour_price));
+        $this->updated_at = date('Y-m-d H:i:s');
+
+        $stmt->bindParam(":y_tunnus", $this->y_tunnus);
+        $stmt->bindParam(":company_name", $this->company_name);
+        $stmt->bindParam(":first_name", $this->first_name);
+        $stmt->bindParam(":last_name", $this->last_name);
+        $stmt->bindParam(":email", $this->email);
+        $stmt->bindParam(":phone", $this->phone);
+        $stmt->bindParam(":address", $this->address);
+        $stmt->bindParam(":city", $this->city);
+        $stmt->bindParam(":state", $this->state);
+        $stmt->bindParam(":postal_code", $this->postal_code);
+        $stmt->bindParam(":country", $this->country);
+        $stmt->bindParam(":notes", $this->notes);
+        $stmt->bindParam(":hour_price", $this->hour_price);
+        $stmt->bindParam(":updated_at", $this->updated_at);
+        $stmt->bindParam(":id", $this->id);
+
+        if($stmt->execute()) {
+            return true;
+        }
+
+        return false;
+    }
+
+    public function delete() {
+        $query = "DELETE FROM " . $this->table_name . " WHERE id = ?";
+
+        $stmt = $this->conn->prepare($query);
+        $stmt->bindParam(1, $this->id);
+
+        if($stmt->execute()) {
+            return true;
+        }
+
+        return false;
+    }
+
+    public function search($search_term) {
+        $query = "SELECT * FROM " . $this->table_name . " WHERE 
+                  y_tunnus LIKE ? OR 
+                  company_name LIKE ? OR 
+                  first_name LIKE ? OR 
+                  last_name LIKE ? OR 
+                  email LIKE ? OR 
+                  phone LIKE ? OR 
+                  address LIKE ? OR 
+                  city LIKE ? OR 
+                  state LIKE ? OR 
+                  postal_code LIKE ? OR 
+                  country LIKE ? OR 
+                  notes LIKE ?
+                  ORDER BY company_name, last_name, first_name";
+
+        $stmt = $this->conn->prepare($query);
+        $search_term = "%{$search_term}%";
+        $stmt->bindParam(1, $search_term);
+        $stmt->bindParam(2, $search_term);
+        $stmt->bindParam(3, $search_term);
+        $stmt->bindParam(4, $search_term);
+        $stmt->bindParam(5, $search_term);
+        $stmt->bindParam(6, $search_term);
+        $stmt->bindParam(7, $search_term);
+        $stmt->bindParam(8, $search_term);
+        $stmt->bindParam(9, $search_term);
+        $stmt->execute();
+
+        return $stmt;
+    }
+
+    public function getProjects($customer_id) {
+        $query = "SELECT * FROM projects WHERE customer_id = ? ORDER BY start_date DESC, created_at DESC";
+
+        $stmt = $this->conn->prepare($query);
+        $stmt->bindParam(1, $customer_id);
+        $stmt->execute();
+
+        return $stmt;
+    }
+
+    public function getDisplayName() {
+        if (!empty($this->company_name)) {
+            return $this->company_name . ' - ' . $this->first_name . ' ' . $this->last_name;
+        } else {
+            return $this->first_name . ' ' . $this->last_name;
+        }
+    }
+
+    public function getPrimaryContact() {
+        $query = "SELECT * FROM contact_persons WHERE client_id = ? AND is_primary = TRUE LIMIT 1";
+
+        $stmt = $this->conn->prepare($query);
+        $stmt->bindParam(1, $this->id);
+        $stmt->execute();
+
+        $row = $stmt->fetch(PDO::FETCH_ASSOC);
+        return $row ? $row['first_name'] . ' ' . $row['last_name'] : 'No primary contact';
+    }
+}
+?>

+ 213 - 0
backend/models/Invoice.php

@@ -0,0 +1,213 @@
+<?php
+class Invoice {
+    private $conn;
+    private $table_name = "invoices";
+
+    public $id;
+    public $client_id;
+    public $invoice_number;
+    public $issue_date;
+    public $due_date;
+    public $status;
+    public $subtotal;
+    public $tax_amount;
+    public $total_amount;
+    public $notes;
+    public $created_at;
+    public $updated_at;
+
+    public function __construct($db) {
+        $this->conn = $db;
+    }
+
+    public function create() {
+        $query = "INSERT INTO " . $this->table_name . " SET client_id=:client_id, invoice_number=:invoice_number, issue_date=:issue_date, due_date=:due_date, status=:status, subtotal=:subtotal, tax_amount=:tax_amount, total_amount=:total_amount, notes=:notes, created_at=:created_at, updated_at=:updated_at";
+
+        $stmt = $this->conn->prepare($query);
+
+        $this->client_id = htmlspecialchars(strip_tags($this->client_id));
+        $this->invoice_number = htmlspecialchars(strip_tags($this->invoice_number));
+        $this->issue_date = htmlspecialchars(strip_tags($this->issue_date));
+        $this->due_date = htmlspecialchars(strip_tags($this->due_date));
+        $this->status = htmlspecialchars(strip_tags($this->status));
+        $this->subtotal = htmlspecialchars(strip_tags($this->subtotal));
+        $this->tax_amount = htmlspecialchars(strip_tags($this->tax_amount));
+        $this->total_amount = htmlspecialchars(strip_tags($this->total_amount));
+        $this->notes = htmlspecialchars(strip_tags($this->notes));
+        $this->created_at = date('Y-m-d H:i:s');
+        $this->updated_at = date('Y-m-d H:i:s');
+
+        $stmt->bindParam(":client_id", $this->client_id);
+        $stmt->bindParam(":invoice_number", $this->invoice_number);
+        $stmt->bindParam(":issue_date", $this->issue_date);
+        $stmt->bindParam(":due_date", $this->due_date);
+        $stmt->bindParam(":status", $this->status);
+        $stmt->bindParam(":subtotal", $this->subtotal);
+        $stmt->bindParam(":tax_amount", $this->tax_amount);
+        $stmt->bindParam(":total_amount", $this->total_amount);
+        $stmt->bindParam(":notes", $this->notes);
+        $stmt->bindParam(":created_at", $this->created_at);
+        $stmt->bindParam(":updated_at", $this->updated_at);
+
+        if($stmt->execute()) {
+            return true;
+        }
+
+        return false;
+    }
+
+    public function read() {
+        $query = "SELECT i.*, c.first_name, c.last_name, c.company_name FROM " . $this->table_name . " i LEFT JOIN clients c ON i.client_id = c.id ORDER BY i.issue_date DESC, i.created_at DESC";
+
+        $stmt = $this->conn->prepare($query);
+        $stmt->execute();
+
+        return $stmt;
+    }
+
+    public function readOne() {
+        $query = "SELECT i.*, c.first_name, c.last_name, c.company_name FROM " . $this->table_name . " i LEFT JOIN clients c ON i.client_id = c.id WHERE i.id = ? LIMIT 0,1";
+
+        $stmt = $this->conn->prepare($query);
+        $stmt->bindParam(1, $this->id);
+        $stmt->execute();
+
+        $row = $stmt->fetch(PDO::FETCH_ASSOC);
+
+        $this->client_id = $row['client_id'];
+        $this->invoice_number = $row['invoice_number'];
+        $this->issue_date = $row['issue_date'];
+        $this->due_date = $row['due_date'];
+        $this->status = $row['status'];
+        $this->subtotal = $row['subtotal'];
+        $this->tax_amount = $row['tax_amount'];
+        $this->total_amount = $row['total_amount'];
+        $this->notes = $row['notes'];
+        $this->created_at = $row['created_at'];
+        $this->updated_at = $row['updated_at'];
+    }
+
+    public function update() {
+        $query = "UPDATE " . $this->table_name . " SET client_id=:client_id, invoice_number=:invoice_number, issue_date=:issue_date, due_date=:due_date, status=:status, subtotal=:subtotal, tax_amount=:tax_amount, total_amount=:total_amount, notes=:notes, updated_at=:updated_at WHERE id=:id";
+
+        $stmt = $this->conn->prepare($query);
+
+        $this->client_id = htmlspecialchars(strip_tags($this->client_id));
+        $this->invoice_number = htmlspecialchars(strip_tags($this->invoice_number));
+        $this->issue_date = htmlspecialchars(strip_tags($this->issue_date));
+        $this->due_date = htmlspecialchars(strip_tags($this->due_date));
+        $this->status = htmlspecialchars(strip_tags($this->status));
+        $this->subtotal = htmlspecialchars(strip_tags($this->subtotal));
+        $this->tax_amount = htmlspecialchars(strip_tags($this->tax_amount));
+        $this->total_amount = htmlspecialchars(strip_tags($this->total_amount));
+        $this->notes = htmlspecialchars(strip_tags($this->notes));
+        $this->updated_at = date('Y-m-d H:i:s');
+
+        $stmt->bindParam(":client_id", $this->client_id);
+        $stmt->bindParam(":invoice_number", $this->invoice_number);
+        $stmt->bindParam(":issue_date", $this->issue_date);
+        $stmt->bindParam(":due_date", $this->due_date);
+        $stmt->bindParam(":status", $this->status);
+        $stmt->bindParam(":subtotal", $this->subtotal);
+        $stmt->bindParam(":tax_amount", $this->tax_amount);
+        $stmt->bindParam(":total_amount", $this->total_amount);
+        $stmt->bindParam(":notes", $this->notes);
+        $stmt->bindParam(":updated_at", $this->updated_at);
+        $stmt->bindParam(":id", $this->id);
+
+        if($stmt->execute()) {
+            return true;
+        }
+
+        return false;
+    }
+
+    public function delete() {
+        $query = "DELETE FROM " . $this->table_name . " WHERE id = ?";
+
+        $stmt = $this->conn->prepare($query);
+        $stmt->bindParam(1, $this->id);
+
+        if($stmt->execute()) {
+            return true;
+        }
+
+        return false;
+    }
+
+    public function search($search_term) {
+        $query = "SELECT i.*, c.first_name, c.last_name, c.company_name FROM " . $this->table_name . " i LEFT JOIN clients c ON i.client_id = c.id WHERE 
+                  i.invoice_number LIKE ? OR 
+                  c.first_name LIKE ? OR 
+                  c.last_name LIKE ? OR 
+                  c.company_name LIKE ? 
+                  i.notes LIKE ?
+                  ORDER BY i.issue_date DESC, i.created_at DESC";
+
+        $stmt = $this->conn->prepare($query);
+        $search_term = "%{$search_term}%";
+        $stmt->bindParam(1, $search_term);
+        $stmt->bindParam(2, $search_term);
+        $stmt->bindParam(3, $search_term);
+        $stmt->bindParam(4, $search_term);
+        $stmt->bindParam(5, $search_term);
+        $stmt->execute();
+
+        return $stmt;
+    }
+
+    public function getInvoiceItems($invoice_id) {
+        $query = "SELECT ii.*, i.name as item_name FROM invoice_items ii LEFT JOIN items i ON ii.item_id = i.id WHERE ii.invoice_id = ? ORDER BY ii.id";
+
+        $stmt = $this->conn->prepare($query);
+        $stmt->bindParam(1, $invoice_id);
+        $stmt->execute();
+
+        return $stmt;
+    }
+
+    public function getPayments($invoice_id) {
+        $query = "SELECT * FROM payments WHERE invoice_id = ? ORDER BY payment_date DESC";
+
+        $stmt = $this->conn->prepare($query);
+        $stmt->bindParam(1, $invoice_id);
+        $stmt->execute();
+
+        return $stmt;
+    }
+
+    public function getDisplayName() {
+        return $this->invoice_number . ' - ' . $this->getClientName();
+    }
+
+    public function getClientName() {
+        $query = "SELECT CONCAT(first_name, ' ', last_name) as client_name FROM clients WHERE id = ?";
+        $stmt = $this->conn->prepare($query);
+        $stmt->bindParam(1, $this->client_id);
+        $stmt->execute();
+        
+        $row = $stmt->fetch(PDO::FETCH_ASSOC);
+        return $row['client_name'];
+    }
+
+    public function getStatusBadge() {
+        $badges = [
+            'draft' => '<span style="background-color: #6c757d; color: white; padding: 2px 6px; border-radius: 4px; font-size: 12px;">Draft</span>',
+            'sent' => '<span style="background-color: #17a2b8; color: white; padding: 2px 6px; border-radius: 4px; font-size: 12px;">Sent</span>',
+            'paid' => '<span style="background-color: #28a745; color: white; padding: 2px 6px; border-radius: 4px; font-size: 12px;">Paid</span>',
+            'overdue' => '<span style="background-color: #dc3545; color: white; padding: 2px 6px; border-radius: 4px; font-size: 12px;">Overdue</span>',
+            'cancelled' => '<span style="background-color: #6c757d; color: white; padding: 2px 6px; border-radius: 4px; font-size: 12px;">Cancelled</span>'
+        ];
+        
+        return $badges[$this->status] ?? $this->status;
+    }
+
+    public function calculateTotalFromItems($items) {
+        $total = 0;
+        foreach ($items as $item) {
+            $total += $item['line_total'];
+        }
+        return $total;
+    }
+}
+?>

+ 121 - 0
backend/models/InvoiceItem.php

@@ -0,0 +1,121 @@
+<?php
+class InvoiceItem {
+    private $conn;
+    private $table_name = "invoice_items";
+
+    public $id;
+    public $invoice_id;
+    public $item_id;
+    public $description;
+    public $quantity;
+    public $unit_price;
+    public $line_total;
+    public $created_at;
+    public $updated_at;
+
+    public function __construct($db) {
+        $this->conn = $db;
+    }
+
+    public function create() {
+        $query = "INSERT INTO " . $this->table_name . " SET invoice_id=:invoice_id, item_id=:item_id, description=:description, quantity=:quantity, unit_price=:unit_price, line_total=:line_total, created_at=:created_at, updated_at=:updated_at";
+
+        $stmt = $this->conn->prepare($query);
+
+        $this->invoice_id = htmlspecialchars(strip_tags($this->invoice_id));
+        $this->item_id = htmlspecialchars(strip_tags($this->item_id));
+        $this->description = htmlspecialchars(strip_tags($this->description));
+        $this->quantity = htmlspecialchars(strip_tags($this->quantity));
+        $this->unit_price = htmlspecialchars(strip_tags($this->unit_price));
+        $this->line_total = htmlspecialchars(strip_tags($this->line_total));
+        $this->created_at = date('Y-m-d H:i:s');
+        $this->updated_at = date('Y-m-d H:i:s');
+
+        $stmt->bindParam(":invoice_id", $this->invoice_id);
+        $stmt->bindParam(":item_id", $this->item_id);
+        $stmt->bindParam(":description", $this->description);
+        $stmt->bindParam(":quantity", $this->quantity);
+        $stmt->bindParam(":unit_price", $this->unit_price);
+        $stmt->bindParam(":line_total", $this->line_total);
+        $stmt->bindParam(":created_at", $this->created_at);
+        $stmt->bindParam(":updated_at", $this->updated_at);
+
+        if($stmt->execute()) {
+            return true;
+        }
+
+        return false;
+    }
+
+    public function read() {
+        $query = "SELECT ii.*, i.name as item_name FROM " . $this->table_name . " ii LEFT JOIN items i ON ii.item_id = i.id WHERE ii.invoice_id = ? ORDER BY ii.id";
+
+        $stmt = $this->conn->prepare($query);
+        $stmt->bindParam(1, $this->invoice_id);
+        $stmt->execute();
+
+        return $stmt;
+    }
+
+    public function readOne() {
+        $query = "SELECT ii.*, i.name as item_name FROM " . $this->table_name . " ii LEFT JOIN items i ON ii.item_id = i.id WHERE ii.id = ? LIMIT 0,1";
+
+        $stmt = $this->conn->prepare($query);
+        $stmt->bindParam(1, $this->id);
+        $stmt->execute();
+
+        $row = $stmt->fetch(PDO::FETCH_ASSOC);
+
+        $this->invoice_id = $row['invoice_id'];
+        $this->item_id = $row['item_id'];
+        $this->description = $row['description'];
+        $this->quantity = $row['quantity'];
+        $this->unit_price = $row['unit_price'];
+        $this->line_total = $row['line_total'];
+        $this->created_at = $row['created_at'];
+        $this->updated_at = $row['updated_at'];
+    }
+
+    public function update() {
+        $query = "UPDATE " . $this->table_name . " SET invoice_id=:invoice_id, item_id=:item_id, description=:description, quantity=:quantity, unit_price=:unit_price, line_total=:line_total, updated_at=:updated_at WHERE id=:id";
+
+        $stmt = $this->conn->prepare($query);
+
+        $this->invoice_id = htmlspecialchars(strip_tags($this->invoice_id));
+        $this->item_id = htmlspecialchars(strip_tags($this->item_id));
+        $this->description = htmlspecialchars(strip_tags($this->description));
+        $this->quantity = htmlspecialchars(strip_tags($this->quantity));
+        $this->unit_price = htmlspecialchars(strip_tags($this->unit_price));
+        $this->line_total = htmlspecialchars(strip_tags($this->line_total));
+        $this->updated_at = date('Y-m-d H:i:s');
+
+        $stmt->bindParam(":invoice_id", $this->invoice_id);
+        $stmt->bindParam(":item_id", $this->item_id);
+        $stmt->bindParam(":description", $this->description);
+        $stmt->bindParam(":quantity", $this->quantity);
+        $stmt->bindParam(":unit_price", $this->unit_price);
+        $stmt->bindParam(":line_total", $this->line_total);
+        $stmt->bindParam(":updated_at", $this->updated_at);
+        $stmt->bindParam(":id", $this->id);
+
+        if($stmt->execute()) {
+            return true;
+        }
+
+        return false;
+    }
+
+    public function delete() {
+        $query = "DELETE FROM " . $this->table_name . " WHERE id = ?";
+
+        $stmt = $this->conn->prepare($query);
+        $stmt->bindParam(1, $this->id);
+
+        if($stmt->execute()) {
+            return true;
+        }
+
+        return false;
+    }
+}
+?>

+ 120 - 0
backend/models/Item.php

@@ -0,0 +1,120 @@
+<?php
+class Item {
+    private $conn;
+    private $table_name = "items";
+
+    public $id;
+    public $name;
+    public $description;
+    public $serial_number;
+    public $picture;
+    public $quantity;
+    public $price;
+    public $created_at;
+    public $updated_at;
+
+    public function __construct($db) {
+        $this->conn = $db;
+    }
+
+    public function create() {
+        $query = "INSERT INTO " . $this->table_name . " SET name=:name, description=:description, serial_number=:serial_number, picture=:picture, quantity=:quantity, price=:price, created_at=:created_at, updated_at=:updated_at";
+
+        $stmt = $this->conn->prepare($query);
+
+        $this->name = htmlspecialchars(strip_tags($this->name));
+        $this->description = htmlspecialchars(strip_tags($this->description));
+        $this->serial_number = htmlspecialchars(strip_tags($this->serial_number));
+        $this->picture = htmlspecialchars(strip_tags($this->picture));
+        $this->quantity = htmlspecialchars(strip_tags($this->quantity));
+        $this->price = htmlspecialchars(strip_tags($this->price));
+        $this->created_at = date('Y-m-d H:i:s');
+        $this->updated_at = date('Y-m-d H:i:s');
+
+        $stmt->bindParam(":name", $this->name);
+        $stmt->bindParam(":description", $this->description);
+        $stmt->bindParam(":serial_number", $this->serial_number);
+        $stmt->bindParam(":picture", $this->picture);
+        $stmt->bindParam(":quantity", $this->quantity);
+        $stmt->bindParam(":price", $this->price);
+        $stmt->bindParam(":created_at", $this->created_at);
+        $stmt->bindParam(":updated_at", $this->updated_at);
+
+        if($stmt->execute()) {
+            return true;
+        }
+
+        return false;
+    }
+
+    public function read() {
+        $query = "SELECT * FROM " . $this->table_name . " ORDER BY created_at DESC";
+
+        $stmt = $this->conn->prepare($query);
+        $stmt->execute();
+
+        return $stmt;
+    }
+
+    public function readOne() {
+        $query = "SELECT * FROM " . $this->table_name . " WHERE id = ? LIMIT 0,1";
+
+        $stmt = $this->conn->prepare($query);
+        $stmt->bindParam(1, $this->id);
+        $stmt->execute();
+
+        $row = $stmt->fetch(PDO::FETCH_ASSOC);
+
+        $this->name = $row['name'];
+        $this->description = $row['description'];
+        $this->serial_number = $row['serial_number'];
+        $this->picture = $row['picture'];
+        $this->quantity = $row['quantity'];
+        $this->price = $row['price'];
+        $this->created_at = $row['created_at'];
+        $this->updated_at = $row['updated_at'];
+    }
+
+    public function update() {
+        $query = "UPDATE " . $this->table_name . " SET name=:name, description=:description, serial_number=:serial_number, picture=:picture, quantity=:quantity, price=:price, updated_at=:updated_at WHERE id=:id";
+
+        $stmt = $this->conn->prepare($query);
+
+        $this->name = htmlspecialchars(strip_tags($this->name));
+        $this->description = htmlspecialchars(strip_tags($this->description));
+        $this->serial_number = htmlspecialchars(strip_tags($this->serial_number));
+        $this->picture = htmlspecialchars(strip_tags($this->picture));
+        $this->quantity = htmlspecialchars(strip_tags($this->quantity));
+        $this->price = htmlspecialchars(strip_tags($this->price));
+        $this->updated_at = date('Y-m-d H:i:s');
+
+        $stmt->bindParam(":name", $this->name);
+        $stmt->bindParam(":description", $this->description);
+        $stmt->bindParam(":serial_number", $this->serial_number);
+        $stmt->bindParam(":picture", $this->picture);
+        $stmt->bindParam(":quantity", $this->quantity);
+        $stmt->bindParam(":price", $this->price);
+        $stmt->bindParam(":updated_at", $this->updated_at);
+        $stmt->bindParam(":id", $this->id);
+
+        if($stmt->execute()) {
+            return true;
+        }
+
+        return false;
+    }
+
+    public function delete() {
+        $query = "DELETE FROM " . $this->table_name . " WHERE id = ?";
+
+        $stmt = $this->conn->prepare($query);
+        $stmt->bindParam(1, $this->id);
+
+        if($stmt->execute()) {
+            return true;
+        }
+
+        return false;
+    }
+}
+?>

+ 159 - 0
backend/models/JournalEntry.php

@@ -0,0 +1,159 @@
+<?php
+class JournalEntry {
+    private $conn;
+    private $table_name = "journal_entries";
+
+    public $id;
+    public $entry_number;
+    public $entry_date;
+    public $description;
+    public $reference_number;
+    public $created_at;
+    public $updated_at;
+
+    public function __construct($db) {
+        $this->conn = $db;
+    }
+
+    public function create() {
+        $query = "INSERT INTO " . $this->table_name . " SET entry_number=:entry_number, entry_date=:entry_date, description=:description, reference_number=:reference_number, created_at=:created_at, updated_at=:updated_at";
+
+        $stmt = $this->conn->prepare($query);
+
+        $this->entry_number = htmlspecialchars(strip_tags($this->entry_number));
+        $this->entry_date = htmlspecialchars(strip_tags($this->entry_date));
+        $this->description = htmlspecialchars(strip_tags($this->description));
+        $this->reference_number = htmlspecialchars(strip_tags($this->reference_number));
+        $this->created_at = date('Y-m-d H:i:s');
+        $this->updated_at = date('Y-m-d H:i:s');
+
+        $stmt->bindParam(":entry_number", $this->entry_number);
+        $stmt->bindParam(":entry_date", $this->entry_date);
+        $stmt->bindParam(":description", $this->description);
+        $stmt->bindParam(":reference_number", $this->reference_number);
+        $stmt->bindParam(":created_at", $this->created_at);
+        $stmt->bindParam(":updated_at", $this->updated_at);
+
+        if($stmt->execute()) {
+            return true;
+        }
+
+        return false;
+    }
+
+    public function read() {
+        $query = "SELECT * FROM " . $this->table_name . " ORDER BY entry_date DESC, created_at DESC";
+
+        $stmt = $this->conn->prepare($query);
+        $stmt->execute();
+
+        return $stmt;
+    }
+
+    public function readOne() {
+        $query = "SELECT * FROM " . $this->table_name . " WHERE id = ? LIMIT 0,1";
+
+        $stmt = $this->conn->prepare($query);
+        $stmt->bindParam(1, $this->id);
+        $stmt->execute();
+
+        $row = $stmt->fetch(PDO::FETCH_ASSOC);
+
+        $this->entry_number = $row['entry_number'];
+        $this->entry_date = $row['entry_date'];
+        $this->description = $row['description'];
+        $this->reference_number = $row['reference_number'];
+        $this->created_at = $row['created_at'];
+        $this->updated_at = $row['updated_at'];
+    }
+
+    public function update() {
+        $query = "UPDATE " . $this->table_name . " SET entry_number=:entry_number, entry_date=:entry_date, description=:description, reference_number=:reference_number, updated_at=:updated_at WHERE id=:id";
+
+        $stmt = $this->conn->prepare($query);
+
+        $this->entry_number = htmlspecialchars(strip_tags($this->entry_number));
+        $this->entry_date = htmlspecialchars(strip_tags($this->entry_date));
+        $this->description = htmlspecialchars(strip_tags($this->description));
+        $this->reference_number = htmlspecialchars(strip_tags($this->reference_number));
+        $this->updated_at = date('Y-m-d H:i:s');
+
+        $stmt->bindParam(":entry_number", $this->entry_number);
+        $stmt->bindParam(":entry_date", $this->entry_date);
+        $stmt->bindParam(":description", $this->description);
+        $stmt->bindParam(":reference_number", $this->reference_number);
+        $stmt->bindParam(":updated_at", $this->updated_at);
+        $stmt->bindParam(":id", $this->id);
+
+        if($stmt->execute()) {
+            return true;
+        }
+
+        return false;
+    }
+
+    public function delete() {
+        $query = "DELETE FROM " . $this->table_name . " WHERE id = ?";
+
+        $stmt = $this->conn->prepare($query);
+        $stmt->bindParam(1, $this->id);
+
+        if($stmt->execute()) {
+            return true;
+        }
+
+        return false;
+    }
+
+    public function search($search_term) {
+        $query = "SELECT * FROM " . $this->table_name . " WHERE 
+                  entry_number LIKE ? OR 
+                  description LIKE ? OR 
+                  reference_number LIKE ?
+                  ORDER BY entry_date DESC, created_at DESC";
+
+        $stmt = $this->conn->prepare($query);
+        $search_term = "%{$search_term}%";
+        $stmt->bindParam(1, $search_term);
+        $stmt->bindParam(2, $search_term);
+        $stmt->bindParam(3, $search_term);
+        $stmt->execute();
+
+        return $stmt;
+    }
+
+    public function getTransactions($entry_id) {
+        $query = "SELECT at.*, coa.account_name, coa.account_type FROM account_transactions at LEFT JOIN chart_of_accounts coa ON at.account_id = coa.id WHERE at.journal_entry_id = ? ORDER BY at.id";
+
+        $stmt = $this->conn->prepare($query);
+        $stmt->bindParam(1, $entry_id);
+        $stmt->execute();
+
+        return $stmt;
+    }
+
+    public function generateEntryNumber() {
+        $query = "SELECT COUNT(*) as count FROM " . $this->table_name . " WHERE YEAR(entry_date) = YEAR(CURRENT_DATE)";
+        $stmt = $this->conn->prepare($query);
+        $stmt->execute();
+        
+        $row = $stmt->fetch(PDO::FETCH_ASSOC);
+        $count = $row['count'] + 1;
+        
+        return date('Y') . '-' . str_pad($count, 4, '0', STR_PAD_LEFT);
+    }
+
+    public function validateEntry() {
+        $query = "SELECT SUM(debit_amount) as total_debit, SUM(credit_amount) as total_credit FROM account_transactions WHERE journal_entry_id = ?";
+        $stmt = $this->conn->prepare($query);
+        $stmt->bindParam(1, $this->id);
+        $stmt->execute();
+        
+        $row = $stmt->fetch(PDO::FETCH_ASSOC);
+        $total_debit = $row['total_debit'];
+        $total_credit = $row['total_credit'];
+        
+        return abs($total_debit - $total_credit) < 0.01; // Allow for small rounding differences
+    }
+}
+?>

+ 161 - 0
backend/models/Payment.php

@@ -0,0 +1,161 @@
+<?php
+class Payment {
+    private $conn;
+    private $table_name = "payments";
+
+    public $id;
+    public $invoice_id;
+    public $client_id;
+    public $payment_date;
+    public $amount;
+    public $payment_method;
+    public $reference_number;
+    public $notes;
+    public $created_at;
+    public $updated_at;
+
+    public function __construct($db) {
+        $this->conn = $db;
+    }
+
+    public function create() {
+        $query = "INSERT INTO " . $this->table_name . " SET invoice_id=:invoice_id, client_id=:client_id, payment_date=:payment_date, amount=:amount, payment_method=:payment_method, reference_number=:reference_number, notes=:notes, created_at=:created_at, updated_at=:updated_at";
+
+        $stmt = $this->conn->prepare($query);
+
+        $this->invoice_id = htmlspecialchars(strip_tags($this->invoice_id));
+        $this->client_id = htmlspecialchars(strip_tags($this->client_id));
+        $this->payment_date = htmlspecialchars(strip_tags($this->payment_date));
+        $this->amount = htmlspecialchars(strip_tags($this->amount));
+        $this->payment_method = htmlspecialchars(strip_tags($this->payment_method));
+        $this->reference_number = htmlspecialchars(strip_tags($this->reference_number));
+        $this->notes = htmlspecialchars(strip_tags($this->notes));
+        $this->created_at = date('Y-m-d H:i:s');
+        $this->updated_at = date('Y-m-d H:i:s');
+
+        $stmt->bindParam(":invoice_id", $this->invoice_id);
+        $stmt->bindParam(":client_id", $this->client_id);
+        $stmt->bindParam(":payment_date", $this->payment_date);
+        $stmt->bindParam(":amount", $this->amount);
+        $stmt->bindParam(":payment_method", $this->payment_method);
+        $stmt->bindParam(":reference_number", $this->reference_number);
+        $stmt->bindParam(":notes", $this->notes);
+        $stmt->bindParam(":created_at", $this->created_at);
+        $stmt->bindParam(":updated_at", $this->updated_at);
+
+        if($stmt->execute()) {
+            return true;
+        }
+
+        return false;
+    }
+
+    public function read() {
+        $query = "SELECT p.*, i.invoice_number, c.first_name, c.last_name, c.company_name FROM " . $this->table_name . " p LEFT JOIN invoices i ON p.invoice_id = i.id LEFT JOIN clients c ON p.client_id = c.id ORDER BY p.payment_date DESC, p.created_at DESC";
+
+        $stmt = $this->conn->prepare($query);
+        $stmt->execute();
+
+        return $stmt;
+    }
+
+    public function readOne() {
+        $query = "SELECT p.*, i.invoice_number, c.first_name, c.last_name, c.company_name FROM " . $this->table_name . " p LEFT JOIN invoices i ON p.invoice_id = i.id LEFT JOIN clients c ON p.client_id = c.id WHERE p.id = ? LIMIT 0,1";
+
+        $stmt = $this->conn->prepare($query);
+        $stmt->bindParam(1, $this->id);
+        $stmt->execute();
+
+        $row = $stmt->fetch(PDO::FETCH_ASSOC);
+
+        $this->invoice_id = $row['invoice_id'];
+        $this->client_id = $row['client_id'];
+        $this->payment_date = $row['payment_date'];
+        $this->amount = $row['amount'];
+        $this->payment_method = $row['payment_method'];
+        $this->reference_number = $row['reference_number'];
+        $this->notes = $row['notes'];
+        $this->created_at = $row['created_at'];
+        $this->updated_at = $row['updated_at'];
+    }
+
+    public function update() {
+        $query = "UPDATE " . $this->table_name . " SET invoice_id=:invoice_id, client_id=:client_id, payment_date=:payment_date, amount=:amount, payment_method=:payment_method, reference_number=:reference_number, notes=:notes, updated_at=:updated_at WHERE id=:id";
+
+        $stmt = $this->conn->prepare($query);
+
+        $this->invoice_id = htmlspecialchars(strip_tags($this->invoice_id));
+        $this->client_id = htmlspecialchars(strip_tags($this->client_id));
+        $this->payment_date = htmlspecialchars(strip_tags($this->payment_date));
+        $this->amount = htmlspecialchars(strip_tags($this->amount));
+        $this->payment_method = htmlspecialchars(strip_tags($this->payment_method));
+        $this->reference_number = htmlspecialchars(strip_tags($this->reference_number));
+        $this->notes = htmlspecialchars(strip_tags($this->notes));
+        $this->updated_at = date('Y-m-d H:i:s');
+
+        $stmt->bindParam(":invoice_id", $this->invoice_id);
+        $stmt->bindParam(":client_id", $this->client_id);
+        $stmt->bindParam(":payment_date", $this->payment_date);
+        $stmt->bindParam(":amount", $this->amount);
+        $stmt->bindParam(":payment_method", $this->payment_method);
+        $stmt->bindParam(":reference_number", $this->reference_number);
+        $stmt->bindParam(":notes", $this->notes);
+        $stmt->bindParam(":updated_at", $this->updated_at);
+        $stmt->bindParam(":id", $this->id);
+
+        if($stmt->execute()) {
+            return true;
+        }
+
+        return false;
+    }
+
+    public function delete() {
+        $query = "DELETE FROM " . $this->table_name . " WHERE id = ?";
+
+        $stmt = $this->conn->prepare($query);
+        $stmt->bindParam(1, $this->id);
+
+        if($stmt->execute()) {
+            return true;
+        }
+
+        return false;
+    }
+
+    public function search($search_term) {
+        $query = "SELECT p.*, i.invoice_number, c.first_name, c.last_name, c.company_name FROM " . $this->table_name . " p LEFT JOIN invoices i ON p.invoice_id = i.id LEFT JOIN clients c ON p.client_id = c.id WHERE 
+                  p.reference_number LIKE ? OR 
+                  p.notes LIKE ? OR 
+                  i.invoice_number LIKE ? OR 
+                  c.first_name LIKE ? OR 
+                  c.last_name LIKE ? OR 
+                  c.company_name LIKE ?
+                  ORDER BY p.payment_date DESC, p.created_at DESC";
+
+        $stmt = $this->conn->prepare($query);
+        $search_term = "%{$search_term}%";
+        $stmt->bindParam(1, $search_term);
+        $stmt->bindParam(2, $search_term);
+        $stmt->bindParam(3, $search_term);
+        $stmt->bindParam(4, $search_term);
+        $stmt->bindParam(5, $search_term);
+        $stmt->bindParam(6, $search_term);
+        $stmt->execute();
+
+        return $stmt;
+    }
+
+    public function getPaymentMethodBadge() {
+        $badges = [
+            'cash' => '<span style="background-color: #28a745; color: white; padding: 2px 6px; border-radius: 4px; font-size: 12px;">Cash</span>',
+            'check' => '<span style="background-color: #17a2b8; color: white; padding: 2px 6px; border-radius: 4px; font-size: 12px;">Check</span>',
+            'credit_card' => '<span style="background-color: #6f42c1; color: white; padding: 2px 6px; border-radius: 4px; font-size: 12px;">Credit Card</span>',
+            'bank_transfer' => '<span style="background-color: #20c997; color: white; padding: 2px 6px; border-radius: 4px; font-size: 12px;">Bank Transfer</span>',
+            'other' => '<span style="background-color: #6c757d; color: white; padding: 2px 6px; border-radius: 4px; font-size: 12px;">Other</span>'
+        ];
+        
+        return $badges[$this->payment_method] ?? $this->payment_method;
+    }
+}
+?>

+ 201 - 0
backend/models/Project.php

@@ -0,0 +1,201 @@
+<?php
+class Project {
+    private $conn;
+    private $table_name = "projects";
+
+    public $id;
+    public $customer_id;
+    public $project_name;
+    public $description;
+    public $status;
+    public $start_date;
+    public $end_date;
+    public $budget;
+    public $created_at;
+    public $updated_at;
+
+    public function __construct($db) {
+        $this->conn = $db;
+    }
+
+    public function create() {
+        $query = "INSERT INTO " . $this->table_name . " SET customer_id=:customer_id, project_name=:project_name, description=:description, status=:status, start_date=:start_date, end_date=:end_date, budget=:budget, created_at=:created_at, updated_at=:updated_at";
+
+        $stmt = $this->conn->prepare($query);
+
+        $this->customer_id = htmlspecialchars(strip_tags($this->customer_id));
+        $this->project_name = htmlspecialchars(strip_tags($this->project_name));
+        $this->description = htmlspecialchars(strip_tags($this->description));
+        $this->status = htmlspecialchars(strip_tags($this->status));
+        $this->start_date = htmlspecialchars(strip_tags($this->start_date));
+        $this->end_date = htmlspecialchars(strip_tags($this->end_date));
+        $this->budget = htmlspecialchars(strip_tags($this->budget));
+        $this->created_at = date('Y-m-d H:i:s');
+        $this->updated_at = date('Y-m-d H:i:s');
+
+        $stmt->bindParam(":customer_id", $this->customer_id);
+        $stmt->bindParam(":project_name", $this->project_name);
+        $stmt->bindParam(":description", $this->description);
+        $stmt->bindParam(":status", $this->status);
+        $stmt->bindParam(":start_date", $this->start_date);
+        $stmt->bindParam(":end_date", $this->end_date);
+        $stmt->bindParam(":budget", $this->budget);
+        $stmt->bindParam(":created_at", $this->created_at);
+        $stmt->bindParam(":updated_at", $this->updated_at);
+
+        if($stmt->execute()) {
+            return true;
+        }
+
+        return false;
+    }
+
+    public function read() {
+        $query = "SELECT p.*, c.first_name, c.last_name, c.company_name FROM " . $this->table_name . " p LEFT JOIN clients c ON p.customer_id = c.id ORDER BY p.start_date DESC, p.created_at DESC";
+
+        $stmt = $this->conn->prepare($query);
+        $stmt->execute();
+
+        return $stmt;
+    }
+
+    public function readOne() {
+        $query = "SELECT p.*, c.first_name, c.last_name, c.company_name FROM " . $this->table_name . " p LEFT JOIN clients c ON p.customer_id = c.id WHERE p.id = ? LIMIT 0,1";
+
+        $stmt = $this->conn->prepare($query);
+        $stmt->bindParam(1, $this->id);
+        $stmt->execute();
+
+        $row = $stmt->fetch(PDO::FETCH_ASSOC);
+
+        $this->customer_id = $row['customer_id'];
+        $this->project_name = $row['project_name'];
+        $this->description = $row['description'];
+        $this->status = $row['status'];
+        $this->start_date = $row['start_date'];
+        $this->end_date = $row['end_date'];
+        $this->budget = $row['budget'];
+        $this->created_at = $row['created_at'];
+        $this->updated_at = $row['updated_at'];
+    }
+
+    public function update() {
+        $query = "UPDATE " . $this->table_name . " SET customer_id=:customer_id, project_name=:project_name, description=:description, status=:status, start_date=:start_date, end_date=:end_date, budget=:budget, updated_at=:updated_at WHERE id=:id";
+
+        $stmt = $this->conn->prepare($query);
+
+        $this->customer_id = htmlspecialchars(strip_tags($this->customer_id));
+        $this->project_name = htmlspecialchars(strip_tags($this->project_name));
+        $this->description = htmlspecialchars(strip_tags($this->description));
+        $this->status = htmlspecialchars(strip_tags($this->status));
+        $this->start_date = htmlspecialchars(strip_tags($this->start_date));
+        $this->end_date = htmlspecialchars(strip_tags($this->end_date));
+        $this->budget = htmlspecialchars(strip_tags($this->budget));
+        $this->updated_at = date('Y-m-d H:i:s');
+
+        $stmt->bindParam(":customer_id", $this->customer_id);
+        $stmt->bindParam(":project_name", $this->project_name);
+        $stmt->bindParam(":description", $this->description);
+        $stmt->bindParam(":status", $this->status);
+        $stmt->bindParam(":start_date", $this->start_date);
+        $stmt->bindParam(":end_date", $this->end_date);
+        $stmt->bindParam(":budget", $this->budget);
+        $stmt->bindParam(":updated_at", $this->updated_at);
+        $stmt->bindParam(":id", $this->id);
+
+        if($stmt->execute()) {
+            return true;
+        }
+
+        return false;
+    }
+
+    public function delete() {
+        $query = "DELETE FROM " . $this->table_name . " WHERE id = ?";
+
+        $stmt = $this->conn->prepare($query);
+        $stmt->bindParam(1, $this->id);
+
+        if($stmt->execute()) {
+            return true;
+        }
+
+        return false;
+    }
+
+    public function search($search_term) {
+        $query = "SELECT p.*, c.first_name, c.last_name, c.company_name FROM " . $this->table_name . " p LEFT JOIN clients c ON p.customer_id = c.id WHERE 
+                  p.project_name LIKE ? OR 
+                  p.description LIKE ? OR 
+                  c.first_name LIKE ? OR 
+                  c.last_name LIKE ? OR 
+                  c.company_name LIKE ?
+                  ORDER BY p.start_date DESC, p.created_at DESC";
+
+        $stmt = $this->conn->prepare($query);
+        $search_term = "%{$search_term}%";
+        $stmt->bindParam(1, $search_term);
+        $stmt->bindParam(2, $search_term);
+        $stmt->bindParam(3, $search_term);
+        $stmt->bindParam(4, $search_term);
+        $stmt->bindParam(5, $search_term);
+        $stmt->execute();
+
+        return $stmt;
+    }
+
+    public function getSubprojects($project_id) {
+        $query = "SELECT * FROM subprojects WHERE project_id = ? ORDER BY created_at DESC";
+
+        $stmt = $this->conn->prepare($query);
+        $stmt->bindParam(1, $project_id);
+        $stmt->execute();
+
+        return $stmt;
+    }
+
+    public function getStatusBadge() {
+        $badges = [
+            'planning' => '<span style="background-color: #6c757d; color: white; padding: 2px 6px; border-radius: 4px; font-size: 12px;">Planning</span>',
+            'in_progress' => '<span style="background-color: #17a2b8; color: white; padding: 2px 6px; border-radius: 4px; font-size: 12px;">In Progress</span>',
+            'completed' => '<span style="background-color: #28a745; color: white; padding: 2px 6px; border-radius: 4px; font-size: 12px;">Completed</span>',
+            'on_hold' => '<span style="background-color: #ffc107; color: black; padding: 2px 6px; border-radius: 4px; font-size: 12px;">On Hold</span>',
+            'cancelled' => '<span style="background-color: #dc3545; color: white; padding: 2px 6px; border-radius: 4px; font-size: 12px;">Cancelled</span>'
+        ];
+        
+        return $badges[$this->status] ?? $this->status;
+    }
+
+    public function getProgress() {
+        if ($this->start_date && $this->end_date) {
+            $start = new DateTime($this->start_date);
+            $end = new DateTime($this->end_date);
+            $now = new DateTime();
+            
+            if ($now < $start) {
+                return 0; // Not started yet
+            } elseif ($now > $end) {
+                return 100; // Completed
+            } else {
+                $total = $end->diff($start)->days;
+                $elapsed = $now->diff($start)->days;
+                return min(100, round(($elapsed / $total) * 100));
+            }
+        }
+        return 0;
+    }
+
+    public function getCustomerName() {
+        $query = "SELECT CONCAT(first_name, ' ', last_name) as customer_name, company_name FROM clients WHERE id = ?";
+        $stmt = $this->conn->prepare($query);
+        $stmt->bindParam(1, $this->customer_id);
+        $stmt->execute();
+        
+        $row = $stmt->fetch(PDO::FETCH_ASSOC);
+        if ($row['company_name']) {
+            return $row['company_name'];
+        }
+        return $row['customer_name'];
+    }
+}
+?>

+ 131 - 0
backend/models/RentalPrice.php

@@ -0,0 +1,131 @@
+<?php
+class RentalPrice {
+    private $conn;
+    private $table_name = "rental_prices";
+
+    public $id;
+    public $item_id;
+    public $client_id;
+    public $start_date;
+    public $end_date;
+    public $daily_price;
+    public $created_at;
+    public $updated_at;
+
+    public function __construct($db) {
+        $this->conn = $db;
+    }
+
+    public function create() {
+        $query = "INSERT INTO " . $this->table_name . " SET item_id=:item_id, client_id=:client_id, start_date=:start_date, end_date=:end_date, daily_price=:daily_price, created_at=:created_at, updated_at=:updated_at";
+
+        $stmt = $this->conn->prepare($query);
+
+        $this->item_id = htmlspecialchars(strip_tags($this->item_id));
+        $this->client_id = htmlspecialchars(strip_tags($this->client_id));
+        $this->start_date = htmlspecialchars(strip_tags($this->start_date));
+        $this->end_date = htmlspecialchars(strip_tags($this->end_date));
+        $this->daily_price = htmlspecialchars(strip_tags($this->daily_price));
+        $this->created_at = date('Y-m-d H:i:s');
+        $this->updated_at = date('Y-m-d H:i:s');
+
+        $stmt->bindParam(":item_id", $this->item_id);
+        $stmt->bindParam(":client_id", $this->client_id);
+        $stmt->bindParam(":start_date", $this->start_date);
+        $stmt->bindParam(":end_date", $this->end_date);
+        $stmt->bindParam(":daily_price", $this->daily_price);
+        $stmt->bindParam(":created_at", $this->created_at);
+        $stmt->bindParam(":updated_at", $this->updated_at);
+
+        if($stmt->execute()) {
+            return true;
+        }
+
+        return false;
+    }
+
+    public function read() {
+        $query = "SELECT * FROM " . $this->table_name . " WHERE item_id = ? ORDER BY start_date ASC";
+
+        $stmt = $this->conn->prepare($query);
+        $stmt->bindParam(1, $this->item_id);
+        $stmt->execute();
+
+        return $stmt;
+    }
+
+    public function readOne() {
+        $query = "SELECT * FROM " . $this->table_name . " WHERE id = ? LIMIT 0,1";
+
+        $stmt = $this->conn->prepare($query);
+        $stmt->bindParam(1, $this->id);
+        $stmt->execute();
+
+        $row = $stmt->fetch(PDO::FETCH_ASSOC);
+
+        $this->item_id = $row['item_id'];
+        $this->client_id = $row['client_id'];
+        $this->start_date = $row['start_date'];
+        $this->end_date = $row['end_date'];
+        $this->daily_price = $row['daily_price'];
+        $this->created_at = $row['created_at'];
+        $this->updated_at = $row['updated_at'];
+    }
+
+    public function update() {
+        $query = "UPDATE " . $this->table_name . " SET item_id=:item_id, client_id=:client_id, start_date=:start_date, end_date=:end_date, daily_price=:daily_price, updated_at=:updated_at WHERE id=:id";
+
+        $stmt = $this->conn->prepare($query);
+
+        $this->item_id = htmlspecialchars(strip_tags($this->item_id));
+        $this->client_id = htmlspecialchars(strip_tags($this->client_id));
+        $this->start_date = htmlspecialchars(strip_tags($this->start_date));
+        $this->end_date = htmlspecialchars(strip_tags($this->end_date));
+        $this->daily_price = htmlspecialchars(strip_tags($this->daily_price));
+        $this->updated_at = date('Y-m-d H:i:s');
+
+        $stmt->bindParam(":item_id", $this->item_id);
+        $stmt->bindParam(":client_id", $this->client_id);
+        $stmt->bindParam(":start_date", $this->start_date);
+        $stmt->bindParam(":end_date", $this->end_date);
+        $stmt->bindParam(":daily_price", $this->daily_price);
+        $stmt->bindParam(":updated_at", $this->updated_at);
+        $stmt->bindParam(":id", $this->id);
+
+        if($stmt->execute()) {
+            return true;
+        }
+
+        return false;
+    }
+
+    public function delete() {
+        $query = "DELETE FROM " . $this->table_name . " WHERE id = ?";
+
+        $stmt = $this->conn->prepare($query);
+        $stmt->bindParam(1, $this->id);
+
+        if($stmt->execute()) {
+            return true;
+        }
+
+        return false;
+    }
+
+    public function getCurrentPrice() {
+        $query = "SELECT daily_price FROM " . $this->table_name . " WHERE item_id = ? AND start_date <= CURDATE() AND end_date >= CURDATE() LIMIT 1";
+
+        $stmt = $this->conn->prepare($query);
+        $stmt->bindParam(1, $this->item_id);
+        $stmt->execute();
+
+        $row = $stmt->fetch(PDO::FETCH_ASSOC);
+
+        if($row) {
+            return $row['daily_price'];
+        }
+
+        return null;
+    }
+}
+?>

+ 172 - 0
backend/models/Subproject.php

@@ -0,0 +1,172 @@
+<?php
+class Subproject {
+    private $conn;
+    private $table_name = "subprojects";
+
+    public $id;
+    public $project_id;
+    public $subproject_name;
+    public $description;
+    public $status;
+    public $start_date;
+    public $end_date;
+    public $budget;
+    public $created_at;
+    public $updated_at;
+
+    public function __construct($db) {
+        $this->conn = $db;
+    }
+
+    public function create() {
+        $query = "INSERT INTO " . $this->table_name . " SET project_id=:project_id, subproject_name=:subproject_name, description=:description, status=:status, start_date=:start_date, end_date=:end_date, budget=:budget, created_at=:created_at, updated_at=:updated_at";
+
+        $stmt = $this->conn->prepare($query);
+
+        $this->project_id = htmlspecialchars(strip_tags($this->project_id));
+        $this->subproject_name = htmlspecialchars(strip_tags($this->subproject_name));
+        $this->description = htmlspecialchars(strip_tags($this->description));
+        $this->status = htmlspecialchars(strip_tags($this->status));
+        $this->start_date = htmlspecialchars(strip_tags($this->start_date));
+        $this->end_date = htmlspecialchars(strip_tags($this->end_date));
+        $this->budget = htmlspecialchars(strip_tags($this->budget));
+        $this->created_at = date('Y-m-d H:i:s');
+        $this->updated_at = date('Y-m-d H:i:s');
+
+        $stmt->bindParam(":project_id", $this->project_id);
+        $stmt->bindParam(":subproject_name", $this->subproject_name);
+        $stmt->bindParam(":description", $this->description);
+        $stmt->bindParam(":status", $this->status);
+        $stmt->bindParam(":start_date", $this->start_date);
+        $stmt->bindParam(":end_date", $this->end_date);
+        $stmt->bindParam(":budget", $this->budget);
+        $stmt->bindParam(":created_at", $this->created_at);
+        $stmt->bindParam(":updated_at", $this->updated_at);
+
+        if($stmt->execute()) {
+            return true;
+        }
+
+        return false;
+    }
+
+    public function read() {
+        $query = "SELECT * FROM " . $this->table_name . " ORDER BY created_at DESC";
+
+        $stmt = $this->conn->prepare($query);
+        $stmt->execute();
+
+        return $stmt;
+    }
+
+    public function readOne() {
+        $query = "SELECT * FROM " . $this->table_name . " WHERE id = ? LIMIT 0,1";
+
+        $stmt = $this->conn->prepare($query);
+        $stmt->bindParam(1, $this->id);
+        $stmt->execute();
+
+        $row = $stmt->fetch(PDO::FETCH_ASSOC);
+
+        $this->project_id = $row['project_id'];
+        $this->subproject_name = $row['subproject_name'];
+        $this->description = $row['description'];
+        $this->status = $row['status'];
+        $this->start_date = $row['start_date'];
+        $this->end_date = $row['end_date'];
+        $this->budget = $row['budget'];
+        $this->created_at = $row['created_at'];
+        $this->updated_at = $row['updated_at'];
+    }
+
+    public function update() {
+        $query = "UPDATE " . $this->table_name . " SET project_id=:project_id, subproject_name=:subproject_name, description=:description, status=:status, start_date=:start_date, end_date=:end_date, budget=:budget, updated_at=:updated_at WHERE id=:id";
+
+        $stmt = $this->conn->prepare($query);
+
+        $this->project_id = htmlspecialchars(strip_tags($this->project_id));
+        $this->subproject_name = htmlspecialchars(strip_tags($this->subproject_name));
+        $this->description = htmlspecialchars(strip_tags($this->description));
+        $this->status = htmlspecialchars(strip_tags($this->status));
+        $this->start_date = htmlspecialchars(strip_tags($this->start_date));
+        $this->end_date = htmlspecialchars(strip_tags($this->end_date));
+        $this->budget = htmlspecialchars(strip_tags($this->budget));
+        $this->updated_at = date('Y-m-d H:i:s');
+
+        $stmt->bindParam(":project_id", $this->project_id);
+        $stmt->bindParam(":subproject_name", $this->subproject_name);
+        $stmt->bindParam(":description", $this->description);
+        $stmt->bindParam(":status", $this->status);
+        $stmt->bindParam(":start_date", $this->start_date);
+        $stmt->bindParam(":end_date", $this->end_date);
+        $stmt->bindParam(":budget", $this->budget);
+        $stmt->bindParam(":updated_at", $this->updated_at);
+        $stmt->bindParam(":id", $this->id);
+
+        if($stmt->execute()) {
+            return true;
+        }
+
+        return false;
+    }
+
+    public function delete() {
+        $query = "DELETE FROM " . $this->table_name . " WHERE id = ?";
+
+        $stmt = $this->conn->prepare($query);
+        $stmt->bindParam(1, $this->id);
+
+        if($stmt->execute()) {
+            return true;
+        }
+
+        return false;
+    }
+
+    public function search($search_term) {
+        $query = "SELECT * FROM " . $this->table_name . " WHERE 
+                  subproject_name LIKE ? OR 
+                  description LIKE ?
+                  ORDER BY created_at DESC";
+
+        $stmt = $this->conn->prepare($query);
+        $search_term = "%{$search_term}%";
+        $stmt->bindParam(1, $search_term);
+        $stmt->bindParam(2, $search_term);
+        $stmt->execute();
+
+        return $stmt;
+    }
+
+    public function getStatusBadge() {
+        $badges = [
+            'planning' => '<span style="background-color: #6c757d; color: white; padding: 2px 6px; border-radius: 4px; font-size: 12px;">Planning</span>',
+            'in_progress' => '<span style="background-color: #17a2b8; color: white; padding: 2px 6px; border-radius: 4px; font-size: 12px;">In Progress</span>',
+            'completed' => '<span style="background-color: #28a745; color: white; padding: 2px 6px; border-radius: 4px; font-size: 12px;">Completed</span>',
+            'on_hold' => '<span style="background-color: #ffc107; color: black; padding: 2px 6px; border-radius: 4px; font-size: 12px;">On Hold</span>',
+            'cancelled' => '<span style="background-color: #dc3545; color: white; padding: 2px 6px; border-radius: 4px; font-size: 12px;">Cancelled</span>'
+        ];
+        
+        return $badges[$this->status] ?? $this->status;
+    }
+
+    public function getProgress() {
+        if ($this->start_date && $this->end_date) {
+            $start = new DateTime($this->start_date);
+            $end = new DateTime($this->end_date);
+            $now = new DateTime();
+            
+            if ($now < $start) {
+                return 0; // Not started yet
+            } elseif ($now > $end) {
+                return 100; // Completed
+            } else {
+                $total = $end->diff($start)->days;
+                $elapsed = $now->diff($start)->days;
+                return min(100, round(($elapsed / $total) * 100));
+            }
+        }
+        return 0;
+    }
+}
+?>

+ 193 - 0
backend/models/User.php

@@ -0,0 +1,193 @@
+<?php
+class User {
+    private $conn;
+    private $table_name = "users";
+
+    public $id;
+    public $username;
+    public $email;
+    public $password_hash;
+    public $first_name;
+    public $last_name;
+    public $role;
+    public $is_active;
+    public $last_login;
+    public $created_at;
+    public $updated_at;
+
+    public function __construct($db) {
+        $this->conn = $db;
+    }
+
+    public function create() {
+        $query = "INSERT INTO " . $this->table_name . " SET username=:username, email=:email, password_hash=:password_hash, first_name=:first_name, last_name=:last_name, role=:role, is_active=:is_active, created_at=:created_at, updated_at=:updated_at";
+
+        $stmt = $this->conn->prepare($query);
+
+        $this->username = htmlspecialchars(strip_tags($this->username));
+        $this->email = htmlspecialchars(strip_tags($this->email));
+        $this->password_hash = password_hash($this->password_hash, PASSWORD_DEFAULT);
+        $this->first_name = htmlspecialchars(strip_tags($this->first_name));
+        $this->last_name = htmlspecialchars(strip_tags($this->last_name));
+        $this->role = htmlspecialchars(strip_tags($this->role));
+        $this->is_active = $this->is_active ? 1 : 0;
+        $this->created_at = date('Y-m-d H:i:s');
+        $this->updated_at = date('Y-m-d H:i:s');
+
+        $stmt->bindParam(":username", $this->username);
+        $stmt->bindParam(":email", $this->email);
+        $stmt->bindParam(":password_hash", $this->password_hash);
+        $stmt->bindParam(":first_name", $this->first_name);
+        $stmt->bindParam(":last_name", $this->last_name);
+        $stmt->bindParam(":role", $this->role);
+        $stmt->bindParam(":is_active", $this->is_active);
+        $stmt->bindParam(":created_at", $this->created_at);
+        $stmt->bindParam(":updated_at", $this->updated_at);
+
+        if($stmt->execute()) {
+            return true;
+        }
+
+        return false;
+    }
+
+    public function read() {
+        $query = "SELECT id, username, email, first_name, last_name, role, is_active, last_login, created_at, updated_at FROM " . $this->table_name . " ORDER BY username";
+
+        $stmt = $this->conn->prepare($query);
+        $stmt->execute();
+
+        return $stmt;
+    }
+
+    public function readOne() {
+        $query = "SELECT * FROM " . $this->table_name . " WHERE id = ? LIMIT 0,1";
+
+        $stmt = $this->conn->prepare($query);
+        $stmt->bindParam(1, $this->id);
+        $stmt->execute();
+
+        $row = $stmt->fetch(PDO::FETCH_ASSOC);
+
+        $this->username = $row['username'];
+        $this->email = $row['email'];
+        $this->password_hash = $row['password_hash'];
+        $this->first_name = $row['first_name'];
+        $this->last_name = $row['last_name'];
+        $this->role = $row['role'];
+        $this->is_active = $row['is_active'];
+        $this->last_login = $row['last_login'];
+        $this->created_at = $row['created_at'];
+        $this->updated_at = $row['updated_at'];
+    }
+
+    public function update() {
+        $query = "UPDATE " . $this->table_name . " SET username=:username, email=:email, first_name=:first_name, last_name=:last_name, role=:role, is_active=:is_active, updated_at=:updated_at WHERE id=:id";
+
+        $stmt = $this->conn->prepare($query);
+
+        $this->username = htmlspecialchars(strip_tags($this->username));
+        $this->email = htmlspecialchars(strip_tags($this->email));
+        $this->first_name = htmlspecialchars(strip_tags($this->first_name));
+        $this->last_name = htmlspecialchars(strip_tags($this->last_name));
+        $this->role = htmlspecialchars(strip_tags($this->role));
+        $this->is_active = $this->is_active ? 1 : 0;
+        $this->updated_at = date('Y-m-d H:i:s');
+
+        $stmt->bindParam(":username", $this->username);
+        $stmt->bindParam(":email", $this->email);
+        $stmt->bindParam(":first_name", $this->first_name);
+        $stmt->bindParam(":last_name", $this->last_name);
+        $stmt->bindParam(":role", $this->role);
+        $stmt->bindParam(":is_active", $this->is_active);
+        $stmt->bindParam(":updated_at", $this->updated_at);
+        $stmt->bindParam(":id", $this->id);
+
+        if($stmt->execute()) {
+            return true;
+        }
+
+        return false;
+    }
+
+    public function delete() {
+        $query = "DELETE FROM " . $this->table_name . " WHERE id = ?";
+
+        $stmt = $this->conn->prepare($query);
+        $stmt->bindParam(1, $this->id);
+
+        if($stmt->execute()) {
+            return true;
+        }
+
+        return false;
+    }
+
+    public function authenticate($username, $password) {
+        $query = "SELECT * FROM " . $this->table_name . " WHERE username = ? AND is_active = TRUE LIMIT 0,1";
+
+        $stmt = $this->conn->prepare($query);
+        $stmt->bindParam(1, $username);
+        $stmt->execute();
+
+        $row = $stmt->fetch(PDO::FETCH_ASSOC);
+
+        if($row && password_verify($password, $row['password_hash'])) {
+            // Update last login
+            $update_query = "UPDATE " . $this->table_name . " SET last_login = ? WHERE id = ?";
+            $update_stmt = $this->conn->prepare($update_query);
+            $update_stmt->bindParam(1, date('Y-m-d H:i:s'));
+            $update_stmt->bindParam(2, $row['id']);
+            $update_stmt->execute();
+
+            // Remove password hash from response
+            unset($row['password_hash']);
+            return $row;
+        }
+
+        return false;
+    }
+
+    public function findByEmail($email) {
+        $query = "SELECT * FROM " . $this->table_name . " WHERE email = ? LIMIT 0,1";
+
+        $stmt = $this->conn->prepare($query);
+        $stmt->bindParam(1, $email);
+        $stmt->execute();
+
+        return $stmt->fetch(PDO::FETCH_ASSOC);
+    }
+
+    public function updatePassword($user_id, $new_password) {
+        $query = "UPDATE " . $this->table_name . " SET password_hash = ?, updated_at = ? WHERE id = ?";
+        $stmt = $this->conn->prepare($query);
+        
+        $password_hash = password_hash($new_password, PASSWORD_DEFAULT);
+        $updated_at = date('Y-m-d H:i:s');
+        
+        $stmt->bindParam(1, $password_hash);
+        $stmt->bindParam(2, $updated_at);
+        $stmt->bindParam(3, $user_id);
+
+        return $stmt->execute();
+    }
+
+    public function getRoleBadge() {
+        $badges = [
+            'admin' => '<span style="background-color: #dc3545; color: white; padding: 2px 6px; border-radius: 4px; font-size: 12px;">Admin</span>',
+            'manager' => '<span style="background-color: #6f42c1; color: white; padding: 2px 6px; border-radius: 4px; font-size: 12px;">Manager</span>',
+            'user' => '<span style="background-color: #6c757d; color: white; padding: 2px 6px; border-radius: 4px; font-size: 12px;">User</span>'
+        ];
+        
+        return $badges[$this->role] ?? $this->role;
+    }
+
+    public function getFullName() {
+        return trim($this->first_name . ' ' . $this->last_name);
+    }
+
+    public function isActive() {
+        return $this->is_active;
+    }
+}
+?>

+ 236 - 0
backend/setup_database.sql

@@ -0,0 +1,236 @@
+CREATE DATABASE IF NOT EXISTS inventory_db;
+
+USE inventory_db;
+
+CREATE TABLE IF NOT EXISTS items (
+    id INT(11) AUTO_INCREMENT PRIMARY KEY,
+    name VARCHAR(255) NOT NULL,
+    description TEXT,
+    serial_number VARCHAR(100) NULL,
+    picture VARCHAR(255) NULL,
+    quantity INT(11) NOT NULL DEFAULT 0,
+    price DECIMAL(10,2) NOT NULL DEFAULT 0.00,
+    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
+);
+
+CREATE TABLE IF NOT EXISTS rental_prices (
+    id INT(11) AUTO_INCREMENT PRIMARY KEY,
+    item_id INT(11) NOT NULL,
+    client_id INT(11) NULL,
+    start_date DATE NOT NULL,
+    end_date DATE NOT NULL,
+    daily_price DECIMAL(10,2) NOT NULL DEFAULT 0.00,
+    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+    FOREIGN KEY (item_id) REFERENCES items(id) ON DELETE CASCADE,
+    FOREIGN KEY (client_id) REFERENCES clients(id) ON DELETE SET NULL,
+    INDEX idx_item_dates (item_id, start_date, end_date)
+);
+
+CREATE TABLE IF NOT EXISTS attachments (
+    id INT(11) AUTO_INCREMENT PRIMARY KEY,
+    item_id INT(11) NOT NULL,
+    filename VARCHAR(255) NOT NULL,
+    original_name VARCHAR(255) NOT NULL,
+    file_type ENUM('receipt', 'warranty', 'other') NOT NULL DEFAULT 'other',
+    file_path VARCHAR(255) NOT NULL,
+    file_size INT(11) NOT NULL,
+    mime_type VARCHAR(100) NOT NULL,
+    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+    FOREIGN KEY (item_id) REFERENCES items(id) ON DELETE CASCADE,
+    INDEX idx_item_type (item_id, file_type)
+);
+
+CREATE TABLE IF NOT EXISTS clients (
+    id INT(11) AUTO_INCREMENT PRIMARY KEY,
+    company_name VARCHAR(255) NULL,
+    y_tunnus VARCHAR(255) NULL,
+    first_name VARCHAR(100) NOT NULL,
+    last_name VARCHAR(100) NOT NULL,
+    email VARCHAR(255) NOT NULL UNIQUE,
+    phone VARCHAR(20) NULL,
+    address VARCHAR(255) NULL,
+    city VARCHAR(100) NULL,
+    state VARCHAR(100) NULL,
+    postal_code VARCHAR(20) NULL,
+    country VARCHAR(100) NULL,
+    notes TEXT NULL,
+    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+    INDEX idx_email (email),
+    INDEX idx_name (last_name, first_name),
+    INDEX idx_y_tunnus (y_tunnus)
+);
+
+CREATE TABLE IF NOT EXISTS contact_persons (
+    id INT(11) AUTO_INCREMENT PRIMARY KEY,
+    client_id INT(11) NOT NULL,
+    first_name VARCHAR(100) NOT NULL,
+    last_name VARCHAR(100) NOT NULL,
+    email VARCHAR(255) NULL,
+    phone VARCHAR(20) NULL,
+    position VARCHAR(100) NULL,
+    is_primary BOOLEAN DEFAULT FALSE,
+    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+    FOREIGN KEY (client_id) REFERENCES clients(id) ON DELETE CASCADE,
+    INDEX idx_client_primary (client_id, is_primary),
+    INDEX idx_name (last_name, first_name)
+);
+
+CREATE TABLE IF NOT EXISTS invoices (
+    id INT(11) AUTO_INCREMENT PRIMARY KEY,
+    client_id INT(11) NOT NULL,
+    invoice_number VARCHAR(50) NOT NULL UNIQUE,
+    issue_date DATE NOT NULL,
+    due_date DATE NOT NULL,
+    status ENUM('draft', 'sent', 'paid', 'overdue', 'cancelled') DEFAULT 'draft',
+    subtotal DECIMAL(10,2) NOT NULL DEFAULT 0.00,
+    tax_amount DECIMAL(10,2) NOT NULL DEFAULT 0.00,
+    total_amount DECIMAL(10,2) NOT NULL DEFAULT 0.00,
+    notes TEXT NULL,
+    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+    FOREIGN KEY (client_id) REFERENCES clients(id) ON DELETE RESTRICT,
+    INDEX idx_client_status (client_id, status),
+    INDEX idx_invoice_number (invoice_number),
+    INDEX idx_due_date (due_date)
+);
+
+CREATE TABLE IF NOT EXISTS invoice_items (
+    id INT(11) AUTO_INCREMENT PRIMARY KEY,
+    invoice_id INT(11) NOT NULL,
+    item_id INT(11) NOT NULL,
+    description VARCHAR(255) NOT NULL,
+    quantity DECIMAL(10,2) NOT NULL DEFAULT 1.00,
+    unit_price DECIMAL(10,2) NOT NULL DEFAULT 0.00,
+    line_total DECIMAL(10,2) NOT NULL DEFAULT 0.00,
+    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+    FOREIGN KEY (invoice_id) REFERENCES invoices(id) ON DELETE CASCADE,
+    FOREIGN KEY (item_id) REFERENCES items(id) ON DELETE RESTRICT,
+    INDEX idx_invoice (invoice_id),
+    INDEX idx_item (item_id)
+);
+
+CREATE TABLE IF NOT EXISTS payments (
+    id INT(11) AUTO_INCREMENT PRIMARY KEY,
+    invoice_id INT(11) NULL,
+    client_id INT(11) NOT NULL,
+    payment_date DATE NOT NULL,
+    amount DECIMAL(10,2) NOT NULL DEFAULT 0.00,
+    payment_method ENUM('cash', 'check', 'credit_card', 'bank_transfer', 'other') DEFAULT 'cash',
+    reference_number VARCHAR(50) NULL,
+    notes TEXT NULL,
+    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+    FOREIGN KEY (invoice_id) REFERENCES invoices(id) ON DELETE SET NULL,
+    FOREIGN KEY (client_id) REFERENCES clients(id) ON DELETE RESTRICT,
+    INDEX idx_invoice (invoice_id),
+    INDEX idx_client (client_id),
+    INDEX idx_payment_date (payment_date)
+);
+
+CREATE TABLE IF NOT EXISTS chart_of_accounts (
+    id INT(11) AUTO_INCREMENT PRIMARY KEY,
+    account_number VARCHAR(20) NOT NULL UNIQUE,
+    account_name VARCHAR(255) NOT NULL,
+    account_type ENUM('asset', 'liability', 'equity', 'revenue', 'expense') NOT NULL,
+    parent_id INT(11) NULL,
+    description TEXT NULL,
+    opening_balance DECIMAL(10,2) DEFAULT 0.00,
+    current_balance DECIMAL(10,2) DEFAULT 0.00,
+    is_active BOOLEAN DEFAULT TRUE,
+    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+    FOREIGN KEY (parent_id) REFERENCES chart_of_accounts(id) ON DELETE SET NULL,
+    INDEX idx_account_type (account_type),
+    INDEX idx_parent (parent_id),
+    INDEX idx_account_number (account_number)
+);
+
+CREATE TABLE IF NOT EXISTS journal_entries (
+    id INT(11) AUTO_INCREMENT PRIMARY KEY,
+    entry_number VARCHAR(50) NOT NULL UNIQUE,
+    entry_date DATE NOT NULL,
+    description TEXT NULL,
+    reference_number VARCHAR(50) NULL,
+    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+    INDEX idx_entry_date (entry_date),
+    INDEX idx_entry_number (entry_number)
+);
+
+CREATE TABLE IF NOT EXISTS account_transactions (
+    id INT(11) AUTO_INCREMENT PRIMARY KEY,
+    journal_entry_id INT(11) NOT NULL,
+    account_id INT(11) NOT NULL,
+    debit_amount DECIMAL(10,2) DEFAULT 0.00,
+    credit_amount DECIMAL(10,2) DEFAULT 0.00,
+    description TEXT NULL,
+    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+    FOREIGN KEY (journal_entry_id) REFERENCES journal_entries(id) ON DELETE CASCADE,
+    FOREIGN KEY (account_id) REFERENCES chart_of_accounts(id) ON DELETE RESTRICT,
+    INDEX idx_journal_entry (journal_entry_id),
+    INDEX idx_account (account_id),
+    INDEX idx_debit_credit (debit_amount, credit_amount)
+);
+
+CREATE TABLE IF NOT EXISTS users (
+    id INT(11) AUTO_INCREMENT PRIMARY KEY,
+    username VARCHAR(50) NOT NULL UNIQUE,
+    email VARCHAR(255) NOT NULL UNIQUE,
+    password_hash VARCHAR(255) NOT NULL,
+    first_name VARCHAR(100) NOT NULL,
+    last_name VARCHAR(100) NOT NULL,
+    role ENUM('admin', 'manager', 'user') DEFAULT 'user',
+    is_active BOOLEAN DEFAULT TRUE,
+    last_login TIMESTAMP NULL,
+    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+    INDEX idx_username (username),
+    INDEX idx_email (email),
+    INDEX idx_role (role),
+    INDEX idx_active (is_active)
+);
+
+CREATE TABLE IF NOT EXISTS projects (
+    id INT(11) AUTO_INCREMENT PRIMARY KEY,
+    customer_id INT(11) NOT NULL,
+    project_name VARCHAR(255) NOT NULL,
+    description TEXT NULL,
+    status ENUM('planning', 'in_progress', 'completed', 'on_hold', 'cancelled') DEFAULT 'planning',
+    start_date DATE NULL,
+    end_date DATE NULL,
+    budget DECIMAL(10,2) NULL,
+    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+    FOREIGN KEY (customer_id) REFERENCES clients(id) ON DELETE CASCADE,
+    INDEX idx_customer (customer_id),
+    INDEX idx_status (status),
+    INDEX idx_dates (start_date, end_date)
+);
+
+CREATE TABLE IF NOT EXISTS subprojects (
+    id INT(11) AUTO_INCREMENT PRIMARY KEY,
+    project_id INT(11) NOT NULL,
+    subproject_name VARCHAR(255) NOT NULL,
+    description TEXT NULL,
+    status ENUM('planning', 'in_progress', 'completed', 'on_hold', 'cancelled') DEFAULT 'planning',
+    start_date DATE NULL,
+    end_date DATE NULL,
+    budget DECIMAL(10,2) NULL,
+    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+    FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE,
+    INDEX idx_project (project_id),
+    INDEX idx_status (status),
+    INDEX idx_dates (start_date, end_date)
+);
+
+INSERT INTO items (name, description, quantity, price) VALUES 
+('Laptop', 'Dell XPS 15 laptop', 5, 1299.99),
+('Mouse', 'Wireless optical mouse', 20, 25.50),
+('Keyboard', 'Mechanical keyboard', 15, 89.99);

+ 201 - 0
frontend/index.html

@@ -0,0 +1,201 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <title>Inventory Management</title>
+    <style>
+        * {
+            margin: 0;
+            padding: 0;
+            box-sizing: border-box;
+        }
+        
+        body {
+            font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
+            background-color: #f5f5f5;
+            color: #333;
+        }
+        
+        .container {
+            max-width: 1200px;
+            margin: 0 auto;
+            padding: 20px;
+        }
+        
+        header {
+            background-color: #2c3e50;
+            color: white;
+            padding: 1rem 0;
+            margin-bottom: 2rem;
+            box-shadow: 0 2px 4px rgba(0,0,0,0.1);
+        }
+        
+        h1 {
+            text-align: center;
+            font-size: 2.5rem;
+            font-weight: 300;
+        }
+        
+        .btn {
+            padding: 8px 16px;
+            border: none;
+            border-radius: 4px;
+            cursor: pointer;
+            font-size: 14px;
+            transition: background-color 0.3s;
+        }
+        
+        .btn-primary {
+            background-color: #3498db;
+            color: white;
+        }
+        
+        .btn-primary:hover {
+            background-color: #2980b9;
+        }
+        
+        .btn-success {
+            background-color: #27ae60;
+            color: white;
+        }
+        
+        .btn-success:hover {
+            background-color: #229954;
+        }
+        
+        .btn-danger {
+            background-color: #e74c3c;
+            color: white;
+        }
+        
+        .btn-danger:hover {
+            background-color: #c0392b;
+        }
+        
+        .btn-warning {
+            background-color: #f39c12;
+            color: white;
+        }
+        
+        .btn-warning:hover {
+            background-color: #e67e22;
+        }
+        
+        .form-group {
+            margin-bottom: 1rem;
+        }
+        
+        label {
+            display: block;
+            margin-bottom: 5px;
+            font-weight: 500;
+        }
+        
+        input, textarea, select {
+            width: 100%;
+            padding: 8px 12px;
+            border: 1px solid #ddd;
+            border-radius: 4px;
+            font-size: 14px;
+        }
+        
+        textarea {
+            resize: vertical;
+            min-height: 80px;
+        }
+        
+        .card {
+            background: white;
+            border-radius: 8px;
+            padding: 20px;
+            box-shadow: 0 2px 4px rgba(0,0,0,0.1);
+            margin-bottom: 20px;
+        }
+        
+        .table {
+            width: 100%;
+            border-collapse: collapse;
+            margin-top: 20px;
+        }
+        
+        .table th,
+        .table td {
+            padding: 12px;
+            text-align: left;
+            border-bottom: 1px solid #ddd;
+        }
+        
+        .table th {
+            background-color: #f8f9fa;
+            font-weight: 600;
+        }
+        
+        .table tr:hover {
+            background-color: #f8f9fa;
+        }
+        
+        .actions {
+            display: flex;
+            gap: 8px;
+        }
+        
+        .modal {
+            display: none;
+            position: fixed;
+            z-index: 1000;
+            left: 0;
+            top: 0;
+            width: 100%;
+            height: 100%;
+            background-color: rgba(0,0,0,0.5);
+        }
+        
+        .modal-content {
+            background-color: white;
+            margin: 10% auto;
+            padding: 20px;
+            border-radius: 8px;
+            width: 90%;
+            max-width: 500px;
+        }
+        
+        .close {
+            color: #aaa;
+            float: right;
+            font-size: 28px;
+            font-weight: bold;
+            cursor: pointer;
+        }
+        
+        .close:hover {
+            color: black;
+        }
+        
+        .loading {
+            text-align: center;
+            padding: 20px;
+        }
+        
+        .error {
+            color: #e74c3c;
+            background-color: #fdf2f2;
+            padding: 10px;
+            border-radius: 4px;
+            margin-bottom: 10px;
+        }
+        
+        .success {
+            color: #27ae60;
+            background-color: #f2f9f4;
+            padding: 10px;
+            border-radius: 4px;
+            margin-bottom: 10px;
+        }
+    </style>
+</head>
+<body>
+    <div id="app"></div>
+    <script type="module" src="/src/main.js"></script>
+</body>
+</html>

+ 18 - 0
frontend/package.json

@@ -0,0 +1,18 @@
+{
+  "name": "inventory-frontend",
+  "version": "1.0.0",
+  "private": true,
+  "scripts": {
+    "dev": "vite",
+    "build": "vite build",
+    "preview": "vite preview"
+  },
+  "dependencies": {
+    "vue": "^3.3.4",
+    "axios": "^1.4.0"
+  },
+  "devDependencies": {
+    "@vitejs/plugin-vue": "^4.2.3",
+    "vite": "^4.4.5"
+  }
+}

+ 2064 - 0
frontend/src/App.vue

@@ -0,0 +1,2064 @@
+<template>
+  <div>
+    <header>
+      <div class="container">
+        <!-- Login Section -->
+        <div v-if="!isLoggedIn" class="login-container">
+          <div class="login-card">
+            <h2>Login</h2>
+            <form @submit.prevent="login">
+              <div class="form-group">
+                <label for="login_username">Username</label>
+                <input 
+                  type="text" 
+                  id="login_username" 
+                  v-model="loginForm.username" 
+                  required 
+                  placeholder="Enter username"
+                >
+              </div>
+              
+              <div class="form-group">
+                <label for="login_password">Password</label>
+                <input 
+                  type="password" 
+                  id="login_password" 
+                  v-model="loginForm.password" 
+                  required 
+                  placeholder="Enter password"
+                >
+              </div>
+              
+              <button type="submit" class="btn btn-primary btn-block">Login</button>
+            </form>
+            
+            <div v-if="loginError" class="error-message">
+              {{ loginError }}
+            </div>
+      
+      <!-- Navigation Tabs -->
+      <div class="nav-tabs">
+        <button 
+          :class="['nav-tab', { active: activeSection === 'items' }]" 
+          @click="activeSection = 'items'"
+        >
+          Items
+        </button>
+        <button 
+          :class="['nav-tab', { active: activeSection === 'clients' }]" 
+          @click="activeSection = 'clients'"
+        >
+          Clients
+        </button>
+        <button 
+          :class="['nav-tab', { active: activeSection === 'accounting' }]" 
+          @click="activeSection = 'accounting'"
+        >
+          Accounting
+        </button>
+      </div>
+      
+      <!-- Items Section -->
+      <div v-if="activeSection === 'items'" class="card">
+        <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px;">
+          <h2>Items</h2>
+          <button class="btn btn-primary" @click="showAddModal">Add New Item</button>
+        </div>
+        
+        <div v-if="loading" class="loading">
+          Loading items...
+        </div>
+        
+        <div v-else-if="items.length === 0" class="loading">
+          No items found. Add your first item!
+        </div>
+        
+        <table v-else class="table">
+          <thead>
+            <tr>
+              <th>ID</th>
+              <th>Name</th>
+              <th>Description</th>
+              <th>Serial Number</th>
+              <th>Picture</th>
+              <th>Quantity</th>
+              <th>Price</th>
+              <th>Created</th>
+              <th>Actions</th>
+            </tr>
+          </thead>
+          <tbody>
+            <tr v-for="item in items" :key="item.id">
+              <td>{{ item.id }}</td>
+              <td>{{ item.name }}</td>
+              <td>{{ item.description || '-' }}</td>
+              <td>{{ item.serial_number || '-' }}</td>
+              <td>
+                <img v-if="item.picture" :src="item.picture" :alt="item.name" style="width: 50px; height: 50px; object-fit: cover; border-radius: 4px;">
+                <span v-else>-</span>
+              </td>
+              <td>{{ item.quantity }}</td>
+              <td>${{ parseFloat(item.price).toFixed(2) }}</td>
+              <td>{{ formatDate(item.created_at) }}</td>
+              <td>
+                <div class="actions">
+                  <button class="btn btn-primary" @click="viewItemDetails(item)">Details</button>
+                  <button class="btn btn-warning" @click="editItem(item)">Edit</button>
+                  <button class="btn btn-danger" @click="deleteItem(item.id)">Delete</button>
+                </div>
+              </td>
+            </tr>
+          </tbody>
+        </table>
+      </div>
+      
+      <!-- Clients Section -->
+      <div v-if="activeSection === 'clients'" class="card">
+        <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px;">
+          <h2>Clients</h2>
+          <div style="display: flex; gap: 10px;">
+            <input 
+              type="text" 
+              v-model="clientSearch" 
+              placeholder="Search clients..." 
+              @input="searchClients"
+              style="padding: 8px; border: 1px solid #ddd; border-radius: 4px;"
+            >
+            <button class="btn btn-primary" @click="showAddClientModal">Add New Client</button>
+          </div>
+        </div>
+        
+        <div v-if="clientsLoading" class="loading">
+          Loading clients...
+        </div>
+        
+        <div v-else-if="clients.length === 0" class="loading">
+          No clients found. Add your first client!
+        </div>
+        
+        <table v-else class="table">
+          <thead>
+            <tr>
+              <th>ID</th>
+              <th>Name</th>
+              <th>Company</th>
+              <th>Email</th>
+              <th>Phone</th>
+              <th>City</th>
+              <th>Actions</th>
+            </tr>
+          </thead>
+          <tbody>
+            <tr v-for="client in clients" :key="client.id">
+              <td>{{ client.id }}</td>
+              <td>{{ client.first_name }} {{ client.last_name }}</td>
+              <td>{{ client.company_name || '-' }}</td>
+              <td>{{ client.email }}</td>
+              <td>{{ client.phone || '-' }}</td>
+              <td>{{ client.city || '-' }}</td>
+              <td>
+                <div class="actions">
+                  <button class="btn btn-warning" @click="editClient(client)">Edit</button>
+                  <button class="btn btn-danger" @click="deleteClient(client.id)">Delete</button>
+                </div>
+              </td>
+            </tr>
+          </tbody>
+        </table>
+      </div>
+    </div>
+    
+    <!-- Add/Edit Modal -->
+    <div v-if="showModal" class="modal" @click.self="closeModal">
+      <div class="modal-content">
+        <span class="close" @click="closeModal">&times;</span>
+        <h2>{{ isEditing ? 'Edit Item' : 'Add New Item' }}</h2>
+        
+        <form @submit.prevent="saveItem">
+          <div class="form-group">
+            <label for="name">Name *</label>
+            <input 
+              type="text" 
+              id="name" 
+              v-model="formData.name" 
+              required 
+              placeholder="Enter item name"
+            >
+          </div>
+          
+          <div class="form-group">
+            <label for="description">Description</label>
+            <textarea 
+              id="description" 
+              v-model="formData.description" 
+              placeholder="Enter item description"
+            ></textarea>
+          </div>
+          
+          <div class="form-group">
+            <label for="serial_number">Serial Number</label>
+            <input 
+              type="text" 
+              id="serial_number" 
+              v-model="formData.serial_number" 
+              placeholder="Enter serial number"
+            >
+          </div>
+          
+          <div class="form-group">
+            <label for="picture">Picture</label>
+            <input 
+              type="file" 
+              id="picture" 
+              @change="handleFileUpload" 
+              accept="image/*"
+            >
+            <div v-if="formData.picture" style="margin-top: 10px;">
+              <img :src="formData.picture" :alt="formData.name" style="max-width: 200px; max-height: 200px; border-radius: 4px;">
+            </div>
+          </div>
+          
+          <div class="form-group">
+            <label for="quantity">Quantity *</label>
+            <input 
+              type="number" 
+              id="quantity" 
+              v-model="formData.quantity" 
+              required 
+              min="0"
+              placeholder="Enter quantity"
+            >
+          </div>
+          
+          <div class="form-group">
+            <label for="price">Price *</label>
+            <input 
+              type="number" 
+              id="price" 
+              v-model="formData.price" 
+              required 
+              min="0" 
+              step="0.01"
+              placeholder="Enter price"
+            >
+          </div>
+          
+          <div style="display: flex; gap: 10px; justify-content: flex-end; margin-top: 20px;">
+            <button type="button" class="btn" @click="closeModal">Cancel</button>
+            <button type="submit" class="btn btn-success">
+              {{ isEditing ? 'Update' : 'Add' }} Item
+            </button>
+          </div>
+        </form>
+      </div>
+    
+    <!-- Item Details Modal -->
+    <div v-if="showDetailsModal" class="modal" @click.self="closeDetailsModal">
+      <div class="modal-content" style="max-width: 800px;">
+        <span class="close" @click="closeDetailsModal">&times;</span>
+        <h2>Item Details: {{ selectedItem.name }}</h2>
+        
+        <div class="tabs">
+          <button 
+            :class="['tab-btn', { active: activeTab === 'info' }]" 
+            @click="activeTab = 'info'"
+          >
+            Information
+          </button>
+          <button 
+            :class="['tab-btn', { active: activeTab === 'rental' }]" 
+            @click="activeTab = 'rental'"
+          >
+            Rental Prices
+          </button>
+          <button 
+            :class="['tab-btn', { active: activeTab === 'attachments' }]" 
+            @click="activeTab = 'attachments'"
+          >
+            Attachments
+          </button>
+          <button 
+            :class="['tab-btn', { active: activeTab === 'contacts' }]" 
+            @click="activeTab = 'contacts'"
+          >
+            Contact Persons
+          </button>
+        </div>
+        
+        <!-- Information Tab -->
+        <div v-if="activeTab === 'info'" class="tab-content">
+          <div class="item-info">
+            <div class="info-row">
+              <strong>ID:</strong> {{ selectedItem.id }}
+            </div>
+            <div class="info-row">
+              <strong>Name:</strong> {{ selectedItem.name }}
+            </div>
+            <div class="info-row">
+              <strong>Description:</strong> {{ selectedItem.description || 'No description' }}
+            </div>
+            <div class="info-row">
+              <strong>Serial Number:</strong> {{ selectedItem.serial_number || 'No serial number' }}
+            </div>
+            <div class="info-row">
+              <strong>Quantity:</strong> {{ selectedItem.quantity }}
+            </div>
+            <div class="info-row">
+              <strong>Price:</strong> ${{ parseFloat(selectedItem.price).toFixed(2) }}
+            </div>
+            <div class="info-row">
+              <strong>Created:</strong> {{ formatDate(selectedItem.created_at) }}
+            </div>
+            <div class="info-row" v-if="selectedItem.picture">
+              <strong>Picture:</strong><br>
+              <img :src="selectedItem.picture" :alt="selectedItem.name" style="max-width: 300px; border-radius: 4px; margin-top: 10px;">
+            </div>
+          </div>
+        </div>
+        
+        <!-- Rental Prices Tab -->
+        <div v-if="activeTab === 'rental'" class="tab-content">
+          <div style="margin-bottom: 20px;">
+            <button class="btn btn-primary" @click="showAddRentalModal">Add Rental Price</button>
+          </div>
+          
+          <div v-if="rentalPrices.length === 0" class="loading">
+            No rental prices found.
+          </div>
+          
+          <table v-else class="table">
+            <thead>
+              <tr>
+                <th>Client</th>
+                <th>Start Date</th>
+                <th>End Date</th>
+                <th>Daily Price</th>
+                <th>Actions</th>
+              </tr>
+            </thead>
+            <tbody>
+              <tr v-for="rental in rentalPrices" :key="rental.id">
+                <td>{{ getClientName(rental.client_id) }}</td>
+                <td>{{ formatDate(rental.start_date) }}</td>
+                <td>{{ formatDate(rental.end_date) }}</td>
+                <td>${{ parseFloat(rental.daily_price).toFixed(2) }}</td>
+                <td>
+                  <button class="btn btn-danger" @click="deleteRentalPrice(rental.id)">Delete</button>
+                </td>
+              </tr>
+            </tbody>
+          </table>
+        </div>
+        
+        <!-- Contact Persons Tab -->
+        <div v-if="activeTab === 'contacts'" class="tab-content">
+          <div style="margin-bottom: 20px;">
+            <button class="btn btn-primary" @click="showAddContactPersonModal">Add Contact Person</button>
+          </div>
+          
+          <div v-if="contactPersons.length === 0" class="loading">
+            No contact persons found.
+          </div>
+          
+          <div v-else class="contacts-list">
+            <div v-for="person in contactPersons" :key="person.id" class="contact-person-item">
+              <div class="contact-person-info">
+                <strong>{{ person.first_name }} {{ person.last_name }}</strong>
+                <span v-if="person.position" class="contact-position">- {{ person.position }}</span>
+                <span v-if="person.is_primary" class="contact-primary">(Primary)</span>
+              </div>
+              <div class="contact-person-details">
+                <div v-if="person.email">📧 {{ person.email }}</div>
+                <div v-if="person.phone">📞 {{ person.phone }}</div>
+              </div>
+              <div class="contact-person-actions">
+                <button class="btn btn-warning" @click="editContactPerson(person)">Edit</button>
+                <button class="btn btn-danger" @click="deleteContactPerson(person.id)">Delete</button>
+              </div>
+            </div>
+          </div>
+        </div>
+        
+        <!-- Attachments Tab -->
+        <div v-if="activeTab === 'attachments'" class="tab-content">
+          <div style="margin-bottom: 20px;">
+            <button class="btn btn-primary" @click="showAddAttachmentModal">Add Attachment</button>
+          </div>
+          
+          <div v-if="attachments.length === 0" class="loading">
+            No attachments found.
+          </div>
+          
+          <div v-else class="attachments-list">
+            <div v-for="attachment in attachments" :key="attachment.id" class="attachment-item">
+              <div class="attachment-info">
+                <strong>{{ attachment.original_name }}</strong>
+                <span class="attachment-type">{{ attachment.file_type }}</span>
+                <span class="attachment-size">({{ formatFileSize(attachment.file_size) }})</span>
+              </div>
+              <div class="attachment-actions">
+                <a :href="attachment.file_path" target="_blank" class="btn btn-primary">View</a>
+                <button class="btn btn-danger" @click="deleteAttachment(attachment.id)">Delete</button>
+              </div>
+            </div>
+          </div>
+        </div>
+      </div>
+    </div>
+    
+    <!-- Add Rental Price Modal -->
+    <div v-if="showRentalModal" class="modal" @click.self="closeRentalModal">
+      <div class="modal-content">
+        <span class="close" @click="closeRentalModal">&times;</span>
+        <h2>Add Rental Price</h2>
+        
+        <form @submit.prevent="saveRentalPrice">
+          <div class="form-group">
+            <label for="client_id">Client</label>
+            <select id="client_id" v-model="rentalForm.client_id">
+              <option value="">Select client (optional)</option>
+              <option v-for="client in clients" :key="client.id" :value="client.id">
+                {{ client.company_name ? client.company_name + ' - ' : '' }}{{ client.first_name }} {{ client.last_name }}
+              </option>
+            </select>
+          </div>
+          
+          <div class="form-group">
+            <label for="start_date">Start Date *</label>
+            <input 
+              type="date" 
+              id="start_date" 
+              v-model="rentalForm.start_date" 
+              required
+            >
+          </div>
+          
+          <div class="form-group">
+            <label for="end_date">End Date *</label>
+            <input 
+              type="date" 
+              id="end_date" 
+              v-model="rentalForm.end_date" 
+              required
+            >
+          </div>
+          
+          <div class="form-group">
+            <label for="daily_price">Daily Price *</label>
+            <input 
+              type="number" 
+              id="daily_price" 
+              v-model="rentalForm.daily_price" 
+              required 
+              min="0" 
+              step="0.01"
+              placeholder="Enter daily price"
+            >
+          </div>
+          
+          <div style="display: flex; gap: 10px; justify-content: flex-end; margin-top: 20px;">
+            <button type="button" class="btn" @click="closeRentalModal">Cancel</button>
+            <button type="submit" class="btn btn-success">Add Rental Price</button>
+          </div>
+        </form>
+      </div>
+    </div>
+    
+    <!-- Add Attachment Modal -->
+    <div v-if="showAttachmentModal" class="modal" @click.self="closeAttachmentModal">
+      <div class="modal-content">
+        <span class="close" @click="closeAttachmentModal">&times;</span>
+        <h2>Add Attachment</h2>
+        
+        <form @submit.prevent="saveAttachment">
+          <div class="form-group">
+            <label for="file_type">Document Type *</label>
+            <select id="file_type" v-model="attachmentForm.file_type" required>
+              <option value="">Select type</option>
+              <option value="receipt">Receipt</option>
+              <option value="warranty">Warranty</option>
+              <option value="other">Other</option>
+            </select>
+          </div>
+          
+          <div class="form-group">
+            <label for="attachment_file">File *</label>
+            <input 
+              type="file" 
+              id="attachment_file" 
+              @change="handleAttachmentUpload" 
+              required
+              accept=".pdf,.doc,.docx,.txt,.jpg,.jpeg,.png,.gif"
+            >
+          </div>
+          
+          <div style="display: flex; gap: 10px; justify-content: flex-end; margin-top: 20px;">
+            <button type="button" class="btn" @click="closeAttachmentModal">Cancel</button>
+            <button type="submit" class="btn btn-success">Add Attachment</button>
+          </div>
+        </form>
+      </div>
+    </div>
+    
+    <!-- Accounting Section -->
+    <div v-if="activeSection === 'accounting'" class="card">
+      <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px;">
+        <h2>Accounting</h2>
+        <div style="display: flex; gap: 10px;">
+          <button class="btn btn-primary" @click="showAddInvoiceModal">Create Invoice</button>
+          <input 
+            type="text" 
+            v-model="invoiceSearch" 
+            placeholder="Search invoices..." 
+            style="padding: 8px; border: 1px solid #ddd; border-radius: 4px;"
+            @input="searchInvoices"
+          >
+        </div>
+      </div>
+      
+      <div v-if="invoicesLoading" class="loading">
+        Loading invoices...
+      </div>
+      
+      <div v-else-if="invoices.length === 0" class="loading">
+        No invoices found. Create your first invoice!
+      </div>
+      
+      <table v-else class="table">
+        <thead>
+          <tr>
+            <th>Invoice #</th>
+            <th>Client</th>
+            <th>Issue Date</th>
+            <th>Due Date</th>
+            <th>Status</th>
+            <th>Total</th>
+            <th>Actions</th>
+          </tr>
+        </thead>
+        <tbody>
+          <tr v-for="invoice in invoices" :key="invoice.id">
+            <td>{{ invoice.invoice_number }}</td>
+            <td>{{ invoice.client_name }}</td>
+            <td>{{ formatDate(invoice.issue_date) }}</td>
+            <td>{{ formatDate(invoice.due_date) }}</td>
+            <td v-html="invoice.getStatusBadge()"></td>
+            <td>${{ parseFloat(invoice.total_amount).toFixed(2) }}</td>
+            <td>
+              <div class="actions">
+                <button class="btn btn-primary" @click="viewInvoice(invoice)">View</button>
+                <button class="btn btn-warning" @click="editInvoice(invoice)">Edit</button>
+                <button class="btn btn-danger" @click="deleteInvoice(invoice.id)">Delete</button>
+              </div>
+            </td>
+          </tr>
+        </tbody>
+      </table>
+    </div>
+    
+    <!-- Add/Edit Client Modal -->
+    <div v-if="showClientModal" class="modal" @click.self="closeClientModal">
+      <div class="modal-content">
+        <span class="close" @click="closeClientModal">&times;</span>
+        <h2>{{ isEditingClient ? 'Edit Client' : 'Add New Client' }}</h2>
+        
+        <form @submit.prevent="saveClient">
+          <div class="form-group">
+            <label for="y_tunnus">Y-Tunnus</label>
+            <input 
+              type="text" 
+              id="y_tunnus" 
+              v-model="clientForm.y_tunnus" 
+              placeholder="Enter Y-tunnus"
+            >
+          </div>
+          
+          <div class="form-group">
+            <label for="company_name">Company Name</label>
+            <input 
+              type="text" 
+              id="company_name" 
+              v-model="clientForm.company_name" 
+              placeholder="Enter company name"
+            >
+          </div>
+          
+          <div class="form-group">
+            <label for="first_name">First Name *</label>
+            <input 
+              type="text" 
+              id="first_name" 
+              v-model="clientForm.first_name" 
+              required 
+              placeholder="Enter first name"
+            >
+          </div>
+          
+          <div class="form-group">
+            <label for="last_name">Last Name *</label>
+            <input 
+              type="text" 
+              id="last_name" 
+              v-model="clientForm.last_name" 
+              required 
+              placeholder="Enter last name"
+            >
+          </div>
+          
+          <div class="form-group">
+            <label for="email">Email *</label>
+            <input 
+              type="email" 
+              id="email" 
+              v-model="clientForm.email" 
+              required 
+              placeholder="Enter email address"
+            >
+          </div>
+          
+          <div class="form-group">
+            <label for="phone">Phone</label>
+            <input 
+              type="tel" 
+              id="phone" 
+              v-model="clientForm.phone" 
+              placeholder="Enter phone number"
+            >
+          </div>
+          
+          <div class="form-group">
+            <label for="address">Address</label>
+            <input 
+              type="text" 
+              id="address" 
+              v-model="clientForm.address" 
+              placeholder="Enter street address"
+            >
+          </div>
+          
+          <div class="form-group">
+            <label for="city">City</label>
+            <input 
+              type="text" 
+              id="city" 
+              v-model="clientForm.city" 
+              placeholder="Enter city"
+            >
+          </div>
+          
+          <div class="form-group">
+            <label for="state">State/Province</label>
+            <input 
+              type="text" 
+              id="state" 
+              v-model="clientForm.state" 
+              placeholder="Enter state or province"
+            >
+          </div>
+          
+          <div class="form-group">
+            <label for="postal_code">Postal Code</label>
+            <input 
+              type="text" 
+              id="postal_code" 
+              v-model="clientForm.postal_code" 
+              placeholder="Enter postal code"
+            >
+          </div>
+          
+          <div class="form-group">
+            <label for="country">Country</label>
+            <input 
+              type="text" 
+              id="country" 
+              v-model="clientForm.country" 
+              placeholder="Enter country"
+            >
+          </div>
+          
+          <div class="form-group">
+            <label for="notes">Notes</label>
+            <textarea 
+              id="notes" 
+              v-model="clientForm.notes" 
+              placeholder="Enter any additional notes"
+              rows="3"
+            ></textarea>
+          </div>
+          
+          <div style="display: flex; gap: 10px; justify-content: flex-end; margin-top: 20px;">
+            <button type="button" class="btn" @click="closeClientModal">Cancel</button>
+            <button type="submit" class="btn btn-success">
+              {{ isEditingClient ? 'Update' : 'Add' }} Client
+            </button>
+          </div>
+        </form>
+      </div>
+    </div>
+    
+    <!-- Add/Edit Contact Person Modal -->
+    <div v-if="showContactPersonModal" class="modal" @click.self="closeContactPersonModal">
+      <div class="modal-content">
+        <span class="close" @click="closeContactPersonModal">&times;</span>
+        <h2>{{ isEditingContactPerson ? 'Edit Contact Person' : 'Add Contact Person' }}</h2>
+        
+        <form @submit.prevent="saveContactPerson">
+          <div class="form-group">
+            <label for="contact_first_name">First Name *</label>
+            <input 
+              type="text" 
+              id="contact_first_name" 
+              v-model="contactPersonForm.first_name" 
+              required 
+              placeholder="Enter first name"
+            >
+          </div>
+          
+          <div class="form-group">
+            <label for="contact_last_name">Last Name *</label>
+            <input 
+              type="text" 
+              id="contact_last_name" 
+              v-model="contactPersonForm.last_name" 
+              required 
+              placeholder="Enter last name"
+            >
+          </div>
+          
+          <div class="form-group">
+            <label for="contact_email">Email</label>
+            <input 
+              type="email" 
+              id="contact_email" 
+              v-model="contactPersonForm.email" 
+              placeholder="Enter email address"
+            >
+          </div>
+          
+          <div class="form-group">
+            <label for="contact_phone">Phone</label>
+            <input 
+              type="tel" 
+              id="contact_phone" 
+              v-model="contactPersonForm.phone" 
+              placeholder="Enter phone number"
+            >
+          </div>
+          
+          <div class="form-group">
+            <label for="contact_position">Position</label>
+            <input 
+              type="text" 
+              id="contact_position" 
+              v-model="contactPersonForm.position" 
+              placeholder="Enter position/title"
+            >
+          </div>
+          
+          <div class="form-group">
+            <label>
+              <input 
+                type="checkbox" 
+                id="contact_is_primary" 
+                v-model="contactPersonForm.is_primary"
+              >
+              Primary Contact
+            </label>
+          </div>
+          
+          <div style="display: flex; gap: 10px; justify-content: flex-end; margin-top: 20px;">
+            <button type="button" class="btn" @click="closeContactPersonModal">Cancel</button>
+            <button type="submit" class="btn btn-success">
+              {{ isEditingContactPerson ? 'Update' : 'Add' }} Contact Person
+            </button>
+          </div>
+        </form>
+      </div>
+    </div>
+    
+    <!-- Invoice Items Modal -->
+    <div v-if="showInvoiceItemsModal" class="modal" @click.self="closeInvoiceItemsModal">
+      <div class="modal-content">
+        <span class="close" @click="closeInvoiceItemsModal">&times;</span>
+        <h2>{{ invoiceItemsForm.id ? 'Edit Invoice Item' : 'Add Invoice Item' }}</h2>
+        
+        <form @submit.prevent="saveInvoiceItem">
+          <div class="form-group">
+            <label for="item_id">Item *</label>
+            <select id="item_id" v-model="invoiceItemsForm.item_id" required>
+              <option value="">Select item</option>
+              <option v-for="item in items" :key="item.id" :value="item.id">{{ item.name }}</option>
+            </select>
+          </div>
+          
+          <div class="form-group">
+            <label for="item_description">Description</label>
+            <input 
+              type="text" 
+              id="item_description" 
+              v-model="invoiceItemsForm.description" 
+              placeholder="Enter item description"
+            >
+          </div>
+          
+          <div class="form-group">
+            <label for="item_quantity">Quantity *</label>
+            <input 
+              type="number" 
+              id="item_quantity" 
+              v-model="invoiceItemsForm.quantity" 
+              required 
+              min="0.01" 
+              step="0.01"
+              placeholder="Enter quantity"
+            >
+          </div>
+          
+          <div class="form-group">
+            <label for="item_unit_price">Unit Price *</label>
+            <input 
+              type="number" 
+              id="item_unit_price" 
+              v-model="invoiceItemsForm.unit_price" 
+              required 
+              min="0.01" 
+              step="0.01"
+              placeholder="Enter unit price"
+            >
+          </div>
+          
+          <div class="form-group">
+            <label for="item_line_total">Line Total</label>
+            <input 
+              type="number" 
+              id="item_line_total" 
+              v-model="invoiceItemsForm.line_total" 
+              required 
+              min="0.01" 
+              step="0.01"
+              placeholder="Enter line total"
+            >
+          </div>
+          
+          <div style="display: flex; gap: 10px; justify-content: flex-end; margin-top: 20px;">
+            <button type="button" class="btn" @click="closeInvoiceItemsModal">Cancel</button>
+            <button type="submit" class="btn btn-success">
+              {{ invoiceItemsForm.id ? 'Update' : 'Add' }} Item
+            </button>
+          </div>
+        </form>
+      </div>
+    </div>
+    
+    <!-- Invoice Details Modal -->
+    <div v-if="showInvoiceModal" class="modal" @click.self="closeInvoiceModal">
+      <div class="modal-content" style="max-width: 900px;">
+        <span class="close" @click="closeInvoiceModal">&times;</span>
+        <h2>{{ isEditingInvoice ? 'Edit Invoice' : 'Create Invoice' }}</h2>
+        
+        <form @submit.prevent="saveInvoice">
+          <div class="form-group">
+            <label for="invoice_client_id">Client *</label>
+            <select id="invoice_client_id" v-model="invoiceForm.client_id" required>
+              <option value="">Select client</option>
+              <option v-for="client in clients" :key="client.id" :value="client.id">{{ client.first_name }} {{ client.last_name }}</option>
+            </select>
+          </div>
+          
+          <div class="form-group">
+            <label for="invoice_number">Invoice Number *</label>
+            <input 
+              type="text" 
+              id="invoice_number" 
+              v-model="invoiceForm.invoice_number" 
+              required 
+              placeholder="Enter invoice number"
+            >
+          </div>
+          
+          <div class="form-group">
+            <label for="issue_date">Issue Date *</label>
+            <input 
+              type="date" 
+              id="issue_date" 
+              v-model="invoiceForm.issue_date" 
+              required
+            >
+          </div>
+          
+          <div class="form-group">
+            <label for="due_date">Due Date *</label>
+            <input 
+              type="date" 
+              id="due_date" 
+              v-model="invoiceForm.due_date" 
+              required
+            >
+          </div>
+          
+          <div class="form-group">
+            <label for="invoice_status">Status</label>
+            <select id="invoice_status" v-model="invoiceForm.status">
+              <option value="draft">Draft</option>
+              <option value="sent">Sent</option>
+              <option value="paid">Paid</option>
+              <option value="overdue">Overdue</option>
+              <option value="cancelled">Cancelled</option>
+            </select>
+          </div>
+          
+          <div class="form-group">
+            <label for="invoice_subtotal">Subtotal</label>
+            <input 
+              type="number" 
+              id="invoice_subtotal" 
+              v-model="invoiceForm.subtotal" 
+              min="0.01" 
+              step="0.01"
+              placeholder="Enter subtotal"
+            >
+          </div>
+          
+          <div class="form-group">
+            <label for="invoice_tax_amount">Tax Amount</label>
+            <input 
+              type="number" 
+              id="invoice_tax_amount" 
+              v-model="invoiceForm.tax_amount" 
+              min="0.01" 
+              step="0.01"
+              placeholder="Enter tax amount"
+            >
+          </div>
+          
+          <div class="form-group">
+            <label for="invoice_total_amount">Total Amount</label>
+            <input 
+              type="number" 
+              id="invoice_total_amount" 
+              v-model="invoiceForm.total_amount" 
+              min="0.01" 
+              step="0.01"
+              placeholder="Enter total amount"
+            >
+          </div>
+          
+          <div class="form-group">
+            <label for="invoice_notes">Notes</label>
+            <textarea 
+              id="invoice_notes" 
+              v-model="invoiceForm.notes" 
+              placeholder="Enter any notes"
+              rows="3"
+            ></textarea>
+          </div>
+          
+          <div style="display: flex; gap: 10px; justify-content: flex-end; margin-top: 20px;">
+            <button type="button" class="btn" @click="closeInvoiceModal">Cancel</button>
+            <button type="submit" class="btn btn-success">
+              {{ isEditingInvoice ? 'Update' : 'Create' }} Invoice
+            </button>
+          </div>
+        </form>
+        
+        <!-- Invoice Items -->
+        <div v-if="selectedInvoice.id" style="margin-top: 20px;">
+          <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px;">
+            <h3>Invoice Items</h3>
+            <button class="btn btn-primary" @click="showAddInvoiceItemModal">Add Item</button>
+          </div>
+          
+          <div v-if="invoiceItems.length === 0" class="loading">
+            No items found for this invoice.
+          </div>
+          
+          <table v-else class="table">
+            <thead>
+              <tr>
+                <th>Item</th>
+                <th>Description</th>
+                <th>Quantity</th>
+                <th>Unit Price</th>
+                <th>Line Total</th>
+                <th>Actions</th>
+              </tr>
+            </thead>
+            <tbody>
+              <tr v-for="item in invoiceItems" :key="item.id">
+                <td>{{ item.item_name }}</td>
+                <td>{{ item.description }}</td>
+                <td>{{ item.quantity }}</td>
+                <td>${{ parseFloat(item.unit_price).toFixed(2) }}</td>
+                <td>${{ parseFloat(item.line_total).toFixed(2) }}</td>
+                <td>
+                  <div class="actions">
+                    <button class="btn btn-warning" @click="editInvoiceItem(item)">Edit</button>
+                    <button class="btn btn-danger" @click="deleteInvoiceItem(item.id)">Delete</button>
+                  </div>
+                </td>
+              </tr>
+            </tbody>
+          </table>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+import axios from 'axios'
+
+export default {
+  name: 'App',
+  data() {
+    return {
+      activeSection: 'items',
+      items: [],
+      loading: false,
+      showModal: false,
+      isEditing: false,
+      message: '',
+      messageType: '',
+      formData: {
+        id: null,
+        name: '',
+        description: '',
+        serial_number: '',
+        picture: '',
+        quantity: 0,
+        price: 0
+      },
+      showDetailsModal: false,
+      selectedItem: {},
+      activeTab: 'info',
+      rentalPrices: [],
+      attachments: [],
+      showRentalModal: false,
+      rentalForm: {
+        item_id: null,
+        client_id: null,
+        start_date: '',
+        end_date: '',
+        daily_price: 0
+      },
+      showAttachmentModal: false,
+      attachmentForm: {
+        item_id: null,
+        file_type: '',
+        file: null
+      },
+      clients: [],
+      clientsLoading: false,
+      clientSearch: '',
+      showClientModal: false,
+      isEditingClient: false,
+      clientForm: {
+        id: null,
+        y_tunnus: '',
+        company_name: '',
+        first_name: '',
+        last_name: '',
+        email: '',
+        phone: '',
+        address: '',
+        city: '',
+        state: '',
+        postal_code: '',
+        country: '',
+        notes: ''
+      },
+      contactPersons: [],
+      contactPersonsLoading: false,
+      contactPersonForm: {
+        id: null,
+        client_id: null,
+        first_name: '',
+        last_name: '',
+        email: '',
+        phone: '',
+        position: '',
+        is_primary: false
+      },
+      showContactPersonModal: false,
+      isEditingContactPerson: false
+    },
+    // Authentication data
+    isLoggedIn: false,
+    currentUser: null,
+    loginForm: {
+      username: '',
+      password: ''
+    },
+    loginError: '',
+    invoices: [],
+    invoicesLoading: false,
+    invoiceSearch: '',
+    showInvoiceModal: false,
+    isEditingInvoice: false,
+    selectedInvoice: {},
+    invoiceForm: {
+      id: null,
+      client_id: null,
+      invoice_number: '',
+      issue_date: '',
+      due_date: '',
+      status: 'draft',
+      subtotal: 0,
+      tax_amount: 0,
+      total_amount: 0,
+      notes: ''
+    },
+    showInvoiceItemsModal: false,
+    invoiceItems: [],
+    invoiceItemsForm: {
+      id: null,
+      invoice_id: null,
+      item_id: null,
+      description: '',
+      quantity: 1,
+      unit_price: 0,
+      line_total: 0
+    }
+  },
+  mounted() {
+    this.checkAuthStatus()
+    this.fetchItems()
+    this.fetchClients()
+    this.fetchInvoices()
+  },
+  methods: {
+    async fetchItems() {
+      this.loading = true
+      try {
+        const response = await axios.get('/api/items.php')
+        this.items = response.data.records || []
+      } catch (error) {
+        this.showMessage('Error fetching items: ' + error.message, 'error')
+      } finally {
+        this.loading = false
+      }
+    },
+    
+    async saveItem() {
+      try {
+        if (this.isEditing) {
+          await axios.put('/api/items.php', this.formData)
+          this.showMessage('Item updated successfully!', 'success')
+        } else {
+          await axios.post('/api/items.php', this.formData)
+          this.showMessage('Item added successfully!', 'success')
+        }
+        
+        this.closeModal()
+        this.fetchItems()
+      } catch (error) {
+        this.showMessage('Error saving item: ' + error.message, 'error')
+      }
+    },
+    
+    async deleteItem(id) {
+      if (!confirm('Are you sure you want to delete this item?')) {
+        return
+      }
+      
+      try {
+        await axios.delete(`/api/items.php?id=${id}`)
+        this.showMessage('Item deleted successfully!', 'success')
+        this.fetchItems()
+      } catch (error) {
+        this.showMessage('Error deleting item: ' + error.message, 'error')
+      }
+    },
+    
+    showAddModal() {
+      this.isEditing = false
+      this.formData = {
+        id: null,
+        name: '',
+        description: '',
+        serial_number: '',
+        picture: '',
+        quantity: 0,
+        price: 0
+      }
+      this.showModal = true
+    },
+    
+    editItem(item) {
+      this.isEditing = true
+      this.formData = {
+        id: item.id,
+        name: item.name,
+        description: item.description || '',
+        serial_number: item.serial_number || '',
+        picture: item.picture || '',
+        quantity: item.quantity,
+        price: item.price
+      }
+      this.showModal = true
+    },
+    
+    closeModal() {
+      this.showModal = false
+      this.formData = {
+        id: null,
+        name: '',
+        description: '',
+        serial_number: '',
+        picture: '',
+        quantity: 0,
+        price: 0
+      }
+    },
+    
+    async handleFileUpload(event) {
+      const file = event.target.files[0]
+      if (!file) return
+      
+      const formData = new FormData()
+      formData.append('picture', file)
+      
+      try {
+        const response = await axios.post('/api/upload.php', formData, {
+          headers: {
+            'Content-Type': 'multipart/form-data'
+          }
+        })
+        
+        this.formData.picture = response.data.url
+      } catch (error) {
+        this.showMessage('Error uploading picture: ' + error.message, 'error')
+      }
+    },
+    
+    showMessage(text, type) {
+      this.message = text
+      this.messageType = type
+      setTimeout(() => {
+        this.message = ''
+        this.messageType = ''
+      }, 5000)
+    },
+    
+    formatDate(dateString) {
+      return new Date(dateString).toLocaleDateString()
+    },
+    
+    async viewItemDetails(item) {
+      this.selectedItem = item
+      this.activeTab = 'info'
+      this.showDetailsModal = true
+      
+      if (this.activeSection === 'items') {
+        await Promise.all([
+          this.fetchRentalPrices(item.id),
+          this.fetchAttachments(item.id)
+        ])
+      } else if (this.activeSection === 'clients') {
+        await this.fetchContactPersons(item.id)
+      }
+    },
+    
+    closeDetailsModal() {
+      this.showDetailsModal = false
+      this.selectedItem = {}
+      this.rentalPrices = []
+      this.attachments = []
+    },
+    
+    async fetchRentalPrices(itemId) {
+      try {
+        const response = await axios.get(`/api/rental_prices.php?item_id=${itemId}`)
+        this.rentalPrices = response.data.records || []
+      } catch (error) {
+        this.showMessage('Error fetching rental prices: ' + error.message, 'error')
+      }
+    },
+    
+    async fetchAttachments(itemId) {
+      try {
+        const response = await axios.get(`/api/attachments.php?item_id=${itemId}`)
+        this.attachments = response.data.records || []
+      } catch (error) {
+        this.showMessage('Error fetching attachments: ' + error.message, 'error')
+      }
+    },
+    
+    showAddRentalModal() {
+      this.rentalForm = {
+        item_id: this.selectedItem.id,
+        client_id: null,
+        start_date: '',
+        end_date: '',
+        daily_price: 0
+      }
+      this.showRentalModal = true
+    },
+    
+    closeRentalModal() {
+      this.showRentalModal = false
+      this.rentalForm = {
+        item_id: null,
+        client_id: null,
+        start_date: '',
+        end_date: '',
+        daily_price: 0
+      }
+    },
+    
+    async saveRentalPrice() {
+      try {
+        await axios.post('/api/rental_prices.php', this.rentalForm)
+        this.showMessage('Rental price added successfully!', 'success')
+        this.closeRentalModal()
+        await this.fetchRentalPrices(this.selectedItem.id)
+      } catch (error) {
+        this.showMessage('Error saving rental price: ' + error.message, 'error')
+      }
+    },
+    
+    async deleteRentalPrice(id) {
+      if (!confirm('Are you sure you want to delete this rental price?')) {
+        return
+      }
+      
+      try {
+        await axios.delete(`/api/rental_prices.php?id=${id}`)
+        this.showMessage('Rental price deleted successfully!', 'success')
+        await this.fetchRentalPrices(this.selectedItem.id)
+      } catch (error) {
+        this.showMessage('Error deleting rental price: ' + error.message, 'error')
+      }
+    },
+    
+    showAddAttachmentModal() {
+      this.attachmentForm = {
+        item_id: this.selectedItem.id,
+        file_type: '',
+        file: null
+      }
+      this.showAttachmentModal = true
+    },
+    
+    closeAttachmentModal() {
+      this.showAttachmentModal = false
+      this.attachmentForm = {
+        item_id: null,
+        file_type: '',
+        file: null
+      }
+    },
+    
+    handleAttachmentUpload(event) {
+      this.attachmentForm.file = event.target.files[0]
+    },
+    
+    async saveAttachment() {
+      if (!this.attachmentForm.file) {
+        this.showMessage('Please select a file to upload', 'error')
+        return
+      }
+      
+      try {
+        const formData = new FormData()
+        formData.append('attachment', this.attachmentForm.file)
+        formData.append('item_id', this.attachmentForm.item_id)
+        formData.append('file_type', this.attachmentForm.file_type)
+        
+        await axios.post('/api/attachments.php', formData, {
+          headers: {
+            'Content-Type': 'multipart/form-data'
+          }
+        })
+        
+        this.showMessage('Attachment uploaded successfully!', 'success')
+        this.closeAttachmentModal()
+        await this.fetchAttachments(this.selectedItem.id)
+      } catch (error) {
+        this.showMessage('Error uploading attachment: ' + error.message, 'error')
+      }
+    },
+    
+    async deleteAttachment(id) {
+      if (!confirm('Are you sure you want to delete this attachment?')) {
+        return
+      }
+      
+      try {
+        await axios.delete(`/api/attachments.php?id=${id}`)
+        this.showMessage('Attachment deleted successfully!', 'success')
+        await this.fetchAttachments(this.selectedItem.id)
+      } catch (error) {
+        this.showMessage('Error deleting attachment: ' + error.message, 'error')
+      }
+    },
+    
+    formatFileSize(bytes) {
+      if (bytes === 0) return '0 Bytes'
+      const k = 1024
+      const sizes = ['Bytes', 'KB', 'MB', 'GB']
+      const i = Math.floor(Math.log(bytes) / Math.log(k))
+      return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]
+    },
+    
+    getClientName(clientId) {
+      if (!clientId) return 'Unassigned'
+      const client = this.clients.find(c => c.id === clientId)
+      if (!client) return 'Unknown Client'
+      return client.company_name ? `${client.company_name} - ${client.first_name} ${client.last_name}` : `${client.first_name} ${client.last_name}`
+    },
+    
+    // Client management methods
+    async fetchClients() {
+      this.clientsLoading = true
+      try {
+        const response = await axios.get('/api/clients.php')
+        this.clients = response.data.records || []
+      } catch (error) {
+        this.showMessage('Error fetching clients: ' + error.message, 'error')
+      } finally {
+        this.clientsLoading = false
+      }
+    },
+    
+    async searchClients() {
+      if (this.clientSearch.trim() === '') {
+        await this.fetchClients()
+        return
+      }
+      
+      this.clientsLoading = true
+      try {
+        const response = await axios.get(`/api/clients.php?search=${this.clientSearch}`)
+        this.clients = response.data.records || []
+      } catch (error) {
+        this.showMessage('Error searching clients: ' + error.message, 'error')
+      } finally {
+        this.clientsLoading = false
+      }
+    },
+    
+    showAddClientModal() {
+      this.isEditingClient = false
+      this.clientForm = {
+        id: null,
+        y_tunnus: '',
+        company_name: '',
+        first_name: '',
+        last_name: '',
+        email: '',
+        phone: '',
+        address: '',
+        city: '',
+        state: '',
+        postal_code: '',
+        country: '',
+        notes: ''
+      }
+      this.showClientModal = true
+    },
+    
+    editClient(client) {
+      this.isEditingClient = true
+      this.clientForm = {
+        id: client.id,
+        y_tunnus: client.y_tunnus || '',
+        company_name: client.company_name || '',
+        first_name: client.first_name,
+        last_name: client.last_name,
+        email: client.email,
+        phone: client.phone || '',
+        address: client.address || '',
+        city: client.city || '',
+        state: client.state || '',
+        postal_code: client.postal_code || '',
+        country: client.country || '',
+        notes: client.notes || ''
+      }
+      this.showClientModal = true
+    },
+    
+    closeClientModal() {
+      this.showClientModal = false
+      this.clientForm = {
+        id: null,
+        y_tunnus: '',
+        company_name: '',
+        first_name: '',
+        last_name: '',
+        email: '',
+        phone: '',
+        address: '',
+        city: '',
+        state: '',
+        postal_code: '',
+        country: '',
+        notes: ''
+      }
+    },
+    
+    async saveClient() {
+      try {
+        if (this.isEditingClient) {
+          await axios.put('/api/clients.php', this.clientForm)
+          this.showMessage('Client updated successfully!', 'success')
+        } else {
+          await axios.post('/api/clients.php', this.clientForm)
+          this.showMessage('Client added successfully!', 'success')
+        }
+        
+        this.closeClientModal()
+        await this.fetchClients()
+      } catch (error) {
+        this.showMessage('Error saving client: ' + error.message, 'error')
+      }
+    },
+    
+    async deleteClient(id) {
+      if (!confirm('Are you sure you want to delete this client?')) {
+        return
+      }
+      
+      try {
+        await axios.delete(`/api/clients.php?id=${id}`)
+        this.showMessage('Client deleted successfully!', 'success')
+        await this.fetchClients()
+      } catch (error) {
+        this.showMessage('Error deleting client: ' + error.message, 'error')
+      }
+    },
+    
+    // Contact Persons management methods
+    async fetchContactPersons(clientId) {
+      this.contactPersonsLoading = true
+      try {
+        const response = await axios.get(`/api/contact_persons.php?client_id=${clientId}`)
+        this.contactPersons = response.data.records || []
+      } catch (error) {
+        this.showMessage('Error fetching contact persons: ' + error.message, 'error')
+      } finally {
+        this.contactPersonsLoading = false
+      }
+    },
+    
+    showAddContactPersonModal() {
+      this.isEditingContactPerson = false
+      this.contactPersonForm = {
+        id: null,
+        client_id: this.selectedClient ? this.selectedClient.id : null,
+        first_name: '',
+        last_name: '',
+        email: '',
+        phone: '',
+        position: '',
+        is_primary: false
+      }
+      this.showContactPersonModal = true
+    },
+    
+    editContactPerson(person) {
+      this.isEditingContactPerson = true
+      this.contactPersonForm = {
+        id: person.id,
+        client_id: person.client_id,
+        first_name: person.first_name,
+        last_name: person.last_name,
+        email: person.email || '',
+        phone: person.phone || '',
+        position: person.position || '',
+        is_primary: person.is_primary
+      }
+      this.showContactPersonModal = true
+    },
+    
+    closeContactPersonModal() {
+      this.showContactPersonModal = false
+      this.contactPersonForm = {
+        id: null,
+        client_id: null,
+        first_name: '',
+        last_name: '',
+        email: '',
+        phone: '',
+        position: '',
+        is_primary: false
+      }
+    },
+    
+    async saveContactPerson() {
+      try {
+        if (this.isEditingContactPerson) {
+          await axios.put('/api/contact_persons.php', this.contactPersonForm)
+          this.showMessage('Contact person updated successfully!', 'success')
+        } else {
+          await axios.post('/api/contact_persons.php', this.contactPersonForm)
+          this.showMessage('Contact person added successfully!', 'success')
+        }
+        
+        this.closeContactPersonModal()
+        await this.fetchContactPersons(this.contactPersonForm.client_id)
+      } catch (error) {
+        this.showMessage('Error saving contact person: ' + error.message, 'error')
+      }
+    },
+    
+    async deleteContactPerson(id) {
+      if (!confirm('Are you sure you want to delete this contact person?')) {
+        return
+      }
+      
+      try {
+        await axios.delete(`/api/contact_persons.php?id=${id}`)
+        this.showMessage('Contact person deleted successfully!', 'success')
+        await this.fetchContactPersons(this.selectedClient ? this.selectedClient.id : null)
+      } catch (error) {
+        this.showMessage('Error deleting contact person: ' + error.message, 'error')
+      }
+    },
+    
+    // Invoice management methods
+    async fetchInvoices() {
+      this.invoicesLoading = true
+      try {
+        const response = await axios.get('/api/invoices.php')
+        this.invoices = response.data.records || []
+      } catch (error) {
+        this.showMessage('Error fetching invoices: ' + error.message, 'error')
+      } finally {
+        this.invoicesLoading = false
+      }
+    },
+    
+    async searchInvoices() {
+      if (this.invoiceSearch.trim() === '') {
+        await this.fetchInvoices()
+        return
+      }
+      
+      this.invoicesLoading = true
+      try {
+        const response = await axios.get(`/api/invoices.php?search=${this.invoiceSearch}`)
+        this.invoices = response.data.records || []
+      } catch (error) {
+        this.showMessage('Error searching invoices: ' + error.message, 'error')
+      } finally {
+        this.invoicesLoading = false
+      }
+    },
+    
+    showAddInvoiceModal() {
+      this.isEditingInvoice = false
+      this.invoiceForm = {
+        id: null,
+        client_id: null,
+        invoice_number: '',
+        issue_date: '',
+        due_date: '',
+        status: 'draft',
+        subtotal: 0,
+        tax_amount: 0,
+        total_amount: 0,
+        notes: ''
+      }
+      this.showInvoiceModal = true
+    },
+    
+    editInvoice(invoice) {
+      this.isEditingInvoice = true
+      this.invoiceForm = {
+        id: invoice.id,
+        client_id: invoice.client_id,
+        invoice_number: invoice.invoice_number,
+        issue_date: invoice.issue_date,
+        due_date: invoice.due_date,
+        status: invoice.status,
+        subtotal: invoice.subtotal,
+        tax_amount: invoice.tax_amount,
+        total_amount: invoice.total_amount,
+        notes: invoice.notes
+      }
+      this.showInvoiceModal = true
+    },
+    
+    closeInvoiceModal() {
+      this.showInvoiceModal = false
+      this.invoiceForm = {
+        id: null,
+        client_id: null,
+        invoice_number: '',
+        issue_date: '',
+        due_date: '',
+        status: 'draft',
+        subtotal: 0,
+        tax_amount: 0,
+        total_amount: 0,
+        notes: ''
+      }
+    },
+    
+    async saveInvoice() {
+      try {
+        if (this.isEditingInvoice) {
+          await axios.put('/api/invoices.php', this.invoiceForm)
+          this.showMessage('Invoice updated successfully!', 'success')
+        } else {
+          await axios.post('/api/invoices.php', this.invoiceForm)
+          this.showMessage('Invoice created successfully!', 'success')
+        }
+        
+        this.closeInvoiceModal()
+        await this.fetchInvoices()
+      } catch (error) {
+        this.showMessage('Error saving invoice: ' + error.message, 'error')
+      }
+    },
+    
+    async deleteInvoice(id) {
+      if (!confirm('Are you sure you want to delete this invoice?')) {
+        return
+      }
+      
+      try {
+        await axios.delete(`/api/invoices.php?id=${id}`)
+        this.showMessage('Invoice deleted successfully!', 'success')
+        await this.fetchInvoices()
+      } catch (error) {
+        this.showMessage('Error deleting invoice: ' + error.message, 'error')
+      }
+    },
+    
+    viewInvoice(invoice) {
+      this.selectedInvoice = invoice
+      this.showInvoiceItemsModal = true
+    },
+    
+    closeInvoiceItemsModal() {
+      this.showInvoiceItemsModal = false
+      this.selectedInvoice = {}
+      this.invoiceItems = []
+    }
+    
+    async saveInvoiceItem() {
+      try {
+        if (this.invoiceItemsForm.id) {
+          await axios.put('/api/invoice_items.php', this.invoiceItemsForm)
+          this.showMessage('Invoice item updated successfully!', 'success')
+        } else {
+          await axios.post('/api/invoice_items.php', this.invoiceItemsForm)
+          this.showMessage('Invoice item added successfully!', 'success')
+        }
+        
+        this.closeInvoiceItemsModal()
+        await this.viewInvoice(this.selectedInvoice)
+      } catch (error) {
+        this.showMessage('Error saving invoice item: ' + error.message, 'error')
+      }
+    },
+    
+    async deleteInvoiceItem(id) {
+      if (!confirm('Are you sure you want to delete this invoice item?')) {
+        return
+      }
+      
+      try {
+        await axios.delete(`/api/invoice_items.php?id=${id}`)
+        this.showMessage('Invoice item deleted successfully!', 'success')
+        await this.viewInvoice(this.selectedInvoice)
+      } catch (error) {
+        this.showMessage('Error deleting invoice item: ' + error.message, 'error')
+      }
+    }
+    },
+    
+    // Authentication methods
+    async login() {
+      this.loginError = ''
+      
+      try {
+        const response = await axios.post('/api/auth.php', {
+          action: 'login',
+          username: this.loginForm.username,
+          password: this.loginForm.password
+        })
+        
+        if (response.data.user) {
+          this.isLoggedIn = true
+          this.currentUser = response.data.user
+          this.loginForm = { username: '', password: '' }
+          this.showMessage('Login successful!', 'success')
+          this.$refs.loginUsername.value = ''
+          this.$refs.loginPassword.value = ''
+        } else {
+          this.loginError = response.data.message || 'Login failed'
+          this.showMessage('Login failed', 'error')
+        }
+      } catch (error) {
+        this.loginError = 'Network error. Please try again.'
+        this.showMessage('Error during login: ' + error.message, 'error')
+      }
+    },
+    
+    logout() {
+      this.isLoggedIn = false
+      this.currentUser = null
+      this.showMessage('Logged out successfully!', 'success')
+    },
+    
+    checkAuthStatus() {
+      try {
+        const response = await axios.get('/api/auth.php?action=status')
+        if (response.data.user) {
+          this.isLoggedIn = true
+          this.currentUser = response.data.user
+        } else {
+          this.isLoggedIn = false
+          this.currentUser = null
+        }
+      } catch (error) {
+        this.isLoggedIn = false
+        this.currentUser = null
+      }
+    }
+  }
+}
+</script>
+
+<style scoped>
+.alert {
+  padding: 10px;
+  border-radius: 4px;
+  margin-bottom: 20px;
+}
+
+.alert.error {
+  background-color: #fdf2f2;
+  color: #e74c3c;
+  border: 1px solid #f5c6cb;
+}
+
+.alert.success {
+  background-color: #f2f9f4;
+  color: #27ae60;
+  border: 1px solid #c3e6cb;
+}
+
+.nav-tabs {
+  display: flex;
+  border-bottom: 2px solid #e9ecef;
+  margin-bottom: 20px;
+}
+
+.nav-tab {
+  background: none;
+  border: none;
+  padding: 12px 24px;
+  cursor: pointer;
+  font-size: 16px;
+  font-weight: 500;
+  border-bottom: 2px solid transparent;
+  transition: all 0.3s;
+  color: #6c757d;
+}
+
+.nav-tab:hover {
+  background-color: #f8f9fa;
+  color: #495057;
+}
+
+.nav-tab.active {
+  border-bottom-color: #3498db;
+  color: #3498db;
+  font-weight: 600;
+}
+
+.tabs {
+  display: flex;
+  border-bottom: 2px solid #e9ecef;
+  margin-bottom: 20px;
+}
+
+.tab-btn {
+  background: none;
+  border: none;
+  padding: 12px 20px;
+  cursor: pointer;
+  font-size: 14px;
+  border-bottom: 2px solid transparent;
+  transition: all 0.3s;
+}
+
+.tab-btn:hover {
+  background-color: #f8f9fa;
+}
+
+.tab-btn.active {
+  border-bottom-color: #3498db;
+  color: #3498db;
+  font-weight: 600;
+}
+
+.tab-content {
+  min-height: 200px;
+}
+
+.item-info {
+  display: grid;
+  gap: 15px;
+}
+
+.info-row {
+  padding: 10px;
+  border-bottom: 1px solid #eee;
+}
+
+.attachments-list {
+  display: flex;
+  flex-direction: column;
+  gap: 15px;
+}
+
+.attachment-item {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  padding: 15px;
+  border: 1px solid #ddd;
+  border-radius: 8px;
+  background-color: #f9f9f9;
+}
+
+.attachment-info {
+  display: flex;
+  flex-direction: column;
+  gap: 5px;
+}
+
+.attachment-type {
+  background-color: #6c757d;
+  color: white;
+  padding: 2px 8px;
+  border-radius: 12px;
+  font-size: 12px;
+  text-transform: uppercase;
+  font-weight: 500;
+  width: fit-content;
+}
+
+.attachment-size {
+  color: #6c757d;
+  font-size: 12px;
+}
+
+.attachment-actions {
+  display: flex;
+  gap: 10px;
+}
+
+.attachment-actions .btn {
+  text-decoration: none;
+  display: inline-block;
+}
+
+.contacts-list {
+  display: flex;
+  flex-direction: column;
+  gap: 15px;
+}
+
+.contact-person-item {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  padding: 15px;
+  border: 1px solid #ddd;
+  border-radius: 8px;
+  background-color: #f9f9f9;
+}
+
+.contact-person-info {
+  display: flex;
+  flex-direction: column;
+  gap: 5px;
+}
+
+.contact-position {
+  background-color: #6c757d;
+  color: white;
+  padding: 2px 8px;
+  border-radius: 12px;
+  font-size: 12px;
+  text-transform: uppercase;
+  font-weight: 500;
+  width: fit-content;
+}
+
+.contact-primary {
+  background-color: #28a745;
+  color: white;
+  padding: 2px 8px;
+  border-radius: 12px;
+  font-size: 12px;
+  font-weight: 500;
+  width: fit-content;
+  margin-left: 5px;
+}
+
+.contact-person-details {
+  display: flex;
+  flex-direction: column;
+  gap: 5px;
+  font-size: 14px;
+}
+
+.contact-person-actions {
+  display: flex;
+  gap: 10px;
+}
+
+/* Login Styles */
+.login-container {
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  min-height: 100vh;
+  background-color: #f5f5f5;
+}
+
+.login-card {
+  background: white;
+  padding: 2rem;
+  border-radius: 8px;
+  box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
+  width: 100%;
+  max-width: 400px;
+}
+
+.app-header {
+  background: white;
+  padding: 1rem 2rem;
+  border-bottom: 1px solid #e9ecef;
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+}
+
+.user-info {
+  display: flex;
+  align-items: center;
+  gap: 1rem;
+}
+
+.user-role {
+  background: #6c757d;
+  color: white;
+  padding: 0.25rem 0.5rem;
+  border-radius: 4px;
+  font-size: 0.875rem;
+  font-weight: 500;
+  text-transform: uppercase;
+}
+
+.main-content {
+  padding-top: 2rem;
+}
+</style>

+ 4 - 0
frontend/src/main.js

@@ -0,0 +1,4 @@
+import { createApp } from 'vue'
+import App from './App.vue'
+
+createApp(App).mount('#app')

+ 16 - 0
frontend/vite.config.js

@@ -0,0 +1,16 @@
+import { defineConfig } from 'vite'
+import vue from '@vitejs/plugin-vue'
+
+export default defineConfig({
+  plugins: [vue()],
+  server: {
+    port: 3000,
+    proxy: {
+      '/api': {
+        target: 'http://localhost:8000',
+        changeOrigin: true,
+        rewrite: (path) => path.replace(/^\/api/, '')
+      }
+    }
+  }
+})