diff --git a/public/invoice-view.js b/public/invoice-view.js
index 5ac9782..3ebd91c 100644
--- a/public/invoice-view.js
+++ b/public/invoice-view.js
@@ -239,6 +239,17 @@ function renderInvoiceRow(invoice) {
paidBtn = ``;
}
+ // Send status button — only for QBO invoices
+ let sendBtn = '';
+ if (hasQbo && !paid) {
+ const isSent = invoice.email_status === 'sent';
+ if (isSent) {
+ sendBtn = ``;
+ } else {
+ sendBtn = ``;
+ }
+ }
+
const delBtn = ``;
const rowClass = paid ? (invoice.payment_status === 'Deposited' ? 'bg-blue-50/50' : 'bg-green-50/50') : partial ? 'bg-yellow-50/30' : overdue ? 'bg-red-50/50' : '';
@@ -252,7 +263,7 @@ function renderInvoiceRow(invoice) {
${invoice.terms} |
${amountDisplay} |
- ${editBtn} ${qboBtn} ${pdfBtn} ${htmlBtn} ${paidBtn} ${delBtn}
+ ${editBtn} ${qboBtn} ${pdfBtn} ${htmlBtn} ${sendBtn} ${paidBtn} ${delBtn}
|
`;
}
@@ -440,6 +451,23 @@ export async function syncFromQBO() {
finally { if (typeof hideSpinner === 'function') hideSpinner(); }
}
+export async function setEmailStatus(id, status) {
+ const label = status === 'sent' ? 'Mark as sent' : 'Mark as not sent';
+ if (!confirm(`${label}?`)) return;
+ if (typeof showSpinner === 'function') showSpinner(`Updating status in QBO...`);
+ try {
+ const r = await fetch(`/api/invoices/${id}/email-status`, {
+ method: 'PATCH',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ status })
+ });
+ const d = await r.json();
+ if (r.ok) loadInvoices();
+ else alert(`❌ ${d.error}`);
+ } catch (e) { alert('Network error.'); }
+ finally { if (typeof hideSpinner === 'function') hideSpinner(); }
+}
+
export async function resetQbo(id) {
if (!confirm('Reset QBO link?\nInvoice must be deleted in QBO first!')) return;
try {
@@ -471,6 +499,6 @@ export async function remove(id) {
// ============================================================
window.invoiceView = {
- viewPDF, viewHTML, exportToQBO, syncToQBO, syncFromQBO, resetQbo, markPaid, edit, remove,
+ viewPDF, viewHTML, syncFromQBO, resetQbo, markPaid, setEmailStatus, edit, remove,
loadInvoices, renderInvoiceView, setStatus
};
\ No newline at end of file
diff --git a/server.js b/server.js
index 3dd3f70..56d59f0 100644
--- a/server.js
+++ b/server.js
@@ -177,7 +177,7 @@ async function exportInvoiceToQbo(invoiceId, client) {
"TxnDate": invoice.invoice_date.toISOString().split('T')[0],
"Line": lineItems,
"CustomerMemo": { "value": invoice.auth_code ? `Auth: ${invoice.auth_code}` : "" },
- "EmailStatus": "EmailSent",
+ "EmailStatus": "NotSet",
"BillEmail": { "Address": invoice.email || "" }
};
@@ -292,7 +292,6 @@ async function syncInvoiceToQbo(invoiceId, client) {
}
-
// Logo endpoints
app.get('/api/logo-info', async (req, res) => {
try {
@@ -833,6 +832,7 @@ app.post('/api/invoices', async (req, res) => {
}
});
+
app.post('/api/quotes/:id/convert-to-invoice', async (req, res) => {
const { id } = req.params;
@@ -1062,6 +1062,69 @@ app.delete('/api/invoices/:id', async (req, res) => {
}
});
+app.patch('/api/invoices/:id/email-status', async (req, res) => {
+ const { id } = req.params;
+ const { status } = req.body; // 'sent' or 'open'
+
+ if (!['sent', 'open'].includes(status)) {
+ return res.status(400).json({ error: 'Status must be "sent" or "open".' });
+ }
+
+ try {
+ const invResult = await pool.query('SELECT qbo_id FROM invoices WHERE id = $1', [id]);
+ if (invResult.rows.length === 0) return res.status(404).json({ error: 'Invoice not found' });
+
+ const invoice = invResult.rows[0];
+
+ // QBO updaten falls vorhanden
+ if (invoice.qbo_id) {
+ const oauthClient = getOAuthClient();
+ const companyId = oauthClient.getToken().realmId;
+ const baseUrl = process.env.QBO_ENVIRONMENT === 'production'
+ ? 'https://quickbooks.api.intuit.com'
+ : 'https://sandbox-quickbooks.api.intuit.com';
+
+ // SyncToken holen
+ const qboRes = await makeQboApiCall({
+ url: `${baseUrl}/v3/company/${companyId}/invoice/${invoice.qbo_id}`,
+ method: 'GET'
+ });
+ const qboData = qboRes.getJson ? qboRes.getJson() : qboRes.json;
+ const syncToken = qboData.Invoice?.SyncToken;
+
+ if (syncToken !== undefined) {
+ const emailStatus = status === 'sent' ? 'EmailSent' : 'NotSet';
+
+ await makeQboApiCall({
+ url: `${baseUrl}/v3/company/${companyId}/invoice`,
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({
+ Id: invoice.qbo_id,
+ SyncToken: syncToken,
+ sparse: true,
+ EmailStatus: emailStatus
+ })
+ });
+
+ console.log(`✅ QBO Invoice ${invoice.qbo_id} email status → ${emailStatus}`);
+ }
+ }
+
+ // Lokal updaten
+ await pool.query(
+ 'UPDATE invoices SET email_status = $1, updated_at = CURRENT_TIMESTAMP WHERE id = $2',
+ [status, id]
+ );
+
+ res.json({ success: true, status });
+
+ } catch (error) {
+ console.error('Error updating email status:', error);
+ res.status(500).json({ error: 'Failed to update status: ' + error.message });
+ }
+});
+
// PDF Generation code continues below...
// PDF Generation using templates and persistent browser