feat: NAS storage for invoices/offers, code cleanup, date/time fixes
- NAS storage for created invoices (PDF via puppeteer), received invoices, and offers with auto-save on create/edit - Deterministic file paths derived from DB fields (no file_path column needed) - Separate NAS mount points: NAS_FINANCIALS_PATH, NAS_OFFERS_PATH - Invoice language field (cs/en) stored per invoice, replaces lang modal - Invoices list filtered by month/year matching KPI card selection - Centralized date helpers (src/utils/date.ts) replacing all .toISOString() calls that returned UTC instead of local time - Attendance project switching uses exact time (not rounded) - Comment cleanup: removed ~100 unnecessary/Czech comments - Removed as-any casts in orders and attendance - Prisma migrations: add invoice language, drop received_invoices BLOB columns Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -345,7 +345,6 @@ export default function OfferDetail() {
|
||||
form.valid_until &&
|
||||
new Date(form.valid_until) < new Date(new Date().toDateString());
|
||||
|
||||
// Load data
|
||||
const fetchDetail = useCallback(async () => {
|
||||
if (!id) return;
|
||||
try {
|
||||
@@ -473,7 +472,6 @@ export default function OfferDetail() {
|
||||
}
|
||||
}, [showCustomerDropdown]);
|
||||
|
||||
// Fetch next quotation number for new offers
|
||||
useEffect(() => {
|
||||
if (isEdit) return;
|
||||
const fetchNextNumber = async () => {
|
||||
@@ -584,7 +582,6 @@ export default function OfferDetail() {
|
||||
setItems((prev) => prev.filter((_, i) => i !== index));
|
||||
};
|
||||
|
||||
// Totals
|
||||
const subtotal = items.reduce((sum, item) => {
|
||||
if (item.is_included_in_total) {
|
||||
return (
|
||||
@@ -624,6 +621,12 @@ export default function OfferDetail() {
|
||||
});
|
||||
const result = await response.json();
|
||||
if (result.success) {
|
||||
const offerId = isEdit ? id : result.data?.id;
|
||||
if (offerId) {
|
||||
await apiFetch(`${API_BASE}/offers-pdf/${offerId}?save=1`).catch(
|
||||
() => {},
|
||||
);
|
||||
}
|
||||
alert.success(
|
||||
result.message ||
|
||||
(isEdit ? "Nabídka byla aktualizována" : "Nabídka byla vytvořena"),
|
||||
@@ -736,22 +739,16 @@ export default function OfferDetail() {
|
||||
if (!isEdit || pdfLoading) return;
|
||||
setPdfLoading(true);
|
||||
try {
|
||||
const response = await apiFetch(`${API_BASE}/offers-pdf/${id}`);
|
||||
const response = await apiFetch(`${API_BASE}/offers/${id}/file`);
|
||||
if (response.status === 401) return;
|
||||
if (!response.ok) {
|
||||
alert.error("Nepodařilo se vygenerovat PDF");
|
||||
alert.error("PDF soubor nenalezen — uložte nabídku pro vygenerování");
|
||||
return;
|
||||
}
|
||||
const html = await response.text();
|
||||
const w = window.open("", "_blank");
|
||||
if (w) {
|
||||
w.document.open();
|
||||
w.document.write(html);
|
||||
w.document.close();
|
||||
w.onload = () => w.print();
|
||||
} else {
|
||||
alert.error("Prohlížeč zablokoval vyskakovací okno");
|
||||
}
|
||||
const blob = await response.blob();
|
||||
const url = URL.createObjectURL(blob);
|
||||
window.open(url, "_blank");
|
||||
setTimeout(() => URL.revokeObjectURL(url), 60000);
|
||||
} catch {
|
||||
alert.error("Chyba při generování PDF");
|
||||
} finally {
|
||||
@@ -858,29 +855,29 @@ export default function OfferDetail() {
|
||||
<button
|
||||
onClick={handlePdf}
|
||||
className="admin-btn admin-btn-secondary"
|
||||
style={{
|
||||
display: "inline-flex",
|
||||
alignItems: "center",
|
||||
gap: "0.4rem",
|
||||
}}
|
||||
disabled={pdfLoading}
|
||||
>
|
||||
{pdfLoading ? (
|
||||
<>
|
||||
<div className="admin-spinner admin-spinner-sm" />
|
||||
PDF...
|
||||
</>
|
||||
<div className="admin-spinner admin-spinner-sm" />
|
||||
) : (
|
||||
<>
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
>
|
||||
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" />
|
||||
<polyline points="14 2 14 8 20 8" />
|
||||
</svg>
|
||||
PDF
|
||||
</>
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
>
|
||||
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" />
|
||||
<polyline points="14 2 14 8 20 8" />
|
||||
</svg>
|
||||
)}
|
||||
Zobrazit nabídku
|
||||
</button>
|
||||
)}
|
||||
{isEdit &&
|
||||
|
||||
Reference in New Issue
Block a user