erp-platform/ui/src/components/reports/TemplateEditor.tsx
2025-10-31 14:56:09 +03:00

437 lines
18 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 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: '',
categoryId: '',
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,
categoryId: template.categoryId!,
tags: template.tags,
parameters: template.parameters,
})
} else {
setFormData({
name: '',
description: '',
htmlContent: '',
categoryId: '',
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(),
templateId: 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
}
console.log('FormData before save:', formData)
console.log('Categories available:', categories)
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.categoryId}
onChange={(e) => {
console.log('Category selected:', e.target.value)
setFormData((prev) => ({
...prev,
categoryId: 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"
>
<option value="">
{translate('::App.Reports.TemplateEditor.Placeholder.SelectCategory')}
</option>
{categories.map((category) => (
<option key={category.id} value={category.id}>
{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>
</>
)
}