Merge branch 'master' into techbar
merge conflict on filter_header but i like my implementation better
This commit is contained in:
21
CHANGELOG.md
21
CHANGELOG.md
@@ -2,6 +2,27 @@
|
||||
|
||||
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]
|
||||
|
||||
### Fixed
|
||||
|
||||
@@ -93,6 +93,7 @@ If you want to improve ITFlow, feel free to fork the repo and create a pull requ
|
||||
We’re incredibly grateful to the organizations and individuals who support the project - a big thank you to:
|
||||
- CompuMatter
|
||||
- F1 for HELP
|
||||
- JetBrains (PhpStorm)
|
||||
|
||||
## 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.
|
||||
|
||||
@@ -73,6 +73,31 @@ require_once "includes/inc_all_admin.php";
|
||||
</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>
|
||||
|
||||
<button type="submit" name="edit_ticket_settings" class="btn btn-primary text-bold"><i class="fas fa-check mr-2"></i>Save</button>
|
||||
|
||||
@@ -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");
|
||||
|
||||
?>
|
||||
<link rel="stylesheet" href="plugins/dragula/dragula.min.css">
|
||||
|
||||
<ol class="breadcrumb d-print-none">
|
||||
<li class="breadcrumb-item">
|
||||
@@ -94,7 +93,7 @@ $sql_task_templates = mysqli_query($mysqli, "SELECT * FROM task_templates WHERE
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<table class="table table-striped table-sm">
|
||||
<table class="table table-sm" id="tasks">
|
||||
<?php
|
||||
while($row = mysqli_fetch_array($sql_task_templates)){
|
||||
$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']);
|
||||
?>
|
||||
<tr data-task-id="<?php echo $task_id; ?>">
|
||||
<td><i class="far fa-fw fa-square text-secondary"></i></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-dark"> - <?php echo $task_name; ?></span>
|
||||
</a>
|
||||
</td>
|
||||
<td class="text-right">
|
||||
<div class="float-right">
|
||||
@@ -145,40 +142,26 @@ $sql_task_templates = mysqli_query($mysqli, "SELECT * FROM task_templates WHERE
|
||||
</div>
|
||||
|
||||
<script src="js/pretty_content.js"></script>
|
||||
<script src="plugins/dragula/dragula.min.js"></script>
|
||||
|
||||
<script src="plugins/SortableJS/Sortable.min.js"></script>
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
var container = $('.table tbody')[0];
|
||||
|
||||
dragula([container])
|
||||
.on('drop', function (el, target, source, sibling) {
|
||||
// Handle the drop event to update the order in the database
|
||||
var rows = $(container).children();
|
||||
var positions = rows.map(function(index, row) {
|
||||
return {
|
||||
id: $(row).data('taskId'),
|
||||
new Sortable(document.querySelector('table#tasks tbody'), {
|
||||
handle: '.drag-handle',
|
||||
animation: 150,
|
||||
onEnd: function (evt) {
|
||||
const rows = document.querySelectorAll('table#tasks tbody tr');
|
||||
const positions = Array.from(rows).map((row, index) => ({
|
||||
id: row.dataset.taskId,
|
||||
order: index
|
||||
};
|
||||
}).get();
|
||||
}));
|
||||
|
||||
// Send the new order to the server
|
||||
$.ajax({
|
||||
url: 'ajax.php',
|
||||
method: 'POST',
|
||||
data: {
|
||||
update_task_templates_order: true, // Adjust the parameter name if needed
|
||||
$.post('ajax.php', {
|
||||
update_task_templates_order: true,
|
||||
ticket_template_id: <?php echo $ticket_template_id; ?>,
|
||||
positions: positions
|
||||
},
|
||||
success: function(data) {
|
||||
// Handle success
|
||||
},
|
||||
error: function(error) {
|
||||
console.error('Error updating order:', error);
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<?php
|
||||
|
||||
@@ -32,8 +32,11 @@ $git_log = shell_exec("git log $repo_branch..origin/$repo_branch --pretty=format
|
||||
<?php } ?>
|
||||
|
||||
<?php if (LATEST_DATABASE_VERSION > CURRENT_DATABASE_VERSION) { ?>
|
||||
<div class="alert alert-warning">
|
||||
<strong>Ensure you have a current <a href="https://docs.itflow.org/backups">app & database backup</a> before updating!</strong>
|
||||
<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>
|
||||
<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>
|
||||
@@ -46,9 +49,17 @@ $git_log = shell_exec("git log $repo_branch..origin/$repo_branch --pretty=format
|
||||
|
||||
<?php } else {
|
||||
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-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?no"><i class="fas fa-fw fa-4x fa-download mb-1"></i><h5>TEST</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 { ?>
|
||||
<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-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>
|
||||
|
||||
<?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">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<?php } ?>
|
||||
|
||||
<?php }
|
||||
}
|
||||
|
||||
|
||||
4
ajax.php
4
ajax.php
@@ -586,13 +586,13 @@ if (isset($_POST['update_recurring_invoice_items_order'])) {
|
||||
enforceUserPermission('module_sales', 2);
|
||||
|
||||
$positions = $_POST['positions'];
|
||||
$recurring_id = intval($_POST['recurring_id']);
|
||||
$recurring_invoice_id = intval($_POST['recurring_invoice_id']);
|
||||
|
||||
foreach ($positions as $position) {
|
||||
$id = intval($position['id']);
|
||||
$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
|
||||
|
||||
@@ -115,7 +115,7 @@ ob_start();
|
||||
|
||||
$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
|
||||
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)) {
|
||||
$user_id_select = intval($row['user_id']);
|
||||
|
||||
@@ -244,7 +244,7 @@ if (isset($_GET['asset_id'])) {
|
||||
data-ajax-id="<?php echo $asset_id; ?>">
|
||||
<i class="fas fa-fw fa-edit"></i>
|
||||
</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) { ?>
|
||||
<img class="img-fluid img-circle p-3" alt="asset_photo" src="<?php echo "uploads/clients/$client_id/$asset_photo"; ?>">
|
||||
<?php } ?>
|
||||
@@ -405,7 +405,7 @@ if (isset($_GET['asset_id'])) {
|
||||
|
||||
<div class="card card-dark">
|
||||
<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="btn-group">
|
||||
<button type="button" class="btn btn-primary" data-toggle="modal" data-target="#addAssetInterfaceModal">
|
||||
|
||||
@@ -550,11 +550,7 @@ if (mysqli_num_rows($os_sql) > 0) {
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<a class="text-dark" href="#"
|
||||
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; ?>">
|
||||
<a class="text-dark" href="asset_details.php?client_id=<?php echo $client_id; ?>&asset_id=<?php echo $asset_id; ?>">
|
||||
<div class="media">
|
||||
<i class="fa fa-fw fa-2x fa-<?php echo $device_icon; ?> mr-3 mt-1"></i>
|
||||
<div class="media-body">
|
||||
|
||||
@@ -319,7 +319,7 @@ $num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()"));
|
||||
} 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_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));
|
||||
if (empty($contact_mobile)) {
|
||||
$contact_mobile_display = "";
|
||||
@@ -445,11 +445,7 @@ $num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()"));
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<a class="text-dark" href="#"
|
||||
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; ?>">
|
||||
<a class="text-dark" href="contact_details.php?client_id=<?php echo $client_id; ?>&contact_id=<?php echo $contact_id; ?>">
|
||||
<div class="media">
|
||||
<?php if ($contact_photo) { ?>
|
||||
<span class="fa-stack fa-2x mr-3 text-center">
|
||||
|
||||
@@ -20,10 +20,11 @@
|
||||
}
|
||||
}
|
||||
|
||||
.grab-cursor {
|
||||
.drag-handle {
|
||||
cursor: grab;
|
||||
touch-action: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.grab-cursor:active {
|
||||
.drag-handle:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
@@ -1,41 +1,83 @@
|
||||
/* General Popover Styling */
|
||||
.popover {
|
||||
max-width: 600px;
|
||||
}
|
||||
|
||||
/* Kanban Board Container */
|
||||
#kanban-board {
|
||||
display: flex;
|
||||
box-sizing: border-box;
|
||||
overflow-x: auto;
|
||||
box-sizing: border-box;
|
||||
min-width: 400px;
|
||||
height: calc(100vh - 210px);
|
||||
}
|
||||
|
||||
/* Kanban Column */
|
||||
.kanban-column {
|
||||
flex: 1; /* Allows columns to grow equally */
|
||||
margin: 0 10px; /* Space between columns */
|
||||
flex: 1;
|
||||
min-width: 300px;
|
||||
max-width: 300px;
|
||||
margin: 0 10px;
|
||||
background: #f4f4f4;
|
||||
padding: 10px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
padding: 10px;
|
||||
min-height: calc(100vh - 230px);
|
||||
max-height: calc(100vh - 230px);
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.kanban-column div {
|
||||
max-height: calc(100vh - 280px); /* Set your desired max height */
|
||||
overflow-y: auto; /* Adds a scrollbar when content exceeds max height */
|
||||
/* Column Inner Scrollable Task Area */
|
||||
.kanban-status {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
min-height: 60px;
|
||||
position: relative;
|
||||
padding: 5px;
|
||||
background-color: #f9f9f9;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
/* Individual Task Cards */
|
||||
.task {
|
||||
background: #fff;
|
||||
margin: 5px 0;
|
||||
padding: 10px;
|
||||
border: 1px solid #ddd;
|
||||
user-select: none; /* Prevent text selection */
|
||||
border-radius: 4px;
|
||||
cursor: grab;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.drag-handle-class {
|
||||
touch-action: none;
|
||||
float: right;
|
||||
/* Grabbing Cursor State */
|
||||
.task:active {
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -151,7 +151,7 @@ if ($user_config_dashboard_financial_enable == 1) {
|
||||
$row = mysqli_fetch_array($sql_unbilled_tickets);
|
||||
$unbilled_tickets = intval($row['unbilled_tickets']);
|
||||
} 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']);
|
||||
}
|
||||
|
||||
|
||||
@@ -1622,10 +1622,14 @@ function getFieldById($table, $id, $field, $escape_method = 'sql') {
|
||||
}
|
||||
|
||||
// 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;
|
||||
|
||||
$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)) {
|
||||
$folder_id = intval($row['folder_id']);
|
||||
$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
|
||||
$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';
|
||||
}
|
||||
|
||||
echo "<option value=\"$folder_id\" $selected>$indentation$folder_name</option>";
|
||||
|
||||
// Recursively display subfolders
|
||||
display_folder_options($folder_id, $client_id, $indent + 1);
|
||||
display_folder_options($folder_id, $client_id, $folder_location, $indent + 1);
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
DEFINE("APP_VERSION", "25.03.4");
|
||||
DEFINE("APP_VERSION", "25.03.6");
|
||||
|
||||
@@ -16,6 +16,10 @@ if (file_exists("config.php")) {
|
||||
<hr>
|
||||
<?php
|
||||
|
||||
if (isset($config_start_page)) { ?>
|
||||
<meta http-equiv="refresh" content="0;url=<?php echo $config_start_page; ?>">
|
||||
<?php }
|
||||
|
||||
require_once "includes/footer.php";
|
||||
|
||||
|
||||
|
||||
50
invoice.php
50
invoice.php
@@ -165,7 +165,6 @@ if (isset($_GET['invoice_id'])) {
|
||||
|
||||
|
||||
?>
|
||||
<link rel="stylesheet" href="plugins/dragula/dragula.min.css">
|
||||
|
||||
<ol class="breadcrumb d-print-none">
|
||||
<?php if (isset($_GET['client_id'])) { ?>
|
||||
@@ -381,6 +380,13 @@ if (isset($_GET['invoice_id'])) {
|
||||
<tr data-item-id="<?php echo $item_id; ?>">
|
||||
<td class="d-print-none">
|
||||
<?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">
|
||||
<button class="btn btn-sm btn-light" type="button" data-toggle="dropdown">
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<?php } ?>
|
||||
</td>
|
||||
<td class="grab-cursor"><?php echo $item_name; ?></td>
|
||||
<td><?php echo $item_name; ?></td>
|
||||
<td><?php echo nl2br($item_description); ?></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>
|
||||
@@ -1178,38 +1185,23 @@ require_once "includes/footer.php";
|
||||
}
|
||||
</script>
|
||||
|
||||
<script src="plugins/dragula/dragula.min.js"></script>
|
||||
<script src="plugins/SortableJS/Sortable.min.js"></script>
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
var container = $('table#items tbody')[0];
|
||||
|
||||
dragula([container])
|
||||
.on('drop', function (el, target, source, sibling) {
|
||||
// Handle the drop event to update the order in the database
|
||||
var rows = $(container).children();
|
||||
var positions = rows.map(function(index, row) {
|
||||
return {
|
||||
id: $(row).data('itemId'),
|
||||
new Sortable(document.querySelector('table#items tbody'), {
|
||||
handle: '.drag-handle',
|
||||
animation: 150,
|
||||
onEnd: function (evt) {
|
||||
const rows = document.querySelectorAll('table#items tbody tr');
|
||||
const positions = Array.from(rows).map((row, index) => ({
|
||||
id: row.dataset.itemId,
|
||||
order: index
|
||||
};
|
||||
}).get();
|
||||
}));
|
||||
|
||||
// Send the new order to the server
|
||||
$.ajax({
|
||||
url: 'ajax.php',
|
||||
method: 'POST',
|
||||
data: {
|
||||
$.post('ajax.php', {
|
||||
update_invoice_items_order: true,
|
||||
invoice_id: <?php echo $invoice_id; ?>,
|
||||
positions: positions
|
||||
},
|
||||
success: function(data) {
|
||||
// Handle success
|
||||
},
|
||||
error: function(error) {
|
||||
console.error('Error updating order:', error);
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -1,146 +1,126 @@
|
||||
$(document).ready(function () {
|
||||
console.log('CONFIG_TICKET_MOVING_COLUMNS: ' + CONFIG_TICKET_MOVING_COLUMNS);
|
||||
console.log('CONFIG_TICKET_ORDERING: ' + CONFIG_TICKET_ORDERING);
|
||||
console.log('CONFIG_TICKET_MOVING_COLUMNS:', CONFIG_TICKET_MOVING_COLUMNS);
|
||||
console.log('CONFIG_TICKET_ORDERING:', CONFIG_TICKET_ORDERING);
|
||||
|
||||
// Function to detect touch devices
|
||||
function isTouchDevice() {
|
||||
return 'ontouchstart' in window || navigator.maxTouchPoints;
|
||||
}
|
||||
|
||||
// Initialize Dragula for the Kanban board
|
||||
let boardDrake = dragula([
|
||||
document.querySelector('#kanban-board')
|
||||
], {
|
||||
moves: function(el, container, handle) {
|
||||
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,
|
||||
// -------------------------------
|
||||
// Drag: Kanban Columns (Statuses)
|
||||
// -------------------------------
|
||||
new Sortable(document.querySelector('#kanban-board'), {
|
||||
animation: 150,
|
||||
handle: '.panel-title',
|
||||
draggable: '.kanban-column',
|
||||
onEnd: function () {
|
||||
const columnPositions = Array.from(document.querySelectorAll('#kanban-board .kanban-column')).map((col, index) => ({
|
||||
status_id: $(col).data('status-id'),
|
||||
status_kanban: index
|
||||
});
|
||||
});
|
||||
}));
|
||||
|
||||
// Send AJAX request to update all column positions
|
||||
$.ajax({
|
||||
url: 'ajax.php',
|
||||
type: 'POST',
|
||||
data: {
|
||||
if (CONFIG_TICKET_MOVING_COLUMNS === 1) {
|
||||
$.post('ajax.php', {
|
||||
update_kanban_status_position: true,
|
||||
positions: columnPositions
|
||||
},
|
||||
success: function(response) {
|
||||
console.log('Ticket status kanban orders updated successfully.');
|
||||
// Optionally, you can refresh the page or update the UI here
|
||||
},
|
||||
error: function(xhr, status, error) {
|
||||
console.error('Error updating ticket status kanban orders:', error);
|
||||
}
|
||||
}).done(() => {
|
||||
console.log('Ticket status kanban orders updated.');
|
||||
}).fail((xhr) => {
|
||||
console.error('Error updating status order:', xhr.responseText);
|
||||
});
|
||||
});
|
||||
|
||||
// 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');
|
||||
moveList.forEach(task => {
|
||||
task.querySelector('.drag-handle-class').style.display = 'inline';
|
||||
});
|
||||
}
|
||||
// -------------------------------
|
||||
// Drag: Tasks within Columns
|
||||
// -------------------------------
|
||||
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) {
|
||||
el.style.cursor = 'grabbing';
|
||||
});
|
||||
|
||||
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
|
||||
// Disallow reordering in same column if config says so
|
||||
if (CONFIG_TICKET_ORDERING === 0 && evt.from === evt.to) {
|
||||
evt.from.insertBefore(movedEl, evt.from.children[evt.oldIndex]);
|
||||
showPlaceholders();
|
||||
return;
|
||||
}
|
||||
|
||||
// Get all cards in the target column and their positions
|
||||
let cards = $(target).children('.task');
|
||||
let positions = [];
|
||||
const columnId = $(target).data('status-id');
|
||||
|
||||
//id of current status / column
|
||||
let columnId = $(target).data('status-id');
|
||||
const positions = Array.from(target.querySelectorAll('.task')).map((card, index) => {
|
||||
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');
|
||||
let movedTicketStatusId = $(el).data('ticket-status-id');
|
||||
$(card).data('ticket-status-id', columnId); // update DOM
|
||||
|
||||
cards.each(function(index, card) {
|
||||
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({
|
||||
return {
|
||||
ticket_id: ticketId,
|
||||
ticket_order: index,
|
||||
ticket_oldStatus: oldStatus,
|
||||
ticket_status: statusId ?? null// Get the new status ID from the target column
|
||||
});
|
||||
ticket_status: columnId
|
||||
};
|
||||
});
|
||||
|
||||
//console.log(positions);
|
||||
// Send AJAX request to update all ticket kanban orders and statuses
|
||||
$.ajax({
|
||||
url: 'ajax.php',
|
||||
type: 'POST',
|
||||
data: {
|
||||
$.post('ajax.php', {
|
||||
update_kanban_ticket: true,
|
||||
positions: positions
|
||||
},
|
||||
success: function(response) {
|
||||
//console.log('Ticket kanban orders and statuses updated successfully.');
|
||||
},
|
||||
error: function(xhr, status, error) {
|
||||
console.error('Error updating ticket kanban orders and statuses:', error);
|
||||
}).done(() => {
|
||||
console.log('Updated kanban ticket positions.');
|
||||
}).fail((xhr) => {
|
||||
console.error('Error updating ticket positions:', xhr.responseText);
|
||||
});
|
||||
|
||||
// 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;
|
||||
}
|
||||
});
|
||||
@@ -8,7 +8,7 @@
|
||||
</button>
|
||||
</div>
|
||||
<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="form-group">
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
</button>
|
||||
</div>
|
||||
<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="form-group">
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
</button>
|
||||
</div>
|
||||
<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="form-group">
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
</button>
|
||||
</div>
|
||||
<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="form-group">
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
</button>
|
||||
</div>
|
||||
<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="form-group">
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
<option value="0">/</option>
|
||||
<?php
|
||||
// Start displaying folder options from the root (parent_folder = 0)
|
||||
display_folder_options(0, $client_id);
|
||||
display_folder_options(0, $client_id, 1);
|
||||
?>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
</button>
|
||||
</div>
|
||||
<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="form-group">
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
</button>
|
||||
</div>
|
||||
<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="form-group">
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
</button>
|
||||
</div>
|
||||
<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="form-group">
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
</button>
|
||||
</div>
|
||||
<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="form-group">
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
</button>
|
||||
</div>
|
||||
<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="form-group">
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
</button>
|
||||
</div>
|
||||
<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="form-group">
|
||||
|
||||
2
plugins/SortableJS/Sortable.min.js
vendored
Normal file
2
plugins/SortableJS/Sortable.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
plugins/dragula/dragula.min.css
vendored
1
plugins/dragula/dragula.min.css
vendored
@@ -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}
|
||||
1
plugins/dragula/dragula.min.js
vendored
1
plugins/dragula/dragula.min.js
vendored
File diff suppressed because one or more lines are too long
@@ -597,6 +597,15 @@ if (isset($_POST["export_client_pdf"])) {
|
||||
enforceUserPermission("module_sales", 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"]);
|
||||
$export_contacts = intval($_POST["export_contacts"]);
|
||||
$export_locations = intval($_POST["export_locations"]);
|
||||
@@ -604,7 +613,7 @@ if (isset($_POST["export_client_pdf"])) {
|
||||
$export_software = intval($_POST["export_software"]);
|
||||
$export_credentials = 0;
|
||||
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_certificates = intval($_POST["export_certificates"]);
|
||||
@@ -747,6 +756,10 @@ if (isset($_POST["export_client_pdf"])) {
|
||||
$pdf->SetAuthor($session_company_name);
|
||||
$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
|
||||
$pdf->SetAutoPageBreak(true, 15);
|
||||
|
||||
@@ -779,17 +792,27 @@ if (isset($_POST["export_client_pdf"])) {
|
||||
";
|
||||
|
||||
// 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 .= "
|
||||
<div class='cover'>
|
||||
<h1>$client_name</h1>
|
||||
<h2>IT Documentation</h2>
|
||||
<p>Export Date: " . date("F j, Y") . "</p>
|
||||
<h1>IT Documentation</h1>
|
||||
<h2>$client_name</h2>
|
||||
<h4>Prepared by $session_name on " . date("F j, Y") . "</h4>
|
||||
</div>
|
||||
<hr>";
|
||||
";
|
||||
$html .= "
|
||||
<br>
|
||||
<h4>$session_company_name</h4>
|
||||
$company_phone<br>$company_email<br>
|
||||
<hr>
|
||||
";
|
||||
|
||||
// Client header information (non-table)
|
||||
$html .= "
|
||||
<div class='client-header'>
|
||||
<h3>$client_name</h3>
|
||||
<p><strong>Address:</strong> $location_address</p>
|
||||
<p><strong>City State Zip:</strong> $location_city $location_state $location_zip</p>
|
||||
<p><strong>Phone:</strong> $contact_phone</p>
|
||||
@@ -1203,6 +1226,8 @@ if (isset($_POST["export_client_pdf"])) {
|
||||
<th>Type</th>
|
||||
<th>License</th>
|
||||
<th>License Key</th>
|
||||
<th>Purchase Date</th>
|
||||
<th>Expiration Date</th>
|
||||
<th>Notes</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -1212,6 +1237,8 @@ if (isset($_POST["export_client_pdf"])) {
|
||||
$software_type = nullable_htmlentities($row["software_type"]);
|
||||
$software_license_type = nullable_htmlentities($row["software_license_type"]);
|
||||
$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"]);
|
||||
$html .= "
|
||||
<tr style='page-break-inside: avoid;'>
|
||||
@@ -1219,6 +1246,8 @@ if (isset($_POST["export_client_pdf"])) {
|
||||
<td>$software_type</td>
|
||||
<td>$software_license_type</td>
|
||||
<td>$software_key</td>
|
||||
<td>$software_purchase</td>
|
||||
<td>$software_expire</td>
|
||||
<td>$software_notes</td>
|
||||
</tr>";
|
||||
}
|
||||
@@ -1326,7 +1355,7 @@ if (isset($_POST["export_client_pdf"])) {
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Domain Name</th>
|
||||
<th>Expire</th>
|
||||
<th>Expiration Date</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>";
|
||||
|
||||
@@ -48,7 +48,7 @@ $sql_projects = mysqli_query(
|
||||
LEFT JOIN clients ON client_id = project_client_id
|
||||
LEFT JOIN users ON user_id = project_manager
|
||||
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
|
||||
$project_permission_snippet
|
||||
AND project_$archive_query
|
||||
|
||||
50
quote.php
50
quote.php
@@ -123,7 +123,6 @@ if (isset($_GET['quote_id'])) {
|
||||
);
|
||||
|
||||
?>
|
||||
<link rel="stylesheet" href="plugins/dragula/dragula.min.css">
|
||||
|
||||
<ol class="breadcrumb d-print-none">
|
||||
<?php if (isset($_GET['client_id'])) { ?>
|
||||
@@ -326,6 +325,14 @@ if (isset($_GET['quote_id'])) {
|
||||
<tr data-item-id="<?php echo $item_id; ?>">
|
||||
<td class="d-print-none">
|
||||
<?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">
|
||||
<button class="btn btn-sm btn-light" type="button" data-toggle="dropdown">
|
||||
<i class="fas fa-ellipsis-v"></i>
|
||||
@@ -344,9 +351,11 @@ if (isset($_GET['quote_id'])) {
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php } ?>
|
||||
</td>
|
||||
<td class="grab-cursor"><?php echo $item_name; ?></td>
|
||||
<td><?php echo $item_name; ?></td>
|
||||
<td><?php echo nl2br($item_description); ?></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>
|
||||
@@ -992,38 +1001,23 @@ require_once "includes/footer.php";
|
||||
}
|
||||
</script>
|
||||
|
||||
<script src="plugins/dragula/dragula.min.js"></script>
|
||||
<script src="plugins/SortableJS/Sortable.min.js"></script>
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
var container = $('table#items tbody')[0];
|
||||
|
||||
dragula([container])
|
||||
.on('drop', function (el, target, source, sibling) {
|
||||
// Handle the drop event to update the order in the database
|
||||
var rows = $(container).children();
|
||||
var positions = rows.map(function(index, row) {
|
||||
return {
|
||||
id: $(row).data('itemId'),
|
||||
new Sortable(document.querySelector('table#items tbody'), {
|
||||
handle: '.drag-handle',
|
||||
animation: 150,
|
||||
onEnd: function (evt) {
|
||||
const rows = document.querySelectorAll('table#items tbody tr');
|
||||
const positions = Array.from(rows).map((row, index) => ({
|
||||
id: row.dataset.itemId,
|
||||
order: index
|
||||
};
|
||||
}).get();
|
||||
}));
|
||||
|
||||
// Send the new order to the server
|
||||
$.ajax({
|
||||
url: 'ajax.php',
|
||||
method: 'POST',
|
||||
data: {
|
||||
$.post('ajax.php', {
|
||||
update_quote_items_order: true,
|
||||
quote_id: <?php echo $quote_id; ?>,
|
||||
positions: positions
|
||||
},
|
||||
success: function(data) {
|
||||
// Handle success
|
||||
},
|
||||
error: function(error) {
|
||||
console.error('Error updating order:', error);
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -272,6 +272,14 @@ if (isset($_GET['recurring_invoice_id'])) {
|
||||
|
||||
<tr data-item-id="<?php echo $item_id; ?>">
|
||||
<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">
|
||||
<button class="btn btn-sm btn-light" type="button" data-toggle="dropdown">
|
||||
<i class="fas fa-ellipsis-v"></i>
|
||||
@@ -286,12 +294,12 @@ if (isset($_GET['recurring_invoice_id'])) {
|
||||
</a>
|
||||
<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>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="grab-cursor"><?php echo $item_name; ?></td>
|
||||
<td><?php echo $item_name; ?></td>
|
||||
<td><?php echo nl2br($item_description); ?></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>
|
||||
@@ -483,39 +491,23 @@ require_once "includes/footer.php";
|
||||
});
|
||||
</script>
|
||||
|
||||
<link rel="stylesheet" href="plugins/dragula/dragula.min.css">
|
||||
<script src="plugins/dragula/dragula.min.js"></script>
|
||||
<script src="plugins/SortableJS/Sortable.min.js"></script>
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
var container = $('table#items tbody')[0];
|
||||
|
||||
dragula([container])
|
||||
.on('drop', function (el, target, source, sibling) {
|
||||
// Handle the drop event to update the order in the database
|
||||
var rows = $(container).children();
|
||||
var positions = rows.map(function(index, row) {
|
||||
return {
|
||||
id: $(row).data('itemId'),
|
||||
new Sortable(document.querySelector('table#items tbody'), {
|
||||
handle: '.drag-handle',
|
||||
animation: 150,
|
||||
onEnd: function (evt) {
|
||||
const rows = document.querySelectorAll('table#items tbody tr');
|
||||
const positions = Array.from(rows).map((row, index) => ({
|
||||
id: row.dataset.itemId,
|
||||
order: index
|
||||
};
|
||||
}).get();
|
||||
}));
|
||||
|
||||
// Send the new order to the server
|
||||
$.ajax({
|
||||
url: 'ajax.php',
|
||||
method: 'POST',
|
||||
data: {
|
||||
$.post('ajax.php', {
|
||||
update_recurring_invoice_items_order: true,
|
||||
recurring_invoice_id: <?php echo $recurring_invoice_id; ?>,
|
||||
positions: positions
|
||||
},
|
||||
success: function(data) {
|
||||
// Handle success
|
||||
},
|
||||
error: function(error) {
|
||||
console.error('Error updating order:', error);
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
48
ticket.php
48
ticket.php
@@ -341,7 +341,6 @@ if (isset($_GET['ticket_id'])) {
|
||||
$ticket_collaborators = nullable_htmlentities($row['user_names']);
|
||||
|
||||
?>
|
||||
<link rel="stylesheet" href="plugins/dragula/dragula.min.css">
|
||||
|
||||
<!-- Breadcrumbs-->
|
||||
<ol class="breadcrumb d-print-none">
|
||||
@@ -940,7 +939,7 @@ if (isset($_GET['ticket_id'])) {
|
||||
</form>
|
||||
<?php } ?>
|
||||
|
||||
<table class="table table-sm">
|
||||
<table class="table table-sm" id="tasks">
|
||||
<?php
|
||||
while($row = mysqli_fetch_array($sql_tasks)){
|
||||
$task_id = intval($row['task_id']);
|
||||
@@ -960,14 +959,14 @@ if (isset($_GET['ticket_id'])) {
|
||||
<?php } ?>
|
||||
</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-dark"> - <?php echo $task_name; ?></span>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<div class="float-right">
|
||||
<?php if (empty($ticket_resolved_at) && lookupUserPermission("module_support") >= 2) { ?>
|
||||
|
||||
<div class="dropdown dropleft text-center">
|
||||
<button class="btn btn-link text-secondary btn-sm" type="button" data-toggle="dropdown">
|
||||
<i class="fas fa-fw fa-ellipsis-v"></i>
|
||||
@@ -991,6 +990,7 @@ if (isset($_GET['ticket_id'])) {
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php } ?>
|
||||
</div>
|
||||
</td>
|
||||
@@ -1207,41 +1207,23 @@ require_once "includes/footer.php";
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
<script src="plugins/dragula/dragula.min.js"></script>
|
||||
<script src="plugins/SortableJS/Sortable.min.js"></script>
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
var container = $('.table tbody')[0];
|
||||
|
||||
dragula([container])
|
||||
.on('drop', function (el, target, source, sibling) {
|
||||
// Handle the drop event to update the order in the database
|
||||
var rows = $(container).children();
|
||||
var positions = rows.map(function(index, row) {
|
||||
return {
|
||||
id: $(row).data('taskId'),
|
||||
new Sortable(document.querySelector('table#tasks tbody'), {
|
||||
handle: '.drag-handle',
|
||||
animation: 150,
|
||||
onEnd: function (evt) {
|
||||
const rows = document.querySelectorAll('table#tasks tbody tr');
|
||||
const positions = Array.from(rows).map((row, index) => ({
|
||||
id: row.dataset.taskId,
|
||||
order: index
|
||||
};
|
||||
}).get();
|
||||
}));
|
||||
|
||||
//console.log('New positions:', positions);
|
||||
|
||||
// Send the new order to the server (example using fetch)
|
||||
$.ajax({
|
||||
url: 'ajax.php',
|
||||
method: 'POST',
|
||||
data: {
|
||||
$.post('ajax.php', {
|
||||
update_ticket_tasks_order: true,
|
||||
ticket_id: <?php echo $ticket_id; ?>,
|
||||
positions: positions
|
||||
},
|
||||
success: function(data) {
|
||||
//console.log('Order updated:', data);
|
||||
},
|
||||
error: function(error) {
|
||||
console.error('Error updating order:', error);
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
<link rel="stylesheet" href="plugins/dragula/dragula.min.css">
|
||||
<link rel="stylesheet" href="css/tickets_kanban.css">
|
||||
|
||||
<?php
|
||||
@@ -82,7 +81,7 @@ $kanban = array_values($statuses);
|
||||
?>
|
||||
<div class="kanban-column card card-dark" data-status-id="<?=htmlspecialchars($kanban_column->id); ?>">
|
||||
<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
|
||||
foreach($kanban_column->tickets as $item){
|
||||
if ($item['ticket_priority'] == "High") {
|
||||
@@ -106,7 +105,7 @@ $kanban = array_values($statuses);
|
||||
<span class='badge badge-secondary'>
|
||||
<?php echo $item['category_name']; ?>
|
||||
</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>
|
||||
</div>
|
||||
<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 "</script>";
|
||||
?>
|
||||
<script src="plugins/dragula/dragula.min.js"></script>
|
||||
<script src="plugins/SortableJS/Sortable.min.js"></script>
|
||||
<script src="js/tickets_kanban.js"></script>
|
||||
Reference in New Issue
Block a user