Fix Card Margin bottoms client header and ticket cards on the right

This commit is contained in:
johnnyq
2026-08-14 19:08:15 -04:00
parent 35239b563d
commit 8c0e5246b4
13 changed files with 228 additions and 191 deletions

View File

@@ -59,6 +59,83 @@ if (isset($_POST['client_set_notes'])) {
} }
/*
* Inline notes for sales documents.
*
* These replace the invoice/quote/recurring-invoice note modals, which posted
* to post.php and reloaded the page. Same permission, client-access and audit
* behaviour as those handlers had - only the delivery changed.
*/
if (isset($_POST['invoice_set_notes'])) {
validateCSRFToken();
enforceUserPermission('module_sales', 2);
$invoice_id = intval($_POST['invoice_id']);
$note = escapeSql($_POST['note']);
$sql = mysqli_query($mysqli, "SELECT invoice_client_id, invoice_number, invoice_prefix FROM invoices WHERE invoice_id = $invoice_id");
$row = mysqli_fetch_assoc($sql);
$invoice_prefix = escapeSql($row['invoice_prefix']);
$invoice_number = intval($row['invoice_number']);
$client_id = intval($row['invoice_client_id']);
enforceClientAccess();
mysqli_query($mysqli, "UPDATE invoices SET invoice_note = '$note' WHERE invoice_id = $invoice_id");
logAudit("Invoice", "Edit", "$session_name edited notes on invoice $invoice_prefix$invoice_number", $client_id, $invoice_id);
}
if (isset($_POST['quote_set_notes'])) {
validateCSRFToken();
enforceUserPermission('module_sales', 2);
$quote_id = intval($_POST['quote_id']);
$note = escapeSql($_POST['note']);
$sql = mysqli_query($mysqli, "SELECT quote_client_id, quote_number, quote_prefix FROM quotes WHERE quote_id = $quote_id");
$row = mysqli_fetch_assoc($sql);
$quote_prefix = escapeSql($row['quote_prefix']);
$quote_number = intval($row['quote_number']);
$client_id = intval($row['quote_client_id']);
enforceClientAccess();
mysqli_query($mysqli, "UPDATE quotes SET quote_note = '$note' WHERE quote_id = $quote_id");
logAudit("Quote", "Edit", "$session_name edited notes on quote $quote_prefix$quote_number", $client_id, $quote_id);
}
if (isset($_POST['recurring_invoice_set_notes'])) {
validateCSRFToken();
enforceUserPermission('module_sales', 2);
$recurring_invoice_id = intval($_POST['recurring_invoice_id']);
$note = escapeSql($_POST['note']);
$sql = mysqli_query($mysqli, "SELECT recurring_invoice_prefix, recurring_invoice_number, recurring_invoice_client_id FROM recurring_invoices WHERE recurring_invoice_id = $recurring_invoice_id");
$row = mysqli_fetch_assoc($sql);
$recurring_invoice_prefix = escapeSql($row['recurring_invoice_prefix']);
$recurring_invoice_number = intval($row['recurring_invoice_number']);
$client_id = intval($row['recurring_invoice_client_id']);
enforceClientAccess();
mysqli_query($mysqli, "UPDATE recurring_invoices SET recurring_invoice_note = '$note' WHERE recurring_invoice_id = $recurring_invoice_id");
logAudit("Recurring Invoice", "Edit", "$session_name edited notes on recurring invoice $recurring_invoice_prefix$recurring_invoice_number", $client_id, $recurring_invoice_id);
}
if (isset($_POST['contact_set_notes'])) { if (isset($_POST['contact_set_notes'])) {
validateCSRFToken(); validateCSRFToken();

View File

@@ -1,6 +1,6 @@
<?php $show_add_credit = 0; // Remove once credits is added hides the button ?> <?php $show_add_credit = 0; // Remove once credits is added hides the button ?>
<div class="card d-print-none"> <div class="card mb-3 d-print-none">
<div class="card-header pb-1 pt-2 px-3"> <div class="card-header pb-1 pt-2 px-3">
<div class="card-title"> <div class="card-title">
<a href="#" data-bs-toggle="collapse" data-bs-target="#clientHeader"><h4 class="text-dark" data-bs-toggle="tooltip" data-bs-placement="right" title="Client ID: <?= $client_id ?>"><strong><?= $client_name ?></strong> <?php if ($client_archived_at) { echo "(archived)"; } ?></h4></a> <a href="#" data-bs-toggle="collapse" data-bs-target="#clientHeader"><h4 class="text-dark" data-bs-toggle="tooltip" data-bs-placement="right" title="Client ID: <?= $client_id ?>"><strong><?= $client_name ?></strong> <?php if ($client_archived_at) { echo "(archived)"; } ?></h4></a>

View File

@@ -508,14 +508,22 @@ if (isset($_GET['invoice_id'])) {
<div class="card"> <div class="card">
<div class="card-header text-bold"> <div class="card-header text-bold">
Notes: Notes:
<div class="card-tools d-print-none"> <span class="d-print-none" data-note-status-for="invoiceNotes"></span>
<a href="#" class="btn btn-light btn-tool" data-bs-toggle="modal" data-bs-target="#invoiceNoteModal">
<i class="fas fa-edit"></i>
</a>
</div>
</div> </div>
<div class="card-body"> <div class="card-body p-2">
<?= nl2br($invoice_note) ?> <?php if (lookupUserPermission("module_sales") >= 2) { ?>
<textarea class="form-control itflow-inline-note d-print-none" rows="6"
id="invoiceNotes"
placeholder="Enter some notes"
data-endpoint="invoice_set_notes"
data-id-field="invoice_id"
data-id="<?= $invoice_id ?>"
data-csrf="<?= $_SESSION['csrf_token'] ?>"><?= $invoice_note ?></textarea>
<?php } else { ?>
<div class="d-print-none"><?= nl2br($invoice_note) ?></div>
<?php } ?>
<!-- Printed output must be plain text, not a form control -->
<div class="d-none d-print-block"><?= nl2br($invoice_note) ?></div>
</div> </div>
</div> </div>
</div> </div>
@@ -738,10 +746,14 @@ if (isset($_GET['invoice_id'])) {
</div> </div>
<?php <?php
include_once "modals/invoice/invoice_add_ticket.php"; include_once "modals/invoice/invoice_add_ticket.php";
include_once "modals/invoice/invoice_note.php";
} }
?>
<script src="/js/inline_notes.js"></script>
<?php
require_once "../includes/footer.php"; require_once "../includes/footer.php";
?> ?>

View File

@@ -1,23 +0,0 @@
<div class="modal" id="invoiceNoteModal" tabindex="-1">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header bg-dark">
<h5 class="modal-title text-white"><i class="fas fa-fw fa-edit me-2"></i>Invoice Notes</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
</div>
<form action="post.php" method="post" autocomplete="off">
<input type="hidden" name="csrf_token" value="<?= $_SESSION['csrf_token'] ?>">
<input type="hidden" name="invoice_id" value="<?= $invoice_id ?>">
<div class="modal-body">
<div class="mb-3">
<textarea class="form-control" rows="8" name="note" placeholder="Enter some notes"><?= $invoice_note ?></textarea>
</div>
</div>
<div class="modal-footer">
<button type="submit" name="invoice_note" class="btn btn-primary text-bold"><i class="fas fa-check me-2"></i>Save</button>
<button type="button" class="btn btn-light" data-bs-dismiss="modal"><i class="fas fa-times me-2"></i>Cancel</button>
</div>
</form>
</div>
</div>
</div>

View File

@@ -1,29 +0,0 @@
<div class="modal" id="quoteNoteModal" tabindex="-1">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header bg-dark">
<h5 class="modal-title text-white">
<i class="fas fa-fw fa-edit me-2"></i>Quote Notes
</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
</div>
<form action="post.php" method="post" autocomplete="off">
<input type="hidden" name="csrf_token" value="<?= $_SESSION['csrf_token'] ?>">
<input type="hidden" name="quote_id" value="<?= $quote_id ?>">
<div class="modal-body">
<div class="mb-3">
<textarea class="form-control" rows="8" name="note" placeholder="Enter some notes"><?= $quote_note ?></textarea>
</div>
</div>
<div class="modal-footer">
<button type="submit" name="quote_note" class="btn btn-primary text-bold">
<i class="fas fa-check me-2"></i>Save
</button>
<button type="button" class="btn btn-light" data-bs-dismiss="modal">
<i class="fas fa-times me-2"></i>Cancel
</button>
</div>
</form>
</div>
</div>
</div>

View File

@@ -1,23 +0,0 @@
<div class="modal" id="recurringInvoiceNoteModal" tabindex="-1">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header bg-dark">
<h5 class="modal-title text-white"><i class="fa fa-fw fa-edit me-2"></i>Editing: <strong>Recurring Invoice</strong> Notes</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
</div>
<form action="post.php" method="post" autocomplete="off">
<input type="hidden" name="csrf_token" value="<?= $_SESSION['csrf_token'] ?>">
<input type="hidden" name="recurring_invoice_id" value="<?= $recurring_invoice_id ?>">
<div class="modal-body">
<div class="mb-3">
<textarea class="form-control" rows="8" name="note" placeholder="Enter some notes"><?= $recurring_invoice_note ?></textarea>
</div>
</div>
<div class="modal-footer">
<button type="submit" name="recurring_invoice_note" class="btn btn-primary text-bold"><i class="fas fa-check me-2"></i>Save</button>
<button type="button" class="btn btn-light" data-bs-dismiss="modal"><i class="fas fa-times me-2"></i>Cancel</button>
</div>
</form>
</div>
</div>
</div>

View File

@@ -393,34 +393,6 @@ if (isset($_POST['add_invoice_item'])) {
} }
if (isset($_POST['invoice_note'])) {
validateCSRFToken();
enforceUserPermission('module_sales', 2);
$invoice_id = intval($_POST['invoice_id']);
$note = escapeSql($_POST['note']);
// Get Invoice Details for logging
$sql = mysqli_query($mysqli,"SELECT invoice_client_id, invoice_number, invoice_prefix FROM invoices WHERE invoice_id = $invoice_id");
$row = mysqli_fetch_assoc($sql);
$invoice_prefix = escapeSql($row['invoice_prefix']);
$invoice_number = intval($row['invoice_number']);
$client_id = intval($row['invoice_client_id']);
enforceClientAccess();
mysqli_query($mysqli,"UPDATE invoices SET invoice_note = '$note' WHERE invoice_id = $invoice_id");
logAudit("Invoice", "Edit", "$session_name added note to invoice $invoice_prefix$invoice_number", $client_id, $invoice_id);
flashAlert("Notes added");
redirect();
}
if (isset($_POST['edit_invoice_item'])) { if (isset($_POST['edit_invoice_item'])) {
validateCSRFToken(); validateCSRFToken();

View File

@@ -341,34 +341,6 @@ if (isset($_POST['edit_quote_item'])) {
} }
if (isset($_POST['quote_note'])) {
validateCSRFToken();
enforceUserPermission('module_sales', 2);
$quote_id = intval($_POST['quote_id']);
$note = escapeSql($_POST['note']);
// Get Quote Details
$sql = mysqli_query($mysqli,"SELECT quote_client_id, quote_number, quote_prefix FROM quotes WHERE quote_id = $quote_id");
$row = mysqli_fetch_assoc($sql);
$quote_prefix = escapeSql($row['quote_prefix']);
$quote_number = escapeSql($row['quote_number']);
$client_id = intval($row['quote_client_id']);
enforceClientAccess();
mysqli_query($mysqli,"UPDATE quotes SET quote_note = '$note' WHERE quote_id = $quote_id");
logAudit("Quote", "Edit", "$session_name added notes to quote $quote_prefix$quote_number", $client_id, $quote_id);
flashAlert("Notes added");
redirect();
}
if (isset($_POST['edit_quote'])) { if (isset($_POST['edit_quote'])) {
validateCSRFToken(); validateCSRFToken();

View File

@@ -319,34 +319,6 @@ if (isset($_POST['edit_recurring_invoice_item'])) {
} }
if (isset($_POST['recurring_invoice_note'])) {
validateCSRFToken();
enforceUserPermission('module_sales', 2);
$recurring_invoice_id = intval($_POST['recurring_invoice_id']);
$note = escapeSql($_POST['note']);
// Get Recurring details for logging
$sql = mysqli_query($mysqli,"SELECT recurring_invoice_prefix, recurring_invoice_number, recurring_invoice_client_id FROM recurring_invoices WHERE recurring_invoice_id = $recurring_invoice_id");
$row = mysqli_fetch_assoc($sql);
$recurring_invoice_prefix = escapeSql($row['recurring_invoice_prefix']);
$recurring_invoice_number = intval($row['recurring_invoice_number']);
$client_id = intval($row['recurring_invoice_client_id']);
enforceClientAccess();
mysqli_query($mysqli,"UPDATE recurring_invoices SET recurring_invoice_note = '$note' WHERE recurring_invoice_id = $recurring_invoice_id");
logAudit("Recurring Invoice", "Edit", "$session_name added note to recurring invoice $recurring_invoice_prefix$recurring_invoice_number", $client_id, $recurring_invoice_id);
flashAlert("Notes added");
redirect();
}
if (isset($_GET['delete_recurring_invoice_item'])) { if (isset($_GET['delete_recurring_invoice_item'])) {
validateCSRFToken(); validateCSRFToken();

View File

@@ -432,16 +432,22 @@ if (isset($_GET['quote_id'])) {
<div class="card"> <div class="card">
<div class="card-header text-bold"> <div class="card-header text-bold">
Notes: Notes:
<div class="card-tools d-print-none"> <span class="d-print-none" data-note-status-for="quoteNotes"></span>
<?php if (lookupUserPermission("module_sales") >= 2) { ?>
<a href="#" class="btn btn-light btn-tool" data-bs-toggle="modal" data-bs-target="#quoteNoteModal">
<i class="fas fa-edit"></i>
</a>
<?php } ?>
</div>
</div> </div>
<div class="card-body"> <div class="card-body p-2">
<?= nl2br($quote_note) ?> <?php if (lookupUserPermission("module_sales") >= 2) { ?>
<textarea class="form-control itflow-inline-note d-print-none" rows="6"
id="quoteNotes"
placeholder="Enter some notes"
data-endpoint="quote_set_notes"
data-id-field="quote_id"
data-id="<?= $quote_id ?>"
data-csrf="<?= $_SESSION['csrf_token'] ?>"><?= $quote_note ?></textarea>
<?php } else { ?>
<div class="d-print-none"><?= nl2br($quote_note) ?></div>
<?php } ?>
<!-- Printed output must be plain text, not a form control -->
<div class="d-none d-print-block"><?= nl2br($quote_note) ?></div>
</div> </div>
</div> </div>
</div> </div>
@@ -579,9 +585,13 @@ if (isset($_GET['quote_id'])) {
</div> </div>
<?php <?php
require_once "modals/quote/quote_note.php";
} }
?>
<script src="/js/inline_notes.js"></script>
<?php
require_once "../includes/footer.php"; require_once "../includes/footer.php";
?> ?>

View File

@@ -396,15 +396,23 @@ if (isset($_GET['recurring_invoice_id'])) {
<div class="col-sm-7"> <div class="col-sm-7">
<div class="card"> <div class="card">
<div class="card-header text-bold"> <div class="card-header text-bold">
Notes Notes:
<div class="card-tools d-print-none"> <span class="d-print-none" data-note-status-for="recurringInvoiceNotes"></span>
<a href="#" class="btn btn-light btn-tool" data-bs-toggle="modal" data-bs-target="#recurringInvoiceNoteModal">
<i class="fas fa-edit"></i>
</a>
</div>
</div> </div>
<div class="card-body"> <div class="card-body p-2">
<?= nl2br($recurring_invoice_note) ?> <?php if (lookupUserPermission("module_sales") >= 2) { ?>
<textarea class="form-control itflow-inline-note d-print-none" rows="6"
id="recurringInvoiceNotes"
placeholder="Enter some notes"
data-endpoint="recurring_invoice_set_notes"
data-id-field="recurring_invoice_id"
data-id="<?= $recurring_invoice_id ?>"
data-csrf="<?= $_SESSION['csrf_token'] ?>"><?= $recurring_invoice_note ?></textarea>
<?php } else { ?>
<div class="d-print-none"><?= nl2br($recurring_invoice_note) ?></div>
<?php } ?>
<!-- Printed output must be plain text, not a form control -->
<div class="d-none d-print-block"><?= nl2br($recurring_invoice_note) ?></div>
</div> </div>
</div> </div>
</div> </div>
@@ -486,10 +494,14 @@ if (isset($_GET['recurring_invoice_id'])) {
<?php <?php
require_once "modals/recurring_invoice/recurring_invoice_note.php";
} }
?>
<script src="/js/inline_notes.js"></script>
<?php
require_once "../includes/footer.php"; require_once "../includes/footer.php";
?> ?>

View File

@@ -416,7 +416,7 @@ if (isset($_GET['ticket_id'])) {
Ticket header - everything you need to identify the ticket and know Ticket header - everything you need to identify the ticket and know
what state it is in, without scrolling or hunting across cards. what state it is in, without scrolling or hunting across cards.
--> -->
<div class="card"> <div class="card mb-3">
<div class="card-body pb-2"> <div class="card-body pb-2">
<div class="d-flex flex-wrap justify-content-between"> <div class="d-flex flex-wrap justify-content-between">
@@ -932,7 +932,7 @@ if (isset($_GET['ticket_id'])) {
<!-- Tasks --> <!-- Tasks -->
<?php if (!$ticket_is_resolved || $task_count) { ?> <?php if (!$ticket_is_resolved || $task_count) { ?>
<div class="card"> <div class="card mb-3">
<div class="card-header px-3 py-2"> <div class="card-header px-3 py-2">
<h5 class="card-title mt-1"> <h5 class="card-title mt-1">
<i class="fas fa-fw fa-tasks me-2"></i>Tasks <i class="fas fa-fw fa-tasks me-2"></i>Tasks
@@ -1092,7 +1092,7 @@ if (isset($_GET['ticket_id'])) {
// is for links - setting the ticket's contact belongs here // is for links - setting the ticket's contact belongs here
$can_set_contact = $can_edit_ticket && !$ticket_is_resolved && $client_id; $can_set_contact = $can_edit_ticket && !$ticket_is_resolved && $client_id;
if ($contact_id || $can_set_contact) { ?> if ($contact_id || $can_set_contact) { ?>
<div class="card"> <div class="card mb-3">
<div class="card-header px-3 py-2"> <div class="card-header px-3 py-2">
<h5 class="card-title mt-1"><i class="fas fa-fw fa-user-check me-2"></i>Contact</h5> <h5 class="card-title mt-1"><i class="fas fa-fw fa-user-check me-2"></i>Contact</h5>
<?php if ($can_set_contact) { ?> <?php if ($can_set_contact) { ?>
@@ -1163,7 +1163,7 @@ if (isset($_GET['ticket_id'])) {
$can_link = $can_edit_ticket && !$ticket_is_closed; $can_link = $can_edit_ticket && !$ticket_is_closed;
$has_links = $asset_id || $vendor_id || $project_id || $watcher_count; $has_links = $asset_id || $vendor_id || $project_id || $watcher_count;
if ($has_links || $can_link) { ?> if ($has_links || $can_link) { ?>
<div class="card"> <div class="card mb-3">
<div class="card-header px-3 py-2"> <div class="card-header px-3 py-2">
<h5 class="card-title mt-1"><i class="fas fa-fw fa-link me-2"></i>Linked</h5> <h5 class="card-title mt-1"><i class="fas fa-fw fa-link me-2"></i>Linked</h5>
<div class="card-tools"> <div class="card-tools">

85
js/inline_notes.js Normal file
View File

@@ -0,0 +1,85 @@
/**
* Inline notes.
*
* Replaces the invoice / quote / recurring-invoice note modals with a textarea
* that saves on blur, the same way the client overview Quick Notes field works.
*
* Markup contract:
* <textarea class="itflow-inline-note"
* data-endpoint="invoice_set_notes"
* data-id-field="invoice_id"
* data-id="42"
* data-csrf="...">notes</textarea>
*
* Losing an explicit Save button means the user needs telling that the save
* happened, so each field reports its own state next to the label rather than
* saving silently.
*/
function itflowInlineNotes(textarea) {
if (!textarea || textarea.dataset.inlineNoteReady) {
return;
}
textarea.dataset.inlineNoteReady = '1';
// Saving only when the text actually changed avoids a write on every
// click-through, which would otherwise spam the audit log.
let lastSaved = textarea.value;
const status = document.createElement('span');
status.className = 'itflow-note-status small ms-2 d-print-none';
const label = document.querySelector('[data-note-status-for="' + textarea.id + '"]');
(label || textarea.parentNode).appendChild(status);
function setStatus(text, cls) {
status.textContent = text;
status.className = 'itflow-note-status small ms-2 d-print-none ' + cls;
}
function save() {
if (textarea.value === lastSaved) {
return;
}
const payload = {
csrf_token: textarea.dataset.csrf,
note: textarea.value
};
payload[textarea.dataset.endpoint] = 'TRUE';
payload[textarea.dataset.idField] = textarea.dataset.id;
setStatus('Saving...', 'text-muted');
itflowPost('ajax.php', payload, function () {
lastSaved = textarea.value;
setStatus('Saved', 'text-success');
setTimeout(function () {
if (status.textContent === 'Saved') {
setStatus('', '');
}
}, 2000);
}, function () {
setStatus('Not saved - check your connection', 'text-danger');
});
}
textarea.addEventListener('blur', save);
// Ctrl/Cmd+Enter saves without leaving the field
textarea.addEventListener('keydown', function (e) {
if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') {
e.preventDefault();
save();
}
});
// Warn rather than lose the text if the page is closed mid-edit
window.addEventListener('beforeunload', function (e) {
if (textarea.value !== lastSaved) {
e.preventDefault();
e.returnValue = '';
}
});
}
document.addEventListener('DOMContentLoaded', function () {
document.querySelectorAll('.itflow-inline-note').forEach(itflowInlineNotes);
});