mirror of
https://github.com/itflow-org/itflow
synced 2026-09-03 21:35:12 +00:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
61bb9dd7cb |
25
CHANGELOG.md
25
CHANGELOG.md
@@ -2,31 +2,6 @@
|
|||||||
|
|
||||||
This file documents all notable changes made to ITFlow.
|
This file documents all notable changes made to ITFlow.
|
||||||
|
|
||||||
## [26.09.2] Maint Release
|
|
||||||
|
|
||||||
- Updates the App Version to a proper version number.
|
|
||||||
|
|
||||||
|
|
||||||
## [26.09.1] Maint Release
|
|
||||||
|
|
||||||
### Upgrading to 26.09.1
|
|
||||||
|
|
||||||
Update from Maintenance > Update — Queue Update hands the job to cron and it applies on its own. There is no database change in this release, so nothing else is required.
|
|
||||||
|
|
||||||
### Breaking Changes and Notes
|
|
||||||
|
|
||||||
- API: a contact must be archived before the delete endpoint will remove it. Deleting an active contact is refused and reports nothing deleted, so archive it first and then delete. Thanks to @Wrongecho.
|
|
||||||
|
|
||||||
### Bug Fixes
|
|
||||||
|
|
||||||
- Assets: IP address fields demanded all three digits of every octet, so `10.0.0.1` had to be entered as `010.000.000.001`. They take natural input again, and a field holding DHCP is now left alone rather than being emptied the moment the modal opens.
|
|
||||||
- Networks: in the IP list, an empty hostname or description shows a dash rather than a blank cell, the column headings match the rest of the app, and the table is tighter so more addresses fit on screen.
|
|
||||||
|
|
||||||
### Developer Updates
|
|
||||||
|
|
||||||
- The IPv4 mask in `js/app.js` is a regex mask rather than four `IMask.MaskedRange` blocks. A pattern mask will not advance past a separator until the current block reaches its `maxLength`, which is what forced the three-digit octets. A regex mask has no per-block completeness rule and tests the whole value on each keystroke, so a partial `10.0.` is valid on its own. Octets are still bounded to 0-255 and leading zeros are still accepted, matching what the old jquery.inputmask `ip` alias allowed. Any value not made purely of digits and dots is skipped, because `interface_ip` and `asset_ip` are `varchar(200)` and also carry the literal `DHCP` written by the checkbox on those same modals.
|
|
||||||
- Dead display variables removed from the asset, expense and product listings. `$asset_description_display`, `$client_name_display` and `$product_description_display` are folded into `?: '-'` at the point of assignment.
|
|
||||||
|
|
||||||
## [26.09]
|
## [26.09]
|
||||||
|
|
||||||
### Upgrading to 26.09
|
### Upgrading to 26.09
|
||||||
|
|||||||
@@ -556,6 +556,11 @@ $num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()"));
|
|||||||
$asset_type = escapeHtml($row['asset_type']);
|
$asset_type = escapeHtml($row['asset_type']);
|
||||||
$asset_name = escapeHtml($row['asset_name']);
|
$asset_name = escapeHtml($row['asset_name']);
|
||||||
$asset_description = escapeHtml($row['asset_description']);
|
$asset_description = escapeHtml($row['asset_description']);
|
||||||
|
if ($asset_description) {
|
||||||
|
$asset_description_display = $asset_description;
|
||||||
|
} else {
|
||||||
|
$asset_description_display = "-";
|
||||||
|
}
|
||||||
$asset_make = escapeHtml($row['asset_make']);
|
$asset_make = escapeHtml($row['asset_make']);
|
||||||
$asset_model = escapeHtml($row['asset_model']);
|
$asset_model = escapeHtml($row['asset_model']);
|
||||||
$asset_serial = escapeHtml($row['asset_serial']);
|
$asset_serial = escapeHtml($row['asset_serial']);
|
||||||
|
|||||||
@@ -1025,40 +1025,14 @@ if ($user_config_dashboard_technical_enable == 1) {
|
|||||||
data: {
|
data: {
|
||||||
labels: [
|
labels: [
|
||||||
<?php
|
<?php
|
||||||
// Cash basis, matching the Cash Flow chart above and the Income Summary
|
mysqli_query($mysqli, "CREATE TEMPORARY TABLE TopCategories SELECT category_name, category_id, SUM(invoice_amount) AS total_income FROM categories, invoices WHERE invoice_category_id = category_id AND invoice_status = 'Paid' AND YEAR(invoice_date) = $year GROUP BY category_name, category_id ORDER BY total_income DESC LIMIT 5");
|
||||||
// report - payments carry their invoice's category, and standalone
|
|
||||||
// revenues count too. Keying off invoice_status = 'Paid' instead would
|
|
||||||
// drop every partially paid invoice and every revenue from the chart.
|
|
||||||
mysqli_query($mysqli, "CREATE TEMPORARY TABLE TopCategories
|
|
||||||
SELECT category_name, category_id, SUM(income.amount) AS total_income
|
|
||||||
FROM (SELECT invoice_category_id AS income_category_id, payment_amount AS amount
|
|
||||||
FROM payments
|
|
||||||
INNER JOIN invoices ON invoice_id = payment_invoice_id
|
|
||||||
WHERE YEAR(payment_date) = $year AND invoice_category_id > 0
|
|
||||||
UNION ALL
|
|
||||||
SELECT revenue_category_id AS income_category_id, revenue_amount AS amount
|
|
||||||
FROM revenues
|
|
||||||
WHERE YEAR(revenue_date) = $year AND revenue_category_id > 0) AS income
|
|
||||||
INNER JOIN categories ON category_id = income.income_category_id
|
|
||||||
GROUP BY category_name, category_id
|
|
||||||
ORDER BY total_income DESC LIMIT 5");
|
|
||||||
$sql_categories = mysqli_query($mysqli, "SELECT category_name FROM TopCategories");
|
$sql_categories = mysqli_query($mysqli, "SELECT category_name FROM TopCategories");
|
||||||
while ($row = mysqli_fetch_assoc($sql_categories)) {
|
while ($row = mysqli_fetch_assoc($sql_categories)) {
|
||||||
$category_name = json_encode($row['category_name']);
|
$category_name = json_encode($row['category_name']);
|
||||||
echo "$category_name,";
|
echo "$category_name,";
|
||||||
}
|
}
|
||||||
|
|
||||||
$sql_other_categories = mysqli_query($mysqli, "SELECT SUM(income.amount) AS other_income
|
$sql_other_categories = mysqli_query($mysqli, "SELECT SUM(invoices.invoice_amount) AS other_income FROM categories LEFT JOIN TopCategories ON categories.category_id = TopCategories.category_id INNER JOIN invoices ON categories.category_id = invoices.invoice_category_id WHERE TopCategories.category_id IS NULL AND invoice_status = 'Paid' AND YEAR(invoice_date) = $year");
|
||||||
FROM (SELECT invoice_category_id AS income_category_id, payment_amount AS amount
|
|
||||||
FROM payments
|
|
||||||
INNER JOIN invoices ON invoice_id = payment_invoice_id
|
|
||||||
WHERE YEAR(payment_date) = $year AND invoice_category_id > 0
|
|
||||||
UNION ALL
|
|
||||||
SELECT revenue_category_id AS income_category_id, revenue_amount AS amount
|
|
||||||
FROM revenues
|
|
||||||
WHERE YEAR(revenue_date) = $year AND revenue_category_id > 0) AS income
|
|
||||||
LEFT JOIN TopCategories ON TopCategories.category_id = income.income_category_id
|
|
||||||
WHERE TopCategories.category_id IS NULL");
|
|
||||||
$row = mysqli_fetch_assoc($sql_other_categories);
|
$row = mysqli_fetch_assoc($sql_other_categories);
|
||||||
$other_income = floatval($row['other_income']);
|
$other_income = floatval($row['other_income']);
|
||||||
if ($other_income > 0) {
|
if ($other_income > 0) {
|
||||||
|
|||||||
@@ -268,7 +268,12 @@ $num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()"));
|
|||||||
$category_name = escapeHtml($row['category_name']);
|
$category_name = escapeHtml($row['category_name']);
|
||||||
$account_name = escapeHtml($row['account_name']);
|
$account_name = escapeHtml($row['account_name']);
|
||||||
$expense_account_id = intval($row['expense_account_id']);
|
$expense_account_id = intval($row['expense_account_id']);
|
||||||
$client_name = escapeHtml($row['client_name']) ?: '-';
|
$client_name = escapeHtml($row['client_name']);
|
||||||
|
if(empty($client_name)) {
|
||||||
|
$client_name_display = "-";
|
||||||
|
} else {
|
||||||
|
$client_name_display = $client_name;
|
||||||
|
}
|
||||||
$expense_client_id = intval($row['expense_client_id']);
|
$expense_client_id = intval($row['expense_client_id']);
|
||||||
|
|
||||||
if (empty($expense_receipt)) {
|
if (empty($expense_receipt)) {
|
||||||
@@ -302,7 +307,7 @@ $num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()"));
|
|||||||
<td><?= $vendor_name ?></td>
|
<td><?= $vendor_name ?></td>
|
||||||
<td class="text-end font-monospace"><?= numfmt_format_currency($currency_format, $expense_amount, $expense_currency_code) ?></td>
|
<td class="text-end font-monospace"><?= numfmt_format_currency($currency_format, $expense_amount, $expense_currency_code) ?></td>
|
||||||
<td><?= $account_name ?></td>
|
<td><?= $account_name ?></td>
|
||||||
<td><?= $client_name ?></td>
|
<td><?= $client_name_display ?></td>
|
||||||
<td>
|
<td>
|
||||||
<div class="dropdown dropstart text-center">
|
<div class="dropdown dropstart text-center">
|
||||||
<button class="btn btn-secondary btn-sm" type="button" data-bs-toggle="dropdown">
|
<button class="btn btn-secondary btn-sm" type="button" data-bs-toggle="dropdown">
|
||||||
|
|||||||
@@ -52,13 +52,10 @@ $sql_total_cancelled_amount = mysqli_query($mysqli, "SELECT SUM(invoice_amount)
|
|||||||
$row = mysqli_fetch_assoc($sql_total_cancelled_amount);
|
$row = mysqli_fetch_assoc($sql_total_cancelled_amount);
|
||||||
$total_cancelled_amount = floatval($row['total_cancelled_amount']);
|
$total_cancelled_amount = floatval($row['total_cancelled_amount']);
|
||||||
|
|
||||||
$sql_total_partial_amount = mysqli_query($mysqli, "SELECT SUM(invoice_amount) AS total_partial_amount FROM invoices WHERE invoice_status = 'Partial' $client_query");
|
$sql_total_partial_amount = mysqli_query($mysqli, "SELECT SUM(invoice_amount) AS total_partial_amount FROM payments, invoices WHERE payment_invoice_id = invoice_id AND invoice_status = 'Partial' $client_query");
|
||||||
$row = mysqli_fetch_assoc($sql_total_partial_amount);
|
$row = mysqli_fetch_assoc($sql_total_partial_amount);
|
||||||
$total_partial_amount = floatval($row['total_partial_amount']);
|
$total_partial_amount = floatval($row['total_partial_amount']);
|
||||||
|
$total_partial_count = mysqli_num_rows($sql_total_partial_amount);
|
||||||
$sql_total_partial_paid_amount = mysqli_query($mysqli, "SELECT SUM(payment_amount) AS total_partial_paid_amount FROM payments, invoices WHERE payment_invoice_id = invoice_id AND invoice_status = 'Partial' $client_query");
|
|
||||||
$row = mysqli_fetch_assoc($sql_total_partial_paid_amount);
|
|
||||||
$total_partial_paid_amount = floatval($row['total_partial_paid_amount']);
|
|
||||||
|
|
||||||
$sql_total_overdue_partial_amount = mysqli_query($mysqli, "SELECT SUM(payment_amount) AS total_overdue_partial_amount FROM payments, invoices WHERE payment_invoice_id = invoice_id AND invoice_status = 'Partial' AND invoice_due < CURDATE() $client_query");
|
$sql_total_overdue_partial_amount = mysqli_query($mysqli, "SELECT SUM(payment_amount) AS total_overdue_partial_amount FROM payments, invoices WHERE payment_invoice_id = invoice_id AND invoice_status = 'Partial' AND invoice_due < CURDATE() $client_query");
|
||||||
$row = mysqli_fetch_assoc($sql_total_overdue_partial_amount);
|
$row = mysqli_fetch_assoc($sql_total_overdue_partial_amount);
|
||||||
@@ -69,7 +66,7 @@ $row = mysqli_fetch_assoc($sql_total_overdue_amount);
|
|||||||
$total_overdue_amount = floatval($row['total_overdue_amount']);
|
$total_overdue_amount = floatval($row['total_overdue_amount']);
|
||||||
|
|
||||||
$real_overdue_amount = $total_overdue_amount - $total_overdue_partial_amount;
|
$real_overdue_amount = $total_overdue_amount - $total_overdue_partial_amount;
|
||||||
$total_unpaid_amount = $total_sent_amount + $total_viewed_amount + $total_partial_amount - $total_partial_paid_amount;
|
$total_unpaid_amount = $total_sent_amount + $total_viewed_amount + $total_partial_amount;
|
||||||
$unpaid_count = $sent_count + $viewed_count + $partial_count;
|
$unpaid_count = $sent_count + $viewed_count + $partial_count;
|
||||||
|
|
||||||
$overdue_query = '';
|
$overdue_query = '';
|
||||||
@@ -101,13 +98,10 @@ $sql = mysqli_query(
|
|||||||
invoice_amount, invoice_created_at, invoice_currency_code, invoice_date,
|
invoice_amount, invoice_created_at, invoice_currency_code, invoice_date,
|
||||||
invoice_discount_amount, invoice_due, invoice_id, invoice_number, invoice_prefix,
|
invoice_discount_amount, invoice_due, invoice_id, invoice_number, invoice_prefix,
|
||||||
invoice_scope, invoice_status, recurring_invoice_id, recurring_invoice_number,
|
invoice_scope, invoice_status, recurring_invoice_id, recurring_invoice_number,
|
||||||
recurring_invoice_prefix, IFNULL(invoice_payments.amount_paid, 0) AS amount_paid FROM invoices
|
recurring_invoice_prefix FROM invoices
|
||||||
LEFT JOIN clients ON invoice_client_id = client_id
|
LEFT JOIN clients ON invoice_client_id = client_id
|
||||||
LEFT JOIN categories ON invoice_category_id = category_id
|
LEFT JOIN categories ON invoice_category_id = category_id
|
||||||
LEFT JOIN recurring_invoices ON invoice_recurring_invoice_id = recurring_invoice_id
|
LEFT JOIN recurring_invoices ON invoice_recurring_invoice_id = recurring_invoice_id
|
||||||
LEFT JOIN (SELECT payment_invoice_id, SUM(payment_amount) AS amount_paid
|
|
||||||
FROM payments
|
|
||||||
GROUP BY payment_invoice_id) AS invoice_payments ON payment_invoice_id = invoice_id
|
|
||||||
WHERE ($status_query)
|
WHERE ($status_query)
|
||||||
$overdue_query
|
$overdue_query
|
||||||
$category_query
|
$category_query
|
||||||
@@ -345,8 +339,6 @@ $num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()"));
|
|||||||
$invoice_due = escapeHtml($row['invoice_due']);
|
$invoice_due = escapeHtml($row['invoice_due']);
|
||||||
$invoice_discount = floatval($row['invoice_discount_amount']);
|
$invoice_discount = floatval($row['invoice_discount_amount']);
|
||||||
$invoice_amount = floatval($row['invoice_amount']);
|
$invoice_amount = floatval($row['invoice_amount']);
|
||||||
$amount_paid = floatval($row['amount_paid']);
|
|
||||||
$invoice_balance = $invoice_amount - $amount_paid;
|
|
||||||
$invoice_currency_code = escapeHtml($row['invoice_currency_code']);
|
$invoice_currency_code = escapeHtml($row['invoice_currency_code']);
|
||||||
$invoice_created_at = escapeHtml($row['invoice_created_at']);
|
$invoice_created_at = escapeHtml($row['invoice_created_at']);
|
||||||
$client_id = intval($row['client_id']);
|
$client_id = intval($row['client_id']);
|
||||||
@@ -403,12 +395,7 @@ $num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()"));
|
|||||||
<?php if (!$client_url) { ?>
|
<?php if (!$client_url) { ?>
|
||||||
<td class="text-bold"><a href="invoices.php?client_id=<?= $client_id ?>"><?= $client_name ?></a></td>
|
<td class="text-bold"><a href="invoices.php?client_id=<?= $client_id ?>"><?= $client_name ?></a></td>
|
||||||
<?php } ?>
|
<?php } ?>
|
||||||
<td class="text-end font-monospace">
|
<td class="text-end font-monospace"><?= numfmt_format_currency($currency_format, $invoice_amount, $invoice_currency_code) ?></td>
|
||||||
<?= numfmt_format_currency($currency_format, $invoice_amount, $invoice_currency_code) ?>
|
|
||||||
<?php if ($amount_paid > 0 && $invoice_balance > 0) { ?>
|
|
||||||
<br><small class="text-danger"><?= numfmt_format_currency($currency_format, $invoice_balance, $invoice_currency_code) ?> due</small>
|
|
||||||
<?php } ?>
|
|
||||||
</td>
|
|
||||||
<td><?= $invoice_date ?></td>
|
<td><?= $invoice_date ?></td>
|
||||||
<td class="<?= $overdue_color ?>"><?= $invoice_due ?></td>
|
<td class="<?= $overdue_color ?>"><?= $invoice_due ?></td>
|
||||||
<td><?= $category_name ?></td>
|
<td><?= $category_name ?></td>
|
||||||
|
|||||||
@@ -86,12 +86,12 @@ ob_start();
|
|||||||
$sql = mysqli_query(
|
$sql = mysqli_query(
|
||||||
$mysqli,
|
$mysqli,
|
||||||
"SELECT user_id, user_name FROM users
|
"SELECT user_id, user_name FROM users
|
||||||
WHERE user_type = 1 AND user_status = 1 AND user_archived_at IS NULL ORDER BY user_name ASC"
|
WHERE user_role_id > 1 AND user_status = 1 AND user_archived_at IS NULL ORDER BY user_name ASC"
|
||||||
);
|
);
|
||||||
while ($row = mysqli_fetch_assoc($sql)) {
|
while ($row = mysqli_fetch_assoc($sql)) {
|
||||||
$user_id = intval($row['user_id']);
|
$user_id = intval($row['user_id']);
|
||||||
$user_name = escapeHtml($row['user_name']); ?>
|
$user_name = escapeHtml($row['user_name']); ?>
|
||||||
<option <?php if ($session_user_id == $user_id) { echo "selected"; } ?> value="<?= $user_id ?>"><?= $user_name ?></option>
|
<option value="<?= $user_id ?>"><?= $user_name ?></option>
|
||||||
<?php } ?>
|
<?php } ?>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -161,7 +161,7 @@ ob_start();
|
|||||||
while ($row = mysqli_fetch_assoc($sql)) {
|
while ($row = mysqli_fetch_assoc($sql)) {
|
||||||
$user_id = intval($row['user_id']);
|
$user_id = intval($row['user_id']);
|
||||||
$user_name = escapeHtml($row['user_name']); ?>
|
$user_name = escapeHtml($row['user_name']); ?>
|
||||||
<option <?php if ($session_user_id == $user_id) { echo "selected"; } ?> value="<?= $user_id ?>"><?= $user_name ?></option>
|
<option value="<?= $user_id ?>"><?= $user_name ?></option>
|
||||||
<?php } ?>
|
<?php } ?>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -224,7 +224,7 @@ if (mysqli_num_rows($sql) == 0) {
|
|||||||
<form id="bulkActions" action="post.php" method="post">
|
<form id="bulkActions" action="post.php" method="post">
|
||||||
<input type="hidden" name="csrf_token" value="<?= $_SESSION['csrf_token'] ?>">
|
<input type="hidden" name="csrf_token" value="<?= $_SESSION['csrf_token'] ?>">
|
||||||
|
|
||||||
<table class="table table-striped table-borderless table-hover table-sm mb-0">
|
<table class="table table-striped table-borderless table-hover mb-0">
|
||||||
<thead class="text-dark <?php if ($num_rows[0] == 0) { echo "d-none"; } ?>">
|
<thead class="text-dark <?php if ($num_rows[0] == 0) { echo "d-none"; } ?>">
|
||||||
<tr>
|
<tr>
|
||||||
<td class="checkbox-column border-end">
|
<td class="checkbox-column border-end">
|
||||||
@@ -233,17 +233,17 @@ if (mysqli_num_rows($sql) == 0) {
|
|||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<th>
|
<th>
|
||||||
<a class="text-dark" href="?<?= $url_query_strings_sort ?>&sort=ip_address&order=<?= $disp ?>">
|
<a class="text-secondary" href="?<?= $url_query_strings_sort ?>&sort=ip_address&order=<?= $disp ?>">
|
||||||
IP Address <?php if ($sort == 'ip_address') { echo $order_icon; } ?>
|
IP Address <?php if ($sort == 'ip_address') { echo $order_icon; } ?>
|
||||||
</a>
|
</a>
|
||||||
</th>
|
</th>
|
||||||
<th>
|
<th>
|
||||||
<a class="text-dark" href="?<?= $url_query_strings_sort ?>&sort=ip_hostname&order=<?= $disp ?>">
|
<a class="text-secondary" href="?<?= $url_query_strings_sort ?>&sort=ip_hostname&order=<?= $disp ?>">
|
||||||
Hostname <?php if ($sort == 'ip_hostname') { echo $order_icon; } ?>
|
Hostname <?php if ($sort == 'ip_hostname') { echo $order_icon; } ?>
|
||||||
</a>
|
</a>
|
||||||
</th>
|
</th>
|
||||||
<th>
|
<th>
|
||||||
<a class="text-dark" href="?<?= $url_query_strings_sort ?>&sort=ip_description&order=<?= $disp ?>">
|
<a class="text-secondary" href="?<?= $url_query_strings_sort ?>&sort=ip_description&order=<?= $disp ?>">
|
||||||
Description <?php if ($sort == 'ip_description') { echo $order_icon; } ?>
|
Description <?php if ($sort == 'ip_description') { echo $order_icon; } ?>
|
||||||
</a>
|
</a>
|
||||||
</th>
|
</th>
|
||||||
@@ -256,8 +256,8 @@ if (mysqli_num_rows($sql) == 0) {
|
|||||||
while ($row = mysqli_fetch_assoc($sql_ips)) {
|
while ($row = mysqli_fetch_assoc($sql_ips)) {
|
||||||
$ip_id = intval($row['ip_id']);
|
$ip_id = intval($row['ip_id']);
|
||||||
$ip_address = escapeHtml($row['ip_address']);
|
$ip_address = escapeHtml($row['ip_address']);
|
||||||
$ip_hostname = escapeHtml($row['ip_hostname']) ?: '-';
|
$ip_hostname = escapeHtml($row['ip_hostname']);
|
||||||
$ip_description = escapeHtml($row['ip_description']) ?: '-';
|
$ip_description = escapeHtml($row['ip_description']);
|
||||||
|
|
||||||
?>
|
?>
|
||||||
<tr>
|
<tr>
|
||||||
|
|||||||
@@ -192,8 +192,7 @@ if (isset($_POST['bulk_force_recurring_tickets'])) {
|
|||||||
$email_subject = "Ticket Created - [$ticket_prefix$ticket_number] - $ticket_subject (scheduled)";
|
$email_subject = "Ticket Created - [$ticket_prefix$ticket_number] - $ticket_subject (scheduled)";
|
||||||
// SLA response commitment for this client + priority, empty when no SLA applies
|
// SLA response commitment for this client + priority, empty when no SLA applies
|
||||||
$sla_notice = escapeSql(getTicketSlaEmailNotice($id, $company_phone));
|
$sla_notice = escapeSql(getTicketSlaEmailNotice($id, $company_phone));
|
||||||
$ticket_status_name = escapeSql(getTicketStatusName($ticket_status));
|
$email_body = "<i style=\'color: #808080\'>##- Please type your reply above this line -##</i><br><br>Hello $contact_name,<br><br>A ticket regarding \"$ticket_subject\" has been automatically created for you.<br><br>--------------------------------<br>$ticket_details--------------------------------<br><br>Ticket: $ticket_prefix$ticket_number<br>Subject: $ticket_subject<br>Status: Open<br>Portal: https://$config_base_url/client/ticket.php?id=$id$sla_notice<br><br>--<br>$company_name - Support<br>$config_ticket_from_email<br>$company_phone";
|
||||||
$email_body = "<i style=\'color: #808080\'>##- Please type your reply above this line -##</i><br><br>Hello $contact_name,<br><br>A ticket regarding \"$ticket_subject\" has been automatically created for you.<br><br>--------------------------------<br>$ticket_details--------------------------------<br><br>Ticket: $ticket_prefix$ticket_number<br>Subject: $ticket_subject<br>Status: $ticket_status_name<br>Portal: https://$config_base_url/client/ticket.php?id=$id$sla_notice<br><br>--<br>$company_name - Support<br>$config_ticket_from_email<br>$company_phone";
|
|
||||||
|
|
||||||
$email = [
|
$email = [
|
||||||
'from' => $config_ticket_from_email,
|
'from' => $config_ticket_from_email,
|
||||||
@@ -342,8 +341,7 @@ if (isset($_GET['force_recurring_ticket'])) {
|
|||||||
$email_subject = "Ticket created - [$ticket_prefix$ticket_number] - $ticket_subject (scheduled)";
|
$email_subject = "Ticket created - [$ticket_prefix$ticket_number] - $ticket_subject (scheduled)";
|
||||||
// SLA response commitment for this client + priority, empty when no SLA applies
|
// SLA response commitment for this client + priority, empty when no SLA applies
|
||||||
$sla_notice = escapeSql(getTicketSlaEmailNotice($id, $company_phone));
|
$sla_notice = escapeSql(getTicketSlaEmailNotice($id, $company_phone));
|
||||||
$ticket_status_name = escapeSql(getTicketStatusName($ticket_status));
|
$email_body = "<i style=\'color: #808080\'>##- Please type your reply above this line -##</i><br><br>Hello $contact_name,<br><br>A ticket regarding \"$ticket_subject\" has been automatically created for you.<br><br>--------------------------------<br>$ticket_details--------------------------------<br><br>Ticket: $ticket_prefix$ticket_number<br>Subject: $ticket_subject<br>Status: Open<br>Portal: https://$config_base_url/client/ticket.php?id=$id$sla_notice<br><br>--<br>$company_name - Support<br>$config_ticket_from_email<br>$company_phone";
|
||||||
$email_body = "<i style=\'color: #808080\'>##- Please type your reply above this line -##</i><br><br>Hello $contact_name,<br><br>A ticket regarding \"$ticket_subject\" has been automatically created for you.<br><br>--------------------------------<br>$ticket_details--------------------------------<br><br>Ticket: $ticket_prefix$ticket_number<br>Subject: $ticket_subject<br>Status: $ticket_status_name<br>Portal: https://$config_base_url/client/ticket.php?id=$id$sla_notice<br><br>--<br>$company_name - Support<br>$config_ticket_from_email<br>$company_phone";
|
|
||||||
|
|
||||||
$email = [
|
$email = [
|
||||||
'from' => $config_ticket_from_email,
|
'from' => $config_ticket_from_email,
|
||||||
|
|||||||
@@ -142,7 +142,7 @@ if (isset($_POST['add_ticket'])) {
|
|||||||
$subject = "Ticket Created [$ticket_prefix$ticket_number] - $ticket_subject";
|
$subject = "Ticket Created [$ticket_prefix$ticket_number] - $ticket_subject";
|
||||||
// SLA response commitment for this client + priority, empty when no SLA applies
|
// SLA response commitment for this client + priority, empty when no SLA applies
|
||||||
$sla_notice = escapeSql(getTicketSlaEmailNotice($ticket_id, $company_phone));
|
$sla_notice = escapeSql(getTicketSlaEmailNotice($ticket_id, $company_phone));
|
||||||
$body = "<i style=\'color: #808080\'>##- Please type your reply above this line -##</i><br><br>Hello $contact_name,<br><br>A ticket regarding \"$ticket_subject\" has been created for you.<br><br>--------------------------------<br>$ticket_details--------------------------------<br><br>Ticket: $ticket_prefix$ticket_number<br>Subject: $ticket_subject<br>Status: $ticket_status_name<br>Portal: <a href=\'https://$config_base_url/guest/guest_view_ticket.php?ticket_id=$ticket_id&url_key=$url_key\'>View ticket</a>$sla_notice<br><br>--<br>$company_name - Support<br>$config_ticket_from_email<br>$company_phone";
|
$body = "<i style=\'color: #808080\'>##- Please type your reply above this line -##</i><br><br>Hello $contact_name,<br><br>A ticket regarding \"$ticket_subject\" has been created for you.<br><br>--------------------------------<br>$ticket_details--------------------------------<br><br>Ticket: $ticket_prefix$ticket_number<br>Subject: $ticket_subject<br>Status: Open<br>Portal: <a href=\'https://$config_base_url/guest/guest_view_ticket.php?ticket_id=$ticket_id&url_key=$url_key\'>View ticket</a>$sla_notice<br><br>--<br>$company_name - Support<br>$config_ticket_from_email<br>$company_phone";
|
||||||
|
|
||||||
// Verify contact email is valid
|
// Verify contact email is valid
|
||||||
if (filter_var($contact_email, FILTER_VALIDATE_EMAIL)) {
|
if (filter_var($contact_email, FILTER_VALIDATE_EMAIL)) {
|
||||||
|
|||||||
@@ -232,7 +232,12 @@ $num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()"));
|
|||||||
while ($row = mysqli_fetch_assoc($sql)) {
|
while ($row = mysqli_fetch_assoc($sql)) {
|
||||||
$product_id = intval($row['product_id']);
|
$product_id = intval($row['product_id']);
|
||||||
$product_name = escapeHtml($row['product_name']);
|
$product_name = escapeHtml($row['product_name']);
|
||||||
$product_description = escapeHtml($row['product_description']) ?: '-';
|
$product_description = escapeHtml($row['product_description']);
|
||||||
|
if (empty($product_description)) {
|
||||||
|
$product_description_display = "-";
|
||||||
|
} else {
|
||||||
|
$product_description_display = "<div style='white-space:pre-line'>$product_description</div>";
|
||||||
|
}
|
||||||
$product_qty = intval($row['product_qty']);
|
$product_qty = intval($row['product_qty']);
|
||||||
$product_code = escapeHtml($row['product_code']);
|
$product_code = escapeHtml($row['product_code']);
|
||||||
$product_location = escapeHtml($row['product_location']) ?: '-';
|
$product_location = escapeHtml($row['product_location']) ?: '-';
|
||||||
@@ -262,7 +267,7 @@ $num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()"));
|
|||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td><?= $category_name ?></td>
|
<td><?= $category_name ?></td>
|
||||||
<td><div style='white-space:pre-line'><?= $product_description ?></div></td>
|
<td><?= $product_description_display ?></td>
|
||||||
<?php if ($type_filter == 'product') { ?>
|
<?php if ($type_filter == 'product') { ?>
|
||||||
<td><?= $product_qty ?></td>
|
<td><?= $product_qty ?></td>
|
||||||
<td><?= $product_location ?></td>
|
<td><?= $product_location ?></td>
|
||||||
|
|||||||
@@ -86,19 +86,21 @@ $sql_tax = mysqli_query($mysqli, "SELECT `tax_name` FROM `taxes`");
|
|||||||
|
|
||||||
if ($view == 'monthly') {
|
if ($view == 'monthly') {
|
||||||
|
|
||||||
// Row total = sum of this tax’s 12 months, accumulated as we go
|
|
||||||
$row_total = 0.0;
|
|
||||||
for ($i = 1; $i <= 12; $i++) {
|
for ($i = 1; $i <= 12; $i++) {
|
||||||
$monthly_tax = (float) getMonthlyTax($tax_name, $i, $year, $mysqli);
|
$monthly_tax = (float) getMonthlyTax($tax_name, $i, $year, $mysqli);
|
||||||
|
|
||||||
// Accumulate totals
|
// Accumulate totals
|
||||||
$monthly_totals[$i] += $monthly_tax;
|
$monthly_totals[$i] += $monthly_tax;
|
||||||
$grand_total += $monthly_tax;
|
$grand_total += $monthly_tax;
|
||||||
$row_total += $monthly_tax;
|
|
||||||
|
|
||||||
echo "<td class='text-end'>" . numfmt_format_currency($currency_format, $monthly_tax, $company_currency) . "</td>";
|
echo "<td class='text-end'>" . numfmt_format_currency($currency_format, $monthly_tax, $company_currency) . "</td>";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Row total = sum of this tax’s 12 months
|
||||||
|
$row_total = 0.0;
|
||||||
|
for ($i = 1; $i <= 12; $i++) {
|
||||||
|
$row_total += (float) getMonthlyTax($tax_name, $i, $year, $mysqli);
|
||||||
|
}
|
||||||
echo "<td class='text-end text-bold'>" . numfmt_format_currency($currency_format, $row_total, $company_currency) . "</td>";
|
echo "<td class='text-end text-bold'>" . numfmt_format_currency($currency_format, $row_total, $company_currency) . "</td>";
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -12,10 +12,10 @@ $contact_id = intval($_POST['contact_id']);
|
|||||||
$delete_count = false;
|
$delete_count = false;
|
||||||
|
|
||||||
if (!empty($contact_id)) {
|
if (!empty($contact_id)) {
|
||||||
$row = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT contact_name FROM contacts WHERE contact_id = $contact_id AND contact_client_id = $client_id AND contact_archived_at IS NOT NULL LIMIT 1"));
|
$row = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT contact_name FROM contacts WHERE contact_id = $contact_id AND contact_client_id = $client_id LIMIT 1"));
|
||||||
$contact_name = escapeSql($row['contact_name'] ?? '');
|
$contact_name = escapeSql($row['contact_name'] ?? '');
|
||||||
|
|
||||||
$delete_sql = mysqli_query($mysqli, "DELETE FROM contacts WHERE contact_id = $contact_id AND contact_client_id = $client_id AND contact_archived_at IS NOT NULL LIMIT 1");
|
$delete_sql = mysqli_query($mysqli, "DELETE FROM contacts WHERE contact_id = $contact_id AND contact_client_id = $client_id LIMIT 1");
|
||||||
|
|
||||||
// Check delete & get affected rows
|
// Check delete & get affected rows
|
||||||
if ($delete_sql && !empty($contact_name)) {
|
if ($delete_sql && !empty($contact_name)) {
|
||||||
|
|||||||
@@ -32,11 +32,6 @@ if (!empty($subject)) {
|
|||||||
$contact = intval($row['contact_id']);
|
$contact = intval($row['contact_id']);
|
||||||
}
|
}
|
||||||
|
|
||||||
$ticket_status = 1; // Default
|
|
||||||
if ($assigned_to > 0) {
|
|
||||||
$ticket_status = 2; // Set to open if we've auto-assigned an agent
|
|
||||||
}
|
|
||||||
|
|
||||||
// Atomically increment and get the new ticket number
|
// Atomically increment and get the new ticket number
|
||||||
mysqli_query($mysqli, "
|
mysqli_query($mysqli, "
|
||||||
UPDATE settings
|
UPDATE settings
|
||||||
@@ -50,7 +45,7 @@ if (!empty($subject)) {
|
|||||||
|
|
||||||
// Insert ticket
|
// Insert ticket
|
||||||
$url_key = randomString(32);
|
$url_key = randomString(32);
|
||||||
$insert_sql = mysqli_query($mysqli,"INSERT INTO tickets SET ticket_prefix = '$config_ticket_prefix', ticket_number = $ticket_number, ticket_source = 'API', ticket_subject = '$subject', ticket_details = '$details', ticket_priority = '$priority', ticket_status = $ticket_status, ticket_billable = $billable, ticket_vendor_ticket_number = '$vendor_ticket_number', ticket_vendor_id = $vendor_id, ticket_created_by = 0, ticket_assigned_to = $assigned_to, ticket_contact_id = $contact, ticket_asset_id = $asset, ticket_url_key = '$url_key', ticket_client_id = $client_id");
|
$insert_sql = mysqli_query($mysqli,"INSERT INTO tickets SET ticket_prefix = '$config_ticket_prefix', ticket_number = $ticket_number, ticket_source = 'API', ticket_subject = '$subject', ticket_details = '$details', ticket_priority = '$priority', ticket_status = 1, ticket_billable = $billable, ticket_vendor_ticket_number = '$vendor_ticket_number', ticket_vendor_id = $vendor_id, ticket_created_by = 0, ticket_assigned_to = $assigned_to, ticket_contact_id = $contact, ticket_asset_id = $asset, ticket_url_key = '$url_key', ticket_client_id = $client_id");
|
||||||
|
|
||||||
// Check insert & get insert ID
|
// Check insert & get insert ID
|
||||||
if ($insert_sql) {
|
if ($insert_sql) {
|
||||||
|
|||||||
@@ -443,8 +443,7 @@ if (mysqli_num_rows($sql_recurring_tickets) > 0) {
|
|||||||
$email_subject = "Ticket created - [$ticket_prefix$ticket_number] - $ticket_subject (scheduled)";
|
$email_subject = "Ticket created - [$ticket_prefix$ticket_number] - $ticket_subject (scheduled)";
|
||||||
// SLA response commitment for this client + priority, empty when no SLA applies
|
// SLA response commitment for this client + priority, empty when no SLA applies
|
||||||
$sla_notice = escapeSql(getTicketSlaEmailNotice($id, $company_phone));
|
$sla_notice = escapeSql(getTicketSlaEmailNotice($id, $company_phone));
|
||||||
$ticket_status_name = escapeSql(getTicketStatusName($ticket_status));
|
$email_body = "<i style=\'color: #808080\'>##- Please type your reply above this line -##</i><br><br>Hello $contact_name,<br><br>A ticket regarding \"$ticket_subject\" has been automatically created for you.<br><br>--------------------------------<br>$ticket_details--------------------------------<br><br>Ticket: $ticket_prefix$ticket_number<br>Subject: $ticket_subject<br>Status: Open<br>Portal: https://$config_base_url/client/ticket.php?id=$id$sla_notice<br><br>--<br>$company_name - Support<br>$config_ticket_from_email<br>$company_phone";
|
||||||
$email_body = "<i style=\'color: #808080\'>##- Please type your reply above this line -##</i><br><br>Hello $contact_name,<br><br>A ticket regarding \"$ticket_subject\" has been automatically created for you.<br><br>--------------------------------<br>$ticket_details--------------------------------<br><br>Ticket: $ticket_prefix$ticket_number<br>Subject: $ticket_subject<br>Status: $ticket_status_name<br>Portal: https://$config_base_url/client/ticket.php?id=$id$sla_notice<br><br>--<br>$company_name - Support<br>$config_ticket_from_email<br>$company_phone";
|
|
||||||
|
|
||||||
$email = [
|
$email = [
|
||||||
'from' => $config_ticket_from_email,
|
'from' => $config_ticket_from_email,
|
||||||
|
|||||||
@@ -644,20 +644,12 @@ function checkForUpdates() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getMonthlyTax($tax_name, $month, $year, $mysqli) {
|
function getMonthlyTax($tax_name, $month, $year, $mysqli) {
|
||||||
// Cash basis - tax is booked to the month the money arrived, in proportion to
|
// SQL to calculate monthly tax
|
||||||
// how much of the invoice that payment covered. Driving off payments (rather
|
$sql = "SELECT SUM(item_tax) AS monthly_tax FROM invoice_items
|
||||||
// than invoice_items) counts each payment exactly once, and pre-aggregating
|
LEFT JOIN invoices ON invoice_items.item_invoice_id = invoices.invoice_id
|
||||||
// the line items stops a multi-payment invoice multiplying its own tax.
|
LEFT JOIN payments ON invoices.invoice_id = payments.payment_invoice_id
|
||||||
$sql = "SELECT SUM(invoice_tax.tax_total * (payments.payment_amount / invoices.invoice_amount)) AS monthly_tax
|
|
||||||
FROM payments
|
|
||||||
INNER JOIN invoices ON invoices.invoice_id = payments.payment_invoice_id
|
|
||||||
INNER JOIN (SELECT item_invoice_id, SUM(item_tax) AS tax_total
|
|
||||||
FROM invoice_items
|
|
||||||
WHERE item_tax_id = (SELECT tax_id FROM taxes WHERE tax_name = '$tax_name')
|
|
||||||
GROUP BY item_invoice_id) AS invoice_tax
|
|
||||||
ON invoice_tax.item_invoice_id = invoices.invoice_id
|
|
||||||
WHERE YEAR(payments.payment_date) = $year AND MONTH(payments.payment_date) = $month
|
WHERE YEAR(payments.payment_date) = $year AND MONTH(payments.payment_date) = $month
|
||||||
AND invoices.invoice_amount > 0";
|
AND invoice_items.item_tax_id = (SELECT tax_id FROM taxes WHERE tax_name = '$tax_name')";
|
||||||
$result = mysqli_query($mysqli, $sql);
|
$result = mysqli_query($mysqli, $sql);
|
||||||
$row = mysqli_fetch_assoc($result);
|
$row = mysqli_fetch_assoc($result);
|
||||||
return $row['monthly_tax'] ?? 0;
|
return $row['monthly_tax'] ?? 0;
|
||||||
@@ -668,17 +660,12 @@ function getQuarterlyTax($tax_name, $quarter, $year, $mysqli) {
|
|||||||
$start_month = ($quarter - 1) * 3 + 1;
|
$start_month = ($quarter - 1) * 3 + 1;
|
||||||
$end_month = $start_month + 2;
|
$end_month = $start_month + 2;
|
||||||
|
|
||||||
// SQL to calculate quarterly tax - see getMonthlyTax for why it is shaped this way
|
// SQL to calculate quarterly tax
|
||||||
$sql = "SELECT SUM(invoice_tax.tax_total * (payments.payment_amount / invoices.invoice_amount)) AS quarterly_tax
|
$sql = "SELECT SUM(item_tax) AS quarterly_tax FROM invoice_items
|
||||||
FROM payments
|
LEFT JOIN invoices ON invoice_items.item_invoice_id = invoices.invoice_id
|
||||||
INNER JOIN invoices ON invoices.invoice_id = payments.payment_invoice_id
|
LEFT JOIN payments ON invoices.invoice_id = payments.payment_invoice_id
|
||||||
INNER JOIN (SELECT item_invoice_id, SUM(item_tax) AS tax_total
|
|
||||||
FROM invoice_items
|
|
||||||
WHERE item_tax_id = (SELECT tax_id FROM taxes WHERE tax_name = '$tax_name')
|
|
||||||
GROUP BY item_invoice_id) AS invoice_tax
|
|
||||||
ON invoice_tax.item_invoice_id = invoices.invoice_id
|
|
||||||
WHERE YEAR(payments.payment_date) = $year AND MONTH(payments.payment_date) BETWEEN $start_month AND $end_month
|
WHERE YEAR(payments.payment_date) = $year AND MONTH(payments.payment_date) BETWEEN $start_month AND $end_month
|
||||||
AND invoices.invoice_amount > 0";
|
AND invoice_items.item_tax_id = (SELECT tax_id FROM taxes WHERE tax_name = '$tax_name')";
|
||||||
$result = mysqli_query($mysqli, $sql);
|
$result = mysqli_query($mysqli, $sql);
|
||||||
$row = mysqli_fetch_assoc($result);
|
$row = mysqli_fetch_assoc($result);
|
||||||
return $row['quarterly_tax'] ?? 0;
|
return $row['quarterly_tax'] ?? 0;
|
||||||
|
|||||||
@@ -5,4 +5,4 @@
|
|||||||
* Update this file each time we merge develop into master. Format is YY.MM (add a .v if there is more than one release a month.
|
* Update this file each time we merge develop into master. Format is YY.MM (add a .v if there is more than one release a month.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
DEFINE("APP_VERSION", "26.09.2");
|
DEFINE("APP_VERSION", "26.09");
|
||||||
|
|||||||
27
js/app.js
27
js/app.js
@@ -568,26 +568,15 @@ function itflowInit() {
|
|||||||
el.dataset.imaskReady = '1';
|
el.dataset.imaskReady = '1';
|
||||||
var spec = el.getAttribute('data-inputmask') || '';
|
var spec = el.getAttribute('data-inputmask') || '';
|
||||||
if (spec.indexOf('ip') !== -1) {
|
if (spec.indexOf('ip') !== -1) {
|
||||||
// A pattern mask built from MaskedRange blocks will not advance past the dot
|
|
||||||
// until the block reaches its maxLength, so 10.0.0.1 had to be entered as
|
|
||||||
// 010.000.000.001. A regex mask has no per-block completeness rule - it tests
|
|
||||||
// the whole value on every keystroke, so partial input like "10.0." is valid on
|
|
||||||
// its own. Octets are still bounded to 0-255 and leading zeros are still
|
|
||||||
// accepted, matching what jquery.inputmask's 'ip' alias allowed.
|
|
||||||
//
|
|
||||||
// interface_ip / asset_ip are varchar(200) and hold free text as well - 'DHCP'
|
|
||||||
// is written there by the checkbox on these same modals. Masking a value like
|
|
||||||
// that would strip it to nothing the moment the modal opened, so anything not
|
|
||||||
// made of digits and dots is left unmasked instead.
|
|
||||||
if (el.value && !/^[\d.]*$/.test(el.value)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
var octet = '(25[0-5]|2[0-4]\\d|[01]?\\d\\d?)';
|
|
||||||
var octet4 = octet + '?';
|
|
||||||
var octet3 = '(' + octet + '(\\.' + octet4 + ')?)?';
|
|
||||||
var octet2 = '(' + octet + '(\\.' + octet3 + ')?)?';
|
|
||||||
IMask(el, {
|
IMask(el, {
|
||||||
mask: new RegExp('^(' + octet + '(\\.' + octet2 + ')?)?$')
|
mask: 'a.b.c.d',
|
||||||
|
blocks: {
|
||||||
|
a: { mask: IMask.MaskedRange, from: 0, to: 255 },
|
||||||
|
b: { mask: IMask.MaskedRange, from: 0, to: 255 },
|
||||||
|
c: { mask: IMask.MaskedRange, from: 0, to: 255 },
|
||||||
|
d: { mask: IMask.MaskedRange, from: 0, to: 255 }
|
||||||
|
},
|
||||||
|
lazy: true
|
||||||
});
|
});
|
||||||
} else if (spec.indexOf('mac') !== -1) {
|
} else if (spec.indexOf('mac') !== -1) {
|
||||||
IMask(el, {
|
IMask(el, {
|
||||||
|
|||||||
Reference in New Issue
Block a user