mirror of
https://github.com/itflow-org/itflow
synced 2026-09-15 19:25:12 +00:00
Compare commits
1 Commits
feat-recur
...
v26.09
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
61bb9dd7cb |
59
CHANGELOG.md
59
CHANGELOG.md
@@ -2,65 +2,6 @@
|
|||||||
|
|
||||||
This file documents all notable changes made to ITFlow.
|
This file documents all notable changes made to ITFlow.
|
||||||
|
|
||||||
## [26.09.3] Maint Release
|
|
||||||
|
|
||||||
### Upgrading to 26.09.3
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
- New tickets are now assigned to whoever is creating them. The assignee on the ticket form and on the client bulk-add form starts on your own name instead of Unassigned, and you can still pick anyone else or Unassigned before saving.
|
|
||||||
- API: a ticket created with an assignee now comes in as Open rather than New, matching what happens when an agent assigns a ticket by hand. Anything of yours that watches for New tickets to pick up work will no longer see assigned ones.
|
|
||||||
- Tax Summary and the dashboard Income by Category chart will show different numbers than they did before. Both were wrong wherever an invoice had been partly paid, and both usually read high. The corrected figures are described under Bug Fixes.
|
|
||||||
|
|
||||||
### New Features & Updates
|
|
||||||
|
|
||||||
- Invoices: a partly paid invoice shows what is still owed in red underneath the invoice total in the list, so you can see the outstanding balance without opening the invoice.
|
|
||||||
|
|
||||||
### Bug Fixes
|
|
||||||
|
|
||||||
- Invoices: the Unpaid figure at the top of the invoice list counted the whole of every partly paid invoice rather than what was left on it, and counted that invoice again for each payment against it. An invoice for $1,000 with three payments of $100 added $3,000 to Unpaid; it now adds the $700 that is actually outstanding.
|
|
||||||
- Reports: Tax Summary counted an invoice's full tax once per payment recorded against it, so a partly paid invoice with three payments reported three times its tax. Tax is now booked to the month the money came in, in proportion to how much of the invoice that payment covered, and each payment is counted once. Yearly totals also no longer disagree with the months they are made of.
|
|
||||||
- Dashboard: the Income by Category chart only counted invoices marked Paid, so partly paid invoices contributed nothing and revenues entered outside an invoice never appeared at all. It now counts payments and revenues as they land, matching the Cash Flow chart above it and the Income Summary report.
|
|
||||||
- Tickets: ticket-created emails told the client the status was Open no matter what the ticket was actually set to. They now carry the real status, on tickets an agent creates and on scheduled tickets from recurring tickets.
|
|
||||||
- Tickets: the assignee list on the client bulk-add form left out agents on the Accountant role and did not match the list on the normal ticket form. Both lists are now the same.
|
|
||||||
- Quotes and Recurring Invoices: picking a product from the item autocomplete put the word "undefined" in the item name, while the description and price filled in correctly. Invoices were not affected. The product list behind the box was also missing information on those two pages, so every entry read "No tax", services showed a stock badge, and searching by product code did not match. Reported by @cthompson.
|
|
||||||
|
|
||||||
### Developer Updates
|
|
||||||
|
|
||||||
- `getMonthlyTax()` and `getQuarterlyTax()` in `functions/app.php` are rewritten. They previously joined `invoice_items` to `invoices` to `payments`, which multiplied the line-item rows by the payment rows — the double counting was row multiplication, not a rounding problem. Both now drive off `payments`, join a pre-aggregated per-invoice tax subquery, and scale by `payment_amount / invoice_amount`, with `invoice_amount > 0` guarding the division. `agent/reports/tax_summary.php` also dropped a second loop that recalculated each row total by calling `getMonthlyTax()` another twelve times; the total accumulates in the first loop instead, cutting the queries behind the monthly view in half.
|
|
||||||
- `agent/invoices.php`: the Partial total no longer selects `SUM(invoice_amount)` across a `payments` join, and payments against partial invoices are subtracted from the unpaid figure. The list query gained a derived `LEFT JOIN (SELECT payment_invoice_id, SUM(payment_amount) ... GROUP BY payment_invoice_id)` for the per-row balance, which keeps it to one query rather than one per row.
|
|
||||||
- `agent/dashboard.php`: the `TopCategories` temporary table is now built from a `UNION ALL` of payments (carrying their invoice's category) and revenues, keyed on payment and revenue dates rather than `invoice_status = 'Paid'` and `invoice_date`. The Other bucket is built from the same union.
|
|
||||||
- `api/v1/tickets/create.php` sets `ticket_status = 2` when `assigned_to > 0`, rather than always inserting status 1.
|
|
||||||
- `agent/modals/client/client_bulk_add_ticket.php` filtered the assignee list on `user_role_id > 1` where every other assignee list uses `user_type = 1`. Role 1 is the built-in Accountant role, so accountant-role agents were missing from it.
|
|
||||||
- Product autocomplete is consolidated. The three pages each carried their own product `SELECT` and their own copy of the autocomplete JavaScript; the queries drifted, and quote and recurring invoice were still on a four-column version that had no `product_name` or `prod_id`, so the shared `onSelect` wrote `undefined` into `#name`. Its last line also assigned to `#product_id`, which only the invoice form has, so `onSelect` threw a `TypeError` on those two pages and the `input` handler under it threw on every keystroke. The query now lives in `getProductsForAutocomplete($mysqli)` in `functions/app.php` and the JavaScript in `js/product_autocomplete.js`, with the hidden `#product_id` added to the quote and recurring item forms and treated as optional in the JavaScript. Net 218 lines removed for 134 added. Note that `item_product_id` is still only written by `add_invoice_item` and the API, so the hidden field on those two forms is inert until the handlers are wired up.
|
|
||||||
|
|
||||||
## [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
|
||||||
|
|||||||
@@ -1,14 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
/*
|
|
||||||
* ITFlow - Database update to version 2.7.9 (from 2.7.8)
|
|
||||||
* Included by admin/database_updates.php - do not access directly
|
|
||||||
*/
|
|
||||||
|
|
||||||
defined('FROM_DB_UPDATER') || die("Direct file access is not allowed");
|
|
||||||
|
|
||||||
// Add auto-send option to recurring invoices (default to enabled) or whether they should be generated as drafts for manual review & sending
|
|
||||||
|
|
||||||
mysqli_query($mysqli, "ALTER TABLE `recurring_invoices`
|
|
||||||
ADD COLUMN `recurring_invoice_auto_send` tinyint(1) NOT NULL DEFAULT 1 AFTER `recurring_invoice_note`
|
|
||||||
");
|
|
||||||
@@ -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']);
|
||||||
|
|||||||
@@ -1,8 +0,0 @@
|
|||||||
/* Custom syling for scaling the brand text (MSP name) in the sidebar */
|
|
||||||
|
|
||||||
.app-sidebar .brand-text {
|
|
||||||
display: inline-block;
|
|
||||||
vertical-align: middle;
|
|
||||||
white-space: nowrap;
|
|
||||||
transform-origin: left center;
|
|
||||||
}
|
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
<!-- Do not modify - copy if you need to edit -->
|
|
||||||
<!-- Main Sidebar Container -->
|
<!-- Main Sidebar Container -->
|
||||||
<aside class="app-sidebar shadow d-print-none" data-bs-theme="dark">
|
<aside class="app-sidebar shadow d-print-none" data-bs-theme="dark">
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
<?php
|
<?php
|
||||||
// Do not modify - copy if you need to edit
|
|
||||||
|
|
||||||
require_once "../../config.php";
|
require_once "../../config.php";
|
||||||
require_once "../../functions.php";
|
require_once "../../functions.php";
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -1,7 +1,3 @@
|
|||||||
<!-- Custom fix for scaling the brand text (MSP name) in the sidebar -->
|
|
||||||
<link rel="stylesheet" href="css/sidebar_brand_fix.css">
|
|
||||||
<script src="js/sidebar_brand_fix.js" defer></script>
|
|
||||||
|
|
||||||
<!-- Main Sidebar Container -->
|
<!-- Main Sidebar Container -->
|
||||||
<aside class="app-sidebar shadow d-print-none" data-bs-theme="dark">
|
<aside class="app-sidebar shadow d-print-none" data-bs-theme="dark">
|
||||||
|
|
||||||
@@ -20,7 +16,7 @@
|
|||||||
|
|
||||||
<div class="sidebar-brand">
|
<div class="sidebar-brand">
|
||||||
<a class="brand-link" href="<?= $brand_link ?>">
|
<a class="brand-link" href="<?= $brand_link ?>">
|
||||||
<span class="brand-text h4 mb-0" id="sidebar-brand-text"><?= escapeHtml($session_company_name) ?></span>
|
<span class="brand-text h4 mb-0"><?= escapeHtml($session_company_name) ?></span>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -141,7 +141,7 @@ if (isset($_GET['invoice_id'])) {
|
|||||||
//Get billable, and unbilled tickets to add to invoice
|
//Get billable, and unbilled tickets to add to invoice
|
||||||
$sql_tickets_billable = mysqli_query(
|
$sql_tickets_billable = mysqli_query(
|
||||||
$mysqli, "
|
$mysqli, "
|
||||||
SELECT ticket_id, ticket_subject, ticket_number, ticket_prefix, ticket_status
|
SELECT 1
|
||||||
FROM
|
FROM
|
||||||
tickets
|
tickets
|
||||||
WHERE
|
WHERE
|
||||||
@@ -151,9 +151,10 @@ if (isset($_GET['invoice_id'])) {
|
|||||||
AND
|
AND
|
||||||
ticket_invoice_id = 0
|
ticket_invoice_id = 0
|
||||||
AND
|
AND
|
||||||
ticket_status IN (4, 5);
|
ticket_status = 5;
|
||||||
");
|
");
|
||||||
|
|
||||||
|
|
||||||
//Add up all the payments for the invoice and get the total amount paid to the invoice
|
//Add up all the payments for the invoice and get the total amount paid to the invoice
|
||||||
$sql_amount_paid = mysqli_query($mysqli, "SELECT SUM(payment_amount) AS amount_paid FROM payments WHERE payment_invoice_id = $invoice_id");
|
$sql_amount_paid = mysqli_query($mysqli, "SELECT SUM(payment_amount) AS amount_paid FROM payments WHERE payment_invoice_id = $invoice_id");
|
||||||
$row = mysqli_fetch_assoc($sql_amount_paid);
|
$row = mysqli_fetch_assoc($sql_amount_paid);
|
||||||
@@ -179,7 +180,32 @@ if (isset($_GET['invoice_id'])) {
|
|||||||
$invoice_badge_color = getInvoiceBadgeColor($invoice_status);
|
$invoice_badge_color = getInvoiceBadgeColor($invoice_status);
|
||||||
|
|
||||||
//Product autocomplete
|
//Product autocomplete
|
||||||
$json_products = getProductsForAutocomplete($mysqli);
|
$products_sql = mysqli_query($mysqli, "
|
||||||
|
SELECT
|
||||||
|
IF(product_code IS NULL OR product_code = '', product_name, CONCAT(product_code, ' - ', product_name)) AS label,
|
||||||
|
product_name,
|
||||||
|
product_code,
|
||||||
|
product_type AS type,
|
||||||
|
product_description AS description,
|
||||||
|
product_price AS price,
|
||||||
|
product_tax_id AS tax,
|
||||||
|
tax_percent,
|
||||||
|
product_id AS prod_id,
|
||||||
|
COALESCE(SUM(product_stock.stock_qty), 0) AS available_stock
|
||||||
|
FROM products
|
||||||
|
LEFT JOIN product_stock ON product_id = stock_product_id
|
||||||
|
LEFT JOIN taxes ON product_tax_id = tax_id
|
||||||
|
WHERE product_archived_at IS NULL
|
||||||
|
GROUP BY product_id
|
||||||
|
ORDER BY product_name ASC
|
||||||
|
");
|
||||||
|
|
||||||
|
if (mysqli_num_rows($products_sql) > 0) {
|
||||||
|
while ($row = mysqli_fetch_assoc($products_sql)) {
|
||||||
|
$products[] = $row;
|
||||||
|
}
|
||||||
|
$json_products = json_encode($products);
|
||||||
|
}
|
||||||
|
|
||||||
// Saved Payment Methods
|
// Saved Payment Methods
|
||||||
$sql_saved_payment_methods = mysqli_query($mysqli, "
|
$sql_saved_payment_methods = mysqli_query($mysqli, "
|
||||||
@@ -696,7 +722,7 @@ if (isset($_GET['invoice_id'])) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-sm d-print-none <?php if (mysqli_num_rows($sql_tickets) == 0 && mysqli_num_rows($sql_tickets_billable) == 0) { echo "d-none"; } ?>">
|
<div class="col-sm d-print-none <?php if (mysqli_num_rows($sql_tickets) == 0) { echo "d-none"; } ?>">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-header text-bold">
|
<div class="card-header text-bold">
|
||||||
<i class="fa fa-life-ring me-2"></i>Tickets
|
<i class="fa fa-life-ring me-2"></i>Tickets
|
||||||
@@ -727,19 +753,15 @@ if (isset($_GET['invoice_id'])) {
|
|||||||
<table class="table">
|
<table class="table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>#</th>
|
|
||||||
<th>Date</th>
|
<th>Date</th>
|
||||||
<th>Subject</th>
|
<th>Subject</th>
|
||||||
<th class="text-end">Time Worked</th>
|
<th class="text-end">Time Worked</th>
|
||||||
<th></th>
|
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
while ($row = mysqli_fetch_assoc($sql_tickets)) {
|
while ($row = mysqli_fetch_assoc($sql_tickets)) {
|
||||||
$ticket_prefix = escapeHtml($row['ticket_prefix']);
|
|
||||||
$ticket_number = escapeHtml($row['ticket_number']);
|
|
||||||
$ticket_id = intval($row['ticket_id']);
|
$ticket_id = intval($row['ticket_id']);
|
||||||
$ticket_created_at = escapeHtml($row['ticket_created_at']);
|
$ticket_created_at = escapeHtml($row['ticket_created_at']);
|
||||||
$ticket_subject = escapeHtml($row['ticket_subject']);
|
$ticket_subject = escapeHtml($row['ticket_subject']);
|
||||||
@@ -747,12 +769,9 @@ if (isset($_GET['invoice_id'])) {
|
|||||||
|
|
||||||
?>
|
?>
|
||||||
<tr>
|
<tr>
|
||||||
<td><a href="ticket.php?ticket_id=<?= $ticket_id ?>"><?= "$ticket_prefix$ticket_number" ?></a></td>
|
|
||||||
<td><?= $ticket_created_at ?></td>
|
<td><?= $ticket_created_at ?></td>
|
||||||
<td><?= $ticket_subject ?></td>
|
<td><?= $ticket_subject ?></td>
|
||||||
<td class="text-end"><?= $ticket_total_time_worked ?></td>
|
<td class="text-end"><?= $ticket_total_time_worked ?></td>
|
||||||
<td align="right"><a class="btn btn-light text-danger confirm-link" title="Remove" href="post.php?remove_ticket_from_invoice&invoice_id=<?= $invoice_id ?>&ticket_id=<?= $ticket_id ?>&csrf_token=<?= $_SESSION['csrf_token'] ?>"><i class="fa fa-times"></i></a></td>
|
|
||||||
|
|
||||||
</tr>
|
</tr>
|
||||||
<?php
|
<?php
|
||||||
}
|
}
|
||||||
@@ -796,12 +815,67 @@ require_once "../includes/footer.php";
|
|||||||
|
|
||||||
?>
|
?>
|
||||||
|
|
||||||
<!-- Product autocomplete for the add-item row -->
|
<!-- JSON Autocomplete / type ahead -->
|
||||||
<script src="/js/product_autocomplete.js"></script>
|
|
||||||
<script>
|
<script>
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', function () {
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
initProductAutocomplete(<?= $json_products ?? '[]' ?>);
|
|
||||||
|
var availableProducts = <?= $json_products ?? '[]' ?>;
|
||||||
|
|
||||||
|
var nameInput = document.getElementById('name');
|
||||||
|
if (!nameInput) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
itflowAutocomplete(nameInput, {
|
||||||
|
minLength: 1,
|
||||||
|
source: availableProducts,
|
||||||
|
match: function (item, term) {
|
||||||
|
return String(item.label || '').toLowerCase().indexOf(term) !== -1
|
||||||
|
|| String(item.product_name || '').toLowerCase().indexOf(term) !== -1
|
||||||
|
|| String(item.product_code || '').toLowerCase().indexOf(term) !== -1;
|
||||||
|
},
|
||||||
|
render: function (item) {
|
||||||
|
var esc = itflowEscapeHtml;
|
||||||
|
var typeText = item.type ? item.type.charAt(0).toUpperCase() + item.type.slice(1).toLowerCase() : "";
|
||||||
|
var showStock = (typeText.toLowerCase() !== "service");
|
||||||
|
var taxText = (item.tax_percent != null) ? (parseFloat(item.tax_percent) + "%") : "No tax";
|
||||||
|
var priceText = (item.price != null && item.price !== "") ? String(item.price) : "";
|
||||||
|
var stockText = (item.available_stock ?? 0);
|
||||||
|
|
||||||
|
return "<div class='d-flex justify-content-between align-items-start'>" +
|
||||||
|
"<div class='flex-fill pe-2'>" +
|
||||||
|
"<div class='fw-bold'>" + esc(item.label) +
|
||||||
|
(typeText ? " <small class='text-muted'>(" + esc(typeText) + ")</small>" : "") +
|
||||||
|
"</div>" +
|
||||||
|
"<div class='small text-muted'>" + esc(item.description) + "</div>" +
|
||||||
|
"<div class='mt-1'>" +
|
||||||
|
"<span class='badge bg-secondary me-1'>Tax: " + esc(taxText) + "</span>" +
|
||||||
|
(showStock ? "<span class='badge " + (stockText > 0 ? "bg-success" : "bg-danger") + "'>Stock: " + esc(stockText) + "</span>" : "") +
|
||||||
|
"</div>" +
|
||||||
|
"</div>" +
|
||||||
|
"<div class='text-end'>" +
|
||||||
|
"<div class='fw-bold'>" + esc(priceText) + "</div>" +
|
||||||
|
"</div>" +
|
||||||
|
"</div>";
|
||||||
|
},
|
||||||
|
onSelect: function (item) {
|
||||||
|
document.getElementById('name').value = item.product_name;
|
||||||
|
document.getElementById('desc').value = item.description;
|
||||||
|
document.getElementById('qty').value = 1;
|
||||||
|
document.getElementById('price').value = item.price;
|
||||||
|
setTomSelectValue(document.getElementById('tax'), item.tax);
|
||||||
|
document.getElementById('product_id').value = item.prod_id;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Typing over the name by hand breaks the link to the product
|
||||||
|
nameInput.addEventListener('input', function () {
|
||||||
|
document.getElementById('product_id').value = 0;
|
||||||
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script src="../libs/SortableJS/Sortable.min.js"></script>
|
<script src="../libs/SortableJS/Sortable.min.js"></script>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -1,19 +0,0 @@
|
|||||||
function fitBrandText() {
|
|
||||||
text.style.transform = 'none';
|
|
||||||
// getBoundingClientRect gives real on-screen edges, unlike
|
|
||||||
// offsetLeft which is unreliable for inline text
|
|
||||||
var available = link.getBoundingClientRect().right - text.getBoundingClientRect().left - 8;
|
|
||||||
var natural = text.scrollWidth;
|
|
||||||
// Scale rather than step font-size down.
|
|
||||||
var scale = natural > available ? available / natural : 1;
|
|
||||||
text.style.transform = 'scale(' + scale.toFixed(4) + ')';
|
|
||||||
}
|
|
||||||
|
|
||||||
var text = document.getElementById('sidebar-brand-text');
|
|
||||||
|
|
||||||
if (text) {
|
|
||||||
var link = text.closest('.brand-link');
|
|
||||||
|
|
||||||
// Fires once on observe as well as any later width change
|
|
||||||
new ResizeObserver(fitBrandText).observe(link);
|
|
||||||
}
|
|
||||||
@@ -76,7 +76,7 @@ ob_start();
|
|||||||
<label>Shortened Name</label>
|
<label>Shortened Name</label>
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<span class="input-group-text"><i class="fa fa-fw fa-id-badge"></i></span>
|
<span class="input-group-text"><i class="fa fa-fw fa-id-badge"></i></span>
|
||||||
<input type="text" class="form-control" name="abbreviation" placeholder="Shortened name for client - Max chars 6" maxlength="6" oninput="this.value = this.value.toUpperCase()">
|
<input type="text" class="form-control" name="abbreviation" placeholder="Shortned name for client - Max chars 6" maxlength="6" oninput="this.value = this.value.toUpperCase()">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -101,7 +101,7 @@ ob_start();
|
|||||||
<label>Shortened Name</label>
|
<label>Shortened Name</label>
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<span class="input-group-text"><i class="fa fa-fw fa-id-badge"></i></span>
|
<span class="input-group-text"><i class="fa fa-fw fa-id-badge"></i></span>
|
||||||
<input type="text" class="form-control" name="abbreviation" placeholder="Shortened name for client - Max chars 6" value="<?= $client_abbreviation ?>" maxlength="6" oninput="this.value = this.value.toUpperCase()">
|
<input type="text" class="form-control" name="abbreviation" placeholder="Shortned name for client - Max chars 6" value="<?= $client_abbreviation ?>" maxlength="6" oninput="this.value = this.value.toUpperCase()">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -44,16 +44,8 @@
|
|||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td><?= $ticket_subject ?></td>
|
<td><?= $ticket_subject ?></td>
|
||||||
<td>
|
<td><a href='ticket.php?ticket_id=<?= $ticket_id ?>&invoice_id=<?= $invoice_id ?>#addInvoiceFromTicketModal'>
|
||||||
<form action="post.php" method="post">
|
<i class="fas fa-fw fa-plus-circle"></i></td>
|
||||||
<input type="hidden" name="csrf_token" value="<?= $_SESSION['csrf_token'] ?>">
|
|
||||||
<input type="hidden" name="invoice_id" value="<?= $invoice_id ?>">
|
|
||||||
<input type="hidden" name="ticket_id" value="<?= $ticket_id ?>">
|
|
||||||
<button class="btn btn-link p-0" type="submit" name="add_ticket_to_invoice" title="Add ticket to invoice">
|
|
||||||
<i class="fas fa-fw fa-plus-circle"></i>
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
</td>
|
|
||||||
</tr>
|
</tr>
|
||||||
<?php } ?>
|
<?php } ?>
|
||||||
</table>
|
</table>
|
||||||
|
|||||||
@@ -9,8 +9,7 @@ $recurring_invoice_id = intval($_GET['id']);
|
|||||||
$sql = mysqli_query($mysqli, "SELECT recurring_invoice_category_id, recurring_invoice_client_id, recurring_invoice_created_at,
|
$sql = mysqli_query($mysqli, "SELECT recurring_invoice_category_id, recurring_invoice_client_id, recurring_invoice_created_at,
|
||||||
recurring_invoice_discount_amount, recurring_invoice_frequency,
|
recurring_invoice_discount_amount, recurring_invoice_frequency,
|
||||||
recurring_invoice_next_date, recurring_invoice_number, recurring_invoice_prefix,
|
recurring_invoice_next_date, recurring_invoice_number, recurring_invoice_prefix,
|
||||||
recurring_invoice_auto_send, recurring_invoice_scope, recurring_invoice_status
|
recurring_invoice_scope, recurring_invoice_status FROM recurring_invoices WHERE recurring_invoice_id = $recurring_invoice_id LIMIT 1");
|
||||||
FROM recurring_invoices WHERE recurring_invoice_id = $recurring_invoice_id LIMIT 1");
|
|
||||||
|
|
||||||
$row = mysqli_fetch_assoc($sql);
|
$row = mysqli_fetch_assoc($sql);
|
||||||
$recurring_invoice_prefix = escapeHtml($row['recurring_invoice_prefix']);
|
$recurring_invoice_prefix = escapeHtml($row['recurring_invoice_prefix']);
|
||||||
@@ -20,7 +19,6 @@ $recurring_invoice_frequency = escapeHtml($row['recurring_invoice_frequency']);
|
|||||||
$recurring_invoice_status = escapeHtml($row['recurring_invoice_status']);
|
$recurring_invoice_status = escapeHtml($row['recurring_invoice_status']);
|
||||||
$recurring_invoice_created_at = date('Y-m-d', strtotime($row['recurring_invoice_created_at']));
|
$recurring_invoice_created_at = date('Y-m-d', strtotime($row['recurring_invoice_created_at']));
|
||||||
$recurring_invoice_next_date = escapeHtml($row['recurring_invoice_next_date']);
|
$recurring_invoice_next_date = escapeHtml($row['recurring_invoice_next_date']);
|
||||||
$recurring_invoice_auto_send = intval($row['recurring_invoice_auto_send']);
|
|
||||||
$recurring_invoice_discount = floatval($row['recurring_invoice_discount_amount']);
|
$recurring_invoice_discount = floatval($row['recurring_invoice_discount_amount']);
|
||||||
$category_id = intval($row['recurring_invoice_category_id']);
|
$category_id = intval($row['recurring_invoice_category_id']);
|
||||||
$client_id = intval($row['recurring_invoice_client_id']);
|
$client_id = intval($row['recurring_invoice_client_id']);
|
||||||
@@ -103,22 +101,11 @@ ob_start();
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-3">
|
|
||||||
<label>Action <strong class="text-danger">*</strong></label>
|
|
||||||
<div class="input-group">
|
|
||||||
<span class="input-group-text"><i class="fa fa-fw fa-paper-plane"></i></span>
|
|
||||||
<select class="form-select select" name="auto_send" required>
|
|
||||||
<option <?php if ($recurring_invoice_auto_send == 1) { echo "selected"; } ?> value="1">Send Automatically</option>
|
|
||||||
<option <?php if ($recurring_invoice_auto_send == 0) { echo "selected"; } ?> value="0">Generate Draft for Review</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label>Status <strong class="text-danger">*</strong></label>
|
<label>Status <strong class="text-danger">*</strong></label>
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<span class="input-group-text"><i class="fa fa-fw fa-clock"></i></span>
|
<span class="input-group-text"><i class="fa fa-fw fa-clock"></i></span>
|
||||||
<select class="form-select select" name="status" required>
|
<select class="form-select select2" name="status" required>
|
||||||
<option <?php if ($recurring_invoice_status == 1) {
|
<option <?php if ($recurring_invoice_status == 1) {
|
||||||
echo "selected";
|
echo "selected";
|
||||||
} ?> value="1">Active</option>
|
} ?> value="1">Active</option>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ $sql = mysqli_query(
|
|||||||
$mysqli,
|
$mysqli,
|
||||||
"SELECT client_id, client_name, location_name, network, network_client_id, network_description,
|
"SELECT client_id, client_name, location_name, network, network_client_id, network_description,
|
||||||
network_dhcp_range, network_gateway, network_id, network_name, network_notes,
|
network_dhcp_range, network_gateway, network_id, network_name, network_notes,
|
||||||
network_primary_dns, network_secondary_dns, network_vlan, network_archived_at FROM networks
|
network_primary_dns, network_secondary_dns, network_vlan FROM networks
|
||||||
LEFT JOIN clients ON client_id = network_client_id
|
LEFT JOIN clients ON client_id = network_client_id
|
||||||
LEFT JOIN locations ON location_id = network_location_id
|
LEFT JOIN locations ON location_id = network_location_id
|
||||||
WHERE network_id = $network_id
|
WHERE network_id = $network_id
|
||||||
@@ -50,7 +50,6 @@ if (mysqli_num_rows($sql) == 0) {
|
|||||||
$network_primary_dns = escapeHtml($row['network_primary_dns']);
|
$network_primary_dns = escapeHtml($row['network_primary_dns']);
|
||||||
$network_secondary_dns = escapeHtml($row['network_secondary_dns']);
|
$network_secondary_dns = escapeHtml($row['network_secondary_dns']);
|
||||||
$network_dhcp_range = escapeHtml($row['network_dhcp_range']);
|
$network_dhcp_range = escapeHtml($row['network_dhcp_range']);
|
||||||
$network_archived_at = escapeHtml($row['network_archived_at']);
|
|
||||||
$network_notes = escapeHtml($row['network_notes']);
|
$network_notes = escapeHtml($row['network_notes']);
|
||||||
$location_name = escapeHtml($row['location_name']);
|
$location_name = escapeHtml($row['location_name']);
|
||||||
|
|
||||||
@@ -92,9 +91,6 @@ if (mysqli_num_rows($sql) == 0) {
|
|||||||
</li>
|
</li>
|
||||||
<li class="breadcrumb-item active">
|
<li class="breadcrumb-item active">
|
||||||
<i class="fas fa-fw fa-network-wired"></i> <?= $network_name ?>
|
<i class="fas fa-fw fa-network-wired"></i> <?= $network_name ?>
|
||||||
<?php if ($network_archived_at) { ?>
|
|
||||||
<span class="text-secondary"> (Archived)</span>
|
|
||||||
<?php } ?>
|
|
||||||
</li>
|
</li>
|
||||||
</ol>
|
</ol>
|
||||||
|
|
||||||
@@ -228,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">
|
||||||
@@ -237,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>
|
||||||
@@ -260,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>
|
||||||
|
|||||||
@@ -1003,7 +1003,6 @@ if (isExportRequest('export_invoices')) {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: This should probably be removed as we now allow multiple invoices to be linked to a single ticket
|
|
||||||
if (isset($_POST['link_invoice_to_ticket'])) {
|
if (isset($_POST['link_invoice_to_ticket'])) {
|
||||||
|
|
||||||
validateCSRFToken();
|
validateCSRFToken();
|
||||||
@@ -1042,28 +1041,7 @@ if (isset($_POST['add_ticket_to_invoice'])) {
|
|||||||
|
|
||||||
flashAlert("Ticket linked to invoice");
|
flashAlert("Ticket linked to invoice");
|
||||||
|
|
||||||
redirect("invoice.php?invoice_id=$invoice_id");
|
redirect("post.php?add_ticket_to_invoice=$invoice_id");
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isset($_GET['remove_ticket_from_invoice'])) {
|
|
||||||
|
|
||||||
validateCSRFToken();
|
|
||||||
|
|
||||||
enforceUserPermission('module_sales', 2);
|
|
||||||
|
|
||||||
$invoice_id = intval($_GET['invoice_id']);
|
|
||||||
$ticket_id = intval($_GET['ticket_id']);
|
|
||||||
|
|
||||||
$client_id = intval(getFieldById('tickets', $ticket_id, 'ticket_client_id'));
|
|
||||||
|
|
||||||
enforceClientAccess();
|
|
||||||
|
|
||||||
mysqli_query($mysqli,"UPDATE tickets SET ticket_invoice_id = 0 WHERE ticket_id = $ticket_id");
|
|
||||||
|
|
||||||
flashAlert("Ticket unlinked from invoice");
|
|
||||||
|
|
||||||
redirect("invoice.php?invoice_id=$invoice_id");
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -124,7 +124,6 @@ if (isset($_POST['edit_recurring_invoice'])) {
|
|||||||
$scope = escapeSql($_POST['scope']);
|
$scope = escapeSql($_POST['scope']);
|
||||||
$status = intval($_POST['status']);
|
$status = intval($_POST['status']);
|
||||||
$recurring_invoice_discount = floatval($_POST['recurring_invoice_discount']);
|
$recurring_invoice_discount = floatval($_POST['recurring_invoice_discount']);
|
||||||
$auto_send = intval($_POST['auto_send']);
|
|
||||||
|
|
||||||
// Get Recurring Invoice Details and Client ID for Logging
|
// Get Recurring Invoice Details and Client ID for Logging
|
||||||
$sql = mysqli_query($mysqli,"SELECT recurring_invoice_prefix, recurring_invoice_number, recurring_invoice_client_id FROM recurring_invoices WHERE recurring_invoice_id = $recurring_invoice_id");
|
$sql = mysqli_query($mysqli,"SELECT recurring_invoice_prefix, recurring_invoice_number, recurring_invoice_client_id FROM recurring_invoices WHERE recurring_invoice_id = $recurring_invoice_id");
|
||||||
@@ -144,7 +143,7 @@ if (isset($_POST['edit_recurring_invoice'])) {
|
|||||||
}
|
}
|
||||||
$recurring_invoice_amount = $recurring_invoice_amount - $recurring_invoice_discount;
|
$recurring_invoice_amount = $recurring_invoice_amount - $recurring_invoice_discount;
|
||||||
|
|
||||||
mysqli_query($mysqli,"UPDATE recurring_invoices SET recurring_invoice_scope = '$scope', recurring_invoice_frequency = '$frequency', recurring_invoice_next_date = '$next_date', recurring_invoice_category_id = $category, recurring_invoice_discount_amount = $recurring_invoice_discount, recurring_invoice_amount = $recurring_invoice_amount, recurring_invoice_auto_send = $auto_send, recurring_invoice_status = $status WHERE recurring_invoice_id = $recurring_invoice_id");
|
mysqli_query($mysqli,"UPDATE recurring_invoices SET recurring_invoice_scope = '$scope', recurring_invoice_frequency = '$frequency', recurring_invoice_next_date = '$next_date', recurring_invoice_category_id = $category, recurring_invoice_discount_amount = $recurring_invoice_discount, recurring_invoice_amount = $recurring_invoice_amount, recurring_invoice_status = $status WHERE recurring_invoice_id = $recurring_invoice_id");
|
||||||
|
|
||||||
mysqli_query($mysqli,"INSERT INTO history SET history_status = '$status', history_description = 'Recurring Invoice edited', history_recurring_invoice_id = $recurring_invoice_id");
|
mysqli_query($mysqli,"INSERT INTO history SET history_status = '$status', history_description = 'Recurring Invoice edited', history_recurring_invoice_id = $recurring_invoice_id");
|
||||||
|
|
||||||
@@ -371,13 +370,12 @@ if (isset($_GET['force_recurring'])) {
|
|||||||
recurring_invoice_client_id, recurring_invoice_currency_code,
|
recurring_invoice_client_id, recurring_invoice_currency_code,
|
||||||
recurring_invoice_discount_amount, recurring_invoice_frequency, recurring_invoice_id,
|
recurring_invoice_discount_amount, recurring_invoice_frequency, recurring_invoice_id,
|
||||||
recurring_invoice_last_sent, recurring_invoice_next_date, recurring_invoice_note,
|
recurring_invoice_last_sent, recurring_invoice_next_date, recurring_invoice_note,
|
||||||
recurring_invoice_scope, recurring_invoice_auto_send, recurring_invoice_status FROM recurring_invoices, clients WHERE client_id = recurring_invoice_client_id AND recurring_invoice_id = $recurring_invoice_id");
|
recurring_invoice_scope, recurring_invoice_status FROM recurring_invoices, clients WHERE client_id = recurring_invoice_client_id AND recurring_invoice_id = $recurring_invoice_id");
|
||||||
|
|
||||||
$row = mysqli_fetch_assoc($sql_recurring_invoices);
|
$row = mysqli_fetch_assoc($sql_recurring_invoices);
|
||||||
$recurring_invoice_id = intval($row['recurring_invoice_id']);
|
$recurring_invoice_id = intval($row['recurring_invoice_id']);
|
||||||
$recurring_invoice_scope = escapeSql($row['recurring_invoice_scope']);
|
$recurring_invoice_scope = escapeSql($row['recurring_invoice_scope']);
|
||||||
$recurring_invoice_frequency = validateRecurringFrequency($row['recurring_invoice_frequency']);
|
$recurring_invoice_frequency = validateRecurringFrequency($row['recurring_invoice_frequency']);
|
||||||
$recurring_invoice_auto_send = intval($row['recurring_invoice_auto_send']);
|
|
||||||
$recurring_invoice_status = escapeSql($row['recurring_invoice_status']);
|
$recurring_invoice_status = escapeSql($row['recurring_invoice_status']);
|
||||||
$recurring_invoice_last_sent = escapeSql($row['recurring_invoice_last_sent']);
|
$recurring_invoice_last_sent = escapeSql($row['recurring_invoice_last_sent']);
|
||||||
$recurring_invoice_next_date = escapeSql($row['recurring_invoice_next_date']);
|
$recurring_invoice_next_date = escapeSql($row['recurring_invoice_next_date']);
|
||||||
@@ -405,12 +403,10 @@ if (isset($_GET['force_recurring'])) {
|
|||||||
//Generate a unique URL key for clients to access
|
//Generate a unique URL key for clients to access
|
||||||
$url_key = randomString(32);
|
$url_key = randomString(32);
|
||||||
|
|
||||||
mysqli_query($mysqli,"INSERT INTO invoices SET invoice_prefix = '$config_invoice_prefix', invoice_number = $new_invoice_number, invoice_scope = '$recurring_invoice_scope', invoice_date = CURDATE(), invoice_due = DATE_ADD(CURDATE(), INTERVAL $client_net_terms day), invoice_discount_amount = $recurring_invoice_discount_amount, invoice_amount = $recurring_invoice_amount, invoice_currency_code = '$recurring_invoice_currency_code', invoice_note = '$recurring_invoice_note', invoice_category_id = $category_id, invoice_status = 'Draft', invoice_url_key = '$url_key', invoice_recurring_invoice_id = $recurring_invoice_id, invoice_client_id = $client_id");
|
mysqli_query($mysqli,"INSERT INTO invoices SET invoice_prefix = '$config_invoice_prefix', invoice_number = $new_invoice_number, invoice_scope = '$recurring_invoice_scope', invoice_date = CURDATE(), invoice_due = DATE_ADD(CURDATE(), INTERVAL $client_net_terms day), invoice_discount_amount = $recurring_invoice_discount_amount, invoice_amount = $recurring_invoice_amount, invoice_currency_code = '$recurring_invoice_currency_code', invoice_note = '$recurring_invoice_note', invoice_category_id = $category_id, invoice_status = 'Sent', invoice_url_key = '$url_key', invoice_recurring_invoice_id = $recurring_invoice_id, invoice_client_id = $client_id");
|
||||||
|
|
||||||
$new_invoice_id = mysqli_insert_id($mysqli);
|
$new_invoice_id = mysqli_insert_id($mysqli);
|
||||||
|
|
||||||
mysqli_query($mysqli,"INSERT INTO history SET history_status = 'Draft', history_description = 'Invoice Generated from Recurring!', history_invoice_id = $new_invoice_id");
|
|
||||||
|
|
||||||
//Copy Items from original invoice to new invoice
|
//Copy Items from original invoice to new invoice
|
||||||
$sql_invoice_items = mysqli_query($mysqli,"SELECT item_description, item_id, item_name, item_order, item_price, item_quantity, item_subtotal,
|
$sql_invoice_items = mysqli_query($mysqli,"SELECT item_description, item_id, item_name, item_order, item_price, item_quantity, item_subtotal,
|
||||||
item_tax_id FROM recurring_invoice_items WHERE item_recurring_invoice_id = $recurring_invoice_id ORDER BY item_id ASC");
|
item_tax_id FROM recurring_invoice_items WHERE item_recurring_invoice_id = $recurring_invoice_id ORDER BY item_id ASC");
|
||||||
@@ -443,90 +439,86 @@ if (isset($_GET['force_recurring'])) {
|
|||||||
mysqli_query($mysqli,"INSERT INTO invoice_items SET item_name = '$item_name', item_description = '$item_description', item_quantity = $item_quantity, item_price = $item_price, item_subtotal = $item_subtotal, item_tax = $item_tax_amount, item_total = $item_total, item_tax_id = $tax_id, item_invoice_id = $new_invoice_id");
|
mysqli_query($mysqli,"INSERT INTO invoice_items SET item_name = '$item_name', item_description = '$item_description', item_quantity = $item_quantity, item_price = $item_price, item_subtotal = $item_subtotal, item_tax = $item_tax_amount, item_total = $item_total, item_tax_id = $tax_id, item_invoice_id = $new_invoice_id");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only send the invoice if the recurring invoice is set to auto-send, otherwise just leave as draft for manual sending
|
mysqli_query($mysqli,"INSERT INTO history SET history_status = 'Sent', history_description = 'Invoice Generated from Recurring!', history_invoice_id = $new_invoice_id");
|
||||||
if ($recurring_invoice_auto_send == 1) {
|
|
||||||
|
|
||||||
mysqli_query($mysqli,"INSERT INTO history SET history_status = 'Sent', history_description = 'Invoice Sent from Recurring!', history_invoice_id = $new_invoice_id");
|
//Update Recurring Balances by tallying up recurring items also update recurring dates
|
||||||
|
$sql_recurring_invoice_total = mysqli_query($mysqli,"SELECT SUM(item_total) AS recurring_invoice_total FROM recurring_invoice_items WHERE item_recurring_invoice_id = $recurring_invoice_id");
|
||||||
|
$row = mysqli_fetch_assoc($sql_recurring_invoice_total);
|
||||||
|
$new_recurring_invoice_amount = floatval($row['recurring_invoice_total']) - $recurring_invoice_discount_amount;
|
||||||
|
|
||||||
//Update Recurring Balances by tallying up recurring items also update recurring dates
|
mysqli_query($mysqli,"UPDATE recurring_invoices SET recurring_invoice_amount = $new_recurring_invoice_amount, recurring_invoice_last_sent = CURDATE(), recurring_invoice_next_date = DATE_ADD(CURDATE(), INTERVAL 1 $recurring_invoice_frequency) WHERE recurring_invoice_id = $recurring_invoice_id");
|
||||||
$sql_recurring_invoice_total = mysqli_query($mysqli,"SELECT SUM(item_total) AS recurring_invoice_total FROM recurring_invoice_items WHERE item_recurring_invoice_id = $recurring_invoice_id");
|
|
||||||
$row = mysqli_fetch_assoc($sql_recurring_invoice_total);
|
|
||||||
$new_recurring_invoice_amount = floatval($row['recurring_invoice_total']) - $recurring_invoice_discount_amount;
|
|
||||||
|
|
||||||
mysqli_query($mysqli,"UPDATE recurring_invoices SET recurring_invoice_amount = $new_recurring_invoice_amount, recurring_invoice_last_sent = CURDATE(), recurring_invoice_next_date = DATE_ADD(CURDATE(), INTERVAL 1 $recurring_invoice_frequency) WHERE recurring_invoice_id = $recurring_invoice_id");
|
//Also update the newly created invoice with the new amounts
|
||||||
|
mysqli_query($mysqli,"UPDATE invoices SET invoice_amount = $new_recurring_invoice_amount WHERE invoice_id = $new_invoice_id");
|
||||||
|
|
||||||
//Also update the newly created invoice with the new amounts
|
if ($config_recurring_auto_send_invoice == 1) {
|
||||||
mysqli_query($mysqli,"UPDATE invoices SET invoice_amount = $new_recurring_invoice_amount WHERE invoice_id = $new_invoice_id");
|
$sql = mysqli_query($mysqli,"SELECT * FROM invoices
|
||||||
|
LEFT JOIN clients ON invoice_client_id = client_id
|
||||||
|
LEFT JOIN contacts ON clients.client_id = contacts.contact_client_id AND contact_primary = 1
|
||||||
|
WHERE invoice_id = $new_invoice_id"
|
||||||
|
);
|
||||||
|
$row = mysqli_fetch_assoc($sql);
|
||||||
|
|
||||||
if ($config_recurring_auto_send_invoice == 1) {
|
$invoice_prefix = escapeSql($row['invoice_prefix']);
|
||||||
$sql = mysqli_query($mysqli,"SELECT * FROM invoices
|
$invoice_number = intval($row['invoice_number']);
|
||||||
LEFT JOIN clients ON invoice_client_id = client_id
|
$invoice_scope = escapeSql($row['invoice_scope']);
|
||||||
LEFT JOIN contacts ON clients.client_id = contacts.contact_client_id AND contact_primary = 1
|
$invoice_date = escapeSql(validateDate($row['invoice_date']));
|
||||||
WHERE invoice_id = $new_invoice_id"
|
$invoice_due = escapeSql($row['invoice_due']);
|
||||||
);
|
$invoice_amount = floatval($row['invoice_amount']);
|
||||||
$row = mysqli_fetch_assoc($sql);
|
$invoice_url_key = escapeSql($row['invoice_url_key']);
|
||||||
|
$client_id = intval($row['client_id']);
|
||||||
|
$client_name = escapeSql($row['client_name']);
|
||||||
|
$contact_name = escapeSql($row['contact_name']);
|
||||||
|
$contact_email = escapeSql($row['contact_email']);
|
||||||
|
$contact_phone = escapeSql(formatPhoneNumber($row['contact_phone'], $row['contact_phone_country_code']));
|
||||||
|
$contact_extension = intval($row['contact_extension']);
|
||||||
|
$contact_mobile = escapeSql(formatPhoneNumber($row['contact_mobile'], $row['contact_mobile_country_code']));
|
||||||
|
|
||||||
$invoice_prefix = escapeSql($row['invoice_prefix']);
|
$sql = mysqli_query($mysqli,"SELECT company_email, company_name, company_phone, company_phone_country_code, company_website FROM companies WHERE company_id = 1");
|
||||||
$invoice_number = intval($row['invoice_number']);
|
$row = mysqli_fetch_assoc($sql);
|
||||||
$invoice_scope = escapeSql($row['invoice_scope']);
|
$company_name = escapeSql($row['company_name']);
|
||||||
$invoice_date = escapeSql(validateDate($row['invoice_date']));
|
$company_phone = escapeSql(formatPhoneNumber($row['company_phone'], $row['company_phone_country_code']));
|
||||||
$invoice_due = escapeSql($row['invoice_due']);
|
$company_email = escapeSql($row['company_email']);
|
||||||
$invoice_amount = floatval($row['invoice_amount']);
|
$company_website = escapeSql($row['company_website']);
|
||||||
$invoice_url_key = escapeSql($row['invoice_url_key']);
|
|
||||||
$client_id = intval($row['client_id']);
|
|
||||||
$client_name = escapeSql($row['client_name']);
|
|
||||||
$contact_name = escapeSql($row['contact_name']);
|
|
||||||
$contact_email = escapeSql($row['contact_email']);
|
|
||||||
$contact_phone = escapeSql(formatPhoneNumber($row['contact_phone'], $row['contact_phone_country_code']));
|
|
||||||
$contact_extension = intval($row['contact_extension']);
|
|
||||||
$contact_mobile = escapeSql(formatPhoneNumber($row['contact_mobile'], $row['contact_mobile_country_code']));
|
|
||||||
|
|
||||||
$sql = mysqli_query($mysqli,"SELECT company_email, company_name, company_phone, company_phone_country_code, company_website FROM companies WHERE company_id = 1");
|
// Sanitize Config Vars
|
||||||
$row = mysqli_fetch_assoc($sql);
|
$config_invoice_from_email = escapeSql($config_invoice_from_email);
|
||||||
$company_name = escapeSql($row['company_name']);
|
$config_invoice_from_name = escapeSql($config_invoice_from_name);
|
||||||
$company_phone = escapeSql(formatPhoneNumber($row['company_phone'], $row['company_phone_country_code']));
|
|
||||||
$company_email = escapeSql($row['company_email']);
|
|
||||||
$company_website = escapeSql($row['company_website']);
|
|
||||||
|
|
||||||
// Sanitize Config Vars
|
// Email to client
|
||||||
$config_invoice_from_email = escapeSql($config_invoice_from_email);
|
|
||||||
$config_invoice_from_name = escapeSql($config_invoice_from_name);
|
|
||||||
|
|
||||||
// Email to client
|
$subject = "Invoice $invoice_prefix$invoice_number";
|
||||||
|
$body = "Hello $contact_name,<br><br>An invoice regarding \"$invoice_scope\" has been generated. Please view the details below.<br><br>Invoice: $invoice_prefix$invoice_number<br>Issue Date: $invoice_date<br>Total: $$invoice_amount<br>Due Date: $invoice_due<br><br><br>To view your invoice, please click <a href=\'https://$config_base_url/guest/guest_view_invoice.php?invoice_id=$new_invoice_id&url_key=$invoice_url_key\'>here</a>.<br><br><br>--<br>$company_name - Billing<br>$company_phone";
|
||||||
$subject = "Invoice $invoice_prefix$invoice_number";
|
|
||||||
$body = "Hello $contact_name,<br><br>An invoice regarding \"$invoice_scope\" has been generated. Please view the details below.<br><br>Invoice: $invoice_prefix$invoice_number<br>Issue Date: $invoice_date<br>Total: $$invoice_amount<br>Due Date: $invoice_due<br><br><br>To view your invoice, please click <a href=\'https://$config_base_url/guest/guest_view_invoice.php?invoice_id=$new_invoice_id&url_key=$invoice_url_key\'>here</a>.<br><br><br>--<br>$company_name - Billing<br>$company_phone";
|
|
||||||
|
|
||||||
|
|
||||||
$data = [
|
$data = [
|
||||||
[
|
[
|
||||||
'from' => $config_invoice_from_email,
|
'from' => $config_invoice_from_email,
|
||||||
'from_name' => $config_invoice_from_name,
|
'from_name' => $config_invoice_from_name,
|
||||||
'recipient' => $contact_email,
|
'recipient' => $contact_email,
|
||||||
'recipient_name' => $contact_name,
|
'recipient_name' => $contact_name,
|
||||||
'subject' => $subject,
|
'subject' => $subject,
|
||||||
'body' => $body
|
'body' => $body
|
||||||
]
|
]
|
||||||
];
|
];
|
||||||
$mail = addToMailQueue($data);
|
$mail = addToMailQueue($data);
|
||||||
|
|
||||||
if ($mail === true) {
|
if ($mail === true) {
|
||||||
// Add send history
|
// Add send history
|
||||||
mysqli_query($mysqli,"INSERT INTO history SET history_status = 'Sent', history_description = 'Force Emailed Invoice!', history_invoice_id = $new_invoice_id");
|
mysqli_query($mysqli,"INSERT INTO history SET history_status = 'Sent', history_description = 'Force Emailed Invoice!', history_invoice_id = $new_invoice_id");
|
||||||
|
|
||||||
// Update Invoice Status to Sent
|
// Update Invoice Status to Sent
|
||||||
mysqli_query($mysqli,"UPDATE invoices SET invoice_status = 'Sent', invoice_client_id = $client_id WHERE invoice_id = $new_invoice_id");
|
mysqli_query($mysqli,"UPDATE invoices SET invoice_status = 'Sent', invoice_client_id = $client_id WHERE invoice_id = $new_invoice_id");
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
// Error reporting
|
// Error reporting
|
||||||
appNotify("Mail", "Failed to send email to $contact_email");
|
appNotify("Mail", "Failed to send email to $contact_email");
|
||||||
|
|
||||||
logAudit("Mail", "Error", "Failed to send email to $contact_email regarding $subject. $mail");
|
logAudit("Mail", "Error", "Failed to send email to $contact_email regarding $subject. $mail");
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
} //End Recurring Invoices Mail Loop
|
} //End Recurring Invoices Loop
|
||||||
}
|
|
||||||
|
|
||||||
logAudit("Invoice", "Create", "$session_name forced recurring invoice into an invoice", $client_id, $new_invoice_id);
|
logAudit("Invoice", "Create", "$session_name forced recurring invoice into an invoice", $client_id, $new_invoice_id);
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -128,7 +128,14 @@ if (isset($_GET['quote_id'])) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
//Product autocomplete
|
//Product autocomplete
|
||||||
$json_products = getProductsForAutocomplete($mysqli);
|
$products_sql = mysqli_query($mysqli, "SELECT product_name AS label, product_description AS description, product_price AS price, product_tax_id AS tax FROM products WHERE product_archived_at IS NULL");
|
||||||
|
|
||||||
|
if (mysqli_num_rows($products_sql) > 0) {
|
||||||
|
while ($row = mysqli_fetch_assoc($products_sql)) {
|
||||||
|
$products[] = $row;
|
||||||
|
}
|
||||||
|
$json_products = json_encode($products);
|
||||||
|
}
|
||||||
|
|
||||||
// Quote File Attachments
|
// Quote File Attachments
|
||||||
$sql_quote_files = mysqli_query(
|
$sql_quote_files = mysqli_query(
|
||||||
@@ -396,7 +403,6 @@ if (isset($_GET['quote_id'])) {
|
|||||||
<form action="post.php" method="post" autocomplete="off">
|
<form action="post.php" method="post" autocomplete="off">
|
||||||
<input type="hidden" name="csrf_token" value="<?= $_SESSION['csrf_token'] ?>">
|
<input type="hidden" name="csrf_token" value="<?= $_SESSION['csrf_token'] ?>">
|
||||||
<input type="hidden" name="quote_id" value="<?= $quote_id ?>">
|
<input type="hidden" name="quote_id" value="<?= $quote_id ?>">
|
||||||
<input type="hidden" id="product_id" name="product_id" value="0">
|
|
||||||
<input type="hidden" name="item_order" value="<?php
|
<input type="hidden" name="item_order" value="<?php
|
||||||
//find largest order number and add 1
|
//find largest order number and add 1
|
||||||
$sql = mysqli_query($mysqli, "SELECT MAX(item_order) AS item_order FROM quote_items WHERE item_quote_id = $quote_id");
|
$sql = mysqli_query($mysqli, "SELECT MAX(item_order) AS item_order FROM quote_items WHERE item_quote_id = $quote_id");
|
||||||
@@ -637,12 +643,68 @@ require_once "../includes/footer.php";
|
|||||||
|
|
||||||
?>
|
?>
|
||||||
|
|
||||||
<!-- Product autocomplete for the add-item row -->
|
<!-- JSON Autocomplete / type ahead -->
|
||||||
<script src="/js/product_autocomplete.js"></script>
|
<!-- //TODO: Move to js/ -->
|
||||||
<script>
|
<script>
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', function () {
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
initProductAutocomplete(<?= $json_products ?? '[]' ?>);
|
|
||||||
|
var availableProducts = <?= $json_products ?? '[]' ?>;
|
||||||
|
|
||||||
|
var nameInput = document.getElementById('name');
|
||||||
|
if (!nameInput) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
itflowAutocomplete(nameInput, {
|
||||||
|
minLength: 1,
|
||||||
|
source: availableProducts,
|
||||||
|
match: function (item, term) {
|
||||||
|
return String(item.label || '').toLowerCase().indexOf(term) !== -1
|
||||||
|
|| String(item.product_name || '').toLowerCase().indexOf(term) !== -1
|
||||||
|
|| String(item.product_code || '').toLowerCase().indexOf(term) !== -1;
|
||||||
|
},
|
||||||
|
render: function (item) {
|
||||||
|
var esc = itflowEscapeHtml;
|
||||||
|
var typeText = item.type ? item.type.charAt(0).toUpperCase() + item.type.slice(1).toLowerCase() : "";
|
||||||
|
var showStock = (typeText.toLowerCase() !== "service");
|
||||||
|
var taxText = (item.tax_percent != null) ? (parseFloat(item.tax_percent) + "%") : "No tax";
|
||||||
|
var priceText = (item.price != null && item.price !== "") ? String(item.price) : "";
|
||||||
|
var stockText = (item.available_stock ?? 0);
|
||||||
|
|
||||||
|
return "<div class='d-flex justify-content-between align-items-start'>" +
|
||||||
|
"<div class='flex-fill pe-2'>" +
|
||||||
|
"<div class='fw-bold'>" + esc(item.label) +
|
||||||
|
(typeText ? " <small class='text-muted'>(" + esc(typeText) + ")</small>" : "") +
|
||||||
|
"</div>" +
|
||||||
|
"<div class='small text-muted'>" + esc(item.description) + "</div>" +
|
||||||
|
"<div class='mt-1'>" +
|
||||||
|
"<span class='badge bg-secondary me-1'>Tax: " + esc(taxText) + "</span>" +
|
||||||
|
(showStock ? "<span class='badge " + (stockText > 0 ? "bg-success" : "bg-danger") + "'>Stock: " + esc(stockText) + "</span>" : "") +
|
||||||
|
"</div>" +
|
||||||
|
"</div>" +
|
||||||
|
"<div class='text-end'>" +
|
||||||
|
"<div class='fw-bold'>" + esc(priceText) + "</div>" +
|
||||||
|
"</div>" +
|
||||||
|
"</div>";
|
||||||
|
},
|
||||||
|
onSelect: function (item) {
|
||||||
|
document.getElementById('name').value = item.product_name;
|
||||||
|
document.getElementById('desc').value = item.description;
|
||||||
|
document.getElementById('qty').value = 1;
|
||||||
|
document.getElementById('price').value = item.price;
|
||||||
|
setTomSelectValue(document.getElementById('tax'), item.tax);
|
||||||
|
document.getElementById('product_id').value = item.prod_id;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Typing over the name by hand breaks the link to the product
|
||||||
|
nameInput.addEventListener('input', function () {
|
||||||
|
document.getElementById('product_id').value = 0;
|
||||||
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script src="../libs/SortableJS/Sortable.min.js"></script>
|
<script src="../libs/SortableJS/Sortable.min.js"></script>
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ if (isset($_GET['recurring_invoice_id'])) {
|
|||||||
recurring_invoice_category_id, recurring_invoice_created_at,
|
recurring_invoice_category_id, recurring_invoice_created_at,
|
||||||
recurring_invoice_currency_code, recurring_invoice_discount_amount,
|
recurring_invoice_currency_code, recurring_invoice_discount_amount,
|
||||||
recurring_invoice_email_notify, recurring_invoice_frequency, recurring_invoice_last_sent,
|
recurring_invoice_email_notify, recurring_invoice_frequency, recurring_invoice_last_sent,
|
||||||
recurring_invoice_next_date, recurring_invoice_note, recurring_invoice_auto_send, recurring_invoice_number,
|
recurring_invoice_next_date, recurring_invoice_note, recurring_invoice_number,
|
||||||
recurring_invoice_prefix, recurring_invoice_scope, recurring_invoice_status,
|
recurring_invoice_prefix, recurring_invoice_scope, recurring_invoice_status,
|
||||||
recurring_payment_id, recurring_payment_method, recurring_payment_recurring_invoice_id,
|
recurring_payment_id, recurring_payment_method, recurring_payment_recurring_invoice_id,
|
||||||
recurring_payment_saved_payment_id FROM recurring_invoices
|
recurring_payment_saved_payment_id FROM recurring_invoices
|
||||||
@@ -61,7 +61,6 @@ if (isset($_GET['recurring_invoice_id'])) {
|
|||||||
$recurring_invoice_discount = floatval($row['recurring_invoice_discount_amount']);
|
$recurring_invoice_discount = floatval($row['recurring_invoice_discount_amount']);
|
||||||
$recurring_invoice_currency_code = escapeHtml($row['recurring_invoice_currency_code']);
|
$recurring_invoice_currency_code = escapeHtml($row['recurring_invoice_currency_code']);
|
||||||
$recurring_invoice_note = escapeHtml($row['recurring_invoice_note']);
|
$recurring_invoice_note = escapeHtml($row['recurring_invoice_note']);
|
||||||
$recurring_invoice_auto_send = intval($row['recurring_invoice_auto_send']);
|
|
||||||
$recurring_invoice_email_notify = intval($row['recurring_invoice_email_notify']);
|
$recurring_invoice_email_notify = intval($row['recurring_invoice_email_notify']);
|
||||||
$category_id = intval($row['recurring_invoice_category_id']);
|
$category_id = intval($row['recurring_invoice_category_id']);
|
||||||
$client_id = intval($row['client_id']);
|
$client_id = intval($row['client_id']);
|
||||||
@@ -118,7 +117,14 @@ if (isset($_GET['recurring_invoice_id'])) {
|
|||||||
$sql_history = mysqli_query($mysqli, "SELECT history_created_at, history_description, history_status FROM history WHERE history_recurring_invoice_id = $recurring_invoice_id ORDER BY history_id DESC");
|
$sql_history = mysqli_query($mysqli, "SELECT history_created_at, history_description, history_status FROM history WHERE history_recurring_invoice_id = $recurring_invoice_id ORDER BY history_id DESC");
|
||||||
|
|
||||||
//Product autocomplete
|
//Product autocomplete
|
||||||
$json_products = getProductsForAutocomplete($mysqli);
|
$products_sql = mysqli_query($mysqli, "SELECT product_name AS label, product_description AS description, product_price AS price, product_tax_id AS tax FROM products WHERE product_archived_at IS NULL");
|
||||||
|
|
||||||
|
if (mysqli_num_rows($products_sql) > 0) {
|
||||||
|
while ($row = mysqli_fetch_assoc($products_sql)) {
|
||||||
|
$products[] = $row;
|
||||||
|
}
|
||||||
|
$json_products = json_encode($products);
|
||||||
|
}
|
||||||
|
|
||||||
enforceClientAccess();
|
enforceClientAccess();
|
||||||
|
|
||||||
@@ -149,12 +155,10 @@ if (isset($_GET['recurring_invoice_id'])) {
|
|||||||
<div class="row">
|
<div class="row">
|
||||||
|
|
||||||
<div class="col-2">
|
<div class="col-2">
|
||||||
<?php if ($recurring_invoice_auto_send) { ?>
|
<?php if ($recurring_invoice_email_notify) { ?>
|
||||||
<?php if ($recurring_invoice_email_notify) { ?>
|
<a href="post.php?recurring_invoice_email_notify=0&recurring_invoice_id=<?= $recurring_invoice_id ?>&csrf_token=<?= $_SESSION['csrf_token'] ?>" class="btn btn-primary"><i class="fas fa-fw fa-bell me-2"></i>Email Notify</a>
|
||||||
<a href="post.php?recurring_invoice_email_notify=0&recurring_invoice_id=<?= $recurring_invoice_id ?>&csrf_token=<?= $_SESSION['csrf_token'] ?>" class="btn btn-primary"><i class="fas fa-fw fa-bell me-2"></i>Email Notify</a>
|
<?php } else { ?>
|
||||||
<?php } else { ?>
|
<a href="post.php?recurring_invoice_email_notify=1&recurring_invoice_id=<?= $recurring_invoice_id ?>&csrf_token=<?= $_SESSION['csrf_token'] ?>" class="btn btn-outline-danger"><i class="fas fa-fw fa-bell-slash me-2"></i>Email Notify</a>
|
||||||
<a href="post.php?recurring_invoice_email_notify=1&recurring_invoice_id=<?= $recurring_invoice_id ?>&csrf_token=<?= $_SESSION['csrf_token'] ?>" class="btn btn-outline-danger"><i class="fas fa-fw fa-bell-slash me-2"></i>Email Notify</a>
|
|
||||||
<?php } ?>
|
|
||||||
<?php } ?>
|
<?php } ?>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-3">
|
<div class="col-3">
|
||||||
@@ -336,7 +340,6 @@ if (isset($_GET['recurring_invoice_id'])) {
|
|||||||
<form action="post.php" method="post">
|
<form 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'] ?>">
|
||||||
<input type="hidden" name="recurring_invoice_id" value="<?= $recurring_invoice_id ?>">
|
<input type="hidden" name="recurring_invoice_id" value="<?= $recurring_invoice_id ?>">
|
||||||
<input type="hidden" id="product_id" name="product_id" value="0">
|
|
||||||
<input type="hidden" name="item_order" value="<?php
|
<input type="hidden" name="item_order" value="<?php
|
||||||
//find largest order number and add 1
|
//find largest order number and add 1
|
||||||
$sql = mysqli_query($mysqli, "SELECT MAX(item_order) AS item_order FROM recurring_invoice_items WHERE item_recurring_invoice_id = $recurring_invoice_id");
|
$sql = mysqli_query($mysqli, "SELECT MAX(item_order) AS item_order FROM recurring_invoice_items WHERE item_recurring_invoice_id = $recurring_invoice_id");
|
||||||
@@ -358,7 +361,7 @@ if (isset($_GET['recurring_invoice_id'])) {
|
|||||||
<input type="text" inputmode="decimal" pattern="[0-9]*\.?[0-9]{0,2}" class="form-control" style="text-align: right;" id="price" name="price" placeholder="Price (<?= $recurring_invoice_currency_code ?>)">
|
<input type="text" inputmode="decimal" pattern="[0-9]*\.?[0-9]{0,2}" class="form-control" style="text-align: right;" id="price" name="price" placeholder="Price (<?= $recurring_invoice_currency_code ?>)">
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<select class="form-select select2" name="tax_id" id="tax" required>
|
<select class="form-select" name="tax_id" id="tax" required>
|
||||||
<option value="0">No Tax</option>
|
<option value="0">No Tax</option>
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
@@ -503,12 +506,67 @@ require_once "../includes/footer.php";
|
|||||||
|
|
||||||
?>
|
?>
|
||||||
|
|
||||||
<!-- Product autocomplete for the add-item row -->
|
<!-- JSON Autocomplete / type ahead -->
|
||||||
<script src="/js/product_autocomplete.js"></script>
|
|
||||||
<script>
|
<script>
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', function () {
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
initProductAutocomplete(<?= $json_products ?? '[]' ?>);
|
|
||||||
|
var availableProducts = <?= $json_products ?? '[]' ?>;
|
||||||
|
|
||||||
|
var nameInput = document.getElementById('name');
|
||||||
|
if (!nameInput) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
itflowAutocomplete(nameInput, {
|
||||||
|
minLength: 1,
|
||||||
|
source: availableProducts,
|
||||||
|
match: function (item, term) {
|
||||||
|
return String(item.label || '').toLowerCase().indexOf(term) !== -1
|
||||||
|
|| String(item.product_name || '').toLowerCase().indexOf(term) !== -1
|
||||||
|
|| String(item.product_code || '').toLowerCase().indexOf(term) !== -1;
|
||||||
|
},
|
||||||
|
render: function (item) {
|
||||||
|
var esc = itflowEscapeHtml;
|
||||||
|
var typeText = item.type ? item.type.charAt(0).toUpperCase() + item.type.slice(1).toLowerCase() : "";
|
||||||
|
var showStock = (typeText.toLowerCase() !== "service");
|
||||||
|
var taxText = (item.tax_percent != null) ? (parseFloat(item.tax_percent) + "%") : "No tax";
|
||||||
|
var priceText = (item.price != null && item.price !== "") ? String(item.price) : "";
|
||||||
|
var stockText = (item.available_stock ?? 0);
|
||||||
|
|
||||||
|
return "<div class='d-flex justify-content-between align-items-start'>" +
|
||||||
|
"<div class='flex-fill pe-2'>" +
|
||||||
|
"<div class='fw-bold'>" + esc(item.label) +
|
||||||
|
(typeText ? " <small class='text-muted'>(" + esc(typeText) + ")</small>" : "") +
|
||||||
|
"</div>" +
|
||||||
|
"<div class='small text-muted'>" + esc(item.description) + "</div>" +
|
||||||
|
"<div class='mt-1'>" +
|
||||||
|
"<span class='badge bg-secondary me-1'>Tax: " + esc(taxText) + "</span>" +
|
||||||
|
(showStock ? "<span class='badge " + (stockText > 0 ? "bg-success" : "bg-danger") + "'>Stock: " + esc(stockText) + "</span>" : "") +
|
||||||
|
"</div>" +
|
||||||
|
"</div>" +
|
||||||
|
"<div class='text-end'>" +
|
||||||
|
"<div class='fw-bold'>" + esc(priceText) + "</div>" +
|
||||||
|
"</div>" +
|
||||||
|
"</div>";
|
||||||
|
},
|
||||||
|
onSelect: function (item) {
|
||||||
|
document.getElementById('name').value = item.product_name;
|
||||||
|
document.getElementById('desc').value = item.description;
|
||||||
|
document.getElementById('qty').value = 1;
|
||||||
|
document.getElementById('price').value = item.price;
|
||||||
|
setTomSelectValue(document.getElementById('tax'), item.tax);
|
||||||
|
document.getElementById('product_id').value = item.prod_id;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Typing over the name by hand breaks the link to the product
|
||||||
|
nameInput.addEventListener('input', function () {
|
||||||
|
document.getElementById('product_id').value = 0;
|
||||||
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script src="../libs/SortableJS/Sortable.min.js"></script>
|
<script src="../libs/SortableJS/Sortable.min.js"></script>
|
||||||
|
|||||||
@@ -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)) {
|
||||||
|
|||||||
@@ -1,44 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
require_once '../validate_api_key.php';
|
|
||||||
require_once '../require_post_method.php';
|
|
||||||
|
|
||||||
// Parse ID
|
|
||||||
$credential_id = intval($_POST['credential_id']);
|
|
||||||
|
|
||||||
// Default
|
|
||||||
$update_count = false;
|
|
||||||
|
|
||||||
if (!empty($credential_id)) {
|
|
||||||
|
|
||||||
// Fetch credential info
|
|
||||||
$row = mysqli_fetch_assoc(mysqli_query($mysqli, "
|
|
||||||
SELECT credential_name, credential_client_id
|
|
||||||
FROM credentials
|
|
||||||
WHERE credential_id = $credential_id AND credential_client_id = $client_id AND credential_archived_at IS NULL
|
|
||||||
LIMIT 1
|
|
||||||
"));
|
|
||||||
|
|
||||||
if ($row) {
|
|
||||||
|
|
||||||
$credential_name = escapeSql($row['credential_name']);
|
|
||||||
|
|
||||||
// Archive credential
|
|
||||||
$update_sql = mysqli_query($mysqli, "
|
|
||||||
UPDATE credentials SET
|
|
||||||
credential_favorite = 0,
|
|
||||||
credential_archived_at = NOW()
|
|
||||||
WHERE credential_id = $credential_id AND credential_client_id = $client_id
|
|
||||||
");
|
|
||||||
|
|
||||||
if ($update_sql) {
|
|
||||||
$update_count = mysqli_affected_rows($mysqli);
|
|
||||||
|
|
||||||
// Logging
|
|
||||||
logAudit("Credential", "Archive", "$credential_name archived via API ($api_key_name)", $client_id, $credential_id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Output
|
|
||||||
require_once '../update_output.php';
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
require_once '../validate_api_key.php';
|
|
||||||
|
|
||||||
require_once '../require_post_method.php';
|
|
||||||
|
|
||||||
|
|
||||||
// Parse ID
|
|
||||||
$credential_id = intval($_POST['credential_id']);
|
|
||||||
|
|
||||||
// Default
|
|
||||||
$delete_count = false;
|
|
||||||
|
|
||||||
if (!empty($credential_id)) {
|
|
||||||
$row = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT credential_name FROM credentials WHERE credential_id = $credential_id AND credential_client_id = $client_id AND credential_archived_at IS NOT NULL LIMIT 1"));
|
|
||||||
$credential_name = escapeSql($row['credential_name'] ?? '');
|
|
||||||
|
|
||||||
$delete_sql = mysqli_query($mysqli, "DELETE FROM credentials WHERE credential_id = $credential_id AND credential_client_id = $client_id AND credential_archived_at IS NOT NULL LIMIT 1");
|
|
||||||
|
|
||||||
// Check delete & get affected rows
|
|
||||||
if ($delete_sql && !empty($credential_name)) {
|
|
||||||
$delete_count = mysqli_affected_rows($mysqli);
|
|
||||||
|
|
||||||
// Logging
|
|
||||||
logAudit("Credential", "Delete", "$credential_name via API ($api_key_name)", $client_id, $credential_id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Output
|
|
||||||
require_once '../delete_output.php';
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
require_once '../validate_api_key.php';
|
|
||||||
require_once '../require_post_method.php';
|
|
||||||
|
|
||||||
// Parse ID
|
|
||||||
$credential_id = intval($_POST['credential_id']);
|
|
||||||
|
|
||||||
// Default
|
|
||||||
$update_count = false;
|
|
||||||
|
|
||||||
if (!empty($credential_id)) {
|
|
||||||
|
|
||||||
// Fetch credential info
|
|
||||||
$row = mysqli_fetch_assoc(mysqli_query($mysqli, "
|
|
||||||
SELECT credential_name, credential_client_id
|
|
||||||
FROM credentials
|
|
||||||
WHERE credential_id = $credential_id AND credential_client_id = $client_id AND credential_archived_at IS NOT NULL
|
|
||||||
LIMIT 1
|
|
||||||
"));
|
|
||||||
|
|
||||||
if ($row) {
|
|
||||||
|
|
||||||
$credential_name = escapeSql($row['credential_name']);
|
|
||||||
|
|
||||||
// Unarchive credential
|
|
||||||
$update_sql = mysqli_query($mysqli, "
|
|
||||||
UPDATE credentials SET credential_archived_at = NULL
|
|
||||||
WHERE credential_id = $credential_id AND credential_client_id = $client_id
|
|
||||||
");
|
|
||||||
|
|
||||||
if ($update_sql) {
|
|
||||||
$update_count = mysqli_affected_rows($mysqli);
|
|
||||||
|
|
||||||
// Logging
|
|
||||||
logAudit("Credential", "Unarchive", "$credential_name unarchived via API ($api_key_name)", $client_id, $credential_id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Output
|
|
||||||
require_once '../update_output.php';
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
require_once '../validate_api_key.php';
|
|
||||||
require_once '../require_post_method.php';
|
|
||||||
|
|
||||||
// Parse ID
|
|
||||||
$document_id = intval($_POST['document_id'] ?? 0);
|
|
||||||
|
|
||||||
// Default
|
|
||||||
$update_count = false;
|
|
||||||
|
|
||||||
if (!empty($document_id)) {
|
|
||||||
|
|
||||||
// Fetch document info
|
|
||||||
$row = mysqli_fetch_assoc(mysqli_query($mysqli, "
|
|
||||||
SELECT document_name
|
|
||||||
FROM documents
|
|
||||||
WHERE document_id = $document_id AND document_client_id = $client_id AND document_archived_at IS NULL
|
|
||||||
LIMIT 1
|
|
||||||
"));
|
|
||||||
|
|
||||||
if ($row) {
|
|
||||||
|
|
||||||
$document_name = escapeSql($row['document_name']);
|
|
||||||
|
|
||||||
// Archive document
|
|
||||||
$update_sql = mysqli_query($mysqli, "
|
|
||||||
UPDATE documents SET document_archived_at = NOW()
|
|
||||||
WHERE document_id = $document_id AND document_client_id = $client_id
|
|
||||||
");
|
|
||||||
|
|
||||||
if ($update_sql) {
|
|
||||||
$update_count = mysqli_affected_rows($mysqli);
|
|
||||||
|
|
||||||
// Logging
|
|
||||||
logAudit("Document", "Archive", "$document_name archived via API ($api_key_name)", $client_id, $document_id);
|
|
||||||
logAudit("API", "Success", "Archived document $document_name via API ($api_key_name)", $client_id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Output
|
|
||||||
require_once '../update_output.php';
|
|
||||||
@@ -8,11 +8,11 @@ require_once '../require_get_method.php';
|
|||||||
if (isset($_GET['document_id'])) {
|
if (isset($_GET['document_id'])) {
|
||||||
// Document via ID (single)
|
// Document via ID (single)
|
||||||
$id = intval($_GET['document_id']);
|
$id = intval($_GET['document_id']);
|
||||||
$sql = mysqli_query($mysqli, "SELECT * FROM documents WHERE document_id = '$id' AND document_archived_at IS NULL AND 1=1 " . apiClientScopeSql('document_client_id') . "");
|
$sql = mysqli_query($mysqli, "SELECT * FROM documents WHERE document_id = '$id' AND 1=1 " . apiClientScopeSql('document_client_id') . "");
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
// All documents (by client ID if given, or all in general if key permits)
|
// All documents (by client ID if given, or all in general if key permits)
|
||||||
$sql = mysqli_query($mysqli, "SELECT * FROM documents WHERE document_archived_at IS NULL AND 1=1 " . apiClientScopeSql('document_client_id') . " ORDER BY document_id LIMIT $limit OFFSET $offset");
|
$sql = mysqli_query($mysqli, "SELECT * FROM documents WHERE 1=1 " . apiClientScopeSql('document_client_id') . " ORDER BY document_id LIMIT $limit OFFSET $offset");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Output
|
// Output
|
||||||
|
|||||||
@@ -1,43 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
require_once '../validate_api_key.php';
|
|
||||||
require_once '../require_post_method.php';
|
|
||||||
|
|
||||||
// Parse ID
|
|
||||||
$document_id = intval($_POST['document_id'] ?? 0);
|
|
||||||
|
|
||||||
// Default
|
|
||||||
$update_count = false;
|
|
||||||
|
|
||||||
if (!empty($document_id)) {
|
|
||||||
|
|
||||||
// Fetch document info
|
|
||||||
$row = mysqli_fetch_assoc(mysqli_query($mysqli, "
|
|
||||||
SELECT document_name
|
|
||||||
FROM documents
|
|
||||||
WHERE document_id = $document_id AND document_client_id = $client_id AND document_archived_at IS NOT NULL
|
|
||||||
LIMIT 1
|
|
||||||
"));
|
|
||||||
|
|
||||||
if ($row) {
|
|
||||||
|
|
||||||
$document_name = escapeSql($row['document_name']);
|
|
||||||
|
|
||||||
// Unarchive document
|
|
||||||
$update_sql = mysqli_query($mysqli, "
|
|
||||||
UPDATE documents SET document_archived_at = NULL
|
|
||||||
WHERE document_id = $document_id AND document_client_id = $client_id
|
|
||||||
");
|
|
||||||
|
|
||||||
if ($update_sql) {
|
|
||||||
$update_count = mysqli_affected_rows($mysqli);
|
|
||||||
|
|
||||||
// Logging
|
|
||||||
logAudit("Document", "Unarchive", "$document_name unarchived via API ($api_key_name)", $client_id, $document_id);
|
|
||||||
logAudit("API", "Success", "Unarchived document $document_name via API ($api_key_name)", $client_id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Output
|
|
||||||
require_once '../update_output.php';
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
require_once '../validate_api_key.php';
|
|
||||||
require_once '../require_post_method.php';
|
|
||||||
|
|
||||||
$domain_id = intval($_POST['domain_id']);
|
|
||||||
$update_count = false;
|
|
||||||
|
|
||||||
if (!empty($domain_id)) {
|
|
||||||
$row = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT domain_name FROM domains WHERE domain_id = $domain_id AND domain_client_id = $client_id AND domain_archived_at IS NULL LIMIT 1"));
|
|
||||||
|
|
||||||
if ($row) {
|
|
||||||
$domain_name = escapeSql($row['domain_name']);
|
|
||||||
$update_sql = mysqli_query($mysqli, "UPDATE domains SET domain_archived_at = NOW() WHERE domain_id = $domain_id AND domain_client_id = $client_id AND domain_archived_at IS NULL");
|
|
||||||
|
|
||||||
if ($update_sql) {
|
|
||||||
$update_count = mysqli_affected_rows($mysqli);
|
|
||||||
logAudit("Domain", "Archive", "$domain_name archived via API ($api_key_name)", $client_id, $domain_id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
require_once '../update_output.php';
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
require_once '../validate_api_key.php';
|
|
||||||
require_once '../require_post_method.php';
|
|
||||||
|
|
||||||
require_once 'domain_model.php';
|
|
||||||
|
|
||||||
$insert_id = false;
|
|
||||||
|
|
||||||
if (!empty($name) && !empty($client_id)) {
|
|
||||||
// Use the supplied expiry when valid, otherwise look it up from WHOIS.
|
|
||||||
if (strtotime($expire)) {
|
|
||||||
$expire = "'$expire'";
|
|
||||||
} else {
|
|
||||||
$expire = getDomainExpirationDate($name);
|
|
||||||
$expire = strtotime($expire) ? "'$expire'" : 'NULL';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Populate DNS and WHOIS data from the domain rather than accepting it from the API.
|
|
||||||
$records = getDnsRecords($name);
|
|
||||||
$ip = escapeSql($records['a']);
|
|
||||||
$name_servers = escapeSql($records['ns']);
|
|
||||||
$mail_servers = escapeSql($records['mx']);
|
|
||||||
$txt = escapeSql($records['txt']);
|
|
||||||
$raw_whois = escapeSql($records['whois']);
|
|
||||||
|
|
||||||
$insert_sql = mysqli_query($mysqli, "INSERT INTO domains SET domain_name = '$name', domain_description = '$description', domain_expire = $expire, domain_ip = '$ip', domain_name_servers = '$name_servers', domain_mail_servers = '$mail_servers', domain_txt = '$txt', domain_raw_whois = '$raw_whois', domain_notes = '$notes', domain_registrar = $registrar, domain_webhost = $webhost, domain_dnshost = $dnshost, domain_mailhost = $mailhost, domain_client_id = $client_id");
|
|
||||||
|
|
||||||
if ($insert_sql) {
|
|
||||||
$insert_id = mysqli_insert_id($mysqli);
|
|
||||||
|
|
||||||
logAudit("Domain", "Create", "$name via API ($api_key_name)", $client_id, $insert_id);
|
|
||||||
logAudit("API", "Success", "Created domain $name via API ($api_key_name)", $client_id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
require_once '../create_output.php';
|
|
||||||
@@ -1,67 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
// Variable assignment from POST (or: blank/from DB is updating)
|
|
||||||
|
|
||||||
if (isset($_POST['domain_name'])) {
|
|
||||||
$name = preg_replace("(^https?://)", "", escapeSql($_POST['domain_name']));
|
|
||||||
} elseif (isset($domain_row) && isset($domain_row['domain_name'])) {
|
|
||||||
$name = mysqli_real_escape_string($mysqli, $domain_row['domain_name']);
|
|
||||||
} else {
|
|
||||||
$name = '';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isset($_POST['domain_description'])) {
|
|
||||||
$description = escapeSql($_POST['domain_description']);
|
|
||||||
} elseif (isset($domain_row) && isset($domain_row['domain_description'])) {
|
|
||||||
$description = mysqli_real_escape_string($mysqli, $domain_row['domain_description']);
|
|
||||||
} else {
|
|
||||||
$description = '';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isset($_POST['domain_expire'])) {
|
|
||||||
$expire = escapeSql($_POST['domain_expire']);
|
|
||||||
} elseif (isset($domain_row) && !empty($domain_row['domain_expire'])) {
|
|
||||||
$expire = mysqli_real_escape_string($mysqli, $domain_row['domain_expire']);
|
|
||||||
} else {
|
|
||||||
$expire = '';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isset($_POST['domain_notes'])) {
|
|
||||||
$notes = escapeSql($_POST['domain_notes']);
|
|
||||||
} elseif (isset($domain_row) && isset($domain_row['domain_notes'])) {
|
|
||||||
$notes = mysqli_real_escape_string($mysqli, $domain_row['domain_notes']);
|
|
||||||
} else {
|
|
||||||
$notes = '';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isset($_POST['domain_registrar'])) {
|
|
||||||
$registrar = intval($_POST['domain_registrar']);
|
|
||||||
} elseif (isset($domain_row) && isset($domain_row['domain_registrar'])) {
|
|
||||||
$registrar = $domain_row['domain_registrar'];
|
|
||||||
} else {
|
|
||||||
$registrar = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isset($_POST['domain_webhost'])) {
|
|
||||||
$webhost = intval($_POST['domain_webhost']);
|
|
||||||
} elseif (isset($domain_row) && isset($domain_row['domain_webhost'])) {
|
|
||||||
$webhost = $domain_row['domain_webhost'];
|
|
||||||
} else {
|
|
||||||
$webhost = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isset($_POST['domain_dnshost'])) {
|
|
||||||
$dnshost = intval($_POST['domain_dnshost']);
|
|
||||||
} elseif (isset($domain_row) && isset($domain_row['domain_dnshost'])) {
|
|
||||||
$dnshost = $domain_row['domain_dnshost'];
|
|
||||||
} else {
|
|
||||||
$dnshost = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isset($_POST['domain_mailhost'])) {
|
|
||||||
$mailhost = intval($_POST['domain_mailhost']);
|
|
||||||
} elseif (isset($domain_row) && isset($domain_row['domain_mailhost'])) {
|
|
||||||
$mailhost = $domain_row['domain_mailhost'];
|
|
||||||
} else {
|
|
||||||
$mailhost = 0;
|
|
||||||
}
|
|
||||||
@@ -8,16 +8,16 @@ require_once '../require_get_method.php';
|
|||||||
// Specific domain via ID (single)
|
// Specific domain via ID (single)
|
||||||
if (isset($_GET['domain_id'])) {
|
if (isset($_GET['domain_id'])) {
|
||||||
$id = intval($_GET['domain_id']);
|
$id = intval($_GET['domain_id']);
|
||||||
$sql = mysqli_query($mysqli, "SELECT * FROM domains WHERE domain_id = '$id' AND domain_archived_at IS NULL AND 1=1 " . apiClientScopeSql('domain_client_id') . "");
|
$sql = mysqli_query($mysqli, "SELECT * FROM domains WHERE domain_id = '$id' AND 1=1 " . apiClientScopeSql('domain_client_id') . "");
|
||||||
|
|
||||||
} elseif (isset($_GET['domain_name'])) {
|
} elseif (isset($_GET['domain_name'])) {
|
||||||
// Domain by name
|
// Domain by name
|
||||||
$name = mysqli_real_escape_string($mysqli, $_GET['domain_name']);
|
$name = mysqli_real_escape_string($mysqli, $_GET['domain_name']);
|
||||||
$sql = mysqli_query($mysqli, "SELECT * FROM domains WHERE domain_name = '$name' AND domain_archived_at IS NULL AND 1=1 " . apiClientScopeSql('domain_client_id') . " ORDER BY domain_id LIMIT $limit OFFSET $offset");
|
$sql = mysqli_query($mysqli, "SELECT * FROM domains WHERE domain_name = '$name' AND 1=1 " . apiClientScopeSql('domain_client_id') . " ORDER BY domain_id LIMIT $limit OFFSET $offset");
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
// All domains (by client ID or all in general if key permits)
|
// All domains (by client ID or all in general if key permits)
|
||||||
$sql = mysqli_query($mysqli, "SELECT * FROM domains WHERE domain_archived_at IS NULL AND 1=1 " . apiClientScopeSql('domain_client_id') . " ORDER BY domain_id LIMIT $limit OFFSET $offset");
|
$sql = mysqli_query($mysqli, "SELECT * FROM domains WHERE 1=1 " . apiClientScopeSql('domain_client_id') . " ORDER BY domain_id LIMIT $limit OFFSET $offset");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Output
|
// Output
|
||||||
|
|||||||
@@ -1,23 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
require_once '../validate_api_key.php';
|
|
||||||
require_once '../require_post_method.php';
|
|
||||||
|
|
||||||
$domain_id = intval($_POST['domain_id']);
|
|
||||||
$update_count = false;
|
|
||||||
|
|
||||||
if (!empty($domain_id)) {
|
|
||||||
$row = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT domain_name FROM domains WHERE domain_id = $domain_id AND domain_client_id = $client_id AND domain_archived_at IS NOT NULL LIMIT 1"));
|
|
||||||
|
|
||||||
if ($row) {
|
|
||||||
$domain_name = escapeSql($row['domain_name']);
|
|
||||||
$update_sql = mysqli_query($mysqli, "UPDATE domains SET domain_archived_at = NULL WHERE domain_id = $domain_id AND domain_client_id = $client_id AND domain_archived_at IS NOT NULL");
|
|
||||||
|
|
||||||
if ($update_sql) {
|
|
||||||
$update_count = mysqli_affected_rows($mysqli);
|
|
||||||
logAudit("Domain", "Unarchive", "$domain_name unarchived via API ($api_key_name)", $client_id, $domain_id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
require_once '../update_output.php';
|
|
||||||
@@ -1,92 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
require_once '../validate_api_key.php';
|
|
||||||
require_once '../require_post_method.php';
|
|
||||||
|
|
||||||
$domain_id = intval($_POST['domain_id']);
|
|
||||||
$update_count = false;
|
|
||||||
|
|
||||||
if (!empty($domain_id)) {
|
|
||||||
$domain_row = mysqli_fetch_assoc(mysqli_query($mysqli, "
|
|
||||||
SELECT
|
|
||||||
domains.*,
|
|
||||||
registrar.vendor_name AS registrar_name,
|
|
||||||
dnshost.vendor_name AS dnshost_name,
|
|
||||||
mailhost.vendor_name AS mailhost_name,
|
|
||||||
webhost.vendor_name AS webhost_name
|
|
||||||
FROM domains
|
|
||||||
LEFT JOIN vendors AS registrar ON domains.domain_registrar = registrar.vendor_id
|
|
||||||
LEFT JOIN vendors AS dnshost ON domains.domain_dnshost = dnshost.vendor_id
|
|
||||||
LEFT JOIN vendors AS mailhost ON domains.domain_mailhost = mailhost.vendor_id
|
|
||||||
LEFT JOIN vendors AS webhost ON domains.domain_webhost = webhost.vendor_id
|
|
||||||
WHERE domain_id = $domain_id AND domain_client_id = $client_id AND domain_archived_at IS NULL
|
|
||||||
LIMIT 1
|
|
||||||
"));
|
|
||||||
|
|
||||||
$original_domain_info = $domain_row;
|
|
||||||
|
|
||||||
require_once 'domain_model.php';
|
|
||||||
|
|
||||||
// Use the supplied expiry when valid, otherwise look it up from WHOIS.
|
|
||||||
if (strtotime($expire)) {
|
|
||||||
$expire = "'$expire'";
|
|
||||||
} else {
|
|
||||||
$expire = getDomainExpirationDate($name);
|
|
||||||
$expire = strtotime($expire) ? "'$expire'" : 'NULL';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Populate DNS and WHOIS data from the domain rather than accepting it from the API.
|
|
||||||
$records = getDnsRecords($name);
|
|
||||||
$ip = escapeSql($records['a']);
|
|
||||||
$name_servers = escapeSql($records['ns']);
|
|
||||||
$mail_servers = escapeSql($records['mx']);
|
|
||||||
$txt = escapeSql($records['txt']);
|
|
||||||
$raw_whois = escapeSql($records['whois']);
|
|
||||||
|
|
||||||
$update_sql = mysqli_query($mysqli, "UPDATE domains SET domain_name = '$name', domain_description = '$description', domain_expire = $expire, domain_ip = '$ip', domain_name_servers = '$name_servers', domain_mail_servers = '$mail_servers', domain_txt = '$txt', domain_raw_whois = '$raw_whois', domain_notes = '$notes', domain_registrar = $registrar, domain_webhost = $webhost, domain_dnshost = $dnshost, domain_mailhost = $mailhost WHERE domain_id = $domain_id AND domain_client_id = $client_id AND domain_archived_at IS NULL LIMIT 1");
|
|
||||||
|
|
||||||
if ($update_sql && $domain_row) {
|
|
||||||
// Capture the update result before any history or audit queries run.
|
|
||||||
$update_count = mysqli_affected_rows($mysqli);
|
|
||||||
|
|
||||||
$new_domain_info = mysqli_fetch_assoc(mysqli_query($mysqli, "
|
|
||||||
SELECT
|
|
||||||
domains.*,
|
|
||||||
registrar.vendor_name AS registrar_name,
|
|
||||||
dnshost.vendor_name AS dnshost_name,
|
|
||||||
mailhost.vendor_name AS mailhost_name,
|
|
||||||
webhost.vendor_name AS webhost_name
|
|
||||||
FROM domains
|
|
||||||
LEFT JOIN vendors AS registrar ON domains.domain_registrar = registrar.vendor_id
|
|
||||||
LEFT JOIN vendors AS dnshost ON domains.domain_dnshost = dnshost.vendor_id
|
|
||||||
LEFT JOIN vendors AS mailhost ON domains.domain_mailhost = mailhost.vendor_id
|
|
||||||
LEFT JOIN vendors AS webhost ON domains.domain_webhost = webhost.vendor_id
|
|
||||||
WHERE domain_id = $domain_id AND domain_client_id = $client_id
|
|
||||||
LIMIT 1
|
|
||||||
"));
|
|
||||||
|
|
||||||
$ignored_columns = [
|
|
||||||
'domain_updated_at',
|
|
||||||
'domain_accessed_at',
|
|
||||||
'domain_registrar',
|
|
||||||
'domain_webhost',
|
|
||||||
'domain_dnshost',
|
|
||||||
'domain_mailhost'
|
|
||||||
];
|
|
||||||
|
|
||||||
foreach ($original_domain_info as $column => $old_value) {
|
|
||||||
$new_value = $new_domain_info[$column];
|
|
||||||
if ($old_value != $new_value && !in_array($column, $ignored_columns)) {
|
|
||||||
$column = escapeSql($column);
|
|
||||||
$old_value = escapeSql($old_value);
|
|
||||||
$new_value = escapeSql($new_value);
|
|
||||||
mysqli_query($mysqli, "INSERT INTO domain_history SET domain_history_column = '$column', domain_history_old_value = '$old_value', domain_history_new_value = '$new_value', domain_history_domain_id = $domain_id");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
logAudit("Domain", "Edit", "$name via API ($api_key_name)", $client_id, $domain_id);
|
|
||||||
logAudit("API", "Success", "Updated domain $name via API ($api_key_name)", $client_id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
require_once '../update_output.php';
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
require_once '../validate_api_key.php';
|
|
||||||
require_once '../require_post_method.php';
|
|
||||||
|
|
||||||
// Parse ID
|
|
||||||
$location_id = intval($_POST['location_id']);
|
|
||||||
|
|
||||||
// Default
|
|
||||||
$update_count = false;
|
|
||||||
|
|
||||||
if (!empty($location_id)) {
|
|
||||||
|
|
||||||
// Fetch location info
|
|
||||||
$row = mysqli_fetch_assoc(mysqli_query($mysqli, "
|
|
||||||
SELECT location_name
|
|
||||||
FROM locations
|
|
||||||
WHERE location_id = $location_id AND location_client_id = $client_id AND location_archived_at IS NULL
|
|
||||||
LIMIT 1
|
|
||||||
"));
|
|
||||||
|
|
||||||
if ($row) {
|
|
||||||
|
|
||||||
$location_name = escapeSql($row['location_name']);
|
|
||||||
|
|
||||||
// Archive location
|
|
||||||
$update_sql = mysqli_query($mysqli, "
|
|
||||||
UPDATE locations SET
|
|
||||||
location_primary = 0,
|
|
||||||
location_archived_at = NOW()
|
|
||||||
WHERE location_id = $location_id AND location_client_id = $client_id
|
|
||||||
");
|
|
||||||
|
|
||||||
if ($update_sql) {
|
|
||||||
$update_count = mysqli_affected_rows($mysqli);
|
|
||||||
|
|
||||||
// Logging
|
|
||||||
logAudit("Location", "Archive", "$location_name archived via API ($api_key_name)", $client_id, $location_id);
|
|
||||||
logAudit("API", "Success", "Archived location $location_name via API ($api_key_name)", $client_id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Output
|
|
||||||
require_once '../update_output.php';
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
require_once '../validate_api_key.php';
|
|
||||||
|
|
||||||
require_once '../require_post_method.php';
|
|
||||||
|
|
||||||
|
|
||||||
// Parse ID
|
|
||||||
$location_id = intval($_POST['location_id']);
|
|
||||||
|
|
||||||
// Default
|
|
||||||
$delete_count = false;
|
|
||||||
|
|
||||||
if (!empty($location_id)) {
|
|
||||||
$row = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT location_name FROM locations WHERE location_id = $location_id AND location_client_id = $client_id AND location_archived_at IS NOT NULL LIMIT 1"));
|
|
||||||
$location_name = escapeSql($row['location_name'] ?? '');
|
|
||||||
|
|
||||||
$delete_sql = mysqli_query($mysqli, "DELETE FROM locations WHERE location_id = $location_id AND location_client_id = $client_id AND location_archived_at IS NOT NULL LIMIT 1");
|
|
||||||
|
|
||||||
// Check delete & get affected rows
|
|
||||||
if ($delete_sql && !empty($location_name)) {
|
|
||||||
$delete_count = mysqli_affected_rows($mysqli);
|
|
||||||
|
|
||||||
// Logging
|
|
||||||
logAudit("Location", "Delete", "$location_name via API ($api_key_name)", $client_id, $location_id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Output
|
|
||||||
require_once '../delete_output.php';
|
|
||||||
@@ -8,11 +8,11 @@ require_once '../require_get_method.php';
|
|||||||
if (isset($_GET['location_id'])) {
|
if (isset($_GET['location_id'])) {
|
||||||
// Location via ID (single)
|
// Location via ID (single)
|
||||||
$id = intval($_GET['location_id']);
|
$id = intval($_GET['location_id']);
|
||||||
$sql = mysqli_query($mysqli, "SELECT * FROM locations WHERE location_id = '$id' AND location_archived_at IS NULL AND 1=1 " . apiClientScopeSql('location_client_id') . "");
|
$sql = mysqli_query($mysqli, "SELECT * FROM locations WHERE location_id = '$id' AND 1=1 " . apiClientScopeSql('location_client_id') . "");
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
// All locations (by client ID if given, or all in general if key permits)
|
// All locations (by client ID if given, or all in general if key permits)
|
||||||
$sql = mysqli_query($mysqli, "SELECT * FROM locations WHERE location_archived_at IS NULL AND 1=1 " . apiClientScopeSql('location_client_id') . " ORDER BY location_id LIMIT $limit OFFSET $offset");
|
$sql = mysqli_query($mysqli, "SELECT * FROM locations WHERE 1=1 " . apiClientScopeSql('location_client_id') . " ORDER BY location_id LIMIT $limit OFFSET $offset");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Output
|
// Output
|
||||||
|
|||||||
@@ -1,43 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
require_once '../validate_api_key.php';
|
|
||||||
require_once '../require_post_method.php';
|
|
||||||
|
|
||||||
// Parse ID
|
|
||||||
$location_id = intval($_POST['location_id']);
|
|
||||||
|
|
||||||
// Default
|
|
||||||
$update_count = false;
|
|
||||||
|
|
||||||
if (!empty($location_id)) {
|
|
||||||
|
|
||||||
// Fetch location info
|
|
||||||
$row = mysqli_fetch_assoc(mysqli_query($mysqli, "
|
|
||||||
SELECT location_name
|
|
||||||
FROM locations
|
|
||||||
WHERE location_id = $location_id AND location_client_id = $client_id AND location_archived_at IS NOT NULL
|
|
||||||
LIMIT 1
|
|
||||||
"));
|
|
||||||
|
|
||||||
if ($row) {
|
|
||||||
|
|
||||||
$location_name = escapeSql($row['location_name']);
|
|
||||||
|
|
||||||
// Unarchive location
|
|
||||||
$update_sql = mysqli_query($mysqli, "
|
|
||||||
UPDATE locations SET location_archived_at = NULL
|
|
||||||
WHERE location_id = $location_id AND location_client_id = $client_id
|
|
||||||
");
|
|
||||||
|
|
||||||
if ($update_sql) {
|
|
||||||
$update_count = mysqli_affected_rows($mysqli);
|
|
||||||
|
|
||||||
// Logging
|
|
||||||
logAudit("Location", "Unarchive", "$location_name unarchived via API ($api_key_name)", $client_id, $location_id);
|
|
||||||
logAudit("API", "Success", "Unarchived location $location_name via API ($api_key_name)", $client_id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Output
|
|
||||||
require_once '../update_output.php';
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
require_once '../validate_api_key.php';
|
|
||||||
|
|
||||||
require_once '../require_post_method.php';
|
|
||||||
|
|
||||||
|
|
||||||
// Parse ID
|
|
||||||
$location_id = intval($_POST['location_id']);
|
|
||||||
|
|
||||||
// Default
|
|
||||||
$update_count = false;
|
|
||||||
|
|
||||||
if (!empty($location_id)) {
|
|
||||||
|
|
||||||
$location_row = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT * FROM locations WHERE location_id = '$location_id' AND location_client_id = $client_id AND location_archived_at IS NULL LIMIT 1"));
|
|
||||||
|
|
||||||
// Variable assignment from POST - assigning the current database value if a value is not provided
|
|
||||||
require_once 'location_model.php';
|
|
||||||
|
|
||||||
if ($location_row) {
|
|
||||||
|
|
||||||
// Reset primary location
|
|
||||||
if ($primary == 1) {
|
|
||||||
mysqli_query($mysqli, "UPDATE locations SET location_primary = 0 WHERE location_client_id = $client_id");
|
|
||||||
}
|
|
||||||
|
|
||||||
$update_sql = mysqli_query($mysqli, "UPDATE locations SET location_name = '$name', location_description = '$description', location_country = '$country', location_address = '$address', location_city = '$city', location_state = '$state', location_zip = '$zip', location_hours = '$hours', location_notes = '$notes', location_primary = '$primary' WHERE location_id = $location_id AND location_client_id = $client_id AND location_archived_at IS NULL LIMIT 1");
|
|
||||||
|
|
||||||
// Check update & get affected rows
|
|
||||||
if ($update_sql) {
|
|
||||||
$update_count = mysqli_affected_rows($mysqli);
|
|
||||||
|
|
||||||
// Logging
|
|
||||||
logAudit("Location", "Edit", "$name via API ($api_key_name)", $client_id, $location_id);
|
|
||||||
logAudit("API", "Success", "Edited location $name via API ($api_key_name)", $client_id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Output
|
|
||||||
require_once '../update_output.php';
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
require_once '../validate_api_key.php';
|
|
||||||
require_once '../require_post_method.php';
|
|
||||||
|
|
||||||
// Parse ID
|
|
||||||
$network_id = intval($_POST['network_id']);
|
|
||||||
|
|
||||||
// Default
|
|
||||||
$update_count = false;
|
|
||||||
|
|
||||||
if (!empty($network_id)) {
|
|
||||||
|
|
||||||
// Fetch network info
|
|
||||||
$row = mysqli_fetch_assoc(mysqli_query($mysqli, "
|
|
||||||
SELECT network_name
|
|
||||||
FROM networks
|
|
||||||
WHERE network_id = $network_id AND network_client_id = $client_id AND network_archived_at IS NULL
|
|
||||||
LIMIT 1
|
|
||||||
"));
|
|
||||||
|
|
||||||
if ($row) {
|
|
||||||
|
|
||||||
$network_name = escapeSql($row['network_name']);
|
|
||||||
|
|
||||||
// Archive network
|
|
||||||
$update_sql = mysqli_query($mysqli, "
|
|
||||||
UPDATE networks SET network_archived_at = NOW()
|
|
||||||
WHERE network_id = $network_id AND network_client_id = $client_id AND network_archived_at IS NULL
|
|
||||||
");
|
|
||||||
|
|
||||||
if ($update_sql) {
|
|
||||||
$update_count = mysqli_affected_rows($mysqli);
|
|
||||||
|
|
||||||
// Logging
|
|
||||||
logAudit("Network", "Archive", "$network_name archived via API ($api_key_name)", $client_id, $network_id);
|
|
||||||
logAudit("API", "Success", "Archived network $network_name via API ($api_key_name)", $client_id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Output
|
|
||||||
require_once '../update_output.php';
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
require_once '../validate_api_key.php';
|
|
||||||
|
|
||||||
require_once '../require_post_method.php';
|
|
||||||
|
|
||||||
// Parse Info
|
|
||||||
require_once 'network_model.php';
|
|
||||||
|
|
||||||
// Default
|
|
||||||
$insert_id = false;
|
|
||||||
|
|
||||||
if (!empty($name) && !empty($client_id) && !empty($network)) {
|
|
||||||
|
|
||||||
// Insert network
|
|
||||||
$insert_sql = mysqli_query($mysqli, "INSERT INTO networks SET network_name = '$name', network_description = '$description', network_vlan = $vlan, network = '$network', network_gateway = '$gateway', network_primary_dns = '$primary_dns', network_secondary_dns = '$secondary_dns', network_dhcp_range = '$dhcp_range', network_notes = '$notes', network_location_id = $location_id, network_client_id = $client_id");
|
|
||||||
|
|
||||||
// Check insert & get insert ID
|
|
||||||
if ($insert_sql) {
|
|
||||||
$insert_id = mysqli_insert_id($mysqli);
|
|
||||||
|
|
||||||
// Logging
|
|
||||||
logAudit("Network", "Create", "$name via API ($api_key_name)", $client_id, $insert_id);
|
|
||||||
logAudit("API", "Success", "Created network $name via API ($api_key_name)", $client_id);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// Output
|
|
||||||
require_once '../create_output.php';
|
|
||||||
@@ -1,83 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
// Variable assignment from POST (or: blank/from DB is updating)
|
|
||||||
|
|
||||||
if (isset($_POST['network_name'])) {
|
|
||||||
$name = escapeSql($_POST['network_name']);
|
|
||||||
} elseif ($network_row) {
|
|
||||||
$name = mysqli_real_escape_string($mysqli, $network_row['network_name']);
|
|
||||||
} else {
|
|
||||||
$name = '';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isset($_POST['network_description'])) {
|
|
||||||
$description = escapeSql($_POST['network_description']);
|
|
||||||
} elseif ($network_row) {
|
|
||||||
$description = mysqli_real_escape_string($mysqli, $network_row['network_description']);
|
|
||||||
} else {
|
|
||||||
$description = '';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isset($_POST['network'])) {
|
|
||||||
$network = escapeSql($_POST['network']);
|
|
||||||
} elseif ($network_row) {
|
|
||||||
$network = mysqli_real_escape_string($mysqli, $network_row['network']);
|
|
||||||
} else {
|
|
||||||
$network = '';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isset($_POST['network_vlan'])) {
|
|
||||||
$vlan = intval($_POST['network_vlan']);
|
|
||||||
} elseif ($network_row) {
|
|
||||||
$vlan = intval($network_row['network_vlan']);
|
|
||||||
} else {
|
|
||||||
$vlan = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isset($_POST['network_gateway'])) {
|
|
||||||
$gateway = escapeSql($_POST['network_gateway']);
|
|
||||||
} elseif ($network_row) {
|
|
||||||
$gateway = mysqli_real_escape_string($mysqli, $network_row['network_gateway']);
|
|
||||||
} else {
|
|
||||||
$gateway = '';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isset($_POST['network_primary_dns'])) {
|
|
||||||
$primary_dns = escapeSql($_POST['network_primary_dns']);
|
|
||||||
} elseif ($network_row) {
|
|
||||||
$primary_dns = mysqli_real_escape_string($mysqli, $network_row['network_primary_dns']);
|
|
||||||
} else {
|
|
||||||
$primary_dns = '';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isset($_POST['network_secondary_dns'])) {
|
|
||||||
$secondary_dns = escapeSql($_POST['network_secondary_dns']);
|
|
||||||
} elseif ($network_row) {
|
|
||||||
$secondary_dns = mysqli_real_escape_string($mysqli, $network_row['network_secondary_dns']);
|
|
||||||
} else {
|
|
||||||
$secondary_dns = '';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isset($_POST['network_dhcp_range'])) {
|
|
||||||
$dhcp_range = escapeSql($_POST['network_dhcp_range']);
|
|
||||||
} elseif ($network_row) {
|
|
||||||
$dhcp_range = mysqli_real_escape_string($mysqli, $network_row['network_dhcp_range']);
|
|
||||||
} else {
|
|
||||||
$dhcp_range = '';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isset($_POST['network_notes'])) {
|
|
||||||
$notes = escapeSql($_POST['network_notes']);
|
|
||||||
} elseif ($network_row) {
|
|
||||||
$notes = mysqli_real_escape_string($mysqli, $network_row['network_notes']);
|
|
||||||
} else {
|
|
||||||
$notes = '';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isset($_POST['network_location_id'])) {
|
|
||||||
$location_id = intval($_POST['network_location_id']);
|
|
||||||
} elseif ($network_row) {
|
|
||||||
$location_id = intval($network_row['network_location_id']);
|
|
||||||
} else {
|
|
||||||
$location_id = 0;
|
|
||||||
}
|
|
||||||
@@ -8,16 +8,16 @@ require_once '../require_get_method.php';
|
|||||||
// Specific network via ID (single)
|
// Specific network via ID (single)
|
||||||
if (isset($_GET['network_id'])) {
|
if (isset($_GET['network_id'])) {
|
||||||
$id = intval($_GET['network_id']);
|
$id = intval($_GET['network_id']);
|
||||||
$sql = mysqli_query($mysqli, "SELECT * FROM networks WHERE network_id = '$id' AND network_archived_at IS NULL AND 1=1 " . apiClientScopeSql('network_client_id') . "");
|
$sql = mysqli_query($mysqli, "SELECT * FROM networks WHERE network_id = '$id' AND 1=1 " . apiClientScopeSql('network_client_id') . "");
|
||||||
|
|
||||||
} elseif (isset($_GET['network_name'])) {
|
} elseif (isset($_GET['network_name'])) {
|
||||||
// Network by name
|
// Network by name
|
||||||
$name = mysqli_real_escape_string($mysqli, $_GET['network_name']);
|
$name = mysqli_real_escape_string($mysqli, $_GET['network_name']);
|
||||||
$sql = mysqli_query($mysqli, "SELECT * FROM networks WHERE network_name = '$name' AND network_archived_at IS NULL AND 1=1 " . apiClientScopeSql('network_client_id') . " ORDER BY network_id LIMIT $limit OFFSET $offset");
|
$sql = mysqli_query($mysqli, "SELECT * FROM networks WHERE network_name = '$name' AND 1=1 " . apiClientScopeSql('network_client_id') . " ORDER BY network_id LIMIT $limit OFFSET $offset");
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
// All networks (by client ID or all in general if key permits)
|
// All networks (by client ID or all in general if key permits)
|
||||||
$sql = mysqli_query($mysqli, "SELECT * FROM networks WHERE network_archived_at IS NULL AND 1=1 " . apiClientScopeSql('network_client_id') . " ORDER BY network_id LIMIT $limit OFFSET $offset");
|
$sql = mysqli_query($mysqli, "SELECT * FROM networks WHERE 1=1 " . apiClientScopeSql('network_client_id') . " ORDER BY network_id LIMIT $limit OFFSET $offset");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Output
|
// Output
|
||||||
|
|||||||
@@ -1,43 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
require_once '../validate_api_key.php';
|
|
||||||
require_once '../require_post_method.php';
|
|
||||||
|
|
||||||
// Parse ID
|
|
||||||
$network_id = intval($_POST['network_id']);
|
|
||||||
|
|
||||||
// Default
|
|
||||||
$update_count = false;
|
|
||||||
|
|
||||||
if (!empty($network_id)) {
|
|
||||||
|
|
||||||
// Fetch network info
|
|
||||||
$row = mysqli_fetch_assoc(mysqli_query($mysqli, "
|
|
||||||
SELECT network_name
|
|
||||||
FROM networks
|
|
||||||
WHERE network_id = $network_id AND network_client_id = $client_id AND network_archived_at IS NOT NULL
|
|
||||||
LIMIT 1
|
|
||||||
"));
|
|
||||||
|
|
||||||
if ($row) {
|
|
||||||
|
|
||||||
$network_name = escapeSql($row['network_name']);
|
|
||||||
|
|
||||||
// Unarchive network
|
|
||||||
$update_sql = mysqli_query($mysqli, "
|
|
||||||
UPDATE networks SET network_archived_at = NULL
|
|
||||||
WHERE network_id = $network_id AND network_client_id = $client_id
|
|
||||||
");
|
|
||||||
|
|
||||||
if ($update_sql) {
|
|
||||||
$update_count = mysqli_affected_rows($mysqli);
|
|
||||||
|
|
||||||
// Logging
|
|
||||||
logAudit("Network", "Unarchive", "$network_name unarchived via API ($api_key_name)", $client_id, $network_id);
|
|
||||||
logAudit("API", "Success", "Unarchived network $network_name via API ($api_key_name)", $client_id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Output
|
|
||||||
require_once '../update_output.php';
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
require_once '../validate_api_key.php';
|
|
||||||
|
|
||||||
require_once '../require_post_method.php';
|
|
||||||
|
|
||||||
// Parse ID
|
|
||||||
$network_id = intval($_POST['network_id']);
|
|
||||||
|
|
||||||
// Default
|
|
||||||
$update_count = false;
|
|
||||||
|
|
||||||
if (!empty($network_id)) {
|
|
||||||
|
|
||||||
$network_row = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT * FROM networks WHERE network_id = '$network_id' AND network_client_id = $client_id AND network_archived_at IS NULL LIMIT 1"));
|
|
||||||
|
|
||||||
// Variable assignment from POST - assigning the current database value if a value is not provided
|
|
||||||
require_once 'network_model.php';
|
|
||||||
|
|
||||||
if ($network_row) {
|
|
||||||
|
|
||||||
$update_sql = mysqli_query($mysqli, "UPDATE networks SET network_name = '$name', network_description = '$description', network_vlan = $vlan, network = '$network', network_gateway = '$gateway', network_primary_dns = '$primary_dns', network_secondary_dns = '$secondary_dns', network_dhcp_range = '$dhcp_range', network_notes = '$notes', network_location_id = $location_id WHERE network_id = $network_id AND network_client_id = $client_id AND network_archived_at IS NULL LIMIT 1");
|
|
||||||
|
|
||||||
// Check update & get affected rows
|
|
||||||
if ($update_sql) {
|
|
||||||
$update_count = mysqli_affected_rows($mysqli);
|
|
||||||
|
|
||||||
// Logging
|
|
||||||
logAudit("Network", "Edit", "$name via API ($api_key_name)", $client_id, $network_id);
|
|
||||||
logAudit("API", "Success", "Edited network $name via API ($api_key_name)", $client_id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Output
|
|
||||||
require_once '../update_output.php';
|
|
||||||
@@ -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) {
|
||||||
|
|||||||
@@ -1,38 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
require_once '../validate_api_key.php';
|
|
||||||
|
|
||||||
require_once '../require_post_method.php';
|
|
||||||
|
|
||||||
// Parse ID
|
|
||||||
$ticket_id = intval($_POST['ticket_id']);
|
|
||||||
|
|
||||||
// Default
|
|
||||||
$update_count = false;
|
|
||||||
|
|
||||||
if (!empty($ticket_id)) {
|
|
||||||
|
|
||||||
$ticket_row = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT * FROM tickets WHERE ticket_id = '$ticket_id' AND ticket_client_id = $client_id LIMIT 1"));
|
|
||||||
|
|
||||||
if ($ticket_row) {
|
|
||||||
// Assign model values from POST, falling back to the current ticket values.
|
|
||||||
require_once 'ticket_model.php';
|
|
||||||
|
|
||||||
$ticket_id = intval($ticket_row['ticket_id']);
|
|
||||||
$ticket_prefix = escapeSql($ticket_row['ticket_prefix']);
|
|
||||||
$ticket_number = intval($ticket_row['ticket_number']);
|
|
||||||
|
|
||||||
$update_sql = mysqli_query($mysqli, "UPDATE tickets SET ticket_subject = '$subject', ticket_details = '$details', ticket_priority = '$priority', ticket_billable = $billable, ticket_vendor_ticket_number = '$vendor_ticket_number', ticket_vendor_id = $vendor_id, ticket_assigned_to = $assigned_to, ticket_contact_id = $contact, ticket_asset_id = $asset WHERE ticket_id = $ticket_id AND ticket_client_id = $client_id LIMIT 1");
|
|
||||||
|
|
||||||
if ($update_sql) {
|
|
||||||
$update_count = mysqli_affected_rows($mysqli);
|
|
||||||
|
|
||||||
logTicketHistory($ticket_id, "Edited via the API ($api_key_name)");
|
|
||||||
logAudit("Ticket", "Edit", "$ticket_prefix$ticket_number ticket via API ($api_key_name)", $client_id, $ticket_id);
|
|
||||||
logAudit("API", "Success", "Edited ticket $ticket_prefix$ticket_number via API ($api_key_name)", $client_id, $ticket_id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Output
|
|
||||||
require_once '../update_output.php';
|
|
||||||
33
api/v1/vendors/archive.php
vendored
33
api/v1/vendors/archive.php
vendored
@@ -1,33 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
require_once '../validate_api_key.php';
|
|
||||||
require_once '../require_post_method.php';
|
|
||||||
|
|
||||||
// Parse ID
|
|
||||||
$vendor_id = intval($_POST['vendor_id']);
|
|
||||||
|
|
||||||
// Default
|
|
||||||
$update_count = false;
|
|
||||||
|
|
||||||
if (!empty($vendor_id)) {
|
|
||||||
|
|
||||||
// Fetch vendor info
|
|
||||||
$row = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT vendor_name FROM vendors WHERE vendor_id = $vendor_id AND vendor_client_id = $client_id AND vendor_archived_at IS NULL LIMIT 1"));
|
|
||||||
|
|
||||||
if ($row) {
|
|
||||||
$vendor_name = escapeSql($row['vendor_name']);
|
|
||||||
|
|
||||||
// Archive vendor
|
|
||||||
$update_sql = mysqli_query($mysqli, "UPDATE vendors SET vendor_archived_at = NOW() WHERE vendor_id = $vendor_id AND vendor_client_id = $client_id AND vendor_archived_at IS NULL");
|
|
||||||
|
|
||||||
if ($update_sql) {
|
|
||||||
$update_count = mysqli_affected_rows($mysqli);
|
|
||||||
|
|
||||||
// Logging
|
|
||||||
logAudit("Vendor", "Archive", "$vendor_name archived via API ($api_key_name)", $client_id, $vendor_id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Output
|
|
||||||
require_once '../update_output.php';
|
|
||||||
29
api/v1/vendors/create.php
vendored
29
api/v1/vendors/create.php
vendored
@@ -1,29 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
require_once '../validate_api_key.php';
|
|
||||||
|
|
||||||
require_once '../require_post_method.php';
|
|
||||||
|
|
||||||
// Parse Info
|
|
||||||
require_once 'vendor_model.php';
|
|
||||||
|
|
||||||
// Default
|
|
||||||
$insert_id = false;
|
|
||||||
|
|
||||||
if (!empty($name)) {
|
|
||||||
|
|
||||||
// Insert vendor
|
|
||||||
$insert_sql = mysqli_query($mysqli, "INSERT INTO vendors SET vendor_name = '$name', vendor_description = '$description', vendor_contact_name = '$contact_name', vendor_phone_country_code = '$phone_country_code', vendor_phone = '$phone', vendor_extension = '$extension', vendor_email = '$email', vendor_website = '$website', vendor_hours = '$hours', vendor_sla = '$sla', vendor_code = '$code', vendor_account_number = '$account_number', vendor_notes = '$notes', vendor_client_id = $client_id");
|
|
||||||
|
|
||||||
// Check insert & get insert ID
|
|
||||||
if ($insert_sql) {
|
|
||||||
$insert_id = mysqli_insert_id($mysqli);
|
|
||||||
|
|
||||||
// Logging
|
|
||||||
logAudit("Vendor", "Create", "$name via API ($api_key_name)", $client_id, $insert_id);
|
|
||||||
logAudit("API", "Success", "Created vendor $name via API ($api_key_name)", $client_id, $insert_id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Output
|
|
||||||
require_once '../create_output.php';
|
|
||||||
4
api/v1/vendors/read.php
vendored
4
api/v1/vendors/read.php
vendored
@@ -7,11 +7,11 @@ require_once '../require_get_method.php';
|
|||||||
// Specific vendor via their ID (single)
|
// Specific vendor via their ID (single)
|
||||||
if (isset($_GET['vendor_id'])) {
|
if (isset($_GET['vendor_id'])) {
|
||||||
$id = intval($_GET['vendor_id']);
|
$id = intval($_GET['vendor_id']);
|
||||||
$sql = mysqli_query($mysqli, "SELECT * FROM vendors WHERE vendor_id = '$id' AND vendor_archived_at IS NULL AND 1=1 " . apiClientScopeSql('vendor_client_id') . "");
|
$sql = mysqli_query($mysqli, "SELECT * FROM vendors WHERE vendor_id = '$id' AND 1=1 " . apiClientScopeSql('vendor_client_id') . "");
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
// All Vendors (by client ID or all in general if key permits)
|
// All Vendors (by client ID or all in general if key permits)
|
||||||
$sql = mysqli_query($mysqli, "SELECT * FROM vendors WHERE vendor_archived_at IS NULL AND 1=1 " . apiClientScopeSql('vendor_client_id') . " ORDER BY vendor_id LIMIT $limit OFFSET $offset");
|
$sql = mysqli_query($mysqli, "SELECT * FROM vendors WHERE 1=1 " . apiClientScopeSql('vendor_client_id') . " ORDER BY vendor_id LIMIT $limit OFFSET $offset");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Output
|
// Output
|
||||||
|
|||||||
33
api/v1/vendors/unarchive.php
vendored
33
api/v1/vendors/unarchive.php
vendored
@@ -1,33 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
require_once '../validate_api_key.php';
|
|
||||||
require_once '../require_post_method.php';
|
|
||||||
|
|
||||||
// Parse ID
|
|
||||||
$vendor_id = intval($_POST['vendor_id']);
|
|
||||||
|
|
||||||
// Default
|
|
||||||
$update_count = false;
|
|
||||||
|
|
||||||
if (!empty($vendor_id)) {
|
|
||||||
|
|
||||||
// Fetch vendor info
|
|
||||||
$row = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT vendor_name FROM vendors WHERE vendor_id = $vendor_id AND vendor_client_id = $client_id AND vendor_archived_at IS NOT NULL LIMIT 1"));
|
|
||||||
|
|
||||||
if ($row) {
|
|
||||||
$vendor_name = escapeSql($row['vendor_name']);
|
|
||||||
|
|
||||||
// Un-archive vendor
|
|
||||||
$update_sql = mysqli_query($mysqli, "UPDATE vendors SET vendor_archived_at = NULL WHERE vendor_id = $vendor_id AND vendor_client_id = $client_id AND vendor_archived_at IS NOT NULL");
|
|
||||||
|
|
||||||
if ($update_sql) {
|
|
||||||
$update_count = mysqli_affected_rows($mysqli);
|
|
||||||
|
|
||||||
// Logging
|
|
||||||
logAudit("Vendor", "Unarchive", "$vendor_name unarchived via API ($api_key_name)", $client_id, $vendor_id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Output
|
|
||||||
require_once '../update_output.php';
|
|
||||||
35
api/v1/vendors/update.php
vendored
35
api/v1/vendors/update.php
vendored
@@ -1,35 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
require_once '../validate_api_key.php';
|
|
||||||
|
|
||||||
require_once '../require_post_method.php';
|
|
||||||
|
|
||||||
// Parse ID
|
|
||||||
$vendor_id = intval($_POST['vendor_id']);
|
|
||||||
|
|
||||||
// Default
|
|
||||||
$update_count = false;
|
|
||||||
|
|
||||||
if (!empty($vendor_id)) {
|
|
||||||
|
|
||||||
$vendor_row = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT * FROM vendors WHERE vendor_id = $vendor_id AND vendor_client_id = $client_id AND vendor_archived_at IS NULL LIMIT 1"));
|
|
||||||
|
|
||||||
// Variable assignment from POST - assigning the current database value if a value is not provided
|
|
||||||
require_once 'vendor_model.php';
|
|
||||||
|
|
||||||
if ($vendor_row) {
|
|
||||||
$update_sql = mysqli_query($mysqli, "UPDATE vendors SET vendor_name = '$name', vendor_description = '$description', vendor_contact_name = '$contact_name', vendor_phone_country_code = '$phone_country_code', vendor_phone = '$phone', vendor_extension = '$extension', vendor_email = '$email', vendor_website = '$website', vendor_hours = '$hours', vendor_sla = '$sla', vendor_code = '$code', vendor_account_number = '$account_number', vendor_notes = '$notes' WHERE vendor_id = $vendor_id AND vendor_client_id = $client_id AND vendor_archived_at IS NULL LIMIT 1");
|
|
||||||
|
|
||||||
// Check update & get affected rows
|
|
||||||
if ($update_sql) {
|
|
||||||
$update_count = mysqli_affected_rows($mysqli);
|
|
||||||
|
|
||||||
// Logging
|
|
||||||
logAudit("Vendor", "Edit", "$name via API ($api_key_name)", $client_id, $vendor_id);
|
|
||||||
logAudit("API", "Success", "Edited vendor $name via API ($api_key_name)", $client_id, $vendor_id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Output
|
|
||||||
require_once '../update_output.php';
|
|
||||||
107
api/v1/vendors/vendor_model.php
vendored
107
api/v1/vendors/vendor_model.php
vendored
@@ -1,107 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
// Variable assignment from POST (or: blank/from DB is updating)
|
|
||||||
|
|
||||||
if (isset($_POST['vendor_name'])) {
|
|
||||||
$name = escapeSql($_POST['vendor_name']);
|
|
||||||
} elseif ($vendor_row) {
|
|
||||||
$name = mysqli_real_escape_string($mysqli, $vendor_row['vendor_name']);
|
|
||||||
} else {
|
|
||||||
$name = '';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isset($_POST['vendor_description'])) {
|
|
||||||
$description = escapeSql($_POST['vendor_description']);
|
|
||||||
} elseif ($vendor_row) {
|
|
||||||
$description = mysqli_real_escape_string($mysqli, $vendor_row['vendor_description']);
|
|
||||||
} else {
|
|
||||||
$description = '';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isset($_POST['vendor_account_number'])) {
|
|
||||||
$account_number = escapeSql($_POST['vendor_account_number']);
|
|
||||||
} elseif ($vendor_row) {
|
|
||||||
$account_number = mysqli_real_escape_string($mysqli, $vendor_row['vendor_account_number']);
|
|
||||||
} else {
|
|
||||||
$account_number = '';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isset($_POST['vendor_contact_name'])) {
|
|
||||||
$contact_name = escapeSql($_POST['vendor_contact_name']);
|
|
||||||
} elseif ($vendor_row) {
|
|
||||||
$contact_name = mysqli_real_escape_string($mysqli, $vendor_row['vendor_contact_name']);
|
|
||||||
} else {
|
|
||||||
$contact_name = '';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isset($_POST['vendor_phone_country_code'])) {
|
|
||||||
$phone_country_code = preg_replace("/[^0-9]/", '', $_POST['vendor_phone_country_code']);
|
|
||||||
} elseif ($vendor_row) {
|
|
||||||
$phone_country_code = mysqli_real_escape_string($mysqli, $vendor_row['vendor_phone_country_code']);
|
|
||||||
} else {
|
|
||||||
$phone_country_code = '';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isset($_POST['vendor_phone'])) {
|
|
||||||
$phone = preg_replace("/[^0-9]/", '', $_POST['vendor_phone']);
|
|
||||||
} elseif ($vendor_row) {
|
|
||||||
$phone = mysqli_real_escape_string($mysqli, $vendor_row['vendor_phone']);
|
|
||||||
} else {
|
|
||||||
$phone = '';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isset($_POST['vendor_extension'])) {
|
|
||||||
$extension = preg_replace("/[^0-9]/", '', $_POST['vendor_extension']);
|
|
||||||
} elseif ($vendor_row) {
|
|
||||||
$extension = mysqli_real_escape_string($mysqli, $vendor_row['vendor_extension']);
|
|
||||||
} else {
|
|
||||||
$extension = '';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isset($_POST['vendor_email'])) {
|
|
||||||
$email = escapeSql($_POST['vendor_email']);
|
|
||||||
} elseif ($vendor_row) {
|
|
||||||
$email = mysqli_real_escape_string($mysqli, $vendor_row['vendor_email']);
|
|
||||||
} else {
|
|
||||||
$email = '';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isset($_POST['vendor_website'])) {
|
|
||||||
$website = preg_replace("(^https?://)", "", escapeSql($_POST['vendor_website']));
|
|
||||||
} elseif ($vendor_row) {
|
|
||||||
$website = mysqli_real_escape_string($mysqli, $vendor_row['vendor_website']);
|
|
||||||
} else {
|
|
||||||
$website = '';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isset($_POST['vendor_hours'])) {
|
|
||||||
$hours = escapeSql($_POST['vendor_hours']);
|
|
||||||
} elseif ($vendor_row) {
|
|
||||||
$hours = mysqli_real_escape_string($mysqli, $vendor_row['vendor_hours']);
|
|
||||||
} else {
|
|
||||||
$hours = '';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isset($_POST['vendor_sla'])) {
|
|
||||||
$sla = escapeSql($_POST['vendor_sla']);
|
|
||||||
} elseif ($vendor_row) {
|
|
||||||
$sla = mysqli_real_escape_string($mysqli, $vendor_row['vendor_sla']);
|
|
||||||
} else {
|
|
||||||
$sla = '';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isset($_POST['vendor_code'])) {
|
|
||||||
$code = escapeSql($_POST['vendor_code']);
|
|
||||||
} elseif ($vendor_row) {
|
|
||||||
$code = mysqli_real_escape_string($mysqli, $vendor_row['vendor_code']);
|
|
||||||
} else {
|
|
||||||
$code = '';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isset($_POST['vendor_notes'])) {
|
|
||||||
$notes = escapeSql($_POST['vendor_notes']);
|
|
||||||
} elseif ($vendor_row) {
|
|
||||||
$notes = mysqli_real_escape_string($mysqli, $vendor_row['vendor_notes']);
|
|
||||||
} else {
|
|
||||||
$notes = '';
|
|
||||||
}
|
|
||||||
@@ -68,7 +68,7 @@ header("X-Frame-Options: DENY"); // Legacy
|
|||||||
<div class="container">
|
<div class="container">
|
||||||
<a class="navbar-brand" href="index.php"><?= escapeHtml($session_company_name) ?></a>
|
<a class="navbar-brand" href="index.php"><?= escapeHtml($session_company_name) ?></a>
|
||||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent">
|
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent">
|
||||||
<i class="fas fa-bars text-white"></i>
|
<span class="navbar-toggler-icon"></span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div class="collapse navbar-collapse" id="navbarSupportedContent">
|
<div class="collapse navbar-collapse" id="navbarSupportedContent">
|
||||||
@@ -166,9 +166,8 @@ header("X-Frame-Options: DENY"); // Legacy
|
|||||||
<!-- Page content container -->
|
<!-- Page content container -->
|
||||||
<div class="container mt-4">
|
<div class="container mt-4">
|
||||||
|
|
||||||
<!-- Flex row rather than col-md-1/col-md-11 -->
|
<div class="row mb-3">
|
||||||
<div class="d-flex flex-wrap align-items-center gap-2 mb-3">
|
<div class="col-md-1 text-center">
|
||||||
<div class="flex-shrink-0">
|
|
||||||
<?php if (!empty($session_contact_photo)) { ?>
|
<?php if (!empty($session_contact_photo)) { ?>
|
||||||
<img src="/uploads/clients/<?= $session_client_id ?>/<?= $session_contact_photo ?>" alt="..." height="50" width="50" class="rounded-circle img-fluid">
|
<img src="/uploads/clients/<?= $session_client_id ?>/<?= $session_contact_photo ?>" alt="..." height="50" width="50" class="rounded-circle img-fluid">
|
||||||
|
|
||||||
@@ -180,11 +179,12 @@ header("X-Frame-Options: DENY"); // Legacy
|
|||||||
<?php } ?>
|
<?php } ?>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h4 class="mb-0 flex-grow-1">Welcome, <strong><?= stripslashes(escapeHtml($session_contact_name)) ?></strong>!</h4>
|
<div class="col-md-11 p-0">
|
||||||
|
<?php if ($session_company_logo) { ?>
|
||||||
<?php if ($session_company_logo) { ?>
|
<img height="48" width="142" class="img-fluid float-end" src="<?= "/uploads/settings/$session_company_logo" ?>">
|
||||||
<img height="48" width="142" class="img-fluid flex-shrink-0" src="<?= "/uploads/settings/$session_company_logo" ?>">
|
<?php } ?>
|
||||||
<?php } ?>
|
<h4>Welcome, <strong><?= stripslashes(escapeHtml($session_contact_name)) ?></strong>!</h4>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<hr>
|
<hr>
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
@@ -674,7 +673,7 @@ if ($config_send_invoice_reminders == 1) {
|
|||||||
// Logging
|
// Logging
|
||||||
// logAudit("Cron", "Task", "Cron created notifications for past due invoices and sent out notifications to the primary and billing contacts email");
|
// logAudit("Cron", "Task", "Cron created notifications for past due invoices and sent out notifications to the primary and billing contacts email");
|
||||||
|
|
||||||
// Generate & Send Recurring Invoices that match todays date and are active
|
// Send Recurring Invoices that match todays date and are active
|
||||||
|
|
||||||
//Loop through all recurring that match today's date and is active
|
//Loop through all recurring that match today's date and is active
|
||||||
$sql_recurring_invoices = mysqli_query($mysqli, "SELECT client_name, client_net_terms, recurring_invoice_amount, recurring_invoice_category_id,
|
$sql_recurring_invoices = mysqli_query($mysqli, "SELECT client_name, client_net_terms, recurring_invoice_amount, recurring_invoice_category_id,
|
||||||
@@ -682,7 +681,7 @@ $sql_recurring_invoices = mysqli_query($mysqli, "SELECT client_name, client_net_
|
|||||||
recurring_invoice_discount_amount, recurring_invoice_email_notify,
|
recurring_invoice_discount_amount, recurring_invoice_email_notify,
|
||||||
recurring_invoice_frequency, recurring_invoice_id, recurring_invoice_last_sent,
|
recurring_invoice_frequency, recurring_invoice_id, recurring_invoice_last_sent,
|
||||||
recurring_invoice_next_date, recurring_invoice_note, recurring_invoice_scope,
|
recurring_invoice_next_date, recurring_invoice_note, recurring_invoice_scope,
|
||||||
recurring_invoice_status, recurring_invoice_auto_send, recurring_payment_account_id, recurring_payment_currency_code,
|
recurring_invoice_status, recurring_payment_account_id, recurring_payment_currency_code,
|
||||||
recurring_payment_method, recurring_payment_recurring_invoice_id FROM recurring_invoices
|
recurring_payment_method, recurring_payment_recurring_invoice_id FROM recurring_invoices
|
||||||
LEFT JOIN recurring_payments ON recurring_invoice_id = recurring_payment_recurring_invoice_id
|
LEFT JOIN recurring_payments ON recurring_invoice_id = recurring_payment_recurring_invoice_id
|
||||||
LEFT JOIN clients ON client_id = recurring_invoice_client_id
|
LEFT JOIN clients ON client_id = recurring_invoice_client_id
|
||||||
@@ -701,7 +700,6 @@ while ($row = mysqli_fetch_assoc($sql_recurring_invoices)) {
|
|||||||
$recurring_invoice_amount = floatval($row['recurring_invoice_amount']);
|
$recurring_invoice_amount = floatval($row['recurring_invoice_amount']);
|
||||||
$recurring_invoice_currency_code = escapeSql($row['recurring_invoice_currency_code']);
|
$recurring_invoice_currency_code = escapeSql($row['recurring_invoice_currency_code']);
|
||||||
$recurring_invoice_note = escapeSql($row['recurring_invoice_note']);
|
$recurring_invoice_note = escapeSql($row['recurring_invoice_note']);
|
||||||
$recurring_invoice_auto_send = intval($row['recurring_invoice_auto_send']);
|
|
||||||
$recurring_invoice_email_notify = intval($row['recurring_invoice_email_notify']);
|
$recurring_invoice_email_notify = intval($row['recurring_invoice_email_notify']);
|
||||||
$category_id = intval($row['recurring_invoice_category_id']);
|
$category_id = intval($row['recurring_invoice_category_id']);
|
||||||
$client_id = intval($row['recurring_invoice_client_id']);
|
$client_id = intval($row['recurring_invoice_client_id']);
|
||||||
@@ -727,7 +725,7 @@ while ($row = mysqli_fetch_assoc($sql_recurring_invoices)) {
|
|||||||
//Generate a unique URL key for clients to access
|
//Generate a unique URL key for clients to access
|
||||||
$url_key = randomString(32);
|
$url_key = randomString(32);
|
||||||
|
|
||||||
mysqli_query($mysqli, "INSERT INTO invoices SET invoice_prefix = '$config_invoice_prefix', invoice_number = $new_invoice_number, invoice_scope = '$recurring_invoice_scope', invoice_date = CURDATE(), invoice_due = DATE_ADD(CURDATE(), INTERVAL $client_net_terms day), invoice_discount_amount = $recurring_invoice_discount_amount, invoice_amount = $recurring_invoice_amount, invoice_currency_code = '$recurring_invoice_currency_code', invoice_note = '$recurring_invoice_note', invoice_category_id = $category_id, invoice_status = 'Draft', invoice_url_key = '$url_key', invoice_recurring_invoice_id = $recurring_invoice_id, invoice_client_id = $client_id");
|
mysqli_query($mysqli, "INSERT INTO invoices SET invoice_prefix = '$config_invoice_prefix', invoice_number = $new_invoice_number, invoice_scope = '$recurring_invoice_scope', invoice_date = CURDATE(), invoice_due = DATE_ADD(CURDATE(), INTERVAL $client_net_terms day), invoice_discount_amount = $recurring_invoice_discount_amount, invoice_amount = $recurring_invoice_amount, invoice_currency_code = '$recurring_invoice_currency_code', invoice_note = '$recurring_invoice_note', invoice_category_id = $category_id, invoice_status = 'Sent', invoice_url_key = '$url_key', invoice_recurring_invoice_id = $recurring_invoice_id, invoice_client_id = $client_id");
|
||||||
|
|
||||||
$new_invoice_id = mysqli_insert_id($mysqli);
|
$new_invoice_id = mysqli_insert_id($mysqli);
|
||||||
|
|
||||||
@@ -752,17 +750,13 @@ while ($row = mysqli_fetch_assoc($sql_recurring_invoices)) {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
mysqli_query($mysqli, "INSERT INTO history SET history_status = 'Draft', history_description = 'Invoice Generated from Recurring!', history_invoice_id = $new_invoice_id");
|
mysqli_query($mysqli, "INSERT INTO history SET history_status = 'Sent', history_description = 'Invoice Generated from Recurring!', history_invoice_id = $new_invoice_id");
|
||||||
|
|
||||||
if ($recurring_invoice_auto_send == 1) {
|
appNotify("Recurring Sent", "Recurring Invoice $config_invoice_prefix$new_invoice_number for $client_name Sent", "/agent/invoice.php?invoice_id=$new_invoice_id", $client_id);
|
||||||
appNotify("Recurring Sent", "Recurring Invoice $config_invoice_prefix$new_invoice_number for $client_name Sent", "/agent/invoice.php?invoice_id=$new_invoice_id", $client_id);
|
|
||||||
} else {
|
|
||||||
appNotify("Recurring Generated", "Recurring Invoice $config_invoice_prefix$new_invoice_number for $client_name Generated", "/agent/invoice.php?invoice_id=$new_invoice_id", $client_id);
|
|
||||||
}
|
|
||||||
|
|
||||||
triggerCustomAction('invoice_create', $new_invoice_id);
|
triggerCustomAction('invoice_create', $new_invoice_id);
|
||||||
|
|
||||||
// Update recurring dates
|
//Update recurring dates
|
||||||
|
|
||||||
mysqli_query($mysqli, "UPDATE recurring_invoices SET recurring_invoice_last_sent = CURDATE(), recurring_invoice_next_date = DATE_ADD(CURDATE(), INTERVAL 1 $recurring_invoice_frequency) WHERE recurring_invoice_id = $recurring_invoice_id");
|
mysqli_query($mysqli, "UPDATE recurring_invoices SET recurring_invoice_last_sent = CURDATE(), recurring_invoice_next_date = DATE_ADD(CURDATE(), INTERVAL 1 $recurring_invoice_frequency) WHERE recurring_invoice_id = $recurring_invoice_id");
|
||||||
|
|
||||||
@@ -788,8 +782,7 @@ while ($row = mysqli_fetch_assoc($sql_recurring_invoices)) {
|
|||||||
$contact_name = escapeSql($row['contact_name']);
|
$contact_name = escapeSql($row['contact_name']);
|
||||||
$contact_email = escapeSql($row['contact_email']);
|
$contact_email = escapeSql($row['contact_email']);
|
||||||
|
|
||||||
// Send invoice email if: (1) Global recurring auto-send is on, (2) the recurring invoice is set to auto-send (than just be a draft), and (3) the recurring invoice is opted in for the client to be notified
|
if ($config_recurring_auto_send_invoice == 1 && $recurring_invoice_email_notify == 1) {
|
||||||
if ($config_recurring_auto_send_invoice == 1 && $recurring_invoice_auto_send == 1 && $recurring_invoice_email_notify == 1) {
|
|
||||||
|
|
||||||
$subject = "Invoice $invoice_prefix$invoice_number";
|
$subject = "Invoice $invoice_prefix$invoice_number";
|
||||||
$body = "Hello $contact_name,<br><br>An invoice regarding \"$invoice_scope\" has been generated. Please view the details below.<br><br>Invoice: $invoice_prefix$invoice_number<br>Issue Date: $invoice_date<br>Total: " . numfmt_format_currency($currency_format, $invoice_amount, $recurring_invoice_currency_code) . "<br>Due Date: $invoice_due<br><br><br>To view your invoice, please click <a href=\'https://$config_base_url/guest/guest_view_invoice.php?invoice_id=$new_invoice_id&url_key=$invoice_url_key\'>here</a>.<br><br><br>--<br>$company_name - Billing<br>$config_invoice_from_email<br>$company_phone";
|
$body = "Hello $contact_name,<br><br>An invoice regarding \"$invoice_scope\" has been generated. Please view the details below.<br><br>Invoice: $invoice_prefix$invoice_number<br>Issue Date: $invoice_date<br>Total: " . numfmt_format_currency($currency_format, $invoice_amount, $recurring_invoice_currency_code) . "<br>Due Date: $invoice_due<br><br><br>To view your invoice, please click <a href=\'https://$config_base_url/guest/guest_view_invoice.php?invoice_id=$new_invoice_id&url_key=$invoice_url_key\'>here</a>.<br><br><br>--<br>$company_name - Billing<br>$config_invoice_from_email<br>$company_phone";
|
||||||
@@ -857,17 +850,6 @@ while ($row = mysqli_fetch_assoc($sql_invalid_recurring_invoices)) {
|
|||||||
}
|
}
|
||||||
// End Flag any active recurring "next run" dates that are in the past
|
// End Flag any active recurring "next run" dates that are in the past
|
||||||
|
|
||||||
// Start Notify draft invoices needing review
|
|
||||||
$sql_invoices_pending_send = mysqli_query($mysqli,"SELECT invoice_id FROM invoices WHERE invoice_status = 'Draft'");
|
|
||||||
$invoices_pending_send = mysqli_num_rows($sql_invoices_pending_send);
|
|
||||||
|
|
||||||
if ($invoices_pending_send > 0) {
|
|
||||||
|
|
||||||
appNotify("Draft Invoices", "There are $invoices_pending_send draft invoices pending review", "/agent/invoices.php?&status=Draft");
|
|
||||||
}
|
|
||||||
// End Notify draft invoices needing review
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Start Recurring Payments
|
// Start Recurring Payments
|
||||||
$sql_recurring_payments = mysqli_query($mysqli, "
|
$sql_recurring_payments = mysqli_query($mysqli, "
|
||||||
|
|||||||
1
db.sql
1
db.sql
@@ -2002,7 +2002,6 @@ CREATE TABLE `recurring_invoices` (
|
|||||||
`recurring_invoice_amount` decimal(15,2) NOT NULL DEFAULT 0.00,
|
`recurring_invoice_amount` decimal(15,2) NOT NULL DEFAULT 0.00,
|
||||||
`recurring_invoice_currency_code` varchar(200) NOT NULL,
|
`recurring_invoice_currency_code` varchar(200) NOT NULL,
|
||||||
`recurring_invoice_note` text DEFAULT NULL,
|
`recurring_invoice_note` text DEFAULT NULL,
|
||||||
`recurring_invoice_auto_send` tinyint(1) NOT NULL DEFAULT 1,
|
|
||||||
`recurring_invoice_email_notify` tinyint(1) NOT NULL DEFAULT 1,
|
`recurring_invoice_email_notify` tinyint(1) NOT NULL DEFAULT 1,
|
||||||
`recurring_invoice_created_at` datetime NOT NULL DEFAULT current_timestamp(),
|
`recurring_invoice_created_at` datetime NOT NULL DEFAULT current_timestamp(),
|
||||||
`recurring_invoice_updated_at` datetime DEFAULT NULL ON UPDATE current_timestamp(),
|
`recurring_invoice_updated_at` datetime DEFAULT NULL ON UPDATE current_timestamp(),
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -871,46 +858,3 @@ function getSentMethods() {
|
|||||||
'Other'
|
'Other'
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Products for the line-item autocomplete on invoices, quotes and recurring
|
|
||||||
* invoices.
|
|
||||||
*
|
|
||||||
* All three pages share js/product_autocomplete.js, so they must all be handed
|
|
||||||
* the same shape. They used to carry a SELECT each and they drifted: quote and
|
|
||||||
* recurring invoice only selected label/description/price/tax, so the shared
|
|
||||||
* onSelect wrote item.product_name - undefined - into the item name field.
|
|
||||||
*
|
|
||||||
* Returns a JSON string ready to emit into the page.
|
|
||||||
*/
|
|
||||||
function getProductsForAutocomplete($mysqli): string
|
|
||||||
{
|
|
||||||
$products = [];
|
|
||||||
|
|
||||||
$sql = mysqli_query($mysqli, "
|
|
||||||
SELECT
|
|
||||||
IF(product_code IS NULL OR product_code = '', product_name, CONCAT(product_code, ' - ', product_name)) AS label,
|
|
||||||
product_name,
|
|
||||||
product_code,
|
|
||||||
product_type AS type,
|
|
||||||
product_description AS description,
|
|
||||||
product_price AS price,
|
|
||||||
product_tax_id AS tax,
|
|
||||||
tax_percent,
|
|
||||||
product_id AS prod_id,
|
|
||||||
COALESCE(SUM(product_stock.stock_qty), 0) AS available_stock
|
|
||||||
FROM products
|
|
||||||
LEFT JOIN product_stock ON product_id = stock_product_id
|
|
||||||
LEFT JOIN taxes ON product_tax_id = tax_id
|
|
||||||
WHERE product_archived_at IS NULL
|
|
||||||
GROUP BY product_id
|
|
||||||
ORDER BY product_name ASC
|
|
||||||
");
|
|
||||||
|
|
||||||
while ($row = mysqli_fetch_assoc($sql)) {
|
|
||||||
$products[] = $row;
|
|
||||||
}
|
|
||||||
|
|
||||||
return json_encode($products) ?: '[]';
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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.3");
|
DEFINE("APP_VERSION", "26.09");
|
||||||
|
|||||||
37
js/app.js
37
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, {
|
||||||
@@ -653,15 +642,7 @@ function itflowInit() {
|
|||||||
|
|
||||||
// Clipboard
|
// Clipboard
|
||||||
itflowStep('clipboard', function () {
|
itflowStep('clipboard', function () {
|
||||||
if (window.itflowClipboard) {
|
var clipboard = new ClipboardJS('.clipboardjs');
|
||||||
window.itflowClipboard.destroy();
|
|
||||||
}
|
|
||||||
|
|
||||||
var modals = document.querySelectorAll('.modal');
|
|
||||||
var clipboard = new ClipboardJS('.clipboardjs', {
|
|
||||||
container: modals.length ? modals[modals.length - 1] : document.body
|
|
||||||
});
|
|
||||||
window.itflowClipboard = clipboard;
|
|
||||||
|
|
||||||
clipboard.on('success', function(e) {
|
clipboard.on('success', function(e) {
|
||||||
flashTooltip(e.trigger, 'Copied!');
|
flashTooltip(e.trigger, 'Copied!');
|
||||||
|
|||||||
@@ -1,76 +0,0 @@
|
|||||||
/*
|
|
||||||
* Product autocomplete for the add-item row on invoices, quotes and recurring
|
|
||||||
* invoices.
|
|
||||||
*
|
|
||||||
* The three pages used to carry a copy of this each, and they drifted - the
|
|
||||||
* quote and recurring invoice copies were fed a four column product query, so
|
|
||||||
* item.product_name was undefined and selecting a product wrote the literal
|
|
||||||
* string "undefined" into the item name.
|
|
||||||
*
|
|
||||||
* Call with the array emitted by getProductsForAutocomplete():
|
|
||||||
*
|
|
||||||
* initProductAutocomplete(<?= $json_products ?? '[]' ?>);
|
|
||||||
*/
|
|
||||||
function initProductAutocomplete(availableProducts) {
|
|
||||||
|
|
||||||
var nameInput = document.getElementById('name');
|
|
||||||
if (!nameInput) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Quote and recurring invoice do not store the product link yet, so treat
|
|
||||||
// the hidden input as optional rather than throwing on every keystroke.
|
|
||||||
var productIdInput = document.getElementById('product_id');
|
|
||||||
|
|
||||||
itflowAutocomplete(nameInput, {
|
|
||||||
minLength: 1,
|
|
||||||
source: availableProducts || [],
|
|
||||||
match: function (item, term) {
|
|
||||||
return String(item.label || '').toLowerCase().indexOf(term) !== -1
|
|
||||||
|| String(item.product_name || '').toLowerCase().indexOf(term) !== -1
|
|
||||||
|| String(item.product_code || '').toLowerCase().indexOf(term) !== -1;
|
|
||||||
},
|
|
||||||
render: function (item) {
|
|
||||||
var esc = itflowEscapeHtml;
|
|
||||||
var typeText = item.type ? item.type.charAt(0).toUpperCase() + item.type.slice(1).toLowerCase() : "";
|
|
||||||
var showStock = (typeText.toLowerCase() !== "service");
|
|
||||||
var taxText = (item.tax_percent != null) ? (parseFloat(item.tax_percent) + "%") : "No tax";
|
|
||||||
var priceText = (item.price != null && item.price !== "") ? String(item.price) : "";
|
|
||||||
var stockText = (item.available_stock ?? 0);
|
|
||||||
|
|
||||||
return "<div class='d-flex justify-content-between align-items-start'>" +
|
|
||||||
"<div class='flex-fill pe-2'>" +
|
|
||||||
"<div class='fw-bold'>" + esc(item.label) +
|
|
||||||
(typeText ? " <small class='text-muted'>(" + esc(typeText) + ")</small>" : "") +
|
|
||||||
"</div>" +
|
|
||||||
"<div class='small text-muted'>" + esc(item.description) + "</div>" +
|
|
||||||
"<div class='mt-1'>" +
|
|
||||||
"<span class='badge bg-secondary me-1'>Tax: " + esc(taxText) + "</span>" +
|
|
||||||
(showStock ? "<span class='badge " + (stockText > 0 ? "bg-success" : "bg-danger") + "'>Stock: " + esc(stockText) + "</span>" : "") +
|
|
||||||
"</div>" +
|
|
||||||
"</div>" +
|
|
||||||
"<div class='text-end'>" +
|
|
||||||
"<div class='fw-bold'>" + esc(priceText) + "</div>" +
|
|
||||||
"</div>" +
|
|
||||||
"</div>";
|
|
||||||
},
|
|
||||||
onSelect: function (item) {
|
|
||||||
nameInput.value = item.product_name;
|
|
||||||
document.getElementById('desc').value = item.description;
|
|
||||||
document.getElementById('qty').value = 1;
|
|
||||||
document.getElementById('price').value = item.price;
|
|
||||||
setTomSelectValue(document.getElementById('tax'), item.tax);
|
|
||||||
if (productIdInput) {
|
|
||||||
productIdInput.value = item.prod_id;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Typing over the name by hand breaks the link to the product
|
|
||||||
if (productIdInput) {
|
|
||||||
nameInput.addEventListener('input', function () {
|
|
||||||
productIdInput.value = 0;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user