Add comment skeleton loaders, workflow validation, monthly schedule fix
- TicketView.php: Show 3 lt-skeleton-card placeholders in the comment list
while "Load more" fetches; skeletons are removed on resolve or error
- ticket.css: Add .comment-skeleton margin spacing
- WorkflowDesignerView.php + manage_workflows.php: Prevent creating/editing
status transitions where from_status === to_status (client + server check)
- RecurringTicketsView.php: Expand monthly day picker from 28 to 31 days
(days 29-31 labelled "last day in short months")
- RecurringTicketModel.php: Clamp monthly schedule day to last day of target
month using format('t') instead of hard-capping at 28
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -79,6 +79,12 @@ try {
|
|||||||
case 'POST':
|
case 'POST':
|
||||||
$data = json_decode(file_get_contents('php://input'), true);
|
$data = json_decode(file_get_contents('php://input'), true);
|
||||||
|
|
||||||
|
if (($data['from_status'] ?? '') === ($data['to_status'] ?? '')) {
|
||||||
|
http_response_code(400);
|
||||||
|
echo json_encode(['success' => false, 'error' => 'From Status and To Status cannot be the same']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
$stmt = $conn->prepare("INSERT INTO status_transitions (from_status, to_status, requires_comment, requires_admin, is_active)
|
$stmt = $conn->prepare("INSERT INTO status_transitions (from_status, to_status, requires_comment, requires_admin, is_active)
|
||||||
VALUES (?, ?, ?, ?, ?)");
|
VALUES (?, ?, ?, ?, ?)");
|
||||||
$wf_from = $data['from_status'];
|
$wf_from = $data['from_status'];
|
||||||
@@ -116,6 +122,12 @@ try {
|
|||||||
|
|
||||||
$data = json_decode(file_get_contents('php://input'), true);
|
$data = json_decode(file_get_contents('php://input'), true);
|
||||||
|
|
||||||
|
if (($data['from_status'] ?? '') === ($data['to_status'] ?? '')) {
|
||||||
|
http_response_code(400);
|
||||||
|
echo json_encode(['success' => false, 'error' => 'From Status and To Status cannot be the same']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
$stmt = $conn->prepare("UPDATE status_transitions SET
|
$stmt = $conn->prepare("UPDATE status_transitions SET
|
||||||
from_status = ?, to_status = ?, requires_comment = ?, requires_admin = ?, is_active = ?
|
from_status = ?, to_status = ?, requires_comment = ?, requires_admin = ?, is_active = ?
|
||||||
WHERE transition_id = ?");
|
WHERE transition_id = ?");
|
||||||
|
|||||||
@@ -365,3 +365,7 @@ kbd {
|
|||||||
|
|
||||||
/* Metadata selects use .lt-display-field (base.css) in read mode
|
/* Metadata selects use .lt-display-field (base.css) in read mode
|
||||||
instead of disabled — full opacity, non-interactive, no fading. */
|
instead of disabled — full opacity, non-interactive, no fading. */
|
||||||
|
|
||||||
|
|
||||||
|
/* Skeleton placeholder for comment lazy-load */
|
||||||
|
.comment-skeleton { margin-bottom: 0.75rem; }
|
||||||
|
|||||||
@@ -181,10 +181,13 @@ class RecurringTicketModel {
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case 'monthly':
|
case 'monthly':
|
||||||
$day = max(1, min(28, $scheduleDay)); // Limit to 28 for safety
|
$day = max(1, min(31, $scheduleDay));
|
||||||
$next = new DateTime();
|
$next = new DateTime();
|
||||||
$next->modify('first day of next month');
|
$next->modify('first day of next month');
|
||||||
$next->setDate($next->format('Y'), $next->format('m'), $day);
|
// Clamp to the last day of the target month (handles Feb, 30-day months, etc.)
|
||||||
|
$daysInMonth = (int)$next->format('t');
|
||||||
|
$day = min($day, $daysInMonth);
|
||||||
|
$next->setDate((int)$next->format('Y'), (int)$next->format('m'), $day);
|
||||||
$next->setTime($time->format('H'), $time->format('i'), 0);
|
$next->setTime($time->format('H'), $time->format('i'), 0);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
|||||||
+24
-2
@@ -1141,10 +1141,33 @@ document.addEventListener('DOMContentLoaded', function () {
|
|||||||
loadMoreBtn.disabled = true;
|
loadMoreBtn.disabled = true;
|
||||||
loadMoreBtn.textContent = 'Loading\u2026';
|
loadMoreBtn.textContent = 'Loading\u2026';
|
||||||
|
|
||||||
|
// Insert skeleton placeholders while fetching
|
||||||
|
var list = document.getElementById('commentsList');
|
||||||
|
var wrap = document.getElementById('loadMoreComments');
|
||||||
|
var skeletons = [];
|
||||||
|
for (var s = 0; s < 3; s++) {
|
||||||
|
var sk = document.createElement('div');
|
||||||
|
sk.className = 'lt-skeleton-card comment-skeleton';
|
||||||
|
sk.setAttribute('aria-hidden', 'true');
|
||||||
|
sk.innerHTML =
|
||||||
|
'<div style="display:flex;gap:0.5rem;align-items:flex-start">' +
|
||||||
|
'<div class="lt-skeleton lt-skeleton-avatar"></div>' +
|
||||||
|
'<div style="flex:1">' +
|
||||||
|
'<div class="lt-skeleton lt-skeleton-title" style="width:35%"></div>' +
|
||||||
|
'<div class="lt-skeleton lt-skeleton-text"></div>' +
|
||||||
|
'<div class="lt-skeleton lt-skeleton-text" style="width:75%"></div>' +
|
||||||
|
'</div></div>';
|
||||||
|
list.insertBefore(sk, wrap);
|
||||||
|
skeletons.push(sk);
|
||||||
|
}
|
||||||
|
|
||||||
var url = '/api/get_comments.php?ticket_id=' + td.ticket_id +
|
var url = '/api/get_comments.php?ticket_id=' + td.ticket_id +
|
||||||
'&offset=' + td.commentOffset +
|
'&offset=' + td.commentOffset +
|
||||||
'&limit=' + td.commentPageSize;
|
'&limit=' + td.commentPageSize;
|
||||||
lt.api.get(url).then(function (data) {
|
lt.api.get(url).then(function (data) {
|
||||||
|
// Remove skeleton placeholders
|
||||||
|
skeletons.forEach(function (sk) { if (sk.parentNode) sk.parentNode.removeChild(sk); });
|
||||||
|
|
||||||
if (!data.success) {
|
if (!data.success) {
|
||||||
lt.toast.error('Failed to load comments: ' + (data.error || 'Unknown error'));
|
lt.toast.error('Failed to load comments: ' + (data.error || 'Unknown error'));
|
||||||
loadMoreBtn.disabled = false;
|
loadMoreBtn.disabled = false;
|
||||||
@@ -1152,8 +1175,6 @@ document.addEventListener('DOMContentLoaded', function () {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var list = document.getElementById('commentsList');
|
|
||||||
var wrap = document.getElementById('loadMoreComments');
|
|
||||||
data.comments.forEach(function (c) {
|
data.comments.forEach(function (c) {
|
||||||
list.insertBefore(buildCommentEl(c, td.currentUserId, td.isAdmin), wrap);
|
list.insertBefore(buildCommentEl(c, td.currentUserId, td.isAdmin), wrap);
|
||||||
});
|
});
|
||||||
@@ -1178,6 +1199,7 @@ document.addEventListener('DOMContentLoaded', function () {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}).catch(function (err) {
|
}).catch(function (err) {
|
||||||
|
skeletons.forEach(function (sk) { if (sk.parentNode) sk.parentNode.removeChild(sk); });
|
||||||
lt.toast.error('Failed to load comments');
|
lt.toast.error('Failed to load comments');
|
||||||
loadMoreBtn.disabled = false;
|
loadMoreBtn.disabled = false;
|
||||||
loadMoreBtn.innerHTML = 'Load more comments';
|
loadMoreBtn.innerHTML = 'Load more comments';
|
||||||
|
|||||||
@@ -212,10 +212,10 @@ function updateScheduleOptions() {
|
|||||||
});
|
});
|
||||||
} else if (type === 'monthly') {
|
} else if (type === 'monthly') {
|
||||||
dayRow.classList.remove('is-hidden');
|
dayRow.classList.remove('is-hidden');
|
||||||
for (var i = 1; i <= 28; i++) {
|
for (var i = 1; i <= 31; i++) {
|
||||||
var opt = document.createElement('option');
|
var opt = document.createElement('option');
|
||||||
opt.value = String(i);
|
opt.value = String(i);
|
||||||
opt.textContent = 'Day ' + i;
|
opt.textContent = 'Day ' + i + (i > 28 ? ' (last day in short months)' : '');
|
||||||
daySelect.appendChild(opt);
|
daySelect.appendChild(opt);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -216,6 +216,10 @@ function saveTransition(e) {
|
|||||||
requires_admin: document.getElementById('requires_admin').checked ? 1 : 0,
|
requires_admin: document.getElementById('requires_admin').checked ? 1 : 0,
|
||||||
is_active: document.getElementById('wf_is_active').checked ? 1 : 0,
|
is_active: document.getElementById('wf_is_active').checked ? 1 : 0,
|
||||||
};
|
};
|
||||||
|
if (data.from_status === data.to_status) {
|
||||||
|
lt.toast.error('From Status and To Status cannot be the same');
|
||||||
|
return;
|
||||||
|
}
|
||||||
var url = '/api/manage_workflows.php' + (data.transition_id ? '?id=' + data.transition_id : '');
|
var url = '/api/manage_workflows.php' + (data.transition_id ? '?id=' + data.transition_id : '');
|
||||||
var apiCall = data.transition_id ? lt.api.put(url, data) : lt.api.post(url, data);
|
var apiCall = data.transition_id ? lt.api.put(url, data) : lt.api.post(url, data);
|
||||||
apiCall.then(function (result) {
|
apiCall.then(function (result) {
|
||||||
|
|||||||
Reference in New Issue
Block a user