Browse Source

AI Import Update

Benjamin Harris 2 tháng trước cách đây
mục cha
commit
124eea21c7

+ 6 - 6
components/newClientModal.php

@@ -51,14 +51,14 @@ require_once __DIR__ . '/../lib/csrf.php';
                         <div class="col-md-6">
                             <div class="mb-3">
                                 <label for="Nname" class="form-label">Client Name <span class="text-danger">*</span></label>
-                                <input type="text" class="form-control" name="Nname" placeholder="Client Name" id="Nname" required maxlength="100">
+                                <input type="text" class="form-control form-control-sm" name="Nname" placeholder="Client Name" id="Nname" required maxlength="100">
                                 <div class="invalid-feedback">Please provide a client name.</div>
                             </div>
                         </div>
                         <div class="col-md-6">
                             <div class="mb-3">
                                 <label for="Ncompany" class="form-label">Company Name</label>
-                                <input type="text" class="form-control" name="Ncompany" placeholder="Company Name" id="Ncompany" maxlength="100">
+                                <input type="text" class="form-control form-control-sm" name="Ncompany" placeholder="Company Name" id="Ncompany" maxlength="100">
                             </div>
                         </div>
                     </div>
@@ -67,14 +67,14 @@ require_once __DIR__ . '/../lib/csrf.php';
                         <div class="col-md-6">
                             <div class="mb-3">
                                 <label for="Nemail" class="form-label">Email Address <span class="text-danger">*</span></label>
-                                <input type="email" class="form-control" name="Nemail" placeholder="Email Address" id="Nemail" required maxlength="255">
+                                <input type="email" class="form-control form-control-sm" name="Nemail" placeholder="Email Address" id="Nemail" required maxlength="255">
                                 <div class="invalid-feedback">Please provide a valid email address.</div>
                             </div>
                         </div>
                         <div class="col-md-6">
                             <div class="mb-3">
                                 <label for="Nmobile" class="form-label">Mobile Number</label>
-                                <input type="tel" class="form-control" name="Nmobile" placeholder="Mobile Number" id="Nmobile" maxlength="20">
+                                <input type="tel" class="form-control form-control-sm" name="Nmobile" placeholder="Mobile Number" id="Nmobile" maxlength="20">
                             </div>
                         </div>
                     </div>
@@ -83,13 +83,13 @@ require_once __DIR__ . '/../lib/csrf.php';
 
                     <div class="mb-3">
                         <label for="Naddress" class="form-label">Address <span class="text-danger">*</span></label>
-                        <input type="text" class="form-control" name="Naddress" placeholder="Street Address" id="Naddress" required maxlength="255">
+                        <input type="text" class="form-control form-control-sm" name="Naddress" placeholder="Street Address" id="Naddress" required maxlength="255">
                         <div class="invalid-feedback">Please provide an address.</div>
                     </div>
 
                     <div class="mb-3">
                         <label for="Nstate" class="form-label">Town / State / Postcode <span class="text-danger">*</span></label>
-                        <input type="text" class="form-control" name="Nstate" placeholder="Town, State, Postcode" id="Nstate" required maxlength="255">
+                        <input type="text" class="form-control form-control-sm" name="Nstate" placeholder="Town, State, Postcode" id="Nstate" required maxlength="255">
                         <div class="invalid-feedback">Please provide town, state, and postcode.</div>
                         <div class="form-text">Format: Town, State, Postcode (e.g., Sydney, NSW, 2000)</div>
                     </div>

+ 4 - 4
components/soilAnalysisForm.php

@@ -71,21 +71,21 @@ $formSections = [
 ?>
 
 <?php foreach ($formSections as $sectionName => $fields): ?>
-    <div class="card mb-4">
+    <div class="card mb-3">
         <div class="card-header">
             <h5 class="mb-0"><?= htmlspecialchars($sectionName, ENT_QUOTES, 'UTF-8') ?></h5>
         </div>
         <div class="card-body">
             <div class="row">
                 <?php foreach ($fields as $fieldName => $fieldConfig): ?>
-                    <div class="col-md-6 mb-3">
+                    <div class="col-md-6">
                         <label for="<?= htmlspecialchars($fieldName, ENT_QUOTES, 'UTF-8') ?>" class="form-label">
                             <?= htmlspecialchars($fieldConfig['label'], ENT_QUOTES, 'UTF-8') ?>
                             <?php if ($fieldConfig['required']): ?><span class="text-danger">*</span><?php endif; ?>
                         </label>
 
                         <?php if ($fieldConfig['type'] === 'select'): ?>
-                            <select class="form-control" id="<?= htmlspecialchars($fieldName, ENT_QUOTES, 'UTF-8') ?>"
+                            <select class="form-control form-control-sm" id="<?= htmlspecialchars($fieldName, ENT_QUOTES, 'UTF-8') ?>"
                                     name="<?= htmlspecialchars($fieldName, ENT_QUOTES, 'UTF-8') ?>"
                                     <?= $fieldConfig['required'] ? 'required' : '' ?>>
                                 <option value="">Select...</option>
@@ -97,7 +97,7 @@ $formSections = [
                             </select>
                         <?php else: ?>
                             <input type="<?= htmlspecialchars($fieldConfig['type'], ENT_QUOTES, 'UTF-8') ?>"
-                                   class="form-control"
+                                   class="form-control form-control-sm"
                                    id="<?= htmlspecialchars($fieldName, ENT_QUOTES, 'UTF-8') ?>"
                                    name="<?= htmlspecialchars($fieldName, ENT_QUOTES, 'UTF-8') ?>"
                                    placeholder="<?= htmlspecialchars($fieldConfig['placeholder'] ?? '', ENT_QUOTES, 'UTF-8') ?>"

+ 1 - 1
composer.json

@@ -4,6 +4,6 @@
         "smalot/pdfparser": "^2.0",
         "erusev/parsedown": "^1.7",
         "daandesmedt/phpheadlesschrome": "^1.1",
-        "phpoffice/phpspreadsheet": "^3.0"
+        "phpoffice/phpspreadsheet": "^5.5"
     }
 }

+ 812 - 0
composer.lock

@@ -0,0 +1,812 @@
+{
+    "_readme": [
+        "This file locks the dependencies of your project to a known state",
+        "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
+        "This file is @generated automatically"
+    ],
+    "content-hash": "52e48cf583816229921dfd69d3d63965",
+    "packages": [
+        {
+            "name": "composer/pcre",
+            "version": "3.3.2",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/composer/pcre.git",
+                "reference": "b2bed4734f0cc156ee1fe9c0da2550420d99a21e"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/composer/pcre/zipball/b2bed4734f0cc156ee1fe9c0da2550420d99a21e",
+                "reference": "b2bed4734f0cc156ee1fe9c0da2550420d99a21e",
+                "shasum": ""
+            },
+            "require": {
+                "php": "^7.4 || ^8.0"
+            },
+            "conflict": {
+                "phpstan/phpstan": "<1.11.10"
+            },
+            "require-dev": {
+                "phpstan/phpstan": "^1.12 || ^2",
+                "phpstan/phpstan-strict-rules": "^1 || ^2",
+                "phpunit/phpunit": "^8 || ^9"
+            },
+            "type": "library",
+            "extra": {
+                "phpstan": {
+                    "includes": [
+                        "extension.neon"
+                    ]
+                },
+                "branch-alias": {
+                    "dev-main": "3.x-dev"
+                }
+            },
+            "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.3.2"
+            },
+            "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-11-12T16:29:46+00:00"
+        },
+        {
+            "name": "daandesmedt/phpheadlesschrome",
+            "version": "v1.1.2",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/DaanDeSmedt/PHPHeadlessChrome.git",
+                "reference": "40c6282f49313be00d0954d5cdcc02c80c246491"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/DaanDeSmedt/PHPHeadlessChrome/zipball/40c6282f49313be00d0954d5cdcc02c80c246491",
+                "reference": "40c6282f49313be00d0954d5cdcc02c80c246491",
+                "shasum": ""
+            },
+            "require": {
+                "mikehaertl/php-shellcommand": "^1.2"
+            },
+            "require-dev": {
+                "phpunit/phpunit": "*"
+            },
+            "type": "library",
+            "autoload": {
+                "psr-4": {
+                    "daandesmedt\\PHPHeadlessChrome\\": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Daan De Smedt",
+                    "email": "daan.de.smedt@hotmail.com"
+                }
+            ],
+            "description": "A PHP wrapper for using Google Chrome Headless mode. Convert URL or HTML to a PDF / screenshot. Easy to use and OOP interfaced.",
+            "keywords": [
+                "chrome",
+                "headless",
+                "html",
+                "pdf",
+                "php",
+                "screenshots"
+            ],
+            "support": {
+                "issues": "https://github.com/DaanDeSmedt/PHPHeadlessChrome/issues",
+                "source": "https://github.com/DaanDeSmedt/PHPHeadlessChrome/tree/v1.1.2"
+            },
+            "time": "2024-06-10T12:03:05+00:00"
+        },
+        {
+            "name": "erusev/parsedown",
+            "version": "1.8.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/erusev/parsedown.git",
+                "reference": "96baaad00f71ba04d76e45b4620f54d3beabd6f7"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/erusev/parsedown/zipball/96baaad00f71ba04d76e45b4620f54d3beabd6f7",
+                "reference": "96baaad00f71ba04d76e45b4620f54d3beabd6f7",
+                "shasum": ""
+            },
+            "require": {
+                "ext-mbstring": "*",
+                "php": ">=7.1"
+            },
+            "require-dev": {
+                "phpunit/phpunit": "^7.5|^8.5|^9.6"
+            },
+            "type": "library",
+            "autoload": {
+                "psr-0": {
+                    "Parsedown": ""
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Emanuil Rusev",
+                    "email": "hello@erusev.com",
+                    "homepage": "http://erusev.com"
+                }
+            ],
+            "description": "Parser for Markdown.",
+            "homepage": "http://parsedown.org",
+            "keywords": [
+                "markdown",
+                "parser"
+            ],
+            "support": {
+                "issues": "https://github.com/erusev/parsedown/issues",
+                "source": "https://github.com/erusev/parsedown/tree/1.8.0"
+            },
+            "funding": [
+                {
+                    "url": "https://github.com/erusev",
+                    "type": "github"
+                }
+            ],
+            "time": "2026-02-16T11:41:01+00:00"
+        },
+        {
+            "name": "maennchen/zipstream-php",
+            "version": "3.1.1",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/maennchen/ZipStream-PHP.git",
+                "reference": "6187e9cc4493da94b9b63eb2315821552015fca9"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/maennchen/ZipStream-PHP/zipball/6187e9cc4493da94b9b63eb2315821552015fca9",
+                "reference": "6187e9cc4493da94b9b63eb2315821552015fca9",
+                "shasum": ""
+            },
+            "require": {
+                "ext-mbstring": "*",
+                "ext-zlib": "*",
+                "php-64bit": "^8.1"
+            },
+            "require-dev": {
+                "ext-zip": "*",
+                "friendsofphp/php-cs-fixer": "^3.16",
+                "guzzlehttp/guzzle": "^7.5",
+                "mikey179/vfsstream": "^1.6",
+                "php-coveralls/php-coveralls": "^2.5",
+                "phpunit/phpunit": "^10.0",
+                "vimeo/psalm": "^5.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.1"
+            },
+            "funding": [
+                {
+                    "url": "https://github.com/maennchen",
+                    "type": "github"
+                }
+            ],
+            "time": "2024-10-10T12:33:01+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": "mikehaertl/php-shellcommand",
+            "version": "1.7.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/mikehaertl/php-shellcommand.git",
+                "reference": "e79ea528be155ffdec6f3bf1a4a46307bb49e545"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/mikehaertl/php-shellcommand/zipball/e79ea528be155ffdec6f3bf1a4a46307bb49e545",
+                "reference": "e79ea528be155ffdec6f3bf1a4a46307bb49e545",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">= 5.3.0"
+            },
+            "require-dev": {
+                "phpunit/phpunit": ">4.0 <=9.4"
+            },
+            "type": "library",
+            "autoload": {
+                "psr-4": {
+                    "mikehaertl\\shellcommand\\": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Michael Härtl",
+                    "email": "haertl.mike@gmail.com"
+                }
+            ],
+            "description": "An object oriented interface to shell commands",
+            "keywords": [
+                "shell"
+            ],
+            "support": {
+                "issues": "https://github.com/mikehaertl/php-shellcommand/issues",
+                "source": "https://github.com/mikehaertl/php-shellcommand/tree/1.7.0"
+            },
+            "time": "2023-04-19T08:25:22+00:00"
+        },
+        {
+            "name": "phpmailer/phpmailer",
+            "version": "v6.12.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/PHPMailer/PHPMailer.git",
+                "reference": "d1ac35d784bf9f5e61b424901d5a014967f15b12"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/PHPMailer/PHPMailer/zipball/d1ac35d784bf9f5e61b424901d5a014967f15b12",
+                "reference": "d1ac35d784bf9f5e61b424901d5a014967f15b12",
+                "shasum": ""
+            },
+            "require": {
+                "ext-ctype": "*",
+                "ext-filter": "*",
+                "ext-hash": "*",
+                "php": ">=5.5.0"
+            },
+            "require-dev": {
+                "dealerdirect/phpcodesniffer-composer-installer": "^1.0",
+                "doctrine/annotations": "^1.2.6 || ^1.13.3",
+                "php-parallel-lint/php-console-highlighter": "^1.0.0",
+                "php-parallel-lint/php-parallel-lint": "^1.3.2",
+                "phpcompatibility/php-compatibility": "^9.3.5",
+                "roave/security-advisories": "dev-latest",
+                "squizlabs/php_codesniffer": "^3.7.2",
+                "yoast/phpunit-polyfills": "^1.0.4"
+            },
+            "suggest": {
+                "decomplexity/SendOauth2": "Adapter for using XOAUTH2 authentication",
+                "ext-mbstring": "Needed to send email in multibyte encoding charset or decode encoded addresses",
+                "ext-openssl": "Needed for secure SMTP sending and DKIM signing",
+                "greew/oauth2-azure-provider": "Needed for Microsoft Azure XOAUTH2 authentication",
+                "hayageek/oauth2-yahoo": "Needed for Yahoo XOAUTH2 authentication",
+                "league/oauth2-google": "Needed for Google XOAUTH2 authentication",
+                "psr/log": "For optional PSR-3 debug logging",
+                "symfony/polyfill-mbstring": "To support UTF-8 if the Mbstring PHP extension is not enabled (^1.2)",
+                "thenetworg/oauth2-azure": "Needed for Microsoft XOAUTH2 authentication"
+            },
+            "type": "library",
+            "autoload": {
+                "psr-4": {
+                    "PHPMailer\\PHPMailer\\": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "LGPL-2.1-only"
+            ],
+            "authors": [
+                {
+                    "name": "Marcus Bointon",
+                    "email": "phpmailer@synchromedia.co.uk"
+                },
+                {
+                    "name": "Jim Jagielski",
+                    "email": "jimjag@gmail.com"
+                },
+                {
+                    "name": "Andy Prevost",
+                    "email": "codeworxtech@users.sourceforge.net"
+                },
+                {
+                    "name": "Brent R. Matzelle"
+                }
+            ],
+            "description": "PHPMailer is a full-featured email creation and transfer class for PHP",
+            "support": {
+                "issues": "https://github.com/PHPMailer/PHPMailer/issues",
+                "source": "https://github.com/PHPMailer/PHPMailer/tree/v6.12.0"
+            },
+            "funding": [
+                {
+                    "url": "https://github.com/Synchro",
+                    "type": "github"
+                }
+            ],
+            "time": "2025-10-15T16:49:08+00:00"
+        },
+        {
+            "name": "phpoffice/phpspreadsheet",
+            "version": "5.5.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/PHPOffice/PhpSpreadsheet.git",
+                "reference": "eecd31b885a1c8192f12738130f85bbc6e8906ba"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/PHPOffice/PhpSpreadsheet/zipball/eecd31b885a1c8192f12738130f85bbc6e8906ba",
+                "reference": "eecd31b885a1c8192f12738130f85bbc6e8906ba",
+                "shasum": ""
+            },
+            "require": {
+                "composer/pcre": "^1||^2||^3",
+                "ext-ctype": "*",
+                "ext-dom": "*",
+                "ext-fileinfo": "*",
+                "ext-filter": "*",
+                "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/simple-cache": "^1.0 || ^2.0 || ^3.0"
+            },
+            "require-dev": {
+                "dealerdirect/phpcodesniffer-composer-installer": "dev-main",
+                "dompdf/dompdf": "^2.0 || ^3.0",
+                "ext-intl": "*",
+                "friendsofphp/php-cs-fixer": "^3.2",
+                "mitoteam/jpgraph": "^10.5",
+                "mpdf/mpdf": "^8.1.1",
+                "phpcompatibility/php-compatibility": "^9.3",
+                "phpstan/phpstan": "^1.1 || ^2.0",
+                "phpstan/phpstan-deprecation-rules": "^1.0 || ^2.0",
+                "phpstan/phpstan-phpunit": "^1.0 || ^2.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, required for NumberFormat Wizard and StringHelper::setLocale()",
+                "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"
+                },
+                {
+                    "name": "Owen Leibman"
+                }
+            ],
+            "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/5.5.0"
+            },
+            "time": "2026-03-01T00:58:56+00:00"
+        },
+        {
+            "name": "psr/simple-cache",
+            "version": "3.0.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/php-fig/simple-cache.git",
+                "reference": "764e0b3939f5ca87cb904f570ef9be2d78a07865"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/php-fig/simple-cache/zipball/764e0b3939f5ca87cb904f570ef9be2d78a07865",
+                "reference": "764e0b3939f5ca87cb904f570ef9be2d78a07865",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=8.0.0"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "3.0.x-dev"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "Psr\\SimpleCache\\": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "PHP-FIG",
+                    "homepage": "https://www.php-fig.org/"
+                }
+            ],
+            "description": "Common interfaces for simple caching",
+            "keywords": [
+                "cache",
+                "caching",
+                "psr",
+                "psr-16",
+                "simple-cache"
+            ],
+            "support": {
+                "source": "https://github.com/php-fig/simple-cache/tree/3.0.0"
+            },
+            "time": "2021-10-29T13:26:27+00:00"
+        },
+        {
+            "name": "smalot/pdfparser",
+            "version": "v2.12.4",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/smalot/pdfparser.git",
+                "reference": "028d7cc0ceff323bc001d763caa2bbdf611866c4"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/smalot/pdfparser/zipball/028d7cc0ceff323bc001d763caa2bbdf611866c4",
+                "reference": "028d7cc0ceff323bc001d763caa2bbdf611866c4",
+                "shasum": ""
+            },
+            "require": {
+                "ext-iconv": "*",
+                "ext-zlib": "*",
+                "php": ">=7.1",
+                "symfony/polyfill-mbstring": "^1.18"
+            },
+            "type": "library",
+            "autoload": {
+                "psr-0": {
+                    "Smalot\\PdfParser\\": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "LGPL-3.0"
+            ],
+            "authors": [
+                {
+                    "name": "Sebastien MALOT",
+                    "email": "sebastien@malot.fr"
+                }
+            ],
+            "description": "Pdf parser library. Can read and extract information from pdf file.",
+            "homepage": "https://www.pdfparser.org",
+            "keywords": [
+                "extract",
+                "parse",
+                "parser",
+                "pdf",
+                "text"
+            ],
+            "support": {
+                "issues": "https://github.com/smalot/pdfparser/issues",
+                "source": "https://github.com/smalot/pdfparser/tree/v2.12.4"
+            },
+            "time": "2026-03-10T15:39:47+00:00"
+        },
+        {
+            "name": "symfony/polyfill-mbstring",
+            "version": "v1.33.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/symfony/polyfill-mbstring.git",
+                "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/6d857f4d76bd4b343eac26d6b539585d2bc56493",
+                "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493",
+                "shasum": ""
+            },
+            "require": {
+                "ext-iconv": "*",
+                "php": ">=7.2"
+            },
+            "provide": {
+                "ext-mbstring": "*"
+            },
+            "suggest": {
+                "ext-mbstring": "For best performance"
+            },
+            "type": "library",
+            "extra": {
+                "thanks": {
+                    "url": "https://github.com/symfony/polyfill",
+                    "name": "symfony/polyfill"
+                }
+            },
+            "autoload": {
+                "files": [
+                    "bootstrap.php"
+                ],
+                "psr-4": {
+                    "Symfony\\Polyfill\\Mbstring\\": ""
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Nicolas Grekas",
+                    "email": "p@tchwork.com"
+                },
+                {
+                    "name": "Symfony Community",
+                    "homepage": "https://symfony.com/contributors"
+                }
+            ],
+            "description": "Symfony polyfill for the Mbstring extension",
+            "homepage": "https://symfony.com",
+            "keywords": [
+                "compatibility",
+                "mbstring",
+                "polyfill",
+                "portable",
+                "shim"
+            ],
+            "support": {
+                "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.33.0"
+            },
+            "funding": [
+                {
+                    "url": "https://symfony.com/sponsor",
+                    "type": "custom"
+                },
+                {
+                    "url": "https://github.com/fabpot",
+                    "type": "github"
+                },
+                {
+                    "url": "https://github.com/nicolas-grekas",
+                    "type": "github"
+                },
+                {
+                    "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+                    "type": "tidelift"
+                }
+            ],
+            "time": "2024-12-23T08:48:59+00:00"
+        }
+    ],
+    "packages-dev": [],
+    "aliases": [],
+    "minimum-stability": "stable",
+    "stability-flags": [],
+    "prefer-stable": false,
+    "prefer-lowest": false,
+    "platform": [],
+    "platform-dev": [],
+    "plugin-api-version": "2.6.0"
+}

+ 101 - 32
controllers/soilImportController.php

@@ -117,36 +117,97 @@ if (empty($rawData)) {
 // ─── format detection ─────────────────────────────────────────────────────────
 //
 // Two layouts found in CSBP lab files:
-//   ROW-BASED    — Row 0 = column headers (LAB_NUMBER, TEXTURE, PH_CACL2 …)
-//                  Rows 1-N = one sample per row.
-//   TRANSPOSED   — Column 0 = row labels (EC 1:5, Total P % …)
-//                  Columns 1-N = one sample per column.
 //
-// Heuristic: if the first cell of row 0 looks like a short code/identifier
-// (< 20 chars, no spaces, all-caps or underscored) → ROW-BASED.
-// Otherwise → TRANSPOSED.
+//   TRANSPOSED (lab card)  — Column 0 = row labels ("EC 1:5", "Total P %", …)
+//                            Columns 1-N = one sample each.
+//                            Example: SOIL CONTROL XNS06189.xls
+//
+//   ROW-BASED (report)     — One row = column headers; subsequent rows = samples.
+//                            May have 1-3 title/subtitle rows above the headers.
+//                            Example: S-C Soil Tests 2006.xls, YOS06 42-48.xlsx
+//
+// Detection strategy:
+//   1. Score column-0 values for soil-chemistry label patterns (units, element
+//      names, "1:5", "ppm", etc.).  ≥2 matches → transposed.
+//   2. Otherwise scan the first 10 rows for a "header row" — the row that best
+//      matches known CSBP column-code keywords.  Everything above it is a title.
 
-function isRowBased(array $rawData): bool
+/**
+ * Returns ['transposed' => bool, 'headerRow' => int]
+ */
+function detectFormat(array $rawData): array
 {
-    $firstCell = $rawData[0][0] ?? '';
-    // Short, code-like headers signal a row-based layout
-    return strlen($firstCell) < 25
-        && !str_contains($firstCell, ' ')
-        && $firstCell !== '';
-}
+    // Phrases that appear in column-0 of a transposed lab card
+    $transposedSignals = [
+        '1:5', 'total p', 'total k', 'total ca', 'total mg', 'total na',
+        'total s', 'total n', 'total b', 'total zn', 'total mn', 'total fe',
+        'total cu', 'total cl', 'organic matter', 'organic carbon',
+        'lab id', 'lab performing', 'field name', 'nitrate ppm',
+        'ph 1:5', 'moisture %', 'consultant',
+    ];
+
+    // Phrases that appear in the header row of a row-based file
+    $rowBasedSignals = [
+        'lab_number', 'custno', 'paddock', 'ph_cacl2', 'ph_h2o',
+        'dtpa_cu', 'dtpa_zn', 'dtpa_mn', 'dtpa_fe', 'conducty',
+        'orgcarbon', 'nitrate', 'nammonium', 'texture', 'gravel',
+        'exc_ca', 'exc_mg', 'exc_na', 'exc_k', 'alum_cacl2',
+        'boron_hot', 'client name', 'lab number', 'sat_ca', 'sat_mg',
+        'sat_k', 'sat_na', 'crop', 'sp%',
+    ];
+
+    // Step 1: score column-0 values for transposed signals
+    $transposedScore = 0;
+    foreach (array_slice($rawData, 0, 20) as $row) {
+        $cell = strtolower($row[0] ?? '');
+        if ($cell === '') {
+            continue;
+        }
+        foreach ($transposedSignals as $signal) {
+            if (str_contains($cell, $signal)) {
+                $transposedScore++;
+                break;
+            }
+        }
+    }
 
-$transposed = !isRowBased($rawData);
+    if ($transposedScore >= 2) {
+        return ['transposed' => true, 'headerRow' => 0];
+    }
+
+    // Step 2: find the best header row in a row-based file
+    $bestRow   = 0;
+    $bestScore = 0;
+    for ($i = 0; $i < min(10, count($rawData)); $i++) {
+        $score = 0;
+        foreach ($rawData[$i] as $cell) {
+            $cell = strtolower(trim($cell));
+            if ($cell === '') {
+                continue;
+            }
+            foreach ($rowBasedSignals as $signal) {
+                if (str_contains($cell, $signal)) {
+                    $score++;
+                    break;
+                }
+            }
+        }
+        if ($score > $bestScore) {
+            $bestScore = $score;
+            $bestRow   = $i;
+        }
+    }
+
+    return ['transposed' => false, 'headerRow' => $bestRow];
+}
 
 // ─── extract samples ──────────────────────────────────────────────────────────
-//
-// Returns an array of samples, each sample being an assoc array of
-// label → value.
 
-function extractSamplesRowBased(array $rawData): array
+function extractSamplesRowBased(array $rawData, int $headerRow): array
 {
-    $headers = $rawData[0];
+    $headers = $rawData[$headerRow];
     $samples = [];
-    for ($r = 1; $r < count($rawData); $r++) {
+    for ($r = $headerRow + 1; $r < count($rawData); $r++) {
         $row    = $rawData[$r];
         $sample = [];
         foreach ($headers as $c => $header) {
@@ -164,30 +225,33 @@ function extractSamplesRowBased(array $rawData): array
 
 function extractSamplesTransposed(array $rawData): array
 {
-    // Column 0 = labels; columns 1-N = samples
-    $labels      = array_column($rawData, 0);
-    $numSamples  = max(array_map('count', $rawData)) - 1;
-    $samples     = [];
+    // Column 0 = labels; columns 1-N = samples.
+    // Count sample columns from the widest row.
+    $labels     = array_column($rawData, 0);
+    $maxCols    = max(array_map('count', $rawData));
+    $samples    = [];
 
-    for ($col = 1; $col <= $numSamples; $col++) {
+    for ($col = 1; $col < $maxCols; $col++) {
         $sample = [];
         foreach ($rawData as $rowIdx => $row) {
-            $label = $labels[$rowIdx] ?? '';
-            $value = $row[$col] ?? '';
+            $label = trim($labels[$rowIdx] ?? '');
+            $value = trim($row[$col] ?? '');
             if ($label !== '' && $value !== '') {
                 $sample[$label] = $value;
             }
         }
-        if (array_filter($sample)) {
+        // Only keep columns that have at least a few populated cells
+        if (count(array_filter($sample)) >= 3) {
             $samples[] = $sample;
         }
     }
     return $samples;
 }
 
-$samples = $transposed
+$fmt     = detectFormat($rawData);
+$samples = $fmt['transposed']
     ? extractSamplesTransposed($rawData)
-    : extractSamplesRowBased($rawData);
+    : extractSamplesRowBased($rawData, $fmt['headerRow']);
 
 if (empty($samples)) {
     jsonError('No samples found in the file.');
@@ -213,7 +277,12 @@ if ($action === 'parse') {
             'site'   => $pad,
         ];
     }
-    jsonOk(['samples' => $list, 'count' => count($samples)]);
+    jsonOk([
+        'samples'    => $list,
+        'count'      => count($samples),
+        'format'     => $fmt['transposed'] ? 'transposed' : 'row-based',
+        'header_row' => $fmt['headerRow'] ?? 0,
+    ]);
 }
 
 // ─── action: import ───────────────────────────────────────────────────────────

+ 1 - 1
dashboard/crop-analysis/soil-test-data/index.php

@@ -68,7 +68,7 @@ include __DIR__ . '/../../../layouts/navbar.php';
                                     <div class="col-md-8">
                                         <label for="lab-file-input" class="form-label fw-semibold">Select lab file</label>
                                         <input type="file"
-                                               class="form-control"
+                                               class="form-control form-control-sm"
                                                id="lab-file-input"
                                                accept=".xls,.xlsx,.csv,.ods">
                                     </div>

BIN
doc/SOIL CONTROL YOS06 42-48 - Copy.xlsx