diff --git a/apps/web/src/lib/ui/data/TaskItem.svelte b/apps/web/src/lib/ui/data/TaskItem.svelte index 4a09402..847c980 100644 --- a/apps/web/src/lib/ui/data/TaskItem.svelte +++ b/apps/web/src/lib/ui/data/TaskItem.svelte @@ -10,6 +10,7 @@ import { tick, onDestroy } from "svelte"; import Popover from "$lib/ui/feedback/Popover.svelte"; import { normalizeDigits, buildWaMeUrl } from "$lib/utils/phone"; + import { colorForGroup } from "$lib/utils/groupColor"; export let id: number; export let description: string; @@ -20,6 +21,7 @@ export let completed: boolean = false; export let completed_at: string | null = null; export let groupName: string | null = null; + export let groupId: string | null = null; const code = display_code ?? id; const codeStr = String(code).padStart(4, "0"); @@ -33,6 +35,7 @@ $: imminent = !!due_date && (isToday(due_date) || isTomorrow(due_date)); $: dateDmy = due_date ? ymdToDmy(due_date) : ""; $: groupLabel = groupName != null ? groupName : "Personal"; + $: gc = groupId ? colorForGroup(groupId) : null; let editing = false; let dateValue: string = due_date ?? ""; @@ -270,7 +273,11 @@
- {groupLabel} + {groupLabel} {#if due_date} >> 0; +} + +/** + * Devuelve un esquema de color determinista para un groupId dado. + * - Si groupId es falsy o vacío, devuelve null (usar estilos neutros por defecto). + */ +export function colorForGroup(groupId: string | null | undefined): GroupColor | null { + const s = String(groupId || '').trim(); + if (!s) return null; + const idx = hashString(s) % PALETTE.length; + return PALETTE[idx]; +} diff --git a/apps/web/src/routes/app/+page.svelte b/apps/web/src/routes/app/+page.svelte index c624793..5b683c8 100644 --- a/apps/web/src/routes/app/+page.svelte +++ b/apps/web/src/routes/app/+page.svelte @@ -85,7 +85,7 @@
    {#each data.openTasks as t} - + {/each}
@@ -108,7 +108,7 @@
    {#each g.tasks as t} - + {/each}
@@ -117,7 +117,7 @@
    {#each data.unassignedOpen as t} - + {/each}
@@ -134,6 +134,7 @@ {/each} diff --git a/tests/web/unit/groupColor.test.ts b/tests/web/unit/groupColor.test.ts new file mode 100644 index 0000000..b546010 --- /dev/null +++ b/tests/web/unit/groupColor.test.ts @@ -0,0 +1,43 @@ +import { describe, it, expect } from 'bun:test'; +import { colorForGroup } from '../../../apps/web/src/lib/utils/groupColor'; + +function isHexColor(s: string): boolean { + return /^#[0-9A-Fa-f]{6}$/.test(s); +} + +describe('groupColor - colorForGroup', () => { + it('devuelve null para groupId vacío o nulo', () => { + expect(colorForGroup(null)).toBeNull(); + expect(colorForGroup(undefined)).toBeNull(); + expect(colorForGroup('')).toBeNull(); + expect(colorForGroup(' ')).toBeNull(); + }); + + it('es determinista: misma entrada → misma salida', () => { + const a = colorForGroup('123@g.us'); + const b = colorForGroup('123@g.us'); + expect(a).not.toBeNull(); + expect(b).not.toBeNull(); + expect(a?.border).toBe(b?.border); + expect(a?.bg).toBe(b?.bg); + expect(a?.text).toBe(b?.text); + }); + + it('devuelve colores hex válidos', () => { + const c = colorForGroup('group-xyz@g.us'); + expect(c).not.toBeNull(); + expect(isHexColor(c!.border)).toBe(true); + expect(isHexColor(c!.bg)).toBe(true); + expect(isHexColor(c!.text)).toBe(true); + }); + + it('tiene distribución razonable en distintos IDs', () => { + const uniq = new Set(); + for (let i = 0; i < 30; i++) { + const c = colorForGroup(`group-${i}@g.us`); + if (c) uniq.add(`${c.border}|${c.bg}|${c.text}`); + } + // Con 12 paletas, deberíamos cubrir bastantes índices con 30 IDs + expect(uniq.size).toBeGreaterThan(8); + }); +});