mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-15 00:01:10 +02:00
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:
parent
cb18384905
commit
3b883af064
5 changed files with 166 additions and 50 deletions
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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' },
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue