stripe
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
// email-modal.js — ES Module
|
||||
// Modal to review and send invoice emails via AWS SES
|
||||
// With Stripe Payment Link integration
|
||||
|
||||
import { showSpinner, hideSpinner } from '../utils/helpers.js';
|
||||
|
||||
@@ -25,8 +26,19 @@ function renderModalContent() {
|
||||
if (!modal) return;
|
||||
|
||||
const defaultEmail = currentInvoice.email || '';
|
||||
|
||||
// Editor-Container hat jetzt eine feste, kompaktere Höhe (h-48 = 12rem/192px) und scrollt bei viel Text
|
||||
const existingStripeUrl = currentInvoice.stripe_payment_link_url || '';
|
||||
const stripeStatus = currentInvoice.stripe_payment_status || '';
|
||||
|
||||
// Status indicator for existing link
|
||||
let stripeBadgeHtml = '';
|
||||
if (existingStripeUrl && stripeStatus === 'paid') {
|
||||
stripeBadgeHtml = '<span class="ml-2 inline-block px-2 py-0.5 text-xs font-semibold rounded-full bg-green-100 text-green-800">Paid</span>';
|
||||
} else if (existingStripeUrl && stripeStatus === 'processing') {
|
||||
stripeBadgeHtml = '<span class="ml-2 inline-block px-2 py-0.5 text-xs font-semibold rounded-full bg-yellow-100 text-yellow-800">Processing</span>';
|
||||
} else if (existingStripeUrl) {
|
||||
stripeBadgeHtml = '<span class="ml-2 inline-block px-2 py-0.5 text-xs font-semibold rounded-full bg-purple-100 text-purple-800">Active</span>';
|
||||
}
|
||||
|
||||
modal.innerHTML = `
|
||||
<div class="bg-white rounded-lg shadow-2xl w-full max-w-3xl mx-auto p-8">
|
||||
<div class="flex justify-between items-center mb-6">
|
||||
@@ -47,9 +59,23 @@ function renderModalContent() {
|
||||
<p class="text-xs text-gray-400 mt-1">You can override this for testing.</p>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Melio Payment Link (Optional)</label>
|
||||
<input type="url" id="email-melio-link" placeholder="https://melio.me/..."
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-blue-500 focus:border-blue-500">
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">
|
||||
Stripe Payment Link${stripeBadgeHtml}
|
||||
</label>
|
||||
<div class="flex gap-2">
|
||||
<input type="url" id="email-stripe-link" value="${existingStripeUrl}" readonly
|
||||
placeholder="Click Generate to create link..."
|
||||
class="flex-1 px-3 py-2 border border-gray-300 rounded-md bg-gray-50 text-sm text-gray-600 focus:ring-purple-500 focus:border-purple-500">
|
||||
<button type="button" id="stripe-generate-btn" onclick="window.emailModal.generateStripeLink()"
|
||||
class="px-4 py-2 bg-purple-600 text-white rounded-md hover:bg-purple-700 text-sm font-semibold whitespace-nowrap">
|
||||
${existingStripeUrl ? '♻️ Regenerate' : '💳 Generate'}
|
||||
</button>
|
||||
</div>
|
||||
<p class="text-xs text-gray-400 mt-1" id="stripe-link-info">
|
||||
${existingStripeUrl
|
||||
? 'Link exists. Regenerate will create a new link for the current balance.'
|
||||
: 'Generates a Stripe Payment Link for Card and ACH payments.'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -100,20 +126,19 @@ function renderModalContent() {
|
||||
dueDateStr = d.toLocaleDateString('en-US', { timeZone: 'UTC' });
|
||||
}
|
||||
|
||||
// Dynamischer Text für die Fälligkeit (Löst das "payable by Upon Receipt" Problem)
|
||||
// Dynamischer Text für die Fälligkeit
|
||||
let paymentText = '';
|
||||
if (currentInvoice.terms && currentInvoice.terms.toLowerCase().includes('receipt')) {
|
||||
paymentText = 'which is due upon receipt.';
|
||||
paymentText = 'Our terms are Net 30.';
|
||||
} else if (dueDateStr !== 'Upon Receipt') {
|
||||
paymentText = `payable by <strong>${dueDateStr}</strong>.`;
|
||||
} else {
|
||||
paymentText = 'which is due upon receipt.';
|
||||
paymentText = 'Our terms are Net 30.';
|
||||
}
|
||||
|
||||
// Der neue Standard-Text
|
||||
const defaultHtml = `
|
||||
<p>Good afternoon,</p>
|
||||
<p>Attached is invoice <strong>#${invoiceNum}</strong> for service performed at your location. The total amount due is <strong>$${totalDue}</strong>, ${paymentText}</p>
|
||||
<p>Attached is invoice <strong>#${invoiceNum}</strong> for service performed at your location. The total amount due is <strong>$${totalDue}</strong>. ${paymentText}</p>
|
||||
<p>Please pay at your earliest convenience. We appreciate your continued business.</p>
|
||||
<p>If you have any questions about the invoice, feel free to reply to this email.</p>
|
||||
<p>Best regards,</p>
|
||||
@@ -127,6 +152,55 @@ function renderModalContent() {
|
||||
document.getElementById('email-send-form').addEventListener('submit', submitEmail);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Stripe Payment Link Generation
|
||||
// ============================================================
|
||||
|
||||
async function generateStripeLink() {
|
||||
const btn = document.getElementById('stripe-generate-btn');
|
||||
const input = document.getElementById('email-stripe-link');
|
||||
const info = document.getElementById('stripe-link-info');
|
||||
|
||||
const originalBtnText = btn.innerHTML;
|
||||
btn.innerHTML = '⏳ Creating...';
|
||||
btn.disabled = true;
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/invoices/${currentInvoice.id}/create-payment-link`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (response.ok) {
|
||||
input.value = result.paymentLinkUrl;
|
||||
currentInvoice.stripe_payment_link_url = result.paymentLinkUrl;
|
||||
currentInvoice.stripe_payment_link_id = result.paymentLinkId;
|
||||
currentInvoice.stripe_payment_status = 'pending';
|
||||
|
||||
btn.innerHTML = '♻️ Regenerate';
|
||||
info.innerHTML = `✅ Payment link created for <strong>$${result.amount.toFixed(2)}</strong>. Will be included in the email.`;
|
||||
info.classList.remove('text-gray-400');
|
||||
info.classList.add('text-green-600');
|
||||
} else {
|
||||
info.textContent = `❌ ${result.error}`;
|
||||
info.classList.remove('text-gray-400');
|
||||
info.classList.add('text-red-500');
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Stripe link generation error:', e);
|
||||
info.textContent = '❌ Network error creating payment link.';
|
||||
info.classList.remove('text-gray-400');
|
||||
info.classList.add('text-red-500');
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
if (btn.innerHTML === '⏳ Creating...') {
|
||||
btn.innerHTML = originalBtnText;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Logic & API
|
||||
// ============================================================
|
||||
@@ -145,7 +219,6 @@ export async function openEmailModal(invoiceId) {
|
||||
|
||||
renderModalContent();
|
||||
|
||||
// Tailwind hidden toggle
|
||||
document.getElementById('email-modal').classList.remove('hidden');
|
||||
document.getElementById('email-modal').classList.add('flex');
|
||||
|
||||
@@ -171,7 +244,6 @@ async function submitEmail(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const recipientEmail = document.getElementById('email-recipient').value.trim();
|
||||
const melioLink = document.getElementById('email-melio-link').value.trim();
|
||||
const customText = quillInstance.root.innerHTML;
|
||||
|
||||
if (!recipientEmail) {
|
||||
@@ -191,7 +263,6 @@ async function submitEmail(e) {
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
recipientEmail,
|
||||
melioLink,
|
||||
customText
|
||||
})
|
||||
});
|
||||
@@ -201,7 +272,6 @@ async function submitEmail(e) {
|
||||
if (response.ok) {
|
||||
alert('✅ Invoice sent successfully!');
|
||||
closeEmailModal();
|
||||
// Reload the invoice view so the "Sent" badge updates
|
||||
if (window.invoiceView) window.invoiceView.loadInvoices();
|
||||
} else {
|
||||
alert(`❌ Error: ${result.error}`);
|
||||
@@ -221,5 +291,6 @@ async function submitEmail(e) {
|
||||
// ============================================================
|
||||
window.emailModal = {
|
||||
open: openEmailModal,
|
||||
close: closeEmailModal
|
||||
close: closeEmailModal,
|
||||
generateStripeLink
|
||||
};
|
||||
Reference in New Issue
Block a user