From 798b149626442888d12766cb0b97d93768cd994f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 1 Jun 2026 18:41:50 +0000 Subject: [PATCH] Refine project intake, dashboard actions, and admin payment settings --- dashboard.php | 36 ++-- includes/portal-helpers.php | 64 +++++++ project.php | 321 ++++++++++++++++++++++++++++++++++-- settings.php | 179 ++++++++++++++++++++ staff/estimate-requests.php | 6 +- staff/paypal-setup.php | 8 +- start-project.php | 286 +++++++++++++++++--------------- 7 files changed, 727 insertions(+), 173 deletions(-) create mode 100644 settings.php diff --git a/dashboard.php b/dashboard.php index f9fa48d..53c9db1 100644 --- a/dashboard.php +++ b/dashboard.php @@ -9,6 +9,7 @@ $role = portalGetRole(); $displayName = $user['display_name'] ?? $user['username'] ?? 'User'; $isStaffRole = in_array($role, ['admin', 'staff'], true); +$isAdminRole = $role === 'admin'; $requests = portalLoadProjectRequests(); $proposals = portalLoadProposals(); @@ -291,6 +292,8 @@ .portal-panel th { color:#5a7a9e;font-size:.68rem;text-transform:uppercase;border-top:none;letter-spacing:.04em; } .portal-muted { color:#7a9ac0;font-size:.8rem; } .portal-mono { font-family:monospace;color:#ffc600; } + .portal-primary-action { display:inline-block;border:1px solid rgba(54,243,255,.35);background:rgba(54,243,255,.08);color:#36f3ff;border-radius:4px;padding:2px 8px;font-size:.68rem;font-weight:700;margin-right:7px;text-transform:uppercase;letter-spacing:.04em; } + .portal-primary-action:hover { color:#ffc600;border-color:#ffc600;text-decoration:none; } .status-chip { display:inline-block;border:1px solid rgba(54,243,255,.28);background:rgba(54,243,255,.08);padding:2px 7px;border-radius:999px;color:#a8bedc;font-size:.68rem;font-weight:700; } .timeline-list,.message-list { list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:7px; } .timeline-list li,.message-list li { border:1px solid rgba(54,243,255,.12);border-radius:8px;padding:9px 10px;background:rgba(5,11,20,.44); } @@ -317,6 +320,7 @@
Start Project All Projects + Settings Sign Out
@@ -338,25 +342,23 @@ Project ID - Project + Project ID Client Status Updated - - No projects yet. + No projects yet. - + Open 0 ? pe(date('M j, Y', (int)$project['last_updated_ts'])) : '—'; ?> - Open @@ -370,23 +372,21 @@ Project ID - Project + Project ID Proposal Agreement - - No active projects right now. + No active projects right now. - + Open - Open @@ -399,24 +399,22 @@ - + - - + - + - @@ -429,22 +427,20 @@
Project IDProject Project Status Created
No pending projects.
No pending projects.
Open 0 ? pe(date('M j, Y', (int)$project['created_ts'])) : '—'; ?>Open
- + - - + - + - diff --git a/includes/portal-helpers.php b/includes/portal-helpers.php index e93d959..474f30e 100644 --- a/includes/portal-helpers.php +++ b/includes/portal-helpers.php @@ -226,6 +226,7 @@ function portalRefreshVerificationTokenByEmail($email, &$userOut = null) { define('PORTAL_ESTIMATES_FILE', PORTAL_DATA_DIR . '/estimate_requests.json'); define('PORTAL_PROPOSALS_FILE', PORTAL_DATA_DIR . '/proposals.json'); define('PORTAL_PROJECT_AGREEMENTS_FILE', PORTAL_DATA_DIR . '/project_agreements.json'); +define('PORTAL_ADMIN_SETTINGS_FILE', PORTAL_DATA_DIR . '/admin_settings.json'); // Session keys define('PORTAL_STAFF_SESSION', 'rls_portal_staff'); @@ -276,6 +277,64 @@ function portalSaveJson($file, $data) { return file_put_contents($file, $json, LOCK_EX) !== false; } +function portalDefaultAdminSettings() { + return [ + 'paypal' => [ + 'client_id' => '', + 'secret' => '', + 'environment' => 'sandbox', + 'business_email' => '', + 'invoice_defaults' => '', + ], + 'email' => [ + 'from_name' => '', + 'from_email' => '', + 'reply_to' => '', + ], + 'site' => [ + 'company_name' => 'Runlevel Systems', + 'support_email' => '', + 'support_phone' => '', + ], + 'business' => [ + 'legal_name' => '', + 'address' => '', + ], + 'updated_at' => '', + 'updated_by' => '', + ]; +} + +function portalLoadAdminSettings() { + $stored = portalLoadJson(PORTAL_ADMIN_SETTINGS_FILE); + $defaults = portalDefaultAdminSettings(); + $paypalStored = isset($stored['paypal']) && is_array($stored['paypal']) ? $stored['paypal'] : []; + $emailStored = isset($stored['email']) && is_array($stored['email']) ? $stored['email'] : []; + $siteStored = isset($stored['site']) && is_array($stored['site']) ? $stored['site'] : []; + $businessStored = isset($stored['business']) && is_array($stored['business']) ? $stored['business'] : []; + return [ + 'paypal' => array_merge($defaults['paypal'], $paypalStored), + 'email' => array_merge($defaults['email'], $emailStored), + 'site' => array_merge($defaults['site'], $siteStored), + 'business' => array_merge($defaults['business'], $businessStored), + 'updated_at' => (string)($stored['updated_at'] ?? ''), + 'updated_by' => (string)($stored['updated_by'] ?? ''), + ]; +} + +function portalSaveAdminSettings(array $settings) { + $defaults = portalDefaultAdminSettings(); + $payload = [ + 'paypal' => array_merge($defaults['paypal'], isset($settings['paypal']) && is_array($settings['paypal']) ? $settings['paypal'] : []), + 'email' => array_merge($defaults['email'], isset($settings['email']) && is_array($settings['email']) ? $settings['email'] : []), + 'site' => array_merge($defaults['site'], isset($settings['site']) && is_array($settings['site']) ? $settings['site'] : []), + 'business' => array_merge($defaults['business'], isset($settings['business']) && is_array($settings['business']) ? $settings['business'] : []), + 'updated_at' => (string)($settings['updated_at'] ?? ''), + 'updated_by' => (string)($settings['updated_by'] ?? ''), + ]; + return portalSaveJson(PORTAL_ADMIN_SETTINGS_FILE, $payload); +} + /** * Sanitize HTML output. */ @@ -681,6 +740,8 @@ function portalNormalizeProjectRequest(array $request) { $request['admin_notes_updated_at'] = (string)($request['admin_notes_updated_at'] ?? ''); $request['client_notes'] = (string)($request['client_notes'] ?? ''); $request['client_notes_updated_at'] = (string)($request['client_notes_updated_at'] ?? ''); + $request['staff_response'] = (string)($request['staff_response'] ?? ''); + $request['staff_response_updated_at'] = (string)($request['staff_response_updated_at'] ?? ''); $request['invoice_reference'] = (string)($request['invoice_reference'] ?? ''); $request['invoice_status'] = (string)($request['invoice_status'] ?? ''); $request['amount_due'] = (string)($request['amount_due'] ?? ''); @@ -688,6 +749,9 @@ function portalNormalizeProjectRequest(array $request) { $request['balance_due'] = (string)($request['balance_due'] ?? ''); $request['payment_notes'] = (string)($request['payment_notes'] ?? ''); $request['payment_received_at'] = (string)($request['payment_received_at'] ?? ''); + $request['payment_link'] = (string)($request['payment_link'] ?? ''); + $request['invoice_sent_at'] = (string)($request['invoice_sent_at'] ?? ''); + $request['attachments'] = isset($request['attachments']) && is_array($request['attachments']) ? array_values($request['attachments']) : []; $request['proposal_ids'] = isset($request['proposal_ids']) && is_array($request['proposal_ids']) ? array_values($request['proposal_ids']) : []; $request['agreement_ids'] = isset($request['agreement_ids']) && is_array($request['agreement_ids']) ? array_values($request['agreement_ids']) : []; if (!isset($request['created_at']) || trim((string)$request['created_at']) === '') { diff --git a/project.php b/project.php index bc27023..4e79c18 100644 --- a/project.php +++ b/project.php @@ -22,6 +22,14 @@ $allRequests = portalLoadProjectRequests(); $allProposals = portalLoadProposals(); $allAgreements = portalLoadProjectAgreements(); +$adminSettings = portalLoadAdminSettings(); +$paypalSettings = isset($adminSettings['paypal']) && is_array($adminSettings['paypal']) ? $adminSettings['paypal'] : []; +$paypalEnv = (string)($paypalSettings['environment'] ?? 'sandbox'); +$paypalBusinessEmail = trim((string)($paypalSettings['business_email'] ?? '')); +$paypalClientId = trim((string)($paypalSettings['client_id'] ?? '')); +$paypalSecret = trim((string)($paypalSettings['secret'] ?? '')); +$paypalInvoiceDefaults = trim((string)($paypalSettings['invoice_defaults'] ?? '')); +$paypalApiConfigured = ($paypalClientId !== '' && $paypalSecret !== '' && $paypalBusinessEmail !== ''); // -- Find the project request $request = null; @@ -97,6 +105,21 @@ function _genAgreementId(array $agreements) { if (!empty($a['agreement_id'])) { $existing[(string)$a['agreement_id']] = true; } + + function _genInvoiceId(array $requests) { + $existing = []; + foreach ($requests as $row) { + $invoiceRef = trim((string)($row['invoice_reference'] ?? '')); + if ($invoiceRef !== '') { + $existing[$invoiceRef] = true; + } + } + $d = date('Ymd'); + do { + $id = 'INV-' . $d . '-' . str_pad((string) random_int(1, 9999), 4, '0', STR_PAD_LEFT); + } while (isset($existing[$id])); + return $id; + } } $d = date('Ymd'); do { @@ -378,6 +401,126 @@ function _reloadData(&$allRequests, &$allProposals, &$allAgreements, $currentProposal, $currentAgreement, $projectId); $notice = 'Agreement signed. Your project is now active.'; } + + if ($isStaff && in_array($action, ['create_invoice', 'send_invoice', 'create_payment_link', 'record_payment'], true)) { + $invoiceAmountDue = trim((string)($_POST['invoice_amount_due'] ?? ($request['amount_due'] ?? ''))); + $invoiceNotes = trim((string)($_POST['invoice_notes'] ?? ($request['payment_notes'] ?? ''))); + $invoiceReference = trim((string)($request['invoice_reference'] ?? '')); + if ($invoiceReference === '') { + $invoiceReference = _genInvoiceId($allRequests); + } + + foreach ($allRequests as &$req) { + if (portalGetRequestDisplayId((array)$req) !== $projectId) { + continue; + } + if ($action === 'create_invoice') { + $req['invoice_reference'] = $invoiceReference; + $req['invoice_status'] = 'draft'; + $req['amount_due'] = $invoiceAmountDue; + $req['payment_notes'] = $invoiceNotes; + $req['balance_due'] = $invoiceAmountDue; + $req['updated_at'] = date('c'); + $notice = 'Invoice draft created.'; + } elseif ($action === 'send_invoice') { + $req['invoice_reference'] = $invoiceReference; + $req['invoice_status'] = 'sent'; + $req['invoice_sent_at'] = date('c'); + $req['amount_due'] = $invoiceAmountDue; + $req['payment_notes'] = $invoiceNotes; + if (trim((string)($req['balance_due'] ?? '')) === '') { + $req['balance_due'] = $invoiceAmountDue; + } + $req['updated_at'] = date('c'); + $notice = 'Invoice marked as sent to client.'; + } elseif ($action === 'create_payment_link') { + $payLink = trim((string)($_POST['payment_link'] ?? '')); + if ($payLink === '' || !filter_var($payLink, FILTER_VALIDATE_URL)) { + $error = 'Enter a valid payment link URL.'; + break; + } + $req['invoice_reference'] = $invoiceReference; + $req['invoice_status'] = 'payment_link_sent'; + $req['payment_link'] = $payLink; + $req['amount_due'] = $invoiceAmountDue; + $req['payment_notes'] = $invoiceNotes; + if (trim((string)($req['balance_due'] ?? '')) === '') { + $req['balance_due'] = $invoiceAmountDue; + } + $req['updated_at'] = date('c'); + $notice = 'Payment link saved and ready to send.'; + } elseif ($action === 'record_payment') { + $amountPaid = trim((string)($_POST['amount_paid'] ?? '')); + $req['invoice_reference'] = $invoiceReference; + $req['invoice_status'] = 'paid'; + $req['amount_due'] = $invoiceAmountDue; + $req['amount_paid'] = $amountPaid; + $req['balance_due'] = ''; + $req['payment_notes'] = $invoiceNotes; + $req['payment_received_at'] = date('c'); + if (in_array((string)($req['status'] ?? ''), ['proposal_sent', 'accepted'], true)) { + $req['status'] = 'active'; + } + $req['updated_at'] = date('c'); + $notice = 'Payment recorded.'; + } + break; + } + unset($req); + + if ($error === '') { + portalSaveProjectRequests($allRequests); + _reloadData($allRequests, $allProposals, $allAgreements, + $request, $linkedProposals, $linkedAgreements, + $currentProposal, $currentAgreement, $projectId); + } + } + + if ($isClient && $action === 'send_client_message') { + $message = trim((string)($_POST['client_message'] ?? '')); + if ($message === '') { + $error = 'Enter a message before sending.'; + } else { + foreach ($allRequests as &$req) { + if (portalGetRequestDisplayId((array)$req) !== $projectId) { + continue; + } + $req['client_notes'] = $message; + $req['client_notes_updated_at'] = date('c'); + $req['updated_at'] = date('c'); + break; + } + unset($req); + portalSaveProjectRequests($allRequests); + _reloadData($allRequests, $allProposals, $allAgreements, + $request, $linkedProposals, $linkedAgreements, + $currentProposal, $currentAgreement, $projectId); + $notice = 'Message sent to staff.'; + } + } + + if ($isStaff && $action === 'send_staff_message') { + $message = trim((string)($_POST['staff_message'] ?? '')); + if ($message === '') { + $error = 'Enter a reply before sending.'; + } else { + foreach ($allRequests as &$req) { + if (portalGetRequestDisplayId((array)$req) !== $projectId) { + continue; + } + $req['staff_response'] = $message; + $req['staff_response_updated_at'] = date('c'); + $req['updated_at'] = date('c'); + break; + } + unset($req); + portalSaveProjectRequests($allRequests); + _reloadData($allRequests, $allProposals, $allAgreements, + $request, $linkedProposals, $linkedAgreements, + $currentProposal, $currentAgreement, $projectId); + $notice = 'Reply sent to client.'; + } + } } // ================================================================ @@ -473,6 +616,35 @@ function buildTimeline(array $request, ?array $proposal, ?array $agreement) { $events[] = ['ts' => $signedAt, 'icon' => '✍️', 'text' => 'Agreement signed' . ($signedBy ? ' by ' . $signedBy : ''), 'color' => '#22c55e']; } } + + $invoiceRef = trim((string)($request['invoice_reference'] ?? '')); + if ($invoiceRef !== '') { + $invoiceTs = strtotime((string)($request['invoice_sent_at'] ?? $request['updated_at'] ?? '')); + if ($invoiceTs) { + $events[] = ['ts' => $invoiceTs, 'icon' => '🧾', 'text' => 'Invoice prepared (' . $invoiceRef . ')', 'color' => '#60a5fa']; + } + } + $invoiceStatus = trim((string)($request['invoice_status'] ?? '')); + if (in_array($invoiceStatus, ['sent', 'payment_link_sent'], true)) { + $sentTs = strtotime((string)($request['invoice_sent_at'] ?? $request['updated_at'] ?? '')); + if ($sentTs) { + $events[] = ['ts' => $sentTs, 'icon' => '📨', 'text' => $invoiceStatus === 'payment_link_sent' ? 'Payment link sent to client' : 'Invoice sent to client', 'color' => '#fbbf24']; + } + } + if ($invoiceStatus === 'paid') { + $paidTs = strtotime((string)($request['payment_received_at'] ?? $request['updated_at'] ?? '')); + if ($paidTs) { + $events[] = ['ts' => $paidTs, 'icon' => '💸', 'text' => 'Payment received', 'color' => '#22c55e']; + } + } + $clientMsgTs = strtotime((string)($request['client_notes_updated_at'] ?? '')); + if ($clientMsgTs && trim((string)($request['client_notes'] ?? '')) !== '') { + $events[] = ['ts' => $clientMsgTs, 'icon' => '💬', 'text' => 'Client message sent', 'color' => '#36f3ff']; + } + $staffMsgTs = strtotime((string)($request['staff_response_updated_at'] ?? '')); + if ($staffMsgTs && trim((string)($request['staff_response'] ?? '')) !== '') { + $events[] = ['ts' => $staffMsgTs, 'icon' => '🗨️', 'text' => 'Staff reply sent', 'color' => '#a78bfa']; + } } $reqStatus = (string)($request['status'] ?? ''); @@ -497,17 +669,16 @@ function buildTimeline(array $request, ?array $proposal, ?array $agreement) { function buildTrackerSteps(array $request, ?array $proposal, ?array $agreement) { $reqStatus = (string)($request['status'] ?? 'new'); $propStatus = $proposal ? (string)($proposal['status'] ?? '') : ''; - $agrStatus = $agreement ? (string)($agreement['status'] ?? '') : ''; + $invoiceRef = trim((string)($request['invoice_reference'] ?? '')); + $invoiceStatus = trim((string)($request['invoice_status'] ?? '')); $steps = []; - $steps[] = ['label' => 'Request Submitted', 'done' => true]; - $steps[] = ['label' => 'Proposal Created', 'done' => $proposal !== null]; - $steps[] = ['label' => 'Proposal Sent', 'done' => in_array($propStatus, ['sent', 'accepted', 'rejected'], true)]; + $steps[] = ['label' => 'Project Request', 'done' => true]; $steps[] = ['label' => 'Proposal Approved', 'done' => $propStatus === 'accepted']; - $steps[] = ['label' => 'Agreement Created', 'done' => $agreement !== null]; - $steps[] = ['label' => 'Agreement Sent', 'done' => in_array($agrStatus, ['sent', 'signed'], true)]; - $steps[] = ['label' => 'Agreement Signed', 'done' => $agrStatus === 'signed']; + $steps[] = ['label' => 'Create Invoice', 'done' => $invoiceRef !== '']; + $steps[] = ['label' => 'Send Payment Link', 'done' => in_array($invoiceStatus, ['sent', 'payment_link_sent', 'paid'], true)]; + $steps[] = ['label' => 'Payment Received', 'done' => $invoiceStatus === 'paid']; $steps[] = ['label' => 'Project Active', 'done' => in_array($reqStatus, ['active', 'accepted', 'completed', 'closed'], true)]; $steps[] = ['label' => 'Project Completed', 'done' => in_array($reqStatus, ['completed', 'closed'], true)]; @@ -950,19 +1121,127 @@ function buildTrackerSteps(array $request, ?array $proposal, ?array $agreement)
Payments
-

Payments are managed via PayPal invoices or approved payment method.

- -
- PayPal Setup - Payment Info + +

Preferred workflow: Project → Proposal Approved → Create Invoice → Send Payment Link → Payment Received → Project Active.

+
+
Invoice Reference
+
Invoice Status
+
Amount Due
+
Amount Paid
+
Invoice Sent
+
Payment Received
+ +
Payment Notes
+ + +
Payment Link
+ + + +
+ + PayPal API configuration is set for . Use invoice actions below to track status and payment records. + + PayPal API credentials are not fully configured. Use fallback payment link workflow, or configure PayPal in admin settings. + +
+
+
+
+ + +
+
+ + +
+
+
+ + +
+
+ + + +
+ +
+ + + + +
+ + PayPal Settings + Payment Info +
+ - Payment Information + +

Payment received. Thank you.

+ + Open Payment Link + +

Payment details will appear here when your invoice or payment link is ready.

+ + Payment Information
- + +
+ Messages +
+ + +
+ Client Message +
+
+ + +
+ Staff Reply +
+
+ + + +
+ + +
+ + + + +
+ + +
+ + +
+
+ +
Project Timeline
@@ -984,14 +1263,24 @@ function buildTrackerSteps(array $request, ?array $proposal, ?array $agreement)
- +
Files & Attachments
-

File attachments and document uploads are coming soon. Contact us directly to share files for this project.

+

Project files and attachments are tracked in this workspace.

Repository / Project Link
+ +
+ Submitted Attachments +
    + +
  • + +
+
+
diff --git a/settings.php b/settings.php new file mode 100644 index 0000000..3654855 --- /dev/null +++ b/settings.php @@ -0,0 +1,179 @@ + $paypal, + 'email' => $email, + 'site' => $site, + 'business' => $business, + 'updated_at' => date('c'), + 'updated_by' => (string)($user['username'] ?? 'admin'), + ]; + if (portalSaveAdminSettings($settings)) { + $notice = 'Settings saved.'; + } else { + $error = 'Unable to save settings.'; + } + } +} +?> + + + + + + + + Settings | Admin | Runlevel Systems + + + + + + + +
+
+
+
+

Settings

+

Admin only configuration dashboard.

+
+ ← Dashboard +
+ +
+
+ +
+
+

PayPal Settings

+

PayPal Configuration (Admin Only).

+ + + + + + + + + + + + + + + +
+
+ +
+

Email Settings

+ + + + + + +
+ +
+

Site Settings

+ + + + + + +
+ +
+

Business Information

+ + + + +
+ + + + + +
+
+ + + + + + diff --git a/staff/estimate-requests.php b/staff/estimate-requests.php index d8f71e8..bd669c8 100644 --- a/staff/estimate-requests.php +++ b/staff/estimate-requests.php @@ -150,12 +150,11 @@ - - + - + - diff --git a/staff/paypal-setup.php b/staff/paypal-setup.php index 9456272..8a24660 100644 --- a/staff/paypal-setup.php +++ b/staff/paypal-setup.php @@ -2,7 +2,7 @@ session_start(); define('WDS_SYSTEM', true); require_once __DIR__ . '/../includes/portal-helpers.php'; -portalRequireStaff(); +portalRequireStaff(['admin']); $current_page = 'staff-portal'; $header_class = 'inner-header'; ?> @@ -13,7 +13,7 @@ - PayPal Setup | Staff Portal | Runlevel Systems + PayPal Setup | Admin Portal | Runlevel Systems
Project IDProject Project Completed
No completed projects yet.
No completed projects yet.
Open 0 ? pe(date('M j, Y', (int)$project['last_updated_ts'])) : '—'; ?>Open
Status Created Last Updated
No project requests yet.
No project requests yet.
Open View