From 8cad59e3d60f189970968596f1958eb34e103ccd Mon Sep 17 00:00:00 2001 From: Claudio Date: Sat, 8 Mar 2025 08:34:00 +0100 Subject: [PATCH] import xls update --- composer.json | 1 + composer.lock | 450 ++++++++++++++---- public/userarea/edit_template_xls.php | 381 ++++++++++++--- public/userarea/import_dashboard.php | 134 ++++++ public/userarea/import_xls.php | 316 ++++++++++++ public/userarea/insert_template_xls.php | 277 +++++++++-- public/userarea/insert_template_xlsbck.php | 411 ++++++++++++++++ public/userarea/load_active_templates.php | 27 ++ public/userarea/load_existing_mappings.php | 60 +++ public/userarea/mapping_template_xls.php | 285 ++++++++++- public/userarea/process_edit_template_xls.php | 13 +- public/userarea/process_import_xls.php | 93 ++++ .../userarea/process_insert_template_xls.php | 22 +- public/userarea/remove_column_mapping.php | 62 +++ public/userarea/save_column_mapping.php | 46 ++ public/userarea/templates_dashboard.php | 136 +++++- public/userarea/update_template_status.php | 30 ++ public/userarea/upload_xls_example.php | 48 ++ .../xlstemplates/5-1740733446-row3.xlsx | Bin 0 -> 8958 bytes 19 files changed, 2583 insertions(+), 209 deletions(-) create mode 100644 public/userarea/import_dashboard.php create mode 100644 public/userarea/import_xls.php create mode 100644 public/userarea/insert_template_xlsbck.php create mode 100644 public/userarea/load_active_templates.php create mode 100644 public/userarea/load_existing_mappings.php create mode 100644 public/userarea/process_import_xls.php create mode 100644 public/userarea/remove_column_mapping.php create mode 100644 public/userarea/save_column_mapping.php create mode 100644 public/userarea/update_template_status.php create mode 100644 public/userarea/upload_xls_example.php create mode 100644 public/userarea/xlstemplates/5-1740733446-row3.xlsx diff --git a/composer.json b/composer.json index 33586ab..c38cbe7 100644 --- a/composer.json +++ b/composer.json @@ -41,6 +41,7 @@ "laravel/tinker": "^2.7", "laravel/ui": "^4.0", "phpmailer/phpmailer": "^6.9", + "phpoffice/phpspreadsheet": "^4.1", "proengsoft/laravel-jsvalidation": "^4.0.0", "spatie/laravel-query-builder": "^5.0", "vanguardapp/activity-log": "^6.0", diff --git a/composer.lock b/composer.lock index 2329f65..8a6ece5 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "3d85faea6470d55b627da7cc0e8c9a67", + "content-hash": "a7749f3b43e37d8fb607bcccd227f5ee", "packages": [ { "name": "akaunting/laravel-setting", @@ -320,6 +320,85 @@ ], "time": "2024-02-09T16:56:22+00:00" }, + { + "name": "composer/pcre", + "version": "3.2.0", + "source": { + "type": "git", + "url": "https://github.com/composer/pcre.git", + "reference": "ea4ab6f9580a4fd221e0418f2c357cdd39102a90" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/pcre/zipball/ea4ab6f9580a4fd221e0418f2c357cdd39102a90", + "reference": "ea4ab6f9580a4fd221e0418f2c357cdd39102a90", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0" + }, + "conflict": { + "phpstan/phpstan": "<1.11.8" + }, + "require-dev": { + "phpstan/phpstan": "^1.11.8", + "phpstan/phpstan-strict-rules": "^1.1", + "phpunit/phpunit": "^8 || ^9" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.x-dev" + }, + "phpstan": { + "includes": [ + "extension.neon" + ] + } + }, + "autoload": { + "psr-4": { + "Composer\\Pcre\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" + } + ], + "description": "PCRE wrapping library that offers type-safe preg_* replacements.", + "keywords": [ + "PCRE", + "preg", + "regex", + "regular expression" + ], + "support": { + "issues": "https://github.com/composer/pcre/issues", + "source": "https://github.com/composer/pcre/tree/3.2.0" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/composer/composer", + "type": "tidelift" + } + ], + "time": "2024-07-25T09:36:02+00:00" + }, { "name": "dasprid/enum", "version": "1.0.5", @@ -2740,6 +2819,191 @@ }, "time": "2022-04-15T14:02:14+00:00" }, + { + "name": "maennchen/zipstream-php", + "version": "3.1.2", + "source": { + "type": "git", + "url": "https://github.com/maennchen/ZipStream-PHP.git", + "reference": "aeadcf5c412332eb426c0f9b4485f6accba2a99f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/maennchen/ZipStream-PHP/zipball/aeadcf5c412332eb426c0f9b4485f6accba2a99f", + "reference": "aeadcf5c412332eb426c0f9b4485f6accba2a99f", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "ext-zlib": "*", + "php-64bit": "^8.2" + }, + "require-dev": { + "brianium/paratest": "^7.7", + "ext-zip": "*", + "friendsofphp/php-cs-fixer": "^3.16", + "guzzlehttp/guzzle": "^7.5", + "mikey179/vfsstream": "^1.6", + "php-coveralls/php-coveralls": "^2.5", + "phpunit/phpunit": "^11.0", + "vimeo/psalm": "^6.0" + }, + "suggest": { + "guzzlehttp/psr7": "^2.4", + "psr/http-message": "^2.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "ZipStream\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Paul Duncan", + "email": "pabs@pablotron.org" + }, + { + "name": "Jonatan Männchen", + "email": "jonatan@maennchen.ch" + }, + { + "name": "Jesse Donat", + "email": "donatj@gmail.com" + }, + { + "name": "András Kolesár", + "email": "kolesar@kolesar.hu" + } + ], + "description": "ZipStream is a library for dynamically streaming dynamic zip files from PHP without writing to the disk at all on the server.", + "keywords": [ + "stream", + "zip" + ], + "support": { + "issues": "https://github.com/maennchen/ZipStream-PHP/issues", + "source": "https://github.com/maennchen/ZipStream-PHP/tree/3.1.2" + }, + "funding": [ + { + "url": "https://github.com/maennchen", + "type": "github" + } + ], + "time": "2025-01-27T12:07:53+00:00" + }, + { + "name": "markbaker/complex", + "version": "3.0.2", + "source": { + "type": "git", + "url": "https://github.com/MarkBaker/PHPComplex.git", + "reference": "95c56caa1cf5c766ad6d65b6344b807c1e8405b9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/MarkBaker/PHPComplex/zipball/95c56caa1cf5c766ad6d65b6344b807c1e8405b9", + "reference": "95c56caa1cf5c766ad6d65b6344b807c1e8405b9", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "require-dev": { + "dealerdirect/phpcodesniffer-composer-installer": "dev-master", + "phpcompatibility/php-compatibility": "^9.3", + "phpunit/phpunit": "^7.0 || ^8.0 || ^9.0", + "squizlabs/php_codesniffer": "^3.7" + }, + "type": "library", + "autoload": { + "psr-4": { + "Complex\\": "classes/src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mark Baker", + "email": "mark@lange.demon.co.uk" + } + ], + "description": "PHP Class for working with complex numbers", + "homepage": "https://github.com/MarkBaker/PHPComplex", + "keywords": [ + "complex", + "mathematics" + ], + "support": { + "issues": "https://github.com/MarkBaker/PHPComplex/issues", + "source": "https://github.com/MarkBaker/PHPComplex/tree/3.0.2" + }, + "time": "2022-12-06T16:21:08+00:00" + }, + { + "name": "markbaker/matrix", + "version": "3.0.1", + "source": { + "type": "git", + "url": "https://github.com/MarkBaker/PHPMatrix.git", + "reference": "728434227fe21be27ff6d86621a1b13107a2562c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/MarkBaker/PHPMatrix/zipball/728434227fe21be27ff6d86621a1b13107a2562c", + "reference": "728434227fe21be27ff6d86621a1b13107a2562c", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "require-dev": { + "dealerdirect/phpcodesniffer-composer-installer": "dev-master", + "phpcompatibility/php-compatibility": "^9.3", + "phpdocumentor/phpdocumentor": "2.*", + "phploc/phploc": "^4.0", + "phpmd/phpmd": "2.*", + "phpunit/phpunit": "^7.0 || ^8.0 || ^9.0", + "sebastian/phpcpd": "^4.0", + "squizlabs/php_codesniffer": "^3.7" + }, + "type": "library", + "autoload": { + "psr-4": { + "Matrix\\": "classes/src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mark Baker", + "email": "mark@demon-angel.eu" + } + ], + "description": "PHP Class for working with matrices", + "homepage": "https://github.com/MarkBaker/PHPMatrix", + "keywords": [ + "mathematics", + "matrix", + "vector" + ], + "support": { + "issues": "https://github.com/MarkBaker/PHPMatrix/issues", + "source": "https://github.com/MarkBaker/PHPMatrix/tree/3.0.1" + }, + "time": "2022-12-02T22:17:43+00:00" + }, { "name": "mobiledetect/mobiledetectlib", "version": "2.8.45", @@ -3501,6 +3765,111 @@ ], "time": "2024-11-24T18:04:13+00:00" }, + { + "name": "phpoffice/phpspreadsheet", + "version": "4.1.0", + "source": { + "type": "git", + "url": "https://github.com/PHPOffice/PhpSpreadsheet.git", + "reference": "6ff18c3a8df3a945492f75ce455d77f7ad55dd5c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PHPOffice/PhpSpreadsheet/zipball/6ff18c3a8df3a945492f75ce455d77f7ad55dd5c", + "reference": "6ff18c3a8df3a945492f75ce455d77f7ad55dd5c", + "shasum": "" + }, + "require": { + "composer/pcre": "^1||^2||^3", + "ext-ctype": "*", + "ext-dom": "*", + "ext-fileinfo": "*", + "ext-gd": "*", + "ext-iconv": "*", + "ext-libxml": "*", + "ext-mbstring": "*", + "ext-simplexml": "*", + "ext-xml": "*", + "ext-xmlreader": "*", + "ext-xmlwriter": "*", + "ext-zip": "*", + "ext-zlib": "*", + "maennchen/zipstream-php": "^2.1 || ^3.0", + "markbaker/complex": "^3.0", + "markbaker/matrix": "^3.0", + "php": "^8.1", + "psr/http-client": "^1.0", + "psr/http-factory": "^1.0", + "psr/simple-cache": "^1.0 || ^2.0 || ^3.0" + }, + "require-dev": { + "dealerdirect/phpcodesniffer-composer-installer": "dev-main", + "dompdf/dompdf": "^2.0 || ^3.0", + "friendsofphp/php-cs-fixer": "^3.2", + "mitoteam/jpgraph": "^10.3", + "mpdf/mpdf": "^8.1.1", + "phpcompatibility/php-compatibility": "^9.3", + "phpstan/phpstan": "^1.1", + "phpstan/phpstan-phpunit": "^1.0", + "phpunit/phpunit": "^10.5", + "squizlabs/php_codesniffer": "^3.7", + "tecnickcom/tcpdf": "^6.5" + }, + "suggest": { + "dompdf/dompdf": "Option for rendering PDF with PDF Writer", + "ext-intl": "PHP Internationalization Functions", + "mitoteam/jpgraph": "Option for rendering charts, or including charts with PDF or HTML Writers", + "mpdf/mpdf": "Option for rendering PDF with PDF Writer", + "tecnickcom/tcpdf": "Option for rendering PDF with PDF Writer" + }, + "type": "library", + "autoload": { + "psr-4": { + "PhpOffice\\PhpSpreadsheet\\": "src/PhpSpreadsheet" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Maarten Balliauw", + "homepage": "https://blog.maartenballiauw.be" + }, + { + "name": "Mark Baker", + "homepage": "https://markbakeruk.net" + }, + { + "name": "Franck Lefevre", + "homepage": "https://rootslabs.net" + }, + { + "name": "Erik Tilt" + }, + { + "name": "Adrien Crivelli" + } + ], + "description": "PHPSpreadsheet - Read, Create and Write Spreadsheet documents in PHP - Spreadsheet engine", + "homepage": "https://github.com/PHPOffice/PhpSpreadsheet", + "keywords": [ + "OpenXML", + "excel", + "gnumeric", + "ods", + "php", + "spreadsheet", + "xls", + "xlsx" + ], + "support": { + "issues": "https://github.com/PHPOffice/PhpSpreadsheet/issues", + "source": "https://github.com/PHPOffice/PhpSpreadsheet/tree/4.1.0" + }, + "time": "2025-03-02T06:52:24+00:00" + }, { "name": "phpoption/phpoption", "version": "1.9.3", @@ -7873,85 +8242,6 @@ ], "time": "2024-06-12T14:13:04+00:00" }, - { - "name": "composer/pcre", - "version": "3.2.0", - "source": { - "type": "git", - "url": "https://github.com/composer/pcre.git", - "reference": "ea4ab6f9580a4fd221e0418f2c357cdd39102a90" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/composer/pcre/zipball/ea4ab6f9580a4fd221e0418f2c357cdd39102a90", - "reference": "ea4ab6f9580a4fd221e0418f2c357cdd39102a90", - "shasum": "" - }, - "require": { - "php": "^7.4 || ^8.0" - }, - "conflict": { - "phpstan/phpstan": "<1.11.8" - }, - "require-dev": { - "phpstan/phpstan": "^1.11.8", - "phpstan/phpstan-strict-rules": "^1.1", - "phpunit/phpunit": "^8 || ^9" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-main": "3.x-dev" - }, - "phpstan": { - "includes": [ - "extension.neon" - ] - } - }, - "autoload": { - "psr-4": { - "Composer\\Pcre\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Jordi Boggiano", - "email": "j.boggiano@seld.be", - "homepage": "http://seld.be" - } - ], - "description": "PCRE wrapping library that offers type-safe preg_* replacements.", - "keywords": [ - "PCRE", - "preg", - "regex", - "regular expression" - ], - "support": { - "issues": "https://github.com/composer/pcre/issues", - "source": "https://github.com/composer/pcre/tree/3.2.0" - }, - "funding": [ - { - "url": "https://packagist.com", - "type": "custom" - }, - { - "url": "https://github.com/composer", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/composer/composer", - "type": "tidelift" - } - ], - "time": "2024-07-25T09:36:02+00:00" - }, { "name": "doctrine/deprecations", "version": "1.1.3", diff --git a/public/userarea/edit_template_xls.php b/public/userarea/edit_template_xls.php index eb5bdb3..80fb0d1 100644 --- a/public/userarea/edit_template_xls.php +++ b/public/userarea/edit_template_xls.php @@ -19,30 +19,56 @@ if (!$template) { header("Location: template_dashboard.php?status=error&message=" . urlencode("Template not found")); exit; } + +// Debug del JSON +$clientSpecificFieldsJson = $template['client_specific_fields'] ?? '{}'; +error_log("Raw client_specific_fields JSON: " . $clientSpecificFieldsJson); + +$clientSpecificFields = json_decode($clientSpecificFieldsJson, true); +if (json_last_error() !== JSON_ERROR_NONE) { + error_log("JSON decode error: " . json_last_error_msg()); + $clientSpecificFields = []; +} else { + error_log("Decoded client_specific_fields: " . print_r($clientSpecificFields, true)); +} ?> - - + Edit Template <?= htmlspecialchars($titlewebsite, ENT_QUOTES, 'UTF-8'); ?> -
- - - - -
@@ -55,8 +81,7 @@ if (!$template) {

4805

+2.5% from last week

-
-
+
@@ -70,8 +95,7 @@ if (!$template) {

$84,245

+5.4% from last week

-
-
+
@@ -85,8 +109,7 @@ if (!$template) {

34.6%

-4.5% from last week

-
-
+
@@ -100,15 +123,12 @@ if (!$template) {

8.4K

+8.4% from last week

-
-
+
- - - +
@@ -116,12 +136,10 @@ if (!$template) {
Edit Template:
-
-
@@ -150,13 +168,76 @@ if (!$template) {
+ +
+ +
+ $fieldData) { + if (is_array($fieldData)) { + $type = $fieldData['type'] ?? 'text'; + $possibleValues = implode(', ', $fieldData['possible_values'] ?? []); + $isRequired = isset($fieldData['is_required']) && $fieldData['is_required'] ? '1' : '0'; + $exportColumnName = $fieldData['export_column_name'] ?? ''; + $defaultValue = $fieldData['default_value'] ?? ''; + error_log("Rendering field: $fieldName, type: $type, possible_values: $possibleValues, required: $isRequired, export: $exportColumnName, default: $defaultValue"); + ?> +
+
+
+ +
+
+ +
+ +
+ +
+
+ +
+
+ +
+
+ +
+
+
+ +
+ +
+ +
Cancel
- @@ -170,57 +251,249 @@ if (!$template) { - - - + + diff --git a/public/userarea/import_dashboard.php b/public/userarea/import_dashboard.php new file mode 100644 index 0000000..b3c0ad0 --- /dev/null +++ b/public/userarea/import_dashboard.php @@ -0,0 +1,134 @@ + + + + + + + + + + Template Buttons - <?= htmlspecialchars($titlewebsite, ENT_QUOTES, 'UTF-8'); ?> + + + + + +
+ + + +
+
+ + +
+
+
Active Templates
+
+
+ + +
+
+
+ +
+
+
+ + +
+ + + + + + + + \ No newline at end of file diff --git a/public/userarea/import_xls.php b/public/userarea/import_xls.php new file mode 100644 index 0000000..e519fde --- /dev/null +++ b/public/userarea/import_xls.php @@ -0,0 +1,316 @@ +getConnection(); +$stmt = $pdo->prepare("SELECT * FROM excel_templates WHERE id = ?"); +$stmt->execute([$id]); +$template = $stmt->fetch(PDO::FETCH_ASSOC); + +if (!$template) { + header("Location: template_dashboard.php?status=error&message=" . urlencode("Template not found")); + exit; +} + +// Debug del template +error_log("Loaded template: " . print_r($template, true)); +?> + + + + + + + + + + + <?= htmlspecialchars($template['name']) ?> - <?= htmlspecialchars($titlewebsite, ENT_QUOTES, 'UTF-8'); ?> + + + +
+ + +
+
+ + +
+
+
+
+
+ Template ID: , Start Row: , Start Column: +
+
+
+
+ +
+
+ + +
+ +
+
+ + + + + +
+
+
+
+
+ +
+ + +
+ + + + + + + + + + + + + + \ No newline at end of file diff --git a/public/userarea/insert_template_xls.php b/public/userarea/insert_template_xls.php index 9d0db66..d03cdb0 100644 --- a/public/userarea/insert_template_xls.php +++ b/public/userarea/insert_template_xls.php @@ -87,15 +87,12 @@ - -
Insert New XLS Template
-
@@ -125,6 +122,73 @@
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ + +
+ +
+ +
+
+
+ +
+
+ +
+ +
+ +
+
+ +
+
+ +
+
+ +
+
+
+
+ +
+
Cancel @@ -146,57 +210,200 @@
- - diff --git a/public/userarea/insert_template_xlsbck.php b/public/userarea/insert_template_xlsbck.php new file mode 100644 index 0000000..d03cdb0 --- /dev/null +++ b/public/userarea/insert_template_xlsbck.php @@ -0,0 +1,411 @@ + + + + + + + + + + + + Insert XLS Template <?= htmlspecialchars($titlewebsite, ENT_QUOTES, 'UTF-8'); ?> + + + + +
+ + + + + + + +
+
+
+
+
+
+
+
+

Total Orders

+

4805

+

+2.5% from last week

+
+
+
+
+
+
+
+
+
+
+
+
+

Total Revenue

+

$84,245

+

+5.4% from last week

+
+
+
+
+
+
+
+
+
+
+
+
+

Bounce Rate

+

34.6%

+

-4.5% from last week

+
+
+
+
+
+
+
+
+
+
+
+
+

Total Customers

+

8.4K

+

+8.4% from last week

+
+
+
+
+
+
+
+
+ +
+
+
+
+
Insert New XLS Template
+
+
+
+
+
+
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ + +
+ +
+ +
+
+
+ +
+
+ +
+ +
+ +
+
+ +
+
+ +
+
+ +
+
+
+
+ +
+ +
+ + Cancel +
+
+
+
+ +
+
+ + +
+ + + + + +
+ + + + + + + + + + + + + + \ No newline at end of file diff --git a/public/userarea/load_active_templates.php b/public/userarea/load_active_templates.php new file mode 100644 index 0000000..b0d9fb7 --- /dev/null +++ b/public/userarea/load_active_templates.php @@ -0,0 +1,27 @@ + false, "data" => [], "message" => ""]; + +try { + $db = DBHandlerSelect::getInstance(); + $pdo = $db->getConnection(); + + if (!$pdo) { + throw new Exception('Database connection failed.'); + } + + // Recupera solo i template attivi + $stmt = $pdo->query("SELECT id, button_label, button_bg_color, button_text_color, button_size FROM excel_templates WHERE status = 'active'"); + $templates = $stmt->fetchAll(PDO::FETCH_ASSOC); + + $response["success"] = true; + $response["data"] = $templates; +} catch (PDOException $e) { + $response["message"] = "Database error: " . $e->getMessage(); +} catch (Exception $e) { + $response["message"] = "Error: " . $e->getMessage(); +} + +echo json_encode($response); diff --git a/public/userarea/load_existing_mappings.php b/public/userarea/load_existing_mappings.php new file mode 100644 index 0000000..14d0bac --- /dev/null +++ b/public/userarea/load_existing_mappings.php @@ -0,0 +1,60 @@ + false, "message" => "Invalid template ID"]); + exit; +} + +$template_id = intval($_GET['template_id']); + +try { + $db = DBHandlerSelect::getInstance(); + $pdo = $db->getConnection(); + + // 1️⃣ Recuperiamo il nome della tabella target da `excel_templates` + $stmt = $pdo->prepare("SELECT target_table FROM excel_templates WHERE id = ?"); + $stmt->execute([$template_id]); + $template = $stmt->fetch(PDO::FETCH_ASSOC); + + if (!$template || empty($template['target_table'])) { + echo json_encode(["success" => false, "message" => "Template not found or missing target table"]); + exit; + } + + $target_table = $template['target_table']; + + // 2️⃣ Recuperiamo le associazioni già esistenti per il template_id + $stmt = $pdo->prepare("SELECT excel_column, mysql_column, headerexcel FROM excel_column_mappings WHERE template_id = ?"); + $stmt->execute([$template_id]); + $existing_mappings = $stmt->fetchAll(PDO::FETCH_ASSOC); + + // Creiamo gli array delle colonne già mappate + $mapped_xls_columns = array_column($existing_mappings, 'excel_column'); + $mapped_mysql_columns = array_column($existing_mappings, 'mysql_column'); // CORRETTO PER FILTRARE! + + // 3️⃣ Recuperiamo tutte le colonne disponibili nella tabella MySQL target + $stmt = $pdo->prepare("SHOW COLUMNS FROM `$target_table`"); + $stmt->execute(); + $table_columns = $stmt->fetchAll(PDO::FETCH_COLUMN); + + // 🔥 FIX: Rimuoviamo le colonne MySQL che sono già state mappate! + $remaining_mysql_columns = array_values(array_diff($table_columns, $mapped_mysql_columns)); + + // 4️⃣ Se abbiamo salvato gli header XLSX in `headerexcel`, li usiamo per calcolare le colonne XLSX non mappate + $headerexcel = !empty($existing_mappings) ? $existing_mappings[0]['headerexcel'] : ''; + $all_xls_columns = !empty($headerexcel) ? explode(',', $headerexcel) : []; + $remaining_xls_columns = array_values(array_diff($all_xls_columns, $mapped_xls_columns)); + + // 5️⃣ Invio dei dati al frontend + echo json_encode([ + "success" => true, + "mappings" => $existing_mappings, + "remaining_xls_columns" => $remaining_xls_columns, + "remaining_mysql_columns" => $remaining_mysql_columns // 🔥 ORA LE COLONNE MYSQL SONO FILTRATE! + ]); +} catch (PDOException $e) { + echo json_encode(["success" => false, "message" => "Database error: " . $e->getMessage()]); +} diff --git a/public/userarea/mapping_template_xls.php b/public/userarea/mapping_template_xls.php index 764b8c9..c78d415 100644 --- a/public/userarea/mapping_template_xls.php +++ b/public/userarea/mapping_template_xls.php @@ -7,10 +7,11 @@ if (!isset($_GET['id']) || !is_numeric($_GET['id'])) { $id = intval($_GET['id']); $db = DBHandlerSelect::getInstance(); $pdo = $db->getConnection(); -$stmt = $pdo->prepare("SELECT name, header_row, start_column, target_table FROM excel_templates WHERE id = ?"); +$stmt = $pdo->prepare("SELECT name, header_row, start_column, target_table, sample_xlsx FROM excel_templates WHERE id = ?"); $stmt->execute([$id]); $template = $stmt->fetch(PDO::FETCH_ASSOC); + if (!$template) { die("Template not found"); } @@ -26,6 +27,8 @@ if (!$template) { Mapping XLS Template <?= htmlspecialchars($titlewebsite, ENT_QUOTES, 'UTF-8'); ?> + + @@ -59,8 +62,20 @@ if (!$template) {
+ + + ✅ Current file: + + + + No file uploaded yet. + +
+ + +
@@ -84,8 +99,9 @@ if (!$template) {
- + ⬅ Back to Template Dashboard
+
@@ -117,8 +133,48 @@ if (!$template) { diff --git a/public/userarea/process_edit_template_xls.php b/public/userarea/process_edit_template_xls.php index 2a0e7cc..8bc5dff 100644 --- a/public/userarea/process_edit_template_xls.php +++ b/public/userarea/process_edit_template_xls.php @@ -16,21 +16,28 @@ try { $start_column = trim($_POST['start_column']); $description = trim($_POST['description'] ?? ''); $target_table = trim($_POST['target_table']); + $client_specific_fields = trim($_POST['client_specific_fields'] ?? '{}'); // Recupera il JSON dei campi specifici // Controllo sui campi obbligatori if (empty($id) || empty($name) || empty($header_row) || empty($start_column) || empty($target_table)) { throw new Exception("All fields marked with * are required."); } + // Validazione opzionale del JSON (per sicurezza) + $decoded_fields = json_decode($client_specific_fields, true); + if (json_last_error() !== JSON_ERROR_NONE && $client_specific_fields !== '{}') { + throw new Exception("Invalid JSON format for client-specific fields."); + } + // Connessione al database $db = DBHandlerSelect::getInstance(); $pdo = $db->getConnection(); - // Aggiorna il database + // Aggiorna il database, includendo client_specific_fields $stmt = $pdo->prepare("UPDATE excel_templates - SET name = ?, header_row = ?, start_column = ?, description = ?, target_table = ?, updated_at = NOW() + SET name = ?, header_row = ?, start_column = ?, description = ?, target_table = ?, client_specific_fields = ?, updated_at = NOW() WHERE id = ?"); - $stmt->execute([$name, $header_row, $start_column, $description, $target_table, $id]); + $stmt->execute([$name, $header_row, $start_column, $description, $target_table, $client_specific_fields, $id]); if ($stmt->rowCount() > 0) { $response["success"] = true; diff --git a/public/userarea/process_import_xls.php b/public/userarea/process_import_xls.php new file mode 100644 index 0000000..5a2dc3a --- /dev/null +++ b/public/userarea/process_import_xls.php @@ -0,0 +1,93 @@ + '', 'rows' => [], 'columns' => [], 'template_id' => 0]; + +try { + if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_FILES['excel_file'])) { + $template_id = isset($_POST['template_id']) ? intval($_POST['template_id']) : 0; + $header_row = isset($_POST['header_row']) ? intval($_POST['header_row']) : 1; + $start_column = isset($_POST['start_column']) ? intval($_POST['start_column']) : 1; + + $file = $_FILES['excel_file']; + $fileError = $file['error']; + + if ($fileError === UPLOAD_ERR_OK) { + $spreadsheet = \PhpOffice\PhpSpreadsheet\IOFactory::load($file['tmp_name']); + $worksheet = $spreadsheet->getActiveSheet(); + $highestRow = $worksheet->getHighestRow(); + $highestColumn = $worksheet->getHighestColumn(); + $highestColumnIndex = \PhpOffice\PhpSpreadsheet\Cell\Coordinate::columnIndexFromString($highestColumn); + + $startRow = max(1, $header_row); + $startColumn = max(1, $start_column); + + // Debug dei parametri + error_log("Processing - startRow: $startRow, startColumn: $startColumn, highestRow: $highestRow, highestColumn: $highestColumn, highestColumnIndex: $highestColumnIndex"); + + // Validazione degli indici + if ($startRow > $highestRow) { + $response['error'] = "La riga di partenza ($startRow) supera il numero totale di righe ($highestRow)."; + } elseif ($startColumn > $highestColumnIndex) { + $response['error'] = "La colonna di partenza ($startColumn) supera il numero totale di colonne ($highestColumnIndex)."; + } else { + $excelData = []; + // Estrai la riga degli header + $headerRowData = []; + for ($col = $startColumn; $col <= $highestColumnIndex; $col++) { + $columnLetter = \PhpOffice\PhpSpreadsheet\Cell\Coordinate::stringFromColumnIndex($col); + $cell = $worksheet->getCell($columnLetter . $header_row); + $cellValue = $cell ? $cell->getCalculatedValue() : ''; // Usa getCalculatedValue per le formule + $headerRowData[] = htmlspecialchars($cellValue ?: ''); + } + + // Estrai i dati a partire dalla riga successiva + for ($row = $startRow + 1; $row <= $highestRow; $row++) { + $rowData = []; + for ($col = $startColumn; $col <= $highestColumnIndex; $col++) { + $columnLetter = \PhpOffice\PhpSpreadsheet\Cell\Coordinate::stringFromColumnIndex($col); + $cell = $worksheet->getCell($columnLetter . $row); + $cellValue = $cell ? $cell->getCalculatedValue() : ''; // Usa getCalculatedValue per le formule + $rowData[] = htmlspecialchars($cellValue ?: ''); + } + if (!empty(array_filter($rowData))) { + $excelData[] = $rowData; + } + } + + // Salva i dati in sessione + $_SESSION['excel_data'] = $excelData; + $_SESSION['template_id'] = $template_id; + $_SESSION['headers'] = $headerRowData; // Salva gli header in sessione + + $response['rows'] = $excelData; + $response['columns'] = $headerRowData; // Usa gli header reali + $response['template_id'] = $template_id; + } + } else { + $response['error'] = "Errore nell'upload del file: Codice errore $fileError."; + } + } else { + $response['error'] = "Richiesta non valida."; + } +} catch (Exception $e) { + $response['error'] = "Errore durante il caricamento del file: " . $e->getMessage(); + error_log("Exception in process_import_xls.php: " . $e->getMessage()); +} + +// Pulisce qualsiasi output indesiderato +ob_end_clean(); + +// Invia la risposta JSON +header('Content-Type: application/json'); +echo json_encode($response); +exit; diff --git a/public/userarea/process_insert_template_xls.php b/public/userarea/process_insert_template_xls.php index 900ab06..1803cf7 100644 --- a/public/userarea/process_insert_template_xls.php +++ b/public/userarea/process_insert_template_xls.php @@ -15,6 +15,19 @@ try { $start_column = trim($_POST['start_column']); $description = trim($_POST['description'] ?? ''); $target_table = trim($_POST['target_table']); + $button_size = trim($_POST['button_size'] ?? 'medium'); + $button_bg_color = trim($_POST['button_bg_color'] ?? '#007bff'); + $button_text_color = trim($_POST['button_text_color'] ?? '#ffffff'); + $button_label = trim($_POST['button_label'] ?? 'Click Me'); + $status = 'active'; // Default + + // Recupera i client_specific_fields (JSON inviato dal form) + $client_specific_fields = trim($_POST['client_specific_fields'] ?? '{}'); + // Decodifica il JSON per verificare che sia valido (opzionale, per sicurezza) + $decoded_fields = json_decode($client_specific_fields, true); + if (json_last_error() !== JSON_ERROR_NONE && !empty($client_specific_fields)) { + throw new Exception("Invalid JSON format for client-specific fields."); + } // Controllo sui campi obbligatori if (empty($name) || empty($header_row) || empty($start_column) || empty($target_table)) { @@ -25,10 +38,11 @@ try { $db = DBHandlerSelect::getInstance(); $pdo = $db->getConnection(); - // Inserisci nel database - $stmt = $pdo->prepare("INSERT INTO excel_templates (name, header_row, start_column, description, target_table, created_at, updated_at) - VALUES (?, ?, ?, ?, ?, NOW(), NOW())"); - $stmt->execute([$name, $header_row, $start_column, $description, $target_table]); + // Inserisci nel database, includendo client_specific_fields + $stmt = $pdo->prepare("INSERT INTO excel_templates + (name, header_row, start_column, description, target_table, button_size, button_bg_color, button_text_color, button_label, status, client_specific_fields, created_at, updated_at) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NOW(), NOW())"); + $stmt->execute([$name, $header_row, $start_column, $description, $target_table, $button_size, $button_bg_color, $button_text_color, $button_label, $status, $client_specific_fields]); if ($stmt->rowCount() > 0) { $response["success"] = true; diff --git a/public/userarea/remove_column_mapping.php b/public/userarea/remove_column_mapping.php new file mode 100644 index 0000000..97316a9 --- /dev/null +++ b/public/userarea/remove_column_mapping.php @@ -0,0 +1,62 @@ +getConnection(); + +$data = json_decode(file_get_contents("php://input"), true); + +if (!isset($data['template_id'], $data['excel_column'], $data['mysql_column'], $data['tablename'])) { + echo json_encode(["success" => false, "message" => "Missing required fields"]); + exit; +} + +// Rimuove l'associazione +$stmtDelete = $pdo->prepare(" + DELETE FROM excel_column_mappings + WHERE template_id = ? AND excel_column = ? AND mysql_column = ? AND tablename = ? +"); +$result = $stmtDelete->execute([ + $data['template_id'], + $data['excel_column'], + $data['mysql_column'], + $data['tablename'] +]); + +if (!$result) { + echo json_encode(["success" => false, "message" => "Failed to delete mapping"]); + exit; +} + +// Dopo la rimozione, aggiorna la lista delle colonne disponibili +$stmtColumns = $pdo->prepare("SHOW COLUMNS FROM " . $data['tablename']); +$stmtColumns->execute(); +$all_mysql_columns = array_column($stmtColumns->fetchAll(PDO::FETCH_ASSOC), 'Field'); + +$stmtHeader = $pdo->prepare("SELECT headerexcel FROM excel_column_mappings WHERE template_id = ? LIMIT 1"); +$stmtHeader->execute([$data['template_id']]); +$headerRow = $stmtHeader->fetch(PDO::FETCH_ASSOC); +$xls_headers = isset($headerRow['headerexcel']) ? explode(",", $headerRow['headerexcel']) : []; + +// Ricalcola le colonne non associate +$stmtMappings = $pdo->prepare("SELECT excel_column, mysql_column FROM excel_column_mappings WHERE template_id = ?"); +$stmtMappings->execute([$data['template_id']]); +$existingMappings = $stmtMappings->fetchAll(PDO::FETCH_ASSOC); + +$mapped_xls_columns = array_column($existingMappings, 'excel_column'); +$mapped_mysql_columns = array_column($existingMappings, 'mysql_column'); + +$remaining_xls_columns = array_diff($xls_headers, $mapped_xls_columns); +$remaining_mysql_columns = array_diff($all_mysql_columns, $mapped_mysql_columns); + +echo json_encode([ + "success" => true, + "remaining_xls_columns" => array_values($remaining_xls_columns), + "remaining_mysql_columns" => array_values($remaining_mysql_columns) +]); +exit; diff --git a/public/userarea/save_column_mapping.php b/public/userarea/save_column_mapping.php new file mode 100644 index 0000000..742e127 --- /dev/null +++ b/public/userarea/save_column_mapping.php @@ -0,0 +1,46 @@ +getConnection(); // Ottieni la connessione + +$data = json_decode(file_get_contents("php://input"), true); + +if (!$data) { + echo json_encode(["success" => false, "message" => "Invalid JSON input"]); + exit; +} + +if (!isset($data['template_id'], $data['tablename'], $data['excel_column'], $data['mysql_column'])) { + echo json_encode(["success" => false, "message" => "Missing required fields"]); + exit; +} + +$stmt = $pdo->prepare(" + INSERT INTO excel_column_mappings + (template_id, tablename, excel_column, mysql_column, data_type, is_required, default_value, headerexcel) + VALUES (?, ?, ?, ?, ?, ?, ?, ?) +"); +$result = $stmt->execute([ + $data['template_id'], + $data['tablename'], + $data['excel_column'], + $data['mysql_column'], + $data['data_type'], + $data['is_required'], + $data['default_value'], + $data['headerexcel'] +]); + +if (!$result) { + echo json_encode(["success" => false, "message" => "Database insert failed"]); + exit; +} + +echo json_encode(["success" => true]); +exit; diff --git a/public/userarea/templates_dashboard.php b/public/userarea/templates_dashboard.php index 46d604f..8b896e9 100644 --- a/public/userarea/templates_dashboard.php +++ b/public/userarea/templates_dashboard.php @@ -11,6 +11,52 @@ TRF-Project - Template Dashboard + @@ -97,49 +143,71 @@ serverSide: false, ajax: 'load_templates.php', columns: [{ - data: 'id' + data: 'name', // Nome del template + title: "Template Name" }, { - data: 'name' - }, - { - data: 'updated_at', + data: 'updated_at', // Ultima modifica, formattata come data leggibile + title: "Last Modified", render: function(data) { return new Date(data).toLocaleDateString(); } }, { - data: 'header_row' + data: 'header_row', // Riga degli header + title: "Header Row" }, { - data: 'start_column' + data: 'start_column', // Colonna di partenza + title: "Start Column" }, { - data: 'description', + data: 'description', // Descrizione del template + title: "Description", defaultContent: 'No description' }, { - data: 'target_table' + data: 'target_table', // Tabella di destinazione + title: "Target Table" }, { - data: 'id', + data: 'status', // Stato con Toggle Switch + title: "Status", orderable: false, searchable: false, + render: function(status, type, row) { + let checked = (status === "active") ? "checked" : ""; + return ` + + `; + } + }, + { + data: 'id', // Azioni: Modifica, Mappatura e Eliminazione + orderable: false, + searchable: false, + title: "Actions", render: function(data) { return ` -
- - - - - - - -
`; +
+ + + + + + + +
+ `; } } + + ], dom: '<"card-header border-bottom p-3"<"d-flex align-items-center"<"card-title mb-0 flex-grow-1"f>>>rt<"card-footer border-top p-3"<"d-flex align-items-center"<"me-auto"l><"d-flex gap-2"ip>>>', lengthMenu: [10, 25, 50, 100], @@ -174,6 +242,32 @@ } }); } + + + $(document).on("change", ".toggle-status", function() { + let templateId = $(this).data("id"); + let newStatus = $(this).is(":checked") ? "active" : "inactive"; + + $.ajax({ + url: "update_template_status.php", + type: "POST", + data: { + id: templateId, + status: newStatus + }, + success: function(response) { + if (response.success) { + console.log("✅ Status updated successfully."); + } else { + console.error("❌ Error updating status:", response.message); + alert("Error updating status: " + response.message); + } + }, + error: function() { + console.error("❌ AJAX error."); + } + }); + }); diff --git a/public/userarea/update_template_status.php b/public/userarea/update_template_status.php new file mode 100644 index 0000000..1f31665 --- /dev/null +++ b/public/userarea/update_template_status.php @@ -0,0 +1,30 @@ + false, "message" => ""]; + +try { + if ($_SERVER["REQUEST_METHOD"] !== "POST") { + throw new Exception("Invalid request."); + } + + $id = intval($_POST['id']); + $status = ($_POST['status'] === "active") ? "active" : "inactive"; + + $db = DBHandlerSelect::getInstance(); + $pdo = $db->getConnection(); + + $stmt = $pdo->prepare("UPDATE excel_templates SET status = ?, updated_at = NOW() WHERE id = ?"); + $stmt->execute([$status, $id]); + + if ($stmt->rowCount() > 0) { + $response["success"] = true; + } else { + throw new Exception("Update failed."); + } +} catch (Exception $e) { + $response["message"] = $e->getMessage(); +} + +echo json_encode($response); diff --git a/public/userarea/upload_xls_example.php b/public/userarea/upload_xls_example.php new file mode 100644 index 0000000..04ce32c --- /dev/null +++ b/public/userarea/upload_xls_example.php @@ -0,0 +1,48 @@ + false, "message" => "Invalid template ID"]); + exit; +} + +$template_id = intval($_POST['template_id']); + +if (!isset($_FILES['xls_file']) || $_FILES['xls_file']['error'] !== UPLOAD_ERR_OK) { + echo json_encode(["success" => false, "message" => "File upload error"]); + exit; +} + +$file = $_FILES['xls_file']; +$originalFilename = pathinfo($file['name'], PATHINFO_FILENAME); +$extension = pathinfo($file['name'], PATHINFO_EXTENSION); + +// Crea il nuovo nome del file: {idtemplate}-{timestamp}-{nomeoriginale}.ext +$newFilename = $template_id . "-" . time() . "-" . preg_replace("/[^a-zA-Z0-9_-]/", "", $originalFilename) . "." . $extension; +$uploadDir = __DIR__ . '/xlstemplates/'; +$uploadPath = $uploadDir . $newFilename; + +// Assicura che la cartella esista +if (!is_dir($uploadDir)) { + mkdir($uploadDir, 0777, true); +} + +// Salva il file +if (!move_uploaded_file($file['tmp_name'], $uploadPath)) { + echo json_encode(["success" => false, "message" => "Failed to save file"]); + exit; +} + +// Aggiorna il database con il nome del file +try { + $db = DBHandlerSelect::getInstance(); + $pdo = $db->getConnection(); + $stmt = $pdo->prepare("UPDATE excel_templates SET sample_xlsx = ? WHERE id = ?"); + $stmt->execute([$newFilename, $template_id]); + + echo json_encode(["success" => true, "filename" => $newFilename, "filepath" => "xlstemplates/" . $newFilename]); +} catch (PDOException $e) { + echo json_encode(["success" => false, "message" => "Database error: " . $e->getMessage()]); +} diff --git a/public/userarea/xlstemplates/5-1740733446-row3.xlsx b/public/userarea/xlstemplates/5-1740733446-row3.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..4641d24c8fecbf4899db61d4109c28c1a8a6f47e GIT binary patch literal 8958 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