Fix company logo missing from PDFs and add AI photo save logging
When a tenant uploads a logo it is stored in Azure Blob Storage and LogoData (the legacy DB byte[]) is cleared. All PDF controllers were still reading the now-null LogoData, so logos never appeared on any PDF after upload. Fixed by injecting ICompanyLogoService into all six affected controllers (Quotes, Invoices, Deposits, GiftCertificates, PurchaseOrders, CatalogItems) and loading the blob-stored logo first before falling back to the legacy DB field. Also added structured logging to the AI photo promotion path in QuotesController Create/Edit POST so upload failures are visible in production logs instead of silently swallowed. Added onclick safety net to the Create and Edit quote submit buttons so dynamically-injected hidden fields (AiPhotoTempIds) are written before iOS Safari collects the form data on submit. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -363,6 +363,10 @@
|
||||
style="z-index:1;font-size:.7rem;" data-photo-id="@photo.Id" title="Delete">
|
||||
<i class="bi bi-x"></i>
|
||||
</button>
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary position-absolute top-0 start-0 m-1 p-0 px-1 edit-caption-btn"
|
||||
style="z-index:1;font-size:.7rem;" data-photo-id="@photo.Id" title="Edit caption">
|
||||
<i class="bi bi-pencil"></i>
|
||||
</button>
|
||||
}
|
||||
<a href="@Url.Action("Photo", "Quotes", new { id = photo.Id })" target="_blank" title="View full size">
|
||||
<img src="@Url.Action("Photo", "Quotes", new { id = photo.Id })"
|
||||
@@ -372,6 +376,17 @@
|
||||
<div class="card-body p-2">
|
||||
<p class="card-text small text-muted text-truncate mb-0" title="@photo.FileName">@photo.FileName</p>
|
||||
<p class="card-text text-muted mb-0" style="font-size:.7rem;">@photo.CreatedAt.ToString("MMM d, yyyy")</p>
|
||||
@if (!photo.IsAiAnalysisPhoto)
|
||||
{
|
||||
<p class="card-text small fst-italic text-truncate mb-0 caption-display" title="@photo.Caption">@photo.Caption</p>
|
||||
<div class="caption-edit-form d-none mt-1" data-photo-id="@photo.Id">
|
||||
<textarea class="form-control form-control-sm caption-textarea" rows="2" placeholder="Add a caption...">@photo.Caption</textarea>
|
||||
<div class="d-flex gap-1 mt-1">
|
||||
<button type="button" class="btn btn-primary btn-sm save-caption-btn flex-grow-1">Save</button>
|
||||
<button type="button" class="btn btn-secondary btn-sm cancel-caption-btn">Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -398,7 +413,7 @@
|
||||
<a asp-action="Details" asp-controller="Quotes" asp-route-id="@Model.Id" class="btn btn-outline-secondary btn-lg">
|
||||
<i class="bi bi-x-circle me-1"></i>Cancel
|
||||
</a>
|
||||
<button type="submit" class="btn btn-primary btn-lg" id="submitBtn">
|
||||
<button type="submit" class="btn btn-primary btn-lg" id="submitBtn" onclick="if(typeof writeHiddenFields==='function')writeHiddenFields()">
|
||||
<i class="bi bi-check-circle me-1"></i>Update Quote
|
||||
</button>
|
||||
</div>
|
||||
@@ -708,6 +723,7 @@
|
||||
const quoteId = @Model.Id;
|
||||
const uploadUrl = '@Url.Action("UploadQuotePhoto", "Quotes")';
|
||||
const deleteUrl = '@Url.Action("DeleteQuotePhoto", "Quotes")';
|
||||
const updateUrl = '@Url.Action("UpdateQuotePhoto", "Quotes")';
|
||||
const token = document.querySelector('input[name="__RequestVerificationToken"]')?.value ?? '';
|
||||
|
||||
const fileInput = document.getElementById('editPhotoFileInput');
|
||||
@@ -744,12 +760,24 @@
|
||||
style="z-index:1;font-size:.7rem;" data-photo-id="${data.id}" title="Delete">
|
||||
<i class="bi bi-x"></i>
|
||||
</button>
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary position-absolute top-0 start-0 m-1 p-0 px-1 edit-caption-btn"
|
||||
style="z-index:1;font-size:.7rem;" data-photo-id="${data.id}" title="Edit caption">
|
||||
<i class="bi bi-pencil"></i>
|
||||
</button>
|
||||
<a href="${data.url}" target="_blank" title="View full size">
|
||||
<img src="${data.url}" class="card-img-top" style="height:160px;object-fit:cover;" loading="lazy">
|
||||
</a>
|
||||
<div class="card-body p-2">
|
||||
<p class="card-text small text-muted text-truncate mb-0">${data.fileName}</p>
|
||||
<p class="card-text text-muted mb-0" style="font-size:.7rem;">Just now</p>
|
||||
<p class="card-text small fst-italic text-truncate mb-0 caption-display"></p>
|
||||
<div class="caption-edit-form d-none mt-1" data-photo-id="${data.id}">
|
||||
<textarea class="form-control form-control-sm caption-textarea" rows="2" placeholder="Add a caption..."></textarea>
|
||||
<div class="d-flex gap-1 mt-1">
|
||||
<button type="button" class="btn btn-primary btn-sm save-caption-btn flex-grow-1">Save</button>
|
||||
<button type="button" class="btn btn-secondary btn-sm cancel-caption-btn">Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
grid.appendChild(col);
|
||||
@@ -771,6 +799,45 @@
|
||||
updateCount(-1);
|
||||
});
|
||||
|
||||
// Show inline caption editor
|
||||
document.addEventListener('click', (e) => {
|
||||
const btn = e.target.closest('.edit-caption-btn');
|
||||
if (!btn) return;
|
||||
const card = btn.closest('.photo-item');
|
||||
card.querySelector('.caption-display')?.classList.add('d-none');
|
||||
card.querySelector('.caption-edit-form')?.classList.remove('d-none');
|
||||
});
|
||||
|
||||
// Cancel inline caption edit
|
||||
document.addEventListener('click', (e) => {
|
||||
const btn = e.target.closest('.cancel-caption-btn');
|
||||
if (!btn) return;
|
||||
const card = btn.closest('.photo-item');
|
||||
card.querySelector('.caption-edit-form')?.classList.add('d-none');
|
||||
card.querySelector('.caption-display')?.classList.remove('d-none');
|
||||
});
|
||||
|
||||
// Save caption
|
||||
document.addEventListener('click', async (e) => {
|
||||
const btn = e.target.closest('.save-caption-btn');
|
||||
if (!btn) return;
|
||||
const card = btn.closest('.photo-item');
|
||||
const editForm = card.querySelector('.caption-edit-form');
|
||||
const photoId = editForm?.dataset.photoId;
|
||||
const caption = card.querySelector('.caption-textarea')?.value.trim() ?? '';
|
||||
const fd = new FormData();
|
||||
fd.append('id', photoId);
|
||||
fd.append('caption', caption);
|
||||
fd.append('__RequestVerificationToken', token);
|
||||
const resp = await fetch(updateUrl, { method: 'POST', body: fd });
|
||||
const data = await resp.json();
|
||||
if (!data.success) { alert(data.error || 'Update failed.'); return; }
|
||||
const displayEl = card.querySelector('.caption-display');
|
||||
if (displayEl) { displayEl.textContent = caption; displayEl.title = caption; }
|
||||
editForm?.classList.add('d-none');
|
||||
card.querySelector('.caption-display')?.classList.remove('d-none');
|
||||
});
|
||||
|
||||
function updateCount(delta) {
|
||||
const badge = document.getElementById('photoCount');
|
||||
if (badge) badge.textContent = parseInt(badge.textContent || '0') + delta;
|
||||
|
||||
Reference in New Issue
Block a user