diff --git a/.github/dependabot.yml b/.github/dependabot.yml deleted file mode 100644 index fabb277..0000000 --- a/.github/dependabot.yml +++ /dev/null @@ -1,18 +0,0 @@ -version: 2 -updates: - - package-ecosystem: "npm" - directory: "/" - schedule: - interval: "weekly" - commit-message: - prefix: "deps" - open-pull-requests-limit: 10 - rebase-strategy: "auto" - - - package-ecosystem: "npm" - directory: "/" - schedule: - interval: "weekly" - commit-message: - prefix: "electron" - open-pull-requests-limit: 5 diff --git a/.github/workflows/codacy.yml b/.github/workflows/codacy.yml deleted file mode 100644 index 21cc0a0..0000000 --- a/.github/workflows/codacy.yml +++ /dev/null @@ -1,61 +0,0 @@ -# This workflow uses actions that are not certified by GitHub. -# They are provided by a third-party and are governed by -# separate terms of service, privacy policy, and support -# documentation. - -# This workflow checks out code, performs a Codacy security scan -# and integrates the results with the -# GitHub Advanced Security code scanning feature. For more information on -# the Codacy security scan action usage and parameters, see -# https://github.com/codacy/codacy-analysis-cli-action. -# For more information on Codacy Analysis CLI in general, see -# https://github.com/codacy/codacy-analysis-cli. - -name: Codacy Security Scan - -on: - push: - branches: [ "master" ] - pull_request: - # The branches below must be a subset of the branches above - branches: [ "master" ] - schedule: - - cron: '23 17 * * 5' - -permissions: - contents: read - -jobs: - codacy-security-scan: - permissions: - contents: read # for actions/checkout to fetch code - security-events: write # for github/codeql-action/upload-sarif to upload SARIF results - actions: read # only required for a private repository by github/codeql-action/upload-sarif to get the Action run status - name: Codacy Security Scan - runs-on: ubuntu-latest - steps: - # Checkout the repository to the GitHub Actions runner - - name: Checkout code - uses: actions/checkout@v4 - - # Execute Codacy Analysis CLI and generate a SARIF output with the security issues identified during the analysis - - name: Run Codacy Analysis CLI - uses: codacy/codacy-analysis-cli-action@d840f886c4bd4edc059706d09c6a1586111c540b - with: - # Check https://github.com/codacy/codacy-analysis-cli#project-token to get your project token from your Codacy repository - # You can also omit the token and run the tools that support default configurations - project-token: ${{ secrets.CODACY_PROJECT_TOKEN }} - verbose: true - output: results.sarif - format: sarif - # Adjust severity of non-security issues - gh-code-scanning-compat: true - # Force 0 exit code to allow SARIF file generation - # This will handover control about PR rejection to the GitHub side - max-allowed-issues: 2147483647 - - # Upload the SARIF file generated in the previous step - - name: Upload SARIF results file - uses: github/codeql-action/upload-sarif@v3 - with: - sarif_file: results.sarif diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml deleted file mode 100644 index f38ffa3..0000000 --- a/.github/workflows/codeql.yml +++ /dev/null @@ -1,100 +0,0 @@ -# For most projects, this workflow file will not need changing; you simply need -# to commit it to your repository. -# -# You may wish to alter this file to override the set of languages analyzed, -# or to provide custom queries or build logic. -# -# ******** NOTE ******** -# We have attempted to detect the languages in your repository. Please check -# the `language` matrix defined below to confirm you have the correct set of -# supported CodeQL languages. -# -name: "CodeQL Advanced" - -on: - push: - branches: [ "master" ] - pull_request: - branches: [ "master" ] - schedule: - - cron: '36 4 * * 1' - -jobs: - analyze: - name: Analyze (${{ matrix.language }}) - # Runner size impacts CodeQL analysis time. To learn more, please see: - # - https://gh.io/recommended-hardware-resources-for-running-codeql - # - https://gh.io/supported-runners-and-hardware-resources - # - https://gh.io/using-larger-runners (GitHub.com only) - # Consider using larger runners or machines with greater resources for possible analysis time improvements. - runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }} - permissions: - # required for all workflows - security-events: write - - # required to fetch internal or private CodeQL packs - packages: read - - # only required for workflows in private repositories - actions: read - contents: read - - strategy: - fail-fast: false - matrix: - include: - - language: javascript-typescript - build-mode: none - - language: python - build-mode: none - # CodeQL supports the following values keywords for 'language': 'actions', 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'swift' - # Use `c-cpp` to analyze code written in C, C++ or both - # Use 'java-kotlin' to analyze code written in Java, Kotlin or both - # Use 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both - # To learn more about changing the languages that are analyzed or customizing the build mode for your analysis, - # see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/customizing-your-advanced-setup-for-code-scanning. - # If you are analyzing a compiled language, you can modify the 'build-mode' for that language to customize how - # your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - # Add any setup steps before running the `github/codeql-action/init` action. - # This includes steps like installing compilers or runtimes (`actions/setup-node` - # or others). This is typically only required for manual builds. - # - name: Setup runtime (example) - # uses: actions/setup-example@v1 - - # Initializes the CodeQL tools for scanning. - - name: Initialize CodeQL - uses: github/codeql-action/init@v3 - with: - languages: ${{ matrix.language }} - build-mode: ${{ matrix.build-mode }} - # If you wish to specify custom queries, you can do so here or in a config file. - # By default, queries listed here will override any specified in a config file. - # Prefix the list here with "+" to use these queries and those in the config file. - - # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs - # queries: security-extended,security-and-quality - - # If the analyze step fails for one of the languages you are analyzing with - # "We were unable to automatically build your code", modify the matrix above - # to set the build mode to "manual" for that language. Then modify this step - # to build your code. - # ℹ️ Command-line programs to run using the OS shell. - # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun - - if: matrix.build-mode == 'manual' - shell: bash - run: | - echo 'If you are using a "manual" build mode for one or more of the' \ - 'languages you are analyzing, replace this with the commands to build' \ - 'your code, for example:' - echo ' make bootstrap' - echo ' make release' - exit 1 - - - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v3 - with: - category: "/language:${{matrix.language}}" diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 0000000..5e78361 --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,46 @@ +name: Deploy to GitHub Pages + +on: + push: + branches: [ master ] # Trigger the workflow on pushes to the main branch + workflow_dispatch: # Allows you to run this workflow manually from the Actions tab + +permissions: + contents: read + pages: write + id-token: write + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: 18 + cache: 'npm' + - name: Install dependencies + run: npm install + - name: Build + run: npm run build + env: + GEMINI_API_KEY: ${{ secrets.VITE_GEMINI_API_KEY }} # Use the secret here + - name: Setup Pages + uses: actions/configure-pages@v4 + - name: Upload artifact + uses: actions/upload-pages-artifact@v3 + with: + path: './dist' # The folder Vite builds to + + deploy: + needs: build + runs-on: ubuntu-latest + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 \ No newline at end of file diff --git a/.gitignore b/.gitignore index 651a94a..251ce6d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,16 +1,23 @@ -# backend/.gitignore -venv/ -__pycache__/ -*.pyc -*.pyo -.env -.flaskenv # Bazen hassas bilgi içerebilir -uploads/ -output/ -instance/ # Flask instance folder -*.sqlite3 # Eğer veritabanı kullanırsanız +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* -# Kök .gitignore (gerekirse ekler) -.vscode/ -.idea/ -*.log \ No newline at end of file +node_modules +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/App.tsx b/App.tsx new file mode 100644 index 0000000..a424e0a --- /dev/null +++ b/App.tsx @@ -0,0 +1,280 @@ + +import React, { useState, useCallback, useEffect } from 'react'; +import { Header } from './components/Header'; +import { FileUploadArea } from './components/FileUploadArea'; +import { ProcessedInvoiceCard } from './components/ProcessedInvoiceCard'; +import { CheckView } from './components/CheckView'; +import { ReviewedView } from './components/ReviewedView'; +import { ProcessedInvoice, FileProcessingStatus } from './types'; +import { uploadAndProcessInvoice, RateLimitError } from './services/apiService'; +import { createFileDataUrl, fileToBase64, base64ToBlobUrl } from './utils'; +import { Spinner } from './components/Spinner'; +import { ConfirmationModal } from './components/ConfirmationModal'; + +const App: React.FC = () => { + const [invoices, setInvoices] = useState([]); + const [view, setView] = useState<'main' | 'check' | 'reviewed'>('main'); + const [debugMode, setDebugMode] = useState(false); + const [isLoaded, setIsLoaded] = useState(false); + const [pendingAction, setPendingAction] = useState<{ action: 'delete' | 'revert', invoiceId: string } | null>(null); + + // State for queue management + const [isProcessing, setIsProcessing] = useState(false); + const [isPausedForRateLimit, setIsPausedForRateLimit] = useState(false); + + + // Load invoices from localStorage on initial render and regenerate preview URLs + useEffect(() => { + console.log("[App] Component mounting. Loading invoices from localStorage."); + try { + const savedInvoicesRaw = localStorage.getItem('invoices'); + if (savedInvoicesRaw) { + const savedInvoices: ProcessedInvoice[] = JSON.parse(savedInvoicesRaw); + console.log(`[App] Loaded ${savedInvoices.length} invoices. Regenerating preview URLs...`); + + const invoicesWithUrls = savedInvoices.map(invoice => { + if (invoice.fileContentBase64) { + try { + const newUrl = base64ToBlobUrl(invoice.fileContentBase64, invoice.fileType); + return { ...invoice, fileDataUrl: newUrl }; + } catch (urlError) { + console.error(`[App] Could not create blob URL for ${invoice.fileName}`, urlError); + return invoice; + } + } + return invoice; + }); + + setInvoices(invoicesWithUrls); + console.log("[App] Finished regenerating URLs for previews."); + } else { + console.log("[App] No invoices found in localStorage."); + } + } catch (error) { + console.error("[App] Failed to load or process invoices from localStorage", error); + } finally { + setIsLoaded(true); + } + }, []); + + // Save invoices to localStorage whenever they change + const updateAndSaveInvoices = useCallback((updater: ProcessedInvoice[] | ((prev: ProcessedInvoice[]) => ProcessedInvoice[])) => { + setInvoices(prev => { + const newInvoices = typeof updater === 'function' ? updater(prev) : updater; + try { + const storableInvoices = newInvoices.map(({ fileDataUrl, ...rest }) => rest); + localStorage.setItem('invoices', JSON.stringify(storableInvoices)); + } catch (error) { + console.error("[App] Failed to save invoices to localStorage", error); + } + if (debugMode) { + console.log('[App Debug] New state:', newInvoices); + } + return newInvoices; + }); + }, [debugMode]); + + // Adds a file to the processing queue + const handleFileSubmit = useCallback(async (file: File) => { + const tempId = `temp-${Date.now()}-${file.name}`; + console.log(`[App] Queuing file ${file.name} with temp ID: ${tempId}`); + + const fileDataUrl = await createFileDataUrl(file); + const fileContentBase64 = await fileToBase64(file); + + const newInvoiceEntry: ProcessedInvoice = { + id: tempId, + fileName: file.name, + fileType: file.type, + status: FileProcessingStatus.QUEUED, + isReviewed: false, + fileDataUrl, + fileContentBase64, + }; + + updateAndSaveInvoices(currentInvoices => [newInvoiceEntry, ...currentInvoices]); + }, [updateAndSaveInvoices]); + + // Effect to manage and process the queue + useEffect(() => { + const processQueue = async () => { + if (isProcessing || isPausedForRateLimit) { + return; // Don't process if already busy or paused + } + + const nextInvoice = invoices.find(inv => inv.status === FileProcessingStatus.QUEUED); + if (!nextInvoice) { + return; // No items in queue + } + + setIsProcessing(true); + console.log(`[App Queue] Processing next invoice: ${nextInvoice.id}`); + + // Update status to PROCESSING + updateAndSaveInvoices(currentInvoices => + currentInvoices.map(inv => + inv.id === nextInvoice.id ? { ...inv, status: FileProcessingStatus.PROCESSING } : inv + ) + ); + + try { + if (!nextInvoice.fileDataUrl) throw new Error("File data URL is missing."); + + // Reconstruct the file object from the blob URL + const fileBlob = await (await fetch(nextInvoice.fileDataUrl)).blob(); + const file = new File([fileBlob], nextInvoice.fileName, { type: nextInvoice.fileType }); + + const result = await uploadAndProcessInvoice(file); + + console.log(`[App Queue] API call successful for ${nextInvoice.id}`); + const finalInvoice: Partial = { + status: result.status || FileProcessingStatus.AWAITING_REVIEW, + extractedData: result.extractedData, + errorMessage: result.errorMessage, + }; + updateAndSaveInvoices(currentInvoices => + currentInvoices.map(inv => (inv.id === nextInvoice.id ? { ...inv, ...finalInvoice } : inv)) + ); + + } catch (error) { + console.error(`[App Queue] Processing error for ${nextInvoice.id}:`, error); + + if (error instanceof RateLimitError) { + console.warn("[App Queue] Rate limit detected. Pausing for 60 seconds."); + setIsPausedForRateLimit(true); + // Set invoice status back to QUEUED to be re-processed + updateAndSaveInvoices(currentInvoices => + currentInvoices.map(inv => (inv.id === nextInvoice.id ? { ...inv, status: FileProcessingStatus.QUEUED } : inv)) + ); + setTimeout(() => { + console.log("[App Queue] Resuming queue processing."); + setIsPausedForRateLimit(false); + }, 60000); + } else { + // Handle other, non-retriable errors + const errorMessage = error instanceof Error ? error.message : 'Bilinmeyen bir hata oluştu.'; + const errorInvoice: Partial = { + status: FileProcessingStatus.ERROR, + errorMessage: `İşleme hatası: ${errorMessage}`, + }; + updateAndSaveInvoices(currentInvoices => + currentInvoices.map(inv => (inv.id === nextInvoice.id ? { ...inv, ...errorInvoice } : inv)) + ); + } + } finally { + setIsProcessing(false); + } + }; + + processQueue(); + }, [invoices, isProcessing, isPausedForRateLimit, updateAndSaveInvoices]); + + // Handlers for the confirmation modal flow + const handleRequestDelete = (invoiceId: string) => { + setPendingAction({ action: 'delete', invoiceId }); + }; + + const handleRequestRevert = (invoiceId: string) => { + setPendingAction({ action: 'revert', invoiceId }); + }; + + const handleConfirmAction = () => { + if (!pendingAction) return; + const { action, invoiceId } = pendingAction; + + if (action === 'delete') { + updateAndSaveInvoices(prevInvoices => + prevInvoices.filter(inv => inv.id !== invoiceId) + ); + } else if (action === 'revert') { + updateAndSaveInvoices(prevInvoices => + prevInvoices.map(inv => + inv.id === invoiceId + ? { ...inv, isReviewed: false, status: FileProcessingStatus.AWAITING_REVIEW } + : inv + ) + ); + } + setPendingAction(null); + }; + + const handleCancelAction = () => { + setPendingAction(null); + }; + + const unreviewedCount = invoices.filter(inv => inv.status === FileProcessingStatus.AWAITING_REVIEW && !inv.isReviewed).length; + const reviewedCount = invoices.filter(inv => inv.isReviewed).length; + + const renderContent = () => { + if (!isLoaded) { + return
; + } + switch(view) { + case 'check': + return ; + case 'reviewed': + return ; + case 'main': + default: + return ( + <> +

Fatura Yükleyin ve Verileri Çıkarın

+ + {invoices.length > 0 && ( +
+
+

İşlem Geçmişi

+ +
+ {debugMode && ( +
+

Invoice State

+
{JSON.stringify(invoices, null, 2)}
+
+ )} +
+ {invoices.map(invoice => ( + + ))} +
+
+ )} + {invoices.length === 0 && ( +
+

Henüz işlenmiş fatura yok. Lütfen bir dosya yükleyin.

+ Placeholder +
+ )} + + ); + } + }; + + return ( +
+
+
+ {renderContent()} +
+ {pendingAction && ( + + )} +
+ ); +}; + +export default App; \ No newline at end of file diff --git a/README.md b/README.md index c1fb4a6..237c10e 100644 --- a/README.md +++ b/README.md @@ -1,135 +1,96 @@ -# Fatrocu v2 - Akıllı Fatura İşleme Asistanı +# 🤖 Fatrocu v2 - Akıllı Fatura İşleme Asistanı -**Muhasebe süreçlerinizi hızlandırın ve doğruluğu en üst düzeye çıkarın. Fatrocu v2, e-Faturaları (XML), PDF'leri ve resimleri analiz ederek fatura verilerini otomatik olarak yapılandırılmış Excel formatına dönüştürür.** +
+ React + TypeScript + Gemini API + Tailwind CSS +
---- - -## Genel Bakış +**Fatrocu v2, faturalarınızı (XML, PDF, Resim) akıllıca işleyen, verileri Google Gemini AI ile çıkaran ve düzenli bir şekilde dışa aktarmanızı sağlayan modern bir web uygulamasıdır.** -Fatrocu v2, mali müşavirler ve işletmeler için fatura veri girişinin zaman alan ve hataya açık sürecini otomatikleştirmek üzere **yeniden tasarlanmış** bir araçtır. Bu yeni sürüm, güvenilir bir Python backend'i ve modern bir web arayüzü üzerine kurulmuştur. +--- -Türkiye'deki **e-Fatura (UBL-TR XML)** standartlarını doğrudan ayrıştırarak **%100'e yakın doğruluk** sağlarken, PDF ve resim formatındaki faturalar için **Google'ın gelişmiş Gemini 2.5 Pro Experimental modelinin** gücünü kullanır. +### 🎬 Uygulama Demosu +![fatrocu demo](https://github.com/user-attachments/assets/dc30f820-33d7-4373-a96d-092164348bbc) -Sezgisel web arayüzü sayesinde faturalarınızı kolayca yükleyin, Fatrocu'nun akıllı analizini izleyin, çıkarılan verileri gözden geçirin ve tek tıklamayla Excel'e aktarın. -**(Not: Bu proje aktif geliştirme aşamasındadır. Mevcut sürüm temel işlevleri içerir ancak tam özellik seti henüz tamamlanmamıştır.)** -## Anahtar Özellikler (v2) +## ✨ Temel Özellikler -* **🚀 Yepyeni Mimari:** Güvenlik, performans ve geliştirilebilirlik için **Python (Flask) Backend + Web (HTML/JS) Frontend** hibrit modeli. -* **🥇 e-Fatura (UBL-TR XML) Desteği:** Yüklenen XML dosyalarını veya PDF'e gömülü XML'leri doğrudan ayrıştırarak **maksimum doğruluk** sağlar. -* **✨ Gelişmiş PDF ve Resim Analizi:** Google Gemini 2.5 Pro experimental kullanarak PDF, PNG, JPG, JPEG gibi formatlardaki faturalardan metin okuma (OCR) ve akıllı veri çıkarma. -* **🧠 Akıllı Veri Çıkarma:** Kritik fatura bilgilerini otomatik olarak tanımlar ve çıkarır: - * Fatura Numarası - * Fatura Tarihi - * Satıcı VKN/TCKN ve Ünvan - * Alıcı VKN/TCKN ve Ünvan (varsa) - * KDV Matrahı, Oranı, Tutarı - * Genel Toplam -* **✔️ Arka Plan Veri Doğrulama:** Çıkarılan temel veriler üzerinde otomatik format (tarih, VKN/TCKN) ve matematiksel tutarlılık (Matrah + KDV ≈ Toplam) kontrolleri. -* **📊 Excel'e Aktarım:** Başarıyla işlenen ve doğrulanan verileri (ana alanlar) yapılandırılmış `.xlsx` dosyası olarak kolayca dışa aktarın. -* **🖥️ Temel Sonuç Görüntüleme Arayüzü:** Kullanıcı dostu web arayüzünde yüklenen faturaların listesi ve AI/XML tarafından çıkarılan verilerin **görüntülenmesi**. -* **☁️ Yerel Web Erişimi:** Flask ile çalışan yerel sunucu üzerinden herhangi bir modern web tarayıcısı ile erişim. -* **🔐 Güvenli API Anahtarı Yönetimi:** Google Gemini API anahtarı, `.env` dosyası aracılığıyla **güvenli bir şekilde backend'de** yönetilir. +- 📄 **Çoklu Dosya Desteği:** PDF, XML, PNG, ve JPEG gibi yaygın fatura formatlarını sorunsuzca işler. +- 🧠 **Yapay Zeka Destekli Veri Çıkarma:** Google Gemini API'sinin gücüyle faturalardan temel bilgileri (Fatura No, Tarih, Taraflar, Tutarlar, Fatura Türü vb.) otomatik olarak çıkarır. +- 🎨 **Etkileşimli Veri Doğrulama:** AI tarafından çıkarılan verileri, faturanın canlı önizlemesiyle yan yana kontrol etme ve düzenleme imkanı sunar. Alana tıkladığınızda faturadaki konumu anında vurgulanır. +- 📤 **Tek Tıkla CSV Aktarımı:** Onayladığınız tüm fatura verilerini, tek bir tıklamayla düzenli ve standart bir CSV dosyası olarak indirin. +- ⏳ **Akıllı API Kuyruk Sistemi:** Yoğun API kullanımı durumunda (rate limit), uygulama otomatik olarak duraklar, 60 saniye bekler ve kaldığı yerden devam eder. Bu, çok sayıda faturanın sorunsuzca işlenmesini sağlar. +- 💾 **Kalıcı Oturum:** Tarayıcının `localStorage` özelliği sayesinde, işlediğiniz faturalar siz sekmeyi kapatsanız veya sayfayı yenileseniz bile kaybolmaz. +- 💅 **Modern ve Duyarlı Arayüz:** Karanlık tema ve Tailwind CSS ile geliştirilmiş şık, sezgisel ve kullanışlı bir tasarıma sahiptir. -## Nasıl Çalışır? +## 🚀 İş Akışı (Workflow) -1. **Kurulum ve Çalıştırma:** Projeyi yerel makinenize kurun ve `flask run` ile backend sunucusunu başlatın (Detaylar için 'Kurulum ve Kullanım' bölümüne bakın). -2. **Yükleme:** Fatura dosyanızı (XML, PDF, PNG, JPG vb.) Fatrocu web arayüzüne (`http://127.0.0.1:5000`) sürükleyip bırakın veya seçin. -3. **Akıllı İşleme (Backend):** - * Dosya tipi kontrol edilir (`mimetypes` kullanılır). XML ise doğrudan `lxml` ile ayrıştırılır. - * PDF/Resim ise güvenli bir şekilde backend'e kaydedilir ve Gemini API'sine gönderilir. - * Gemini metin okuma (OCR) ve yapılandırılmış veri çıkarma işlemlerini gerçekleştirir. - * Otomatik veri doğrulama kuralları uygulanır. - * İşlem sonucu (başarılı veya hatalı) JSON olarak kaydedilir. -4. **Sonuç Görüntüleme (Frontend):** İşlem sonucu (çıkarılan veriler veya hata mesajı) kullanıcı arayüzünde gösterilir. -5. **Aktarım:** Başarılı işlenen faturalar için "Excel'e Aktar" butonu ile `.xlsx` dosyası indirilir. +1. **Yükleme:** Dosyalarınızı sürükleyip bırakın veya seçin. +2. **İşleme:** Sistem dosyaları sıraya alır ve Gemini AI aracılığıyla verileri otomatik olarak çıkarır. +3. **Kontrol:** "Kontrol & Dışa Aktar" ekranında, çıkarılan verileri faturanın önizlemesiyle karşılaştırın, düzenleyin ve onaylayın. +4. **Yönetim:** Onaylanmış faturaları "Onaylananlar" sekmesinde görüntüleyin, gerekirse silin veya kontrole geri alın. +5. **Dışa Aktarma:** Onayladığınız tüm verileri tek bir tuşla `YYYY-AA-GG_onaylanan_faturalar.csv` formatında indirin. -## Teknoloji Mimarisi +## 🛠️ Teknoloji Stack'i -* **Backend:** Python 3.x, Flask -* **AI Model:** Google Gemini `gemini-2.5-pro-experimental` (via Google AI API) -* **Frontend:** HTML5, CSS3, JavaScript (Vanilla JS) -* **Veri İşleme:** Pandas, openpyxl (Excel için), lxml (XML için) -* **Dosya Tipi Tespiti:** mimetypes (Python standard library) -* **API İletişimi:** Google AI Python SDK +- **Frontend:** React 19, TypeScript +- **Yapay Zeka:** Google Gemini API (`@google/genai`) +- **Styling:** Tailwind CSS +- **Modül Yönetimi:** ES Modules (ESM) via `esm.sh` +- **Veri Saklama:** Tarayıcı `localStorage` API -## Gereksinimler +## ⚙️ Kurulum ve Çalıştırma -* **Python:** Sürüm 3.8 veya üzeri önerilir. -* **Pip:** Python paket yöneticisi. -* **Google Gemini API Anahtarı:** PDF/Resim formatındaki faturaları işleyebilmek için gereklidir. - * [Google AI Studio](https://aistudio.google.com/app/apikey) üzerinden **ücretsiz** bir anahtar edinebilirsiniz. - * Ücretsiz katman, (`gemini-2.5-pro-experimental` vb.) gibi belirli modeller için geçerlidir ve kullanım limitleri vardır. (Kullanmak istediğiniz modelin kota durumunu kontrol edin). - * API anahtarı, proje kurulumunda `.env` dosyasına eklenecektir. +Bu uygulama, tüm bağımlılıkların bir web tabanlı geliştirme ortamı tarafından otomatik olarak sağlandığı bir platformda çalışacak şekilde tasarlanmıştır. -## Kurulum ve Kullanım (Yerel Makine) +Yerel bir makinede çalıştırmak için temel adımlar şunlardır: -1. **Projeyi Klonlayın/İndirin:** - ```bash - git clone https://github.com/Nec0ti/Fatrocu.git - cd Fatrocu - ``` -2. **Backend Klasörüne Gidin:** - ```bash - cd backend - ``` -3. **Sanal Ortam Oluşturun ve Aktive Edin:** - * Windows: - ```bash - python -m venv venv - .\venv\Scripts\activate - ``` - * macOS/Linux: - ```bash - python3 -m venv venv - source venv/bin/activate - ``` -4. **Gerekli Kütüphaneleri Kurun:** - ```bash - pip install -r requirements.txt - ``` -5. **API Anahtarını Ayarlayın:** - * `backend` klasörü içinde `.env` adında bir dosya oluşturun. - * Dosyanın içine aşağıdaki satırı ekleyin ve `YOUR_GEMINI_API_KEY_HERE` kısmını kendi API anahtarınızla değiştirin: - ``` - GOOGLE_API_KEY="YOUR_GEMINI_API_KEY_HERE" - ``` -6. **Uygulamayı Başlatın:** +1. **API Anahtarını Ayarlama:** + Uygulamanın Google Gemini API'si ile iletişim kurabilmesi için geçerli bir API anahtarına ihtiyacı vardır. Bu anahtarın bir ortam değişkeni (environment variable) olarak ayarlanması gerekmektedir. Proje kök dizininde `.env` dosyası oluşturup içine ekleyin: ```bash - flask run + API_KEY="YOUR_GEMINI_API_KEY" ``` - * Uygulama genellikle `http://127.0.0.1:5000` adresinde çalışmaya başlayacaktır. -7. **Kullanım:** - * Web tarayıcınızdan `http://127.0.0.1:5000` adresini açın. - * "Dosya Seç veya Sürükle Bırak" alanını kullanarak fatura dosyalarınızı (XML, PDF, PNG, JPG vb.) yükleyin. - * "Yükle ve İşle" butonuna tıklayın. - * "İşlem Sonucu" bölümünde çıkarılan verileri veya hata mesajını görün. - * "Son İşlenen Dosyalar" listesinden önceki işlemlerin sonuçlarını tekrar görebilir veya Excel olarak indirebilirsiniz. - -## Doğruluk Üzerine Not - -Fatrocu v2, e-Faturalar için çok yüksek doğruluk hedefler. PDF/Resim formatları için kullanılan Gemini modelleri güçlü olsa da, fatura kalitesi, düzeni ve karmaşıklığına bağlı olarak %100 doğruluk garanti edilemez. Bu sürümün amacı, veri girişini **önemli ölçüde hızlandırmak** ve kullanıcıya **hızlı bir kontrol** imkanı sunmaktır. Gelecek sürümlerde eklenecek etkileşimli doğrulama arayüzü bu süreci daha da iyileştirecektir. - -## Yol Haritası / Gelecek Planları - -* [ ] **Etkileşimli Doğrulama Arayüzü:** Kullanıcının arayüz üzerinden çıkarılan verileri **düzenlemesi ve onaylaması**. (Yüksek Öncelik) -* [ ] **Fatura Görseli Entegrasyonu:** Doğrulama ekranında fatura görselini (PDF/Resim) gösterme. -* [ ] **Gelişmiş Veri Çıkarma:** Ürün/Hizmet kalemleri, para birimi, ödeme vadesi, IBAN vb. -* [ ] **ÖKC Fişi Optimizasyonu:** Yazar kasa fişleri için özel analiz. -* [ ] **Fatura Görseli Üzerinde İşaretleme:** AI'ın bulduğu verinin faturadaki yerini vurgulama. -* [ ] **Model/Prompt İyileştirme:** Farklı fatura tipleri için Gemini prompt optimizasyonu. -* [ ] **Kullanıcı Tanımlı Alanlar:** Özel alan çıkarma talepleri. -* [ ] **Toplu İşlem (Batch Processing):** Çoklu dosya yükleme ve işleme. -* [ ] **Muhasebe Yazılımı Entegrasyonları:** API veya dosya formatı ile entegrasyon. -* [ ] **Kullanıcı Yönetimi / Veritabanı:** İşlenen faturaları ve ayarları kalıcı olarak saklama. - -## Katkıda Bulunma - -Katkılarınız memnuniyetle karşılanır! Projeyi geliştirmeye yardımcı olmak isterseniz, lütfen repoyu forklayın, değişikliklerinizi yapın ve bir pull request gönderin. Hataları bildirmek veya yeni özellikler önermek için [GitHub Issues](https://github.com/Nec0ti/Fatrocu/issues) sayfasını kullanmaktan çekinmeyin. - -## Lisans - -Bu proje MIT Lisansı altında lisanslanmıştır - detaylar için `LICENSE` dosyasına bakın. + > **Not:** Uygulama, `process.env.API_KEY` üzerinden bu anahtara erişir. + +2. **Bağımlılıklar:** + Proje `package.json` dosyası içermediğinden, bağımlılıklar `index.html` içerisindeki `importmap` aracılığıyla CDN (`esm.sh`) üzerinden dinamik olarak çekilir. Ek bir `npm install` adımına gerek yoktur. + +3. **Uygulamayı Başlatma:** + `index.html` dosyasını sunacak basit bir yerel sunucu (örneğin, VS Code **Live Server** eklentisi veya `npx serve`) çalıştırın. + +## 📂 Proje Yapısı + +``` +. +├── index.html # Ana HTML dosyası, importmap ve başlangıç noktası +├── index.tsx # React uygulamasının root render dosyası +├── App.tsx # Ana uygulama bileşeni (state yönetimi, yönlendirme) +├── README.md # Proje tanıtım dosyası +├── types.ts # TypeScript arayüzleri ve enum'ları (Invoice, Status vb.) +├── utils.ts # Yardımcı fonksiyonlar (base64 dönüştürme, URL oluşturma) +├── services/ +│ └── apiService.ts # Gemini API çağrıları ve CSV dışa aktarma mantığı +└── components/ + ├── Header.tsx # Sayfa başlığı ve ana navigasyon + ├── FileUploadArea.tsx # Dosya yükleme bileşeni + ├── CheckView.tsx # Veri kontrol ve düzenleme ekranı + ├── ReviewedView.tsx # Onaylanmış faturaların listelendiği ekran + ├── ProcessedInvoiceCard.tsx # Tek bir faturanın durumunu gösteren kart + ├── ConfirmationModal.tsx # Silme/geri alma işlemleri için onay penceresi + ├── AlertMessage.tsx # Başarı/hata bildirimleri + └── Spinner.tsx # Yüklenme animasyonu +``` + +## 🔮 Gelecek Planları (Roadmap) + +- [ ] **Gelişmiş Arama ve Filtreleme:** Faturaları tarihe, tutara veya satıcıya göre arama. +- [ ] **Farklı Dışa Aktarma Formatları:** Excel (.xlsx) veya JSON olarak dışa aktarma seçeneği. +- [ ] **Kullanıcı Hesapları:** Çoklu kullanıcı desteği ve kişisel fatura yönetimi. +- [ ] **Dashboard:** Toplam tutarlar, en sık işlem yapılan satıcılar gibi istatistiksel verilerin görselleştirildiği bir ana sayfa. +- [ ] **Testler:** Uygulama kararlılığını artırmak için birim ve entegrasyon testleri eklemek. --- +*Bu proje, fatura işleme süreçlerini otomatize etmek ve basitleştirmek için tasarlanmıştır.* diff --git a/SECURITY.md b/SECURITY.md deleted file mode 100644 index a386255..0000000 --- a/SECURITY.md +++ /dev/null @@ -1,38 +0,0 @@ -# Security Policy - -## Supported Versions - -The table below shows the versions of the project that receive security updates: - -| Version | Support Status | -| -------- | ------------------- | -| 1.2.1 | :white_check_mark: | -| < 1.2.0 | :x: | - -Please note that new features will only be supported in the latest major version (1.2.1 >), but critical security vulnerabilities in older versions may be patched for a limited time. - ---- - -## Reporting a Vulnerability - -To report a security vulnerability, please follow the steps below: - -1. **Communication Channels** - To report a vulnerability, please submit it to the issues page: - Include the following details about the issue: - - A description of the vulnerability - - Steps to reproduce the issue - - Potential impacts and exploitation scenarios - -2. **Vulnerability Process** - Accepted vulnerabilities will go through the following process: - - Verification and impact analysis of the issue - - Development and testing of a fix - - Release of the updated version - -3. **Confidentiality Policy** - Reports will remain confidential until the issue is resolved. Upon request from the reporter, contributions made to resolving the issue will be properly acknowledged. - ---- - -If you need further information, please visit the **[repository/issues](https://github.com/Nec0ti/Fatrocu/issues)** section. diff --git a/backend/.flaskenv b/backend/.flaskenv deleted file mode 100644 index 80e1416..0000000 --- a/backend/.flaskenv +++ /dev/null @@ -1,3 +0,0 @@ -FLASK_APP=run.py -FLASK_ENV=development -FLASK_DEBUG=1 \ No newline at end of file diff --git a/backend/config.py b/backend/config.py deleted file mode 100644 index 0b80419..0000000 --- a/backend/config.py +++ /dev/null @@ -1,18 +0,0 @@ -import os -from dotenv import load_dotenv - -basedir = os.path.abspath(os.path.dirname(__file__)) -load_dotenv(os.path.join(basedir, '.env')) # .env dosyasını yükle - -class Config: - SECRET_KEY = os.environ.get('SECRET_KEY') or 'you-will-never-guess' - GOOGLE_API_KEY = os.environ.get('GOOGLE_API_KEY') - UPLOAD_FOLDER = os.path.join(basedir, 'uploads') - OUTPUT_FOLDER = os.path.join(basedir, 'output') - MAX_CONTENT_LENGTH = 16 * 1024 * 1024 # Örnek: Maksimum 16MB yükleme limiti - - # Klasörlerin var olduğundan emin ol - if not os.path.exists(UPLOAD_FOLDER): - os.makedirs(UPLOAD_FOLDER) - if not os.path.exists(OUTPUT_FOLDER): - os.makedirs(OUTPUT_FOLDER) \ No newline at end of file diff --git a/backend/fatrocu_app/__init__.py b/backend/fatrocu_app/__init__.py deleted file mode 100644 index 7de8eb1..0000000 --- a/backend/fatrocu_app/__init__.py +++ /dev/null @@ -1,47 +0,0 @@ -# backend/fatrocu_app/__init__.py -from flask import Flask, render_template # render_template eklendi -from config import Config -import os - -def create_app(config_class=Config): - app = Flask(__name__, - static_folder='../static', # static klasörün göreceli yolu doğru olmalı - template_folder='../templates') # templates klasörün göreceli yolu doğru olmalı - app.config.from_object(config_class) - - # Gerekli klasörlerin varlığını kontrol et ve oluştur - required_folders = [app.config['UPLOAD_FOLDER'], app.config['OUTPUT_FOLDER']] - for folder in required_folders: - if not os.path.exists(folder): - try: - os.makedirs(folder) - print(f"Klasör oluşturuldu: {folder}") - except OSError as e: - print(f"Klasör oluşturulamadı: {folder}, Hata: {e}") - - # API Anahtarının yüklenip yüklenmediğini kontrol et - if not app.config.get('GOOGLE_API_KEY'): - print("UYARI: GOOGLE_API_KEY ortam değişkeni bulunamadı veya .env dosyasında ayarlanmadı.") - - # ----- Blueprint Kaydı ----- - # routes modülünü burada import et ve blueprint'i kaydet - from .routes import api_bp # Relative import - app.register_blueprint(api_bp) - # -------------------------- - - # ----- Ana Sayfa Route'u ----- - # API olmayan route'ları doğrudan app üzerinde tanımlayabiliriz (Blueprint sonrası) - @app.route('/') - def index(): - print("Ana sayfa isteği alındı.") - # templates klasöründeki index.html'i render et - return render_template('index.html') - # -------------------------- - - print("Flask uygulaması oluşturuldu ve yapılandırıldı.") - print(f"Static folder: {app.static_folder}") - print(f"Template folder: {app.template_folder}") - print(f"Upload folder: {app.config['UPLOAD_FOLDER']}") - print(f"Output folder: {app.config['OUTPUT_FOLDER']}") - - return app \ No newline at end of file diff --git a/backend/fatrocu_app/routes.py b/backend/fatrocu_app/routes.py deleted file mode 100644 index 776ee40..0000000 --- a/backend/fatrocu_app/routes.py +++ /dev/null @@ -1,116 +0,0 @@ -# backend/fatrocu_app/routes.py -from flask import Blueprint, jsonify, request, send_from_directory, current_app -from werkzeug.utils import secure_filename -import os -import pandas as pd - -# Servis fonksiyonlarını import et -from .services.file_handler import process_invoice, load_result, save_result # save_result eklendi -from .services.excel_exporter import create_excel # Ayrı bir excel modülü oluşturalım - -api_bp = Blueprint('api', __name__, url_prefix='/api') - -# Örnek endpoint (değişiklik yok) -@api_bp.route('/hello') -def hello(): - return jsonify(message="Backend'den Merhaba!") - -# ---- DOSYA YÜKLEME & İŞLEME ENDPOINT'İ ---- -@api_bp.route('/upload', methods=['POST']) -def upload_file(): - app_config = current_app.config - upload_folder = app_config['UPLOAD_FOLDER'] - - if not os.path.exists(upload_folder): - os.makedirs(upload_folder) - - if 'file' not in request.files: - return jsonify(error="Dosya bulunamadı"), 400 - file = request.files['file'] - if file.filename == '': - return jsonify(error="Dosya seçilmedi"), 400 - - if file: - filename = secure_filename(file.filename) - filepath = os.path.join(upload_folder, filename) - try: - file.save(filepath) - print(f"Dosya kaydedildi: {filepath}") - - # ------ FATURA İŞLEME ------ - # file_handler'daki ana işleme fonksiyonunu çağır - processing_result = process_invoice(filepath) - # -------------------------- - - # İşleme sonucunu (başarılı veya hatalı) doğrudan döndür - if processing_result.get("status") == "error" or processing_result.get("status") == "failed": - # Belki hata durumunda farklı HTTP kodu dönebiliriz? 500 Internal Server Error gibi? - # Şimdilik 200 OK ile birlikte hatayı JSON içinde dönelim. - print(f"İşleme başarısız/hatalı: {filename}") - # return jsonify(processing_result), 500 - else: - print(f"İşleme başarılı: {filename}") - - return jsonify(processing_result), 200 # Başarı veya hata JSON'ı döndür - - except Exception as e: - # Dosya kaydetme veya process_invoice çağrısı sırasında genel hata - print(f"Dosya yükleme/işleme sırasında genel hata: {e}") # Log the detailed error - # Hata durumunu da JSON olarak kaydetmeye çalışalım - error_data = { - "status": "error", "filename": filename, "extracted_data": None, - "error": "Yükleme/İşleme sırasında beklenmedik bir sunucu hatası oluştu." - } - save_result(filename, error_data) # Fonksiyon None dönebilir ama denemekte fayda var - return jsonify(error_data), 500 - return jsonify(error="Geçersiz dosya"), 400 - -# ---- SONUÇLARI ALMA ENDPOINT'İ ---- -@api_bp.route('/results/', methods=['GET']) -def get_results(filename): - # Kaydedilmiş JSON sonucunu yükle - result_data = load_result(secure_filename(filename)) # filename'i güvenli hale getir - if result_data: - return jsonify(result_data), 200 - else: - # Henüz işlenmemiş veya bulunamayan dosya - return jsonify(error=f"'{filename}' için sonuç bulunamadı veya henüz işlenmedi."), 404 - -# ---- EXCEL İNDİRME ENDPOINT'İ ---- -@api_bp.route('/export/', methods=['GET']) -def export_excel_route(filename): # Fonksiyon adını route'dan ayırmak iyi pratik - app_config = current_app.config - output_folder = app_config['OUTPUT_FOLDER'] - safe_filename = secure_filename(filename) - - if not os.path.exists(output_folder): - os.makedirs(output_folder) - - # Önce işlenmiş veriyi JSON dosyasından yükle - result_data = load_result(safe_filename) - - if not result_data or result_data.get("status") not in ["completed", "verified"]: # Sadece başarılı/doğrulanmış olanları export et - error_msg = f"'{safe_filename}' için dışa aktarılacak onaylanmış veri bulunamadı." - if result_data and result_data.get('error'): - error_msg += f" Hata: {result_data.get('error')}" - elif not result_data: - error_msg = f"'{safe_filename}' dosyası veya işlenmiş verisi bulunamadı." - return jsonify(error=error_msg), 404 - - - # Excel oluşturma işlemini ayrı bir fonksiyona taşıyalım - excel_filepath = create_excel(result_data.get("extracted_data", {}), safe_filename, output_folder) - - if excel_filepath and os.path.exists(excel_filepath): - try: - # Göreceli path yerine dosya adını verelim - excel_basename = os.path.basename(excel_filepath) - return send_from_directory(output_folder, excel_basename, as_attachment=True) - except FileNotFoundError: - print(f"Oluşturulan Excel dosyası bulunamadı: {excel_filepath}") - return jsonify(error="Oluşturulan Excel dosyası gönderilemedi."), 404 - except Exception as e: - current_app.logger.error(f"Excel gönderilirken hata: {e}") - return jsonify(error="Excel gönderilirken sunucu hatası oluştu."), 500 - else: - return jsonify(error="Excel dosyası oluşturulamadı veya bulunamadı."), 500 \ No newline at end of file diff --git a/backend/fatrocu_app/services/data_validator.py b/backend/fatrocu_app/services/data_validator.py deleted file mode 100644 index 85260d1..0000000 --- a/backend/fatrocu_app/services/data_validator.py +++ /dev/null @@ -1,106 +0,0 @@ -import re -from datetime import datetime -import math - -def validate_date(date_str): - """Tarihi GG.AA.YYYY formatında doğrular ve datetime nesnesi döner.""" - if not date_str or not isinstance(date_str, str): - return None, "Tarih boş veya geçersiz tip." - try: - # Yaygın ayırıcıları tolere etmeye çalışalım - cleaned_date_str = date_str.replace('-', '.').replace('/', '.') - # Gün ve ay tek haneli ise başına 0 ekle (örn: 1.1.2024 -> 01.01.2024) - parts = cleaned_date_str.split('.') - if len(parts) == 3: - parts = [p.zfill(2) for p in parts[:2]] + [parts[2]] - cleaned_date_str = '.'.join(parts) - - dt_obj = datetime.strptime(cleaned_date_str, '%d.%m.%Y') - return dt_obj.strftime('%d.%m.%Y'), None # Başarılı, formatlanmış string dön - except ValueError: - return None, f"Geçersiz tarih formatı: {date_str}. GG.AA.YYYY bekleniyor." - -def validate_vkn_tckn(number_str): - """VKN (10 hane) veya TCKN (11 hane) formatını doğrular.""" - if not number_str or not isinstance(number_str, str): - return False, "VKN/TCKN boş veya geçersiz tip." - cleaned_number = re.sub(r'\D', '', number_str) # Sadece rakamları al - length = len(cleaned_number) - if length == 10 or length == 11: - # Burada daha gelişmiş VKN/TCKN algoritma kontrolleri eklenebilir (opsiyonel) - return True, None - else: - return False, f"Geçersiz VKN/TCKN uzunluğu ({length} hane): {number_str}" - -def validate_amount(amount_val): - """Tutarı sayısal (float) olarak doğrular ve döner.""" - if amount_val is None: return None, "Tutar boş." - try: - # String gelirse virgülü noktaya çevir, boşlukları kaldır - if isinstance(amount_val, str): - cleaned_amount = amount_val.replace('.', '', amount_val.count('.') -1).replace(',', '.').strip() # Binlik ayıracını kaldır, ondalığı nokta yap - return float(cleaned_amount), None - elif isinstance(amount_val, (int, float)): - return float(amount_val), None - else: - return None, f"Geçersiz tutar tipi: {type(amount_val)}" - except (ValueError, TypeError): - return None, f"Geçersiz tutar formatı: {amount_val}" - - -def validate_invoice_data(data): - """AI veya XML'den çıkarılan veriyi doğrular ve notlar ekler.""" - if not data: - return {"validation_errors": ["Doğrulanacak veri yok."]} - - validated_data = data.copy() # Orijinal veriyi bozmayalım - errors = [] - warnings = [] - - # Tarih Doğrulama - date_val, date_err = validate_date(data.get('tarih')) - if date_err: errors.append(f"Tarih: {date_err}") - validated_data['tarih'] = date_val # Başarılıysa formatlanmış halini kaydet - - # VKN/TCKN Doğrulama - satici_vkn_valid, satici_vkn_err = validate_vkn_tckn(data.get('satici_vkn_tckn')) - if satici_vkn_err: errors.append(f"Satıcı VKN/TCKN: {satici_vkn_err}") - if data.get('alici_vkn_tckn'): # Alıcı zorunlu değil - alici_vkn_valid, alici_vkn_err = validate_vkn_tckn(data.get('alici_vkn_tckn')) - if alici_vkn_err: warnings.append(f"Alıcı VKN/TCKN: {alici_vkn_err}") # Hata yerine uyarı? - - # Tutar Doğrulama - matrah_f, matrah_err = validate_amount(data.get('kdv_matrah')) - if matrah_err: errors.append(f"Matrah: {matrah_err}") - validated_data['kdv_matrah'] = matrah_f - - kdv_tutari_f, kdv_tutari_err = validate_amount(data.get('kdv_tutari')) - if kdv_tutari_err: errors.append(f"KDV Tutarı: {kdv_tutari_err}") - validated_data['kdv_tutari'] = kdv_tutari_f - - toplam_f, toplam_err = validate_amount(data.get('genel_toplam')) - if toplam_err: errors.append(f"Genel Toplam: {toplam_err}") - validated_data['genel_toplam'] = toplam_f - - kdv_orani_f, _ = validate_amount(data.get('kdv_orani')) # Oran hatası kritik olmayabilir - validated_data['kdv_orani'] = kdv_orani_f - - - # Matematiksel Tutarlılık Kontrolü - if matrah_f is not None and kdv_tutari_f is not None and toplam_f is not None: - calculated_total = matrah_f + kdv_tutari_f - # Kuruş farklarını tolere etmek için math.isclose kullanalım - if not math.isclose(calculated_total, toplam_f, rel_tol=1e-2): # %1 tolerans veya abs_tol=0.02 gibi - warnings.append(f"Matematiksel tutarsızlık: Matrah ({matrah_f:.2f}) + KDV ({kdv_tutari_f:.2f}) = {calculated_total:.2f} != Genel Toplam ({toplam_f:.2f})") - - # Oran üzerinden de kontrol (eğer oran varsa) - if kdv_orani_f is not None and kdv_orani_f > 0 and matrah_f != 0: - calculated_kdv = matrah_f * (kdv_orani_f / 100.0) - if not math.isclose(calculated_kdv, kdv_tutari_f, rel_tol=1e-2): - warnings.append(f"KDV Tutar tutarsızlığı: Matrah*Oran ({matrah_f:.2f}*{kdv_orani_f}%) = {calculated_kdv:.2f} != KDV Tutarı ({kdv_tutari_f:.2f})") - - validated_data['validation_errors'] = errors - validated_data['validation_warnings'] = warnings - - print(f"Doğrulama tamamlandı. Hatalar: {len(errors)}, Uyarılar: {len(warnings)}") - return validated_data \ No newline at end of file diff --git a/backend/fatrocu_app/services/excel_exporter.py b/backend/fatrocu_app/services/excel_exporter.py deleted file mode 100644 index 4afc471..0000000 --- a/backend/fatrocu_app/services/excel_exporter.py +++ /dev/null @@ -1,47 +0,0 @@ -import pandas as pd -import os -from flask import current_app - -def create_excel(data, original_filename, output_folder): - """Verilen data dict'inden bir Excel dosyası oluşturur.""" - if not data: - print("Excel oluşturmak için veri yok.") - return None - - try: - # Excel'e yazılacak veriyi hazırla (DataFrame için uygun format) - # Sadece 'extracted_data' içindeki anahtar alanları alalım - excel_data = { - "Fatura No": [data.get('fatura_no', '')], - "Tarih": [data.get('tarih', '')], - "Satıcı VKN/TCKN": [data.get('satici_vkn_tckn', '')], - "Satıcı Ünvan": [data.get('firma_unvan', '')], - "Alıcı VKN/TCKN": [data.get('alici_vkn_tckn', '')], - "Alıcı Ünvan": [data.get('alici_firma_unvan', '')], - "KDV Matrahı": [data.get('kdv_matrah', None)], - "KDV Oranı (%)": [data.get('kdv_orani', None)], - "KDV Tutarı": [data.get('kdv_tutari', None)], - "Genel Toplam": [data.get('genel_toplam', None)], - # Doğrulama notlarını ekleyebiliriz (opsiyonel) - "Doğrulama Hataları": [", ".join(data.get('validation_errors', []))], - "Doğrulama Uyarıları": [", ".join(data.get('validation_warnings', []))] - } - # Eğer KDV detayları varsa, ayrı satırlar/sütunlar olarak eklenebilir - # Şimdilik ana bilgileri ekleyelim. - - df = pd.DataFrame(excel_data) - - # Çıktı dosya adı - base_filename = os.path.splitext(original_filename)[0] - excel_filename = f"{base_filename}.xlsx" - excel_filepath = os.path.join(output_folder, excel_filename) - - # Excel'e yaz - df.to_excel(excel_filepath, index=False, engine='openpyxl') - - print(f"Excel oluşturuldu: {excel_filepath}") - return excel_filepath - - except Exception as e: - print(f"Excel oluşturulurken hata: {e}") - return None \ No newline at end of file diff --git a/backend/fatrocu_app/services/file_handler.py b/backend/fatrocu_app/services/file_handler.py deleted file mode 100644 index b27bca5..0000000 --- a/backend/fatrocu_app/services/file_handler.py +++ /dev/null @@ -1,147 +0,0 @@ -import os -from werkzeug.utils import secure_filename -import mimetypes -from flask import current_app -import json - -# Eğer python-magic ile sorun yaşarsanız (özellikle Windows'ta DLL gerekebilir), -# sadece dosya uzantısına göre de kontrol yapabilirsiniz. -# import mimetypes - -# XML ayrıştırma, Gemini işleme ve doğrulama fonksiyonlarını import edeceğiz -from .xml_parser import parse_ubl_xml -from .gemini_processor import process_with_gemini -from .data_validator import validate_invoice_data - -# Sonuçları saklamak için basit bir yöntem (JSON dosyası olarak) -def save_result(filename, data): - """İşlenmiş veriyi uploads klasörüne JSON olarak kaydeder.""" - try: - base_filename = os.path.splitext(filename)[0] - json_filename = f"{base_filename}.json" - json_filepath = os.path.join(current_app.config['UPLOAD_FOLDER'], json_filename) - with open(json_filepath, 'w', encoding='utf-8') as f: - json.dump(data, f, ensure_ascii=False, indent=4) - print(f"Sonuçlar kaydedildi: {json_filepath}") - return json_filename - except Exception as e: - print(f"Sonuçlar kaydedilirken hata: {e}") - return None - -def load_result(filename): - """Kaydedilmiş JSON sonucunu yükler.""" - try: - base_filename = os.path.splitext(filename)[0] - json_filename = f"{base_filename}.json" - json_filepath = os.path.join(current_app.config['UPLOAD_FOLDER'], json_filename) - if os.path.exists(json_filepath): - with open(json_filepath, 'r', encoding='utf-8') as f: - data = json.load(f) - return data - else: - return None - except Exception as e: - print(f"Sonuçlar yüklenirken hata: {e}") - return None - - -def get_file_type(filepath): - """Dosya tipini belirler (XML, PDF, Image) - mimetypes kullanarak.""" - try: - # mimetypes ile tahmin et - mime_type, _ = mimetypes.guess_type(filepath) - print(f"Tahmin edilen MIME tipi (mimetypes): {mime_type}") # Logu değiştirildi - - if mime_type: - if 'xml' in mime_type: - return 'xml' - elif 'pdf' in mime_type: - return 'pdf' - elif mime_type.startswith('image/'): - return 'image' - else: - print(f"Desteklenmeyen MIME tipi: {mime_type}. Uzantıya bakılıyor.") - ext = os.path.splitext(filepath)[1].lower() - if ext == '.xml': return 'xml' - if ext == '.pdf': return 'pdf' - if ext in ['.png', '.jpg', '.jpeg', '.gif', '.bmp', '.tiff', '.webp', '.heic']: return 'image' - return 'unsupported' - else: - print("MIME tipi tahmin edilemedi, sadece uzantıya bakılıyor.") - ext = os.path.splitext(filepath)[1].lower() - if ext == '.xml': return 'xml' - if ext == '.pdf': return 'pdf' - if ext in ['.png', '.jpg', '.jpeg', '.gif', '.bmp', '.tiff', '.webp', '.heic']: return 'image' - return 'unsupported' - - except Exception as e: - print(f"Dosya tipi belirlenirken hata: {e}") - return 'error' - -def process_invoice(filepath): - """Ana fatura işleme fonksiyonu.""" - filename = os.path.basename(filepath) - print(f"'{filename}' işleniyor...") - extracted_data = None - processing_status = "failed" - error_message = None - source_type = "unknown" - - file_type = get_file_type(filepath) - source_type = file_type # İşlem başarılı olursa bu tip kaydedilir - - try: - if file_type == 'xml': - print("XML dosyası tespit edildi, ayrıştırılıyor...") - extracted_data = parse_ubl_xml(filepath) - if not extracted_data: - error_message = "XML ayrıştırılamadı veya gerekli bilgiler bulunamadı." - - elif file_type in ['pdf', 'image']: - print(f"{file_type.upper()} dosyası tespit edildi, Gemini ile işleniyor...") - extracted_data = process_with_gemini(filepath, file_type) - if not extracted_data: - error_message = "Gemini API ile işlenemedi veya veri çıkarılamadı." - - else: - error_message = f"Desteklenmeyen dosya tipi: {file_type}" - source_type = "unsupported" - - - # Veri başarıyla çıkarıldıysa doğrula - if extracted_data: - print("Veri çıkarıldı, doğrulama yapılıyor...") - validated_data = validate_invoice_data(extracted_data) - processing_status = "completed" # Doğrulama sonrası tamamlandı sayalım - print("İşlem tamamlandı.") - final_data = { - "status": processing_status, - "source_type": source_type, - "filename": filename, - "extracted_data": validated_data, # Doğrulanmış veri - "error": None - } - else: - final_data = { - "status": processing_status, - "source_type": source_type, - "filename": filename, - "extracted_data": None, - "error": error_message - } - - # Sonucu JSON olarak kaydet - save_result(filename, final_data) - return final_data - - except Exception as e: - print(f"'{filename}' işlenirken beklenmedik hata: {e}") - error_result = { - "status": "error", - "source_type": source_type, - "filename": filename, - "extracted_data": None, - "error": f"İşlem sırasında genel bir hata oluştu: {str(e)}" - } - save_result(filename, error_result) # Hata durumunu da kaydedelim - return error_result \ No newline at end of file diff --git a/backend/fatrocu_app/services/gemini_processor.py b/backend/fatrocu_app/services/gemini_processor.py deleted file mode 100644 index 843f74f..0000000 --- a/backend/fatrocu_app/services/gemini_processor.py +++ /dev/null @@ -1,251 +0,0 @@ -# backend/fatrocu_app/services/gemini_processor.py - -import google.generativeai as genai -from flask import current_app -import os -import json -import traceback # Hata ayıklama için - -# --- Sabitler --- -# KULLANILACAK MODEL ADI - İSTEĞİN ÜZERİNE GÜNCELLENDİ -# DİKKAT: Bu model adının API anahtarınızla erişilebilir olduğundan emin olun! -# Eğer erişiminiz yoksa 'gemini-1.5-pro-latest' veya 'gemini-1.5-flash-latest' kullanın. -GEMINI_MODEL_NAME = 'models/gemini-2.5-pro-exp-03-25' # Google API'leri genellikle bu formatı kullanır (veya sadece 'gemini-2.5-pro-preview-0606') - Doğru adı kontrol et! - -# JSON formatında çıktı alınmasını sağlayacak detaylı prompt -# (Prompt içeriği aynı kalabilir) -EXTRACTION_PROMPT = """ -GÖREV: Sağlanan fatura belgesini (PDF veya Resim) dikkatlice analiz et. -ÇIKTI FORMATI: Aşağıda belirtilen alanları çıkarıp **sadece ve sadece geçerli bir JSON nesnesi** olarak yanıt ver. JSON dışında KESİNLİKLE başka bir metin ekleme (açıklama, giriş, sonuç yazma). - -İSTENEN ALANLAR (JSON Anahtarları ve Açıklamaları): -- "fatura_no": Faturanın/Belgenin üzerinde yazan benzersiz numara (Seri ve Sıra No birleşik olabilir, örn: "ABC20240000123"). Bulunamazsa null. -- "tarih": Faturanın düzenlenme tarihi (Format: "GG.AA.YYYY"). Bulunamazsa null. -- "satici_vkn_tckn": Satıcı firmanın/şahsın Vergi Kimlik Numarası (10 hane VKN) veya TC Kimlik Numarası (11 hane TCKN). Sadece rakamları içermeli. Bulunamazsa null. -- "firma_unvan": Satıcı firmanın/şahsın tam ticari ünvanı veya adı soyadı. Bulunamazsa null. -- "alici_vkn_tckn": Alıcı firmanın/şahsın VKN veya TCKN'si (varsa). Sadece rakamları içermeli. Bulunamazsa veya okunamazsa null. -- "alici_firma_unvan": Alıcı firmanın/şahsın tam ünvanı veya adı soyadı (varsa). Bulunamazsa veya okunamazsa null. -- "kdv_matrah": KDV hesaplamasına esas alınan tutar(lar)ın toplamı (KDV hariç genel ara toplam). Sayısal (float) değer olmalı. Bulunamazsa null. -- "kdv_orani": Faturadaki en yaygın veya ana KDV oranı (%). Sadece sayısal değer (örn: 20, 10, 1, 0). Birden fazla farklı oran varsa veya belirlenemiyorsa null. -- "kdv_tutari": Hesaplanan toplam KDV tutarı. Sayısal (float) değer olmalı. Bulunamazsa null. -- "genel_toplam": Faturanın tüm vergiler dahil ödenecek nihai toplam tutarı. Sayısal (float) değer olmalı. Bulunamazsa null. - -KURALLAR: -1. Yanıt SADECE JSON formatında olmalı. ```json ... ``` gibi işaretleyiciler KULLANMA. -2. Değerler bulunamazsa veya okunamıyorsa JSON içinde anahtarın değeri `null` olmalı. -3. Tarih "GG.AA.YYYY" formatında olmalı. Farklı formatta bulursan bu formata çevir. -4. VKN/TCKN alanları sadece rakamlardan oluşmalı. -5. Sayısal alanlar (matrah, tutar, toplam) ondalık ayracı olarak nokta (.) kullanılarak float tipinde olmalı. Binlik ayıracı KULLANMA. -6. Para birimi sembollerini (TL, $, € vb.) sayısal değerlere EKLEME. - -ÖRNEK GEÇERLİ JSON ÇIKTISI: -{ - "fatura_no": "FAT202412345", - "tarih": "21.07.2024", - "satici_vkn_tckn": "1234567890", - "firma_unvan": "SATICI ANONİM ŞİRKETİ", - "alici_vkn_tckn": "09876543210", - "alici_firma_unvan": "ALICI LİMİTED ŞİRKETİ", - "kdv_matrah": 2500.50, - "kdv_orani": 20, - "kdv_tutari": 500.10, - "genel_toplam": 3000.60 -} -""" - -# --- Yardımcı Fonksiyonlar --- -# (_configure_gemini, _upload_file_to_gemini, _parse_gemini_response fonksiyonları önceki mesajdaki gibi aynı kalır) -def _configure_gemini(): - """API anahtarını yükler ve Gemini kütüphanesini yapılandırır.""" - try: - api_key = current_app.config.get('GOOGLE_API_KEY') - if not api_key: - print("--- HATA (Gemini Configure): GOOGLE_API_KEY bulunamadı!") - raise ValueError("Google API Anahtarı yapılandırmada eksik.") - genai.configure(api_key=api_key) - print(f"--- Gemini Configure: API Anahtarı ile yapılandırma başarılı.") - return True - except Exception as e: - print(f"--- HATA (Gemini Configure): Yapılandırma sırasında hata: {e}") - return False - -def _upload_file_to_gemini(filepath): - """Verilen dosya yolundaki dosyayı Gemini Media API'ye yükler.""" - print(f"--- Gemini Upload: Dosya yükleniyor: {filepath}") - try: - # Dosyanın varlığını kontrol et - if not os.path.exists(filepath): - print(f"--- HATA (Gemini Upload): Dosya bulunamadı: {filepath}") - return None - - # Dosyayı yükle - uploaded_file = genai.upload_file(path=filepath) - - # Yükleme sonucunu kontrol et (SDK bazen hata vermeden None dönebilir) - if uploaded_file is None: - print(f"--- HATA (Gemini Upload): SDK dosyayı yükleyemedi veya None döndü: {filepath}") - return None - - print(f"--- Gemini Upload: Dosya başarıyla yüklendi: {uploaded_file.name} ({uploaded_file.uri})") - return uploaded_file - except Exception as e: - print(f"--- HATA (Gemini Upload): Dosya yüklenirken istisna oluştu: {e}") - traceback.print_exc() # Hatanın detayını yazdır - return None - -def _parse_gemini_response(response): - """Gemini API'den gelen yanıtı kontrol eder ve JSON'u ayrıştırır.""" - print("--- Gemini Parse: Yanıt ayrıştırılıyor...") - error_msg_prefix = "--- HATA (Gemini Parse):" # Hata mesajı ön eki - - # 1. Yanıt var mı kontrolü - if not response: - print(f"{error_msg_prefix} API'den yanıt alınamadı (response objesi boş).") - return None, "API'den yanıt alınamadı." - - # 2. Metin içeriği var mı ve JSON'a çevrilebilir mi KONTROLÜ (Engellenme kontrolünden önce!) - raw_text = None - try: - # Önce candidates listesini kontrol et (daha yeni yapı) - if response.candidates and hasattr(response.candidates[0].content, 'parts') and response.candidates[0].content.parts: - raw_text = response.candidates[0].content.parts[0].text - # Yoksa doğrudan .text özelliğini dene (eski yapı) - elif hasattr(response, 'text'): - raw_text = response.text - - if raw_text and raw_text.strip(): - print(f"--- Gemini Raw Text (Before Clean):\n{raw_text}\n---") - cleaned_text = raw_text.strip() - if cleaned_text.startswith("```json"): - cleaned_text = cleaned_text[7:] - if cleaned_text.endswith("```"): - cleaned_text = cleaned_text[:-3] - cleaned_text = cleaned_text.strip() - - print(f"--- Gemini Cleaned Text (For JSON Parse):\n{cleaned_text}\n---") - - try: - extracted_data = json.loads(cleaned_text) - print("--- Gemini Parse: JSON başarıyla ayrıştırıldı.") - # JSON başarılı ise engellenme durumuna bakmadan veriyi döndür! - # Model bazen veriyi üretip yine de bir güvenlik flag'i verebilir. - return extracted_data, None # Başarılı: veri ve None (hata yok) - except json.JSONDecodeError as json_e: - error_msg = f"JSON ayrıştırma hatası: {json_e}. Satır: {json_e.lineno}, Sütun: {json_e.colno}" - print(f"{error_msg_prefix} {error_msg}") - print(f"{error_msg_prefix} JSON'a çevrilemeyen metin:\n{cleaned_text}") - # JSON parse edilemezse, belki engellenmiştir, aşağıda kontrol edilecek. - else: - print(f"{error_msg_prefix} API yanıt metni boş veya bulunamadı.") - # Metin yoksa, engellenme nedenini kontrol et. - - except Exception as e: - error_msg = f"Yanıt metni işlenirken beklenmedik bir hata oluştu: {e}" - print(f"{error_msg_prefix} {error_msg}") - traceback.print_exc() - # Metin işlenemezse, engellenme nedenini kontrol et. - - # 3. Engellenme Kontrolü (Eğer yukarıda JSON parse edilemediyse veya metin boşsa buraya düşer) - finish_reason_str = "Bilinmiyor" - safety_ratings_str = "Bilinmiyor" - try: - if response.candidates: - candidate = response.candidates[0] - finish_reason = getattr(candidate, 'finish_reason', None) - # Gelen finish_reason'ı string'e çevirerek karşılaştıralım - finish_reason_str = str(finish_reason) if finish_reason is not None else "None" - safety_ratings = getattr(candidate, 'safety_ratings', []) - safety_ratings_str = str(safety_ratings) - - print(f"--- Gemini Parse: Finish Reason = {finish_reason_str}") # Loglama - - # Eğer finish_reason "STOP" değilse (veya '1' gibi garip bir değerse) bunu logla - if finish_reason_str != 'STOP': - final_error_msg = f"İçerik engellendi veya tamamlanamadı. Neden: {finish_reason_str}, Güvenlik: {safety_ratings_str}" - print(f"{error_msg_prefix} {final_error_msg}") - return None, final_error_msg - # Fallback for prompt_feedback (if needed) - elif hasattr(response, 'prompt_feedback') and response.prompt_feedback.block_reason: - final_error_msg = f"İçerik engellendi (prompt_feedback). Neden: {response.prompt_feedback.block_reason}" - print(f"{error_msg_prefix} {final_error_msg}") - return None, final_error_msg - - except (AttributeError, IndexError, TypeError) as e: - print(f"--- UYARI (Gemini Parse): Engellenme durumu kontrol edilirken hata: {e}. Yanıt yapısı beklenenden farklı olabilir.") - - # Eğer buraya kadar geldiyse ve JSON parse edilemediyse genel bir hata dönelim - default_error = "Veri çıkarılamadı (JSON ayrıştırılamadı veya yanıt metni boş/hatalı)." - print(f"{error_msg_prefix} {default_error}") - return None, default_error - - -# --- Ana Fonksiyon --- - -def process_with_gemini(filepath, file_type): - """ - Verilen PDF veya Resim dosyasını Gemini API kullanarak işler ve - çıkarılan fatura verilerini bir dictionary olarak döndürür. - Başarısız olursa None döner. - """ - print(f"--- Gemini Process START: Dosya: {os.path.basename(filepath)}, Tip: {file_type}") - extracted_data = None - error_reason = "Bilinmeyen Hata" # Başlangıç değeri - - try: - # 1. Gemini'yi Yapılandır - if not _configure_gemini(): - error_reason = "API yapılandırma hatası." - return None # Yapılandırma başarısızsa devam etme - - # 2. Dosyayı Gemini'ye Yükle - uploaded_file = _upload_file_to_gemini(filepath) - if not uploaded_file: - error_reason = "Dosya yükleme hatası." - return None # Dosya yüklenemezse devam etme - - # 3. Gemini Modelini Oluştur - print(f"--- Gemini Process: Model oluşturuluyor: {GEMINI_MODEL_NAME}") - # Doğru model adını kullandığınızdan emin olun! - model = genai.GenerativeModel(GEMINI_MODEL_NAME) - - # 4. API İsteğini Gönder - print(f"--- Gemini Process: API'ye istek gönderiliyor ('{uploaded_file.name}' ile)...") - try: - response = model.generate_content( - [EXTRACTION_PROMPT, uploaded_file], - generation_config=genai.types.GenerationConfig( - # JSON çıktısı için response_mime_type belirlemek daha güvenilir olabilir - # ANCAK BU ÖZELLİK HENÜZ TÜM MODELLERDE DESTEKLENMEYEBİLİR! - # response_mime_type="application/json", - temperature=0.1 # Daha tutarlı çıktılar için düşük sıcaklık - ) - ) - print(f"--- Gemini Process: API'den yanıt alındı.") - except Exception as api_e: - error_reason = f"Gemini API çağrısı sırasında hata: {api_e}" - print(f"--- HATA (Gemini Process - API Call): {error_reason}") - # API hatası yanıtını yazdırmak faydalı olabilir - if hasattr(api_e, 'response'): print(f"--- API Error Response: {api_e.response}") - traceback.print_exc() - return None # API hatası olursa devam etme - - # 5. Yanıtı Ayrıştır ve Veriyi Çıkar - extracted_data, error_reason = _parse_gemini_response(response) - if extracted_data: - print(f"--- Gemini Process SUCCESS: Veri başarıyla çıkarıldı.") - else: - print(f"--- Gemini Process FAILED: Veri çıkarılamadı. Neden: {error_reason}") - # extracted_data zaten None olacak - - except Exception as e: - error_reason = f"Gemini işleme sırasında genel bir istisna oluştu: {e}" - print(f"--- HATA (Gemini Process - Genel): {error_reason}") - traceback.print_exc() - extracted_data = None # Hata durumunda None döndüğünden emin ol - - finally: - # Yüklenen dosyayı silme... (önceki gibi) - pass - - print(f"--- Gemini Process END: Sonuç: {'Başarılı' if extracted_data else 'Başarısız'}, Neden: {error_reason if not extracted_data else 'Yok'}") - return extracted_data \ No newline at end of file diff --git a/backend/fatrocu_app/services/xml_parser.py b/backend/fatrocu_app/services/xml_parser.py deleted file mode 100644 index 209de4e..0000000 --- a/backend/fatrocu_app/services/xml_parser.py +++ /dev/null @@ -1,127 +0,0 @@ -from lxml import etree # lxml kütüphanesini kullanalım (pip install lxml) -import os - -# UBL-TR için yaygın kullanılan namespace'ler -# Bunlar fatura XML'inin başında tanımlanır ve değişebilir, -# Bu yüzden dinamik olarak almak veya wildcard kullanmak daha iyi olabilir. -# Şimdilik statik tanımlayalım, gerekirse geliştiririz. -NS_MAP = { - 'cac': "urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2", - 'cbc': "urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2", - # Diğer olası namespace'ler eklenebilir (ext, sig, etc.) -} - -def find_value(element, xpath_query): - """Verilen XPath sorgusu ile değeri bulur, yoksa None döner.""" - try: - # Birden fazla sonuç dönebilecek sorgular için ilkini alalım - result = element.xpath(xpath_query, namespaces=NS_MAP) - if result: - # Eğer sonuç bir element ise text içeriğini, değilse doğrudan sonucu al - return result[0].text if hasattr(result[0], 'text') else result[0] - return None - except Exception as e: - print(f"XPath hatası: {xpath_query}, Hata: {e}") - return None - -def parse_ubl_xml(xml_filepath): - """Bir UBL-TR XML faturasını ayrıştırır ve temel bilgileri dict olarak döner.""" - try: - tree = etree.parse(xml_filepath) - root = tree.getroot() - - data = {} - - # Temel Fatura Bilgileri - data['fatura_no'] = find_value(root, './cbc:ID/text()') - data['tarih'] = find_value(root, './cbc:IssueDate/text()') # YYYY-AA-GG formatında gelir genelde - # Tarihi GG.AA.YYYY formatına çevirebiliriz - if data['tarih']: - try: - from datetime import datetime - dt_obj = datetime.strptime(data['tarih'], '%Y-%m-%d') - data['tarih'] = dt_obj.strftime('%d.%m.%Y') - except ValueError: - print(f"XML Tarih formatı beklenenden farklı: {data['tarih']}") - # Ham veriyi bırakabilir veya hata verebiliriz - - # Satıcı Bilgileri (AccountingSupplierParty) - supplier_party = root.find('.//cac:AccountingSupplierParty/cac:Party', namespaces=NS_MAP) - if supplier_party is not None: - data['satici_vkn_tckn'] = find_value(supplier_party, './cac:PartyIdentification[cbc:ID/@schemeID="VKN"]/cbc:ID/text()') or \ - find_value(supplier_party, './cac:PartyIdentification[cbc:ID/@schemeID="TCKN"]/cbc:ID/text()') - data['firma_unvan'] = find_value(supplier_party, './cac:PartyName/cbc:Name/text()') or \ - find_value(supplier_party, './cac:Person/cbc:FirstName/text()') # Şahıs firması olabilir - - # Müşteri Bilgileri (AccountingCustomerParty) - Opsiyonel, bazen gerekli olmayabilir - customer_party = root.find('.//cac:AccountingCustomerParty/cac:Party', namespaces=NS_MAP) - if customer_party is not None: - data['alici_vkn_tckn'] = find_value(customer_party, './cac:PartyIdentification[cbc:ID/@schemeID="VKN"]/cbc:ID/text()') or \ - find_value(customer_party, './cac:PartyIdentification[cbc:ID/@schemeID="TCKN"]/cbc:ID/text()') - data['alici_firma_unvan'] = find_value(customer_party, './cac:PartyName/cbc:Name/text()') # Ekleme - - - # Toplamlar (LegalMonetaryTotal) - monetary_total = root.find('.//cac:LegalMonetaryTotal', namespaces=NS_MAP) - if monetary_total is not None: - data['matrah'] = find_value(monetary_total, './cbc:LineExtensionAmount/text()') # Vergisiz Toplam (Genellikle KDV Matrahıdır) - data['genel_toplam'] = find_value(monetary_total, './cbc:PayableAmount/text()') # Ödenecek Tutar - - # Vergiler (TaxTotal) - Birden fazla KDV oranı olabilir - tax_total_elements = root.findall('.//cac:TaxTotal', namespaces=NS_MAP) - kdv_details = [] - total_kdv_amount = 0.0 - primary_kdv_rate = None # İlk bulunan oranı alalım şimdilik - - for tax_total in tax_total_elements: - tax_amount_str = find_value(tax_total, './cbc:TaxAmount/text()') - if tax_amount_str: - try: - tax_amount = float(tax_amount_str) - total_kdv_amount += tax_amount - - # Alt kırılımları (TaxSubtotal) bul - for subtotal in tax_total.findall('.//cac:TaxSubtotal', namespaces=NS_MAP): - percent_str = find_value(subtotal, './/cac:TaxCategory/cbc:Percent/text()') - taxable_amount_str = find_value(subtotal, './cbc:TaxableAmount/text()') # Matrah (Bu vergi oranı için) - sub_tax_amount_str = find_value(subtotal, './cbc:TaxAmount/text()') # Tutar (Bu vergi oranı için) - - detail = { - 'oran': float(percent_str) if percent_str else None, - 'matrah': float(taxable_amount_str) if taxable_amount_str else None, - 'tutar': float(sub_tax_amount_str) if sub_tax_amount_str else None - } - kdv_details.append(detail) - if primary_kdv_rate is None and detail['oran'] is not None: - primary_kdv_rate = detail['oran'] - - except (ValueError, TypeError) as e: - print(f"Vergi tutarı/oranı dönüştürme hatası: {e}") - - - data['kdv_tutari'] = str(total_kdv_amount) # Toplam KDV tutarı - data['kdv_orani'] = str(primary_kdv_rate) if primary_kdv_rate is not None else None # Şimdilik ilk bulunan oran - data['kdv_detaylari'] = kdv_details # Tüm KDV kırılımları - - # Sayısal değerleri float'a çevirmeyi deneyelim (opsiyonel, doğrulama adımında da yapılabilir) - for key in ['matrah', 'genel_toplam', 'kdv_tutari']: - if data.get(key): - try: - data[key] = float(data[key]) - except (ValueError, TypeError): - print(f"XML'den gelen '{key}' değeri sayıya çevrilemedi: {data[key]}") - data[key] = None # Veya ham string bırakılabilir - - # Eksik anahtar alanları kontrol et (en azından fatura no, tarih, toplam olmalı) - if not all(data.get(k) for k in ['fatura_no', 'tarih', 'genel_toplam']): - print("XML'den temel fatura bilgileri (No, Tarih, Toplam) çıkarılamadı.") - return None # Veya kısmi veriyi döndür - - return data - - except etree.XMLSyntaxError as e: - print(f"XML dosyası bozuk veya hatalı: {xml_filepath}, Hata: {e}") - return None - except Exception as e: - print(f"XML ayrıştırılırken beklenmedik hata: {xml_filepath}, Hata: {e}") - return None \ No newline at end of file diff --git a/backend/fatrocu_app/utils.py b/backend/fatrocu_app/utils.py deleted file mode 100644 index e69de29..0000000 diff --git a/backend/requirements.txt b/backend/requirements.txt deleted file mode 100644 index a7922a3..0000000 --- a/backend/requirements.txt +++ /dev/null @@ -1,17 +0,0 @@ -Flask -python-dotenv -google-generativeai -requests -pandas -openpyxl -lxml - -PyMuPDF - -Pillow - -Werkzeug -click -itsdangerous -Jinja2 -MarkupSafe \ No newline at end of file diff --git a/backend/run.py b/backend/run.py deleted file mode 100644 index 354bac2..0000000 --- a/backend/run.py +++ /dev/null @@ -1,8 +0,0 @@ -from fatrocu_app import create_app - -app = create_app() - -if __name__ == '__main__': - # Debug modunu .flaskenv'den alacak, ama burada da belirtebiliriz - # Host='0.0.0.0' tüm ağ arayüzlerinden erişime izin verir (dikkatli kullanın) - app.run(host='127.0.0.1', port=5000) \ No newline at end of file diff --git a/backend/static/css/style.css b/backend/static/css/style.css deleted file mode 100644 index 97b03f9..0000000 --- a/backend/static/css/style.css +++ /dev/null @@ -1,244 +0,0 @@ -/* Genel Sıfırlamalar ve Temel Ayarlar */ -* { - box-sizing: border-box; - margin: 0; - padding: 0; -} - -body { - font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; - line-height: 1.6; - background-color: #f4f7f9; /* Açık gri arka plan */ - color: #333; - padding: 20px; - display: flex; /* Container'ı ortalamak için */ - justify-content: center; -} - -.container { - max-width: 900px; /* Maksimum genişlik */ - width: 100%; - background-color: #ffffff; /* Beyaz container arka planı */ - padding: 30px; - border-radius: 8px; - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08); /* Hafif gölge */ -} - -h1 { - text-align: center; - color: #2c3e50; /* Koyu mavi başlık */ - margin-bottom: 10px; -} - -h2 { - color: #34495e; /* Biraz daha açık mavi */ - margin-top: 30px; - margin-bottom: 15px; - border-bottom: 1px solid #ecf0f1; /* İnce ayırıcı çizgi */ - padding-bottom: 5px; -} - -p { - margin-bottom: 15px; - color: #555; -} - -/* Kart Görünümü */ -.card { - background-color: #fff; /* Kartlar da beyaz olsun */ - border: 1px solid #e0e4e8; /* Çok hafif kenarlık */ - border-radius: 6px; - padding: 20px; - margin-bottom: 25px; - /* box-shadow: 0 2px 5px rgba(0, 0, 0, 0.05); */ /* İsteğe bağlı iç gölge */ -} - -/* Yükleme Alanı */ -.upload-area { - border: 2px dashed #bdc3c7; /* Kesikli kenarlık */ - padding: 25px; - text-align: center; - border-radius: 6px; - background-color: #f8f9fa; /* Çok hafif gri arka plan */ - margin-bottom: 20px; - transition: background-color 0.2s ease; -} -.upload-area:hover { - background-color: #f1f3f5; -} - -#upload-form { - display: flex; - flex-direction: column; /* Öğeleri alt alta diz */ - align-items: center; /* Ortala */ -} - -/* Özel Dosya Seçim Stili */ -.file-label { - display: inline-block; /* Buton gibi davranması için */ - padding: 12px 20px; - background-color: #3498db; /* Mavi arka plan */ - color: white; - border: none; - border-radius: 5px; - cursor: pointer; - font-size: 1rem; - margin-bottom: 15px; /* Butonla arasına boşluk */ - transition: background-color 0.2s ease; -} - -.file-label:hover { - background-color: #2980b9; -} - -#file-input { - display: none; /* Gerçek input'u gizle */ -} - -/* Genel Buton Stili */ -.button { - padding: 12px 25px; - border: none; - border-radius: 5px; - cursor: pointer; - font-size: 1rem; - font-weight: 500; - transition: background-color 0.2s ease, transform 0.1s ease; - text-decoration: none; /* Linkler için */ - display: inline-block; /* Linkler için */ - text-align: center; -} - -.button:hover { - opacity: 0.9; - transform: translateY(-1px); /* Hafif yukarı kalkma efekti */ -} - -.button.primary { - background-color: #2ecc71; /* Yeşil ana işlem butonu */ - color: white; -} -.button.primary:hover { - background-color: #27ae60; -} - -.button.secondary { /* "Sonuçları Gör" butonu için */ - background-color: #ecf0f1; - color: #34495e; - padding: 6px 12px; /* Daha küçük buton */ - font-size: 0.9rem; -} -.button.secondary:hover { - background-color: #dadedf; -} - -/* Sonuç Alanı */ -.result-area { - background-color: #fdfdfe; /* Hafif farklı arka plan */ -} - -#upload-status { - margin-top: 10px; - padding: 15px; - border: 1px solid #eee; - border-radius: 5px; - min-height: 50px; /* Boşken de yer kaplasın */ - word-wrap: break-word; /* Uzun metinleri kır */ -} - -#upload-status p { - margin-bottom: 10px; - font-weight: 500; -} -/* JS'nin eklediği color inline stillerini yakalayalım (ideal olmasa da pratik) */ -#upload-status[style*="color: red"] { - border-left: 4px solid #e74c3c; /* Kırmızı kenar */ - background-color: #fceded; -} -#upload-status[style*="color: green"] { - border-left: 4px solid #2ecc71; /* Yeşil kenar */ - background-color: #eafaf1; -} -#upload-status[style*="color: orange"] { - border-left: 4px solid #f39c12; /* Turuncu kenar */ - background-color: #fef5e7; -} - -#upload-status pre { - background-color: #ecf0f1; /* JSON arka planı */ - padding: 15px; - border-radius: 4px; - font-family: 'Courier New', Courier, monospace; /* Monospace font */ - font-size: 0.9rem; - overflow-x: auto; /* Yatayda kaydırma */ - white-space: pre-wrap; /* Satırları koru ama taşırma */ - word-wrap: break-word; /* Kelimeleri kır */ - border: 1px solid #dce1e4; -} - -/* İşlenen Dosyalar Listesi */ -#processed-files { - list-style: none; /* Madde işaretlerini kaldır */ - padding: 0; -} - -#processed-files li { - border-bottom: 1px solid #ecf0f1; - padding: 12px 5px; - display: flex; - justify-content: space-between; /* Öğeleri iki uca yasla */ - align-items: center; /* Dikeyde ortala */ - font-size: 0.95rem; -} - -#processed-files li:last-child { - border-bottom: none; /* Son öğenin alt çizgisini kaldır */ -} - -#processed-files button, -#processed-files a { - margin-left: 10px; /* Butonlar/linkler arasına boşluk */ -} - -/* Excel linkini de buton gibi gösterelim */ -#processed-files a { - background-color: #95a5a6; - color: white; - padding: 6px 12px; - font-size: 0.9rem; - border-radius: 5px; - text-decoration: none; -} -#processed-files a:hover { - background-color: #7f8c8d; -} - -/* Küçük Ekranlar İçin (Basit Ayar) */ -@media (max-width: 600px) { - body { - padding: 10px; - } - .container { - padding: 15px; - } - h1 { - font-size: 1.8rem; - } - h2 { - font-size: 1.3rem; - } - .button, .file-label { - padding: 10px 15px; - font-size: 0.95rem; - } - #processed-files li { - flex-direction: column; /* Mobil'de alt alta diz */ - align-items: flex-start; /* Sola yasla */ - } - #processed-files li > span { /* Dosya adını span içine alırsak */ - margin-bottom: 8px; - } - #processed-files div.actions { /* Butonları bir div içine alırsak */ - margin-top: 8px; - } -} \ No newline at end of file diff --git a/backend/static/js/api.js b/backend/static/js/api.js deleted file mode 100644 index 5933d8e..0000000 --- a/backend/static/js/api.js +++ /dev/null @@ -1,63 +0,0 @@ -const API_BASE_URL = '/api'; // Flask aynı domain'de çalıştığı için göreceli path yeterli - -async function uploadFile(file) { - const formData = new FormData(); - formData.append('file', file); - - try { - const response = await fetch(`${API_BASE_URL}/upload`, { - method: 'POST', - body: formData, - }); - - const result = await response.json(); // Backend'den gelen JSON - - // === DÖNÜŞ DEĞERİNİ LOGLA === - console.log("uploadFile: Backend'den gelen JSON:", result); - // ============================ - - // ÖNEMLİ: Burada doğrudan 'result' mı dönüyor? - // Eğer response.ok değilse farklı bir şey mi dönüyor? Kontrol edelim. - if (!response.ok) { - console.error("Yükleme hatası (API):", result.error || `HTTP ${response.status}`); - // Hata durumunda da backend'den gelen JSON'u döndürelim ki displayProcessingResult işleyebilsin - return result; // Hata JSON'unu döndür - // Önceki hali: return { success: false, error: result.error || `Sunucu hatası: ${response.status}` }; idi, bu yapı farklıydı. - } - - console.log("Yükleme başarılı (API):", result); - return result; // Başarı durumunda backend'den gelen JSON'u döndür - // Önceki hali: return { success: true, message: result.message, filename: result.filename }; idi, bu yapı farklıydı. - - } catch (error) { - console.error("Ağ veya fetch hatası:", error); - // Ağ hatası durumunda displayProcessingResult'ın anlayacağı bir yapı döndür - return { status: 'error', error: 'Sunucuya bağlanılamadı veya bir ağ hatası oluştu.', filename: file.name }; - } -} - -async function getResults(filename) { - try { - const response = await fetch(`${API_BASE_URL}/results/${filename}`); - const result = await response.json(); - - if (!response.ok) { - console.error("Sonuç alma hatası:", result.error || `HTTP ${response.status}`); - return { success: false, error: result.error || `Sunucu hatası: ${response.status}` }; - } - console.log("Sonuçlar alındı:", result.data); - return { success: true, data: result.data }; - - } catch (error) { - console.error("Ağ veya fetch hatası:", error); - return { success: false, error: 'Sunucuya bağlanılamadı veya bir ağ hatası oluştu.' }; - } -} - -// Henüz UI'da kullanmıyoruz ama yapısı hazır olsun -function getExportUrl(filename) { - return `${API_BASE_URL}/export/${filename}`; // Doğrudan indirme linki -} - -// UI'dan fonksiyonları export etmeye gerek yok, direkt main.js'de kullanacağız -// export { uploadFile, getResults, getExportUrl }; // Eğer modül olarak kullanılacaksa \ No newline at end of file diff --git a/backend/static/js/main.js b/backend/static/js/main.js deleted file mode 100644 index 024fdf3..0000000 --- a/backend/static/js/main.js +++ /dev/null @@ -1,47 +0,0 @@ -document.addEventListener('DOMContentLoaded', () => { - console.log("Fatrocu frontend başlatıldı."); - - const uploadForm = document.getElementById('upload-form'); - const fileInput = document.getElementById('file-input'); - - if (uploadForm && fileInput) { - uploadForm.addEventListener('submit', async (event) => { - event.preventDefault(); - const file = fileInput.files[0]; - if (!file) { - showUploadStatus("Lütfen bir dosya seçin.", true); - return; - } - - showUploadStatus("Dosya yükleniyor ve işleniyor...", false); - - const uploadResult = await uploadFile(file); // uploadFile artık işlenmiş sonucu dönüyor - - // === VERİYİ LOGLA === - console.log("displayProcessingResult'a gönderilen veri:", uploadResult); - // ===================== - - // Gelen sonucu doğrudan gösterelim - displayProcessingResult(uploadResult); // uploadResult tüm JSON yanıtını içerir - - // Sadece başarılı ise listeye ekleyelim - if (uploadResult && uploadResult.status && uploadResult.status !== 'error' && uploadResult.status !== 'failed' && uploadResult.filename) { - addProcessedFileToList(uploadResult.filename); - } - - // Formu temizle - uploadForm.reset(); - }); - } else { - console.error("Yükleme formu veya dosya girişi bulunamadı."); - } - - // Sayfa yüklendiğinde belki mevcut işlenmiş dosyaları listeleyebiliriz - // (Bunun için backend'de bir endpoint daha gerekir) - // loadProcessedFiles(); -}); - -// function loadProcessedFiles() { -// // TODO: Backend'den işlenmiş dosyaların listesini alıp -// // addProcessedFileToList ile ekleyen fonksiyon -// } \ No newline at end of file diff --git a/backend/static/js/ui.js b/backend/static/js/ui.js deleted file mode 100644 index 9120479..0000000 --- a/backend/static/js/ui.js +++ /dev/null @@ -1,98 +0,0 @@ -// backend/static/js/ui.js - -function showUploadStatus(message, isError = false) { - const statusDiv = document.getElementById('upload-status'); - if (statusDiv) { - statusDiv.textContent = message; - statusDiv.style.color = isError ? 'red' : 'green'; - } -} - -// === displayProcessingResult fonksiyonunu BURAYA, DIŞARIYA TAŞIDIK === -function displayProcessingResult(resultData) { - const statusDiv = document.getElementById('upload-status'); // Sonucu göstereceğimiz alan - if (!statusDiv) return; - - // Sonuçları temiz bir şekilde gösterelim - statusDiv.innerHTML = ''; // Önceki içeriği temizle - - if (resultData && resultData.filename) { - // Dosya adını ve durumu ekle - const infoP = document.createElement('p'); - infoP.textContent = `Dosya: ${resultData.filename} - Durum: ${resultData.status}`; - statusDiv.appendChild(infoP); - - if (resultData.status === 'completed' || resultData.status === 'verified') { - statusDiv.style.color = 'green'; - // Başarılı ise çıkarılan veriyi göster - if (resultData.extracted_data) { - const dataPre = document.createElement('pre'); - // JSON'u formatlı string'e çevir - dataPre.textContent = JSON.stringify(resultData.extracted_data, null, 2); - statusDiv.appendChild(dataPre); - // TODO: Burayı daha sonra düzenlenebilir form alanlarına çevirebiliriz. - } else { - const noDataP = document.createElement('p'); - noDataP.textContent = "Veri bulunamadı."; - statusDiv.appendChild(noDataP); - } - } else { - // Hata durumu - statusDiv.style.color = 'red'; - const errorP = document.createElement('p'); - errorP.textContent = `Hata: ${resultData.error || 'Bilinmeyen bir hata oluştu.'}`; - statusDiv.appendChild(errorP); - } - } else { - statusDiv.textContent = "Geçersiz veya eksik sonuç verisi alındı."; - statusDiv.style.color = 'orange'; - } -} -// =================================================================== - -function addProcessedFileToList(filename) { - const list = document.getElementById('processed-files'); - if (list) { - // Liste boşsa "Liste boş" yazısını kaldıralım - const placeholder = list.querySelector('.list-placeholder'); - if (placeholder) { - list.removeChild(placeholder); - } - - // Aynı dosya adıyla zaten bir liste öğesi var mı kontrol et - if (document.getElementById(`file-item-${filename.replace(/\./g, '-')}`)) { // Noktaları da replace edelim ID için - console.log(`${filename} zaten listede.`); - return; - } - - const listItem = document.createElement('li'); - listItem.id = `file-item-${filename.replace(/\./g, '-')}`; - const safeFilename = encodeURIComponent(filename); - - // Dosya adı ve aksiyonları ayıralım (mobil için daha iyi olabilir) - listItem.innerHTML = ` - ${filename} -
- - Excel İndir -
- `; - list.appendChild(listItem); - } else { - console.error("#processed-files listesi bulunamadı."); - } -} - -async function viewResults(filename) { - // ... (Bu fonksiyon aynı kalabilir) ... - showUploadStatus(`'${filename}' için sonuçlar getiriliyor...`); - const result = await getResults(filename); - if (result.success && result.data) { - displayProcessingResult(result.data); - } else { - showUploadStatus(`Sonuçlar alınamadı: ${result.error || 'Sunucu hatası'}`, true); - } -} - -// Not: getExportUrl fonksiyonu api.js içinde tanımlıysa burada tekrar tanımlamaya gerek yok. -// Eğer ui.js içinde tanımlıysa, sadece bir tanım kalmalı. api.js içinde kalması daha mantıklı. \ No newline at end of file diff --git a/backend/templates/index.html b/backend/templates/index.html deleted file mode 100644 index a1a40a5..0000000 --- a/backend/templates/index.html +++ /dev/null @@ -1,58 +0,0 @@ - - - - - - Fatrocu - Akıllı Fatura İşleme - - - - - - -
-

Fatrocu v2

-

Faturalarınızı (XML, PDF, Resim) yükleyerek verileri otomatik çıkarın ve Excel'e aktarın.

- - -
-

Dosya Yükle

- -
-
- - - -
-
-
- - -
-

İşlem Sonucu

-
- -

Henüz bir dosya işlenmedi.

-
-
- - -
-

Son İşlenen Dosyalar

-
    - -
  • Liste boş.
  • -
-
- -
- - - - - - - \ No newline at end of file diff --git a/components/AlertMessage.tsx b/components/AlertMessage.tsx new file mode 100644 index 0000000..b27af77 --- /dev/null +++ b/components/AlertMessage.tsx @@ -0,0 +1,72 @@ + +import React from 'react'; +import { AlertType } from '../types'; + +interface AlertMessageProps { + message: string; + type: AlertType; + onClose?: () => void; +} + +export const AlertMessage: React.FC = ({ message, type, onClose }) => { + const baseClasses = "p-4 mb-4 text-sm rounded-lg shadow-md flex items-center justify-between"; + let specificClasses = ""; + let Icon: React.ReactNode; + + switch (type) { + case 'success': + specificClasses = "bg-green-700/30 text-green-300 border border-green-600"; + Icon = ( + + + + ); + break; + case 'error': + specificClasses = "bg-red-700/30 text-red-300 border border-red-600"; + Icon = ( + + + + ); + break; + case 'info': + specificClasses = "bg-blue-700/30 text-blue-300 border border-blue-600"; + Icon = ( + + + + ); + break; + default: // warning + specificClasses = "bg-yellow-700/30 text-yellow-300 border border-yellow-600"; + Icon = ( + + + + ); + break; + } + + return ( +
+
+ {Icon} + {message} +
+ {onClose && ( + + )} +
+ ); +}; diff --git a/components/CheckView.tsx b/components/CheckView.tsx new file mode 100644 index 0000000..caeffc1 --- /dev/null +++ b/components/CheckView.tsx @@ -0,0 +1,330 @@ +import React, { useState, useEffect, useMemo, useRef } from 'react'; +import { ProcessedInvoice, ExtractedInvoiceFields, FileProcessingStatus, AlertType, BoundingBox } from '../types'; +import { downloadInvoicesAsCsv } from '../services/apiService'; +import { AlertMessage } from './AlertMessage'; + +interface FilePreviewWithHighlightProps { + invoice: ProcessedInvoice; + editedData?: ExtractedInvoiceFields | null; + hoveredFieldKey?: keyof ExtractedInvoiceFields | null; +} + +const FilePreviewWithHighlight: React.FC = ({ invoice, editedData, hoveredFieldKey }) => { + const previewContainerRef = useRef(null); + const imageRef = useRef(null); + const [containerSize, setContainerSize] = useState({ width: 0, height: 0 }); + const [renderedImgPos, setRenderedImgPos] = useState({ width: 0, height: 0, x: 0, y: 0 }); + + const getPreviewSrc = () => { + // Prefer the transient, efficient Object URL if it's available from the current session + if (invoice.fileDataUrl) { + return invoice.fileDataUrl; + } + // Otherwise, reconstruct the Data URI from the persisted base64 content + if (invoice.fileContentBase64) { + return `data:${invoice.fileType};base64,${invoice.fileContentBase64}`; + } + return ''; // Return empty if no viewable source is found + }; + + const previewSrc = getPreviewSrc(); + + useEffect(() => { + const container = previewContainerRef.current; + if (!container) return; + + const calculateSizes = () => { + const image = imageRef.current; + const containerWidth = container.offsetWidth; + const containerHeight = container.offsetHeight; + setContainerSize({ width: containerWidth, height: containerHeight }); + + if (image && image.naturalWidth > 0 && invoice.fileType.startsWith('image/')) { + const imgNaturalWidth = image.naturalWidth; + const imgNaturalHeight = image.naturalHeight; + const containerRatio = containerWidth / containerHeight; + const imgRatio = imgNaturalWidth / imgNaturalHeight; + + let renderedWidth, renderedHeight, x, y; + if (imgRatio > containerRatio) { // Image is wider than container, letterboxed + renderedWidth = containerWidth; + renderedHeight = renderedWidth / imgRatio; + x = 0; + y = (containerHeight - renderedHeight) / 2; + } else { // Image is taller or same ratio, pillarboxed + renderedHeight = containerHeight; + renderedWidth = renderedHeight * imgRatio; + y = 0; + x = (containerWidth - renderedWidth) / 2; + } + setRenderedImgPos({ width: renderedWidth, height: renderedHeight, x, y }); + } else { + // For iframes/other, assume it fills the container + setRenderedImgPos({ width: containerWidth, height: containerHeight, x: 0, y: 0 }); + } + }; + + const image = imageRef.current; + if (image) { + image.onload = calculateSizes; + if(image.complete) calculateSizes(); + } else { + calculateSizes(); + } + + const resizeObserver = new ResizeObserver(calculateSizes); + resizeObserver.observe(container); + return () => resizeObserver.disconnect(); + }, [previewSrc, invoice.fileType]); + + if (!previewSrc) return

Önizleme mevcut değil.

; + + const getHighlightBox = () => { + if (!hoveredFieldKey || !editedData?.[hoveredFieldKey]?.boundingBox || containerSize.width === 0) { + return null; + } + const box = editedData[hoveredFieldKey]!.boundingBox as BoundingBox; + + const basePos = invoice.fileType.startsWith('image/') ? renderedImgPos : { width: containerSize.width, height: containerSize.height, x: 0, y: 0 }; + + const left = (box.x_min * basePos.width) + basePos.x; + const top = (box.y_min * basePos.height) + basePos.y; + const width = (box.x_max - box.x_min) * basePos.width; + const height = (box.y_max - box.y_min) * basePos.height; + + return ( +
+ ); + }; + + const Previewer = () => { + if (invoice.fileType.startsWith('image/')) { + return {`Önizleme:; + } + if (invoice.fileType === 'application/pdf' || invoice.fileType.includes('xml')) { + const bgClass = invoice.fileType.includes('xml') ? 'bg-white' : ''; + return