feat(manacore): improve todo dashboard widgets and fix port mismatch

- Fix todo dev port in APP_URLS (5188, was swapped with inventory 5189)
- Use APP_URLS for all links instead of hardcoded localhost ports
- TasksTodayWidget: add priority dots, subtask progress, label tags,
  completed/total counter, clickable task links
- TasksUpcomingWidget: add priority dots, label tags, overdue/today
  date highlighting with colored badges, clickable task links
- Extend Task type with labels and subtasks for richer widget display

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Till JS 2026-03-23 21:48:29 +01:00
parent cb18384905
commit 3b883af064
5 changed files with 166 additions and 50 deletions

View file

@ -4,7 +4,7 @@
* Re-exports all app-specific services for the dashboard.
*/
export { todoService, type Task, type Project } from './todo';
export { todoService, type Task, type Project, type Label, type Subtask } from './todo';
export { calendarService, type Calendar, type CalendarEvent } from './calendar';
export { chatService, type Conversation, type Message, type AiModel } from './chat';
export { contactsService, type Contact, type ContactActivity } from './contacts';

View file

@ -31,6 +31,24 @@ function getClient() {
return _client;
}
/**
* Label entity from Todo backend
*/
export interface Label {
id: string;
name: string;
color: string;
}
/**
* Subtask entity from Todo backend
*/
export interface Subtask {
id: string;
title: string;
isCompleted: boolean;
}
/**
* Task entity from Todo backend
*/
@ -44,7 +62,8 @@ export interface Task {
dueTime?: string;
isCompleted: boolean;
status: 'pending' | 'in_progress' | 'completed' | 'cancelled';
labelIds: string[];
labels?: Label[];
subtasks?: Subtask[] | null;
createdAt: string;
updatedAt: string;
}

View file

@ -6,6 +6,7 @@
import { onMount } from 'svelte';
import { _ } from 'svelte-i18n';
import { todoService, type Task } from '$lib/api/services';
import { APP_URLS } from '@manacore/shared-branding';
import WidgetSkeleton from '../WidgetSkeleton.svelte';
import WidgetError from '../WidgetError.svelte';
@ -17,6 +18,16 @@
const MAX_DISPLAY = 5;
const isDev = typeof window !== 'undefined' && window.location.hostname === 'localhost';
const todoUrl = isDev ? APP_URLS.todo.dev : APP_URLS.todo.prod;
const priorityColors: Record<string, string> = {
urgent: '#ef4444',
high: '#f97316',
medium: '#eab308',
low: '#22c55e',
};
async function load() {
state = 'loading';
retrying = true;
@ -31,7 +42,6 @@
error = result.error;
state = 'error';
// Don't retry if service is unavailable (network error)
const isServiceUnavailable = error?.includes('nicht erreichbar');
if (!isServiceUnavailable && retryCount < 3) {
retryCount++;
@ -44,21 +54,16 @@
onMount(load);
function getPriorityColor(priority: string): string {
switch (priority) {
case 'urgent':
return 'text-red-500';
case 'high':
return 'text-orange-500';
case 'medium':
return 'text-yellow-500';
default:
return 'text-muted-foreground';
}
}
const displayedTasks = $derived((data || []).slice(0, MAX_DISPLAY));
const remainingCount = $derived(Math.max(0, (data || []).length - MAX_DISPLAY));
const completedCount = $derived((data || []).filter((t) => t.isCompleted).length);
const totalCount = $derived((data || []).length);
function getSubtaskProgress(task: Task): string | null {
if (!task.subtasks || task.subtasks.length === 0) return null;
const done = task.subtasks.filter((s) => s.isCompleted).length;
return `${done}/${task.subtasks.length}`;
}
</script>
<div>
@ -67,9 +72,9 @@
<span></span>
{$_('dashboard.widgets.tasks_today.title')}
</h3>
{#if (data || []).length > 0}
<span class="rounded-full bg-primary/10 px-2 py-0.5 text-sm font-medium text-primary">
{(data || []).length}
{#if totalCount > 0}
<span class="rounded-full bg-primary/10 px-2.5 py-0.5 text-sm font-medium text-primary">
{completedCount}/{totalCount}
</span>
{/if}
</div>
@ -78,7 +83,7 @@
<WidgetSkeleton lines={4} />
{:else if state === 'error'}
<WidgetError {error} onRetry={load} {retrying} />
{:else if (data || []).length === 0}
{:else if totalCount === 0}
<div class="py-6 text-center">
<div class="mb-2 text-3xl">🎉</div>
<p class="text-sm text-muted-foreground">
@ -86,15 +91,23 @@
</p>
</div>
{:else}
<div class="space-y-2">
<div class="space-y-1">
{#each displayedTasks as task}
<div
class="flex items-center gap-3 rounded-lg p-2 transition-colors hover:bg-surface-hover"
<a
href="{todoUrl}/task/{task.id}"
class="flex items-center gap-2.5 rounded-lg px-2 py-1.5 transition-colors hover:bg-surface-hover"
>
<!-- Priority dot -->
<div
class="h-4 w-4 rounded border-2 {task.isCompleted
class="h-2 w-2 flex-shrink-0 rounded-full"
style="background-color: {priorityColors[task.priority] || priorityColors.medium}"
></div>
<!-- Checkbox -->
<div
class="flex h-4 w-4 flex-shrink-0 items-center justify-center rounded border-2 {task.isCompleted
? 'border-primary bg-primary'
: 'border-muted-foreground'}"
: 'border-muted-foreground/40'}"
>
{#if task.isCompleted}
<svg
@ -108,6 +121,8 @@
</svg>
{/if}
</div>
<!-- Content -->
<div class="min-w-0 flex-1">
<p
class="truncate text-sm font-medium {task.isCompleted
@ -116,19 +131,51 @@
>
{task.title}
</p>
{#if task.dueTime}
<p class="text-xs text-muted-foreground">{task.dueTime}</p>
<!-- Meta row: time, subtasks, labels -->
{#if task.dueTime || getSubtaskProgress(task) || (task.labels && task.labels.length > 0)}
<div class="mt-0.5 flex items-center gap-2 text-xs text-muted-foreground">
{#if task.dueTime}
<span>{task.dueTime}</span>
{/if}
{#if getSubtaskProgress(task)}
<span class="flex items-center gap-0.5">
<svg
class="h-3 w-3"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
stroke-width="2"
>
<path
d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"
/>
</svg>
{getSubtaskProgress(task)}
</span>
{/if}
{#if task.labels && task.labels.length > 0}
{#each task.labels.slice(0, 2) as label}
<span class="flex items-center gap-1">
<span
class="inline-block h-2 w-2 rounded-full"
style="background-color: {label.color}"
></span>
{label.name}
</span>
{/each}
{#if task.labels.length > 2}
<span>+{task.labels.length - 2}</span>
{/if}
{/if}
</div>
{/if}
</div>
{#if !task.isCompleted && task.priority !== 'low'}
<span class={getPriorityColor(task.priority)}>●</span>
{/if}
</div>
</a>
{/each}
{#if remainingCount > 0}
<a
href="http://localhost:5188"
href={todoUrl}
target="_blank"
rel="noopener"
class="block rounded-lg py-2 text-center text-sm text-primary hover:bg-primary/5"

View file

@ -6,6 +6,7 @@
import { onMount } from 'svelte';
import { _ } from 'svelte-i18n';
import { todoService, type Task } from '$lib/api/services';
import { APP_URLS } from '@manacore/shared-branding';
import WidgetSkeleton from '../WidgetSkeleton.svelte';
import WidgetError from '../WidgetError.svelte';
@ -17,6 +18,16 @@
const MAX_DISPLAY = 5;
const isDev = typeof window !== 'undefined' && window.location.hostname === 'localhost';
const todoUrl = isDev ? APP_URLS.todo.dev : APP_URLS.todo.prod;
const priorityColors: Record<string, string> = {
urgent: '#ef4444',
high: '#f97316',
medium: '#eab308',
low: '#22c55e',
};
async function load() {
state = 'loading';
retrying = true;
@ -31,7 +42,6 @@
error = result.error;
state = 'error';
// Don't retry if service is unavailable (network error)
const isServiceUnavailable = error?.includes('nicht erreichbar');
if (!isServiceUnavailable && retryCount < 3) {
retryCount++;
@ -50,16 +60,22 @@
const tomorrow = new Date(today);
tomorrow.setDate(tomorrow.getDate() + 1);
if (date.toDateString() === today.toDateString()) {
return 'Heute';
}
if (date.toDateString() === tomorrow.toDateString()) {
return 'Morgen';
}
if (date.toDateString() === today.toDateString()) return 'Heute';
if (date.toDateString() === tomorrow.toDateString()) return 'Morgen';
return date.toLocaleDateString('de-DE', { weekday: 'short', day: 'numeric', month: 'short' });
}
function isOverdue(dateStr: string): boolean {
const date = new Date(dateStr);
const today = new Date();
today.setHours(0, 0, 0, 0);
return date < today;
}
function isToday(dateStr: string): boolean {
return new Date(dateStr).toDateString() === new Date().toDateString();
}
const displayedTasks = $derived(data.slice(0, MAX_DISPLAY));
const remainingCount = $derived(Math.max(0, data.length - MAX_DISPLAY));
</script>
@ -71,7 +87,7 @@
{$_('dashboard.widgets.tasks_upcoming.title')}
</h3>
{#if data.length > 0}
<span class="rounded-full bg-primary/10 px-2 py-0.5 text-sm font-medium text-primary">
<span class="rounded-full bg-primary/10 px-2.5 py-0.5 text-sm font-medium text-primary">
{data.length}
</span>
{/if}
@ -89,23 +105,57 @@
</p>
</div>
{:else}
<div class="space-y-2">
<div class="space-y-1">
{#each displayedTasks as task}
<div
class="flex items-center gap-3 rounded-lg p-2 transition-colors hover:bg-surface-hover"
<a
href="{todoUrl}/task/{task.id}"
class="flex items-center gap-2.5 rounded-lg px-2 py-1.5 transition-colors hover:bg-surface-hover"
>
<!-- Priority dot -->
<div
class="h-2 w-2 flex-shrink-0 rounded-full"
style="background-color: {priorityColors[task.priority] || priorityColors.medium}"
></div>
<!-- Content -->
<div class="min-w-0 flex-1">
<p class="truncate text-sm font-medium">{task.title}</p>
{#if task.dueDate}
<p class="text-xs text-muted-foreground">{formatDate(task.dueDate)}</p>
<!-- Meta row: labels -->
{#if task.labels && task.labels.length > 0}
<div class="mt-0.5 flex items-center gap-2 text-xs text-muted-foreground">
{#each task.labels.slice(0, 2) as label}
<span class="flex items-center gap-1">
<span
class="inline-block h-2 w-2 rounded-full"
style="background-color: {label.color}"
></span>
{label.name}
</span>
{/each}
</div>
{/if}
</div>
</div>
<!-- Date badge -->
{#if task.dueDate}
<span
class="flex-shrink-0 rounded-md px-1.5 py-0.5 text-xs font-medium {isOverdue(
task.dueDate
)
? 'bg-red-500/10 text-red-500'
: isToday(task.dueDate)
? 'bg-orange-500/10 text-orange-500'
: 'bg-muted text-muted-foreground'}"
>
{formatDate(task.dueDate)}
</span>
{/if}
</a>
{/each}
{#if remainingCount > 0}
<a
href="http://localhost:5188"
href={todoUrl}
target="_blank"
rel="noopener"
class="block rounded-lg py-2 text-center text-sm text-primary hover:bg-primary/5"

View file

@ -439,9 +439,9 @@ export const APP_URLS: Record<AppIconId, { dev: string; prod: string }> = {
calendar: { dev: 'http://localhost:5179', prod: 'https://calendar.mana.how' },
storage: { dev: 'http://localhost:5185', prod: 'https://storage.mana.how' },
clock: { dev: 'http://localhost:5187', prod: 'https://clock.mana.how' },
todo: { dev: 'http://localhost:5189', prod: 'https://todo.mana.how' },
todo: { dev: 'http://localhost:5188', prod: 'https://todo.mana.how' },
mail: { dev: 'http://localhost:5186', prod: 'https://mail.mana.how' },
inventory: { dev: 'http://localhost:5188', prod: 'https://inventory.mana.how' },
inventory: { dev: 'http://localhost:5189', prod: 'https://inventory.mana.how' },
questions: { dev: 'http://localhost:5111', prod: 'https://questions.mana.how' },
matrix: { dev: 'http://localhost:5180', prod: 'https://matrix.mana.how' },
playground: { dev: 'http://localhost:5190', prod: 'https://playground.mana.how' },