erp-platform/ui/src/components/reports/TemplateEditor.tsx

438 lines
18 KiB
TypeScript
Raw Normal View History

2025-08-15 08:07:51 +00:00
import React, { useState, useEffect } from 'react'
2025-08-16 19:47:24 +00:00
import { FaSave, FaTimes, FaFileAlt, FaCode, FaCog } from 'react-icons/fa'
2025-08-15 08:07:51 +00:00
import { ReportHtmlEditor } from './ReportHtmlEditor'
2025-08-15 19:39:11 +00:00
import { ReportParameterDto, ReportTemplateDto, ReportCategoryDto } from '@/proxy/reports/models'
2025-08-15 08:07:51 +00:00
import { Button, Input, Dialog } from '../ui'
2025-08-15 19:39:11 +00:00
import { useLocalization } from '@/utils/hooks/useLocalization'
2025-08-15 08:07:51 +00:00
interface TemplateEditorProps {
isOpen: boolean
onClose: () => void
onSave: (template: ReportTemplateDto) => Promise<void>
template?: ReportTemplateDto | null
2025-08-15 19:39:11 +00:00
categories: ReportCategoryDto[]
2025-08-15 08:07:51 +00:00
}
export const TemplateEditor: React.FC<TemplateEditorProps> = ({
isOpen,
onClose,
onSave,
template,
2025-08-15 19:39:11 +00:00
categories,
2025-08-15 08:07:51 +00:00
}) => {
2025-08-15 19:39:11 +00:00
const [activeTab, setActiveTab] = useState<'info' | 'parameters' | 'content'>('info')
2025-08-15 08:07:51 +00:00
const [formData, setFormData] = useState({
name: '',
description: '',
htmlContent: '',
2025-10-08 11:31:29 +00:00
categoryId: '',
2025-08-15 08:07:51 +00:00
tags: [] as string[],
parameters: [] as ReportParameterDto[],
})
2025-08-15 19:39:11 +00:00
const { translate } = useLocalization()
2025-08-15 08:07:51 +00:00
const [tagInput, setTagInput] = useState('')
const [isSaving, setIsSaving] = useState(false)
useEffect(() => {
if (template) {
setFormData({
name: template.name,
2025-08-15 11:52:30 +00:00
description: template.description || '',
2025-08-15 08:07:51 +00:00
htmlContent: template.htmlContent,
2025-10-08 11:31:29 +00:00
categoryId: template.categoryId!,
2025-08-15 08:07:51 +00:00
tags: template.tags,
parameters: template.parameters,
})
} else {
setFormData({
name: '',
description: '',
htmlContent: '',
2025-10-08 11:31:29 +00:00
categoryId: '',
2025-08-15 08:07:51 +00:00
tags: [],
parameters: [],
})
}
setActiveTab('info')
}, [template, isOpen])
// Otomatik parametre algılama
useEffect(() => {
const extractParameters = (htmlContent: string) => {
2025-08-15 19:39:11 +00:00
const paramRegex = /@@([\p{L}0-9_]+)/gu
2025-08-15 08:07:51 +00:00
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(),
2025-10-08 11:31:29 +00:00
templateId: template?.id || '',
2025-08-15 08:07:51 +00:00
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
}
2025-10-08 11:31:29 +00:00
console.log('FormData before save:', formData)
console.log('Categories available:', categories)
2025-08-15 08:07:51 +00:00
setIsSaving(true)
try {
2025-08-15 11:52:30 +00:00
await onSave(formData as unknown as ReportTemplateDto)
2025-08-15 08:07:51 +00:00
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),
}))
}
2025-08-15 19:39:11 +00:00
const updateParameter = (paramId: string, updates: Partial<ReportParameterDto>) => {
setFormData((prev) => ({
...prev,
parameters: prev.parameters.map((param) =>
2025-08-16 19:47:24 +00:00
param.id === paramId ? { ...param, ...updates } : param,
2025-08-15 19:39:11 +00:00
),
}))
}
2025-08-15 08:07:51 +00:00
const tabs = [
2025-08-17 12:51:31 +00:00
{ 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 },
2025-08-15 08:07:51 +00:00
]
return (
<>
2025-08-15 19:39:11 +00:00
<Dialog isOpen={isOpen} onClose={onClose} width="100%">
2025-08-17 12:51:31 +00:00
<h5 className="mb-4">
{template
? translate('::App.Reports.TemplateEditor.TitleEdit')
: translate('::App.Reports.TemplateEditor.TitleNew')}
</h5>
2025-08-15 08:07:51 +00:00
<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}
2025-08-15 19:39:11 +00:00
onClick={() => setActiveTab(tab.id as 'info' | 'content' | 'parameters')}
2025-08-15 08:07:51 +00:00
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' && (
2025-08-15 19:39:11 +00:00
<div className="overflow-y-auto flex-1 p-3">
2025-08-21 13:54:01 +00:00
<div className="mx-auto">
2025-08-15 19:39:11 +00:00
<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">
2025-08-17 12:51:31 +00:00
{translate('::App.Reports.TemplateEditor.Label.Name')}
2025-08-15 19:39:11 +00:00
</label>
2025-08-15 08:07:51 +00:00
<input
2025-08-15 19:39:11 +00:00
autoFocus
value={formData.name}
onChange={(e) =>
setFormData((prev) => ({
...prev,
name: e.target.value,
}))
}
2025-08-17 12:51:31 +00:00
placeholder={translate('::App.Reports.TemplateEditor.Placeholder.Name')}
2025-08-15 19:39:11 +00:00
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"
2025-08-15 08:07:51 +00:00
/>
</div>
2025-08-15 19:39:11 +00:00
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
2025-08-17 12:51:31 +00:00
{translate('::App.Reports.TemplateEditor.Label.Category')}
2025-08-15 19:39:11 +00:00
</label>
<select
2025-10-08 11:31:29 +00:00
value={formData.categoryId}
onChange={(e) => {
console.log('Category selected:', e.target.value)
2025-08-15 19:39:11 +00:00
setFormData((prev) => ({
...prev,
2025-10-08 11:31:29 +00:00
categoryId: e.target.value,
2025-08-15 19:39:11 +00:00
}))
2025-10-08 11:31:29 +00:00
}}
2025-08-15 19:39:11 +00:00
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"
>
2025-10-08 11:31:29 +00:00
<option value="">
{translate('::App.Reports.TemplateEditor.Placeholder.SelectCategory')}
</option>
2025-08-15 19:39:11 +00:00
{categories.map((category) => (
2025-10-08 11:31:29 +00:00
<option key={category.id} value={category.id}>
2025-08-15 19:39:11 +00:00
{category.name}
</option>
))}
</select>
2025-08-15 08:07:51 +00:00
</div>
2025-08-15 19:39:11 +00:00
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
2025-08-17 12:51:31 +00:00
{translate('::App.Reports.TemplateEditor.Label.Tags')}
2025-08-15 19:39:11 +00:00
</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()}
2025-08-17 12:51:31 +00:00
placeholder={translate(
'::App.Reports.TemplateEditor.Placeholder.AddTag',
)}
2025-08-15 19:39:11 +00:00
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">
2025-08-17 12:51:31 +00:00
{translate('::App.Reports.TemplateEditor.Button.Add')}
2025-08-15 19:39:11 +00:00
</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"
2025-08-15 08:07:51 +00:00
>
2025-08-15 19:39:11 +00:00
{tag}
<button
onClick={() => removeTag(tag)}
className="ml-2 text-blue-600 hover:text-blue-800"
>
2025-08-16 19:47:24 +00:00
<FaTimes className="h-3 w-3" />
2025-08-15 19:39:11 +00:00
</button>
</span>
2025-08-15 08:07:51 +00:00
))}
</div>
</div>
2025-08-15 19:39:11 +00:00
</div>
{/* Right Column - Description */}
<div className="space-y-6">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
2025-08-17 12:51:31 +00:00
{translate('::App.Reports.TemplateEditor.Label.Description')}
2025-08-15 19:39:11 +00:00
</label>
<Input
value={formData.description}
onChange={(e) =>
setFormData((prev) => ({
...prev,
description: e.target.value,
}))
}
2025-08-17 12:51:31 +00:00
placeholder={translate(
'::App.Reports.TemplateEditor.Placeholder.Description',
)}
2025-08-15 19:39:11 +00:00
textArea={true}
rows={12}
className="text-left h-full"
/>
</div>
</div>
2025-08-15 08:07:51 +00:00
</div>
</div>
</div>
)}
{activeTab === 'content' && (
2025-08-15 19:39:11 +00:00
<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">
2025-08-16 19:47:24 +00:00
<FaCog className="h-16 w-16 text-gray-400 mx-auto mb-4" />
2025-08-17 12:51:31 +00:00
<p className="text-gray-500 text-lg mb-2">
{translate('::App.Reports.TemplateEditor.NoParameters')}
</p>
2025-08-15 19:39:11 +00:00
<p className="text-gray-400 text-sm">
2025-08-17 12:51:31 +00:00
{translate('::App.Reports.TemplateEditor.NoParametersDescription')}
2025-08-15 19:39:11 +00:00
</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}
2025-08-16 19:47:24 +00:00
onChange={(e) =>
updateParameter(param.id, { type: e.target.value as any })
}
2025-08-15 19:39:11 +00:00
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>
2025-08-16 19:47:24 +00:00
2025-08-15 19:39:11 +00:00
<div className="mb-2">
<input
type="text"
value={param.description || ''}
2025-08-16 19:47:24 +00:00
onChange={(e) =>
updateParameter(param.id, { description: e.target.value })
}
2025-08-17 12:51:31 +00:00
placeholder={translate(
'::App.Reports.TemplateEditor.Placeholder.ParameterDescription',
)}
2025-08-15 19:39:11 +00:00
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 || ''}
2025-08-16 19:47:24 +00:00
onChange={(e) =>
updateParameter(param.id, { defaultValue: e.target.value })
}
2025-08-17 12:51:31 +00:00
placeholder={translate(
'::App.Reports.TemplateEditor.Placeholder.DefaultValue',
)}
2025-08-15 19:39:11 +00:00
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}
2025-08-16 19:47:24 +00:00
onChange={(e) =>
updateParameter(param.id, { required: e.target.checked })
}
2025-08-15 19:39:11 +00:00
className="w-3 h-3 text-red-600 rounded border-gray-300 focus:ring-red-500"
/>
2025-08-17 12:51:31 +00:00
<span className="text-xs text-gray-600">
{translate('::App.Reports.TemplateEditor.Label.Required')}
</span>
2025-08-15 19:39:11 +00:00
</label>
</div>
</div>
))}
</div>
)}
</div>
2025-08-15 08:07:51 +00:00
</div>
)}
</div>
{/* Tab Footer */}
<div className="flex justify-between">
2025-08-15 19:39:11 +00:00
<Button variant="default" onClick={onClose} disabled={isSaving}>
2025-08-17 12:51:31 +00:00
{translate('::App.Reports.TemplateEditor.Button.Cancel')}
2025-08-15 08:07:51 +00:00
</Button>
2025-08-15 19:39:11 +00:00
<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"
>
2025-08-16 19:47:24 +00:00
<FaSave className="h-5 w-5" />
2025-08-17 12:51:31 +00:00
{isSaving
? translate('::App.Reports.TemplateEditor.Button.Saving')
: template
? translate('::App.Reports.TemplateEditor.Button.Update')
: translate('::App.Reports.TemplateEditor.Button.Save')}
2025-08-15 08:07:51 +00:00
</Button>
</div>
</div>
</Dialog>
</>
)
}