Report Template Editor eklenmesi

This commit is contained in:
Sedat ÖZTÜRK 2025-08-15 11:07:51 +03:00
parent 4ae6ca4655
commit 5b6b7acf19
14 changed files with 1861 additions and 115 deletions

125
ui/package-lock.json generated
View file

@ -51,7 +51,6 @@
"react-highlight-words": "^0.20.0",
"react-icons": "^5.4.0",
"react-modal": "^3.16.3",
"react-quill": "^2.0.0",
"react-router-dom": "^6.14.1",
"react-select": "^5.9.0",
"redux-state-sync": "^3.1.4",
@ -3158,21 +3157,6 @@
"version": "15.7.13",
"license": "MIT"
},
"node_modules/@types/quill": {
"version": "1.3.10",
"resolved": "https://registry.npmjs.org/@types/quill/-/quill-1.3.10.tgz",
"integrity": "sha512-IhW3fPW+bkt9MLNlycw8u8fWb7oO7W5URC9MfZYHBlA24rex9rs23D5DETChu1zvgVdc5ka64ICjJOgQMr6Shw==",
"license": "MIT",
"dependencies": {
"parchment": "^1.1.2"
}
},
"node_modules/@types/quill/node_modules/parchment": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/parchment/-/parchment-1.1.4.tgz",
"integrity": "sha512-J5FBQt/pM2inLzg4hEWmzQx/8h8D0CiDxaG3vyp9rKrQRSDgBlhjdP5jQGgosEajXPSQouXGHOmVdgo7QmJuOg==",
"license": "BSD-3-Clause"
},
"node_modules/@types/raf": {
"version": "3.4.3",
"license": "MIT",
@ -4103,6 +4087,7 @@
},
"node_modules/call-bind": {
"version": "1.0.8",
"dev": true,
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.0",
@ -4282,15 +4267,6 @@
"node": ">=12"
}
},
"node_modules/clone": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz",
"integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==",
"license": "MIT",
"engines": {
"node": ">=0.8"
}
},
"node_modules/color-convert": {
"version": "2.0.1",
"license": "MIT",
@ -4800,6 +4776,7 @@
},
"node_modules/define-data-property": {
"version": "1.1.4",
"dev": true,
"license": "MIT",
"dependencies": {
"es-define-property": "^1.0.0",
@ -4824,6 +4801,7 @@
},
"node_modules/define-properties": {
"version": "1.2.1",
"dev": true,
"license": "MIT",
"dependencies": {
"define-data-property": "^1.0.1",
@ -6028,12 +6006,6 @@
"version": "1.2.2",
"license": "BSD-3-Clause"
},
"node_modules/extend": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
"integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==",
"license": "MIT"
},
"node_modules/fast-csv": {
"version": "4.3.6",
"license": "MIT",
@ -6401,6 +6373,7 @@
},
"node_modules/functions-have-names": {
"version": "1.2.3",
"dev": true,
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/ljharb"
@ -6621,6 +6594,7 @@
},
"node_modules/has-property-descriptors": {
"version": "1.0.2",
"dev": true,
"license": "MIT",
"dependencies": {
"es-define-property": "^1.0.0"
@ -6888,6 +6862,7 @@
},
"node_modules/is-arguments": {
"version": "1.1.1",
"dev": true,
"license": "MIT",
"dependencies": {
"call-bind": "^1.0.2",
@ -6979,6 +6954,7 @@
},
"node_modules/is-date-object": {
"version": "1.0.5",
"dev": true,
"license": "MIT",
"dependencies": {
"has-tostringtag": "^1.0.0"
@ -7092,6 +7068,7 @@
},
"node_modules/is-regex": {
"version": "1.1.4",
"dev": true,
"license": "MIT",
"dependencies": {
"call-bind": "^1.0.2",
@ -7839,6 +7816,7 @@
},
"node_modules/object-is": {
"version": "1.1.5",
"dev": true,
"license": "MIT",
"dependencies": {
"call-bind": "^1.0.2",
@ -7853,6 +7831,7 @@
},
"node_modules/object-keys": {
"version": "1.1.1",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
@ -9239,20 +9218,6 @@
],
"license": "MIT"
},
"node_modules/quill": {
"version": "1.3.7",
"resolved": "https://registry.npmjs.org/quill/-/quill-1.3.7.tgz",
"integrity": "sha512-hG/DVzh/TiknWtE6QmWAF/pxoZKYxfe3J/d/+ShUWkDvvkZQVTPeVmUJVu1uE6DDooC4fWTiCLh84ul89oNz5g==",
"license": "BSD-3-Clause",
"dependencies": {
"clone": "^2.1.1",
"deep-equal": "^1.0.1",
"eventemitter3": "^2.0.3",
"extend": "^3.0.2",
"parchment": "^1.1.4",
"quill-delta": "^3.6.2"
}
},
"node_modules/quill-delta": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/quill-delta/-/quill-delta-5.1.0.tgz",
@ -9267,58 +9232,6 @@
"node": ">= 12.0.0"
}
},
"node_modules/quill/node_modules/deep-equal": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.1.2.tgz",
"integrity": "sha512-5tdhKF6DbU7iIzrIOa1AOUt39ZRm13cmL1cGEh//aqR8x9+tNfbywRf0n5FD/18OKMdo7DNEtrX2t22ZAkI+eg==",
"license": "MIT",
"dependencies": {
"is-arguments": "^1.1.1",
"is-date-object": "^1.0.5",
"is-regex": "^1.1.4",
"object-is": "^1.1.5",
"object-keys": "^1.1.1",
"regexp.prototype.flags": "^1.5.1"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/quill/node_modules/eventemitter3": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-2.0.3.tgz",
"integrity": "sha512-jLN68Dx5kyFHaePoXWPsCGW5qdyZQtLYHkxkg02/Mz6g0kYpDx4FyP6XfArhQdlOC4b8Mv+EMxPo/8La7Tzghg==",
"license": "MIT"
},
"node_modules/quill/node_modules/fast-diff": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.1.2.tgz",
"integrity": "sha512-KaJUt+M9t1qaIteSvjc6P3RbMdXsNhK61GRftR6SNxqmhthcd9MGIi4T+o0jD8LUSpSnSKXE20nLtJ3fOHxQig==",
"license": "Apache-2.0"
},
"node_modules/quill/node_modules/parchment": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/parchment/-/parchment-1.1.4.tgz",
"integrity": "sha512-J5FBQt/pM2inLzg4hEWmzQx/8h8D0CiDxaG3vyp9rKrQRSDgBlhjdP5jQGgosEajXPSQouXGHOmVdgo7QmJuOg==",
"license": "BSD-3-Clause"
},
"node_modules/quill/node_modules/quill-delta": {
"version": "3.6.3",
"resolved": "https://registry.npmjs.org/quill-delta/-/quill-delta-3.6.3.tgz",
"integrity": "sha512-wdIGBlcX13tCHOXGMVnnTVFtGRLoP0imqxM696fIPwIf5ODIYUHIvHbZcyvGlZFiFhK5XzDC2lpjbxRhnM05Tg==",
"license": "MIT",
"dependencies": {
"deep-equal": "^1.0.1",
"extend": "^3.0.2",
"fast-diff": "1.1.2"
},
"engines": {
"node": ">=0.10"
}
},
"node_modules/raf": {
"version": "3.4.1",
"license": "MIT",
@ -9517,21 +9430,6 @@
"react-dom": "^0.14.0 || ^15.0.0 || ^16 || ^17 || ^18 || ^19"
}
},
"node_modules/react-quill": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/react-quill/-/react-quill-2.0.0.tgz",
"integrity": "sha512-4qQtv1FtCfLgoD3PXAur5RyxuUbPXQGOHgTlFie3jtxp43mXDtzCKaOgQ3mLyZfi1PUlyjycfivKelFhy13QUg==",
"license": "MIT",
"dependencies": {
"@types/quill": "^1.3.10",
"lodash": "^4.17.4",
"quill": "^1.3.7"
},
"peerDependencies": {
"react": "^16 || ^17 || ^18",
"react-dom": "^16 || ^17 || ^18"
}
},
"node_modules/react-refresh": {
"version": "0.14.2",
"dev": true,
@ -9729,6 +9627,7 @@
},
"node_modules/regexp.prototype.flags": {
"version": "1.5.4",
"dev": true,
"license": "MIT",
"dependencies": {
"call-bind": "^1.0.8",
@ -10745,6 +10644,7 @@
},
"node_modules/set-function-length": {
"version": "1.2.2",
"dev": true,
"license": "MIT",
"dependencies": {
"define-data-property": "^1.1.4",
@ -10760,6 +10660,7 @@
},
"node_modules/set-function-name": {
"version": "2.0.2",
"dev": true,
"license": "MIT",
"dependencies": {
"define-data-property": "^1.1.4",

View file

@ -0,0 +1,256 @@
import React, { useState, useMemo } from 'react'
import { TemplateEditor } from '../reports/TemplateEditor'
import { ReportGenerator } from '../reports/ReportGenerator'
import { TemplateCard } from './TemplateCard'
import { Button } from '../ui/Button'
import { Input } from '../ui/Input'
import { Plus, Search, Filter, FileText, BarChart3 } from 'lucide-react'
import { ReportTemplateDto } from '@/proxy/reports/models'
import { useReports } from '@/utils/hooks/useReports'
export const Dashboard: React.FC = () => {
const { templates, isLoading, createTemplate, updateTemplate, deleteTemplate, generateReport } =
useReports()
const [showEditor, setShowEditor] = useState(false)
const [showGenerator, setShowGenerator] = useState(false)
const [editingTemplate, setEditingTemplate] = useState<ReportTemplateDto | null>(null)
const [generatingTemplate, setGeneratingTemplate] = useState<ReportTemplateDto | null>(null)
const [searchQuery, setSearchQuery] = useState('')
const [selectedCategory, setSelectedCategory] = useState('Tümü')
const categories = useMemo(() => {
const cats = ['Tümü', ...new Set(templates.map((t) => t.category))]
return cats
}, [templates])
const filteredTemplates = useMemo(() => {
return templates.filter((template) => {
const matchesSearch =
template.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
template.description.toLowerCase().includes(searchQuery.toLowerCase()) ||
template.tags.some((tag: any) => tag.toLowerCase().includes(searchQuery.toLowerCase()))
const matchesCategory = selectedCategory === 'Tümü' || template.category === selectedCategory
return matchesSearch && matchesCategory
})
}, [templates, searchQuery, selectedCategory])
const handleCreateTemplate = () => {
setEditingTemplate(null)
setShowEditor(true)
}
const handleEditTemplate = (template: ReportTemplateDto) => {
setEditingTemplate(template)
setShowEditor(true)
}
const handleSaveTemplate = async (templateData: ReportTemplateDto) => {
try {
if (editingTemplate) {
await updateTemplate(editingTemplate.id, templateData)
} else {
await createTemplate(templateData)
}
setShowEditor(false)
setEditingTemplate(null)
} catch (error) {
console.error('Error saving template:', error)
// Handle error - could show toast notification
}
}
const handleDeleteTemplate = async (id: string) => {
if (window.confirm('Bu şablonu silmek istediğinizden emin misiniz?')) {
try {
await deleteTemplate(id)
} catch (error) {
console.error('Error deleting template:', error)
// Handle error - could show toast notification
}
}
}
const handleGenerateReport = (template: ReportTemplateDto) => {
setGeneratingTemplate(template)
setShowGenerator(true)
}
const handleReportGeneration = async (
templateId: string,
parameters: Record<string, string>,
): Promise<string | null> => {
try {
const report = await generateReport(templateId, parameters)
return report ? report.id : null
} catch (error) {
console.error('Error generating report:', error)
return null
}
}
return (
<div className="min-h-screen bg-gradient-to-br from-gray-50 to-gray-100">
{/* Header */}
<header className="bg-white shadow-sm border-b border-gray-200">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex items-center justify-between h-16">
<div className="flex items-center space-x-3">
<div className="bg-gradient-to-r from-blue-600 to-indigo-600 p-2 rounded-lg">
<BarChart3 className="h-6 w-6 text-white" />
</div>
<div>
<h1 className="text-xl font-bold text-gray-900">Raporlama Sistemi</h1>
<p className="text-sm text-gray-500">Dinamik rapor şablonları ve üretimi</p>
</div>
</div>
<Button onClick={handleCreateTemplate} disabled={isLoading}>
<Plus className="h-4 w-4 mr-2" />
Yeni Şablon
</Button>
</div>
</div>
</header>
{isLoading ? (
<div className="flex items-center justify-center min-h-96">
<div className="text-center">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mx-auto mb-4"></div>
<p className="text-gray-600">Yükleniyor...</p>
</div>
</div>
) : (
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
{/* Stats */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 mb-8">
<div className="bg-white rounded-xl shadow-md p-6 border border-gray-200">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600">Toplam Şablon</p>
<p className="text-2xl font-bold text-gray-900">{templates.length}</p>
</div>
<div className="bg-blue-100 p-3 rounded-full">
<FileText className="h-6 w-6 text-blue-600" />
</div>
</div>
</div>
<div className="bg-white rounded-xl shadow-md p-6 border border-gray-200">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600">Aktif Kategoriler</p>
<p className="text-2xl font-bold text-gray-900">{categories.length - 1}</p>
</div>
<div className="bg-emerald-100 p-3 rounded-full">
<Filter className="h-6 w-6 text-emerald-600" />
</div>
</div>
</div>
<div className="bg-white rounded-xl shadow-md p-6 border border-gray-200">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600">Toplam Parametre</p>
<p className="text-2xl font-bold text-gray-900">
{templates.reduce((sum, t) => sum + t.parameters.length, 0)}
</p>
</div>
<div className="bg-purple-100 p-3 rounded-full">
<BarChart3 className="h-6 w-6 text-purple-600" />
</div>
</div>
</div>
</div>
{/* Filters */}
<div className="bg-white rounded-xl shadow-md p-6 mb-8 border border-gray-200">
<div className="flex flex-col md:flex-row gap-4">
<div className="flex-1">
<div className="relative">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-gray-400" />
<Input
placeholder="Şablon ara..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="pl-10"
/>
</div>
</div>
<div className="md:w-48">
<select
value={selectedCategory}
onChange={(e) => setSelectedCategory(e.target.value)}
className="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} value={category}>
{category}
</option>
))}
</select>
</div>
</div>
</div>
{/* Templates Grid */}
{filteredTemplates.length === 0 ? (
<div className="bg-white rounded-xl shadow-md p-12 border border-gray-200">
<FileText className="h-16 w-16 text-gray-400 mx-auto mb-4" />
<h3 className="text-lg font-medium text-gray-900 mb-2">
{templates.length === 0 ? 'Henüz şablon oluşturulmamış' : 'Şablon bulunamadı'}
</h3>
<p className="text-gray-500 mb-6">
{templates.length === 0
? 'İlk rapor şablonunuzu oluşturarak başlayın.'
: 'Arama kriterlerinize uygun şablon bulunamadı.'}
</p>
{templates.length === 0 && (
<Button onClick={handleCreateTemplate}>
<Plus className="h-4 w-4 mr-2" />
İlk Şablonu Oluştur
</Button>
)}
</div>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{filteredTemplates.map((template) => (
<TemplateCard
key={template.id}
template={template}
onEdit={handleEditTemplate}
onDelete={handleDeleteTemplate}
onGenerate={handleGenerateReport}
/>
))}
</div>
)}
</main>
)}
{/* Modals */}
<TemplateEditor
isOpen={showEditor}
onClose={() => {
setShowEditor(false)
setEditingTemplate(null)
}}
onSave={handleSaveTemplate}
template={editingTemplate}
/>
<ReportGenerator
isOpen={showGenerator}
onClose={() => {
setShowGenerator(false)
setGeneratingTemplate(null)
}}
template={generatingTemplate}
onGenerate={handleReportGeneration}
/>
</div>
)
}

View file

@ -0,0 +1,140 @@
import React from 'react'
import { Plus, Trash2 } from 'lucide-react'
import { ReportParameterDto } from '@/proxy/reports/models'
import { Button, Input } from '../ui'
interface ParameterFormProps {
parameters: ReportParameterDto[]
onChange: (parameters: ReportParameterDto[]) => void
}
export const ParameterForm: React.FC<ParameterFormProps> = ({ parameters, onChange }) => {
const addParameter = () => {
const newParameter: ReportParameterDto = {
id: crypto.randomUUID(),
name: '',
placeholder: '',
type: 'text',
required: false,
description: '',
}
onChange([...parameters, newParameter])
}
const updateParameter = (id: string, updates: Partial<ReportParameterDto>) => {
onChange(parameters.map((param) => (param.id === id ? { ...param, ...updates } : param)))
}
const removeParameter = (id: string) => {
onChange(parameters.filter((param) => param.id !== id))
}
const generatePlaceholder = (name: string) => {
return `@@${name.toUpperCase()}`
}
return (
<div className="space-y-6">
<div className="flex items-center justify-between">
<h3 className="text-lg font-medium text-gray-900">Parametre Tanımları</h3>
<Button onClick={addParameter} size="sm">
<Plus className="h-4 w-4 mr-2" />
Parametre Ekle
</Button>
</div>
{parameters.length === 0 ? (
<div className="py-8 text-gray-500">
<p>Henüz parametre eklenmemiş.</p>
<p className="text-sm">Dinamik içerik için parametreler ekleyin.</p>
</div>
) : (
<div className="space-y-4">
{parameters.map((param, index) => (
<div key={param.id} className="bg-gray-50 p-4 rounded-lg border border-gray-200">
<div className="flex items-start justify-between mb-4">
<h4 className="font-medium text-gray-900">Parametre #{index + 1}</h4>
<Button variant="solid" size="sm" onClick={() => removeParameter(param.id)}>
<Trash2 className="h-4 w-4" />
</Button>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<Input
value={param.name}
onChange={(e) => {
const name = e.target.value
updateParameter(param.id, {
name,
placeholder: generatePlaceholder(name),
})
}}
placeholder="Parametre Adı Örn: SIRKETADI"
/>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Parametre Tipi
</label>
<select
value={param.type}
onChange={(e) =>
updateParameter(param.id, {
type: e.target.value as ReportParameterDto['type'],
})
}
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="text">Metin</option>
<option value="number">Sayı</option>
<option value="date">Tarih</option>
<option value="email">E-posta</option>
<option value="url">URL</option>
</select>
</div>
<Input
placeholder="Varsayılan Değer"
value={param.defaultValue || ''}
onChange={(e) => updateParameter(param.id, { defaultValue: e.target.value })}
/>
<div className="flex items-center">
<input
type="checkbox"
id={`required-${param.id}`}
checked={param.required}
onChange={(e) => updateParameter(param.id, { required: e.target.checked })}
className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded"
/>
<label htmlFor={`required-${param.id}`} className="ml-2 text-sm text-gray-700">
Zorunlu parametre
</label>
</div>
</div>
<div className="mt-4">
<Input
value={param.description || ''}
onChange={(e) => updateParameter(param.id, { description: e.target.value })}
placeholder="Açıklama"
rows={2}
textArea={true}
/>
</div>
{param.name && (
<div className="mt-3 p-2 bg-blue-50 rounded border-l-4 border-blue-400">
<p className="text-sm text-blue-700">
<strong>Kullanım:</strong>{' '}
<code className="bg-blue-100 px-1 rounded">{param.placeholder}</code>
</p>
</div>
)}
</div>
))}
</div>
)}
</div>
)
}

View file

@ -0,0 +1,131 @@
import React, { useState } from 'react'
import { Button } from '../ui/Button'
import { Input } from '../ui/Input'
import { FileText } from 'lucide-react'
import { ReportTemplateDto } from '@/proxy/reports/models'
import { Dialog } from '../ui'
interface ReportGeneratorProps {
isOpen: boolean
onClose: () => void
template: ReportTemplateDto | null
onGenerate: (templateId: string, parameters: Record<string, string>) => Promise<string | null> // Rapor ID'si döndürmek için (async)
}
export const ReportGenerator: React.FC<ReportGeneratorProps> = ({
isOpen,
onClose,
template,
onGenerate,
}) => {
const [parameterValues, setParameterValues] = useState<Record<string, string>>({})
const [isGenerating, setIsGenerating] = useState(false)
// const [showPrintPreview, setShowPrintPreview] = useState(false);
React.useEffect(() => {
if (template && isOpen) {
const initialValues: Record<string, string> = {}
template.parameters.forEach((param) => {
initialValues[param.name] = param.defaultValue || ''
})
setParameterValues(initialValues)
}
}, [template, isOpen])
const handleParameterChange = (paramName: string, value: string) => {
setParameterValues((prev) => ({
...prev,
[paramName]: value,
}))
}
const handleGenerateAndShow = async () => {
if (!template) return
setIsGenerating(true)
try {
// Rapor oluştur ve ID'yi al
const reportId = await onGenerate(template.id, parameterValues)
if (reportId) {
// Yeni sekmede rapor URL'sini aç
const reportUrl = `/admin/report/${reportId}`
window.open(reportUrl, '_blank')
onClose() // Modal'ı kapat
}
} catch (error) {
console.error('Error generating report:', error)
// Handle error - could show toast notification
} finally {
setIsGenerating(false)
}
}
if (!template) return null
const isValid = template.parameters.every(
(param) =>
!param.required || (parameterValues[param.name] && parameterValues[param.name].trim()),
)
return (
<>
<Dialog isOpen={isOpen} onClose={onClose} width="60%">
<h5 className="mb-4">{`${template.name} - Rapor Parametreleri`}</h5>
<div className="p-6 space-y-6">
<div className="bg-gray-50 p-4 rounded-lg">
<h3 className="font-medium text-gray-900 mb-2">{template.name}</h3>
<p className="text-gray-600 text-sm">{template.description}</p>
<div className="flex items-center mt-2 space-x-2">
<span className="text-xs bg-blue-100 text-blue-800 px-2 py-1 rounded">
{template.category}
</span>
{template.tags.map((tag) => (
<span key={tag} className="text-xs bg-gray-100 text-gray-800 px-2 py-1 rounded">
{tag}
</span>
))}
</div>
</div>
{template.parameters.length > 0 ? (
<div className="space-y-4">
<h4 className="font-medium text-gray-900">Parametre Değerleri</h4>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{template.parameters.map((param) => (
<Input
key={param.id}
type={param.type}
value={parameterValues[param.name] || ''}
onChange={(e) => handleParameterChange(param.name, e.target.value)}
placeholder={`${param.name} ${param.required ? '*' : ''}`}
title={param.description}
/>
))}
</div>
{template.parameters.some((p) => p.required) && (
<p className="text-sm text-gray-500">* Zorunlu alanlar</p>
)}
</div>
) : (
<div className="py-8 text-gray-500">
<FileText className="h-12 w-12 text-gray-400 mx-auto mb-4" />
<p>Bu şablon için parametre tanımlanmamış.</p>
<p className="text-sm">Direkt rapor oluşturabilirsiniz.</p>
</div>
)}
<div className="flex justify-end space-x-3">
<Button variant="solid" onClick={onClose} disabled={isGenerating}>
İptal
</Button>
<Button onClick={handleGenerateAndShow} disabled={!isValid || isGenerating}>
<FileText className="h-4 w-4 mr-2" />
{isGenerating ? 'Oluşturuluyor...' : 'Rapor Oluştur'}
</Button>
</div>
</div>
</Dialog>
</>
)
}

View file

@ -0,0 +1,80 @@
import React from 'react'
import { HtmlEditor, ImageUpload, Item, MediaResizing, Toolbar } from 'devextreme-react/html-editor'
import 'devextreme/dist/css/dx.light.css'
import {
fontFamilyOptions,
fontSizeOptions,
fontValues,
headerOptions,
headerValues,
sizeValues,
} from '@/proxy/reports/data'
interface ReportHtmlEditorProps {
value: string
onChange: (value: string) => void
placeholder?: string
height?: string
}
export const ReportHtmlEditor: React.FC<ReportHtmlEditorProps> = ({
value,
onChange,
placeholder = 'Rapor şablonunuzu buraya yazın...',
height = '100%',
}) => {
return (
<div className="flex flex-col space-y-2" style={{ height }}>
<HtmlEditor
value={value}
onValueChanged={(e) => onChange(e.value)}
height={height || '400px'}
placeholder={placeholder}
>
<MediaResizing enabled={true} />
<ImageUpload fileUploadMode="base64" />
<Toolbar multiline={true}>
<Item name="undo" />
<Item name="redo" />
<Item name="separator" />
<Item name="size" acceptedValues={sizeValues} options={fontSizeOptions} />
<Item name="font" acceptedValues={fontValues} options={fontFamilyOptions} />
<Item name="separator" />
<Item name="bold" />
<Item name="italic" />
<Item name="strike" />
<Item name="underline" />
<Item name="separator" />
<Item name="alignLeft" />
<Item name="alignCenter" />
<Item name="alignRight" />
<Item name="alignJustify" />
<Item name="separator" />
<Item name="orderedList" />
<Item name="bulletList" />
<Item name="separator" />
<Item name="header" acceptedValues={headerValues} options={headerOptions} />
<Item name="separator" />
<Item name="color" />
<Item name="background" />
<Item name="separator" />
<Item name="link" />
<Item name="image" />
<Item name="separator" />
<Item name="clear" />
<Item name="codeBlock" />
<Item name="blockquote" />
<Item name="separator" />
<Item name="insertTable" />
<Item name="deleteTable" />
<Item name="insertRowAbove" />
<Item name="insertRowBelow" />
<Item name="deleteRow" />
<Item name="insertColumnLeft" />
<Item name="insertColumnRight" />
<Item name="deleteColumn" />
</Toolbar>
</HtmlEditor>
</div>
)
}

View file

@ -0,0 +1,475 @@
import React, { useState, useEffect } from 'react'
import { useParams, useNavigate } from 'react-router-dom'
import { Button } from '../ui/Button'
import { ArrowLeft, Calendar, FileText, Download, ZoomIn, ZoomOut } from 'lucide-react'
import html2canvas from 'html2canvas'
import jsPDF from 'jspdf'
import { GeneratedReportDto, ReportTemplateDto } from '@/proxy/reports/models'
import { useReports } from '@/utils/hooks/useReports'
export const ReportViewer: React.FC = () => {
const { reportId } = useParams<{ reportId: string }>()
const navigate = useNavigate()
const [zoomLevel, setZoomLevel] = useState(100)
const [report, setReport] = useState<GeneratedReportDto | null>(null)
const [template, setTemplate] = useState<ReportTemplateDto | null>(null)
const [isLoading, setIsLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
const { getReportById, getTemplateById } = useReports()
// Asenkron veri yükleme
useEffect(() => {
const loadReportData = async () => {
if (!reportId) {
setError("Rapor ID'si bulunamadı")
setIsLoading(false)
return
}
try {
setIsLoading(true)
setError(null)
// Raporu yükle
const reportData = await getReportById(reportId)
if (!reportData) {
setError('Rapor bulunamadı')
setIsLoading(false)
return
}
setReport(reportData)
// Şablonu yükle
if (reportData.templateId) {
const templateData = await getTemplateById(reportData.templateId)
setTemplate(templateData || null)
}
} catch (err) {
console.error('Error loading report data:', err)
setError('Rapor yüklenirken bir hata oluştu')
} finally {
setIsLoading(false)
}
}
loadReportData()
}, [reportId, getReportById, getTemplateById])
// Zoom fonksiyonları
const handleZoomIn = () => {
setZoomLevel((prev) => Math.min(prev + 25, 200)) // Maksimum %200
}
const handleZoomOut = () => {
setZoomLevel((prev) => Math.max(prev - 25, 50)) // Minimum %50
}
// İçeriği sayfalara bölen fonksiyon
const splitContentIntoPages = (content: string) => {
// Basit olarak içeriği paragraf ve tablo bazında bölelim
const tempDiv = document.createElement('div')
tempDiv.innerHTML = content
const elements = Array.from(tempDiv.children)
const pages: string[] = []
let currentPage = ''
let currentPageHeight = 0
const maxPageHeight = 257 // 297mm - 40mm padding (top+bottom)
elements.forEach((element) => {
const elementHtml = element.outerHTML
// Basit yükseklik tahmini (gerçek uygulamada daha karmaşık olabilir)
let estimatedHeight = 20 // Default height
if (element.tagName === 'TABLE') {
const rows = element.querySelectorAll('tr')
estimatedHeight = rows.length * 25 // Her satır için 25mm
} else if (element.tagName.startsWith('H')) {
estimatedHeight = 15
} else if (element.tagName === 'P') {
estimatedHeight = 10
}
if (currentPageHeight + estimatedHeight > maxPageHeight && currentPage) {
pages.push(currentPage)
currentPage = elementHtml
currentPageHeight = estimatedHeight
} else {
currentPage += elementHtml
currentPageHeight += estimatedHeight
}
})
if (currentPage) {
pages.push(currentPage)
}
return pages.length > 0 ? pages : [content]
}
// Loading durumu
if (isLoading) {
return (
<div className="min-h-screen bg-gray-50 flex items-center justify-center">
<div className="text-center">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mx-auto mb-4"></div>
<h1 className="text-xl font-semibold text-gray-900 mb-2">Rapor yükleniyor...</h1>
<p className="text-gray-600">Lütfen bekleyin</p>
</div>
</div>
)
}
// Error durumu
if (error || !reportId) {
return (
<div className="min-h-screen bg-gray-50 flex items-center justify-center">
<div className="text-center">
<h1 className="text-2xl font-bold text-gray-900 mb-4">{error || 'Rapor bulunamadı'}</h1>
<Button onClick={() => navigate('/')}>
<ArrowLeft className="h-4 w-4 mr-2" />
Ana Sayfaya Dön
</Button>
</div>
</div>
)
}
if (!reportId) {
return (
<div className="min-h-screen bg-gray-50 flex items-center justify-center">
<div className="text-center">
<h1 className="text-2xl font-bold text-gray-900 mb-4">Rapor bulunamadı</h1>
<Button onClick={() => navigate('/')}>
<ArrowLeft className="h-4 w-4 mr-2" />
Ana Sayfaya Dön
</Button>
</div>
</div>
)
}
// Report yüklenmemiş ise
if (!report) {
return (
<div className="min-h-screen bg-gray-50 flex items-center justify-center">
<div className="text-center">
<h1 className="text-2xl font-bold text-gray-900 mb-4">Rapor bulunamadı</h1>
<p className="text-gray-600 mb-6">
Aradığınız rapor mevcut değil veya silinmiş olabilir.
</p>
<Button onClick={() => navigate('/')}>
<ArrowLeft className="h-4 w-4 mr-2" />
Ana Sayfaya Dön
</Button>
</div>
</div>
)
}
const handlePrint = () => {
// Yazdırma sırasında zoom seviyesini geçici olarak %100'e ayarla
const currentZoom = zoomLevel
setZoomLevel(100)
// DOM'un güncellenmesi için kısa bir gecikme
setTimeout(() => {
window.print()
// Yazdırma işlemi tamamlandıktan sonra orijinal zoom seviyesine geri dön
// Print dialog kapandıktan sonra zoom'u eski haline getir
setTimeout(() => {
setZoomLevel(currentZoom)
}, 100)
}, 100)
}
const handleDownloadPdf = async () => {
const pages = splitContentIntoPages(report.generatedContent)
try {
const pdf = new jsPDF({
orientation: 'portrait',
unit: 'mm',
format: 'a4',
})
for (let i = 0; i < pages.length; i++) {
const elementId = i === 0 ? 'report-content' : `report-content-page-${i + 1}`
const element = document.getElementById(elementId)
if (!element) continue
const canvas = await html2canvas(element, {
scale: 2,
useCORS: true,
allowTaint: true,
backgroundColor: '#ffffff',
})
const imgData = canvas.toDataURL('image/png')
const imgWidth = 210 // A4 width in mm
const imgHeight = 297 // A4 height in mm
if (i > 0) {
pdf.addPage()
}
pdf.addImage(imgData, 'PNG', 0, 0, imgWidth, imgHeight)
}
pdf.save(
`${report.templateName}_${new Date(report.generatedAt).toLocaleDateString('tr-TR')}.pdf`,
)
} catch (error) {
console.error('PDF oluşturma hatası:', error)
alert('PDF oluşturulurken bir hata oluştu.')
}
}
return (
<div className="min-h-screen bg-gray-50">
{/* Header - Print edilmeyecek */}
<div className="print:hidden bg-white shadow-sm border-b border-gray-200 sticky top-0 z-10">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex items-center justify-between h-16">
<div className="flex items-center space-x-4">
<Button variant="solid" onClick={() => navigate('/')}>
<ArrowLeft className="h-4 w-4 mr-2" />
Geri Dön
</Button>
<div>
<h1 className="text-lg font-semibold text-gray-900">{report.templateName}</h1>
<div className="flex items-center space-x-4 text-sm text-gray-500">
<span className="flex items-center">
<Calendar className="h-4 w-4 mr-1" />
{new Date(report.generatedAt).toLocaleDateString('tr-TR', {
year: 'numeric',
month: 'long',
day: 'numeric',
hour: '2-digit',
minute: '2-digit',
})}
</span>
{template && (
<span className="flex items-center">
<FileText className="h-4 w-4 mr-1" />
{template.category}
</span>
)}
</div>
</div>
</div>
<div className="flex items-center space-x-2">
<Button variant="solid" onClick={handleZoomOut} disabled={zoomLevel <= 50} size="sm">
<ZoomOut className="h-4 w-4" />
</Button>
<span className="text-sm text-gray-600 px-2 min-w-[4rem] text-center">
{zoomLevel}%
</span>
<Button variant="solid" onClick={handleZoomIn} disabled={zoomLevel >= 200} size="sm">
<ZoomIn className="h-4 w-4" />
</Button>
<div className="w-px h-6 bg-gray-300 mx-2"></div>
<Button variant="solid" onClick={handleDownloadPdf}>
<Download className="h-4 w-4 mr-2" />
PDF İndir
</Button>
<Button onClick={handlePrint}>Yazdır</Button>
</div>
</div>
</div>
</div>
{/* Rapor İçeriği */}
<div className="flex justify-center bg-gray-200 py-8 print:bg-white print:py-0">
<div
className="space-y-8 print:space-y-0 transition-transform duration-300 ease-in-out"
style={{
transform: `scale(${zoomLevel / 100})`,
transformOrigin: 'top center',
}}
>
{splitContentIntoPages(report.generatedContent).map((pageContent, index) => (
<div
key={index}
id={index === 0 ? 'report-content' : `report-content-page-${index + 1}`}
className="bg-white shadow-lg print:shadow-none page-container"
style={{
width: '210mm',
minHeight: '297mm',
padding: '0',
boxSizing: 'border-box',
position: 'relative',
display: 'flex',
flexDirection: 'column',
}}
>
{index === 0 && (
<style>
{`
/* Sayfa kırılması için CSS */
.page-container {
page-break-after: always;
margin-bottom: 20px;
}
.page-container:last-child {
page-break-after: auto;
margin-bottom: 0;
}
/* Header ve Footer stilleri */
.page-header {
height: 15mm;
width: 100%;
display: flex;
align-items: center;
justify-content: right;
background-color: transparent;
color: #666;
font-size: 10px;
padding: 0 20mm;
box-sizing: border-box;
}
.page-footer {
height: 15mm;
width: 100%;
display: flex;
align-items: center;
justify-content: flex-end;
background-color: transparent;
color: #666;
font-size: 10px;
padding: 0 20mm;
box-sizing: border-box;
margin-top: auto;
}
.page-content-area {
flex: 1;
padding: 0 20mm;
min-height: 267mm; /* 297mm - 15mm header - 15mm footer */
box-sizing: border-box;
}
/* İçerik için sayfa kırılması kuralları */
.page-container h1, .page-container h2, .page-container h3 {
page-break-after: avoid;
}
.page-container table {
page-break-inside: avoid;
border-collapse: collapse;
width: 100%;
margin: 0;
}
.page-container table td,
.page-container table th {
border: 1px solid #ddd;
text-align: left;
vertical-align: top;
}
/* Print specific styles */
@media print {
body {
margin: 0 !important;
padding: 0 !important;
}
.page-container {
width: 210mm !important;
min-height: 297mm !important;
margin: 0 !important;
padding: 0 !important;
box-shadow: none !important;
page-break-after: always !important;
}
.page-container:last-child {
page-break-after: auto !important;
}
.page-header {
height: 15mm !important;
color: #666 !important;
-webkit-print-color-adjust: exact !important;
}
.page-footer {
height: 15mm !important;
color: #666 !important;
-webkit-print-color-adjust: exact !important;
}
.page-content-area {
padding: 0 20mm !important;
min-height: 267mm !important;
}
.print\\:hidden {
display: none !important;
}
/* Table print styles */
.page-container table {
border-collapse: collapse !important;
page-break-inside: avoid !important;
}
.page-container table th,
.page-container table td {
border: 1px solid #000 !important;
page-break-inside: avoid !important;
}
.page-container table th {
background-color: #f0f0f0 !important;
-webkit-print-color-adjust: exact !important;
color-adjust: exact !important;
}
.page-container table tr:nth-child(even) {
background-color: #f5f5f5 !important;
-webkit-print-color-adjust: exact !important;
color-adjust: exact !important;
}
}
`}
</style>
)}
{/* Sayfa Header - Tarih ve Saat */}
<div className="page-header">
{new Date().toLocaleDateString('tr-TR', {
year: 'numeric',
month: 'long',
day: 'numeric',
hour: '2-digit',
minute: '2-digit',
})}
</div>
{/* Sayfa İçeriği */}
<div className="page-content-area">
<div dangerouslySetInnerHTML={{ __html: pageContent }} />
</div>
{/* Sayfa Footer - Sayfa Numarası */}
<div className="page-footer">
Sayfa {index + 1} / {splitContentIntoPages(report.generatedContent).length}
</div>
</div>
))}
</div>
</div>
</div>
)
}

View file

@ -0,0 +1,94 @@
import React from 'react'
import { Button } from '../ui/Button'
import { FileText, Edit, Trash2, Play } from 'lucide-react'
import { ReportTemplateDto } from '@/proxy/reports/models'
interface TemplateCardProps {
template: ReportTemplateDto
onEdit: (template: ReportTemplateDto) => void
onDelete: (id: string) => void
onGenerate: (template: ReportTemplateDto) => void
}
export const TemplateCard: React.FC<TemplateCardProps> = ({
template,
onEdit,
onDelete,
onGenerate,
}) => {
const formatDate = (date: Date) => {
return new Date(date).toLocaleDateString('tr-TR')
}
return (
<div className="bg-white rounded-xl shadow-md hover:shadow-lg transition-all duration-200 border border-gray-200">
<div className="p-6">
<div className="flex items-start justify-between mb-4">
<div className="flex-1">
<h3 className="text-lg font-semibold text-gray-900 mb-2">{template.name}</h3>
<p className="text-gray-600 text-sm mb-3">{template.description}</p>
<div className="flex items-center space-x-2 mb-3">
<span className="text-xs bg-blue-100 text-blue-800 px-2 py-1 rounded-full font-medium">
{template.category}
</span>
{template.tags.slice(0, 2).map((tag) => (
<span
key={tag}
className="text-xs bg-gray-100 text-gray-600 px-2 py-1 rounded-full"
>
{tag}
</span>
))}
{template.tags.length > 2 && (
<span className="text-xs text-gray-500">+{template.tags.length - 2}</span>
)}
</div>
</div>
<div className="flex items-center space-x-2">
<FileText className="h-5 w-5 text-blue-500" />
<span className="text-sm text-gray-500">{template.parameters.length} parametre</span>
</div>
</div>
<div className="flex items-center justify-between">
<div className="text-xs text-gray-500">
<p>Güncellenme: {formatDate(template.lastModificationTime)}</p>
</div>
<div className="flex items-center space-x-2">
<Button
variant="solid"
size="sm"
onClick={() => onGenerate(template)}
className="hover:scale-105 transform transition-transform"
>
<Play className="h-4 w-4 mr-1" />
Üret
</Button>
<Button
variant="solid"
size="sm"
onClick={() => onEdit(template)}
className="hover:scale-105 transform transition-transform"
>
<Edit className="h-4 w-4" />
Düzenle
</Button>
<Button
variant="solid"
size="sm"
onClick={() => onDelete(template.id)}
className="hover:scale-105 transform transition-transform"
>
<Trash2 className="h-4 w-4" />
</Button>
</div>
</div>
</div>
</div>
)
}

View file

@ -0,0 +1,316 @@
import React, { useState, useEffect } from 'react'
import { Save, X, FileText, Code } from 'lucide-react'
import { ReportHtmlEditor } from './ReportHtmlEditor'
import { ReportParameterDto, ReportTemplateDto } from '@/proxy/reports/models'
import { Button, Input, Dialog } from '../ui'
interface TemplateEditorProps {
isOpen: boolean
onClose: () => void
onSave: (template: ReportTemplateDto) => Promise<void>
template?: ReportTemplateDto | null
}
export const TemplateEditor: React.FC<TemplateEditorProps> = ({
isOpen,
onClose,
onSave,
template,
}) => {
const [activeTab, setActiveTab] = useState<'info' | 'content'>('info')
const [formData, setFormData] = useState({
name: '',
description: '',
htmlContent: '',
category: 'Genel',
tags: [] as string[],
parameters: [] as ReportParameterDto[],
})
const [tagInput, setTagInput] = useState('')
const [isSaving, setIsSaving] = useState(false)
useEffect(() => {
if (template) {
setFormData({
name: template.name,
description: template.description,
htmlContent: template.htmlContent,
category: template.category,
tags: template.tags,
parameters: template.parameters,
})
} else {
setFormData({
name: '',
description: '',
htmlContent: '',
category: 'Genel',
tags: [],
parameters: [],
})
}
setActiveTab('info')
}, [template, isOpen])
// Otomatik parametre algılama
useEffect(() => {
const extractParameters = (htmlContent: string) => {
const paramRegex = /@@([A-Z_][A-Z0-9_]*)/g
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(),
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)
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 tabs = [
{ id: 'info', label: 'Şablon Bilgileri', icon: FileText },
{ id: 'content', label: 'HTML İçerik', icon: Code },
]
return (
<>
<Dialog isOpen={isOpen} onClose={onClose} width="60%">
<h5 className="mb-4">{template ? 'Şablon Düzenle' : 'Yeni Şablon Oluştur'}</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')}
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-6">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 h-full">
<div className="space-y-4">
<Input
value={formData.name}
onChange={(e) =>
setFormData((prev) => ({
...prev,
name: e.target.value,
}))
}
placeholder="Rapor şablonu adı"
className="text-left"
/>
<Input
value={formData.description}
onChange={(e) =>
setFormData((prev) => ({
...prev,
description: e.target.value,
}))
}
placeholder="Şablon açıklaması"
textArea={true}
rows={3}
className="text-left"
/>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Kategori
</label>
<select
value={formData.category}
onChange={(e) =>
setFormData((prev) => ({
...prev,
category: 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="Genel">Genel</option>
<option value="Finansal">Finansal</option>
<option value="İnsan Kaynakları">İnsan Kaynakları</option>
<option value="Satış">Satış</option>
<option value="Operasyonel">Operasyonel</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Etiketler
</label>
<div className="flex space-x-2 mb-2">
<input
type="text"
value={tagInput}
onChange={(e) => setTagInput(e.target.value)}
onKeyPress={(e) => e.key === 'Enter' && addTag()}
placeholder="Etiket ekle..."
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">
Ekle
</Button>
</div>
<div className="flex flex-wrap gap-2">
{formData.tags.map((tag) => (
<span
key={tag}
className="inline-flex items-center px-2 py-1 text-sm bg-blue-100 text-blue-800 rounded-full"
>
{tag}
<button
onClick={() => removeTag(tag)}
className="ml-1 text-blue-600 hover:text-blue-800"
>
<X className="h-3 w-3" />
</button>
</span>
))}
</div>
</div>
</div>
<div className="bg-gray-50 rounded-lg p-4 flex flex-col">
<h3 className="font-medium text-gray-900 mb-4 flex-shrink-0">
Algılanan Parametreler
</h3>
{formData.parameters.length === 0 ? (
<p className="text-gray-500 text-sm">
HTML içeriğinde @@PARAMETRE formatında parametreler kullandığınızda burada
görünecek.
</p>
) : (
<div className="flex-1 overflow-y-auto max-h-80">
<div className="grid grid-cols-1 gap-3 pr-2">
{formData.parameters.map((param) => (
<div
key={param.id}
className="bg-white p-4 rounded-lg border border-gray-200 shadow-sm hover:shadow-md transition-shadow"
>
<div className="flex items-start justify-between mb-2">
<div className="flex items-center space-x-2">
<div className="w-2 h-2 bg-blue-500 rounded-full"></div>
<code className="text-sm font-mono text-blue-700 bg-blue-50 px-2 py-1 rounded">
@@{param.name}
</code>
</div>
<span className="text-xs bg-gray-100 text-gray-600 px-2 py-1 rounded-full">
{param.type}
</span>
</div>
<p className="text-sm text-gray-600 text-left">{param.description}</p>
</div>
))}
</div>
</div>
)}
</div>
</div>
</div>
)}
{activeTab === 'content' && (
<div className="flex-1 flex flex-col p-6 min-h-0">
<ReportHtmlEditor
value={formData.htmlContent}
onChange={(content) => setFormData((prev) => ({ ...prev, htmlContent: content }))}
height="80%"
/>
</div>
)}
</div>
{/* Tab Footer */}
<div className="flex justify-between">
<Button variant="solid" onClick={onClose} disabled={isSaving}>
İptal
</Button>
<Button onClick={handleSave} disabled={isSaving}>
<Save className="h-4 w-4 mr-2" />
{isSaving ? 'Kaydediliyor...' : template ? 'Güncelle' : 'Kaydet'}
</Button>
</div>
</div>
</Dialog>
</>
)
}

View file

@ -0,0 +1,32 @@
export interface ReportTemplateDto {
id: string;
name: string;
description: string;
htmlContent: string;
parameters: ReportParameterDto[];
creationTime: Date;
lastModificationTime: Date;
category: string;
tags: string[];
}
export type ReportParameterType = "text" | "number" | "date" | "email" | "url";
export interface ReportParameterDto {
id: string;
name: string;
placeholder: string;
type: ReportParameterType;
defaultValue?: string;
required: boolean;
description?: string;
}
export interface GeneratedReportDto {
id: string;
templateId: string;
templateName: string;
generatedContent: string;
parameters: Record<string, string>;
generatedAt: Date;
}

View file

@ -0,0 +1,125 @@
import { GeneratedReportDto, ReportTemplateDto } from '@/proxy/reports/models'
import apiService, { Config } from './api.service'
import { PagedAndSortedResultRequestDto, PagedResultDto } from '@/proxy'
export interface ReportsData {
templates: ReportTemplateDto[]
generatedReports: GeneratedReportDto[]
}
export interface GenerateReportRequestDto {
templateId: string
parameters: Record<string, string>
}
export class ReportsService {
apiName = 'Default'
// Template operations
getTemplates = (input: PagedAndSortedResultRequestDto) =>
apiService.fetchData<PagedResultDto<ReportTemplateDto>, PagedAndSortedResultRequestDto>(
{
method: 'GET',
url: '/api/app/reports/templates',
params: {
sorting: input.sorting,
skipCount: input.skipCount,
maxResultCount: input.maxResultCount,
},
},
{ apiName: this.apiName },
)
getTemplateById = (id: string) =>
apiService.fetchData<ReportTemplateDto>(
{
method: 'GET',
url: `/api/app/reports/templates/${id}`,
},
{ apiName: this.apiName },
)
createTemplate = (input: ReportTemplateDto) =>
apiService.fetchData(
{
method: 'POST',
url: '/api/app/reports/templates',
data: input,
},
{ apiName: this.apiName },
)
updateTemplate = (id: string, input: ReportTemplateDto) =>
apiService.fetchData<ReportTemplateDto, ReportTemplateDto>(
{
method: 'PUT',
url: `/api/app/reports/templates/${id}`,
data: input as any,
},
{ apiName: this.apiName },
)
deleteTemplate = (id: string) =>
apiService.fetchData(
{
method: 'DELETE',
url: `/api/app/reports/templates/${id}`,
},
{ apiName: this.apiName },
)
// Generated Reports operations
getGeneratedReports = (input: PagedAndSortedResultRequestDto) =>
apiService.fetchData<PagedResultDto<GeneratedReportDto>, PagedAndSortedResultRequestDto>(
{
method: 'GET',
url: '/api/app/reports/generated',
params: {
sorting: input.sorting,
skipCount: input.skipCount,
maxResultCount: input.maxResultCount,
},
},
{ apiName: this.apiName },
)
getGeneratedReportById = (id: string) =>
apiService.fetchData<GeneratedReportDto>(
{
method: 'GET',
url: `/api/app/reports/generated/${id}`,
},
{ apiName: this.apiName },
)
generateReport = (input: GeneratedReportDto) =>
apiService.fetchData<GeneratedReportDto>(
{
method: 'POST',
url: '/api/app/reports/generate',
data: input as any,
},
{ apiName: this.apiName },
)
deleteGeneratedReport = (id: string) =>
apiService.fetchData(
{
method: 'DELETE',
url: `/api/app/reports/generated/${id}`,
},
{ apiName: this.apiName },
)
// Bulk operations
getAllData = () =>
apiService.fetchData<ReportsData>(
{
method: 'GET',
url: '/api/app/reports/all',
},
{ apiName: this.apiName },
)
}
export default ReportsService

View file

@ -0,0 +1,197 @@
import { GeneratedReportDto, ReportTemplateDto } from '@/proxy/reports/models'
import ReportsService from '@/services/reports.service'
import { useState, useCallback, useEffect } from 'react'
const reportsService = new ReportsService()
interface ReportData {
templates: ReportTemplateDto[]
generatedReports: GeneratedReportDto[]
}
export const useReports = () => {
const [data, setData] = useState<ReportData>({
templates: [],
generatedReports: [],
})
const [isLoading, setIsLoading] = useState(false)
useEffect(() => {
const loadData = async () => {
setIsLoading(true)
try {
await new Promise((resolve) => setTimeout(resolve, 200))
const [templatesResponse, generatedReportsResponse] = await Promise.all([
reportsService.getTemplates({
sorting: '',
skipCount: 0,
maxResultCount: 1000,
}),
reportsService.getGeneratedReports({
sorting: '',
skipCount: 0,
maxResultCount: 1000,
}),
])
setData({
templates: templatesResponse.data.items || [],
generatedReports: generatedReportsResponse.data.items || [],
})
} catch (error) {
console.error('Error loading data:', error)
// Fallback to default data
setData({
templates: [],
generatedReports: [],
})
} finally {
setIsLoading(false)
}
}
loadData()
}, [])
const createTemplate = useCallback(
async (template: ReportTemplateDto) => {
setIsLoading(true)
try {
const response = await reportsService.createTemplate(template as ReportTemplateDto)
const newTemplate = response.data as ReportTemplateDto
// Update local state
setData((prevData) => ({
...prevData,
templates: [...prevData.templates, newTemplate],
}))
return newTemplate
} catch (error) {
console.error('Error creating template:', error)
throw error
} finally {
setIsLoading(false)
}
},
[data],
)
const updateTemplate = useCallback(async (id: string, updates: Partial<ReportTemplateDto>) => {
setIsLoading(true)
try {
// First get the current template to merge with updates
const currentTemplateResponse = await reportsService.getTemplateById(id)
const currentTemplate = currentTemplateResponse.data as ReportTemplateDto
const updatedTemplate = { ...currentTemplate, ...updates }
await reportsService.updateTemplate(id, updatedTemplate)
// Update local state
setData((prevData) => ({
...prevData,
templates: prevData.templates.map((template) =>
template.id === id
? { ...template, ...updates, lastModificationTime: new Date() }
: template,
),
}))
} catch (error) {
console.error('Error updating template:', error)
throw error
} finally {
setIsLoading(false)
}
}, [])
const deleteTemplate = useCallback(async (id: string) => {
setIsLoading(true)
try {
await reportsService.deleteTemplate(id)
// Update local state
setData((prevData) => ({
...prevData,
templates: prevData.templates.filter((template) => template.id !== id),
}))
} catch (error) {
console.error('Error deleting template:', error)
throw error
} finally {
setIsLoading(false)
}
}, [])
const generateReport = useCallback(
async (templateId: string, parameterValues: Record<string, string>) => {
setIsLoading(true)
try {
const reportData: GeneratedReportDto = {
templateId,
parameters: parameterValues,
} as GeneratedReportDto
const response = await reportsService.generateReport(reportData)
const report = response.data as GeneratedReportDto
if (report) {
// Update local state
setData((prevData) => ({
...prevData,
generatedReports: [...prevData.generatedReports, report],
}))
}
return report
} catch (error) {
console.error('Error generating report:', error)
throw error
} finally {
setIsLoading(false)
}
},
[],
)
const getReportById = useCallback(
async (reportId: string) => {
try {
const response = await reportsService.getGeneratedReportById(reportId)
return response.data as GeneratedReportDto
} catch (error) {
console.error('Error getting report by id:', error)
// Fallback to local data
return data.generatedReports.find((report) => report.id === reportId)
}
},
[data.generatedReports],
)
const getTemplateById = useCallback(
async (templateId: string) => {
try {
const response = await reportsService.getTemplateById(templateId)
return response.data as ReportTemplateDto
} catch (error) {
console.error('Error getting template by id:', error)
// Fallback to local data
return data.templates.find((template) => template.id === templateId)
}
},
[data.templates],
)
return {
templates: data.templates,
generatedReports: data.generatedReports,
isLoading,
setIsLoading,
createTemplate,
updateTemplate,
deleteTemplate,
generateReport,
getReportById,
getTemplateById,
}
}

View file

@ -15,7 +15,7 @@ import {
headerOptions,
headerValues,
sizeValues,
} from '@/proxy/html-editor/data'
} from '@/proxy/reports/data'
interface PostManagementProps {
posts: ForumPost[]

View file

@ -1,4 +1,3 @@
import React from 'react'
import { X } from 'lucide-react'
import { Formik, Form, Field, FieldProps } from 'formik'
import * as Yup from 'yup'
@ -13,7 +12,7 @@ import {
headerOptions,
headerValues,
sizeValues,
} from '@/proxy/html-editor/data'
} from '@/proxy/reports/data'
interface CreatePostModalProps {
onClose: () => void