inou/templates/dossier.tmpl

438 lines
18 KiB
Cheetah
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

{{define "dossier"}}
<div class="sg-container">
<div class="dossier-header" style="display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 32px;">
<div>
<h1 style="font-size: 2.5rem; font-weight: 700; margin-bottom: 8px;">{{.TargetDossier.Name}}</h1>
{{if .ShowDetails}}
<p style="font-size: 1.15rem; font-weight: 300; color: var(--text-muted);">
{{if .TargetDossier.DateOfBirth}}{{.T.born}}: {{printf "%.10s" .TargetDossier.DateOfBirth}}{{end}}
{{if .TargetDossier.Sex}} · {{sexT .TargetDossier.Sex .Lang}}{{end}}
</p>
{{end}}
</div>
<a href="/dashboard" class="btn btn-secondary btn-small">← {{.T.back_to_dossiers}}</a>
</div>
{{if .Error}}<div class="error">{{.Error}}</div>{{end}}
{{if .Success}}<div class="success">{{.Success}}</div>{{end}}
{{/* Render all sections using unified template */}}
{{range .Sections}}
{{template "section_block" .}}
{{end}}
{{/* Privacy section - special structure */}}
<div class="data-card">
<div class="data-card-header">
<div class="data-card-indicator privacy"></div>
<div class="data-card-title">
<span class="section-heading">{{$.T.section_privacy}}</span>
<span class="data-card-summary">{{len $.AccessList}} {{$.T.people_with_access_count}}</span>
</div>
</div>
<div class="data-table">
{{range $.AccessList}}
<div class="data-row">
<div class="data-row-main">
<span class="data-label">{{.Name}}{{if .IsSelf}} ({{$.T.you}}){{else if .IsPending}} ({{$.T.pending}}){{end}}</span>
<span class="data-meta">{{.Relation}}{{if .CanEdit}} · {{$.T.can_edit}}{{end}}</span>
</div>
{{if and $.CanManageAccess (not .IsSelf)}}
<div style="display: flex; gap: 8px;">
<a href="/dossier/{{$.TargetDossier.DossierID}}/access/{{.DossierID}}" class="btn btn-secondary btn-small">Edit</a>
<form action="/dossier/{{$.TargetDossier.DossierID}}/revoke" method="POST" style="display:inline;">
<input type="hidden" name="accessor_id" value="{{.DossierID}}">
<button type="submit" class="btn btn-danger btn-small" onclick="return confirm('Remove access for {{.Name}} to {{$.TargetDossier.Name}}?')">{{$.T.remove}}</button>
</form>
</div>
{{end}}
</div>
{{end}}
{{if not $.AccessList}}
<div class="data-row">
<span class="text-muted">{{$.T.no_access_yet}}</span>
</div>
{{end}}
<div class="data-row privacy-actions">
<a href="/dossier/{{$.TargetDossier.DossierID}}/share" class="privacy-action">{{$.T.share_access}}</a>
{{if $.CanManageAccess}}<a href="/dossier/{{$.TargetDossier.DossierID}}/permissions" class="privacy-action">{{$.T.manage_permissions}}</a>{{end}}
<a href="/dossier/{{$.TargetDossier.DossierID}}/audit" class="privacy-action">{{$.T.view_audit_log}}</a>
{{if or (eq $.Dossier.DossierID $.TargetDossier.DossierID) $.CanManageAccess}}<a href="/dossier/{{$.TargetDossier.DossierID}}/export" class="privacy-action">{{$.T.export_data}}</a>{{end}}
</div>
</div>
</div>
{{template "footer"}}
</div>
{{/* Genetics Warning Modal */}}
<div id="genetics-warning-modal" class="modal" style="display:none;">
<div class="modal-content" style="max-width: 520px;">
<h3 style="margin-bottom: 16px;">⚠️ Before you continue</h3>
<p style="margin-bottom: 16px;">Here you can browse all your raw genetic variants. However, the real value comes from using <a href="/connect">Claude and other LLMs with your health dossier</a> — they can interpret these variants and correlate them with your labs, imaging, and medical history.</p>
<p style="margin-bottom: 12px;"><strong>Keep in mind:</strong></p>
<ul style="margin-bottom: 16px; padding-left: 20px; line-height: 1.6;">
<li>Many associations are based on early or limited research</li>
<li>A "risk variant" means slightly higher odds — not a diagnosis</li>
<li>Consumer tests (23andMe, AncestryDNA) can have false positives</li>
</ul>
<p style="margin-bottom: 20px;">These findings can be a starting point for conversations with your doctor — especially if certain conditions run in your family.</p>
<div style="display: flex; gap: 12px; justify-content: flex-end;">
<button class="btn btn-secondary" onclick="closeGeneticsWarning()">Close</button>
<button class="btn btn-primary" onclick="confirmShowAllGenetics()">I understand, show all</button>
</div>
</div>
</div>
<script>
const dossierGUID = "{{.TargetDossier.DossierID}}";
const userLang = "{{.Lang}}";
// Section expand/collapse
function toggleSection(el) {
el.classList.toggle('expanded');
const icon = el.querySelector('.expand-icon');
if (icon) icon.textContent = el.classList.contains('expanded') ? '' : '+';
const children = el.nextElementSibling;
if (children && children.classList.contains('section-children')) {
children.classList.toggle('show');
}
}
// Format dates from YYYYMMDD
document.querySelectorAll('[data-date]').forEach(el => {
const d = el.dataset.date;
if (d && d.length === 8) {
const date = new Date(d.slice(0,4) + '-' + d.slice(4,6) + '-' + d.slice(6,8));
el.textContent = date.toLocaleDateString();
}
});
// Genetics dynamic loading (if genetics section exists)
{{if .HasGenome}}
const i18n = {
genomeEnglishOnly: "{{.T.genome_english_only}}",
genomeVariants: "{{.T.genome_variants}}",
genomeHidden: "{{.T.genome_hidden}}",
genomeShowAllCategories: "{{.T.genome_show_all_categories}}"
};
let showAllGenetics = false;
let showAllCategories = false;
let allCategories = {};
async function loadGeneticsCategories() {
const container = document.getElementById('genetics-content');
if (!container) return;
try {
const resp = await fetch(`/api/categories?dossier=${dossierGUID}&type=genome`);
allCategories = await resp.json();
const totalHidden = Object.values(allCategories).reduce((sum, c) => sum + (c.hidden || 0), 0);
const showAllBtn = document.getElementById('show-all-genetics');
if (showAllBtn && totalHidden > 0) {
showAllBtn.style.display = '';
}
renderGeneticsCategories();
} catch (e) {
container.innerHTML = '<div class="data-row"><span class="text-muted">Error loading genetics</span></div>';
}
}
function renderGeneticsCategories() {
const container = document.getElementById('genetics-content');
const summaryEl = document.getElementById('genetics-summary');
if (!container) return;
let totalShown = 0, totalHidden = 0;
for (const counts of Object.values(allCategories)) {
totalShown += counts.shown || 0;
totalHidden += counts.hidden || 0;
}
if (totalShown === 0 && totalHidden === 0) {
document.getElementById('section-genetics')?.remove();
return;
}
let summary = showAllGenetics ? `${totalShown + totalHidden} ${i18n.genomeVariants}` : `${totalShown} ${i18n.genomeVariants}`;
if (totalHidden > 0 && !showAllGenetics) {
summary += ` <span class="text-muted">(${totalHidden} ${i18n.genomeHidden})</span>`;
}
if (summaryEl) summaryEl.innerHTML = summary;
const btn = document.getElementById('show-all-genetics');
if (btn) {
if (showAllGenetics) {
btn.textContent = 'Hide negative';
btn.onclick = () => { showAllGenetics = false; renderGeneticsCategories(); return false; };
} else {
btn.textContent = 'Show all';
btn.onclick = () => { showGeneticsWarning(); return false; };
}
}
const categoryPriority = {
'traits': 1, 'metabolism': 2, 'longevity': 3, 'blood': 4, 'cardiovascular': 5,
'neurological': 6, 'mental_health': 7, 'autoimmune': 8, 'medication': 9,
'fertility': 10, 'disease': 11, 'cancer': 12, 'other': 13
};
const sorted = Object.entries(allCategories).sort((a, b) => {
const aPriority = categoryPriority[a[0]] || 99;
const bPriority = categoryPriority[b[0]] || 99;
return aPriority !== bPriority ? aPriority - bPriority :
(showAllGenetics ? (b[1].shown + b[1].hidden) : b[1].shown) -
(showAllGenetics ? (a[1].shown + a[1].hidden) : a[1].shown);
});
const maxCategories = 5;
const displayCategories = showAllCategories ? sorted : sorted.slice(0, maxCategories);
let html = '';
if (userLang !== 'en' && i18n.genomeEnglishOnly) {
html += `<div class="data-row" style="background: var(--bg-muted); color: var(--text-muted); font-size: 0.9rem; padding: 12px 16px;">
<span>🌐 ${i18n.genomeEnglishOnly}</span>
</div>`;
}
for (const [cat, counts] of displayCategories) {
const shown = counts.shown || 0, hidden = counts.hidden || 0;
const displayCount = showAllGenetics ? (shown + hidden) : shown;
if (displayCount === 0) continue;
const label = cat.charAt(0).toUpperCase() + cat.slice(1).replace(/_/g, ' ');
let countText = `${displayCount} ${i18n.genomeVariants}`;
if (hidden > 0 && !showAllGenetics) {
countText = `${shown} ${i18n.genomeVariants} <span class="text-muted">(${hidden} ${i18n.genomeHidden})</span>`;
}
html += `
<div class="data-row expandable" data-category="${cat}" onclick="toggleGeneticsCategory(this, '${cat}')">
<div class="data-row-main">
<span class="expand-icon">+</span>
<span class="data-label">${label}</span>
</div>
<div class="data-values">
<span class="data-meta">${countText}</span>
</div>
</div>
<div class="section-children" data-category="${cat}"></div>`;
}
if (!showAllCategories && sorted.length > maxCategories) {
html += `<div class="data-row show-more" style="cursor: pointer;" onclick="showAllCategories = true; renderGeneticsCategories();">${i18n.genomeShowAllCategories.replace('%d', sorted.length)} →</div>`;
}
container.innerHTML = html;
}
function showGeneticsWarning() {
document.getElementById('genetics-warning-modal').style.display = 'flex';
}
function closeGeneticsWarning() {
document.getElementById('genetics-warning-modal').style.display = 'none';
}
function confirmShowAllGenetics() {
closeGeneticsWarning();
showAllGenetics = true;
renderGeneticsCategories();
}
async function toggleGeneticsCategory(el, category) {
el.classList.toggle('expanded');
const icon = el.querySelector('.expand-icon');
if (icon) icon.textContent = el.classList.contains('expanded') ? '' : '+';
const children = el.nextElementSibling;
if (!children) return;
children.classList.toggle('show');
if (children.dataset.loaded) return;
children.dataset.loaded = 'true';
children.innerHTML = '<div class="data-row child"><span class="text-muted">Loading...</span></div>';
try {
const includeHidden = showAllGenetics ? '&include_hidden=true' : '';
const resp = await fetch(`/api/genome?dossier=${dossierGUID}&category=${encodeURIComponent(category)}${includeHidden}`);
const data = await resp.json();
if (!data.matches || data.matches.length === 0) {
children.innerHTML = '<div class="data-row child"><span class="text-muted">No variants found</span></div>';
return;
}
let html = '';
data.matches.slice(0, 5).forEach(m => {
const mag = m.magnitude ? m.magnitude.toFixed(1) : '0';
const allele = m.genotype.replace(/;/g, '').split('').join(';');
html += `
<div class="data-row child" style="flex-direction: column; align-items: flex-start; gap: 8px; padding: 12px 16px;">
<div class="sg-gene-row" style="width: 100%;">
<div style="display: flex; justify-content: space-between; align-items: flex-start; width: 100%;">
<div class="sg-gene-main">
<span class="sg-gene-name">${m.gene || ''}</span>
<span class="sg-gene-rsid">${m.rsid}</span>
</div>
<div style="display: flex; align-items: center; gap: 8px;">
<span class="sg-gene-allele">${allele}</span>
<span class="badge" style="font-size: 0.7rem;">mag ${mag}</span>
</div>
</div>
<div class="sg-gene-summary">${m.summary || ''}</div>
</div>
</div>`;
});
if (data.total > 5) {
const remaining = data.total - 5;
html += `<div class="sg-show-more" data-offset="5" data-total="${data.total}" data-category="${category}" onclick="loadMoreGenetics(this)">${remaining > 50 ? `${remaining} more — <a href="/connect">use Claude</a>` : `Show more (${remaining}) →`}</div>`;
}
children.innerHTML = html;
} catch (e) {
children.innerHTML = '<div class="data-row child"><span class="text-muted">Error loading</span></div>';
}
}
async function loadMoreGenetics(el) {
const offset = parseInt(el.dataset.offset, 10);
const total = parseInt(el.dataset.total, 10);
const category = el.dataset.category;
el.textContent = 'Loading...';
try {
const includeHidden = showAllGenetics ? '&include_hidden=true' : '';
const resp = await fetch(`/api/genome?dossier=${dossierGUID}&category=${encodeURIComponent(category)}&offset=${offset}&limit=20${includeHidden}`);
const data = await resp.json();
let html = '';
data.matches.forEach(m => {
const mag = m.magnitude ? m.magnitude.toFixed(1) : '0';
const allele = m.genotype.replace(/;/g, '').split('').join(';');
html += `
<div class="data-row child" style="flex-direction: column; align-items: flex-start; gap: 8px; padding: 12px 16px;">
<div class="sg-gene-row" style="width: 100%;">
<div style="display: flex; justify-content: space-between; align-items: flex-start; width: 100%;">
<div class="sg-gene-main">
<span class="sg-gene-name">${m.gene || ''}</span>
<span class="sg-gene-rsid">${m.rsid}</span>
</div>
<div style="display: flex; align-items: center; gap: 8px;">
<span class="sg-gene-allele">${allele}</span>
<span class="badge" style="font-size: 0.7rem;">mag ${mag}</span>
</div>
</div>
<div class="sg-gene-summary">${m.summary || ''}</div>
</div>
</div>`;
});
el.insertAdjacentHTML('beforebegin', html);
const newOffset = offset + data.matches.length;
const remaining = total - newOffset;
if (remaining > 0) {
el.dataset.offset = newOffset;
el.innerHTML = remaining > 50 ? `${remaining} more — <a href="/connect">use Claude</a>` : `Show more (${remaining}) →`;
} else {
el.remove();
}
} catch (e) {
el.textContent = 'Error';
}
}
loadGeneticsCategories();
{{end}}
</script>
{{end}}
{{/* Unified section block template */}}
{{define "section_block"}}
<div class="data-card{{if .ComingSoon}} coming-soon{{end}}" id="section-{{.ID}}"{{if and .HideEmpty (eq (len .Items) 0) (not .Dynamic) (not .ComingSoon)}} style="display:none;"{{end}}>
<div class="data-card-header">
<div class="data-card-indicator" style="background: #{{.Color}};"></div>
<div class="data-card-title">
<span class="section-heading">{{.Heading}}</span>
<span class="data-card-summary"{{if .Dynamic}} id="{{.ID}}-summary"{{end}}>{{.Summary}}</span>
</div>
{{if .ComingSoon}}
<span class="badge-soon">Coming soon</span>
{{else if eq .ID "genetics"}}
<a href="#" class="btn btn-small" id="show-all-genetics" onclick="showGeneticsWarning(); return false;">Show all</a>
{{else if .ActionURL}}
<a href="{{.ActionURL}}" {{if eq .ID "imaging"}}target="_blank"{{end}} class="btn btn-small{{if eq .ID "checkin"}} btn-primary{{end}}">{{.ActionLabel}}</a>
{{end}}
</div>
{{if .ShowBuildPrompt}}
<div class="build-profile-prompt">
<p class="build-profile-text">📋 Build your health profile</p>
<p class="build-profile-hint">{{.Summary}}</p>
<div class="build-profile-buttons">
{{range .PromptButtons}}
<a href="{{.URL}}" class="build-profile-btn">
<span class="build-profile-icon">{{.Icon}}</span>
<span>{{.Label}}</span>
</a>
{{end}}
</div>
</div>
{{end}}
{{if .Dynamic}}
<div class="data-table" id="{{.ID}}-content"></div>
{{else if .Items}}
<div class="data-table">
{{range $i, $item := .Items}}
{{if $item.Expandable}}
<div class="data-row expandable{{if gt $i 4}} hidden-row{{end}}" data-index="{{$i}}" onclick="toggleSection(this)">
<div class="data-row-main">
<span class="expand-icon">+</span>
<span class="data-label">{{$item.Label}}</span>
</div>
<div class="data-values">
{{if $item.Value}}<span class="data-value mono">{{$item.Value}}</span>{{end}}
{{if $item.Date}}<span class="data-date" data-date="{{$item.Date}}"></span>{{end}}
{{if $item.LinkURL}}<a href="{{$item.LinkURL}}" target="_blank" class="btn-icon" onclick="event.stopPropagation()" title="{{$item.LinkTitle}}">→</a>{{end}}
</div>
</div>
<div class="section-children{{if gt $i 4}} hidden-row{{end}}" data-index="{{$i}}">
{{range $item.Children}}
<div class="data-row child">
<span class="data-label">{{.Label}}</span>
{{if .Value}}<span class="data-value mono">{{.Value}}</span>{{end}}
{{if .LinkURL}}<a href="{{.LinkURL}}" target="_blank" class="btn-icon" title="{{.LinkTitle}}">→</a>{{end}}
</div>
{{end}}
</div>
{{else}}
<div class="data-row{{if gt $i 4}} hidden-row{{end}}" data-index="{{$i}}">
<div class="data-row-main">
<span class="data-label">{{$item.Label}}</span>
{{if $item.Meta}}<span class="data-meta">{{$item.Meta}}</span>{{end}}
</div>
<div class="data-values">
{{if $item.Type}}<span class="data-value">{{$item.Type}}</span>{{end}}
{{if $item.Date}}<span class="data-date" data-date="{{$item.Date}}"></span>{{end}}
{{if $item.LinkURL}}<a href="{{$item.LinkURL}}" target="_blank" class="btn-icon" title="{{$item.LinkTitle}}">→</a>{{end}}
</div>
</div>
{{end}}
{{end}}
{{if gt (len .Items) 5}}
<div class="data-row show-more" onclick="this.parentElement.querySelectorAll('.hidden-row').forEach(el => el.classList.remove('hidden-row')); this.remove();">
<span class="text-muted">Show all {{len .Items}}...</span>
</div>
{{end}}
</div>
{{end}}
</div>
{{end}}