from datetime import datetime, date, timedelta import json from flask_login import UserMixin from werkzeug.security import generate_password_hash, check_password_hash from . import db service_professionals = db.Table( 'service_professionals', db.Column('service_id', db.Integer, db.ForeignKey('service.id'), primary_key=True), db.Column('professional_id', db.Integer, db.ForeignKey('professional_profile.id'), primary_key=True) ) class TimestampMixin: created_at = db.Column(db.DateTime, default=datetime.utcnow) updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) class Institution(TimestampMixin, db.Model): __tablename__ = 'institution' id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String(180), nullable=False, index=True) cuit = db.Column(db.String(30), unique=True, index=True) domicilio = db.Column(db.String(220)) ciudad = db.Column(db.String(120)) provincia = db.Column(db.String(120)) telefono = db.Column(db.String(80)) responsable = db.Column(db.String(160)) situacion_juridica = db.Column(db.String(120)) nro_habilitacion = db.Column(db.String(120)) expediente_autorizacion = db.Column(db.String(160)) email = db.Column(db.String(160)) telefono_responsable = db.Column(db.String(80)) logo_path = db.Column(db.String(255)) active = db.Column(db.Boolean, default=True, index=True) users = db.relationship('User', back_populates='institution') professionals = db.relationship('ProfessionalProfile', back_populates='institution') patients = db.relationship('Patient', back_populates='institution') branches = db.relationship('InstitutionBranch', back_populates='institution', cascade='all, delete-orphan') @property def display_name(self): return self.name or f'Institución #{self.id}' class InstitutionBranch(TimestampMixin, db.Model): __tablename__ = 'institution_branch' id = db.Column(db.Integer, primary_key=True) institution_id = db.Column(db.Integer, db.ForeignKey('institution.id'), nullable=False, index=True) name = db.Column(db.String(160), nullable=False) address = db.Column(db.String(220)) province = db.Column(db.String(120), index=True) city = db.Column(db.String(120), index=True) phone = db.Column(db.String(80)) email = db.Column(db.String(160)) notes = db.Column(db.Text) active = db.Column(db.Boolean, default=True, index=True) institution = db.relationship('Institution', back_populates='branches') appointments = db.relationship('Appointment', back_populates='branch') @property def display_name(self): return self.name or f'Sede #{self.id}' @property def full_address(self): bits = [self.address, self.city, self.province] return ', '.join([b for b in bits if b]) class User(UserMixin, TimestampMixin, db.Model): id = db.Column(db.Integer, primary_key=True) full_name = db.Column(db.String(150), nullable=False) email = db.Column(db.String(150), unique=True, nullable=False) password_hash = db.Column(db.String(255), nullable=False) role = db.Column(db.String(30), nullable=False, default='professional') is_active_user = db.Column(db.Boolean, default=True) institution_id = db.Column(db.Integer, db.ForeignKey('institution.id'), index=True) institution = db.relationship('Institution', back_populates='users') professional_profile = db.relationship('ProfessionalProfile', back_populates='user', uselist=False) issued_prescriptions = db.relationship('Prescription', back_populates='issued_by_user', foreign_keys='Prescription.issued_by_user_id') def set_password(self, password): self.password_hash = generate_password_hash(password) def check_password(self, password): return check_password_hash(self.password_hash, password) @property def is_active(self): return self.is_active_user class Specialty(TimestampMixin, db.Model): id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String(150), unique=True, nullable=False) active = db.Column(db.Boolean, default=True) class AppSetting(TimestampMixin, db.Model): id = db.Column(db.Integer, primary_key=True) key = db.Column(db.String(100), unique=True, nullable=False) value = db.Column(db.Text) class ProfessionalProfile(TimestampMixin, db.Model): id = db.Column(db.Integer, primary_key=True) user_id = db.Column(db.Integer, db.ForeignKey('user.id'), unique=True) display_name = db.Column(db.String(150), nullable=False) specialty = db.Column(db.String(150), nullable=False) bio = db.Column(db.Text) location = db.Column(db.String(150), default='Consultorio Central') phone = db.Column(db.String(50)) contact_email = db.Column(db.String(150)) is_bookable = db.Column(db.Boolean, default=True) color = db.Column(db.String(20), default='#0d6efd') institution_id = db.Column(db.Integer, db.ForeignKey('institution.id'), index=True) source_mode = db.Column(db.String(20), default='manual') matricula = db.Column(db.String(100)) profession_name = db.Column(db.String(150)) jurisdiction_name = db.Column(db.String(150)) state_name = db.Column(db.String(120)) province = db.Column(db.String(120)) municipality = db.Column(db.String(120)) city = db.Column(db.String(120)) address = db.Column(db.String(200)) address_number = db.Column(db.String(30)) specialty_id = db.Column(db.Integer, db.ForeignKey('specialty.id')) institution = db.relationship('Institution', back_populates='professionals') user = db.relationship('User', back_populates='professional_profile') specialty_manual = db.relationship('Specialty') working_hours = db.relationship('WorkingHour', back_populates='professional', cascade='all, delete-orphan') leaves = db.relationship('Leave', back_populates='professional', cascade='all, delete-orphan') appointments = db.relationship('Appointment', back_populates='professional') services = db.relationship('Service', secondary=service_professionals, back_populates='professionals') prescriptions = db.relationship('Prescription', back_populates='professional') @property def full_address(self): bits = [self.address, self.address_number, self.city, self.municipality, self.province] return ', '.join([b for b in bits if b]) class Service(TimestampMixin, db.Model): id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String(150), nullable=False, unique=True) description = db.Column(db.Text) duration_minutes = db.Column(db.Integer, nullable=False, default=30) buffer_before_minutes = db.Column(db.Integer, nullable=False, default=0) buffer_after_minutes = db.Column(db.Integer, nullable=False, default=0) price = db.Column(db.Float, default=0) mode = db.Column(db.String(30), default='Presencial') active = db.Column(db.Boolean, default=True) institution_id = db.Column(db.Integer, db.ForeignKey('institution.id'), index=True) min_notice_hours = db.Column(db.Integer, default=4) cancel_notice_hours = db.Column(db.Integer, default=12) allow_online_cancel = db.Column(db.Boolean, default=True) institution = db.relationship('Institution') appointments = db.relationship('Appointment', back_populates='service') professionals = db.relationship('ProfessionalProfile', secondary=service_professionals, back_populates='services') class WorkingHour(TimestampMixin, db.Model): id = db.Column(db.Integer, primary_key=True) professional_id = db.Column(db.Integer, db.ForeignKey('professional_profile.id'), nullable=False) weekday = db.Column(db.Integer, nullable=False) start_time = db.Column(db.Time, nullable=False) end_time = db.Column(db.Time, nullable=False) is_active = db.Column(db.Boolean, default=True) professional = db.relationship('ProfessionalProfile', back_populates='working_hours') class Leave(TimestampMixin, db.Model): id = db.Column(db.Integer, primary_key=True) professional_id = db.Column(db.Integer, db.ForeignKey('professional_profile.id'), nullable=False) start_date = db.Column(db.Date, nullable=False) end_date = db.Column(db.Date, nullable=False) reason = db.Column(db.String(200), nullable=False) professional = db.relationship('ProfessionalProfile', back_populates='leaves') class ObraSocialPageSnapshot(TimestampMixin, db.Model): id = db.Column(db.Integer, primary_key=True) tipo = db.Column(db.Integer, unique=True, nullable=False) categoria_oficial = db.Column(db.String(255)) origen_datos = db.Column(db.String(255)) status = db.Column(db.String(50), default='never_synced') row_count = db.Column(db.Integer, default=0) last_hash = db.Column(db.String(128)) last_error = db.Column(db.Text) last_synced_at = db.Column(db.DateTime) class ObraSocialCatalog(TimestampMixin, db.Model): __tablename__ = 'obras_sociales_catalogo' id = db.Column(db.Integer, primary_key=True) tipo = db.Column(db.Integer, index=True) categoria_oficial = db.Column(db.String(255), index=True) rnas = db.Column(db.String(50), index=True) denominacion = db.Column(db.String(255), nullable=False, index=True) domicilio = db.Column(db.String(255)) localidad = db.Column(db.String(255), index=True) telefono = db.Column(db.String(120)) linea_gratuita = db.Column(db.String(120)) habilitada_opciones = db.Column(db.String(50), index=True) vigente = db.Column(db.Boolean, default=True, index=True) row_hash = db.Column(db.String(128)) last_seen_at = db.Column(db.DateTime) page_snapshot_id = db.Column(db.Integer, db.ForeignKey('obra_social_page_snapshot.id')) class Patient(TimestampMixin, db.Model): id = db.Column(db.Integer, primary_key=True) nombre = db.Column(db.String(120), nullable=False) apellido = db.Column(db.String(120), nullable=False) documento = db.Column(db.String(40), unique=True, nullable=False, index=True) tipo_documento = db.Column(db.String(30), default='DNI') fecha_nacimiento = db.Column(db.String(20)) genero = db.Column(db.String(30)) telefono = db.Column(db.String(60)) email = db.Column(db.String(150)) calle = db.Column(db.String(150)) numero = db.Column(db.String(30)) piso = db.Column(db.String(30)) provincia = db.Column(db.String(120)) municipio = db.Column(db.String(120)) localidad = db.Column(db.String(120)) cp = db.Column(db.String(20)) obra_social_id = db.Column(db.Integer, db.ForeignKey('obras_sociales_catalogo.id')) afiliado_nro = db.Column(db.String(80)) observaciones = db.Column(db.Text) nombre_contacto = db.Column(db.String(150)) telefono_contacto = db.Column(db.String(60)) estado = db.Column(db.String(30), default='Activo', index=True) fecha_creacion = db.Column(db.DateTime, default=datetime.utcnow) institution_id = db.Column(db.Integer, db.ForeignKey('institution.id'), index=True) institution = db.relationship('Institution', back_populates='patients') obra_social = db.relationship('ObraSocialCatalog') appointments = db.relationship('Appointment', back_populates='patient') prescriptions = db.relationship('Prescription', back_populates='patient') @property def nombre_completo(self): return f"{self.apellido}, {self.nombre}" @property def domicilio_completo(self): bits = [self.calle, self.numero, self.piso, self.localidad, self.municipio, self.provincia] return ', '.join([b for b in bits if b]) class Appointment(TimestampMixin, db.Model): id = db.Column(db.Integer, primary_key=True) service_id = db.Column(db.Integer, db.ForeignKey('service.id'), nullable=False) professional_id = db.Column(db.Integer, db.ForeignKey('professional_profile.id'), nullable=False) patient_id = db.Column(db.Integer, db.ForeignKey('patient.id')) client_name = db.Column(db.String(150), nullable=False) client_email = db.Column(db.String(150), nullable=False) client_phone = db.Column(db.String(50)) notes = db.Column(db.Text) appointment_date = db.Column(db.Date, nullable=False) start_time = db.Column(db.Time, nullable=False) end_time = db.Column(db.Time, nullable=False) status = db.Column(db.String(30), default='confirmed') booking_source = db.Column(db.String(50), default='website') public_token = db.Column(db.String(120), unique=True, nullable=False) internal_notes = db.Column(db.Text) reminder_sent = db.Column(db.Boolean, default=False) institution_id = db.Column(db.Integer, db.ForeignKey('institution.id'), index=True) branch_id = db.Column(db.Integer, db.ForeignKey('institution_branch.id'), index=True) institution = db.relationship('Institution') branch = db.relationship('InstitutionBranch', back_populates='appointments') service = db.relationship('Service', back_populates='appointments') professional = db.relationship('ProfessionalProfile', back_populates='appointments') patient = db.relationship('Patient', back_populates='appointments') @property def starts_at(self): return datetime.combine(self.appointment_date, self.start_time) @property def ends_at(self): return datetime.combine(self.appointment_date, self.end_time) @property def is_future(self): return self.starts_at >= datetime.now() class Prescription(TimestampMixin, db.Model): id = db.Column(db.Integer, primary_key=True) patient_id = db.Column(db.Integer, db.ForeignKey('patient.id'), nullable=False, index=True) professional_id = db.Column(db.Integer, db.ForeignKey('professional_profile.id'), nullable=False, index=True) issued_by_user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False) issued_at = db.Column(db.DateTime, default=datetime.utcnow, nullable=False, index=True) prescription_date = db.Column(db.Date, nullable=False, default=date.today) expires_at = db.Column(db.Date, nullable=False) status = db.Column(db.String(30), default='Activa', nullable=False, index=True) institution_id = db.Column(db.Integer, db.ForeignKey('institution.id'), index=True) legal_number = db.Column(db.String(40), unique=True, nullable=False, index=True) cuir = db.Column(db.String(50), unique=True, nullable=False, index=True) platform_number = db.Column(db.String(4)) repository_number = db.Column(db.String(4)) jurisdiction_code = db.Column(db.String(2)) prescription_type = db.Column(db.String(2), default='01') prescription_subtype = db.Column(db.String(2), default='02') prescription_group = db.Column(db.String(25)) item_number = db.Column(db.String(2), default='01') document_kind = db.Column(db.String(30), default='recipe', nullable=False, index=True) document_title = db.Column(db.String(255)) document_body = db.Column(db.Text) portal_visible = db.Column(db.Boolean, default=True, nullable=False) patient_full_name = db.Column(db.String(150), nullable=False) patient_document = db.Column(db.String(40), nullable=False) patient_birth_date = db.Column(db.String(20)) patient_gender = db.Column(db.String(30)) patient_obra_social = db.Column(db.String(255)) patient_plan = db.Column(db.String(120)) professional_display_name = db.Column(db.String(150), nullable=False) professional_profession_name = db.Column(db.String(150)) professional_specialty = db.Column(db.String(150)) professional_matricula = db.Column(db.String(120)) professional_jurisdiction_name = db.Column(db.String(150)) professional_address = db.Column(db.String(255)) medication_generic_name = db.Column(db.String(255), nullable=False) medication_presentation = db.Column(db.String(255)) pharmaceutical_form = db.Column(db.String(255)) quantity_units = db.Column(db.String(120)) diagnosis = db.Column(db.String(255)) dosage_instructions = db.Column(db.Text) barcode_value = db.Column(db.String(64)) platform_registry_legend = db.Column(db.String(255)) internal_notes = db.Column(db.Text) suspended_at = db.Column(db.DateTime) suspended_reason = db.Column(db.String(255)) institution = db.relationship('Institution') patient = db.relationship('Patient', back_populates='prescriptions') professional = db.relationship('ProfessionalProfile', back_populates='prescriptions') issued_by_user = db.relationship('User', back_populates='issued_prescriptions', foreign_keys=[issued_by_user_id]) @property def computed_status(self): if (self.status or '').lower() in {'suspendida', 'anulada'}: return self.status if self.expires_at and date.today() > self.expires_at: return 'Vencida' return self.status or 'Activa' @property def patient_document_last4(self): digits = ''.join(ch for ch in (self.patient_document or '') if ch.isdigit()) return digits[-4:] if len(digits) >= 4 else digits class BackupRecord(TimestampMixin, db.Model): id = db.Column(db.Integer, primary_key=True) scope = db.Column(db.String(30), nullable=False, default='full', index=True) provider = db.Column(db.String(30), nullable=False, default='local') filename = db.Column(db.String(255), nullable=False) relative_path = db.Column(db.String(255)) target_path = db.Column(db.String(255)) status = db.Column(db.String(30), nullable=False, default='created', index=True) size_bytes = db.Column(db.Integer, default=0) sha256 = db.Column(db.String(128)) notes = db.Column(db.Text) class FrontendBlock(TimestampMixin, db.Model): id = db.Column(db.Integer, primary_key=True) section = db.Column(db.String(80), nullable=False, index=True) key = db.Column(db.String(80), default='default', index=True) title = db.Column(db.String(255)) subtitle = db.Column(db.String(255)) body = db.Column(db.Text) icon = db.Column(db.String(80)) image_path = db.Column(db.String(255)) link_url = db.Column(db.String(255)) link_text = db.Column(db.String(120)) sort_order = db.Column(db.Integer, default=0, index=True) is_active = db.Column(db.Boolean, default=True, index=True) extra_json = db.Column(db.Text) class ContactInquiry(TimestampMixin, db.Model): id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String(150), nullable=False) email = db.Column(db.String(150), nullable=False, index=True) phone = db.Column(db.String(60)) detail = db.Column(db.Text, nullable=False) status = db.Column(db.String(30), default='No Leido', index=True) source = db.Column(db.String(50), default='frontend') read_at = db.Column(db.DateTime) class ClinicalRecord(TimestampMixin, db.Model): id = db.Column(db.Integer, primary_key=True) patient_id = db.Column(db.Integer, db.ForeignKey('patient.id'), nullable=False, unique=True, index=True) legajo_number = db.Column(db.String(80), nullable=False, unique=True, index=True) opened_at = db.Column(db.DateTime, default=datetime.utcnow, nullable=False) status = db.Column(db.String(30), default='Activo', index=True) confidentiality_level = db.Column(db.String(30), default='Restringido') retention_until = db.Column(db.Date) notes = db.Column(db.Text) summary_payload = db.Column(db.Text) institution_id = db.Column(db.Integer, db.ForeignKey('institution.id'), index=True) patient = db.relationship('Patient', backref=db.backref('clinical_record', uselist=False, cascade='all, delete-orphan')) episodes = db.relationship('ClinicalEpisode', back_populates='record', cascade='all, delete-orphan', order_by='ClinicalEpisode.started_at.desc()') entries = db.relationship('ClinicalEntry', back_populates='record', cascade='all, delete-orphan', order_by='ClinicalEntry.entry_datetime.desc()') class ClinicalEpisode(TimestampMixin, db.Model): id = db.Column(db.Integer, primary_key=True) record_id = db.Column(db.Integer, db.ForeignKey('clinical_record.id'), nullable=False, index=True) patient_id = db.Column(db.Integer, db.ForeignKey('patient.id'), nullable=False, index=True) created_by_user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False) title = db.Column(db.String(180), nullable=False) specialty_name = db.Column(db.String(150)) reason = db.Column(db.String(255)) diagnosis_summary = db.Column(db.String(255)) care_level = db.Column(db.String(80), default='Ambulatorio') status = db.Column(db.String(30), default='Abierto', index=True) visibility_scope = db.Column(db.String(30), default='Institucional') started_at = db.Column(db.DateTime, default=datetime.utcnow, nullable=False, index=True) closed_at = db.Column(db.DateTime) notes = db.Column(db.Text) institution_id = db.Column(db.Integer, db.ForeignKey('institution.id'), index=True) record = db.relationship('ClinicalRecord', back_populates='episodes') patient = db.relationship('Patient', backref=db.backref('clinical_episodes', lazy='dynamic')) created_by_user = db.relationship('User', backref=db.backref('clinical_episodes_created', lazy='dynamic')) members = db.relationship('ClinicalEpisodeMember', back_populates='episode', cascade='all, delete-orphan', order_by='ClinicalEpisodeMember.created_at.asc()') entries = db.relationship('ClinicalEntry', back_populates='episode', order_by='ClinicalEntry.entry_datetime.desc()') class ClinicalEpisodeTemplate(TimestampMixin, db.Model): """Template reutilizable para abrir episodios clínicos frecuentes. No representa una nueva Historia Clínica: solo guarda la estructura de apertura de un episodio para que el profesional pueda reutilizarla en otros pacientes. """ id = db.Column(db.Integer, primary_key=True) professional_id = db.Column(db.Integer, db.ForeignKey('professional_profile.id'), nullable=False, index=True) created_by_user_id = db.Column(db.Integer, db.ForeignKey('user.id'), index=True) title = db.Column(db.String(180), nullable=False, index=True) category = db.Column(db.String(120), default='General', index=True) specialty_name = db.Column(db.String(150), index=True) reason = db.Column(db.String(255)) diagnosis_summary = db.Column(db.String(255)) care_level = db.Column(db.String(80), default='Ambulatorio') visibility_scope = db.Column(db.String(30), default='Institucional') notes = db.Column(db.Text) source_episode_id = db.Column(db.Integer, db.ForeignKey('clinical_episode.id'), index=True) usage_count = db.Column(db.Integer, default=0) is_active = db.Column(db.Boolean, default=True, index=True) institution_id = db.Column(db.Integer, db.ForeignKey('institution.id'), index=True) professional = db.relationship('ProfessionalProfile', backref=db.backref('clinical_episode_templates', lazy='dynamic', cascade='all, delete-orphan')) created_by_user = db.relationship('User', backref=db.backref('clinical_episode_templates_created', lazy='dynamic')) source_episode = db.relationship('ClinicalEpisode', backref=db.backref('saved_as_episode_templates', lazy='dynamic')) def to_payload(self): return { 'id': self.id, 'title': self.title or '', 'category': self.category or 'General', 'specialty_name': self.specialty_name or '', 'reason': self.reason or '', 'diagnosis_summary': self.diagnosis_summary or '', 'care_level': self.care_level or 'Ambulatorio', 'visibility_scope': self.visibility_scope or 'Institucional', 'notes': self.notes or '', 'usage_count': self.usage_count or 0, } class ClinicalEpisodeMember(TimestampMixin, db.Model): id = db.Column(db.Integer, primary_key=True) episode_id = db.Column(db.Integer, db.ForeignKey('clinical_episode.id'), nullable=False, index=True) user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False, index=True) added_by_user_id = db.Column(db.Integer, db.ForeignKey('user.id')) role_label = db.Column(db.String(120), default='Integrante') can_view = db.Column(db.Boolean, default=True) can_write = db.Column(db.Boolean, default=False) can_sign = db.Column(db.Boolean, default=False) can_export = db.Column(db.Boolean, default=False) is_active = db.Column(db.Boolean, default=True, index=True) joined_at = db.Column(db.DateTime, default=datetime.utcnow) left_at = db.Column(db.DateTime) episode = db.relationship('ClinicalEpisode', back_populates='members') user = db.relationship('User', foreign_keys=[user_id], backref=db.backref('clinical_episode_memberships', lazy='dynamic')) added_by_user = db.relationship('User', foreign_keys=[added_by_user_id], backref=db.backref('clinical_episode_members_added', lazy='dynamic')) class ClinicalEntry(TimestampMixin, db.Model): id = db.Column(db.Integer, primary_key=True) record_id = db.Column(db.Integer, db.ForeignKey('clinical_record.id'), nullable=False, index=True) episode_id = db.Column(db.Integer, db.ForeignKey('clinical_episode.id'), index=True) patient_id = db.Column(db.Integer, db.ForeignKey('patient.id'), nullable=False, index=True) professional_id = db.Column(db.Integer, db.ForeignKey('professional_profile.id'), nullable=False, index=True) created_by_user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False) last_edited_by_user_id = db.Column(db.Integer, db.ForeignKey('user.id')) entry_datetime = db.Column(db.DateTime, default=datetime.utcnow, nullable=False, index=True) folio_number = db.Column(db.Integer, nullable=False, index=True) specialty_name = db.Column(db.String(150)) encounter_type = db.Column(db.String(80), default='Evolución médica') diagnosis_text = db.Column(db.String(255)) chief_complaint = db.Column(db.String(255)) provisional_diagnosis = db.Column(db.String(255)) specialty_template = db.Column(db.String(80), default='general_medicine', index=True) cie10_code = db.Column(db.String(20), index=True) snomed_term = db.Column(db.String(255)) snomed_code = db.Column(db.String(50), index=True) subjective = db.Column(db.Text) objective = db.Column(db.Text) assessment = db.Column(db.Text) plan = db.Column(db.Text) treatment = db.Column(db.Text) study_results = db.Column(db.Text) vitals_payload = db.Column(db.Text) structured_payload = db.Column(db.Text) consent_reference = db.Column(db.String(120)) entry_status = db.Column(db.String(30), default='Firmado', index=True) locked_at = db.Column(db.DateTime) signed_name = db.Column(db.String(150)) signed_hash = db.Column(db.String(128)) signature_payload = db.Column(db.Text) visibility_scope = db.Column(db.String(30), default='Institucional') edit_revision = db.Column(db.Integer, default=1) last_edited_at = db.Column(db.DateTime) institution_id = db.Column(db.Integer, db.ForeignKey('institution.id'), index=True) record = db.relationship('ClinicalRecord', back_populates='entries') episode = db.relationship('ClinicalEpisode', back_populates='entries') patient = db.relationship('Patient', backref=db.backref('clinical_entries', lazy='dynamic')) professional = db.relationship('ProfessionalProfile', backref=db.backref('clinical_entries', lazy='dynamic')) created_by_user = db.relationship('User', foreign_keys=[created_by_user_id], backref=db.backref('clinical_entries_created', lazy='dynamic')) last_edited_by_user = db.relationship('User', foreign_keys=[last_edited_by_user_id], backref=db.backref('clinical_entries_edited', lazy='dynamic')) attachments = db.relationship('ClinicalAttachment', back_populates='entry', cascade='all, delete-orphan') class ClinicalEntryTemplate(TimestampMixin, db.Model): id = db.Column(db.Integer, primary_key=True) professional_id = db.Column(db.Integer, db.ForeignKey('professional_profile.id'), nullable=False, index=True) created_by_user_id = db.Column(db.Integer, db.ForeignKey('user.id'), index=True) title = db.Column(db.String(180), nullable=False, index=True) category = db.Column(db.String(120), default='General', index=True) specialty_name = db.Column(db.String(150), index=True) specialty_template = db.Column(db.String(80), default='general_medicine', index=True) encounter_type = db.Column(db.String(80), default='Evolución médica') chief_complaint = db.Column(db.String(255)) provisional_diagnosis = db.Column(db.String(255)) diagnosis_text = db.Column(db.String(255)) cie10_code = db.Column(db.String(20)) snomed_term = db.Column(db.String(255)) snomed_code = db.Column(db.String(50)) subjective = db.Column(db.Text) objective = db.Column(db.Text) assessment = db.Column(db.Text) plan = db.Column(db.Text) treatment = db.Column(db.Text) study_results = db.Column(db.Text) vitals_payload = db.Column(db.Text) structured_payload = db.Column(db.Text) consent_reference = db.Column(db.String(120)) visibility_scope = db.Column(db.String(30), default='Institucional') source_entry_id = db.Column(db.Integer, db.ForeignKey('clinical_entry.id'), index=True) usage_count = db.Column(db.Integer, default=0) is_active = db.Column(db.Boolean, default=True, index=True) institution_id = db.Column(db.Integer, db.ForeignKey('institution.id'), index=True) professional = db.relationship('ProfessionalProfile', backref=db.backref('clinical_entry_templates', lazy='dynamic', cascade='all, delete-orphan')) created_by_user = db.relationship('User', backref=db.backref('clinical_templates_created', lazy='dynamic')) source_entry = db.relationship('ClinicalEntry', backref=db.backref('saved_as_templates', lazy='dynamic')) def _safe_json(self, raw): try: payload = json.loads(raw or '{}') return payload if isinstance(payload, dict) else {} except Exception: return {} def to_payload(self): return { 'id': self.id, 'title': self.title, 'category': self.category or 'General', 'specialty_name': self.specialty_name or '', 'specialty_template': self.specialty_template or 'general_medicine', 'encounter_type': self.encounter_type or 'Evolución médica', 'chief_complaint': self.chief_complaint or '', 'provisional_diagnosis': self.provisional_diagnosis or '', 'diagnosis_text': self.diagnosis_text or '', 'cie10_code': self.cie10_code or '', 'snomed_term': self.snomed_term or '', 'snomed_code': self.snomed_code or '', 'subjective': self.subjective or '', 'objective': self.objective or '', 'assessment': self.assessment or '', 'plan': self.plan or '', 'treatment': self.treatment or '', 'study_results': self.study_results or '', 'vitals': self._safe_json(self.vitals_payload), 'structured': self._safe_json(self.structured_payload), 'consent_reference': self.consent_reference or '', 'visibility_scope': self.visibility_scope or 'Institucional', 'usage_count': self.usage_count or 0, } class ClinicalAttachment(TimestampMixin, db.Model): id = db.Column(db.Integer, primary_key=True) entry_id = db.Column(db.Integer, db.ForeignKey('clinical_entry.id'), nullable=False, index=True) filename = db.Column(db.String(255), nullable=False) file_path = db.Column(db.String(255), nullable=False) mime_type = db.Column(db.String(120)) is_patient_visible = db.Column(db.Boolean, default=False) entry = db.relationship('ClinicalEntry', back_populates='attachments') class AccountingEntry(TimestampMixin, db.Model): id = db.Column(db.Integer, primary_key=True) entry_date = db.Column(db.Date, nullable=False, default=date.today, index=True) entry_type = db.Column(db.String(20), nullable=False, index=True) # ingreso / egreso category = db.Column(db.String(120), nullable=False, index=True) description = db.Column(db.String(255), nullable=False) amount = db.Column(db.Float, nullable=False, default=0) tax_profile = db.Column(db.String(80)) receipt_type = db.Column(db.String(40)) receipt_number = db.Column(db.String(80), index=True) counterparty_name = db.Column(db.String(150)) counterparty_cuit = db.Column(db.String(20)) payment_method = db.Column(db.String(60)) status = db.Column(db.String(30), default='Registrado', index=True) notes = db.Column(db.Text) created_by_user_id = db.Column(db.Integer, db.ForeignKey('user.id')) institution_id = db.Column(db.Integer, db.ForeignKey('institution.id'), index=True) created_by_user = db.relationship('User', backref=db.backref('accounting_entries_created', lazy='dynamic')) class SaasPlan(TimestampMixin, db.Model): __tablename__ = 'saas_plan' id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String(140), nullable=False, unique=True) description = db.Column(db.Text) monthly_price = db.Column(db.Float, nullable=False, default=0) price_per_appointment = db.Column(db.Float, nullable=False, default=0) included_professionals = db.Column(db.Integer, default=1) extra_professional_price = db.Column(db.Float, default=0) included_appointments = db.Column(db.Integer, default=0) active = db.Column(db.Boolean, default=True, index=True) class InstitutionSubscription(TimestampMixin, db.Model): __tablename__ = 'institution_subscription' id = db.Column(db.Integer, primary_key=True) institution_id = db.Column(db.Integer, db.ForeignKey('institution.id'), nullable=False, index=True) plan_id = db.Column(db.Integer, db.ForeignKey('saas_plan.id'), nullable=False, index=True) start_date = db.Column(db.Date, default=date.today, nullable=False) billing_day = db.Column(db.Integer, default=1) status = db.Column(db.String(30), default='activa', index=True) notes = db.Column(db.Text) institution = db.relationship('Institution', backref=db.backref('subscriptions', lazy='dynamic')) plan = db.relationship('SaasPlan', backref=db.backref('subscriptions', lazy='dynamic')) class SaasInvoice(TimestampMixin, db.Model): __tablename__ = 'saas_invoice' id = db.Column(db.Integer, primary_key=True) institution_id = db.Column(db.Integer, db.ForeignKey('institution.id'), nullable=False, index=True) subscription_id = db.Column(db.Integer, db.ForeignKey('institution_subscription.id'), index=True) period = db.Column(db.String(7), nullable=False, index=True) # YYYY-MM issue_date = db.Column(db.Date, default=date.today, nullable=False) due_date = db.Column(db.Date, nullable=False) monthly_amount = db.Column(db.Float, default=0) appointment_count = db.Column(db.Integer, default=0) appointment_amount = db.Column(db.Float, default=0) professional_count = db.Column(db.Integer, default=0) extra_professional_amount = db.Column(db.Float, default=0) total_amount = db.Column(db.Float, default=0) paid_amount = db.Column(db.Float, default=0) status = db.Column(db.String(30), default='pendiente', index=True) payment_method = db.Column(db.String(40)) mercadopago_preference_id = db.Column(db.String(160), index=True) mercadopago_payment_id = db.Column(db.String(160), index=True) mercadopago_status = db.Column(db.String(80), index=True) mercadopago_status_detail = db.Column(db.String(160)) mercadopago_init_point = db.Column(db.String(600)) mercadopago_sandbox_init_point = db.Column(db.String(600)) mercadopago_payload = db.Column(db.Text) notes = db.Column(db.Text) institution = db.relationship('Institution', backref=db.backref('saas_invoices', lazy='dynamic')) subscription = db.relationship('InstitutionSubscription', backref=db.backref('invoices', lazy='dynamic')) @property def debt_amount(self): return max((self.total_amount or 0) - (self.paid_amount or 0), 0) class SaasPayment(TimestampMixin, db.Model): __tablename__ = 'saas_payment' id = db.Column(db.Integer, primary_key=True) invoice_id = db.Column(db.Integer, db.ForeignKey('saas_invoice.id'), nullable=False, index=True) payment_date = db.Column(db.Date, default=date.today, nullable=False) amount = db.Column(db.Float, nullable=False, default=0) method = db.Column(db.String(40), default='efectivo') reference = db.Column(db.String(160)) status = db.Column(db.String(40), default='registrado', index=True) mercadopago_payment_id = db.Column(db.String(160), index=True) mercadopago_status = db.Column(db.String(80)) mercadopago_status_detail = db.Column(db.String(160)) mercadopago_payload = db.Column(db.Text) notes = db.Column(db.Text) created_by_user_id = db.Column(db.Integer, db.ForeignKey('user.id')) invoice = db.relationship('SaasInvoice', backref=db.backref('payments', lazy='dynamic')) created_by_user = db.relationship('User') class AuditLog(TimestampMixin, db.Model): id = db.Column(db.Integer, primary_key=True) actor_email = db.Column(db.String(150)) action = db.Column(db.String(120), nullable=False) entity_type = db.Column(db.String(80), nullable=False) entity_id = db.Column(db.String(50)) details = db.Column(db.Text) class ErrorLog(TimestampMixin, db.Model): id = db.Column(db.Integer, primary_key=True) actor_email = db.Column(db.String(150)) route = db.Column(db.String(255)) function_name = db.Column(db.String(120)) error_type = db.Column(db.String(120)) message = db.Column(db.Text) stacktrace = db.Column(db.Text) extra = db.Column(db.Text) class ChatConversation(TimestampMixin, db.Model): id = db.Column(db.Integer, primary_key=True) title = db.Column(db.String(180)) is_group = db.Column(db.Boolean, default=False, index=True) created_by_user_id = db.Column(db.Integer, db.ForeignKey('user.id')) is_active = db.Column(db.Boolean, default=True, index=True) institution_id = db.Column(db.Integer, db.ForeignKey('institution.id'), index=True) institution = db.relationship('Institution') created_by_user = db.relationship('User', backref=db.backref('chat_conversations_created', lazy='dynamic')) members = db.relationship('ChatConversationMember', back_populates='conversation', cascade='all, delete-orphan') messages = db.relationship('ChatMessage', back_populates='conversation', cascade='all, delete-orphan', order_by='ChatMessage.created_at.asc()') @property def display_title(self): return self.title or ('Grupo' if self.is_group else 'Chat privado') class ChatConversationMember(TimestampMixin, db.Model): id = db.Column(db.Integer, primary_key=True) conversation_id = db.Column(db.Integer, db.ForeignKey('chat_conversation.id'), nullable=False, index=True) user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False, index=True) is_admin = db.Column(db.Boolean, default=False) last_read_at = db.Column(db.DateTime) conversation = db.relationship('ChatConversation', back_populates='members') user = db.relationship('User', backref=db.backref('chat_memberships', lazy='dynamic')) class ChatMessage(TimestampMixin, db.Model): id = db.Column(db.Integer, primary_key=True) conversation_id = db.Column(db.Integer, db.ForeignKey('chat_conversation.id'), nullable=False, index=True) sender_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False, index=True) body = db.Column(db.Text) system_flag = db.Column(db.Boolean, default=False) conversation = db.relationship('ChatConversation', back_populates='messages') sender = db.relationship('User', backref=db.backref('chat_messages_sent', lazy='dynamic')) attachments = db.relationship('ChatAttachment', back_populates='message', cascade='all, delete-orphan') class ChatAttachment(TimestampMixin, db.Model): id = db.Column(db.Integer, primary_key=True) message_id = db.Column(db.Integer, db.ForeignKey('chat_message.id'), nullable=False, index=True) filename = db.Column(db.String(255), nullable=False) file_path = db.Column(db.String(255), nullable=False) mime_type = db.Column(db.String(120)) file_size = db.Column(db.Integer) message = db.relationship('ChatMessage', back_populates='attachments') class WhatsappBotRule(TimestampMixin, db.Model): id = db.Column(db.Integer, primary_key=True) trigger = db.Column(db.String(120), nullable=False, index=True) title = db.Column(db.String(180)) response_html = db.Column(db.Text, nullable=False) match_mode = db.Column(db.String(30), default='contains') sort_order = db.Column(db.Integer, default=0, index=True) is_active = db.Column(db.Boolean, default=True, index=True) institution_id = db.Column(db.Integer, db.ForeignKey('institution.id'), index=True) class WhatsappBotKnowledge(TimestampMixin, db.Model): id = db.Column(db.Integer, primary_key=True) source_url = db.Column(db.String(500), index=True) title = db.Column(db.String(255)) content = db.Column(db.Text, nullable=False) keywords = db.Column(db.String(500)) is_active = db.Column(db.Boolean, default=True, index=True) institution_id = db.Column(db.Integer, db.ForeignKey('institution.id'), index=True) class WhatsappMessageLog(TimestampMixin, db.Model): id = db.Column(db.Integer, primary_key=True) wa_id = db.Column(db.String(80), index=True) customer_name = db.Column(db.String(180)) incoming_text = db.Column(db.Text) outgoing_text = db.Column(db.Text) provider_message_id = db.Column(db.String(120)) status = db.Column(db.String(40), default='received', index=True) raw_payload = db.Column(db.Text) error = db.Column(db.Text) institution_id = db.Column(db.Integer, db.ForeignKey('institution.id'), index=True) class AiKnowledgeItem(TimestampMixin, db.Model): __tablename__ = 'ai_knowledge_item' id = db.Column(db.Integer, primary_key=True) source_type = db.Column(db.String(60), default='manual', index=True) source_ref = db.Column(db.String(500), index=True) title = db.Column(db.String(255), nullable=False) content = db.Column(db.Text, nullable=False) keywords = db.Column(db.String(500)) is_active = db.Column(db.Boolean, default=True, index=True) institution_id = db.Column(db.Integer, db.ForeignKey('institution.id'), index=True) last_synced_at = db.Column(db.DateTime) class AiChatLog(TimestampMixin, db.Model): __tablename__ = 'ai_chat_log' id = db.Column(db.Integer, primary_key=True) session_id = db.Column(db.String(120), index=True) customer_name = db.Column(db.String(180)) customer_email = db.Column(db.String(180)) incoming_text = db.Column(db.Text) outgoing_text = db.Column(db.Text) intent = db.Column(db.String(80), index=True) handoff_status = db.Column(db.String(40), default='auto', index=True) context_snapshot = db.Column(db.Text) ip_address = db.Column(db.String(80)) institution_id = db.Column(db.Integer, db.ForeignKey('institution.id'), index=True)