sent,mark sent
This commit is contained in:
parent
ec3cd2b659
commit
73b869e2d9
|
|
@ -239,6 +239,17 @@ function renderInvoiceRow(invoice) {
|
|||
paidBtn = `<button onclick="window.paymentModal.open([${invoice.id}])" class="text-emerald-600 hover:text-emerald-800" title="Record Payment in QBO">💰 Payment</button>`;
|
||||
}
|
||||
|
||||
// Send status button — only for QBO invoices
|
||||
let sendBtn = '';
|
||||
if (hasQbo && !paid) {
|
||||
const isSent = invoice.email_status === 'sent';
|
||||
if (isSent) {
|
||||
sendBtn = `<button onclick="window.invoiceView.setEmailStatus(${invoice.id}, 'open')" class="text-gray-400 hover:text-gray-600 text-xs" title="Mark as not sent">✉️ Sent</button>`;
|
||||
} else {
|
||||
sendBtn = `<button onclick="window.invoiceView.setEmailStatus(${invoice.id}, 'sent')" class="text-indigo-600 hover:text-indigo-800 text-xs" title="Mark as sent to customer">📤 Mark Sent</button>`;
|
||||
}
|
||||
}
|
||||
|
||||
const delBtn = `<button onclick="window.invoiceView.remove(${invoice.id})" class="text-red-600 hover:text-red-900">Del</button>`;
|
||||
|
||||
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) {
|
|||
<td class="px-4 py-3 whitespace-nowrap text-sm text-gray-500">${invoice.terms}</td>
|
||||
<td class="px-4 py-3 whitespace-nowrap text-sm text-gray-900 font-semibold">${amountDisplay}</td>
|
||||
<td class="px-4 py-3 whitespace-nowrap text-sm font-medium space-x-1">
|
||||
${editBtn} ${qboBtn} ${pdfBtn} ${htmlBtn} ${paidBtn} ${delBtn}
|
||||
${editBtn} ${qboBtn} ${pdfBtn} ${htmlBtn} ${sendBtn} ${paidBtn} ${delBtn}
|
||||
</td>
|
||||
</tr>`;
|
||||
}
|
||||
|
|
@ -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
|
||||
};
|
||||
67
server.js
67
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
|
||||
|
|
|
|||
Loading…
Reference in New Issue