From dc67cbe4695c65707389bb3c3aac42b82dc7b4e4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 31 May 2026 14:18:26 +0000 Subject: [PATCH] Implement unified project request workflow and editable proposal/agreement tools --- client/agreement.php | 64 +++++++ client/contracts.php | 82 ++++---- client/new-request.php | 241 +----------------------- client/proposal.php | 64 +++++++ client/proposals.php | 83 +++++---- client/request.php | 69 +++++++ client/requests.php | 138 +++++--------- contracts.php | 145 ++------------- dashboard.php | 351 ++++++++++------------------------- data/project_agreements.json | 3 + data/project_requests.json | 3 + data/proposals.json | 3 + estimate.php | 275 ++++++++++++++------------- includes/email.php | 24 +++ includes/portal-helpers.php | 137 ++++++++++++-- payments.php | 67 ++----- proposals.php | 76 ++------ staff/contract-template.php | 218 +--------------------- staff/create-agreement.php | 267 ++++++++++++++++++++++++++ staff/create-proposal.php | 241 ++++++++++++++++++++++++ staff/estimate-requests.php | 273 +++++++++++++++++---------- staff/proposal-template.php | 171 +---------------- staff/requests.php | 93 +--------- 23 files changed, 1470 insertions(+), 1618 deletions(-) create mode 100644 client/agreement.php create mode 100644 client/proposal.php create mode 100644 client/request.php create mode 100644 data/project_agreements.json create mode 100644 data/project_requests.json create mode 100644 data/proposals.json create mode 100644 staff/create-agreement.php create mode 100644 staff/create-proposal.php diff --git a/client/agreement.php b/client/agreement.php new file mode 100644 index 0000000..23efa79 --- /dev/null +++ b/client/agreement.php @@ -0,0 +1,64 @@ + + +Project Agreement | Client Portal + + + + +
โ† Back to project agreements
+

Project agreement not found.

+

Project Agreement

+
Agreement ID
+
Request ID
+
Proposal ID
+
Project Title
+
Scope Of Work
+
Deliverables
+
Timeline
+
Payment Terms
+
Revision Terms
+
Customer Responsibilities
+ +
+ + + diff --git a/client/contracts.php b/client/contracts.php index b4637cb..08c1d1f 100644 --- a/client/contracts.php +++ b/client/contracts.php @@ -3,7 +3,27 @@ define('WDS_SYSTEM', true); require_once __DIR__ . '/../includes/portal-helpers.php'; -portalRequireClient(); +portalRequireLogin(); +if (portalGetRole() !== 'client') { + header('Location: /dashboard.php'); + exit; +} + +$user = portalGetUser(); +$username = (string)($user['username'] ?? ''); +$requests = array_values(array_filter(portalLoadProjectRequests(), function ($r) use ($username) { + return (string)($r['client_username'] ?? '') === $username; +})); +$requestIds = array_map(function ($r) { return portalGetRequestDisplayId((array)$r); }, $requests); + +$agreements = array_values(array_filter(portalLoadProjectAgreements(), function ($a) use ($requestIds, $username) { + return in_array((string)($a['request_id'] ?? ''), $requestIds, true) + || (string)($a['client_username'] ?? '') === $username; +})); + +usort($agreements, function ($a, $b) { + return strcmp((string)($b['updated_at'] ?? $b['created_at'] ?? ''), (string)($a['updated_at'] ?? $a['created_at'] ?? '')); +}); $current_page = 'client-portal'; $header_class = 'inner-header'; @@ -15,51 +35,41 @@ - My Contracts | Client Portal | Runlevel Systems + My Project Agreements | Client Portal | Runlevel Systems -
-
- โ† Back to Dashboard - -
-

๐Ÿ“‘ My Contracts

- -
-

About Contracts

-

- For smaller jobs, the accepted proposal and Runlevel Systems Terms of Service typically serve as - the project agreement. For larger, commercial, or ongoing work, a written project agreement may - be used. Contracts spell out project scope, payment terms, deliverables, and both parties' - responsibilities in more detail. -

-
- -
-
๐Ÿ”ง
-

Contracts for your projects will appear here when applicable.

-

- Learn more about how contracts work โ†’ -

-
-
+
+ โ† Back to Dashboard +
+

My Project Agreements

+ +

No project agreements available yet.

+ + +
+
+
+
+
+
+
Request ID:
+ View project agreement +
+ + +

TODO: Add online acceptance/signature workflow later.

+
- diff --git a/client/new-request.php b/client/new-request.php index 0cc4d49..8736c72 100644 --- a/client/new-request.php +++ b/client/new-request.php @@ -1,240 +1,3 @@ bin2hex(random_bytes(8)), - 'client_username' => $client['username'], - 'project_title' => $title, - 'request_type' => $type, - 'description' => $description, - 'budget_range' => $budget, - 'timeline' => $timeline, - 'repo_link' => $repoLink, - 'contact_method' => $contactMethod, - 'status' => 'new', - 'created_at' => date('c'), - ]; - if (portalAppendRequest($request)) { - $success = true; - } else { - $error = 'There was an error saving your request. Please try again or contact us directly.'; - } - } - } -} - -$current_page = 'client-portal'; -$header_class = 'inner-header'; -?> - - - - - - - - Submit Request | Client Portal | Runlevel Systems - - - - - - - -
-
- โ† Back to Dashboard - - -
- โœ… Request submitted!
- Thank you. We have received your request and will review it. We'll follow up using the contact method you provided. - All requests are reviewed before a quote or proposal is prepared. -

- View my requests ยท - Back to dashboard -
- - -
-

โž• Submit a New Request

-

- Tell us about your project. This is a request for review โ€” not a final quote. - We will review your request and follow up with an estimate or proposal. -

- - - - -
- - -
- - -
- - -
- -
- - -
- -
- - -
- -
- - -
- -
- - -
- -
- - -
- -
- - -
- -
-
- > - -
-
- - -
-
- - -
-
- - - - - - +header('Location: /estimate.php', true, 302); +exit; diff --git a/client/proposal.php b/client/proposal.php new file mode 100644 index 0000000..e7ab8e9 --- /dev/null +++ b/client/proposal.php @@ -0,0 +1,64 @@ + + +Proposal | Client Portal + + + + +
โ† Back to proposals
+

Proposal not found.

+

Project Proposal

+
Proposal ID
+
Request ID
+
Project Title
+
Status
+
Request Summary
+
Proposed Work
+
Deliverables
+
Estimated Cost
+
Estimated Time
+
Payment Required To Begin
+
Next Steps
+ +
+ + + diff --git a/client/proposals.php b/client/proposals.php index 7975d6e..6ff7a7a 100644 --- a/client/proposals.php +++ b/client/proposals.php @@ -3,7 +3,27 @@ define('WDS_SYSTEM', true); require_once __DIR__ . '/../includes/portal-helpers.php'; -portalRequireClient(); +portalRequireLogin(); +if (portalGetRole() !== 'client') { + header('Location: /dashboard.php'); + exit; +} + +$user = portalGetUser(); +$username = (string)($user['username'] ?? ''); +$requests = array_values(array_filter(portalLoadProjectRequests(), function ($r) use ($username) { + return (string)($r['client_username'] ?? '') === $username; +})); +$requestIds = array_map(function ($r) { return portalGetRequestDisplayId((array)$r); }, $requests); + +$proposals = array_values(array_filter(portalLoadProposals(), function ($p) use ($requestIds, $username) { + return in_array((string)($p['request_id'] ?? ''), $requestIds, true) + || (string)($p['client_username'] ?? '') === $username; +})); + +usort($proposals, function ($a, $b) { + return strcmp((string)($b['updated_at'] ?? $b['created_at'] ?? ''), (string)($a['updated_at'] ?? $a['created_at'] ?? '')); +}); $current_page = 'client-portal'; $header_class = 'inner-header'; @@ -18,52 +38,37 @@ My Proposals | Client Portal | Runlevel Systems -
-
- โ† Back to Dashboard - -
-

๐Ÿ“„ My Proposals

- -
-

What is a Proposal?

-

- After reviewing your request, Runlevel Systems will prepare a written proposal. A proposal explains - what we plan to do, estimated cost, timeline, deliverables, and payment required to begin. - You will need to approve the proposal before any work starts. -

-
- -
-
๐Ÿ”ง
-

Proposals will appear here once they have been prepared for your requests.

-

- If you have submitted a request and haven't heard back yet, - please allow time for review. You can also - contact us if you have questions. -

-

- Learn more about how proposals work โ†’ -

-
-
+
+ โ† Back to Dashboard +
+

My Proposals

+ +

No proposals available yet.

+ + +
+
+
+
+
+
+
Request ID:
+ View proposal +
+ +
+
- diff --git a/client/request.php b/client/request.php new file mode 100644 index 0000000..d4a1a44 --- /dev/null +++ b/client/request.php @@ -0,0 +1,69 @@ + + + + + + + Request Detail | Client Portal + + + + + + +
โ† Back to requests
+ +

Request not found.

+ +

Project Request Detail

+
+
Request ID
+
Status
+
Project Type
+
Project Stage
+
Project Size
+
Timeline
+
Budget Comfort
+
Preferred Contact
+
+
Description
+ +
+ + + diff --git a/client/requests.php b/client/requests.php index 45fbd96..77715af 100644 --- a/client/requests.php +++ b/client/requests.php @@ -3,29 +3,24 @@ define('WDS_SYSTEM', true); require_once __DIR__ . '/../includes/portal-helpers.php'; -portalRequireClient(); -$client = portalGetClientUser(); +portalRequireLogin(); +if (portalGetRole() !== 'client') { + header('Location: /dashboard.php'); + exit; +} -$allRequests = portalLoadRequests(); -$myRequests = array_values(array_filter($allRequests, function($r) use ($client) { - return ($r['client_username'] ?? '') === ($client['username'] ?? ''); +$user = portalGetUser(); +$username = (string)($user['username'] ?? ''); +$requests = array_values(array_filter(portalLoadProjectRequests(), function ($r) use ($username) { + return (string)($r['client_username'] ?? '') === $username; })); +$proposals = portalLoadProposals(); +$agreements = portalLoadProjectAgreements(); -// Newest first -usort($myRequests, function($a, $b) { - return strcmp($b['created_at'] ?? '', $a['created_at'] ?? ''); +usort($requests, function ($a, $b) { + return strcmp((string)($b['created_at'] ?? ''), (string)($a['created_at'] ?? '')); }); -$statusLabels = [ - 'new' => ['label' => 'New', 'color' => '#36f3ff'], - 'reviewing' => ['label' => 'Reviewing', 'color' => '#ffc600'], - 'quoted' => ['label' => 'Quoted', 'color' => '#a78bfa'], - 'approved' => ['label' => 'Approved', 'color' => '#22c55e'], - 'in_progress'=> ['label' => 'In Progress', 'color' => '#0a84ff'], - 'completed' => ['label' => 'Completed', 'color' => '#6ee7b7'], - 'closed' => ['label' => 'Closed', 'color' => '#5a7a9e'], -]; - $current_page = 'client-portal'; $header_class = 'inner-header'; ?> @@ -36,93 +31,62 @@ - My Requests | Client Portal | Runlevel Systems + My Project Requests | Client Portal | Runlevel Systems -
-
- โ† Back to Dashboard - -
-
-

๐Ÿ“ฅ My Requests

- + New Request -
- - -
-
๐Ÿ“ญ
-

You haven't submitted any requests yet.

- Submit Your First Request -
- -

- request submitted -

+
+ โ† Back to Dashboard +
+
+

My Project Requests

+ Submit New Project Request +
- ucfirst($statusKey), 'color' => '#5a7a9e']; - $createdAt = $req['created_at'] ?? ''; - $dateStr = $createdAt ? date('M j, Y', strtotime($createdAt)) : ''; + +

No project requests yet.

+ + +
-
+
-
-
-
-
- +
+
+
-
- -
-
- - ๐Ÿ“… - - - ๐Ÿ’ฐ - - - โฑ - - - ๐Ÿ”— Link - +
Submitted
+
- - -
+ +
+
- diff --git a/contracts.php b/contracts.php index cb7663a..a3d89a1 100644 --- a/contracts.php +++ b/contracts.php @@ -10,139 +10,32 @@ Project Agreements | Runlevel Systems - + - -
-
- -
-

๐Ÿ“‘ Project Agreements

-

- We believe agreements should be clear and fair. Here's how Runlevel Systems handles project agreements - โ€” from simple jobs to larger commercial projects. -

-
- -
-

Small Jobs Keep It Simple

-

- For most small jobs, quick fixes, and starter website work, you don't need a lengthy contract. - Your accepted project proposal and the - Runlevel Systems Terms of Service - together form the project agreement. -

-

- This covers how the work is done, what's delivered, what happens if changes are needed, - and how disputes are handled โ€” without needing a separate document. -

-
- -
-

When Is a Written Agreement Used?

-

For larger, commercial, or ongoing projects, Runlevel Systems may use a written project agreement. This is more common when:

-
-
-

๐Ÿข Commercial Projects

-

Business applications, training simulation, infrastructure platforms, backend systems, or branded products.

-
-
-

๐Ÿ” Ongoing Work

-

Long-term partnerships, managed development, recurring support, or multi-phase projects.

-
-
-

๐Ÿ“ฑ Full Applications

-

Mobile apps, full-featured web applications, or complex multi-platform projects.

-
-
-

๐Ÿ’ผ Larger Budgets

-

Projects involving significant custom development, milestone payments, or extended timelines.

-
-
-
- -
-

What a Written Agreement Covers

-
    -
  • Parties involved
  • -
  • Project description and scope of work
  • -
  • Deliverables โ€” what you'll actually receive
  • -
  • Payment terms, deposits, and milestones
  • -
  • Your responsibilities as the client
  • -
  • Third-party tools, licenses, or assets used
  • -
  • Who owns the source code and final work
  • -
  • How change requests are handled
  • -
  • Testing, acceptance, and revisions
  • -
  • Refunds, disputes, and how they're resolved
  • -
  • Confidentiality
  • -
  • Termination conditions
  • -
-

- Agreements are provided in plain, readable language. We're not trying to trick anyone โ€” we just want - both sides to be clear about what's expected. -

-
- -
-

Source Code & Ownership

-

- How source code is handled depends on the project type. For most custom development work, - you'll receive the deliverables as agreed in the proposal. For managed or ongoing development, - Runlevel Systems retains management rights while delivering the agreed product. -

-

- Full source code transfer options are available and will be stated clearly in the project proposal - or agreement. If this matters to you โ€” and it often should โ€” ask about it early. -

-
- -
-

Plain English, Always

-

- We don't want agreements to be scary. Contracts are tools for clarity โ€” not weapons. - If you have questions about any part of an agreement, just ask. - We'll explain it without the legalese. -

-

- Note: For legally binding commercial agreements, we recommend having an attorney review any - project contract before signing. -

-
- - - -
-
- +
+

Project Agreements

+

Small jobs may only need an accepted proposal and the Terms of Service.

+

Larger or more detailed work may use a written Project Agreement.

+

This agreement helps both sides understand scope, deliverables, cost, timeline, ownership, and responsibilities.

+

Why this exists

+

We keep the public explanation short and clear. Detailed legal text is handled in staff workflow documents when needed for a specific project.

+ +
diff --git a/dashboard.php b/dashboard.php index 22b6fd2..b5c08e5 100644 --- a/dashboard.php +++ b/dashboard.php @@ -1,32 +1,42 @@ Dashboard | Runlevel Systems @@ -119,206 +82,96 @@
-
-

๐Ÿ–ฅ๏ธ Dashboard

-
- Signed in as - -
+

Dashboard

+
Signed in as
Sign Out
- - -

Admin Overview

- -

Quick Templates

- - - -

Staff Overview

- - -

My Dashboard

-
-

My Estimate Requests

- +
+

My Project Requests

+
- - - - + + + + + + - - + + - + + - - - - + + + + + +
Estimate IDProject TypeStatusSubmitted DateRequest IDProject TypeStatusSubmitted DateLinked ProposalLinked Project Agreement
No estimate requests yet.
No project requests yet.
+ + + โ€” + + + + โ€” +
+

TODO: Add online acceptance/signature workflow later.

- -
-

- Dashboard Status: - Proposals, contracts, and additional portal features are being expanded. - - Estimate and project requests submitted through the public form appear in the queues above. - - Submitted requests are reviewed by our team and we will follow up using the contact method you provided. - -

-
-
diff --git a/data/project_agreements.json b/data/project_agreements.json new file mode 100644 index 0000000..a11b9db --- /dev/null +++ b/data/project_agreements.json @@ -0,0 +1,3 @@ +{ + "agreements": [] +} diff --git a/data/project_requests.json b/data/project_requests.json new file mode 100644 index 0000000..1ca2671 --- /dev/null +++ b/data/project_requests.json @@ -0,0 +1,3 @@ +{ + "requests": [] +} diff --git a/data/proposals.json b/data/proposals.json new file mode 100644 index 0000000..f86134a --- /dev/null +++ b/data/proposals.json @@ -0,0 +1,3 @@ +{ + "proposals": [] +} diff --git a/estimate.php b/estimate.php index 0d41886..41c6236 100644 --- a/estimate.php +++ b/estimate.php @@ -14,12 +14,15 @@ 'Training / Simulation', 'Game / Interactive project', 'Server / Mod / Script', - 'Existing project fix', 'Infrastructure / Backend', - 'Not sure', + 'Existing project fix', + 'Other', ]; +$projectStages = ['Idea', 'Planning', 'In progress', 'Needs rescue', 'Maintenance', 'Not sure']; +$projectSizes = ['Small task', 'Small project', 'Medium project', 'Large project', 'Not sure']; +$timelineOptions = ['ASAP', '1-2 weeks', '2-4 weeks', '1-3 months', 'Flexible']; +$budgetComfortOptions = ['Under $250', '$250-$1,000', '$1,000-$5,000', '$5,000+', 'Not sure yet']; $contactOptions = ['Email', 'Phone', 'Google Meet', 'Discord', 'Not sure']; -$gameTypes = ['Game / Interactive project', 'Server / Mod / Script']; $error = ''; $success = false; @@ -39,6 +42,10 @@ 'discord_username' => (string)($fullUser['discord_username'] ?? ''), 'contact_method' => 'Email', 'project_type' => '', + 'project_stage' => '', + 'project_size' => '', + 'timeline' => '', + 'budget_comfort' => '', 'repo_link' => '', 'description' => '', 'desired_username' => '', @@ -51,29 +58,39 @@ } } -if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['submit_estimate'])) { +if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['submit_request'])) { $name = $form['name']; $email = $form['email']; $phone = $form['phone']; $discordUsername = $form['discord_username']; $contactMethod = $form['contact_method']; $projectType = $form['project_type']; + $projectStage = $form['project_stage']; + $projectSize = $form['project_size']; + $timeline = $form['timeline']; + $budgetComfort = $form['budget_comfort']; $repoLink = $form['repo_link']; $description = $form['description']; $desiredUsername = strtolower($form['desired_username']); if ($name === '') { $error = 'Please enter your name.'; - } elseif ($email === '') { - $error = 'Email is required.'; - } elseif (!filter_var($email, FILTER_VALIDATE_EMAIL)) { + } elseif ($email === '' || !filter_var($email, FILTER_VALIDATE_EMAIL)) { $error = 'Please enter a valid email address.'; } elseif (!in_array($projectType, $projectTypes, true)) { $error = 'Please select a project type.'; - } elseif ($description === '') { - $error = 'Please describe your request.'; + } elseif (!in_array($projectStage, $projectStages, true)) { + $error = 'Please select a project stage.'; + } elseif (!in_array($projectSize, $projectSizes, true)) { + $error = 'Please select a project size.'; + } elseif (!in_array($timeline, $timelineOptions, true)) { + $error = 'Please select a timeline.'; + } elseif (!in_array($budgetComfort, $budgetComfortOptions, true)) { + $error = 'Please select your budget comfort.'; } elseif (!in_array($contactMethod, $contactOptions, true)) { $error = 'Please select a preferred contact method.'; + } elseif ($description === '') { + $error = 'Please describe your request.'; } elseif ($repoLink !== '' && !filter_var($repoLink, FILTER_VALIDATE_URL)) { $error = 'The repository or project link does not appear to be a valid URL.'; } elseif (!$isLoggedIn && $desiredUsername === '') { @@ -106,10 +123,11 @@ } if ($error === '') { - $estimateId = portalGenerateUniqueEstimateId(); + $requestId = portalGenerateUniqueRequestId(); $request = [ 'id' => bin2hex(random_bytes(8)), - 'estimate_id' => $estimateId, + 'request_id' => $requestId, + 'estimate_id' => $requestId, 'client_username' => $clientUsername, 'name' => $name, 'email' => $email, @@ -117,37 +135,53 @@ 'discord_username' => $discordUsername, 'contact_method' => $contactMethod, 'project_type' => $projectType, + 'project_stage' => $projectStage, + 'project_size' => $projectSize, + 'timeline' => $timeline, + 'budget_comfort' => $budgetComfort, 'repo_link' => $repoLink, 'description' => $description, 'status' => 'new', - 'notes' => '', + 'estimated_cost_range' => '', + 'estimated_time_range' => '', + 'staff_summary' => '', + 'recommended_next_step' => '', + 'internal_notes' => '', + 'proposal_ids' => [], + 'agreement_ids' => [], 'created_at' => date('c'), ]; - if (!portalAppendEstimateRequest($request)) { + if (!portalAppendProjectRequest($request)) { $error = 'There was an error saving your request. Please try again or contact us directly.'; } else { $success = true; $submitted = [ 'request' => $request, 'created_account' => $createdAccount, - 'needs_verification' => !$isLoggedIn, - 'show_discord' => in_array($projectType, $gameTypes, true), ]; + $ackSubject = 'Project Request Received - ' . $requestId; + $ackBody = "Hello {$name},\n\n" + . "Project Request Received\n\n" + . "Request ID:\n{$requestId}\n\n" + . "Please save this ID. You can reference it if you contact us by email, phone, Google Meet, or Discord.\n\n" + . "Runlevel Systems\n" + . "DESIGN โ€ข DEBUG โ€ข DEPLOY\n"; + send_email($email, $ackSubject, $ackBody); + if ($createdAccount !== null) { $verifyLink = 'https://' . ($_SERVER['HTTP_HOST'] ?? 'runlevel.systems') . '/verify-email.php?token=' . urlencode((string)$createdAccount['verification_token']); - $subject = 'Verify your Runlevel Systems account'; + $subject = 'Verify your Runlevel Systems account - ' . $requestId; $body = "Hello {$name},\n\n" - . "We received your project estimate request.\n\n" - . "Estimate ID:\n{$estimateId}\n\n" + . "We received your project request.\n\n" + . "Request ID:\n{$requestId}\n\n" . "A client dashboard account was created for you:\n\n" . "Username:\n{$clientUsername}\n\n" . "Temporary password:\n{$createdAccount['temporary_password']}\n\n" . "Please verify your email before logging in:\n\n" . "{$verifyLink}\n\n" - . "After verification, you can log in to view your dashboard and track project information.\n\n" . "Runlevel Systems\n" . "DESIGN โ€ข DEBUG โ€ข DEPLOY\n"; send_email($email, $subject, $body); @@ -155,8 +189,6 @@ } } } - -$showDiscordHint = in_array($form['project_type'], $gameTypes, true) || $form['contact_method'] === 'Discord'; ?> @@ -165,36 +197,31 @@ - Project Requirements & Estimate | Runlevel Systems + Project Request | Runlevel Systems @@ -205,8 +232,8 @@
-

Project Requirements & Estimate

-

Please complete and submit this estimate request so we have the information needed to review your project. After submission, we will give you an Estimate ID that you can reference when contacting us.

+

Project Request

+

Tell us what you need. We will review it and respond with the next step.

@@ -215,12 +242,10 @@
-

โœ… Request Received

-

We received your estimate request.

-
-
Your Estimate ID:
-
-
+

Project Request Received

+

Your Request ID:

+
+

Please save this ID. You can reference it if you contact us by email, phone, Google Meet, or Discord.

Your client account:

@@ -229,23 +254,11 @@

Please check your email to verify your account before logging in.

Temporary password:

-

Please save this. You can change it later once account settings are added.

-

TODO: Add password reset flow and remove temporary-password delivery.

- - -

We will review your request and contact you using the information provided.

- - -
- Game or server project? You can also join our Discord and mention your Estimate ID. - - Join Discord -
@@ -256,13 +269,12 @@
- +

Account Information

-

We will create a free client dashboard account so you can track this request, view estimates, and communicate about the project.

-

Already have an account? Log in and continue estimate

+

We create a client dashboard account so you can track your project request, proposal, and agreement.

@@ -284,53 +296,90 @@
- +
- - + +
- -
- - -
- -
- Game or server project? You can also join our Discord and mention your Estimate ID. - - Join Discord +
+ +

Project Details

-
- - +
+
+ + +
+
+ + +
-
- - +
+
+ + +
+
+ + +
+
+
+
+ + +
+
+ + +
- +
+

This form starts the process. We review your request and may respond with questions, a rough estimate, a proposal, or a project agreement depending on the size and type of work.

- + @@ -340,31 +389,5 @@ - diff --git a/includes/email.php b/includes/email.php index 5a4f829..3eab597 100644 --- a/includes/email.php +++ b/includes/email.php @@ -19,3 +19,27 @@ function send_email($to, $subject, $body) { return @mail($to, (string)$subject, (string)$body, $headers); } + +function send_project_proposal_email($to, $name, $requestId, $link) { + $subject = 'Runlevel Systems Proposal - ' . $requestId; + $body = "Hello {$name},\n\n" + . "Your proposal is ready for review.\n\n" + . "Request ID:\n{$requestId}\n\n" + . "Please review the linked document:\n{$link}\n\n" + . "Runlevel Systems\n" + . "DESIGN โ€ข DEBUG โ€ข DEPLOY\n\n" + . "TODO: Add PDF attachment generation later.\n"; + return send_email($to, $subject, $body); +} + +function send_project_agreement_email($to, $name, $requestId, $link) { + $subject = 'Runlevel Systems Project Agreement - ' . $requestId; + $body = "Hello {$name},\n\n" + . "Your project agreement is ready for review.\n\n" + . "Request ID:\n{$requestId}\n\n" + . "Please review the linked document:\n{$link}\n\n" + . "Runlevel Systems\n" + . "DESIGN โ€ข DEBUG โ€ข DEPLOY\n\n" + . "TODO: Add PDF attachment generation later.\n"; + return send_email($to, $subject, $body); +} diff --git a/includes/portal-helpers.php b/includes/portal-helpers.php index e4e1ba5..3fab90d 100644 --- a/includes/portal-helpers.php +++ b/includes/portal-helpers.php @@ -45,11 +45,13 @@ function portalGenerateTemporaryPassword($length = 12) { return $out; } -function portalGenerateUniqueEstimateId() { - $requests = portalLoadEstimateRequests(); +function portalGenerateUniqueRequestId() { + $requests = portalLoadProjectRequests(); $existing = []; foreach ($requests as $r) { - if (!empty($r['estimate_id'])) { + if (!empty($r['request_id'])) { + $existing[(string)$r['request_id']] = true; + } elseif (!empty($r['estimate_id'])) { $existing[(string)$r['estimate_id']] = true; } } @@ -62,7 +64,15 @@ function portalGenerateUniqueEstimateId() { return $estimateId; } -function portalGetEstimateDisplayId(array $request) { +function portalGenerateUniqueEstimateId() { + return portalGenerateUniqueRequestId(); +} + +function portalGetRequestDisplayId(array $request) { + $requestId = trim((string)($request['request_id'] ?? '')); + if ($requestId !== '') { + return $requestId; + } $estimateId = trim((string)($request['estimate_id'] ?? '')); if ($estimateId !== '') { return $estimateId; @@ -70,6 +80,10 @@ function portalGetEstimateDisplayId(array $request) { return 'Legacy Request'; } +function portalGetEstimateDisplayId(array $request) { + return portalGetRequestDisplayId($request); +} + /** * Create a new client-style user entry in users.json for estimate submissions. * @@ -208,7 +222,10 @@ function portalRefreshVerificationTokenByEmail($email, &$userOut = null) { define('PORTAL_CLIENTS_FILE', PORTAL_DATA_DIR . '/clients.json'); define('PORTAL_REQUESTS_FILE', PORTAL_DATA_DIR . '/requests.json'); define('PORTAL_COMMERCIAL_FILE', PORTAL_DATA_DIR . '/commercial_requests.json'); -define('PORTAL_ESTIMATES_FILE', PORTAL_DATA_DIR . '/estimate_requests.json'); +define('PORTAL_PROJECT_REQUESTS_FILE', PORTAL_DATA_DIR . '/project_requests.json'); +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'); // Session keys define('PORTAL_STAFF_SESSION', 'rls_portal_staff'); @@ -598,15 +615,11 @@ function portalClientLogout() { // ------------------------------------------------------------------ function portalLoadRequests() { - $data = portalLoadJson(PORTAL_REQUESTS_FILE); - return isset($data['requests']) && is_array($data['requests']) ? $data['requests'] : []; + return portalLoadProjectRequests(); } function portalAppendRequest(array $request) { - $data = portalLoadJson(PORTAL_REQUESTS_FILE); - $requests = isset($data['requests']) && is_array($data['requests']) ? $data['requests'] : []; - $requests[] = $request; - return portalSaveJson(PORTAL_REQUESTS_FILE, ['requests' => $requests]); + return portalAppendProjectRequest($request); } function portalLoadCommercialRequests() { @@ -622,17 +635,105 @@ function portalAppendCommercialRequest(array $request) { } function portalLoadEstimateRequests() { - $data = portalLoadJson(PORTAL_ESTIMATES_FILE); - return isset($data['requests']) && is_array($data['requests']) ? $data['requests'] : []; + return portalLoadProjectRequests(); } function portalAppendEstimateRequest(array $request) { - $data = portalLoadJson(PORTAL_ESTIMATES_FILE); - $requests = isset($data['requests']) && is_array($data['requests']) ? $data['requests'] : []; - $requests[] = $request; - return portalSaveJson(PORTAL_ESTIMATES_FILE, ['requests' => $requests]); + return portalAppendProjectRequest($request); } function portalSaveEstimateRequests(array $requests) { - return portalSaveJson(PORTAL_ESTIMATES_FILE, ['requests' => array_values($requests)]); + return portalSaveProjectRequests($requests); +} + +function portalNormalizeProjectRequest(array $request) { + if (!isset($request['request_id']) || trim((string)$request['request_id']) === '') { + $legacy = trim((string)($request['estimate_id'] ?? '')); + if ($legacy !== '') { + $request['request_id'] = $legacy; + } else { + $seed = (string)($request['id'] ?? '') . '|' . (string)($request['created_at'] ?? ''); + $hash = str_pad((string)((abs(crc32($seed)) % 9000) + 1000), 4, '0', STR_PAD_LEFT); + $request['request_id'] = 'RLS-' . date('Ymd', strtotime((string)($request['created_at'] ?? 'now'))) . '-' . $hash; + } + } + if (!isset($request['estimate_id']) || trim((string)$request['estimate_id']) === '') { + $request['estimate_id'] = $request['request_id']; + } + $status = trim((string)($request['status'] ?? 'new')); + $statusMap = [ + 'reviewed' => 'reviewing', + 'proposal_needed' => 'proposal_drafted', + 'quoted' => 'proposal_drafted', + 'approved' => 'accepted', + ]; + $request['status'] = isset($statusMap[$status]) ? $statusMap[$status] : $status; + $request['timeline'] = (string)($request['timeline'] ?? ($request['desired_timeline'] ?? '')); + $request['budget_comfort'] = (string)($request['budget_comfort'] ?? ($request['budget_range'] ?? '')); + $request['project_stage'] = (string)($request['project_stage'] ?? ''); + $request['project_size'] = (string)($request['project_size'] ?? ''); + $request['estimated_cost_range'] = (string)($request['estimated_cost_range'] ?? ''); + $request['estimated_time_range'] = (string)($request['estimated_time_range'] ?? ''); + $request['staff_summary'] = (string)($request['staff_summary'] ?? ''); + $request['recommended_next_step'] = (string)($request['recommended_next_step'] ?? ''); + $request['internal_notes'] = (string)($request['internal_notes'] ?? ($request['notes'] ?? '')); + $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']) === '') { + $request['created_at'] = date('c'); + } + return $request; +} + +function portalLoadProjectRequests() { + $projectData = portalLoadJson(PORTAL_PROJECT_REQUESTS_FILE); + $projectRequests = isset($projectData['requests']) && is_array($projectData['requests']) ? $projectData['requests'] : []; + + $legacyData = portalLoadJson(PORTAL_ESTIMATES_FILE); + $legacyRequests = isset($legacyData['requests']) && is_array($legacyData['requests']) ? $legacyData['requests'] : []; + + $seen = []; + $all = []; + foreach (array_merge($projectRequests, $legacyRequests) as $row) { + $normalized = portalNormalizeProjectRequest((array)$row); + $key = (string)($normalized['id'] ?? '') . '|' . (string)($normalized['request_id'] ?? ''); + if (isset($seen[$key])) { + continue; + } + $seen[$key] = true; + $all[] = $normalized; + } + return $all; +} + +function portalSaveProjectRequests(array $requests) { + $normalized = []; + foreach ($requests as $request) { + $normalized[] = portalNormalizeProjectRequest((array)$request); + } + return portalSaveJson(PORTAL_PROJECT_REQUESTS_FILE, ['requests' => array_values($normalized)]); +} + +function portalAppendProjectRequest(array $request) { + $requests = portalLoadProjectRequests(); + $requests[] = portalNormalizeProjectRequest($request); + return portalSaveProjectRequests($requests); +} + +function portalLoadProposals() { + $data = portalLoadJson(PORTAL_PROPOSALS_FILE); + return isset($data['proposals']) && is_array($data['proposals']) ? $data['proposals'] : []; +} + +function portalSaveProposals(array $proposals) { + return portalSaveJson(PORTAL_PROPOSALS_FILE, ['proposals' => array_values($proposals)]); +} + +function portalLoadProjectAgreements() { + $data = portalLoadJson(PORTAL_PROJECT_AGREEMENTS_FILE); + return isset($data['agreements']) && is_array($data['agreements']) ? $data['agreements'] : []; +} + +function portalSaveProjectAgreements(array $agreements) { + return portalSaveJson(PORTAL_PROJECT_AGREEMENTS_FILE, ['agreements' => array_values($agreements)]); } diff --git a/payments.php b/payments.php index 0ed8e3a..31165fa 100644 --- a/payments.php +++ b/payments.php @@ -10,67 +10,26 @@ Payments | Runlevel Systems - + - -
-
-

Payments

-

Simple, transparent payment process โ€” you always know what you are paying for before you pay.

-
-
- -
-
-

- Runlevel Systems normally reviews your request before asking for payment. -

-

- For small tasks, payment may be requested before work begins. - For larger projects, we may use deposits, milestones, or written proposals. -

-

- Payments are usually handled through PayPal invoice or payment link. - You will always know what the payment is for before you pay. -

- -

Simple Payment Flow

-
    -
  1. 1 Submit your request or estimate form.
  2. -
  3. 2 Review the estimate or proposal we prepare.
  4. -
  5. 3 Approve the work and confirm the terms.
  6. -
  7. 4 Pay through PayPal invoice or approved payment link.
  8. -
  9. 5 Work begins after payment is confirmed.
  10. -
  11. 6 Review completed work against the agreed deliverables.
  12. -
- -
- Payment allows work to begin. Final acceptance happens after the agreed work is delivered - and reviewed according to the proposal or terms. -
- - Start With An Estimate -
-
- +
+

Payments

+

Payments are normally requested only after we review your project request and confirm the next step.

+

For small jobs, we may request payment before work begins.

+

For larger projects, we may use deposits, milestones, or written project agreements.

+

Payments are handled through PayPal invoices, PayPal payment links, or another approved method.

+Start A Project Request +
diff --git a/proposals.php b/proposals.php index abcfed4..a14e214 100644 --- a/proposals.php +++ b/proposals.php @@ -10,78 +10,24 @@ Proposals | Runlevel Systems - + - -
-
-

Proposals

-

Clear, written agreements before work begins.

-
-
- -
-
-

- For many projects, Runlevel Systems will provide a short proposal before work begins. - A proposal explains what we understand, what we plan to do, the expected deliverables, - estimated cost, and next steps. -

-

- Small tasks may be approved informally. Larger projects, business applications, and - multi-phase work will typically include a written proposal for clarity on both sides. -

- -

What A Proposal Includes

-
-
-
๐Ÿ“‹ What We Will Do
-

A clear description of the work we plan to complete.

-
-
-
๐Ÿ“ฆ What You Will Receive
-

The specific deliverables included at the end of the project.

-
-
-
๐Ÿ’ฐ Estimated Cost
-

The agreed price based on project scope and requirements.

-
-
-
๐Ÿ“… Timeline
-

Estimated timeframe and any milestones or phases.

-
-
-
๐Ÿ’ณ Payment Needed To Begin
-

Deposit or initial payment required before work starts.

-
-
-
โ“ Questions Or Assumptions
-

Any open items, assumptions, or clarifications noted up front.

-
-
- - Start With An Estimate -
-
- +
+

Proposals

+

A proposal is a written plan for the work.

+

It includes what we understand, what we plan to do, estimated cost, estimated time, and next steps.

+Start A Project Request +
diff --git a/staff/contract-template.php b/staff/contract-template.php index a2d1edf..1a91875 100644 --- a/staff/contract-template.php +++ b/staff/contract-template.php @@ -1,217 +1,3 @@ - - - - - - - - Contract Template | Staff Portal | Runlevel Systems - - - - - - - -
-
- โ† Back to Dashboard - -

๐Ÿ“‹ Project Agreement Template

- - - -
-
-

Runlevel Systems

-

Project Agreement

-

Effective Date: [Date]

-
- -
-

1. Parties

-

This agreement is between Runlevel Systems ("Developer") and - [Client full name or organization] ("Client").

-
- -
-

2. Project Description

-

[Describe the project in plain language]

-
- -
-

3. Scope of Work

-

The Developer will provide the following services:

-

[List specific tasks, features, or deliverables]

-

Work not listed above is considered out of scope and may be quoted separately.

-
- -
-

4. Deliverables

-

[List specific deliverables: files, code, website, app, etc.]

-
- -
-

5. Payment Terms

-

Total project cost: $[Amount] USD

-

Payment schedule: [e.g., 50% deposit to begin / 50% on delivery]

-

Payment method: PayPal invoice or approved payment link.

-

Work begins after required deposit or payment is received.

-
- -
-

6. Deposits and Milestones

-

The following deposit or milestone payments apply:

-

[List milestone payments if applicable, or write "Full payment before work begins" for small jobs]

-
- -
-

7. Customer Responsibilities

-

The Client agrees to:

-
    -
  • Provide accurate project requirements in a timely manner.
  • -
  • Respond to questions or requests for review within a reasonable time.
  • -
  • Provide access to any existing systems, accounts, or assets required for the project.
  • -
  • Review deliverables and communicate feedback during the revision period.
  • -
-
- -
-

8. Third-Party Licenses

-

The Client is responsible for obtaining any required licenses, assets, fonts, stock media, or third-party software required for the project. - Runlevel Systems will not include unlicensed third-party content in deliverables.

-
- -
-

9. Source Code and Ownership

-

Upon final payment, the Client receives ownership of the custom code written specifically for this project, - unless otherwise agreed in writing. Standard libraries, frameworks, and third-party components remain under their - respective licenses.

-
- -
-

10. Managed Development Option

-

For ongoing support or managed development arrangements, the Developer may retain access to the project - for maintenance purposes. Terms of managed access are agreed separately.

-
- -
-

11. Full Source Transfer Option

-

If the Client requests full source transfer and access, this must be agreed upon in writing before work begins. - Full source transfer may affect pricing.

-
- -
-

12. Change Requests

-

Changes to the scope of work after the project has started will be evaluated and quoted separately. - The Client will be informed of any cost or timeline impact before changes are made.

-
- -
-

13. Revisions

-

The project includes [number] rounds of revisions. - Additional revision rounds will be billed at the Developer's standard rate. - Revisions are limited to the agreed scope; new features are treated as change requests.

-
- -
-

14. Testing and Acceptance

-

The Client will review and test deliverables within [e.g., 7 business days] - of delivery. If no feedback is received within this period, the deliverable is considered accepted. - Acceptance does not waive the right to report bugs related to the original scope.

-
- -
-

15. Refunds and Disputes

-

Refunds, revisions, and disputes are handled according to the - Runlevel Systems Terms of Service - and the terms of this agreement. Deposit payments are non-refundable once work has begun, - unless the Developer fails to deliver the agreed scope.

-
- -
-

16. Confidentiality

-

Both parties agree to keep project-specific information, credentials, and business details confidential - and not share them with third parties without written consent.

-
- -
-

17. Limitation of Liability

-

Runlevel Systems is not liable for damages exceeding the total amount paid under this agreement. - The Developer is not responsible for issues caused by third-party services, hosting providers, - or changes made by the Client after delivery.

-
- -
-

18. Termination

-

Either party may terminate this agreement with written notice. If the Client terminates after work has begun, - payment for work completed to that point is due. If the Developer terminates for reasons other than non-payment, - a pro-rated refund of unused deposit funds will be issued.

-
- -
-

19. Governing Law

-

This agreement is governed by the laws of [State / Province / Country โ€” to be determined]. - Any disputes will be resolved through good-faith negotiation before formal proceedings.

-
- -
-

20. Signatures

-

- By signing below, both parties agree to the terms of this project agreement. -

-
-
-

Client signature:

-
-

Printed name & date

-
-
-

Runlevel Systems representative:

-
-

Printed name & date

-
-
-
-
- -
-
- - - - - - +header('Location: /staff/create-agreement.php', true, 302); +exit; diff --git a/staff/create-agreement.php b/staff/create-agreement.php new file mode 100644 index 0000000..e873bde --- /dev/null +++ b/staff/create-agreement.php @@ -0,0 +1,267 @@ + $agreement['agreement_id'] ?? '', + 'proposal_id' => $agreement['proposal_id'] ?? ($proposal['proposal_id'] ?? ''), + 'request_id' => $agreement['request_id'] ?? $requestIdQuery, + 'client_name' => $agreement['client_name'] ?? ($proposal['client_name'] ?? ($request['name'] ?? '')), + 'client_email' => $agreement['client_email'] ?? ($proposal['client_email'] ?? ($request['email'] ?? '')), + 'client_username' => $agreement['client_username'] ?? ($proposal['client_username'] ?? ($request['client_username'] ?? '')), + 'project_title' => $agreement['project_title'] ?? ($proposal['project_title'] ?? ($request['project_type'] ?? '')), + 'effective_date' => $agreement['effective_date'] ?? date('Y-m-d'), + 'scope_of_work' => $agreement['scope_of_work'] ?? ($proposal['proposed_work'] ?? ''), + 'deliverables' => $agreement['deliverables'] ?? ($proposal['deliverables'] ?? ''), + 'timeline' => $agreement['timeline'] ?? ($proposal['estimated_time'] ?? ($request['timeline'] ?? '')), + 'payment_terms' => $agreement['payment_terms'] ?? ($proposal['payment_required_to_begin'] ?? ''), + 'revision_terms' => $agreement['revision_terms'] ?? ($proposal['revision_terms'] ?? ''), + 'change_request_policy' => $agreement['change_request_policy'] ?? 'Changes outside scope require approval and may affect timeline and cost.', + 'customer_responsibilities' => $agreement['customer_responsibilities'] ?? ($proposal['customer_responsibilities'] ?? ''), + 'third_party_licenses' => $agreement['third_party_licenses'] ?? 'Client supplies or approves all required third-party licenses and assets.', + 'source_code_ownership_terms' => $agreement['source_code_ownership_terms'] ?? 'Ownership terms follow approved proposal and Runlevel Systems Terms of Service.', + 'testing_and_acceptance' => $agreement['testing_and_acceptance'] ?? 'Client reviews deliverables within agreed review window and confirms acceptance in writing.', + 'termination' => $agreement['termination'] ?? 'Either party may terminate with written notice; completed work remains billable.', + 'legal_notice' => $agreement['legal_notice'] ?? 'This agreement should be reviewed by legal counsel before final signature when required.', + 'signature_placeholder' => $agreement['signature_placeholder'] ?? 'Client Signature: ____________________ Date: __________', + 'status' => $agreement['status'] ?? 'draft', +]; + +$form = $defaults; +if ($_SERVER['REQUEST_METHOD'] === 'POST') { + foreach ($form as $key => $value) { + $form[$key] = trim((string)($_POST[$key] ?? '')); + } + + if ($form['agreement_id'] === '') { + $form['agreement_id'] = generateAgreementId($agreements); + } + + if ($form['request_id'] === '' || $form['client_name'] === '' || $form['client_email'] === '') { + $error = 'Request ID, client name, and client email are required.'; + } else { + $action = trim((string)($_POST['action'] ?? 'save_draft')); + if ($action === 'send_email') { + $form['status'] = 'sent'; + } elseif ($form['status'] === '') { + $form['status'] = 'draft'; + } + + $record = $form; + $record['updated_at'] = date('c'); + if (empty($record['created_at'])) { + $record['created_at'] = date('c'); + } + + $found = false; + foreach ($agreements as &$item) { + if ((string)($item['agreement_id'] ?? '') === $record['agreement_id']) { + $record['created_at'] = (string)($item['created_at'] ?? $record['created_at']); + $item = $record; + $found = true; + break; + } + } + unset($item); + if (!$found) { + $agreements[] = $record; + } + portalSaveProjectAgreements($agreements); + + foreach ($requests as &$req) { + if (portalGetRequestDisplayId((array)$req) !== $record['request_id']) { + continue; + } + $ids = isset($req['agreement_ids']) && is_array($req['agreement_ids']) ? $req['agreement_ids'] : []; + if (!in_array($record['agreement_id'], $ids, true)) { + $ids[] = $record['agreement_id']; + } + $req['agreement_ids'] = array_values($ids); + if ($action === 'send_email') { + $req['status'] = 'accepted'; + } + break; + } + unset($req); + portalSaveProjectRequests($requests); + + if ($action === 'send_email') { + $host = $_SERVER['HTTP_HOST'] ?? 'runlevel.systems'; + $clientLink = 'https://' . $host . '/client/contracts.php?agreement_id=' . urlencode($record['agreement_id']); + send_project_agreement_email($record['client_email'], $record['client_name'], $record['request_id'], $clientLink); + $notice = 'Project agreement saved and sent by email.'; + } else { + $notice = 'Project agreement draft saved.'; + } + + $agreement = $record; + $form = $record; + } +} + +$current_page = 'staff-portal'; +$header_class = 'inner-header'; +?> + + + + + + + + Create Project Agreement | Staff | Runlevel Systems + + + + + + +
+
+ โ† Back To Request + +
+
+

Project Agreement

+ +
+

Runlevel Systems ยท Request ID:

+
+ +
+
+ +
+
+
+
+
+
+
+
+
+
+
+
+
+ +
Scope & Deliverables +
+
+
+ +
Terms +
+
+
+
+
+
+
+
+
+
+ +
+ + + + Back To Proposal + + Back To Request +
+
+
+
+ + + + + diff --git a/staff/create-proposal.php b/staff/create-proposal.php new file mode 100644 index 0000000..1dfd72c --- /dev/null +++ b/staff/create-proposal.php @@ -0,0 +1,241 @@ + 'Draft', 'sent' => 'Sent', 'accepted' => 'Accepted', 'rejected' => 'Rejected', 'expired' => 'Expired']; +$requests = portalLoadProjectRequests(); +$proposals = portalLoadProposals(); + +$requestIdQuery = trim((string)($_GET['request_id'] ?? '')); +$proposalIdQuery = trim((string)($_GET['proposal_id'] ?? '')); +$request = null; +$proposal = null; +$requestRecordId = ''; + +if ($proposalIdQuery !== '') { + foreach ($proposals as $row) { + if ((string)($row['proposal_id'] ?? '') === $proposalIdQuery) { + $proposal = $row; + $requestIdQuery = (string)($row['request_id'] ?? $requestIdQuery); + break; + } + } +} + +if ($requestIdQuery !== '') { + foreach ($requests as $row) { + if (portalGetRequestDisplayId((array)$row) === $requestIdQuery) { + $request = portalNormalizeProjectRequest((array)$row); + $requestRecordId = (string)($row['id'] ?? ''); + break; + } + } +} + +$notice = ''; +$error = ''; + +$defaults = [ + 'proposal_id' => $proposal['proposal_id'] ?? '', + 'request_id' => $requestIdQuery, + 'client_name' => $proposal['client_name'] ?? ($request['name'] ?? ''), + 'client_email' => $proposal['client_email'] ?? ($request['email'] ?? ''), + 'client_username' => $proposal['client_username'] ?? ($request['client_username'] ?? ''), + 'project_title' => $proposal['project_title'] ?? ($request['project_type'] ?? ''), + 'request_summary' => $proposal['request_summary'] ?? ($request['description'] ?? ''), + 'proposed_work' => $proposal['proposed_work'] ?? '', + 'deliverables' => $proposal['deliverables'] ?? '', + 'estimated_cost' => $proposal['estimated_cost'] ?? ($request['estimated_cost_range'] ?? ''), + 'estimated_time' => $proposal['estimated_time'] ?? ($request['estimated_time_range'] ?? ''), + 'payment_required_to_begin' => $proposal['payment_required_to_begin'] ?? '', + 'revision_terms' => $proposal['revision_terms'] ?? '', + 'assumptions' => $proposal['assumptions'] ?? '', + 'customer_responsibilities' => $proposal['customer_responsibilities'] ?? '', + 'next_steps' => $proposal['next_steps'] ?? ($request['recommended_next_step'] ?? ''), + 'status' => $proposal['status'] ?? 'draft', + 'repo_link' => $proposal['repo_link'] ?? ($request['repo_link'] ?? ''), + 'timeline' => $proposal['timeline'] ?? ($request['timeline'] ?? ''), + 'budget_comfort' => $proposal['budget_comfort'] ?? ($request['budget_comfort'] ?? ''), + 'staff_summary' => $proposal['staff_summary'] ?? ($request['staff_summary'] ?? ''), +]; + +$form = $defaults; +if ($_SERVER['REQUEST_METHOD'] === 'POST') { + foreach ($form as $key => $value) { + $form[$key] = trim((string)($_POST[$key] ?? '')); + } + if ($form['proposal_id'] === '') { + $form['proposal_id'] = generateProposalId($proposals); + } + + if ($form['request_id'] === '' || $form['client_name'] === '' || $form['client_email'] === '') { + $error = 'Request ID, client name, and client email are required.'; + } else { + $action = trim((string)($_POST['action'] ?? 'save_draft')); + $form['status'] = ($action === 'send_email') ? 'sent' : ($proposalStatuses[$form['status']] ?? false ? $form['status'] : 'draft'); + + $record = $form; + $record['updated_at'] = date('c'); + if (empty($record['created_at'])) { + $record['created_at'] = date('c'); + } + + $found = false; + foreach ($proposals as &$item) { + if ((string)($item['proposal_id'] ?? '') === $record['proposal_id']) { + $record['created_at'] = (string)($item['created_at'] ?? $record['created_at']); + $item = $record; + $found = true; + break; + } + } + unset($item); + if (!$found) { + $proposals[] = $record; + } + portalSaveProposals($proposals); + + foreach ($requests as &$req) { + if (portalGetRequestDisplayId((array)$req) !== $record['request_id']) { + continue; + } + $ids = isset($req['proposal_ids']) && is_array($req['proposal_ids']) ? $req['proposal_ids'] : []; + if (!in_array($record['proposal_id'], $ids, true)) { + $ids[] = $record['proposal_id']; + } + $req['proposal_ids'] = array_values($ids); + if ($action === 'send_email') { + $req['status'] = 'proposal_sent'; + } elseif (($req['status'] ?? '') === 'new' || ($req['status'] ?? '') === 'reviewing') { + $req['status'] = 'proposal_drafted'; + } + break; + } + unset($req); + portalSaveProjectRequests($requests); + + $host = $_SERVER['HTTP_HOST'] ?? 'runlevel.systems'; + $clientLink = 'https://' . $host . '/client/proposals.php?proposal_id=' . urlencode($record['proposal_id']); + if ($action === 'send_email') { + send_project_proposal_email($record['client_email'], $record['client_name'], $record['request_id'], $clientLink); + $notice = 'Proposal saved and sent by email.'; + } else { + $notice = 'Proposal draft saved.'; + } + + $proposal = $record; + $form = $record; + } +} + +$current_page = 'staff-portal'; +$header_class = 'inner-header'; +?> + + + + + + + + Create Proposal | Staff | Runlevel Systems + + + + + + +
+
+ โ† Back To Request + +
+
+

Project Proposal

+ +
+

Runlevel Systems ยท Request ID:

+
+ +
+
+ +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ +
+
+
+
+
+
+
+
+ +
+ + + + Create Project Agreement + + Back To Request +
+
+
+
+ + + + + diff --git a/staff/estimate-requests.php b/staff/estimate-requests.php index cdac5f7..7c125d9 100644 --- a/staff/estimate-requests.php +++ b/staff/estimate-requests.php @@ -2,46 +2,74 @@ session_start(); define('WDS_SYSTEM', true); require_once __DIR__ . '/../includes/portal-helpers.php'; +require_once __DIR__ . '/../includes/email.php'; portalRequireStaff(); -$requests = portalLoadEstimateRequests(); +$statuses = [ + 'new' => 'New', + 'reviewing' => 'Reviewing', + 'contacted' => 'Contacted', + 'needs_info' => 'Needs Info', + 'proposal_drafted' => 'Proposal Drafted', + 'proposal_sent' => 'Proposal Sent', + 'accepted' => 'Accepted', + 'declined' => 'Declined', + 'closed' => 'Closed', +]; + +$requests = portalLoadProjectRequests(); +$notice = ''; if ($_SERVER['REQUEST_METHOD'] === 'POST') { - $id = trim($_POST['request_id'] ?? ''); - $action = trim($_POST['action'] ?? ''); - - if ($id !== '') { - foreach ($requests as &$r) { - if (($r['id'] ?? '') !== $id) { - continue; - } - if ($action === 'set_status') { - $newStatus = trim($_POST['new_status'] ?? ''); - $allowed = ['new', 'reviewed', 'contacted', 'proposal_needed', 'proposal_sent', 'closed']; - if (in_array($newStatus, $allowed, true)) { - $r['status'] = $newStatus; - } - } elseif ($action === 'save_notes') { - $r['notes'] = trim($_POST['notes'] ?? ''); - } - break; + $recordId = trim((string)($_POST['record_id'] ?? '')); + $action = trim((string)($_POST['action'] ?? 'save_updates')); + foreach ($requests as &$req) { + if ((string)($req['id'] ?? '') !== $recordId) { + continue; + } + + if ($action === 'email_customer') { + $name = (string)($req['name'] ?? 'there'); + $email = (string)($req['email'] ?? ''); + $requestId = portalGetRequestDisplayId($req); + $subject = 'Runlevel Systems Project Request - ' . $requestId; + $body = "Hello {$name},\n\n" + . "We reviewed your project request and have an update.\n\n" + . "Request ID:\n{$requestId}\n\n" + . "Current status:\n" . ($statuses[$req['status']] ?? ucfirst((string)$req['status'])) . "\n\n" + . "If you have any updates, reply to this email and include your Request ID.\n\n" + . "Runlevel Systems\n" + . "DESIGN โ€ข DEBUG โ€ข DEPLOY\n"; + send_email($email, $subject, $body); + $notice = 'Customer email sent.'; + } else { + $newStatus = trim((string)($_POST['status'] ?? 'new')); + $req['status'] = isset($statuses[$newStatus]) ? $newStatus : 'new'; + $req['estimated_cost_range'] = trim((string)($_POST['estimated_cost_range'] ?? '')); + $req['estimated_time_range'] = trim((string)($_POST['estimated_time_range'] ?? '')); + $req['staff_summary'] = trim((string)($_POST['staff_summary'] ?? '')); + $req['recommended_next_step'] = trim((string)($_POST['recommended_next_step'] ?? '')); + $req['internal_notes'] = trim((string)($_POST['internal_notes'] ?? '')); + $notice = 'Request updates saved.'; } - unset($r); - portalSaveEstimateRequests($requests); - header('Location: /staff/estimate-requests.php' . ($action === 'save_notes' ? '?view=' . urlencode($id) : '')); - exit; + + $req['updated_at'] = date('c'); + break; } + unset($req); + portalSaveProjectRequests($requests); } -$viewId = trim($_GET['view'] ?? ''); +$proposals = portalLoadProposals(); +$agreements = portalLoadProjectAgreements(); + +$viewId = trim((string)($_GET['view'] ?? ($_POST['record_id'] ?? ''))); $viewRequest = null; -if ($viewId !== '') { - foreach ($requests as $r) { - if (($r['id'] ?? '') === $viewId) { - $viewRequest = $r; - break; - } +foreach ($requests as $request) { + if ((string)($request['id'] ?? '') === $viewId) { + $viewRequest = portalNormalizeProjectRequest((array)$request); + break; } } @@ -49,15 +77,6 @@ return strcmp((string)($b['created_at'] ?? ''), (string)($a['created_at'] ?? '')); }); -$statusLabels = [ - 'new' => ['label' => 'New', 'color' => '#ffc600'], - 'reviewed' => ['label' => 'Reviewed', 'color' => '#0a84ff'], - 'contacted' => ['label' => 'Contacted', 'color' => '#36f3ff'], - 'proposal_needed' => ['label' => 'Proposal Needed', 'color' => '#a78bfa'], - 'proposal_sent' => ['label' => 'Proposal Sent', 'color' => '#22c55e'], - 'closed' => ['label' => 'Closed', 'color' => '#5a7a9e'], -]; - $current_page = 'staff-portal'; $header_class = 'inner-header'; ?> @@ -68,24 +87,36 @@ - Estimate Requests | Staff | Runlevel Systems + Project Requests | Staff | Runlevel Systems @@ -96,66 +127,106 @@
โ† Dashboard +
+ - ucfirst((string)$st), 'color' => '#7a9ac0']; ?> +
-
-

Estimate Request Detail

- +
+

Project Request Detail

+
+ + Create Proposal + Create Project Agreement +
-
- - - - - -
-
-
-
-
+
+
+
+
-
+
-
-
-
-
View Linkโ€”
+
+
+
+
+
+
+
+
View Linkโ€”
-
- -
+
+ +
-
- - - - - + + + +
+
+
+
+
+
+
+
+
+ +
+ +
+ + + +
+ +
+ + +
No linked proposals yet.
+ + +
+ + +
+ +
+ + +
No linked project agreements yet.
+ + +
+ + +
-

Estimate Requests

+

Project Requests

- - - + + - - - @@ -164,22 +235,18 @@ - + - ucfirst((string)$st), 'color' => '#7a9ac0']; ?> + - + - - - - - - - + + + diff --git a/staff/proposal-template.php b/staff/proposal-template.php index 558dc2b..4bc44fc 100644 --- a/staff/proposal-template.php +++ b/staff/proposal-template.php @@ -1,170 +1,3 @@ - - - - - - - - Proposal Template | Staff Portal | Runlevel Systems - - - - - - - -
-
- โ† Back to Dashboard - -

โœ๏ธ Proposal Template

-

Use this template as a starting point. Fill in each field with the project-specific information.

- -
-
-

Runlevel Systems

-

Project Proposal

-

Date: ___________________________

-
- -
- -
[Enter project name]
-
-
-
- -
[Client full name or organization]
-
-
- -
[Email or preferred contact]
-
-
- -
-

Request Summary

-
- -
[Summarize the client's original request in plain language]
-
-
- -
-

Proposed Work

-
- -
[Describe exactly what will be built, fixed, or delivered]
-
-
- -
[List specific deliverables, e.g., working script, website pages, Unity scene, etc.]
-
-
- -
-

Pricing & Payment

-
-
- -
$[Amount] USD
-
-
- -
$[Amount] USD  (or N/A for small jobs)
-
-
- -
PayPal (invoice or payment link)
-
-
-
- -
[e.g., Full payment before work begins / 50% deposit + 50% on delivery / milestone payments]
-
-
- -
-

Timeline & Revisions

-
-
- -
[e.g., 2โ€“5 business days / 1โ€“2 weeks]
-
-
- -
[e.g., Up to 2 revision rounds included. Additional changes quoted separately.]
-
-
-
- -
-

Assumptions & Limitations

-
-
[List any assumptions, known limitations, out-of-scope items, or dependencies]
-
-
- -
-

Approval

-

- By approving this proposal (in writing, by email, or by making the deposit payment), the client agrees to the terms described above - and the Runlevel Systems Terms of Service. -

-
-
-

Client signature / written approval:

-
-

Name & date

-
-
-

Runlevel Systems representative:

-
-

Name & date

-
-
-
-
- -
-

- Staff note: This is a working template for internal use. Customise each proposal for the specific project before sending to the client. - Future versions will include editable fields and PDF generation. -

-
-
-
- - - - - - +header('Location: /staff/create-proposal.php', true, 302); +exit; diff --git a/staff/requests.php b/staff/requests.php index c81dd6b..0c92003 100644 --- a/staff/requests.php +++ b/staff/requests.php @@ -1,92 +1,3 @@ - - - - - - - - Client Requests | Staff Portal | Runlevel Systems - - - - - - - -
-
- โ† Back to Dashboard -

๐Ÿ“ฅ Client Requests

- - -
-
๐Ÿ“ญ
-

No client requests yet.

-
- - -
-

- - - - -

-
- Submitted -  ยท  Client: -
-
Type:
-
Budget:
-
Timeline:
-
Contact:
- -
Link:
- -
Description:
- -
-
- - -
-
- - - - - - +header('Location: /staff/estimate-requests.php', true, 302); +exit;
Estimate IDNameUsernameRequest IDClient EmailPhoneDiscord usernamePreferred Contact Project Type Status Submitted Date
No estimate requests yet.
No project requests yet.
ViewView