430 lines
17 KiB
TypeScript
430 lines
17 KiB
TypeScript
import React, { useState, useEffect } from 'react'
|
||
import { FaSave, FaTimes, FaFileAlt, FaCode, FaCog } from 'react-icons/fa'
|
||
import { ReportHtmlEditor } from './ReportHtmlEditor'
|
||
import { ReportParameterDto, ReportTemplateDto, ReportCategoryDto } from '@/proxy/reports/models'
|
||
import { Button, Input, Dialog } from '../ui'
|
||
import { useLocalization } from '@/utils/hooks/useLocalization'
|
||
|
||
interface TemplateEditorProps {
|
||
isOpen: boolean
|
||
onClose: () => void
|
||
onSave: (template: ReportTemplateDto) => Promise<void>
|
||
template?: ReportTemplateDto | null
|
||
categories: ReportCategoryDto[]
|
||
}
|
||
|
||
export const TemplateEditor: React.FC<TemplateEditorProps> = ({
|
||
isOpen,
|
||
onClose,
|
||
onSave,
|
||
template,
|
||
categories,
|
||
}) => {
|
||
const [activeTab, setActiveTab] = useState<'info' | 'parameters' | 'content'>('info')
|
||
const [formData, setFormData] = useState({
|
||
name: '',
|
||
description: '',
|
||
htmlContent: '',
|
||
categoryName: 'Genel Raporlar',
|
||
tags: [] as string[],
|
||
parameters: [] as ReportParameterDto[],
|
||
})
|
||
|
||
const { translate } = useLocalization()
|
||
const [tagInput, setTagInput] = useState('')
|
||
const [isSaving, setIsSaving] = useState(false)
|
||
|
||
useEffect(() => {
|
||
if (template) {
|
||
setFormData({
|
||
name: template.name,
|
||
description: template.description || '',
|
||
htmlContent: template.htmlContent,
|
||
categoryName: template.categoryName!,
|
||
tags: template.tags,
|
||
parameters: template.parameters,
|
||
})
|
||
} else {
|
||
setFormData({
|
||
name: '',
|
||
description: '',
|
||
htmlContent: '',
|
||
categoryName: 'Genel Raporlar',
|
||
tags: [],
|
||
parameters: [],
|
||
})
|
||
}
|
||
setActiveTab('info')
|
||
}, [template, isOpen])
|
||
|
||
// Otomatik parametre algılama
|
||
useEffect(() => {
|
||
const extractParameters = (htmlContent: string) => {
|
||
const paramRegex = /@@([\p{L}0-9_]+)/gu
|
||
const matches = [...htmlContent.matchAll(paramRegex)]
|
||
const uniqueParams = [...new Set(matches.map((match) => match[1]))]
|
||
|
||
const newParameters: ReportParameterDto[] = uniqueParams.map((paramName) => {
|
||
// Mevcut parametreyi kontrol et
|
||
const existingParam = formData.parameters.find((p) => p.name === paramName)
|
||
if (existingParam) {
|
||
return existingParam
|
||
}
|
||
|
||
// Yeni parametre oluştur
|
||
return {
|
||
id: crypto.randomUUID(),
|
||
reportTemplateId: template?.id || '',
|
||
name: paramName,
|
||
placeholder: `@@${paramName}`,
|
||
type: 'text',
|
||
required: true,
|
||
description: `${paramName} parametresi`,
|
||
}
|
||
})
|
||
|
||
setFormData((prev) => ({
|
||
...prev,
|
||
parameters: newParameters,
|
||
}))
|
||
}
|
||
|
||
if (formData.htmlContent) {
|
||
extractParameters(formData.htmlContent)
|
||
}
|
||
}, [formData.htmlContent])
|
||
|
||
const handleSave = async () => {
|
||
if (!formData.name.trim() || !formData.htmlContent.trim()) {
|
||
return
|
||
}
|
||
|
||
setIsSaving(true)
|
||
try {
|
||
await onSave(formData as unknown as ReportTemplateDto)
|
||
onClose()
|
||
} catch (error) {
|
||
console.error('Error saving template:', error)
|
||
// Handle error - could show toast notification
|
||
} finally {
|
||
setIsSaving(false)
|
||
}
|
||
}
|
||
|
||
const addTag = () => {
|
||
if (tagInput.trim() && !formData.tags.includes(tagInput.trim())) {
|
||
setFormData((prev) => ({
|
||
...prev,
|
||
tags: [...prev.tags, tagInput.trim()],
|
||
}))
|
||
setTagInput('')
|
||
}
|
||
}
|
||
|
||
const removeTag = (tagToRemove: string) => {
|
||
setFormData((prev) => ({
|
||
...prev,
|
||
tags: prev.tags.filter((tag) => tag !== tagToRemove),
|
||
}))
|
||
}
|
||
|
||
const updateParameter = (paramId: string, updates: Partial<ReportParameterDto>) => {
|
||
setFormData((prev) => ({
|
||
...prev,
|
||
parameters: prev.parameters.map((param) =>
|
||
param.id === paramId ? { ...param, ...updates } : param,
|
||
),
|
||
}))
|
||
}
|
||
|
||
const tabs = [
|
||
{ id: 'info', label: translate('::App.Reports.TemplateEditor.Tab.Info'), icon: FaFileAlt },
|
||
{
|
||
id: 'parameters',
|
||
label: translate('::App.Reports.TemplateEditor.Tab.Parameters'),
|
||
icon: FaCog,
|
||
},
|
||
{ id: 'content', label: translate('::App.Reports.TemplateEditor.Tab.Content'), icon: FaCode },
|
||
]
|
||
|
||
return (
|
||
<>
|
||
<Dialog isOpen={isOpen} onClose={onClose} width="100%">
|
||
<h5 className="mb-4">
|
||
{template
|
||
? translate('::App.Reports.TemplateEditor.TitleEdit')
|
||
: translate('::App.Reports.TemplateEditor.TitleNew')}
|
||
</h5>
|
||
<div className="flex flex-col h-full">
|
||
{/* Tab Navigation */}
|
||
<div className="border-b border-gray-200 bg-gray-50 flex-shrink-0">
|
||
<nav className="flex space-x-8 px-6">
|
||
{tabs.map((tab) => {
|
||
const Icon = tab.icon
|
||
return (
|
||
<button
|
||
key={tab.id}
|
||
onClick={() => setActiveTab(tab.id as 'info' | 'content' | 'parameters')}
|
||
className={`
|
||
flex items-center py-4 px-1 border-b-2 font-medium text-sm transition-colors
|
||
${
|
||
activeTab === tab.id
|
||
? 'border-blue-500 text-blue-600'
|
||
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
|
||
}
|
||
`}
|
||
>
|
||
<Icon className="h-4 w-4 mr-2" />
|
||
{tab.label}
|
||
</button>
|
||
)
|
||
})}
|
||
</nav>
|
||
</div>
|
||
|
||
{/* Tab Content */}
|
||
<div className="flex-1 flex flex-col min-h-0">
|
||
{activeTab === 'info' && (
|
||
<div className="overflow-y-auto flex-1 p-3">
|
||
<div className="mx-auto">
|
||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
|
||
{/* Left Column - Basic Info */}
|
||
<div className="space-y-6">
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||
{translate('::App.Reports.TemplateEditor.Label.Name')}
|
||
</label>
|
||
<input
|
||
autoFocus
|
||
value={formData.name}
|
||
onChange={(e) =>
|
||
setFormData((prev) => ({
|
||
...prev,
|
||
name: e.target.value,
|
||
}))
|
||
}
|
||
placeholder={translate('::App.Reports.TemplateEditor.Placeholder.Name')}
|
||
className="w-full flex-1 px-3 py-2 border border-gray-300 rounded-lg shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||
/>
|
||
</div>
|
||
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||
{translate('::App.Reports.TemplateEditor.Label.Category')}
|
||
</label>
|
||
<select
|
||
value={formData.categoryName}
|
||
onChange={(e) =>
|
||
setFormData((prev) => ({
|
||
...prev,
|
||
categoryName: e.target.value,
|
||
}))
|
||
}
|
||
className="block w-full px-3 py-2 border border-gray-300 rounded-lg shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||
>
|
||
{categories.map((category) => (
|
||
<option key={category.id} value={category.name}>
|
||
{category.name}
|
||
</option>
|
||
))}
|
||
</select>
|
||
</div>
|
||
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||
{translate('::App.Reports.TemplateEditor.Label.Tags')}
|
||
</label>
|
||
<div className="flex space-x-2 mb-3">
|
||
<input
|
||
type="text"
|
||
value={tagInput}
|
||
onChange={(e) => setTagInput(e.target.value)}
|
||
onKeyPress={(e) => e.key === 'Enter' && addTag()}
|
||
placeholder={translate(
|
||
'::App.Reports.TemplateEditor.Placeholder.AddTag',
|
||
)}
|
||
className="flex-1 px-3 py-2 border border-gray-300 rounded-lg shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||
/>
|
||
<Button type="button" onClick={addTag} size="sm">
|
||
{translate('::App.Reports.TemplateEditor.Button.Add')}
|
||
</Button>
|
||
</div>
|
||
<div className="flex flex-wrap gap-2">
|
||
{formData.tags.map((tag) => (
|
||
<span
|
||
key={tag}
|
||
className="inline-flex items-center px-3 py-1 text-sm bg-blue-100 text-blue-800 rounded-full"
|
||
>
|
||
{tag}
|
||
<button
|
||
onClick={() => removeTag(tag)}
|
||
className="ml-2 text-blue-600 hover:text-blue-800"
|
||
>
|
||
<FaTimes className="h-3 w-3" />
|
||
</button>
|
||
</span>
|
||
))}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Right Column - Description */}
|
||
<div className="space-y-6">
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||
{translate('::App.Reports.TemplateEditor.Label.Description')}
|
||
</label>
|
||
<Input
|
||
value={formData.description}
|
||
onChange={(e) =>
|
||
setFormData((prev) => ({
|
||
...prev,
|
||
description: e.target.value,
|
||
}))
|
||
}
|
||
placeholder={translate(
|
||
'::App.Reports.TemplateEditor.Placeholder.Description',
|
||
)}
|
||
textArea={true}
|
||
rows={12}
|
||
className="text-left h-full"
|
||
/>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{activeTab === 'content' && (
|
||
<div className="flex-1 flex flex-col min-h-0">
|
||
<div className="flex-1 p-3 min-h-0">
|
||
<ReportHtmlEditor
|
||
value={formData.htmlContent}
|
||
onChange={(content) =>
|
||
setFormData((prev) => ({ ...prev, htmlContent: content }))
|
||
}
|
||
height="50vh"
|
||
/>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{activeTab === 'parameters' && (
|
||
<div className="overflow-y-auto flex-1 p-6">
|
||
<div className="max-w-full mx-auto">
|
||
{formData.parameters.length === 0 ? (
|
||
<div className="text-center py-12">
|
||
<FaCog className="h-16 w-16 text-gray-400 mx-auto mb-4" />
|
||
<p className="text-gray-500 text-lg mb-2">
|
||
{translate('::App.Reports.TemplateEditor.NoParameters')}
|
||
</p>
|
||
<p className="text-gray-400 text-sm">
|
||
{translate('::App.Reports.TemplateEditor.NoParametersDescription')}
|
||
</p>
|
||
</div>
|
||
) : (
|
||
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
|
||
{formData.parameters.map((param) => (
|
||
<div
|
||
key={param.id}
|
||
className="bg-white p-3 rounded-lg border border-gray-200 shadow-sm hover:shadow-md transition-shadow min-w-0"
|
||
>
|
||
<div className="flex items-start justify-between mb-2 min-w-0">
|
||
<div className="flex items-center gap-1 min-w-0 flex-1">
|
||
<div className="w-2 h-2 bg-blue-500 rounded-full flex-shrink-0"></div>
|
||
<code className="text-xs font-mono text-blue-700 bg-blue-50 px-1.5 py-0.5 rounded truncate">
|
||
@@{param.name}
|
||
</code>
|
||
</div>
|
||
<select
|
||
value={param.type}
|
||
onChange={(e) =>
|
||
updateParameter(param.id, { type: e.target.value as any })
|
||
}
|
||
className="text-xs bg-gray-100 text-gray-600 px-1.5 py-0.5 rounded-full flex-shrink-0 ml-1 border-none outline-none cursor-pointer"
|
||
>
|
||
<option value="text">text</option>
|
||
<option value="number">number</option>
|
||
<option value="date">date</option>
|
||
<option value="select">select</option>
|
||
<option value="checkbox">checkbox</option>
|
||
</select>
|
||
</div>
|
||
|
||
<div className="mb-2">
|
||
<input
|
||
type="text"
|
||
value={param.description || ''}
|
||
onChange={(e) =>
|
||
updateParameter(param.id, { description: e.target.value })
|
||
}
|
||
placeholder={translate(
|
||
'::App.Reports.TemplateEditor.Placeholder.ParameterDescription',
|
||
)}
|
||
className="w-full text-xs text-gray-600 bg-transparent border-none outline-none resize-none"
|
||
/>
|
||
</div>
|
||
|
||
<div className="mb-2">
|
||
<input
|
||
type="text"
|
||
value={param.defaultValue || ''}
|
||
onChange={(e) =>
|
||
updateParameter(param.id, { defaultValue: e.target.value })
|
||
}
|
||
placeholder={translate(
|
||
'::App.Reports.TemplateEditor.Placeholder.DefaultValue',
|
||
)}
|
||
className="w-full text-xs bg-gray-50 px-1.5 py-0.5 rounded border border-gray-200 outline-none"
|
||
/>
|
||
</div>
|
||
|
||
<div className="flex items-center gap-2">
|
||
<label className="flex items-center gap-1 cursor-pointer">
|
||
<input
|
||
type="checkbox"
|
||
checked={param.required}
|
||
onChange={(e) =>
|
||
updateParameter(param.id, { required: e.target.checked })
|
||
}
|
||
className="w-3 h-3 text-red-600 rounded border-gray-300 focus:ring-red-500"
|
||
/>
|
||
<span className="text-xs text-gray-600">
|
||
{translate('::App.Reports.TemplateEditor.Label.Required')}
|
||
</span>
|
||
</label>
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
{/* Tab Footer */}
|
||
<div className="flex justify-between">
|
||
<Button variant="default" onClick={onClose} disabled={isSaving}>
|
||
{translate('::App.Reports.TemplateEditor.Button.Cancel')}
|
||
</Button>
|
||
|
||
<Button
|
||
variant="solid"
|
||
onClick={handleSave}
|
||
className="bg-blue-600 hover:bg-blue-700 font-medium px-2 py-2 rounded-lg shadow-md hover:shadow-lg transition-all duration-200 flex items-center space-x-2"
|
||
>
|
||
<FaSave className="h-5 w-5" />
|
||
{isSaving
|
||
? translate('::App.Reports.TemplateEditor.Button.Saving')
|
||
: template
|
||
? translate('::App.Reports.TemplateEditor.Button.Update')
|
||
: translate('::App.Reports.TemplateEditor.Button.Save')}
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
</Dialog>
|
||
</>
|
||
)
|
||
}
|