mirror of
https://github.com/itflow-org/itflow
synced 2026-11-11 15:07:24 +00:00
396 lines
17 KiB
PHP
396 lines
17 KiB
PHP
<?php
|
|
|
|
require_once "../config.php";
|
|
require_once "../functions.php";
|
|
require_once "../includes/load_global_settings.php";
|
|
|
|
require_once __DIR__ . "/../includes/session_init.php";
|
|
|
|
require_once "../includes/inc_set_timezone.php"; // Must be included after session_start to work
|
|
|
|
// logAudit() reads these globals - without them guest audit rows have no IP
|
|
$session_ip = escapeSql(getIP());
|
|
$session_user_agent = escapeSql($_SERVER['HTTP_USER_AGENT'] ?? '');
|
|
|
|
/*
|
|
* A quote accepted or declined from its guest page (guest_view_quote.php) -
|
|
* posted, never a link: mail scanners and link previews open links, and must
|
|
* not answer a quote. Its link key is the only proof, matched exactly.
|
|
*/
|
|
if (isset($_POST['quote_id'], $_POST['url_key']) && (isset($_POST['accept_quote']) || isset($_POST['decline_quote']))) {
|
|
|
|
// The answer and what follows it - functions/billing.php, shared with the client portal
|
|
$accept = isset($_POST['accept_quote']);
|
|
$quote_id = intval($_POST['quote_id']);
|
|
$url_key = is_string($_POST['url_key']) ? $_POST['url_key'] : '';
|
|
$quote_key = (string) (mysqli_fetch_row(mysqli_query($mysqli, "SELECT quote_url_key FROM quotes WHERE quote_id = $quote_id"))[0] ?? '');
|
|
|
|
if ($quote_key !== '' && hash_equals($quote_key, $url_key)) {
|
|
|
|
$answer = billingQuoteAnswer($quote_id, $accept);
|
|
|
|
if (!$answer['ok']) {
|
|
flashAlert($answer['error'], 'error');
|
|
redirect();
|
|
}
|
|
|
|
if ($accept) {
|
|
flashAlert("Quote Accepted");
|
|
} else {
|
|
flashAlert("Quote Declined", 'danger');
|
|
}
|
|
|
|
redirect();
|
|
|
|
} else {
|
|
echo "Invalid!!";
|
|
}
|
|
|
|
}
|
|
|
|
/*
|
|
* A contract signed from its link (guest_view_contract.php) - the round's id
|
|
* and key are the only proof, matched exactly. Signed once, and only the
|
|
* version on the page: a request withdrawn or replaced meanwhile isn't
|
|
* signed. Who signed is the name given, noted as from the link.
|
|
*/
|
|
if (isset($_POST['sign_contract'])) {
|
|
|
|
$round = contractSignaturesReady() ? contractSignatureByKey($_POST['contract_signature_id'] ?? 0, $_POST['key'] ?? '') : null;
|
|
if (!$round) {
|
|
http_response_code(404);
|
|
exit("This contract link isn't working - open it again from the email.");
|
|
}
|
|
$back = '/guest/guest_view_contract.php?id=' . intval($round['contract_signature_id']) . '&key=' . rawurlencode((string) $round['contract_signature_url_key']);
|
|
$state = contractSignatureState($round);
|
|
if (!in_array($state['key'], ['sent', 'viewed'], true)) {
|
|
flashAlert($state['key'] === 'withdrawn' ? "This request was withdrawn, so it wasn't signed." : "It has already been signed.", 'error');
|
|
redirect($back);
|
|
}
|
|
if (!hash_equals((string) $round['contract_signature_document_hash'], (string) ($_POST['document_hash'] ?? ''))) {
|
|
flashAlert("The contract changed while you had it open - read it again before signing.", 'error');
|
|
redirect($back);
|
|
}
|
|
[$f, $error] = contractSignatureFromInput($_POST);
|
|
if ($error !== '') {
|
|
flashAlert(escapeHtml($error) . ' It was not signed yet.', 'error');
|
|
redirect($back);
|
|
}
|
|
if (!contractSignatureClientSign($round, $f, 'link')) {
|
|
flashAlert("It couldn't be signed - it may have just been withdrawn.", 'error');
|
|
redirect($back);
|
|
}
|
|
flashAlert("Signed - thank you. We'll countersign it and email you a copy.");
|
|
redirect($back);
|
|
|
|
}
|
|
|
|
if (isset($_GET['export_contract_pdf'])) {
|
|
|
|
$round = contractSignaturesReady() ? contractSignatureByKey($_GET['id'] ?? 0, $_GET['key'] ?? '') : null;
|
|
if (!$round) {
|
|
http_response_code(404);
|
|
exit("This contract link isn't working - open it again from the email.");
|
|
}
|
|
billingPdfSend(contractPdf(contractRow(intval($round['contract_id'])), $round), true);
|
|
|
|
}
|
|
|
|
/*
|
|
* The guest ticket page's buttons (guest_view_ticket.php): close or reopen a
|
|
* resolved ticket, rate a closed one. Posted, never links - mail scanners and
|
|
* link previews open links, and must not close a ticket. The ticket's link
|
|
* key is the only proof, matched exactly (ticketByGuestKey()). Who pressed
|
|
* it isn't known - the link goes to watchers too - so it is noted as from
|
|
* the guest link, never as the contact. Each happens once: a second press,
|
|
* or a ticket an agent has moved on since, just goes back to the page.
|
|
*/
|
|
if (isset($_POST['close_ticket']) || isset($_POST['reopen_ticket']) || isset($_POST['add_ticket_feedback'])) {
|
|
|
|
$row = ticketByGuestKey($_POST['ticket_id'] ?? 0, $_POST['url_key'] ?? '');
|
|
if (!$row) {
|
|
http_response_code(404);
|
|
exit("This ticket link isn't working - open it again from the email.");
|
|
}
|
|
|
|
$ticket_id = intval($row['ticket_id']);
|
|
$client_id = intval($row['ticket_client_id']);
|
|
$reference = escapeSql(ticketReference($row));
|
|
$back = ticketGuestPath($row);
|
|
|
|
if (isset($_POST['close_ticket'])) {
|
|
|
|
mysqli_query($mysqli, "UPDATE tickets SET ticket_status = 5, ticket_closed_at = NOW() WHERE ticket_id = $ticket_id AND ticket_resolved_at IS NOT NULL AND ticket_closed_at IS NULL");
|
|
if (mysqli_affected_rows($mysqli) === 1) {
|
|
syncTicketSlaClock($ticket_id);
|
|
logTicketHistory($ticket_id, "The client closed the ticket from the guest link");
|
|
mysqli_query($mysqli, "INSERT INTO ticket_replies SET ticket_reply = 'Ticket closed by client (guest URL).', ticket_reply_type = 'Internal', ticket_reply_by = 0, ticket_reply_ticket_id = $ticket_id");
|
|
logAudit("Ticket", "Edit", "Ticket $reference closed from its guest link", $client_id, $ticket_id);
|
|
triggerCustomAction('ticket_close', $ticket_id);
|
|
flashAlert("Ticket closed - thank you");
|
|
}
|
|
|
|
} elseif (isset($_POST['reopen_ticket'])) {
|
|
|
|
mysqli_query($mysqli, "UPDATE tickets SET ticket_status = 2, ticket_resolved_at = NULL WHERE ticket_id = $ticket_id AND ticket_resolved_at IS NOT NULL AND ticket_closed_at IS NULL");
|
|
if (mysqli_affected_rows($mysqli) === 1) {
|
|
// The resolution clock runs again, as when the portal or an emailed reply reopens it
|
|
resetTicketResolutionSla($ticket_id);
|
|
syncTicketSlaClock($ticket_id);
|
|
logTicketHistory($ticket_id, "The client reopened the ticket from the guest link");
|
|
mysqli_query($mysqli, "INSERT INTO ticket_replies SET ticket_reply = 'Ticket reopened by client (guest URL).', ticket_reply_type = 'Internal', ticket_reply_by = 0, ticket_reply_ticket_id = $ticket_id");
|
|
logAudit("Ticket", "Edit", "Ticket $reference reopened from its guest link", $client_id, $ticket_id);
|
|
// Nothing else tells you - no reply came with it
|
|
appNotify("Ticket", "Ticket $reference was reopened from its guest link - " . escapeSql($row['ticket_subject']), "/agent/ticket.php?ticket_id=$ticket_id" . ($client_id ? "&client_id=$client_id" : ''), $client_id, $ticket_id);
|
|
triggerCustomAction('ticket_update', $ticket_id);
|
|
flashAlert("Ticket reopened - now tell us what is still wrong: reply to our email, or give us a call");
|
|
}
|
|
|
|
} else {
|
|
|
|
// Only the two ratings the page offers - the reports count Good against Bad - and only the first one
|
|
$feedback = $_POST['add_ticket_feedback'] === 'Bad' ? 'Bad' : 'Good';
|
|
mysqli_query($mysqli, "UPDATE tickets SET ticket_feedback = '$feedback' WHERE ticket_id = $ticket_id AND ticket_closed_at IS NOT NULL AND (ticket_feedback IS NULL OR ticket_feedback = '')");
|
|
if (mysqli_affected_rows($mysqli) === 1) {
|
|
logTicketHistory($ticket_id, "The client rated the ticket " . ($feedback === 'Bad' ? 'not good' : 'good') . " from the guest link");
|
|
if ($feedback === 'Bad') {
|
|
appNotify("Feedback", "Guest rated ticket number $reference (ID: $ticket_id) as bad", "/agent/ticket.php?ticket_id=$ticket_id" . ($client_id ? "&client_id=$client_id" : ''), $client_id, $ticket_id);
|
|
}
|
|
triggerCustomAction('ticket_feedback', $ticket_id);
|
|
flashAlert("Feedback recorded - thank you");
|
|
}
|
|
|
|
}
|
|
|
|
redirect($back);
|
|
|
|
}
|
|
|
|
/*
|
|
* Add to calendar on the guest ticket page - the calendar file the booking
|
|
* email carried (ticketScheduleIcs()), for work booked on an open ticket.
|
|
* The same event, so it updates one already in the calendar.
|
|
*/
|
|
if (isset($_GET['ticket_calendar'])) {
|
|
|
|
$row = ticketByGuestKey($_GET['ticket_calendar'], $_GET['url_key'] ?? '');
|
|
if (!$row || empty($row['ticket_schedule']) || !ticketIsOpen($row)) {
|
|
http_response_code(404);
|
|
exit('Nothing is booked on this ticket.');
|
|
}
|
|
|
|
$ics = ticketScheduleIcs(ticketScheduleRow($row['ticket_id']), $row['ticket_schedule']);
|
|
header('Content-Type: text/calendar; charset=utf-8');
|
|
header('Content-Disposition: attachment; filename="' . preg_replace('/[^A-Za-z0-9_-]/', '', ticketReference($row)) . '.ics"');
|
|
header('Content-Length: ' . strlen($ics));
|
|
echo $ics;
|
|
exit;
|
|
|
|
}
|
|
|
|
if (isset($_GET['approve_ticket_task'])) {
|
|
|
|
$task_id = intval($_GET['approve_ticket_task']);
|
|
$approval_id = intval($_GET['approval_id']);
|
|
$url_key = escapeSql($_GET['approval_url_key']);
|
|
|
|
$approval_row = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT approval_created_by, approval_required_user_id, approval_scope, approval_type, task_name,
|
|
task_ticket_id FROM task_approvals LEFT JOIN tasks on task_id = approval_task_id WHERE approval_id = $approval_id AND approval_task_id = $task_id AND approval_url_key = '$url_key' AND approval_status = 'pending'"));
|
|
|
|
$task_name = escapeSql($approval_row['task_name']);
|
|
$scope = escapeHtml($approval_row['approval_scope']);
|
|
$type = escapeHtml($approval_row['approval_type']);
|
|
$required_user = intval($approval_row['approval_required_user_id']);
|
|
$created_by = intval($approval_row['approval_created_by']);
|
|
$ticket_id = intval($approval_row['task_ticket_id']);
|
|
|
|
if (!$approval_row) {
|
|
exit("Cannot find/approve that task");
|
|
}
|
|
|
|
// Approve
|
|
mysqli_query($mysqli, "UPDATE task_approvals SET approval_status = 'approved', approval_approved_by = $required_user WHERE approval_id = $approval_id AND approval_task_id = $task_id AND approval_url_key = '$url_key' AND approval_status = 'pending'");
|
|
|
|
// Notify tech
|
|
mysqli_query($mysqli, "INSERT INTO notifications SET notification_type = 'Ticket', notification = 'Guest approved ticket task $task_name', notification_action = 'ticket.php?ticket_id=$ticket_id', notification_user_id = $created_by");
|
|
|
|
// Logging
|
|
logAudit("Task", "Edit", "Guest user approved task $task_name via link (approval $approval_id)", 0, $task_id);
|
|
|
|
flashAlert("Task Approved");
|
|
redirect();
|
|
|
|
}
|
|
|
|
if (isset($_GET['export_quote_pdf'])) {
|
|
|
|
// The quote's own link key is the only way in - the same one its guest page checks
|
|
$quote = billingRow('quote', intval($_GET['export_quote_pdf']), false);
|
|
if (!$quote || (string) $quote['quote_url_key'] === '' || !hash_equals((string) $quote['quote_url_key'], (string) ($_GET['url_key'] ?? ''))) {
|
|
http_response_code(404);
|
|
exit('That quote is not available.');
|
|
}
|
|
|
|
billingPdfSend(billingPdf('quote', $quote));
|
|
|
|
}
|
|
|
|
if (isset($_GET['export_invoice_pdf'])) {
|
|
|
|
// The invoice's own link key is the only way in - the same one its guest page checks
|
|
$invoice = billingRow('invoice', intval($_GET['export_invoice_pdf']), false);
|
|
if (!$invoice || (string) $invoice['invoice_url_key'] === '' || !hash_equals((string) $invoice['invoice_url_key'], (string) ($_GET['url_key'] ?? ''))) {
|
|
http_response_code(404);
|
|
exit('That invoice is not available.');
|
|
}
|
|
|
|
billingPdfSend(billingPdf('invoice', $invoice));
|
|
|
|
}
|
|
|
|
if (isset($_POST['guest_quote_upload_file'])) {
|
|
|
|
$quote_id = intval($_POST['quote_id']);
|
|
$url_key = escapeSql($_POST['url_key']);
|
|
|
|
// Select only the necessary fields
|
|
$sql = mysqli_query($mysqli, "SELECT quote_id FROM quotes WHERE quote_id = $quote_id AND quote_url_key = '$url_key'");
|
|
|
|
if (mysqli_num_rows($sql) == 1) {
|
|
|
|
// Each PDF to the client's Client Uploads and onto the quote - functions/billing.php, shared with the client portal
|
|
if (!empty($_FILES['file'])) {
|
|
$upload = billingQuoteAddFiles($quote_id, $_FILES['file']);
|
|
foreach ($upload['added'] as $added) {
|
|
flashAlert('File uploaded!');
|
|
}
|
|
if ($upload['failed']) {
|
|
flashAlert('Something went wrong uploading the file - please let the support team know.', 'error');
|
|
logApp("Guest", "error", "Error uploading file to invoice");
|
|
}
|
|
}
|
|
|
|
redirect();
|
|
|
|
} else {
|
|
echo "Invalid!!";
|
|
}
|
|
|
|
}
|
|
|
|
if (isset($_POST['create_stripe_customer'])) {
|
|
|
|
$invoice_url_key = escapeSql($_POST['url_key']);
|
|
$invoice_id = intval($_POST['invoice_id']);
|
|
$name = escapeSql($_POST['name']);
|
|
$email = escapeSql($_POST['email']);
|
|
|
|
// Query invoice details
|
|
$sql = mysqli_query(
|
|
$mysqli,
|
|
"SELECT client_id, client_name, invoice_amount, invoice_currency_code, invoice_date,
|
|
invoice_discount_amount, invoice_due, invoice_id, invoice_number, invoice_prefix,
|
|
invoice_status FROM invoices
|
|
LEFT JOIN clients ON invoice_client_id = client_id
|
|
WHERE invoice_id = $invoice_id
|
|
AND invoice_url_key = '$invoice_url_key'
|
|
AND invoice_status NOT IN ('Draft', 'Paid', 'Cancelled')
|
|
LIMIT 1"
|
|
);
|
|
|
|
// Ensure valid invoice
|
|
if (!$sql || mysqli_num_rows($sql) !== 1) {
|
|
echo "<br><h2>Oops, something went wrong! Please ensure you have the correct URL and have not already paid this invoice.</h2>";
|
|
error_log("Stripe payment error - Invoice with ID $invoice_id not found or not eligible.");
|
|
exit();
|
|
}
|
|
|
|
$row = mysqli_fetch_assoc($sql);
|
|
$invoice_id = intval($row['invoice_id']);
|
|
$invoice_prefix = escapeHtml($row['invoice_prefix']);
|
|
$invoice_number = intval($row['invoice_number']);
|
|
$client_id = intval($row['client_id']);
|
|
$client_name = escapeHtml($row['client_name']);
|
|
|
|
// Get Stripe provider config
|
|
$stripe_provider_result = mysqli_query($mysqli, "
|
|
SELECT payment_provider_id, payment_provider_private_key
|
|
FROM payment_providers
|
|
WHERE payment_provider_name = 'Stripe'
|
|
AND payment_provider_active = 1
|
|
LIMIT 1
|
|
");
|
|
|
|
$stripe_provider = mysqli_fetch_assoc($stripe_provider_result);
|
|
if (!$stripe_provider) {
|
|
flashAlert("Stripe provider is not configured in the system.", 'danger');
|
|
redirect("saved_payment_methods.php");
|
|
}
|
|
|
|
$stripe_provider_id = intval($stripe_provider['payment_provider_id']);
|
|
$stripe_secret_key = escapeHtml($stripe_provider['payment_provider_private_key']);
|
|
|
|
if (empty($stripe_secret_key)) {
|
|
flashAlert("Stripe credentials missing. Please contact support.", 'danger');
|
|
redirect("saved_payment_methods.php");
|
|
}
|
|
|
|
// Check if client already has a Stripe customer
|
|
$existing_customer = mysqli_fetch_assoc(mysqli_query($mysqli, "
|
|
SELECT payment_provider_client
|
|
FROM client_payment_provider
|
|
WHERE client_id = $client_id
|
|
AND payment_provider_id = $stripe_provider_id
|
|
LIMIT 1
|
|
"));
|
|
|
|
if (!$existing_customer) {
|
|
try {
|
|
// Initialize Stripe
|
|
require_once '../includes/stripe_init.php';
|
|
$stripe = new \Stripe\StripeClient($stripe_secret_key);
|
|
|
|
// Create new customer in Stripe
|
|
$customer = $stripe->customers->create([
|
|
'name' => $client_name,
|
|
'email' => $email,
|
|
'metadata' => [
|
|
'itflow_client_id' => $client_id,
|
|
'consent_by' => $name,
|
|
]
|
|
]);
|
|
|
|
$stripe_customer_id = escapeSql($customer->id);
|
|
|
|
// Insert customer into client_payment_provider
|
|
mysqli_query($mysqli, "
|
|
INSERT INTO client_payment_provider
|
|
SET client_id = $client_id,
|
|
payment_provider_id = $stripe_provider_id,
|
|
payment_provider_client = '$stripe_customer_id',
|
|
client_payment_provider_created_at = NOW()
|
|
");
|
|
|
|
logAudit("Stripe", "Create", "Guest $name created Stripe customer for $client_name as $stripe_customer_id and authorized future automatic payments", $client_id);
|
|
|
|
flashAlert("Stripe customer created. Thank you for your consent.");
|
|
|
|
} catch (Exception $e) {
|
|
$error = $e->getMessage();
|
|
|
|
error_log("Stripe error while creating customer for $client_name: $error");
|
|
|
|
logApp("Stripe", "error", "Failed to create Stripe customer for $client_name: $error");
|
|
|
|
flashAlert("An error occurred while creating your Stripe customer. Please try again.", 'danger');
|
|
|
|
}
|
|
|
|
} else {
|
|
flashAlert("Stripe customer already exists for your account.", 'danger');
|
|
}
|
|
|
|
redirect('guest_view_invoice.php?invoice_id=' . $invoice_id . '&url_key=' . urlencode($invoice_url_key));
|
|
}
|
|
|