Skip to content

Commit a215cd6

Browse files
committed
Improved tooltips and absence
1 parent a5c4c7e commit a215cd6

8 files changed

Lines changed: 142 additions & 2 deletions

src/app/benefits-demo/dentistry-record/dentistry-odontogram-anatomic/dentistry-odontogram-anatomic.component.css

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,22 @@
3737
color: #2f49d1;
3838
}
3939

40+
.tooth.absent {
41+
color: #bcc3ce;
42+
}
43+
44+
.tooth.absent .tooth-shadow {
45+
opacity: 0.16;
46+
}
47+
48+
.tooth.absent .tooth-highlight {
49+
opacity: 0.35;
50+
}
51+
52+
.tooth.absent:hover {
53+
color: #aeb7c4;
54+
}
55+
4056
.tooth-outline {
4157
fill: none;
4258
stroke: currentColor;
@@ -161,6 +177,18 @@
161177
color: #cdd7ec;
162178
}
163179

180+
.tooltip-records {
181+
margin-top: 5px;
182+
border-top: 1px solid rgba(141, 164, 210, 0.25);
183+
padding-top: 5px;
184+
}
185+
186+
.tooltip-record-line {
187+
font-size: 0.68rem;
188+
line-height: 1.25;
189+
color: #dce5f4;
190+
}
191+
164192
@media (max-width: 640px) {
165193
.odontogram-canvas-container {
166194
padding: 8px;

src/app/benefits-demo/dentistry-record/dentistry-odontogram-anatomic/dentistry-odontogram-anatomic.component.html

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,7 @@
100100
class="tooth"
101101
*ngFor="let tooth of getTeethForQuadrant(quadrant.prefix); trackBy: trackByToothId"
102102
[class.selected]="isSelected(tooth.id)"
103+
[class.absent]="isToothAbsent(tooth.id)"
103104
[attr.aria-label]="'Tooth ' + tooth.notations.fdi"
104105
[attr.aria-selected]="isSelected(tooth.id)"
105106
(mousedown)="$event.preventDefault()"
@@ -136,6 +137,11 @@
136137
[style.top.px]="tooltipY">
137138
<div class="tooltip-number">Tooth {{ hoveredTooth.notations.fdi }}</div>
138139
<div class="tooltip-type">{{ hoveredTooth.type }}</div>
140+
<ng-container *ngIf="getToothTooltipLines(hoveredTooth.id) as tooltipLines">
141+
<div class="tooltip-records" *ngIf="tooltipLines.length">
142+
<div class="tooltip-record-line" *ngFor="let line of tooltipLines">{{ line }}</div>
143+
</div>
144+
</ng-container>
139145
</div>
140146

141147
<div class="surface-legend" aria-label="Surface color legend">

src/app/benefits-demo/dentistry-record/dentistry-odontogram-anatomic/dentistry-odontogram-anatomic.component.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ export class DentistryOdontogramAnatomicComponent {
2424
@Input() getTeethForQuadrant: (prefix: string) => OdontogramTooth[] = () => [];
2525
@Input() trackByToothId: (_: number, tooth: OdontogramTooth) => string = (_: number, tooth: OdontogramTooth) => tooth.id;
2626
@Input() isSelected: (toothId: string) => boolean = () => false;
27+
@Input() isToothAbsent: (toothId: string) => boolean = () => false;
2728
@Input() getLinePaths: (tooth: OdontogramTooth) => string[] = () => [];
2829
@Input() hasSurfaceVisual: (toothId: string, surfaceCode: string) => boolean = () => false;
2930
@Input() getSurfaceVisualType: (toothId: string, surfaceCode: string) => 'finding' | 'procedure-planned' | 'procedure-completed' | null = () => null;
@@ -33,6 +34,7 @@ export class DentistryOdontogramAnatomicComponent {
3334
@Input() getSurfaceFill: (surfaceCode: string, tooth: OdontogramTooth, quadrantPrefix: string) => string = () => 'none';
3435
@Input() getSurfaceStroke: (surfaceCode: string) => string | null = () => null;
3536
@Input() getSurfaceStrokeWidth: (surfaceCode: string) => string | null = () => null;
37+
@Input() getToothTooltipLines: (toothId: string) => string[] = () => [];
3638

3739
@Output() toothPinned = new EventEmitter<OdontogramTooth>();
3840
@Output() toothMouseEnter = new EventEmitter<{ tooth: OdontogramTooth; event: MouseEvent }>();

src/app/benefits-demo/dentistry-record/dentistry-odontogram-root-surface/dentistry-odontogram-root-surface.component.css

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,18 @@
5555
fill: rgba(47, 73, 209, 0.24);
5656
}
5757

58+
.root-tooth.absent {
59+
color: #b8c0cb;
60+
}
61+
62+
.root-tooth.absent .tooth-node {
63+
fill: #eef1f5;
64+
}
65+
66+
.root-tooth.absent .tooth-label {
67+
fill: #8e98a6;
68+
}
69+
5870
.tooth-node {
5971
fill: #f5f7fa;
6072
stroke: currentColor;
@@ -147,6 +159,18 @@
147159
color: #cdd7ec;
148160
}
149161

162+
.tooltip-records {
163+
margin-top: 5px;
164+
border-top: 1px solid rgba(141, 164, 210, 0.25);
165+
padding-top: 5px;
166+
}
167+
168+
.tooltip-record-line {
169+
font-size: 0.68rem;
170+
line-height: 1.25;
171+
color: #dce5f4;
172+
}
173+
150174
@media (max-width: 640px) {
151175
.root-surface-container {
152176
padding: 8px;

src/app/benefits-demo/dentistry-record/dentistry-odontogram-root-surface/dentistry-odontogram-root-surface.component.html

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ <h3>Root Surface Odontogram (Dennison)</h3>
2424
<g
2525
class="root-tooth"
2626
[class.selected]="isSelected(positioned.tooth.id)"
27+
[class.absent]="isToothAbsent(positioned.tooth.id)"
2728
[attr.transform]="'translate(' + positioned.x + ',' + positioned.y + ') scale(' + getToothScale(positioned.tooth) + ')'"
2829
[attr.aria-label]="'Tooth ' + positioned.tooth.notations.fdi"
2930
[attr.aria-selected]="isSelected(positioned.tooth.id)"
@@ -67,6 +68,7 @@ <h3>Root Surface Odontogram (Dennison)</h3>
6768
<g
6869
class="root-tooth"
6970
[class.selected]="isSelected(positioned.tooth.id)"
71+
[class.absent]="isToothAbsent(positioned.tooth.id)"
7072
[attr.transform]="'translate(' + positioned.x + ',' + positioned.y + ') scale(' + getToothScale(positioned.tooth) + ')'"
7173
[attr.aria-label]="'Tooth ' + positioned.tooth.notations.fdi"
7274
[attr.aria-selected]="isSelected(positioned.tooth.id)"
@@ -110,6 +112,7 @@ <h3>Root Surface Odontogram (Dennison)</h3>
110112
<g
111113
class="root-tooth"
112114
[class.selected]="isSelected(positioned.tooth.id)"
115+
[class.absent]="isToothAbsent(positioned.tooth.id)"
113116
[attr.transform]="'translate(' + positioned.x + ',' + positioned.y + ') scale(' + getToothScale(positioned.tooth) + ')'"
114117
[attr.aria-label]="'Tooth ' + positioned.tooth.notations.fdi"
115118
[attr.aria-selected]="isSelected(positioned.tooth.id)"
@@ -153,6 +156,7 @@ <h3>Root Surface Odontogram (Dennison)</h3>
153156
<g
154157
class="root-tooth"
155158
[class.selected]="isSelected(positioned.tooth.id)"
159+
[class.absent]="isToothAbsent(positioned.tooth.id)"
156160
[attr.transform]="'translate(' + positioned.x + ',' + positioned.y + ') scale(' + getToothScale(positioned.tooth) + ')'"
157161
[attr.aria-label]="'Tooth ' + positioned.tooth.notations.fdi"
158162
[attr.aria-selected]="isSelected(positioned.tooth.id)"
@@ -200,6 +204,11 @@ <h3>Root Surface Odontogram (Dennison)</h3>
200204
[style.top.px]="tooltipY">
201205
<div class="tooltip-number">Tooth {{ hoveredTooth.notations.fdi }}</div>
202206
<div class="tooltip-type">{{ hoveredTooth.type }}</div>
207+
<ng-container *ngIf="getToothTooltipLines(hoveredTooth.id) as tooltipLines">
208+
<div class="tooltip-records" *ngIf="tooltipLines.length">
209+
<div class="tooltip-record-line" *ngFor="let line of tooltipLines">{{ line }}</div>
210+
</div>
211+
</ng-container>
203212
</div>
204213

205214
<div class="surface-legend" aria-label="Surface color legend">

src/app/benefits-demo/dentistry-record/dentistry-odontogram-root-surface/dentistry-odontogram-root-surface.component.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,14 @@ interface ConnectorLine {
2525
export class DentistryOdontogramRootSurfaceComponent implements OnInit, OnChanges {
2626
@Input() getTeethForQuadrant: (prefix: string) => OdontogramTooth[] = () => [];
2727
@Input() isSelected: (toothId: string) => boolean = () => false;
28+
@Input() isToothAbsent: (toothId: string) => boolean = () => false;
2829
@Input() hasSurfaceVisual: (toothId: string, surfaceCode: string) => boolean = () => false;
2930
@Input() getSurfaceVisualType: (toothId: string, surfaceCode: string) => 'finding' | 'procedure-planned' | 'procedure-completed' | null = () => null;
3031
@Input() isSurfacePreview: (toothId: string, surfaceCode: string) => boolean = () => false;
3132
@Input() hoveredTooth: OdontogramTooth | null = null;
3233
@Input() tooltipX = 0;
3334
@Input() tooltipY = 0;
35+
@Input() getToothTooltipLines: (toothId: string) => string[] = () => [];
3436

3537
@Input() surfaceCodeMesial = '';
3638
@Input() surfaceCodeDistal = '';

src/app/benefits-demo/dentistry-record/dentistry-record.component.html

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ <h2>Odontogram</h2>
3939
[getTeethForQuadrant]="getTeethForQuadrantFn"
4040
[trackByToothId]="trackByToothIdFn"
4141
[isSelected]="isSelectedFn"
42+
[isToothAbsent]="isToothAbsentFn"
4243
[getLinePaths]="getLinePathsFn"
4344
[hasSurfaceVisual]="hasSurfaceVisualFn"
4445
[getSurfaceVisualType]="getSurfaceVisualTypeFn"
@@ -48,6 +49,7 @@ <h2>Odontogram</h2>
4849
[getSurfaceFill]="getSurfaceFillFn"
4950
[getSurfaceStroke]="getSurfaceStrokeFn"
5051
[getSurfaceStrokeWidth]="getSurfaceStrokeWidthFn"
52+
[getToothTooltipLines]="getToothTooltipLinesFn"
5153
(toothPinned)="pinTooth($event)"
5254
(toothMouseEnter)="onToothMouseEnter($event.tooth, $event.event)"
5355
(toothMouseMove)="onToothMouseMove($event)"
@@ -58,6 +60,7 @@ <h2>Odontogram</h2>
5860
*ngIf="viewMode === 'rootSurface'"
5961
[getTeethForQuadrant]="getTeethForQuadrantFn"
6062
[isSelected]="isSelectedFn"
63+
[isToothAbsent]="isToothAbsentFn"
6164
[hasSurfaceVisual]="hasSurfaceVisualFn"
6265
[getSurfaceVisualType]="getSurfaceVisualTypeFn"
6366
[isSurfacePreview]="isSurfacePreviewFn"
@@ -71,6 +74,7 @@ <h2>Odontogram</h2>
7174
[surfaceCodeVestibular]="SURFACE_CODE_VESTIBULAR"
7275
[surfaceCodeEntire]="SURFACE_CODE_COMPLETE"
7376
[surfaceCodePeriodontal]="SURFACE_CODE_PERIODONTAL"
77+
[getToothTooltipLines]="getToothTooltipLinesFn"
7478
(toothPinned)="pinTooth($event)"
7579
(toothMouseEnter)="onToothMouseEnter($event.tooth, $event.event)"
7680
(toothMouseMove)="onToothMouseMove($event)"

src/app/benefits-demo/dentistry-record/dentistry-record.component.ts

Lines changed: 67 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ export class DentistryRecordComponent implements OnChanges {
4343
readonly SURFACE_CODE_VESTIBULAR = '62579006';
4444
readonly SURFACE_CODE_COMPLETE = '302214001';
4545
readonly SURFACE_CODE_PERIODONTAL = '8711009';
46+
private readonly TOOTH_ABSENT_FINDING_CODE = '234948008';
4647
private readonly FINDING_SITE_ATTRIBUTE_CODE = '363698007';
4748
private readonly FINDING_SITE_ATTRIBUTE_DISPLAY = 'Finding site (attribute)';
4849
private readonly PROCEDURE_SITE_ATTRIBUTE_CODE = '405813007';
@@ -59,6 +60,7 @@ export class DentistryRecordComponent implements OnChanges {
5960

6061
dentalFindingList: DentalFindingListItem[] = [];
6162
savedSurfaceByToothId: Record<string, Record<string, SurfaceVisualType>> = {};
63+
private absentToothIds = new Set<string>();
6264

6365
readonly surfaceOptions: SnomedConceptOption[] = DENTAL_SURFACE_OPTIONS;
6466
readonly findingOptions: SnomedConceptOption[] = DENTAL_FINDING_OPTIONS;
@@ -79,6 +81,7 @@ export class DentistryRecordComponent implements OnChanges {
7981
readonly getLinePathsFn = (tooth: OdontogramTooth) => this.getLinePaths(tooth);
8082
readonly hasSurfaceVisualFn = (toothId: string, surfaceCode: string) => this.hasSurfaceVisual(toothId, surfaceCode);
8183
readonly getSurfaceVisualTypeFn = (toothId: string, surfaceCode: string) => this.getSurfaceVisualType(toothId, surfaceCode);
84+
readonly isToothAbsentFn = (toothId: string) => this.isToothAbsent(toothId);
8285
readonly getSurfaceOverlayClassFn = (surfaceCode: string, tooth: OdontogramTooth, quadrantPrefix: string) =>
8386
this.getSurfaceOverlayClass(surfaceCode, tooth, quadrantPrefix);
8487
readonly isSurfacePreviewFn = (toothId: string, surfaceCode: string) => this.isSurfacePreview(toothId, surfaceCode);
@@ -87,6 +90,7 @@ export class DentistryRecordComponent implements OnChanges {
8790
this.getSurfaceFill(surfaceCode, tooth, quadrantPrefix);
8891
readonly getSurfaceStrokeFn = (surfaceCode: string) => this.getSurfaceStroke(surfaceCode);
8992
readonly getSurfaceStrokeWidthFn = (surfaceCode: string) => this.getSurfaceStrokeWidth(surfaceCode);
93+
readonly getToothTooltipLinesFn = (toothId: string) => this.getToothTooltipLines(toothId);
9094

9195
constructor(
9296
private patientService: PatientService,
@@ -175,6 +179,9 @@ export class DentistryRecordComponent implements OnChanges {
175179
}
176180

177181
hasSurfaceVisual(toothId: string, surfaceCode: string): boolean {
182+
if (this.isToothAbsent(toothId)) {
183+
return false;
184+
}
178185
return this.hasSavedSurface(toothId, surfaceCode) || this.isSurfacePreview(toothId, surfaceCode);
179186
}
180187

@@ -188,12 +195,19 @@ export class DentistryRecordComponent implements OnChanges {
188195
}
189196

190197
getSurfaceVisualType(toothId: string, surfaceCode: string): SurfaceVisualType | null {
198+
if (this.isToothAbsent(toothId)) {
199+
return null;
200+
}
191201
if (this.isSurfacePreview(toothId, surfaceCode)) {
192202
return this.getPinnedEntryType() === 'procedure' ? 'procedure-planned' : 'finding';
193203
}
194204
return this.getSavedSurfaceVisualType(toothId, surfaceCode);
195205
}
196206

207+
isToothAbsent(toothId: string): boolean {
208+
return this.absentToothIds.has(toothId);
209+
}
210+
197211
isPinnedSiteSelected(siteCode: string): boolean {
198212
return this.getPinnedSiteCodes().includes(siteCode);
199213
}
@@ -992,6 +1006,7 @@ export class DentistryRecordComponent implements OnChanges {
9921006
}, {} as Record<string, BodyStructure>);
9931007

9941008
this.savedSurfaceByToothId = {};
1009+
this.absentToothIds = new Set<string>();
9951010

9961011
const conditionItems = conditions
9971012
.filter((condition) => this.isDentalCondition(condition))
@@ -1011,8 +1026,14 @@ export class DentistryRecordComponent implements OnChanges {
10111026
const clinicalStatusCode = this.getConditionClinicalStatusCode(condition);
10121027
const clinicalStatusDisplay = this.getConditionClinicalStatusDisplay(condition, clinicalStatusCode);
10131028
const isResolved = clinicalStatusCode === 'resolved';
1029+
const findingCode = condition.code?.coding?.[0]?.code || '';
1030+
const isToothAbsentFinding = findingCode === this.TOOTH_ABSENT_FINDING_CODE;
10141031

1015-
if (!isResolved) {
1032+
if (!isResolved && toothId && isToothAbsentFinding) {
1033+
this.absentToothIds.add(toothId);
1034+
}
1035+
1036+
if (!isResolved && !isToothAbsentFinding) {
10161037
siteCodes.forEach((siteCode) => {
10171038
if (!toothId) {
10181039
return;
@@ -1026,7 +1047,6 @@ export class DentistryRecordComponent implements OnChanges {
10261047
.map((siteCode) => this.surfaceOptions.find((option) => option.code === siteCode)?.display || siteCode)
10271048
.filter(Boolean);
10281049

1029-
const findingCode = condition.code?.coding?.[0]?.code || '';
10301050
const findingDisplay = condition.code?.coding?.[0]?.display || condition.code?.text || 'Not specified';
10311051

10321052
return {
@@ -1193,6 +1213,51 @@ export class DentistryRecordComponent implements OnChanges {
11931213
this.tooltipY = event.clientY - offset;
11941214
}
11951215

1216+
private getToothTooltipLines(toothId: string): string[] {
1217+
const items = this.dentalFindingList.filter((item) => item.toothId === toothId);
1218+
if (!items.length) {
1219+
return [];
1220+
}
1221+
1222+
const findingLabels = this.toUniqueCompactLabels(
1223+
items
1224+
.filter((item) => item.entryType === 'finding' && !item.isResolved)
1225+
.map((item) => item.findingDisplay)
1226+
);
1227+
const procedureLabels = this.toUniqueCompactLabels(
1228+
items
1229+
.filter((item) => item.entryType === 'procedure')
1230+
.map((item) => `${this.toTooltipProcedureName(item.findingDisplay)} (${item.isResolved ? 'completed' : 'planned'})`)
1231+
);
1232+
1233+
const lines: string[] = [];
1234+
if (findingLabels.length) {
1235+
lines.push(this.compactTooltipList(findingLabels, 2));
1236+
}
1237+
if (procedureLabels.length) {
1238+
lines.push(this.compactTooltipList(procedureLabels, 2));
1239+
}
1240+
return lines;
1241+
}
1242+
1243+
private toUniqueCompactLabels(values: string[]): string[] {
1244+
const cleaned = values
1245+
.map((value) => (value || '').trim())
1246+
.filter((value) => !!value);
1247+
return Array.from(new Set(cleaned));
1248+
}
1249+
1250+
private toTooltipProcedureName(display: string): string {
1251+
return (display || '').replace(/\s*\(procedure\)\s*$/i, '').trim();
1252+
}
1253+
1254+
private compactTooltipList(values: string[], maxItems: number): string {
1255+
if (values.length <= maxItems) {
1256+
return values.join(', ');
1257+
}
1258+
return `${values.slice(0, maxItems).join(', ')} +${values.length - maxItems}`;
1259+
}
1260+
11961261
private clearToothDraft(toothId: string): void {
11971262
delete this.toothDraftById[toothId];
11981263
delete this.findingQueryByToothId[toothId];

0 commit comments

Comments
 (0)