diff --git a/ui/package-lock.json b/ui/package-lock.json index 7eac2ad1..56182df4 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -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", diff --git a/ui/src/components/reports/Dashboard.tsx b/ui/src/components/reports/Dashboard.tsx new file mode 100644 index 00000000..05c95519 --- /dev/null +++ b/ui/src/components/reports/Dashboard.tsx @@ -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(null) + const [generatingTemplate, setGeneratingTemplate] = useState(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, + ): Promise => { + try { + const report = await generateReport(templateId, parameters) + return report ? report.id : null + } catch (error) { + console.error('Error generating report:', error) + return null + } + } + + return ( +
+ {/* Header */} +
+
+
+
+
+ +
+
+

Raporlama Sistemi

+

Dinamik rapor şablonları ve üretimi

+
+
+ + +
+
+
+ + {isLoading ? ( +
+
+
+

Yükleniyor...

+
+
+ ) : ( +
+ {/* Stats */} +
+
+
+
+

Toplam Şablon

+

{templates.length}

+
+
+ +
+
+
+ +
+
+
+

Aktif Kategoriler

+

{categories.length - 1}

+
+
+ +
+
+
+ +
+
+
+

Toplam Parametre

+

+ {templates.reduce((sum, t) => sum + t.parameters.length, 0)} +

+
+
+ +
+
+
+
+ + {/* Filters */} +
+
+
+
+ + setSearchQuery(e.target.value)} + className="pl-10" + /> +
+
+ +
+ +
+
+
+ + {/* Templates Grid */} + {filteredTemplates.length === 0 ? ( +
+ +

+ {templates.length === 0 ? 'Henüz şablon oluşturulmamış' : 'Şablon bulunamadı'} +

+

+ {templates.length === 0 + ? 'İlk rapor şablonunuzu oluşturarak başlayın.' + : 'Arama kriterlerinize uygun şablon bulunamadı.'} +

+ {templates.length === 0 && ( + + )} +
+ ) : ( +
+ {filteredTemplates.map((template) => ( + + ))} +
+ )} +
+ )} + + {/* Modals */} + { + setShowEditor(false) + setEditingTemplate(null) + }} + onSave={handleSaveTemplate} + template={editingTemplate} + /> + + { + setShowGenerator(false) + setGeneratingTemplate(null) + }} + template={generatingTemplate} + onGenerate={handleReportGeneration} + /> +
+ ) +} diff --git a/ui/src/components/reports/ParameterForm.tsx b/ui/src/components/reports/ParameterForm.tsx new file mode 100644 index 00000000..dd9f1818 --- /dev/null +++ b/ui/src/components/reports/ParameterForm.tsx @@ -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 = ({ 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) => { + 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 ( +
+
+

Parametre Tanımları

+ +
+ + {parameters.length === 0 ? ( +
+

Henüz parametre eklenmemiş.

+

Dinamik içerik için parametreler ekleyin.

+
+ ) : ( +
+ {parameters.map((param, index) => ( +
+
+

Parametre #{index + 1}

+ +
+ +
+ { + const name = e.target.value + updateParameter(param.id, { + name, + placeholder: generatePlaceholder(name), + }) + }} + placeholder="Parametre Adı Örn: SIRKETADI" + /> + +
+ + +
+ + updateParameter(param.id, { defaultValue: e.target.value })} + /> + +
+ updateParameter(param.id, { required: e.target.checked })} + className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded" + /> + +
+
+ +
+ updateParameter(param.id, { description: e.target.value })} + placeholder="Açıklama" + rows={2} + textArea={true} + /> +
+ + {param.name && ( +
+

+ Kullanım:{' '} + {param.placeholder} +

+
+ )} +
+ ))} +
+ )} +
+ ) +} diff --git a/ui/src/components/reports/ReportGenerator.tsx b/ui/src/components/reports/ReportGenerator.tsx new file mode 100644 index 00000000..b36d4215 --- /dev/null +++ b/ui/src/components/reports/ReportGenerator.tsx @@ -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) => Promise // Rapor ID'si döndürmek için (async) +} + +export const ReportGenerator: React.FC = ({ + isOpen, + onClose, + template, + onGenerate, +}) => { + const [parameterValues, setParameterValues] = useState>({}) + const [isGenerating, setIsGenerating] = useState(false) + // const [showPrintPreview, setShowPrintPreview] = useState(false); + + React.useEffect(() => { + if (template && isOpen) { + const initialValues: Record = {} + 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 ( + <> + +
{`${template.name} - Rapor Parametreleri`}
+
+
+

{template.name}

+

{template.description}

+
+ + {template.category} + + {template.tags.map((tag) => ( + + {tag} + + ))} +
+
+ + {template.parameters.length > 0 ? ( +
+

Parametre Değerleri

+
+ {template.parameters.map((param) => ( + handleParameterChange(param.name, e.target.value)} + placeholder={`${param.name} ${param.required ? '*' : ''}`} + title={param.description} + /> + ))} +
+ {template.parameters.some((p) => p.required) && ( +

* Zorunlu alanlar

+ )} +
+ ) : ( +
+ +

Bu şablon için parametre tanımlanmamış.

+

Direkt rapor oluşturabilirsiniz.

+
+ )} + +
+ + +
+
+
+ + ) +} diff --git a/ui/src/components/reports/ReportHtmlEditor.tsx b/ui/src/components/reports/ReportHtmlEditor.tsx new file mode 100644 index 00000000..90a53b14 --- /dev/null +++ b/ui/src/components/reports/ReportHtmlEditor.tsx @@ -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 = ({ + value, + onChange, + placeholder = 'Rapor şablonunuzu buraya yazın...', + height = '100%', +}) => { + return ( +
+ onChange(e.value)} + height={height || '400px'} + placeholder={placeholder} + > + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ ) +} diff --git a/ui/src/components/reports/ReportViewer.tsx b/ui/src/components/reports/ReportViewer.tsx new file mode 100644 index 00000000..71b58ad6 --- /dev/null +++ b/ui/src/components/reports/ReportViewer.tsx @@ -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(null) + const [template, setTemplate] = useState(null) + const [isLoading, setIsLoading] = useState(true) + const [error, setError] = useState(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 ( +
+
+
+

Rapor yükleniyor...

+

Lütfen bekleyin

+
+
+ ) + } + + // Error durumu + if (error || !reportId) { + return ( +
+
+

{error || 'Rapor bulunamadı'}

+ +
+
+ ) + } + + if (!reportId) { + return ( +
+
+

Rapor bulunamadı

+ +
+
+ ) + } + + // Report yüklenmemiş ise + if (!report) { + return ( +
+
+

Rapor bulunamadı

+

+ Aradığınız rapor mevcut değil veya silinmiş olabilir. +

+ +
+
+ ) + } + + 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 ( +
+ {/* Header - Print edilmeyecek */} +
+
+
+
+ +
+

{report.templateName}

+
+ + + {new Date(report.generatedAt).toLocaleDateString('tr-TR', { + year: 'numeric', + month: 'long', + day: 'numeric', + hour: '2-digit', + minute: '2-digit', + })} + + {template && ( + + + {template.category} + + )} +
+
+
+ +
+ + + {zoomLevel}% + + +
+ + +
+
+
+
+ + {/* Rapor İçeriği */} +
+
+ {splitContentIntoPages(report.generatedContent).map((pageContent, index) => ( +
+ {index === 0 && ( + + )} + + {/* Sayfa Header - Tarih ve Saat */} +
+ {new Date().toLocaleDateString('tr-TR', { + year: 'numeric', + month: 'long', + day: 'numeric', + hour: '2-digit', + minute: '2-digit', + })} +
+ + {/* Sayfa İçeriği */} +
+
+
+ + {/* Sayfa Footer - Sayfa Numarası */} +
+ Sayfa {index + 1} / {splitContentIntoPages(report.generatedContent).length} +
+
+ ))} +
+
+
+ ) +} diff --git a/ui/src/components/reports/TemplateCard.tsx b/ui/src/components/reports/TemplateCard.tsx new file mode 100644 index 00000000..58a13205 --- /dev/null +++ b/ui/src/components/reports/TemplateCard.tsx @@ -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 = ({ + template, + onEdit, + onDelete, + onGenerate, +}) => { + const formatDate = (date: Date) => { + return new Date(date).toLocaleDateString('tr-TR') + } + + return ( +
+
+
+
+

{template.name}

+

{template.description}

+ +
+ + {template.category} + + {template.tags.slice(0, 2).map((tag) => ( + + {tag} + + ))} + {template.tags.length > 2 && ( + +{template.tags.length - 2} + )} +
+
+ +
+ + {template.parameters.length} parametre +
+
+ +
+
+

Güncellenme: {formatDate(template.lastModificationTime)}

+
+ +
+ + + + + +
+
+
+
+ ) +} diff --git a/ui/src/components/reports/TemplateEditor.tsx b/ui/src/components/reports/TemplateEditor.tsx new file mode 100644 index 00000000..c49fc5df --- /dev/null +++ b/ui/src/components/reports/TemplateEditor.tsx @@ -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 + template?: ReportTemplateDto | null +} + +export const TemplateEditor: React.FC = ({ + 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 ( + <> + +
{template ? 'Şablon Düzenle' : 'Yeni Şablon Oluştur'}
+
+ {/* Tab Navigation */} +
+ +
+ + {/* Tab Content */} +
+ {activeTab === 'info' && ( +
+
+
+ + setFormData((prev) => ({ + ...prev, + name: e.target.value, + })) + } + placeholder="Rapor şablonu adı" + className="text-left" + /> + + + setFormData((prev) => ({ + ...prev, + description: e.target.value, + })) + } + placeholder="Şablon açıklaması" + textArea={true} + rows={3} + className="text-left" + /> + +
+ + +
+ +
+ +
+ 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" + /> + +
+
+ {formData.tags.map((tag) => ( + + {tag} + + + ))} +
+
+
+ +
+

+ Algılanan Parametreler +

+ {formData.parameters.length === 0 ? ( +

+ HTML içeriğinde @@PARAMETRE formatında parametreler kullandığınızda burada + görünecek. +

+ ) : ( +
+
+ {formData.parameters.map((param) => ( +
+
+
+
+ + @@{param.name} + +
+ + {param.type} + +
+

{param.description}

+
+ ))} +
+
+ )} +
+
+
+ )} + + {activeTab === 'content' && ( +
+ setFormData((prev) => ({ ...prev, htmlContent: content }))} + height="80%" + /> +
+ )} +
+ + {/* Tab Footer */} +
+ + +
+
+
+ + ) +} diff --git a/ui/src/proxy/html-editor/data.ts b/ui/src/proxy/reports/data.ts similarity index 100% rename from ui/src/proxy/html-editor/data.ts rename to ui/src/proxy/reports/data.ts diff --git a/ui/src/proxy/reports/models.ts b/ui/src/proxy/reports/models.ts new file mode 100644 index 00000000..e78aa1c2 --- /dev/null +++ b/ui/src/proxy/reports/models.ts @@ -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; + generatedAt: Date; +} diff --git a/ui/src/services/reports.service.ts b/ui/src/services/reports.service.ts new file mode 100644 index 00000000..922fe7d2 --- /dev/null +++ b/ui/src/services/reports.service.ts @@ -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 +} + +export class ReportsService { + apiName = 'Default' + + // Template operations + getTemplates = (input: PagedAndSortedResultRequestDto) => + apiService.fetchData, 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( + { + 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( + { + 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, 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( + { + method: 'GET', + url: `/api/app/reports/generated/${id}`, + }, + { apiName: this.apiName }, + ) + + generateReport = (input: GeneratedReportDto) => + apiService.fetchData( + { + 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( + { + method: 'GET', + url: '/api/app/reports/all', + }, + { apiName: this.apiName }, + ) +} + +export default ReportsService diff --git a/ui/src/utils/hooks/useReports.ts b/ui/src/utils/hooks/useReports.ts new file mode 100644 index 00000000..a785f898 --- /dev/null +++ b/ui/src/utils/hooks/useReports.ts @@ -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({ + 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) => { + 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) => { + 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, + } +} diff --git a/ui/src/views/forum/admin/PostManagement.tsx b/ui/src/views/forum/admin/PostManagement.tsx index 94fffa52..064dba17 100644 --- a/ui/src/views/forum/admin/PostManagement.tsx +++ b/ui/src/views/forum/admin/PostManagement.tsx @@ -15,7 +15,7 @@ import { headerOptions, headerValues, sizeValues, -} from '@/proxy/html-editor/data' +} from '@/proxy/reports/data' interface PostManagementProps { posts: ForumPost[] diff --git a/ui/src/views/forum/forum/CreatePostModal.tsx b/ui/src/views/forum/forum/CreatePostModal.tsx index c92ea8f9..7483a36f 100644 --- a/ui/src/views/forum/forum/CreatePostModal.tsx +++ b/ui/src/views/forum/forum/CreatePostModal.tsx @@ -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