-
Project type *
-
- โ Select โ
-
- >
-
-
+
+
+ Project type *
+
+ โ Select โ
+
+ >
+
+
+
+
+ Project stage *
+
+ โ Select โ
+
+ >
+
+
+
-
-
Repository / project link optional
-
+
+
+ Project size *
+
+ โ Select โ
+
+ >
+
+
+
+
+ Timeline *
+
+ โ Select โ
+
+ >
+
+
+
+
+
+
+ Budget comfort *
+
+ โ Select โ
+
+ >
+
+
+
+
+ Repository / project link
+
+
- Description *
+ Project description *
+
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.
-
Submit Estimate Request
+
Submit Project Request
@@ -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 Submit your request or estimate form.
- 2 Review the estimate or proposal we prepare.
- 3 Approve the work and confirm the terms.
- 4 Pay through PayPal invoice or approved payment link.
- 5 Work begins after payment is confirmed.
- 6 Review completed work against the agreed deliverables.
-
-
-
- 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
-
๐จ๏ธ Print / Save as PDF
-
๐ Project Agreement Template
-
-
- โ๏ธ Legal Notice: This template is provided for internal business planning purposes only.
- It should be reviewed by a qualified attorney before being used in any final public or binding agreement.
-
-
-
-
-
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
+ Print / Save PDF
+
+
Runlevel Systems ยท Request ID:
+
+
+
+
+
+
+
+
+
+
+
+
+
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
+ Print / Save PDF
+
+
Runlevel Systems ยท Request ID:
+
+
+
+
+
+
+
+
+ Staff Summary
+ Request Summary
+ Proposed Work
+ Deliverables
+ Revision Terms
+ Assumptions
+ Customer Responsibilities
+ Next Steps
+
+
+
+
+
+
+
+
+
+
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
+
-
-
-
- Update Status
-
- $lbl): ?>
- >
-
-
- Update
-
-
-
-
-
+
+
+
+
-
+
-
-
-
-
Repository / Project Link
+
+
+
+
+
+
+
+
Repository / project link
-
-
Description
-
+
-
-
-
- Internal Notes
-
- Save Notes
+
+
+
+
+
Status $label): ?>>
+
Estimated Cost Range
+
Estimated Time Range
+
Recommended Next Step
+
+ Staff Summary
+ Internal Notes
+
+ Save Updates
+
+
+
+
+
+ Email Customer
+
+
+
+
Linked Proposals
+
+
No linked proposals yet.
+
+
+
+
+
+
+
+
+
Linked Project Agreements
+
+
No linked project agreements yet.
+
+
+
+
+
+
-
Estimate Requests
+
Project Requests
- Estimate ID
- Name
- Username
+ Request ID
+ Client
Email
- Phone
- Discord username
- Preferred Contact
Project Type
Status
Submitted Date
@@ -164,22 +235,18 @@
- No estimate requests yet.
+ No project requests yet.
- ucfirst((string)$st), 'color' => '#7a9ac0']; ?>
+
-
+
-
-
-
-
-
-
- View
+
+
+ View
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
-
๐จ๏ธ Print / Save as PDF
-
โ๏ธ Proposal Template
-
Use this template as a starting point. Fill in each field with the project-specific information.
-
-
-
-
Runlevel Systems
-
Project Proposal
-
Date: ___________________________
-
-
-
-
Project Name
-
[Enter project name]
-
-
-
-
Client Name
-
[Client full name or organization]
-
-
-
Client Email / Contact
-
[Email or preferred contact]
-
-
-
-
-
Request Summary
-
-
What the client asked for
-
[Summarize the client's original request in plain language]
-
-
-
-
-
Proposed Work
-
-
What Runlevel Systems will do
-
[Describe exactly what will be built, fixed, or delivered]
-
-
-
Deliverables
-
[List specific deliverables, e.g., working script, website pages, Unity scene, etc.]
-
-
-
-
-
Pricing & Payment
-
-
-
Estimated Total Cost
-
$[Amount] USD
-
-
-
Deposit Required to Begin
-
$[Amount] USD (or N/A for small jobs)
-
-
-
Payment Method
-
PayPal (invoice or payment link)
-
-
-
-
Payment Terms
-
[e.g., Full payment before work begins / 50% deposit + 50% on delivery / milestone payments]
-
-
-
-
-
Timeline & Revisions
-
-
-
Estimated Completion Time
-
[e.g., 2โ5 business days / 1โ2 weeks]
-
-
-
Revision Terms
-
[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:
-
-
-
-
Description:
-
-
-
-
-
-
-
-
-
-
-
-
-
+header('Location: /staff/estimate-requests.php', true, 302);
+exit;