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'])) {
validateCSRFToken();

View File

@@ -1,6 +1,6 @@
<?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-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>

View File

@@ -508,14 +508,22 @@ if (isset($_GET['invoice_id'])) {
<div class="card">
<div class="card-header text-bold">
Notes:
<div class="card-tools d-print-none">
<a href="#" class="btn btn-light btn-tool" data-bs-toggle="modal" data-bs-target="#invoiceNoteModal">
<i class="fas fa-edit"></i>
</a>
</div>
<span class="d-print-none" data-note-status-for="invoiceNotes"></span>
</div>
<div class="card-body">
<?= nl2br($invoice_note) ?>
<div class="card-body p-2">
<?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>
@@ -738,10 +746,14 @@ if (isset($_GET['invoice_id'])) {
</div>
<?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";
?>

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'])) {
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'])) {
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'])) {
validateCSRFToken();

View File

@@ -432,16 +432,22 @@ if (isset($_GET['quote_id'])) {
<div class="card">
<div class="card-header text-bold">
Notes:
<div class="card-tools d-print-none">
<?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>
<span class="d-print-none" data-note-status-for="quoteNotes"></span>
</div>
<div class="card-body">
<?= nl2br($quote_note) ?>
<div class="card-body p-2">
<?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>
@@ -579,9 +585,13 @@ if (isset($_GET['quote_id'])) {
</div>
<?php
require_once "modals/quote/quote_note.php";
}
?>
<script src="/js/inline_notes.js"></script>
<?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="card">
<div class="card-header text-bold">
Notes
<div class="card-tools d-print-none">
<a href="#" class="btn btn-light btn-tool" data-bs-toggle="modal" data-bs-target="#recurringInvoiceNoteModal">
<i class="fas fa-edit"></i>
</a>
</div>
Notes:
<span class="d-print-none" data-note-status-for="recurringInvoiceNotes"></span>
</div>
<div class="card-body">
<?= nl2br($recurring_invoice_note) ?>
<div class="card-body p-2">
<?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>
@@ -486,10 +494,14 @@ if (isset($_GET['recurring_invoice_id'])) {
<?php
require_once "modals/recurring_invoice/recurring_invoice_note.php";
}
?>
<script src="/js/inline_notes.js"></script>
<?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
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="d-flex flex-wrap justify-content-between">
@@ -932,7 +932,7 @@ if (isset($_GET['ticket_id'])) {
<!-- Tasks -->
<?php if (!$ticket_is_resolved || $task_count) { ?>
<div class="card">
<div class="card mb-3">
<div class="card-header px-3 py-2">
<h5 class="card-title mt-1">
<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
$can_set_contact = $can_edit_ticket && !$ticket_is_resolved && $client_id;
if ($contact_id || $can_set_contact) { ?>
<div class="card">
<div class="card mb-3">
<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>
<?php if ($can_set_contact) { ?>
@@ -1163,7 +1163,7 @@ if (isset($_GET['ticket_id'])) {
$can_link = $can_edit_ticket && !$ticket_is_closed;
$has_links = $asset_id || $vendor_id || $project_id || $watcher_count;
if ($has_links || $can_link) { ?>
<div class="card">
<div class="card mb-3">
<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>
<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);
});