diff --git a/convert_to_invoice.php b/convert_to_invoice.php
new file mode 100644
index 0000000..3a00226
--- /dev/null
+++ b/convert_to_invoice.php
@@ -0,0 +1,92 @@
+beginTransaction();
+
+ // Fetch quotation
+ $stmt = db()->prepare("SELECT * FROM quotations WHERE id = ? AND deleted_at IS NULL");
+ $stmt->execute([$quotation_id]);
+ $quotation = $stmt->fetch();
+
+ if (!$quotation) {
+ throw new Exception("Quotation not found");
+ }
+
+ if ($quotation['status'] !== 'Approved') {
+ throw new Exception("Only approved quotations can be converted to invoices");
+ }
+
+ // Generate invoice number
+ $invoice_number = 'INV-' . date('Ymd') . '-' . strtoupper(bin2hex(random_bytes(2)));
+ $due_date = date('Y-m-d', strtotime('+14 days'));
+
+ // Create invoice
+ $stmt = db()->prepare("INSERT INTO invoices (invoice_number, quotation_id, customer_id, user_id, issue_date, due_date, status, subtotal, tax_amount, total_amount, notes)
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)");
+ $stmt->execute([
+ $invoice_number,
+ $quotation_id,
+ $quotation['customer_id'],
+ $_SESSION['user_id'], // Current user (Finance/Admin) who converts it
+ date('Y-m-d'),
+ $due_date,
+ 'Unpaid',
+ $quotation['subtotal'],
+ $quotation['tax_amount'],
+ $quotation['total_amount'],
+ $quotation['notes']
+ ]);
+ $invoice_id = db()->lastInsertId();
+
+ // Copy items
+ $stmt = db()->prepare("SELECT * FROM quotation_items WHERE quotation_id = ?");
+ $stmt->execute([$quotation_id]);
+ $items = $stmt->fetchAll();
+
+ $item_stmt = db()->prepare("INSERT INTO invoice_items (invoice_id, product_id, quantity, unit_price, total_price) VALUES (?, ?, ?, ?, ?)");
+ foreach ($items as $item) {
+ $item_stmt->execute([
+ $invoice_id,
+ $item['product_id'],
+ $item['quantity'],
+ $item['unit_price'],
+ $item['total_price']
+ ]);
+ }
+
+ // Log action
+ $log_stmt = db()->prepare("INSERT INTO audit_logs (user_id, action, entity_type, entity_id, details) VALUES (?, ?, ?, ?, ?)");
+ $log_stmt->execute([$_SESSION['user_id'], 'CREATE', 'INVOICE', $invoice_id, "Number: $invoice_number (from QTN ID: $quotation_id)"]);
+
+ db()->commit();
+ $_SESSION['success'] = "Quotation converted to invoice successfully.";
+ header("Location: invoices.php");
+ exit;
+} catch (Exception $e) {
+ db()->rollBack();
+ $_SESSION['error'] = "Error converting to invoice: " . $e->getMessage();
+ header("Location: quotations.php");
+ exit;
+}
\ No newline at end of file
diff --git a/cron/reminder_scheduler.php b/cron/reminder_scheduler.php
new file mode 100644
index 0000000..7f12878
--- /dev/null
+++ b/cron/reminder_scheduler.php
@@ -0,0 +1,101 @@
+query("
+ SELECT i.*, c.name as customer_name, c.email as customer_email
+ FROM invoices i
+ JOIN customers c ON i.customer_id = c.id
+ WHERE i.status IN ('Unpaid', 'Partial', 'Overdue')
+ AND i.due_date < CURDATE()
+ AND (i.last_reminded_at IS NULL OR i.last_reminded_at < DATE_SUB(NOW(), INTERVAL 7 DAY))
+ AND i.deleted_at IS NULL
+")->fetchAll();
+
+foreach ($overdue_invoices as $invoice) {
+ echo "Sending reminder for Invoice #{$invoice['invoice_number']} to {$invoice['customer_email']}..." . PHP_EOL;
+
+ $subject = "Payment Reminder: Invoice #{$invoice['invoice_number']}";
+ $body = "
+
Payment Reminder
+ Dear {$invoice['customer_name']},
+ This is a friendly reminder that your invoice #{$invoice['invoice_number']} with an amount of " . format_currency($invoice['total_amount']) . " was due on {$invoice['due_date']} .
+ Please kindly arrange for payment at your earliest convenience. We have attached the invoice for your reference.
+ Thank you for your business!
+ ";
+
+ $pdf_content = DocumentService::generateInvoicePDF($invoice['id'], 'S');
+ $opts = [
+ 'attachments' => [
+ [
+ 'data' => $pdf_content,
+ 'name' => "Invoice_{$invoice['invoice_number']}.pdf"
+ ]
+ ]
+ ];
+
+ $res = MailService::sendMail($invoice['customer_email'], $subject, $body, null, $opts);
+
+ if ($res['success']) {
+ $stmt = db()->prepare("UPDATE invoices SET last_reminded_at = NOW(), status = 'Overdue' WHERE id = ?");
+ $stmt->execute([$invoice['id']]);
+ echo "Successfully reminded." . PHP_EOL;
+ } else {
+ echo "Failed to send: " . $res['error'] . PHP_EOL;
+ }
+}
+
+// 2. Quotation Follow-up Reminders
+$pending_quotations = db()->query("
+ SELECT q.*, c.name as customer_name, c.email as customer_email
+ FROM quotations q
+ JOIN customers c ON q.customer_id = c.id
+ WHERE q.status = 'Sent'
+ AND q.expiry_date >= CURDATE()
+ AND (q.last_reminded_at IS NULL OR q.last_reminded_at < DATE_SUB(NOW(), INTERVAL 5 DAY))
+ AND q.deleted_at IS NULL
+")->fetchAll();
+
+foreach ($pending_quotations as $quote) {
+ echo "Sending follow-up for Quotation #{$quote['quotation_number']} to {$quote['customer_email']}..." . PHP_EOL;
+
+ $subject = "Follow-up: Quotation #{$quote['quotation_number']}";
+ $body = "
+ Quotation Follow-up
+ Dear {$quote['customer_name']},
+ We're following up on the quotation #{$quote['quotation_number']} we sent you on {$quote['issue_date']} .
+ The quotation is valid until {$quote['expiry_date']} . Please let us know if you have any questions or if you're ready to proceed. We have attached the quotation for your convenience.
+ Best regards, The Sales Team
+ ";
+
+ $pdf_content = DocumentService::generateQuotationPDF($quote['id'], 'S');
+ $opts = [
+ 'attachments' => [
+ [
+ 'data' => $pdf_content,
+ 'name' => "Quotation_{$quote['quotation_number']}.pdf"
+ ]
+ ]
+ ];
+
+ $res = MailService::sendMail($quote['customer_email'], $subject, $body, null, $opts);
+
+ if ($res['success']) {
+ $stmt = db()->prepare("UPDATE quotations SET last_reminded_at = NOW() WHERE id = ?");
+ $stmt->execute([$quote['id']]);
+ echo "Successfully sent follow-up." . PHP_EOL;
+ } else {
+ echo "Failed to send: " . $res['error'] . PHP_EOL;
+ }
+}
+
+echo "Reminder Scheduler finished." . PHP_EOL;
\ No newline at end of file
diff --git a/customer_delete.php b/customer_delete.php
new file mode 100644
index 0000000..b4a652b
--- /dev/null
+++ b/customer_delete.php
@@ -0,0 +1,24 @@
+prepare("UPDATE customers SET deleted_at = NOW() WHERE id = ?");
+ $stmt->execute([$id]);
+
+ // Log action
+ $log_stmt = db()->prepare("INSERT INTO audit_logs (user_id, action, entity_type, entity_id, details) VALUES (?, ?, ?, ?, ?)");
+ $log_stmt->execute([$_SESSION['user_id'], 'DELETE', 'CUSTOMER', $id, "Soft deleted"]);
+}
+
+$_SESSION['success'] = "Customer deleted successfully.";
+header("Location: customers.php");
+exit;
\ No newline at end of file
diff --git a/customer_form.php b/customer_form.php
new file mode 100644
index 0000000..f7b81fa
--- /dev/null
+++ b/customer_form.php
@@ -0,0 +1,111 @@
+prepare("SELECT * FROM customers WHERE id = ? AND deleted_at IS NULL");
+ $stmt->execute([$id]);
+ $customer = $stmt->fetch();
+}
+
+$page_title = $id ? "Edit Customer" : "Add Customer";
+
+if ($_SERVER['REQUEST_METHOD'] === 'POST') {
+ if (!validate_csrf($_POST['csrf_token'] ?? '')) {
+ die("CSRF token validation failed");
+ }
+
+ $name = $_POST['name'];
+ $email = $_POST['email'];
+ $phone = $_POST['phone'];
+ $address = $_POST['address'];
+ $category = $_POST['category'];
+ $status = $_POST['status'];
+
+ if ($id) {
+ $stmt = db()->prepare("UPDATE customers SET name = ?, email = ?, phone = ?, address = ?, category = ?, status = ? WHERE id = ?");
+ $stmt->execute([$name, $email, $phone, $address, $category, $status, $id]);
+ } else {
+ $stmt = db()->prepare("INSERT INTO customers (name, email, phone, address, category, status) VALUES (?, ?, ?, ?, ?, ?)");
+ $stmt->execute([$name, $email, $phone, $address, $category, $status]);
+ $id = db()->lastInsertId();
+ }
+
+ // Log action
+ $log_stmt = db()->prepare("INSERT INTO audit_logs (user_id, action, entity_type, entity_id, details) VALUES (?, ?, ?, ?, ?)");
+ $log_stmt->execute([$_SESSION['user_id'], $id ? 'UPDATE' : 'CREATE', 'CUSTOMER', $id, "Name: $name"]);
+
+ $_SESSION['success'] = "Customer " . ($id ? "updated" : "created") . " successfully.";
+ header("Location: customers.php");
+ exit;
+}
+
+require_once 'includes/header.php';
+?>
+
+
+
+
+
= $page_title ?>
+
Cancel
+
+
+
+
+
+
+
diff --git a/customers.php b/customers.php
new file mode 100644
index 0000000..7be4d89
--- /dev/null
+++ b/customers.php
@@ -0,0 +1,115 @@
+prepare("SELECT * FROM customers WHERE $where ORDER BY created_at DESC");
+$stmt->execute($params);
+$customers = $stmt->fetchAll();
+?>
+
+
+
Customers
+
+
+ New Customer
+
+
+
+
+
+
+
+
+
+
+ Search
+
+
+
+
+
+
+
+
+
+
+ Name
+ Contact
+ Category
+ Status
+ Created
+ Actions
+
+
+
+
+
+
+
+ No customers found.
+
+
+
+
+
+
+ = e($c['name']) ?>
+ = e($c['address']) ?>
+
+
+ = e($c['email']) ?>
+ = e($c['phone']) ?>
+
+ = e($c['category']) ?>
+
+
+ = e($c['status']) ?>
+
+ = date('M d, Y', strtotime($c['created_at'])) ?>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/db/config.php b/db/config.php
index 6250722..f1d39ba 100644
--- a/db/config.php
+++ b/db/config.php
@@ -5,13 +5,53 @@ define('DB_NAME', 'app_38220');
define('DB_USER', 'app_38220');
define('DB_PASS', '5f905595-f08d-48bc-9b00-3e1a868017ea');
+if (session_status() === PHP_SESSION_NONE) {
+ session_start();
+}
+
function db() {
static $pdo;
if (!$pdo) {
- $pdo = new PDO('mysql:host='.DB_HOST.';dbname='.DB_NAME.';charset=utf8mb4', DB_USER, DB_PASS, [
- PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
- PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
- ]);
+ try {
+ $pdo = new PDO('mysql:host='.DB_HOST.';dbname='.DB_NAME.';charset=utf8mb4', DB_USER, DB_PASS, [
+ PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
+ PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
+ PDO::ATTR_EMULATE_PREPARES => false,
+ ]);
+ } catch (PDOException $e) {
+ die("Connection failed: " . $e->getMessage());
+ }
}
return $pdo;
}
+
+/**
+ * XSS Protection helper
+ */
+function e($value) {
+ return htmlspecialchars($value, ENT_QUOTES, 'UTF-8');
+}
+
+/**
+ * CSRF Token generation
+ */
+function csrf_token() {
+ if (empty($_SESSION['csrf_token'])) {
+ $_SESSION['csrf_token'] = bin2hex(random_bytes(32));
+ }
+ return $_SESSION['csrf_token'];
+}
+
+/**
+ * CSRF Token validation
+ */
+function validate_csrf($token) {
+ return !empty($token) && hash_equals($_SESSION['csrf_token'] ?? '', $token);
+}
+
+/**
+ * Format currency
+ */
+function format_currency($amount) {
+ return '$' . number_format((float)$amount, 2);
+}
\ No newline at end of file
diff --git a/db/migrate.php b/db/migrate.php
new file mode 100644
index 0000000..5852270
--- /dev/null
+++ b/db/migrate.php
@@ -0,0 +1,42 @@
+exec($sql);
+ } catch (PDOException $e) {
+ echo "Warning: " . $e->getMessage() . PHP_EOL;
+ }
+}
+
+echo "Migration finished." . PHP_EOL;
diff --git a/db/schema.sql b/db/schema.sql
new file mode 100644
index 0000000..430589d
--- /dev/null
+++ b/db/schema.sql
@@ -0,0 +1,138 @@
+-- CRM Database Schema - Migration Ready for Laravel 10
+-- Charset: utf8mb4, Engine: InnoDB
+
+-- Users Table
+CREATE TABLE IF NOT EXISTS users (
+ id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
+ username VARCHAR(50) NOT NULL UNIQUE,
+ password VARCHAR(255) NOT NULL,
+ full_name VARCHAR(100) NOT NULL,
+ role ENUM('Admin', 'Sales', 'Finance') NOT NULL DEFAULT 'Sales',
+ created_at TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP,
+ updated_at TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+ deleted_at TIMESTAMP NULL DEFAULT NULL,
+ INDEX idx_users_username (username),
+ INDEX idx_users_role (role)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+
+-- Customers Table
+CREATE TABLE IF NOT EXISTS customers (
+ id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
+ name VARCHAR(255) NOT NULL,
+ email VARCHAR(255),
+ phone VARCHAR(50),
+ address TEXT,
+ category VARCHAR(100),
+ status ENUM('Prospect', 'Active', 'Inactive') NOT NULL DEFAULT 'Prospect',
+ created_at TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP,
+ updated_at TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+ deleted_at TIMESTAMP NULL DEFAULT NULL,
+ INDEX idx_customers_email (email),
+ INDEX idx_customers_status (status),
+ INDEX idx_customers_deleted_at (deleted_at)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+
+-- Products Table
+CREATE TABLE IF NOT EXISTS products (
+ id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
+ name VARCHAR(255) NOT NULL,
+ description TEXT,
+ price DECIMAL(15, 2) NOT NULL DEFAULT 0.00,
+ created_at TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP,
+ updated_at TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+ deleted_at TIMESTAMP NULL DEFAULT NULL,
+ INDEX idx_products_name (name),
+ INDEX idx_products_deleted_at (deleted_at)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+
+-- Quotations Table
+CREATE TABLE IF NOT EXISTS quotations (
+ id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
+ quotation_number VARCHAR(50) NOT NULL UNIQUE,
+ customer_id BIGINT UNSIGNED NOT NULL,
+ user_id BIGINT UNSIGNED NOT NULL,
+ issue_date DATE NOT NULL,
+ expiry_date DATE NOT NULL,
+ status ENUM('Draft', 'Sent', 'Approved', 'Rejected') NOT NULL DEFAULT 'Draft',
+ subtotal DECIMAL(15, 2) NOT NULL DEFAULT 0.00,
+ tax_amount DECIMAL(15, 2) NOT NULL DEFAULT 0.00,
+ total_amount DECIMAL(15, 2) NOT NULL DEFAULT 0.00,
+ notes TEXT,
+ last_reminded_at TIMESTAMP NULL DEFAULT NULL,
+ created_at TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP,
+ updated_at TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+ deleted_at TIMESTAMP NULL DEFAULT NULL,
+ INDEX idx_quotations_customer_id (customer_id),
+ INDEX idx_quotations_user_id (user_id),
+ INDEX idx_quotations_status (status),
+ INDEX idx_quotations_expiry_date (expiry_date),
+ FOREIGN KEY (customer_id) REFERENCES customers(id) ON DELETE CASCADE,
+ FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+
+-- Quotation Items Table
+CREATE TABLE IF NOT EXISTS quotation_items (
+ id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
+ quotation_id BIGINT UNSIGNED NOT NULL,
+ product_id BIGINT UNSIGNED NOT NULL,
+ quantity DECIMAL(10, 2) NOT NULL DEFAULT 1.00,
+ unit_price DECIMAL(15, 2) NOT NULL,
+ discount_amount DECIMAL(15, 2) DEFAULT 0.00,
+ total_price DECIMAL(15, 2) NOT NULL,
+ FOREIGN KEY (quotation_id) REFERENCES quotations(id) ON DELETE CASCADE,
+ FOREIGN KEY (product_id) REFERENCES products(id) ON DELETE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+
+-- Invoices Table
+CREATE TABLE IF NOT EXISTS invoices (
+ id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
+ invoice_number VARCHAR(50) NOT NULL UNIQUE,
+ quotation_id BIGINT UNSIGNED DEFAULT NULL,
+ customer_id BIGINT UNSIGNED NOT NULL,
+ user_id BIGINT UNSIGNED NOT NULL,
+ issue_date DATE NOT NULL,
+ due_date DATE NOT NULL,
+ status ENUM('Unpaid', 'Partial', 'Paid', 'Overdue') NOT NULL DEFAULT 'Unpaid',
+ subtotal DECIMAL(15, 2) NOT NULL DEFAULT 0.00,
+ tax_amount DECIMAL(15, 2) NOT NULL DEFAULT 0.00,
+ total_amount DECIMAL(15, 2) NOT NULL DEFAULT 0.00,
+ payment_proof VARCHAR(255) DEFAULT NULL,
+ notes TEXT,
+ last_reminded_at TIMESTAMP NULL DEFAULT NULL,
+ created_at TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP,
+ updated_at TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+ deleted_at TIMESTAMP NULL DEFAULT NULL,
+ INDEX idx_invoices_customer_id (customer_id),
+ INDEX idx_invoices_status (status),
+ INDEX idx_invoices_due_date (due_date),
+ FOREIGN KEY (customer_id) REFERENCES customers(id) ON DELETE CASCADE,
+ FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
+ FOREIGN KEY (quotation_id) REFERENCES quotations(id) ON DELETE SET NULL
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+
+-- Invoice Items Table
+CREATE TABLE IF NOT EXISTS invoice_items (
+ id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
+ invoice_id BIGINT UNSIGNED NOT NULL,
+ product_id BIGINT UNSIGNED NOT NULL,
+ quantity DECIMAL(10, 2) NOT NULL DEFAULT 1.00,
+ unit_price DECIMAL(15, 2) NOT NULL,
+ discount_amount DECIMAL(15, 2) DEFAULT 0.00,
+ total_price DECIMAL(15, 2) NOT NULL,
+ FOREIGN KEY (invoice_id) REFERENCES invoices(id) ON DELETE CASCADE,
+ FOREIGN KEY (product_id) REFERENCES products(id) ON DELETE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+
+-- Audit Logs Table
+CREATE TABLE IF NOT EXISTS audit_logs (
+ id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
+ user_id BIGINT UNSIGNED,
+ action VARCHAR(100) NOT NULL,
+ entity_type VARCHAR(50) NOT NULL,
+ entity_id BIGINT UNSIGNED NOT NULL,
+ details TEXT,
+ created_at TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP,
+ INDEX idx_audit_logs_user_id (user_id),
+ INDEX idx_audit_logs_entity (entity_type, entity_id),
+ FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE SET NULL
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
\ No newline at end of file
diff --git a/includes/DocumentService.php b/includes/DocumentService.php
new file mode 100644
index 0000000..1c32e30
--- /dev/null
+++ b/includes/DocumentService.php
@@ -0,0 +1,170 @@
+SetFont('helvetica', 'B', 20);
+ $this->Cell(0, 15, 'CRM PRO DOCUMENT', 0, false, 'L', 0, '', 0, false, 'M', 'M');
+ $this->Ln(10);
+ $this->SetFont('helvetica', '', 10);
+ $this->Cell(0, 10, 'Your Trusted Partner in Education', 0, false, 'L', 0, '', 0, false, 'M', 'M');
+ $this->Line(10, 30, 200, 30);
+ }
+
+ public function Footer() {
+ $this->SetY(-15);
+ $this->SetFont('helvetica', 'I', 8);
+ $this->Cell(0, 10, 'Page '.$this->getAliasNumPage().'/'.$this->getAliasNbPages(), 0, false, 'C', 0, '', 0, false, 'T', 'M');
+ }
+}
+
+class DocumentService {
+
+ public static function generateQuotationPDF($id, $dest = 'I') {
+ $stmt = db()->prepare("SELECT q.*, c.name as customer_name, c.email as customer_email, c.phone as customer_phone, c.address as customer_address
+ FROM quotations q
+ JOIN customers c ON q.customer_id = c.id
+ WHERE q.id = ? AND q.deleted_at IS NULL");
+ $stmt->execute([$id]);
+ $quotation = $stmt->fetch();
+
+ if (!$quotation) return null;
+
+ $stmt = db()->prepare("SELECT qi.*, p.name as product_name
+ FROM quotation_items qi
+ JOIN products p ON qi.product_id = p.id
+ WHERE qi.quotation_id = ?");
+ $stmt->execute([$id]);
+ $items = $stmt->fetchAll();
+
+ $pdf = new MYPDF(PDF_PAGE_ORIENTATION, PDF_UNIT, PDF_PAGE_FORMAT, true, 'UTF-8', false);
+ $pdf->SetCreator(PDF_CREATOR);
+ $pdf->SetAuthor('CRM PRO');
+ $pdf->SetTitle('Quotation ' . $quotation['quotation_number']);
+ $pdf->SetMargins(PDF_MARGIN_LEFT, 40, PDF_MARGIN_RIGHT);
+ $pdf->SetHeaderMargin(PDF_MARGIN_HEADER);
+ $pdf->SetFooterMargin(PDF_MARGIN_FOOTER);
+ $pdf->SetAutoPageBreak(TRUE, PDF_MARGIN_BOTTOM);
+ $pdf->SetFont('helvetica', '', 10);
+ $pdf->AddPage();
+
+ $html = self::buildHtml($quotation, $items, 'Quotation');
+ $pdf->writeHTML($html, true, false, true, false, '');
+
+ return $pdf->Output('Quotation_' . $quotation['quotation_number'] . '.pdf', $dest);
+ }
+
+ public static function generateInvoicePDF($id, $dest = 'I') {
+ $stmt = db()->prepare("SELECT i.*, c.name as customer_name, c.email as customer_email, c.phone as customer_phone, c.address as customer_address
+ FROM invoices i
+ JOIN customers c ON i.customer_id = c.id
+ WHERE i.id = ? AND i.deleted_at IS NULL");
+ $stmt->execute([$id]);
+ $invoice = $stmt->fetch();
+
+ if (!$invoice) return null;
+
+ $stmt = db()->prepare("SELECT ii.*, p.name as product_name
+ FROM invoice_items ii
+ JOIN products p ON ii.product_id = p.id
+ WHERE ii.invoice_id = ?");
+ $stmt->execute([$id]);
+ $items = $stmt->fetchAll();
+
+ $pdf = new MYPDF(PDF_PAGE_ORIENTATION, PDF_UNIT, PDF_PAGE_FORMAT, true, 'UTF-8', false);
+ $pdf->SetCreator(PDF_CREATOR);
+ $pdf->SetAuthor('CRM PRO');
+ $pdf->SetTitle('Invoice ' . $invoice['invoice_number']);
+ $pdf->SetMargins(PDF_MARGIN_LEFT, 40, PDF_MARGIN_RIGHT);
+ $pdf->SetHeaderMargin(PDF_MARGIN_HEADER);
+ $pdf->SetFooterMargin(PDF_MARGIN_FOOTER);
+ $pdf->SetAutoPageBreak(TRUE, PDF_MARGIN_BOTTOM);
+ $pdf->SetFont('helvetica', '', 10);
+ $pdf->AddPage();
+
+ $html = self::buildHtml($invoice, $items, 'Invoice');
+ $pdf->writeHTML($html, true, false, true, false, '');
+
+ return $pdf->Output('Invoice_' . $invoice['invoice_number'] . '.pdf', $dest);
+ }
+
+ private static function buildHtml($doc, $items, $type) {
+ $number_key = ($type == 'Quotation') ? 'quotation_number' : 'invoice_number';
+ $date_key = 'issue_date';
+ $expiry_key = ($type == 'Quotation') ? 'expiry_date' : 'due_date';
+ $expiry_label = ($type == 'Quotation') ? 'Expiry' : 'Due Date';
+
+ $html = '
+
+
+
+ Bill To:
+ ' . e($doc['customer_name']) . '
+ ' . nl2br(e($doc['customer_address'])) . '
+ Email: ' . e($doc['customer_email']) . '
+ Phone: ' . e($doc['customer_phone']) . '
+
+
+ ' . $type . ' Details:
+ Number: ' . e($doc[$number_key]) . '
+ Date: ' . date('M d, Y', strtotime($doc[$date_key])) . '
+ ' . $expiry_label . ': ' . date('M d, Y', strtotime($doc[$expiry_key])) . '
+ Status: ' . e($doc['status']) . '
+
+
+
+
+
+
+
+ Product/Service Description
+ Qty
+ Unit Price
+ Total
+
+
+ ';
+
+ foreach ($items as $item) {
+ $html .= '
+
+ ' . e($item['product_name']) . '
+ ' . number_format($item['quantity'], 2) . '
+ ' . format_currency($item['unit_price']) . '
+ ' . format_currency($item['total_price']) . '
+ ';
+ }
+
+ $html .= '
+
+
+
+
+
+
+ Notes:
+ ' . nl2br(e($doc['notes'])) . '
+
+
+
+
+ Subtotal:
+ ' . format_currency($doc['subtotal']) . '
+
+
+ Tax (10%):
+ ' . format_currency($doc['tax_amount']) . '
+
+
+ Total:
+ ' . format_currency($doc['total_amount']) . '
+
+
+
+
+
';
+ return $html;
+ }
+}
diff --git a/includes/footer.php b/includes/footer.php
new file mode 100644
index 0000000..fcddb28
--- /dev/null
+++ b/includes/footer.php
@@ -0,0 +1,5 @@
+
+
+
+