sozsoft-platform/ui/src/views/list/ChartDrawer.tsx
2026-06-18 17:03:22 +03:00

385 lines
17 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { Button, FormContainer, Input, Notification, Select, toast } from '@/components/ui'
import { Field, FieldArray, Form, Formik, FieldProps } from 'formik'
import { FaMinus, FaPlus, FaTimes, FaSave } from 'react-icons/fa'
import { SelectBoxOption } from '@/types/shared'
import {
chartSeriesTypeOptions,
columnSummaryTypeListOptions,
} from '../admin/listForm/edit/options'
import { ChartSeriesDto } from '@/proxy/admin/charts/models'
import { useLocalization } from '@/utils/hooks/useLocalization'
import { useStoreState } from '@/store/store'
import { object, array, string } from 'yup'
import { SummaryTypeEnum } from '@/proxy/form/models'
import { useState, useEffect } from 'react'
interface ChartDrawerProps {
open: boolean
onClose: () => void
initialSeries: ChartSeriesDto[]
fieldList: SelectBoxOption[]
onSave: (series: ChartSeriesDto[]) => void
onPreviewChange: (series: ChartSeriesDto[]) => void
}
const ChartDrawer = ({
open,
onClose,
initialSeries,
fieldList,
onSave,
onPreviewChange,
}: ChartDrawerProps) => {
const { translate } = useLocalization()
const { userName } = useStoreState((s) => s.auth.user)
const [selectedSeriesIndex, setSelectedSeriesIndex] = useState<number>(-1)
const schema = object().shape({
series: array().of(
object().shape({
name: string().required(),
argumentField: string().required(),
valueField: string().required(),
summaryType: string().required(),
}),
),
})
const newSeriesValue = () => {
return {
index: -1,
type: 'line',
name: '',
argumentField: '',
valueField: '',
summaryType: SummaryTypeEnum.Sum,
axis: '',
barOverlapGroup: '',
barPadding: 0,
barWidth: 0,
color: '',
cornerRadius: 0,
dashStyle: 'solid',
ignoreEmptyPoints: false,
pane: '',
rangeValue1Field: '',
rangeValue2Field: '',
selectionMode: 'none',
showInLegend: true,
visible: true,
width: 2,
label: {
visible: true,
backgroundColor: '#f05b41',
customizeText: '',
format: 'decimal',
font: {
color: '#FFFFFF',
family: '"Segoe UI", "Helvetica Neue", "Trebuchet MS", Verdana, sans-serif',
size: 12,
weight: 400,
},
},
userId: userName ?? '',
}
}
if (!open) return null
return (
<>
{/* Drawer */}
<div
className={`fixed right-0 top-0 h-full w-[500px] bg-white text-gray-900 shadow-2xl z-50 transform transition-transform duration-300 border-l-2 border-gray-300 dark:bg-gray-900 dark:text-gray-100 dark:border-gray-700 ${
open ? 'translate-x-0' : 'translate-x-full'
}`}
>
<Formik
enableReinitialize
initialValues={{
series: initialSeries && initialSeries.length > 0 ? initialSeries : [newSeriesValue()],
}}
validationSchema={schema}
onSubmit={async (values, { setSubmitting }) => {
try {
await onSave(values.series)
toast.push(
<Notification type="success">
{translate('::App.Platform.ChartDrawer.ChartSaved')}
</Notification>,
{
placement: 'top-end',
},
)
onClose()
} catch (error: any) {
toast.push(
<Notification type="danger">
{translate('::App.Platform.Error')}
<code>{error}</code>
</Notification>,
{ placement: 'top-end' },
)
} finally {
setSubmitting(false)
}
}}
>
{({ setFieldValue, values, isSubmitting }) => {
// Preview'ı her değişiklikte otomatik güncelle
useEffect(() => {
const validSeries = values.series.filter(
(s) => s.argumentField && s.valueField && s.summaryType,
)
if (validSeries.length > 0) {
onPreviewChange(validSeries)
}
}, [values.series])
return (
<Form className="flex flex-col h-full">
{/* Header */}
<div className="flex items-center justify-between p-4 border-b border-gray-200 bg-gray-50 dark:border-gray-700 dark:bg-gray-800">
<h2 className="text-lg font-semibold flex items-center gap-2 text-gray-900 dark:text-gray-100">
<span>📊</span>
{translate('::App.Platform.ChartDrawer.ChartSeries')}
</h2>
<Button
size="xs"
icon={<FaTimes />}
type="button"
onClick={onClose}
className="border-0"
></Button>
</div>
<FormContainer size="sm" className="flex flex-col flex-1 overflow-hidden">
{/* Add Series Button */}
<div className="p-3 border-b border-gray-200 dark:border-gray-700">
<Button
size="sm"
block
variant="solid"
type="button"
icon={<FaPlus />}
onClick={() => {
setFieldValue('series', [...values.series, newSeriesValue()])
}}
>
<div className="flex items-center justify-center gap-2">
{translate('::App.Platform.ChartDrawer.AddNewSeries')}
</div>
</Button>
</div>
{/* Scrollable Content */}
<div className="flex-1 overflow-y-auto p-3">
<FieldArray name="series">
{({ remove }) => (
<div className="space-y-4">
{values.series.map((series, index) => (
<div
key={index}
className="border border-gray-200 rounded-lg p-3 bg-gray-50 hover:bg-gray-100 transition-colors dark:border-gray-700 dark:bg-gray-800 dark:hover:bg-gray-700"
>
{/* Header */}
<div className="flex items-center justify-between mb-3">
<span className="font-semibold text-sm text-gray-900 dark:text-gray-100">
{translate('::App.Platform.ChartDrawer.Series')} #{index + 1}
</span>
<Button
type="button"
size="xs"
variant="plain"
icon={<FaMinus />}
className="border-0 text-red-500 hover:bg-red-100 dark:text-red-400 dark:hover:bg-red-900/30"
onClick={() => {
remove(index)
}}
/>
</div>
{/* Chart Type Selector */}
<div className="mb-3">
<label className="text-xs font-medium mb-1 block text-gray-700 dark:text-gray-300">
{translate('::App.Platform.ChartDrawer.ChartType')}
</label>
<Field name={`series[${index}].type`}>
{({ field, form }: FieldProps) => (
<div className="relative">
<Button
type="button"
onClick={() =>
setSelectedSeriesIndex(
selectedSeriesIndex === index ? -1 : index,
)
}
className="w-full px-3 py-2 text-left border border-gray-300 rounded bg-white hover:bg-gray-50 flex items-center gap-2 transition-colors dark:border-gray-600 dark:bg-gray-900 dark:hover:bg-gray-800"
>
<span className="text-xl">
{chartSeriesTypeOptions.find(
(t) => t.label === field.value,
)?.icon || '📊'}
</span>
<span className="text-sm font-medium">
{chartSeriesTypeOptions.find(
(t) => t.label === field.value,
)?.label || field.value}
</span>
</Button>
{selectedSeriesIndex === index && (
<div className="absolute z-50 mt-1 w-full bg-white border border-gray-200 rounded-lg shadow-xl p-2 dark:bg-gray-900 dark:border-gray-700">
<div className="grid grid-cols-2 gap-2">
{chartSeriesTypeOptions.map((chartType) => (
<Button
key={chartType.label}
type="button"
onClick={() => {
form.setFieldValue(field.name, chartType.label)
setSelectedSeriesIndex(-1)
}}
className={`p-2 rounded-lg hover:bg-blue-50 flex items-center gap-2 transition-all dark:hover:bg-blue-900/30 ${
field.value === chartType.label
? 'bg-blue-100 ring-2 ring-blue-400 dark:bg-blue-900/40 dark:ring-blue-500'
: 'bg-gray-50 dark:bg-gray-800'
}`}
>
<span className="text-xl">{chartType.icon}</span>
<span className="text-xs font-medium">
{chartType.label}
</span>
</Button>
))}
</div>
</div>
)}
</div>
)}
</Field>
</div>
{/* Name */}
<div className="mb-3">
<label className="text-xs font-medium mb-1 block text-gray-700 dark:text-gray-300">
{translate('::App.Listform.ListformField.Name')}
</label>
<Field
size="sm"
name={`series[${index}].name`}
type="text"
component={Input}
placeholder={translate('::App.Platform.ChartDrawer.SeriesName')}
/>
</div>
{/* Argument Field */}
<div className="mb-3">
<label className="text-xs font-medium mb-1 block text-gray-700 dark:text-gray-300">
{translate('::App.Platform.ChartDrawer.ArgumentField')}
</label>
<Field name={`series[${index}].argumentField`}>
{({ field, form }: FieldProps) => (
<Select
field={field}
form={form}
isClearable={true}
options={fieldList}
value={fieldList?.find(
(option) => option.value === field.value,
)}
onChange={(option) => {
form.setFieldValue(field.name, option?.value)
}}
placeholder={translate(
'::App.Platform.ChartDrawer.SelectField',
)}
/>
)}
</Field>
</div>
{/* Value Field */}
<div className="mb-3">
<label className="text-xs font-medium mb-1 block text-gray-700 dark:text-gray-300">
{translate('::App.Platform.ChartDrawer.ValueField')}
</label>
<Field name={`series[${index}].valueField`}>
{({ field, form }: FieldProps) => (
<Select
field={field}
form={form}
isClearable={true}
options={fieldList}
value={fieldList?.find(
(option) => option.value === field.value,
)}
onChange={(option) => {
form.setFieldValue(field.name, option?.value)
}}
placeholder={translate(
'::App.Platform.ChartDrawer.SelectField',
)}
/>
)}
</Field>
</div>
{/* Summary Type */}
<div className="mb-3">
<label className="text-xs font-medium mb-1 block text-gray-700 dark:text-gray-300">
{translate('::App.Platform.ChartDrawer.SummaryType')}
</label>
<Field name={`series[${index}].summaryType`}>
{({ field, form }: FieldProps) => (
<Select
field={field}
form={form}
isClearable={true}
options={columnSummaryTypeListOptions}
value={columnSummaryTypeListOptions.find(
(option) => option.value === field.value,
)}
onChange={(option) => {
form.setFieldValue(field.name, option?.value)
}}
/>
)}
</Field>
</div>
</div>
))}
</div>
)}
</FieldArray>
</div>
{/* Footer */}
<div className="p-4 border-t border-gray-200 bg-gray-50 flex gap-2 dark:border-gray-700 dark:bg-gray-800">
<Button type="button" variant="plain" onClick={onClose} className="flex-1">
{translate('::Cancel')}
</Button>
<Button
icon={<FaSave />}
variant="solid"
loading={isSubmitting}
type="submit"
className="flex-1"
>
<div className="flex items-center justify-center gap-2">
{isSubmitting
? translate('::SavingWithThreeDot')
: translate('::App.Platform.ChartDrawer.Save')}
</div>
</Button>
</div>
</FormContainer>
</Form>
)
}}
</Formik>
</div>
</>
)
}
export default ChartDrawer