diff --git a/apps/web/app/[locale]/(dashboard)/__tests__/layout.spec.tsx b/apps/web/app/[locale]/(dashboard)/__tests__/layout.spec.tsx
new file mode 100644
index 0000000..9847d81
--- /dev/null
+++ b/apps/web/app/[locale]/(dashboard)/__tests__/layout.spec.tsx
@@ -0,0 +1,110 @@
+/* eslint-disable import-x/order */
+/**
+ * Kiểm thử layout dashboard: header role=banner, status bar role=status,
+ * ticker hiển thị, sidebar và main content.
+ */
+import { render, screen } from '@testing-library/react';
+import * as React from 'react';
+import { describe, expect, it } from 'vitest';
+
+import { DashboardLayout } from '@/components/design-system/dashboard-layout';
+
+describe('DashboardLayout', () => {
+ it('renders header với role=banner', () => {
+ render(
+ Header}
+ statusBar={Online}
+ >
+ Content
+ ,
+ );
+ expect(screen.getByRole('banner')).toBeInTheDocument();
+ });
+
+ it('renders status bar với role=status', () => {
+ render(
+ Header}
+ statusBar={Đã kết nối}
+ >
+ Content
+ ,
+ );
+ expect(screen.getByRole('status')).toBeInTheDocument();
+ expect(screen.getByRole('status')).toHaveTextContent('Đã kết nối');
+ });
+
+ it('renders ticker strip khi được truyền prop ticker', () => {
+ render(
+ Ticker}
+ header={}
+ >
+ Content
+ ,
+ );
+ expect(screen.getByTestId('ticker')).toBeInTheDocument();
+ });
+
+ it('không render ticker khi không truyền prop', () => {
+ render(
+ Header}>
+ Content
+ ,
+ );
+ // Không có vùng ticker
+ expect(screen.queryByTestId('ticker')).not.toBeInTheDocument();
+ });
+
+ it('renders sidebar khi được truyền prop', () => {
+ render(
+ Sidebar}
+ header={}
+ >
+ Nội dung chính
+ ,
+ );
+ expect(screen.getByRole('navigation', { name: 'sidebar-nav' })).toBeInTheDocument();
+ });
+
+ it('renders children trong main content', () => {
+ render(
+ Header}>
+ Nội dung con
+ ,
+ );
+ expect(screen.getByText('Nội dung con')).toBeInTheDocument();
+ });
+
+ it('sidebar collapsed có width 56px', () => {
+ const { container } = render(
+ Nav}
+ sidebarCollapsed
+ header={}
+ >
+ Content
+ ,
+ );
+ // aside phải có inline style width: 56px
+ const aside = container.querySelector('aside');
+ expect(aside).toHaveStyle({ width: '56px' });
+ });
+
+ it('sidebar expanded sử dụng sidebarWidth prop', () => {
+ const { container } = render(
+ Nav}
+ sidebarCollapsed={false}
+ sidebarWidth={240}
+ header={}
+ >
+ Content
+ ,
+ );
+ const aside = container.querySelector('aside');
+ expect(aside).toHaveStyle({ width: '240px' });
+ });
+});
diff --git a/apps/web/app/[locale]/(dashboard)/layout.tsx b/apps/web/app/[locale]/(dashboard)/layout.tsx
index 987c126..ebe6be9 100644
--- a/apps/web/app/[locale]/(dashboard)/layout.tsx
+++ b/apps/web/app/[locale]/(dashboard)/layout.tsx
@@ -27,6 +27,8 @@ import { useTranslations } from 'next-intl';
import { useEffect, useState } from 'react';
import { DashboardLayout } from '@/components/design-system/dashboard-layout';
import { CompactHeader } from '@/components/design-system/compact-header';
+import { TickerStrip } from '@/components/design-system/ticker-strip';
+import type { TickerItem } from '@/components/design-system/ticker-strip';
import { NotificationBell } from '@/components/notifications/notification-bell';
import { useTheme } from '@/components/providers/theme-provider';
import { Button } from '@/components/ui/button';
@@ -296,6 +298,20 @@ export default function AppDashboardLayout({ children }: { children: React.React
/>
);
+ // ── Ticker strip (top 8 quận, placeholder → TODO: /analytics/districts) ───
+ // TODO: thay thế bằng dữ liệu thực từ /analytics/districts khi API sẵn sàng (TEC-3047)
+ const tickerItems: TickerItem[] = [
+ { id: 'q1', label: 'Quận 1', changePercent: 2.4, direction: 'up' },
+ { id: 'q2', label: 'Quận 2', changePercent: -0.8, direction: 'down' },
+ { id: 'q3', label: 'Quận 3', changePercent: 1.1, direction: 'up' },
+ { id: 'q7', label: 'Quận 7', changePercent: 3.2, direction: 'up' },
+ { id: 'binhthanh', label: 'Bình Thạnh', changePercent: 0.0, direction: 'neutral' },
+ { id: 'thuduc', label: 'Thủ Đức', changePercent: 1.7, direction: 'up' },
+ { id: 'tanbinh', label: 'Tân Bình', changePercent: -1.3, direction: 'down' },
+ { id: 'phuninh', label: 'Phú Nhuận', changePercent: 0.5, direction: 'up' },
+ ];
+ const ticker = ;
+
// ── Status bar ───────────────────────────────────────────────────────────
const statusBar = (
<>
@@ -315,6 +331,7 @@ export default function AppDashboardLayout({ children }: { children: React.React
diff --git a/apps/web/components/design-system/dashboard-layout.tsx b/apps/web/components/design-system/dashboard-layout.tsx
index 1722aa5..be998ee 100644
--- a/apps/web/components/design-system/dashboard-layout.tsx
+++ b/apps/web/components/design-system/dashboard-layout.tsx
@@ -65,7 +65,11 @@ export function DashboardLayout({
{statusBar ? (
-