@pattern-stack/frontend-patterns 0.0.6 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/atoms/composed/index.d.ts +0 -1
- package/dist/atoms/composed/index.d.ts.map +1 -1
- package/dist/atoms/types/index.d.ts +0 -2
- package/dist/atoms/types/index.d.ts.map +1 -1
- package/dist/atoms/ui/ErrorBoundary.d.ts +1 -1
- package/dist/atoms/ui/button.d.ts +1 -1
- package/dist/atoms/utils/utils.d.ts +0 -2
- package/dist/atoms/utils/utils.d.ts.map +1 -1
- package/dist/features/auth/components/ProtectedRoute.d.ts +1 -1
- package/dist/frontend-patterns.css +1 -1
- package/dist/index.es.js +15 -403
- package/dist/index.es.js.map +1 -1
- package/dist/index.js +14 -403
- package/dist/index.js.map +1 -1
- package/dist/molecules/layout/Sidebar.d.ts.map +1 -1
- package/dist/molecules/layout/SidebarButton/SidebarButton.d.ts +0 -2
- package/dist/molecules/layout/SidebarButton/SidebarButton.d.ts.map +1 -1
- package/dist/molecules/layout/index.d.ts +0 -3
- package/dist/molecules/layout/index.d.ts.map +1 -1
- package/dist/templates/factory.d.ts +1 -2
- package/dist/templates/factory.d.ts.map +1 -1
- package/dist/templates/index.d.ts.map +1 -1
- package/package.json +3 -7
- package/src/App.tsx +1 -11
- package/src/atoms/composed/index.ts +0 -1
- package/src/atoms/types/index.ts +1 -3
- package/src/atoms/utils/utils.ts +2 -4
- package/src/molecules/layout/Sidebar.tsx +23 -10
- package/src/molecules/layout/SidebarButton/SidebarButton.tsx +10 -32
- package/src/molecules/layout/index.ts +1 -4
- package/src/organisms/index.ts +1 -5
- package/src/pages/AdminShowcase/AdminDashboardShowcase.tsx +75 -77
- package/src/pages/AdminShowcase/index.tsx +1 -2
- package/src/pages/index.ts +1 -2
- package/src/templates/factory.tsx +7 -14
- package/src/templates/index.ts +0 -4
- package/dist/atoms/composed/SalesPanel/SalesPanel.d.ts +0 -19
- package/dist/atoms/composed/SalesPanel/SalesPanel.d.ts.map +0 -1
- package/dist/atoms/composed/SalesPanel/index.d.ts +0 -2
- package/dist/atoms/composed/SalesPanel/index.d.ts.map +0 -1
- package/dist/atoms/composed/SalesPanel/mockSalesData.d.ts +0 -63
- package/dist/atoms/composed/SalesPanel/mockSalesData.d.ts.map +0 -1
- package/dist/atoms/types/entity-config.d.ts +0 -117
- package/dist/atoms/types/entity-config.d.ts.map +0 -1
- package/dist/atoms/types/navigation.d.ts +0 -30
- package/dist/atoms/types/navigation.d.ts.map +0 -1
- package/dist/atoms/utils/icon-resolver.d.ts +0 -72
- package/dist/atoms/utils/icon-resolver.d.ts.map +0 -1
- package/dist/atoms/utils/metric-engine.d.ts +0 -30
- package/dist/atoms/utils/metric-engine.d.ts.map +0 -1
- package/dist/molecules/layout/DashboardWithSidePanel/DashboardWithSidePanel.d.ts +0 -16
- package/dist/molecules/layout/DashboardWithSidePanel/DashboardWithSidePanel.d.ts.map +0 -1
- package/dist/molecules/layout/DashboardWithSidePanel/index.d.ts +0 -2
- package/dist/molecules/layout/DashboardWithSidePanel/index.d.ts.map +0 -1
- package/dist/molecules/layout/NavigationContext.d.ts +0 -15
- package/dist/molecules/layout/NavigationContext.d.ts.map +0 -1
- package/src/__tests__/atoms/composed/databadge.test.tsx +0 -106
- package/src/__tests__/atoms/composed/statcard.test.tsx +0 -133
- package/src/__tests__/atoms/utils/icon-resolver.test.tsx +0 -140
- package/src/atoms/composed/SalesPanel/SalesPanel.tsx +0 -116
- package/src/atoms/composed/SalesPanel/index.ts +0 -1
- package/src/atoms/composed/SalesPanel/mockSalesData.ts +0 -151
- package/src/atoms/types/entity-config.ts +0 -127
- package/src/atoms/types/navigation.ts +0 -43
- package/src/atoms/utils/icon-resolver.tsx +0 -54
- package/src/atoms/utils/metric-engine.ts +0 -236
- package/src/molecules/layout/DashboardWithSidePanel/DashboardWithSidePanel.tsx +0 -42
- package/src/molecules/layout/DashboardWithSidePanel/index.ts +0 -1
- package/src/molecules/layout/NavigationContext.tsx +0 -63
- package/src/organisms/entity/CategoryBreakdownPanel.tsx +0 -427
- package/src/organisms/entity/EntityListPanel.tsx +0 -339
- package/src/organisms/entity/MetricsOverviewPanel.tsx +0 -236
- package/src/organisms/entity/TrendAnalysisPanel.tsx +0 -337
- package/src/organisms/entity/index.ts +0 -4
- package/src/pages/AdminShowcase/SalesPerformanceDashboard.tsx +0 -158
- package/src/pages/EntityShowcase/EntityManagementShowcase.tsx +0 -137
- package/src/pages/EntityShowcase/EntityPerformanceShowcase.tsx +0 -117
- package/src/pages/EntityShowcase/index.ts +0 -2
- package/src/pages/EntityTemplateExample.tsx +0 -229
- package/src/pages/TestEntityTemplate.tsx +0 -40
- package/src/templates/entity/EntityManagementTemplate.tsx +0 -430
- package/src/templates/entity/EntityPerformanceDashboardTemplate.tsx +0 -277
- package/src/templates/entity/configs/financial-config.ts +0 -141
- package/src/templates/entity/configs/index.ts +0 -1
- package/src/templates/entity/index.ts +0 -3
- package/src/templates/financial/FinancialDashboardTemplate.tsx +0 -326
|
@@ -1,133 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi } from 'vitest'
|
|
2
|
-
import { render, screen, userEvent } from '../../utils'
|
|
3
|
-
import { StatCard } from '../../../atoms/composed/StatCard'
|
|
4
|
-
|
|
5
|
-
describe('StatCard (Composed)', () => {
|
|
6
|
-
it('renders basic stat card with title and value', () => {
|
|
7
|
-
render(<StatCard title="Total Users" value="1,234" />)
|
|
8
|
-
|
|
9
|
-
expect(screen.getByText('Total Users')).toBeInTheDocument()
|
|
10
|
-
expect(screen.getByText('1,234')).toBeInTheDocument()
|
|
11
|
-
})
|
|
12
|
-
|
|
13
|
-
it('renders with subtitle', () => {
|
|
14
|
-
render(
|
|
15
|
-
<StatCard
|
|
16
|
-
title="Revenue"
|
|
17
|
-
value="$45,678"
|
|
18
|
-
subtitle="+12% from last month"
|
|
19
|
-
/>
|
|
20
|
-
)
|
|
21
|
-
|
|
22
|
-
expect(screen.getByText('Revenue')).toBeInTheDocument()
|
|
23
|
-
expect(screen.getByText('$45,678')).toBeInTheDocument()
|
|
24
|
-
expect(screen.getByText('+12% from last month')).toBeInTheDocument()
|
|
25
|
-
})
|
|
26
|
-
|
|
27
|
-
it('renders with icon', () => {
|
|
28
|
-
render(
|
|
29
|
-
<StatCard
|
|
30
|
-
title="Orders"
|
|
31
|
-
value="567"
|
|
32
|
-
icon="ShoppingCart"
|
|
33
|
-
/>
|
|
34
|
-
)
|
|
35
|
-
|
|
36
|
-
expect(screen.getByText('Orders')).toBeInTheDocument()
|
|
37
|
-
expect(screen.getByText('567')).toBeInTheDocument()
|
|
38
|
-
// Icon should be rendered (we can't easily test the actual icon, but we can test it doesn't crash)
|
|
39
|
-
})
|
|
40
|
-
|
|
41
|
-
it('applies category theming', () => {
|
|
42
|
-
render(<StatCard title="Test" value="123" category={3} />)
|
|
43
|
-
|
|
44
|
-
const card = screen.getByText('Test').closest('[data-component-name="StatCard"]')
|
|
45
|
-
expect(card).toBeInTheDocument()
|
|
46
|
-
expect(card).toHaveAttribute('data-component-name', 'StatCard')
|
|
47
|
-
})
|
|
48
|
-
|
|
49
|
-
it('handles click events', async () => {
|
|
50
|
-
const user = userEvent.setup()
|
|
51
|
-
const handleClick = vi.fn()
|
|
52
|
-
|
|
53
|
-
render(
|
|
54
|
-
<StatCard
|
|
55
|
-
title="Clickable Card"
|
|
56
|
-
value="999"
|
|
57
|
-
onClick={handleClick}
|
|
58
|
-
/>
|
|
59
|
-
)
|
|
60
|
-
|
|
61
|
-
const card = screen.getByText('Clickable Card').closest('[role="button"]')
|
|
62
|
-
expect(card).toBeInTheDocument()
|
|
63
|
-
|
|
64
|
-
await user.click(card!)
|
|
65
|
-
expect(handleClick).toHaveBeenCalledOnce()
|
|
66
|
-
})
|
|
67
|
-
|
|
68
|
-
it('applies hover effects when clickable', () => {
|
|
69
|
-
render(
|
|
70
|
-
<StatCard
|
|
71
|
-
title="Hoverable"
|
|
72
|
-
value="555"
|
|
73
|
-
onClick={() => {}}
|
|
74
|
-
/>
|
|
75
|
-
)
|
|
76
|
-
|
|
77
|
-
const card = screen.getByText('Hoverable').closest('[role="button"]')
|
|
78
|
-
expect(card).toHaveClass('cursor-pointer')
|
|
79
|
-
expect(card).toHaveClass('hover:shadow-md')
|
|
80
|
-
})
|
|
81
|
-
|
|
82
|
-
it('does not apply click styles when not clickable', () => {
|
|
83
|
-
render(<StatCard title="Static Card" value="111" />)
|
|
84
|
-
|
|
85
|
-
const card = screen.getByText('Static Card').closest('div')
|
|
86
|
-
expect(card).not.toHaveAttribute('role', 'button')
|
|
87
|
-
expect(card).not.toHaveClass('cursor-pointer')
|
|
88
|
-
})
|
|
89
|
-
|
|
90
|
-
it('displays numbers as provided', () => {
|
|
91
|
-
render(<StatCard title="Big Number" value={1234567} />)
|
|
92
|
-
|
|
93
|
-
expect(screen.getByText('1234567')).toBeInTheDocument()
|
|
94
|
-
})
|
|
95
|
-
|
|
96
|
-
it('handles string and number values', () => {
|
|
97
|
-
const { rerender } = render(<StatCard title="String" value="Custom Text" />)
|
|
98
|
-
expect(screen.getByText('Custom Text')).toBeInTheDocument()
|
|
99
|
-
|
|
100
|
-
rerender(<StatCard title="Number" value={42} />)
|
|
101
|
-
expect(screen.getByText('42')).toBeInTheDocument()
|
|
102
|
-
})
|
|
103
|
-
|
|
104
|
-
it('applies custom className', () => {
|
|
105
|
-
render(
|
|
106
|
-
<StatCard
|
|
107
|
-
title="Custom"
|
|
108
|
-
value="123"
|
|
109
|
-
className="custom-stat-card"
|
|
110
|
-
/>
|
|
111
|
-
)
|
|
112
|
-
|
|
113
|
-
const card = screen.getByText('Custom').closest('.custom-stat-card')
|
|
114
|
-
expect(card).toBeInTheDocument()
|
|
115
|
-
})
|
|
116
|
-
|
|
117
|
-
it('renders loading state', () => {
|
|
118
|
-
render(<StatCard title="Loading" value="..." isLoading={true} />)
|
|
119
|
-
|
|
120
|
-
const card = screen.getByText('Loading').closest('[data-component-name="StatCard"]')
|
|
121
|
-
expect(card).toBeInTheDocument()
|
|
122
|
-
// Check for skeleton elements in loading state
|
|
123
|
-
const skeletons = card?.querySelectorAll('.animate-pulse')
|
|
124
|
-
expect(skeletons?.length).toBeGreaterThan(0)
|
|
125
|
-
})
|
|
126
|
-
|
|
127
|
-
it('renders with data component name', () => {
|
|
128
|
-
render(<StatCard title="Test Card" value="123" />)
|
|
129
|
-
|
|
130
|
-
const card = screen.getByText('Test Card').closest('[data-component-name="StatCard"]')
|
|
131
|
-
expect(card).toHaveAttribute('data-component-name', 'StatCard')
|
|
132
|
-
})
|
|
133
|
-
})
|
|
@@ -1,140 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi } from 'vitest'
|
|
2
|
-
import { render, screen } from '../../utils'
|
|
3
|
-
import { Icon, getIcon, iconMap } from '../../../atoms/utils/icon-resolver'
|
|
4
|
-
|
|
5
|
-
// Mock console.warn to avoid noise in tests
|
|
6
|
-
const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
|
|
7
|
-
|
|
8
|
-
describe('Icon Resolver', () => {
|
|
9
|
-
afterEach(() => {
|
|
10
|
-
consoleSpy.mockClear()
|
|
11
|
-
})
|
|
12
|
-
|
|
13
|
-
describe('Icon component', () => {
|
|
14
|
-
it('renders valid icon', () => {
|
|
15
|
-
render(<Icon name="Home" data-testid="home-icon" />)
|
|
16
|
-
|
|
17
|
-
const icon = screen.getByTestId('home-icon')
|
|
18
|
-
expect(icon).toBeInTheDocument()
|
|
19
|
-
})
|
|
20
|
-
|
|
21
|
-
it('applies default className', () => {
|
|
22
|
-
render(<Icon name="Users" data-testid="users-icon" />)
|
|
23
|
-
|
|
24
|
-
const icon = screen.getByTestId('users-icon')
|
|
25
|
-
expect(icon).toHaveClass('w-5', 'h-5')
|
|
26
|
-
})
|
|
27
|
-
|
|
28
|
-
it('applies custom className', () => {
|
|
29
|
-
render(<Icon name="Settings" className="w-8 h-8 text-blue-500" data-testid="settings-icon" />)
|
|
30
|
-
|
|
31
|
-
const icon = screen.getByTestId('settings-icon')
|
|
32
|
-
expect(icon).toHaveClass('w-8', 'h-8', 'text-blue-500')
|
|
33
|
-
})
|
|
34
|
-
|
|
35
|
-
it('applies size prop', () => {
|
|
36
|
-
render(<Icon name="Search" size={24} data-testid="search-icon" />)
|
|
37
|
-
|
|
38
|
-
const icon = screen.getByTestId('search-icon')
|
|
39
|
-
expect(icon).toHaveAttribute('width', '24')
|
|
40
|
-
expect(icon).toHaveAttribute('height', '24')
|
|
41
|
-
})
|
|
42
|
-
|
|
43
|
-
it('renders fallback icon for invalid name', () => {
|
|
44
|
-
render(<Icon name="InvalidIcon" data-testid="fallback-icon" />)
|
|
45
|
-
|
|
46
|
-
const icon = screen.getByTestId('fallback-icon')
|
|
47
|
-
expect(icon).toBeInTheDocument()
|
|
48
|
-
expect(consoleSpy).toHaveBeenCalledWith('Icon "InvalidIcon" not found. Using default Menu icon.')
|
|
49
|
-
})
|
|
50
|
-
|
|
51
|
-
it('handles undefined icon name gracefully', () => {
|
|
52
|
-
render(<Icon name={undefined as any} data-testid="undefined-icon" />)
|
|
53
|
-
|
|
54
|
-
const icon = screen.getByTestId('undefined-icon')
|
|
55
|
-
expect(icon).toBeInTheDocument()
|
|
56
|
-
expect(consoleSpy).toHaveBeenCalled()
|
|
57
|
-
})
|
|
58
|
-
})
|
|
59
|
-
|
|
60
|
-
describe('getIcon function', () => {
|
|
61
|
-
it('returns icon component for valid name', () => {
|
|
62
|
-
const HomeIcon = getIcon('Home')
|
|
63
|
-
expect(HomeIcon).toBeDefined()
|
|
64
|
-
expect(typeof HomeIcon).toBe('function')
|
|
65
|
-
})
|
|
66
|
-
|
|
67
|
-
it('returns Menu icon for invalid name', () => {
|
|
68
|
-
const InvalidIcon = getIcon('InvalidIcon' as any)
|
|
69
|
-
expect(InvalidIcon).toBeDefined()
|
|
70
|
-
expect(typeof InvalidIcon).toBe('function')
|
|
71
|
-
})
|
|
72
|
-
|
|
73
|
-
it('returns Menu icon for undefined name', () => {
|
|
74
|
-
const UndefinedIcon = getIcon(undefined as any)
|
|
75
|
-
expect(UndefinedIcon).toBeDefined()
|
|
76
|
-
})
|
|
77
|
-
})
|
|
78
|
-
|
|
79
|
-
describe('iconMap', () => {
|
|
80
|
-
it('contains expected common icons', () => {
|
|
81
|
-
const expectedIcons = [
|
|
82
|
-
'Home', 'Users', 'Settings', 'Search', 'Menu',
|
|
83
|
-
'ChevronDown', 'ChevronRight', 'X', 'Plus',
|
|
84
|
-
'ShoppingCart', 'Calendar', 'Mail', 'Phone'
|
|
85
|
-
]
|
|
86
|
-
|
|
87
|
-
expectedIcons.forEach(iconName => {
|
|
88
|
-
expect(iconMap).toHaveProperty(iconName)
|
|
89
|
-
expect(typeof iconMap[iconName]).toBe('function')
|
|
90
|
-
})
|
|
91
|
-
})
|
|
92
|
-
|
|
93
|
-
it('has reasonable number of icons', () => {
|
|
94
|
-
const iconCount = Object.keys(iconMap).length
|
|
95
|
-
expect(iconCount).toBeGreaterThan(30)
|
|
96
|
-
expect(iconCount).toBeLessThan(100) // Keep it manageable
|
|
97
|
-
})
|
|
98
|
-
|
|
99
|
-
it('all icon map values are functions', () => {
|
|
100
|
-
Object.values(iconMap).forEach(iconComponent => {
|
|
101
|
-
expect(typeof iconComponent).toBe('function')
|
|
102
|
-
})
|
|
103
|
-
})
|
|
104
|
-
})
|
|
105
|
-
|
|
106
|
-
describe('Icon rendering variations', () => {
|
|
107
|
-
const testIcons = ['Home', 'Users', 'Settings', 'Search', 'Bell']
|
|
108
|
-
|
|
109
|
-
testIcons.forEach(iconName => {
|
|
110
|
-
it(`renders ${iconName} icon without errors`, () => {
|
|
111
|
-
render(<Icon name={iconName} data-testid={`${iconName.toLowerCase()}-icon`} />)
|
|
112
|
-
|
|
113
|
-
const icon = screen.getByTestId(`${iconName.toLowerCase()}-icon`)
|
|
114
|
-
expect(icon).toBeInTheDocument()
|
|
115
|
-
})
|
|
116
|
-
})
|
|
117
|
-
})
|
|
118
|
-
|
|
119
|
-
describe('Integration with real icon names', () => {
|
|
120
|
-
it('renders navigation icons correctly', () => {
|
|
121
|
-
const navIcons = ['Home', 'Users', 'Settings', 'FileText', 'BarChart']
|
|
122
|
-
|
|
123
|
-
navIcons.forEach(iconName => {
|
|
124
|
-
const { unmount } = render(<Icon name={iconName} />)
|
|
125
|
-
// If it renders without throwing, it's valid
|
|
126
|
-
unmount()
|
|
127
|
-
})
|
|
128
|
-
})
|
|
129
|
-
|
|
130
|
-
it('renders action icons correctly', () => {
|
|
131
|
-
const actionIcons = ['Plus', 'Edit', 'Trash2', 'Save', 'Download']
|
|
132
|
-
|
|
133
|
-
actionIcons.forEach(iconName => {
|
|
134
|
-
const { unmount } = render(<Icon name={iconName} />)
|
|
135
|
-
// If it renders without throwing, it's valid
|
|
136
|
-
unmount()
|
|
137
|
-
})
|
|
138
|
-
})
|
|
139
|
-
})
|
|
140
|
-
})
|
|
@@ -1,116 +0,0 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
|
-
import { DataBadge } from '../DataBadge';
|
|
3
|
-
import { Button } from '../../ui/button';
|
|
4
|
-
import {
|
|
5
|
-
Target,
|
|
6
|
-
ExternalLink
|
|
7
|
-
} from 'lucide-react';
|
|
8
|
-
|
|
9
|
-
interface SalesPanelProps {
|
|
10
|
-
sales: Array<{
|
|
11
|
-
id: string;
|
|
12
|
-
customer: string;
|
|
13
|
-
product: string;
|
|
14
|
-
amount: number;
|
|
15
|
-
stage: string;
|
|
16
|
-
salesperson: string;
|
|
17
|
-
region: string;
|
|
18
|
-
closeDate: string;
|
|
19
|
-
dealSize: string;
|
|
20
|
-
}>;
|
|
21
|
-
onSaleClick?: (sale: any) => void;
|
|
22
|
-
onClose?: () => void;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
const getStageStatus = (stage: string) => {
|
|
26
|
-
switch (stage) {
|
|
27
|
-
case 'Closed Won': return 'success';
|
|
28
|
-
case 'Negotiation': return 'warning';
|
|
29
|
-
case 'Proposal Sent': return 'info';
|
|
30
|
-
case 'Qualified': return 'info';
|
|
31
|
-
case 'Discovery': return 'neutral';
|
|
32
|
-
default: return 'neutral';
|
|
33
|
-
}
|
|
34
|
-
};
|
|
35
|
-
|
|
36
|
-
export const SalesPanel: React.FC<SalesPanelProps> = ({
|
|
37
|
-
sales,
|
|
38
|
-
onSaleClick,
|
|
39
|
-
onClose
|
|
40
|
-
}) => {
|
|
41
|
-
const topDeals = sales.slice(0, 5); // Show only top 5 recent deals
|
|
42
|
-
const totalValue = sales.reduce((sum, sale) => sum + sale.amount, 0);
|
|
43
|
-
|
|
44
|
-
return (
|
|
45
|
-
<div className="fixed right-0 top-16 h-[calc(100vh-4rem)] w-72 bg-background border-l border-border shadow-lg flex flex-col z-30">
|
|
46
|
-
{/* Header */}
|
|
47
|
-
<div className="p-3 border-b border-border">
|
|
48
|
-
<div className="flex items-center justify-between">
|
|
49
|
-
<h3 className="font-medium flex items-center gap-2">
|
|
50
|
-
<Target className="w-4 h-4" />
|
|
51
|
-
Pipeline
|
|
52
|
-
</h3>
|
|
53
|
-
{onClose && (
|
|
54
|
-
<Button variant="ghost" size="sm" onClick={onClose}>
|
|
55
|
-
×
|
|
56
|
-
</Button>
|
|
57
|
-
)}
|
|
58
|
-
</div>
|
|
59
|
-
</div>
|
|
60
|
-
|
|
61
|
-
{/* Quick Stats */}
|
|
62
|
-
<div className="p-3 border-b border-border">
|
|
63
|
-
<div className="space-y-2">
|
|
64
|
-
<div className="flex justify-between">
|
|
65
|
-
<span className="text-sm text-muted-foreground">Total Value</span>
|
|
66
|
-
<span className="font-semibold text-green-600">${totalValue.toLocaleString()}</span>
|
|
67
|
-
</div>
|
|
68
|
-
<div className="flex justify-between">
|
|
69
|
-
<span className="text-sm text-muted-foreground">Active Deals</span>
|
|
70
|
-
<span className="font-semibold">{sales.length}</span>
|
|
71
|
-
</div>
|
|
72
|
-
</div>
|
|
73
|
-
</div>
|
|
74
|
-
|
|
75
|
-
{/* Recent Deals */}
|
|
76
|
-
<div className="flex-1 overflow-auto p-3">
|
|
77
|
-
<h4 className="text-sm font-medium mb-3">Recent Opportunities</h4>
|
|
78
|
-
<div className="space-y-2">
|
|
79
|
-
{topDeals.map((sale) => (
|
|
80
|
-
<div
|
|
81
|
-
key={sale.id}
|
|
82
|
-
className="p-2 border border-border rounded-md hover:bg-muted/50 cursor-pointer transition-colors"
|
|
83
|
-
onClick={() => onSaleClick?.(sale)}
|
|
84
|
-
>
|
|
85
|
-
<div className="flex justify-between items-start mb-1">
|
|
86
|
-
<div className="flex-1 min-w-0">
|
|
87
|
-
<p className="text-sm font-medium truncate">{sale.customer}</p>
|
|
88
|
-
<p className="text-xs text-muted-foreground truncate">{sale.product}</p>
|
|
89
|
-
</div>
|
|
90
|
-
<div className="text-right ml-2">
|
|
91
|
-
<p className="text-sm font-semibold text-green-600">
|
|
92
|
-
${sale.amount.toLocaleString()}
|
|
93
|
-
</p>
|
|
94
|
-
</div>
|
|
95
|
-
</div>
|
|
96
|
-
<div className="flex justify-between items-center">
|
|
97
|
-
<DataBadge variant="status" status={getStageStatus(sale.stage)}>
|
|
98
|
-
{sale.stage}
|
|
99
|
-
</DataBadge>
|
|
100
|
-
<span className="text-xs text-muted-foreground">{sale.salesperson}</span>
|
|
101
|
-
</div>
|
|
102
|
-
</div>
|
|
103
|
-
))}
|
|
104
|
-
</div>
|
|
105
|
-
</div>
|
|
106
|
-
|
|
107
|
-
{/* Footer */}
|
|
108
|
-
<div className="p-3 border-t border-border">
|
|
109
|
-
<Button variant="outline" size="sm" className="w-full">
|
|
110
|
-
<ExternalLink className="w-4 h-4 mr-2" />
|
|
111
|
-
View All Deals
|
|
112
|
-
</Button>
|
|
113
|
-
</div>
|
|
114
|
-
</div>
|
|
115
|
-
);
|
|
116
|
-
};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export { SalesPanel } from './SalesPanel';
|
|
@@ -1,151 +0,0 @@
|
|
|
1
|
-
// Mock sales data for demonstrations
|
|
2
|
-
export const mockSalesData = [
|
|
3
|
-
{
|
|
4
|
-
id: 'SALE-2024-001',
|
|
5
|
-
customer: 'TechCorp Industries',
|
|
6
|
-
product: 'Enterprise License',
|
|
7
|
-
amount: 45000,
|
|
8
|
-
stage: 'Closed Won',
|
|
9
|
-
salesperson: 'Sarah Chen',
|
|
10
|
-
region: 'North America',
|
|
11
|
-
closeDate: '2024-06-08',
|
|
12
|
-
dealSize: 'Large'
|
|
13
|
-
},
|
|
14
|
-
{
|
|
15
|
-
id: 'SALE-2024-002',
|
|
16
|
-
customer: 'StartupXYZ',
|
|
17
|
-
product: 'Professional Plan',
|
|
18
|
-
amount: 12000,
|
|
19
|
-
stage: 'Proposal Sent',
|
|
20
|
-
salesperson: 'Mike Johnson',
|
|
21
|
-
region: 'North America',
|
|
22
|
-
closeDate: '2024-06-15',
|
|
23
|
-
dealSize: 'Medium'
|
|
24
|
-
},
|
|
25
|
-
{
|
|
26
|
-
id: 'SALE-2024-003',
|
|
27
|
-
customer: 'Global Enterprises Ltd',
|
|
28
|
-
product: 'Enterprise Suite',
|
|
29
|
-
amount: 78000,
|
|
30
|
-
stage: 'Negotiation',
|
|
31
|
-
salesperson: 'Lisa Wang',
|
|
32
|
-
region: 'Europe',
|
|
33
|
-
closeDate: '2024-06-20',
|
|
34
|
-
dealSize: 'Large'
|
|
35
|
-
},
|
|
36
|
-
{
|
|
37
|
-
id: 'SALE-2024-004',
|
|
38
|
-
customer: 'Innovation Labs',
|
|
39
|
-
product: 'Starter Package',
|
|
40
|
-
amount: 5000,
|
|
41
|
-
stage: 'Qualified',
|
|
42
|
-
salesperson: 'David Kim',
|
|
43
|
-
region: 'Asia Pacific',
|
|
44
|
-
closeDate: '2024-06-25',
|
|
45
|
-
dealSize: 'Small'
|
|
46
|
-
},
|
|
47
|
-
{
|
|
48
|
-
id: 'SALE-2024-005',
|
|
49
|
-
customer: 'FutureTech Solutions',
|
|
50
|
-
product: 'Custom Integration',
|
|
51
|
-
amount: 95000,
|
|
52
|
-
stage: 'Discovery',
|
|
53
|
-
salesperson: 'Sarah Chen',
|
|
54
|
-
region: 'North America',
|
|
55
|
-
closeDate: '2024-07-01',
|
|
56
|
-
dealSize: 'Large'
|
|
57
|
-
}
|
|
58
|
-
];
|
|
59
|
-
|
|
60
|
-
// Sales configuration for entity template
|
|
61
|
-
export const salesConfig = {
|
|
62
|
-
entityType: 'performance' as const,
|
|
63
|
-
display: {
|
|
64
|
-
title: 'Sales Performance',
|
|
65
|
-
subtitle: 'Monitor sales metrics, team performance, and revenue trends',
|
|
66
|
-
category: 3 as const
|
|
67
|
-
},
|
|
68
|
-
metrics: [
|
|
69
|
-
{
|
|
70
|
-
key: 'revenue',
|
|
71
|
-
label: 'Total Revenue',
|
|
72
|
-
type: 'currency' as const,
|
|
73
|
-
aggregate: 'sum',
|
|
74
|
-
showTrend: true,
|
|
75
|
-
category: 2
|
|
76
|
-
},
|
|
77
|
-
{
|
|
78
|
-
key: 'deals',
|
|
79
|
-
label: 'Deals Closed',
|
|
80
|
-
type: 'number' as const,
|
|
81
|
-
aggregate: 'count',
|
|
82
|
-
showTrend: true,
|
|
83
|
-
category: 3
|
|
84
|
-
},
|
|
85
|
-
{
|
|
86
|
-
key: 'conversion',
|
|
87
|
-
label: 'Conversion Rate',
|
|
88
|
-
type: 'percentage' as const,
|
|
89
|
-
aggregate: 'average',
|
|
90
|
-
showTrend: true,
|
|
91
|
-
category: 1
|
|
92
|
-
},
|
|
93
|
-
{
|
|
94
|
-
key: 'pipeline',
|
|
95
|
-
label: 'Pipeline Value',
|
|
96
|
-
type: 'currency' as const,
|
|
97
|
-
aggregate: 'sum',
|
|
98
|
-
showTrend: true,
|
|
99
|
-
category: 4
|
|
100
|
-
}
|
|
101
|
-
],
|
|
102
|
-
categories: [
|
|
103
|
-
{ key: 'region', label: 'By Region', type: 'categorical' },
|
|
104
|
-
{ key: 'product', label: 'By Product', type: 'categorical' },
|
|
105
|
-
{ key: 'stage', label: 'By Stage', type: 'categorical' }
|
|
106
|
-
],
|
|
107
|
-
trends: {
|
|
108
|
-
periods: ['7d', '30d', '90d', '1y'],
|
|
109
|
-
defaultPeriod: '30d'
|
|
110
|
-
}
|
|
111
|
-
};
|
|
112
|
-
|
|
113
|
-
// Sample sales performance data
|
|
114
|
-
export const salesPerformanceData = [
|
|
115
|
-
{
|
|
116
|
-
id: 'sale-1',
|
|
117
|
-
revenue: 45000,
|
|
118
|
-
deals: 12,
|
|
119
|
-
conversion: 0.24,
|
|
120
|
-
pipeline: 125000,
|
|
121
|
-
region: 'North America',
|
|
122
|
-
product: 'Enterprise',
|
|
123
|
-
stage: 'Closed Won',
|
|
124
|
-
date: '2024-06-01',
|
|
125
|
-
salesperson: 'Sarah Chen'
|
|
126
|
-
},
|
|
127
|
-
{
|
|
128
|
-
id: 'sale-2',
|
|
129
|
-
revenue: 32000,
|
|
130
|
-
deals: 8,
|
|
131
|
-
conversion: 0.18,
|
|
132
|
-
pipeline: 89000,
|
|
133
|
-
region: 'Europe',
|
|
134
|
-
product: 'Professional',
|
|
135
|
-
stage: 'Negotiation',
|
|
136
|
-
date: '2024-06-02',
|
|
137
|
-
salesperson: 'Mike Johnson'
|
|
138
|
-
},
|
|
139
|
-
{
|
|
140
|
-
id: 'sale-3',
|
|
141
|
-
revenue: 67000,
|
|
142
|
-
deals: 15,
|
|
143
|
-
conversion: 0.31,
|
|
144
|
-
pipeline: 156000,
|
|
145
|
-
region: 'Asia Pacific',
|
|
146
|
-
product: 'Enterprise',
|
|
147
|
-
stage: 'Closed Won',
|
|
148
|
-
date: '2024-06-03',
|
|
149
|
-
salesperson: 'Lisa Wang'
|
|
150
|
-
}
|
|
151
|
-
];
|
|
@@ -1,127 +0,0 @@
|
|
|
1
|
-
import * as React from 'react';
|
|
2
|
-
|
|
3
|
-
export type EntityType = 'transactional' | 'categorical' | 'temporal';
|
|
4
|
-
|
|
5
|
-
export type MetricType = 'currency' | 'percentage' | 'count' | 'duration' | 'ratio';
|
|
6
|
-
|
|
7
|
-
export type AggregationType = 'sum' | 'avg' | 'count' | 'min' | 'max';
|
|
8
|
-
|
|
9
|
-
export type TemporalCycle = 'daily' | 'weekly' | 'monthly' | 'quarterly' | 'yearly';
|
|
10
|
-
|
|
11
|
-
export type ForecastingAlgorithm = 'linear' | 'seasonal' | 'ml';
|
|
12
|
-
|
|
13
|
-
export type ActionType = 'primary' | 'secondary' | 'danger';
|
|
14
|
-
|
|
15
|
-
export interface FormatConfig {
|
|
16
|
-
decimals?: number;
|
|
17
|
-
prefix?: string;
|
|
18
|
-
suffix?: string;
|
|
19
|
-
thousands?: boolean;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export interface MetricConfig {
|
|
23
|
-
key: string;
|
|
24
|
-
label: string;
|
|
25
|
-
type: MetricType;
|
|
26
|
-
format?: FormatConfig;
|
|
27
|
-
trend?: boolean;
|
|
28
|
-
target?: number | ((data: EntityData[]) => number);
|
|
29
|
-
aggregation?: AggregationType;
|
|
30
|
-
icon?: React.ComponentType;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
export interface TemporalConfig {
|
|
34
|
-
cycles: TemporalCycle[];
|
|
35
|
-
defaultCycle: TemporalCycle;
|
|
36
|
-
enableComparisons: boolean;
|
|
37
|
-
forecasting?: {
|
|
38
|
-
enabled: boolean;
|
|
39
|
-
periods: number;
|
|
40
|
-
algorithm: ForecastingAlgorithm;
|
|
41
|
-
};
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
export interface CategoryConfig {
|
|
45
|
-
hierarchy: string[];
|
|
46
|
-
defaultGroupBy: string;
|
|
47
|
-
enableDrillDown: boolean;
|
|
48
|
-
colorCoding?: boolean;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
export interface ActionConfig {
|
|
52
|
-
label: string;
|
|
53
|
-
type: ActionType;
|
|
54
|
-
onClick: (context: { selectedItems?: EntityData[]; data?: EntityData[]; config?: EntityTemplateConfig }) => void | Promise<void>;
|
|
55
|
-
icon?: React.ComponentType;
|
|
56
|
-
permissions?: string[];
|
|
57
|
-
validation?: (context: { selectedItems?: EntityData[]; data?: EntityData[]; config?: EntityTemplateConfig }) => boolean;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
export interface BusinessRulesConfig<T> {
|
|
61
|
-
validation?: (item: T) => string[];
|
|
62
|
-
constraints?: {
|
|
63
|
-
[key: string]: {
|
|
64
|
-
min?: number;
|
|
65
|
-
max?: number;
|
|
66
|
-
required?: boolean;
|
|
67
|
-
pattern?: RegExp;
|
|
68
|
-
};
|
|
69
|
-
};
|
|
70
|
-
workflows?: {
|
|
71
|
-
[status: string]: string[];
|
|
72
|
-
};
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
export interface EntityDisplayConfig {
|
|
76
|
-
title: string;
|
|
77
|
-
description?: string;
|
|
78
|
-
category?: 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
export interface EntityTemplateConfig<T = EntityData> {
|
|
82
|
-
entityType: EntityType;
|
|
83
|
-
display: EntityDisplayConfig;
|
|
84
|
-
metrics: MetricConfig[];
|
|
85
|
-
temporal?: TemporalConfig;
|
|
86
|
-
categories?: CategoryConfig;
|
|
87
|
-
businessRules?: BusinessRulesConfig<T>;
|
|
88
|
-
actions?: ActionConfig[];
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
export interface EntityData {
|
|
92
|
-
id: string | number;
|
|
93
|
-
[key: string]: unknown;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
export interface MetricValue {
|
|
97
|
-
current: number;
|
|
98
|
-
previous?: number;
|
|
99
|
-
trend?: 'up' | 'down' | 'neutral';
|
|
100
|
-
target?: number;
|
|
101
|
-
formattedValue: string;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
export interface CategoryBreakdown {
|
|
105
|
-
category: string;
|
|
106
|
-
value: number;
|
|
107
|
-
percentage: number;
|
|
108
|
-
color?: string;
|
|
109
|
-
subcategories?: CategoryBreakdown[];
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
export interface TrendDataPoint {
|
|
113
|
-
date: string;
|
|
114
|
-
value: number;
|
|
115
|
-
label?: string;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
export interface EntityAnalyticsData {
|
|
119
|
-
metrics: Record<string, MetricValue>;
|
|
120
|
-
categories: CategoryBreakdown[];
|
|
121
|
-
trends: Record<string, TrendDataPoint[]>;
|
|
122
|
-
insights?: {
|
|
123
|
-
type: 'positive' | 'negative' | 'neutral';
|
|
124
|
-
message: string;
|
|
125
|
-
value?: number;
|
|
126
|
-
}[];
|
|
127
|
-
}
|