diff --git a/api.php b/api.php index 4c3d1a2..9e37142 100644 --- a/api.php +++ b/api.php @@ -42,7 +42,7 @@ function get_csv_sample($filePath) { return implode('\n', $sample); } -function call_openai_api($inputSample, $outputSample) { +function call_openai_api($inputSample, $outputSample, $customInstructions) { $apiKey = getenv('OPENAI_API_KEY'); if (!$apiKey) { return ['error' => 'OpenAI API key not configured.']; @@ -50,7 +50,10 @@ function call_openai_api($inputSample, $outputSample) { $prompt = "I have two CSV files. I want to convert the input file to the output file format.\n\n"; $prompt .= "Here is a sample of the input CSV file:\n---\n" . $inputSample . "\n---\n\n"; - $prompt .= "Here is a sample of the output CSV file:\n---\n" . $outputSample . "\n---\n\n"; + $prompt .= "Here is a sample of the output CSV file:\n---" . $outputSample . "---\n\n"; + if (!empty($customInstructions)) { + $prompt .= "Additionally, here are some custom instructions to follow:\n---" . $customInstructions . "---\n\n"; + } $prompt .= "Please generate a PHP script that contains a single function `convert_csv($input_file_path, $output_file_path)`.\n"; $prompt .= "This function should read the data from the `$input_file_path`, transform it into the format of the output sample, and write the result to `$output_file_path`.\n"; $prompt .= "The script should handle CSV headers correctly. Do not include any explanations, just the raw PHP code starting with ` true, 'mappings' => $mappings]); + exit; +} + if ($action === 'build_mapping') { if (!isset($_FILES['inputSample']) || !isset($_FILES['outputSample'])) { echo json_encode(['success' => false, 'error' => 'Input and output sample files are required.']); @@ -114,13 +143,14 @@ if ($action === 'build_mapping') { $inputSampleContent = get_csv_sample($inputTmpPath); $outputSampleContent = get_csv_sample($outputTmpPath); + $customInstructions = $_POST['customInstructions'] ?? ''; if ($inputSampleContent === null || $outputSampleContent === null) { echo json_encode(['success' => false, 'error' => 'Failed to read sample files.']); exit; } - $result = call_openai_api($inputSampleContent, $outputSampleContent); + $result = call_openai_api($inputSampleContent, $outputSampleContent, $customInstructions); if (isset($result['error'])) { echo json_encode(['success' => false, 'error' => $result['error'], 'details' => $result['details'] ?? null]); @@ -131,10 +161,20 @@ if ($action === 'build_mapping') { if (!is_dir($mappingDir)) { mkdir($mappingDir, 0775, true); } - $mappingFile = 'mappings/mapping_' . time() . '.php'; - file_put_contents($mappingFile, $result['code']); - echo json_encode(['success' => true, 'mapping_file' => $mappingFile]); + $mappingName = $_POST['mappingName'] ?? ''; + if (empty($mappingName)) { + $inputName = pathinfo($_FILES['inputSample']['name'], PATHINFO_FILENAME); + $outputName = pathinfo($_FILES['outputSample']['name'], PATHINFO_FILENAME); + $mappingName = $inputName . '-to-' . $outputName; + } + + $mappingFile = sanitize_filename($mappingName); + $mappingPath = $mappingDir . '/' . $mappingFile; + + file_put_contents($mappingPath, $result['code']); + + echo json_encode(['success' => true, 'mapping_file' => 'mappings/' . $mappingFile]); exit; } diff --git a/assets/js/main.js b/assets/js/main.js index ad989d5..c26aec4 100644 --- a/assets/js/main.js +++ b/assets/js/main.js @@ -8,8 +8,8 @@ document.addEventListener('DOMContentLoaded', function () { const fullInputFile = document.getElementById('fullInputFile'); const convertBtn = document.getElementById('convertBtn'); + const existingMappings = document.getElementById('existingMappings'); - const convertSection = document.getElementById('convertSection'); const convertResult = document.getElementById('convertResult'); const mappingFileInput = document.getElementById('mappingFile'); @@ -17,13 +17,41 @@ document.addEventListener('DOMContentLoaded', function () { buildMappingBtn.disabled = !(inputSample.files.length > 0 && outputSample.files.length > 0); } - function checkConvertFile() { - convertBtn.disabled = !(fullInputFile.files.length > 0); + function checkConvertState() { + const mappingSelected = mappingFileInput.value !== ''; + const fileSelected = fullInputFile.files.length > 0; + convertBtn.disabled = !(mappingSelected && fileSelected); + } + + function loadMappings() { + fetch('api.php?action=list_mappings') + .then(response => response.json()) + .then(data => { + if (data.success && data.mappings.length > 0) { + existingMappings.innerHTML = ''; + data.mappings.forEach(mapping => { + const option = document.createElement('option'); + option.value = `mappings/${mapping}`; + option.textContent = mapping; + existingMappings.appendChild(option); + }); + } else { + existingMappings.innerHTML = ''; + } + }) + .catch(error => { + console.error('Error loading mappings:', error); + existingMappings.innerHTML = ''; + }); } inputSample.addEventListener('change', checkMappingFiles); outputSample.addEventListener('change', checkMappingFiles); - fullInputFile.addEventListener('change', checkConvertFile); + fullInputFile.addEventListener('change', checkConvertState); + existingMappings.addEventListener('change', function() { + mappingFileInput.value = this.value; + checkConvertState(); + }); mappingForm.addEventListener('submit', function (e) { e.preventDefault(); @@ -42,7 +70,12 @@ document.addEventListener('DOMContentLoaded', function () { if (data.success) { alert('Mapping built successfully!'); mappingFileInput.value = data.mapping_file; - convertSection.classList.remove('d-none'); + loadMappings(); // Refresh the list + // Select the new mapping in the dropdown + setTimeout(() => { + existingMappings.value = data.mapping_file; + checkConvertState(); + }, 500); // Give it a moment to reload } else { let errorMsg = 'Error building mapping: ' + data.error; if(data.details) { @@ -89,6 +122,10 @@ document.addEventListener('DOMContentLoaded', function () { .finally(() => { convertBtn.disabled = false; convertBtn.innerHTML = 'Convert'; + checkConvertState(); }); }); -}); \ No newline at end of file + + // Initial load + loadMappings(); +}); diff --git a/index.php b/index.php index 97524a7..b0bd09c 100644 --- a/index.php +++ b/index.php @@ -29,19 +29,33 @@
-
-
-
-

1Build Mapping

+
+
+
+

1Build New Mapping

Provide input and output sample files (CSV or XLSX) to generate a mapping configuration using AI.

-
-
- - + +
+
+
+ + +
+
+
+
+ + +
+
- - + + +
+
+ +
-
-
-
-

2Convert Data

-

Use the generated mapping to convert your full input file.

- +
+ +
+ +
+
+
+
+

2Convert Data with Mapping

+ -
- - +
+
+
+ + +
+
+
+
+ + +
+
- diff --git a/mappings/mapping_1760650861.php b/mappings/mapping_1760650861.php new file mode 100644 index 0000000..ef35c9c --- /dev/null +++ b/mappings/mapping_1760650861.php @@ -0,0 +1,108 @@ + $name) { + $name = trim($strip_bom($name)); + $index[$name] = $i; + } + + $get = function ($row, $name) use ($index) { + if (!isset($index[$name])) return ''; + $i = $index[$name]; + return isset($row[$i]) ? trim($row[$i]) : ''; + }; + + $out = @fopen($outputFile, 'w'); + if ($out === false) { + fclose($in); + return false; + } + + // Output header using semicolon separator + $outHeader = [ + 'Data księgowania', + 'Nadawca / Odbiorca', + 'Adres nadawcy / odbiorcy', + 'Rachunek źródłowy', + 'Rachunek docelowy', + 'Tytuł', + 'Kwota' + ]; + fputcsv($out, $outHeader, ';'); + + while (($row = fgetcsv($in, 0, ',', '"', '\\')) !== false) { + // Skip completely empty rows + $nonEmpty = false; + foreach ($row as $v) { + if ($v !== null && $v !== '') { $nonEmpty = true; break; } + } + if (!$nonEmpty) continue; + + $date = $get($row, 'Date completed (UTC)'); + if ($date === '') { + $date = $get($row, 'Date started (UTC)'); + } + + $description = $get($row, 'Description'); + $payer = $get($row, 'Payer'); + $counterparty = $description !== '' ? $description : $payer; + + $sourceAccount = $get($row, 'Account'); + + $benefIban = $get($row, 'Beneficiary IBAN'); + $benefAccNo = $get($row, 'Beneficiary account number'); + $destAccount = $benefIban !== '' ? $benefIban : $benefAccNo; + + $reference = $get($row, 'Reference'); + $type = $get($row, 'Type'); + $title = $reference !== '' ? $reference : str_replace('_', ' ', $type); + if ($title === '' && $counterparty !== '') { + $title = $counterparty; + } + + // Prefer Amount, then Total amount, then Orig amount + $amount = $get($row, 'Amount'); + if ($amount === '') $amount = $get($row, 'Total amount'); + if ($amount === '') $amount = $get($row, 'Orig amount'); + + // Normalize decimal separator just in case + $amount = str_replace(',', '.', $amount); + + $outputRow = [ + $date, + $counterparty, + '', + $sourceAccount, + $destAccount, + $title, + $amount + ]; + + fputcsv($out, $outputRow, ';'); + } + + fclose($in); + fclose($out); + return true; +} \ No newline at end of file diff --git a/outputs/converted_1760650923.csv b/outputs/converted_1760650923.csv new file mode 100644 index 0000000..8347ce6 --- /dev/null +++ b/outputs/converted_1760650923.csv @@ -0,0 +1,47 @@ +"Data księgowania";"Nadawca / Odbiorca";"Adres nadawcy / odbiorcy";"Rachunek źródłowy";"Rachunek docelowy";Tytuł;Kwota +2025-08-23;"Web Innovative Softwar";;"EUR Card EUR";;"CARD PAYMENT";-18.48 +2025-08-23;"Web Innovative Softwar";;"EUR Card EUR";;"CARD PAYMENT";-0.24 +2025-08-23;"Web Innovative Softwar";;"EUR Card EUR";;"CARD PAYMENT";-34.35 +2025-08-23;"Web Innovative Softwar";;"EUR Card EUR";;"CARD PAYMENT";-0.24 +2025-08-23;"Discord* 12xserverboos";;"EUR Card EUR";;"CARD PAYMENT";-44.54 +2025-08-22;"To Flatlogic Poland";;"EUR Main";PL51109025900000000161061759;"Przelew na wlasne konto";-50.00 +2025-08-22;Cloudflare;;"EUR Card EUR";;"CARD PAYMENT";-8.03 +2025-08-20;Cloudflare;;"EUR Card EUR";;"CARD PAYMENT";-25.71 +2025-08-20;"Openai *chatgpt Subscr";;"EUR Card EUR";;"CARD PAYMENT";-186.18 +2025-08-20;Calendly;;"EUR Card EUR";;"CARD PAYMENT";-42.24 +2025-08-20;"Facebk *whhzuwgcy2";;"EUR Card EUR";;"CARD PAYMENT";-25.74 +2025-08-19;"Main · EUR → Main · USD";;"EUR Main";;EXCHANGE;-4115.97 +2025-08-19;"Card EUR · EUR → Card USD · USD";;"EUR Card EUR";;EXCHANGE;-250.15 +2025-08-19;"To Card EUR";;"EUR Main";;TRANSFER;-1300.00 +2025-08-19;"From Main";;"EUR Card EUR";;TRANSFER;1300.00 +2025-08-14;"Main · EUR → Main · PLN";;"EUR Main";;"Transfer to Main to recover negative balance";-41.07 +2025-08-14;"Money added from MOGANADEN MOORGHEN";;"EUR Main";;"July 2025 software development";6468.75 +2025-08-12;Ing*meeting15.com;;"EUR Card EUR";;"CARD PAYMENT";-59.48 +2025-08-07;"Google*cloud Qzxlb5";;"EUR Card EUR";;"CARD PAYMENT";-417.23 +2025-08-07;"Google Cloud Sv93dn";;"EUR Card EUR";;"CARD PAYMENT";-500.00 +2025-08-06;"Card USD · USD → Card EUR · EUR";;"EUR Card EUR";;EXCHANGE;1000.00 +2025-08-04;"Main · EUR → Card USD · USD";;"EUR Main";;EXCHANGE;-6007.69 +2025-08-04;"Main · EUR → Card USD · USD";;"EUR Main";;EXCHANGE;-3460.84 +2025-08-04;"Money added from FLATLOGIC POLAND SPOLKA Z OGRANICZONA ODPOWIEDZIALNOSCIA";;"EUR Main";;"Przelew na wlasny rachunek";8800.00 +2025-08-04;"Openai *chatgpt Subscr";;"EUR Card EUR";;"CARD PAYMENT";-17.41 +2025-07-29;"Github, Inc.";;"EUR Card EUR";;"CARD PAYMENT";-81.96 +2025-07-29;Dropbox*sgvfd1d5tz31;;"EUR Card EUR";;"CARD PAYMENT";-11.99 +2025-07-28;"To Flatlogic EU";;"EUR Main";;"Invoice FL EU #759";-400.00 +2025-07-28;Figma;;"EUR Card EUR";;"CARD PAYMENT";-102.39 +2025-07-27;"Trello.com* Atlassian";;"EUR Card EUR";;"CARD PAYMENT";-5.12 +2025-07-26;Paypro-charge.com;;"EUR Card EUR";;"CARD PAYMENT";-11.62 +2025-07-23;"Discord* 12xserverboos";;"EUR Card EUR";;"CARD PAYMENT";-44.27 +2025-07-21;"To Flatlogic Poland";;"EUR Main";PL51109025900000000161061759;"Przelew na wlasne konto";-450.00 +2025-07-20;Cloudflare;;"EUR Card EUR";;"CARD PAYMENT";-25.83 +2025-07-20;Calendly;;"EUR Card EUR";;"CARD PAYMENT";-42.36 +2025-07-18;"Main · EUR → Main · USD";;"EUR Main";;EXCHANGE;-972.24 +2025-07-18;"To Flatlogic Poland";;"EUR Main";PL51109025900000000161061759;"Przelew na wlasne konto";-250.00 +2025-07-17;"Openai *chatgpt Subscr";;"EUR Card EUR";;"CARD PAYMENT";-186.18 +2025-07-16;"To Card EUR";;"EUR Main";;TRANSFER;-500.00 +2025-07-16;"From Main";;"EUR Card EUR";;TRANSFER;500.00 +2025-07-16;"Veed.io Basic";;"EUR Card EUR";;"CARD PAYMENT";-44.00 +2025-07-14;"Main · EUR → Main · PLN";;"EUR Main";;"Transfer to Main to recover negative balance";-8.49 +2025-07-14;"Main · EUR → Main · USD";;"EUR Main";;EXCHANGE;-17.12 +2025-07-14;"Main · EUR → Main · USD";;"EUR Main";;EXCHANGE;-2121.00 +2025-07-14;"Main · EUR → Main · USD";;"EUR Main";;EXCHANGE;-1711.65 +2025-07-14;"Money added from MOGANADEN MOORGHEN";;"EUR Main";;TOPUP;1631.25