| import os |
| import uuid |
| from datetime import datetime |
| from flask import Response, current_app |
| from PIL import Image |
| from reportlab.lib.pagesizes import letter |
| from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle |
| from reportlab.lib.units import inch |
| from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle |
| from reportlab.lib import colors |
| from io import BytesIO |
|
|
| ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg', 'gif'} |
|
|
| def allowed_file(filename): |
| """Check if the uploaded file has an allowed extension.""" |
| return '.' in filename and \ |
| filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS |
|
|
| def save_uploaded_image(file): |
| """Save an uploaded image file with a unique filename.""" |
| if not file: |
| return None |
| |
| |
| filename = str(uuid.uuid4()) + '.' + file.filename.rsplit('.', 1)[1].lower() |
| filepath = os.path.join(current_app.config['UPLOAD_FOLDER'], filename) |
| |
| |
| try: |
| image = Image.open(file.stream) |
| |
| |
| if image.mode in ('RGBA', 'LA'): |
| background = Image.new('RGB', image.size, (255, 255, 255)) |
| background.paste(image, mask=image.split()[-1] if image.mode == 'RGBA' else None) |
| image = background |
| |
| |
| max_size = 1920 |
| if max(image.size) > max_size: |
| image.thumbnail((max_size, max_size), Image.Resampling.LANCZOS) |
| |
| |
| image.save(filepath, optimize=True, quality=85) |
| |
| except Exception as e: |
| current_app.logger.error(f"Error processing image: {e}") |
| |
| file.seek(0) |
| file.save(filepath) |
| |
| return filename |
|
|
| def generate_treatment_pdf(treatment_plan): |
| """Generate a PDF for the treatment plan.""" |
| buffer = BytesIO() |
| doc = SimpleDocTemplate(buffer, pagesize=letter) |
| |
| |
| styles = getSampleStyleSheet() |
| title_style = ParagraphStyle( |
| 'CustomTitle', |
| parent=styles['Heading1'], |
| fontSize=18, |
| spaceAfter=30, |
| textColor=colors.HexColor('#0F766E') |
| ) |
| |
| heading_style = ParagraphStyle( |
| 'CustomHeading', |
| parent=styles['Heading2'], |
| fontSize=14, |
| spaceBefore=20, |
| spaceAfter=10, |
| textColor=colors.HexColor('#1E40AF') |
| ) |
| |
| |
| content = [] |
| |
| |
| content.append(Paragraph("Doctor's Sidekick - Treatment Plan", title_style)) |
| content.append(Spacer(1, 20)) |
| |
| |
| patient_info = [ |
| ['Patient:', treatment_plan.patient.full_name], |
| ['Patient ID:', treatment_plan.patient.patient_id], |
| ['Date of Birth:', treatment_plan.patient.date_of_birth.strftime('%B %d, %Y')], |
| ['Plan Date:', treatment_plan.created_at.strftime('%B %d, %Y')], |
| ['Created By:', treatment_plan.created_by.full_name] |
| ] |
| |
| patient_table = Table(patient_info, colWidths=[1.5*inch, 4*inch]) |
| patient_table.setStyle(TableStyle([ |
| ('ALIGN', (0, 0), (-1, -1), 'LEFT'), |
| ('FONTNAME', (0, 0), (0, -1), 'Helvetica-Bold'), |
| ('FONTSIZE', (0, 0), (-1, -1), 12), |
| ('BOTTOMPADDING', (0, 0), (-1, -1), 6), |
| ])) |
| |
| content.append(patient_table) |
| content.append(Spacer(1, 30)) |
| |
| |
| content.append(Paragraph("Diagnosis", heading_style)) |
| content.append(Paragraph(treatment_plan.diagnosis or "Not specified", styles['Normal'])) |
| content.append(Spacer(1, 15)) |
| |
| |
| if treatment_plan.medications: |
| content.append(Paragraph("Medications", heading_style)) |
| content.append(Paragraph(treatment_plan.medications, styles['Normal'])) |
| content.append(Spacer(1, 15)) |
| |
| |
| if treatment_plan.instructions: |
| content.append(Paragraph("Instructions", heading_style)) |
| content.append(Paragraph(treatment_plan.instructions, styles['Normal'])) |
| content.append(Spacer(1, 15)) |
| |
| |
| if treatment_plan.duration or treatment_plan.follow_up_date: |
| content.append(Paragraph("Treatment Details", heading_style)) |
| |
| if treatment_plan.duration: |
| content.append(Paragraph(f"<b>Duration:</b> {treatment_plan.duration}", styles['Normal'])) |
| |
| if treatment_plan.follow_up_date: |
| content.append(Paragraph( |
| f"<b>Follow-up Date:</b> {treatment_plan.follow_up_date.strftime('%B %d, %Y')}", |
| styles['Normal'] |
| )) |
| |
| content.append(Spacer(1, 15)) |
| |
| |
| if treatment_plan.notes: |
| content.append(Paragraph("Additional Notes", heading_style)) |
| content.append(Paragraph(treatment_plan.notes, styles['Normal'])) |
| |
| |
| content.append(Spacer(1, 50)) |
| footer_style = ParagraphStyle( |
| 'Footer', |
| parent=styles['Normal'], |
| fontSize=10, |
| textColor=colors.grey, |
| alignment=1 |
| ) |
| content.append(Paragraph( |
| f"Generated on {datetime.now().strftime('%B %d, %Y at %I:%M %p')}", |
| footer_style |
| )) |
| |
| |
| doc.build(content) |
| |
| |
| buffer.seek(0) |
| return Response( |
| buffer.getvalue(), |
| mimetype='application/pdf', |
| headers={ |
| 'Content-Disposition': f'attachment; filename=treatment_plan_{treatment_plan.id}.pdf' |
| } |
| ) |
|
|
| def calculate_age(birth_date): |
| """Calculate age from birth date.""" |
| today = datetime.now().date() |
| return today.year - birth_date.year - ((today.month, today.day) < (birth_date.month, birth_date.day)) |
|
|
| def format_phone_number(phone): |
| """Format phone number for display.""" |
| if not phone: |
| return "" |
| |
| |
| digits = ''.join(filter(str.isdigit, phone)) |
| |
| |
| if len(digits) == 10: |
| return f"({digits[:3]}) {digits[3:6]}-{digits[6:]}" |
| |
| return phone |
|
|
| def get_dashboard_stats(): |
| """Get statistics for the dashboard.""" |
| from models import Patient, Visit, Inventory |
| from datetime import date, timedelta |
| |
| today = date.today() |
| week_ago = today - timedelta(days=7) |
| |
| stats = { |
| 'total_patients': Patient.query.count(), |
| 'new_patients_week': Patient.query.filter(Patient.created_at >= week_ago).count(), |
| 'visits_today': Visit.query.filter(Visit.visit_date >= today).count(), |
| 'low_stock_items': Inventory.query.filter( |
| Inventory.current_stock <= Inventory.min_threshold |
| ).count() |
| } |
| |
| return stats |
|
|