Compare commits

...

26 Commits

Author SHA1 Message Date
ThaMunsta e65d231d1d Merge branch 'master' into techbar
merge conflict on filter_header but i like my implementation better
2025-04-22 09:46:24 -04:00
Johnny 83e15e9e4a Merge pull request #1216 from itflow-org/develop
Develop to Master 25.03.6
2025-04-21 17:28:14 -04:00
johnnyq b309081d75 Allow to search by project reference number 2025-04-21 17:16:35 -04:00
johnnyq f1a7b35aa6 Update Changelog and App Version fix date to 2025-04-21 17:00:32 -04:00
Marcus Hill 469c5ef06d Update client pdf export
- Fix HTML formatting for the cover div, other div styling is still broken
- Adjust layout of cover info and add MSP logo
- Add software purchase and expiry dates
2025-04-19 16:30:00 +01:00
Marcus Hill 07cbe561bd Add stupidly bigger update warning to update page. Add reminder note to check ITFlow backup (one in every ten page loads) 2025-04-19 15:14:40 +01:00
Johnny b69a70cfc3 Merge pull request #1213 from itflow-org/develop
Develop to Master - 25.03.5 Release
2025-04-18 19:08:18 -04:00
johnnyq 923001928c Update Changelog and version 2025-04-18 18:08:40 -04:00
johnnyq 75ed461c67 Asset and Contact Links now goto the details page instead of the details modal 2025-04-16 18:51:53 -04:00
johnnyq 691aebce91 Revert Fix 2025-04-13 15:15:18 -04:00
johnnyq 846947ff49 Change the button handlebar class 2025-04-13 15:11:37 -04:00
johnnyq 65e107d154 Totally remove Dragula in Favor of the modern SortableJS library, updated the Kanban 2025-04-13 15:01:52 -04:00
johnnyq 19b809b699 Added SortableJS Library, and updated Invoice, Quote and Recurring to use it. Added Grab Bar Icons next to action buttons. Will now sort in Mobile much more efficiently, update ajax vars for recurring invoice 2025-04-13 13:29:16 -04:00
johnnyq 60fe02bb47 Comment 2025-04-13 11:57:26 -04:00
johnnyq 3e708059c6 Fix not showing File folders instead of Document Folders when creating a document. 2025-04-13 11:55:14 -04:00
johnnyq 424104bb66 Default Date between max date to 9999-12-31 instead of current date for filtered listings, this fixes the issue if you post date an entity it would not show in the listing by default unless you selected a a great to date in the filter 2025-04-12 12:08:30 -04:00
johnnyq 62696b9ebe Fix Mobile Country Code in contact list 2025-04-12 11:58:32 -04:00
Johnny 87403e8c2d Merge pull request #1209 from itflow-org/index-redirect
Redirect the blank index page to the start page
2025-04-12 11:54:05 -04:00
wrongecho 7a5a607ff6 Redirect the blank index page to the start page 2025-04-11 14:53:02 +01:00
wrongecho a195774726 Update README.md
Add JetBrains to sponsors, as they provide FOSS licenses of PhpStorm to us
2025-04-11 14:31:01 +01:00
johnnyq 8d0da7b55b Fix Entity Linking in Asset and contact details 2025-04-09 14:00:51 -04:00
Johnny 58c315cd09 Merge pull request #1207 from TamirSlo/fix-dashboard-db-update-1-9-7
Fix Dashboard following DB Update 1.9.7
2025-04-08 17:00:40 -04:00
Johnny dd6c4602db Merge pull request #1194 from ssteeltm/develop
fix: missing kanban ticket settings
2025-04-08 16:59:07 -04:00
Tamir Slobodskoy d413e0c8ff Fix Copy Trip Modal 2025-04-08 05:27:56 +01:00
Tamir Slobodskoy b356658635 Fix Dashboard following DB Update 1.9.7 2025-04-08 05:02:46 +01:00
ssteeltm a5f7b7fa9c fix: missing kanban ticket settings 2025-03-28 12:00:48 -03:00
39 changed files with 585 additions and 521 deletions
+21
View File
@@ -2,6 +2,27 @@
This file documents all notable changes made to ITFlow. This file documents all notable changes made to ITFlow.
## [25.03.6]
### Fixed
- Set default to date to 2035-12-31 as 9999-12-31 and 2999-12-31 broke certain browsers.
- Update Client PDF Export, add header added company logo.
- Present Larger clearer Warning about updates on update page.
- Allow to search by project reference.
## [25.03.5]
### Fixed
- Fixed the user listing issue when copying a trip.
- Corrected the display of recurring invoice amounts on the dashboard.
- Fixed the linking of entities with assets and contacts.
- Resolved the issue with displaying the correct mobile country code in the contact listing.
- Set the default date to `9999-12-31` to ensure future items (like invoices) are displayed by default.
- Fixed the display issue where file folders were not showing properly during document creation.
- Migrated from Dragula to SortableJS for a more modern, mobile-friendly solution.
- Added Handlebars icons for drag-and-drop items.
- Changed behavior to open Contact and Asset Details pages directly instead of using a modal.
## [25.03.4] ## [25.03.4]
### Fixed ### Fixed
+1
View File
@@ -93,6 +93,7 @@ If you want to improve ITFlow, feel free to fork the repo and create a pull requ
Were incredibly grateful to the organizations and individuals who support the project - a big thank you to: Were incredibly grateful to the organizations and individuals who support the project - a big thank you to:
- CompuMatter - CompuMatter
- F1 for HELP - F1 for HELP
- JetBrains (PhpStorm)
## License ## License
ITFlow is distributed "as is" under the GPL License, WITHOUT WARRANTY OF ANY KIND. See [`LICENSE`](https://github.com/itflow-org/itflow/blob/master/LICENSE) for details. ITFlow is distributed "as is" under the GPL License, WITHOUT WARRANTY OF ANY KIND. See [`LICENSE`](https://github.com/itflow-org/itflow/blob/master/LICENSE) for details.
+25
View File
@@ -73,6 +73,31 @@ require_once "includes/inc_all_admin.php";
</div> </div>
</div> </div>
<div class="form-group">
<label>Tickets Default View</label>
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text"><i class="fa fa-fw fa-eye"></i></span>
</div>
<select class="form-control" name="config_ticket_default_view">
<option value=0 <?php if ($config_ticket_default_view == 0) { echo "selected"; } ?>>List</option>
<option value=1 <?php if ($config_ticket_default_view == 1) { echo "selected"; } ?>>Compact</option>
<option value=2 <?php if ($config_ticket_default_view == 2) { echo "selected"; } ?>>Kanban</option>
</select>
</div>
</div>
<div class="form-group">
<label>Kanban Settings</label>
<div class="custom-control custom-switch">
<input type="checkbox" class="custom-control-input" name="config_ticket_ordering" <?php if ($config_ticket_ordering == 1) { echo "checked"; } ?> value="1" id="ticketOrderingSwitch">
<label class="custom-control-label" for="ticketOrderingSwitch">Allow ticket ordering within its column<small class="text-secondary">(uncheked will result in ordering it by priority and id)</small></label>
</div>
<div class="custom-control custom-switch">
<input type="checkbox" class="custom-control-input" name="config_ticket_moving_columns" <?php if ($config_ticket_moving_columns == 1) { echo "checked"; } ?> value="1" id="ticketMovingColumnsSwitch">
<label class="custom-control-label" for="ticketMovingColumnsSwitch">Allow moving columns</label>
</div>
</div>
<hr> <hr>
<button type="submit" name="edit_ticket_settings" class="btn btn-primary text-bold"><i class="fas fa-check mr-2"></i>Save</button> <button type="submit" name="edit_ticket_settings" class="btn btn-primary text-bold"><i class="fas fa-check mr-2"></i>Save</button>
+15 -32
View File
@@ -30,7 +30,6 @@ $ticket_template_updated_at = nullable_htmlentities($row['ticket_template_update
$sql_task_templates = mysqli_query($mysqli, "SELECT * FROM task_templates WHERE task_template_ticket_template_id = $ticket_template_id ORDER BY task_template_order ASC, task_template_id ASC"); $sql_task_templates = mysqli_query($mysqli, "SELECT * FROM task_templates WHERE task_template_ticket_template_id = $ticket_template_id ORDER BY task_template_order ASC, task_template_id ASC");
?> ?>
<link rel="stylesheet" href="plugins/dragula/dragula.min.css">
<ol class="breadcrumb d-print-none"> <ol class="breadcrumb d-print-none">
<li class="breadcrumb-item"> <li class="breadcrumb-item">
@@ -94,7 +93,7 @@ $sql_task_templates = mysqli_query($mysqli, "SELECT * FROM task_templates WHERE
</div> </div>
</div> </div>
</form> </form>
<table class="table table-striped table-sm"> <table class="table table-sm" id="tasks">
<?php <?php
while($row = mysqli_fetch_array($sql_task_templates)){ while($row = mysqli_fetch_array($sql_task_templates)){
$task_id = intval($row['task_template_id']); $task_id = intval($row['task_template_id']);
@@ -103,12 +102,10 @@ $sql_task_templates = mysqli_query($mysqli, "SELECT * FROM task_templates WHERE
$task_description = nullable_htmlentities($row['task_template_description']); $task_description = nullable_htmlentities($row['task_template_description']);
?> ?>
<tr data-task-id="<?php echo $task_id; ?>"> <tr data-task-id="<?php echo $task_id; ?>">
<td><i class="far fa-fw fa-square text-secondary"></i></td>
<td> <td>
<a href="#" class="grab-cursor"> <a href="#" class="drag-handle"><i class="fas fa-bars text-muted mr-1"></i></a>
<span class="text-secondary"><?php echo $task_completion_estimate; ?>m</span> <span class="text-secondary"><?php echo $task_completion_estimate; ?>m</span>
<span class="text-dark"> - <?php echo $task_name; ?></span> <span class="text-dark"> - <?php echo $task_name; ?></span>
</a>
</td> </td>
<td class="text-right"> <td class="text-right">
<div class="float-right"> <div class="float-right">
@@ -145,40 +142,26 @@ $sql_task_templates = mysqli_query($mysqli, "SELECT * FROM task_templates WHERE
</div> </div>
<script src="js/pretty_content.js"></script> <script src="js/pretty_content.js"></script>
<script src="plugins/dragula/dragula.min.js"></script>
<script src="plugins/SortableJS/Sortable.min.js"></script>
<script> <script>
$(document).ready(function() { new Sortable(document.querySelector('table#tasks tbody'), {
var container = $('.table tbody')[0]; handle: '.drag-handle',
animation: 150,
dragula([container]) onEnd: function (evt) {
.on('drop', function (el, target, source, sibling) { const rows = document.querySelectorAll('table#tasks tbody tr');
// Handle the drop event to update the order in the database const positions = Array.from(rows).map((row, index) => ({
var rows = $(container).children(); id: row.dataset.taskId,
var positions = rows.map(function(index, row) {
return {
id: $(row).data('taskId'),
order: index order: index
}; }));
}).get();
// Send the new order to the server $.post('ajax.php', {
$.ajax({ update_task_templates_order: true,
url: 'ajax.php',
method: 'POST',
data: {
update_task_templates_order: true, // Adjust the parameter name if needed
ticket_template_id: <?php echo $ticket_template_id; ?>, ticket_template_id: <?php echo $ticket_template_id; ?>,
positions: positions positions: positions
}, });
success: function(data) {
// Handle success
},
error: function(error) {
console.error('Error updating order:', error);
} }
}); });
});
});
</script> </script>
<?php <?php
+26 -4
View File
@@ -32,8 +32,11 @@ $git_log = shell_exec("git log $repo_branch..origin/$repo_branch --pretty=format
<?php } ?> <?php } ?>
<?php if (LATEST_DATABASE_VERSION > CURRENT_DATABASE_VERSION) { ?> <?php if (LATEST_DATABASE_VERSION > CURRENT_DATABASE_VERSION) { ?>
<div class="alert alert-warning"> <div class="alert alert-danger">
<strong>Ensure you have a current <a href="https://docs.itflow.org/backups">app & database backup</a> before updating!</strong> <h1 class="font-weight-bold text-center">⚠️ DANGER ⚠️</h1>
<h2 class="font-weight-bold text-center">Do NOT run updates without first taking a backup</h2>
<p>VM Snapshots are highly recommended over other methods - see the <a href="https://docs.itflow.org/backups" class="alert-link" target="_blank">docs</a>. Review the <a href="https://github.com/itflow-org/itflow/blob/master/CHANGELOG.md" class="alert-link" target="_blank">changelog</a> for breaking changes that may require manual remediation.</p>
<p class="text-center font-weight-bold">Ignore this warning at your own risk.</p>
</div> </div>
<br> <br>
<a class="btn btn-dark btn-lg my-4" href="post.php?update_db"><i class="fas fa-fw fa-4x fa-download mb-1"></i><h5>Update Database</h5></a> <a class="btn btn-dark btn-lg my-4" href="post.php?update_db"><i class="fas fa-fw fa-4x fa-download mb-1"></i><h5>Update Database</h5></a>
@@ -46,9 +49,17 @@ $git_log = shell_exec("git log $repo_branch..origin/$repo_branch --pretty=format
<?php } else { <?php } else {
if (!empty($git_log)) { ?> if (!empty($git_log)) { ?>
<div class="alert alert-danger">
<h1 class="font-weight-bold text-center">⚠️ DANGER ⚠️</h1>
<h2 class="font-weight-bold text-center">Do NOT run updates without first taking a backup</h2>
<p>VM Snapshots are highly recommended over other methods - see the <a href="https://docs.itflow.org/backups" class="alert-link" target="_blank">docs</a>. Review the <a href="https://github.com/itflow-org/itflow/blob/master/CHANGELOG.md" class="alert-link" target="_blank">changelog</a> for breaking changes that may require manual remediation.</p>
<p class="text-center font-weight-bold">Ignore this warning at your own risk.</p>
</div>
<a class="btn btn-primary btn-lg my-4" href="post.php?update"><i class="fas fa-fw fa-4x fa-download mb-1"></i><h5>Update App</h5></a> <a class="btn btn-primary btn-lg my-4 confirm-link" href="post.php?no"><i class="fas fa-fw fa-4x fa-download mb-1"></i><h5>TEST</h5></a>
<a class="btn btn-danger btn-lg" href="post.php?update&force_update=1"><i class="fas fa-fw fa-4x fa-hammer mb-1"></i><h5>FORCE Update App</h5></a>
<a class="btn btn-primary btn-lg my-4 confirm-link" href="post.php?update"><i class="fas fa-fw fa-4x fa-download mb-1"></i><h5>Update App</h5></a>
<a class="btn btn-danger btn-lg confirm-link" href="post.php?update&force_update=1"><i class="fas fa-fw fa-4x fa-hammer mb-1"></i><h5>FORCE Update App</h5></a>
<?php } else { ?> <?php } else { ?>
<p><strong>Application Release Version:<br><strong class="text-dark"><?php echo APP_VERSION; ?></strong></p> <p><strong>Application Release Version:<br><strong class="text-dark"><?php echo APP_VERSION; ?></strong></p>
@@ -56,6 +67,17 @@ $git_log = shell_exec("git log $repo_branch..origin/$repo_branch --pretty=format
<p class="text-secondary">Code Commit:<br><strong class="text-dark"><?php echo $current_version; ?></strong></p> <p class="text-secondary">Code Commit:<br><strong class="text-dark"><?php echo $current_version; ?></strong></p>
<p class="text-muted">You are up to date!<br>Everything is going to be alright</p> <p class="text-muted">You are up to date!<br>Everything is going to be alright</p>
<i class="far fa-3x text-dark fa-smile-wink"></i><br> <i class="far fa-3x text-dark fa-smile-wink"></i><br>
<?php if (rand(1,10) == 1) { ?>
<br>
<div class="alert alert-info alert-dismissible fade show" role="alert">
You're up to date, but when was the last time you checked your ITFlow backup works?
<button type="button" class="close" data-dismiss="alert" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<?php } ?>
<?php } <?php }
} }
+2 -2
View File
@@ -586,13 +586,13 @@ if (isset($_POST['update_recurring_invoice_items_order'])) {
enforceUserPermission('module_sales', 2); enforceUserPermission('module_sales', 2);
$positions = $_POST['positions']; $positions = $_POST['positions'];
$recurring_id = intval($_POST['recurring_id']); $recurring_invoice_id = intval($_POST['recurring_invoice_id']);
foreach ($positions as $position) { foreach ($positions as $position) {
$id = intval($position['id']); $id = intval($position['id']);
$order = intval($position['order']); $order = intval($position['order']);
mysqli_query($mysqli, "UPDATE invoice_items SET item_order = $order WHERE item_recurring_id = $recurring_id AND item_id = $id"); mysqli_query($mysqli, "UPDATE invoice_items SET item_order = $order WHERE item_recurring_invoice_id = $recurring_invoice_id AND item_id = $id");
} }
// return a response // return a response
+1 -1
View File
@@ -115,7 +115,7 @@ ob_start();
$sql_users = mysqli_query($mysqli, "SELECT users.user_id, user_name FROM users $sql_users = mysqli_query($mysqli, "SELECT users.user_id, user_name FROM users
LEFT JOIN user_settings on users.user_id = user_settings.user_id LEFT JOIN user_settings on users.user_id = user_settings.user_id
WHERE user_role > 1 AND user_archived_at IS NULL ORDER BY user_name ASC" WHERE user_role_id > 1 AND user_archived_at IS NULL ORDER BY user_name ASC"
); );
while ($row = mysqli_fetch_array($sql_users)) { while ($row = mysqli_fetch_array($sql_users)) {
$user_id_select = intval($row['user_id']); $user_id_select = intval($row['user_id']);
+2 -2
View File
@@ -244,7 +244,7 @@ if (isset($_GET['asset_id'])) {
data-ajax-id="<?php echo $asset_id; ?>"> data-ajax-id="<?php echo $asset_id; ?>">
<i class="fas fa-fw fa-edit"></i> <i class="fas fa-fw fa-edit"></i>
</button> </button>
<h3 class="text-bold"><i class="fa fa-fw text-secondary fa-<?php echo $device_icon; ?> mr-3"></i><?php echo $asset_name; ?></h3> <h4 class="text-bold"><i class="fa fa-fw text-secondary fa-<?php echo $device_icon; ?> mr-3"></i><?php echo $asset_name; ?></h4>
<?php if ($asset_photo) { ?> <?php if ($asset_photo) { ?>
<img class="img-fluid img-circle p-3" alt="asset_photo" src="<?php echo "uploads/clients/$client_id/$asset_photo"; ?>"> <img class="img-fluid img-circle p-3" alt="asset_photo" src="<?php echo "uploads/clients/$client_id/$asset_photo"; ?>">
<?php } ?> <?php } ?>
@@ -405,7 +405,7 @@ if (isset($_GET['asset_id'])) {
<div class="card card-dark"> <div class="card card-dark">
<div class="card-header py-2"> <div class="card-header py-2">
<h3 class="card-title mt-2"><i class="fa fa-fw fa-ethernet mr-2"></i><?php echo $asset_name; ?> Network Interfaces</h3> <h3 class="card-title mt-2"><i class="fa fa-fw fa-ethernet mr-2"></i>Interfaces</h3>
<div class="card-tools"> <div class="card-tools">
<div class="btn-group"> <div class="btn-group">
<button type="button" class="btn btn-primary" data-toggle="modal" data-target="#addAssetInterfaceModal"> <button type="button" class="btn btn-primary" data-toggle="modal" data-target="#addAssetInterfaceModal">
+1 -5
View File
@@ -550,11 +550,7 @@ if (mysqli_num_rows($os_sql) > 0) {
</div> </div>
</td> </td>
<td> <td>
<a class="text-dark" href="#" <a class="text-dark" href="asset_details.php?client_id=<?php echo $client_id; ?>&asset_id=<?php echo $asset_id; ?>">
data-toggle="ajax-modal"
data-modal-size="lg"
data-ajax-url="ajax/ajax_asset_details.php?<?php echo $client_url; ?>"
data-ajax-id="<?php echo $asset_id; ?>">
<div class="media"> <div class="media">
<i class="fa fa-fw fa-2x fa-<?php echo $device_icon; ?> mr-3 mt-1"></i> <i class="fa fa-fw fa-2x fa-<?php echo $device_icon; ?> mr-3 mt-1"></i>
<div class="media-body"> <div class="media-body">
+2 -6
View File
@@ -319,7 +319,7 @@ $num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()"));
} else { } else {
$contact_phone_display = "<div><i class='fas fa-fw fa-phone mr-2'></i><a href='tel:$contact_phone'>$contact_phone$contact_extension_display</a></div>"; $contact_phone_display = "<div><i class='fas fa-fw fa-phone mr-2'></i><a href='tel:$contact_phone'>$contact_phone$contact_extension_display</a></div>";
} }
$contact_mobile_country_code = nullable_htmlentities($row['contact_phone_country_code']); $contact_mobile_country_code = nullable_htmlentities($row['contact_mobile_country_code']);
$contact_mobile = nullable_htmlentities(formatPhoneNumber($row['contact_mobile'], $contact_mobile_country_code)); $contact_mobile = nullable_htmlentities(formatPhoneNumber($row['contact_mobile'], $contact_mobile_country_code));
if (empty($contact_mobile)) { if (empty($contact_mobile)) {
$contact_mobile_display = ""; $contact_mobile_display = "";
@@ -445,11 +445,7 @@ $num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()"));
</div> </div>
</td> </td>
<td> <td>
<a class="text-dark" href="#" <a class="text-dark" href="contact_details.php?client_id=<?php echo $client_id; ?>&contact_id=<?php echo $contact_id; ?>">
data-toggle="ajax-modal"
data-modal-size="lg"
data-ajax-url="ajax/ajax_contact_details.php?<?php echo $client_url; ?>"
data-ajax-id="<?php echo $contact_id; ?>">
<div class="media"> <div class="media">
<?php if ($contact_photo) { ?> <?php if ($contact_photo) { ?>
<span class="fa-stack fa-2x mr-3 text-center"> <span class="fa-stack fa-2x mr-3 text-center">
+4 -3
View File
@@ -20,10 +20,11 @@
} }
} }
.grab-cursor { .drag-handle {
cursor: grab; cursor: grab;
touch-action: none;
user-select: none;
} }
.drag-handle:active {
.grab-cursor:active {
cursor: grabbing; cursor: grabbing;
} }
+53 -11
View File
@@ -1,41 +1,83 @@
/* General Popover Styling */
.popover { .popover {
max-width: 600px; max-width: 600px;
} }
/* Kanban Board Container */
#kanban-board { #kanban-board {
display: flex; display: flex;
box-sizing: border-box;
overflow-x: auto; overflow-x: auto;
box-sizing: border-box;
min-width: 400px; min-width: 400px;
height: calc(100vh - 210px); height: calc(100vh - 210px);
} }
/* Kanban Column */
.kanban-column { .kanban-column {
flex: 1; /* Allows columns to grow equally */ flex: 1;
margin: 0 10px; /* Space between columns */
min-width: 300px; min-width: 300px;
max-width: 300px; max-width: 300px;
margin: 0 10px;
background: #f4f4f4; background: #f4f4f4;
padding: 10px;
border: 1px solid #ccc; border: 1px solid #ccc;
border-radius: 4px;
padding: 10px;
min-height: calc(100vh - 230px); min-height: calc(100vh - 230px);
max-height: calc(100vh - 230px); max-height: calc(100vh - 230px);
box-sizing: border-box; box-sizing: border-box;
display: flex;
flex-direction: column;
} }
.kanban-column div { /* Column Inner Scrollable Task Area */
max-height: calc(100vh - 280px); /* Set your desired max height */ .kanban-status {
overflow-y: auto; /* Adds a scrollbar when content exceeds max height */ flex: 1;
overflow-y: auto;
min-height: 60px;
position: relative;
padding: 5px;
background-color: #f9f9f9;
border-radius: 4px;
} }
/* Individual Task Cards */
.task { .task {
background: #fff; background: #fff;
margin: 5px 0; margin: 5px 0;
padding: 10px; padding: 10px;
border: 1px solid #ddd; border: 1px solid #ddd;
user-select: none; /* Prevent text selection */ border-radius: 4px;
cursor: grab;
user-select: none;
} }
.drag-handle-class { /* Grabbing Cursor State */
touch-action: none; .task:active {
float: right; cursor: grabbing;
}
/* Drag Handle (shown on mobile or with class targeting) */
.drag-handle-class {
float: right;
touch-action: none;
cursor: grab;
}
/* Placeholder shown in empty columns */
.empty-placeholder {
border: 2px dashed #ccc;
background-color: #fcfcfc;
color: #999;
font-style: italic;
padding: 12px;
margin: 10px 0;
text-align: center;
border-radius: 4px;
pointer-events: none;
}
/* Sortable drop zone feedback (optional visual cue) */
.kanban-status.sortable-over {
background-color: #eaf6ff;
transition: background-color 0.2s ease;
} }
+1 -1
View File
@@ -151,7 +151,7 @@ if ($user_config_dashboard_financial_enable == 1) {
$row = mysqli_fetch_array($sql_unbilled_tickets); $row = mysqli_fetch_array($sql_unbilled_tickets);
$unbilled_tickets = intval($row['unbilled_tickets']); $unbilled_tickets = intval($row['unbilled_tickets']);
} else { } else {
$row = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT COUNT(recurring_id) AS recurring_invoices_added FROM recurring WHERE YEAR(recurring_created_at) = $year")); $row = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT COUNT(recurring_invoice_id) AS recurring_invoices_added FROM recurring_invoices WHERE YEAR(recurring_invoice_created_at) = $year"));
$recurring_invoices_added = intval($row['recurring_invoices_added']); $recurring_invoices_added = intval($row['recurring_invoices_added']);
} }
+9 -4
View File
@@ -1622,10 +1622,14 @@ function getFieldById($table, $id, $field, $escape_method = 'sql') {
} }
// Recursive function to display folder options - Used in folders files and documents // Recursive function to display folder options - Used in folders files and documents
function display_folder_options($parent_folder_id, $client_id, $indent = 0) { function display_folder_options($parent_folder_id, $client_id, $folder_location = 0, $indent = 0) {
global $mysqli; global $mysqli;
$sql_folders = mysqli_query($mysqli, "SELECT * FROM folders WHERE parent_folder = $parent_folder_id AND folder_location = 1 AND folder_client_id = $client_id ORDER BY folder_name ASC"); $folder_location = intval($folder_location);
// 0 = Document Folders
// 1 = File Folders
$sql_folders = mysqli_query($mysqli, "SELECT * FROM folders WHERE parent_folder = $parent_folder_id AND folder_location = $folder_location AND folder_client_id = $client_id ORDER BY folder_name ASC");
while ($row = mysqli_fetch_array($sql_folders)) { while ($row = mysqli_fetch_array($sql_folders)) {
$folder_id = intval($row['folder_id']); $folder_id = intval($row['folder_id']);
$folder_name = nullable_htmlentities($row['folder_name']); $folder_name = nullable_htmlentities($row['folder_name']);
@@ -1635,13 +1639,14 @@ function display_folder_options($parent_folder_id, $client_id, $indent = 0) {
// Check if this folder is selected // Check if this folder is selected
$selected = ''; $selected = '';
if ((isset($_GET['folder_id']) && $_GET['folder_id'] == $folder_id) || (isset($_POST['folder']) && $_POST['folder'] == $folder_id)) { if ((isset($_GET['folder_id']) && intval($_GET['folder_id']) === $folder_id) ||
(isset($_POST['folder']) && intval($_POST['folder']) === $folder_id)) {
$selected = 'selected'; $selected = 'selected';
} }
echo "<option value=\"$folder_id\" $selected>$indentation$folder_name</option>"; echo "<option value=\"$folder_id\" $selected>$indentation$folder_name</option>";
// Recursively display subfolders // Recursively display subfolders
display_folder_options($folder_id, $client_id, $indent + 1); display_folder_options($folder_id, $client_id, $folder_location, $indent + 1);
} }
} }
+1 -1
View File
@@ -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", "25.03.4"); DEFINE("APP_VERSION", "25.03.6");
+4
View File
@@ -16,6 +16,10 @@ if (file_exists("config.php")) {
<hr> <hr>
<?php <?php
if (isset($config_start_page)) { ?>
<meta http-equiv="refresh" content="0;url=<?php echo $config_start_page; ?>">
<?php }
require_once "includes/footer.php"; require_once "includes/footer.php";
+21 -29
View File
@@ -165,7 +165,6 @@ if (isset($_GET['invoice_id'])) {
?> ?>
<link rel="stylesheet" href="plugins/dragula/dragula.min.css">
<ol class="breadcrumb d-print-none"> <ol class="breadcrumb d-print-none">
<?php if (isset($_GET['client_id'])) { ?> <?php if (isset($_GET['client_id'])) { ?>
@@ -381,6 +380,13 @@ if (isset($_GET['invoice_id'])) {
<tr data-item-id="<?php echo $item_id; ?>"> <tr data-item-id="<?php echo $item_id; ?>">
<td class="d-print-none"> <td class="d-print-none">
<?php if ($invoice_status !== "Paid" && $invoice_status !== "Cancelled") { ?> <?php if ($invoice_status !== "Paid" && $invoice_status !== "Cancelled") { ?>
<div class="row">
<div class="col">
<button type="button" class="btn btn-sm btn-light drag-handle">
<i class="fas fa-bars text-muted"></i>
</button>
</div>
<div class="col">
<div class="dropdown"> <div class="dropdown">
<button class="btn btn-sm btn-light" type="button" data-toggle="dropdown"> <button class="btn btn-sm btn-light" type="button" data-toggle="dropdown">
<i class="fas fa-ellipsis-v"></i> <i class="fas fa-ellipsis-v"></i>
@@ -397,10 +403,11 @@ if (isset($_GET['invoice_id'])) {
<a class="dropdown-item text-danger confirm-link" href="post.php?delete_invoice_item=<?php echo $item_id; ?>"><i class="fa fa-fw fa-trash mr-2"></i>Delete</a> <a class="dropdown-item text-danger confirm-link" href="post.php?delete_invoice_item=<?php echo $item_id; ?>"><i class="fa fa-fw fa-trash mr-2"></i>Delete</a>
</div> </div>
</div> </div>
</div>
</div>
<?php } ?> <?php } ?>
</td> </td>
<td class="grab-cursor"><?php echo $item_name; ?></td> <td><?php echo $item_name; ?></td>
<td><?php echo nl2br($item_description); ?></td> <td><?php echo nl2br($item_description); ?></td>
<td class="text-center"><?php echo number_format($item_quantity, 2); ?></td> <td class="text-center"><?php echo number_format($item_quantity, 2); ?></td>
<td class="text-right"><?php echo numfmt_format_currency($currency_format, $item_price, $invoice_currency_code); ?></td> <td class="text-right"><?php echo numfmt_format_currency($currency_format, $item_price, $invoice_currency_code); ?></td>
@@ -1178,38 +1185,23 @@ require_once "includes/footer.php";
} }
</script> </script>
<script src="plugins/dragula/dragula.min.js"></script> <script src="plugins/SortableJS/Sortable.min.js"></script>
<script> <script>
$(document).ready(function() { new Sortable(document.querySelector('table#items tbody'), {
var container = $('table#items tbody')[0]; handle: '.drag-handle',
animation: 150,
dragula([container]) onEnd: function (evt) {
.on('drop', function (el, target, source, sibling) { const rows = document.querySelectorAll('table#items tbody tr');
// Handle the drop event to update the order in the database const positions = Array.from(rows).map((row, index) => ({
var rows = $(container).children(); id: row.dataset.itemId,
var positions = rows.map(function(index, row) {
return {
id: $(row).data('itemId'),
order: index order: index
}; }));
}).get();
// Send the new order to the server $.post('ajax.php', {
$.ajax({
url: 'ajax.php',
method: 'POST',
data: {
update_invoice_items_order: true, update_invoice_items_order: true,
invoice_id: <?php echo $invoice_id; ?>, invoice_id: <?php echo $invoice_id; ?>,
positions: positions positions: positions
}, });
success: function(data) {
// Handle success
},
error: function(error) {
console.error('Error updating order:', error);
} }
}); });
});
});
</script> </script>
+97 -117
View File
@@ -1,146 +1,126 @@
$(document).ready(function () { $(document).ready(function () {
console.log('CONFIG_TICKET_MOVING_COLUMNS: ' + CONFIG_TICKET_MOVING_COLUMNS); console.log('CONFIG_TICKET_MOVING_COLUMNS:', CONFIG_TICKET_MOVING_COLUMNS);
console.log('CONFIG_TICKET_ORDERING: ' + CONFIG_TICKET_ORDERING); console.log('CONFIG_TICKET_ORDERING:', CONFIG_TICKET_ORDERING);
// Function to detect touch devices // -------------------------------
function isTouchDevice() { // Drag: Kanban Columns (Statuses)
return 'ontouchstart' in window || navigator.maxTouchPoints; // -------------------------------
} new Sortable(document.querySelector('#kanban-board'), {
animation: 150,
// Initialize Dragula for the Kanban board handle: '.panel-title',
let boardDrake = dragula([ draggable: '.kanban-column',
document.querySelector('#kanban-board') onEnd: function () {
], { const columnPositions = Array.from(document.querySelectorAll('#kanban-board .kanban-column')).map((col, index) => ({
moves: function(el, container, handle) { status_id: $(col).data('status-id'),
return handle.classList.contains('panel-title');
},
accepts: function(el, target, source, sibling) {
return CONFIG_TICKET_MOVING_COLUMNS === 1;
}
});
// Log the event of moving the column panel-title
boardDrake.on('drag', function(el) {
//console.log('Dragging column:', el.querySelector('.panel-title').innerText);
});
boardDrake.on('drop', function(el, target, source, sibling) {
//console.log('Dropped column:', el.querySelector('.panel-title').innerText);
// Get all columns and their positions
let columns = document.querySelectorAll('#kanban-board .kanban-column');
let columnPositions = [];
columns.forEach(function(column, index) {
let statusId = $(column).data('status-id'); // Assuming you have a data attribute for status ID
columnPositions.push({
status_id: statusId,
status_kanban: index status_kanban: index
}); }));
});
// Send AJAX request to update all column positions if (CONFIG_TICKET_MOVING_COLUMNS === 1) {
$.ajax({ $.post('ajax.php', {
url: 'ajax.php',
type: 'POST',
data: {
update_kanban_status_position: true, update_kanban_status_position: true,
positions: columnPositions positions: columnPositions
}, }).done(() => {
success: function(response) { console.log('Ticket status kanban orders updated.');
console.log('Ticket status kanban orders updated successfully.'); }).fail((xhr) => {
// Optionally, you can refresh the page or update the UI here console.error('Error updating status order:', xhr.responseText);
},
error: function(xhr, status, error) {
console.error('Error updating ticket status kanban orders:', error);
}
}); });
});
// Initialize Dragula for the Kanban Cards
let drake = dragula([
...document.querySelectorAll('#status')
], {
moves: function(el, container, handle) {
if (isTouchDevice()) {
return handle.classList.contains('drag-handle-class');
} else {
return true; // Allow dragging on the entire task element for desktop
} }
} }
}); });
if (isTouchDevice()) { // -------------------------------
const moveList = document.querySelectorAll('.task'); // Drag: Tasks within Columns
moveList.forEach(task => { // -------------------------------
task.querySelector('.drag-handle-class').style.display = 'inline'; document.querySelectorAll('.kanban-status').forEach(statusCol => {
}); new Sortable(statusCol, {
} group: 'tickets',
animation: 150,
handle: isTouchDevice() ? '.drag-handle-class' : undefined,
onStart: () => hidePlaceholders(),
onEnd: function (evt) {
const target = evt.to;
const movedEl = evt.item;
drake.on('drag', function(el) { // Disallow reordering in same column if config says so
el.style.cursor = 'grabbing'; if (CONFIG_TICKET_ORDERING === 0 && evt.from === evt.to) {
}); evt.from.insertBefore(movedEl, evt.from.children[evt.oldIndex]);
showPlaceholders();
drake.on('dragend', function(el) {
el.style.cursor = 'grab';
});
// Add event listener for the drop event
drake.on('drop', function (el, target, source, sibling) {
// Log the target ID to the console
//console.log('Dropped into:', target.getAttribute('data-column-name'));
if (CONFIG_TICKET_ORDERING === 0 && source == target) {
drake.cancel(true); // Move the card back to its original position
return; return;
} }
// Get all cards in the target column and their positions const columnId = $(target).data('status-id');
let cards = $(target).children('.task');
let positions = [];
//id of current status / column const positions = Array.from(target.querySelectorAll('.task')).map((card, index) => {
let columnId = $(target).data('status-id'); const ticketId = $(card).data('ticket-id');
const oldStatus = ticketId === $(movedEl).data('ticket-id')
? $(movedEl).data('ticket-status-id')
: false;
let movedTicketId = $(el).data('ticket-id'); $(card).data('ticket-status-id', columnId); // update DOM
let movedTicketStatusId = $(el).data('ticket-status-id');
cards.each(function(index, card) { return {
let ticketId = $(card).data('ticket-id');
let statusId = $(card).data('ticket-status-id');
let oldStatus = false;
if (ticketId == movedTicketId) {
oldStatus = movedTicketStatusId;
}
//update the status id of the card if needed
if (statusId != columnId) {
$(card).data('ticket-status-id', columnId);
statusId = columnId;
}
positions.push({
ticket_id: ticketId, ticket_id: ticketId,
ticket_order: index, ticket_order: index,
ticket_oldStatus: oldStatus, ticket_oldStatus: oldStatus,
ticket_status: statusId ?? null// Get the new status ID from the target column ticket_status: columnId
}); };
}); });
//console.log(positions); $.post('ajax.php', {
// Send AJAX request to update all ticket kanban orders and statuses
$.ajax({
url: 'ajax.php',
type: 'POST',
data: {
update_kanban_ticket: true, update_kanban_ticket: true,
positions: positions positions: positions
}, }).done(() => {
success: function(response) { console.log('Updated kanban ticket positions.');
//console.log('Ticket kanban orders and statuses updated successfully.'); }).fail((xhr) => {
}, console.error('Error updating ticket positions:', xhr.responseText);
error: function(xhr, status, error) { });
console.error('Error updating ticket kanban orders and statuses:', error);
// Refresh placeholders after update
showPlaceholders();
} }
}); });
}); });
// -------------------------------
// 📱 Touch Support: Show drag handle on mobile
// -------------------------------
if (isTouchDevice()) {
$('.drag-handle-class').css('display', 'inline');
}
// -------------------------------
// Placeholder Management
// -------------------------------
function showPlaceholders() {
document.querySelectorAll('.kanban-status').forEach(status => {
const placeholderClass = 'empty-placeholder';
// Remove existing placeholder
const existing = status.querySelector(`.${placeholderClass}`);
if (existing) existing.remove();
// Only show if there are no tasks
if (status.querySelectorAll('.task').length === 0) {
const placeholder = document.createElement('div');
placeholder.className = `${placeholderClass} text-muted text-center p-2`;
placeholder.innerText = 'Drop ticket here';
placeholder.style.pointerEvents = 'none';
status.appendChild(placeholder);
}
});
}
function hidePlaceholders() {
document.querySelectorAll('.empty-placeholder').forEach(el => el.remove());
}
// Run once on load
showPlaceholders();
// -------------------------------
// Utility: Detect touch device
// -------------------------------
function isTouchDevice() {
return 'ontouchstart' in window || navigator.maxTouchPoints > 0;
}
}); });
+1 -1
View File
@@ -8,7 +8,7 @@
</button> </button>
</div> </div>
<form action="post.php" method="post" autocomplete="off"> <form action="post.php" method="post" autocomplete="off">
<input type="hidden" name="asset_id" value="<?php echo $asset_id; ?>"> <input type="hidden" name="asset_id" value="<?php echo intval($_GET['asset_id']); ?>">
<div class="modal-body bg-white"> <div class="modal-body bg-white">
<div class="form-group"> <div class="form-group">
+1 -1
View File
@@ -8,7 +8,7 @@
</button> </button>
</div> </div>
<form action="post.php" method="post" autocomplete="off"> <form action="post.php" method="post" autocomplete="off">
<input type="hidden" name="asset_id" value="<?php echo $asset_id; ?>"> <input type="hidden" name="asset_id" value="<?php echo intval($_GET['asset_id']); ?>">
<div class="modal-body bg-white"> <div class="modal-body bg-white">
<div class="form-group"> <div class="form-group">
+1 -1
View File
@@ -8,7 +8,7 @@
</button> </button>
</div> </div>
<form action="post.php" method="post" autocomplete="off"> <form action="post.php" method="post" autocomplete="off">
<input type="hidden" name="asset_id" value="<?php echo $asset_id; ?>"> <input type="hidden" name="asset_id" value="<?php echo intval($_GET['asset_id']); ?>">
<div class="modal-body bg-white"> <div class="modal-body bg-white">
<div class="form-group"> <div class="form-group">
+1 -1
View File
@@ -8,7 +8,7 @@
</button> </button>
</div> </div>
<form action="post.php" method="post" autocomplete="off"> <form action="post.php" method="post" autocomplete="off">
<input type="hidden" name="asset_id" value="<?php echo $asset_id; ?>"> <input type="hidden" name="asset_id" value="<?php echo intval($_GET['asset_id']); ?>">
<div class="modal-body bg-white"> <div class="modal-body bg-white">
<div class="form-group"> <div class="form-group">
+1 -1
View File
@@ -8,7 +8,7 @@
</button> </button>
</div> </div>
<form action="post.php" method="post" autocomplete="off"> <form action="post.php" method="post" autocomplete="off">
<input type="hidden" name="asset_id" value="<?php echo $asset_id; ?>"> <input type="hidden" name="asset_id" value="<?php echo intval($_GET['asset_id']); ?>">
<div class="modal-body bg-white"> <div class="modal-body bg-white">
<div class="form-group"> <div class="form-group">
+1 -1
View File
@@ -37,7 +37,7 @@
<option value="0">/</option> <option value="0">/</option>
<?php <?php
// Start displaying folder options from the root (parent_folder = 0) // Start displaying folder options from the root (parent_folder = 0)
display_folder_options(0, $client_id); display_folder_options(0, $client_id, 1);
?> ?>
</select> </select>
</div> </div>
+1 -1
View File
@@ -8,7 +8,7 @@
</button> </button>
</div> </div>
<form action="post.php" method="post" autocomplete="off"> <form action="post.php" method="post" autocomplete="off">
<input type="hidden" name="contact_id" value="<?php echo $contact_id; ?>"> <input type="hidden" name="contact_id" value="<?php echo intval($_GET['contact_id']); ?>">
<div class="modal-body bg-white"> <div class="modal-body bg-white">
<div class="form-group"> <div class="form-group">
+1 -1
View File
@@ -8,7 +8,7 @@
</button> </button>
</div> </div>
<form action="post.php" method="post" autocomplete="off"> <form action="post.php" method="post" autocomplete="off">
<input type="hidden" name="contact_id" value="<?php echo $contact_id; ?>"> <input type="hidden" name="contact_id" value="<?php echo intval($_GET['contact_id']); ?>">
<div class="modal-body bg-white"> <div class="modal-body bg-white">
<div class="form-group"> <div class="form-group">
+1 -1
View File
@@ -8,7 +8,7 @@
</button> </button>
</div> </div>
<form action="post.php" method="post" autocomplete="off"> <form action="post.php" method="post" autocomplete="off">
<input type="hidden" name="contact_id" value="<?php echo $contact_id; ?>"> <input type="hidden" name="contact_id" value="<?php echo intval($_GET['contact_id']); ?>">
<div class="modal-body bg-white"> <div class="modal-body bg-white">
<div class="form-group"> <div class="form-group">
+1 -1
View File
@@ -8,7 +8,7 @@
</button> </button>
</div> </div>
<form action="post.php" method="post" autocomplete="off"> <form action="post.php" method="post" autocomplete="off">
<input type="hidden" name="contact_id" value="<?php echo $contact_id; ?>"> <input type="hidden" name="contact_id" value="<?php echo intval($_GET['contact_id']); ?>">
<div class="modal-body bg-white"> <div class="modal-body bg-white">
<div class="form-group"> <div class="form-group">
+1 -1
View File
@@ -8,7 +8,7 @@
</button> </button>
</div> </div>
<form action="post.php" method="post" autocomplete="off"> <form action="post.php" method="post" autocomplete="off">
<input type="hidden" name="contact_id" value="<?php echo $contact_id; ?>"> <input type="hidden" name="contact_id" value="<?php echo intval($_GET['contact_id']); ?>">
<div class="modal-body bg-white"> <div class="modal-body bg-white">
<div class="form-group"> <div class="form-group">
+1 -1
View File
@@ -8,7 +8,7 @@
</button> </button>
</div> </div>
<form action="post.php" method="post" autocomplete="off"> <form action="post.php" method="post" autocomplete="off">
<input type="hidden" name="contact_id" value="<?php echo $contact_id; ?>"> <input type="hidden" name="contact_id" value="<?php echo intval($_GET['contact_id']); ?>">
<div class="modal-body bg-white"> <div class="modal-body bg-white">
<div class="form-group"> <div class="form-group">
File diff suppressed because one or more lines are too long
-1
View File
@@ -1 +0,0 @@
.gu-mirror{position:fixed!important;margin:0!important;z-index:9999!important;opacity:.8}.gu-hide{display:none!important}.gu-unselectable{-webkit-user-select:none!important;-moz-user-select:none!important;-ms-user-select:none!important;user-select:none!important}.gu-transit{opacity:.2}
File diff suppressed because one or more lines are too long
+36 -7
View File
@@ -597,6 +597,15 @@ if (isset($_POST["export_client_pdf"])) {
enforceUserPermission("module_sales", 1); enforceUserPermission("module_sales", 1);
enforceUserPermission("module_financial", 1); enforceUserPermission("module_financial", 1);
$sql = mysqli_query($mysqli, "SELECT * FROM companies, settings WHERE companies.company_id = settings.company_id AND companies.company_id = 1");
$row = mysqli_fetch_array($sql);
$company_name = nullable_htmlentities($row['company_name']);
$company_phone_country_code = nullable_htmlentities($row['company_phone_country_code']);
$company_phone = nullable_htmlentities(formatPhoneNumber($row['company_phone'], $company_phone_country_code));
$company_email = nullable_htmlentities($row['company_email']);
$company_website = nullable_htmlentities($row['company_website']);
$company_logo = nullable_htmlentities($row['company_logo']);
$client_id = intval($_POST["client_id"]); $client_id = intval($_POST["client_id"]);
$export_contacts = intval($_POST["export_contacts"]); $export_contacts = intval($_POST["export_contacts"]);
$export_locations = intval($_POST["export_locations"]); $export_locations = intval($_POST["export_locations"]);
@@ -604,7 +613,7 @@ if (isset($_POST["export_client_pdf"])) {
$export_software = intval($_POST["export_software"]); $export_software = intval($_POST["export_software"]);
$export_credentials = 0; $export_credentials = 0;
if (lookupUserPermission("module_credential") >= 1) { if (lookupUserPermission("module_credential") >= 1) {
$export_credentials = intval($_POST["export_credentials"]); $export_credentials = intval($_POST["export_credentials"] ?? 0);
} }
$export_networks = intval($_POST["export_networks"]); $export_networks = intval($_POST["export_networks"]);
$export_certificates = intval($_POST["export_certificates"]); $export_certificates = intval($_POST["export_certificates"]);
@@ -747,6 +756,10 @@ if (isset($_POST["export_client_pdf"])) {
$pdf->SetAuthor($session_company_name); $pdf->SetAuthor($session_company_name);
$pdf->SetTitle("$client_name - IT Documentation"); $pdf->SetTitle("$client_name - IT Documentation");
// TODO: Add page numbers to footer, but can't work out how to do it without the ugly line
// $pdf->SetFooterMargin(PDF_MARGIN_FOOTER);
// $pdf->setFooterData();
// Enable auto page breaks with a margin from the bottom // Enable auto page breaks with a margin from the bottom
$pdf->SetAutoPageBreak(true, 15); $pdf->SetAutoPageBreak(true, 15);
@@ -779,17 +792,27 @@ if (isset($_POST["export_client_pdf"])) {
"; ";
// Cover page section (for main content, not the TOC) // Cover page section (for main content, not the TOC)
$html .= '<div class="cover">';
if (!empty($company_logo)) {
$pdf->Image('uploads/settings/' . $company_logo, '', '', 35, 35, '', '', 'L', false, 300, '', false, false, 1, false, false, false);
}
$html .= " $html .= "
<div class='cover'> <h1>IT Documentation</h1>
<h1>$client_name</h1> <h2>$client_name</h2>
<h2>IT Documentation</h2> <h4>Prepared by $session_name on " . date("F j, Y") . "</h4>
<p>Export Date: " . date("F j, Y") . "</p>
</div> </div>
<hr>"; ";
$html .= "
<br>
<h4>$session_company_name</h4>
$company_phone<br>$company_email<br>
<hr>
";
// Client header information (non-table) // Client header information (non-table)
$html .= " $html .= "
<div class='client-header'> <div class='client-header'>
<h3>$client_name</h3>
<p><strong>Address:</strong> $location_address</p> <p><strong>Address:</strong> $location_address</p>
<p><strong>City State Zip:</strong> $location_city $location_state $location_zip</p> <p><strong>City State Zip:</strong> $location_city $location_state $location_zip</p>
<p><strong>Phone:</strong> $contact_phone</p> <p><strong>Phone:</strong> $contact_phone</p>
@@ -1203,6 +1226,8 @@ if (isset($_POST["export_client_pdf"])) {
<th>Type</th> <th>Type</th>
<th>License</th> <th>License</th>
<th>License Key</th> <th>License Key</th>
<th>Purchase Date</th>
<th>Expiration Date</th>
<th>Notes</th> <th>Notes</th>
</tr> </tr>
</thead> </thead>
@@ -1212,6 +1237,8 @@ if (isset($_POST["export_client_pdf"])) {
$software_type = nullable_htmlentities($row["software_type"]); $software_type = nullable_htmlentities($row["software_type"]);
$software_license_type = nullable_htmlentities($row["software_license_type"]); $software_license_type = nullable_htmlentities($row["software_license_type"]);
$software_key = nullable_htmlentities($row["software_key"]); $software_key = nullable_htmlentities($row["software_key"]);
$software_purchase = nullable_htmlentities($row['software_purchase']);
$software_expire = nullable_htmlentities($row['software_expire']);
$software_notes = nullable_htmlentities($row["software_notes"]); $software_notes = nullable_htmlentities($row["software_notes"]);
$html .= " $html .= "
<tr style='page-break-inside: avoid;'> <tr style='page-break-inside: avoid;'>
@@ -1219,6 +1246,8 @@ if (isset($_POST["export_client_pdf"])) {
<td>$software_type</td> <td>$software_type</td>
<td>$software_license_type</td> <td>$software_license_type</td>
<td>$software_key</td> <td>$software_key</td>
<td>$software_purchase</td>
<td>$software_expire</td>
<td>$software_notes</td> <td>$software_notes</td>
</tr>"; </tr>";
} }
@@ -1326,7 +1355,7 @@ if (isset($_POST["export_client_pdf"])) {
<thead> <thead>
<tr> <tr>
<th>Domain Name</th> <th>Domain Name</th>
<th>Expire</th> <th>Expiration Date</th>
</tr> </tr>
</thead> </thead>
<tbody>"; <tbody>";
+1 -1
View File
@@ -48,7 +48,7 @@ $sql_projects = mysqli_query(
LEFT JOIN clients ON client_id = project_client_id LEFT JOIN clients ON client_id = project_client_id
LEFT JOIN users ON user_id = project_manager LEFT JOIN users ON user_id = project_manager
WHERE DATE(project_created_at) BETWEEN '$dtf' AND '$dtt' WHERE DATE(project_created_at) BETWEEN '$dtf' AND '$dtt'
AND (project_name LIKE '%$q%' OR project_description LIKE '%$q%' OR user_name LIKE '%$q%') AND (CONCAT(project_prefix,project_number) LIKE '%$q%' OR project_name LIKE '%$q%' OR project_description LIKE '%$q%' OR user_name LIKE '%$q%')
AND project_completed_at $status_query AND project_completed_at $status_query
$project_permission_snippet $project_permission_snippet
AND project_$archive_query AND project_$archive_query
+22 -28
View File
@@ -123,7 +123,6 @@ if (isset($_GET['quote_id'])) {
); );
?> ?>
<link rel="stylesheet" href="plugins/dragula/dragula.min.css">
<ol class="breadcrumb d-print-none"> <ol class="breadcrumb d-print-none">
<?php if (isset($_GET['client_id'])) { ?> <?php if (isset($_GET['client_id'])) { ?>
@@ -326,6 +325,14 @@ if (isset($_GET['quote_id'])) {
<tr data-item-id="<?php echo $item_id; ?>"> <tr data-item-id="<?php echo $item_id; ?>">
<td class="d-print-none"> <td class="d-print-none">
<?php if ($quote_status !== "Invoiced" && $quote_status !== "Accepted" && $quote_status !== "Declined" && lookupUserPermission("module_sales") >= 2) { ?> <?php if ($quote_status !== "Invoiced" && $quote_status !== "Accepted" && $quote_status !== "Declined" && lookupUserPermission("module_sales") >= 2) { ?>
<div class="row">
<div class="col">
<button type="button" class="btn btn-sm btn-light drag-handle">
<i class="fas fa-bars text-muted"></i>
</button>
</div>
<div class="col">
<div class="dropdown"> <div class="dropdown">
<button class="btn btn-sm btn-light" type="button" data-toggle="dropdown"> <button class="btn btn-sm btn-light" type="button" data-toggle="dropdown">
<i class="fas fa-ellipsis-v"></i> <i class="fas fa-ellipsis-v"></i>
@@ -344,9 +351,11 @@ if (isset($_GET['quote_id'])) {
</a> </a>
</div> </div>
</div> </div>
</div>
</div>
<?php } ?> <?php } ?>
</td> </td>
<td class="grab-cursor"><?php echo $item_name; ?></td> <td><?php echo $item_name; ?></td>
<td><?php echo nl2br($item_description); ?></td> <td><?php echo nl2br($item_description); ?></td>
<td class="text-center"><?php echo number_format($item_quantity, 2); ?></td> <td class="text-center"><?php echo number_format($item_quantity, 2); ?></td>
<td class="text-right"><?php echo numfmt_format_currency($currency_format, $item_price, $quote_currency_code); ?></td> <td class="text-right"><?php echo numfmt_format_currency($currency_format, $item_price, $quote_currency_code); ?></td>
@@ -992,38 +1001,23 @@ require_once "includes/footer.php";
} }
</script> </script>
<script src="plugins/dragula/dragula.min.js"></script> <script src="plugins/SortableJS/Sortable.min.js"></script>
<script> <script>
$(document).ready(function() { new Sortable(document.querySelector('table#items tbody'), {
var container = $('table#items tbody')[0]; handle: '.drag-handle',
animation: 150,
dragula([container]) onEnd: function (evt) {
.on('drop', function (el, target, source, sibling) { const rows = document.querySelectorAll('table#items tbody tr');
// Handle the drop event to update the order in the database const positions = Array.from(rows).map((row, index) => ({
var rows = $(container).children(); id: row.dataset.itemId,
var positions = rows.map(function(index, row) {
return {
id: $(row).data('itemId'),
order: index order: index
}; }));
}).get();
// Send the new order to the server $.post('ajax.php', {
$.ajax({
url: 'ajax.php',
method: 'POST',
data: {
update_quote_items_order: true, update_quote_items_order: true,
quote_id: <?php echo $quote_id; ?>, quote_id: <?php echo $quote_id; ?>,
positions: positions positions: positions
}, });
success: function(data) {
// Handle success
},
error: function(error) {
console.error('Error updating order:', error);
} }
}); });
});
});
</script> </script>
+22 -30
View File
@@ -272,6 +272,14 @@ if (isset($_GET['recurring_invoice_id'])) {
<tr data-item-id="<?php echo $item_id; ?>"> <tr data-item-id="<?php echo $item_id; ?>">
<td class="d-print-none"> <td class="d-print-none">
<div class="row">
<div class="col">
<button type="button" class="btn btn-sm btn-light drag-handle">
<i class="fas fa-bars text-muted"></i>
</button>
</div>
<div class="col">
<div class="dropdown"> <div class="dropdown">
<button class="btn btn-sm btn-light" type="button" data-toggle="dropdown"> <button class="btn btn-sm btn-light" type="button" data-toggle="dropdown">
<i class="fas fa-ellipsis-v"></i> <i class="fas fa-ellipsis-v"></i>
@@ -286,12 +294,12 @@ if (isset($_GET['recurring_invoice_id'])) {
</a> </a>
<div class="dropdown-divider"></div> <div class="dropdown-divider"></div>
<a class="dropdown-item text-danger confirm-link" href="post.php?delete_recurring_invoice_item=<?php echo $item_id; ?>"><i class="fa fa-fw fa-trash mr-2"></i>Delete</a> <a class="dropdown-item text-danger confirm-link" href="post.php?delete_recurring_invoice_item=<?php echo $item_id; ?>"><i class="fa fa-fw fa-trash mr-2"></i>Delete</a>
</div>
</div>
</div> </div>
</div> </div>
</td> </td>
<td class="grab-cursor"><?php echo $item_name; ?></td> <td><?php echo $item_name; ?></td>
<td><?php echo nl2br($item_description); ?></td> <td><?php echo nl2br($item_description); ?></td>
<td class="text-center"><?php echo $item_quantity; ?></td> <td class="text-center"><?php echo $item_quantity; ?></td>
<td class="text-right"><?php echo numfmt_format_currency($currency_format, $item_price, $recurring_invoice_currency_code); ?></td> <td class="text-right"><?php echo numfmt_format_currency($currency_format, $item_price, $recurring_invoice_currency_code); ?></td>
@@ -483,39 +491,23 @@ require_once "includes/footer.php";
}); });
</script> </script>
<link rel="stylesheet" href="plugins/dragula/dragula.min.css"> <script src="plugins/SortableJS/Sortable.min.js"></script>
<script src="plugins/dragula/dragula.min.js"></script>
<script> <script>
$(document).ready(function() { new Sortable(document.querySelector('table#items tbody'), {
var container = $('table#items tbody')[0]; handle: '.drag-handle',
animation: 150,
dragula([container]) onEnd: function (evt) {
.on('drop', function (el, target, source, sibling) { const rows = document.querySelectorAll('table#items tbody tr');
// Handle the drop event to update the order in the database const positions = Array.from(rows).map((row, index) => ({
var rows = $(container).children(); id: row.dataset.itemId,
var positions = rows.map(function(index, row) {
return {
id: $(row).data('itemId'),
order: index order: index
}; }));
}).get();
// Send the new order to the server $.post('ajax.php', {
$.ajax({
url: 'ajax.php',
method: 'POST',
data: {
update_recurring_invoice_items_order: true, update_recurring_invoice_items_order: true,
recurring_invoice_id: <?php echo $recurring_invoice_id; ?>, recurring_invoice_id: <?php echo $recurring_invoice_id; ?>,
positions: positions positions: positions
}, });
success: function(data) {
// Handle success
},
error: function(error) {
console.error('Error updating order:', error);
} }
}); });
});
});
</script> </script>
+15 -33
View File
@@ -341,7 +341,6 @@ if (isset($_GET['ticket_id'])) {
$ticket_collaborators = nullable_htmlentities($row['user_names']); $ticket_collaborators = nullable_htmlentities($row['user_names']);
?> ?>
<link rel="stylesheet" href="plugins/dragula/dragula.min.css">
<!-- Breadcrumbs--> <!-- Breadcrumbs-->
<ol class="breadcrumb d-print-none"> <ol class="breadcrumb d-print-none">
@@ -940,7 +939,7 @@ if (isset($_GET['ticket_id'])) {
</form> </form>
<?php } ?> <?php } ?>
<table class="table table-sm"> <table class="table table-sm" id="tasks">
<?php <?php
while($row = mysqli_fetch_array($sql_tasks)){ while($row = mysqli_fetch_array($sql_tasks)){
$task_id = intval($row['task_id']); $task_id = intval($row['task_id']);
@@ -960,14 +959,14 @@ if (isset($_GET['ticket_id'])) {
<?php } ?> <?php } ?>
</td> </td>
<td> <td>
<a href="#" class="grab-cursor"> <a href="#" class="drag-handle"><i class="fas fa-bars text-muted mr-1"></i></a>
<span class="text-secondary"><?php echo $task_completion_estimate; ?>m</span> <span class="text-secondary"><?php echo $task_completion_estimate; ?>m</span>
<span class="text-dark"> - <?php echo $task_name; ?></span> <span class="text-dark"> - <?php echo $task_name; ?></span>
</a>
</td> </td>
<td> <td>
<div class="float-right"> <div class="float-right">
<?php if (empty($ticket_resolved_at) && lookupUserPermission("module_support") >= 2) { ?> <?php if (empty($ticket_resolved_at) && lookupUserPermission("module_support") >= 2) { ?>
<div class="dropdown dropleft text-center"> <div class="dropdown dropleft text-center">
<button class="btn btn-link text-secondary btn-sm" type="button" data-toggle="dropdown"> <button class="btn btn-link text-secondary btn-sm" type="button" data-toggle="dropdown">
<i class="fas fa-fw fa-ellipsis-v"></i> <i class="fas fa-fw fa-ellipsis-v"></i>
@@ -991,6 +990,7 @@ if (isset($_GET['ticket_id'])) {
</a> </a>
</div> </div>
</div> </div>
<?php } ?> <?php } ?>
</div> </div>
</td> </td>
@@ -1207,41 +1207,23 @@ require_once "includes/footer.php";
}); });
</script> </script>
<script src="plugins/SortableJS/Sortable.min.js"></script>
<script src="plugins/dragula/dragula.min.js"></script>
<script> <script>
$(document).ready(function() { new Sortable(document.querySelector('table#tasks tbody'), {
var container = $('.table tbody')[0]; handle: '.drag-handle',
animation: 150,
dragula([container]) onEnd: function (evt) {
.on('drop', function (el, target, source, sibling) { const rows = document.querySelectorAll('table#tasks tbody tr');
// Handle the drop event to update the order in the database const positions = Array.from(rows).map((row, index) => ({
var rows = $(container).children(); id: row.dataset.taskId,
var positions = rows.map(function(index, row) {
return {
id: $(row).data('taskId'),
order: index order: index
}; }));
}).get();
//console.log('New positions:', positions); $.post('ajax.php', {
// Send the new order to the server (example using fetch)
$.ajax({
url: 'ajax.php',
method: 'POST',
data: {
update_ticket_tasks_order: true, update_ticket_tasks_order: true,
ticket_id: <?php echo $ticket_id; ?>, ticket_id: <?php echo $ticket_id; ?>,
positions: positions positions: positions
}, });
success: function(data) {
//console.log('Order updated:', data);
},
error: function(error) {
console.error('Error updating order:', error);
} }
}); });
});
});
</script> </script>
+3 -4
View File
@@ -1,4 +1,3 @@
<link rel="stylesheet" href="plugins/dragula/dragula.min.css">
<link rel="stylesheet" href="css/tickets_kanban.css"> <link rel="stylesheet" href="css/tickets_kanban.css">
<?php <?php
@@ -82,7 +81,7 @@ $kanban = array_values($statuses);
?> ?>
<div class="kanban-column card card-dark" data-status-id="<?=htmlspecialchars($kanban_column->id); ?>"> <div class="kanban-column card card-dark" data-status-id="<?=htmlspecialchars($kanban_column->id); ?>">
<h6 class="panel-title"><?=htmlspecialchars($kanban_column->name); ?></h6> <h6 class="panel-title"><?=htmlspecialchars($kanban_column->name); ?></h6>
<div id="status" data-column-name="<?=$kanban_column->name?>" data-status-id="<?=htmlspecialchars($kanban_column->id); ?>" style="height: 100%;" > <div class="kanban-status" data-column-name="<?=$kanban_column->name?>" data-status-id="<?=htmlspecialchars($kanban_column->id); ?>">
<?php <?php
foreach($kanban_column->tickets as $item){ foreach($kanban_column->tickets as $item){
if ($item['ticket_priority'] == "High") { if ($item['ticket_priority'] == "High") {
@@ -106,7 +105,7 @@ $kanban = array_values($statuses);
<span class='badge badge-secondary'> <span class='badge badge-secondary'>
<?php echo $item['category_name']; ?> <?php echo $item['category_name']; ?>
</span> </span>
<div class='btn btn-secondary drag-handle-class' style="display: none;"> <div class='btn btn-light drag-handle-class' style="display: none;">
<i class="drag-handle-class fas fa-bars"></i> <i class="drag-handle-class fas fa-bars"></i>
</div> </div>
<br> <br>
@@ -154,5 +153,5 @@ echo "const CONFIG_TICKET_MOVING_COLUMNS = " . json_encode($config_ticket_movi
echo "const CONFIG_TICKET_ORDERING = " . json_encode($config_ticket_ordering) . ";"; echo "const CONFIG_TICKET_ORDERING = " . json_encode($config_ticket_ordering) . ";";
echo "</script>"; echo "</script>";
?> ?>
<script src="plugins/dragula/dragula.min.js"></script> <script src="plugins/SortableJS/Sortable.min.js"></script>
<script src="js/tickets_kanban.js"></script> <script src="js/tickets_kanban.js"></script>