diff --git a/docs/api/openapi.yaml b/docs/api/openapi.yaml index 615a13b0b..ec63a5ca0 100644 --- a/docs/api/openapi.yaml +++ b/docs/api/openapi.yaml @@ -78354,6 +78354,13 @@ components: items: type: string description: Names that become aliases of the target. + adoptedPanel: + description: "The panel the target takes over: when the target has none, the first panel a source carries, in the order + the sources were given. `null` when the target keeps its own panel or no source has one. Reported on a dry + run too, while `target.panel` there is still the stored one." + type: + - string + - "null" mergeId: description: "`null` on a dry run." type: @@ -78366,6 +78373,7 @@ components: - movedReadings - convertedReadings - aliasesAdded + - adoptedPanel - mergeId additionalProperties: false description: What a merge did, or would do on a dry run. Readings in another unit are converted where the analyte's diff --git a/messages/de.json b/messages/de.json index ce094622f..c958b77cb 100644 --- a/messages/de.json +++ b/messages/de.json @@ -10569,6 +10569,7 @@ "convertedOther": "{count} Messungen, umgerechnet von {unit} in {targetUnit}", "refused": "{unit} lässt sich nicht in {targetUnit} umrechnen.", "aliasesAdded": "Diese Namen führen danach zum behaltenen Biomarker:", + "adoptsPanel": "Der behaltene Biomarker übernimmt das Panel {panel}.", "blocked": "Solange sich eine Einheit nicht umrechnen lässt, wird nichts zusammengeführt. Lass diesen Biomarker weg oder behalte einen anderen.", "noUndo": "Das Zusammenführen lässt sich nicht rückgängig machen.", "doneToastOne": "In {name} zusammengeführt, {count} Messung verschoben", diff --git a/messages/en.json b/messages/en.json index 4691baf80..9e061c059 100644 --- a/messages/en.json +++ b/messages/en.json @@ -10569,6 +10569,7 @@ "convertedOther": "{count} readings, converted from {unit} to {targetUnit}", "refused": "{unit} cannot be converted to {targetUnit}.", "aliasesAdded": "These names will lead to the kept biomarker:", + "adoptsPanel": "The kept biomarker takes over the panel {panel}.", "blocked": "Nothing can be merged while a unit cannot be converted. Leave that biomarker out or keep another one.", "noUndo": "A merge cannot be undone.", "doneToastOne": "Merged into {name}, {count} reading moved", diff --git a/messages/es.json b/messages/es.json index 3df9d94d7..b8abc50ff 100644 --- a/messages/es.json +++ b/messages/es.json @@ -10569,6 +10569,7 @@ "convertedOther": "{count} mediciones, convertidas de {unit} a {targetUnit}", "refused": "{unit} no se puede convertir a {targetUnit}.", "aliasesAdded": "Estos nombres llevarán al biomarcador conservado:", + "adoptsPanel": "El biomarcador conservado adopta el panel {panel}.", "blocked": "No se combina nada mientras una unidad no se pueda convertir. Deja fuera ese biomarcador o conserva otro.", "noUndo": "Una combinación no se puede deshacer.", "doneToastOne": "Combinado en {name}, {count} medición movida", diff --git a/messages/fr.json b/messages/fr.json index a61b5944f..8a5e789cc 100644 --- a/messages/fr.json +++ b/messages/fr.json @@ -10569,6 +10569,7 @@ "convertedOther": "{count} mesures, converties de {unit} en {targetUnit}", "refused": "{unit} ne peut pas être converti en {targetUnit}.", "aliasesAdded": "Ces noms mèneront au biomarqueur conservé :", + "adoptsPanel": "Le biomarqueur conservé reprend le panel {panel}.", "blocked": "Rien n'est fusionné tant qu'une unité ne peut pas être convertie. Laissez ce biomarqueur de côté ou conservez-en un autre.", "noUndo": "Une fusion ne peut pas être annulée.", "doneToastOne": "Fusionné dans {name}, {count} mesure déplacée", diff --git a/messages/it.json b/messages/it.json index b1c7e66af..545294812 100644 --- a/messages/it.json +++ b/messages/it.json @@ -10569,6 +10569,7 @@ "convertedOther": "{count} misurazioni, convertite da {unit} a {targetUnit}", "refused": "{unit} non può essere convertito in {targetUnit}.", "aliasesAdded": "Questi nomi porteranno al biomarcatore mantenuto:", + "adoptsPanel": "Il biomarcatore mantenuto assume il panel {panel}.", "blocked": "Non si unisce nulla finché un'unità non può essere convertita. Escludi quel biomarcatore o mantienine un altro.", "noUndo": "Un'unione non può essere annullata.", "doneToastOne": "Unito in {name}, {count} misurazione spostata", diff --git a/messages/ko.json b/messages/ko.json index 3834a66ac..4ae094853 100644 --- a/messages/ko.json +++ b/messages/ko.json @@ -10569,6 +10569,7 @@ "convertedOther": "측정값 {count}개, {unit}에서 {targetUnit}(으)로 환산", "refused": "{unit}은(는) {targetUnit}(으)로 환산할 수 없어요.", "aliasesAdded": "이 이름들은 남긴 바이오마커로 연결돼요:", + "adoptsPanel": "남긴 바이오마커가 {panel} 패널을 이어받아요.", "blocked": "환산할 수 없는 단위가 있으면 아무것도 합치지 않아요. 그 바이오마커를 빼거나 다른 항목을 남겨 주세요.", "noUndo": "합친 뒤에는 되돌릴 수 없어요.", "doneToastOne": "{name}(으)로 합쳤어요, 측정값 {count}개 이동", diff --git a/messages/nl.json b/messages/nl.json index c0c3eeda5..9df3a49be 100644 --- a/messages/nl.json +++ b/messages/nl.json @@ -10569,6 +10569,7 @@ "convertedOther": "{count} metingen, omgerekend van {unit} naar {targetUnit}", "refused": "{unit} kan niet worden omgerekend naar {targetUnit}.", "aliasesAdded": "Deze namen verwijzen voortaan naar de behouden biomarker:", + "adoptsPanel": "De behouden biomarker neemt de groep {panel} over.", "blocked": "Er kan niets worden samengevoegd zolang een eenheid niet kan worden omgerekend. Laat die biomarker weg of behoud een andere.", "noUndo": "Samenvoegen kan niet ongedaan worden gemaakt.", "doneToastOne": "Samengevoegd in {name}, {count} meting verplaatst", diff --git a/messages/pl.json b/messages/pl.json index 5d10dd0f0..7caba6896 100644 --- a/messages/pl.json +++ b/messages/pl.json @@ -10569,6 +10569,7 @@ "convertedOther": "{count} pomiarów, przeliczonych z {unit} na {targetUnit}", "refused": "Nie można przeliczyć {unit} na {targetUnit}.", "aliasesAdded": "Te nazwy będą prowadzić do zachowanego biomarkera:", + "adoptsPanel": "Zachowany biomarker przejmuje panel {panel}.", "blocked": "Nic nie zostanie scalone, dopóki jednej jednostki nie da się przeliczyć. Pomiń ten biomarker albo zachowaj inny.", "noUndo": "Scalenia nie można cofnąć.", "doneToastOne": "Scalono w {name}, przeniesiono {count} pomiar", diff --git a/messages/pt-BR.json b/messages/pt-BR.json index ddd915acd..903c659d5 100644 --- a/messages/pt-BR.json +++ b/messages/pt-BR.json @@ -10569,6 +10569,7 @@ "convertedOther": "{count} leituras, convertidas de {unit} para {targetUnit}", "refused": "{unit} não pode ser convertida para {targetUnit}.", "aliasesAdded": "Estes nomes levarão ao biomarcador mantido:", + "adoptsPanel": "O biomarcador mantido assume o painel {panel}.", "blocked": "Nada pode ser mesclado enquanto uma unidade não puder ser convertida. Deixe esse biomarcador fora ou mantenha outro.", "noUndo": "Uma mesclagem não pode ser desfeita.", "doneToastOne": "Mesclado em {name}, {count} leitura movida", diff --git a/messages/ru.json b/messages/ru.json index 7be6be626..cd0a0418c 100644 --- a/messages/ru.json +++ b/messages/ru.json @@ -10569,6 +10569,7 @@ "convertedOther": "{count} измерений, пересчитаны из {unit} в {targetUnit}", "refused": "{unit} нельзя пересчитать в {targetUnit}.", "aliasesAdded": "Эти имена будут вести к оставленному показателю:", + "adoptsPanel": "Оставленный показатель получает панель {panel}.", "blocked": "Объединить нельзя, пока единицу не удаётся пересчитать. Исключите этот показатель или оставьте другой.", "noUndo": "Объединение нельзя отменить.", "doneToastOne": "Объединено в {name}, перенесено измерений: {count}", diff --git a/src/__tests__/baselines/source-function-lines.json b/src/__tests__/baselines/source-function-lines.json index 4fa80a682..1e4ab45ca 100644 --- a/src/__tests__/baselines/source-function-lines.json +++ b/src/__tests__/baselines/source-function-lines.json @@ -1,7 +1,7 @@ { "$comment": "Production functions over 200 lines, keyed \"file :: name\", and their length. Maintained by src/__tests__/source-size-ratchet.test.ts.", "count": 344, - "total": 111553, + "total": 111520, "entries": { "app/achievements/page.tsx :: AchievementsPage": 206, "app/api/admin/backups/upload/route.ts :: apiHandler(arg)": 275, @@ -132,7 +132,7 @@ "components/insights/trends-row.tsx :: TrendsRow": 215, "components/labs/biomarker-form.tsx :: BiomarkerForm": 229, "components/labs/biomarker-manager.tsx :: BiomarkerManager": 242, - "components/labs/biomarker-merge-sheet.tsx :: BiomarkerMergeSheet": 250, + "components/labs/biomarker-merge-sheet.tsx :: BiomarkerMergeSheet": 239, "components/labs/lab-biomarker-chart.tsx :: LabBiomarkerChart": 370, "components/labs/lab-biomarker-detail.tsx :: LabBiomarkerDetail": 481, "components/labs/lab-form.tsx :: LabForm": 473, @@ -314,7 +314,7 @@ "lib/jobs/reminder/medication-reminder-check.ts :: handleReminderCheck": 592, "lib/jobs/reminder/medication-reminder-check.ts :: handleReminderCheck > withBackgroundEvent(arg)": 587, "lib/jobs/workout-insight-generate.ts :: runWorkoutInsightGenerate": 324, - "lib/labs/biomarker-merge.ts :: runMerge": 254, + "lib/labs/biomarker-merge.ts :: runMerge": 232, "lib/measurements/consolidate-daily-mean.ts :: consolidateDailyMean": 278, "lib/measurements/consolidate-legacy-steps.ts :: consolidateLegacySteps": 263, "lib/measurements/dense-intraday-retention.ts :: runDenseIntradayRetention": 539, diff --git a/src/components/labs/biomarker-merge-sheet.tsx b/src/components/labs/biomarker-merge-sheet.tsx index 721af3157..6073c4d91 100644 --- a/src/components/labs/biomarker-merge-sheet.tsx +++ b/src/components/labs/biomarker-merge-sheet.tsx @@ -204,18 +204,7 @@ export function BiomarkerMergeSheet({ ))} - {preview.aliasesAdded.length > 0 ? ( -
- {t("labs.merge.aliasesAdded")} -
-{t("labs.merge.blocked")} @@ -283,3 +272,29 @@ export function BiomarkerMergeSheet({ ); } + +/** What the kept marker gains besides the readings: spellings and a panel. */ +function MergePreviewNotes({ preview }: { preview: BiomarkerMergeResultDto }) { + const { t } = useTranslations(); + return ( + <> + {preview.aliasesAdded.length > 0 ? ( +
+ {t("labs.merge.aliasesAdded")} +
++ {t("labs.merge.adoptsPanel", { panel: preview.adoptedPanel })} +
+ ) : null} + > + ); +} diff --git a/src/components/labs/types.ts b/src/components/labs/types.ts index 9ff962598..b8971843f 100644 --- a/src/components/labs/types.ts +++ b/src/components/labs/types.ts @@ -183,5 +183,7 @@ export interface BiomarkerMergeResultDto { movedReadings: number; convertedReadings: number; aliasesAdded: string[]; + /** The panel the target takes over from a source, or null. */ + adoptedPanel: string | null; mergeId: string | null; } diff --git a/src/lib/labs/biomarker-merge.ts b/src/lib/labs/biomarker-merge.ts index ac8c545f8..b1a93165a 100644 --- a/src/lib/labs/biomarker-merge.ts +++ b/src/lib/labs/biomarker-merge.ts @@ -22,6 +22,8 @@ * - one `biomarker_merges` row per source, and the rows that already pointed * at a source now point at the target, so an id merged twice still finds * where it went; + * - a target without a panel takes the first panel a source carries, in + * the order the sources were given (the dry run reports it too); * - the sources are deleted. * * There is no undo. The ledger keeps what a later split would need to know @@ -62,6 +64,8 @@ export interface BiomarkerMergeResult { movedReadings: number; convertedReadings: number; aliasesAdded: string[]; + /** The panel the target takes over from a source, or null (dry run too). */ + adoptedPanel: string | null; mergeId: string | null; } @@ -230,47 +234,18 @@ async function runMerge( } const convertedReadings = convertedLive; - // The spellings the target gains: each source's name and its aliases, once - // each, except what the target's name already is or another marker (not a - // source) already answers to. - const sourceAliases = await tx.biomarkerAlias.findMany({ - where: { userId, biomarkerId: { in: [...sourceIds] } }, - select: { id: true, name: true, nameFolded: true }, + const { sourceAliases, added } = await planMergeAliases(tx, { + userId, + target, + sources, }); - const otherAliases = await tx.biomarkerAlias.findMany({ - where: { userId, biomarkerId: { notIn: [...sourceIds] } }, - select: { biomarkerId: true, nameFolded: true }, - }); - const blocked = new Set( - otherAliases - .filter((a) => a.biomarkerId !== targetId) - .map((a) => a.nameFolded), - ); - const targetAliasFolds = new Set( - otherAliases - .filter((a) => a.biomarkerId === targetId) - .map((a) => a.nameFolded), - ); - const otherMarkerFolds = new Set( - ( - await tx.biomarker.findMany({ - where: { userId, id: { notIn: [targetId, ...sourceIds] } }, - select: { name: true }, - }) - ).map((m) => foldForMatch(m.name)), - ); - const targetFold = foldForMatch(target.name); - const added = new Map