753 lines
29 KiB
Cheetah
753 lines
29 KiB
Cheetah
{{define "dossier"}}
|
||
<script>function toggleExpand(el) { el.classList.toggle("expanded"); var icon = el.querySelector(".expand-icon"); if (icon) icon.textContent = el.classList.contains("expanded") ? "−" : "+"; var children = el.nextElementSibling; if (children && children.classList.contains("data-row-children")) children.classList.toggle("show"); }</script>
|
||
<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}}
|
||
|
||
<!-- Imaging Section -->
|
||
<div class="data-card">
|
||
<div class="data-card-header">
|
||
<div class="data-card-indicator imaging"></div>
|
||
<div class="data-card-title">
|
||
<span class="section-heading">{{.T.section_imaging}}</span>
|
||
{{if .Studies}}
|
||
<span class="data-card-summary" data-studies="{{.StudyCount}}" data-slices="{{.TotalSlices}}"></span>
|
||
{{else}}
|
||
<span class="data-card-summary">{{.T.no_imaging}}</span>
|
||
{{end}}
|
||
</div>
|
||
{{if .HasImaging}}
|
||
<a href="/viewer/?token={{.TargetDossier.DossierID}}" target="_blank" class="btn btn-small">{{.T.open_viewer}}</a>
|
||
{{end}}
|
||
</div>
|
||
|
||
{{if .Studies}}
|
||
<div class="data-table" id="studies-table">
|
||
{{range $i, $s := .Studies}}
|
||
{{if eq $s.SeriesCount 1}}
|
||
<div class="data-row single study-row" data-index="{{$i}}">
|
||
<div class="data-row-main">
|
||
<span class="data-label">{{$s.Description}}</span>
|
||
</div>
|
||
<div class="data-values">
|
||
<span class="data-date" data-date="{{$s.Date}}"></span>
|
||
<a href="/viewer/?token={{$.TargetDossier.DossierID}}&study={{$s.ID}}" target="_blank" class="btn-icon" title="{{$.T.open_viewer}}">→</a>
|
||
</div>
|
||
</div>
|
||
{{else}}
|
||
<div class="data-row expandable study-row" data-index="{{$i}}" onclick="toggleExpand(this)">
|
||
<div class="data-row-main">
|
||
<span class="expand-icon">+</span>
|
||
<span class="data-label">{{$s.Description}}</span>
|
||
</div>
|
||
<div class="data-values">
|
||
<span class="data-value mono series-count" data-count="{{$s.SeriesCount}}"></span>
|
||
<span class="data-date" data-date="{{$s.Date}}"></span>
|
||
<a href="/viewer/?token={{$.TargetDossier.DossierID}}&study={{$s.ID}}" target="_blank" class="btn-icon" onclick="event.stopPropagation()" title="{{$.T.open_viewer}}">→</a>
|
||
</div>
|
||
</div>
|
||
<div class="data-row-children" data-index="{{$i}}">
|
||
{{range $s.Series}}{{if gt .SliceCount 0}}
|
||
<div class="data-row child">
|
||
<span class="data-label">{{if .Description}}{{.Description}}{{else}}{{.Modality}}{{end}}</span>
|
||
<span class="data-value mono slice-count" data-count="{{.SliceCount}}"></span>
|
||
<a href="/viewer/?token={{$.TargetDossier.DossierID}}&study={{$s.ID}}&series={{.ID}}" target="_blank" class="btn-icon" title="{{$.T.open_viewer}}">→</a>
|
||
</div>
|
||
{{end}}{{end}}
|
||
</div>
|
||
{{end}}
|
||
{{end}}
|
||
{{if gt .StudyCount 5}}
|
||
<div class="data-row show-more" id="show-more-studies" data-count="{{.StudyCount}}"></div>
|
||
{{end}}
|
||
</div>
|
||
{{end}}
|
||
</div>
|
||
|
||
<!-- Labs Section -->
|
||
<div class="data-card">
|
||
<div class="data-card-header">
|
||
<div class="data-card-indicator labs"></div>
|
||
<div class="data-card-title">
|
||
<span class="section-heading">{{.T.section_labs}}</span>
|
||
{{if .Labs}}
|
||
<span class="data-card-summary">{{len .Labs}} results</span>
|
||
{{else}}
|
||
<span class="data-card-summary">{{.T.no_lab_data}}</span>
|
||
{{end}}
|
||
</div>
|
||
</div>
|
||
{{if .Labs}}
|
||
<div class="data-table">
|
||
{{range .Labs}}
|
||
<div class="data-row">
|
||
<div class="data-row-main">
|
||
<span class="data-label">{{.Value}}</span>
|
||
{{if .Summary}}<span class="data-meta">{{.Summary}}</span>{{end}}
|
||
</div>
|
||
<div class="data-values">
|
||
{{if .Date}}<span class="data-date">{{.Date}}</span>{{end}}
|
||
</div>
|
||
</div>
|
||
{{end}}
|
||
</div>
|
||
{{end}}
|
||
</div>
|
||
|
||
<!-- Documents Section -->
|
||
<div class="data-card" {{if not .Documents}}style="display:none;"{{end}}>
|
||
<div class="data-card-header">
|
||
<div class="data-card-indicator records"></div>
|
||
<div class="data-card-title">
|
||
<span class="section-heading">{{.T.section_records}}</span>
|
||
<span class="data-card-summary">{{len .Documents}} documents</span>
|
||
</div>
|
||
</div>
|
||
{{if .Documents}}
|
||
<div class="data-table">
|
||
{{range .Documents}}
|
||
<div class="data-row">
|
||
<div class="data-row-main">
|
||
<span class="data-label">{{.Value}}</span>
|
||
{{if .Summary}}<span class="data-meta">{{.Summary}}</span>{{end}}
|
||
</div>
|
||
<div class="data-values">
|
||
{{if .Type}}<span class="data-value">{{.Type}}</span>{{end}}
|
||
{{if .Date}}<span class="data-date">{{.Date}}</span>{{end}}
|
||
</div>
|
||
</div>
|
||
{{end}}
|
||
</div>
|
||
{{end}}
|
||
</div>
|
||
|
||
<!-- Procedures Section -->
|
||
<div class="data-card" {{if not .Procedures}}style="display:none;"{{end}}>
|
||
<div class="data-card-header">
|
||
<div class="data-card-indicator" style="background: #DC2626;"></div>
|
||
<div class="data-card-title">
|
||
<span class="section-heading">Procedures & Surgery</span>
|
||
<span class="data-card-summary">{{len .Procedures}} procedures</span>
|
||
</div>
|
||
</div>
|
||
{{if .Procedures}}
|
||
<div class="data-table">
|
||
{{range .Procedures}}
|
||
<div class="data-row">
|
||
<div class="data-row-main">
|
||
<span class="data-label">{{.Value}}</span>
|
||
{{if .Summary}}<span class="data-meta">{{.Summary}}</span>{{end}}
|
||
</div>
|
||
<div class="data-values">
|
||
{{if .Date}}<span class="data-date">{{.Date}}</span>{{end}}
|
||
</div>
|
||
</div>
|
||
{{end}}
|
||
</div>
|
||
{{end}}
|
||
</div>
|
||
|
||
<!-- Assessments Section -->
|
||
<div class="data-card" {{if not .Assessments}}style="display:none;"{{end}}>
|
||
<div class="data-card-header">
|
||
<div class="data-card-indicator" style="background: #7C3AED;"></div>
|
||
<div class="data-card-title">
|
||
<span class="section-heading">Clinical Assessments</span>
|
||
<span class="data-card-summary">{{len .Assessments}} assessments</span>
|
||
</div>
|
||
</div>
|
||
{{if .Assessments}}
|
||
<div class="data-table">
|
||
{{range .Assessments}}
|
||
<div class="data-row">
|
||
<div class="data-row-main">
|
||
<span class="data-label">{{.Value}}</span>
|
||
{{if .Summary}}<span class="data-meta">{{.Summary}}</span>{{end}}
|
||
</div>
|
||
<div class="data-values">
|
||
{{if .Date}}<span class="data-date">{{.Date}}</span>{{end}}
|
||
</div>
|
||
</div>
|
||
{{end}}
|
||
</div>
|
||
{{end}}
|
||
</div>
|
||
|
||
<!-- Genetics Section -->
|
||
<div class="data-card" id="genetics-card" {{if not .HasGenome}}style="display:none;"{{end}}>
|
||
<div class="data-card-header">
|
||
<div class="data-card-indicator" style="background: #8B5CF6;"></div>
|
||
<div class="data-card-title">
|
||
<span class="section-heading">{{.T.section_genetics}}</span>
|
||
<span class="data-card-summary" id="genetics-summary">Loading...</span>
|
||
</div>
|
||
<a href="#" class="btn btn-small" id="show-all-genetics" style="display:none;" onclick="showAllGeneticsWarning(); return false;">Show all</a>
|
||
</div>
|
||
<div class="data-table" id="genetics-table"></div>
|
||
</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>
|
||
|
||
<!-- Uploads Section -->
|
||
<div class="data-card">
|
||
<div class="data-card-header">
|
||
<div class="data-card-indicator uploads"></div>
|
||
<div class="data-card-title">
|
||
<span class="section-heading">{{.T.section_uploads}}</span>
|
||
<span class="data-card-summary">
|
||
{{if .Uploads}}<span data-files="{{.UploadCount}}" data-size="{{.UploadSize}}"></span>{{else}}{{.T.no_files}}{{end}}
|
||
</span>
|
||
</div>
|
||
{{if .CanEdit}}<a href="/dossier/{{.TargetDossier.DossierID}}/upload" class="btn btn-small">{{.T.manage}}</a>{{else}}<span class="btn btn-small btn-disabled" title="{{.T.no_upload_access}}">{{.T.manage}}</span>{{end}}
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Medications Section -->
|
||
<div class="data-card" {{if not .Medications}}style="display:none;"{{end}}>
|
||
<div class="data-card-header">
|
||
<div class="data-card-indicator medications"></div>
|
||
<div class="data-card-title">
|
||
<span class="section-heading">{{.T.section_medications}}</span>
|
||
<span class="data-card-summary">{{len .Medications}} medications</span>
|
||
</div>
|
||
</div>
|
||
{{if .Medications}}
|
||
<div class="data-table">
|
||
{{range .Medications}}
|
||
<div class="data-row">
|
||
<div class="data-row-main">
|
||
<span class="data-label">{{.Value}}</span>
|
||
{{if .Summary}}<span class="data-meta">{{.Summary}}</span>{{end}}
|
||
</div>
|
||
<div class="data-values">
|
||
{{if .Date}}<span class="data-date">{{.Date}}</span>{{end}}
|
||
</div>
|
||
</div>
|
||
{{end}}
|
||
</div>
|
||
{{end}}
|
||
</div>
|
||
|
||
<!-- Symptoms Section -->
|
||
<div class="data-card" {{if not .Symptoms}}style="display:none;"{{end}}>
|
||
<div class="data-card-header">
|
||
<div class="data-card-indicator" style="background: #F59E0B;"></div>
|
||
<div class="data-card-title">
|
||
<span class="section-heading">Symptoms</span>
|
||
<span class="data-card-summary">{{len .Symptoms}} symptoms</span>
|
||
</div>
|
||
</div>
|
||
{{if .Symptoms}}
|
||
<div class="data-table">
|
||
{{range .Symptoms}}
|
||
<div class="data-row">
|
||
<div class="data-row-main">
|
||
<span class="data-label">{{.Value}}</span>
|
||
{{if .Summary}}<span class="data-meta">{{.Summary}}</span>{{end}}
|
||
</div>
|
||
<div class="data-values">
|
||
{{if .Date}}<span class="data-date">{{.Date}}</span>{{end}}
|
||
</div>
|
||
</div>
|
||
{{end}}
|
||
</div>
|
||
{{end}}
|
||
</div>
|
||
|
||
<!-- Hospitalizations Section -->
|
||
<div class="data-card" {{if not .Hospitalizations}}style="display:none;"{{end}}>
|
||
<div class="data-card-header">
|
||
<div class="data-card-indicator" style="background: #EF4444;"></div>
|
||
<div class="data-card-title">
|
||
<span class="section-heading">Hospitalizations</span>
|
||
<span class="data-card-summary">{{len .Hospitalizations}} hospitalizations</span>
|
||
</div>
|
||
</div>
|
||
{{if .Hospitalizations}}
|
||
<div class="data-table">
|
||
{{range .Hospitalizations}}
|
||
<div class="data-row">
|
||
<div class="data-row-main">
|
||
<span class="data-label">{{.Value}}</span>
|
||
{{if .Summary}}<span class="data-meta">{{.Summary}}</span>{{end}}
|
||
</div>
|
||
<div class="data-values">
|
||
{{if .Date}}<span class="data-date">{{.Date}}</span>{{end}}
|
||
</div>
|
||
</div>
|
||
{{end}}
|
||
</div>
|
||
{{end}}
|
||
</div>
|
||
|
||
<!-- Therapies Section -->
|
||
<div class="data-card" {{if not .Therapies}}style="display:none;"{{end}}>
|
||
<div class="data-card-header">
|
||
<div class="data-card-indicator" style="background: #10B981;"></div>
|
||
<div class="data-card-title">
|
||
<span class="section-heading">Therapies</span>
|
||
<span class="data-card-summary">{{len .Therapies}} therapies</span>
|
||
</div>
|
||
</div>
|
||
{{if .Therapies}}
|
||
<div class="data-table">
|
||
{{range .Therapies}}
|
||
<div class="data-row">
|
||
<div class="data-row-main">
|
||
<span class="data-label">{{.Value}}</span>
|
||
{{if .Summary}}<span class="data-meta">{{.Summary}}</span>{{end}}
|
||
</div>
|
||
<div class="data-values">
|
||
{{if .Date}}<span class="data-date">{{.Date}}</span>{{end}}
|
||
</div>
|
||
</div>
|
||
{{end}}
|
||
</div>
|
||
{{end}}
|
||
</div>
|
||
|
||
<!-- Vitals Section - Coming Soon -->
|
||
<div class="data-card coming-soon">
|
||
<div class="data-card-header">
|
||
<div class="data-card-indicator vitals"></div>
|
||
<div class="data-card-title">
|
||
<span class="section-heading">{{.T.section_vitals}}</span>
|
||
<span class="data-card-summary">{{.T.vitals_desc}}</span>
|
||
</div>
|
||
<span class="badge-soon">{{.T.coming_soon}}</span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Privacy Section -->
|
||
<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}}
|
||
|
||
<!-- Privacy actions -->
|
||
<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>
|
||
|
||
<script>
|
||
const dossierGUID = "{{.TargetDossier.DossierID}}";
|
||
const hasGenome = {{if .HasGenome}}true{{else}}false{{end}};
|
||
const userLang = "{{.Lang}}";
|
||
|
||
// Show all categories, but hide negative variants by default
|
||
let showAllGenetics = false; // show negative variants
|
||
let showAllCategories = false; // show all 13 categories (vs top 5)
|
||
let allCategories = {};
|
||
|
||
// i18n strings from server
|
||
const i18n = {
|
||
studies: "{{.T.imaging_summary}}",
|
||
series: "{{.T.series_count}}",
|
||
files: "{{.T.files_summary}}",
|
||
showAll: "{{.T.show_all_studies}}",
|
||
genomeEnglishOnly: "{{.T.genome_english_only}}",
|
||
genomeVariants: "{{.T.genome_variants}}",
|
||
genomeHidden: "{{.T.genome_hidden}}",
|
||
genomeShowAllCategories: "{{.T.genome_show_all_categories}}"
|
||
};
|
||
|
||
function toggleExpand(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('data-row-children')) {
|
||
children.classList.toggle('show');
|
||
}
|
||
}
|
||
|
||
// Format allele: ensure single semicolon between letters
|
||
function formatAllele(genotype) {
|
||
const letters = genotype.replace(/;/g, '');
|
||
return letters.split('').join(';');
|
||
}
|
||
|
||
// Load genetics categories from API
|
||
async function loadGeneticsCategories() {
|
||
if (!hasGenome) return;
|
||
|
||
try {
|
||
// Fetch category counts (includes shown/hidden per category)
|
||
const resp = await fetch(`/api/categories?dossier=${dossierGUID}&type=genome`);
|
||
allCategories = await resp.json();
|
||
|
||
// Show "Show all" link if there are any hidden variants
|
||
const totalHidden = Object.values(allCategories).reduce((sum, c) => sum + (c.hidden || 0), 0);
|
||
if (totalHidden > 0) {
|
||
document.getElementById('show-all-genetics').style.display = '';
|
||
}
|
||
|
||
renderGeneticsCategories();
|
||
} catch (e) {
|
||
document.getElementById('genetics-summary').textContent = 'Error loading';
|
||
}
|
||
}
|
||
|
||
function renderGeneticsCategories() {
|
||
let totalShown = 0;
|
||
let totalHidden = 0;
|
||
|
||
for (const counts of Object.values(allCategories)) {
|
||
totalShown += counts.shown || 0;
|
||
totalHidden += counts.hidden || 0;
|
||
}
|
||
|
||
if (totalShown === 0 && totalHidden === 0) {
|
||
document.getElementById('genetics-card').style.display = 'none';
|
||
return;
|
||
}
|
||
|
||
let summary = showAllGenetics ? `${totalShown + totalHidden} ${i18n.genomeVariants}` : `${totalShown} ${i18n.genomeVariants}`;
|
||
if (totalHidden > 0 && !showAllGenetics) {
|
||
summary += ` <span class="text-muted">(${totalHidden} ${i18n.genomeHidden})</span>`;
|
||
}
|
||
document.getElementById('genetics-summary').innerHTML = summary;
|
||
|
||
const btn = document.getElementById('show-all-genetics');
|
||
if (showAllGenetics) {
|
||
btn.textContent = 'Hide negative';
|
||
btn.onclick = () => { showAllGenetics = false; reloadAllExpandedCategories(); renderGeneticsCategories(); return false; };
|
||
} else {
|
||
btn.textContent = 'Show all';
|
||
btn.onclick = () => { showAllGeneticsWarning(); return false; };
|
||
}
|
||
|
||
const table = document.getElementById('genetics-table');
|
||
let html = '';
|
||
|
||
// Show language notice for non-English users
|
||
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>`;
|
||
}
|
||
|
||
// Sort by predefined priority (less alarming first), then by count within same priority
|
||
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;
|
||
if (aPriority !== bPriority) return aPriority - bPriority;
|
||
// Same priority: sort by count
|
||
const aCount = showAllGenetics ? (a[1].shown + a[1].hidden) : a[1].shown;
|
||
const bCount = showAllGenetics ? (b[1].shown + b[1].hidden) : b[1].shown;
|
||
return bCount - aCount;
|
||
});
|
||
|
||
const maxCategories = 5;
|
||
const displayCategories = showAllCategories ? sorted : sorted.slice(0, maxCategories);
|
||
|
||
for (const [cat, counts] of displayCategories) {
|
||
const shown = counts.shown || 0;
|
||
const 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 genetics-category" 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="data-row-children genetics-children" data-category="${cat}"></div>`;
|
||
}
|
||
|
||
// Show "Show all X categories" link if there are more
|
||
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>`;
|
||
}
|
||
|
||
table.innerHTML = html;
|
||
}
|
||
|
||
// Reload data for any expanded categories (when toggling show all)
|
||
function reloadAllExpandedCategories() {
|
||
document.querySelectorAll('.genetics-children[data-loaded="true"]').forEach(el => {
|
||
el.removeAttribute('data-loaded');
|
||
});
|
||
}
|
||
|
||
function showAllGeneticsWarning() {
|
||
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 = '';
|
||
const maxShow = 5;
|
||
data.matches.slice(0, maxShow).forEach(m => {
|
||
const mag = m.magnitude ? m.magnitude.toFixed(1) : '0';
|
||
const gene = m.gene || '';
|
||
const allele = formatAllele(m.genotype);
|
||
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">${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 > maxShow) {
|
||
const remaining = data.total - maxShow;
|
||
const showMoreText = remaining > 50
|
||
? `${remaining} more variants — <a href="/connect" style="color: var(--primary);">use Claude</a> to explore intelligently`
|
||
: `Show more (${remaining} remaining) →`;
|
||
html += `<div class="sg-show-more" data-offset="${maxShow}" data-total="${data.total}" onclick="loadMoreGenetics('${category}', this)">${showMoreText}</div>`;
|
||
}
|
||
|
||
children.innerHTML = html;
|
||
} catch (e) {
|
||
children.innerHTML = '<div class="data-row child"><span class="text-muted">Error loading variants</span></div>';
|
||
}
|
||
}
|
||
|
||
async function loadMoreGenetics(category, el) {
|
||
const batchSize = 20;
|
||
const currentOffset = parseInt(el.dataset.offset || '0', 10);
|
||
const total = parseInt(el.dataset.total || '0', 10);
|
||
|
||
el.textContent = 'Loading...';
|
||
|
||
try {
|
||
const includeHidden = showAllGenetics ? '&include_hidden=true' : '';
|
||
const resp = await fetch(`/api/genome?dossier=${dossierGUID}&category=${encodeURIComponent(category)}&offset=${currentOffset}&limit=${batchSize}${includeHidden}`);
|
||
const data = await resp.json();
|
||
|
||
let html = '';
|
||
data.matches.forEach(m => {
|
||
const mag = m.magnitude ? m.magnitude.toFixed(1) : '0';
|
||
const gene = m.gene || '';
|
||
const allele = formatAllele(m.genotype);
|
||
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">${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>`;
|
||
});
|
||
|
||
// Insert new items before the "Show more" button
|
||
el.insertAdjacentHTML('beforebegin', html);
|
||
|
||
const newOffset = currentOffset + data.matches.length;
|
||
const remaining = total - newOffset;
|
||
|
||
if (remaining > 0) {
|
||
el.dataset.offset = newOffset;
|
||
if (remaining > 50) {
|
||
el.innerHTML = `${remaining} more variants — <a href="/connect" style="color: var(--primary);">use Claude</a> to explore intelligently`;
|
||
} else {
|
||
el.textContent = `Show more (${remaining} remaining) →`;
|
||
}
|
||
} else {
|
||
el.remove();
|
||
}
|
||
} catch (e) {
|
||
el.textContent = 'Error loading variants';
|
||
}
|
||
}
|
||
|
||
loadGeneticsCategories();
|
||
|
||
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();
|
||
}
|
||
});
|
||
|
||
document.querySelectorAll('[data-studies]').forEach(el => {
|
||
const studies = el.dataset.studies;
|
||
const slices = el.dataset.slices;
|
||
el.textContent = i18n.studies.replace('%d', studies).replace('%d', slices);
|
||
});
|
||
|
||
document.querySelectorAll('.series-count').forEach(el => {
|
||
const n = parseInt(el.dataset.count, 10);
|
||
el.textContent = i18n.series.replace('%d', n);
|
||
});
|
||
|
||
const plural = new Intl.PluralRules(navigator.language);
|
||
document.querySelectorAll('.slice-count').forEach(el => {
|
||
const n = parseInt(el.dataset.count, 10);
|
||
el.textContent = n + (plural.select(n) === 'one' ? ' slice' : ' slices');
|
||
});
|
||
|
||
document.querySelectorAll('[data-files]').forEach(el => {
|
||
const files = el.dataset.files;
|
||
const size = el.dataset.size;
|
||
el.textContent = i18n.files.replace('%d', files).replace('%s', size);
|
||
});
|
||
|
||
const maxVisible = 5;
|
||
document.querySelectorAll('[data-index]').forEach(el => {
|
||
if (parseInt(el.dataset.index, 10) >= maxVisible) {
|
||
el.style.display = 'none';
|
||
}
|
||
});
|
||
|
||
const showMore = document.getElementById('show-more-studies');
|
||
if (showMore) {
|
||
const count = showMore.dataset.count;
|
||
showMore.innerHTML = '<span class="text-muted">' + i18n.showAll.replace('%d', count) + '</span>';
|
||
showMore.style.cursor = 'pointer';
|
||
showMore.onclick = () => {
|
||
document.querySelectorAll('[data-index]').forEach(el => el.style.display = '');
|
||
showMore.style.display = 'none';
|
||
};
|
||
}
|
||
|
||
if (new URLSearchParams(window.location.search).get("requested") === "1") {
|
||
alert("Your request has been sent. The dossier owner will be notified and can approve access.");
|
||
history.replaceState(null, "", window.location.pathname);
|
||
}
|
||
</script>
|
||
{{end}}
|