276 lines
8.0 KiB
Python
276 lines
8.0 KiB
Python
from django.conf import settings
|
|
from django.db import models
|
|
from django.utils.translation import gettext_lazy as _
|
|
from apps.core.models import TimeStampedModel, UUIDModel
|
|
|
|
|
|
class PresentationTheme(models.TextChoices):
|
|
HEALTHCARE_MODERN = 'healthcare_modern', _('Healthcare Modern')
|
|
CORPORATE_NAVY = 'corporate_navy', _('Corporate Navy')
|
|
DARK_COMMAND = 'dark_command', _('Dark Command Center')
|
|
|
|
|
|
class SlideLayout(models.TextChoices):
|
|
COVER = 'cover', _('Cover')
|
|
SECTION_DIVIDER = 'section_divider', _('Section Divider')
|
|
KPI_DASHBOARD = 'kpi_dashboard', _('KPI Dashboard')
|
|
FULL_CHART = 'full_chart', _('Full Chart')
|
|
CHART_METRICS = 'chart_metrics', _('Chart + Metrics')
|
|
DATA_TABLE = 'data_table', _('Data Table')
|
|
TWO_COLUMN = 'two_column', _('Two Column')
|
|
QUOTE = 'quote', _('Quote / Callout')
|
|
TIMELINE = 'timeline', _('Timeline')
|
|
COMPARISON = 'comparison', _('Comparison')
|
|
TEAM_GRID = 'team_grid', _('Team / Department Grid')
|
|
CLOSING = 'closing', _('Closing')
|
|
|
|
|
|
class PresentationStatus(models.TextChoices):
|
|
DRAFT = 'draft', _('Draft')
|
|
PUBLISHED = 'published', _('Published')
|
|
ARCHIVED = 'archived', _('Archived')
|
|
|
|
|
|
class Presentation(UUIDModel, TimeStampedModel):
|
|
title = models.CharField(max_length=300)
|
|
subtitle = models.CharField(max_length=500, blank=True)
|
|
description = models.TextField(blank=True)
|
|
|
|
theme = models.CharField(
|
|
max_length=30,
|
|
choices=PresentationTheme.choices,
|
|
default=PresentationTheme.HEALTHCARE_MODERN,
|
|
)
|
|
status = models.CharField(
|
|
max_length=20,
|
|
choices=PresentationStatus.choices,
|
|
default=PresentationStatus.DRAFT,
|
|
)
|
|
|
|
presentation_type = models.CharField(
|
|
max_length=50,
|
|
blank=True,
|
|
help_text=_('Type of report (e.g., quarterly, monthly, custom)'),
|
|
)
|
|
|
|
created_by = models.ForeignKey(
|
|
settings.AUTH_USER_MODEL,
|
|
on_delete=models.CASCADE,
|
|
related_name='presentations',
|
|
)
|
|
hospital = models.ForeignKey(
|
|
'organizations.Hospital',
|
|
on_delete=models.CASCADE,
|
|
related_name='presentations',
|
|
null=True,
|
|
blank=True,
|
|
)
|
|
|
|
thumbnail = models.ImageField(
|
|
upload_to='presentations/thumbnails/',
|
|
null=True,
|
|
blank=True,
|
|
)
|
|
|
|
presentation_date = models.DateField(null=True, blank=True)
|
|
is_shared = models.BooleanField(default=False)
|
|
|
|
class Meta:
|
|
ordering = ['-created_at']
|
|
indexes = [
|
|
models.Index(fields=['status', '-created_at']),
|
|
models.Index(fields=['hospital', '-created_at']),
|
|
models.Index(fields=['created_by', '-created_at']),
|
|
]
|
|
|
|
def __str__(self):
|
|
return self.title
|
|
|
|
@property
|
|
def slide_count(self):
|
|
return self.slides.count()
|
|
|
|
|
|
class Slide(UUIDModel, TimeStampedModel):
|
|
presentation = models.ForeignKey(
|
|
Presentation,
|
|
on_delete=models.CASCADE,
|
|
related_name='slides',
|
|
)
|
|
|
|
layout = models.CharField(
|
|
max_length=30,
|
|
choices=SlideLayout.choices,
|
|
default=SlideLayout.COVER,
|
|
)
|
|
order = models.PositiveIntegerField(default=0)
|
|
|
|
title = models.CharField(max_length=300, blank=True)
|
|
subtitle = models.CharField(max_length=500, blank=True)
|
|
|
|
content = models.JSONField(
|
|
default=dict,
|
|
blank=True,
|
|
help_text=_(
|
|
'Layout-specific content. Structure varies by slide type: '
|
|
'kpi_dashboard={metrics:[...]}, '
|
|
'full_chart={chart_config:{...}}, '
|
|
'data_table={headers:[...], rows:[...]}, etc.'
|
|
),
|
|
)
|
|
|
|
background_color = models.CharField(max_length=20, blank=True)
|
|
speaker_notes = models.TextField(blank=True)
|
|
|
|
class Meta:
|
|
ordering = ['order']
|
|
indexes = [
|
|
models.Index(fields=['presentation', 'order']),
|
|
]
|
|
|
|
def __str__(self):
|
|
return f'{self.presentation.title} — Slide {self.order}: {self.title or self.get_layout_display()}'
|
|
|
|
@property
|
|
def template_name(self):
|
|
return f'presentations/slides/_{self.layout}.html'
|
|
|
|
|
|
class ReportTemplate(UUIDModel, TimeStampedModel):
|
|
name = models.CharField(max_length=200)
|
|
slug = models.SlugField(unique=True, max_length=220)
|
|
description = models.TextField(blank=True)
|
|
|
|
data_source = models.CharField(
|
|
max_length=50,
|
|
help_text=_('Key in REPORT_DATA_SOURCES registry'),
|
|
)
|
|
reference_pdf = models.FileField(
|
|
upload_to='report_templates/',
|
|
null=True,
|
|
blank=True,
|
|
)
|
|
parsed_structure = models.JSONField(
|
|
default=dict,
|
|
blank=True,
|
|
help_text=_('Raw AI analysis of the reference PDF'),
|
|
)
|
|
style_config = models.JSONField(
|
|
default=dict,
|
|
blank=True,
|
|
help_text=_('Theme colors, row colors, fonts'),
|
|
)
|
|
ai_prompt_template = models.TextField(
|
|
blank=True,
|
|
help_text=_('Prompt template for AI insight generation. Use {{ data_summary }} placeholder.'),
|
|
)
|
|
active = models.BooleanField(default=True)
|
|
|
|
hospital = models.ForeignKey(
|
|
'organizations.Hospital',
|
|
on_delete=models.CASCADE,
|
|
related_name='report_templates',
|
|
null=True,
|
|
blank=True,
|
|
help_text=_('Null = available for all hospitals'),
|
|
)
|
|
created_by = models.ForeignKey(
|
|
settings.AUTH_USER_MODEL,
|
|
on_delete=models.CASCADE,
|
|
related_name='report_templates',
|
|
)
|
|
|
|
class Meta:
|
|
ordering = ['name']
|
|
indexes = [
|
|
models.Index(fields=['active', '-created_at']),
|
|
models.Index(fields=['data_source']),
|
|
]
|
|
|
|
def __str__(self):
|
|
return self.name
|
|
|
|
@property
|
|
def slide_count(self):
|
|
return self.template_slides.count()
|
|
|
|
|
|
class ReportTemplateSlide(UUIDModel, TimeStampedModel):
|
|
template = models.ForeignKey(
|
|
ReportTemplate,
|
|
on_delete=models.CASCADE,
|
|
related_name='template_slides',
|
|
)
|
|
order = models.PositiveIntegerField(default=0)
|
|
|
|
layout = models.CharField(
|
|
max_length=30,
|
|
choices=SlideLayout.choices,
|
|
default=SlideLayout.COVER,
|
|
)
|
|
section_label = models.CharField(
|
|
max_length=50,
|
|
blank=True,
|
|
help_text=_('For section dividers: e.g. "01", "02"'),
|
|
)
|
|
title_template = models.CharField(
|
|
max_length=300,
|
|
blank=True,
|
|
help_text=_('Supports {{ variable }} substitution'),
|
|
)
|
|
subtitle_template = models.CharField(
|
|
max_length=500,
|
|
blank=True,
|
|
help_text=_('Supports {{ variable }} substitution'),
|
|
)
|
|
|
|
content_mapping = models.JSONField(
|
|
default=dict,
|
|
blank=True,
|
|
help_text=_(
|
|
'How data maps to slide content. '
|
|
'Structure depends on layout type.'
|
|
),
|
|
)
|
|
repeat_source = models.CharField(
|
|
max_length=100,
|
|
blank=True,
|
|
help_text=_(
|
|
'Data key to repeat over, e.g. "by_department". '
|
|
'Creates one slide per item.'
|
|
),
|
|
)
|
|
repeat_title_key = models.CharField(
|
|
max_length=100,
|
|
blank=True,
|
|
default='name',
|
|
help_text=_('Key in repeat item for slide title, e.g. "department_name"'),
|
|
)
|
|
repeat_subtitle_template = models.CharField(
|
|
max_length=300,
|
|
blank=True,
|
|
help_text=_('Subtitle template for repeated slides. {{ item.X }} available.'),
|
|
)
|
|
max_rows = models.PositiveIntegerField(
|
|
default=18,
|
|
help_text=_('Max data rows per slide (tables split across slides)'),
|
|
)
|
|
style_overrides = models.JSONField(
|
|
default=dict,
|
|
blank=True,
|
|
help_text=_('Per-slide style overrides (row colors, etc.)'),
|
|
)
|
|
speaker_notes_template = models.TextField(
|
|
blank=True,
|
|
help_text=_('Speaker notes template with {{ variable }} support'),
|
|
)
|
|
|
|
class Meta:
|
|
ordering = ['order']
|
|
indexes = [
|
|
models.Index(fields=['template', 'order']),
|
|
]
|
|
|
|
def __str__(self):
|
|
return f'{self.template.name} — Slide {self.order}: {self.title_template or self.get_layout_display()}'
|