import { useState, useEffect } from "react"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from "@/components/ui/dialog"; import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage, } from "@/components/ui/form"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { Accordion, AccordionContent, AccordionItem, AccordionTrigger, } from "@/components/ui/accordion"; import { Input } from "@/components/ui/input"; import { Button } from "@/components/ui/button"; import { Switch } from "@/components/ui/switch"; import { Slider } from "@/components/ui/slider"; import { Checkbox } from "@/components/ui/checkbox"; import { ColorPicker } from "@/components/ui/color-picker"; import { useForm } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; import * as z from "zod"; import { X, Plus, Palette } from "lucide-react"; const chartTypeOptions = [ { value: "bar", label: "Bar Chart" }, { value: "line", label: "Line Chart" }, { value: "area", label: "Area Chart" }, { value: "pie", label: "Pie Chart" }, { value: "table", label: "Table" }, { value: "number", label: "Number (Sum)" }, ]; const widgetSchema = z.object({ id: z.string().optional(), title: z.string().min(1, "Title is required"), type: z.enum(["bar", "line", "area", "pie", "table", "number"]), dataSource: z.string().min(1, "Data source is required"), config: z.object({ xAxis: z.string().optional(), yAxis: z.array(z.string()).optional(), dimensions: z.array(z.string()).optional(), metrics: z.array(z.string()).optional(), showLegend: z.boolean().default(true), stacked: z.boolean().default(false), precision: z.number().min(0).max(10).default(2), // New chart options colors: z.array(z.string()).optional(), showGrid: z.boolean().default(true), showTooltip: z.boolean().default(true), enableAnimation: z.boolean().default(true), valueFormatter: z.enum(["none", "number", "percent", "currency"]).default("none"), currencySymbol: z.string().default("$"), minValue: z.number().nullable().default(null), maxValue: z.number().nullable().default(null), }), }); export type WidgetFormValues = z.infer; interface WidgetEditorProps { isOpen: boolean; onClose: () => void; onSave: (values: WidgetFormValues) => void; editWidget?: WidgetFormValues; availableFields: Array<{ name: string; type: string }>; availableDataSources: Array<{ id: string; name: string }>; } export function WidgetEditor({ isOpen, onClose, onSave, editWidget, availableFields, availableDataSources }: WidgetEditorProps) { const [selectedFields, setSelectedFields] = useState([]); const form = useForm({ resolver: zodResolver(widgetSchema), defaultValues: { id: editWidget?.id || undefined, title: editWidget?.title || "", type: editWidget?.type || "table", dataSource: editWidget?.dataSource || "", config: { xAxis: editWidget?.config?.xAxis || "", yAxis: editWidget?.config?.yAxis || [], dimensions: editWidget?.config?.dimensions || [], metrics: editWidget?.config?.metrics || [], showLegend: editWidget?.config?.showLegend ?? true, stacked: editWidget?.config?.stacked ?? false, precision: editWidget?.config?.precision ?? 2, // New chart options with defaults colors: editWidget?.config?.colors || [], showGrid: editWidget?.config?.showGrid ?? true, showTooltip: editWidget?.config?.showTooltip ?? true, enableAnimation: editWidget?.config?.enableAnimation ?? true, valueFormatter: editWidget?.config?.valueFormatter || "none", currencySymbol: editWidget?.config?.currencySymbol || "$", minValue: editWidget?.config?.minValue ?? null, maxValue: editWidget?.config?.maxValue ?? null, }, }, }); // Reset form when edit widget changes useEffect(() => { if (editWidget) { form.reset({ id: editWidget.id, title: editWidget.title, type: editWidget.type, dataSource: editWidget.dataSource, config: { xAxis: editWidget.config.xAxis || "", yAxis: editWidget.config.yAxis || [], dimensions: editWidget.config.dimensions || [], metrics: editWidget.config.metrics || [], showLegend: editWidget.config.showLegend ?? true, stacked: editWidget.config.stacked ?? false, precision: editWidget.config.precision ?? 2, // Add new chart options with defaults if not provided colors: editWidget.config.colors || [], showGrid: editWidget.config.showGrid ?? true, showTooltip: editWidget.config.showTooltip ?? true, enableAnimation: editWidget.config.enableAnimation ?? true, valueFormatter: editWidget.config.valueFormatter || "none", currencySymbol: editWidget.config.currencySymbol || "$", minValue: editWidget.config.minValue ?? null, maxValue: editWidget.config.maxValue ?? null, }, }); // Also update selected fields const fields = [ ...(editWidget.config.dimensions || []), ...(editWidget.config.metrics || []), ]; setSelectedFields(fields); } else { form.reset({ id: undefined, title: "", type: "table", dataSource: "", config: { xAxis: "", yAxis: [], dimensions: [], metrics: [], showLegend: true, stacked: false, precision: 2, // New chart options with defaults colors: [], showGrid: true, showTooltip: true, enableAnimation: true, valueFormatter: "none", currencySymbol: "$", minValue: null, maxValue: null, }, }); setSelectedFields([]); } }, [editWidget, form]); const watchType = form.watch("type"); const watchDataSource = form.watch("dataSource"); const handleSubmit = (values: WidgetFormValues) => { onSave(values); onClose(); }; const addField = (field: string) => { if (!selectedFields.includes(field)) { setSelectedFields([...selectedFields, field]); // Automatically categorize the field as dimension or metric based on its type const fieldInfo = availableFields.find(f => f.name === field); if (fieldInfo) { // Numbers and dates are typically metrics, other types are dimensions const isMetric = ['number', 'integer', 'float', 'date', 'datetime'].includes(fieldInfo.type.toLowerCase()); if (isMetric) { const currentMetrics = form.getValues('config.metrics') || []; form.setValue('config.metrics', [...currentMetrics, field]); } else { const currentDimensions = form.getValues('config.dimensions') || []; form.setValue('config.dimensions', [...currentDimensions, field]); // If no X-axis is set and this is a dimension, use it as X-axis if (!form.getValues('config.xAxis')) { form.setValue('config.xAxis', field); } } } } }; const removeField = (field: string) => { setSelectedFields(selectedFields.filter(f => f !== field)); // Also remove from config const currentDimensions = form.getValues('config.dimensions') || []; const currentMetrics = form.getValues('config.metrics') || []; const currentYAxis = form.getValues('config.yAxis') || []; form.setValue('config.dimensions', currentDimensions.filter(d => d !== field)); form.setValue('config.metrics', currentMetrics.filter(m => m !== field)); form.setValue('config.yAxis', currentYAxis.filter(y => y !== field)); // If this was the X-axis, clear it if (form.getValues('config.xAxis') === field) { form.setValue('config.xAxis', ''); } }; const isFieldDimension = (field: string) => { const dimensions = form.getValues('config.dimensions') || []; return dimensions.includes(field); }; const toggleFieldType = (field: string) => { const dimensions = form.getValues('config.dimensions') || []; const metrics = form.getValues('config.metrics') || []; if (dimensions.includes(field)) { // Move from dimension to metric form.setValue('config.dimensions', dimensions.filter(d => d !== field)); form.setValue('config.metrics', [...metrics, field]); // If this was the X-axis, clear it if (form.getValues('config.xAxis') === field) { form.setValue('config.xAxis', ''); } } else if (metrics.includes(field)) { // Move from metric to dimension form.setValue('config.metrics', metrics.filter(m => m !== field)); form.setValue('config.dimensions', [...dimensions, field]); } }; const setAsXAxis = (field: string) => { form.setValue('config.xAxis', field); }; const toggleAsYAxis = (field: string) => { const currentYAxis = form.getValues('config.yAxis') || []; if (currentYAxis.includes(field)) { form.setValue('config.yAxis', currentYAxis.filter(y => y !== field)); } else { form.setValue('config.yAxis', [...currentYAxis, field]); } }; return ( !open && onClose()}> {editWidget ? "Edit Widget" : "Add New Widget"} Configure your dashboard widget. Select data source, chart type, and fields to display.
( Widget Title )} /> ( Chart Type )} /> ( Data Source Select the data source for this widget )} />
{watchDataSource && (

Available Fields

{availableFields .filter(field => !selectedFields.includes(field.name)) .map(field => (
addField(field.name)} > {field.name} {field.type}
))}

Selected Fields

{selectedFields.length === 0 ? (
No fields selected
) : ( selectedFields.map(field => (
{field}
)) )}
Chart Options
{/* Data Configuration section */} {["bar", "line", "area"].includes(watchType) && (

Data Configuration

( X-Axis Field Choose a dimension field for the X-Axis )} />
Y-Axis Fields
{(form.getValues('config.metrics') || []).map((metric) => (
{ if (checked) { toggleAsYAxis(metric); } else { toggleAsYAxis(metric); } }} id={`y-axis-${metric}`} />
))}
Select metric fields to display on the Y-Axis
(
Stacked Chart Stack the values on top of each other
)} />
{/* Display Options section */}

Display Options

{/* Show/Hide Grid Lines */} (
Show Grid Lines Display grid lines in the chart background
)} /> {/* Show/Hide Tooltip */} (
Show Tooltip Display tooltip when hovering over data points
)} /> {/* Enable Animation */} (
Enable Animation Animate chart when loading data
)} /> {/* Value Formatter */} ( Value Format Format for displaying values in the chart )} /> {/* Currency Symbol (only if valueFormatter is 'currency') */} {form.watch('config.valueFormatter') === 'currency' && ( ( Currency Symbol Symbol to display before values (e.g., $, €, £) )} /> )}
{/* Axis Limits */}

Axis Limits

( Min Value field.onChange(e.target.value === '' ? null : Number(e.target.value))} /> Minimum value for Y-axis (optional) )} /> ( Max Value field.onChange(e.target.value === '' ? null : Number(e.target.value))} /> Maximum value for Y-axis (optional) )} />
{/* Visualization Options */}

Visualization Options

(
Show Grid Lines Display grid lines on the chart
)} /> (
Show Tooltips Display tooltips on hover
)} /> (
Enable Animations Animate chart transitions
)} /> ( Value Format How to format the values )} /> {form.watch("config.valueFormatter") === "currency" && ( ( Currency Symbol )} /> )}
{/* Color Customization */}

Color Customization

{/* Colors for each data series */} {(form.getValues('config.yAxis') || []).length > 0 && (
Data Series Colors
{(form.getValues('config.yAxis') || []).map((metric, index) => { // Get current colors array or initialize if empty const colorsArray = form.getValues('config.colors') || []; return (
{ const newColors = [...colorsArray]; newColors[index] = color; form.setValue('config.colors', newColors); }} /> {metric}
); })}
Customize colors for each data series
)}
)} {watchType === "pie" && (
( Label Field Choose a field for the pie chart labels )} /> ( Value Field Choose a field for the pie chart values )} />
)} {watchType === "number" && (
( Metric to Sum Choose a field to sum )} /> ( Decimal Precision: {field.value} field.onChange(values[0])} /> Number of decimal places to display )} />
)} {["bar", "line", "area", "pie"].includes(watchType) && ( (
Show Legend Display a legend for the chart
)} /> )}
)}
); }