Initial commit - WhyRating Engine (Google Reviews Scraper)
This commit is contained in:
@@ -10,6 +10,7 @@ interface DashboardSectionProps {
|
||||
section: DashboardSectionType;
|
||||
pipelineId: string;
|
||||
businessId?: string;
|
||||
jobId?: string;
|
||||
timeRange?: string;
|
||||
}
|
||||
|
||||
@@ -20,6 +21,7 @@ export function DashboardSection({
|
||||
section,
|
||||
pipelineId,
|
||||
businessId,
|
||||
jobId,
|
||||
timeRange = '30d',
|
||||
}: DashboardSectionProps) {
|
||||
const [collapsed, setCollapsed] = useState(section.collapsed ?? false);
|
||||
@@ -37,6 +39,7 @@ export function DashboardSection({
|
||||
try {
|
||||
const data = await getWidgetData(pipelineId, widgetId, {
|
||||
business_id: businessId,
|
||||
job_id: jobId,
|
||||
time_range: timeRange,
|
||||
page: page || tablePagination[widgetId] || 1,
|
||||
});
|
||||
@@ -50,7 +53,7 @@ export function DashboardSection({
|
||||
setWidgetLoading((prev) => ({ ...prev, [widgetId]: false }));
|
||||
}
|
||||
},
|
||||
[pipelineId, businessId, timeRange, tablePagination]
|
||||
[pipelineId, businessId, jobId, timeRange, tablePagination]
|
||||
);
|
||||
|
||||
// Fetch all widget data on mount and when params change
|
||||
@@ -60,7 +63,7 @@ export function DashboardSection({
|
||||
fetchWidgetData(widget.id);
|
||||
});
|
||||
}
|
||||
}, [section.widgets, collapsed, pipelineId, businessId, timeRange]);
|
||||
}, [section.widgets, collapsed, pipelineId, businessId, jobId, timeRange]);
|
||||
|
||||
// Handle page change for tables
|
||||
const handlePageChange = (widgetId: string, page: number) => {
|
||||
@@ -109,7 +112,7 @@ export function DashboardSection({
|
||||
<ChevronDown className="w-5 h-5 text-gray-500 mr-2" />
|
||||
)}
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold text-gray-900 dark:text-gray-100 group-hover:text-blue-600">
|
||||
<h2 className="text-lg font-semibold text-gray-900 group-hover:text-blue-600">
|
||||
{section.title}
|
||||
</h2>
|
||||
{section.description && (
|
||||
|
||||
@@ -9,6 +9,7 @@ interface DynamicDashboardProps {
|
||||
pipelineId: string;
|
||||
config: DashboardConfig;
|
||||
businessId?: string;
|
||||
jobId?: string;
|
||||
}
|
||||
|
||||
// Time range options
|
||||
@@ -31,6 +32,7 @@ export function DynamicDashboard({
|
||||
pipelineId,
|
||||
config,
|
||||
businessId: initialBusinessId,
|
||||
jobId,
|
||||
}: DynamicDashboardProps) {
|
||||
const [timeRange, setTimeRange] = useState(config.default_time_range || '30d');
|
||||
const [businessId, setBusinessId] = useState(initialBusinessId);
|
||||
@@ -46,11 +48,11 @@ export function DynamicDashboard({
|
||||
{/* Dashboard Header */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-gray-900 dark:text-gray-100">
|
||||
<h1 className="text-2xl font-bold text-gray-900">
|
||||
{config.title}
|
||||
</h1>
|
||||
{config.description && (
|
||||
<p className="text-gray-500 mt-1">{config.description}</p>
|
||||
<p className="text-gray-600 mt-1">{config.description}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -58,7 +60,7 @@ export function DynamicDashboard({
|
||||
<div className="flex items-center space-x-3">
|
||||
{/* Business Filter (placeholder) */}
|
||||
{businessId && (
|
||||
<div className="flex items-center text-sm text-gray-600 dark:text-gray-400 bg-gray-100 dark:bg-gray-800 px-3 py-2 rounded-md">
|
||||
<div className="flex items-center text-sm text-gray-700 bg-gray-100 px-3 py-2 rounded-md">
|
||||
<Building2 className="w-4 h-4 mr-2" />
|
||||
<span className="truncate max-w-[150px]">{businessId}</span>
|
||||
</div>
|
||||
@@ -69,7 +71,7 @@ export function DynamicDashboard({
|
||||
<select
|
||||
value={timeRange}
|
||||
onChange={(e) => setTimeRange(e.target.value)}
|
||||
className="appearance-none bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 rounded-md pl-9 pr-8 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
className="appearance-none bg-white border border-gray-300 rounded-md pl-9 pr-8 py-2 text-sm text-gray-900 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
>
|
||||
{TIME_RANGES.map((range) => (
|
||||
<option key={range.value} value={range.value}>
|
||||
@@ -83,7 +85,7 @@ export function DynamicDashboard({
|
||||
{/* Refresh Button */}
|
||||
<button
|
||||
onClick={handleRefresh}
|
||||
className="p-2 text-gray-500 hover:text-gray-700 dark:hover:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-md"
|
||||
className="p-2 text-gray-500 hover:text-gray-700 hover:bg-gray-200 rounded-md"
|
||||
title="Refresh all widgets"
|
||||
>
|
||||
<RefreshCw className="w-5 h-5" />
|
||||
@@ -98,6 +100,7 @@ export function DynamicDashboard({
|
||||
section={section}
|
||||
pipelineId={pipelineId}
|
||||
businessId={businessId}
|
||||
jobId={jobId}
|
||||
timeRange={timeRange}
|
||||
/>
|
||||
))}
|
||||
|
||||
@@ -42,7 +42,7 @@ export function BarChartWidget({
|
||||
error,
|
||||
onRefresh,
|
||||
}: BarChartWidgetProps) {
|
||||
const chartConfig = config.config as ChartWidgetConfig;
|
||||
const chartConfig = config.config as unknown as ChartWidgetConfig;
|
||||
const chartData = data?.data || [];
|
||||
|
||||
return (
|
||||
@@ -58,25 +58,25 @@ export function BarChartWidget({
|
||||
margin={{ top: 5, right: 20, left: 0, bottom: 5 }}
|
||||
>
|
||||
{chartConfig.show_grid !== false && (
|
||||
<CartesianGrid strokeDasharray="3 3" stroke="#e5e7eb" />
|
||||
<CartesianGrid strokeDasharray="3 3" stroke="#d1d5db" />
|
||||
)}
|
||||
<XAxis
|
||||
dataKey={chartConfig.x_axis?.key || 'x'}
|
||||
tick={{ fontSize: 12 }}
|
||||
tick={{ fontSize: 12, fill: '#374151' }}
|
||||
tickLine={false}
|
||||
axisLine={{ stroke: '#e5e7eb' }}
|
||||
axisLine={{ stroke: '#d1d5db' }}
|
||||
/>
|
||||
<YAxis
|
||||
tick={{ fontSize: 12 }}
|
||||
tick={{ fontSize: 12, fill: '#374151' }}
|
||||
tickLine={false}
|
||||
axisLine={{ stroke: '#e5e7eb' }}
|
||||
axisLine={{ stroke: '#d1d5db' }}
|
||||
label={
|
||||
chartConfig.y_axis?.label
|
||||
? {
|
||||
value: chartConfig.y_axis.label,
|
||||
angle: -90,
|
||||
position: 'insideLeft',
|
||||
style: { fontSize: 12 },
|
||||
style: { fontSize: 12, fill: '#374151' },
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ export function DataTableWidget({
|
||||
onPageChange,
|
||||
currentPage = 1,
|
||||
}: DataTableWidgetProps) {
|
||||
const tableConfig = config.config as TableWidgetConfig;
|
||||
const tableConfig = config.config as unknown as TableWidgetConfig;
|
||||
const rows = data?.data || [];
|
||||
const total = data?.total || 0;
|
||||
const pageSize = tableConfig.page_size || 10;
|
||||
@@ -42,13 +42,13 @@ export function DataTableWidget({
|
||||
<div className="flex flex-col h-full">
|
||||
{/* Table */}
|
||||
<div className="flex-1 overflow-auto">
|
||||
<table className="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
|
||||
<thead className="bg-gray-50 dark:bg-gray-800 sticky top-0">
|
||||
<table className="min-w-full divide-y divide-gray-200">
|
||||
<thead className="bg-gray-50 sticky top-0">
|
||||
<tr>
|
||||
{tableConfig.columns.map((col) => (
|
||||
<th
|
||||
key={col.key}
|
||||
className={`px-4 py-3 text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider ${
|
||||
className={`px-4 py-3 text-xs font-semibold text-gray-600 uppercase tracking-wider ${
|
||||
col.align === 'right'
|
||||
? 'text-right'
|
||||
: col.align === 'center'
|
||||
@@ -62,16 +62,16 @@ export function DataTableWidget({
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="bg-white dark:bg-gray-900 divide-y divide-gray-200 dark:divide-gray-700">
|
||||
<tbody className="bg-white divide-y divide-gray-200">
|
||||
{rows.map((row, rowIndex) => (
|
||||
<tr
|
||||
key={row[tableConfig.row_key] as string || rowIndex}
|
||||
className="hover:bg-gray-50 dark:hover:bg-gray-800"
|
||||
className="hover:bg-gray-50"
|
||||
>
|
||||
{tableConfig.columns.map((col) => (
|
||||
<td
|
||||
key={col.key}
|
||||
className={`px-4 py-3 text-sm text-gray-900 dark:text-gray-100 whitespace-nowrap ${
|
||||
className={`px-4 py-3 text-sm text-gray-900 whitespace-nowrap ${
|
||||
col.align === 'right'
|
||||
? 'text-right'
|
||||
: col.align === 'center'
|
||||
@@ -90,8 +90,8 @@ export function DataTableWidget({
|
||||
|
||||
{/* Pagination */}
|
||||
{tableConfig.show_pagination !== false && totalPages > 1 && onPageChange && (
|
||||
<div className="flex items-center justify-between px-4 py-3 border-t border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-800">
|
||||
<div className="text-sm text-gray-500">
|
||||
<div className="flex items-center justify-between px-4 py-3 border-t border-gray-200 bg-gray-50">
|
||||
<div className="text-sm text-gray-600">
|
||||
Showing {(currentPage - 1) * pageSize + 1} to{' '}
|
||||
{Math.min(currentPage * pageSize, total)} of {total}
|
||||
</div>
|
||||
@@ -99,17 +99,17 @@ export function DataTableWidget({
|
||||
<button
|
||||
onClick={() => onPageChange(currentPage - 1)}
|
||||
disabled={currentPage <= 1}
|
||||
className="p-1 rounded hover:bg-gray-200 dark:hover:bg-gray-700 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
className="p-1 rounded text-gray-600 hover:bg-gray-200 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
<ChevronLeft className="w-5 h-5" />
|
||||
</button>
|
||||
<span className="text-sm text-gray-700 dark:text-gray-300">
|
||||
<span className="text-sm text-gray-700">
|
||||
Page {currentPage} of {totalPages}
|
||||
</span>
|
||||
<button
|
||||
onClick={() => onPageChange(currentPage + 1)}
|
||||
disabled={currentPage >= totalPages}
|
||||
className="p-1 rounded hover:bg-gray-200 dark:hover:bg-gray-700 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
className="p-1 rounded text-gray-600 hover:bg-gray-200 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
<ChevronRight className="w-5 h-5" />
|
||||
</button>
|
||||
|
||||
@@ -21,7 +21,7 @@ export function HeatmapWidget({
|
||||
error,
|
||||
onRefresh,
|
||||
}: HeatmapWidgetProps) {
|
||||
const heatmapConfig = config.config as HeatmapConfig;
|
||||
const heatmapConfig = config.config as unknown as HeatmapConfig;
|
||||
const rawData = data?.data || [];
|
||||
|
||||
// Extract unique x and y values
|
||||
@@ -77,7 +77,7 @@ export function HeatmapWidget({
|
||||
<tbody>
|
||||
{yValues.map((y) => (
|
||||
<tr key={y}>
|
||||
<td className="px-2 py-2 text-xs font-medium text-gray-700 dark:text-gray-300">
|
||||
<td className="px-2 py-2 text-xs font-medium text-gray-700">
|
||||
{y}
|
||||
</td>
|
||||
{xValues.map((x) => {
|
||||
|
||||
@@ -42,7 +42,7 @@ export function LineChartWidget({
|
||||
error,
|
||||
onRefresh,
|
||||
}: LineChartWidgetProps) {
|
||||
const chartConfig = config.config as ChartWidgetConfig;
|
||||
const chartConfig = config.config as unknown as ChartWidgetConfig;
|
||||
const chartData = data?.data || [];
|
||||
|
||||
return (
|
||||
@@ -58,25 +58,25 @@ export function LineChartWidget({
|
||||
margin={{ top: 5, right: 20, left: 0, bottom: 5 }}
|
||||
>
|
||||
{chartConfig.show_grid !== false && (
|
||||
<CartesianGrid strokeDasharray="3 3" stroke="#e5e7eb" />
|
||||
<CartesianGrid strokeDasharray="3 3" stroke="#d1d5db" />
|
||||
)}
|
||||
<XAxis
|
||||
dataKey={chartConfig.x_axis?.key || 'x'}
|
||||
tick={{ fontSize: 12 }}
|
||||
tick={{ fontSize: 12, fill: '#374151' }}
|
||||
tickLine={false}
|
||||
axisLine={{ stroke: '#e5e7eb' }}
|
||||
axisLine={{ stroke: '#d1d5db' }}
|
||||
/>
|
||||
<YAxis
|
||||
tick={{ fontSize: 12 }}
|
||||
tick={{ fontSize: 12, fill: '#374151' }}
|
||||
tickLine={false}
|
||||
axisLine={{ stroke: '#e5e7eb' }}
|
||||
axisLine={{ stroke: '#d1d5db' }}
|
||||
label={
|
||||
chartConfig.y_axis?.label
|
||||
? {
|
||||
value: chartConfig.y_axis.label,
|
||||
angle: -90,
|
||||
position: 'insideLeft',
|
||||
style: { fontSize: 12 },
|
||||
style: { fontSize: 12, fill: '#374151' },
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
|
||||
@@ -41,7 +41,7 @@ export function PieChartWidget({
|
||||
error,
|
||||
onRefresh,
|
||||
}: PieChartWidgetProps) {
|
||||
const chartConfig = config.config as PieChartConfig;
|
||||
const chartConfig = config.config as unknown as PieChartConfig;
|
||||
const chartData = data?.data || [];
|
||||
const colors = chartConfig.colors || DEFAULT_COLORS;
|
||||
const innerRadius = chartConfig.inner_radius || 0; // 0 = pie, > 0 = donut
|
||||
@@ -71,7 +71,7 @@ export function PieChartWidget({
|
||||
nameKey="name"
|
||||
label={
|
||||
chartConfig.show_labels !== false
|
||||
? ({ name, percent }) => `${name} ${(percent * 100).toFixed(0)}%`
|
||||
? ({ name, percent }) => `${name} ${((percent ?? 0) * 100).toFixed(0)}%`
|
||||
: undefined
|
||||
}
|
||||
labelLine={chartConfig.show_labels !== false}
|
||||
@@ -89,7 +89,7 @@ export function PieChartWidget({
|
||||
border: '1px solid #e5e7eb',
|
||||
borderRadius: '0.375rem',
|
||||
}}
|
||||
formatter={(value: number) => [value.toLocaleString(), 'Count']}
|
||||
formatter={(value) => [(value ?? 0).toLocaleString(), 'Count']}
|
||||
/>
|
||||
{chartConfig.show_legend !== false && (
|
||||
<Legend
|
||||
|
||||
@@ -29,14 +29,14 @@ const ICONS: Record<string, React.ComponentType<{ className?: string }>> = {
|
||||
activity: Activity,
|
||||
};
|
||||
|
||||
// Color mapping
|
||||
// Color mapping (light mode optimized for bg-gray-50 background)
|
||||
const COLORS: Record<string, string> = {
|
||||
blue: 'text-blue-600 bg-blue-100 dark:text-blue-400 dark:bg-blue-900/30',
|
||||
green: 'text-green-600 bg-green-100 dark:text-green-400 dark:bg-green-900/30',
|
||||
red: 'text-red-600 bg-red-100 dark:text-red-400 dark:bg-red-900/30',
|
||||
yellow: 'text-yellow-600 bg-yellow-100 dark:text-yellow-400 dark:bg-yellow-900/30',
|
||||
purple: 'text-purple-600 bg-purple-100 dark:text-purple-400 dark:bg-purple-900/30',
|
||||
gray: 'text-gray-600 bg-gray-100 dark:text-gray-400 dark:bg-gray-700',
|
||||
blue: 'text-blue-700 bg-blue-100',
|
||||
green: 'text-green-700 bg-green-100',
|
||||
red: 'text-red-700 bg-red-100',
|
||||
yellow: 'text-yellow-700 bg-yellow-100',
|
||||
purple: 'text-purple-700 bg-purple-100',
|
||||
gray: 'text-gray-700 bg-gray-100',
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -70,7 +70,7 @@ function formatValue(value: number | string, format?: string): string {
|
||||
* Stat card widget for displaying KPIs.
|
||||
*/
|
||||
export function StatCard({ config, data, loading, error, onRefresh }: StatCardProps) {
|
||||
const widgetConfig = config.config as StatCardConfig;
|
||||
const widgetConfig = config.config as unknown as StatCardConfig;
|
||||
const Icon = widgetConfig.icon ? ICONS[widgetConfig.icon] : Activity;
|
||||
const colorClass = widgetConfig.color ? COLORS[widgetConfig.color] : COLORS.gray;
|
||||
|
||||
@@ -82,7 +82,7 @@ export function StatCard({ config, data, loading, error, onRefresh }: StatCardPr
|
||||
<WidgetWrapper config={config} loading={loading} error={error} onRefresh={onRefresh}>
|
||||
<div className="flex items-center justify-between h-full">
|
||||
<div className="flex-1">
|
||||
<p className="text-3xl font-bold text-gray-900 dark:text-gray-100">
|
||||
<p className="text-3xl font-bold text-gray-900">
|
||||
{formatValue(value, widgetConfig.format)}
|
||||
</p>
|
||||
{trend !== undefined && (
|
||||
|
||||
@@ -23,17 +23,17 @@ export function WidgetWrapper({
|
||||
children,
|
||||
}: WidgetWrapperProps) {
|
||||
return (
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 shadow-sm h-full flex flex-col">
|
||||
<div className="bg-white rounded-lg border-2 border-gray-200 shadow-sm h-full flex flex-col">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between px-4 py-3 border-b border-gray-200 dark:border-gray-700">
|
||||
<h3 className="font-medium text-gray-900 dark:text-gray-100 text-sm">
|
||||
<div className="flex items-center justify-between px-4 py-3 border-b border-gray-200">
|
||||
<h3 className="font-semibold text-gray-900 text-sm">
|
||||
{config.title}
|
||||
</h3>
|
||||
{onRefresh && (
|
||||
<button
|
||||
onClick={onRefresh}
|
||||
disabled={loading}
|
||||
className="p-1 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 disabled:opacity-50"
|
||||
className="p-1 text-gray-400 hover:text-gray-600 disabled:opacity-50"
|
||||
title="Refresh"
|
||||
>
|
||||
<RefreshCw className={`w-4 h-4 ${loading ? 'animate-spin' : ''}`} />
|
||||
@@ -47,14 +47,14 @@ export function WidgetWrapper({
|
||||
<div className="flex items-center justify-center h-full">
|
||||
<div className="text-center">
|
||||
<AlertCircle className="w-8 h-8 text-red-500 mx-auto mb-2" />
|
||||
<p className="text-sm text-red-600 dark:text-red-400">{error}</p>
|
||||
<p className="text-sm text-red-600">{error}</p>
|
||||
</div>
|
||||
</div>
|
||||
) : loading ? (
|
||||
<div className="flex items-center justify-center h-full">
|
||||
<div className="animate-pulse flex flex-col items-center">
|
||||
<div className="h-4 w-24 bg-gray-200 dark:bg-gray-700 rounded mb-2" />
|
||||
<div className="h-3 w-16 bg-gray-200 dark:bg-gray-700 rounded" />
|
||||
<div className="h-4 w-24 bg-gray-200 rounded mb-2" />
|
||||
<div className="h-3 w-16 bg-gray-200 rounded" />
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
|
||||
Reference in New Issue
Block a user