mirror of
https://github.com/DRYTRIX/TimeTracker.git
synced 2026-01-28 07:28:59 -06:00
- Fix script block name from extra_js to scripts_extra to match base.html - Replace inline onclick handlers with event listeners to fix scope issues - Fix ReferenceError for toggleViewMode and insertVariable functions - Improve editor initialization flow with proper script loading detection - Add error handling and fallback to textarea if Toast UI Editor fails to load - Add debug logging for troubleshooting initialization issues - Ensure default templates are editable (no restrictions in backend) - Add email templates link to admin menu in base.html - Remove ENV file configuration details from email support page The editor now properly initializes and all interactive features work correctly.
101 lines
3.9 KiB
Python
101 lines
3.9 KiB
Python
from datetime import datetime
|
|
from app import db
|
|
from app.utils.timezone import now_in_app_timezone
|
|
|
|
|
|
def local_now():
|
|
"""Get current time in local timezone as naive datetime (for database storage)"""
|
|
return now_in_app_timezone().replace(tzinfo=None)
|
|
|
|
|
|
class InvoiceEmail(db.Model):
|
|
"""Model for tracking invoice emails sent to clients"""
|
|
|
|
__tablename__ = 'invoice_emails'
|
|
|
|
id = db.Column(db.Integer, primary_key=True)
|
|
invoice_id = db.Column(db.Integer, db.ForeignKey('invoices.id'), nullable=False, index=True)
|
|
|
|
# Email details
|
|
recipient_email = db.Column(db.String(200), nullable=False)
|
|
subject = db.Column(db.String(500), nullable=False)
|
|
sent_at = db.Column(db.DateTime, nullable=False, default=local_now)
|
|
sent_by = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
|
|
|
|
# Tracking
|
|
opened_at = db.Column(db.DateTime, nullable=True) # When email was opened (if tracked)
|
|
opened_count = db.Column(db.Integer, nullable=False, default=0) # Number of times opened
|
|
last_opened_at = db.Column(db.DateTime, nullable=True) # Last time email was opened
|
|
|
|
# Payment tracking
|
|
paid_at = db.Column(db.DateTime, nullable=True) # When invoice was marked as paid (if after email)
|
|
|
|
# Status
|
|
status = db.Column(db.String(20), nullable=False, default='sent') # 'sent', 'opened', 'paid', 'bounced', 'failed'
|
|
|
|
# Error tracking
|
|
error_message = db.Column(db.Text, nullable=True) # Error message if send failed
|
|
|
|
# Metadata
|
|
created_at = db.Column(db.DateTime, default=local_now, nullable=False)
|
|
updated_at = db.Column(db.DateTime, default=local_now, onupdate=local_now, nullable=False)
|
|
|
|
# Relationships
|
|
invoice = db.relationship('Invoice', backref='email_records')
|
|
sender = db.relationship('User', backref='sent_invoice_emails')
|
|
|
|
def __init__(self, invoice_id, recipient_email, subject, sent_by, **kwargs):
|
|
self.invoice_id = invoice_id
|
|
self.recipient_email = recipient_email
|
|
self.subject = subject
|
|
self.sent_by = sent_by
|
|
self.status = kwargs.get('status', 'sent')
|
|
self.error_message = kwargs.get('error_message')
|
|
|
|
def __repr__(self):
|
|
return f'<InvoiceEmail {self.invoice_id} -> {self.recipient_email} ({self.status})>'
|
|
|
|
def mark_opened(self):
|
|
"""Mark email as opened"""
|
|
if not self.opened_at:
|
|
self.opened_at = local_now()
|
|
self.last_opened_at = local_now()
|
|
self.opened_count += 1
|
|
if self.status == 'sent':
|
|
self.status = 'opened'
|
|
|
|
def mark_paid(self):
|
|
"""Mark invoice as paid (after email was sent)"""
|
|
if not self.paid_at:
|
|
self.paid_at = local_now()
|
|
self.status = 'paid'
|
|
|
|
def mark_failed(self, error_message):
|
|
"""Mark email send as failed"""
|
|
self.status = 'failed'
|
|
self.error_message = error_message
|
|
|
|
def mark_bounced(self):
|
|
"""Mark email as bounced"""
|
|
self.status = 'bounced'
|
|
|
|
def to_dict(self):
|
|
"""Convert invoice email to dictionary"""
|
|
return {
|
|
'id': self.id,
|
|
'invoice_id': self.invoice_id,
|
|
'recipient_email': self.recipient_email,
|
|
'subject': self.subject,
|
|
'sent_at': self.sent_at.isoformat() if self.sent_at else None,
|
|
'sent_by': self.sent_by,
|
|
'opened_at': self.opened_at.isoformat() if self.opened_at else None,
|
|
'opened_count': self.opened_count,
|
|
'last_opened_at': self.last_opened_at.isoformat() if self.last_opened_at else None,
|
|
'paid_at': self.paid_at.isoformat() if self.paid_at else None,
|
|
'status': self.status,
|
|
'error_message': self.error_message,
|
|
'created_at': self.created_at.isoformat() if self.created_at else None,
|
|
'updated_at': self.updated_at.isoformat() if self.updated_at else None
|
|
}
|
|
|