Report Template Editor eklenmesi
This commit is contained in:
parent
4ae6ca4655
commit
5b6b7acf19
14 changed files with 1861 additions and 115 deletions
125
ui/package-lock.json
generated
125
ui/package-lock.json
generated
|
|
@ -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",
|
||||
|
|
|
|||
256
ui/src/components/reports/Dashboard.tsx
Normal file
256
ui/src/components/reports/Dashboard.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
140
ui/src/components/reports/ParameterForm.tsx
Normal file
140
ui/src/components/reports/ParameterForm.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
131
ui/src/components/reports/ReportGenerator.tsx
Normal file
131
ui/src/components/reports/ReportGenerator.tsx
Normal 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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
80
ui/src/components/reports/ReportHtmlEditor.tsx
Normal file
80
ui/src/components/reports/ReportHtmlEditor.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
475
ui/src/components/reports/ReportViewer.tsx
Normal file
475
ui/src/components/reports/ReportViewer.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
94
ui/src/components/reports/TemplateCard.tsx
Normal file
94
ui/src/components/reports/TemplateCard.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
316
ui/src/components/reports/TemplateEditor.tsx
Normal file
316
ui/src/components/reports/TemplateEditor.tsx
Normal 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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
32
ui/src/proxy/reports/models.ts
Normal file
32
ui/src/proxy/reports/models.ts
Normal 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;
|
||||
}
|
||||
125
ui/src/services/reports.service.ts
Normal file
125
ui/src/services/reports.service.ts
Normal 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
|
||||
197
ui/src/utils/hooks/useReports.ts
Normal file
197
ui/src/utils/hooks/useReports.ts
Normal 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,
|
||||
}
|
||||
}
|
||||
|
|
@ -15,7 +15,7 @@ import {
|
|||
headerOptions,
|
||||
headerValues,
|
||||
sizeValues,
|
||||
} from '@/proxy/html-editor/data'
|
||||
} from '@/proxy/reports/data'
|
||||
|
||||
interface PostManagementProps {
|
||||
posts: ForumPost[]
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue