============================= File: /home/ayrshireaccounts/manage/api/create_client.php ============================= 'Missing required fields']); exit; } // Sanitize and prepare $first_name = $mysqli->real_escape_string($data['first_name']); $surname = $mysqli->real_escape_string($data['surname']); $email = $mysqli->real_escape_string($data['email']); $telephone = $mysqli->real_escape_string($data['telephone'] ?? ''); $address1 = $mysqli->real_escape_string($data['address1'] ?? ''); $town = $mysqli->real_escape_string($data['town'] ?? ''); $postcode = $mysqli->real_escape_string($data['postcode'] ?? ''); // Insert client $stmt = $mysqli->prepare("INSERT INTO clients (first_name, surname, email, telephone, address1, town, postcode, username) VALUES (?, ?, ?, ?, ?, ?, ?, ?)"); $username = $email; // Reuse email as login $stmt->bind_param("ssssssss", $first_name, $surname, $email, $telephone, $address1, $town, $postcode, $username); $stmt->execute(); echo json_encode(['success' => true, 'client_id' => $stmt->insert_id]); ============================= File: /home/ayrshireaccounts/manage/config/db.php ============================= app_env('APP_ENV', 'production'), 'key' => app_env('APP_KEY', ''), // can be used for your own crypto/HMAC later ]; /* -------------------------- DATABASE (PDO) -------------------------- */ $dbHost = app_env('DB_HOST', 'localhost'); $dbName = app_env('DB_NAME', ''); $dbUser = app_env('DB_USER', ''); $dbPass = app_env('DB_PASS', ''); try { $pdo = new PDO( "mysql:host={$dbHost};dbname={$dbName};charset=utf8mb4", $dbUser, $dbPass, [ PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, PDO::MYSQL_ATTR_FOUND_ROWS => true, ] ); } catch (Throwable $e) { http_response_code(500); echo "Database connection failed."; if (($appConfig['env'] ?? 'production') !== 'production') { echo "
" . e($e->getMessage()) . "
"; } exit; } /* -------------------------- MAIL CONFIG (for PHPMailer or similar) -------------------------- */ $mailConfig = [ 'host' => app_env('MAIL_HOST', ''), 'port' => (int) app_env('MAIL_PORT', 587), 'user' => app_env('MAIL_USER', ''), 'pass' => app_env('MAIL_PASS', ''), 'from' => app_env('MAIL_FROM', 'no-reply@example.com'), 'encr' => app_env('MAIL_ENCRYPTION', 'tls'), // tls|ssl|none ]; /** * Optional factory for PHPMailer (if you use it). * Usage: * require_once __DIR__ . '/bootstrap.php'; * $mail = newMailer(); * $mail->addAddress('client@example.com'); * $mail->Subject = 'Hello'; * $mail->Body = 'Message'; * $mail->send(); */ function newMailer(): ?PHPMailer\PHPMailer\PHPMailer { if (!class_exists(PHPMailer\PHPMailer\PHPMailer::class)) { return null; // PHPMailer not loaded; include it where appropriate } global $mailConfig; $mail = new PHPMailer\PHPMailer\PHPMailer(true); $mail->isSMTP(); $mail->Host = $mailConfig['host']; $mail->Port = $mailConfig['port']; $mail->SMTPAuth = true; $mail->Username = $mailConfig['user']; $mail->Password = $mailConfig['pass']; $mail->setFrom($mailConfig['from']); $enc = strtolower((string)$mailConfig['encr']); if ($enc === 'ssl') { $mail->SMTPSecure = PHPMailer\PHPMailer\PHPMailer::ENCRYPTION_SMTPS; } elseif ($enc === 'tls') { $mail->SMTPSecure = PHPMailer\PHPMailer\PHPMailer::ENCRYPTION_STARTTLS; } else { $mail->SMTPSecure = false; } return $mail; } /* -------------------------- CSRF TOKEN (global, simple) -------------------------- */ if (empty($_SESSION['csrf_token'])) { $_SESSION['csrf_token'] = bin2hex(random_bytes(32)); } function csrf_field(): string { return ''; } function csrf_check(): void { $ok = isset($_POST['csrf_token']) && hash_equals($_SESSION['csrf_token'], $_POST['csrf_token']); if (!$ok) { http_response_code(400); exit('Invalid CSRF token'); } } /* -------------------------- ROLE GUARD (owner/staff) -------------------------- */ function requireRole(array $roles): void { $role = $_SESSION['role'] ?? null; if (!$role || !in_array($role, $roles, true)) { http_response_code(403); exit('Forbidden'); } } /** * Lightweight PDO helpers * Usage: * $rows = db_all("SELECT * FROM clients WHERE active = ?", [1]); * $row = db_one("SELECT * FROM clients WHERE id = ?", [$id]); * $count = db_exec("UPDATE clients SET active=0 WHERE id=?", [$id]); * $id = db_insert("INSERT INTO clients(first_name,surname,email) VALUES(?,?,?)", * [$first, $last, $email]); */ function db_all(string $sql, array $params = []): array { global $pdo; $st = $pdo->prepare($sql); $st->execute($params); return $st->fetchAll(); } function db_one(string $sql, array $params = []): ?array { global $pdo; $st = $pdo->prepare($sql); $st->execute($params); $row = $st->fetch(); return $row === false ? null : $row; } function db_exec(string $sql, array $params = []): int { global $pdo; $st = $pdo->prepare($sql); $st->execute($params); return $st->rowCount(); } function db_insert(string $sql, array $params = []): int { global $pdo; $st = $pdo->prepare($sql); $st->execute($params); return (int)$pdo->lastInsertId(); } function auth_user(): array { return [ 'id' => (int)($_SESSION['user_id'] ?? 0), 'username' => (string)($_SESSION['username'] ?? ''), ]; } function user_roles(int $user_id): array { $rows = db_all(" SELECT r.name FROM user_roles ur JOIN roles r ON ur.role_id = r.id WHERE ur.user_id = ? ", [$user_id]); return array_map(fn($r) => $r['name'], $rows ?: []); } function user_permissions(int $user_id): array { $rows = db_all(" SELECT DISTINCT p.code FROM user_roles ur JOIN role_permissions rp ON ur.role_id = rp.role_id JOIN permissions p ON rp.permission_id = p.id WHERE ur.user_id = ? ", [$user_id]); return array_map(fn($r) => $r['code'], $rows ?: []); } function can(string $perm): bool { $u = auth_user(); if (!$u['id']) return false; $perms = user_permissions($u['id']); return in_array($perm, $perms, true); } function requirePermission(string $perm): void { if (!can($perm)) { http_response_code(403); exit('Forbidden'); } } ============================= File: /home/ayrshireaccounts/manage/config/db_old.php ============================= connect_errno) { echo "Failed to connect to MySQL: " . $mysqli->connect_error; exit(); } ?> ============================= File: /home/ayrshireaccounts/manage/includes/footer.php ============================= ============================= File: /home/ayrshireaccounts/manage/includes/header.php ============================= CRM
============================= File: /home/ayrshireaccounts/manage/business_manage.php ============================= '', 'trading_name' => '', 'business_utr' => '', 'start_date' => '', 'end_date' => '', 'business_type' => '', 'industry' => '', 'address1' => '', 'address2' => '', 'town' => '', 'postcode' => '', 'telephone' => '', 'email' => '', 'client_id' => $client_id, ]; /* ----------------------------- Edit mode → load record ----------------------------- */ if ($action === 'edit' && $business_id) { $row = db_one("SELECT * FROM businesses WHERE id = ? LIMIT 1", [$business_id]); if ($row) { $business = $row; $client_id = (int)$row['client_id']; } else { exit('Business not found.'); } } /* ----------------------------- Save (create / update) ----------------------------- */ if ($_SERVER['REQUEST_METHOD'] === 'POST' && ($action === 'add' || ($action === 'edit' && $business_id))) { csrf_check(); // Collect fields safely (trim and allow empty → NULL for dates) $business_name = trim($_POST['business_name'] ?? ''); $trading_name = trim($_POST['trading_name'] ?? ''); $business_utr = trim($_POST['business_utr'] ?? ''); $start_date = trim($_POST['start_date'] ?? ''); $end_date = trim($_POST['end_date'] ?? ''); $business_type = trim($_POST['business_type'] ?? ''); $industry = trim($_POST['industry'] ?? ''); $address1 = trim($_POST['address1'] ?? ''); $address2 = trim($_POST['address2'] ?? ''); $town = trim($_POST['town'] ?? ''); $postcode = trim($_POST['postcode'] ?? ''); $telephone = trim($_POST['telephone'] ?? ''); $email = trim($_POST['email'] ?? ''); // Normalise dates: empty string → NULL $start_date = $start_date !== '' ? $start_date : null; $end_date = $end_date !== '' ? $end_date : null; if ($action === 'edit') { db_exec( "UPDATE businesses SET business_name=?, trading_name=?, business_utr=?, start_date=?, end_date=?, business_type=?, industry=?, address1=?, address2=?, town=?, postcode=?, telephone=?, email=? WHERE id=?", [ $business_name, $trading_name, $business_utr, $start_date, $end_date, $business_type, $industry, $address1, $address2, $town, $postcode, $telephone, $email, $business_id ] ); } else { db_insert( "INSERT INTO businesses ( business_name, trading_name, business_utr, start_date, end_date, business_type, industry, address1, address2, town, postcode, telephone, email, client_id, active ) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,1)", [ $business_name, $trading_name, $business_utr, $start_date, $end_date, $business_type, $industry, $address1, $address2, $town, $postcode, $telephone, $email, $client_id ] ); } header("Location: client_view.php?id=" . (int)$client_id); exit; } ?>

Business

'Business Name', 'trading_name' => 'Trading Name', 'business_utr' => 'Business UTR', 'start_date' => 'Start Date', 'end_date' => 'End Date', 'business_type' => 'Business Type', 'industry' => 'Industry', 'address1' => 'Address Line 1', 'address2' => 'Address Line 2', 'town' => 'Town', 'postcode' => 'Postcode', 'telephone' => 'Telephone', 'email' => 'Email', ]; $businessTypes = [ 'Sole Trader', 'Partnership', 'Limited Company', 'LLP', 'Charity', 'Other' ]; foreach ($fields as $name => $label): $value = $business[$name] ?? ''; ?>
Cancel
============================= File: /home/ayrshireaccounts/manage/client_manage.php ============================= '', 'middle_names' => '', 'surname' => '', 'date_of_birth' => '', 'address1' => '', 'address2' => '', 'town' => '', 'postcode' => '', 'telephone' => '', 'email' => '', 'ni_number' => '', 'utr_number' => '', 'active' => 1, // new fields 'client_status' => 'active', // enum 'engage_date' => '', 'disengage_date' => '', 'risk_level' => null, // enum 'aml_checked' => 0, 'aml_check_date' => '', 'manager_name' => '', 'proprietor_name' => '', 'marketing_source' => '', 'records_location' => '', 'software_primary' => null, // enum ]; /* ----------------------------- Edit mode → load record ----------------------------- */ if ($action === 'edit' && $id) { $row = db_one("SELECT * FROM clients WHERE id = ? LIMIT 1", [$id]); if (!$row) exit('Client not found.'); $client = array_merge($client, $row); } /* ----------------------------- Save (create / update) ----------------------------- */ if ($_SERVER['REQUEST_METHOD'] === 'POST' && ($action === 'add' || ($action === 'edit' && $id))) { csrf_check(); // Helpers $post = fn($k) => trim($_POST[$k] ?? ''); $dateOrNull = function($k) use ($post) { $v = $post($k); return ($v === '' ? null : $v); }; $bool = fn($k) => isset($_POST[$k]) ? (int)!!$_POST[$k] : 0; // Read inputs $data = [ 'first_name' => $post('first_name'), 'middle_names' => $post('middle_names'), 'surname' => $post('surname'), 'date_of_birth' => $dateOrNull('date_of_birth'), 'address1' => $post('address1'), 'address2' => $post('address2'), 'town' => $post('town'), 'postcode' => $post('postcode'), 'telephone' => $post('telephone'), 'email' => $post('email'), 'ni_number' => $post('ni_number'), 'utr_number' => $post('utr_number'), 'active' => $bool('active'), 'client_status' => $_POST['client_status'] ?? 'active', 'engage_date' => $dateOrNull('engage_date'), 'disengage_date' => $dateOrNull('disengage_date'), 'risk_level' => ($_POST['risk_level'] ?? null) ?: null, 'aml_checked' => $bool('aml_checked'), 'aml_check_date' => $dateOrNull('aml_check_date'), 'manager_name' => $post('manager_name'), 'proprietor_name' => $post('proprietor_name'), 'marketing_source' => $post('marketing_source'), 'records_location' => $post('records_location'), 'software_primary' => ($_POST['software_primary'] ?? null) ?: null, ]; // Basic sanity: keep status aligned with active flag if archiving manually if ((int)$data['active'] === 0 && $data['client_status'] === 'active') { $data['client_status'] = 'disengaged'; if (!$data['disengage_date']) $data['disengage_date'] = date('Y-m-d'); } if ($action === 'edit') { db_exec( "UPDATE clients SET first_name=?, middle_names=?, surname=?, date_of_birth=?, address1=?, address2=?, town=?, postcode=?, telephone=?, email=?, ni_number=?, utr_number=?, active=?, client_status=?, engage_date=?, disengage_date=?, risk_level=?, aml_checked=?, aml_check_date=?, manager_name=?, proprietor_name=?, marketing_source=?, records_location=?, software_primary=? WHERE id=?", [ $data['first_name'], $data['middle_names'], $data['surname'], $data['date_of_birth'], $data['address1'], $data['address2'], $data['town'], $data['postcode'], $data['telephone'], $data['email'], $data['ni_number'], $data['utr_number'], $data['active'], $data['client_status'], $data['engage_date'], $data['disengage_date'], $data['risk_level'], $data['aml_checked'], $data['aml_check_date'], $data['manager_name'], $data['proprietor_name'], $data['marketing_source'], $data['records_location'], $data['software_primary'], $id ] ); } else { db_insert( "INSERT INTO clients ( first_name, middle_names, surname, date_of_birth, address1, address2, town, postcode, telephone, email, ni_number, utr_number, active, client_status, engage_date, disengage_date, risk_level, aml_checked, aml_check_date, manager_name, proprietor_name, marketing_source, records_location, software_primary ) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)", [ $data['first_name'], $data['middle_names'], $data['surname'], $data['date_of_birth'], $data['address1'], $data['address2'], $data['town'], $data['postcode'], $data['telephone'], $data['email'], $data['ni_number'], $data['utr_number'], $data['active'], $data['client_status'], $data['engage_date'], $data['disengage_date'], $data['risk_level'], $data['aml_checked'], $data['aml_check_date'], $data['manager_name'], $data['proprietor_name'], $data['marketing_source'], $data['records_location'], $data['software_primary'] ] ); } header("Location: clients.php"); exit; } // enums for selects $statuses = ['active'=>'Active','paused'=>'Paused','disengaged'=>'Disengaged']; $risks = ['low'=>'Low','medium'=>'Medium','high'=>'High']; $software = ['Xero','QuickBooks','Sage','FreeAgent','Other']; include 'includes/header.php'; ?>

Client

>
>
Cancel
============================= File: /home/ayrshireaccounts/manage/client_tab_tasks.php =============================
Overdue Due Soon Completed
Business Job Title Task Name Status Job Due Task Due
============================= File: /home/ayrshireaccounts/manage/client_view.php =============================

Client:

Contact Details

Name:

Address:

Telephone:

Email:

Tax References

NI Number:

UTR:

Edit Client

Businesses

Add Business
Business Name Type Industry Start Date Actions
No businesses found.

Jobs

Add Job
Job Title Business Status Due Date Actions
No jobs found.

Invoices

Invoice # Date Due Date Total Actions
£ View PDF
No invoices found.
============================= File: /home/ayrshireaccounts/manage/clients.php ============================= only active=1 $where = []; $params = []; // active flag (legacy) unless user chooses "show all" if (!$show_all) { $where[] = 'active = ?'; $params[] = 1; } // status if ($status !== '') { $where[] = 'client_status = ?'; $params[] = $status; } // software if ($software !== '') { $where[] = 'software_primary = ?'; $params[] = $software; } // risk if ($risk !== '') { $where[] = 'risk_level = ?'; $params[] = $risk; } // manager (simple LIKE to catch partials) if ($manager !== '') { $where[] = 'manager_name LIKE ?'; $params[] = '%' . $manager . '%'; } // q search on name/email/telephone/utr if ($q !== '') { $where[] = '(CONCAT_WS(" ", first_name, surname) LIKE ? OR email LIKE ? OR telephone LIKE ? OR utr_number LIKE ?)'; $like = '%' . $q . '%'; array_push($params, $like, $like, $like, $like); } $sql = 'SELECT * FROM clients'; if ($where) $sql .= ' WHERE ' . implode(' AND ', $where); $sql .= ' ORDER BY surname, first_name'; $rows = db_all($sql, $params); // Fetch distinct managers for the filter dropdown (only from current result-set criteria except manager) $mgrSql = 'SELECT DISTINCT manager_name AS m FROM clients'; if ($where) { // reuse same WHERE but strip out manager predicate for a useful dropdown list $mgrWhere = array_values(array_filter($where, fn($w) => stripos($w, 'manager_name') === false)); $mgrParams = []; $pIndex = 0; for ($i = 0; $i < count($where); $i++) { if (stripos($where[$i], 'manager_name') === false) { $mgrParams[] = $params[$pIndex]; } $pIndex++; } if ($mgrWhere) $mgrSql .= ' WHERE ' . implode(' AND ', $mgrWhere); } else { $mgrParams = []; } $mgrSql .= ' ORDER BY m'; $managers = db_all($mgrSql, $mgrParams); // Helper badge renderers function statusBadge(?string $s): string { $s = (string)$s; $map = [ 'active' => 'success', 'paused' => 'secondary', 'disengaged' => 'dark', ]; $class = $map[$s] ?? 'secondary'; return '' . e($s ?: '—') . ''; } function riskBadge(?string $r): string { $r = (string)$r; $map = [ 'low' => 'success', 'medium' => 'warning', 'high' => 'danger', ]; $class = $map[$r] ?? 'secondary'; return '' . e($r ?: '—') . ''; } function softwareBadge(?string $s): string { $s = (string)$s; $map = [ 'Xero' => 'info', 'QuickBooks' => 'success', 'Sage' => 'secondary', 'FreeAgent' => 'primary', 'Other' => 'dark', ]; $class = $map[$s] ?? 'light text-dark'; return $s ? '' . e($s) . '' : '—'; } function niceDate(?string $d): string { if (!$d) return ''; $ts = strtotime($d); return $ts ? date('j M Y', $ts) : e($d); } ?>

Client List

>
client found
Name Email Telephone Status Risk Software Manager Engaged Actions
No clients match your filters.
Proprietor:
View Edit Files Archive
============================= File: /home/ayrshireaccounts/manage/create_job_from_template.php ============================= beginTransaction(); // Insert job $job_id = db_insert( "INSERT INTO jobs (business_id, client_id, title, description, type, status, due_date, notes) VALUES (?, ?, ?, ?, ?, 'Not Started', ?, ?)", [ $business_id, $client_id, $tpl['name'], $tpl['description'], $tpl['type'], $deadline, $notes ] ); // Fetch template tasks $tasks = db_all( "SELECT task_name, sort_order, deadline_offset_days FROM job_template_tasks WHERE template_id = ? ORDER BY sort_order", [$template_id] ); // Insert tasks if ($tasks) { $ins = $pdo->prepare( "INSERT INTO job_tasks (job_id, task_name, sort_order, due_date) VALUES (?, ?, ?, ?)" ); foreach ($tasks as $t) { $task_name = (string)$t['task_name']; $sort_order = (int)$t['sort_order']; $offset_days = (int)$t['deadline_offset_days']; // Task due date relative to job deadline (e.g., “deadline - N days”) $task_due = date('Y-m-d', strtotime($deadline . " -{$offset_days} days")); $ins->execute([$job_id, $task_name, $sort_order, $task_due]); } } $pdo->commit(); header("Location: client_view.php?id=" . (int)$client_id); exit; } catch (Throwable $e) { if ($pdo->inTransaction()) $pdo->rollBack(); // Optionally log $e->getMessage() http_response_code(500); echo "Could not create job from template."; if (app_env('APP_ENV','production') !== 'production') { echo "
" . e($e->getMessage()) . "
"; } exit; } ============================= File: /home/ayrshireaccounts/manage/delete_job.php ============================= beginTransaction(); // If you have FK ON DELETE CASCADE from job_tasks.job_id -> jobs.id, // the first delete is optional. Keeping it explicit is fine. db_exec("DELETE FROM job_tasks WHERE job_id = ?", [$job_id]); db_exec("DELETE FROM jobs WHERE id = ?", [$job_id]); $pdo->commit(); header("Location: client_view.php?id=" . (int)$client_id); exit; } catch (Throwable $e) { if ($pdo->inTransaction()) $pdo->rollBack(); http_response_code(500); echo "Could not delete job."; if (app_env('APP_ENV','production') !== 'production') { echo "
" . e($e->getMessage()) . "
"; } exit; } ============================= File: /home/ayrshireaccounts/manage/delete_template.php ============================= 0) { try { $pdo->beginTransaction(); // Delete related template tasks first db_exec("DELETE FROM job_template_tasks WHERE template_id = ?", [$id]); // Delete the template itself db_exec("DELETE FROM job_templates WHERE id = ?", [$id]); $pdo->commit(); } catch (Throwable $e) { if ($pdo->inTransaction()) { $pdo->rollBack(); } http_response_code(500); echo "Could not delete job template."; if (app_env('APP_ENV','production') !== 'production') { echo "
" . e($e->getMessage()) . "
"; } exit; } } header('Location: list_job_templates.php'); exit; ============================= File: /home/ayrshireaccounts/manage/download.php ============================= 0) { ob_end_clean(); } if (function_exists('apache_setenv')) { @apache_setenv('no-gzip', '1'); } ini_set('zlib.output_compression', '0'); header('Content-Description: File Transfer'); header('Content-Type: ' . $mime); header('Content-Disposition: attachment; filename="' . $origNameSafe . '"; filename*=UTF-8\'\'' . rawurlencode($origName)); header('Content-Transfer-Encoding: binary'); header('Cache-Control: private, no-transform, max-age=0'); header('Expires: 0'); header('Pragma: public'); header('Content-Length: ' . filesize($real)); // Stream the file readfile($real); exit; ============================= File: /home/ayrshireaccounts/manage/files.php ============================= 0) { $sql .= " WHERE f.client_id = ?"; $params[] = $selected_client_id; } $sql .= " ORDER BY f.uploaded_at DESC"; $files = db_all($sql, $params); ?>

Uploaded Files

Client Original Name Filename Uploaded At Actions
Download
No files found.
============================= File: /home/ayrshireaccounts/manage/invoices.php ============================= 0) { $where[] = "i.client_id = ?"; $params[] = $client_id; } } // Business if (isset($_GET['business_id']) && $_GET['business_id'] !== '') { $business_id = (int)$_GET['business_id']; if ($business_id > 0) { $where[] = "i.business_id = ?"; $params[] = $business_id; } } // Paid flag (allow '0' and '1') if (isset($_GET['paid']) && $_GET['paid'] !== '') { $paid = $_GET['paid'] === '1' ? 1 : 0; $where[] = "i.paid = ?"; $params[] = $paid; } // Due date (<= selected date) if (!empty($_GET['due_date'])) { $due_date = trim($_GET['due_date']); // Basic YYYY-MM-DD check if (preg_match('/^\d{4}-\d{2}-\d{2}$/', $due_date)) { $where[] = "i.due_date <= ?"; $params[] = $due_date; } } $filter_sql = $where ? (" WHERE " . implode(' AND ', $where)) : ""; // ---- Query invoices (parameterised) ---- $sql = " SELECT i.*, c.first_name, c.surname, b.business_name FROM invoices i JOIN clients c ON i.client_id = c.id JOIN businesses b ON i.business_id = b.id $filter_sql ORDER BY i.invoice_date DESC "; $invoices = db_all($sql, $params); // ---- Dropdown data ---- $clients = db_all("SELECT id, first_name, surname FROM clients ORDER BY surname, first_name"); $businesses = db_all("SELECT id, business_name FROM businesses ORDER BY business_name"); // keep selecteds $sel_client = isset($_GET['client_id']) ? (string)$_GET['client_id'] : ''; $sel_business = isset($_GET['business_id']) ? (string)$_GET['business_id'] : ''; $sel_paid = isset($_GET['paid']) ? (string)$_GET['paid'] : ''; $sel_due = isset($_GET['due_date']) ? (string)$_GET['due_date'] : ''; ?>

Invoices

Invoice # Client Business Date Due Total Paid Actions
£ View PDF
No invoices found.
============================= File: /home/ayrshireaccounts/manage/job_templates.php ============================= 0; $template = [ 'name' => '', 'description' => '' ]; $tasks = []; /* ----------------------------- Load existing template + tasks ----------------------------- */ if ($editing) { $row = db_one("SELECT * FROM job_templates WHERE id = ? LIMIT 1", [$template_id]); if ($row) { $template = $row; } else { http_response_code(404); exit('Template not found.'); } $tasks = db_all( "SELECT id, task_name, sort_order, deadline_offset_days FROM job_template_tasks WHERE template_id = ? ORDER BY sort_order ASC", [$template_id] ); } /* ----------------------------- Save (create/update) ----------------------------- */ if ($_SERVER['REQUEST_METHOD'] === 'POST') { csrf_check(); $template['name'] = trim($_POST['name'] ?? ''); $template['description'] = trim($_POST['description'] ?? ''); if ($template['name'] === '') { echo '
Template name is required.
'; } else { try { $pdo->beginTransaction(); if ($editing) { db_exec( "UPDATE job_templates SET name = ?, description = ? WHERE id = ?", [$template['name'], $template['description'], $template_id] ); } else { $template_id = db_insert( "INSERT INTO job_templates (name, description) VALUES (?, ?)", [$template['name'], $template['description']] ); $editing = true; } // Replace tasks (simple, reliable approach) db_exec("DELETE FROM job_template_tasks WHERE template_id = ?", [$template_id]); $postedTasks = $_POST['tasks'] ?? []; if (is_array($postedTasks) && $postedTasks) { // Reindex to ensure sort_order = visual order $ordered = array_values($postedTasks); $ins = $pdo->prepare( "INSERT INTO job_template_tasks (template_id, task_name, sort_order, deadline_offset_days) VALUES (?, ?, ?, ?)" ); foreach ($ordered as $i => $task) { $task_name = trim($task['task_name'] ?? ''); // field name aligned to DB column name $offset = isset($task['deadline_offset_days']) ? (int)$task['deadline_offset_days'] : (isset($task['relative_days']) ? (int)$task['relative_days'] : 0); // backward compat if ($task_name !== '') { $ins->execute([$template_id, $task_name, $i, $offset]); } } } $pdo->commit(); header("Location: job_templates.php"); exit; } catch (Throwable $e) { if ($pdo->inTransaction()) $pdo->rollBack(); echo '
Could not save template.
'; if (app_env('APP_ENV','production') !== 'production') { echo "
" . e($e->getMessage()) . "
"; } } } } ?>

Job Template


Template Tasks
$task): ?>
- days
Task due = Job deadline minus this many days
Cancel
============================= File: /home/ayrshireaccounts/manage/jobs.php ============================= 0) { $where[] = "j.client_id = ?"; $params[] = $cid; } } // Business filter if ($business_id !== '') { $bid = (int)$business_id; if ($bid > 0) { $where[] = "j.business_id = ?"; $params[] = $bid; } } // Status filter (allowlist) $allowed_statuses = ['Not Started', 'In Progress', 'Completed', 'On Hold', 'Cancelled']; if ($status !== '' && in_array($status, $allowed_statuses, true)) { $where[] = "j.status = ?"; $params[] = $status; } // Due date range if ($due_from !== '' && preg_match('/^\d{4}-\d{2}-\d{2}$/', $due_from)) { $where[] = "j.due_date >= ?"; $params[] = $due_from; } if ($due_to !== '' && preg_match('/^\d{4}-\d{2}-\d{2}$/', $due_to)) { $where[] = "j.due_date <= ?"; $params[] = $due_to; } $where_clause = $where ? ('WHERE ' . implode(' AND ', $where)) : ''; // ---- Query jobs (parameterised) ---- $sql = " SELECT j.*, b.business_name, c.first_name, c.surname FROM jobs j JOIN businesses b ON j.business_id = b.id JOIN clients c ON j.client_id = c.id $where_clause ORDER BY j.due_date IS NULL, j.due_date ASC "; $jobs = db_all($sql, $params); // ---- Dropdown data ---- $clients = db_all("SELECT id, first_name, surname FROM clients ORDER BY surname, first_name"); $businesses = db_all("SELECT id, business_name FROM businesses ORDER BY business_name"); ?>

Manage Jobs

Clear
Job Title Client Business Status Due Date Actions
Edit
No jobs found.
============================= File: /home/ayrshireaccounts/manage/list_job_templates.php =============================

Job Templates

Create New Template
Name Type Default Deadline Actions
Edit
No templates found.
============================= File: /home/ayrshireaccounts/manage/login.php =============================

Login

============================= File: /home/ayrshireaccounts/manage/logout.php ============================= date('Y-m-d'), 'due_date' => '', 'invoice_number' => '', 'business_id' => '', 'client_id' => '', 'total' => 0.00, 'notes' => '' ]; $invoice_items = []; /* ----------------------------- Load existing invoice ----------------------------- */ if ($invoice_id) { $row = db_one("SELECT * FROM invoices WHERE id = ? LIMIT 1", [$invoice_id]); if (!$row) { exit('Invoice not found.'); } $invoice = $row; $invoice_items = db_all( "SELECT * FROM invoice_items WHERE invoice_id = ? ORDER BY id ASC", [$invoice_id] ); } /* ----------------------------- Save (create / update) ----------------------------- */ if ($_SERVER['REQUEST_METHOD'] === 'POST') { csrf_check(); // Gather inputs $invoice['invoice_date'] = trim($_POST['invoice_date'] ?? ''); $invoice['due_date'] = trim($_POST['due_date'] ?? ''); $invoice['business_id'] = (int)($_POST['business_id'] ?? 0); $invoice['client_id'] = (int)($_POST['client_id'] ?? 0); $invoice['notes'] = trim($_POST['notes'] ?? ''); // Items array $posted_items = $_POST['items'] ?? []; if (!is_array($posted_items)) $posted_items = []; // Auto-generate invoice number when adding if ($action === 'add') { // Keep your original spirit: base on max(id) to get a monotonic-ish sequence $max = db_one("SELECT MAX(id) AS max_id FROM invoices"); $next_number = 1001 + (int)($max['max_id'] ?? 0) + 1; $invoice['invoice_number'] = 'INV' . $next_number; } else { $invoice['invoice_number'] = trim($_POST['invoice_number'] ?? ''); } // Compute total from items $total = 0.0; foreach ($posted_items as $item) { $amt = isset($item['amount']) ? (float)$item['amount'] : 0.0; $total += $amt; } $invoice['total'] = $total; try { $pdo->beginTransaction(); if ($action === 'edit') { db_exec( "UPDATE invoices SET invoice_date=?, due_date=?, invoice_number=?, business_id=?, client_id=?, total=?, notes=? WHERE id=?", [ $invoice['invoice_date'] !== '' ? $invoice['invoice_date'] : null, $invoice['due_date'] !== '' ? $invoice['due_date'] : null, $invoice['invoice_number'], $invoice['business_id'], $invoice['client_id'], $invoice['total'], $invoice['notes'], $invoice_id ] ); } else { $invoice_id = db_insert( "INSERT INTO invoices (invoice_date, due_date, invoice_number, business_id, client_id, total, notes) VALUES (?,?,?,?,?,?,?)", [ $invoice['invoice_date'] !== '' ? $invoice['invoice_date'] : null, $invoice['due_date'] !== '' ? $invoice['due_date'] : null, $invoice['invoice_number'], $invoice['business_id'], $invoice['client_id'], $invoice['total'], $invoice['notes'] ] ); } // Replace invoice_items with the new set db_exec("DELETE FROM invoice_items WHERE invoice_id = ?", [$invoice_id]); if ($posted_items) { $ins = $pdo->prepare( "INSERT INTO invoice_items (invoice_id, description, amount, job_id) VALUES (?, ?, ?, ?)" ); foreach ($posted_items as $item) { $desc = trim($item['description'] ?? ''); if ($desc === '') continue; // skip empty lines $amount = isset($item['amount']) ? (float)$item['amount'] : 0.0; $job_id = isset($item['job_id']) && $item['job_id'] !== '' ? (int)$item['job_id'] : null; $ins->execute([$invoice_id, $desc, $amount, $job_id]); } } $pdo->commit(); header("Location: view_invoice.php?id=" . (int)$invoice_id); exit; } catch (Throwable $e) { if ($pdo->inTransaction()) $pdo->rollBack(); echo '
Could not save invoice.
'; if (app_env('APP_ENV','production') !== 'production') { echo "
" . e($e->getMessage()) . "
"; } } } /* ----------------------------- Data for dropdowns / JS ----------------------------- */ // All businesses with their client (for Business dropdown and auto-select client) $business_rows = db_all( "SELECT b.id, b.business_name, c.id AS client_id, c.first_name, c.surname FROM businesses b JOIN clients c ON b.client_id = c.id ORDER BY c.surname, b.business_name" ); // Map business -> client id for JS $business_clients = []; foreach ($business_rows as $b) { $business_clients[(int)$b['id']] = (int)$b['client_id']; } // Client dropdown $client_rows = db_all("SELECT id, first_name, surname FROM clients ORDER BY surname, first_name"); // Jobs grouped by business (for job dropdown options) $jobs_all = db_all("SELECT id, title, business_id FROM jobs ORDER BY title"); $jobs_by_business = []; foreach ($jobs_all as $j) { $jobs_by_business[(int)$j['business_id']][] = [ 'id' => (int)$j['id'], 'title' => $j['title'] ]; } ?>

Invoice


Invoice Items
$item): ?>
Cancel
============================= File: /home/ayrshireaccounts/manage/manage_job.php ============================= '', 'description' => '', 'type' => '', 'status' => 'Not Started', 'start_date' => '', 'due_date' => '', 'completed_date' => '', 'fee' => '', 'notes' => '', 'business_id' => '' ]; /* ----------------------------- Load job if editing ----------------------------- */ if ($job_id) { $row = db_one("SELECT * FROM jobs WHERE id = ? LIMIT 1", [$job_id]); if (!$row) { exit('Job not found.'); } $job = $row; $business_id = (int)$job['business_id']; // Resolve client_id via business $br = db_one("SELECT client_id FROM businesses WHERE id = ? LIMIT 1", [$business_id]); $client_id = $br ? (int)$br['client_id'] : 0; } /* ----------------------------- Handle form submission ----------------------------- */ if ($_SERVER['REQUEST_METHOD'] === 'POST') { csrf_check(); $business_id = (int)($_POST['business_id'] ?? 0); // Look up client_id from selected business $client_row = db_one("SELECT client_id FROM businesses WHERE id = ? LIMIT 1", [$business_id]); if (!$client_row) { exit('Selected business not found.'); } $client_id = (int)$client_row['client_id']; // Gather & normalise fields $title = trim($_POST['title'] ?? ''); $description = trim($_POST['description'] ?? ''); $type = trim($_POST['type'] ?? ''); $status = trim($_POST['status'] ?? 'Not Started'); $start_date = trim($_POST['start_date'] ?? ''); $due_date = trim($_POST['due_date'] ?? ''); $completed_date = trim($_POST['completed_date'] ?? ''); $fee = ($_POST['fee'] ?? '') === '' ? null : (float)$_POST['fee']; $notes = trim($_POST['notes'] ?? ''); // Empty dates → NULL $start_date = $start_date !== '' ? $start_date : null; $due_date = $due_date !== '' ? $due_date : null; $completed_date = $completed_date !== '' ? $completed_date : null; if ($action === 'edit') { db_exec( "UPDATE jobs SET title=?, description=?, type=?, status=?, start_date=?, due_date=?, completed_date=?, fee=?, notes=?, business_id=?, client_id=? WHERE id=?", [ $title, $description, $type, $status, $start_date, $due_date, $completed_date, $fee, $notes, $business_id, $client_id, $job_id ] ); } else { $job_id = db_insert( "INSERT INTO jobs (title, description, type, status, start_date, due_date, completed_date, fee, notes, business_id, client_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", [ $title, $description, $type, $status, $start_date, $due_date, $completed_date, $fee, $notes, $business_id, $client_id ] ); } header("Location: client_view.php?id=" . (int)$client_id); exit; } /* ----------------------------- Data for Business dropdown ----------------------------- */ $business_rows = db_all(" SELECT b.id, b.business_name, c.first_name, c.surname FROM businesses b JOIN clients c ON b.client_id = c.id WHERE b.active = 1 ORDER BY c.surname, b.business_name "); ?>

Job

Cancel
============================= File: /home/ayrshireaccounts/manage/manage_vat.php ============================= '', 'registration_date' => '', 'deregistration_date' => '', 'scheme' => '', 'frequency' => '', 'quarter_start_month' => '', 'return_cycle' => '', 'annual_period_start' => '', 'annual_period_end' => '', 'notes' => '', ]; $existing = db_one("SELECT * FROM vat WHERE business_id = ? LIMIT 1", [$business_id]); $vat_id = 0; if ($existing) { $vat = $existing; $vat_id = (int)$existing['id']; } /* ----------------------------- Save (create or update) ----------------------------- */ if ($_SERVER['REQUEST_METHOD'] === 'POST') { csrf_check(); $vat_number = trim($_POST['vat_number'] ?? ''); $registration_date = trim($_POST['registration_date'] ?? ''); $deregistration_date = trim($_POST['deregistration_date'] ?? ''); $scheme = trim($_POST['scheme'] ?? ''); $frequency = trim($_POST['frequency'] ?? ''); $quarter_start_month = ($_POST['quarter_start_month'] ?? '') === '' ? null : (int)$_POST['quarter_start_month']; $return_cycle = trim($_POST['return_cycle'] ?? ''); $annual_start = trim($_POST['annual_period_start'] ?? ''); $annual_end = trim($_POST['annual_period_end'] ?? ''); $notes = trim($_POST['notes'] ?? ''); // Normalise empty strings -> NULL for date fields $registration_date = $registration_date !== '' ? $registration_date : null; $deregistration_date = $deregistration_date !== '' ? $deregistration_date : null; $annual_start = $annual_start !== '' ? $annual_start : null; $annual_end = $annual_end !== '' ? $annual_end : null; if ($vat_id) { db_exec( "UPDATE vat SET vat_number=?, registration_date=?, deregistration_date=?, scheme=?, frequency=?, quarter_start_month=?, return_cycle=?, annual_period_start=?, annual_period_end=?, notes=? WHERE id=?", [ $vat_number, $registration_date, $deregistration_date, $scheme, $frequency, $quarter_start_month, $return_cycle, $annual_start, $annual_end, $notes, $vat_id ] ); } else { db_insert( "INSERT INTO vat (vat_number, registration_date, deregistration_date, scheme, frequency, quarter_start_month, return_cycle, annual_period_start, annual_period_end, notes, business_id) VALUES (?,?,?,?,?,?,?,?,?,?,?)", [ $vat_number, $registration_date, $deregistration_date, $scheme, $frequency, $quarter_start_month, $return_cycle, $annual_start, $annual_end, $notes, $business_id ] ); } // Redirect back to the client's page (not the business id) header("Location: client_view.php?id=" . (int)$client_id); exit; } ?>

Manage VAT for

Back
============================= File: /home/ayrshireaccounts/manage/pass.php ============================= 0) { $sql .= " WHERE j.client_id = ?"; $params[] = $filter_client_id; } $sql .= " ORDER BY jt.due_date ASC, jt.sort_order ASC"; $rows = db_all($sql, $params); // Helper for row colour function task_row_class(?string $taskDue, string $status): string { if ($status === 'Done') return 'table-success'; if (!$taskDue) return ''; $dueTs = strtotime($taskDue); $today = strtotime(date('Y-m-d')); $days = (int) floor(($dueTs - $today) / 86400); if ($days < 0) return 'table-danger'; if ($days <= 7) return 'table-warning'; return ''; } ?>

Task List

Show All Tasks
Overdue Due Soon Completed
Client Job Title Task Name Status Job Due Task Due
No tasks found.
============================= File: /home/ayrshireaccounts/manage/trigger_xama.php ============================= [ 'id' => (int)$client['id'], 'first_name' => (string)$client['first_name'], 'surname' => (string)$client['surname'], 'email' => (string)$client['email'], ] ], JSON_UNESCAPED_UNICODE); // Zapier hook URL (put it in .env: XAMA_WEBHOOK_URL=https://hooks.zapier.com/hooks/catch/954319/2x163uh/) $url = app_env('XAMA_WEBHOOK_URL', 'https://hooks.zapier.com/hooks/catch/954319/2x163uh/'); // cURL request $ch = curl_init($url); curl_setopt_array($ch, [ CURLOPT_CUSTOMREQUEST => 'POST', CURLOPT_POSTFIELDS => $payload, CURLOPT_HTTPHEADER => ['Content-Type: application/json'], CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 15, ]); $response = curl_exec($ch); $errno = curl_errno($ch); $error = curl_error($ch); $httpCode = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); if ($errno !== 0) { http_response_code(502); echo 'Upstream error: ' . $error; exit; } if ($httpCode < 200 || $httpCode >= 300) { http_response_code(502); echo 'Upstream HTTP ' . $httpCode . ': ' . $response; exit; } echo 'Success'; ============================= File: /home/ayrshireaccounts/manage/update_task_status.php ============================= getMessage(); } } ============================= File: /home/ayrshireaccounts/manage/upload_file.php ============================= $maxBytes) { $message = 'File too large (max 20 MB).'; } else { // Derive safe original name + extension allowlist $original_name = basename($file['name']); $ext = strtolower(pathinfo($original_name, PATHINFO_EXTENSION)); if (!in_array($ext, $allowedExt, true)) { $message = 'Unsupported file type.'; } else { // Detect MIME $finfo = new finfo(FILEINFO_MIME_TYPE); $mime = $finfo->file($file['tmp_name']) ?: 'application/octet-stream'; // Generate random filename $new_name = bin2hex(random_bytes(16)) . ($ext ? ".{$ext}" : ''); $destination = $uploadsDir . '/' . $new_name; // Move + set permissions if (move_uploaded_file($file['tmp_name'], $destination)) { @chmod($destination, 0640); // Insert DB row (store size + mime) db_insert( "INSERT INTO client_files (client_id, filename, original_name, mime_type, size_bytes, uploaded_at) VALUES (?, ?, ?, ?, ?, NOW())", [$client_id, $new_name, $original_name, $mime, (int)$file['size']] ); $message = 'File uploaded successfully.'; } else { $message = 'Failed to move file.'; } } } } } // Fetch clients for dropdown $clients = db_all("SELECT id, first_name, surname FROM clients ORDER BY surname, first_name"); ?>

Upload File to Client

Max 20 MB. Allowed: .
============================= File: /home/ayrshireaccounts/manage/view_invoice.php ============================= {$desc} 1 £{$amount} £{$amount} "; } $totalFormatted = number_format((float)$invoice['total'], 2); $html = "
From:
Ayrshire Accountancy
143 Castleview
Dundonald
KA2 9JD

To:
" . e($invoice['first_name'] . ' ' . $invoice['surname']) . "
" . e($invoice['address1']) . "
" . e($invoice['address2']) . "
" . e($invoice['town']) . "
" . e($invoice['postcode']) . "

INVOICE

Invoice #: " . e($invoice['invoice_number']) . "
Date: {$invoice_date_formatted}
Due: {$due_date_formatted}
{$rowsHtml}
Description Quantity Unit Price Total
Total £{$totalFormatted}
Thank you!
"; $dompdf = new Dompdf(); $dompdf->loadHtml($html); $dompdf->setPaper('A4', 'portrait'); $dompdf->render(); $dompdf->stream("Invoice_" . preg_replace('/[^A-Za-z0-9_\-]/', '_', $invoice['invoice_number']) . ".pdf", ['Attachment' => true]); exit; } /* --------------------------------- Handle payment submission --------------------------------- */ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['payment_amount'])) { csrf_check(); $payment_date = trim($_POST['payment_date'] ?? date('Y-m-d')); $payment_amount = (float)($_POST['payment_amount'] ?? 0); $method = trim($_POST['method'] ?? ''); $notes = trim($_POST['payment_notes'] ?? ''); if ($payment_amount > 0) { db_insert( "INSERT INTO payments (invoice_id, payment_date, amount, method, notes) VALUES (?, ?, ?, ?, ?)", [$invoice_id, $payment_date !== '' ? $payment_date : date('Y-m-d'), $payment_amount, $method, $notes] ); // Recompute paid total and set invoices.paid if balance cleared $paid = db_one("SELECT COALESCE(SUM(amount),0) AS total FROM payments WHERE invoice_id = ?", [$invoice_id]); $total_paid = (float)($paid['total'] ?? 0); $is_paid = ($total_paid + 0.00001) >= (float)$invoice['total']; // tiny epsilon db_exec("UPDATE invoices SET paid = ? WHERE id = ?", [$is_paid ? 1 : 0, $invoice_id]); } header("Location: view_invoice.php?id=" . (int)$invoice_id); exit; } /* --------------------------------- Fetch payments + totals --------------------------------- */ $payments = db_all("SELECT * FROM payments WHERE invoice_id = ? ORDER BY payment_date ASC, id ASC", [$invoice_id]); $total_paid = 0.0; foreach ($payments as $p) { $total_paid += (float)$p['amount']; } $balance = (float)$invoice['total'] - $total_paid; include 'includes/header.php'; ?>

Invoice #

Client:
Business:
Invoice Date:
Due Date:
Total:
£
Notes:

Invoice Items

Description Job (if linked) Amount
£
No items.

Payments

Date Method Amount Notes
£
No payments recorded.
Total Paid £
Balance Due £
Add Payment
Edit Invoice Back to Invoices Download PDF