mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 21:21:10 +02:00
fix(manacore): improve API response handling and auth flow
- Fix calendar/todo API services to handle wrapped response format
({ events: [...] }, { tasks: [...] }, etc.)
- Add null-safety guards in dashboard widgets for data arrays
- Simplify default dashboard to 3 widgets: Clock, Tasks, Calendar
- Fix auth layout initialization to prevent redirect race conditions
- Update auth store import path in dashboard page
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
6d918315c7
commit
6f8585e9bb
8 changed files with 110 additions and 80 deletions
|
|
@ -59,7 +59,15 @@ export const calendarService = {
|
|||
const startDate = new Date().toISOString().split('T')[0];
|
||||
const endDate = new Date(Date.now() + days * 24 * 60 * 60 * 1000).toISOString().split('T')[0];
|
||||
|
||||
return client.get<CalendarEvent[]>(`/events?startDate=${startDate}&endDate=${endDate}`);
|
||||
const result = await client.get<{ events: CalendarEvent[] }>(
|
||||
`/events?startDate=${startDate}&endDate=${endDate}`
|
||||
);
|
||||
|
||||
if (result.error || !result.data) {
|
||||
return { data: null, error: result.error };
|
||||
}
|
||||
|
||||
return { data: result.data.events || [], error: null };
|
||||
},
|
||||
|
||||
/**
|
||||
|
|
@ -67,14 +75,28 @@ export const calendarService = {
|
|||
*/
|
||||
async getTodayEvents(): Promise<ApiResult<CalendarEvent[]>> {
|
||||
const today = new Date().toISOString().split('T')[0];
|
||||
return client.get<CalendarEvent[]>(`/events?startDate=${today}&endDate=${today}`);
|
||||
const result = await client.get<{ events: CalendarEvent[] }>(
|
||||
`/events?startDate=${today}&endDate=${today}`
|
||||
);
|
||||
|
||||
if (result.error || !result.data) {
|
||||
return { data: null, error: result.error };
|
||||
}
|
||||
|
||||
return { data: result.data.events || [], error: null };
|
||||
},
|
||||
|
||||
/**
|
||||
* Get all calendars
|
||||
*/
|
||||
async getCalendars(): Promise<ApiResult<Calendar[]>> {
|
||||
return client.get<Calendar[]>('/calendars');
|
||||
const result = await client.get<{ calendars: Calendar[] }>('/calendars');
|
||||
|
||||
if (result.error || !result.data) {
|
||||
return { data: null, error: result.error };
|
||||
}
|
||||
|
||||
return { data: result.data.calendars || [], error: null };
|
||||
},
|
||||
|
||||
/**
|
||||
|
|
@ -87,8 +109,14 @@ export const calendarService = {
|
|||
const startDate = new Date().toISOString().split('T')[0];
|
||||
const endDate = new Date(Date.now() + days * 24 * 60 * 60 * 1000).toISOString().split('T')[0];
|
||||
|
||||
return client.get<CalendarEvent[]>(
|
||||
const result = await client.get<{ events: CalendarEvent[] }>(
|
||||
`/events?calendarIds=${calendarId}&startDate=${startDate}&endDate=${endDate}`
|
||||
);
|
||||
|
||||
if (result.error || !result.data) {
|
||||
return { data: null, error: result.error };
|
||||
}
|
||||
|
||||
return { data: result.data.events || [], error: null };
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -49,28 +49,52 @@ export const todoService = {
|
|||
* Get today's tasks
|
||||
*/
|
||||
async getTodayTasks(): Promise<ApiResult<Task[]>> {
|
||||
return client.get<Task[]>('/tasks/today');
|
||||
const result = await client.get<{ tasks: Task[] }>('/tasks/today');
|
||||
|
||||
if (result.error || !result.data) {
|
||||
return { data: null, error: result.error };
|
||||
}
|
||||
|
||||
return { data: result.data.tasks || [], error: null };
|
||||
},
|
||||
|
||||
/**
|
||||
* Get upcoming tasks for the next N days
|
||||
*/
|
||||
async getUpcomingTasks(days: number = 7): Promise<ApiResult<Task[]>> {
|
||||
return client.get<Task[]>(`/tasks/upcoming?days=${days}`);
|
||||
const result = await client.get<{ tasks: Task[] }>(`/tasks/upcoming?days=${days}`);
|
||||
|
||||
if (result.error || !result.data) {
|
||||
return { data: null, error: result.error };
|
||||
}
|
||||
|
||||
return { data: result.data.tasks || [], error: null };
|
||||
},
|
||||
|
||||
/**
|
||||
* Get inbox tasks (unassigned to project)
|
||||
*/
|
||||
async getInboxTasks(): Promise<ApiResult<Task[]>> {
|
||||
return client.get<Task[]>('/tasks/inbox');
|
||||
const result = await client.get<{ tasks: Task[] }>('/tasks/inbox');
|
||||
|
||||
if (result.error || !result.data) {
|
||||
return { data: null, error: result.error };
|
||||
}
|
||||
|
||||
return { data: result.data.tasks || [], error: null };
|
||||
},
|
||||
|
||||
/**
|
||||
* Get all projects
|
||||
*/
|
||||
async getProjects(): Promise<ApiResult<Project[]>> {
|
||||
return client.get<Project[]>('/projects');
|
||||
const result = await client.get<{ projects: Project[] }>('/projects');
|
||||
|
||||
if (result.error || !result.data) {
|
||||
return { data: null, error: result.error };
|
||||
}
|
||||
|
||||
return { data: result.data.projects || [], error: null };
|
||||
},
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -71,8 +71,8 @@
|
|||
return `${dateStr}, ${timeStr}`;
|
||||
}
|
||||
|
||||
const displayedEvents = $derived(data.slice(0, MAX_DISPLAY));
|
||||
const remainingCount = $derived(Math.max(0, data.length - MAX_DISPLAY));
|
||||
const displayedEvents = $derived((data || []).slice(0, MAX_DISPLAY));
|
||||
const remainingCount = $derived(Math.max(0, (data || []).length - MAX_DISPLAY));
|
||||
</script>
|
||||
|
||||
<div>
|
||||
|
|
@ -81,9 +81,9 @@
|
|||
<span>🗓️</span>
|
||||
{$_('dashboard.widgets.calendar.title')}
|
||||
</h3>
|
||||
{#if data.length > 0}
|
||||
{#if (data || []).length > 0}
|
||||
<span class="rounded-full bg-primary/10 px-2 py-0.5 text-sm font-medium text-primary">
|
||||
{data.length}
|
||||
{(data || []).length}
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
|
|
@ -92,7 +92,7 @@
|
|||
<WidgetSkeleton lines={4} />
|
||||
{:else if state === 'error'}
|
||||
<WidgetError {error} onRetry={load} {retrying} />
|
||||
{:else if data.length === 0}
|
||||
{:else if (data || []).length === 0}
|
||||
<div class="py-6 text-center">
|
||||
<div class="mb-2 text-3xl">📅</div>
|
||||
<p class="text-sm text-muted-foreground">
|
||||
|
|
|
|||
|
|
@ -57,8 +57,8 @@
|
|||
}
|
||||
}
|
||||
|
||||
const displayedTasks = $derived(data.slice(0, MAX_DISPLAY));
|
||||
const remainingCount = $derived(Math.max(0, data.length - MAX_DISPLAY));
|
||||
const displayedTasks = $derived((data || []).slice(0, MAX_DISPLAY));
|
||||
const remainingCount = $derived(Math.max(0, (data || []).length - MAX_DISPLAY));
|
||||
</script>
|
||||
|
||||
<div>
|
||||
|
|
@ -67,9 +67,9 @@
|
|||
<span>✅</span>
|
||||
{$_('dashboard.widgets.tasks_today.title')}
|
||||
</h3>
|
||||
{#if data.length > 0}
|
||||
{#if (data || []).length > 0}
|
||||
<span class="rounded-full bg-primary/10 px-2 py-0.5 text-sm font-medium text-primary">
|
||||
{data.length}
|
||||
{(data || []).length}
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
|
|
@ -78,7 +78,7 @@
|
|||
<WidgetSkeleton lines={4} />
|
||||
{:else if state === 'error'}
|
||||
<WidgetError {error} onRetry={load} {retrying} />
|
||||
{:else if data.length === 0}
|
||||
{:else if (data || []).length === 0}
|
||||
<div class="py-6 text-center">
|
||||
<div class="mb-2 text-3xl">🎉</div>
|
||||
<p class="text-sm text-muted-foreground">
|
||||
|
|
|
|||
|
|
@ -7,15 +7,15 @@
|
|||
import type { DashboardConfig } from '$lib/types/dashboard';
|
||||
|
||||
/**
|
||||
* Default dashboard configuration with 6 widgets in a 2-column layout
|
||||
* Default dashboard configuration with 3 widgets: Clock, Tasks, Calendar
|
||||
*/
|
||||
export const DEFAULT_DASHBOARD_CONFIG: DashboardConfig = {
|
||||
widgets: [
|
||||
// Row 0: Credits and Tasks Today
|
||||
// Row 0: Clock and Tasks Today
|
||||
{
|
||||
id: 'credits-1',
|
||||
type: 'credits',
|
||||
title: 'dashboard.widgets.credits.title',
|
||||
id: 'clock-timers-1',
|
||||
type: 'clock-timers',
|
||||
title: 'dashboard.widgets.clock.title',
|
||||
size: 'medium',
|
||||
position: { x: 0, y: 0 },
|
||||
visible: true,
|
||||
|
|
@ -28,40 +28,15 @@ export const DEFAULT_DASHBOARD_CONFIG: DashboardConfig = {
|
|||
position: { x: 6, y: 0 },
|
||||
visible: true,
|
||||
},
|
||||
// Row 1: Calendar and Quick Actions
|
||||
// Row 1: Calendar (full width)
|
||||
{
|
||||
id: 'calendar-events-1',
|
||||
type: 'calendar-events',
|
||||
title: 'dashboard.widgets.calendar.title',
|
||||
size: 'medium',
|
||||
size: 'large',
|
||||
position: { x: 0, y: 1 },
|
||||
visible: true,
|
||||
},
|
||||
{
|
||||
id: 'quick-actions-1',
|
||||
type: 'quick-actions',
|
||||
title: 'dashboard.widgets.quick_actions.title',
|
||||
size: 'medium',
|
||||
position: { x: 6, y: 1 },
|
||||
visible: true,
|
||||
},
|
||||
// Row 2: Chat and Contacts
|
||||
{
|
||||
id: 'chat-recent-1',
|
||||
type: 'chat-recent',
|
||||
title: 'dashboard.widgets.chat.title',
|
||||
size: 'medium',
|
||||
position: { x: 0, y: 2 },
|
||||
visible: true,
|
||||
},
|
||||
{
|
||||
id: 'contacts-favorites-1',
|
||||
type: 'contacts-favorites',
|
||||
title: 'dashboard.widgets.contacts.title',
|
||||
size: 'medium',
|
||||
position: { x: 6, y: 2 },
|
||||
visible: true,
|
||||
},
|
||||
],
|
||||
gridColumns: 12,
|
||||
lastModified: new Date().toISOString(),
|
||||
|
|
|
|||
|
|
@ -123,23 +123,22 @@
|
|||
goto('/login');
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
// Redirect to login if not authenticated (after initialization)
|
||||
// Use a small delay to ensure state has propagated after navigation
|
||||
if (authStore.initialized && !authStore.loading && !authStore.isAuthenticated) {
|
||||
// Small delay to handle navigation timing
|
||||
setTimeout(() => {
|
||||
if (!authStore.isAuthenticated) {
|
||||
goto('/login');
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
});
|
||||
// Track initialization state
|
||||
let isInitializing = $state(true);
|
||||
|
||||
onMount(async () => {
|
||||
// Initialize auth store first
|
||||
await authStore.initialize();
|
||||
|
||||
// Only after initialization is complete, check auth status
|
||||
isInitializing = false;
|
||||
|
||||
// Redirect to login if not authenticated
|
||||
if (!authStore.isAuthenticated) {
|
||||
goto('/login');
|
||||
return;
|
||||
}
|
||||
|
||||
// Initialize sidebar mode from localStorage
|
||||
const savedSidebar = localStorage.getItem('manacore-nav-sidebar');
|
||||
if (savedSidebar === 'true') {
|
||||
|
|
@ -155,19 +154,10 @@
|
|||
}
|
||||
|
||||
// Load user settings from server (don't await - let it load in background)
|
||||
if (authStore.isAuthenticated) {
|
||||
userSettings.load().then(() => {
|
||||
// Redirect to start page if on /dashboard and a custom start page is set
|
||||
const currentPath = window.location.pathname;
|
||||
if (
|
||||
currentPath === '/dashboard' &&
|
||||
userSettings.startPage &&
|
||||
userSettings.startPage !== '/dashboard'
|
||||
) {
|
||||
goto(userSettings.startPage, { replaceState: true });
|
||||
}
|
||||
});
|
||||
}
|
||||
// Silently catch errors since settings endpoint may not exist yet
|
||||
userSettings.load().catch(() => {
|
||||
// Settings API not available - use defaults
|
||||
});
|
||||
|
||||
loading = false;
|
||||
});
|
||||
|
|
@ -175,7 +165,7 @@
|
|||
|
||||
<svelte:window onkeydown={handleKeydown} />
|
||||
|
||||
{#if loading || authStore.loading}
|
||||
{#if isInitializing || loading || authStore.loading}
|
||||
<div class="flex min-h-screen items-center justify-center bg-background">
|
||||
<div class="text-center">
|
||||
<div
|
||||
|
|
|
|||
|
|
@ -1,9 +1,12 @@
|
|||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { _ } from 'svelte-i18n';
|
||||
import { Card, PageHeader } from '@manacore/shared-ui';
|
||||
import { creditsService } from '$lib/api/credits';
|
||||
import type { CreditBalance, CreditTransaction } from '$lib/api/credits';
|
||||
import { authStore } from '$lib/stores/authStore.svelte';
|
||||
import { authStore } from '$lib/stores/auth.svelte';
|
||||
import { dashboardStore } from '$lib/stores/dashboard.svelte';
|
||||
import DashboardGrid from '$lib/components/dashboard/DashboardGrid.svelte';
|
||||
|
||||
onMount(() => {
|
||||
dashboardStore.initialize();
|
||||
|
|
|
|||
|
|
@ -1,14 +1,24 @@
|
|||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import { onMount } from 'svelte';
|
||||
import type { Snippet } from 'svelte';
|
||||
import { authStore } from '$lib/stores/auth.svelte';
|
||||
|
||||
let { children }: { children: Snippet } = $props();
|
||||
let hasCheckedAuth = $state(false);
|
||||
|
||||
// Redirect authenticated users to dashboard
|
||||
// Auth state is managed by Mana Core Auth via authStore
|
||||
// Check auth status on mount
|
||||
onMount(async () => {
|
||||
await authStore.initialize();
|
||||
hasCheckedAuth = true;
|
||||
if (authStore.isAuthenticated) {
|
||||
goto('/dashboard');
|
||||
}
|
||||
});
|
||||
|
||||
// Also react to auth state changes (e.g., after successful login)
|
||||
$effect(() => {
|
||||
if (authStore.initialized && !authStore.loading && authStore.isAuthenticated) {
|
||||
if (hasCheckedAuth && authStore.isAuthenticated) {
|
||||
goto('/dashboard');
|
||||
}
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue