# Dashboard Page Patterns

Complete guide to building dashboard and overview pages with metrics, charts, and manual data fetching.

## When to Use Dashboard Pattern

Use dashboard pattern for:
- **Analytics pages** - Display metrics and KPIs
- **Overview pages** - Summary of system state
- **Admin dashboards** - System monitoring
- **Reports** - Data visualization and insights

**Key characteristics:**
- Uses AwPage (with menu visible)
- Manual data fetching (not collection-based)
- Metrics cards and visualizations
- Responsive grid layouts
- Custom API endpoints

## Basic Dashboard Layout

### Minimal Metrics Dashboard

```markup
<template>
    <AwPage title="Dashboard">
        <AwGrid :col="{ md: 2, lg: 4 }" class="mb-8">
            <AwCard>
                <div class="text-3xl font-bold">{{ metrics.total_customers }}</div>
                <div class="text-sm text-secondary">Total Customers</div>
            </AwCard>

            <AwCard>
                <div class="text-3xl font-bold">{{ metrics.total_orders }}</div>
                <div class="text-sm text-secondary">Total Orders</div>
            </AwCard>

            <AwCard>
                <div class="text-3xl font-bold">${{ metrics.total_revenue }}</div>
                <div class="text-sm text-secondary">Total Revenue</div>
            </AwCard>

            <AwCard>
                <div class="text-3xl font-bold">{{ metrics.active_subscriptions }}</div>
                <div class="text-sm text-secondary">Active Subscriptions</div>
            </AwCard>
        </AwGrid>
    </AwPage>
</template>

<script>
export default {
    middleware: 'auth',

    data() {
        return {
            metrics: {
                total_customers: 0,
                total_orders: 0,
                total_revenue: 0,
                active_subscriptions: 0
            },
            loading: true
        }
    },

    async mounted() {
        await this.fetchMetrics()
    },

    methods: {
        async fetchMetrics() {
            this.loading = true
            try {
                const { data } = await this.$axios.get(
                    `/api/shops/${this.$route.params.shop_uuid}/metrics`
                )
                this.metrics = data
            } catch (error) {
                this.$notify({
                    message: 'Failed to load metrics',
                    type: 'error'
                })
            } finally {
                this.loading = false
            }
        }
    }
}
</script>
```

## Metric Cards

### Simple Metric Card

```markup
<AwCard>
    <div class="text-3xl font-bold text-accent">1,234</div>
    <div class="text-sm text-secondary mt-1">Active Users</div>
</AwCard>
```

### Metric with Icon

```markup
<AwCard class="flex items-start gap-4">
    <div class="p-3 bg-accent/10 rounded-lg">
        <AwIcon name="users" class="text-accent" size="24" />
    </div>
    <div>
        <div class="text-3xl font-bold">1,234</div>
        <div class="text-sm text-secondary">Active Users</div>
    </div>
</AwCard>
```

### Metric with Trend

```markup
<AwCard>
    <div class="flex justify-between items-start">
        <div>
            <div class="text-3xl font-bold">$12,345</div>
            <div class="text-sm text-secondary">Monthly Revenue</div>
        </div>
        <div class="flex items-center gap-1 text-success">
            <AwIcon name="arrow-up" size="16" />
            <span class="text-sm font-medium">+12%</span>
        </div>
    </div>
</AwCard>
```

### Metric with Progress

```markup
<AwCard>
    <div class="text-2xl font-bold">75%</div>
    <div class="text-sm text-secondary mb-3">Goal Progress</div>
    <AwProgress :value="75" :max="100" />
    <div class="text-xs text-secondary mt-2">
        $7,500 of $10,000 goal
    </div>
</AwCard>
```

## Responsive Grid Layouts

### 2-Column Layout

```markup
<template>
    <AwPage title="Analytics">
        <AwGrid :col="{ lg: 2 }">
            <!-- Left column -->
            <div class="space-y-6">
                <AwCard title="Revenue Overview">
                    <AwChart :data="revenueData" type="line" />
                </AwCard>

                <AwCard title="Top Products">
                    <AwTableBuilder :collection="topProducts">
                        <AwTableCol field="name" title="Product" />
                        <AwTableCol field="sales" title="Sales" />
                    </AwTableBuilder>
                </AwCard>
            </div>

            <!-- Right column -->
            <div class="space-y-6">
                <AwCard title="Customer Growth">
                    <AwChart :data="customerData" type="bar" />
                </AwCard>

                <AwCard title="Recent Orders">
                    <AwTableBuilder :collection="recentOrders">
                        <AwTableCol field="id" title="Order #" />
                        <AwTableCol field="total" title="Total" />
                    </AwTableBuilder>
                </AwCard>
            </div>
        </AwGrid>
    </AwPage>
</template>
```

### 3-Column Layout

```markup
<AwGrid :col="{ md: 2, lg: 3 }">
    <!-- Metrics cards -->
    <AwCard>
        <div class="text-3xl font-bold">1,234</div>
        <div class="text-sm text-secondary">Total Users</div>
    </AwCard>

    <AwCard>
        <div class="text-3xl font-bold">$45,678</div>
        <div class="text-sm text-secondary">Revenue</div>
    </AwCard>

    <AwCard>
        <div class="text-3xl font-bold">567</div>
        <div class="text-sm text-secondary">Active Orders</div>
    </AwCard>
</AwGrid>
```

### Mixed Layout (Metrics + Content)

```markup
<div class="space-y-6">
    <!-- Metrics grid -->
    <AwGrid :col="{ md: 2, lg: 4 }" :gap="4">
        <AwCard v-for="metric in metrics" :key="metric.key">
            <div class="text-2xl font-bold">{{ metric.value }}</div>
            <div class="text-sm text-secondary">{{ metric.label }}</div>
        </AwCard>
    </AwGrid>

    <!-- Full-width chart -->
    <AwCard title="Revenue Trend">
        <AwChart :data="chartData" type="line" height="300" />
    </AwCard>

    <!-- Two-column content -->
    <AwGrid :col="{ lg: 2 }">
        <AwCard title="Recent Activity">
            <!-- Activity list -->
        </AwCard>

        <AwCard title="Top Performers">
            <!-- Performance list -->
        </AwCard>
    </AwGrid>
</div>
```

## Manual Data Fetching

### Basic Fetch Pattern

```javascript
export default {
    data() {
        return {
            dashboardData: null,
            loading: true
        }
    },

    async mounted() {
        await this.fetchDashboard()
    },

    methods: {
        async fetchDashboard() {
            this.loading = true
            try {
                const { data } = await this.$axios.get(
                    `/api/shops/${this.$route.params.shop_uuid}/dashboard`
                )
                this.dashboardData = data
            } catch (error) {
                this.$notify({
                    message: 'Failed to load dashboard',
                    type: 'error'
                })
            } finally {
                this.loading = false
            }
        }
    }
}
```

### Multiple Endpoints

```javascript
async mounted() {
    await Promise.all([
        this.fetchMetrics(),
        this.fetchRecentOrders(),
        this.fetchTopProducts()
    ])
},

methods: {
    async fetchMetrics() {
        try {
            const { data } = await this.$axios.get('/api/metrics')
            this.metrics = data
        } catch (error) {
            console.error('Failed to load metrics:', error)
        }
    },

    async fetchRecentOrders() {
        try {
            const { data } = await this.$axios.get('/api/orders/recent')
            this.recentOrders = data
        } catch (error) {
            console.error('Failed to load orders:', error)
        }
    },

    async fetchTopProducts() {
        try {
            const { data } = await this.$axios.get('/api/products/top')
            this.topProducts = data
        } catch (error) {
            console.error('Failed to load products:', error)
        }
    }
}
```

### With Date Range Filter

```markup
<template>
    <AwPage title="Analytics">
        <div class="flex justify-end mb-6">
            <AwDate
                v-model="dateRange"
                label="Date Range"
                range
                @input="fetchData"
            />
        </div>

        <AwGrid :col="{ md: 3 }" :gap="4">
            <AwCard v-for="metric in metrics" :key="metric.key">
                <div class="text-3xl font-bold">{{ metric.value }}</div>
                <div class="text-sm text-secondary">{{ metric.label }}</div>
            </AwCard>
        </AwGrid>
    </AwPage>
</template>

<script>
export default {
    data() {
        return {
            dateRange: {
                start: this.$dayjs().subtract(30, 'days').format('YYYY-MM-DD'),
                end: this.$dayjs().format('YYYY-MM-DD')
            },
            metrics: []
        }
    },

    async mounted() {
        await this.fetchData()
    },

    methods: {
        async fetchData() {
            try {
                const { data } = await this.$axios.get('/api/analytics', {
                    params: {
                        start_date: this.dateRange.start,
                        end_date: this.dateRange.end
                    }
                })
                this.metrics = data.metrics
            } catch (error) {
                this.$notify({
                    message: 'Failed to load analytics',
                    type: 'error'
                })
            }
        }
    }
}
</script>
```

## Charts & Visualizations

### Line Chart

```markup
<AwCard title="Revenue Trend">
    <AwChart
        :data="revenueData"
        type="line"
        :height="300"
    />
</AwCard>

<script>
export default {
    data() {
        return {
            revenueData: {
                labels: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun'],
                datasets: [{
                    label: 'Revenue',
                    data: [12000, 19000, 15000, 25000, 22000, 30000],
                    borderColor: '#6366f1',
                    backgroundColor: 'rgba(99, 102, 241, 0.1)'
                }]
            }
        }
    }
}
</script>
```

### Bar Chart

```markup
<AwCard title="Sales by Category">
    <AwChart
        :data="categoryData"
        type="bar"
        :height="300"
    />
</AwCard>

<script>
export default {
    data() {
        return {
            categoryData: {
                labels: ['Electronics', 'Clothing', 'Food', 'Books', 'Sports'],
                datasets: [{
                    label: 'Sales',
                    data: [45, 32, 28, 19, 15],
                    backgroundColor: [
                        '#6366f1',
                        '#8b5cf6',
                        '#ec4899',
                        '#f97316',
                        '#10b981'
                    ]
                }]
            }
        }
    }
}
</script>
```

### Donut Chart

```markup
<AwCard title="Traffic Sources">
    <AwChart
        :data="trafficData"
        type="doughnut"
        :height="300"
    />
</AwCard>

<script>
export default {
    data() {
        return {
            trafficData: {
                labels: ['Direct', 'Organic', 'Referral', 'Social'],
                datasets: [{
                    data: [35, 30, 20, 15],
                    backgroundColor: [
                        '#6366f1',
                        '#10b981',
                        '#f59e0b',
                        '#ec4899'
                    ]
                }]
            }
        }
    }
}
</script>
```

## Loading States

### Skeleton Loading

```markup
<template>
    <AwPage title="Dashboard">
        <AwGrid v-if="loading" :col="{ md: 4 }" :gap="4">
            <AwCard v-for="i in 4" :key="i" class="animate-pulse">
                <div class="h-8 bg-gray-200 rounded mb-2"></div>
                <div class="h-4 bg-gray-200 rounded w-2/3"></div>
            </AwCard>
        </AwGrid>

        <AwGrid v-else :col="{ md: 4 }" :gap="4">
            <AwCard v-for="metric in metrics" :key="metric.key">
                <div class="text-3xl font-bold">{{ metric.value }}</div>
                <div class="text-sm text-secondary">{{ metric.label }}</div>
            </AwCard>
        </AwGrid>
    </AwPage>
</template>
```

### Progress Indicator

```markup
<template>
    <AwPage title="Dashboard">
        <div v-if="loading" class="flex justify-center py-12">
            <AwProgress indeterminate />
        </div>

        <div v-else>
            <!-- Dashboard content -->
        </div>
    </AwPage>
</template>
```

### Per-Section Loading

```markup
<template>
    <AwPage title="Dashboard">
        <div class="space-y-6">
            <!-- Metrics always visible -->
            <AwGrid :col="4" :gap="4">
                <AwCard v-for="metric in metrics" :key="metric.key">
                    <!-- Metrics content -->
                </AwCard>
            </AwGrid>

            <!-- Chart with loading state -->
            <AwCard title="Revenue Trend">
                <div v-if="chartLoading" class="h-64 flex items-center justify-center">
                    <AwProgress indeterminate />
                </div>
                <AwChart v-else :data="chartData" type="line" />
            </AwCard>
        </div>
    </AwPage>
</template>
```

## Refresh & Real-Time Updates

### Manual Refresh Button

```markup
<template>
    <AwPage
        title="Dashboard"
        :action="{
            key: 'refresh',
            label: 'Refresh',
            loading: refreshing,
            icon: 'refresh',
            theme: 'outline'
        }"
        @action="handleAction"
    >
        <!-- Dashboard content -->
    </AwPage>
</template>

<script>
export default {
    data() {
        return {
            refreshing: false
        }
    },

    methods: {
        handleAction(action) {
            if (action.key === 'refresh') {
                this.refresh()
            }
        },

        async refresh() {
            this.refreshing = true
            try {
                await Promise.all([
                    this.fetchMetrics(),
                    this.fetchChartData()
                ])
                this.$notify({
                    message: 'Dashboard refreshed',
                    type: 'success'
                })
            } catch (error) {
                this.$notify({
                    message: 'Failed to refresh',
                    type: 'error'
                })
            } finally {
                this.refreshing = false
            }
        }
    }
}
</script>
```

### Auto-Refresh with Interval

```markup
<script>
export default {
    data() {
        return {
            refreshInterval: null
        }
    },

    mounted() {
        // Initial fetch
        this.fetchData()

        // Auto-refresh every 30 seconds
        this.refreshInterval = setInterval(() => {
            this.fetchData()
        }, 30000)
    },

    beforeDestroy() {
        // Clean up interval
        if (this.refreshInterval) {
            clearInterval(this.refreshInterval)
        }
    }
}
</script>
```

### Last Updated Indicator

```markup
<template>
    <AwPage title="Dashboard">
        <div class="text-sm text-secondary mb-4">
            Last updated: {{ $dayjs(lastUpdated).format('LLL') }}
        </div>

        <!-- Dashboard content -->
    </AwPage>
</template>

<script>
export default {
    data() {
        return {
            lastUpdated: null
        }
    },

    methods: {
        async fetchData() {
            // Fetch logic
            this.lastUpdated = new Date()
        }
    }
}
</script>
```

## Complete Dashboard Example

```markup
<template>
    <AwPage
        title="Dashboard"
        :action="{
            key: 'refresh',
            label: 'Refresh',
            loading: refreshing,
            icon: 'refresh',
            theme: 'outline'
        }"
        @action="handleAction"
    >
        <!-- Loading State -->
        <div v-if="initialLoading" class="flex justify-center py-12">
            <AwProgress indeterminate />
        </div>

        <!-- Dashboard Content -->
        <div v-else class="space-y-6">
            <!-- Date Range Filter -->
            <div class="flex justify-between items-center">
                <div class="text-sm text-secondary">
                    Last updated: {{ $dayjs(lastUpdated).fromNow() }}
                </div>
                <AwDate
                    v-model="dateRange"
                    label="Date Range"
                    range
                    @input="fetchData"
                />
            </div>

            <!-- Metrics Grid -->
            <AwGrid :col="{ md: 2, lg: 4 }" :gap="4">
                <AwCard>
                    <div class="flex items-start gap-4">
                        <div class="p-3 bg-accent/10 rounded-lg">
                            <AwIcon name="users" class="text-accent" size="24" />
                        </div>
                        <div>
                            <div class="text-3xl font-bold">{{ metrics.total_customers }}</div>
                            <div class="text-sm text-secondary">Total Customers</div>
                        </div>
                    </div>
                </AwCard>

                <AwCard>
                    <div class="flex justify-between items-start">
                        <div>
                            <div class="text-3xl font-bold">${{ formatMoney(metrics.revenue) }}</div>
                            <div class="text-sm text-secondary">Revenue</div>
                        </div>
                        <div class="flex items-center gap-1 text-success">
                            <AwIcon name="arrow-up" size="16" />
                            <span class="text-sm font-medium">+{{ metrics.revenue_growth }}%</span>
                        </div>
                    </div>
                </AwCard>

                <AwCard>
                    <div class="text-3xl font-bold">{{ metrics.total_orders }}</div>
                    <div class="text-sm text-secondary mb-3">Total Orders</div>
                    <AwProgress :value="metrics.orders_today" :max="metrics.orders_goal" />
                    <div class="text-xs text-secondary mt-2">
                        {{ metrics.orders_today }} of {{ metrics.orders_goal }} daily goal
                    </div>
                </AwCard>

                <AwCard>
                    <div class="text-3xl font-bold">{{ metrics.active_subscriptions }}</div>
                    <div class="text-sm text-secondary">Active Subscriptions</div>
                </AwCard>
            </AwGrid>

            <!-- Charts Row -->
            <AwGrid :col="{ lg: 2 }">
                <AwCard title="Revenue Trend">
                    <AwChart :data="revenueChartData" type="line" :height="300" />
                </AwCard>

                <AwCard title="Sales by Category">
                    <AwChart :data="categoryChartData" type="bar" :height="300" />
                </AwCard>
            </AwGrid>

            <!-- Tables Row -->
            <AwGrid :col="{ lg: 2 }">
                <AwCard title="Recent Orders">
                    <AwTableBuilder :collection="recentOrders" :no-pagination="true">
                        <AwTableCol field="id" title="Order #" />
                        <AwTableCol title="Customer">
                            <template #default="{ cell }">
                                {{ cell.customer.name }}
                            </template>
                        </AwTableCol>
                        <AwTableCol title="Total">
                            <template #default="{ cell }">
                                ${{ formatMoney(cell.total) }}
                            </template>
                        </AwTableCol>
                        <AwTableCol title="Status">
                            <template #default="{ cell }">
                                <AwLabel
                                    :label="cell.status"
                                    :color="getStatusColor(cell.status)"
                                />
                            </template>
                        </AwTableCol>
                    </AwTableBuilder>
                </AwCard>

                <AwCard title="Top Products">
                    <AwTableBuilder :collection="topProducts" :no-pagination="true">
                        <AwTableCol field="name" title="Product" />
                        <AwTableCol title="Sales">
                            <template #default="{ cell }">
                                {{ cell.sales_count }} units
                            </template>
                        </AwTableCol>
                        <AwTableCol title="Revenue">
                            <template #default="{ cell }">
                                ${{ formatMoney(cell.revenue) }}
                            </template>
                        </AwTableCol>
                    </AwTableBuilder>
                </AwCard>
            </AwGrid>
        </div>
    </AwPage>
</template>

<script>
import RecentOrders from '~/collections/RecentOrders'
import TopProducts from '~/collections/TopProducts'

export default {
    middleware: 'auth',

    data() {
        return {
            initialLoading: true,
            refreshing: false,
            lastUpdated: null,
            dateRange: {
                start: this.$dayjs().subtract(30, 'days').format('YYYY-MM-DD'),
                end: this.$dayjs().format('YYYY-MM-DD')
            },
            metrics: {
                total_customers: 0,
                revenue: 0,
                revenue_growth: 0,
                total_orders: 0,
                orders_today: 0,
                orders_goal: 100,
                active_subscriptions: 0
            },
            revenueChartData: null,
            categoryChartData: null,
            recentOrders: new RecentOrders([], {
                shop_uuid: this.$route.params.shop_uuid
            }),
            topProducts: new TopProducts([], {
                shop_uuid: this.$route.params.shop_uuid
            })
        }
    },

    async mounted() {
        await this.fetchData()
        this.initialLoading = false
    },

    methods: {
        handleAction(action) {
            if (action.key === 'refresh') {
                this.refresh()
            }
        },

        async fetchData() {
            try {
                await Promise.all([
                    this.fetchMetrics(),
                    this.fetchCharts()
                ])
                this.lastUpdated = new Date()
            } catch (error) {
                this.$notify({
                    message: 'Failed to load dashboard data',
                    type: 'error'
                })
            }
        },

        async fetchMetrics() {
            const { data } = await this.$axios.get(
                `/api/shops/${this.$route.params.shop_uuid}/dashboard/metrics`,
                {
                    params: {
                        start_date: this.dateRange.start,
                        end_date: this.dateRange.end
                    }
                }
            )
            this.metrics = data
        },

        async fetchCharts() {
            const { data } = await this.$axios.get(
                `/api/shops/${this.$route.params.shop_uuid}/dashboard/charts`,
                {
                    params: {
                        start_date: this.dateRange.start,
                        end_date: this.dateRange.end
                    }
                }
            )
            this.revenueChartData = data.revenue
            this.categoryChartData = data.categories
        },

        async refresh() {
            this.refreshing = true
            try {
                await this.fetchData()
                // Refresh collections
                await Promise.all([
                    this.recentOrders.fetch(),
                    this.topProducts.fetch()
                ])
                this.$notify({
                    message: 'Dashboard refreshed',
                    type: 'success'
                })
            } catch (error) {
                this.$notify({
                    message: 'Failed to refresh',
                    type: 'error'
                })
            } finally {
                this.refreshing = false
            }
        },

        formatMoney(cents) {
            return (cents / 100).toFixed(2)
        },

        getStatusColor(status) {
            const colors = {
                pending: 'warning',
                processing: 'info',
                completed: 'success',
                cancelled: 'error'
            }
            return colors[status] || 'mono'
        }
    }
}
</script>
```

## Best Practices

### 1. Manual Fetching for Dashboards

✅ **Use manual fetch, not AwTableBuilder auto-fetch:**
```javascript
async mounted() {
    await this.fetchDashboardData()
}
```

### 2. Loading States

✅ **Show loading during initial fetch:**
```markup
<div v-if="loading">
    <AwProgress indeterminate />
</div>
```

### 3. Error Handling

✅ **Handle errors gracefully:**
```javascript
try {
    await this.fetchData()
} catch (error) {
    this.$notify({
        message: 'Failed to load data',
        type: 'error'
    })
}
```

### 4. Date Formatting

✅ **Use $dayjs for consistency:**
```markup
{{ $dayjs(date).format('ll') }}
{{ $dayjs(date).fromNow() }}
```

### 5. Responsive Layouts

✅ **Use AwGrid component:**
```markup
<AwGrid :col="{ md: 2, lg: 4 }">
  <!-- content -->
</AwGrid>
```

## See Also

- [List Pages](./list-pages.md) - Table-based list pages
- [Detail Pages](./detail-pages.md) - Create and edit forms
- [AwChart](../../components/organisms/aw-chart.md) - Chart component reference
- [AwPage](../../components/pages/aw-page.md) - Page component reference
- [Data Fetching Guide](../data-fetching.md) - Working with API data
