From be0379347881308a24407c4ec1cf86a57b1c2b71 Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Sun, 21 Jun 2026 11:52:48 -0500 Subject: [PATCH 01/64] =?UTF-8?q?fix:=20review=20#18=20=E2=80=94=20Warrant?= =?UTF-8?q?yHelper=20$db=20before=20init,=20TruckStock=20negative=20guard?= =?UTF-8?q?=20+=20raw=20SQL,=20tech=5Fid=20column=20name?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/Helper/CustomerSatisfactionHelper.php | 2 +- .../src/Helper/TruckStockHelper.php | 11 ++++++++--- .../src/Helper/WarrantyHelper.php | 4 ++-- 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/source/packages/plg_system_mokosuitefield/src/Helper/CustomerSatisfactionHelper.php b/source/packages/plg_system_mokosuitefield/src/Helper/CustomerSatisfactionHelper.php index 29e78cb..80bf6f9 100644 --- a/source/packages/plg_system_mokosuitefield/src/Helper/CustomerSatisfactionHelper.php +++ b/source/packages/plg_system_mokosuitefield/src/Helper/CustomerSatisfactionHelper.php @@ -101,7 +101,7 @@ class CustomerSatisfactionHelper ->select('SUM(CASE WHEN s.rating >= 4 THEN 1 ELSE 0 END) AS five_star_count') ->from($db->quoteName('#__mokosuitefield_surveys', 's')) ->join('INNER', $db->quoteName('#__mokosuitefield_work_orders', 'wo') . ' ON wo.id = s.work_order_id') - ->join('INNER', $db->quoteName('#__mokosuitefield_technicians', 't') . ' ON t.id = wo.tech_id') + ->join('INNER', $db->quoteName('#__mokosuitefield_technicians', 't') . ' ON t.id = wo.technician_id') ->join('LEFT', $db->quoteName('#__contact_details', 'cd') . ' ON cd.id = t.contact_id') ->group('t.id, cd.name') ->having('COUNT(s.id) >= 3') diff --git a/source/packages/plg_system_mokosuitefield/src/Helper/TruckStockHelper.php b/source/packages/plg_system_mokosuitefield/src/Helper/TruckStockHelper.php index fbd7aa6..7ed3a91 100644 --- a/source/packages/plg_system_mokosuitefield/src/Helper/TruckStockHelper.php +++ b/source/packages/plg_system_mokosuitefield/src/Helper/TruckStockHelper.php @@ -47,8 +47,9 @@ class TruckStockHelper $db->setQuery($db->getQuery(true) ->update('#__mokosuitefield_truck_stock') ->set('quantity = quantity - ' . (float) $qty) - ->where('vehicle_id = ' . $vehicleId) - ->where('product_id = ' . $productId)); + ->where('vehicle_id = ' . (int) $vehicleId) + ->where('product_id = ' . (int) $productId) + ->where('quantity >= ' . (float) $qty)); $db->execute(); return $db->getAffectedRows() > 0; @@ -58,7 +59,11 @@ class TruckStockHelper { $db = Factory::getContainer()->get(DatabaseInterface::class); - $db->setQuery("INSERT INTO #__mokosuitefield_truck_stock (vehicle_id, product_id, quantity, last_restocked) VALUES ({$vehicleId}, {$productId}, {$qty}, CURDATE()) ON DUPLICATE KEY UPDATE quantity = quantity + {$qty}, last_restocked = CURDATE()"); + $db->setQuery( + 'INSERT INTO #__mokosuitefield_truck_stock (vehicle_id, product_id, quantity, last_restocked)' + . ' VALUES (' . (int) $vehicleId . ', ' . (int) $productId . ', ' . (float) $qty . ', CURDATE())' + . ' ON DUPLICATE KEY UPDATE quantity = quantity + ' . (float) $qty . ', last_restocked = CURDATE()' + ); $db->execute(); } } diff --git a/source/packages/plg_system_mokosuitefield/src/Helper/WarrantyHelper.php b/source/packages/plg_system_mokosuitefield/src/Helper/WarrantyHelper.php index 093fef5..ae56d77 100644 --- a/source/packages/plg_system_mokosuitefield/src/Helper/WarrantyHelper.php +++ b/source/packages/plg_system_mokosuitefield/src/Helper/WarrantyHelper.php @@ -57,14 +57,14 @@ class WarrantyHelper if (empty($warranty->id)) return (object) ['success' => false, 'error' => 'Equipment not found']; if (!$warranty->under_warranty) return (object) ['success' => false, 'error' => 'Warranty expired']; + $db = Factory::getContainer()->get(DatabaseInterface::class); + // Verify work order exists and is linked to this equipment $db->setQuery($db->getQuery(true)->select('id')->from('#__mokosuitefield_work_orders') ->where('id = ' . (int) $woId)); if (!(int) $db->loadResult()) { return (object) ['success' => false, 'error' => 'Invalid work order']; } - - $db = Factory::getContainer()->get(DatabaseInterface::class); $now = Factory::getDate()->toSql(); $claim = (object) [ From 162298f8f987d99385adba26653acfa434a732cb Mon Sep 17 00:00:00 2001 From: "gitea-actions[bot]" Date: Sun, 21 Jun 2026 16:53:23 +0000 Subject: [PATCH 02/64] chore(version): auto-bump patch 01.08.01-dev [skip ci] --- .mokogitea/workflows/issue-branch.yml | 2 +- source/pkg_mokosuitefield.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.mokogitea/workflows/issue-branch.yml b/.mokogitea/workflows/issue-branch.yml index 5b2b0d0..e4afee6 100644 --- a/.mokogitea/workflows/issue-branch.yml +++ b/.mokogitea/workflows/issue-branch.yml @@ -5,7 +5,7 @@ # FILE INFORMATION # DEFGROUP: Gitea.Workflow # INGROUP: mokocli.Automation -# VERSION: 01.08.00 +# VERSION: 01.08.01 # BRIEF: Auto-create feature branch when an issue is opened name: "Universal: Issue Branch" diff --git a/source/pkg_mokosuitefield.xml b/source/pkg_mokosuitefield.xml index de66a2c..780d6b9 100644 --- a/source/pkg_mokosuitefield.xml +++ b/source/pkg_mokosuitefield.xml @@ -2,7 +2,7 @@ Package - MokoSuite Field mokosuitefield - 01.08.00 + 01.08.01 2026-06-12 Moko Consulting hello@mokoconsulting.tech From d7e6ec338e2d611f5d21dc777ebc01289c9ba3dd Mon Sep 17 00:00:00 2001 From: "gitea-actions[bot]" Date: Sun, 21 Jun 2026 16:53:32 +0000 Subject: [PATCH 03/64] chore(version): pre-release bump to 01.08.02-dev [skip ci] --- .mokogitea/workflows/issue-branch.yml | 2 +- source/pkg_mokosuitefield.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.mokogitea/workflows/issue-branch.yml b/.mokogitea/workflows/issue-branch.yml index e4afee6..e72b7b8 100644 --- a/.mokogitea/workflows/issue-branch.yml +++ b/.mokogitea/workflows/issue-branch.yml @@ -5,7 +5,7 @@ # FILE INFORMATION # DEFGROUP: Gitea.Workflow # INGROUP: mokocli.Automation -# VERSION: 01.08.01 +# VERSION: 01.08.02 # BRIEF: Auto-create feature branch when an issue is opened name: "Universal: Issue Branch" diff --git a/source/pkg_mokosuitefield.xml b/source/pkg_mokosuitefield.xml index 780d6b9..0e5590d 100644 --- a/source/pkg_mokosuitefield.xml +++ b/source/pkg_mokosuitefield.xml @@ -2,7 +2,7 @@ Package - MokoSuite Field mokosuitefield - 01.08.01 + 01.08.02 2026-06-12 Moko Consulting hello@mokoconsulting.tech From 63ffd7ea2858771534b38eee76421e60651f42de Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Mon, 22 Jun 2026 22:21:54 -0500 Subject: [PATCH 04/64] =?UTF-8?q?chore:=20fix=20workflows=20=E2=80=94=20re?= =?UTF-8?q?move=20generic=20template=20extras,=20add=20Joomla=20template?= =?UTF-8?q?=20files?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .mokogitea/workflows/ci-joomla.yml | 903 +++++++++++++++++++++ .mokogitea/workflows/composer-publish.yml | 76 -- .mokogitea/workflows/deploy-manual.yml | 126 --- .mokogitea/workflows/pr-metadata-check.yml | 71 ++ 4 files changed, 974 insertions(+), 202 deletions(-) create mode 100644 .mokogitea/workflows/ci-joomla.yml delete mode 100644 .mokogitea/workflows/composer-publish.yml delete mode 100644 .mokogitea/workflows/deploy-manual.yml create mode 100644 .mokogitea/workflows/pr-metadata-check.yml diff --git a/.mokogitea/workflows/ci-joomla.yml b/.mokogitea/workflows/ci-joomla.yml new file mode 100644 index 0000000..727f661 --- /dev/null +++ b/.mokogitea/workflows/ci-joomla.yml @@ -0,0 +1,903 @@ +# Copyright (C) 2026 Moko Consulting +# +# This file is part of a Moko Consulting project. +# +# SPDX-License-Identifier: GPL-3.0-or-later +# +# FILE INFORMATION +# DEFGROUP: Gitea.Workflow.Template +# INGROUP: MokoStandards.CI +# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/MokoStandards-API +# PATH: /templates/workflows/joomla/ci-joomla.yml.template +# VERSION: 04.06.00 +# BRIEF: CI workflow for Joomla extensions — lint, validate, test + +name: "Joomla: Extension CI" + +on: + pull_request: + branches: + - main + - 'dev/**' + workflow_dispatch: + +permissions: + contents: read + pull-requests: write + +env: + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true + +jobs: + lint-and-validate: + name: Lint & Validate + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup PHP + run: | + if ! command -v php &> /dev/null; then + sudo apt-get update -qq + sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1 + fi + php -v && composer --version + + - name: Setup mokocli tools + env: + MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN || secrets.GA_TOKEN || github.token }} + MOKO_CLONE_HOST: ${{ secrets.MOKOGITEA_TOKEN && 'git.mokoconsulting.tech/MokoConsulting' || 'github.com/mokoconsulting-tech' }} + run: | + if [ -d "/opt/mokocli" ] || [ -d "/tmp/mokocli" ]; then + echo "mokocli already available on runner — skipping clone" + else + git clone --depth 1 --branch main --quiet \ + "https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/mokocli.git" \ + /tmp/mokocli 2>/dev/null || echo "mokocli clone skipped — continuing without it" + fi + + - name: Install dependencies + env: + COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_TOKEN || secrets.GA_TOKEN || github.token }}"}}' + run: | + if [ -f "composer.json" ]; then + composer install \ + --no-interaction \ + --prefer-dist \ + --optimize-autoloader + else + echo "No composer.json found — skipping dependency install" + fi + + - name: PHP syntax check + run: | + ERRORS=0 + for DIR in src/ htdocs/; do + if [ -d "$DIR" ]; then + FOUND=1 + while IFS= read -r -d '' FILE; do + OUTPUT=$(php -l "$FILE" 2>&1) + if echo "$OUTPUT" | grep -q "Parse error"; then + echo "::error file=${FILE}::${OUTPUT}" + ERRORS=$((ERRORS + 1)) + fi + done < <(find "$DIR" -name "*.php" -print0) + fi + done + echo "### PHP Syntax Check" >> $GITHUB_STEP_SUMMARY + if [ "${ERRORS}" -gt 0 ]; then + echo "**${ERRORS} syntax error(s) found.**" >> $GITHUB_STEP_SUMMARY + exit 1 + else + echo "All PHP files passed syntax check." >> $GITHUB_STEP_SUMMARY + fi + + - name: XML manifest validation + run: | + echo "### XML Manifest Validation" >> $GITHUB_STEP_SUMMARY + ERRORS=0 + + # Find the extension manifest (XML with /dev/null; then + MANIFEST="$XML_FILE" + break + fi + done + + if [ -z "$MANIFEST" ]; then + echo "No Joomla extension manifest found (XML file with \`> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS + 1)) + else + echo "Manifest found: \`${MANIFEST}\`" >> $GITHUB_STEP_SUMMARY + + # Validate well-formed XML + php -r " + \$xml = @simplexml_load_file('$MANIFEST'); + if (\$xml === false) { + echo 'INVALID'; + exit(1); + } + echo 'VALID'; + " > /tmp/xml_result 2>&1 + XML_RESULT=$(cat /tmp/xml_result) + if [ "$XML_RESULT" != "VALID" ]; then + echo "Manifest is not well-formed XML." >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS + 1)) + else + echo "Manifest is well-formed XML." >> $GITHUB_STEP_SUMMARY + fi + + # Check required tags: name, version, author + for TAG in name version author; do + if ! grep -q "<${TAG}>" "$MANIFEST" 2>/dev/null; then + echo "Missing required tag: \`<${TAG}>\`" >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS + 1)) + else + echo "Found required tag: \`<${TAG}>\`" >> $GITHUB_STEP_SUMMARY + fi + done + + # Namespace is required for components/plugins but not packages + EXT_TYPE=$(grep -oP ']*\btype="\K[^"]+' "$MANIFEST" | head -1) + if [ "$EXT_TYPE" != "package" ]; then + if ! grep -q "/dev/null; then + echo "Missing required tag: \`\` (required for Joomla 5+ ${EXT_TYPE} extensions)" >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS + 1)) + else + echo "Found required tag: \`\`" >> $GITHUB_STEP_SUMMARY + fi + else + echo "Package extension — \`\` not required." >> $GITHUB_STEP_SUMMARY + fi + fi + + if [ "${ERRORS}" -gt 0 ]; then + echo "" >> $GITHUB_STEP_SUMMARY + echo "**${ERRORS} manifest issue(s) found.**" >> $GITHUB_STEP_SUMMARY + exit 1 + else + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Manifest validation passed.**" >> $GITHUB_STEP_SUMMARY + fi + + - name: Check language files referenced in manifest + run: | + echo "### Language File Check" >> $GITHUB_STEP_SUMMARY + ERRORS=0 + + MANIFEST="" + for XML_FILE in $(find . -maxdepth 2 -name "*.xml" -not -path "./.git/*" -not -path "./vendor/*"); do + if grep -q "/dev/null; then + MANIFEST="$XML_FILE" + break + fi + done + + if [ -n "$MANIFEST" ]; then + # Extract language file references from manifest + LANG_FILES=$(grep -oP 'language\s+tag="[^"]*"[^>]*>\K[^<]+' "$MANIFEST" 2>/dev/null || true) + if [ -z "$LANG_FILES" ]; then + echo "No language file references found in manifest — skipping." >> $GITHUB_STEP_SUMMARY + else + while IFS= read -r LANG_FILE; do + LANG_FILE=$(echo "$LANG_FILE" | xargs) + if [ -z "$LANG_FILE" ]; then + continue + fi + # Check in common locations + FOUND=0 + for BASE in "." "src" "htdocs"; do + if [ -f "${BASE}/${LANG_FILE}" ]; then + FOUND=1 + break + fi + done + if [ "$FOUND" -eq 0 ]; then + echo "Missing language file: \`${LANG_FILE}\`" >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS + 1)) + else + echo "Language file present: \`${LANG_FILE}\`" >> $GITHUB_STEP_SUMMARY + fi + done <<< "$LANG_FILES" + fi + else + echo "No manifest found — skipping language check." >> $GITHUB_STEP_SUMMARY + fi + + if [ "${ERRORS}" -gt 0 ]; then + echo "" >> $GITHUB_STEP_SUMMARY + echo "**${ERRORS} missing language file(s).**" >> $GITHUB_STEP_SUMMARY + exit 1 + else + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Language file check passed.**" >> $GITHUB_STEP_SUMMARY + fi + + - name: Check index.html files in directories + run: | + echo "### Index.html Check" >> $GITHUB_STEP_SUMMARY + MISSING=0 + CHECKED=0 + + for DIR in src/ htdocs/; do + if [ -d "$DIR" ]; then + while IFS= read -r -d '' SUBDIR; do + CHECKED=$((CHECKED + 1)) + if [ ! -f "${SUBDIR}/index.html" ]; then + echo "Missing index.html in: \`${SUBDIR}\`" >> $GITHUB_STEP_SUMMARY + MISSING=$((MISSING + 1)) + fi + done < <(find "$DIR" -type d -print0) + fi + done + + if [ "${CHECKED}" -eq 0 ]; then + echo "No src/ or htdocs/ directories found — skipping." >> $GITHUB_STEP_SUMMARY + elif [ "${MISSING}" -gt 0 ]; then + echo "" >> $GITHUB_STEP_SUMMARY + echo "**${MISSING} director(ies) missing index.html out of ${CHECKED} checked.**" >> $GITHUB_STEP_SUMMARY + exit 1 + else + echo "All ${CHECKED} directories contain index.html." >> $GITHUB_STEP_SUMMARY + fi + + - name: Check config.xml and access.xml for components + run: | + echo "### Component Config & ACL Check" >> $GITHUB_STEP_SUMMARY + ERRORS=0 + + # Find all component manifests (XML with type="component") + COMP_MANIFESTS=$(find . -maxdepth 4 -name "*.xml" -not -path "./.git/*" -not -path "./vendor/*" -exec grep -l ']*type="component"' {} ; 2>/dev/null || true) + + if [ -z "$COMP_MANIFESTS" ]; then + echo "No component extensions found — skipping." >> $GITHUB_STEP_SUMMARY + else + for MANIFEST in $COMP_MANIFESTS; do + COMP_DIR=$(dirname "$MANIFEST") + COMP_NAME=$(basename "$COMP_DIR") + echo "Component: `${COMP_NAME}` (manifest: `${MANIFEST}`)" >> $GITHUB_STEP_SUMMARY + + # Check access.xml exists + ACCESS_FILE=$(find "$COMP_DIR" -name "access.xml" -not -path "./.git/*" 2>/dev/null | head -1) + if [ -z "$ACCESS_FILE" ]; then + echo "- Missing `access.xml` — ACL permissions will not work." >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS + 1)) + else + if command -v php &> /dev/null; then + if ! php -r "@simplexml_load_file('$ACCESS_FILE') ?: exit(1);" 2>/dev/null; then + echo "- `access.xml` is not well-formed XML." >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS + 1)) + else + for ACTION in core.admin core.manage; do + if ! grep -q "name=\"${ACTION}\"" "$ACCESS_FILE" 2>/dev/null; then + echo "- `access.xml` missing required action: `${ACTION}`" >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS + 1)) + fi + done + echo "- `access.xml`: valid" >> $GITHUB_STEP_SUMMARY + fi + fi + fi + + # Check config.xml exists + CONFIG_FILE=$(find "$COMP_DIR" -name "config.xml" -not -path "./.git/*" 2>/dev/null | head -1) + if [ -z "$CONFIG_FILE" ]; then + echo "- Missing `config.xml` — component Options page will be empty." >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS + 1)) + else + if command -v php &> /dev/null; then + if ! php -r "@simplexml_load_file('$CONFIG_FILE') ?: exit(1);" 2>/dev/null; then + echo "- `config.xml` is not well-formed XML." >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS + 1)) + else + echo "- `config.xml`: valid" >> $GITHUB_STEP_SUMMARY + fi + fi + fi + done + fi + + echo "" >> $GITHUB_STEP_SUMMARY + if [ "${ERRORS}" -gt 0 ]; then + echo "**${ERRORS} config/ACL issue(s) found.**" >> $GITHUB_STEP_SUMMARY + exit 1 + else + echo "**Component config & ACL check passed.**" >> $GITHUB_STEP_SUMMARY + fi + + - name: SQL schema validation + run: | + echo "### SQL Schema Validation" >> $GITHUB_STEP_SUMMARY + ERRORS=0 + + # Find SQL files in source/htdocs + SQL_FILES=$(find . -name "*.sql" -path "*/sql/*" -not -path "./.git/*" -not -path "./vendor/*" 2>/dev/null) + if [ -z "$SQL_FILES" ]; then + echo "No SQL files found — skipping." >> $GITHUB_STEP_SUMMARY + else + echo "Found $(echo "$SQL_FILES" | wc -l) SQL file(s)" >> $GITHUB_STEP_SUMMARY + + for FILE in $SQL_FILES; do + # Basic syntax check: balanced parentheses, no empty files + SIZE=$(wc -c < "$FILE" | tr -d ' ') + if [ "$SIZE" -eq 0 ]; then + echo "- Empty SQL file: \`${FILE}\`" >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS + 1)) + continue + fi + + # Check for common SQL errors + if grep -qP '^\s*$' "$FILE" && [ "$SIZE" -lt 5 ]; then + echo "- Whitespace-only SQL file: \`${FILE}\`" >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS + 1)) + continue + fi + + echo "- \`${FILE}\`: ${SIZE} bytes" >> $GITHUB_STEP_SUMMARY + done + + # Check update SQL files follow version numbering pattern + UPDATE_DIR=$(find . -path "*/sql/updates/mysql" -type d -not -path "./.git/*" 2>/dev/null | head -1) + if [ -n "$UPDATE_DIR" ]; then + BAD_NAMES=0 + for UFILE in "$UPDATE_DIR"/*.sql; do + [ ! -f "$UFILE" ] && continue + BASENAME=$(basename "$UFILE" .sql) + if ! echo "$BASENAME" | grep -qP '^\d+\.\d+\.\d+'; then + echo "- Update file \`${UFILE}\` does not follow version naming (expected X.Y.Z.sql)" >> $GITHUB_STEP_SUMMARY + BAD_NAMES=$((BAD_NAMES + 1)) + fi + done + if [ "$BAD_NAMES" -gt 0 ]; then + ERRORS=$((ERRORS + BAD_NAMES)) + fi + fi + fi + + echo "" >> $GITHUB_STEP_SUMMARY + if [ "${ERRORS}" -gt 0 ]; then + echo "**${ERRORS} SQL issue(s) found.**" >> $GITHUB_STEP_SUMMARY + exit 1 + else + echo "**SQL schema validation passed.**" >> $GITHUB_STEP_SUMMARY + fi + + - name: Manifest file references check + run: | + echo "### Manifest File References" >> $GITHUB_STEP_SUMMARY + ERRORS=0 + + MANIFEST="" + for XML_FILE in $(find . -maxdepth 2 -name "*.xml" -not -path "./.git/*" -not -path "./vendor/*"); do + if grep -q "/dev/null; then + MANIFEST="$XML_FILE" + break + fi + done + + if [ -z "$MANIFEST" ]; then + echo "No manifest found — skipping." >> $GITHUB_STEP_SUMMARY + else + MANIFEST_DIR=$(dirname "$MANIFEST") + + # Check references + FILENAMES=$(grep -oP ']*>\K[^<]+' "$MANIFEST" 2>/dev/null || true) + for F in $FILENAMES; do + if [ ! -f "${MANIFEST_DIR}/${F}" ] && [ ! -d "${MANIFEST_DIR}/${F}" ]; then + echo "- Missing: \`${F}\` (referenced in manifest)" >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS + 1)) + fi + done + + # Check references + FOLDERS=$(grep -oP ']*>\K[^<]+' "$MANIFEST" 2>/dev/null || true) + for F in $FOLDERS; do + if [ ! -d "${MANIFEST_DIR}/${F}" ]; then + echo "- Missing folder: \`${F}\` (referenced in manifest)" >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS + 1)) + fi + done + + # Check references in package manifests (ZIP files won't exist in source) + EXT_TYPE=$(grep -oP ']*\btype="\K[^"]+' "$MANIFEST" | head -1) + if [ "$EXT_TYPE" != "package" ]; then + FILES=$(grep -oP ']*>\K[^<]+' "$MANIFEST" 2>/dev/null || true) + for F in $FILES; do + if [ ! -f "${MANIFEST_DIR}/${F}" ]; then + echo "- Missing file: \`${F}\` (referenced in manifest)" >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS + 1)) + fi + done + fi + fi + + echo "" >> $GITHUB_STEP_SUMMARY + if [ "${ERRORS}" -gt 0 ]; then + echo "**${ERRORS} missing file reference(s).**" >> $GITHUB_STEP_SUMMARY + exit 1 + else + echo "**Manifest file references check passed.**" >> $GITHUB_STEP_SUMMARY + fi + + - name: Form XML validation + run: | + echo "### Form XML Validation" >> $GITHUB_STEP_SUMMARY + ERRORS=0 + + FORM_FILES=$(find . -name "*.xml" -path "*/forms/*" -not -path "./.git/*" -not -path "./vendor/*" 2>/dev/null) + if [ -z "$FORM_FILES" ]; then + echo "No form XML files found — skipping." >> $GITHUB_STEP_SUMMARY + else + echo "Found $(echo "$FORM_FILES" | wc -l) form file(s)" >> $GITHUB_STEP_SUMMARY + for FILE in $FORM_FILES; do + if command -v php &> /dev/null; then + if ! php -r "@simplexml_load_file('$FILE') ?: exit(1);" 2>/dev/null; then + echo "- \`${FILE}\`: malformed XML" >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS + 1)) + else + # Check for valid Joomla form structure + if ! grep -qE '/dev/null; then + echo "- \`${FILE}\`: no \`
\`, \`\`, or \`
\` elements found" >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS + 1)) + else + echo "- \`${FILE}\`: valid" >> $GITHUB_STEP_SUMMARY + fi + fi + fi + done + fi + + echo "" >> $GITHUB_STEP_SUMMARY + if [ "${ERRORS}" -gt 0 ]; then + echo "**${ERRORS} form XML issue(s).**" >> $GITHUB_STEP_SUMMARY + exit 1 + else + echo "**Form XML validation passed.**" >> $GITHUB_STEP_SUMMARY + fi + + - name: Deprecated Joomla API check + continue-on-error: true + run: | + echo "### Deprecated Joomla API Check" >> $GITHUB_STEP_SUMMARY + WARNINGS=0 + + SRC_DIR="" + for DIR in source/ src/ htdocs/; do + [ -d "$DIR" ] && SRC_DIR="$DIR" && break + done + + if [ -z "$SRC_DIR" ]; then + echo "No source directory found — skipping." >> $GITHUB_STEP_SUMMARY + else + # Joomla 3/4 deprecated patterns that break in Joomla 6 + PATTERNS=( + 'JFactory::' + 'JText::' + 'JHtml::' + 'JRoute::' + 'JUri::' + 'JLog::' + 'JTable::' + 'JInput' + 'CMSFactory::\$application' + 'JApplicationCms' + ) + + for PATTERN in "${PATTERNS[@]}"; do + HITS=$(grep -rnl "$PATTERN" "$SRC_DIR" --include="*.php" 2>/dev/null || true) + if [ -n "$HITS" ]; then + COUNT=$(echo "$HITS" | wc -l) + echo "- \`${PATTERN}\` found in ${COUNT} file(s)" >> $GITHUB_STEP_SUMMARY + WARNINGS=$((WARNINGS + COUNT)) + fi + done + + echo "" >> $GITHUB_STEP_SUMMARY + if [ "$WARNINGS" -gt 0 ]; then + echo "**${WARNINGS} deprecated API usage(s) found.** These will break in Joomla 6." >> $GITHUB_STEP_SUMMARY + else + echo "**No deprecated APIs found.**" >> $GITHUB_STEP_SUMMARY + fi + fi + + - name: Template output escaping check + continue-on-error: true + run: | + echo "### Template Output Escaping" >> $GITHUB_STEP_SUMMARY + WARNINGS=0 + + TMPL_FILES=$(find . -name "*.php" -path "*/tmpl/*" -not -path "./.git/*" -not -path "./vendor/*" 2>/dev/null) + if [ -z "$TMPL_FILES" ]; then + echo "No template files found — skipping." >> $GITHUB_STEP_SUMMARY + else + echo "Found $(echo "$TMPL_FILES" | wc -l) template file(s)" >> $GITHUB_STEP_SUMMARY + + for FILE in $TMPL_FILES; do + # Check for unescaped output: or echo $var without escape() + UNESCAPED=$(grep -nP '<\?=\s*\$(?!this->escape)' "$FILE" 2>/dev/null || true) + if [ -n "$UNESCAPED" ]; then + HITS=$(echo "$UNESCAPED" | wc -l) + echo "- \`${FILE}\`: ${HITS} unescaped \`\` output(s) — use \`escape(\$var) ?>\`" >> $GITHUB_STEP_SUMMARY + WARNINGS=$((WARNINGS + HITS)) + fi + + # Check for echo without escaping in template context + RAW_ECHO=$(grep -nP '^\s*echo\s+\$(?!this->escape)' "$FILE" 2>/dev/null || true) + if [ -n "$RAW_ECHO" ]; then + HITS=$(echo "$RAW_ECHO" | wc -l) + echo "- \`${FILE}\`: ${HITS} raw \`echo \$var\` — consider \`echo \$this->escape(\$var)\`" >> $GITHUB_STEP_SUMMARY + WARNINGS=$((WARNINGS + HITS)) + fi + done + + echo "" >> $GITHUB_STEP_SUMMARY + if [ "$WARNINGS" -gt 0 ]; then + echo "**${WARNINGS} potential XSS risk(s) in templates.** Review unescaped output." >> $GITHUB_STEP_SUMMARY + else + echo "**All template output appears properly escaped.**" >> $GITHUB_STEP_SUMMARY + fi + fi + + - name: Namespace consistency check + run: | + echo "### Namespace Consistency" >> $GITHUB_STEP_SUMMARY + ERRORS=0 + + # Find component/plugin manifests with tags + MANIFESTS=$(find . -maxdepth 4 -name "*.xml" -not -path "./.git/*" -not -path "./vendor/*" -exec grep -l '/dev/null || true) + + if [ -z "$MANIFESTS" ]; then + echo "No manifests with \`\` found — skipping." >> $GITHUB_STEP_SUMMARY + else + for MANIFEST in $MANIFESTS; do + NS_PATH=$(grep -oP ']*>\K[^<]+' "$MANIFEST" 2>/dev/null | head -1) + [ -z "$NS_PATH" ] && continue + MANIFEST_DIR=$(dirname "$MANIFEST") + + echo "Manifest: \`${MANIFEST}\` → namespace \`${NS_PATH}\`" >> $GITHUB_STEP_SUMMARY + + # Check PHP files have matching namespace + while IFS= read -r -d '' PHP_FILE; do + FILE_NS=$(grep -oP '^\s*namespace\s+\K[^;]+' "$PHP_FILE" 2>/dev/null | head -1) + [ -z "$FILE_NS" ] && continue + + # Namespace should start with the manifest namespace path + if ! echo "$FILE_NS" | grep -qF "${NS_PATH}"; then + echo "- \`${PHP_FILE}\`: namespace \`${FILE_NS}\` doesn't match manifest \`${NS_PATH}\`" >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS + 1)) + fi + done < <(find "$MANIFEST_DIR" -name "*.php" -path "*/src/*" -not -path "./vendor/*" -print0 2>/dev/null) + done + fi + + echo "" >> $GITHUB_STEP_SUMMARY + if [ "${ERRORS}" -gt 0 ]; then + echo "**${ERRORS} namespace mismatch(es).**" >> $GITHUB_STEP_SUMMARY + exit 1 + else + echo "**Namespace consistency check passed.**" >> $GITHUB_STEP_SUMMARY + fi + + - name: SPDX license header check + continue-on-error: true + run: | + echo "### SPDX License Headers" >> $GITHUB_STEP_SUMMARY + MISSING=0 + + SRC_DIR="" + for DIR in source/ src/ htdocs/; do + [ -d "$DIR" ] && SRC_DIR="$DIR" && break + done + + if [ -z "$SRC_DIR" ]; then + echo "No source directory found — skipping." >> $GITHUB_STEP_SUMMARY + else + TOTAL=0 + while IFS= read -r -d '' FILE; do + TOTAL=$((TOTAL + 1)) + if ! head -10 "$FILE" | grep -qi "SPDX"; then + echo "- Missing SPDX header: \`${FILE}\`" >> $GITHUB_STEP_SUMMARY + MISSING=$((MISSING + 1)) + fi + done < <(find "$SRC_DIR" -name "*.php" -not -path "./vendor/*" -print0) + + echo "" >> $GITHUB_STEP_SUMMARY + if [ "$MISSING" -gt 0 ]; then + echo "**${MISSING}/${TOTAL} PHP file(s) missing SPDX license header.**" >> $GITHUB_STEP_SUMMARY + else + echo "**All ${TOTAL} PHP files have SPDX headers.**" >> $GITHUB_STEP_SUMMARY + fi + fi + + - name: Service provider check + run: | + echo "### Service Provider Check" >> $GITHUB_STEP_SUMMARY + ERRORS=0 + + PROVIDERS=$(find . -name "provider.php" -path "*/services/*" -not -path "./.git/*" -not -path "./vendor/*" 2>/dev/null) + if [ -z "$PROVIDERS" ]; then + echo "No service providers found — skipping." >> $GITHUB_STEP_SUMMARY + else + for FILE in $PROVIDERS; do + # Must return a ServiceProviderInterface + if ! grep -qP 'ServiceProviderInterface|ComponentInterface|MVCFactoryInterface|DispatcherInterface' "$FILE" 2>/dev/null; then + echo "- \`${FILE}\`: does not reference ServiceProviderInterface or component interfaces" >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS + 1)) + else + echo "- \`${FILE}\`: valid service provider" >> $GITHUB_STEP_SUMMARY + fi + + # Must have return statement + if ! grep -qP '^\s*return\s+new\s+' "$FILE" 2>/dev/null; then + echo "- \`${FILE}\`: missing \`return new ...\` statement" >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS + 1)) + fi + done + fi + + echo "" >> $GITHUB_STEP_SUMMARY + if [ "${ERRORS}" -gt 0 ]; then + echo "**${ERRORS} service provider issue(s).**" >> $GITHUB_STEP_SUMMARY + exit 1 + else + echo "**Service provider check passed.**" >> $GITHUB_STEP_SUMMARY + fi + + release-readiness: + name: Release Readiness Check + runs-on: ubuntu-latest + if: github.event_name == 'pull_request' && github.base_ref == 'main' + continue-on-error: true + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Validate release readiness + run: | + echo "## Release Readiness" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + ERRORS=0 + + # Extract version from README.md + README_VERSION=$(grep -oP '^\s*VERSION:\s*\K[0-9]{2}\.[0-9]{2}\.[0-9]{2}' README.md | head -1) + if [ -z "$README_VERSION" ]; then + echo "No VERSION found in README.md FILE INFORMATION block." >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS + 1)) + else + echo "README version: \`${README_VERSION}\`" >> $GITHUB_STEP_SUMMARY + fi + + # Find the extension manifest + MANIFEST="" + for XML_FILE in $(find . -maxdepth 2 -name "*.xml" -not -path "./.git/*" -not -path "./vendor/*"); do + if grep -q "/dev/null; then + MANIFEST="$XML_FILE" + break + fi + done + + if [ -z "$MANIFEST" ]; then + echo "No Joomla extension manifest found." >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS + 1)) + else + echo "Manifest: \`${MANIFEST}\`" >> $GITHUB_STEP_SUMMARY + + # Check matches README VERSION + MANIFEST_VERSION=$(grep -oP '\K[^<]+' "$MANIFEST" | head -1) + if [ -z "$MANIFEST_VERSION" ]; then + echo "No \`\` tag in manifest." >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS + 1)) + elif [ -n "$README_VERSION" ] && [ "$MANIFEST_VERSION" != "$README_VERSION" ]; then + echo "Manifest version \`${MANIFEST_VERSION}\` does not match README \`${README_VERSION}\`." >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS + 1)) + else + echo "Manifest version: \`${MANIFEST_VERSION}\`" >> $GITHUB_STEP_SUMMARY + fi + + # Check extension type, element, client attributes + EXT_TYPE=$(grep -oP ']*\btype="\K[^"]+' "$MANIFEST" | head -1) + if [ -z "$EXT_TYPE" ]; then + echo "Missing \`type\` attribute on \`\` tag." >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS + 1)) + else + echo "Extension type: \`${EXT_TYPE}\`" >> $GITHUB_STEP_SUMMARY + fi + + # Element check (component/module/plugin name) + HAS_ELEMENT=$(grep -cP '<(element|name)>' "$MANIFEST" 2>/dev/null || echo "0") + if [ "$HAS_ELEMENT" -eq 0 ]; then + echo "Missing \`\` or \`\` in manifest." >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS + 1)) + fi + + # Client attribute for site/admin modules and plugins + if echo "$EXT_TYPE" | grep -qP "^(module|plugin)$"; then + HAS_CLIENT=$(grep -cP ']*\bclient=' "$MANIFEST" 2>/dev/null || echo "0") + if [ "$HAS_CLIENT" -eq 0 ]; then + echo "Missing \`client\` attribute for ${EXT_TYPE} extension." >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS + 1)) + fi + fi + fi + + # Check updates.xml exists + if [ -f "updates.xml" ] || [ -f "updates.xml" ]; then + echo "Update XML present." >> $GITHUB_STEP_SUMMARY + else + echo "No updates.xml found." >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS + 1)) + fi + + # Check CHANGELOG.md exists + if [ -f "CHANGELOG.md" ]; then + echo "CHANGELOG.md present." >> $GITHUB_STEP_SUMMARY + else + echo "No CHANGELOG.md found." >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS + 1)) + fi + + echo "" >> $GITHUB_STEP_SUMMARY + if [ $ERRORS -gt 0 ]; then + echo "**${ERRORS} issue(s) must be resolved before release.**" >> $GITHUB_STEP_SUMMARY + exit 1 + else + echo "**Extension is ready for release.**" >> $GITHUB_STEP_SUMMARY + fi + + test: + name: Tests (PHP ${{ matrix.php }}) + runs-on: ubuntu-latest + needs: lint-and-validate + + strategy: + fail-fast: false + matrix: + php: ['8.2', '8.3'] + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup PHP ${{ matrix.php }} + run: | + if ! command -v php &> /dev/null; then + sudo apt-get update -qq + sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1 + fi + php -v && composer --version + + - name: Install dependencies + env: + COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_TOKEN || secrets.GA_TOKEN || github.token }}"}}' + run: | + if [ -f "composer.json" ]; then + composer install \ + --no-interaction \ + --prefer-dist \ + --optimize-autoloader + else + echo "No composer.json found — skipping dependency install" + fi + + - name: Run tests + run: | + echo "### Test Results (PHP ${{ matrix.php }})" >> $GITHUB_STEP_SUMMARY + if [ -f "phpunit.xml" ] || [ -f "phpunit.xml.dist" ]; then + vendor/bin/phpunit --testdox 2>&1 | tee /tmp/test-output.log + EXIT=${PIPESTATUS[0]} + if [ $EXIT -eq 0 ]; then + echo "All tests passed." >> $GITHUB_STEP_SUMMARY + else + echo "Test failures detected — see log." >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + cat /tmp/test-output.log >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + fi + exit $EXIT + else + echo "No phpunit.xml found — skipping tests." >> $GITHUB_STEP_SUMMARY + fi + + static-analysis: + name: PHPStan Analysis + runs-on: ubuntu-latest + needs: lint-and-validate + continue-on-error: true + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup PHP + run: | + if ! command -v php &> /dev/null; then + sudo apt-get update -qq + sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1 + fi + php -v && composer --version + + - name: Install dependencies + env: + COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_TOKEN || secrets.GA_TOKEN || github.token }}"}}' + run: | + if [ -f "composer.json" ]; then + composer install --no-interaction --prefer-dist --optimize-autoloader + fi + + - name: Install PHPStan + run: | + if ! command -v vendor/bin/phpstan &> /dev/null; then + composer require --dev phpstan/phpstan --no-interaction 2>/dev/null || \ + composer global require phpstan/phpstan --no-interaction + fi + + - name: Run PHPStan + run: | + echo "### PHPStan Static Analysis" >> $GITHUB_STEP_SUMMARY + PHPSTAN="vendor/bin/phpstan" + if [ ! -f "$PHPSTAN" ]; then + PHPSTAN=$(composer global config bin-dir --absolute 2>/dev/null)/phpstan + fi + + # Determine source directory + SRC_DIR="" + for DIR in src/ htdocs/ lib/; do + if [ -d "$DIR" ]; then + SRC_DIR="$DIR" + break + fi + done + + if [ -z "$SRC_DIR" ]; then + echo "No source directory found (src/, htdocs/, lib/) — skipping." >> $GITHUB_STEP_SUMMARY + exit 0 + fi + + # Use repo phpstan.neon if present, otherwise use baseline config + ARGS="analyse ${SRC_DIR} --memory-limit=512M --no-progress --error-format=table" + if [ -f "phpstan.neon" ] || [ -f "phpstan.neon.dist" ]; then + echo "Using project PHPStan config." >> $GITHUB_STEP_SUMMARY + else + ARGS="$ARGS --level=3" + echo "No phpstan.neon found — using level 3 (type inference)." >> $GITHUB_STEP_SUMMARY + fi + + $PHPSTAN $ARGS 2>&1 | tee /tmp/phpstan-output.txt + EXIT=${PIPESTATUS[0]} + + if [ $EXIT -eq 0 ]; then + echo "**No errors found.**" >> $GITHUB_STEP_SUMMARY + else + ERRORS=$(grep -c "ERROR" /tmp/phpstan-output.txt 2>/dev/null || echo "some") + echo "**${ERRORS} error(s) found.** Review output above." >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + tail -30 /tmp/phpstan-output.txt >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + fi + exit $EXIT + + pre-release: + name: Build RC Pre-Release + runs-on: ubuntu-latest + needs: [lint-and-validate, test] + if: github.event_name == 'pull_request' + + steps: + - name: Trigger pre-release build + env: + GA_TOKEN: ${{ secrets.GA_TOKEN }} + REPO: ${{ github.repository }} + BRANCH: ${{ github.head_ref }} + run: | + curl -s -X POST \ + "${GITEA_URL:-https://git.mokoconsulting.tech}/api/v1/repos/${REPO}/actions/workflows/pre-release.yml/dispatches" \ + -H "Authorization: token ${GA_TOKEN}" \ + -H "Content-Type: application/json" \ + -d "{\"ref\":\"${BRANCH}\",\"inputs\":{\"stability\":\"release-candidate\"}}" + echo "### Pre-Release" >> $GITHUB_STEP_SUMMARY + echo "Triggered RC build on branch \`${BRANCH}\`" >> $GITHUB_STEP_SUMMARY diff --git a/.mokogitea/workflows/composer-publish.yml b/.mokogitea/workflows/composer-publish.yml deleted file mode 100644 index 03735c9..0000000 --- a/.mokogitea/workflows/composer-publish.yml +++ /dev/null @@ -1,76 +0,0 @@ -# Copyright (C) 2026 Moko Consulting -# SPDX-License-Identifier: GPL-3.0-or-later - -name: "Publish to Composer" - -on: - push: - tags: - - 'v*' - - '[0-9]*.[0-9]*.[0-9]*' - release: - types: [published] - workflow_dispatch: - -env: - GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }} - -jobs: - publish: - name: Publish Package - runs-on: ubuntu-latest - if: >- - !contains(github.event.head_commit.message, '[skip ci]') && - !contains(github.event.head_commit.message, '[skip publish]') - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Setup PHP - run: | - if ! command -v php &> /dev/null; then - sudo apt-get update -qq - sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1 - fi - - - name: Install dependencies - run: composer install --no-dev --no-interaction --prefer-dist --quiet - - - name: Determine version - id: version - run: | - VERSION=$(php -r "echo json_decode(file_get_contents('composer.json'))->version;") - echo "version=${VERSION}" >> "$GITHUB_OUTPUT" - echo "Package version: ${VERSION}" - - # Gitea Composer Registry — auto-publishes from tags - # The tag push itself registers the package at: - # https://git.mokoconsulting.tech/api/packages/MokoConsulting/composer - - name: Verify Gitea registry - run: | - echo "Gitea Composer registry auto-publishes from tags." - echo "Package available at: ${GITEA_URL}/api/packages/MokoConsulting/composer" - echo "Install: composer require mokoconsulting/mokocli" - - # Packagist — notify of new version - - name: Notify Packagist - if: secrets.PACKAGIST_TOKEN != '' - run: | - VERSION="${{ steps.version.outputs.version }}" - echo "Notifying Packagist of version ${VERSION}..." - curl -sf -X POST \ - -H "Content-Type: application/json" \ - -d '{"repository":{"url":"https://git.mokoconsulting.tech/MokoConsulting/mokocli"}}' \ - "https://packagist.org/api/update-package?username=mokoconsulting&apiToken=${{ secrets.PACKAGIST_TOKEN }}" \ - && echo "Packagist notified" \ - || echo "::warning::Packagist notification failed (package may not be registered yet)" - - - name: Summary - run: | - VERSION="${{ steps.version.outputs.version }}" - echo "## Composer Package Published" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "| Registry | Status |" >> $GITHUB_STEP_SUMMARY - echo "|----------|--------|" >> $GITHUB_STEP_SUMMARY - echo "| Gitea | \`composer require mokoconsulting/mokocli:${VERSION}\` |" >> $GITHUB_STEP_SUMMARY - echo "| Packagist | \`composer require mokoconsulting/mokocli\` |" >> $GITHUB_STEP_SUMMARY diff --git a/.mokogitea/workflows/deploy-manual.yml b/.mokogitea/workflows/deploy-manual.yml deleted file mode 100644 index bb133ed..0000000 --- a/.mokogitea/workflows/deploy-manual.yml +++ /dev/null @@ -1,126 +0,0 @@ -# Copyright (C) 2026 Moko Consulting -# -# SPDX-License-Identifier: GPL-3.0-or-later -# -# FILE INFORMATION -# DEFGROUP: Gitea.Workflow -# INGROUP: MokoStandards.Deploy -# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards-API -# PATH: /templates/workflows/joomla/deploy-manual.yml.template -# VERSION: 04.07.00 -# BRIEF: Manual SFTP deploy to dev server for Joomla repos - -name: "Universal: Deploy to Dev (Manual)" - -on: - workflow_dispatch: - inputs: - clear_remote: - description: 'Delete all remote files before uploading' - required: false - default: 'false' - type: boolean - -env: - FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true - -permissions: - contents: read - -jobs: - deploy: - name: SFTP Deploy to Dev - runs-on: ubuntu-latest - - steps: - - name: Checkout repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - - - name: Setup PHP - run: | - php -v && composer --version - - - name: Setup MokoStandards tools - env: - GA_TOKEN: ${{ secrets.GA_TOKEN || secrets.GA_TOKEN || github.token }} - MOKO_CLONE_TOKEN: ${{ secrets.GA_TOKEN || secrets.GA_TOKEN || github.token }} - MOKO_CLONE_HOST: ${{ secrets.GA_TOKEN && 'git.mokoconsulting.tech/MokoConsulting' || 'github.com/mokoconsulting-tech' }} - COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GA_TOKEN || github.token }}"}}' - run: | - git clone --depth 1 --branch main --quiet \ - "https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/MokoStandards-API.git" \ - /tmp/mokostandards-api 2>/dev/null || true - if [ -d "/tmp/mokostandards-api" ] && [ -f "/tmp/mokostandards-api/composer.json" ]; then - cd /tmp/mokostandards-api && composer install --no-dev --no-interaction --quiet 2>/dev/null || true - fi - - - name: Check FTP configuration - id: check - env: - HOST: ${{ vars.DEV_FTP_HOST }} - PATH_VAR: ${{ vars.DEV_FTP_PATH }} - PORT: ${{ vars.DEV_FTP_PORT }} - run: | - if [ -z "$HOST" ] || [ -z "$PATH_VAR" ]; then - echo "DEV_FTP_HOST or DEV_FTP_PATH not configured -- cannot deploy" - echo "skip=true" >> "$GITHUB_OUTPUT" - exit 0 - fi - echo "skip=false" >> "$GITHUB_OUTPUT" - echo "host=$HOST" >> "$GITHUB_OUTPUT" - - REMOTE="${PATH_VAR%/}" - echo "remote=$REMOTE" >> "$GITHUB_OUTPUT" - - [ -z "$PORT" ] && PORT="22" - echo "port=$PORT" >> "$GITHUB_OUTPUT" - - - name: Deploy via SFTP - if: steps.check.outputs.skip != 'true' - env: - SFTP_KEY: ${{ secrets.DEV_FTP_KEY }} - SFTP_PASS: ${{ secrets.DEV_FTP_PASSWORD }} - SFTP_USER: ${{ vars.DEV_FTP_USERNAME }} - run: | - SOURCE_DIR="src" - [ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs" - [ ! -d "$SOURCE_DIR" ] && { echo "No src/ or htdocs/ -- nothing to deploy"; exit 0; } - - printf '{"host":"%s","port":%s,"username":"%s","remotePath":"%s"' \ - "${{ steps.check.outputs.host }}" "${{ steps.check.outputs.port }}" "$SFTP_USER" "${{ steps.check.outputs.remote }}" \ - > /tmp/sftp-config.json - - if [ -n "$SFTP_KEY" ]; then - echo "$SFTP_KEY" > /tmp/deploy_key - chmod 600 /tmp/deploy_key - printf ',"privateKeyPath":"/tmp/deploy_key"}' >> /tmp/sftp-config.json - else - printf ',"password":"%s"}' "$SFTP_PASS" >> /tmp/sftp-config.json - fi - - DEPLOY_ARGS=(--path . --src-dir "$SOURCE_DIR" --config /tmp/sftp-config.json) - [ "${{ inputs.clear_remote }}" = "true" ] && DEPLOY_ARGS+=(--clear-remote) - - PLATFORM=$(php /tmp/mokostandards-api/cli/platform_detect.php --path . 2>/dev/null || true) - if [ "$PLATFORM" = "waas-component" ] && [ -f "/tmp/mokostandards-api/deploy/deploy-joomla.php" ]; then - php /tmp/mokostandards-api/deploy/deploy-joomla.php "${DEPLOY_ARGS[@]}" - else - php /tmp/mokostandards-api/deploy/deploy-sftp.php "${DEPLOY_ARGS[@]}" - fi - - rm -f /tmp/deploy_key /tmp/sftp-config.json - - - name: Summary - if: always() - run: | - if [ "${{ steps.check.outputs.skip }}" = "true" ]; then - echo "### Deploy Skipped -- FTP not configured" >> $GITHUB_STEP_SUMMARY - else - echo "### Manual Dev Deploy Complete" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "| Field | Value |" >> $GITHUB_STEP_SUMMARY - echo "|-------|-------|" >> $GITHUB_STEP_SUMMARY - echo "| Host | \`${{ steps.check.outputs.host }}\` |" >> $GITHUB_STEP_SUMMARY - echo "| Remote | \`${{ steps.check.outputs.remote }}\` |" >> $GITHUB_STEP_SUMMARY - echo "| Clear | ${{ inputs.clear_remote }} |" >> $GITHUB_STEP_SUMMARY - fi diff --git a/.mokogitea/workflows/pr-metadata-check.yml b/.mokogitea/workflows/pr-metadata-check.yml new file mode 100644 index 0000000..68b7589 --- /dev/null +++ b/.mokogitea/workflows/pr-metadata-check.yml @@ -0,0 +1,71 @@ +# Copyright (C) 2026 Moko Consulting +# +# SPDX-License-Identifier: GPL-3.0-or-later +# +# FILE INFORMATION +# DEFGROUP: Gitea.Workflow +# INGROUP: mokocli.Validation +# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli +# PATH: /templates/workflows/joomla/pr-metadata-check.yml.template +# VERSION: 01.00.00 +# BRIEF: Validate MokoGitea metadata matches Joomla extension manifest on PRs + +name: "Joomla: Metadata Validation" + +on: + pull_request: + types: [opened, synchronize, reopened, converted_to_draft, ready_for_review] + +permissions: + contents: read + +env: + GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }} + GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }} + GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }} + +jobs: + validate-metadata: + name: "Validate Joomla Metadata" + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup mokocli tools + env: + MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }} + MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting + run: | + if [ -f /opt/mokocli/cli/joomla_metadata_validate.php ] && [ -f /opt/mokocli/vendor/autoload.php ]; then + echo Using pre-installed /opt/mokocli + echo MOKO_CLI=/opt/mokocli/cli >> $GITHUB_ENV + else + echo Falling back to fresh clone + if ! command -v composer > /dev/null 2>&1; then + sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer > /dev/null 2>&1 + fi + rm -rf /tmp/mokocli + CLONE_URL=https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/mokocli.git + git clone --depth 1 --branch main --quiet $CLONE_URL /tmp/mokocli + cd /tmp/mokocli && composer install --no-dev --no-interaction --quiet + echo MOKO_CLI=/tmp/mokocli/cli >> $GITHUB_ENV + fi + + - name: Validate metadata against Joomla manifest + env: + GITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }} + run: | + php ${MOKO_CLI}/joomla_metadata_validate.php \ + --path . \ + --token "${GITEA_TOKEN}" \ + --org "${GITEA_ORG}" \ + --repo "${GITEA_REPO}" \ + --api-base "${GITEA_URL}/api/v1" \ + --ci + + if [ $? -ne 0 ]; then + echo "::error::Joomla metadata mismatch — update delivery will fail. Run 'php cli/joomla_metadata_validate.php' locally to see details." + exit 1 + fi From 4ee155c8f3d6746820d51ed7f0fec0164887ce90 Mon Sep 17 00:00:00 2001 From: "gitea-actions[bot]" Date: Tue, 23 Jun 2026 12:45:14 +0000 Subject: [PATCH 05/64] chore(version): auto-bump patch 01.08.03-dev [skip ci] --- .mokogitea/workflows/issue-branch.yml | 2 +- source/pkg_mokosuitefield.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.mokogitea/workflows/issue-branch.yml b/.mokogitea/workflows/issue-branch.yml index e72b7b8..e86f06a 100644 --- a/.mokogitea/workflows/issue-branch.yml +++ b/.mokogitea/workflows/issue-branch.yml @@ -5,7 +5,7 @@ # FILE INFORMATION # DEFGROUP: Gitea.Workflow # INGROUP: mokocli.Automation -# VERSION: 01.08.02 +# VERSION: 01.08.03 # BRIEF: Auto-create feature branch when an issue is opened name: "Universal: Issue Branch" diff --git a/source/pkg_mokosuitefield.xml b/source/pkg_mokosuitefield.xml index 0e5590d..cdb74d2 100644 --- a/source/pkg_mokosuitefield.xml +++ b/source/pkg_mokosuitefield.xml @@ -2,7 +2,7 @@ Package - MokoSuite Field mokosuitefield - 01.08.02 + 01.08.03 2026-06-12 Moko Consulting hello@mokoconsulting.tech From 36a8f96bebdba9c6715bc8f6c6c343649a86d3f3 Mon Sep 17 00:00:00 2001 From: "gitea-actions[bot]" Date: Tue, 23 Jun 2026 12:45:24 +0000 Subject: [PATCH 06/64] chore(version): pre-release bump to 01.08.04-dev [skip ci] --- .mokogitea/workflows/issue-branch.yml | 2 +- source/pkg_mokosuitefield.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.mokogitea/workflows/issue-branch.yml b/.mokogitea/workflows/issue-branch.yml index e86f06a..82428bc 100644 --- a/.mokogitea/workflows/issue-branch.yml +++ b/.mokogitea/workflows/issue-branch.yml @@ -5,7 +5,7 @@ # FILE INFORMATION # DEFGROUP: Gitea.Workflow # INGROUP: mokocli.Automation -# VERSION: 01.08.03 +# VERSION: 01.08.04 # BRIEF: Auto-create feature branch when an issue is opened name: "Universal: Issue Branch" diff --git a/source/pkg_mokosuitefield.xml b/source/pkg_mokosuitefield.xml index cdb74d2..9486998 100644 --- a/source/pkg_mokosuitefield.xml +++ b/source/pkg_mokosuitefield.xml @@ -2,7 +2,7 @@ Package - MokoSuite Field mokosuitefield - 01.08.03 + 01.08.04 2026-06-12 Moko Consulting hello@mokoconsulting.tech From 65fcfd03d21b6c559985d29d76c9b3e77ef280a3 Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Tue, 23 Jun 2026 10:24:02 -0500 Subject: [PATCH 07/64] =?UTF-8?q?chore:=20add=20ERP=20submodule=20?= =?UTF-8?q?=E2=80=94=20Field=20requires=20ERP?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitmodules | 3 +++ packages/MokoSuiteERP | 1 + 2 files changed, 4 insertions(+) create mode 160000 packages/MokoSuiteERP diff --git a/.gitmodules b/.gitmodules index 1321885..f3b894a 100644 --- a/.gitmodules +++ b/.gitmodules @@ -4,3 +4,6 @@ [submodule "packages/MokoSuiteCRM"] path = packages/MokoSuiteCRM url = https://git.mokoconsulting.tech/MokoConsulting/MokoSuiteCRM.git +[submodule "packages/MokoSuiteERP"] + path = packages/MokoSuiteERP + url = https://git.mokoconsulting.tech/MokoConsulting/MokoSuiteERP.git diff --git a/packages/MokoSuiteERP b/packages/MokoSuiteERP new file mode 160000 index 0000000..8c76969 --- /dev/null +++ b/packages/MokoSuiteERP @@ -0,0 +1 @@ +Subproject commit 8c76969ee850c86dd9d3ccb69e86ebd108a1c919 From 7aaa41048d992f6dc4e9515417adc686a9b23930 Mon Sep 17 00:00:00 2001 From: "gitea-actions[bot]" Date: Tue, 23 Jun 2026 15:32:29 +0000 Subject: [PATCH 08/64] chore(version): auto-bump patch 01.08.05-dev [skip ci] --- .mokogitea/workflows/issue-branch.yml | 2 +- source/pkg_mokosuitefield.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.mokogitea/workflows/issue-branch.yml b/.mokogitea/workflows/issue-branch.yml index 82428bc..c49c974 100644 --- a/.mokogitea/workflows/issue-branch.yml +++ b/.mokogitea/workflows/issue-branch.yml @@ -5,7 +5,7 @@ # FILE INFORMATION # DEFGROUP: Gitea.Workflow # INGROUP: mokocli.Automation -# VERSION: 01.08.04 +# VERSION: 01.08.05 # BRIEF: Auto-create feature branch when an issue is opened name: "Universal: Issue Branch" diff --git a/source/pkg_mokosuitefield.xml b/source/pkg_mokosuitefield.xml index 9486998..5d2ff86 100644 --- a/source/pkg_mokosuitefield.xml +++ b/source/pkg_mokosuitefield.xml @@ -2,7 +2,7 @@ Package - MokoSuite Field mokosuitefield - 01.08.04 + 01.08.05 2026-06-12 Moko Consulting hello@mokoconsulting.tech From cc36efc60df22c24fe00a5d905b29498669a188e Mon Sep 17 00:00:00 2001 From: "gitea-actions[bot]" Date: Tue, 23 Jun 2026 15:32:38 +0000 Subject: [PATCH 09/64] chore(version): pre-release bump to 01.08.06-dev [skip ci] --- .mokogitea/workflows/issue-branch.yml | 2 +- source/pkg_mokosuitefield.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.mokogitea/workflows/issue-branch.yml b/.mokogitea/workflows/issue-branch.yml index c49c974..0f6cc83 100644 --- a/.mokogitea/workflows/issue-branch.yml +++ b/.mokogitea/workflows/issue-branch.yml @@ -5,7 +5,7 @@ # FILE INFORMATION # DEFGROUP: Gitea.Workflow # INGROUP: mokocli.Automation -# VERSION: 01.08.05 +# VERSION: 01.08.06 # BRIEF: Auto-create feature branch when an issue is opened name: "Universal: Issue Branch" diff --git a/source/pkg_mokosuitefield.xml b/source/pkg_mokosuitefield.xml index 5d2ff86..6f9d5f0 100644 --- a/source/pkg_mokosuitefield.xml +++ b/source/pkg_mokosuitefield.xml @@ -2,7 +2,7 @@ Package - MokoSuite Field mokosuitefield - 01.08.05 + 01.08.06 2026-06-12 Moko Consulting hello@mokoconsulting.tech From 267fa178df76b2aa8ec5949da71011bd6ee3cffc Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Tue, 23 Jun 2026 12:41:04 -0500 Subject: [PATCH 10/64] =?UTF-8?q?chore:=20remove=20static=20updates.xml=20?= =?UTF-8?q?=E2=80=94=20MokoGitea=20generates=20dynamically?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- source/updates.xml | 9 --------- 1 file changed, 9 deletions(-) delete mode 100644 source/updates.xml diff --git a/source/updates.xml b/source/updates.xml deleted file mode 100644 index 39bccee..0000000 --- a/source/updates.xml +++ /dev/null @@ -1,9 +0,0 @@ - - -Package - MokoSuite Field -pkg_mokosuitefield -package -01.01.00 - -8.3 - From feac684898a220a275fa0432b654cf45e4fe177f Mon Sep 17 00:00:00 2001 From: "gitea-actions[bot]" Date: Tue, 23 Jun 2026 17:42:45 +0000 Subject: [PATCH 11/64] chore(version): auto-bump patch 01.08.07-dev [skip ci] --- .mokogitea/workflows/issue-branch.yml | 2 +- source/pkg_mokosuitefield.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.mokogitea/workflows/issue-branch.yml b/.mokogitea/workflows/issue-branch.yml index 0f6cc83..86dc96d 100644 --- a/.mokogitea/workflows/issue-branch.yml +++ b/.mokogitea/workflows/issue-branch.yml @@ -5,7 +5,7 @@ # FILE INFORMATION # DEFGROUP: Gitea.Workflow # INGROUP: mokocli.Automation -# VERSION: 01.08.06 +# VERSION: 01.08.07 # BRIEF: Auto-create feature branch when an issue is opened name: "Universal: Issue Branch" diff --git a/source/pkg_mokosuitefield.xml b/source/pkg_mokosuitefield.xml index 6f9d5f0..fef5c8b 100644 --- a/source/pkg_mokosuitefield.xml +++ b/source/pkg_mokosuitefield.xml @@ -2,7 +2,7 @@ Package - MokoSuite Field mokosuitefield - 01.08.06 + 01.08.07 2026-06-12 Moko Consulting hello@mokoconsulting.tech From 2069d499fedb1a2d963c4075f5b8bb01c2781ae3 Mon Sep 17 00:00:00 2001 From: "gitea-actions[bot]" Date: Tue, 23 Jun 2026 17:42:59 +0000 Subject: [PATCH 12/64] chore(version): pre-release bump to 01.08.08-dev [skip ci] --- .mokogitea/workflows/issue-branch.yml | 2 +- source/pkg_mokosuitefield.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.mokogitea/workflows/issue-branch.yml b/.mokogitea/workflows/issue-branch.yml index 86dc96d..d6e4cfb 100644 --- a/.mokogitea/workflows/issue-branch.yml +++ b/.mokogitea/workflows/issue-branch.yml @@ -5,7 +5,7 @@ # FILE INFORMATION # DEFGROUP: Gitea.Workflow # INGROUP: mokocli.Automation -# VERSION: 01.08.07 +# VERSION: 01.08.08 # BRIEF: Auto-create feature branch when an issue is opened name: "Universal: Issue Branch" diff --git a/source/pkg_mokosuitefield.xml b/source/pkg_mokosuitefield.xml index fef5c8b..4fbe27d 100644 --- a/source/pkg_mokosuitefield.xml +++ b/source/pkg_mokosuitefield.xml @@ -2,7 +2,7 @@ Package - MokoSuite Field mokosuitefield - 01.08.07 + 01.08.08 2026-06-12 Moko Consulting hello@mokoconsulting.tech From c996eaca2b3c8a89c8dcae97843283e51b07de0b Mon Sep 17 00:00:00 2001 From: Jonathan Miller <1+jmiller@noreply.git.mokoconsulting.tech> Date: Tue, 23 Jun 2026 18:05:28 +0000 Subject: [PATCH 13/64] chore: remove security-audit.yml -- handled by MokoGitea --- .mokogitea/workflows/security-audit.yml | 82 ------------------------- 1 file changed, 82 deletions(-) delete mode 100644 .mokogitea/workflows/security-audit.yml diff --git a/.mokogitea/workflows/security-audit.yml b/.mokogitea/workflows/security-audit.yml deleted file mode 100644 index 789325a..0000000 --- a/.mokogitea/workflows/security-audit.yml +++ /dev/null @@ -1,82 +0,0 @@ -# Copyright (C) 2026 Moko Consulting -# -# SPDX-License-Identifier: GPL-3.0-or-later -# -# FILE INFORMATION -# DEFGROUP: Gitea.Workflow -# INGROUP: MokoStandards.Security -# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards -# PATH: /.gitea/workflows/security-audit.yml -# VERSION: 01.00.00 -# BRIEF: Dependency vulnerability scanning for composer and npm packages - -name: "Universal: Security Audit" - -on: - schedule: - - cron: '0 6 * * 1' # Weekly on Monday at 06:00 UTC - pull_request: - branches: - - main - paths: - - 'composer.json' - - 'composer.lock' - - 'package.json' - - 'package-lock.json' - workflow_dispatch: - -permissions: - contents: read - -env: - NTFY_URL: ${{ vars.NTFY_URL || 'https://ntfy.mokoconsulting.tech' }} - NTFY_TOPIC: ${{ vars.NTFY_TOPIC || 'gitea-security' }} - -jobs: - audit: - name: Dependency Audit - runs-on: ubuntu-latest - - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Composer audit - if: hashFiles('composer.lock') != '' - run: | - echo "=== Composer Security Audit ===" - if ! command -v composer &> /dev/null; then - sudo apt-get update -qq - sudo apt-get install -y -qq php-cli composer >/dev/null 2>&1 - fi - composer audit --format=plain 2>&1 | tee /tmp/composer-audit.txt - RESULT=$? - if [ $RESULT -ne 0 ]; then - echo "::warning::Composer vulnerabilities found" - echo "composer_vulnerable=true" >> "$GITHUB_ENV" - else - echo "No known vulnerabilities in composer dependencies" - fi - - - name: NPM audit - if: hashFiles('package-lock.json') != '' - run: | - echo "=== NPM Security Audit ===" - npm audit --production 2>&1 | tee /tmp/npm-audit.txt || true - if npm audit --production 2>&1 | grep -q "found 0 vulnerabilities"; then - echo "No known vulnerabilities in npm dependencies" - else - echo "::warning::NPM vulnerabilities found" - echo "npm_vulnerable=true" >> "$GITHUB_ENV" - fi - - - name: Notify on vulnerabilities - if: env.composer_vulnerable == 'true' || env.npm_vulnerable == 'true' - run: | - REPO="${{ github.event.repository.name }}" - curl -sS \ - -H "Title: ${REPO} has vulnerable dependencies" \ - -H "Tags: lock,warning" \ - -H "Priority: high" \ - -d "Security audit found vulnerabilities. Review dependency updates." \ - "${NTFY_URL}/${NTFY_TOPIC}" || true From 627ed7e48f8220d166e3575268f08ee0fd978b0a Mon Sep 17 00:00:00 2001 From: "gitea-actions[bot]" Date: Tue, 23 Jun 2026 18:23:07 +0000 Subject: [PATCH 14/64] chore(version): auto-bump patch 01.08.09-dev [skip ci] --- .mokogitea/workflows/issue-branch.yml | 2 +- source/pkg_mokosuitefield.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.mokogitea/workflows/issue-branch.yml b/.mokogitea/workflows/issue-branch.yml index d6e4cfb..94630a0 100644 --- a/.mokogitea/workflows/issue-branch.yml +++ b/.mokogitea/workflows/issue-branch.yml @@ -5,7 +5,7 @@ # FILE INFORMATION # DEFGROUP: Gitea.Workflow # INGROUP: mokocli.Automation -# VERSION: 01.08.08 +# VERSION: 01.08.09 # BRIEF: Auto-create feature branch when an issue is opened name: "Universal: Issue Branch" diff --git a/source/pkg_mokosuitefield.xml b/source/pkg_mokosuitefield.xml index 4fbe27d..ab9daf6 100644 --- a/source/pkg_mokosuitefield.xml +++ b/source/pkg_mokosuitefield.xml @@ -2,7 +2,7 @@ Package - MokoSuite Field mokosuitefield - 01.08.08 + 01.08.09 2026-06-12 Moko Consulting hello@mokoconsulting.tech From 6f29a17b9576cbacfc00cb53feb09a72e611567f Mon Sep 17 00:00:00 2001 From: "gitea-actions[bot]" Date: Tue, 23 Jun 2026 18:23:39 +0000 Subject: [PATCH 15/64] chore(version): pre-release bump to 01.08.10-dev [skip ci] --- .mokogitea/workflows/issue-branch.yml | 2 +- source/pkg_mokosuitefield.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.mokogitea/workflows/issue-branch.yml b/.mokogitea/workflows/issue-branch.yml index 94630a0..88230d4 100644 --- a/.mokogitea/workflows/issue-branch.yml +++ b/.mokogitea/workflows/issue-branch.yml @@ -5,7 +5,7 @@ # FILE INFORMATION # DEFGROUP: Gitea.Workflow # INGROUP: mokocli.Automation -# VERSION: 01.08.09 +# VERSION: 01.08.10 # BRIEF: Auto-create feature branch when an issue is opened name: "Universal: Issue Branch" diff --git a/source/pkg_mokosuitefield.xml b/source/pkg_mokosuitefield.xml index ab9daf6..b840485 100644 --- a/source/pkg_mokosuitefield.xml +++ b/source/pkg_mokosuitefield.xml @@ -2,7 +2,7 @@ Package - MokoSuite Field mokosuitefield - 01.08.09 + 01.08.10 2026-06-12 Moko Consulting hello@mokoconsulting.tech From 9b5060f7721c91ec8832c2a81d0c21f3accf4c9e Mon Sep 17 00:00:00 2001 From: "gitea-actions[bot]" Date: Wed, 24 Jun 2026 12:17:58 +0000 Subject: [PATCH 16/64] chore(version): pre-release bump to 01.08.11-dev [skip ci] --- .mokogitea/workflows/issue-branch.yml | 2 +- source/pkg_mokosuitefield.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.mokogitea/workflows/issue-branch.yml b/.mokogitea/workflows/issue-branch.yml index 88230d4..d6137d3 100644 --- a/.mokogitea/workflows/issue-branch.yml +++ b/.mokogitea/workflows/issue-branch.yml @@ -5,7 +5,7 @@ # FILE INFORMATION # DEFGROUP: Gitea.Workflow # INGROUP: mokocli.Automation -# VERSION: 01.08.10 +# VERSION: 01.08.11 # BRIEF: Auto-create feature branch when an issue is opened name: "Universal: Issue Branch" diff --git a/source/pkg_mokosuitefield.xml b/source/pkg_mokosuitefield.xml index b840485..7ab1413 100644 --- a/source/pkg_mokosuitefield.xml +++ b/source/pkg_mokosuitefield.xml @@ -2,7 +2,7 @@ Package - MokoSuite Field mokosuitefield - 01.08.10 + 01.08.11 2026-06-12 Moko Consulting hello@mokoconsulting.tech From 5a498cf3f6b73fc57207bde3fc8bb6c66c8159e7 Mon Sep 17 00:00:00 2001 From: Jonathan Miller <1+jmiller@noreply.git.mokoconsulting.tech> Date: Sat, 27 Jun 2026 20:17:43 +0000 Subject: [PATCH 17/64] feat: initial scaffold with component, system plugin, and webservices plugin --- .gitignore | 7 + CHANGELOG.md | 23 +- CLAUDE.md | 25 ++ README.md | 26 +- .../components/com_mokosuitefield/access.xml | 17 + .../com_mokosuitefield/com_mokosuitefield.xml | 46 +++ .../components/com_mokosuitefield/config.xml | 19 + .../language/en-GB/com_mokosuitefield.ini | 14 + .../language/en-GB/com_mokosuitefield.sys.ini | 14 + .../com_mokosuitefield/services/provider.php | 41 +++ .../src/Controller/DisplayController.php | 18 + .../src/View/FieldAgreements/HtmlView.php | 24 ++ .../src/View/FieldChecklists/HtmlView.php | 24 ++ .../src/View/FieldDashboard/HtmlView.php | 24 ++ .../src/View/FieldDispatches/HtmlView.php | 24 ++ .../src/View/FieldEquipment/HtmlView.php | 24 ++ .../src/View/FieldParts/HtmlView.php | 24 ++ .../src/View/FieldTechnicians/HtmlView.php | 24 ++ .../src/View/FieldWorkorders/HtmlView.php | 24 ++ .../tmpl/fieldagreements/default.php | 13 + .../tmpl/fieldchecklists/default.php | 13 + .../tmpl/fielddashboard/default.php | 13 + .../tmpl/fielddispatches/default.php | 13 + .../tmpl/fieldequipment/default.php | 13 + .../tmpl/fieldparts/default.php | 13 + .../tmpl/fieldtechnicians/default.php | 13 + .../tmpl/fieldworkorders/default.php | 13 + .../com_mokosuitefield/admin/access.xml | 11 - .../com_mokosuitefield/admin/config.xml | 25 -- .../admin/services/provider.php | 18 - .../src/Controller/DisplayController.php | 11 - .../admin/src/Model/DispatchModel.php | 37 -- .../admin/src/Model/EquipmentModel.php | 29 -- .../admin/src/Model/EstimatesModel.php | 28 -- .../src/Model/ServiceAgreementsModel.php | 39 -- .../admin/src/Model/TechniciansModel.php | 27 -- .../admin/src/Model/VehiclesModel.php | 26 -- .../admin/src/Model/WorkOrdersModel.php | 62 ---- .../admin/src/View/Dashboard/HtmlView.php | 40 --- .../admin/src/View/Dispatch/HtmlView.php | 28 -- .../admin/src/View/Equipment/HtmlView.php | 34 -- .../src/View/ServiceAgreements/HtmlView.php | 23 -- .../admin/src/View/Technicians/HtmlView.php | 32 -- .../admin/src/View/Vehicles/HtmlView.php | 23 -- .../admin/src/View/WorkOrders/HtmlView.php | 52 --- .../admin/tmpl/dashboard/default.php | 33 -- .../admin/tmpl/dispatch/default.php | 10 - .../admin/tmpl/equipment/default.php | 10 - .../admin/tmpl/serviceagreements/default.php | 8 - .../admin/tmpl/technicians/default.php | 7 - .../admin/tmpl/vehicles/default.php | 9 - .../admin/tmpl/workorders/default.php | 34 -- .../Controller/FieldEquipmentController.php | 178 ---------- .../Controller/FieldEstimatesController.php | 150 -------- .../src/Controller/FieldMobileController.php | 252 ------------- .../src/Controller/FieldReportsController.php | 122 ------- .../Controller/FieldSchedulingController.php | 83 ----- .../Controller/FieldWorkOrderController.php | 131 ------- .../com_mokosuitefield/media/css/field.css | 9 - .../com_mokosuitefield/media/js/dispatch.js | 6 - .../site/src/Controller/DisplayController.php | 8 - .../site/src/Service/Router.php | 21 -- .../site/src/View/BookService/HtmlView.php | 78 ---- .../site/src/View/CustomerPortal/HtmlView.php | 89 ----- .../site/src/View/EstimateView/HtmlView.php | 90 ----- .../site/src/View/TechMobile/HtmlView.php | 70 ---- .../site/tmpl/bookservice/default.php | 33 -- .../site/tmpl/customerportal/default.php | 14 - .../site/tmpl/estimateview/default.php | 96 ----- .../site/tmpl/techmobile/default.php | 56 --- .../en-GB/plg_system_mokosuitefield.ini | 2 - .../en-GB/plg_system_mokosuitefield.sys.ini | 2 - .../sql/install.mysql.sql | 332 ------------------ .../sql/uninstall.mysql.sql | 12 - .../src/Helper/CustomerFeedbackHelper.php | 132 ------- .../src/Helper/CustomerSatisfactionHelper.php | 118 ------- .../src/Helper/DispatchHelper.php | 144 -------- .../src/Helper/EquipmentHelper.php | 77 ---- .../src/Helper/EstimateHelper.php | 83 ----- .../src/Helper/GpsTrackingHelper.php | 112 ------ .../src/Helper/InvoiceHelper.php | 151 -------- .../src/Helper/PartsHelper.php | 107 ------ .../src/Helper/RouteHelper.php | 239 ------------- .../src/Helper/SafetyChecklistHelper.php | 147 -------- .../src/Helper/SchedulingHelper.php | 152 -------- .../src/Helper/ServiceAgreementHelper.php | 78 ---- .../src/Helper/TechnicianSkillHelper.php | 97 ----- .../src/Helper/TruckStockHelper.php | 69 ---- .../src/Helper/VehicleHelper.php | 44 --- .../src/Helper/WarrantyHelper.php | 102 ------ .../src/Helper/WorkOrderHelper.php | 162 --------- .../src/Extension/FieldAutomation.php | 144 -------- .../src/Extension/MokoSuiteFieldApi.php | 27 -- source/pkg_mokosuitefield.xml | 46 +-- .../en-GB/plg_system_mokosuitefield.ini | 13 + .../en-GB/plg_system_mokosuitefield.sys.ini | 6 + .../system/mokosuitefield/mokosuitefield.xml | 106 ++++++ .../mokosuitefield/services/provider.php | 36 ++ .../system/mokosuitefield/sql/install.sql | 170 +++++++++ .../system/mokosuitefield/sql/uninstall.sql | 14 + .../src/Extension/MokoSuiteField.php | 23 ++ .../en-GB/plg_webservices_mokosuitefield.ini | 6 + .../plg_webservices_mokosuitefield.sys.ini | 6 + .../mokosuitefield/mokosuitefield.xml | 30 ++ .../mokosuitefield/services/provider.php | 36 ++ .../src/Extension/MokoSuiteField.php | 50 +++ 106 files changed, 1057 insertions(+), 4706 deletions(-) create mode 100644 .gitignore create mode 100644 CLAUDE.md create mode 100644 source/components/com_mokosuitefield/access.xml create mode 100644 source/components/com_mokosuitefield/com_mokosuitefield.xml create mode 100644 source/components/com_mokosuitefield/config.xml create mode 100644 source/components/com_mokosuitefield/language/en-GB/com_mokosuitefield.ini create mode 100644 source/components/com_mokosuitefield/language/en-GB/com_mokosuitefield.sys.ini create mode 100644 source/components/com_mokosuitefield/services/provider.php create mode 100644 source/components/com_mokosuitefield/src/Controller/DisplayController.php create mode 100644 source/components/com_mokosuitefield/src/View/FieldAgreements/HtmlView.php create mode 100644 source/components/com_mokosuitefield/src/View/FieldChecklists/HtmlView.php create mode 100644 source/components/com_mokosuitefield/src/View/FieldDashboard/HtmlView.php create mode 100644 source/components/com_mokosuitefield/src/View/FieldDispatches/HtmlView.php create mode 100644 source/components/com_mokosuitefield/src/View/FieldEquipment/HtmlView.php create mode 100644 source/components/com_mokosuitefield/src/View/FieldParts/HtmlView.php create mode 100644 source/components/com_mokosuitefield/src/View/FieldTechnicians/HtmlView.php create mode 100644 source/components/com_mokosuitefield/src/View/FieldWorkorders/HtmlView.php create mode 100644 source/components/com_mokosuitefield/tmpl/fieldagreements/default.php create mode 100644 source/components/com_mokosuitefield/tmpl/fieldchecklists/default.php create mode 100644 source/components/com_mokosuitefield/tmpl/fielddashboard/default.php create mode 100644 source/components/com_mokosuitefield/tmpl/fielddispatches/default.php create mode 100644 source/components/com_mokosuitefield/tmpl/fieldequipment/default.php create mode 100644 source/components/com_mokosuitefield/tmpl/fieldparts/default.php create mode 100644 source/components/com_mokosuitefield/tmpl/fieldtechnicians/default.php create mode 100644 source/components/com_mokosuitefield/tmpl/fieldworkorders/default.php delete mode 100644 source/packages/com_mokosuitefield/admin/access.xml delete mode 100644 source/packages/com_mokosuitefield/admin/config.xml delete mode 100644 source/packages/com_mokosuitefield/admin/services/provider.php delete mode 100644 source/packages/com_mokosuitefield/admin/src/Controller/DisplayController.php delete mode 100644 source/packages/com_mokosuitefield/admin/src/Model/DispatchModel.php delete mode 100644 source/packages/com_mokosuitefield/admin/src/Model/EquipmentModel.php delete mode 100644 source/packages/com_mokosuitefield/admin/src/Model/EstimatesModel.php delete mode 100644 source/packages/com_mokosuitefield/admin/src/Model/ServiceAgreementsModel.php delete mode 100644 source/packages/com_mokosuitefield/admin/src/Model/TechniciansModel.php delete mode 100644 source/packages/com_mokosuitefield/admin/src/Model/VehiclesModel.php delete mode 100644 source/packages/com_mokosuitefield/admin/src/Model/WorkOrdersModel.php delete mode 100644 source/packages/com_mokosuitefield/admin/src/View/Dashboard/HtmlView.php delete mode 100644 source/packages/com_mokosuitefield/admin/src/View/Dispatch/HtmlView.php delete mode 100644 source/packages/com_mokosuitefield/admin/src/View/Equipment/HtmlView.php delete mode 100644 source/packages/com_mokosuitefield/admin/src/View/ServiceAgreements/HtmlView.php delete mode 100644 source/packages/com_mokosuitefield/admin/src/View/Technicians/HtmlView.php delete mode 100644 source/packages/com_mokosuitefield/admin/src/View/Vehicles/HtmlView.php delete mode 100644 source/packages/com_mokosuitefield/admin/src/View/WorkOrders/HtmlView.php delete mode 100644 source/packages/com_mokosuitefield/admin/tmpl/dashboard/default.php delete mode 100644 source/packages/com_mokosuitefield/admin/tmpl/dispatch/default.php delete mode 100644 source/packages/com_mokosuitefield/admin/tmpl/equipment/default.php delete mode 100644 source/packages/com_mokosuitefield/admin/tmpl/serviceagreements/default.php delete mode 100644 source/packages/com_mokosuitefield/admin/tmpl/technicians/default.php delete mode 100644 source/packages/com_mokosuitefield/admin/tmpl/vehicles/default.php delete mode 100644 source/packages/com_mokosuitefield/admin/tmpl/workorders/default.php delete mode 100644 source/packages/com_mokosuitefield/api/src/Controller/FieldEquipmentController.php delete mode 100644 source/packages/com_mokosuitefield/api/src/Controller/FieldEstimatesController.php delete mode 100644 source/packages/com_mokosuitefield/api/src/Controller/FieldMobileController.php delete mode 100644 source/packages/com_mokosuitefield/api/src/Controller/FieldReportsController.php delete mode 100644 source/packages/com_mokosuitefield/api/src/Controller/FieldSchedulingController.php delete mode 100644 source/packages/com_mokosuitefield/api/src/Controller/FieldWorkOrderController.php delete mode 100644 source/packages/com_mokosuitefield/media/css/field.css delete mode 100644 source/packages/com_mokosuitefield/media/js/dispatch.js delete mode 100644 source/packages/com_mokosuitefield/site/src/Controller/DisplayController.php delete mode 100644 source/packages/com_mokosuitefield/site/src/Service/Router.php delete mode 100644 source/packages/com_mokosuitefield/site/src/View/BookService/HtmlView.php delete mode 100644 source/packages/com_mokosuitefield/site/src/View/CustomerPortal/HtmlView.php delete mode 100644 source/packages/com_mokosuitefield/site/src/View/EstimateView/HtmlView.php delete mode 100644 source/packages/com_mokosuitefield/site/src/View/TechMobile/HtmlView.php delete mode 100644 source/packages/com_mokosuitefield/site/tmpl/bookservice/default.php delete mode 100644 source/packages/com_mokosuitefield/site/tmpl/customerportal/default.php delete mode 100644 source/packages/com_mokosuitefield/site/tmpl/estimateview/default.php delete mode 100644 source/packages/com_mokosuitefield/site/tmpl/techmobile/default.php delete mode 100644 source/packages/plg_system_mokosuitefield/language/en-GB/plg_system_mokosuitefield.ini delete mode 100644 source/packages/plg_system_mokosuitefield/language/en-GB/plg_system_mokosuitefield.sys.ini delete mode 100644 source/packages/plg_system_mokosuitefield/sql/install.mysql.sql delete mode 100644 source/packages/plg_system_mokosuitefield/sql/uninstall.mysql.sql delete mode 100644 source/packages/plg_system_mokosuitefield/src/Helper/CustomerFeedbackHelper.php delete mode 100644 source/packages/plg_system_mokosuitefield/src/Helper/CustomerSatisfactionHelper.php delete mode 100644 source/packages/plg_system_mokosuitefield/src/Helper/DispatchHelper.php delete mode 100644 source/packages/plg_system_mokosuitefield/src/Helper/EquipmentHelper.php delete mode 100644 source/packages/plg_system_mokosuitefield/src/Helper/EstimateHelper.php delete mode 100644 source/packages/plg_system_mokosuitefield/src/Helper/GpsTrackingHelper.php delete mode 100644 source/packages/plg_system_mokosuitefield/src/Helper/InvoiceHelper.php delete mode 100644 source/packages/plg_system_mokosuitefield/src/Helper/PartsHelper.php delete mode 100644 source/packages/plg_system_mokosuitefield/src/Helper/RouteHelper.php delete mode 100644 source/packages/plg_system_mokosuitefield/src/Helper/SafetyChecklistHelper.php delete mode 100644 source/packages/plg_system_mokosuitefield/src/Helper/SchedulingHelper.php delete mode 100644 source/packages/plg_system_mokosuitefield/src/Helper/ServiceAgreementHelper.php delete mode 100644 source/packages/plg_system_mokosuitefield/src/Helper/TechnicianSkillHelper.php delete mode 100644 source/packages/plg_system_mokosuitefield/src/Helper/TruckStockHelper.php delete mode 100644 source/packages/plg_system_mokosuitefield/src/Helper/VehicleHelper.php delete mode 100644 source/packages/plg_system_mokosuitefield/src/Helper/WarrantyHelper.php delete mode 100644 source/packages/plg_system_mokosuitefield/src/Helper/WorkOrderHelper.php delete mode 100644 source/packages/plg_task_mokosuitefield/src/Extension/FieldAutomation.php delete mode 100644 source/packages/plg_webservices_mokosuitefield/src/Extension/MokoSuiteFieldApi.php create mode 100644 source/plugins/system/mokosuitefield/language/en-GB/plg_system_mokosuitefield.ini create mode 100644 source/plugins/system/mokosuitefield/language/en-GB/plg_system_mokosuitefield.sys.ini create mode 100644 source/plugins/system/mokosuitefield/mokosuitefield.xml create mode 100644 source/plugins/system/mokosuitefield/services/provider.php create mode 100644 source/plugins/system/mokosuitefield/sql/install.sql create mode 100644 source/plugins/system/mokosuitefield/sql/uninstall.sql create mode 100644 source/plugins/system/mokosuitefield/src/Extension/MokoSuiteField.php create mode 100644 source/plugins/webservices/mokosuitefield/language/en-GB/plg_webservices_mokosuitefield.ini create mode 100644 source/plugins/webservices/mokosuitefield/language/en-GB/plg_webservices_mokosuitefield.sys.ini create mode 100644 source/plugins/webservices/mokosuitefield/mokosuitefield.xml create mode 100644 source/plugins/webservices/mokosuitefield/services/provider.php create mode 100644 source/plugins/webservices/mokosuitefield/src/Extension/MokoSuiteField.php diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c8a642e --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +.claude/ +.mcp.json +TODO.md +*.min.css +*.min.js +vendor/ +node_modules/ diff --git a/CHANGELOG.md b/CHANGELOG.md index d001f04..f76722d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,12 +1,17 @@ + + # Changelog -## [01.01.00] - 2026-06-12 +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). + +## [Unreleased] + ### Added -- Initial scaffold: field service management for MokoSuite -- 12 database tables for technicians, work orders, service agreements, equipment, vehicles, estimates -- 7 helpers: Dispatch, WorkOrder, ServiceAgreement, Equipment, Estimate, TruckStock, Vehicle -- Admin views: Dashboard, Work Orders, Technicians, Service Agreements, Equipment, Dispatch, Vehicles -- Site views: Tech Mobile (tablet), Book Service (public form) -- API controller with 6 endpoints -- Task scheduler: service reminders, agreement renewals, equipment warranty, truck stock reorder -- Joomla 6 architecture (PHP 8.3+) + +- **Initial scaffold** — Package structure with component, system plugin, and webservices plugin diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..1f90bfc --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,25 @@ + + +# MokoSuiteField + +Field service management for contractors — a Layer 2 vertical for the MokoSuite platform. + +## Structure + +- `source/` — Joomla extension source (package, component, plugins) +- All work happens on the `dev` branch; never commit directly to `main` +- Use conventional commits (`feat:`, `fix:`, `chore:`, etc.) + +## Build + +Packaged via MokoCLI. Run `mokocli build` from the repo root. + +## Standards + +- PHP 8.3+ / Joomla 6 architecture +- `$this->getDatabase()` — never use `Factory::getDbo()` +- Namespace root: `Moko\Component\MokoSuiteField` (component), `Moko\Plugin\System\MokoSuiteField` (system plugin), `Moko\Plugin\WebServices\MokoSuiteField` (API plugin) diff --git a/README.md b/README.md index 74f5607..f45cd5c 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,27 @@ + + # MokoSuiteField -MokoSuite Field Service - dispatch, work orders, scheduling, mobile tech, plumbing, electrical, HVAC, service agreements. Layer 2 add-on for MokoSuite (requires CRM). \ No newline at end of file +Field service management for contractors — a MokoSuite Layer 2 vertical extension for Joomla. + +## Overview + +MokoSuiteField provides work order management, technician dispatch, equipment tracking, parts inventory, checklists, and preventive maintenance agreements for field service businesses. + +## Requirements + +- Joomla 6.x +- PHP 8.3+ +- MokoSuite (base platform) + +## Installation + +Install via the MokoSuite package manager or upload `pkg_mokosuitefield.zip` through Joomla's extension installer. + +## License + +GPL-3.0-or-later diff --git a/source/components/com_mokosuitefield/access.xml b/source/components/com_mokosuitefield/access.xml new file mode 100644 index 0000000..1a23e16 --- /dev/null +++ b/source/components/com_mokosuitefield/access.xml @@ -0,0 +1,17 @@ + + + +
+ + + + + + + +
+
diff --git a/source/components/com_mokosuitefield/com_mokosuitefield.xml b/source/components/com_mokosuitefield/com_mokosuitefield.xml new file mode 100644 index 0000000..a3a73f0 --- /dev/null +++ b/source/components/com_mokosuitefield/com_mokosuitefield.xml @@ -0,0 +1,46 @@ + + + + com_mokosuitefield + 0.1.0 + 2026-06-27 + Moko Consulting + hello@mokoconsulting.tech + https://mokoconsulting.tech + Copyright (C) 2026 Moko Consulting + GPL-3.0-or-later + COM_MOKOSUITEFIELD_DESCRIPTION + + Moko\Component\MokoSuiteField + + + COM_MOKOSUITEFIELD + + COM_MOKOSUITEFIELD_MENU_DASHBOARD + COM_MOKOSUITEFIELD_MENU_WORKORDERS + COM_MOKOSUITEFIELD_MENU_TECHNICIANS + COM_MOKOSUITEFIELD_MENU_EQUIPMENT + COM_MOKOSUITEFIELD_MENU_PARTS + COM_MOKOSUITEFIELD_MENU_CHECKLISTS + COM_MOKOSUITEFIELD_MENU_AGREEMENTS + COM_MOKOSUITEFIELD_MENU_DISPATCHES + + + src + tmpl + services + language + access.xml + config.xml + + + + + en-GB/com_mokosuitefield.ini + en-GB/com_mokosuitefield.sys.ini + + diff --git a/source/components/com_mokosuitefield/config.xml b/source/components/com_mokosuitefield/config.xml new file mode 100644 index 0000000..9de19da --- /dev/null +++ b/source/components/com_mokosuitefield/config.xml @@ -0,0 +1,19 @@ + + + +
+ +
+
diff --git a/source/components/com_mokosuitefield/language/en-GB/com_mokosuitefield.ini b/source/components/com_mokosuitefield/language/en-GB/com_mokosuitefield.ini new file mode 100644 index 0000000..3e6a97f --- /dev/null +++ b/source/components/com_mokosuitefield/language/en-GB/com_mokosuitefield.ini @@ -0,0 +1,14 @@ +; Copyright (C) 2026 Moko Consulting +; SPDX-License-Identifier: GPL-3.0-or-later +; Authored-by: Moko Consulting + +COM_MOKOSUITEFIELD="MokoSuite Field" +COM_MOKOSUITEFIELD_DESCRIPTION="Field service management for contractors." +COM_MOKOSUITEFIELD_MENU_DASHBOARD="Dashboard" +COM_MOKOSUITEFIELD_MENU_WORKORDERS="Work Orders" +COM_MOKOSUITEFIELD_MENU_TECHNICIANS="Technicians" +COM_MOKOSUITEFIELD_MENU_EQUIPMENT="Equipment" +COM_MOKOSUITEFIELD_MENU_PARTS="Parts" +COM_MOKOSUITEFIELD_MENU_CHECKLISTS="Checklists" +COM_MOKOSUITEFIELD_MENU_AGREEMENTS="PM Agreements" +COM_MOKOSUITEFIELD_MENU_DISPATCHES="Dispatches" diff --git a/source/components/com_mokosuitefield/language/en-GB/com_mokosuitefield.sys.ini b/source/components/com_mokosuitefield/language/en-GB/com_mokosuitefield.sys.ini new file mode 100644 index 0000000..3e6a97f --- /dev/null +++ b/source/components/com_mokosuitefield/language/en-GB/com_mokosuitefield.sys.ini @@ -0,0 +1,14 @@ +; Copyright (C) 2026 Moko Consulting +; SPDX-License-Identifier: GPL-3.0-or-later +; Authored-by: Moko Consulting + +COM_MOKOSUITEFIELD="MokoSuite Field" +COM_MOKOSUITEFIELD_DESCRIPTION="Field service management for contractors." +COM_MOKOSUITEFIELD_MENU_DASHBOARD="Dashboard" +COM_MOKOSUITEFIELD_MENU_WORKORDERS="Work Orders" +COM_MOKOSUITEFIELD_MENU_TECHNICIANS="Technicians" +COM_MOKOSUITEFIELD_MENU_EQUIPMENT="Equipment" +COM_MOKOSUITEFIELD_MENU_PARTS="Parts" +COM_MOKOSUITEFIELD_MENU_CHECKLISTS="Checklists" +COM_MOKOSUITEFIELD_MENU_AGREEMENTS="PM Agreements" +COM_MOKOSUITEFIELD_MENU_DISPATCHES="Dispatches" diff --git a/source/components/com_mokosuitefield/services/provider.php b/source/components/com_mokosuitefield/services/provider.php new file mode 100644 index 0000000..b341658 --- /dev/null +++ b/source/components/com_mokosuitefield/services/provider.php @@ -0,0 +1,41 @@ + + * @license GPL-3.0-or-later + * @author Moko Consulting + */ + +\defined('_JEXEC') or die; + +use Joomla\CMS\Dispatcher\ComponentDispatcherFactoryInterface; +use Joomla\CMS\Extension\ComponentInterface; +use Joomla\CMS\Extension\MVCComponent; +use Joomla\CMS\Extension\Service\Provider\ComponentDispatcherFactory; +use Joomla\CMS\Extension\Service\Provider\MVCFactory; +use Joomla\CMS\Extension\Service\Provider\RouterFactory; +use Joomla\CMS\MVC\Factory\MVCFactoryInterface; +use Joomla\CMS\Component\Router\RouterFactoryInterface; +use Joomla\DI\Container; +use Joomla\DI\ServiceProviderInterface; + +return new class () implements ServiceProviderInterface { + public function register(Container $container): void + { + $container->registerServiceProvider(new ComponentDispatcherFactory('\\Moko\\Component\\MokoSuiteField')); + $container->registerServiceProvider(new MVCFactory('\\Moko\\Component\\MokoSuiteField')); + $container->registerServiceProvider(new RouterFactory('\\Moko\\Component\\MokoSuiteField')); + + $container->set( + ComponentInterface::class, + function (Container $container) { + $component = new MVCComponent(); + $component->setMVCFactory($container->get(MVCFactoryInterface::class)); + $component->setDispatcherFactory($container->get(ComponentDispatcherFactoryInterface::class)); + $component->setRouterFactory($container->get(RouterFactoryInterface::class)); + + return $component; + } + ); + } +}; diff --git a/source/components/com_mokosuitefield/src/Controller/DisplayController.php b/source/components/com_mokosuitefield/src/Controller/DisplayController.php new file mode 100644 index 0000000..fe41440 --- /dev/null +++ b/source/components/com_mokosuitefield/src/Controller/DisplayController.php @@ -0,0 +1,18 @@ + + * @license GPL-3.0-or-later + * @author Moko Consulting + */ + +namespace Moko\Component\MokoSuiteField\Administrator\Controller; + +use Joomla\CMS\MVC\Controller\BaseController; + +\defined('_JEXEC') or die; + +class DisplayController extends BaseController +{ + protected $default_view = 'fielddashboard'; +} diff --git a/source/components/com_mokosuitefield/src/View/FieldAgreements/HtmlView.php b/source/components/com_mokosuitefield/src/View/FieldAgreements/HtmlView.php new file mode 100644 index 0000000..6450a43 --- /dev/null +++ b/source/components/com_mokosuitefield/src/View/FieldAgreements/HtmlView.php @@ -0,0 +1,24 @@ + + * @license GPL-3.0-or-later + * @author Moko Consulting + */ + +namespace Moko\Component\MokoSuiteField\Administrator\View\FieldAgreements; + +use Joomla\CMS\MVC\View\HtmlView as BaseHtmlView; +use Joomla\CMS\Toolbar\ToolbarHelper; + +\defined('_JEXEC') or die; + +class HtmlView extends BaseHtmlView +{ + public function display($tpl = null): void + { + ToolbarHelper::title('MokoSuite Field — PM Agreements', 'contract'); + + parent::display($tpl); + } +} diff --git a/source/components/com_mokosuitefield/src/View/FieldChecklists/HtmlView.php b/source/components/com_mokosuitefield/src/View/FieldChecklists/HtmlView.php new file mode 100644 index 0000000..9205936 --- /dev/null +++ b/source/components/com_mokosuitefield/src/View/FieldChecklists/HtmlView.php @@ -0,0 +1,24 @@ + + * @license GPL-3.0-or-later + * @author Moko Consulting + */ + +namespace Moko\Component\MokoSuiteField\Administrator\View\FieldChecklists; + +use Joomla\CMS\MVC\View\HtmlView as BaseHtmlView; +use Joomla\CMS\Toolbar\ToolbarHelper; + +\defined('_JEXEC') or die; + +class HtmlView extends BaseHtmlView +{ + public function display($tpl = null): void + { + ToolbarHelper::title('MokoSuite Field — Checklists', 'checklist'); + + parent::display($tpl); + } +} diff --git a/source/components/com_mokosuitefield/src/View/FieldDashboard/HtmlView.php b/source/components/com_mokosuitefield/src/View/FieldDashboard/HtmlView.php new file mode 100644 index 0000000..463d969 --- /dev/null +++ b/source/components/com_mokosuitefield/src/View/FieldDashboard/HtmlView.php @@ -0,0 +1,24 @@ + + * @license GPL-3.0-or-later + * @author Moko Consulting + */ + +namespace Moko\Component\MokoSuiteField\Administrator\View\FieldDashboard; + +use Joomla\CMS\MVC\View\HtmlView as BaseHtmlView; +use Joomla\CMS\Toolbar\ToolbarHelper; + +\defined('_JEXEC') or die; + +class HtmlView extends BaseHtmlView +{ + public function display($tpl = null): void + { + ToolbarHelper::title('MokoSuite Field — Dashboard', 'home'); + + parent::display($tpl); + } +} diff --git a/source/components/com_mokosuitefield/src/View/FieldDispatches/HtmlView.php b/source/components/com_mokosuitefield/src/View/FieldDispatches/HtmlView.php new file mode 100644 index 0000000..e53eca5 --- /dev/null +++ b/source/components/com_mokosuitefield/src/View/FieldDispatches/HtmlView.php @@ -0,0 +1,24 @@ + + * @license GPL-3.0-or-later + * @author Moko Consulting + */ + +namespace Moko\Component\MokoSuiteField\Administrator\View\FieldDispatches; + +use Joomla\CMS\MVC\View\HtmlView as BaseHtmlView; +use Joomla\CMS\Toolbar\ToolbarHelper; + +\defined('_JEXEC') or die; + +class HtmlView extends BaseHtmlView +{ + public function display($tpl = null): void + { + ToolbarHelper::title('MokoSuite Field — Dispatches', 'location'); + + parent::display($tpl); + } +} diff --git a/source/components/com_mokosuitefield/src/View/FieldEquipment/HtmlView.php b/source/components/com_mokosuitefield/src/View/FieldEquipment/HtmlView.php new file mode 100644 index 0000000..5c87dac --- /dev/null +++ b/source/components/com_mokosuitefield/src/View/FieldEquipment/HtmlView.php @@ -0,0 +1,24 @@ + + * @license GPL-3.0-or-later + * @author Moko Consulting + */ + +namespace Moko\Component\MokoSuiteField\Administrator\View\FieldEquipment; + +use Joomla\CMS\MVC\View\HtmlView as BaseHtmlView; +use Joomla\CMS\Toolbar\ToolbarHelper; + +\defined('_JEXEC') or die; + +class HtmlView extends BaseHtmlView +{ + public function display($tpl = null): void + { + ToolbarHelper::title('MokoSuite Field — Equipment', 'cogs'); + + parent::display($tpl); + } +} diff --git a/source/components/com_mokosuitefield/src/View/FieldParts/HtmlView.php b/source/components/com_mokosuitefield/src/View/FieldParts/HtmlView.php new file mode 100644 index 0000000..3de237f --- /dev/null +++ b/source/components/com_mokosuitefield/src/View/FieldParts/HtmlView.php @@ -0,0 +1,24 @@ + + * @license GPL-3.0-or-later + * @author Moko Consulting + */ + +namespace Moko\Component\MokoSuiteField\Administrator\View\FieldParts; + +use Joomla\CMS\MVC\View\HtmlView as BaseHtmlView; +use Joomla\CMS\Toolbar\ToolbarHelper; + +\defined('_JEXEC') or die; + +class HtmlView extends BaseHtmlView +{ + public function display($tpl = null): void + { + ToolbarHelper::title('MokoSuite Field — Parts', 'cube'); + + parent::display($tpl); + } +} diff --git a/source/components/com_mokosuitefield/src/View/FieldTechnicians/HtmlView.php b/source/components/com_mokosuitefield/src/View/FieldTechnicians/HtmlView.php new file mode 100644 index 0000000..565314d --- /dev/null +++ b/source/components/com_mokosuitefield/src/View/FieldTechnicians/HtmlView.php @@ -0,0 +1,24 @@ + + * @license GPL-3.0-or-later + * @author Moko Consulting + */ + +namespace Moko\Component\MokoSuiteField\Administrator\View\FieldTechnicians; + +use Joomla\CMS\MVC\View\HtmlView as BaseHtmlView; +use Joomla\CMS\Toolbar\ToolbarHelper; + +\defined('_JEXEC') or die; + +class HtmlView extends BaseHtmlView +{ + public function display($tpl = null): void + { + ToolbarHelper::title('MokoSuite Field — Technicians', 'users'); + + parent::display($tpl); + } +} diff --git a/source/components/com_mokosuitefield/src/View/FieldWorkorders/HtmlView.php b/source/components/com_mokosuitefield/src/View/FieldWorkorders/HtmlView.php new file mode 100644 index 0000000..309eba0 --- /dev/null +++ b/source/components/com_mokosuitefield/src/View/FieldWorkorders/HtmlView.php @@ -0,0 +1,24 @@ + + * @license GPL-3.0-or-later + * @author Moko Consulting + */ + +namespace Moko\Component\MokoSuiteField\Administrator\View\FieldWorkorders; + +use Joomla\CMS\MVC\View\HtmlView as BaseHtmlView; +use Joomla\CMS\Toolbar\ToolbarHelper; + +\defined('_JEXEC') or die; + +class HtmlView extends BaseHtmlView +{ + public function display($tpl = null): void + { + ToolbarHelper::title('MokoSuite Field — Work Orders', 'file-2'); + + parent::display($tpl); + } +} diff --git a/source/components/com_mokosuitefield/tmpl/fieldagreements/default.php b/source/components/com_mokosuitefield/tmpl/fieldagreements/default.php new file mode 100644 index 0000000..3b1fd45 --- /dev/null +++ b/source/components/com_mokosuitefield/tmpl/fieldagreements/default.php @@ -0,0 +1,13 @@ + + * @license GPL-3.0-or-later + * @author Moko Consulting + */ + +\defined('_JEXEC') or die; +?> +
+

MokoSuite Field — PM Agreements

+
diff --git a/source/components/com_mokosuitefield/tmpl/fieldchecklists/default.php b/source/components/com_mokosuitefield/tmpl/fieldchecklists/default.php new file mode 100644 index 0000000..539738a --- /dev/null +++ b/source/components/com_mokosuitefield/tmpl/fieldchecklists/default.php @@ -0,0 +1,13 @@ + + * @license GPL-3.0-or-later + * @author Moko Consulting + */ + +\defined('_JEXEC') or die; +?> +
+

MokoSuite Field — Checklists

+
diff --git a/source/components/com_mokosuitefield/tmpl/fielddashboard/default.php b/source/components/com_mokosuitefield/tmpl/fielddashboard/default.php new file mode 100644 index 0000000..6edbc0a --- /dev/null +++ b/source/components/com_mokosuitefield/tmpl/fielddashboard/default.php @@ -0,0 +1,13 @@ + + * @license GPL-3.0-or-later + * @author Moko Consulting + */ + +\defined('_JEXEC') or die; +?> +
+

MokoSuite Field — Dashboard

+
diff --git a/source/components/com_mokosuitefield/tmpl/fielddispatches/default.php b/source/components/com_mokosuitefield/tmpl/fielddispatches/default.php new file mode 100644 index 0000000..a85727f --- /dev/null +++ b/source/components/com_mokosuitefield/tmpl/fielddispatches/default.php @@ -0,0 +1,13 @@ + + * @license GPL-3.0-or-later + * @author Moko Consulting + */ + +\defined('_JEXEC') or die; +?> +
+

MokoSuite Field — Dispatches

+
diff --git a/source/components/com_mokosuitefield/tmpl/fieldequipment/default.php b/source/components/com_mokosuitefield/tmpl/fieldequipment/default.php new file mode 100644 index 0000000..d603886 --- /dev/null +++ b/source/components/com_mokosuitefield/tmpl/fieldequipment/default.php @@ -0,0 +1,13 @@ + + * @license GPL-3.0-or-later + * @author Moko Consulting + */ + +\defined('_JEXEC') or die; +?> +
+

MokoSuite Field — Equipment

+
diff --git a/source/components/com_mokosuitefield/tmpl/fieldparts/default.php b/source/components/com_mokosuitefield/tmpl/fieldparts/default.php new file mode 100644 index 0000000..0442669 --- /dev/null +++ b/source/components/com_mokosuitefield/tmpl/fieldparts/default.php @@ -0,0 +1,13 @@ + + * @license GPL-3.0-or-later + * @author Moko Consulting + */ + +\defined('_JEXEC') or die; +?> +
+

MokoSuite Field — Parts

+
diff --git a/source/components/com_mokosuitefield/tmpl/fieldtechnicians/default.php b/source/components/com_mokosuitefield/tmpl/fieldtechnicians/default.php new file mode 100644 index 0000000..42d3720 --- /dev/null +++ b/source/components/com_mokosuitefield/tmpl/fieldtechnicians/default.php @@ -0,0 +1,13 @@ + + * @license GPL-3.0-or-later + * @author Moko Consulting + */ + +\defined('_JEXEC') or die; +?> +
+

MokoSuite Field — Technicians

+
diff --git a/source/components/com_mokosuitefield/tmpl/fieldworkorders/default.php b/source/components/com_mokosuitefield/tmpl/fieldworkorders/default.php new file mode 100644 index 0000000..27c139d --- /dev/null +++ b/source/components/com_mokosuitefield/tmpl/fieldworkorders/default.php @@ -0,0 +1,13 @@ + + * @license GPL-3.0-or-later + * @author Moko Consulting + */ + +\defined('_JEXEC') or die; +?> +
+

MokoSuite Field — Work Orders

+
diff --git a/source/packages/com_mokosuitefield/admin/access.xml b/source/packages/com_mokosuitefield/admin/access.xml deleted file mode 100644 index 6f4f869..0000000 --- a/source/packages/com_mokosuitefield/admin/access.xml +++ /dev/null @@ -1,11 +0,0 @@ - - -
- - - - - - -
-
diff --git a/source/packages/com_mokosuitefield/admin/config.xml b/source/packages/com_mokosuitefield/admin/config.xml deleted file mode 100644 index b17bb28..0000000 --- a/source/packages/com_mokosuitefield/admin/config.xml +++ /dev/null @@ -1,25 +0,0 @@ - - -
- - - - - - - - -
-
- - -
-
- - - -
-
- -
-
diff --git a/source/packages/com_mokosuitefield/admin/services/provider.php b/source/packages/com_mokosuitefield/admin/services/provider.php deleted file mode 100644 index f056f8e..0000000 --- a/source/packages/com_mokosuitefield/admin/services/provider.php +++ /dev/null @@ -1,18 +0,0 @@ -set(ComponentInterface::class, function (Container $container) { - $component = new MVCComponent($container->get(ComponentDispatcherFactoryInterface::class)); - $component->setMVCFactory($container->get(MVCFactoryInterface::class)); - return $component; - }); - } -}; diff --git a/source/packages/com_mokosuitefield/admin/src/Controller/DisplayController.php b/source/packages/com_mokosuitefield/admin/src/Controller/DisplayController.php deleted file mode 100644 index 0129bef..0000000 --- a/source/packages/com_mokosuitefield/admin/src/Controller/DisplayController.php +++ /dev/null @@ -1,11 +0,0 @@ -getDatabase(); - $db->setQuery($db->getQuery(true) - ->select('t.id AS tech_id, cd.name AS tech_name, t.trade, t.status AS tech_status') - ->select('(SELECT COUNT(*) FROM #__mokosuitefield_work_orders wo WHERE wo.technician_id = t.id AND wo.scheduled_date = CURDATE() AND wo.status NOT IN (' . $db->quote('completed') . ',' . $db->quote('cancelled') . ')) AS pending_jobs') - ->select('(SELECT COUNT(*) FROM #__mokosuitefield_work_orders wo WHERE wo.technician_id = t.id AND wo.scheduled_date = CURDATE() AND wo.status = ' . $db->quote('completed') . ') AS completed_jobs') - ->from($db->quoteName('#__mokosuitefield_technicians', 't')) - ->join('LEFT', $db->quoteName('#__contact_details', 'cd') . ' ON cd.id = t.contact_id') - ->where($db->quoteName('t.status') . ' != ' . $db->quote('inactive')) - ->order('cd.name ASC')); - return $db->loadObjectList() ?: []; - } - - public function getGpsLog(int $techId, string $date = ''): array - { - $db = $this->getDatabase(); - $date = $date ?: date('Y-m-d'); - - $db->setQuery($db->getQuery(true) - ->select('*') - ->from('#__mokosuitefield_dispatch_log') - ->where('technician_id = ' . $techId) - ->where('DATE(recorded_at) = ' . $db->quote($date)) - ->order('recorded_at ASC')); - return $db->loadObjectList() ?: []; - } -} diff --git a/source/packages/com_mokosuitefield/admin/src/Model/EquipmentModel.php b/source/packages/com_mokosuitefield/admin/src/Model/EquipmentModel.php deleted file mode 100644 index 2f244ad..0000000 --- a/source/packages/com_mokosuitefield/admin/src/Model/EquipmentModel.php +++ /dev/null @@ -1,29 +0,0 @@ -getDatabase(); - $query = $db->getQuery(true) - ->select('eq.*, loc.name AS location_name, loc.address, cd.name AS customer_name') - ->from($db->quoteName('#__mokosuitefield_equipment', 'eq')) - ->join('LEFT', $db->quoteName('#__mokosuitefield_locations', 'loc') . ' ON loc.id = eq.location_id') - ->join('LEFT', $db->quoteName('#__contact_details', 'cd') . ' ON cd.id = eq.contact_id') - ->order('eq.name ASC'); - - if ($type) $query->where($db->quoteName('eq.type') . ' = ' . $db->quote($type)); - if ($status) $query->where($db->quoteName('eq.status') . ' = ' . $db->quote($status)); - if ($locationId) $query->where('eq.location_id = ' . $locationId); - if ($search) $query->where('(' . $db->quoteName('eq.name') . ' LIKE ' . $db->quote('%' . $search . '%') - . ' OR ' . $db->quoteName('eq.serial_number') . ' LIKE ' . $db->quote('%' . $search . '%') . ')'); - - $db->setQuery($query, 0, $limit); - return $db->loadObjectList() ?: []; - } -} diff --git a/source/packages/com_mokosuitefield/admin/src/Model/EstimatesModel.php b/source/packages/com_mokosuitefield/admin/src/Model/EstimatesModel.php deleted file mode 100644 index 7332a46..0000000 --- a/source/packages/com_mokosuitefield/admin/src/Model/EstimatesModel.php +++ /dev/null @@ -1,28 +0,0 @@ -getDatabase(); - $query = $db->getQuery(true) - ->select('e.*, cd.name AS customer_name, loc.address') - ->from($db->quoteName('#__mokosuitefield_estimates', 'e')) - ->join('LEFT', $db->quoteName('#__contact_details', 'cd') . ' ON cd.id = e.contact_id') - ->join('LEFT', $db->quoteName('#__mokosuitefield_locations', 'loc') . ' ON loc.id = e.location_id') - ->order('e.created DESC'); - - if ($status) $query->where($db->quoteName('e.status') . ' = ' . $db->quote($status)); - if ($techId) $query->where('e.technician_id = ' . $techId); - if ($search) $query->where('(' . $db->quoteName('cd.name') . ' LIKE ' . $db->quote('%' . $search . '%') - . ' OR ' . $db->quoteName('e.estimate_number') . ' LIKE ' . $db->quote('%' . $search . '%') . ')'); - - $db->setQuery($query, 0, $limit); - return $db->loadObjectList() ?: []; - } -} diff --git a/source/packages/com_mokosuitefield/admin/src/Model/ServiceAgreementsModel.php b/source/packages/com_mokosuitefield/admin/src/Model/ServiceAgreementsModel.php deleted file mode 100644 index c01a746..0000000 --- a/source/packages/com_mokosuitefield/admin/src/Model/ServiceAgreementsModel.php +++ /dev/null @@ -1,39 +0,0 @@ -getDatabase(); - $query = $db->getQuery(true) - ->select('sa.*, cd.name AS customer_name, loc.address') - ->from($db->quoteName('#__mokosuitefield_service_agreements', 'sa')) - ->join('LEFT', $db->quoteName('#__contact_details', 'cd') . ' ON cd.id = sa.contact_id') - ->join('LEFT', $db->quoteName('#__mokosuitefield_locations', 'loc') . ' ON loc.id = sa.location_id') - ->order('sa.end_date ASC'); - - if ($status) $query->where($db->quoteName('sa.status') . ' = ' . $db->quote($status)); - if ($search) $query->where($db->quoteName('cd.name') . ' LIKE ' . $db->quote('%' . $search . '%')); - - $db->setQuery($query, 0, $limit); - return $db->loadObjectList() ?: []; - } - - public function getExpiringSoon(int $days = 30): array - { - $db = $this->getDatabase(); - $db->setQuery($db->getQuery(true) - ->select('sa.*, cd.name AS customer_name') - ->from($db->quoteName('#__mokosuitefield_service_agreements', 'sa')) - ->join('LEFT', $db->quoteName('#__contact_details', 'cd') . ' ON cd.id = sa.contact_id') - ->where($db->quoteName('sa.status') . ' = ' . $db->quote('active')) - ->where($db->quoteName('sa.end_date') . ' BETWEEN CURDATE() AND DATE_ADD(CURDATE(), INTERVAL ' . $days . ' DAY)') - ->order('sa.end_date ASC')); - return $db->loadObjectList() ?: []; - } -} diff --git a/source/packages/com_mokosuitefield/admin/src/Model/TechniciansModel.php b/source/packages/com_mokosuitefield/admin/src/Model/TechniciansModel.php deleted file mode 100644 index 6a32578..0000000 --- a/source/packages/com_mokosuitefield/admin/src/Model/TechniciansModel.php +++ /dev/null @@ -1,27 +0,0 @@ -getDatabase(); - $query = $db->getQuery(true) - ->select('t.*, cd.name AS tech_name, cd.email_to, cd.telephone') - ->select('(SELECT COUNT(*) FROM #__mokosuitefield_work_orders wo WHERE wo.technician_id = t.id AND wo.status IN (' . $db->quote('dispatched') . ',' . $db->quote('in_progress') . ')) AS active_jobs') - ->from($db->quoteName('#__mokosuitefield_technicians', 't')) - ->join('LEFT', $db->quoteName('#__contact_details', 'cd') . ' ON cd.id = t.contact_id') - ->order('cd.name ASC'); - - if ($status) $query->where($db->quoteName('t.status') . ' = ' . $db->quote($status)); - if ($trade) $query->where($db->quoteName('t.trade') . ' = ' . $db->quote($trade)); - if ($search) $query->where($db->quoteName('cd.name') . ' LIKE ' . $db->quote('%' . $search . '%')); - - $db->setQuery($query, 0, $limit); - return $db->loadObjectList() ?: []; - } -} diff --git a/source/packages/com_mokosuitefield/admin/src/Model/VehiclesModel.php b/source/packages/com_mokosuitefield/admin/src/Model/VehiclesModel.php deleted file mode 100644 index 0429c3a..0000000 --- a/source/packages/com_mokosuitefield/admin/src/Model/VehiclesModel.php +++ /dev/null @@ -1,26 +0,0 @@ -getDatabase(); - $query = $db->getQuery(true) - ->select('v.*, cd.name AS tech_name') - ->from($db->quoteName('#__mokosuitefield_vehicles', 'v')) - ->join('LEFT', $db->quoteName('#__mokosuitefield_technicians', 't') . ' ON t.id = v.technician_id') - ->join('LEFT', $db->quoteName('#__contact_details', 'cd') . ' ON cd.id = t.contact_id') - ->order('v.vehicle_name ASC'); - - if ($status) $query->where($db->quoteName('v.status') . ' = ' . $db->quote($status)); - if ($techId) $query->where('v.technician_id = ' . $techId); - - $db->setQuery($query, 0, $limit); - return $db->loadObjectList() ?: []; - } -} diff --git a/source/packages/com_mokosuitefield/admin/src/Model/WorkOrdersModel.php b/source/packages/com_mokosuitefield/admin/src/Model/WorkOrdersModel.php deleted file mode 100644 index af03434..0000000 --- a/source/packages/com_mokosuitefield/admin/src/Model/WorkOrdersModel.php +++ /dev/null @@ -1,62 +0,0 @@ -getDatabase(); - $query = $db->getQuery(true) - ->select('wo.*, cd.name AS customer_name, loc.address, loc.city, loc.state, loc.zip') - ->select('t_cd.name AS tech_name') - ->from($db->quoteName('#__mokosuitefield_work_orders', 'wo')) - ->join('LEFT', $db->quoteName('#__contact_details', 'cd') . ' ON cd.id = wo.contact_id') - ->join('LEFT', $db->quoteName('#__mokosuitefield_locations', 'loc') . ' ON loc.id = wo.location_id') - ->join('LEFT', $db->quoteName('#__mokosuitefield_technicians', 't') . ' ON t.id = wo.technician_id') - ->join('LEFT', $db->quoteName('#__contact_details', 't_cd') . ' ON t_cd.id = t.contact_id') - ->order('wo.scheduled_date DESC, wo.priority DESC'); - - if ($status) $query->where($db->quoteName('wo.status') . ' = ' . $db->quote($status)); - if ($trade) $query->where($db->quoteName('wo.trade') . ' = ' . $db->quote($trade)); - if ($techId) $query->where('wo.technician_id = ' . $techId); - if ($date) $query->where('wo.scheduled_date = ' . $db->quote($date)); - if ($search) { - $query->where('(' . $db->quoteName('wo.wo_number') . ' LIKE ' . $db->quote('%' . $search . '%') - . ' OR ' . $db->quoteName('wo.description') . ' LIKE ' . $db->quote('%' . $search . '%') - . ' OR ' . $db->quoteName('cd.name') . ' LIKE ' . $db->quote('%' . $search . '%') . ')'); - } - - $db->setQuery($query, $offset, $limit); - return $db->loadObjectList() ?: []; - } - - public function getWorkOrder(int $id): ?object - { - $db = $this->getDatabase(); - $db->setQuery($db->getQuery(true) - ->select('wo.*, cd.name AS customer_name, t_cd.name AS tech_name') - ->select('loc.address, loc.city, loc.state, loc.zip, loc.latitude, loc.longitude, loc.name AS location_name') - ->from($db->quoteName('#__mokosuitefield_work_orders', 'wo')) - ->join('LEFT', $db->quoteName('#__contact_details', 'cd') . ' ON cd.id = wo.contact_id') - ->join('LEFT', $db->quoteName('#__mokosuitefield_locations', 'loc') . ' ON loc.id = wo.location_id') - ->join('LEFT', $db->quoteName('#__mokosuitefield_technicians', 't') . ' ON t.id = wo.technician_id') - ->join('LEFT', $db->quoteName('#__contact_details', 't_cd') . ' ON t_cd.id = t.contact_id') - ->where('wo.id = ' . (int) $id)); - return $db->loadObject(); - } - - public function getStatusCounts(): object - { - $db = $this->getDatabase(); - $db->setQuery($db->getQuery(true) - ->select('status, COUNT(*) AS cnt') - ->from('#__mokosuitefield_work_orders') - ->group('status')); - $rows = $db->loadObjectList('status') ?: []; - return (object) array_map(fn($r) => (int) $r->cnt, (array) $rows); - } -} diff --git a/source/packages/com_mokosuitefield/admin/src/View/Dashboard/HtmlView.php b/source/packages/com_mokosuitefield/admin/src/View/Dashboard/HtmlView.php deleted file mode 100644 index c6b9eaf..0000000 --- a/source/packages/com_mokosuitefield/admin/src/View/Dashboard/HtmlView.php +++ /dev/null @@ -1,40 +0,0 @@ -stats = \Moko\Plugin\System\MokoSuiteField\Helper\WorkOrderHelper::getDashboardStats(); - $this->dispatchBoard = \Moko\Plugin\System\MokoSuiteField\Helper\DispatchHelper::getDispatchBoard(); - $this->unassigned = \Moko\Plugin\System\MokoSuiteField\Helper\DispatchHelper::getUnassigned(); - - $db = Factory::getContainer()->get(DatabaseInterface::class); - - // Urgent/emergency jobs - $db->setQuery($db->getQuery(true) - ->select('wo.*, cd.name AS customer_name, loc.address') - ->from($db->quoteName('#__mokosuitefield_work_orders', 'wo')) - ->join('LEFT', $db->quoteName('#__contact_details', 'cd') . ' ON cd.id = wo.contact_id') - ->join('LEFT', $db->quoteName('#__mokosuitefield_locations', 'loc') . ' ON loc.id = wo.location_id') - ->where($db->quoteName('wo.priority') . ' IN (' . $db->quote('emergency') . ',' . $db->quote('urgent') . ')') - ->where($db->quoteName('wo.status') . ' NOT IN (' . $db->quote('completed') . ',' . $db->quote('cancelled') . ',' . $db->quote('invoiced') . ')') - ->order('FIELD(wo.priority,' . $db->quote('emergency') . ',' . $db->quote('urgent') . ') ASC, wo.created ASC'), 0, 10); - $this->urgent = $db->loadObjectList() ?: []; - - ToolbarHelper::title('MokoSuite Field Service', 'icon-wrench'); - parent::display($tpl); - } -} diff --git a/source/packages/com_mokosuitefield/admin/src/View/Dispatch/HtmlView.php b/source/packages/com_mokosuitefield/admin/src/View/Dispatch/HtmlView.php deleted file mode 100644 index e59bd25..0000000 --- a/source/packages/com_mokosuitefield/admin/src/View/Dispatch/HtmlView.php +++ /dev/null @@ -1,28 +0,0 @@ -date = Factory::getApplication()->getInput()->getString('date', date('Y-m-d')); - - $this->board = \Moko\Plugin\System\MokoSuiteField\Helper\DispatchHelper::getDispatchBoard($this->date); - $this->unassigned = \Moko\Plugin\System\MokoSuiteField\Helper\DispatchHelper::getUnassigned(); - $this->stats = \Moko\Plugin\System\MokoSuiteField\Helper\WorkOrderHelper::getDashboardStats(); - - ToolbarHelper::title('Field Service - Dispatch Board', 'icon-map'); - parent::display($tpl); - } -} diff --git a/source/packages/com_mokosuitefield/admin/src/View/Equipment/HtmlView.php b/source/packages/com_mokosuitefield/admin/src/View/Equipment/HtmlView.php deleted file mode 100644 index 5188748..0000000 --- a/source/packages/com_mokosuitefield/admin/src/View/Equipment/HtmlView.php +++ /dev/null @@ -1,34 +0,0 @@ -get(DatabaseInterface::class); - - $db->setQuery($db->getQuery(true) - ->select('e.*, loc.address, loc.city, cd.name AS owner_name') - ->from($db->quoteName('#__mokosuitefield_equipment', 'e')) - ->join('LEFT', $db->quoteName('#__mokosuitefield_locations', 'loc') . ' ON loc.id = e.location_id') - ->join('LEFT', $db->quoteName('#__contact_details', 'cd') . ' ON cd.id = e.contact_id') - ->order('e.equipment_type ASC, e.make ASC')); - $this->equipment = $db->loadObjectList() ?: []; - - $this->serviceDue = \Moko\Plugin\System\MokoSuiteField\Helper\EquipmentHelper::getDueForService(30); - - ToolbarHelper::title('Field Service — Equipment', 'icon-cogs'); - ToolbarHelper::addNew('equipment.add'); - parent::display($tpl); - } -} diff --git a/source/packages/com_mokosuitefield/admin/src/View/ServiceAgreements/HtmlView.php b/source/packages/com_mokosuitefield/admin/src/View/ServiceAgreements/HtmlView.php deleted file mode 100644 index ecec27f..0000000 --- a/source/packages/com_mokosuitefield/admin/src/View/ServiceAgreements/HtmlView.php +++ /dev/null @@ -1,23 +0,0 @@ -agreements = \Moko\Plugin\System\MokoSuiteField\Helper\ServiceAgreementHelper::getActiveAgreements(); - $this->revenue = \Moko\Plugin\System\MokoSuiteField\Helper\ServiceAgreementHelper::getRevenueSummary(); - - ToolbarHelper::title('Field Service — Service Agreements', 'icon-file-contract'); - ToolbarHelper::addNew('serviceagreements.add'); - parent::display($tpl); - } -} diff --git a/source/packages/com_mokosuitefield/admin/src/View/Technicians/HtmlView.php b/source/packages/com_mokosuitefield/admin/src/View/Technicians/HtmlView.php deleted file mode 100644 index 9f6f903..0000000 --- a/source/packages/com_mokosuitefield/admin/src/View/Technicians/HtmlView.php +++ /dev/null @@ -1,32 +0,0 @@ -get(DatabaseInterface::class); - - $db->setQuery($db->getQuery(true) - ->select('t.*, cd.name AS tech_name, cd.telephone, cd.email_to, v.vehicle_number') - ->select('(SELECT COUNT(*) FROM #__mokosuitefield_work_orders wo WHERE wo.technician_id = t.id AND wo.status = ' . $db->quote('completed') . ' AND MONTH(wo.actual_departure) = MONTH(NOW())) AS jobs_this_month') - ->from($db->quoteName('#__mokosuitefield_technicians', 't')) - ->join('LEFT', $db->quoteName('#__contact_details', 'cd') . ' ON cd.id = t.contact_id') - ->join('LEFT', $db->quoteName('#__mokosuitefield_vehicles', 'v') . ' ON v.id = t.vehicle_id') - ->order('cd.name ASC')); - $this->technicians = $db->loadObjectList() ?: []; - - ToolbarHelper::title('Field Service — Technicians', 'icon-users'); - ToolbarHelper::addNew('technicians.add'); - parent::display($tpl); - } -} diff --git a/source/packages/com_mokosuitefield/admin/src/View/Vehicles/HtmlView.php b/source/packages/com_mokosuitefield/admin/src/View/Vehicles/HtmlView.php deleted file mode 100644 index 1ead5bc..0000000 --- a/source/packages/com_mokosuitefield/admin/src/View/Vehicles/HtmlView.php +++ /dev/null @@ -1,23 +0,0 @@ -vehicles = \Moko\Plugin\System\MokoSuiteField\Helper\VehicleHelper::getFleet(); - $this->inspectionsDue = \Moko\Plugin\System\MokoSuiteField\Helper\VehicleHelper::getInspectionsDue(30); - - ToolbarHelper::title('Field Service — Vehicles', 'icon-truck'); - ToolbarHelper::addNew('vehicles.add'); - parent::display($tpl); - } -} diff --git a/source/packages/com_mokosuitefield/admin/src/View/WorkOrders/HtmlView.php b/source/packages/com_mokosuitefield/admin/src/View/WorkOrders/HtmlView.php deleted file mode 100644 index 06d57c4..0000000 --- a/source/packages/com_mokosuitefield/admin/src/View/WorkOrders/HtmlView.php +++ /dev/null @@ -1,52 +0,0 @@ -get(DatabaseInterface::class); - $input = Factory::getApplication()->getInput(); - - $this->filters = [ - 'status' => $input->getString('filter_status', ''), - 'trade' => $input->getString('filter_trade', ''), - 'date' => $input->getString('filter_date', ''), - 'search' => $input->getString('filter_search', ''), - ]; - - $query = $db->getQuery(true) - ->select('wo.*, cd.name AS customer_name, loc.address, loc.city, t_cd.name AS tech_name') - ->from($db->quoteName('#__mokosuitefield_work_orders', 'wo')) - ->join('LEFT', $db->quoteName('#__contact_details', 'cd') . ' ON cd.id = wo.contact_id') - ->join('LEFT', $db->quoteName('#__mokosuitefield_locations', 'loc') . ' ON loc.id = wo.location_id') - ->join('LEFT', $db->quoteName('#__mokosuitefield_technicians', 't') . ' ON t.id = wo.technician_id') - ->join('LEFT', $db->quoteName('#__contact_details', 't_cd') . ' ON t_cd.id = t.contact_id') - ->order('wo.created DESC'); - - if ($this->filters['status']) $query->where($db->quoteName('wo.status') . ' = ' . $db->quote($this->filters['status'])); - if ($this->filters['trade']) $query->where($db->quoteName('wo.trade') . ' = ' . $db->quote($this->filters['trade'])); - if ($this->filters['date']) $query->where($db->quoteName('wo.scheduled_date') . ' = ' . $db->quote($this->filters['date'])); - if ($this->filters['search']) { - $like = $db->quote('%' . $db->escape($this->filters['search'], true) . '%'); - $query->where('(wo.wo_number LIKE ' . $like . ' OR cd.name LIKE ' . $like . ')'); - } - - $db->setQuery($query, 0, 100); - $this->orders = $db->loadObjectList() ?: []; - - ToolbarHelper::title('Field Service — Work Orders', 'icon-wrench'); - ToolbarHelper::addNew('workorders.add'); - parent::display($tpl); - } -} diff --git a/source/packages/com_mokosuitefield/admin/tmpl/dashboard/default.php b/source/packages/com_mokosuitefield/admin/tmpl/dashboard/default.php deleted file mode 100644 index ecd06c8..0000000 --- a/source/packages/com_mokosuitefield/admin/tmpl/dashboard/default.php +++ /dev/null @@ -1,33 +0,0 @@ -stats; -$board = $this->dispatchBoard; -$unassigned = $this->unassigned; -$urgent = $this->urgent; -?> -
-
total_today; ?>
Today
-
urgent; ?>
Urgent
-
unassigned; ?>
Unassigned
-
en_route; ?>
En Route
-
on_site; ?>
On Site
-
completed; ?>
Done
-
-
-
Dispatch Board
- -
-
escape($tech->tech_name); ?>trade); ?>
-jobs)) : foreach ($tech->jobs as $job) : ?> -
escape($job->wo_number); ?> escape($job->customer_name ?? ''); ?> escape($job->city ?? ''); ?>
-
No jobs
-
- -
-
Unassigned ()
- -
escape($u->customer_name ?? ''); ?>
escape($u->category ?? $u->trade); ?>
- -
All assigned
-
-
diff --git a/source/packages/com_mokosuitefield/admin/tmpl/dispatch/default.php b/source/packages/com_mokosuitefield/admin/tmpl/dispatch/default.php deleted file mode 100644 index 4cf6a08..0000000 --- a/source/packages/com_mokosuitefield/admin/tmpl/dispatch/default.php +++ /dev/null @@ -1,10 +0,0 @@ -board;$s=$this->stats; -?> -
total_today; ?>
Today
urgent; ?>
Urgent
en_route; ?>
En Route
completed; ?>
Done
- -
escape($tech->tech_name); ?> -jobs as $job): ?>
escape($job->wo_number); ?> escape($job->customer_name); ?>
-
- diff --git a/source/packages/com_mokosuitefield/admin/tmpl/equipment/default.php b/source/packages/com_mokosuitefield/admin/tmpl/equipment/default.php deleted file mode 100644 index 12ec110..0000000 --- a/source/packages/com_mokosuitefield/admin/tmpl/equipment/default.php +++ /dev/null @@ -1,10 +0,0 @@ -equipment;$due=$this->serviceDue; -?> -
equipment due
- - - - -
TypeMake/ModelSerialOwnerLast Service
equipment_type)); ?>escape($e->make." ".$e->model); ?>escape($e->serial_number); ?>escape($e->owner_name); ?>last_service_date?date("M j",strtotime($e->last_service_date)):"Never"; ?>
diff --git a/source/packages/com_mokosuitefield/admin/tmpl/serviceagreements/default.php b/source/packages/com_mokosuitefield/admin/tmpl/serviceagreements/default.php deleted file mode 100644 index 13e6bb3..0000000 --- a/source/packages/com_mokosuitefield/admin/tmpl/serviceagreements/default.php +++ /dev/null @@ -1,8 +0,0 @@ -agreements; $rev=$this->revenue; ?> -
active_agreements; ?>
Active Agreements
$annual_recurring,0); ?>
Annual Recurring
$monthly_recurring,0); ?>
Monthly
- - - - - -
AgreementCustomerTradeVisitsAnnualStatusExpires
title); ?>customer_name??""); ?>trade); ?>visits_remaining; ?> of visits_per_year; ?> left$annual_amount,0); ?>">status); ?>end_date?date("M j, Y",strtotime($a->end_date)):"—"; ?>
No agreements
diff --git a/source/packages/com_mokosuitefield/admin/tmpl/technicians/default.php b/source/packages/com_mokosuitefield/admin/tmpl/technicians/default.php deleted file mode 100644 index 8a25af9..0000000 --- a/source/packages/com_mokosuitefield/admin/tmpl/technicians/default.php +++ /dev/null @@ -1,7 +0,0 @@ -technicians; $statusColors=["available"=>"success","dispatched"=>"info","en_route"=>"warning","on_site"=>"primary","off_duty"=>"secondary","on_leave"=>"dark"]; ?> - - - - - -
TechTradeStatusPhoneVehicleLicenseJobs/Month
tech_name??""); ?>trade); ?>">status)); ?>telephone??""); ?>vehicle_number??"—"); ?>license_number??"—"); ?>jobs_this_month??0); ?>
No technicians
diff --git a/source/packages/com_mokosuitefield/admin/tmpl/vehicles/default.php b/source/packages/com_mokosuitefield/admin/tmpl/vehicles/default.php deleted file mode 100644 index 4ec6984..0000000 --- a/source/packages/com_mokosuitefield/admin/tmpl/vehicles/default.php +++ /dev/null @@ -1,9 +0,0 @@ -vehicles; -?> - - - - -
VehicleMake/ModelAssigned ToMileageStatus
escape($v->vehicle_number); ?>escape($v->make." ".$v->model); ?>escape($v->assigned_tech_name); ?>mileage?number_format((int)$v->mileage):"—"; ?>status); ?>
diff --git a/source/packages/com_mokosuitefield/admin/tmpl/workorders/default.php b/source/packages/com_mokosuitefield/admin/tmpl/workorders/default.php deleted file mode 100644 index 00b7a70..0000000 --- a/source/packages/com_mokosuitefield/admin/tmpl/workorders/default.php +++ /dev/null @@ -1,34 +0,0 @@ -orders; -$f = $this->filters; -$statusColors = ['new'=>'secondary','dispatched'=>'info','en_route'=>'warning','on_site'=>'primary','in_progress'=>'primary','parts_needed'=>'danger','completed'=>'success','invoiced'=>'dark','cancelled'=>'danger']; -$priorityColors = ['emergency'=>'danger','urgent'=>'warning','high'=>'info','normal'=>'primary','low'=>'secondary','scheduled'=>'dark']; -?> - -
-
-
-
-
-
- - - - - - - - - - - - - - -
WO#CustomerTradePriorityStatusTechnicianScheduledTotal
escape($wo->wo_number); ?>escape($wo->customer_name??''); ?>
escape($wo->city??''); ?>
trade); ?>priority); ?>status)); ?>escape($wo->tech_name??'Unassigned'); ?>scheduled_date?date('M j',strtotime($wo->scheduled_date)):'—'; ?>total>0?'$'.number_format((float)$wo->total,2):'—'; ?>
No work orders
- - diff --git a/source/packages/com_mokosuitefield/api/src/Controller/FieldEquipmentController.php b/source/packages/com_mokosuitefield/api/src/Controller/FieldEquipmentController.php deleted file mode 100644 index 881eece..0000000 --- a/source/packages/com_mokosuitefield/api/src/Controller/FieldEquipmentController.php +++ /dev/null @@ -1,178 +0,0 @@ -getIdentity(); - if (!$user || $user->guest || (!$user->authorise('core.admin') && !$user->authorise($action, 'com_mokosuitefield'))) { - http_response_code(403); - echo json_encode(['error' => 'Access denied.']); - Factory::getApplication()->close(); - } - } - - public function listEquipment(): void - { - $this->requireAuth('core.manage'); - $db = Factory::getContainer()->get(DatabaseInterface::class); - $input = Factory::getApplication()->getInput(); - - $query = $db->getQuery(true) - ->select('eq.*, loc.name AS location_name, loc.address') - ->select('cd.name AS customer_name') - ->from($db->quoteName('#__mokosuitefield_equipment', 'eq')) - ->join('LEFT', $db->quoteName('#__mokosuitefield_locations', 'loc') . ' ON loc.id = eq.location_id') - ->join('LEFT', $db->quoteName('#__contact_details', 'cd') . ' ON cd.id = eq.contact_id') - ->order('eq.name ASC'); - - $type = $input->getString('type', ''); - if ($type) $query->where($db->quoteName('eq.type') . ' = ' . $db->quote($type)); - - $status = $input->getString('status', ''); - if ($status) $query->where($db->quoteName('eq.status') . ' = ' . $db->quote($status)); - - $locationId = $input->getInt('location_id', 0); - if ($locationId) $query->where('eq.location_id = ' . $locationId); - - $db->setQuery($query, 0, 100); - $this->sendJson($db->loadObjectList() ?: []); - } - - public function getEquipment(): void - { - $this->requireAuth('core.manage'); - $db = Factory::getContainer()->get(DatabaseInterface::class); - $id = Factory::getApplication()->getInput()->getInt('id', 0); - - $db->setQuery($db->getQuery(true) - ->select('eq.*, loc.name AS location_name, loc.address, cd.name AS customer_name') - ->from($db->quoteName('#__mokosuitefield_equipment', 'eq')) - ->join('LEFT', $db->quoteName('#__mokosuitefield_locations', 'loc') . ' ON loc.id = eq.location_id') - ->join('LEFT', $db->quoteName('#__contact_details', 'cd') . ' ON cd.id = eq.contact_id') - ->where('eq.id = ' . $id)); - $equipment = $db->loadObject(); - - if (!$equipment) { - http_response_code(404); - $this->sendJson(['error' => 'Equipment not found']); - return; - } - - // Service history - $db->setQuery($db->getQuery(true) - ->select('wo.id, wo.wo_number, wo.description, wo.status, wo.completed_at, wo.trade') - ->from($db->quoteName('#__mokosuitefield_work_orders', 'wo')) - ->where('wo.equipment_id = ' . $id) - ->order('wo.scheduled_date DESC'), 0, 20); - $equipment->service_history = $db->loadObjectList() ?: []; - - $this->sendJson($equipment); - } - - public function listVehicles(): void - { - $this->requireAuth('core.manage'); - $db = Factory::getContainer()->get(DatabaseInterface::class); - - $db->setQuery($db->getQuery(true) - ->select('v.*, t_cd.name AS tech_name') - ->from($db->quoteName('#__mokosuitefield_vehicles', 'v')) - ->join('LEFT', $db->quoteName('#__mokosuitefield_technicians', 't') . ' ON t.id = v.technician_id') - ->join('LEFT', $db->quoteName('#__contact_details', 't_cd') . ' ON t_cd.id = t.contact_id') - ->order('v.vehicle_name ASC')); - - $this->sendJson($db->loadObjectList() ?: []); - } - - public function truckStock(): void - { - $this->requireAuth('core.manage'); - $db = Factory::getContainer()->get(DatabaseInterface::class); - $vehicleId = Factory::getApplication()->getInput()->getInt('id', 0); - - $db->setQuery($db->getQuery(true) - ->select('ts.*, p.title AS product_name') - ->from($db->quoteName('#__mokosuitefield_truck_stock', 'ts')) - ->join('LEFT', $db->quoteName('#__mokosuite_crm_products', 'p') . ' ON p.id = ts.product_id') - ->where('ts.vehicle_id = ' . $vehicleId) - ->order('p.title ASC')); - - $this->sendJson($db->loadObjectList() ?: []); - } - - public function listAgreements(): void - { - $this->requireAuth('core.manage'); - $db = Factory::getContainer()->get(DatabaseInterface::class); - $input = Factory::getApplication()->getInput(); - - $query = $db->getQuery(true) - ->select('sa.*, cd.name AS customer_name, loc.address') - ->from($db->quoteName('#__mokosuitefield_service_agreements', 'sa')) - ->join('LEFT', $db->quoteName('#__contact_details', 'cd') . ' ON cd.id = sa.contact_id') - ->join('LEFT', $db->quoteName('#__mokosuitefield_locations', 'loc') . ' ON loc.id = sa.location_id') - ->order('sa.end_date ASC'); - - $status = $input->getString('status', ''); - if ($status) $query->where($db->quoteName('sa.status') . ' = ' . $db->quote($status)); - - $db->setQuery($query, 0, 100); - $this->sendJson($db->loadObjectList() ?: []); - } - - public function getAgreement(): void - { - $this->requireAuth('core.manage'); - $db = Factory::getContainer()->get(DatabaseInterface::class); - $id = Factory::getApplication()->getInput()->getInt('id', 0); - - $db->setQuery($db->getQuery(true) - ->select('sa.*, cd.name AS customer_name, loc.name AS location_name, loc.address') - ->from($db->quoteName('#__mokosuitefield_service_agreements', 'sa')) - ->join('LEFT', $db->quoteName('#__contact_details', 'cd') . ' ON cd.id = sa.contact_id') - ->join('LEFT', $db->quoteName('#__mokosuitefield_locations', 'loc') . ' ON loc.id = sa.location_id') - ->where('sa.id = ' . $id)); - $agreement = $db->loadObject(); - - if (!$agreement) { - http_response_code(404); - $this->sendJson(['error' => 'Agreement not found']); - return; - } - - // Work orders under this agreement - $db->setQuery($db->getQuery(true) - ->select('wo.id, wo.wo_number, wo.description, wo.status, wo.scheduled_date, wo.trade') - ->from($db->quoteName('#__mokosuitefield_work_orders', 'wo')) - ->where('wo.agreement_id = ' . $id) - ->order('wo.scheduled_date DESC'), 0, 30); - $agreement->work_orders = $db->loadObjectList() ?: []; - - $this->sendJson($agreement); - } - - private function sendJson(mixed $data): void - { - header('Content-Type: application/json; charset=utf-8'); - echo json_encode($data, JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE); - Factory::getApplication()->close(); - } -} diff --git a/source/packages/com_mokosuitefield/api/src/Controller/FieldEstimatesController.php b/source/packages/com_mokosuitefield/api/src/Controller/FieldEstimatesController.php deleted file mode 100644 index 5e598c9..0000000 --- a/source/packages/com_mokosuitefield/api/src/Controller/FieldEstimatesController.php +++ /dev/null @@ -1,150 +0,0 @@ -getIdentity(); - if (!$user || $user->guest || (!$user->authorise('core.admin') && !$user->authorise($action, 'com_mokosuitefield'))) { - http_response_code(403); - echo json_encode(['error' => 'Access denied.']); - Factory::getApplication()->close(); - } - } - - public function listEstimates(): void - { - $this->requireAuth('core.manage'); - $db = Factory::getContainer()->get(DatabaseInterface::class); - $input = Factory::getApplication()->getInput(); - - $query = $db->getQuery(true) - ->select('e.*, cd.name AS customer_name, loc.address') - ->from($db->quoteName('#__mokosuitefield_estimates', 'e')) - ->join('LEFT', $db->quoteName('#__contact_details', 'cd') . ' ON cd.id = e.contact_id') - ->join('LEFT', $db->quoteName('#__mokosuitefield_locations', 'loc') . ' ON loc.id = e.location_id') - ->order('e.created DESC'); - - $status = $input->getString('status', ''); - if ($status) $query->where($db->quoteName('e.status') . ' = ' . $db->quote($status)); - - $techId = $input->getInt('technician_id', 0); - if ($techId) $query->where('e.technician_id = ' . $techId); - - $db->setQuery($query, 0, 100); - $this->sendJson($db->loadObjectList() ?: []); - } - - public function createEstimate(): void - { - $this->requireAuth('core.create'); - $input = Factory::getApplication()->getInput(); - - $estimateId = \Moko\Plugin\System\MokoSuiteField\Helper\EstimateHelper::createEstimate( - $input->getInt('contact_id', 0), - $input->getInt('location_id', 0), - $input->getString('trade', 'general'), - $input->getString('description', ''), - json_decode($input->getString('line_items', '[]'), true) ?: [] - ); - - $this->sendJson(['success' => true, 'estimate_id' => $estimateId]); - } - - public function updateStatus(): void - { - $this->requireAuth('core.edit'); - $input = Factory::getApplication()->getInput(); - $id = $input->getInt('id', 0); - $status = $input->getString('status', ''); - - if (!in_array($status, ['sent', 'approved', 'rejected', 'expired'])) { - http_response_code(400); - $this->sendJson(['error' => 'Invalid status']); - return; - } - - $db = Factory::getContainer()->get(DatabaseInterface::class); - $update = (object) [ - 'id' => $id, - 'status' => $status, - ]; - - if ($status === 'approved') { - $update->approved_at = Factory::getDate()->toSql(); - } - - $db->updateObject('#__mokosuitefield_estimates', $update, 'id'); - $this->sendJson(['success' => true]); - } - - public function convertToWorkOrder(): void - { - $this->requireAuth('core.create'); - $id = Factory::getApplication()->getInput()->getInt('id', 0); - - $woId = \Moko\Plugin\System\MokoSuiteField\Helper\EstimateHelper::convertToWorkOrder($id); - - if (!$woId) { - http_response_code(400); - $this->sendJson(['error' => 'Could not convert estimate']); - return; - } - - $this->sendJson(['success' => true, 'work_order_id' => $woId]); - } - - public function getRoute(): void - { - $this->requireAuth('core.manage'); - $techId = Factory::getApplication()->getInput()->getInt('tech_id', 0); - $date = Factory::getApplication()->getInput()->getString('date', date('Y-m-d')); - - $route = \Moko\Plugin\System\MokoSuiteField\Helper\RouteHelper::getTechRoute($techId, $date); - $metrics = \Moko\Plugin\System\MokoSuiteField\Helper\RouteHelper::estimateRouteMetrics($techId, $date); - - $this->sendJson([ - 'route' => $route, - 'metrics' => $metrics, - ]); - } - - public function optimizeRoute(): void - { - $this->requireAuth('core.manage'); - $techId = Factory::getApplication()->getInput()->getInt('tech_id', 0); - $date = Factory::getApplication()->getInput()->getString('date', date('Y-m-d')); - - $optimized = \Moko\Plugin\System\MokoSuiteField\Helper\RouteHelper::optimizeRoute($techId, $date); - $metrics = \Moko\Plugin\System\MokoSuiteField\Helper\RouteHelper::estimateRouteMetrics($techId, $date); - - $this->sendJson([ - 'route' => $optimized, - 'metrics' => $metrics, - ]); - } - - private function sendJson(mixed $data): void - { - header('Content-Type: application/json; charset=utf-8'); - echo json_encode($data, JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE); - Factory::getApplication()->close(); - } -} diff --git a/source/packages/com_mokosuitefield/api/src/Controller/FieldMobileController.php b/source/packages/com_mokosuitefield/api/src/Controller/FieldMobileController.php deleted file mode 100644 index f433e4c..0000000 --- a/source/packages/com_mokosuitefield/api/src/Controller/FieldMobileController.php +++ /dev/null @@ -1,252 +0,0 @@ -getIdentity(); - if (!$user || $user->guest) { - http_response_code(401); - echo json_encode(['error' => 'Authentication required.']); - Factory::getApplication()->close(); - } - - $db = Factory::getContainer()->get(DatabaseInterface::class); - $db->setQuery($db->getQuery(true) - ->select('t.*, cd.name AS tech_name') - ->from($db->quoteName('#__mokosuitefield_technicians', 't')) - ->join('INNER', $db->quoteName('#__contact_details', 'cd') . ' ON cd.id = t.contact_id') - ->where('cd.user_id = ' . (int) $user->id)); - $tech = $db->loadObject(); - - if (!$tech) { - http_response_code(403); - echo json_encode(['error' => 'No technician profile.']); - Factory::getApplication()->close(); - } - - return $tech; - } - - public function myJobs(): void - { - $tech = $this->requireTech(); - $db = Factory::getContainer()->get(DatabaseInterface::class); - $today = date('Y-m-d'); - - $db->setQuery($db->getQuery(true) - ->select('wo.*, cd.name AS customer_name, cd.telephone AS customer_phone') - ->select('loc.address, loc.city, loc.state, loc.zip, loc.latitude, loc.longitude, loc.access_notes') - ->from($db->quoteName('#__mokosuitefield_work_orders', 'wo')) - ->join('LEFT', $db->quoteName('#__contact_details', 'cd') . ' ON cd.id = wo.contact_id') - ->join('LEFT', $db->quoteName('#__mokosuitefield_locations', 'loc') . ' ON loc.id = wo.location_id') - ->where('wo.technician_id = ' . (int) $tech->id) - ->where('(wo.scheduled_date = ' . $db->quote($today) . ' OR wo.status IN (' . $db->quote('dispatched') . ',' . $db->quote('en_route') . ',' . $db->quote('on_site') . ',' . $db->quote('in_progress') . '))') - ->order('FIELD(wo.priority,' . $db->quote('emergency') . ',' . $db->quote('urgent') . ',' . $db->quote('high') . ',' . $db->quote('normal') . ') ASC')); - - $this->sendJson($db->loadObjectList() ?: []); - } - - public function updateStatus(): void - { - $tech = $this->requireTech(); - $input = Factory::getApplication()->getInput(); - - \Moko\Plugin\System\MokoSuiteField\Helper\WorkOrderHelper::updateStatus( - $input->getInt('work_order_id', 0), - $input->getString('status', ''), - $input->getFloat('lat', 0) ?: null, - $input->getFloat('lng', 0) ?: null - ); - - $this->sendJson(['message' => 'Status updated.']); - } - - public function uploadPhoto(): void - { - $tech = $this->requireTech(); - $input = Factory::getApplication()->getInput(); - $db = Factory::getContainer()->get(DatabaseInterface::class); - - $woId = $input->getInt('work_order_id', 0); - $photoType = $input->getString('photo_type', 'other'); - $caption = $input->getString('caption', ''); - $lat = $input->getFloat('lat', 0) ?: null; - $lng = $input->getFloat('lng', 0) ?: null; - - // Handle file upload - $file = $input->files->get('photo'); - if (!$file || $file['error'] !== 0) { - http_response_code(400); - $this->sendJson(['error' => 'No photo uploaded.']); - return; - } - - $uploadDir = 'media/com_mokosuitefield/photos/' . date('Y/m/'); - if (!is_dir(JPATH_ROOT . '/' . $uploadDir)) { - mkdir(JPATH_ROOT . '/' . $uploadDir, 0755, true); - } - - $ext = pathinfo($file['name'], PATHINFO_EXTENSION); - $filename = 'wo' . $woId . '_' . time() . '_' . bin2hex(random_bytes(4)) . '.' . $ext; - $filePath = $uploadDir . $filename; - - move_uploaded_file($file['tmp_name'], JPATH_ROOT . '/' . $filePath); - - $db->insertObject('#__mokosuitefield_wo_photos', (object) [ - 'work_order_id' => $woId, - 'file_path' => $filePath, - 'photo_type' => $photoType, - 'caption' => $caption, - 'latitude' => $lat, - 'longitude' => $lng, - 'taken_at' => Factory::getDate()->toSql(), - 'uploaded_by' => Factory::getApplication()->getIdentity()->id, - 'created' => Factory::getDate()->toSql(), - ], 'id'); - - $this->sendJson(['message' => 'Photo uploaded.', 'path' => $filePath]); - } - - public function startTime(): void - { - $tech = $this->requireTech(); - $input = Factory::getApplication()->getInput(); - $db = Factory::getContainer()->get(DatabaseInterface::class); - - $db->insertObject('#__mokosuitefield_time_entries', (object) [ - 'work_order_id' => $input->getInt('work_order_id', 0), - 'technician_id' => $tech->id, - 'start_time' => Factory::getDate()->toSql(), - 'is_travel' => $input->getInt('is_travel', 0), - 'rate' => $tech->hourly_rate, - 'created' => Factory::getDate()->toSql(), - ], 'id'); - - $this->sendJson(['message' => 'Timer started.']); - } - - public function stopTime(): void - { - $tech = $this->requireTech(); - $input = Factory::getApplication()->getInput(); - $db = Factory::getContainer()->get(DatabaseInterface::class); - - $entryId = $input->getInt('entry_id', 0); - $now = Factory::getDate()->toSql(); - - $db->setQuery($db->getQuery(true)->select('start_time, rate')->from('#__mokosuitefield_time_entries')->where('id = ' . $entryId)); - $entry = $db->loadObject(); - - if (!$entry) { - http_response_code(404); - $this->sendJson(['error' => 'Time entry not found.']); - return; - } - - $hours = round((strtotime($now) - strtotime($entry->start_time)) / 3600, 2); - - $db->updateObject('#__mokosuitefield_time_entries', (object) [ - 'id' => $entryId, - 'end_time' => $now, - 'hours' => $hours, - ], 'id'); - - $this->sendJson(['message' => 'Timer stopped.', 'hours' => $hours]); - } - - public function logPart(): void - { - $tech = $this->requireTech(); - $input = Factory::getApplication()->getInput(); - - $productId = $input->getInt('product_id', 0); - $qty = $input->getFloat('quantity', 1); - $woId = $input->getInt('work_order_id', 0); - - // Deduct from truck stock - \Moko\Plugin\System\MokoSuiteField\Helper\TruckStockHelper::usePart( - (int) $tech->vehicle_id, $productId, $qty - ); - - // Add as WO line item - $db = Factory::getContainer()->get(DatabaseInterface::class); - $db->setQuery($db->getQuery(true)->select('name, cost_price, price')->from('#__mokosuite_crm_products')->where('id = ' . $productId)); - $product = $db->loadObject(); - - if ($product) { - $db->insertObject('#__mokosuitefield_wo_items', (object) [ - 'work_order_id' => $woId, - 'item_type' => 'part', - 'product_id' => $productId, - 'description' => $product->name, - 'quantity' => $qty, - 'unit_price' => (float) $product->price, - 'line_total' => $qty * (float) $product->price, - 'from_truck_stock' => 1, - ]); - } - - $this->sendJson(['message' => 'Part logged.']); - } - - public function gpsHeartbeat(): void - { - $tech = $this->requireTech(); - $input = Factory::getApplication()->getInput(); - $db = Factory::getContainer()->get(DatabaseInterface::class); - - $db->updateObject('#__mokosuitefield_technicians', (object) [ - 'id' => $tech->id, - 'current_lat' => $input->getFloat('lat', 0), - 'current_lng' => $input->getFloat('lng', 0), - 'last_location_update' => Factory::getDate()->toSql(), - ], 'id'); - - $this->sendJson(['message' => 'Location updated.']); - } - - public function equipmentLookup(): void - { - $this->requireTech(); - $qr = Factory::getApplication()->getInput()->getString('qr', ''); - - $equipment = \Moko\Plugin\System\MokoSuiteField\Helper\EquipmentHelper::getByQrCode($qr); - - if (!$equipment) { - http_response_code(404); - $this->sendJson(['error' => 'Equipment not found.']); - return; - } - - $this->sendJson($equipment); - } - - private function sendJson(mixed $data): void - { - $app = Factory::getApplication(); - $app->getDocument()->setMimeEncoding('application/json'); - echo json_encode(['data' => $data], JSON_THROW_ON_ERROR); - $app->close(); - } -} diff --git a/source/packages/com_mokosuitefield/api/src/Controller/FieldReportsController.php b/source/packages/com_mokosuitefield/api/src/Controller/FieldReportsController.php deleted file mode 100644 index e987004..0000000 --- a/source/packages/com_mokosuitefield/api/src/Controller/FieldReportsController.php +++ /dev/null @@ -1,122 +0,0 @@ -getIdentity(); - if (!$user || $user->guest || (!$user->authorise('core.admin') && !$user->authorise('core.manage', 'com_mokosuitefield'))) { - http_response_code(403); - echo json_encode(['error' => 'Access denied']); - Factory::getApplication()->close(); - } - } - - public function techPerformance(): void - { - $this->requireAuth(); - $db = Factory::getContainer()->get(DatabaseInterface::class); - $input = Factory::getApplication()->getInput(); - $from = $input->getString('from', date('Y-m-01')); - $to = $input->getString('to', date('Y-m-d')); - - $db->setQuery($db->getQuery(true) - ->select('t.id, cd.name AS tech_name, t.trade') - ->select('COUNT(wo.id) AS total_jobs') - ->select('SUM(CASE WHEN wo.status = ' . $db->quote('completed') . ' THEN 1 ELSE 0 END) AS completed') - ->select('COALESCE(AVG(TIMESTAMPDIFF(MINUTE, wo.dispatched_at, wo.completed_at)), 0) AS avg_resolution_min') - ->select('COALESCE(SUM(te.hours), 0) AS total_hours') - ->from($db->quoteName('#__mokosuitefield_technicians', 't')) - ->join('LEFT', $db->quoteName('#__contact_details', 'cd') . ' ON cd.id = t.contact_id') - ->join('LEFT', $db->quoteName('#__mokosuitefield_work_orders', 'wo') . ' ON wo.technician_id = t.id AND wo.scheduled_date BETWEEN ' . $db->quote($from) . ' AND ' . $db->quote($to)) - ->join('LEFT', $db->quoteName('#__mokosuitefield_time_entries', 'te') . ' ON te.wo_id = wo.id') - ->group('t.id') - ->order('completed DESC')); - - $techs = $db->loadObjectList() ?: []; - - foreach ($techs as &$tech) { - $tech->completion_rate = (int) $tech->total_jobs > 0 - ? round((int) $tech->completed / (int) $tech->total_jobs * 100, 1) : 0; - } - - $this->sendJson($techs); - } - - public function revenueByTrade(): void - { - $this->requireAuth(); - $db = Factory::getContainer()->get(DatabaseInterface::class); - $input = Factory::getApplication()->getInput(); - $from = $input->getString('from', date('Y-m-01')); - $to = $input->getString('to', date('Y-m-d')); - - $db->setQuery($db->getQuery(true) - ->select('wo.trade') - ->select('COUNT(wo.id) AS job_count') - ->select('COALESCE(SUM(i.total), 0) AS revenue') - ->select('COALESCE(AVG(i.total), 0) AS avg_invoice') - ->from($db->quoteName('#__mokosuitefield_work_orders', 'wo')) - ->join('LEFT', $db->quoteName('#__mokosuite_crm_invoices', 'i') . ' ON i.id = wo.invoice_id') - ->where('wo.scheduled_date BETWEEN ' . $db->quote($from) . ' AND ' . $db->quote($to)) - ->where($db->quoteName('wo.status') . ' = ' . $db->quote('completed')) - ->group('wo.trade') - ->order('revenue DESC')); - - $this->sendJson($db->loadObjectList() ?: []); - } - - public function partsUsage(): void - { - $this->requireAuth(); - $parts = \Moko\Plugin\System\MokoSuiteField\Helper\PartsHelper::getCommonParts('', 30); - $lowStock = \Moko\Plugin\System\MokoSuiteField\Helper\PartsHelper::getLowStockParts(20); - - $this->sendJson(['top_used' => $parts, 'low_stock' => $lowStock]); - } - - public function slaCompliance(): void - { - $this->requireAuth(); - $db = Factory::getContainer()->get(DatabaseInterface::class); - $input = Factory::getApplication()->getInput(); - $from = $input->getString('from', date('Y-m-01')); - $to = $input->getString('to', date('Y-m-d')); - $slaHours = $input->getInt('sla_hours', 24); - - $db->setQuery($db->getQuery(true) - ->select('COUNT(*) AS total') - ->select('SUM(CASE WHEN TIMESTAMPDIFF(HOUR, wo.created, wo.completed_at) <= ' . (int) $slaHours . ' THEN 1 ELSE 0 END) AS within_sla') - ->from($db->quoteName('#__mokosuitefield_work_orders', 'wo')) - ->where($db->quoteName('wo.status') . ' = ' . $db->quote('completed')) - ->where('wo.scheduled_date BETWEEN ' . $db->quote($from) . ' AND ' . $db->quote($to))); - - $stats = $db->loadObject() ?: (object) ['total' => 0, 'within_sla' => 0]; - $stats->sla_pct = (int) $stats->total > 0 ? round((int) $stats->within_sla / (int) $stats->total * 100, 1) : 0; - $stats->sla_target_hours = $slaHours; - - $this->sendJson($stats); - } - - private function sendJson(mixed $data): void - { - header('Content-Type: application/json; charset=utf-8'); - echo json_encode($data, JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE); - Factory::getApplication()->close(); - } -} diff --git a/source/packages/com_mokosuitefield/api/src/Controller/FieldSchedulingController.php b/source/packages/com_mokosuitefield/api/src/Controller/FieldSchedulingController.php deleted file mode 100644 index 75b125c..0000000 --- a/source/packages/com_mokosuitefield/api/src/Controller/FieldSchedulingController.php +++ /dev/null @@ -1,83 +0,0 @@ -getIdentity(); - if (!$user || $user->guest || (!$user->authorise('core.admin') && !$user->authorise($action, 'com_mokosuitefield'))) { - http_response_code(403); - echo json_encode(['error' => 'Access denied']); - Factory::getApplication()->close(); - } - } - - public function availableSlots(): void - { - $this->requireAuth(); - $input = Factory::getApplication()->getInput(); - - $slots = \Moko\Plugin\System\MokoSuiteField\Helper\SchedulingHelper::getAvailableSlots( - $input->getString('date', date('Y-m-d')), - $input->getString('trade', 'general'), - $input->getInt('duration', 60) - ); - - $this->sendJson($slots); - } - - public function bookSlot(): void - { - $this->requireAuth('core.create'); - $input = Factory::getApplication()->getInput(); - - $result = \Moko\Plugin\System\MokoSuiteField\Helper\SchedulingHelper::scheduleWorkOrder( - $input->getInt('wo_id', 0), - $input->getString('date', ''), - $input->getString('time', ''), - $input->getInt('tech_id', 0) ?: null - ); - - $this->sendJson(['success' => $result]); - } - - public function todaySchedule(): void - { - $this->requireAuth(); - $schedule = \Moko\Plugin\System\MokoSuiteField\Helper\SchedulingHelper::getTodaySchedule(); - $this->sendJson($schedule); - } - - public function techRoute(): void - { - $this->requireAuth(); - $techId = Factory::getApplication()->getInput()->getInt('tech_id', 0); - $date = Factory::getApplication()->getInput()->getString('date', date('Y-m-d')); - - $route = \Moko\Plugin\System\MokoSuiteField\Helper\RouteHelper::getTechRoute($techId, $date); - $metrics = \Moko\Plugin\System\MokoSuiteField\Helper\RouteHelper::estimateRouteMetrics($techId, $date); - - $this->sendJson(['route' => $route, 'metrics' => $metrics]); - } - - private function sendJson(mixed $data): void - { - header('Content-Type: application/json; charset=utf-8'); - echo json_encode($data, JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE); - Factory::getApplication()->close(); - } -} diff --git a/source/packages/com_mokosuitefield/api/src/Controller/FieldWorkOrderController.php b/source/packages/com_mokosuitefield/api/src/Controller/FieldWorkOrderController.php deleted file mode 100644 index d8fbe4c..0000000 --- a/source/packages/com_mokosuitefield/api/src/Controller/FieldWorkOrderController.php +++ /dev/null @@ -1,131 +0,0 @@ -getIdentity(); - if (!$user || $user->guest || (!$user->authorise('core.admin') && !$user->authorise($action, 'com_mokosuitefield'))) { - http_response_code(403); - echo json_encode(['error' => 'Access denied.']); - Factory::getApplication()->close(); - } - } - - public function list(): void - { - $this->requireAuth('core.manage'); - $db = Factory::getContainer()->get(DatabaseInterface::class); - $input = Factory::getApplication()->getInput(); - $status = $input->getString('status', ''); - $techId = $input->getInt('technician_id', 0); - $date = $input->getString('date', ''); - - $query = $db->getQuery(true) - ->select('wo.*, cd.name AS customer_name, loc.address, loc.city, t_cd.name AS tech_name') - ->from($db->quoteName('#__mokosuitefield_work_orders', 'wo')) - ->join('LEFT', $db->quoteName('#__contact_details', 'cd') . ' ON cd.id = wo.contact_id') - ->join('LEFT', $db->quoteName('#__mokosuitefield_locations', 'loc') . ' ON loc.id = wo.location_id') - ->join('LEFT', $db->quoteName('#__mokosuitefield_technicians', 't') . ' ON t.id = wo.technician_id') - ->join('LEFT', $db->quoteName('#__contact_details', 't_cd') . ' ON t_cd.id = t.contact_id') - ->order('wo.scheduled_date ASC, wo.scheduled_time_start ASC'); - - if ($status) $query->where($db->quoteName('wo.status') . ' = ' . $db->quote($status)); - if ($techId) $query->where('wo.technician_id = ' . $techId); - if ($date) $query->where('wo.scheduled_date = ' . $db->quote($date)); - - $db->setQuery($query, 0, 100); - $this->sendJson($db->loadObjectList() ?: []); - } - - public function create(): void - { - $this->requireAuth('core.create'); - $input = Factory::getApplication()->getInput(); - - $woId = \Moko\Plugin\System\MokoSuiteField\Helper\WorkOrderHelper::create( - $input->getInt('contact_id', 0), - $input->getString('trade', 'general'), - $input->getString('description', ''), - [ - 'location_id' => $input->getInt('location_id', 0), - 'priority' => $input->getString('priority', 'normal'), - 'category' => $input->getString('category', ''), - 'scheduled_date' => $input->getString('scheduled_date', ''), - 'time_start' => $input->getString('time_start', ''), - 'source' => $input->getString('source', 'phone'), - ] - ); - - $this->sendJson(['id' => $woId, 'message' => 'Work order created.']); - } - - public function updateStatus(): void - { - $input = Factory::getApplication()->getInput(); - - \Moko\Plugin\System\MokoSuiteField\Helper\WorkOrderHelper::updateStatus( - $input->getInt('id', 0), - $input->getString('status', ''), - $input->getFloat('lat', 0) ?: null, - $input->getFloat('lng', 0) ?: null - ); - - $this->sendJson(['message' => 'Status updated.']); - } - - public function dispatchToTech(): void - { - $this->requireAuth('field.dispatch'); - $input = Factory::getApplication()->getInput(); - - \Moko\Plugin\System\MokoSuiteField\Helper\DispatchHelper::dispatch( - $input->getInt('work_order_id', 0), - $input->getInt('technician_id', 0) - ); - - $this->sendJson(['message' => 'Dispatched.']); - } - - public function board(): void - { - $date = Factory::getApplication()->getInput()->getString('date', ''); - $this->sendJson(\Moko\Plugin\System\MokoSuiteField\Helper\DispatchHelper::getDispatchBoard($date)); - } - - public function availableTechs(): void - { - $input = Factory::getApplication()->getInput(); - $tech = \Moko\Plugin\System\MokoSuiteField\Helper\DispatchHelper::findBestTech( - $input->getString('trade', 'general'), - $input->getString('zip', '') - ); - - $this->sendJson($tech ? [$tech] : []); - } - - private function sendJson(mixed $data): void - { - $app = Factory::getApplication(); - $app->getDocument()->setMimeEncoding('application/json'); - echo json_encode(['data' => $data], JSON_THROW_ON_ERROR); - $app->close(); - } -} diff --git a/source/packages/com_mokosuitefield/media/css/field.css b/source/packages/com_mokosuitefield/media/css/field.css deleted file mode 100644 index a22318a..0000000 --- a/source/packages/com_mokosuitefield/media/css/field.css +++ /dev/null @@ -1,9 +0,0 @@ -/* MokoSuite Field Service Styles */ -.dispatch-board .tech-card { border-left: 4px solid #198754; } -.dispatch-board .tech-card.dispatched { border-left-color: #0d6efd; } -.dispatch-board .tech-card.on-site { border-left-color: #ffc107; } -.wo-priority-emergency { background-color: rgba(220, 53, 69, 0.1) !important; } -.wo-priority-urgent { background-color: rgba(255, 193, 7, 0.08) !important; } -.tech-mobile .current-job { border: 2px solid #0d6efd; } -@media (max-width: 768px) { .tech-mobile .card { margin-bottom: 0.5rem; } } -@media print { .btn, .toolbar { display: none !important; } } diff --git a/source/packages/com_mokosuitefield/media/js/dispatch.js b/source/packages/com_mokosuitefield/media/js/dispatch.js deleted file mode 100644 index f0d9d81..0000000 --- a/source/packages/com_mokosuitefield/media/js/dispatch.js +++ /dev/null @@ -1,6 +0,0 @@ -document.addEventListener('DOMContentLoaded', function() { - // Auto-refresh dispatch board every 30 seconds - if (document.querySelector('.dispatch-board')) { - setInterval(function() { location.reload(); }, 30000); - } -}); diff --git a/source/packages/com_mokosuitefield/site/src/Controller/DisplayController.php b/source/packages/com_mokosuitefield/site/src/Controller/DisplayController.php deleted file mode 100644 index 9ca2166..0000000 --- a/source/packages/com_mokosuitefield/site/src/Controller/DisplayController.php +++ /dev/null @@ -1,8 +0,0 @@ -getParams('com_mokosuitefield'); - $this->companyName = $params->get('company_name', $app->get('sitename')); - - $this->trades = [ - 'plumbing' => 'Plumbing', - 'electrical' => 'Electrical', - 'hvac' => 'HVAC / Heating & Cooling', - 'appliance' => 'Appliance Repair', - 'general' => 'General Maintenance', - 'carpentry' => 'Carpentry', - 'painting' => 'Painting', - 'roofing' => 'Roofing', - 'landscaping' => 'Landscaping', - 'locksmith' => 'Locksmith', - ]; - - if ($app->getInput()->getMethod() === 'POST' && \Joomla\CMS\Session\Session::checkToken()) { - $input = $app->getInput(); - $db = Factory::getContainer()->get(DatabaseInterface::class); - - $name = $input->getString('name', ''); - $email = $input->getString('email', ''); - $phone = $input->getString('phone', ''); - $address = $input->getString('address', ''); - $trade = $input->getString('trade', 'general'); - $desc = $input->getString('description', ''); - $priority = $input->getString('priority', 'normal'); - - if ($name && $phone && $desc) { - // Find or create contact - $db->setQuery($db->getQuery(true)->select('id')->from('#__contact_details') - ->where($db->quoteName('telephone') . ' = ' . $db->quote($phone))); - $contactId = (int) $db->loadResult(); - - if (!$contactId) { - $db->insertObject('#__contact_details', (object) [ - 'name' => $name, 'email_to' => $email, 'telephone' => $phone, - 'address' => $address, 'published' => 1, 'created' => Factory::getDate()->toSql(), - ], 'id'); - $contactId = $db->insertid(); - } - - // Create work order - \Moko\Plugin\System\MokoSuiteField\Helper\WorkOrderHelper::create( - (int) $contactId, $trade, $desc, [ - 'priority' => $priority, - 'source' => 'website', - ] - ); - - $this->submitted = true; - } - } - - parent::display($tpl); - } -} diff --git a/source/packages/com_mokosuitefield/site/src/View/CustomerPortal/HtmlView.php b/source/packages/com_mokosuitefield/site/src/View/CustomerPortal/HtmlView.php deleted file mode 100644 index 1aef77c..0000000 --- a/source/packages/com_mokosuitefield/site/src/View/CustomerPortal/HtmlView.php +++ /dev/null @@ -1,89 +0,0 @@ -getIdentity(); - - if (!$user || $user->guest) { - $app->redirect('index.php?option=com_users&view=login'); - return; - } - - $db = Factory::getContainer()->get(DatabaseInterface::class); - - // Resolve contact from Joomla user - $db->setQuery($db->getQuery(true)->select('id')->from('#__contact_details')->where('user_id = ' . (int) $user->id)); - $this->contactId = (int) $db->loadResult(); - - if (!$this->contactId) { - $app->enqueueMessage('No customer profile found.', 'warning'); - return; - } - - // Active work orders (in-progress) - $db->setQuery($db->getQuery(true) - ->select('wo.*, t_cd.name AS tech_name, t.trade AS tech_trade, loc.address') - ->from($db->quoteName('#__mokosuitefield_work_orders', 'wo')) - ->join('LEFT', $db->quoteName('#__mokosuitefield_technicians', 't') . ' ON t.id = wo.technician_id') - ->join('LEFT', $db->quoteName('#__contact_details', 't_cd') . ' ON t_cd.id = t.contact_id') - ->join('LEFT', $db->quoteName('#__mokosuitefield_locations', 'loc') . ' ON loc.id = wo.location_id') - ->where('wo.contact_id = ' . $this->contactId) - ->where($db->quoteName('wo.status') . ' NOT IN (' . $db->quote('completed') . ',' . $db->quote('invoiced') . ',' . $db->quote('cancelled') . ')') - ->order('FIELD(wo.priority,' . $db->quote('emergency') . ',' . $db->quote('urgent') . ',' . $db->quote('high') . ',' . $db->quote('normal') . ') ASC')); - $this->activeOrders = $db->loadObjectList() ?: []; - - // Recent completed orders (last 10) - $db->setQuery($db->getQuery(true) - ->select('wo.wo_number, wo.trade, wo.category, wo.total, wo.actual_departure AS completed_at, wo.work_performed') - ->from($db->quoteName('#__mokosuitefield_work_orders', 'wo')) - ->where('wo.contact_id = ' . $this->contactId) - ->where($db->quoteName('wo.status') . ' IN (' . $db->quote('completed') . ',' . $db->quote('invoiced') . ')') - ->order('wo.actual_departure DESC'), 0, 10); - $this->orderHistory = $db->loadObjectList() ?: []; - - // Equipment at customer locations - $db->setQuery($db->getQuery(true) - ->select('e.*, loc.address') - ->from($db->quoteName('#__mokosuitefield_equipment', 'e')) - ->join('LEFT', $db->quoteName('#__mokosuitefield_locations', 'loc') . ' ON loc.id = e.location_id') - ->where('e.contact_id = ' . $this->contactId) - ->order('e.equipment_type ASC')); - $this->equipment = $db->loadObjectList() ?: []; - - // Active service agreements - $this->agreements = \Moko\Plugin\System\MokoSuiteField\Helper\ServiceAgreementHelper::getActiveAgreements($this->contactId); - - // Next scheduled service - $db->setQuery($db->getQuery(true) - ->select('wo.wo_number, wo.scheduled_date, wo.scheduled_time_start, wo.trade, wo.category') - ->from($db->quoteName('#__mokosuitefield_work_orders', 'wo')) - ->where('wo.contact_id = ' . $this->contactId) - ->where($db->quoteName('wo.scheduled_date') . ' >= CURDATE()') - ->where($db->quoteName('wo.status') . ' NOT IN (' . $db->quote('completed') . ',' . $db->quote('cancelled') . ')') - ->order('wo.scheduled_date ASC, wo.scheduled_time_start ASC'), 0, 1); - $this->nextService = $db->loadObject(); - - parent::display($tpl); - } -} diff --git a/source/packages/com_mokosuitefield/site/src/View/EstimateView/HtmlView.php b/source/packages/com_mokosuitefield/site/src/View/EstimateView/HtmlView.php deleted file mode 100644 index 6c6f8eb..0000000 --- a/source/packages/com_mokosuitefield/site/src/View/EstimateView/HtmlView.php +++ /dev/null @@ -1,90 +0,0 @@ -getInput(); - $db = Factory::getContainer()->get(DatabaseInterface::class); - - $token = $input->getString('token', ''); - - if (!$token) { - $app->enqueueMessage('Invalid estimate link.', 'warning'); - parent::display($tpl); - return; - } - - // Load estimate by token - $db->setQuery($db->getQuery(true) - ->select('e.*, cd.name AS customer_name, cd.email_to, cd.telephone') - ->select('l.address, l.city, l.state, l.zip') - ->from($db->quoteName('#__mokosuitefield_estimates', 'e')) - ->join('LEFT', $db->quoteName('#__contact_details', 'cd') . ' ON cd.id = e.contact_id') - ->join('LEFT', $db->quoteName('#__mokosuitefield_locations', 'l') . ' ON l.id = e.location_id') - ->where($db->quoteName('e.approval_token') . ' = ' . $db->quote($token))); - $this->estimate = $db->loadObject(); - - if (!$this->estimate) { - $app->enqueueMessage('Estimate not found or link expired.', 'warning'); - parent::display($tpl); - return; - } - - // Load line items - $db->setQuery($db->getQuery(true) - ->select('*') - ->from('#__mokosuitefield_estimate_items') - ->where('estimate_id = ' . (int) $this->estimate->id) - ->order('ordering ASC')); - $this->lineItems = $db->loadObjectList() ?: []; - - // Handle approval/rejection (CSRF check not required — token-based public page) - if ($input->getMethod() === 'POST' && \Joomla\CMS\Session\Session::checkToken()) { - $action = $input->getString('action', ''); - - if ($action === 'approve' && $this->estimate->status === 'sent') { - $db->setQuery($db->getQuery(true) - ->update('#__mokosuitefield_estimates') - ->set($db->quoteName('status') . ' = ' . $db->quote('approved')) - ->set($db->quoteName('approved_at') . ' = ' . $db->quote(Factory::getDate()->toSql())) - ->set($db->quoteName('customer_signature') . ' = ' . $db->quote($input->getString('signature', ''))) - ->where('id = ' . (int) $this->estimate->id)); - $db->execute(); - - $this->actioned = true; - $this->actionResult = 'approved'; - $this->estimate->status = 'approved'; - } elseif ($action === 'reject' && $this->estimate->status === 'sent') { - $db->setQuery($db->getQuery(true) - ->update('#__mokosuitefield_estimates') - ->set($db->quoteName('status') . ' = ' . $db->quote('rejected')) - ->set($db->quoteName('rejection_reason') . ' = ' . $db->quote($input->getString('reason', ''))) - ->where('id = ' . (int) $this->estimate->id)); - $db->execute(); - - $this->actioned = true; - $this->actionResult = 'rejected'; - $this->estimate->status = 'rejected'; - } - } - - parent::display($tpl); - } -} diff --git a/source/packages/com_mokosuitefield/site/src/View/TechMobile/HtmlView.php b/source/packages/com_mokosuitefield/site/src/View/TechMobile/HtmlView.php deleted file mode 100644 index 7695292..0000000 --- a/source/packages/com_mokosuitefield/site/src/View/TechMobile/HtmlView.php +++ /dev/null @@ -1,70 +0,0 @@ -getIdentity(); - - if (!$user || $user->guest) { - $app->redirect('index.php?option=com_users&view=login'); - return; - } - - $db = Factory::getContainer()->get(DatabaseInterface::class); - - // Find technician record for logged-in user - $db->setQuery($db->getQuery(true) - ->select('t.*, cd.name AS tech_name') - ->from($db->quoteName('#__mokosuitefield_technicians', 't')) - ->join('INNER', $db->quoteName('#__contact_details', 'cd') . ' ON cd.id = t.contact_id') - ->join('INNER', $db->quoteName('#__users', 'u') . ' ON u.id = ' . (int) $user->id) - ->where('cd.user_id = ' . (int) $user->id)); - $this->tech = $db->loadObject(); - - if (!$this->tech) { - $app->enqueueMessage('No technician profile found for your account.', 'warning'); - return; - } - - $today = date('Y-m-d'); - - // Today's assigned jobs - $db->setQuery($db->getQuery(true) - ->select('wo.*, cd.name AS customer_name, cd.telephone AS customer_phone') - ->select('loc.address, loc.city, loc.state, loc.zip, loc.latitude, loc.longitude, loc.access_notes') - ->from($db->quoteName('#__mokosuitefield_work_orders', 'wo')) - ->join('LEFT', $db->quoteName('#__contact_details', 'cd') . ' ON cd.id = wo.contact_id') - ->join('LEFT', $db->quoteName('#__mokosuitefield_locations', 'loc') . ' ON loc.id = wo.location_id') - ->where('wo.technician_id = ' . (int) $this->tech->id) - ->where('(wo.scheduled_date = ' . $db->quote($today) . ' OR (wo.scheduled_date IS NULL AND wo.status NOT IN (' . $db->quote('completed') . ',' . $db->quote('cancelled') . ',' . $db->quote('invoiced') . ')))') - ->order('FIELD(wo.priority,' . $db->quote('emergency') . ',' . $db->quote('urgent') . ',' . $db->quote('high') . ',' . $db->quote('normal') . ',' . $db->quote('low') . ') ASC, wo.scheduled_time_start ASC')); - $this->todayJobs = $db->loadObjectList() ?: []; - - // Current active job (en_route, on_site, or in_progress) - foreach ($this->todayJobs as $job) { - if (in_array($job->status, ['en_route', 'on_site', 'in_progress'])) { - $this->currentJob = $job; - break; - } - } - - parent::display($tpl); - } -} diff --git a/source/packages/com_mokosuitefield/site/tmpl/bookservice/default.php b/source/packages/com_mokosuitefield/site/tmpl/bookservice/default.php deleted file mode 100644 index 237af91..0000000 --- a/source/packages/com_mokosuitefield/site/tmpl/bookservice/default.php +++ /dev/null @@ -1,33 +0,0 @@ -companyName; -$trades = $this->trades; -if ($this->submitted) : ?> -

Service Request Received

We will contact you shortly to schedule your appointment.

- -
-

Request Service

-

escape($company); ?>

-
-
-
-
-
-
-
-
-
-
-
-
-
- - -
-
-
diff --git a/source/packages/com_mokosuitefield/site/tmpl/customerportal/default.php b/source/packages/com_mokosuitefield/site/tmpl/customerportal/default.php deleted file mode 100644 index 9f7da25..0000000 --- a/source/packages/com_mokosuitefield/site/tmpl/customerportal/default.php +++ /dev/null @@ -1,14 +0,0 @@ -contactId) return; -$active=$this->activeOrders;$history=$this->orderHistory;$next=$this->nextService; -?> -

My Service Portal

-
Next Service
trade); ?>scheduled_date)); ?>
-
Active Work Orders
- -
WO#ServiceStatus
escape($wo->wo_number); ?>trade); ?>status)); ?>
-
Service History
- -
WO#ServiceTotalDate
escape($h->wo_number); ?>trade); ?>total>0?"$".number_format((float)$h->total,2):""; ?>completed_at?date("M j",strtotime($h->completed_at)):""; ?>
-
diff --git a/source/packages/com_mokosuitefield/site/tmpl/estimateview/default.php b/source/packages/com_mokosuitefield/site/tmpl/estimateview/default.php deleted file mode 100644 index 50fa071..0000000 --- a/source/packages/com_mokosuitefield/site/tmpl/estimateview/default.php +++ /dev/null @@ -1,96 +0,0 @@ -estimate) return; -$e = $this->estimate; -$subtotal = 0; -foreach ($this->lineItems as $li) { $subtotal += (float) $li->line_total; } -$tax = round($subtotal * 0.07, 2); -$total = $subtotal + $tax; -?> -
- actioned) : ?> -
-

Estimate actionResult); ?>

-

actionResult === 'approved' ? 'Thank you! We will schedule the work shortly.' : 'The estimate has been declined.'; ?>

-
- - -
-
-

Service Estimate

- status); ?> -
-
-
-
- Customer: customer_name ?? ''); ?>
- address) : ?>Location: address . ', ' . $e->city . ', ' . $e->state . ' ' . $e->zip); ?>
-
-
- Estimate #: estimate_number ?? $e->id); ?>
- Date: created)); ?>
- valid_until) : ?>Valid Until: valid_until)); ?>
-
-
- - description) : ?> -

Scope of Work: description)); ?>

- - - - - - lineItems as $li) : ?> - - - - - - - - - - - - - -
DescriptionQtyUnit PriceTotal
description); ?>quantity; ?>$unit_price, 2); ?>$line_total, 2); ?>
Subtotal$
Tax$
Total$
-
-
- - status === 'sent') : ?> -
-
-
- - -
-
-
Approve Estimate
-
- - -
- -
-
-
-
-
-
- - -
-
-
Decline Estimate
-
- - -
- -
-
-
-
-
- -
diff --git a/source/packages/com_mokosuitefield/site/tmpl/techmobile/default.php b/source/packages/com_mokosuitefield/site/tmpl/techmobile/default.php deleted file mode 100644 index 691ee15..0000000 --- a/source/packages/com_mokosuitefield/site/tmpl/techmobile/default.php +++ /dev/null @@ -1,56 +0,0 @@ -tech; -$jobs = $this->todayJobs; -$current = $this->currentJob; -if (!$tech) return; -$statusColors = ['new'=>'secondary','dispatched'=>'info','en_route'=>'warning','on_site'=>'primary','in_progress'=>'primary','parts_needed'=>'danger','completed'=>'success']; -?> -
-
-

escape($tech->tech_name); ?>

-status)); ?> -
- - -
-
Current Job: escape($current->wo_number); ?>
-
-
escape($current->customer_name); ?>
-

escape($current->address); ?>, escape($current->city); ?>

-customer_phone) : ?>

customer_phone; ?>

-access_notes) : ?>
Access: escape($current->access_notes); ?>
-

escape($current->description); ?>

-
-status === 'dispatched') : ?> -status === 'en_route') : ?> -status === 'on_site') : ?> -status === 'in_progress') : ?> -latitude && $current->longitude) : ?> - - -
-
-
- - -
Today ( jobs)
-id === $current->id) continue; ?> -
-
-
escape($job->customer_name); ?>status)); ?>
-escape($job->city??''); ?> | scheduled_time_start?date('g:ia',strtotime($job->scheduled_time_start)):'TBD'; ?> | trade); ?> -
-
- -

No jobs today.

-
- diff --git a/source/packages/plg_system_mokosuitefield/language/en-GB/plg_system_mokosuitefield.ini b/source/packages/plg_system_mokosuitefield/language/en-GB/plg_system_mokosuitefield.ini deleted file mode 100644 index c507f86..0000000 --- a/source/packages/plg_system_mokosuitefield/language/en-GB/plg_system_mokosuitefield.ini +++ /dev/null @@ -1,2 +0,0 @@ -PLG_SYSTEM_MOKOSUITEFIELD="System - MokoSuite Field" -PLG_SYSTEM_MOKOSUITEFIELD_DESC="MokoSuite Field Service system plugin - database schema and helpers." diff --git a/source/packages/plg_system_mokosuitefield/language/en-GB/plg_system_mokosuitefield.sys.ini b/source/packages/plg_system_mokosuitefield/language/en-GB/plg_system_mokosuitefield.sys.ini deleted file mode 100644 index 26bf5c4..0000000 --- a/source/packages/plg_system_mokosuitefield/language/en-GB/plg_system_mokosuitefield.sys.ini +++ /dev/null @@ -1,2 +0,0 @@ -PLG_SYSTEM_MOKOSUITEFIELD="System - MokoSuite Field" -PLG_SYSTEM_MOKOSUITEFIELD_DESC="MokoSuite Field Service system plugin." diff --git a/source/packages/plg_system_mokosuitefield/sql/install.mysql.sql b/source/packages/plg_system_mokosuitefield/sql/install.mysql.sql deleted file mode 100644 index e0a9d8d..0000000 --- a/source/packages/plg_system_mokosuitefield/sql/install.mysql.sql +++ /dev/null @@ -1,332 +0,0 @@ --- --- MokoSuite Field Service Tables --- - --- ============================================================ --- Technicians — extends CRM contacts / HRM employees --- ============================================================ -CREATE TABLE IF NOT EXISTS `#__mokosuitefield_technicians` ( - `id` INT UNSIGNED NOT NULL AUTO_INCREMENT, - `contact_id` INT NOT NULL COMMENT 'FK to #__contact_details', - `employee_id` INT UNSIGNED DEFAULT NULL COMMENT 'FK to HRM employees if installed', - `tech_number` VARCHAR(20) NOT NULL DEFAULT '', - `status` ENUM('available','dispatched','en_route','on_site','off_duty','on_leave') NOT NULL DEFAULT 'available', - `trade` ENUM('general','plumbing','electrical','hvac','appliance','carpentry','painting','roofing','landscaping','pest_control','locksmith','multi_trade') NOT NULL DEFAULT 'general', - `license_number` VARCHAR(100) DEFAULT NULL COMMENT 'Trade license (e.g., master plumber)', - `license_expiry` DATE DEFAULT NULL, - `certifications` JSON DEFAULT NULL COMMENT '["EPA 608","backflow certified","journeyman electrician"]', - `skills` JSON DEFAULT NULL, - `hourly_rate` DECIMAL(10,2) DEFAULT NULL, - `overtime_rate` DECIMAL(10,2) DEFAULT NULL, - `max_daily_jobs` INT UNSIGNED NOT NULL DEFAULT 8, - `service_radius_miles` INT UNSIGNED NOT NULL DEFAULT 30, - `home_zip` VARCHAR(10) DEFAULT NULL, - `home_lat` DECIMAL(10,7) DEFAULT NULL, - `home_lng` DECIMAL(10,7) DEFAULT NULL, - `current_lat` DECIMAL(10,7) DEFAULT NULL, - `current_lng` DECIMAL(10,7) DEFAULT NULL, - `last_location_update` DATETIME DEFAULT NULL, - `vehicle_id` INT UNSIGNED DEFAULT NULL, - `notes` TEXT, - `created` DATETIME NOT NULL, - `modified` DATETIME DEFAULT NULL, - PRIMARY KEY (`id`), - UNIQUE KEY `idx_contact` (`contact_id`), - KEY `idx_status` (`status`), - KEY `idx_trade` (`trade`), - KEY `idx_vehicle` (`vehicle_id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; - --- ============================================================ --- Service Locations — customer property/site records --- ============================================================ -CREATE TABLE IF NOT EXISTS `#__mokosuitefield_locations` ( - `id` INT UNSIGNED NOT NULL AUTO_INCREMENT, - `contact_id` INT NOT NULL COMMENT 'FK to CRM contact (property owner)', - `name` VARCHAR(255) NOT NULL DEFAULT '' COMMENT 'e.g., "Main Office", "123 Oak St Residence"', - `address` TEXT NOT NULL, - `city` VARCHAR(100) NOT NULL DEFAULT '', - `state` VARCHAR(50) NOT NULL DEFAULT '', - `zip` VARCHAR(20) NOT NULL DEFAULT '', - `latitude` DECIMAL(10,7) DEFAULT NULL, - `longitude` DECIMAL(10,7) DEFAULT NULL, - `property_type` ENUM('residential','commercial','industrial','multi_family','government','other') NOT NULL DEFAULT 'residential', - `access_notes` TEXT COMMENT 'Gate codes, parking, pet warnings, key location', - `equipment_notes` TEXT COMMENT 'Known equipment at this location', - `service_history_count` INT UNSIGNED NOT NULL DEFAULT 0, - `last_service_date` DATE DEFAULT NULL, - `created` DATETIME NOT NULL, - `modified` DATETIME DEFAULT NULL, - PRIMARY KEY (`id`), - KEY `idx_contact` (`contact_id`), - KEY `idx_zip` (`zip`), - KEY `idx_type` (`property_type`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; - --- ============================================================ --- Work Orders — the core job record --- ============================================================ -CREATE TABLE IF NOT EXISTS `#__mokosuitefield_work_orders` ( - `id` INT UNSIGNED NOT NULL AUTO_INCREMENT, - `wo_number` VARCHAR(30) NOT NULL DEFAULT '', - `contact_id` INT NOT NULL COMMENT 'Customer', - `location_id` INT UNSIGNED DEFAULT NULL, - `technician_id` INT UNSIGNED DEFAULT NULL, - `trade` ENUM('general','plumbing','electrical','hvac','appliance','carpentry','painting','roofing','landscaping','pest_control','locksmith','multi_trade') NOT NULL DEFAULT 'general', - `priority` ENUM('emergency','urgent','high','normal','low','scheduled') NOT NULL DEFAULT 'normal', - `status` ENUM('new','dispatched','en_route','on_site','in_progress','parts_needed','on_hold','completed','invoiced','cancelled') NOT NULL DEFAULT 'new', - `category` VARCHAR(100) DEFAULT NULL COMMENT 'e.g., "water heater", "panel upgrade", "AC repair"', - `description` TEXT NOT NULL, - `customer_po` VARCHAR(100) DEFAULT NULL COMMENT 'Customer PO number', - `scheduled_date` DATE DEFAULT NULL, - `scheduled_time_start` TIME DEFAULT NULL, - `scheduled_time_end` TIME DEFAULT NULL, - `actual_arrival` DATETIME DEFAULT NULL, - `actual_departure` DATETIME DEFAULT NULL, - `work_performed` TEXT, - `diagnosis` TEXT, - `parts_total` DECIMAL(10,2) NOT NULL DEFAULT 0.00, - `labor_total` DECIMAL(10,2) NOT NULL DEFAULT 0.00, - `tax` DECIMAL(10,2) NOT NULL DEFAULT 0.00, - `total` DECIMAL(10,2) NOT NULL DEFAULT 0.00, - `payment_status` ENUM('unpaid','partial','paid','waived') NOT NULL DEFAULT 'unpaid', - `payment_method` VARCHAR(50) DEFAULT NULL, - `customer_signature` TEXT COMMENT 'Base64 signature data', - `customer_signed_at` DATETIME DEFAULT NULL, - `service_agreement_id` INT UNSIGNED DEFAULT NULL, - `invoice_id` INT UNSIGNED DEFAULT NULL COMMENT 'FK to CRM invoices', - `deal_id` INT UNSIGNED DEFAULT NULL COMMENT 'FK to CRM deals', - `source` ENUM('phone','website','walk_in','referral','service_agreement','emergency','recurring') NOT NULL DEFAULT 'phone', - `created_by` INT NOT NULL DEFAULT 0, - `created` DATETIME NOT NULL, - `modified` DATETIME DEFAULT NULL, - PRIMARY KEY (`id`), - UNIQUE KEY `idx_wo_number` (`wo_number`), - KEY `idx_contact` (`contact_id`), - KEY `idx_location` (`location_id`), - KEY `idx_tech` (`technician_id`), - KEY `idx_status` (`status`), - KEY `idx_priority` (`priority`), - KEY `idx_trade` (`trade`), - KEY `idx_scheduled` (`scheduled_date`), - KEY `idx_agreement` (`service_agreement_id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; - --- ============================================================ --- Work Order Line Items — parts and labor --- ============================================================ -CREATE TABLE IF NOT EXISTS `#__mokosuitefield_wo_items` ( - `id` INT UNSIGNED NOT NULL AUTO_INCREMENT, - `work_order_id` INT UNSIGNED NOT NULL, - `item_type` ENUM('labor','part','material','flat_rate','discount','permit','disposal') NOT NULL DEFAULT 'labor', - `product_id` INT UNSIGNED DEFAULT NULL COMMENT 'FK to CRM products for parts', - `description` VARCHAR(500) NOT NULL, - `quantity` DECIMAL(10,2) NOT NULL DEFAULT 1.00, - `unit_price` DECIMAL(10,2) NOT NULL DEFAULT 0.00, - `line_total` DECIMAL(10,2) NOT NULL DEFAULT 0.00, - `taxable` TINYINT NOT NULL DEFAULT 1, - `from_truck_stock` TINYINT NOT NULL DEFAULT 0 COMMENT 'Taken from tech truck inventory', - `sort_order` INT NOT NULL DEFAULT 0, - PRIMARY KEY (`id`), - KEY `idx_wo` (`work_order_id`), - KEY `idx_type` (`item_type`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; - --- ============================================================ --- Work Order Photos — before/after, damage, equipment --- ============================================================ -CREATE TABLE IF NOT EXISTS `#__mokosuitefield_wo_photos` ( - `id` INT UNSIGNED NOT NULL AUTO_INCREMENT, - `work_order_id` INT UNSIGNED NOT NULL, - `file_path` VARCHAR(500) NOT NULL, - `thumbnail_path` VARCHAR(500) DEFAULT NULL, - `photo_type` ENUM('before','during','after','damage','equipment','permit','other') NOT NULL DEFAULT 'other', - `caption` VARCHAR(500) DEFAULT NULL, - `latitude` DECIMAL(10,7) DEFAULT NULL, - `longitude` DECIMAL(10,7) DEFAULT NULL, - `taken_at` DATETIME DEFAULT NULL, - `uploaded_by` INT NOT NULL DEFAULT 0, - `created` DATETIME NOT NULL, - PRIMARY KEY (`id`), - KEY `idx_wo` (`work_order_id`), - KEY `idx_type` (`photo_type`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; - --- ============================================================ --- Service Agreements — recurring maintenance contracts --- ============================================================ -CREATE TABLE IF NOT EXISTS `#__mokosuitefield_agreements` ( - `id` INT UNSIGNED NOT NULL AUTO_INCREMENT, - `contact_id` INT NOT NULL, - `location_id` INT UNSIGNED DEFAULT NULL, - `agreement_number` VARCHAR(30) NOT NULL DEFAULT '', - `title` VARCHAR(255) NOT NULL, - `trade` ENUM('general','plumbing','electrical','hvac','appliance','multi_trade') NOT NULL DEFAULT 'general', - `agreement_type` ENUM('preventive','full_service','parts_only','labor_only','priority_response') NOT NULL DEFAULT 'preventive', - `visits_per_year` INT UNSIGNED NOT NULL DEFAULT 2, - `visits_used` INT UNSIGNED NOT NULL DEFAULT 0, - `annual_amount` DECIMAL(10,2) NOT NULL DEFAULT 0.00, - `billing_frequency` ENUM('monthly','quarterly','semi_annual','annual') NOT NULL DEFAULT 'annual', - `start_date` DATE NOT NULL, - `end_date` DATE DEFAULT NULL, - `auto_renew` TINYINT NOT NULL DEFAULT 1, - `sla_response_hours` INT UNSIGNED DEFAULT NULL COMMENT 'Guaranteed response time', - `parts_discount_pct` DECIMAL(5,2) NOT NULL DEFAULT 0.00, - `labor_discount_pct` DECIMAL(5,2) NOT NULL DEFAULT 0.00, - `status` ENUM('active','expired','cancelled','pending_renewal') NOT NULL DEFAULT 'active', - `equipment_covered` JSON DEFAULT NULL COMMENT 'List of equipment IDs covered', - `notes` TEXT, - `created` DATETIME NOT NULL, - `modified` DATETIME DEFAULT NULL, - PRIMARY KEY (`id`), - UNIQUE KEY `idx_number` (`agreement_number`), - KEY `idx_contact` (`contact_id`), - KEY `idx_location` (`location_id`), - KEY `idx_status` (`status`), - KEY `idx_end` (`end_date`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; - --- ============================================================ --- Equipment — customer equipment tracked for service history --- ============================================================ -CREATE TABLE IF NOT EXISTS `#__mokosuitefield_equipment` ( - `id` INT UNSIGNED NOT NULL AUTO_INCREMENT, - `location_id` INT UNSIGNED NOT NULL, - `contact_id` INT NOT NULL, - `equipment_type` ENUM('water_heater','furnace','ac_unit','heat_pump','boiler','electrical_panel','generator','sump_pump','water_softener','tankless_heater','mini_split','rooftop_unit','compressor','chiller','other') NOT NULL DEFAULT 'other', - `make` VARCHAR(100) DEFAULT NULL, - `model` VARCHAR(100) DEFAULT NULL, - `serial_number` VARCHAR(100) DEFAULT NULL, - `install_date` DATE DEFAULT NULL, - `warranty_expiry` DATE DEFAULT NULL, - `last_service_date` DATE DEFAULT NULL, - `next_service_date` DATE DEFAULT NULL, - `condition` ENUM('excellent','good','fair','poor','needs_replacement') DEFAULT NULL, - `location_detail` VARCHAR(255) DEFAULT NULL COMMENT 'e.g., "basement", "roof", "garage"', - `refrigerant_type` VARCHAR(20) DEFAULT NULL COMMENT 'HVAC: R-410A, R-22, etc.', - `capacity` VARCHAR(50) DEFAULT NULL COMMENT 'e.g., "50 gal", "3 ton", "200 amp"', - `fuel_type` VARCHAR(20) DEFAULT NULL COMMENT 'gas, electric, oil, propane', - `notes` TEXT, - `photo_path` VARCHAR(500) DEFAULT NULL, - `qr_code` VARCHAR(100) DEFAULT NULL COMMENT 'QR code on equipment sticker for quick lookup', - `created` DATETIME NOT NULL, - `modified` DATETIME DEFAULT NULL, - PRIMARY KEY (`id`), - KEY `idx_location` (`location_id`), - KEY `idx_contact` (`contact_id`), - KEY `idx_type` (`equipment_type`), - KEY `idx_serial` (`serial_number`), - KEY `idx_qr` (`qr_code`), - KEY `idx_next_service` (`next_service_date`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; - --- ============================================================ --- Vehicles — service fleet tracking --- ============================================================ -CREATE TABLE IF NOT EXISTS `#__mokosuitefield_vehicles` ( - `id` INT UNSIGNED NOT NULL AUTO_INCREMENT, - `vehicle_number` VARCHAR(20) NOT NULL, - `make` VARCHAR(50) DEFAULT NULL, - `model` VARCHAR(50) DEFAULT NULL, - `year` INT UNSIGNED DEFAULT NULL, - `vin` VARCHAR(17) DEFAULT NULL, - `license_plate` VARCHAR(20) DEFAULT NULL, - `assigned_tech_id` INT UNSIGNED DEFAULT NULL, - `status` ENUM('active','maintenance','retired') NOT NULL DEFAULT 'active', - `mileage` INT UNSIGNED DEFAULT NULL, - `last_inspection` DATE DEFAULT NULL, - `next_inspection` DATE DEFAULT NULL, - `insurance_expiry` DATE DEFAULT NULL, - `gps_device_id` VARCHAR(100) DEFAULT NULL, - `notes` TEXT, - `created` DATETIME NOT NULL, - PRIMARY KEY (`id`), - KEY `idx_tech` (`assigned_tech_id`), - KEY `idx_status` (`status`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; - --- ============================================================ --- Truck Stock — parts inventory per vehicle --- ============================================================ -CREATE TABLE IF NOT EXISTS `#__mokosuitefield_truck_stock` ( - `id` INT UNSIGNED NOT NULL AUTO_INCREMENT, - `vehicle_id` INT UNSIGNED NOT NULL, - `product_id` INT UNSIGNED NOT NULL COMMENT 'FK to CRM products', - `quantity` DECIMAL(10,2) NOT NULL DEFAULT 0.00, - `min_quantity` DECIMAL(10,2) NOT NULL DEFAULT 1.00 COMMENT 'Reorder point', - `last_restocked` DATE DEFAULT NULL, - PRIMARY KEY (`id`), - UNIQUE KEY `idx_vehicle_product` (`vehicle_id`, `product_id`), - KEY `idx_vehicle` (`vehicle_id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; - --- ============================================================ --- Dispatch Log — assignment and routing history --- ============================================================ -CREATE TABLE IF NOT EXISTS `#__mokosuitefield_dispatch_log` ( - `id` INT UNSIGNED NOT NULL AUTO_INCREMENT, - `work_order_id` INT UNSIGNED NOT NULL, - `technician_id` INT UNSIGNED NOT NULL, - `action` ENUM('assigned','accepted','rejected','en_route','arrived','completed','reassigned','cancelled') NOT NULL, - `notes` TEXT, - `latitude` DECIMAL(10,7) DEFAULT NULL, - `longitude` DECIMAL(10,7) DEFAULT NULL, - `created_by` INT NOT NULL DEFAULT 0, - `created` DATETIME NOT NULL, - PRIMARY KEY (`id`), - KEY `idx_wo` (`work_order_id`), - KEY `idx_tech` (`technician_id`), - KEY `idx_action` (`action`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; - --- ============================================================ --- Estimates — on-site quoting --- ============================================================ -CREATE TABLE IF NOT EXISTS `#__mokosuitefield_estimates` ( - `id` INT UNSIGNED NOT NULL AUTO_INCREMENT, - `work_order_id` INT UNSIGNED DEFAULT NULL COMMENT 'Generated from a WO inspection', - `contact_id` INT NOT NULL, - `location_id` INT UNSIGNED DEFAULT NULL, - `estimate_number` VARCHAR(30) NOT NULL DEFAULT '', - `title` VARCHAR(255) NOT NULL, - `description` TEXT, - `parts_total` DECIMAL(10,2) NOT NULL DEFAULT 0.00, - `labor_total` DECIMAL(10,2) NOT NULL DEFAULT 0.00, - `tax` DECIMAL(10,2) NOT NULL DEFAULT 0.00, - `total` DECIMAL(10,2) NOT NULL DEFAULT 0.00, - `status` ENUM('draft','sent','viewed','accepted','declined','expired','converted') NOT NULL DEFAULT 'draft', - `valid_days` INT UNSIGNED NOT NULL DEFAULT 30, - `token` VARCHAR(64) DEFAULT NULL COMMENT 'Public view/accept token', - `accepted_at` DATETIME DEFAULT NULL, - `customer_signature` TEXT, - `converted_wo_id` INT UNSIGNED DEFAULT NULL COMMENT 'WO created from accepted estimate', - `created_by` INT NOT NULL DEFAULT 0, - `created` DATETIME NOT NULL, - `modified` DATETIME DEFAULT NULL, - PRIMARY KEY (`id`), - UNIQUE KEY `idx_number` (`estimate_number`), - KEY `idx_contact` (`contact_id`), - KEY `idx_wo` (`work_order_id`), - KEY `idx_status` (`status`), - KEY `idx_token` (`token`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; - --- ============================================================ --- Time Entries — tech labor tracking per work order --- ============================================================ -CREATE TABLE IF NOT EXISTS `#__mokosuitefield_time_entries` ( - `id` INT UNSIGNED NOT NULL AUTO_INCREMENT, - `work_order_id` INT UNSIGNED NOT NULL, - `technician_id` INT UNSIGNED NOT NULL, - `start_time` DATETIME NOT NULL, - `end_time` DATETIME DEFAULT NULL, - `hours` DECIMAL(5,2) DEFAULT NULL, - `is_overtime` TINYINT NOT NULL DEFAULT 0, - `is_travel` TINYINT NOT NULL DEFAULT 0, - `rate` DECIMAL(10,2) DEFAULT NULL, - `notes` TEXT, - `created` DATETIME NOT NULL, - PRIMARY KEY (`id`), - KEY `idx_wo` (`work_order_id`), - KEY `idx_tech` (`technician_id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; diff --git a/source/packages/plg_system_mokosuitefield/sql/uninstall.mysql.sql b/source/packages/plg_system_mokosuitefield/sql/uninstall.mysql.sql deleted file mode 100644 index e804a60..0000000 --- a/source/packages/plg_system_mokosuitefield/sql/uninstall.mysql.sql +++ /dev/null @@ -1,12 +0,0 @@ -DROP TABLE IF EXISTS `#__mokosuitefield_time_entries`; -DROP TABLE IF EXISTS `#__mokosuitefield_dispatch_log`; -DROP TABLE IF EXISTS `#__mokosuitefield_estimates`; -DROP TABLE IF EXISTS `#__mokosuitefield_truck_stock`; -DROP TABLE IF EXISTS `#__mokosuitefield_vehicles`; -DROP TABLE IF EXISTS `#__mokosuitefield_wo_photos`; -DROP TABLE IF EXISTS `#__mokosuitefield_wo_items`; -DROP TABLE IF EXISTS `#__mokosuitefield_work_orders`; -DROP TABLE IF EXISTS `#__mokosuitefield_agreements`; -DROP TABLE IF EXISTS `#__mokosuitefield_equipment`; -DROP TABLE IF EXISTS `#__mokosuitefield_locations`; -DROP TABLE IF EXISTS `#__mokosuitefield_technicians`; diff --git a/source/packages/plg_system_mokosuitefield/src/Helper/CustomerFeedbackHelper.php b/source/packages/plg_system_mokosuitefield/src/Helper/CustomerFeedbackHelper.php deleted file mode 100644 index 21df3a5..0000000 --- a/source/packages/plg_system_mokosuitefield/src/Helper/CustomerFeedbackHelper.php +++ /dev/null @@ -1,132 +0,0 @@ -get(DatabaseInterface::class); - - $db->setQuery($db->getQuery(true) - ->select('wo.id, wo.wo_number, wo.contact_id, cd.name AS customer_name, cd.email_to, cd.telephone') - ->from($db->quoteName('#__mokosuitefield_work_orders', 'wo')) - ->join('LEFT', $db->quoteName('#__contact_details', 'cd') . ' ON cd.id = wo.contact_id') - ->where('wo.id = ' . (int) $woId)); - $wo = $db->loadObject(); - - if (!$wo || !$wo->email_to) { - return (object) ['success' => false, 'error' => 'No email for feedback request']; - } - - // Generate unique feedback token - $token = bin2hex(random_bytes(16)); - - $request = (object) [ - 'wo_id' => $woId, - 'contact_id' => $wo->contact_id, - 'token' => $token, - 'status' => 'sent', - 'sent_at' => Factory::getDate()->toSql(), - ]; - $db->insertObject('#__mokosuitefield_feedback_requests', $request, 'id'); - - return (object) ['success' => true, 'token' => $token, 'email' => $wo->email_to]; - } - - /** - * Submit feedback (called from public survey page via token). - */ - public static function submitFeedback(string $token, int $rating, int $npsScore, string $comments = ''): object - { - $db = Factory::getContainer()->get(DatabaseInterface::class); - - // Validate inputs before DB access - $rating = max(1, min(5, $rating)); - $npsScore = max(0, min(10, $npsScore)); - - $filter = \Joomla\Filter\InputFilter::getInstance(); - $comments = $filter->clean($comments, 'STRING'); - - $db->transactionStart(); - try { - // Lock the request row to prevent race condition on token reuse - $db->setQuery('SELECT id, wo_id, contact_id, status FROM #__mokosuitefield_feedback_requests WHERE ' - . $db->quoteName('token') . ' = ' . $db->quote($token) . ' FOR UPDATE'); - $request = $db->loadObject(); - - if (!$request) { - $db->transactionRollback(); - return (object) ['success' => false, 'error' => 'Invalid feedback link']; - } - if ($request->status === 'completed') { - $db->transactionRollback(); - return (object) ['success' => false, 'error' => 'Feedback already submitted']; - } - - $feedback = (object) [ - 'request_id' => $request->id, - 'wo_id' => $request->wo_id, - 'contact_id' => $request->contact_id, - 'rating' => $rating, - 'nps_score' => $npsScore, - 'comments' => $comments, - 'submitted_at' => Factory::getDate()->toSql(), - ]; - $db->insertObject('#__mokosuitefield_feedback', $feedback); - - $db->setQuery($db->getQuery(true) - ->update('#__mokosuitefield_feedback_requests') - ->set($db->quoteName('status') . ' = ' . $db->quote('completed')) - ->where('id = ' . (int) $request->id)); - $db->execute(); - - $db->transactionCommit(); - } catch (\Throwable $e) { - $db->transactionRollback(); - // Don't leak internal errors to public users - return (object) ['success' => false, 'error' => 'Unable to save feedback. Please try again.']; - } - - return (object) ['success' => true, 'rating' => $rating, 'nps' => $npsScore]; - } - - /** - * Get NPS score and satisfaction summary. - */ - public static function getSatisfactionSummary(string $from = '', string $to = ''): object - { - $db = Factory::getContainer()->get(DatabaseInterface::class); - $from = $from ?: date('Y-01-01'); - $to = $to ?: date('Y-m-d'); - - $db->setQuery($db->getQuery(true) - ->select('COUNT(*) AS total_responses') - ->select('COALESCE(AVG(rating), 0) AS avg_rating') - ->select('COALESCE(AVG(nps_score), 0) AS avg_nps') - ->select('SUM(CASE WHEN nps_score >= 9 THEN 1 ELSE 0 END) AS promoters') - ->select('SUM(CASE WHEN nps_score BETWEEN 7 AND 8 THEN 1 ELSE 0 END) AS passives') - ->select('SUM(CASE WHEN nps_score <= 6 THEN 1 ELSE 0 END) AS detractors') - ->from('#__mokosuitefield_feedback') - ->where('DATE(submitted_at) BETWEEN ' . $db->quote($from) . ' AND ' . $db->quote($to))); - - $stats = $db->loadObject() ?: (object) ['total_responses' => 0, 'avg_rating' => 0, 'avg_nps' => 0, 'promoters' => 0, 'passives' => 0, 'detractors' => 0]; - - $total = (int) $stats->total_responses; - $stats->nps_score = $total > 0 - ? round(((int) $stats->promoters - (int) $stats->detractors) / $total * 100) - : 0; - - return $stats; - } -} diff --git a/source/packages/plg_system_mokosuitefield/src/Helper/CustomerSatisfactionHelper.php b/source/packages/plg_system_mokosuitefield/src/Helper/CustomerSatisfactionHelper.php deleted file mode 100644 index 80bf6f9..0000000 --- a/source/packages/plg_system_mokosuitefield/src/Helper/CustomerSatisfactionHelper.php +++ /dev/null @@ -1,118 +0,0 @@ - 5) { - throw new \InvalidArgumentException('Rating must be 1-5.'); - } - - $db = Factory::getContainer()->get(DatabaseInterface::class); - - // Prevent duplicate surveys per work order - $db->setQuery($db->getQuery(true) - ->select('id') - ->from('#__mokosuitefield_surveys') - ->where('work_order_id = ' . (int) $workOrderId) - ->where('contact_id = ' . (int) $contactId)); - - if ($db->loadResult()) { - return (object) ['success' => false, 'error' => 'Survey already submitted for this work order']; - } - - $filter = \Joomla\Filter\InputFilter::getInstance(); - - $survey = (object) [ - 'work_order_id' => $workOrderId, - 'contact_id' => $contactId, - 'rating' => $rating, - 'comment' => $comment !== null ? $filter->clean($comment, 'STRING') : null, - 'nps_score' => $rating >= 4 ? 'promoter' : ($rating >= 3 ? 'passive' : 'detractor'), - 'created_at' => Factory::getDate()->toSql(), - ]; - - $db->insertObject('#__mokosuitefield_surveys', $survey, 'id'); - - return (object) ['success' => true, 'survey_id' => (int) $survey->id]; - } - - /** - * Get NPS (Net Promoter Score) for a period. - */ - public static function getNps(string $from = '', string $to = ''): object - { - $from = $from ?: date('Y-01-01'); - $to = $to ?: date('Y-m-d'); - - if (!\DateTime::createFromFormat('Y-m-d', $from) || !\DateTime::createFromFormat('Y-m-d', $to)) { - throw new \InvalidArgumentException('Date parameters must be Y-m-d format.'); - } - - $db = Factory::getContainer()->get(DatabaseInterface::class); - - $db->setQuery($db->getQuery(true) - ->select('COUNT(*) AS total_responses') - ->select('SUM(CASE WHEN rating >= 4 THEN 1 ELSE 0 END) AS promoters') - ->select('SUM(CASE WHEN rating = 3 THEN 1 ELSE 0 END) AS passives') - ->select('SUM(CASE WHEN rating <= 2 THEN 1 ELSE 0 END) AS detractors') - ->select('AVG(rating) AS avg_rating') - ->from('#__mokosuitefield_surveys') - ->where('DATE(created_at) BETWEEN ' . $db->quote($from) . ' AND ' . $db->quote($to))); - - $stats = $db->loadObject(); - $total = (int) ($stats->total_responses ?? 0); - $promoterPct = $total > 0 ? (int) $stats->promoters / $total * 100 : 0; - $detractorPct = $total > 0 ? (int) $stats->detractors / $total * 100 : 0; - - return (object) [ - 'nps' => round($promoterPct - $detractorPct), - 'total_responses' => $total, - 'promoters' => (int) ($stats->promoters ?? 0), - 'passives' => (int) ($stats->passives ?? 0), - 'detractors' => (int) ($stats->detractors ?? 0), - 'avg_rating' => round((float) ($stats->avg_rating ?? 0), 1), - ]; - } - - /** - * Get technician satisfaction rankings. - */ - public static function getTechnicianRankings(int $limit = 20): array - { - $db = Factory::getContainer()->get(DatabaseInterface::class); - - $db->setQuery($db->getQuery(true) - ->select('t.id AS tech_id, cd.name AS tech_name') - ->select('COUNT(s.id) AS survey_count') - ->select('AVG(s.rating) AS avg_rating') - ->select('SUM(CASE WHEN s.rating >= 4 THEN 1 ELSE 0 END) AS five_star_count') - ->from($db->quoteName('#__mokosuitefield_surveys', 's')) - ->join('INNER', $db->quoteName('#__mokosuitefield_work_orders', 'wo') . ' ON wo.id = s.work_order_id') - ->join('INNER', $db->quoteName('#__mokosuitefield_technicians', 't') . ' ON t.id = wo.technician_id') - ->join('LEFT', $db->quoteName('#__contact_details', 'cd') . ' ON cd.id = t.contact_id') - ->group('t.id, cd.name') - ->having('COUNT(s.id) >= 3') - ->order('avg_rating DESC'), 0, min(max(1, $limit), 100)); - - $results = $db->loadObjectList() ?: []; - - foreach ($results as &$r) { - $r->avg_rating = round((float) $r->avg_rating, 1); - } - - return $results; - } -} diff --git a/source/packages/plg_system_mokosuitefield/src/Helper/DispatchHelper.php b/source/packages/plg_system_mokosuitefield/src/Helper/DispatchHelper.php deleted file mode 100644 index 487b566..0000000 --- a/source/packages/plg_system_mokosuitefield/src/Helper/DispatchHelper.php +++ /dev/null @@ -1,144 +0,0 @@ -get(DatabaseInterface::class); - - $query = $db->getQuery(true) - ->select('t.*, cd.name AS tech_name, cd.telephone') - ->select('(SELECT COUNT(*) FROM #__mokosuitefield_work_orders wo WHERE wo.technician_id = t.id AND wo.status IN (' . $db->quote('dispatched') . ',' . $db->quote('en_route') . ',' . $db->quote('on_site') . ',' . $db->quote('in_progress') . ') AND wo.scheduled_date = CURDATE()) AS today_jobs') - ->from($db->quoteName('#__mokosuitefield_technicians', 't')) - ->join('LEFT', $db->quoteName('#__contact_details', 'cd') . ' ON cd.id = t.contact_id') - ->where($db->quoteName('t.status') . ' = ' . $db->quote('available')) - ->where('(' . $db->quoteName('t.trade') . ' = ' . $db->quote($trade) . ' OR ' . $db->quoteName('t.trade') . ' = ' . $db->quote('multi_trade') . ')') - ->having('today_jobs < t.max_daily_jobs') - ->order('today_jobs ASC'); - - $db->setQuery($query); - $techs = $db->loadObjectList() ?: []; - - if (empty($techs)) return null; - - // If skills required, filter - if ($requiredSkills) { - $techs = array_filter($techs, function ($t) use ($requiredSkills) { - $techSkills = json_decode($t->skills ?? '[]', true) ?: []; - foreach ($requiredSkills as $skill) { - if (!in_array($skill, $techSkills)) return false; - } - return true; - }); - } - - return !empty($techs) ? reset($techs) : null; - } - - /** - * Dispatch a work order to a technician. - */ - public static function dispatch(int $workOrderId, int $technicianId): bool - { - $db = Factory::getContainer()->get(DatabaseInterface::class); - $now = Factory::getDate()->toSql(); - - $db->updateObject('#__mokosuitefield_work_orders', (object) [ - 'id' => $workOrderId, - 'technician_id' => $technicianId, - 'status' => 'dispatched', - 'modified' => $now, - ], 'id'); - - $db->updateObject('#__mokosuitefield_technicians', (object) [ - 'id' => $technicianId, - 'status' => 'dispatched', - ], 'id'); - - // Log dispatch - $db->insertObject('#__mokosuitefield_dispatch_log', (object) [ - 'work_order_id' => $workOrderId, - 'technician_id' => $technicianId, - 'action' => 'assigned', - 'created_by' => Factory::getApplication()->getIdentity()->id, - 'created' => $now, - ]); - - return true; - } - - /** - * Get today's dispatch board — all jobs organized by tech. - */ - public static function getDispatchBoard(string $date = ''): array - { - $db = Factory::getContainer()->get(DatabaseInterface::class); - $date = $date ?: date('Y-m-d'); - - $db->setQuery($db->getQuery(true) - ->select('t.id AS tech_id, cd.name AS tech_name, t.status AS tech_status, t.trade') - ->select('wo.id AS wo_id, wo.wo_number, wo.status AS wo_status, wo.priority, wo.category') - ->select('wo.scheduled_time_start, wo.scheduled_time_end') - ->select('loc.address, loc.city, cust.name AS customer_name') - ->from($db->quoteName('#__mokosuitefield_technicians', 't')) - ->join('LEFT', $db->quoteName('#__contact_details', 'cd') . ' ON cd.id = t.contact_id') - ->join('LEFT', $db->quoteName('#__mokosuitefield_work_orders', 'wo') . ' ON wo.technician_id = t.id AND wo.scheduled_date = ' . $db->quote($date) . ' AND wo.status NOT IN (' . $db->quote('completed') . ',' . $db->quote('cancelled') . ')') - ->join('LEFT', $db->quoteName('#__mokosuitefield_locations', 'loc') . ' ON loc.id = wo.location_id') - ->join('LEFT', $db->quoteName('#__contact_details', 'cust') . ' ON cust.id = wo.contact_id') - ->order('t.id ASC, wo.scheduled_time_start ASC')); - - $rows = $db->loadObjectList() ?: []; - - // Group by tech - $board = []; - foreach ($rows as $row) { - $techId = (int) $row->tech_id; - if (!isset($board[$techId])) { - $board[$techId] = (object) [ - 'tech_id' => $techId, - 'tech_name' => $row->tech_name, - 'status' => $row->tech_status, - 'trade' => $row->trade, - 'jobs' => [], - ]; - } - if ($row->wo_id) { - $board[$techId]->jobs[] = $row; - } - } - - return array_values($board); - } - - /** - * Get unassigned work orders. - */ - public static function getUnassigned(): array - { - $db = Factory::getContainer()->get(DatabaseInterface::class); - - $db->setQuery($db->getQuery(true) - ->select('wo.*, cd.name AS customer_name, loc.address, loc.city, loc.zip') - ->from($db->quoteName('#__mokosuitefield_work_orders', 'wo')) - ->join('LEFT', $db->quoteName('#__contact_details', 'cd') . ' ON cd.id = wo.contact_id') - ->join('LEFT', $db->quoteName('#__mokosuitefield_locations', 'loc') . ' ON loc.id = wo.location_id') - ->where($db->quoteName('wo.technician_id') . ' IS NULL') - ->where($db->quoteName('wo.status') . ' = ' . $db->quote('new')) - ->order('FIELD(wo.priority, ' . $db->quote('emergency') . ',' . $db->quote('urgent') . ',' . $db->quote('high') . ',' . $db->quote('normal') . ',' . $db->quote('low') . ',' . $db->quote('scheduled') . ') ASC, wo.created ASC')); - - return $db->loadObjectList() ?: []; - } -} diff --git a/source/packages/plg_system_mokosuitefield/src/Helper/EquipmentHelper.php b/source/packages/plg_system_mokosuitefield/src/Helper/EquipmentHelper.php deleted file mode 100644 index 1d56d11..0000000 --- a/source/packages/plg_system_mokosuitefield/src/Helper/EquipmentHelper.php +++ /dev/null @@ -1,77 +0,0 @@ -get(DatabaseInterface::class); - - $db->setQuery($db->getQuery(true) - ->select('*') - ->from('#__mokosuitefield_equipment') - ->where('location_id = ' . $locationId) - ->order('equipment_type ASC, make ASC')); - - return $db->loadObjectList() ?: []; - } - - public static function getByQrCode(string $qrCode): ?object - { - $db = Factory::getContainer()->get(DatabaseInterface::class); - - $db->setQuery($db->getQuery(true)->select('e.*, loc.address, loc.city, cd.name AS owner_name') - ->from($db->quoteName('#__mokosuitefield_equipment', 'e')) - ->join('LEFT', $db->quoteName('#__mokosuitefield_locations', 'loc') . ' ON loc.id = e.location_id') - ->join('LEFT', $db->quoteName('#__contact_details', 'cd') . ' ON cd.id = e.contact_id') - ->where($db->quoteName('e.qr_code') . ' = ' . $db->quote($qrCode))); - - return $db->loadObject() ?: null; - } - - public static function getDueForService(int $daysAhead = 30): array - { - $db = Factory::getContainer()->get(DatabaseInterface::class); - - $db->setQuery($db->getQuery(true) - ->select('e.*, loc.address, loc.city, cd.name AS owner_name') - ->from($db->quoteName('#__mokosuitefield_equipment', 'e')) - ->join('LEFT', $db->quoteName('#__mokosuitefield_locations', 'loc') . ' ON loc.id = e.location_id') - ->join('LEFT', $db->quoteName('#__contact_details', 'cd') . ' ON cd.id = e.contact_id') - ->where($db->quoteName('e.next_service_date') . ' BETWEEN CURDATE() AND DATE_ADD(CURDATE(), INTERVAL ' . $daysAhead . ' DAY)') - ->order('e.next_service_date ASC')); - - return $db->loadObjectList() ?: []; - } - - public static function getWarrantyExpiring(int $daysAhead = 90): array - { - $db = Factory::getContainer()->get(DatabaseInterface::class); - - $db->setQuery($db->getQuery(true) - ->select('e.*, loc.address, cd.name AS owner_name') - ->from($db->quoteName('#__mokosuitefield_equipment', 'e')) - ->join('LEFT', $db->quoteName('#__mokosuitefield_locations', 'loc') . ' ON loc.id = e.location_id') - ->join('LEFT', $db->quoteName('#__contact_details', 'cd') . ' ON cd.id = e.contact_id') - ->where($db->quoteName('e.warranty_expiry') . ' BETWEEN CURDATE() AND DATE_ADD(CURDATE(), INTERVAL ' . $daysAhead . ' DAY)') - ->order('e.warranty_expiry ASC')); - - return $db->loadObjectList() ?: []; - } - - public static function recordService(int $equipmentId): void - { - $db = Factory::getContainer()->get(DatabaseInterface::class); - $db->updateObject('#__mokosuitefield_equipment', (object) [ - 'id' => $equipmentId, 'last_service_date' => date('Y-m-d'), 'modified' => Factory::getDate()->toSql(), - ], 'id'); - } -} diff --git a/source/packages/plg_system_mokosuitefield/src/Helper/EstimateHelper.php b/source/packages/plg_system_mokosuitefield/src/Helper/EstimateHelper.php deleted file mode 100644 index 3ef628c..0000000 --- a/source/packages/plg_system_mokosuitefield/src/Helper/EstimateHelper.php +++ /dev/null @@ -1,83 +0,0 @@ -get(DatabaseInterface::class); - $now = Factory::getDate()->toSql(); - $seq = (int) $db->setQuery($db->getQuery(true)->select('COUNT(*)')->from('#__mokosuitefield_estimates'))->loadResult() + 1; - - $estimate = (object) [ - 'estimate_number' => 'EST-' . date('Ymd') . '-' . str_pad($seq, 4, '0', STR_PAD_LEFT), - 'contact_id' => $contactId, - 'location_id' => (int) ($data['location_id'] ?? 0) ?: null, - 'work_order_id' => (int) ($data['work_order_id'] ?? 0) ?: null, - 'title' => $title, - 'description' => $data['description'] ?? '', - 'total' => (float) ($data['total'] ?? 0), - 'status' => 'draft', - 'valid_days' => (int) ($data['valid_days'] ?? 30), - 'token' => bin2hex(random_bytes(32)), - 'created_by' => Factory::getApplication()->getIdentity()->id, - 'created' => $now, - ]; - - $db->insertObject('#__mokosuitefield_estimates', $estimate, 'id'); - return (int) $estimate->id; - } - - public static function send(int $estimateId): bool - { - $db = Factory::getContainer()->get(DatabaseInterface::class); - $db->setQuery($db->getQuery(true) - ->select('e.*, cd.name AS customer_name, cd.email_to') - ->from($db->quoteName('#__mokosuitefield_estimates', 'e')) - ->join('LEFT', $db->quoteName('#__contact_details', 'cd') . ' ON cd.id = e.contact_id') - ->where('e.id = ' . $estimateId)); - $est = $db->loadObject(); - - if (!$est || !$est->email_to) return false; - - $url = Uri::root() . 'index.php?option=com_mokosuitefield&view=estimate&token=' . $est->token; - $mailer = Factory::getMailer(); - $mailer->addRecipient($est->email_to, $est->customer_name); - $mailer->setSubject('Estimate ' . $est->estimate_number); - $mailer->setBody("Estimate ready for review:\n{$url}\n\nTotal: \$" . number_format((float) $est->total, 2)); - $result = $mailer->Send(); - - if ($result) { - $db->updateObject('#__mokosuitefield_estimates', (object) ['id' => $estimateId, 'status' => 'sent'], 'id'); - } - return (bool) $result; - } - - public static function accept(string $token, ?string $signature = null): ?int - { - $db = Factory::getContainer()->get(DatabaseInterface::class); - $db->setQuery($db->getQuery(true)->select('*')->from('#__mokosuitefield_estimates') - ->where($db->quoteName('token') . ' = ' . $db->quote($token)) - ->where($db->quoteName('status') . ' IN (' . $db->quote('sent') . ',' . $db->quote('viewed') . ')')); - $est = $db->loadObject(); - if (!$est) return null; - - $db->updateObject('#__mokosuitefield_estimates', (object) [ - 'id' => $est->id, 'status' => 'accepted', 'accepted_at' => Factory::getDate()->toSql(), - 'customer_signature' => $signature, - ], 'id'); - - $woId = WorkOrderHelper::create((int) $est->contact_id, 'general', $est->title, [ - 'location_id' => $est->location_id, 'source' => 'website', - ]); - - $db->updateObject('#__mokosuitefield_estimates', (object) ['id' => $est->id, 'status' => 'converted', 'converted_wo_id' => $woId], 'id'); - return $woId; - } -} diff --git a/source/packages/plg_system_mokosuitefield/src/Helper/GpsTrackingHelper.php b/source/packages/plg_system_mokosuitefield/src/Helper/GpsTrackingHelper.php deleted file mode 100644 index 99c8cf8..0000000 --- a/source/packages/plg_system_mokosuitefield/src/Helper/GpsTrackingHelper.php +++ /dev/null @@ -1,112 +0,0 @@ - 90 || $longitude < -180 || $longitude > 180) { - throw new \InvalidArgumentException('Invalid GPS coordinates.'); - } - - $db = Factory::getContainer()->get(DatabaseInterface::class); - - $ping = (object) [ - 'vehicle_id' => $vehicleId, - 'latitude' => round($latitude, 6), - 'longitude' => round($longitude, 6), - 'speed_mph' => max(0, round($speed, 1)), - 'recorded_at'=> Factory::getDate()->toSql(), - ]; - - $db->insertObject('#__mokosuitefield_gps_pings', $ping); - - return true; - } - - /** - * Get latest position for all active vehicles. - */ - public static function getFleetPositions(): array - { - $db = Factory::getContainer()->get(DatabaseInterface::class); - - $db->setQuery($db->getQuery(true) - ->select('v.id AS vehicle_id, v.name AS vehicle_name, v.license_plate') - ->select('gp.latitude, gp.longitude, gp.speed_mph, gp.recorded_at') - ->select('cd.name AS assigned_tech') - ->from($db->quoteName('#__mokosuitefield_vehicles', 'v')) - ->join('LEFT', '(SELECT g1.* FROM #__mokosuitefield_gps_pings g1' - . ' INNER JOIN (SELECT vehicle_id, MAX(recorded_at) AS max_at' - . ' FROM #__mokosuitefield_gps_pings GROUP BY vehicle_id) g2' - . ' ON g1.vehicle_id = g2.vehicle_id AND g1.recorded_at = g2.max_at) AS gp' - . ' ON gp.vehicle_id = v.id') - ->join('LEFT', $db->quoteName('#__mokosuitefield_technicians', 't') . ' ON t.vehicle_id = v.id') - ->join('LEFT', $db->quoteName('#__contact_details', 'cd') . ' ON cd.id = t.contact_id') - ->where($db->quoteName('v.status') . ' = ' . $db->quote('active')) - ->order('v.name ASC')); - - return $db->loadObjectList() ?: []; - } - - /** - * Get drive history for a vehicle on a specific date. - */ - public static function getDriveHistory(int $vehicleId, string $date = ''): array - { - $date = $date ?: date('Y-m-d'); - - if (!\DateTime::createFromFormat('Y-m-d', $date)) { - throw new \InvalidArgumentException('Date must be Y-m-d format.'); - } - - $db = Factory::getContainer()->get(DatabaseInterface::class); - - $db->setQuery($db->getQuery(true) - ->select('gp.latitude, gp.longitude, gp.speed_mph, gp.recorded_at') - ->from($db->quoteName('#__mokosuitefield_gps_pings', 'gp')) - ->where('gp.vehicle_id = ' . (int) $vehicleId) - ->where('DATE(gp.recorded_at) = ' . $db->quote($date)) - ->order('gp.recorded_at ASC')); - - return $db->loadObjectList() ?: []; - } - - /** - * Get vehicles currently exceeding speed threshold. - */ - public static function getSpeeding(float $thresholdMph = 70): array - { - $db = Factory::getContainer()->get(DatabaseInterface::class); - - $db->setQuery($db->getQuery(true) - ->select('v.id AS vehicle_id, v.name AS vehicle_name, v.license_plate') - ->select('gp.latitude, gp.longitude, gp.speed_mph, gp.recorded_at') - ->select('cd.name AS assigned_tech') - ->from($db->quoteName('#__mokosuitefield_vehicles', 'v')) - ->join('INNER', '(SELECT g1.* FROM #__mokosuitefield_gps_pings g1' - . ' INNER JOIN (SELECT vehicle_id, MAX(recorded_at) AS max_at' - . ' FROM #__mokosuitefield_gps_pings GROUP BY vehicle_id) g2' - . ' ON g1.vehicle_id = g2.vehicle_id AND g1.recorded_at = g2.max_at) AS gp' - . ' ON gp.vehicle_id = v.id') - ->join('LEFT', $db->quoteName('#__mokosuitefield_technicians', 't') . ' ON t.vehicle_id = v.id') - ->join('LEFT', $db->quoteName('#__contact_details', 'cd') . ' ON cd.id = t.contact_id') - ->where($db->quoteName('v.status') . ' = ' . $db->quote('active')) - ->where('gp.speed_mph > ' . (float) $thresholdMph) - ->where('gp.recorded_at > DATE_SUB(NOW(), INTERVAL 10 MINUTE)') - ->order('gp.speed_mph DESC')); - - return $db->loadObjectList() ?: []; - } -} diff --git a/source/packages/plg_system_mokosuitefield/src/Helper/InvoiceHelper.php b/source/packages/plg_system_mokosuitefield/src/Helper/InvoiceHelper.php deleted file mode 100644 index 83e51b7..0000000 --- a/source/packages/plg_system_mokosuitefield/src/Helper/InvoiceHelper.php +++ /dev/null @@ -1,151 +0,0 @@ -get(DatabaseInterface::class); - $now = Factory::getDate()->toSql(); - $woId = (int) $woId; - - // Load work order - $db->setQuery($db->getQuery(true) - ->select('wo.*, cd.name AS customer_name') - ->from($db->quoteName('#__mokosuitefield_work_orders', 'wo')) - ->join('LEFT', $db->quoteName('#__contact_details', 'cd') . ' ON cd.id = wo.contact_id') - ->where('wo.id = ' . $woId)); - $wo = $db->loadObject(); - - if (!$wo) throw new \RuntimeException('Work order not found: ' . $woId); - - $db->transactionStart(); - - $params = Factory::getApplication()->getParams('com_mokosuitefield'); - $laborRate = (float) $params->get('default_labor_rate', 85.00); - - // Get time entries - $db->setQuery($db->getQuery(true) - ->select('*') - ->from('#__mokosuitefield_time_entries') - ->where('wo_id = ' . $woId)); - $timeEntries = $db->loadObjectList() ?: []; - - $totalHours = 0; - foreach ($timeEntries as $te) { - $totalHours += (float) $te->hours; - } - - // Get parts used - $db->setQuery($db->getQuery(true) - ->select('wi.*, p.title AS part_name, p.price') - ->from($db->quoteName('#__mokosuitefield_wo_items', 'wi')) - ->join('LEFT', $db->quoteName('#__mokosuite_crm_products', 'p') . ' ON p.id = wi.product_id') - ->where('wi.wo_id = ' . $woId)); - $parts = $db->loadObjectList() ?: []; - - $partsTotal = 0; - foreach ($parts as $part) { - $partsTotal += (float) ($part->price ?? 0) * (int) $part->quantity; - } - - $laborTotal = round($totalHours * $laborRate, 2); - $subtotal = $laborTotal + $partsTotal; - $taxRate = (float) $params->get('default_tax_rate', 0.07); - $tax = round($subtotal * $taxRate, 2); - $total = $subtotal + $tax; - - // Create CRM invoice - $seq = (int) $db->setQuery($db->getQuery(true)->select('COUNT(*)')->from('#__mokosuite_crm_invoices'))->loadResult() + 1; - - $invoice = (object) [ - 'contact_id' => $wo->contact_id, - 'invoice_number' => 'FSI-' . date('Ymd') . '-' . str_pad($seq, 4, '0', STR_PAD_LEFT), - 'type' => 'standard', - 'subtotal' => $subtotal, - 'tax' => $tax, - 'total' => $total, - 'balance_due' => $total, - 'status' => 'draft', - 'due_date' => date('Y-m-d', strtotime('+30 days')), - 'notes' => 'Field Service Invoice for WO #' . ($wo->wo_number ?? $woId), - 'created' => $now, - 'created_by' => Factory::getApplication()->getIdentity()->id, - ]; - $db->insertObject('#__mokosuite_crm_invoices', $invoice, 'id'); - $invoiceId = (int) $invoice->id; - - // Add labor line item - if ($totalHours > 0) { - $db->insertObject('#__mokosuite_crm_invoice_items', (object) [ - 'invoice_id' => $invoiceId, - 'description' => 'Labor — ' . number_format($totalHours, 1) . ' hours @ $' . number_format($laborRate, 2) . '/hr', - 'quantity' => $totalHours, - 'unit_price' => $laborRate, - 'line_total' => $laborTotal, - ]); - } - - // Add parts line items - foreach ($parts as $part) { - $db->insertObject('#__mokosuite_crm_invoice_items', (object) [ - 'invoice_id' => $invoiceId, - 'product_id' => $part->product_id, - 'description' => $part->part_name ?? 'Part', - 'quantity' => $part->quantity, - 'unit_price' => $part->price ?? 0, - 'line_total' => round((float) ($part->price ?? 0) * (int) $part->quantity, 2), - ]); - } - - // Link invoice to work order - $db->setQuery($db->getQuery(true) - ->update('#__mokosuitefield_work_orders') - ->set('invoice_id = ' . $invoiceId) - ->where('id = ' . $woId)); - $db->execute(); - - $db->transactionCommit(); - - return $invoiceId; - } - - /** - * Batch generate invoices for all completed, uninvoiced work orders. - */ - public static function batchGenerate(): array - { - $db = Factory::getContainer()->get(DatabaseInterface::class); - - $db->setQuery($db->getQuery(true) - ->select('id') - ->from('#__mokosuitefield_work_orders') - ->where($db->quoteName('status') . ' = ' . $db->quote('completed')) - ->where('invoice_id IS NULL OR invoice_id = 0') - ->order('completed_at ASC')); - $woIds = $db->loadColumn() ?: []; - - $results = []; - foreach ($woIds as $woId) { - try { - $invoiceId = self::generateFromWorkOrder((int) $woId); - $results[] = ['wo_id' => $woId, 'invoice_id' => $invoiceId, 'success' => true]; - } catch (\Throwable $e) { - $results[] = ['wo_id' => $woId, 'error' => $e->getMessage(), 'success' => false]; - } - } - - return $results; - } -} diff --git a/source/packages/plg_system_mokosuitefield/src/Helper/PartsHelper.php b/source/packages/plg_system_mokosuitefield/src/Helper/PartsHelper.php deleted file mode 100644 index 38532a0..0000000 --- a/source/packages/plg_system_mokosuitefield/src/Helper/PartsHelper.php +++ /dev/null @@ -1,107 +0,0 @@ -get(DatabaseInterface::class); - - $query = $db->getQuery(true) - ->select('p.id, p.title AS name, p.sku, p.price, p.cost_price, p.stock_quantity') - ->select('COUNT(wi.id) AS usage_count') - ->from($db->quoteName('#__mokosuite_crm_products', 'p')) - ->join('LEFT', $db->quoteName('#__mokosuitefield_wo_items', 'wi') . ' ON wi.product_id = p.id') - ->where($db->quoteName('p.published') . ' = 1') - ->group('p.id') - ->order('usage_count DESC'); - - if ($trade) { - $query->join('LEFT', $db->quoteName('#__mokosuitefield_work_orders', 'wo') . ' ON wo.id = wi.wo_id') - ->where($db->quoteName('wo.trade') . ' = ' . $db->quote($trade)); - } - - $db->setQuery($query, 0, $limit); - return $db->loadObjectList() ?: []; - } - - /** - * Record part usage on a work order. - */ - public static function usePart(int $woId, int $productId, int $quantity, ?float $unitPrice = null): int - { - $db = Factory::getContainer()->get(DatabaseInterface::class); - - if ($unitPrice === null) { - $db->setQuery($db->getQuery(true)->select('price')->from('#__mokosuite_crm_products')->where('id = ' . (int) $productId)); - $unitPrice = (float) $db->loadResult(); - } - - $item = (object) [ - 'wo_id' => $woId, - 'product_id' => $productId, - 'quantity' => $quantity, - 'unit_price' => $unitPrice, - 'line_total' => round($unitPrice * $quantity, 2), - 'created' => Factory::getDate()->toSql(), - ]; - - $db->insertObject('#__mokosuitefield_wo_items', $item, 'id'); - - // Deduct from stock - $db->setQuery($db->getQuery(true) - ->update('#__mokosuite_crm_products') - ->set('stock_quantity = stock_quantity - ' . (int) $quantity) - ->where('id = ' . (int) $productId)); - $db->execute(); - - return (int) $item->id; - } - - /** - * Get parts that are low on stock and frequently used in field service. - */ - public static function getLowStockParts(int $limit = 20): array - { - $db = Factory::getContainer()->get(DatabaseInterface::class); - - $db->setQuery($db->getQuery(true) - ->select('p.id, p.title AS name, p.sku, p.stock_quantity, p.reorder_point, p.cost_price') - ->select('(SELECT COUNT(*) FROM #__mokosuitefield_wo_items wi WHERE wi.product_id = p.id) AS field_usage') - ->from($db->quoteName('#__mokosuite_crm_products', 'p')) - ->where($db->quoteName('p.stock_quantity') . ' <= ' . $db->quoteName('p.reorder_point')) - ->where($db->quoteName('p.reorder_point') . ' > 0') - ->where('p.id IN (SELECT DISTINCT product_id FROM #__mokosuitefield_wo_items)') - ->order('(p.reorder_point - p.stock_quantity) DESC'), 0, $limit); - - return $db->loadObjectList() ?: []; - } - - /** - * Get parts cost summary for a work order. - */ - public static function getWoPartsCost(int $woId): object - { - $db = Factory::getContainer()->get(DatabaseInterface::class); - - $db->setQuery($db->getQuery(true) - ->select('COUNT(*) AS item_count') - ->select('SUM(quantity) AS total_qty') - ->select('COALESCE(SUM(line_total), 0) AS total_cost') - ->from('#__mokosuitefield_wo_items') - ->where('wo_id = ' . (int) $woId)); - - return $db->loadObject() ?: (object) ['item_count' => 0, 'total_qty' => 0, 'total_cost' => 0]; - } -} diff --git a/source/packages/plg_system_mokosuitefield/src/Helper/RouteHelper.php b/source/packages/plg_system_mokosuitefield/src/Helper/RouteHelper.php deleted file mode 100644 index 701348c..0000000 --- a/source/packages/plg_system_mokosuitefield/src/Helper/RouteHelper.php +++ /dev/null @@ -1,239 +0,0 @@ -get(DatabaseInterface::class); - $date = $date ?: date('Y-m-d'); - - $db->setQuery($db->getQuery(true) - ->select('wo.id, wo.wo_number, wo.priority, wo.status, wo.trade') - ->select('wo.scheduled_date, wo.scheduled_time, wo.estimated_duration') - ->select('wo.route_order') - ->select('l.name AS location_name, l.address, l.city, l.state, l.zip') - ->select('l.latitude, l.longitude') - ->select('cd.name AS customer_name, cd.telephone AS customer_phone') - ->from($db->quoteName('#__mokosuitefield_work_orders', 'wo')) - ->join('LEFT', $db->quoteName('#__mokosuitefield_locations', 'l') . ' ON l.id = wo.location_id') - ->join('LEFT', $db->quoteName('#__contact_details', 'cd') . ' ON cd.id = wo.contact_id') - ->where($db->quoteName('wo.technician_id') . ' = ' . (int) $techId) - ->where($db->quoteName('wo.scheduled_date') . ' = ' . $db->quote($date)) - ->where($db->quoteName('wo.status') . ' NOT IN (' . $db->quote('cancelled') . ',' . $db->quote('completed') . ')') - ->order('wo.route_order ASC, wo.scheduled_time ASC')); - - return $db->loadObjectList() ?: []; - } - - /** - * Auto-assign route order based on geographic proximity (nearest-neighbor heuristic). - * Starts from the tech's home base or first WO location. - */ - public static function optimizeRoute(int $techId, string $date = ''): array - { - $stops = self::getTechRoute($techId, $date); - if (count($stops) <= 1) return $stops; - - // Get tech home location - $db = Factory::getContainer()->get(DatabaseInterface::class); - $db->setQuery($db->getQuery(true) - ->select('home_latitude, home_longitude') - ->from('#__mokosuitefield_technicians') - ->where('id = ' . (int) $techId)); - $tech = $db->loadObject(); - - $currentLat = (float) ($tech->home_latitude ?? 0); - $currentLng = (float) ($tech->home_longitude ?? 0); - - // Nearest-neighbor sort - $ordered = []; - $remaining = $stops; - - while (!empty($remaining)) { - $bestIdx = 0; - $bestDist = PHP_FLOAT_MAX; - - foreach ($remaining as $idx => $stop) { - $lat = (float) ($stop->latitude ?? 0); - $lng = (float) ($stop->longitude ?? 0); - - if ($lat === 0.0 && $lng === 0.0) { - // No coordinates — keep in original position - $dist = PHP_FLOAT_MAX - 1; - } else { - $dist = self::haversine($currentLat, $currentLng, $lat, $lng); - } - - if ($dist < $bestDist) { - $bestDist = $dist; - $bestIdx = $idx; - } - } - - $next = $remaining[$bestIdx]; - $ordered[] = $next; - $currentLat = (float) ($next->latitude ?? $currentLat); - $currentLng = (float) ($next->longitude ?? $currentLng); - array_splice($remaining, $bestIdx, 1); - $remaining = array_values($remaining); - } - - // Save route order - foreach ($ordered as $i => $stop) { - $update = (object) [ - 'id' => $stop->id, - 'route_order' => $i + 1, - ]; - $db->updateObject('#__mokosuitefield_work_orders', $update, 'id'); - $ordered[$i]->route_order = $i + 1; - } - - return $ordered; - } - - /** - * Manually reorder a stop within a tech's route. - */ - public static function reorderStop(int $woId, int $newPosition): bool - { - $db = Factory::getContainer()->get(DatabaseInterface::class); - - $db->setQuery($db->getQuery(true) - ->select('technician_id, scheduled_date') - ->from('#__mokosuitefield_work_orders') - ->where('id = ' . (int) $woId)); - $wo = $db->loadObject(); - if (!$wo) return false; - - $stops = self::getTechRoute((int) $wo->technician_id, $wo->scheduled_date); - - // Find and remove the target WO - $target = null; - $filtered = []; - foreach ($stops as $stop) { - if ((int) $stop->id === $woId) { - $target = $stop; - } else { - $filtered[] = $stop; - } - } - - if (!$target) return false; - - // Insert at new position - $newPosition = max(1, min($newPosition, count($filtered) + 1)); - array_splice($filtered, $newPosition - 1, 0, [$target]); - - // Save new order - foreach ($filtered as $i => $stop) { - $update = (object) [ - 'id' => $stop->id, - 'route_order' => $i + 1, - ]; - $db->updateObject('#__mokosuitefield_work_orders', $update, 'id'); - } - - return true; - } - - /** - * Estimate total drive time and distance for a route (using straight-line approximation). - */ - public static function estimateRouteMetrics(int $techId, string $date = ''): object - { - $stops = self::getTechRoute($techId, $date); - - $totalDistance = 0.0; - $totalJobTime = 0; - $legs = []; - - $db = Factory::getContainer()->get(DatabaseInterface::class); - $db->setQuery($db->getQuery(true) - ->select('home_latitude, home_longitude') - ->from('#__mokosuitefield_technicians') - ->where('id = ' . (int) $techId)); - $tech = $db->loadObject(); - - $prevLat = (float) ($tech->home_latitude ?? 0); - $prevLng = (float) ($tech->home_longitude ?? 0); - - foreach ($stops as $stop) { - $lat = (float) ($stop->latitude ?? 0); - $lng = (float) ($stop->longitude ?? 0); - $dist = 0; - - if ($lat && $lng && ($prevLat || $prevLng)) { - $dist = self::haversine($prevLat, $prevLng, $lat, $lng); - } - - $totalDistance += $dist; - $totalJobTime += (int) ($stop->estimated_duration ?? 60); - - $legs[] = (object) [ - 'wo_id' => $stop->id, - 'location' => $stop->location_name ?? $stop->address, - 'distance' => round($dist, 1), - 'drive_min'=> round($dist / self::AVG_SPEED_MPH * 60, 0), - ]; - - $prevLat = $lat ?: $prevLat; - $prevLng = $lng ?: $prevLng; - } - - $totalDriveMin = $totalDistance > 0 ? round($totalDistance / self::AVG_SPEED_MPH * 60) : 0; - - return (object) [ - 'stop_count' => count($stops), - 'total_distance' => round($totalDistance, 1), - 'total_drive_min'=> $totalDriveMin, - 'total_job_min' => $totalJobTime, - 'total_day_min' => $totalDriveMin + $totalJobTime, - 'legs' => $legs, - ]; - } - - /** - * Haversine distance in miles. - */ - private static function haversine(float $lat1, float $lon1, float $lat2, float $lon2): float - { - $R = 3959; // Earth radius in miles - $dLat = deg2rad($lat2 - $lat1); - $dLon = deg2rad($lon2 - $lon1); - $a = sin($dLat / 2) ** 2 + cos(deg2rad($lat1)) * cos(deg2rad($lat2)) * sin($dLon / 2) ** 2; - $c = 2 * atan2(sqrt($a), sqrt(1 - $a)); - return $R * $c; - } - - /** - * Log a GPS breadcrumb for a tech (called from mobile app). - */ - public static function logGpsBreadcrumb(int $techId, float $lat, float $lng, ?int $woId = null): void - { - $db = Factory::getContainer()->get(DatabaseInterface::class); - - $crumb = (object) [ - 'technician_id' => $techId, - 'latitude' => $lat, - 'longitude' => $lng, - 'wo_id' => $woId, - 'recorded_at' => Factory::getDate()->toSql(), - ]; - - $db->insertObject('#__mokosuitefield_dispatch_log', $crumb); - } -} diff --git a/source/packages/plg_system_mokosuitefield/src/Helper/SafetyChecklistHelper.php b/source/packages/plg_system_mokosuitefield/src/Helper/SafetyChecklistHelper.php deleted file mode 100644 index c789fde..0000000 --- a/source/packages/plg_system_mokosuitefield/src/Helper/SafetyChecklistHelper.php +++ /dev/null @@ -1,147 +0,0 @@ -get(DatabaseInterface::class); - $now = Factory::getDate()->toSql(); - - $items = self::getDefaultItems($trade); - - $checklist = (object) [ - 'wo_id' => $woId, - 'trade' => $trade, - 'status' => 'pending', - 'created' => $now, - 'created_by' => Factory::getApplication()->getIdentity()->id, - ]; - $db->insertObject('#__mokosuitefield_safety_checklists', $checklist, 'id'); - $checklistId = (int) $checklist->id; - - foreach ($items as $i => $item) { - $db->insertObject('#__mokosuitefield_safety_checklist_items', (object) [ - 'checklist_id' => $checklistId, - 'item_text' => $item, - 'checked' => 0, - 'ordering' => $i + 1, - ]); - } - - return $checklistId; - } - - /** - * Complete a checklist item. - */ - public static function checkItem(int $itemId, bool $passed, string $notes = ''): bool - { - $db = Factory::getContainer()->get(DatabaseInterface::class); - - // Verify item belongs to a pending checklist and is not already checked - $db->setQuery($db->getQuery(true) - ->select('sci.id, sci.checked, sc.status AS checklist_status') - ->from($db->quoteName('#__mokosuitefield_safety_checklist_items', 'sci')) - ->join('INNER', $db->quoteName('#__mokosuitefield_safety_checklists', 'sc') . ' ON sc.id = sci.checklist_id') - ->where('sci.id = ' . (int) $itemId)); - $existing = $db->loadObject(); - - if (!$existing || $existing->checklist_status !== 'pending' || (int) $existing->checked === 1) { - return false; - } - - $update = (object) [ - 'id' => $itemId, - 'checked' => 1, - 'passed' => $passed ? 1 : 0, - 'notes' => $notes, - 'checked_at' => Factory::getDate()->toSql(), - 'checked_by' => Factory::getApplication()->getIdentity()->id, - ]; - - $db->updateObject('#__mokosuitefield_safety_checklist_items', $update, 'id'); - - // Auto-complete checklist if all items are checked - $db->setQuery($db->getQuery(true) - ->select('sc.id, COUNT(sci2.id) AS total, SUM(CASE WHEN sci2.checked = 1 THEN 1 ELSE 0 END) AS done') - ->from($db->quoteName('#__mokosuitefield_safety_checklist_items', 'sci2')) - ->join('INNER', $db->quoteName('#__mokosuitefield_safety_checklists', 'sc') . ' ON sc.id = sci2.checklist_id') - ->where('sci2.id = ' . (int) $itemId) - ->group('sc.id')); - $progress = $db->loadObject(); - - if ($progress && (int) $progress->done === (int) $progress->total) { - $db->setQuery($db->getQuery(true) - ->update('#__mokosuitefield_safety_checklists') - ->set($db->quoteName('status') . ' = ' . $db->quote('completed')) - ->where('id = ' . (int) $progress->id)); - $db->execute(); - } - - return true; - } - - /** - * Get checklist completion status for a work order. - */ - public static function getStatus(int $woId): ?object - { - $db = Factory::getContainer()->get(DatabaseInterface::class); - - $db->setQuery($db->getQuery(true) - ->select('sc.id, sc.status') - ->select('COUNT(sci.id) AS total_items') - ->select('SUM(CASE WHEN sci.checked = 1 THEN 1 ELSE 0 END) AS checked_items') - ->select('SUM(CASE WHEN sci.checked = 1 AND sci.passed = 0 THEN 1 ELSE 0 END) AS failed_items') - ->from($db->quoteName('#__mokosuitefield_safety_checklists', 'sc')) - ->join('LEFT', $db->quoteName('#__mokosuitefield_safety_checklist_items', 'sci') . ' ON sci.checklist_id = sc.id') - ->where('sc.wo_id = ' . (int) $woId) - ->group('sc.id') - ->order('sc.created DESC')); - - $status = $db->loadObject(); - if (!$status) return null; - - $status->complete = (int) $status->checked_items === (int) $status->total_items; - $status->all_passed = (int) $status->failed_items === 0; - $status->safe_to_proceed = $status->complete && $status->all_passed; - - return $status; - } - - /** - * Get default safety items by trade. - */ - private static function getDefaultItems(string $trade): array - { - $common = [ - 'PPE worn (gloves, safety glasses, boots)', - 'Work area inspected for hazards', - 'Tools and equipment in good condition', - 'Fire extinguisher accessible', - 'Emergency exits identified', - ]; - - $tradeSpecific = match ($trade) { - 'electrical' => ['Lockout/tagout verified', 'Voltage tester functional', 'Grounding confirmed', 'Arc flash boundaries marked'], - 'plumbing' => ['Water supply shut off', 'Gas lines identified and marked', 'Asbestos check for older buildings', 'Drain protection in place'], - 'hvac' => ['Refrigerant handling certification verified', 'Electrical isolation confirmed', 'Ductwork supports inspected', 'Ladder/scaffold secured'], - 'general' => ['Work permit obtained if required', 'Material Safety Data Sheets reviewed'], - default => [], - }; - - return array_merge($common, $tradeSpecific); - } -} diff --git a/source/packages/plg_system_mokosuitefield/src/Helper/SchedulingHelper.php b/source/packages/plg_system_mokosuitefield/src/Helper/SchedulingHelper.php deleted file mode 100644 index c62c6d4..0000000 --- a/source/packages/plg_system_mokosuitefield/src/Helper/SchedulingHelper.php +++ /dev/null @@ -1,152 +0,0 @@ -get(DatabaseInterface::class); - $params = Factory::getApplication()->getParams('com_mokosuitefield'); - - $startHour = (int) $params->get('schedule_start_hour', 8); - $endHour = (int) $params->get('schedule_end_hour', 17); - $slotInterval = (int) $params->get('slot_interval_minutes', 30); - - // Get available techs for this trade - $db->setQuery($db->getQuery(true) - ->select('t.id, cd.name AS tech_name, t.max_daily_jobs') - ->select('(SELECT COUNT(*) FROM #__mokosuitefield_work_orders wo WHERE wo.technician_id = t.id AND wo.scheduled_date = ' . $db->quote($date) . ' AND wo.status NOT IN (' . $db->quote('cancelled') . ',' . $db->quote('completed') . ')) AS booked_count') - ->from($db->quoteName('#__mokosuitefield_technicians', 't')) - ->join('LEFT', $db->quoteName('#__contact_details', 'cd') . ' ON cd.id = t.contact_id') - ->where($db->quoteName('t.status') . ' = ' . $db->quote('available')) - ->where('(' . $db->quoteName('t.trade') . ' = ' . $db->quote($trade) . ' OR ' . $db->quoteName('t.trade') . ' = ' . $db->quote('multi_trade') . ')') - ->having('booked_count < t.max_daily_jobs')); - $techs = $db->loadObjectList() ?: []; - - if (empty($techs)) return []; - - // Get existing WO times for these techs - $techIds = array_column($techs, 'id'); - $db->setQuery($db->getQuery(true) - ->select('technician_id, scheduled_time, estimated_duration') - ->from('#__mokosuitefield_work_orders') - ->where('scheduled_date = ' . $db->quote($date)) - ->where('technician_id IN (' . implode(',', array_map('intval', $techIds)) . ')') - ->where($db->quoteName('status') . ' NOT IN (' . $db->quote('cancelled') . ',' . $db->quote('completed') . ')')); - $existingWOs = $db->loadObjectList() ?: []; - - // Build blocked time ranges per tech - $blocked = []; - foreach ($existingWOs as $wo) { - $start = strtotime($wo->scheduled_time); - $end = $start + ((int) ($wo->estimated_duration ?? 60)) * 60; - $blocked[$wo->technician_id][] = [$start, $end]; - } - - // Generate slots - $slots = []; - $current = strtotime($date . ' ' . str_pad($startHour, 2, '0', STR_PAD_LEFT) . ':00:00'); - $dayEnd = strtotime($date . ' ' . str_pad($endHour, 2, '0', STR_PAD_LEFT) . ':00:00') - ($durationMin * 60); - - while ($current <= $dayEnd) { - $slotEnd = $current + ($durationMin * 60); - - // Find at least one available tech for this slot - $availableTechs = []; - foreach ($techs as $tech) { - $conflict = false; - foreach ($blocked[$tech->id] ?? [] as [$bStart, $bEnd]) { - if ($current < $bEnd && $slotEnd > $bStart) { - $conflict = true; - break; - } - } - if (!$conflict) { - $availableTechs[] = $tech->tech_name; - } - } - - if (!empty($availableTechs)) { - $slots[] = (object) [ - 'time' => date('H:i', $current), - 'display' => date('g:i A', $current), - 'available_techs' => count($availableTechs), - ]; - } - - $current += $slotInterval * 60; - } - - return $slots; - } - - /** - * Schedule a work order into a specific slot. - */ - public static function scheduleWorkOrder(int $woId, string $date, string $time, ?int $techId = null): bool - { - $db = Factory::getContainer()->get(DatabaseInterface::class); - - // Auto-assign tech if not specified - if (!$techId) { - $wo = $db->setQuery($db->getQuery(true)->select('trade')->from('#__mokosuitefield_work_orders')->where('id = ' . (int) $woId))->loadObject(); - $bestTech = DispatchHelper::findBestTech($wo->trade ?? 'general', ''); - $techId = $bestTech ? (int) $bestTech->id : null; - } - - $update = (object) [ - 'id' => $woId, - 'scheduled_date' => $date, - 'scheduled_time' => $time, - 'technician_id' => $techId, - 'status' => 'scheduled', - ]; - - return $db->updateObject('#__mokosuitefield_work_orders', $update, 'id'); - } - - /** - * Get today's schedule for all techs. - */ - public static function getTodaySchedule(): array - { - $db = Factory::getContainer()->get(DatabaseInterface::class); - - $db->setQuery($db->getQuery(true) - ->select('t.id AS tech_id, cd.name AS tech_name, t.trade') - ->from($db->quoteName('#__mokosuitefield_technicians', 't')) - ->join('LEFT', $db->quoteName('#__contact_details', 'cd') . ' ON cd.id = t.contact_id') - ->where($db->quoteName('t.status') . ' != ' . $db->quote('inactive')) - ->order('cd.name ASC')); - $techs = $db->loadObjectList() ?: []; - - foreach ($techs as &$tech) { - $db->setQuery($db->getQuery(true) - ->select('wo.id, wo.wo_number, wo.scheduled_time, wo.estimated_duration, wo.status, wo.priority') - ->select('loc.address, loc.city') - ->select('cd2.name AS customer_name') - ->from($db->quoteName('#__mokosuitefield_work_orders', 'wo')) - ->join('LEFT', $db->quoteName('#__mokosuitefield_locations', 'loc') . ' ON loc.id = wo.location_id') - ->join('LEFT', $db->quoteName('#__contact_details', 'cd2') . ' ON cd2.id = wo.contact_id') - ->where('wo.technician_id = ' . (int) $tech->tech_id) - ->where('wo.scheduled_date = CURDATE()') - ->where($db->quoteName('wo.status') . ' != ' . $db->quote('cancelled')) - ->order('wo.scheduled_time ASC')); - $tech->jobs = $db->loadObjectList() ?: []; - $tech->job_count = count($tech->jobs); - } - - return $techs; - } -} diff --git a/source/packages/plg_system_mokosuitefield/src/Helper/ServiceAgreementHelper.php b/source/packages/plg_system_mokosuitefield/src/Helper/ServiceAgreementHelper.php deleted file mode 100644 index ab2f919..0000000 --- a/source/packages/plg_system_mokosuitefield/src/Helper/ServiceAgreementHelper.php +++ /dev/null @@ -1,78 +0,0 @@ -get(DatabaseInterface::class); - - $query = $db->getQuery(true) - ->select('a.*, cd.name AS customer_name, loc.address') - ->from($db->quoteName('#__mokosuitefield_agreements', 'a')) - ->join('LEFT', $db->quoteName('#__contact_details', 'cd') . ' ON cd.id = a.contact_id') - ->join('LEFT', $db->quoteName('#__mokosuitefield_locations', 'loc') . ' ON loc.id = a.location_id') - ->where($db->quoteName('a.status') . ' = ' . $db->quote('active')) - ->order('a.end_date ASC'); - - if ($contactId) $query->where('a.contact_id = ' . $contactId); - - $db->setQuery($query); - $agreements = $db->loadObjectList() ?: []; - - foreach ($agreements as &$a) { - $a->visits_remaining = max(0, (int) $a->visits_per_year - (int) $a->visits_used); - $a->days_until_expiry = $a->end_date ? max(0, round((strtotime($a->end_date) - time()) / 86400)) : null; - } - - return $agreements; - } - - public static function getExpiring(int $daysAhead = 30): array - { - $db = Factory::getContainer()->get(DatabaseInterface::class); - - $db->setQuery($db->getQuery(true) - ->select('a.*, cd.name AS customer_name, cd.email_to') - ->from($db->quoteName('#__mokosuitefield_agreements', 'a')) - ->join('LEFT', $db->quoteName('#__contact_details', 'cd') . ' ON cd.id = a.contact_id') - ->where($db->quoteName('a.status') . ' = ' . $db->quote('active')) - ->where($db->quoteName('a.end_date') . ' BETWEEN CURDATE() AND DATE_ADD(CURDATE(), INTERVAL ' . $daysAhead . ' DAY)') - ->order('a.end_date ASC')); - - return $db->loadObjectList() ?: []; - } - - public static function getRevenueSummary(): object - { - $db = Factory::getContainer()->get(DatabaseInterface::class); - - $db->setQuery($db->getQuery(true) - ->select('COUNT(*) AS active_agreements') - ->select('COALESCE(SUM(annual_amount), 0) AS annual_recurring') - ->select('COALESCE(SUM(annual_amount / 12), 0) AS monthly_recurring') - ->from('#__mokosuitefield_agreements') - ->where($db->quoteName('status') . ' = ' . $db->quote('active'))); - - return $db->loadObject() ?: (object) ['active_agreements' => 0, 'annual_recurring' => 0, 'monthly_recurring' => 0]; - } - - public static function recordVisit(int $agreementId, int $workOrderId): void - { - $db = Factory::getContainer()->get(DatabaseInterface::class); - - $db->setQuery($db->getQuery(true) - ->update('#__mokosuitefield_agreements') - ->set('visits_used = visits_used + 1') - ->where('id = ' . $agreementId)); - $db->execute(); - } -} diff --git a/source/packages/plg_system_mokosuitefield/src/Helper/TechnicianSkillHelper.php b/source/packages/plg_system_mokosuitefield/src/Helper/TechnicianSkillHelper.php deleted file mode 100644 index bbc95d1..0000000 --- a/source/packages/plg_system_mokosuitefield/src/Helper/TechnicianSkillHelper.php +++ /dev/null @@ -1,97 +0,0 @@ -get(DatabaseInterface::class); - - $db->setQuery($db->getQuery(true) - ->select('t.id AS tech_id, cd.name AS tech_name') - ->select('GROUP_CONCAT(ts.skill_name ORDER BY ts.skill_name SEPARATOR ", ") AS skills') - ->select('COUNT(ts.id) AS skill_count') - ->select('SUM(CASE WHEN ts.certification_expires IS NOT NULL AND ts.certification_expires < NOW() THEN 1 ELSE 0 END) AS expired_certs') - ->from($db->quoteName('#__mokosuitefield_technicians', 't')) - ->join('LEFT', $db->quoteName('#__contact_details', 'cd') . ' ON cd.id = t.contact_id') - ->join('LEFT', $db->quoteName('#__mokosuitefield_tech_skills', 'ts') . ' ON ts.tech_id = t.id') - ->where($db->quoteName('t.status') . ' = ' . $db->quote('active')) - ->group('t.id') - ->order('cd.name ASC')); - - return $db->loadObjectList() ?: []; - } - - /** - * Find best technician match for a work order based on required skills. - */ - public static function findBestMatch(array $requiredSkills, string $date = ''): array - { - if (empty($requiredSkills)) { - return []; - } - - $date = $date ?: date('Y-m-d'); - if (!\DateTime::createFromFormat('Y-m-d', $date)) { - throw new \InvalidArgumentException('Date must be Y-m-d format.'); - } - - $db = Factory::getContainer()->get(DatabaseInterface::class); - - $skillPlaceholders = implode(',', array_map(fn($s) => $db->quote($s), $requiredSkills)); - - $db->setQuery($db->getQuery(true) - ->select('t.id AS tech_id, cd.name AS tech_name, t.hourly_rate') - ->select('COUNT(DISTINCT ts.skill_name) AS matching_skills') - ->select((string) count($requiredSkills) . ' AS required_skills') - ->from($db->quoteName('#__mokosuitefield_technicians', 't')) - ->join('LEFT', $db->quoteName('#__contact_details', 'cd') . ' ON cd.id = t.contact_id') - ->join('INNER', $db->quoteName('#__mokosuitefield_tech_skills', 'ts') - . ' ON ts.tech_id = t.id AND ts.skill_name IN (' . $skillPlaceholders . ')' - . ' AND (ts.certification_expires IS NULL OR ts.certification_expires > ' . $db->quote($date) . ')') - ->where($db->quoteName('t.status') . ' = ' . $db->quote('active')) - ->group('t.id') - ->order('matching_skills DESC, t.hourly_rate ASC')); - - $matches = $db->loadObjectList() ?: []; - - foreach ($matches as &$m) { - $m->match_pct = round((int) $m->matching_skills / (int) $m->required_skills * 100, 1); - } - - return $matches; - } - - /** - * Get expiring certifications within N days. - */ - public static function getExpiringCertifications(int $days = 30): array - { - $db = Factory::getContainer()->get(DatabaseInterface::class); - $cutoff = date('Y-m-d', strtotime("+{$days} days")); - - $db->setQuery($db->getQuery(true) - ->select('ts.id, ts.skill_name, ts.certification_number, ts.certification_expires') - ->select('cd.name AS tech_name, cd.email_to') - ->from($db->quoteName('#__mokosuitefield_tech_skills', 'ts')) - ->join('INNER', $db->quoteName('#__mokosuitefield_technicians', 't') . ' ON t.id = ts.tech_id') - ->join('LEFT', $db->quoteName('#__contact_details', 'cd') . ' ON cd.id = t.contact_id') - ->where($db->quoteName('t.status') . ' = ' . $db->quote('active')) - ->where('ts.certification_expires IS NOT NULL') - ->where('ts.certification_expires BETWEEN NOW() AND ' . $db->quote($cutoff)) - ->order('ts.certification_expires ASC')); - - return $db->loadObjectList() ?: []; - } -} diff --git a/source/packages/plg_system_mokosuitefield/src/Helper/TruckStockHelper.php b/source/packages/plg_system_mokosuitefield/src/Helper/TruckStockHelper.php deleted file mode 100644 index 7ed3a91..0000000 --- a/source/packages/plg_system_mokosuitefield/src/Helper/TruckStockHelper.php +++ /dev/null @@ -1,69 +0,0 @@ -get(DatabaseInterface::class); - - $db->setQuery($db->getQuery(true) - ->select('ts.*, p.name AS part_name, p.sku, p.cost_price') - ->from($db->quoteName('#__mokosuitefield_truck_stock', 'ts')) - ->join('INNER', $db->quoteName('#__mokosuite_crm_products', 'p') . ' ON p.id = ts.product_id') - ->where('ts.vehicle_id = ' . $vehicleId) - ->order('p.name ASC')); - - return $db->loadObjectList() ?: []; - } - - public static function getLowStock(): array - { - $db = Factory::getContainer()->get(DatabaseInterface::class); - - $db->setQuery($db->getQuery(true) - ->select('ts.*, p.name AS part_name, p.sku, v.vehicle_number') - ->from($db->quoteName('#__mokosuitefield_truck_stock', 'ts')) - ->join('INNER', $db->quoteName('#__mokosuite_crm_products', 'p') . ' ON p.id = ts.product_id') - ->join('INNER', $db->quoteName('#__mokosuitefield_vehicles', 'v') . ' ON v.id = ts.vehicle_id') - ->where('ts.quantity <= ts.min_quantity') - ->order('ts.quantity ASC')); - - return $db->loadObjectList() ?: []; - } - - public static function usePart(int $vehicleId, int $productId, float $qty = 1): bool - { - $db = Factory::getContainer()->get(DatabaseInterface::class); - - $db->setQuery($db->getQuery(true) - ->update('#__mokosuitefield_truck_stock') - ->set('quantity = quantity - ' . (float) $qty) - ->where('vehicle_id = ' . (int) $vehicleId) - ->where('product_id = ' . (int) $productId) - ->where('quantity >= ' . (float) $qty)); - $db->execute(); - - return $db->getAffectedRows() > 0; - } - - public static function restock(int $vehicleId, int $productId, float $qty): void - { - $db = Factory::getContainer()->get(DatabaseInterface::class); - - $db->setQuery( - 'INSERT INTO #__mokosuitefield_truck_stock (vehicle_id, product_id, quantity, last_restocked)' - . ' VALUES (' . (int) $vehicleId . ', ' . (int) $productId . ', ' . (float) $qty . ', CURDATE())' - . ' ON DUPLICATE KEY UPDATE quantity = quantity + ' . (float) $qty . ', last_restocked = CURDATE()' - ); - $db->execute(); - } -} diff --git a/source/packages/plg_system_mokosuitefield/src/Helper/VehicleHelper.php b/source/packages/plg_system_mokosuitefield/src/Helper/VehicleHelper.php deleted file mode 100644 index 0d32603..0000000 --- a/source/packages/plg_system_mokosuitefield/src/Helper/VehicleHelper.php +++ /dev/null @@ -1,44 +0,0 @@ -get(DatabaseInterface::class); - - $db->setQuery($db->getQuery(true) - ->select('v.*, cd.name AS assigned_tech_name') - ->select('(SELECT COUNT(*) FROM #__mokosuitefield_truck_stock ts WHERE ts.vehicle_id = v.id AND ts.quantity <= ts.min_quantity) AS low_stock_items') - ->from($db->quoteName('#__mokosuitefield_vehicles', 'v')) - ->join('LEFT', $db->quoteName('#__mokosuitefield_technicians', 't') . ' ON t.id = v.assigned_tech_id') - ->join('LEFT', $db->quoteName('#__contact_details', 'cd') . ' ON cd.id = t.contact_id') - ->order('v.vehicle_number ASC')); - - return $db->loadObjectList() ?: []; - } - - public static function getInspectionsDue(int $daysAhead = 30): array - { - $db = Factory::getContainer()->get(DatabaseInterface::class); - - $db->setQuery($db->getQuery(true) - ->select('v.*, cd.name AS tech_name') - ->from($db->quoteName('#__mokosuitefield_vehicles', 'v')) - ->join('LEFT', $db->quoteName('#__mokosuitefield_technicians', 't') . ' ON t.id = v.assigned_tech_id') - ->join('LEFT', $db->quoteName('#__contact_details', 'cd') . ' ON cd.id = t.contact_id') - ->where($db->quoteName('v.status') . ' = ' . $db->quote('active')) - ->where($db->quoteName('v.next_inspection') . ' BETWEEN CURDATE() AND DATE_ADD(CURDATE(), INTERVAL ' . $daysAhead . ' DAY)') - ->order('v.next_inspection ASC')); - - return $db->loadObjectList() ?: []; - } -} diff --git a/source/packages/plg_system_mokosuitefield/src/Helper/WarrantyHelper.php b/source/packages/plg_system_mokosuitefield/src/Helper/WarrantyHelper.php deleted file mode 100644 index ae56d77..0000000 --- a/source/packages/plg_system_mokosuitefield/src/Helper/WarrantyHelper.php +++ /dev/null @@ -1,102 +0,0 @@ -get(DatabaseInterface::class); - - $db->setQuery($db->getQuery(true) - ->select('e.id, e.serial_number, e.model, e.install_date') - ->select('e.warranty_start, e.warranty_end, e.warranty_provider, e.warranty_type') - ->from($db->quoteName('#__mokosuitefield_equipment', 'e')) - ->where('e.id = ' . (int) $equipmentId)); - $equipment = $db->loadObject(); - - if (!$equipment) return (object) ['found' => false]; - - $now = new \DateTime('today'); - $warrantyEnd = $equipment->warranty_end ? new \DateTime($equipment->warranty_end) : null; - - $equipment->under_warranty = $warrantyEnd && $now <= $warrantyEnd; - $equipment->days_remaining = $warrantyEnd ? max(0, (int) $now->diff($warrantyEnd)->format('%r%a')) : null; - $equipment->warranty_expired = $warrantyEnd && $now > $warrantyEnd; - - // Get claim history - $db->setQuery($db->getQuery(true) - ->select('COUNT(*) AS total_claims, COALESCE(SUM(claim_amount), 0) AS total_claimed') - ->from('#__mokosuitefield_warranty_claims') - ->where('equipment_id = ' . (int) $equipmentId)); - $claims = $db->loadObject(); - - $equipment->total_claims = (int) ($claims->total_claims ?? 0); - $equipment->total_claimed = (float) ($claims->total_claimed ?? 0); - - return $equipment; - } - - /** - * Submit a warranty claim. - */ - public static function submitClaim(int $equipmentId, int $woId, string $description, float $claimAmount): object - { - $warranty = self::checkWarranty($equipmentId); - - if (empty($warranty->id)) return (object) ['success' => false, 'error' => 'Equipment not found']; - if (!$warranty->under_warranty) return (object) ['success' => false, 'error' => 'Warranty expired']; - - $db = Factory::getContainer()->get(DatabaseInterface::class); - - // Verify work order exists and is linked to this equipment - $db->setQuery($db->getQuery(true)->select('id')->from('#__mokosuitefield_work_orders') - ->where('id = ' . (int) $woId)); - if (!(int) $db->loadResult()) { - return (object) ['success' => false, 'error' => 'Invalid work order']; - } - $now = Factory::getDate()->toSql(); - - $claim = (object) [ - 'equipment_id' => $equipmentId, - 'wo_id' => $woId, - 'description' => $description, - 'claim_amount' => $claimAmount, - 'status' => 'submitted', - 'submitted_at' => $now, - 'submitted_by' => Factory::getApplication()->getIdentity()->id, - ]; - - $db->insertObject('#__mokosuitefield_warranty_claims', $claim, 'id'); - return (object) ['success' => true, 'claim_id' => (int) $claim->id]; - } - - /** - * Get equipment with warranties expiring within N days. - */ - public static function getExpiringSoon(int $days = 90): array - { - $db = Factory::getContainer()->get(DatabaseInterface::class); - - $db->setQuery($db->getQuery(true) - ->select('e.*, l.name AS location_name, cd.name AS customer_name') - ->from($db->quoteName('#__mokosuitefield_equipment', 'e')) - ->join('LEFT', $db->quoteName('#__mokosuitefield_locations', 'l') . ' ON l.id = e.location_id') - ->join('LEFT', $db->quoteName('#__contact_details', 'cd') . ' ON cd.id = e.contact_id') - ->where($db->quoteName('e.warranty_end') . ' IS NOT NULL') - ->where($db->quoteName('e.warranty_end') . ' BETWEEN CURDATE() AND DATE_ADD(CURDATE(), INTERVAL ' . (int) $days . ' DAY)') - ->order('e.warranty_end ASC')); - - return $db->loadObjectList() ?: []; - } -} diff --git a/source/packages/plg_system_mokosuitefield/src/Helper/WorkOrderHelper.php b/source/packages/plg_system_mokosuitefield/src/Helper/WorkOrderHelper.php deleted file mode 100644 index e6270e2..0000000 --- a/source/packages/plg_system_mokosuitefield/src/Helper/WorkOrderHelper.php +++ /dev/null @@ -1,162 +0,0 @@ -get(DatabaseInterface::class); - $now = Factory::getDate()->toSql(); - - // Read prefix from component config (config.xml) - $params = Factory::getApplication()->getParams('com_mokosuitefield'); - $woPrefix = $params->get('wo_prefix', 'WO'); - $defaultTrade = $params->get('default_trade', 'general'); - - if (!$trade) $trade = $defaultTrade; - - $seq = (int) $db->setQuery($db->getQuery(true)->select('COUNT(*)')->from('#__mokosuitefield_work_orders'))->loadResult() + 1; - - $wo = (object) [ - 'wo_number' => $woPrefix . '-' . date('Ymd') . '-' . str_pad($seq, 4, '0', STR_PAD_LEFT), - 'contact_id' => $contactId, - 'location_id' => (int) ($data['location_id'] ?? 0) ?: null, - 'trade' => $trade, - 'priority' => $data['priority'] ?? 'normal', - 'status' => 'new', - 'category' => $data['category'] ?? null, - 'description' => $description, - 'customer_po' => $data['customer_po'] ?? null, - 'scheduled_date' => $data['scheduled_date'] ?? null, - 'scheduled_time_start' => $data['time_start'] ?? null, - 'scheduled_time_end' => $data['time_end'] ?? null, - 'service_agreement_id' => (int) ($data['agreement_id'] ?? 0) ?: null, - 'source' => $data['source'] ?? 'phone', - 'created_by' => Factory::getApplication()->getIdentity()->id, - 'created' => $now, - ]; - - $db->insertObject('#__mokosuitefield_work_orders', $wo, 'id'); - - return (int) $wo->id; - } - - public static function updateStatus(int $woId, string $status, ?float $lat = null, ?float $lng = null): void - { - $db = Factory::getContainer()->get(DatabaseInterface::class); - $now = Factory::getDate()->toSql(); - - $update = (object) ['id' => $woId, 'status' => $status, 'modified' => $now]; - - if ($status === 'on_site') $update->actual_arrival = $now; - if ($status === 'completed') $update->actual_departure = $now; - - $db->updateObject('#__mokosuitefield_work_orders', $update, 'id'); - - // Get tech ID for dispatch log - $db->setQuery($db->getQuery(true)->select('technician_id')->from('#__mokosuitefield_work_orders')->where('id = ' . $woId)); - $techId = (int) $db->loadResult(); - - if ($techId) { - $action = match ($status) { - 'en_route' => 'en_route', - 'on_site' => 'arrived', - 'completed' => 'completed', - 'cancelled' => 'cancelled', - default => null, - }; - - if ($action) { - $db->insertObject('#__mokosuitefield_dispatch_log', (object) [ - 'work_order_id' => $woId, - 'technician_id' => $techId, - 'action' => $action, - 'latitude' => $lat, - 'longitude' => $lng, - 'created_by' => Factory::getApplication()->getIdentity()->id, - 'created' => $now, - ]); - } - - // Update tech status - if ($status === 'completed' || $status === 'cancelled') { - $db->updateObject('#__mokosuitefield_technicians', (object) ['id' => $techId, 'status' => 'available'], 'id'); - } elseif ($status === 'en_route') { - $db->updateObject('#__mokosuitefield_technicians', (object) [ - 'id' => $techId, 'status' => 'en_route', 'current_lat' => $lat, 'current_lng' => $lng, 'last_location_update' => $now, - ], 'id'); - } elseif ($status === 'on_site') { - $db->updateObject('#__mokosuitefield_technicians', (object) ['id' => $techId, 'status' => 'on_site'], 'id'); - } - } - } - - public static function complete(int $woId, string $workPerformed, ?string $signature = null): void - { - $db = Factory::getContainer()->get(DatabaseInterface::class); - $now = Factory::getDate()->toSql(); - - // Calculate totals from line items - $db->setQuery($db->getQuery(true) - ->select('COALESCE(SUM(CASE WHEN item_type = ' . $db->quote('labor') . ' THEN line_total ELSE 0 END), 0) AS labor') - ->select('COALESCE(SUM(CASE WHEN item_type != ' . $db->quote('labor') . ' THEN line_total ELSE 0 END), 0) AS parts') - ->from('#__mokosuitefield_wo_items') - ->where('work_order_id = ' . $woId)); - $totals = $db->loadObject(); - - $laborTotal = (float) ($totals->labor ?? 0); - $partsTotal = (float) ($totals->parts ?? 0); - $total = $laborTotal + $partsTotal; - - $db->updateObject('#__mokosuitefield_work_orders', (object) [ - 'id' => $woId, - 'status' => 'completed', - 'work_performed' => $workPerformed, - 'parts_total' => $partsTotal, - 'labor_total' => $laborTotal, - 'total' => $total, - 'customer_signature' => $signature, - 'customer_signed_at' => $signature ? $now : null, - 'actual_departure' => $now, - 'modified' => $now, - ], 'id'); - - // Update location service history - $db->setQuery($db->getQuery(true)->select('location_id')->from('#__mokosuitefield_work_orders')->where('id = ' . $woId)); - $locId = (int) $db->loadResult(); - if ($locId) { - $db->setQuery($db->getQuery(true) - ->update('#__mokosuitefield_locations') - ->set('service_history_count = service_history_count + 1') - ->set('last_service_date = ' . $db->quote(date('Y-m-d'))) - ->where('id = ' . $locId)); - $db->execute(); - } - } - - public static function getDashboardStats(): object - { - $db = Factory::getContainer()->get(DatabaseInterface::class); - $today = date('Y-m-d'); - - $db->setQuery($db->getQuery(true) - ->select('COUNT(*) AS total_today') - ->select('SUM(CASE WHEN status = ' . $db->quote('new') . ' THEN 1 ELSE 0 END) AS unassigned') - ->select('SUM(CASE WHEN status IN (' . $db->quote('dispatched') . ',' . $db->quote('en_route') . ') THEN 1 ELSE 0 END) AS en_route') - ->select('SUM(CASE WHEN status IN (' . $db->quote('on_site') . ',' . $db->quote('in_progress') . ') THEN 1 ELSE 0 END) AS on_site') - ->select('SUM(CASE WHEN status = ' . $db->quote('completed') . ' THEN 1 ELSE 0 END) AS completed') - ->select('SUM(CASE WHEN priority IN (' . $db->quote('emergency') . ',' . $db->quote('urgent') . ') THEN 1 ELSE 0 END) AS urgent') - ->from('#__mokosuitefield_work_orders') - ->where('scheduled_date = ' . $db->quote($today) . ' OR (scheduled_date IS NULL AND DATE(created) = ' . $db->quote($today) . ')')); - - return $db->loadObject() ?: (object) ['total_today' => 0, 'unassigned' => 0, 'en_route' => 0, 'on_site' => 0, 'completed' => 0, 'urgent' => 0]; - } -} diff --git a/source/packages/plg_task_mokosuitefield/src/Extension/FieldAutomation.php b/source/packages/plg_task_mokosuitefield/src/Extension/FieldAutomation.php deleted file mode 100644 index 9fbdc96..0000000 --- a/source/packages/plg_task_mokosuitefield/src/Extension/FieldAutomation.php +++ /dev/null @@ -1,144 +0,0 @@ - [ - 'langConstPrefix' => 'PLG_TASK_MOKOSUITEFIELD_SERVICE_REMINDERS', - 'method' => 'sendServiceReminders', - ], - 'mokosuite.field.agreement.renewal' => [ - 'langConstPrefix' => 'PLG_TASK_MOKOSUITEFIELD_AGREEMENT_RENEWAL', - 'method' => 'checkAgreementRenewals', - ], - 'mokosuite.field.equipment.maintenance' => [ - 'langConstPrefix' => 'PLG_TASK_MOKOSUITEFIELD_EQUIPMENT_MAINTENANCE', - 'method' => 'checkEquipmentMaintenance', - ], - 'mokosuite.field.truck.reorder' => [ - 'langConstPrefix' => 'PLG_TASK_MOKOSUITEFIELD_TRUCK_REORDER', - 'method' => 'checkTruckStock', - ], - ]; - - public static function getSubscribedEvents(): array - { - return [ - 'onExecuteTask' => 'standardRoutineHandler', - 'onContentPrepareForm' => 'enhanceTaskItemForm', - 'onTaskOptionsList' => 'advertiseRoutines', - ]; - } - - private function sendServiceReminders(ExecuteTaskEvent $event): int - { - $equipment = \Moko\Plugin\System\MokoSuiteField\Helper\EquipmentHelper::getDueForService(14); - - if (empty($equipment)) return Status::OK; - - $body = "Equipment due for service (next 14 days):\n\n"; - foreach ($equipment as $e) { - $body .= "- {$e->equipment_type} ({$e->make} {$e->model}) at {$e->address}, {$e->owner_name}\n"; - $body .= " Due: " . date('M j, Y', strtotime($e->next_service_date)) . "\n"; - } - - $mailer = Factory::getMailer(); - $mailer->addRecipient(Factory::getApplication()->get('mailfrom')); - $mailer->setSubject('Field Service: ' . count($equipment) . ' equipment due for maintenance'); - $mailer->setBody($body); - $mailer->Send(); - - Log::add("Field equipment reminders: " . count($equipment) . " due", Log::INFO, 'mokosuite.field'); - return Status::OK; - } - - private function checkAgreementRenewals(ExecuteTaskEvent $event): int - { - $expiring = \Moko\Plugin\System\MokoSuiteField\Helper\ServiceAgreementHelper::getExpiring(30); - - foreach ($expiring as $a) { - if (!$a->email_to) continue; - - $mailer = Factory::getMailer(); - $mailer->addRecipient($a->email_to, $a->customer_name); - $mailer->setSubject('Service Agreement Renewal — ' . $a->title); - $mailer->setBody( - "Hi {$a->customer_name},\n\n" - . "Your service agreement \"{$a->title}\" expires on " . date('F j, Y', strtotime($a->end_date)) . ".\n\n" - . "Please contact us to renew.\n" - ); - $mailer->Send(); - } - - Log::add("Field agreement renewals: " . count($expiring) . " expiring", Log::INFO, 'mokosuite.field'); - return Status::OK; - } - - private function checkEquipmentMaintenance(ExecuteTaskEvent $event): int - { - $warranty = \Moko\Plugin\System\MokoSuiteField\Helper\EquipmentHelper::getWarrantyExpiring(90); - - if (!empty($warranty)) { - $body = "Equipment warranties expiring (90 days):\n\n"; - foreach ($warranty as $e) { - $body .= "- {$e->make} {$e->model} (SN: {$e->serial_number}) — {$e->owner_name}\n"; - $body .= " Warranty expires: " . date('M j, Y', strtotime($e->warranty_expiry)) . "\n"; - } - - $mailer = Factory::getMailer(); - $mailer->addRecipient(Factory::getApplication()->get('mailfrom')); - $mailer->setSubject('Field: ' . count($warranty) . ' equipment warranties expiring'); - $mailer->setBody($body); - $mailer->Send(); - } - - return Status::OK; - } - - private function checkTruckStock(ExecuteTaskEvent $event): int - { - $db = Factory::getContainer()->get(DatabaseInterface::class); - - $db->setQuery($db->getQuery(true) - ->select('ts.*, p.name AS part_name, p.sku, v.vehicle_number') - ->from($db->quoteName('#__mokosuitefield_truck_stock', 'ts')) - ->join('INNER', $db->quoteName('#__mokosuite_crm_products', 'p') . ' ON p.id = ts.product_id') - ->join('INNER', $db->quoteName('#__mokosuitefield_vehicles', 'v') . ' ON v.id = ts.vehicle_id') - ->where('ts.quantity <= ts.min_quantity')); - $low = $db->loadObjectList() ?: []; - - if (!empty($low)) { - $body = "Truck stock below reorder point:\n\n"; - foreach ($low as $item) { - $body .= "- Vehicle {$item->vehicle_number}: {$item->part_name} ({$item->sku}) — {$item->quantity} remaining (min: {$item->min_quantity})\n"; - } - - $mailer = Factory::getMailer(); - $mailer->addRecipient(Factory::getApplication()->get('mailfrom')); - $mailer->setSubject('Field: ' . count($low) . ' truck stock items need reorder'); - $mailer->setBody($body); - $mailer->Send(); - } - - Log::add("Field truck stock: " . count($low) . " items low", Log::INFO, 'mokosuite.field'); - return Status::OK; - } -} diff --git a/source/packages/plg_webservices_mokosuitefield/src/Extension/MokoSuiteFieldApi.php b/source/packages/plg_webservices_mokosuitefield/src/Extension/MokoSuiteFieldApi.php deleted file mode 100644 index 4dfb1fa..0000000 --- a/source/packages/plg_webservices_mokosuitefield/src/Extension/MokoSuiteFieldApi.php +++ /dev/null @@ -1,27 +0,0 @@ - 'onBeforeApiRoute']; - } - - public function onBeforeApiRoute(BeforeApiRouteEvent $event): void - { - $router = $event->getRouter(); - $router->createCRUDRoutes('v1/mokosuite/field/workorders', 'fieldworkorders', ['component' => 'com_mokosuitefield']); - $router->createCRUDRoutes('v1/mokosuite/field/technicians', 'fieldtechnicians', ['component' => 'com_mokosuitefield']); - $router->createCRUDRoutes('v1/mokosuite/field/equipment', 'fieldequipment', ['component' => 'com_mokosuitefield']); - $router->createCRUDRoutes('v1/mokosuite/field/agreements', 'fieldagreements', ['component' => 'com_mokosuitefield']); - $router->createCRUDRoutes('v1/mokosuite/field/estimates', 'fieldestimates', ['component' => 'com_mokosuitefield']); - $router->createCRUDRoutes('v1/mokosuite/field/locations', 'fieldlocations', ['component' => 'com_mokosuitefield']); - } -} diff --git a/source/pkg_mokosuitefield.xml b/source/pkg_mokosuitefield.xml index 7ab1413..eef4e51 100644 --- a/source/pkg_mokosuitefield.xml +++ b/source/pkg_mokosuitefield.xml @@ -1,24 +1,28 @@ + - Package - MokoSuite Field - mokosuitefield - 01.08.11 - 2026-06-12 - Moko Consulting - hello@mokoconsulting.tech - https://mokoconsulting.tech - Copyright (C) 2026 Moko Consulting. All rights reserved. - GNU General Public License version 3 or later; see LICENSE - MokoSuite Field Service - dispatch, work orders, scheduling, mobile tech. Layer 2 add-on for MokoSuite (requires CRM). - 8.3 - - true - - plg_system_mokosuitefield.zip - com_mokosuitefield.zip - plg_webservices_mokosuitefield.zip - - - https://git.mokoconsulting.tech/MokoConsulting/MokoSuiteField/updates.xml - + pkg_mokosuitefield + mokosuitefield + 0.1.0 + 2026-06-27 + Moko Consulting + hello@mokoconsulting.tech + https://mokoconsulting.tech + Copyright (C) 2026 Moko Consulting + GPL-3.0-or-later + PKG_MOKOSUITEFIELD_DESCRIPTION + + + components/com_mokosuitefield + plugins/system/mokosuitefield + plugins/webservices/mokosuitefield + + + + https://git.mokoconsulting.tech/api/packages/MokoConsulting/generic/pkg_mokosuitefield/latest/updates.xml + diff --git a/source/plugins/system/mokosuitefield/language/en-GB/plg_system_mokosuitefield.ini b/source/plugins/system/mokosuitefield/language/en-GB/plg_system_mokosuitefield.ini new file mode 100644 index 0000000..bdb135a --- /dev/null +++ b/source/plugins/system/mokosuitefield/language/en-GB/plg_system_mokosuitefield.ini @@ -0,0 +1,13 @@ +; Copyright (C) 2026 Moko Consulting +; SPDX-License-Identifier: GPL-3.0-or-later +; Authored-by: Moko Consulting + +PLG_SYSTEM_MOKOSUITEFIELD="System - MokoSuite Field" +PLG_SYSTEM_MOKOSUITEFIELD_DESCRIPTION="Field service management system plugin for MokoSuite." +PLG_SYSTEM_MOKOSUITEFIELD_PARAM_COMPANY_NAME="Company Name" +PLG_SYSTEM_MOKOSUITEFIELD_PARAM_SERVICE_RADIUS="Service Radius (km)" +PLG_SYSTEM_MOKOSUITEFIELD_PARAM_AUTO_DISPATCH="Auto-dispatch" +PLG_SYSTEM_MOKOSUITEFIELD_PARAM_TIMEOUT_MINUTES="Dispatch Timeout (minutes)" +PLG_SYSTEM_MOKOSUITEFIELD_PARAM_DEFAULT_HOURLY_RATE="Default Hourly Rate" +PLG_SYSTEM_MOKOSUITEFIELD_PARAM_TAX_RATE="Tax Rate (%)" +PLG_SYSTEM_MOKOSUITEFIELD_PARAM_LOW_STOCK_THRESHOLD="Low Stock Threshold" diff --git a/source/plugins/system/mokosuitefield/language/en-GB/plg_system_mokosuitefield.sys.ini b/source/plugins/system/mokosuitefield/language/en-GB/plg_system_mokosuitefield.sys.ini new file mode 100644 index 0000000..fcc87ca --- /dev/null +++ b/source/plugins/system/mokosuitefield/language/en-GB/plg_system_mokosuitefield.sys.ini @@ -0,0 +1,6 @@ +; Copyright (C) 2026 Moko Consulting +; SPDX-License-Identifier: GPL-3.0-or-later +; Authored-by: Moko Consulting + +PLG_SYSTEM_MOKOSUITEFIELD="System - MokoSuite Field" +PLG_SYSTEM_MOKOSUITEFIELD_DESCRIPTION="Field service management system plugin for MokoSuite." diff --git a/source/plugins/system/mokosuitefield/mokosuitefield.xml b/source/plugins/system/mokosuitefield/mokosuitefield.xml new file mode 100644 index 0000000..b056150 --- /dev/null +++ b/source/plugins/system/mokosuitefield/mokosuitefield.xml @@ -0,0 +1,106 @@ + + + + plg_system_mokosuitefield + 0.1.0 + 2026-06-27 + Moko Consulting + hello@mokoconsulting.tech + https://mokoconsulting.tech + Copyright (C) 2026 Moko Consulting + GPL-3.0-or-later + PLG_SYSTEM_MOKOSUITEFIELD_DESCRIPTION + + Moko\Plugin\System\MokoSuiteField + + + src + services + sql + language + + + + sql/install.sql + + + sql/uninstall.sql + + + + en-GB/plg_system_mokosuitefield.ini + en-GB/plg_system_mokosuitefield.sys.ini + + + + +
+ + +
+
+ + + + + +
+
+ + +
+
+ +
+
+
+
diff --git a/source/plugins/system/mokosuitefield/services/provider.php b/source/plugins/system/mokosuitefield/services/provider.php new file mode 100644 index 0000000..8f148f1 --- /dev/null +++ b/source/plugins/system/mokosuitefield/services/provider.php @@ -0,0 +1,36 @@ + + * @license GPL-3.0-or-later + * @author Moko Consulting + */ + +\defined('_JEXEC') or die; + +use Joomla\CMS\Extension\PluginInterface; +use Joomla\CMS\Factory; +use Joomla\CMS\Plugin\PluginHelper; +use Joomla\DI\Container; +use Joomla\DI\ServiceProviderInterface; +use Joomla\Event\DispatcherInterface; +use Moko\Plugin\System\MokoSuiteField\Extension\MokoSuiteField; + +return new class () implements ServiceProviderInterface { + public function register(Container $container): void + { + $container->set( + PluginInterface::class, + function (Container $container) { + $dispatcher = $container->get(DispatcherInterface::class); + $plugin = new MokoSuiteField( + $dispatcher, + (array) PluginHelper::getPlugin('system', 'mokosuitefield') + ); + $plugin->setApplication(Factory::getApplication()); + + return $plugin; + } + ); + } +}; diff --git a/source/plugins/system/mokosuitefield/sql/install.sql b/source/plugins/system/mokosuitefield/sql/install.sql new file mode 100644 index 0000000..199f5f4 --- /dev/null +++ b/source/plugins/system/mokosuitefield/sql/install.sql @@ -0,0 +1,170 @@ +-- Copyright (C) 2026 Moko Consulting +-- SPDX-License-Identifier: GPL-3.0-or-later +-- Authored-by: Moko Consulting + +CREATE TABLE IF NOT EXISTS `#__mokosuitefield_technicians` ( + `id` INT UNSIGNED NOT NULL AUTO_INCREMENT, + `contact_id` INT DEFAULT NULL, + `name` VARCHAR(255) NOT NULL, + `email` VARCHAR(255) NOT NULL DEFAULT '', + `phone` VARCHAR(50) NOT NULL DEFAULT '', + `skills` VARCHAR(500) NOT NULL DEFAULT '', + `hourly_rate` DECIMAL(10,2) NOT NULL DEFAULT 0.00, + `status` ENUM('active','inactive','on_leave','training') NOT NULL DEFAULT 'active', + `current_lat` DECIMAL(10,7) DEFAULT NULL, + `current_lng` DECIMAL(10,7) DEFAULT NULL, + `published` TINYINT NOT NULL DEFAULT 1, + `created` DATETIME NOT NULL, + PRIMARY KEY (`id`), + KEY `idx_contact` (`contact_id`), + KEY `idx_status` (`status`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +CREATE TABLE IF NOT EXISTS `#__mokosuitefield_equipment` ( + `id` INT UNSIGNED NOT NULL AUTO_INCREMENT, + `customer_contact_id` INT DEFAULT NULL, + `name` VARCHAR(255) NOT NULL, + `equipment_type` ENUM('hvac','plumbing','electrical','appliance','generator','elevator','fire_system','other') NOT NULL DEFAULT 'other', + `brand` VARCHAR(100) NOT NULL DEFAULT '', + `model` VARCHAR(100) NOT NULL DEFAULT '', + `serial_number` VARCHAR(100) NOT NULL DEFAULT '', + `install_date` DATE DEFAULT NULL, + `warranty_expiry` DATE DEFAULT NULL, + `location_address` VARCHAR(500) NOT NULL DEFAULT '', + `notes` TEXT, + `published` TINYINT NOT NULL DEFAULT 1, + `created` DATETIME NOT NULL, + PRIMARY KEY (`id`), + KEY `idx_customer` (`customer_contact_id`), + KEY `idx_type` (`equipment_type`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +CREATE TABLE IF NOT EXISTS `#__mokosuitefield_equipment_history` ( + `id` INT UNSIGNED NOT NULL AUTO_INCREMENT, + `equipment_id` INT UNSIGNED NOT NULL, + `work_order_id` INT UNSIGNED DEFAULT NULL, + `action` ENUM('install','repair','maintenance','inspection','replacement','decommission') NOT NULL DEFAULT 'maintenance', + `description` VARCHAR(500) NOT NULL DEFAULT '', + `technician_id` INT UNSIGNED DEFAULT NULL, + `action_date` DATE NOT NULL, + `cost` DECIMAL(10,2) DEFAULT NULL, + `created` DATETIME NOT NULL, + PRIMARY KEY (`id`), + KEY `idx_equipment` (`equipment_id`), + KEY `idx_date` (`action_date`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +CREATE TABLE IF NOT EXISTS `#__mokosuitefield_work_orders` ( + `id` INT UNSIGNED NOT NULL AUTO_INCREMENT, + `work_order_ref` VARCHAR(20) NOT NULL, + `customer_contact_id` INT DEFAULT NULL, + `customer_name` VARCHAR(255) NOT NULL, + `customer_phone` VARCHAR(50) NOT NULL DEFAULT '', + `equipment_id` INT UNSIGNED DEFAULT NULL, + `technician_id` INT UNSIGNED DEFAULT NULL, + `status` ENUM('requested','scheduled','dispatched','in_progress','on_hold','completed','invoiced','cancelled') NOT NULL DEFAULT 'requested', + `priority` ENUM('emergency','high','normal','low') NOT NULL DEFAULT 'normal', + `work_type` ENUM('repair','maintenance','installation','inspection','warranty','callback') NOT NULL DEFAULT 'repair', + `title` VARCHAR(255) NOT NULL, + `description` TEXT, + `site_address` VARCHAR(500) NOT NULL, + `site_lat` DECIMAL(10,7) DEFAULT NULL, + `site_lng` DECIMAL(10,7) DEFAULT NULL, + `scheduled_date` DATE DEFAULT NULL, + `dispatched_at` DATETIME DEFAULT NULL, + `completed_at` DATETIME DEFAULT NULL, + `labor_hours` DECIMAL(5,2) NOT NULL DEFAULT 0.00, + `labor_cost` DECIMAL(10,2) NOT NULL DEFAULT 0.00, + `parts_cost` DECIMAL(10,2) NOT NULL DEFAULT 0.00, + `total_cost` DECIMAL(10,2) NOT NULL DEFAULT 0.00, + `customer_signature` TEXT, + `notes` TEXT, + `created` DATETIME NOT NULL, + `created_by` INT NOT NULL DEFAULT 0, + PRIMARY KEY (`id`), + UNIQUE KEY `idx_ref` (`work_order_ref`), + KEY `idx_customer` (`customer_contact_id`), + KEY `idx_technician` (`technician_id`), + KEY `idx_status` (`status`), + KEY `idx_scheduled` (`scheduled_date`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +CREATE TABLE IF NOT EXISTS `#__mokosuitefield_parts` ( + `id` INT UNSIGNED NOT NULL AUTO_INCREMENT, + `name` VARCHAR(255) NOT NULL, + `part_number` VARCHAR(100) NOT NULL DEFAULT '', + `category` VARCHAR(100) NOT NULL DEFAULT '', + `unit_cost` DECIMAL(10,2) NOT NULL DEFAULT 0.00, + `sell_price` DECIMAL(10,2) NOT NULL DEFAULT 0.00, + `stock_qty` INT NOT NULL DEFAULT 0, + `reorder_level` INT UNSIGNED NOT NULL DEFAULT 5, + `supplier` VARCHAR(255) NOT NULL DEFAULT '', + `published` TINYINT NOT NULL DEFAULT 1, + `created` DATETIME NOT NULL, + PRIMARY KEY (`id`), + KEY `idx_part_number` (`part_number`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +CREATE TABLE IF NOT EXISTS `#__mokosuitefield_truck_inventory` ( + `id` INT UNSIGNED NOT NULL AUTO_INCREMENT, + `technician_id` INT UNSIGNED NOT NULL, + `part_id` INT UNSIGNED NOT NULL, + `quantity` INT NOT NULL DEFAULT 0, + `last_restocked` DATE DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `idx_tech_part` (`technician_id`, `part_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +CREATE TABLE IF NOT EXISTS `#__mokosuitefield_checklists` ( + `id` INT UNSIGNED NOT NULL AUTO_INCREMENT, + `name` VARCHAR(255) NOT NULL, + `work_type` ENUM('repair','maintenance','installation','inspection','warranty','callback','all') NOT NULL DEFAULT 'all', + `published` TINYINT NOT NULL DEFAULT 1, + `ordering` INT NOT NULL DEFAULT 0, + `created` DATETIME NOT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +CREATE TABLE IF NOT EXISTS `#__mokosuitefield_checklist_items` ( + `id` INT UNSIGNED NOT NULL AUTO_INCREMENT, + `checklist_id` INT UNSIGNED NOT NULL, + `label` VARCHAR(255) NOT NULL, + `item_type` ENUM('checkbox','text','number','photo','pass_fail') NOT NULL DEFAULT 'checkbox', + `required` TINYINT NOT NULL DEFAULT 0, + `ordering` INT NOT NULL DEFAULT 0, + PRIMARY KEY (`id`), + KEY `idx_checklist` (`checklist_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +CREATE TABLE IF NOT EXISTS `#__mokosuitefield_pm_agreements` ( + `id` INT UNSIGNED NOT NULL AUTO_INCREMENT, + `customer_contact_id` INT NOT NULL, + `equipment_id` INT UNSIGNED DEFAULT NULL, + `name` VARCHAR(255) NOT NULL, + `frequency` ENUM('monthly','quarterly','semi_annual','annual') NOT NULL DEFAULT 'annual', + `next_service_date` DATE DEFAULT NULL, + `annual_price` DECIMAL(10,2) NOT NULL DEFAULT 0.00, + `status` ENUM('active','expired','cancelled') NOT NULL DEFAULT 'active', + `start_date` DATE NOT NULL, + `end_date` DATE DEFAULT NULL, + `auto_renew` TINYINT NOT NULL DEFAULT 1, + `created` DATETIME NOT NULL, + PRIMARY KEY (`id`), + KEY `idx_customer` (`customer_contact_id`), + KEY `idx_next_service` (`next_service_date`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +CREATE TABLE IF NOT EXISTS `#__mokosuitefield_dispatches` ( + `id` INT UNSIGNED NOT NULL AUTO_INCREMENT, + `work_order_id` INT UNSIGNED NOT NULL, + `technician_id` INT UNSIGNED NOT NULL, + `status` ENUM('offered','accepted','rejected','expired','cancelled') NOT NULL DEFAULT 'offered', + `offered_at` DATETIME NOT NULL, + `responded_at` DATETIME DEFAULT NULL, + `distance_km` DECIMAL(10,2) DEFAULT NULL, + `eta_minutes` DECIMAL(10,2) DEFAULT NULL, + `attempt_number` TINYINT UNSIGNED NOT NULL DEFAULT 1, + PRIMARY KEY (`id`), + KEY `idx_work_order` (`work_order_id`), + KEY `idx_technician` (`technician_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; diff --git a/source/plugins/system/mokosuitefield/sql/uninstall.sql b/source/plugins/system/mokosuitefield/sql/uninstall.sql new file mode 100644 index 0000000..d919198 --- /dev/null +++ b/source/plugins/system/mokosuitefield/sql/uninstall.sql @@ -0,0 +1,14 @@ +-- Copyright (C) 2026 Moko Consulting +-- SPDX-License-Identifier: GPL-3.0-or-later +-- Authored-by: Moko Consulting + +DROP TABLE IF EXISTS `#__mokosuitefield_dispatches`; +DROP TABLE IF EXISTS `#__mokosuitefield_pm_agreements`; +DROP TABLE IF EXISTS `#__mokosuitefield_checklist_items`; +DROP TABLE IF EXISTS `#__mokosuitefield_checklists`; +DROP TABLE IF EXISTS `#__mokosuitefield_truck_inventory`; +DROP TABLE IF EXISTS `#__mokosuitefield_parts`; +DROP TABLE IF EXISTS `#__mokosuitefield_work_orders`; +DROP TABLE IF EXISTS `#__mokosuitefield_equipment_history`; +DROP TABLE IF EXISTS `#__mokosuitefield_equipment`; +DROP TABLE IF EXISTS `#__mokosuitefield_technicians`; diff --git a/source/plugins/system/mokosuitefield/src/Extension/MokoSuiteField.php b/source/plugins/system/mokosuitefield/src/Extension/MokoSuiteField.php new file mode 100644 index 0000000..b7627a0 --- /dev/null +++ b/source/plugins/system/mokosuitefield/src/Extension/MokoSuiteField.php @@ -0,0 +1,23 @@ + + * @license GPL-3.0-or-later + * @author Moko Consulting + */ + +namespace Moko\Plugin\System\MokoSuiteField\Extension; + +use Joomla\CMS\Plugin\CMSPlugin; +use Joomla\Event\DispatcherInterface; +use Joomla\Event\SubscriberInterface; + +\defined('_JEXEC') or die; + +final class MokoSuiteField extends CMSPlugin implements SubscriberInterface +{ + public static function getSubscribedEvents(): array + { + return []; + } +} diff --git a/source/plugins/webservices/mokosuitefield/language/en-GB/plg_webservices_mokosuitefield.ini b/source/plugins/webservices/mokosuitefield/language/en-GB/plg_webservices_mokosuitefield.ini new file mode 100644 index 0000000..38d8d0d --- /dev/null +++ b/source/plugins/webservices/mokosuitefield/language/en-GB/plg_webservices_mokosuitefield.ini @@ -0,0 +1,6 @@ +; Copyright (C) 2026 Moko Consulting +; SPDX-License-Identifier: GPL-3.0-or-later +; Authored-by: Moko Consulting + +PLG_WEBSERVICES_MOKOSUITEFIELD="Web Services - MokoSuite Field" +PLG_WEBSERVICES_MOKOSUITEFIELD_DESCRIPTION="Provides API routes for MokoSuite Field service management." diff --git a/source/plugins/webservices/mokosuitefield/language/en-GB/plg_webservices_mokosuitefield.sys.ini b/source/plugins/webservices/mokosuitefield/language/en-GB/plg_webservices_mokosuitefield.sys.ini new file mode 100644 index 0000000..38d8d0d --- /dev/null +++ b/source/plugins/webservices/mokosuitefield/language/en-GB/plg_webservices_mokosuitefield.sys.ini @@ -0,0 +1,6 @@ +; Copyright (C) 2026 Moko Consulting +; SPDX-License-Identifier: GPL-3.0-or-later +; Authored-by: Moko Consulting + +PLG_WEBSERVICES_MOKOSUITEFIELD="Web Services - MokoSuite Field" +PLG_WEBSERVICES_MOKOSUITEFIELD_DESCRIPTION="Provides API routes for MokoSuite Field service management." diff --git a/source/plugins/webservices/mokosuitefield/mokosuitefield.xml b/source/plugins/webservices/mokosuitefield/mokosuitefield.xml new file mode 100644 index 0000000..63c2610 --- /dev/null +++ b/source/plugins/webservices/mokosuitefield/mokosuitefield.xml @@ -0,0 +1,30 @@ + + + + plg_webservices_mokosuitefield + 0.1.0 + 2026-06-27 + Moko Consulting + hello@mokoconsulting.tech + https://mokoconsulting.tech + Copyright (C) 2026 Moko Consulting + GPL-3.0-or-later + PLG_WEBSERVICES_MOKOSUITEFIELD_DESCRIPTION + + Moko\Plugin\WebServices\MokoSuiteField + + + src + services + language + + + + en-GB/plg_webservices_mokosuitefield.ini + en-GB/plg_webservices_mokosuitefield.sys.ini + + diff --git a/source/plugins/webservices/mokosuitefield/services/provider.php b/source/plugins/webservices/mokosuitefield/services/provider.php new file mode 100644 index 0000000..f46086f --- /dev/null +++ b/source/plugins/webservices/mokosuitefield/services/provider.php @@ -0,0 +1,36 @@ + + * @license GPL-3.0-or-later + * @author Moko Consulting + */ + +\defined('_JEXEC') or die; + +use Joomla\CMS\Extension\PluginInterface; +use Joomla\CMS\Factory; +use Joomla\CMS\Plugin\PluginHelper; +use Joomla\DI\Container; +use Joomla\DI\ServiceProviderInterface; +use Joomla\Event\DispatcherInterface; +use Moko\Plugin\WebServices\MokoSuiteField\Extension\MokoSuiteField; + +return new class () implements ServiceProviderInterface { + public function register(Container $container): void + { + $container->set( + PluginInterface::class, + function (Container $container) { + $dispatcher = $container->get(DispatcherInterface::class); + $plugin = new MokoSuiteField( + $dispatcher, + (array) PluginHelper::getPlugin('webservices', 'mokosuitefield') + ); + $plugin->setApplication(Factory::getApplication()); + + return $plugin; + } + ); + } +}; diff --git a/source/plugins/webservices/mokosuitefield/src/Extension/MokoSuiteField.php b/source/plugins/webservices/mokosuitefield/src/Extension/MokoSuiteField.php new file mode 100644 index 0000000..ce2562f --- /dev/null +++ b/source/plugins/webservices/mokosuitefield/src/Extension/MokoSuiteField.php @@ -0,0 +1,50 @@ + + * @license GPL-3.0-or-later + * @author Moko Consulting + */ + +namespace Moko\Plugin\WebServices\MokoSuiteField\Extension; + +use Joomla\CMS\Plugin\CMSPlugin; +use Joomla\CMS\Router\ApiRouter; +use Joomla\Event\SubscriberInterface; +use Joomla\Router\Route; + +\defined('_JEXEC') or die; + +final class MokoSuiteField extends CMSPlugin implements SubscriberInterface +{ + public static function getSubscribedEvents(): array + { + return [ + 'onBeforeApiRoute' => 'onBeforeApiRoute', + ]; + } + + public function onBeforeApiRoute(&$router): void + { + $routes = [ + 'workorders' => 'fieldworkorders', + 'technicians' => 'fieldtechnicians', + 'equipment' => 'fieldequipment', + 'parts' => 'fieldparts', + 'checklists' => 'fieldchecklists', + 'agreements' => 'fieldagreements', + 'dispatches' => 'fielddispatches', + ]; + + $component = 'com_mokosuitefield'; + $defaults = ['component' => $component, 'public' => false]; + + foreach ($routes as $endpoint => $view) { + $router->createCRUDRoutes( + "v1/mokosuitefield/{$endpoint}", + $view, + $defaults + ); + } + } +} From 77f08380e2d7132450142023922c3cdaf11f33d9 Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Sat, 27 Jun 2026 15:32:20 -0500 Subject: [PATCH 18/64] feat: standard scaffold files --- .gitignore | 9 +- CHANGELOG.md | 22 +- CLAUDE.md | 40 ++-- README.md | 35 +++- .../com_mokosuitefield/admin/access.xml | 24 +++ .../com_mokosuitefield/admin/config.xml | 36 ++++ .../admin/services/provider.php | 26 +++ .../src/Controller/DisplayController.php | 18 ++ .../admin/src/Model/FieldDashboardModel.php | 17 ++ .../admin/src/View/Checklists/HtmlView.php | 23 +++ .../admin/src/View/Dispatches/HtmlView.php | 23 +++ .../admin/src/View/Equipment/HtmlView.php | 23 +++ .../src/View/FieldDashboard/HtmlView.php | 23 +++ .../admin/src/View/Parts/HtmlView.php | 23 +++ .../admin/src/View/PmAgreements/HtmlView.php | 23 +++ .../admin/src/View/Technicians/HtmlView.php | 23 +++ .../src/View/TruckInventory/HtmlView.php | 23 +++ .../admin/src/View/WorkOrders/HtmlView.php | 23 +++ .../admin/tmpl/checklists/default.php | 1 + .../admin/tmpl/dispatches/default.php | 1 + .../admin/tmpl/equipment/default.php | 1 + .../admin/tmpl/fielddashboard/default.php | 1 + .../admin/tmpl/parts/default.php | 1 + .../admin/tmpl/pmagreements/default.php | 1 + .../admin/tmpl/technicians/default.php | 1 + .../admin/tmpl/truckinventory/default.php | 1 + .../admin/tmpl/workorders/default.php | 1 + .../com_mokosuitefield/mokosuitefield.xml | 14 ++ .../en-GB/plg_system_mokosuitefield.ini | 2 + .../en-GB/plg_system_mokosuitefield.sys.ini | 2 + .../mokosuitefield.xml | 64 ++++++ .../services/provider.php | 33 +++ .../sql/install.mysql.sql | 191 ++++++++++++++++++ .../sql/uninstall.mysql.sql | 10 + .../src/Extension/Field.php | 24 +++ .../mokosuitefield.xml | 10 + .../services/provider.php | 24 +++ .../src/Extension/MokoSuiteField.php | 37 ++++ source/pkg_mokosuitefield.xml | 28 +-- 39 files changed, 826 insertions(+), 56 deletions(-) create mode 100644 source/packages/com_mokosuitefield/admin/access.xml create mode 100644 source/packages/com_mokosuitefield/admin/config.xml create mode 100644 source/packages/com_mokosuitefield/admin/services/provider.php create mode 100644 source/packages/com_mokosuitefield/admin/src/Controller/DisplayController.php create mode 100644 source/packages/com_mokosuitefield/admin/src/Model/FieldDashboardModel.php create mode 100644 source/packages/com_mokosuitefield/admin/src/View/Checklists/HtmlView.php create mode 100644 source/packages/com_mokosuitefield/admin/src/View/Dispatches/HtmlView.php create mode 100644 source/packages/com_mokosuitefield/admin/src/View/Equipment/HtmlView.php create mode 100644 source/packages/com_mokosuitefield/admin/src/View/FieldDashboard/HtmlView.php create mode 100644 source/packages/com_mokosuitefield/admin/src/View/Parts/HtmlView.php create mode 100644 source/packages/com_mokosuitefield/admin/src/View/PmAgreements/HtmlView.php create mode 100644 source/packages/com_mokosuitefield/admin/src/View/Technicians/HtmlView.php create mode 100644 source/packages/com_mokosuitefield/admin/src/View/TruckInventory/HtmlView.php create mode 100644 source/packages/com_mokosuitefield/admin/src/View/WorkOrders/HtmlView.php create mode 100644 source/packages/com_mokosuitefield/admin/tmpl/checklists/default.php create mode 100644 source/packages/com_mokosuitefield/admin/tmpl/dispatches/default.php create mode 100644 source/packages/com_mokosuitefield/admin/tmpl/equipment/default.php create mode 100644 source/packages/com_mokosuitefield/admin/tmpl/fielddashboard/default.php create mode 100644 source/packages/com_mokosuitefield/admin/tmpl/parts/default.php create mode 100644 source/packages/com_mokosuitefield/admin/tmpl/pmagreements/default.php create mode 100644 source/packages/com_mokosuitefield/admin/tmpl/technicians/default.php create mode 100644 source/packages/com_mokosuitefield/admin/tmpl/truckinventory/default.php create mode 100644 source/packages/com_mokosuitefield/admin/tmpl/workorders/default.php create mode 100644 source/packages/com_mokosuitefield/mokosuitefield.xml create mode 100644 source/packages/plg_system_mokosuitefield/language/en-GB/plg_system_mokosuitefield.ini create mode 100644 source/packages/plg_system_mokosuitefield/language/en-GB/plg_system_mokosuitefield.sys.ini create mode 100644 source/packages/plg_system_mokosuitefield/mokosuitefield.xml create mode 100644 source/packages/plg_system_mokosuitefield/services/provider.php create mode 100644 source/packages/plg_system_mokosuitefield/sql/install.mysql.sql create mode 100644 source/packages/plg_system_mokosuitefield/sql/uninstall.mysql.sql create mode 100644 source/packages/plg_system_mokosuitefield/src/Extension/Field.php create mode 100644 source/packages/plg_webservices_mokosuitefield/mokosuitefield.xml create mode 100644 source/packages/plg_webservices_mokosuitefield/services/provider.php create mode 100644 source/packages/plg_webservices_mokosuitefield/src/Extension/MokoSuiteField.php diff --git a/.gitignore b/.gitignore index c8a642e..9d9fe2e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,8 @@ +*.min.css +*.min.js +node_modules/ .claude/ .mcp.json TODO.md -*.min.css -*.min.js -vendor/ -node_modules/ +*.zip +*.tar.gz diff --git a/CHANGELOG.md b/CHANGELOG.md index f76722d..525c203 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,17 +1,21 @@ + # Changelog -All notable changes to this project will be documented in this file. - -The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). - ## [Unreleased] ### Added - -- **Initial scaffold** — Package structure with component, system plugin, and webservices plugin +- **Repository** -- initial repo creation with scaffolding +- **System Plugin** -- Extension class, service provider +- **SQL Schema** -- 10 tables: work_orders, equipment, equipment_history, technicians, parts, truck_inventory, work_order_parts, checklists, pm_agreements, dispatches +- **Admin Component** -- 9 views: dashboard, work orders, equipment, technicians, parts, truck inventory, checklists, PM agreements, dispatches +- **Webservices Plugin** -- API route stubs +- **Configuration** -- settings across basic, dispatch, billing, scheduling +- **Access Control** -- permissions for granular role-based access diff --git a/CLAUDE.md b/CLAUDE.md index 1f90bfc..b1609ce 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,25 +1,31 @@ - - # MokoSuiteField -Field service management for contractors — a Layer 2 vertical for the MokoSuite platform. +Work orders, technician dispatch, equipment registry, parts inventory, and PM agreements for Joomla 6. -## Structure +## Quick Reference -- `source/` — Joomla extension source (package, component, plugins) -- All work happens on the `dev` branch; never commit directly to `main` -- Use conventional commits (`feat:`, `fix:`, `chore:`, etc.) +| Field | Value | +|---|---| +| **Package** | `pkg_mokosuitefield` | +| **Layer** | 2 (requires: Client, CRM) | +| **Language** | PHP 8.3+ | +| **Branch** | develop on `dev`, merge to `main` (protected) | +| **Wiki** | [MokoSuiteField Wiki](https://git.mokoconsulting.tech/MokoConsulting/MokoSuiteField/wiki) | -## Build +## Architecture -Packaged via MokoCLI. Run `mokocli build` from the repo root. +Joomla **package** -- Layer 2 add-on. CRM contacts as customers/technicians, work order management with dispatch, equipment tracking, parts inventory with truck stock, preventive maintenance agreements. -## Standards +## Rules -- PHP 8.3+ / Joomla 6 architecture -- `$this->getDatabase()` — never use `Factory::getDbo()` -- Namespace root: `Moko\Component\MokoSuiteField` (component), `Moko\Plugin\System\MokoSuiteField` (system plugin), `Moko\Plugin\WebServices\MokoSuiteField` (API plugin) +- **Never commit** `.claude/`, `.mcp.json`, `TODO.md`, `*.min.css`/`*.min.js` +- **Attribution**: `Authored-by: Moko Consulting` +- **Workflow directory**: `.mokogitea/` +- **Standards**: [MokoStandards](https://git.mokoconsulting.tech/MokoConsulting/MokoCLI/wiki) +- **Changelog**: `[Unreleased]` only -- release system assigns versions + +## Coding Standards + +- PHP 8.3+ / Joomla 6 patterns +- `$this->getDatabase()` in models, `Factory::getContainer()->get(DatabaseInterface::class)` in helpers +- `Factory::getApplication()->getIdentity()` for user diff --git a/README.md b/README.md index f45cd5c..4a0eaf8 100644 --- a/README.md +++ b/README.md @@ -1,27 +1,44 @@ -# MokoSuiteField +# MokoSuite Field -Field service management for contractors — a MokoSuite Layer 2 vertical extension for Joomla. +Work orders, technician dispatch, equipment registry, parts inventory, and PM agreements module for MokoSuite on Joomla 6. ## Overview -MokoSuiteField provides work order management, technician dispatch, equipment tracking, parts inventory, checklists, and preventive maintenance agreements for field service businesses. +MokoSuiteField is a Layer 2 module in the MokoSuite platform, building on MokoSuiteClient (Layer 0) and MokoSuiteCRM (Layer 1) to provide complete field service management operations. + +## Features + +- **Work Orders** -- service requests with categories, priorities, scheduling, photos, signatures +- **Technician Management** -- profiles linked to CRM contacts, specialties, certifications, GPS tracking +- **Equipment Registry** -- customer equipment tracking with service history, warranty, condition ratings +- **Parts Inventory** -- warehouse stock with pricing, minimum levels, supplier tracking +- **Truck Inventory** -- per-technician mobile parts stock with restocking alerts +- **Checklists** -- configurable inspection and safety checklists by category +- **PM Agreements** -- preventive maintenance contracts with visit scheduling and auto-renewal +- **Dispatch** -- technician assignment with distance/ETA tracking and multi-attempt offers ## Requirements - Joomla 6.x - PHP 8.3+ -- MokoSuite (base platform) +- MokoSuiteClient (Layer 0) +- MokoSuiteCRM (Layer 1) ## Installation -Install via the MokoSuite package manager or upload `pkg_mokosuitefield.zip` through Joomla's extension installer. +Install via Joomla Extension Manager using the package file `pkg_mokosuitefield.zip`. ## License -GPL-3.0-or-later +GNU General Public License v3.0 or later -- see [LICENSE](LICENSE). + +## Links + +- [Documentation](https://git.mokoconsulting.tech/MokoConsulting/MokoSuiteField/wiki) +- [Issues](https://git.mokoconsulting.tech/MokoConsulting/MokoSuiteField/issues) +- [MokoSuite Platform](https://mokoconsulting.tech) diff --git a/source/packages/com_mokosuitefield/admin/access.xml b/source/packages/com_mokosuitefield/admin/access.xml new file mode 100644 index 0000000..6df9f60 --- /dev/null +++ b/source/packages/com_mokosuitefield/admin/access.xml @@ -0,0 +1,24 @@ + + +
+ + + + + + + + + + + + + + + + + + + +
+
diff --git a/source/packages/com_mokosuitefield/admin/config.xml b/source/packages/com_mokosuitefield/admin/config.xml new file mode 100644 index 0000000..a036fac --- /dev/null +++ b/source/packages/com_mokosuitefield/admin/config.xml @@ -0,0 +1,36 @@ + + +
+ + + + + + + +
+
+ + + + + + + + + +
+
+ + + +
+
+ + + + + + +
+
diff --git a/source/packages/com_mokosuitefield/admin/services/provider.php b/source/packages/com_mokosuitefield/admin/services/provider.php new file mode 100644 index 0000000..6c33848 --- /dev/null +++ b/source/packages/com_mokosuitefield/admin/services/provider.php @@ -0,0 +1,26 @@ + + * SPDX-License-Identifier: GPL-3.0-or-later + * Authored-by: Moko Consulting + */ + +defined('_JEXEC') or die; + +use Joomla\CMS\Extension\ComponentInterface; +use Joomla\CMS\Extension\MVCComponent; +use Joomla\CMS\Dispatcher\ComponentDispatcherFactoryInterface; +use Joomla\CMS\MVC\Factory\MVCFactoryInterface; +use Joomla\DI\Container; +use Joomla\DI\ServiceProviderInterface; + +return new class implements ServiceProviderInterface { + public function register(Container $container): void { + $container->set(ComponentInterface::class, function (Container $container) { + $c = new MVCComponent($container->get(ComponentDispatcherFactoryInterface::class)); + $c->setMVCFactory($container->get(MVCFactoryInterface::class)); + return $c; + }); + } +}; diff --git a/source/packages/com_mokosuitefield/admin/src/Controller/DisplayController.php b/source/packages/com_mokosuitefield/admin/src/Controller/DisplayController.php new file mode 100644 index 0000000..bf9436b --- /dev/null +++ b/source/packages/com_mokosuitefield/admin/src/Controller/DisplayController.php @@ -0,0 +1,18 @@ + + * SPDX-License-Identifier: GPL-3.0-or-later + * Authored-by: Moko Consulting + */ + +namespace Moko\Component\MokoSuiteField\Administrator\Controller; + +defined('_JEXEC') or die; + +use Joomla\CMS\MVC\Controller\BaseController; + +class DisplayController extends BaseController +{ + protected $default_view = 'fielddashboard'; +} diff --git a/source/packages/com_mokosuitefield/admin/src/Model/FieldDashboardModel.php b/source/packages/com_mokosuitefield/admin/src/Model/FieldDashboardModel.php new file mode 100644 index 0000000..4e7135e --- /dev/null +++ b/source/packages/com_mokosuitefield/admin/src/Model/FieldDashboardModel.php @@ -0,0 +1,17 @@ + + * SPDX-License-Identifier: GPL-3.0-or-later + * Authored-by: Moko Consulting + */ + +namespace Moko\Component\MokoSuiteField\Administrator\Model; + +defined('_JEXEC') or die; + +use Joomla\CMS\MVC\Model\BaseDatabaseModel; + +class FieldDashboardModel extends BaseDatabaseModel +{ +} diff --git a/source/packages/com_mokosuitefield/admin/src/View/Checklists/HtmlView.php b/source/packages/com_mokosuitefield/admin/src/View/Checklists/HtmlView.php new file mode 100644 index 0000000..2a11464 --- /dev/null +++ b/source/packages/com_mokosuitefield/admin/src/View/Checklists/HtmlView.php @@ -0,0 +1,23 @@ + + * SPDX-License-Identifier: GPL-3.0-or-later + * Authored-by: Moko Consulting + */ + +namespace Moko\Component\MokoSuiteField\Administrator\View\Checklists; + +defined('_JEXEC') or die; + +use Joomla\CMS\MVC\View\HtmlView as BaseHtmlView; +use Joomla\CMS\Toolbar\ToolbarHelper; + +class HtmlView extends BaseHtmlView +{ + public function display($tpl = null): void + { + ToolbarHelper::title('MokoSuite Field - Checklists'); + parent::display($tpl); + } +} diff --git a/source/packages/com_mokosuitefield/admin/src/View/Dispatches/HtmlView.php b/source/packages/com_mokosuitefield/admin/src/View/Dispatches/HtmlView.php new file mode 100644 index 0000000..13798a5 --- /dev/null +++ b/source/packages/com_mokosuitefield/admin/src/View/Dispatches/HtmlView.php @@ -0,0 +1,23 @@ + + * SPDX-License-Identifier: GPL-3.0-or-later + * Authored-by: Moko Consulting + */ + +namespace Moko\Component\MokoSuiteField\Administrator\View\Dispatches; + +defined('_JEXEC') or die; + +use Joomla\CMS\MVC\View\HtmlView as BaseHtmlView; +use Joomla\CMS\Toolbar\ToolbarHelper; + +class HtmlView extends BaseHtmlView +{ + public function display($tpl = null): void + { + ToolbarHelper::title('MokoSuite Field - Dispatches'); + parent::display($tpl); + } +} diff --git a/source/packages/com_mokosuitefield/admin/src/View/Equipment/HtmlView.php b/source/packages/com_mokosuitefield/admin/src/View/Equipment/HtmlView.php new file mode 100644 index 0000000..e9c6e74 --- /dev/null +++ b/source/packages/com_mokosuitefield/admin/src/View/Equipment/HtmlView.php @@ -0,0 +1,23 @@ + + * SPDX-License-Identifier: GPL-3.0-or-later + * Authored-by: Moko Consulting + */ + +namespace Moko\Component\MokoSuiteField\Administrator\View\Equipment; + +defined('_JEXEC') or die; + +use Joomla\CMS\MVC\View\HtmlView as BaseHtmlView; +use Joomla\CMS\Toolbar\ToolbarHelper; + +class HtmlView extends BaseHtmlView +{ + public function display($tpl = null): void + { + ToolbarHelper::title('MokoSuite Field - Equipment'); + parent::display($tpl); + } +} diff --git a/source/packages/com_mokosuitefield/admin/src/View/FieldDashboard/HtmlView.php b/source/packages/com_mokosuitefield/admin/src/View/FieldDashboard/HtmlView.php new file mode 100644 index 0000000..a1a7bd7 --- /dev/null +++ b/source/packages/com_mokosuitefield/admin/src/View/FieldDashboard/HtmlView.php @@ -0,0 +1,23 @@ + + * SPDX-License-Identifier: GPL-3.0-or-later + * Authored-by: Moko Consulting + */ + +namespace Moko\Component\MokoSuiteField\Administrator\View\FieldDashboard; + +defined('_JEXEC') or die; + +use Joomla\CMS\MVC\View\HtmlView as BaseHtmlView; +use Joomla\CMS\Toolbar\ToolbarHelper; + +class HtmlView extends BaseHtmlView +{ + public function display($tpl = null): void + { + ToolbarHelper::title('MokoSuite Field - Dashboard'); + parent::display($tpl); + } +} diff --git a/source/packages/com_mokosuitefield/admin/src/View/Parts/HtmlView.php b/source/packages/com_mokosuitefield/admin/src/View/Parts/HtmlView.php new file mode 100644 index 0000000..5297f04 --- /dev/null +++ b/source/packages/com_mokosuitefield/admin/src/View/Parts/HtmlView.php @@ -0,0 +1,23 @@ + + * SPDX-License-Identifier: GPL-3.0-or-later + * Authored-by: Moko Consulting + */ + +namespace Moko\Component\MokoSuiteField\Administrator\View\Parts; + +defined('_JEXEC') or die; + +use Joomla\CMS\MVC\View\HtmlView as BaseHtmlView; +use Joomla\CMS\Toolbar\ToolbarHelper; + +class HtmlView extends BaseHtmlView +{ + public function display($tpl = null): void + { + ToolbarHelper::title('MokoSuite Field - Parts'); + parent::display($tpl); + } +} diff --git a/source/packages/com_mokosuitefield/admin/src/View/PmAgreements/HtmlView.php b/source/packages/com_mokosuitefield/admin/src/View/PmAgreements/HtmlView.php new file mode 100644 index 0000000..6f7ed2c --- /dev/null +++ b/source/packages/com_mokosuitefield/admin/src/View/PmAgreements/HtmlView.php @@ -0,0 +1,23 @@ + + * SPDX-License-Identifier: GPL-3.0-or-later + * Authored-by: Moko Consulting + */ + +namespace Moko\Component\MokoSuiteField\Administrator\View\PmAgreements; + +defined('_JEXEC') or die; + +use Joomla\CMS\MVC\View\HtmlView as BaseHtmlView; +use Joomla\CMS\Toolbar\ToolbarHelper; + +class HtmlView extends BaseHtmlView +{ + public function display($tpl = null): void + { + ToolbarHelper::title('MokoSuite Field - PM Agreements'); + parent::display($tpl); + } +} diff --git a/source/packages/com_mokosuitefield/admin/src/View/Technicians/HtmlView.php b/source/packages/com_mokosuitefield/admin/src/View/Technicians/HtmlView.php new file mode 100644 index 0000000..3d73165 --- /dev/null +++ b/source/packages/com_mokosuitefield/admin/src/View/Technicians/HtmlView.php @@ -0,0 +1,23 @@ + + * SPDX-License-Identifier: GPL-3.0-or-later + * Authored-by: Moko Consulting + */ + +namespace Moko\Component\MokoSuiteField\Administrator\View\Technicians; + +defined('_JEXEC') or die; + +use Joomla\CMS\MVC\View\HtmlView as BaseHtmlView; +use Joomla\CMS\Toolbar\ToolbarHelper; + +class HtmlView extends BaseHtmlView +{ + public function display($tpl = null): void + { + ToolbarHelper::title('MokoSuite Field - Technicians'); + parent::display($tpl); + } +} diff --git a/source/packages/com_mokosuitefield/admin/src/View/TruckInventory/HtmlView.php b/source/packages/com_mokosuitefield/admin/src/View/TruckInventory/HtmlView.php new file mode 100644 index 0000000..6c21e29 --- /dev/null +++ b/source/packages/com_mokosuitefield/admin/src/View/TruckInventory/HtmlView.php @@ -0,0 +1,23 @@ + + * SPDX-License-Identifier: GPL-3.0-or-later + * Authored-by: Moko Consulting + */ + +namespace Moko\Component\MokoSuiteField\Administrator\View\TruckInventory; + +defined('_JEXEC') or die; + +use Joomla\CMS\MVC\View\HtmlView as BaseHtmlView; +use Joomla\CMS\Toolbar\ToolbarHelper; + +class HtmlView extends BaseHtmlView +{ + public function display($tpl = null): void + { + ToolbarHelper::title('MokoSuite Field - Truck Inventory'); + parent::display($tpl); + } +} diff --git a/source/packages/com_mokosuitefield/admin/src/View/WorkOrders/HtmlView.php b/source/packages/com_mokosuitefield/admin/src/View/WorkOrders/HtmlView.php new file mode 100644 index 0000000..0cd88b0 --- /dev/null +++ b/source/packages/com_mokosuitefield/admin/src/View/WorkOrders/HtmlView.php @@ -0,0 +1,23 @@ + + * SPDX-License-Identifier: GPL-3.0-or-later + * Authored-by: Moko Consulting + */ + +namespace Moko\Component\MokoSuiteField\Administrator\View\WorkOrders; + +defined('_JEXEC') or die; + +use Joomla\CMS\MVC\View\HtmlView as BaseHtmlView; +use Joomla\CMS\Toolbar\ToolbarHelper; + +class HtmlView extends BaseHtmlView +{ + public function display($tpl = null): void + { + ToolbarHelper::title('MokoSuite Field - Work Orders'); + parent::display($tpl); + } +} diff --git a/source/packages/com_mokosuitefield/admin/tmpl/checklists/default.php b/source/packages/com_mokosuitefield/admin/tmpl/checklists/default.php new file mode 100644 index 0000000..6ea858b --- /dev/null +++ b/source/packages/com_mokosuitefield/admin/tmpl/checklists/default.php @@ -0,0 +1 @@ +

Checklists

Coming soon.

diff --git a/source/packages/com_mokosuitefield/admin/tmpl/dispatches/default.php b/source/packages/com_mokosuitefield/admin/tmpl/dispatches/default.php new file mode 100644 index 0000000..491c1d4 --- /dev/null +++ b/source/packages/com_mokosuitefield/admin/tmpl/dispatches/default.php @@ -0,0 +1 @@ +

Dispatches

Coming soon.

diff --git a/source/packages/com_mokosuitefield/admin/tmpl/equipment/default.php b/source/packages/com_mokosuitefield/admin/tmpl/equipment/default.php new file mode 100644 index 0000000..289ad6c --- /dev/null +++ b/source/packages/com_mokosuitefield/admin/tmpl/equipment/default.php @@ -0,0 +1 @@ +

Equipment

Coming soon.

diff --git a/source/packages/com_mokosuitefield/admin/tmpl/fielddashboard/default.php b/source/packages/com_mokosuitefield/admin/tmpl/fielddashboard/default.php new file mode 100644 index 0000000..e80e602 --- /dev/null +++ b/source/packages/com_mokosuitefield/admin/tmpl/fielddashboard/default.php @@ -0,0 +1 @@ +

Dashboard

Coming soon.

diff --git a/source/packages/com_mokosuitefield/admin/tmpl/parts/default.php b/source/packages/com_mokosuitefield/admin/tmpl/parts/default.php new file mode 100644 index 0000000..33bddb6 --- /dev/null +++ b/source/packages/com_mokosuitefield/admin/tmpl/parts/default.php @@ -0,0 +1 @@ +

Parts

Coming soon.

diff --git a/source/packages/com_mokosuitefield/admin/tmpl/pmagreements/default.php b/source/packages/com_mokosuitefield/admin/tmpl/pmagreements/default.php new file mode 100644 index 0000000..823c4ef --- /dev/null +++ b/source/packages/com_mokosuitefield/admin/tmpl/pmagreements/default.php @@ -0,0 +1 @@ +

PM Agreements

Coming soon.

diff --git a/source/packages/com_mokosuitefield/admin/tmpl/technicians/default.php b/source/packages/com_mokosuitefield/admin/tmpl/technicians/default.php new file mode 100644 index 0000000..7812c7d --- /dev/null +++ b/source/packages/com_mokosuitefield/admin/tmpl/technicians/default.php @@ -0,0 +1 @@ +

Technicians

Coming soon.

diff --git a/source/packages/com_mokosuitefield/admin/tmpl/truckinventory/default.php b/source/packages/com_mokosuitefield/admin/tmpl/truckinventory/default.php new file mode 100644 index 0000000..42b2624 --- /dev/null +++ b/source/packages/com_mokosuitefield/admin/tmpl/truckinventory/default.php @@ -0,0 +1 @@ +

Truck Inventory

Coming soon.

diff --git a/source/packages/com_mokosuitefield/admin/tmpl/workorders/default.php b/source/packages/com_mokosuitefield/admin/tmpl/workorders/default.php new file mode 100644 index 0000000..f820055 --- /dev/null +++ b/source/packages/com_mokosuitefield/admin/tmpl/workorders/default.php @@ -0,0 +1 @@ +

Work Orders

Coming soon.

diff --git a/source/packages/com_mokosuitefield/mokosuitefield.xml b/source/packages/com_mokosuitefield/mokosuitefield.xml new file mode 100644 index 0000000..98052e0 --- /dev/null +++ b/source/packages/com_mokosuitefield/mokosuitefield.xml @@ -0,0 +1,14 @@ + + + MokoSuite Field + Moko Consulting + 2026-06-27 + Copyright (C) 2026 Moko Consulting. + GPL-3.0-or-later + 06.00.00 + Moko\Component\MokoSuiteField + + servicessrctmpl + COM_MOKOSUITEFIELD + + diff --git a/source/packages/plg_system_mokosuitefield/language/en-GB/plg_system_mokosuitefield.ini b/source/packages/plg_system_mokosuitefield/language/en-GB/plg_system_mokosuitefield.ini new file mode 100644 index 0000000..4ca8379 --- /dev/null +++ b/source/packages/plg_system_mokosuitefield/language/en-GB/plg_system_mokosuitefield.ini @@ -0,0 +1,2 @@ +PLG_SYSTEM_MOKOSUITEFIELD="System - MokoSuite Field" +PLG_SYSTEM_MOKOSUITEFIELD_DESC="Work orders, technician dispatch, equipment registry, parts inventory, PM agreements" diff --git a/source/packages/plg_system_mokosuitefield/language/en-GB/plg_system_mokosuitefield.sys.ini b/source/packages/plg_system_mokosuitefield/language/en-GB/plg_system_mokosuitefield.sys.ini new file mode 100644 index 0000000..4ca8379 --- /dev/null +++ b/source/packages/plg_system_mokosuitefield/language/en-GB/plg_system_mokosuitefield.sys.ini @@ -0,0 +1,2 @@ +PLG_SYSTEM_MOKOSUITEFIELD="System - MokoSuite Field" +PLG_SYSTEM_MOKOSUITEFIELD_DESC="Work orders, technician dispatch, equipment registry, parts inventory, PM agreements" diff --git a/source/packages/plg_system_mokosuitefield/mokosuitefield.xml b/source/packages/plg_system_mokosuitefield/mokosuitefield.xml new file mode 100644 index 0000000..5fa1340 --- /dev/null +++ b/source/packages/plg_system_mokosuitefield/mokosuitefield.xml @@ -0,0 +1,64 @@ + + + System - MokoSuite Field + mokosuitefield + Moko Consulting + 2026-06-27 + Copyright (C) 2026 Moko Consulting. All rights reserved. + GPL-3.0-or-later + hello@mokoconsulting.tech + https://mokoconsulting.tech + 06.00.00 + 8.3 + PLG_SYSTEM_MOKOSUITEFIELD_DESC + Moko\Plugin\System\MokoSuiteField + + src + services + language + sql + + + en-GB/plg_system_mokosuitefield.ini + en-GB/plg_system_mokosuitefield.sys.ini + + sql/install.mysql.sql + sql/uninstall.mysql.sql + + +
+ + + + + + + +
+
+ + + + + + + + + +
+
+ + + +
+
+ + + + + + +
+
+
+
diff --git a/source/packages/plg_system_mokosuitefield/services/provider.php b/source/packages/plg_system_mokosuitefield/services/provider.php new file mode 100644 index 0000000..dbcce3a --- /dev/null +++ b/source/packages/plg_system_mokosuitefield/services/provider.php @@ -0,0 +1,33 @@ + + * SPDX-License-Identifier: GPL-3.0-or-later + * Authored-by: Moko Consulting + */ + +defined('_JEXEC') or die; + +use Joomla\CMS\Extension\PluginInterface; +use Joomla\CMS\Factory; +use Joomla\CMS\Plugin\PluginHelper; +use Joomla\DI\Container; +use Joomla\DI\ServiceProviderInterface; +use Joomla\Event\DispatcherInterface; +use Moko\Plugin\System\MokoSuiteField\Extension\Field; + +return new class implements ServiceProviderInterface +{ + public function register(Container $container): void + { + $container->set( + PluginInterface::class, + function (Container $container) { + $dispatcher = $container->get(DispatcherInterface::class); + $plugin = new Field($dispatcher, (array) PluginHelper::getPlugin('system', 'mokosuitefield')); + $plugin->setApplication(Factory::getApplication()); + return $plugin; + } + ); + } +}; diff --git a/source/packages/plg_system_mokosuitefield/sql/install.mysql.sql b/source/packages/plg_system_mokosuitefield/sql/install.mysql.sql new file mode 100644 index 0000000..8aaeac2 --- /dev/null +++ b/source/packages/plg_system_mokosuitefield/sql/install.mysql.sql @@ -0,0 +1,191 @@ +CREATE TABLE IF NOT EXISTS `#__mokosuitefield_work_orders` ( + `id` INT UNSIGNED NOT NULL AUTO_INCREMENT, + `work_order_ref` VARCHAR(20) NOT NULL, + `customer_contact_id` INT DEFAULT NULL, + `customer_name` VARCHAR(255) NOT NULL, + `customer_phone` VARCHAR(50) NOT NULL DEFAULT '', + `service_address` VARCHAR(500) NOT NULL, + `service_lat` DECIMAL(10,7) DEFAULT NULL, + `service_lng` DECIMAL(10,7) DEFAULT NULL, + `category` ENUM('plumbing','electrical','hvac','appliance','general','emergency','inspection','installation','other') NOT NULL DEFAULT 'general', + `priority` ENUM('emergency','high','medium','low','scheduled') NOT NULL DEFAULT 'medium', + `status` ENUM('requested','scheduled','dispatched','en_route','in_progress','completed','invoiced','cancelled','on_hold') NOT NULL DEFAULT 'requested', + `title` VARCHAR(255) NOT NULL, + `description` TEXT, + `technician_id` INT UNSIGNED DEFAULT NULL, + `pm_agreement_id` INT UNSIGNED DEFAULT NULL, + `scheduled_date` DATE DEFAULT NULL, + `labor_hours` DECIMAL(5,2) NOT NULL DEFAULT 0.00, + `labor_total` DECIMAL(10,2) NOT NULL DEFAULT 0.00, + `parts_total` DECIMAL(10,2) NOT NULL DEFAULT 0.00, + `total` DECIMAL(10,2) NOT NULL DEFAULT 0.00, + `payment_status` ENUM('pending','partial','paid','void') NOT NULL DEFAULT 'pending', + `customer_signature` TEXT, + `photos_before` JSON DEFAULT NULL, + `photos_after` JSON DEFAULT NULL, + `notes` TEXT, + `created` DATETIME NOT NULL, + `created_by` INT NOT NULL DEFAULT 0, + PRIMARY KEY (`id`), + UNIQUE KEY `idx_ref` (`work_order_ref`), + KEY `idx_customer` (`customer_contact_id`), + KEY `idx_technician` (`technician_id`), + KEY `idx_status` (`status`), + KEY `idx_category` (`category`), + KEY `idx_priority` (`priority`), + KEY `idx_scheduled` (`scheduled_date`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +CREATE TABLE IF NOT EXISTS `#__mokosuitefield_equipment` ( + `id` INT UNSIGNED NOT NULL AUTO_INCREMENT, + `customer_contact_id` INT DEFAULT NULL, + `service_address` VARCHAR(500) NOT NULL DEFAULT '', + `equipment_type` ENUM('hvac','furnace','ac','water_heater','boiler','plumbing','electrical_panel','generator','appliance','other') NOT NULL DEFAULT 'other', + `brand` VARCHAR(100) NOT NULL DEFAULT '', + `model` VARCHAR(100) NOT NULL DEFAULT '', + `serial_number` VARCHAR(100) NOT NULL DEFAULT '', + `install_date` DATE DEFAULT NULL, + `warranty_expiry` DATE DEFAULT NULL, + `condition_rating` ENUM('excellent','good','fair','poor','replace') NOT NULL DEFAULT 'good', + `last_service_date` DATE DEFAULT NULL, + `next_service_date` DATE DEFAULT NULL, + `notes` TEXT, + `published` TINYINT NOT NULL DEFAULT 1, + `created` DATETIME NOT NULL, + PRIMARY KEY (`id`), + KEY `idx_customer` (`customer_contact_id`), + KEY `idx_type` (`equipment_type`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +CREATE TABLE IF NOT EXISTS `#__mokosuitefield_equipment_history` ( + `id` INT UNSIGNED NOT NULL AUTO_INCREMENT, + `equipment_id` INT UNSIGNED NOT NULL, + `work_order_id` INT UNSIGNED DEFAULT NULL, + `service_type` ENUM('installation','repair','maintenance','inspection','replacement','warranty') NOT NULL DEFAULT 'maintenance', + `description` TEXT, + `service_date` DATE NOT NULL, + `cost` DECIMAL(10,2) DEFAULT NULL, + `created` DATETIME NOT NULL, + PRIMARY KEY (`id`), + KEY `idx_equipment` (`equipment_id`), + KEY `idx_work_order` (`work_order_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +CREATE TABLE IF NOT EXISTS `#__mokosuitefield_technicians` ( + `id` INT UNSIGNED NOT NULL AUTO_INCREMENT, + `contact_id` INT DEFAULT NULL, + `name` VARCHAR(255) NOT NULL, + `email` VARCHAR(255) NOT NULL DEFAULT '', + `phone` VARCHAR(50) NOT NULL DEFAULT '', + `specialties` JSON DEFAULT NULL, + `certifications` VARCHAR(500) NOT NULL DEFAULT '', + `license_number` VARCHAR(100) NOT NULL DEFAULT '', + `hourly_rate` DECIMAL(10,2) NOT NULL DEFAULT 0.00, + `status` ENUM('available','on_job','off_duty','vacation','inactive') NOT NULL DEFAULT 'available', + `current_lat` DECIMAL(10,7) DEFAULT NULL, + `current_lng` DECIMAL(10,7) DEFAULT NULL, + `rating` DECIMAL(3,2) NOT NULL DEFAULT 5.00, + `total_jobs` INT UNSIGNED NOT NULL DEFAULT 0, + `photo` VARCHAR(500) NOT NULL DEFAULT '', + `published` TINYINT NOT NULL DEFAULT 1, + `created` DATETIME NOT NULL, + PRIMARY KEY (`id`), + KEY `idx_contact` (`contact_id`), + KEY `idx_status` (`status`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +CREATE TABLE IF NOT EXISTS `#__mokosuitefield_parts` ( + `id` INT UNSIGNED NOT NULL AUTO_INCREMENT, + `part_number` VARCHAR(50) NOT NULL, + `name` VARCHAR(255) NOT NULL, + `category` VARCHAR(100) NOT NULL DEFAULT '', + `unit_cost` DECIMAL(10,2) NOT NULL DEFAULT 0.00, + `retail_price` DECIMAL(10,2) NOT NULL DEFAULT 0.00, + `warehouse_stock` INT NOT NULL DEFAULT 0, + `minimum_stock` INT NOT NULL DEFAULT 0, + `supplier` VARCHAR(255) NOT NULL DEFAULT '', + `published` TINYINT NOT NULL DEFAULT 1, + `created` DATETIME NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `idx_part_number` (`part_number`), + KEY `idx_category` (`category`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +CREATE TABLE IF NOT EXISTS `#__mokosuitefield_truck_inventory` ( + `id` INT UNSIGNED NOT NULL AUTO_INCREMENT, + `technician_id` INT UNSIGNED NOT NULL, + `part_id` INT UNSIGNED NOT NULL, + `quantity` INT NOT NULL DEFAULT 0, + `min_quantity` INT NOT NULL DEFAULT 0, + `last_restocked` DATE DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `idx_tech_part` (`technician_id`, `part_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +CREATE TABLE IF NOT EXISTS `#__mokosuitefield_work_order_parts` ( + `id` INT UNSIGNED NOT NULL AUTO_INCREMENT, + `work_order_id` INT UNSIGNED NOT NULL, + `part_id` INT UNSIGNED NOT NULL, + `quantity` INT UNSIGNED NOT NULL DEFAULT 1, + `unit_price` DECIMAL(10,2) NOT NULL, + `line_total` DECIMAL(10,2) NOT NULL, + `source` ENUM('warehouse','truck','purchased','customer_supplied') NOT NULL DEFAULT 'truck', + PRIMARY KEY (`id`), + KEY `idx_work_order` (`work_order_id`), + KEY `idx_part` (`part_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +CREATE TABLE IF NOT EXISTS `#__mokosuitefield_checklists` ( + `id` INT UNSIGNED NOT NULL AUTO_INCREMENT, + `name` VARCHAR(255) NOT NULL, + `category` ENUM('plumbing','electrical','hvac','appliance','general','safety','inspection') NOT NULL DEFAULT 'general', + `items` JSON NOT NULL, + `published` TINYINT NOT NULL DEFAULT 1, + `ordering` INT NOT NULL DEFAULT 0, + `created` DATETIME NOT NULL, + PRIMARY KEY (`id`), + KEY `idx_category` (`category`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +CREATE TABLE IF NOT EXISTS `#__mokosuitefield_pm_agreements` ( + `id` INT UNSIGNED NOT NULL AUTO_INCREMENT, + `agreement_ref` VARCHAR(20) NOT NULL, + `customer_contact_id` INT NOT NULL, + `customer_name` VARCHAR(255) NOT NULL, + `service_address` VARCHAR(500) NOT NULL, + `plan_name` VARCHAR(255) NOT NULL, + `status` ENUM('active','expired','cancelled','pending_renewal') NOT NULL DEFAULT 'active', + `start_date` DATE NOT NULL, + `end_date` DATE DEFAULT NULL, + `visit_frequency` ENUM('monthly','quarterly','semi_annual','annual') NOT NULL DEFAULT 'semi_annual', + `visits_per_year` TINYINT UNSIGNED NOT NULL DEFAULT 2, + `visits_completed` TINYINT UNSIGNED NOT NULL DEFAULT 0, + `annual_cost` DECIMAL(10,2) NOT NULL DEFAULT 0.00, + `auto_renew` TINYINT NOT NULL DEFAULT 0, + `notes` TEXT, + `created` DATETIME NOT NULL, + `created_by` INT NOT NULL DEFAULT 0, + PRIMARY KEY (`id`), + UNIQUE KEY `idx_ref` (`agreement_ref`), + KEY `idx_customer` (`customer_contact_id`), + KEY `idx_status` (`status`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +CREATE TABLE IF NOT EXISTS `#__mokosuitefield_dispatches` ( + `id` INT UNSIGNED NOT NULL AUTO_INCREMENT, + `work_order_id` INT UNSIGNED NOT NULL, + `technician_id` INT UNSIGNED NOT NULL, + `status` ENUM('offered','accepted','rejected','en_route','arrived','completed','cancelled') NOT NULL DEFAULT 'offered', + `offered_at` DATETIME NOT NULL, + `responded_at` DATETIME DEFAULT NULL, + `arrived_at` DATETIME DEFAULT NULL, + `distance_km` DECIMAL(10,2) DEFAULT NULL, + `eta_minutes` DECIMAL(10,2) DEFAULT NULL, + `attempt_number` TINYINT UNSIGNED NOT NULL DEFAULT 1, + `notes` TEXT, + `created` DATETIME NOT NULL, + PRIMARY KEY (`id`), + KEY `idx_work_order` (`work_order_id`), + KEY `idx_technician` (`technician_id`), + KEY `idx_status` (`status`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; diff --git a/source/packages/plg_system_mokosuitefield/sql/uninstall.mysql.sql b/source/packages/plg_system_mokosuitefield/sql/uninstall.mysql.sql new file mode 100644 index 0000000..a7d8a93 --- /dev/null +++ b/source/packages/plg_system_mokosuitefield/sql/uninstall.mysql.sql @@ -0,0 +1,10 @@ +DROP TABLE IF EXISTS `#__mokosuitefield_dispatches`; +DROP TABLE IF EXISTS `#__mokosuitefield_pm_agreements`; +DROP TABLE IF EXISTS `#__mokosuitefield_checklists`; +DROP TABLE IF EXISTS `#__mokosuitefield_work_order_parts`; +DROP TABLE IF EXISTS `#__mokosuitefield_truck_inventory`; +DROP TABLE IF EXISTS `#__mokosuitefield_parts`; +DROP TABLE IF EXISTS `#__mokosuitefield_equipment_history`; +DROP TABLE IF EXISTS `#__mokosuitefield_technicians`; +DROP TABLE IF EXISTS `#__mokosuitefield_equipment`; +DROP TABLE IF EXISTS `#__mokosuitefield_work_orders`; diff --git a/source/packages/plg_system_mokosuitefield/src/Extension/Field.php b/source/packages/plg_system_mokosuitefield/src/Extension/Field.php new file mode 100644 index 0000000..d92d8d9 --- /dev/null +++ b/source/packages/plg_system_mokosuitefield/src/Extension/Field.php @@ -0,0 +1,24 @@ + + * SPDX-License-Identifier: GPL-3.0-or-later + * Authored-by: Moko Consulting + */ + +namespace Moko\Plugin\System\MokoSuiteField\Extension; + +defined('_JEXEC') or die; + +use Joomla\CMS\Plugin\CMSPlugin; +use Joomla\Event\SubscriberInterface; + +class Field extends CMSPlugin implements SubscriberInterface +{ + protected $autoloadLanguage = true; + + public static function getSubscribedEvents(): array + { + return []; + } +} diff --git a/source/packages/plg_webservices_mokosuitefield/mokosuitefield.xml b/source/packages/plg_webservices_mokosuitefield/mokosuitefield.xml new file mode 100644 index 0000000..d5648ef --- /dev/null +++ b/source/packages/plg_webservices_mokosuitefield/mokosuitefield.xml @@ -0,0 +1,10 @@ + + + Web Services - MokoSuite Field + mokosuitefield + Moko Consulting + 06.00.00 + GPL-3.0-or-later + Moko\Plugin\WebServices\MokoSuiteField + srcservices + diff --git a/source/packages/plg_webservices_mokosuitefield/services/provider.php b/source/packages/plg_webservices_mokosuitefield/services/provider.php new file mode 100644 index 0000000..a05218a --- /dev/null +++ b/source/packages/plg_webservices_mokosuitefield/services/provider.php @@ -0,0 +1,24 @@ + + * SPDX-License-Identifier: GPL-3.0-or-later + * Authored-by: Moko Consulting + */ + +defined('_JEXEC') or die; + +use Joomla\CMS\Extension\PluginInterface; +use Joomla\CMS\Plugin\PluginHelper; +use Joomla\DI\Container; +use Joomla\DI\ServiceProviderInterface; +use Joomla\Event\DispatcherInterface; +use Moko\Plugin\WebServices\MokoSuiteField\Extension\MokoSuiteField; + +return new class implements ServiceProviderInterface { + public function register(Container $container): void { + $container->set(PluginInterface::class, function (Container $container) { + return new MokoSuiteField($container->get(DispatcherInterface::class), (array) PluginHelper::getPlugin('webservices', 'mokosuitefield')); + }); + } +}; diff --git a/source/packages/plg_webservices_mokosuitefield/src/Extension/MokoSuiteField.php b/source/packages/plg_webservices_mokosuitefield/src/Extension/MokoSuiteField.php new file mode 100644 index 0000000..e3f8268 --- /dev/null +++ b/source/packages/plg_webservices_mokosuitefield/src/Extension/MokoSuiteField.php @@ -0,0 +1,37 @@ + + * SPDX-License-Identifier: GPL-3.0-or-later + * Authored-by: Moko Consulting + */ + +namespace Moko\Plugin\WebServices\MokoSuiteField\Extension; + +defined('_JEXEC') or die; + +use Joomla\CMS\Plugin\CMSPlugin; +use Joomla\Event\SubscriberInterface; + +class MokoSuiteField extends CMSPlugin implements SubscriberInterface +{ + public static function getSubscribedEvents(): array + { + return ['onBeforeApiRoute' => 'onBeforeApiRoute']; + } + + public function onBeforeApiRoute(&$event): void + { + $router = $event->getArgument('router'); + $opts = ['component' => 'com_mokosuitefield']; + + $router->createCRUDRoutes('v1/mokosuitefield/workorders', 'workorders', $opts); + $router->createCRUDRoutes('v1/mokosuitefield/equipment', 'equipment', $opts); + $router->createCRUDRoutes('v1/mokosuitefield/technicians', 'technicians', $opts); + $router->createCRUDRoutes('v1/mokosuitefield/parts', 'parts', $opts); + $router->createCRUDRoutes('v1/mokosuitefield/truckinventory', 'truckinventory', $opts); + $router->createCRUDRoutes('v1/mokosuitefield/checklists', 'checklists', $opts); + $router->createCRUDRoutes('v1/mokosuitefield/pmagreements', 'pmagreements', $opts); + $router->createCRUDRoutes('v1/mokosuitefield/dispatches', 'dispatches', $opts); + } +} diff --git a/source/pkg_mokosuitefield.xml b/source/pkg_mokosuitefield.xml index eef4e51..a9f780e 100644 --- a/source/pkg_mokosuitefield.xml +++ b/source/pkg_mokosuitefield.xml @@ -1,28 +1,22 @@ - - pkg_mokosuitefield + Package - MokoSuite Field mokosuitefield - 0.1.0 + 06.00.00 2026-06-27 Moko Consulting hello@mokoconsulting.tech https://mokoconsulting.tech - Copyright (C) 2026 Moko Consulting - GPL-3.0-or-later - PKG_MOKOSUITEFIELD_DESCRIPTION - - - components/com_mokosuitefield - plugins/system/mokosuitefield - plugins/webservices/mokosuitefield + Copyright (C) 2026 Moko Consulting. All rights reserved. + GNU General Public License version 3 or later; see LICENSE + Work orders, technician dispatch, equipment registry, parts inventory, PM agreements + 8.3 + + true + + plg_system_mokosuitefield.zip - - https://git.mokoconsulting.tech/api/packages/MokoConsulting/generic/pkg_mokosuitefield/latest/updates.xml + https://git.mokoconsulting.tech/MokoConsulting/MokoSuiteField/updates.xml From 7229b4cae400ca6df30a73136a4210c1a76abdab Mon Sep 17 00:00:00 2001 From: "gitea-actions[bot]" Date: Sat, 27 Jun 2026 20:32:54 +0000 Subject: [PATCH 19/64] chore(version): pre-release bump to 06.00.01-dev [skip ci] --- .mokogitea/workflows/issue-branch.yml | 2 +- source/packages/com_mokosuitefield/mokosuitefield.xml | 2 +- source/packages/plg_system_mokosuitefield/mokosuitefield.xml | 2 +- .../packages/plg_webservices_mokosuitefield/mokosuitefield.xml | 2 +- source/pkg_mokosuitefield.xml | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.mokogitea/workflows/issue-branch.yml b/.mokogitea/workflows/issue-branch.yml index d6137d3..1b074d8 100644 --- a/.mokogitea/workflows/issue-branch.yml +++ b/.mokogitea/workflows/issue-branch.yml @@ -5,7 +5,7 @@ # FILE INFORMATION # DEFGROUP: Gitea.Workflow # INGROUP: mokocli.Automation -# VERSION: 01.08.11 +# VERSION: 06.00.01 # BRIEF: Auto-create feature branch when an issue is opened name: "Universal: Issue Branch" diff --git a/source/packages/com_mokosuitefield/mokosuitefield.xml b/source/packages/com_mokosuitefield/mokosuitefield.xml index 98052e0..1835567 100644 --- a/source/packages/com_mokosuitefield/mokosuitefield.xml +++ b/source/packages/com_mokosuitefield/mokosuitefield.xml @@ -5,7 +5,7 @@ 2026-06-27 Copyright (C) 2026 Moko Consulting. GPL-3.0-or-later - 06.00.00 + 06.00.01 Moko\Component\MokoSuiteField servicessrctmpl diff --git a/source/packages/plg_system_mokosuitefield/mokosuitefield.xml b/source/packages/plg_system_mokosuitefield/mokosuitefield.xml index 5fa1340..92333fa 100644 --- a/source/packages/plg_system_mokosuitefield/mokosuitefield.xml +++ b/source/packages/plg_system_mokosuitefield/mokosuitefield.xml @@ -8,7 +8,7 @@ GPL-3.0-or-later hello@mokoconsulting.tech https://mokoconsulting.tech - 06.00.00 + 06.00.01 8.3 PLG_SYSTEM_MOKOSUITEFIELD_DESC Moko\Plugin\System\MokoSuiteField diff --git a/source/packages/plg_webservices_mokosuitefield/mokosuitefield.xml b/source/packages/plg_webservices_mokosuitefield/mokosuitefield.xml index d5648ef..cc901f2 100644 --- a/source/packages/plg_webservices_mokosuitefield/mokosuitefield.xml +++ b/source/packages/plg_webservices_mokosuitefield/mokosuitefield.xml @@ -3,7 +3,7 @@ Web Services - MokoSuite Field mokosuitefield Moko Consulting - 06.00.00 + 06.00.01 GPL-3.0-or-later Moko\Plugin\WebServices\MokoSuiteField srcservices diff --git a/source/pkg_mokosuitefield.xml b/source/pkg_mokosuitefield.xml index a9f780e..c5fa1df 100644 --- a/source/pkg_mokosuitefield.xml +++ b/source/pkg_mokosuitefield.xml @@ -2,7 +2,7 @@ Package - MokoSuite Field mokosuitefield - 06.00.00 + 06.00.01 2026-06-27 Moko Consulting hello@mokoconsulting.tech From e4f00dfa6606cb08c86a2d674320220d1a5832e2 Mon Sep 17 00:00:00 2001 From: Jonathan Miller <1+jmiller@noreply.git.mokoconsulting.tech> Date: Sun, 28 Jun 2026 07:10:05 +0000 Subject: [PATCH 20/64] chore: add .editorconfig from Template-Joomla --- .editorconfig | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 .editorconfig diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..09975f7 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,31 @@ +# EditorConfig https://editorconfig.org +root = true + +[*] +indent_style = tab +indent_size = 2 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + +[*.ps1] +end_of_line = crlf + +[*.md] +trim_trailing_whitespace = false + +[*.{json,yml,yaml}] +indent_style = space +indent_size = 2 + +[*.{mak,Makefile}] +indent_style = tab + +[*.bat] +end_of_line = crlf + +[*.sh] +end_of_line = lf +indent_style = space +indent_size = 2 From c22748b0ac30a8a2a68b62f6a5dd8626df4ef35f Mon Sep 17 00:00:00 2001 From: Jonathan Miller <1+jmiller@noreply.git.mokoconsulting.tech> Date: Sun, 28 Jun 2026 07:10:42 +0000 Subject: [PATCH 21/64] chore: add composer.json from Template-Joomla --- composer.json | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 composer.json diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..d938818 --- /dev/null +++ b/composer.json @@ -0,0 +1,26 @@ +{ + "name": "mokoconsulting/mokojoomgallery", + "description": "Joomla extension by Moko Consulting", + "type": "joomla-package", + "license": "GPL-3.0-or-later", + "homepage": "https://mokoconsulting.tech", + "authors": [ + { + "name": "Moko Consulting", + "email": "hello@mokoconsulting.tech" + } + ], + "require": { + "php": ">=8.1" + }, + "require-dev": { + "squizlabs/php_codesniffer": "^3.0", + "phpstan/phpstan": "^2.0", + "joomla/coding-standards": "dev-main" + }, + "config": { + "allow-plugins": { + "dealerdirect/phpcodesniffer-composer-installer": true + } + } +} From 6c7740da8c87cf68e3c9d1acbc10239f84b6e58c Mon Sep 17 00:00:00 2001 From: Jonathan Miller <1+jmiller@noreply.git.mokoconsulting.tech> Date: Sun, 28 Jun 2026 07:11:07 +0000 Subject: [PATCH 22/64] chore: add phpstan.neon from Template-Joomla --- phpstan.neon | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 phpstan.neon diff --git a/phpstan.neon b/phpstan.neon new file mode 100644 index 0000000..d6e669c --- /dev/null +++ b/phpstan.neon @@ -0,0 +1,20 @@ +# Copyright (C) 2026 Moko Consulting +# SPDX-License-Identifier: GPL-3.0-or-later +# FILE INFORMATION +# DEFGROUP: Config.StaticAnalysis +# INGROUP: Development +# BRIEF: PHPStan configuration for Joomla extension static analysis + +parameters: + level: 5 + paths: + - src + scanDirectories: + # Joomla framework stubs (if available) + - vendor/joomla + ignoreErrors: + # Joomla service-container architecture: Factory/Container returns mixed + - '#Call to an undefined method Joomla\\CMS\\Application\\.*::get#i' + - '#Call to method .* on an unknown class Joomla\\Cms\\Extension\\.*#' + # Joomla MVC pattern: Table::getInstance returns Table|bool + - '#Method Joomla\\CMS\\Table\\Table::getInstance#' From f026386c2695a582b03db9134d0f27afba112fd2 Mon Sep 17 00:00:00 2001 From: Jonathan Miller <1+jmiller@noreply.git.mokoconsulting.tech> Date: Sun, 28 Jun 2026 07:11:44 +0000 Subject: [PATCH 23/64] chore: add CODE_OF_CONDUCT.md from Template-Joomla --- CODE_OF_CONDUCT.md | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 CODE_OF_CONDUCT.md diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..2ecc653 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,35 @@ + + +# Contributor Covenant Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, caste, colour, religion, or sexual +identity and orientation. + +## Our Standards + +Examples of behaviour that contributes to a positive environment: + +- Using welcoming and inclusive language +- Being respectful of differing viewpoints and experiences +- Accepting constructive criticism gracefully +- Focusing on what is best for the community + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behaviour may be +reported by contacting the project team at +[info@mokoconsulting.tech](mailto:info@mokoconsulting.tech). + +## Attribution + +This Code of Conduct is adapted from the +[Contributor Covenant](https://www.contributor-covenant.org), version 2.1. From 598ba7742215623596207f6729abbb337a130ec2 Mon Sep 17 00:00:00 2001 From: Jonathan Miller <1+jmiller@noreply.git.mokoconsulting.tech> Date: Sun, 28 Jun 2026 07:13:42 +0000 Subject: [PATCH 24/64] chore: add CONTRIBUTING.md from Template-Joomla --- CONTRIBUTING.md | 88 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 88 insertions(+) create mode 100644 CONTRIBUTING.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..e8697d2 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,88 @@ + + +# Contributing to Moko Consulting Projects + +Thank you for your interest in contributing! This guide explains our workflow, +conventions, and how to get your changes merged. + +## Branching Workflow + +We use a **stability-gated** branching model: + +``` +feature/* ──── PR ───→ dev + │ RC cut → rc → main +fix/* ───────── PR ────────────┘ +``` + +1. **Create a branch** from `dev`: + - `feature/` for new functionality + - `fix/` for bug fixes + - `chore/` for maintenance +2. **Open a PR** into `dev`. +3. **CI must pass** before merge. +4. **Release cuts**: `dev → rc → main` are handled by maintainers. + +> **Never commit directly to `main` or `dev`.** + +## Version Policy + +All repositories use the **XX.YY.ZZ** versioning scheme (two-digit segments): + +- `XX` -- major (breaking changes) +- `YY` -- minor (new features, backward-compatible) +- `ZZ` -- patch (bug fixes, security patches) + +**Stability suffixes** may be appended during pre-release: + +| Suffix | Meaning | Example | +|---|---|---| +| `-alpha.N` | Early testing | `06.01.00-alpha.1` | +| `-beta.N` | Feature complete | `06.01.00-beta.2` | +| `-rc.N` | Release candidate | `06.01.00-rc.1` | +| *(none)* | Stable release | `06.01.00` | + +## Auto-Bump + +Version bumps are **automated** via the `auto-bump` workflow: + +- Merges into `dev` trigger a minor/patch bump. +- The workflow updates all version references (manifests, changelog, etc.). +- **Do not manually edit version numbers** -- let the workflow handle it. + +## Commit Messages + +We follow [Conventional Commits](https://www.conventionalcommits.org/): + +``` +(): + + + + +``` + +**Types**: `feat`, `fix`, `docs`, `style`, `refactor`, `perf`, `test`, `chore`, `ci`, `build`, `revert` + +## Pull Request Checklist + +Before submitting a PR, ensure: + +- [ ] Branch is based on latest `dev` +- [ ] Commit messages follow conventional commits +- [ ] CHANGELOG.md updated under `[Unreleased]` +- [ ] No `TODO.md`, `.claude/`, `.mcp.json`, or minified files included +- [ ] Code follows [MokoStandards](https://git.mokoconsulting.tech/MokoConsulting/MokoStandards) +- [ ] All CI checks pass + +## Code of Conduct + +All contributors are expected to follow our [Code of Conduct](CODE_OF_CONDUCT.md). + +## Questions? + +Open a [Question issue](../../issues/new?template=question.md) or contact +us at [hello@mokoconsulting.tech](mailto:hello@mokoconsulting.tech). From 69fde3f53274a6b2c77957d293581441eab6534f Mon Sep 17 00:00:00 2001 From: Jonathan Miller <1+jmiller@noreply.git.mokoconsulting.tech> Date: Sun, 28 Jun 2026 07:15:09 +0000 Subject: [PATCH 25/64] chore: add GOVERNANCE.md from Template-Joomla --- GOVERNANCE.md | 49 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 GOVERNANCE.md diff --git a/GOVERNANCE.md b/GOVERNANCE.md new file mode 100644 index 0000000..aeb3e4b --- /dev/null +++ b/GOVERNANCE.md @@ -0,0 +1,49 @@ + + +# Governance + +## Project Leadership + +This repository is maintained by **Moko Consulting** under a **sole operator** model. + +- **Lead Maintainer**: Jonathan Miller (@jmiller) +- **Organisation**: [Moko Consulting](https://mokoconsulting.tech) + +## Decision Making + +- All architectural decisions are made by the lead maintainer. +- Community feedback is welcome via [RFC issues](../../issues/new?template=rfc.md). +- Breaking changes are documented via [ADR issues](../../issues/new?template=adr.md). + +## Contribution Policy + +- **All changes** must go through a pull request (PR). +- **CI checks** are mandatory before merge. +- **Direct push** to `main` and `dev` is restricted to automated workflows. + +## Code of Conduct + +All participants must adhere to our [Code of Conduct](CODE_OF_CONDUCT.md). + +## Licensing + +All contributions are licensed under the same license as the project +(GPL-3.0-or-later unless otherwise stated in the repository root). + +## Security + +Security vulnerabilities should be reported privately. +See [SECURITY.md](SECURITY.md) for details. + +## Dispute Resolution + +Disputes are resolved by the lead maintainer. For escalation, +contact [info@mokoconsulting.tech](mailto:info@mokoconsulting.tech). + +## Changes to Governance + +This document may be updated at any time by the lead maintainer. +Significant changes will be announced via an RFC issue. From 8d9ccbe448a958692cbe10978107c8f4399c9043 Mon Sep 17 00:00:00 2001 From: Jonathan Miller <1+jmiller@noreply.git.mokoconsulting.tech> Date: Sun, 28 Jun 2026 07:15:34 +0000 Subject: [PATCH 26/64] chore: add SECURITY.md from Template-Joomla --- SECURITY.md | 90 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 90 insertions(+) create mode 100644 SECURITY.md diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..7f29d29 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,90 @@ + + +# Security Policy + +## Supported Versions + +| Version | Supported | +|---|---| +| Latest stable | ✅ Full support | +| Previous major | ⚠️ Critical fixes only | +| Older | ❌ No support | + +## Reporting a Vulnerability + +**Do not report security vulnerabilities via public issues.** + +Instead, please report them privately: + +1. **Email**: [security@mokoconsulting.tech](mailto:security@mokoconsulting.tech) +2. **Subject**: `[SECURITY] - ` + +### What to Include + +- Description of the vulnerability +- Steps to reproduce +- Affected versions +- Potential impact +- Suggested fix (if any) + +## Severity Classification + +| Severity | Description | Response Time | +|---|---|---| +| **Critical** | Remote code execution, SQL injection, auth bypass | 24 hours | +| **High** | XSS, CSRF, privilege escalation | 48 hours | +| **Medium** | Information disclosure, path traversal | 72 hours | +| **Low** | Best practice violation, hardening suggestion | Next release | + +## Remediation Timeline + +1. **Acknowledgement**: Within 24 hours of report +2. **Assessment**: Within 72 hours +3. **Fix development**: Based on severity +4. **Release**: Patch release with security advisory +5. **Disclosure**: Coordinated disclosure after fix is available + +## Security Best Practices + +### For Contributors + +- Never commit secrets, credentials, or API keys +- Use parameterised queries (no raw SQL concatenation) +- Validate and sanitise all user input +- Follow Joomla API for access control checks +- Use Joomla's `HTMLHelper` for output escaping +- Include SPDX license headers in all source files + +### For Users + +- Keep Joomla and all extensions updated +- Use strong, unique passwords +- Enable two-factor authentication +- Review file permissions regularly +- Monitor Joomla error logs + +## Security Updates + +Security patches are delivered through the standard update channel. +Critical fixes may receive an emergency out-of-band release. + +## Responsible Disclosure + +We follow coordinated disclosure practices: + +- We will work with reporters to understand and reproduce the issue +- We will develop and test a fix +- We will credit reporters (with permission) in security advisories +- We ask that reporters allow reasonable time for a fix before public disclosure + +## Contact + +- **Security team**: [security@mokoconsulting.tech](mailto:security@mokoconsulting.tech) +- **General**: [hello@mokoconsulting.tech](mailto:hello@mokoconsulting.tech) + +--- + +Thank you for helping keep Moko Consulting projects secure. From daa4074cb8580e7a78978793aa8dc377bfa62c0a Mon Sep 17 00:00:00 2001 From: "gitea-actions[bot]" Date: Sun, 28 Jun 2026 07:16:21 +0000 Subject: [PATCH 27/64] chore(version): pre-release bump to 06.00.02-dev [skip ci] --- .mokogitea/workflows/issue-branch.yml | 2 +- source/packages/com_mokosuitefield/mokosuitefield.xml | 2 +- source/packages/plg_system_mokosuitefield/mokosuitefield.xml | 2 +- .../packages/plg_webservices_mokosuitefield/mokosuitefield.xml | 2 +- source/pkg_mokosuitefield.xml | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.mokogitea/workflows/issue-branch.yml b/.mokogitea/workflows/issue-branch.yml index 1b074d8..5e79fd2 100644 --- a/.mokogitea/workflows/issue-branch.yml +++ b/.mokogitea/workflows/issue-branch.yml @@ -5,7 +5,7 @@ # FILE INFORMATION # DEFGROUP: Gitea.Workflow # INGROUP: mokocli.Automation -# VERSION: 06.00.01 +# VERSION: 06.00.02 # BRIEF: Auto-create feature branch when an issue is opened name: "Universal: Issue Branch" diff --git a/source/packages/com_mokosuitefield/mokosuitefield.xml b/source/packages/com_mokosuitefield/mokosuitefield.xml index 1835567..dc0df20 100644 --- a/source/packages/com_mokosuitefield/mokosuitefield.xml +++ b/source/packages/com_mokosuitefield/mokosuitefield.xml @@ -5,7 +5,7 @@ 2026-06-27 Copyright (C) 2026 Moko Consulting. GPL-3.0-or-later - 06.00.01 + 06.00.02 Moko\Component\MokoSuiteField servicessrctmpl diff --git a/source/packages/plg_system_mokosuitefield/mokosuitefield.xml b/source/packages/plg_system_mokosuitefield/mokosuitefield.xml index 92333fa..77c907d 100644 --- a/source/packages/plg_system_mokosuitefield/mokosuitefield.xml +++ b/source/packages/plg_system_mokosuitefield/mokosuitefield.xml @@ -8,7 +8,7 @@ GPL-3.0-or-later hello@mokoconsulting.tech https://mokoconsulting.tech - 06.00.01 + 06.00.02 8.3 PLG_SYSTEM_MOKOSUITEFIELD_DESC Moko\Plugin\System\MokoSuiteField diff --git a/source/packages/plg_webservices_mokosuitefield/mokosuitefield.xml b/source/packages/plg_webservices_mokosuitefield/mokosuitefield.xml index cc901f2..6e365d7 100644 --- a/source/packages/plg_webservices_mokosuitefield/mokosuitefield.xml +++ b/source/packages/plg_webservices_mokosuitefield/mokosuitefield.xml @@ -3,7 +3,7 @@ Web Services - MokoSuite Field mokosuitefield Moko Consulting - 06.00.01 + 06.00.02 GPL-3.0-or-later Moko\Plugin\WebServices\MokoSuiteField srcservices diff --git a/source/pkg_mokosuitefield.xml b/source/pkg_mokosuitefield.xml index c5fa1df..a325bd2 100644 --- a/source/pkg_mokosuitefield.xml +++ b/source/pkg_mokosuitefield.xml @@ -2,7 +2,7 @@ Package - MokoSuite Field mokosuitefield - 06.00.01 + 06.00.02 2026-06-27 Moko Consulting hello@mokoconsulting.tech From 85bad541903cdf805dfe3a60b01d24b82d9d4e7c Mon Sep 17 00:00:00 2001 From: Jonathan Miller <1+jmiller@noreply.git.mokoconsulting.tech> Date: Sun, 28 Jun 2026 07:17:32 +0000 Subject: [PATCH 28/64] chore: add issue templates from Template-Joomla --- .mokogitea/ISSUE_TEMPLATE/bug_report.md | 48 +++++++++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 .mokogitea/ISSUE_TEMPLATE/bug_report.md diff --git a/.mokogitea/ISSUE_TEMPLATE/bug_report.md b/.mokogitea/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..38a16a7 --- /dev/null +++ b/.mokogitea/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,48 @@ +--- +name: Bug Report +about: Report a bug or issue with the project +title: '[BUG] ' +labels: 'bug' +assignees: '' + +--- + + +## Bug Description +A clear and concise description of what the bug is. + +## Steps to Reproduce +1. Go to '...' +2. Click on '...' +3. Scroll down to '...' +4. See error + +## Expected Behavior +A clear and concise description of what you expected to happen. + +## Actual Behavior +A clear and concise description of what actually happened. + +## Screenshots +If applicable, add screenshots to help explain your problem. + +## Environment +- **Project**: [e.g., MokoDoliTools, moko-cassiopeia] +- **Version**: [e.g., 1.2.3] +- **Platform**: [e.g., Dolibarr 18.0, Joomla 5.0] +- **PHP Version**: [e.g., 8.1] +- **Database**: [e.g., MySQL 8.0, PostgreSQL 14] +- **Browser** (if applicable): [e.g., Chrome 120, Firefox 121] +- **OS**: [e.g., Ubuntu 22.04, Windows 11] + +## Additional Context +Add any other context about the problem here. + +## Possible Solution +If you have suggestions on how to fix the issue, please describe them here. + +## Checklist +- [ ] I have searched for similar issues before creating this one +- [ ] I have provided all the requested information +- [ ] I have tested this on the latest stable version +- [ ] I have checked the documentation and couldn't find a solution From 3a4c0181557f2ae786b62595b3d2535ba3a68236 Mon Sep 17 00:00:00 2001 From: Jonathan Miller <1+jmiller@noreply.git.mokoconsulting.tech> Date: Sun, 28 Jun 2026 07:17:59 +0000 Subject: [PATCH 29/64] chore: add feature_request template from Template-Joomla --- .mokogitea/ISSUE_TEMPLATE/feature_request.md | 51 ++++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 .mokogitea/ISSUE_TEMPLATE/feature_request.md diff --git a/.mokogitea/ISSUE_TEMPLATE/feature_request.md b/.mokogitea/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..7b76dc9 --- /dev/null +++ b/.mokogitea/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,51 @@ +--- +name: Feature Request +about: Suggest a new feature or enhancement +title: '[FEATURE] ' +labels: 'enhancement' +assignees: '' + +--- + + +## Feature Description +A clear and concise description of the feature you'd like to see. + +## Problem or Use Case +Describe the problem this feature would solve or the use case it addresses. +Ex. I'm always frustrated when [...] + +## Proposed Solution +A clear and concise description of what you want to happen. + +## Alternative Solutions +A clear and concise description of any alternative solutions or features you've considered. + +## Benefits +Describe how this feature would benefit users: +- Who would use this feature? +- What problems does it solve? +- What value does it add? + +## Implementation Details (Optional) +If you have ideas about how this could be implemented, share them here: +- Technical approach +- Files/components that might need changes +- Any concerns or challenges you foresee + +## Additional Context +Add any other context, mockups, or screenshots about the feature request here. + +## Relevant Standards +Does this relate to any standards in [MokoStandards](https://git.mokoconsulting.tech/MokoConsulting/MokoStandards)? +- [ ] Accessibility (WCAG 2.1 AA) +- [ ] Localization (en_US/en_GB) +- [ ] Security best practices +- [ ] Code quality standards +- [ ] Other: [specify] + +## Checklist +- [ ] I have searched for similar feature requests before creating this one +- [ ] I have clearly described the use case and benefits +- [ ] I have considered alternative solutions +- [ ] This feature aligns with the project's goals and scope From 408516c3051e3a1f002af8a8981f9904b067a709 Mon Sep 17 00:00:00 2001 From: Jonathan Miller <1+jmiller@noreply.git.mokoconsulting.tech> Date: Sun, 28 Jun 2026 07:19:11 +0000 Subject: [PATCH 30/64] chore: add issue template config from Template-Joomla --- .mokogitea/ISSUE_TEMPLATE/config.yml | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 .mokogitea/ISSUE_TEMPLATE/config.yml diff --git a/.mokogitea/ISSUE_TEMPLATE/config.yml b/.mokogitea/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..d4d49ec --- /dev/null +++ b/.mokogitea/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,18 @@ +--- +blank_issues_enabled: true +contact_links: + - name: 💼 Enterprise Support + url: https://mokoconsulting.tech/enterprise + about: Enterprise-level support and consultation services + - name: 💬 Ask a Question + url: https://mokoconsulting.tech/ + about: Get help or ask questions through our website + - name: 📚 MokoStandards Documentation + url: https://git.mokoconsulting.tech/MokoConsulting/moko-platform + about: View our coding standards and best practices + - name: 🔒 Report a Security Vulnerability + url: https://git.mokoconsulting.tech/mokoconsulting-tech/.github-private/security/advisories/new + about: Report security vulnerabilities privately (for critical issues) + - name: 💡 Community Discussions + url: https://github.com/orgs/mokoconsulting-tech/discussions + about: Join community discussions and Q&A From 504452e7acf5f7ee1b85fbf88d1396c65d09ee6c Mon Sep 17 00:00:00 2001 From: Jonathan Miller <1+jmiller@noreply.git.mokoconsulting.tech> Date: Sun, 28 Jun 2026 07:19:24 +0000 Subject: [PATCH 31/64] chore: add joomla_issue template from Template-Joomla --- .mokogitea/ISSUE_TEMPLATE/joomla_issue.md | 87 +++++++++++++++++++++++ 1 file changed, 87 insertions(+) create mode 100644 .mokogitea/ISSUE_TEMPLATE/joomla_issue.md diff --git a/.mokogitea/ISSUE_TEMPLATE/joomla_issue.md b/.mokogitea/ISSUE_TEMPLATE/joomla_issue.md new file mode 100644 index 0000000..d808f79 --- /dev/null +++ b/.mokogitea/ISSUE_TEMPLATE/joomla_issue.md @@ -0,0 +1,87 @@ +--- +name: Joomla Extension Issue +about: Report an issue with a Joomla extension +title: '[JOOMLA] ' +labels: 'joomla' +assignees: '' + +--- + + +## Issue Type +- [ ] Component Issue +- [ ] Module Issue +- [ ] Plugin Issue +- [ ] Template Issue + +## Extension Details +- **Extension Name**: [e.g., moko-cassiopeia] +- **Extension Version**: [e.g., 1.2.3] +- **Extension Type**: [Component / Module / Plugin / Template] + +## Joomla Environment +- **Joomla Version**: [e.g., 4.4.0, 5.0.0] +- **PHP Version**: [e.g., 8.1.0] +- **Database**: [MySQL / PostgreSQL / MariaDB] +- **Database Version**: [e.g., 8.0] +- **Server**: [Apache / Nginx / IIS] +- **Hosting**: [Shared / VPS / Dedicated / Cloud] + +## Issue Description +Provide a clear and detailed description of the issue. + +## Steps to Reproduce +1. Go to '...' +2. Click on '...' +3. Configure '...' +4. See error + +## Expected Behavior +What you expected to happen. + +## Actual Behavior +What actually happened. + +## Error Messages +``` +# Paste any error messages from Joomla error logs +# Location: administrator/logs/error.php +``` + +## Browser Console Errors +```javascript +// Paste any JavaScript console errors (F12 in browser) +``` + +## Screenshots +Add screenshots to help explain the issue. + +## Configuration +```ini +# Paste extension configuration (sanitize sensitive data) +``` + +## Installed Extensions +List other installed extensions that might conflict: +- Extension 1 (version) +- Extension 2 (version) + +## Template Overrides +- [ ] Using template overrides +- [ ] Custom CSS +- [ ] Custom JavaScript + +## Additional Context +- **Multilingual Site**: [Yes / No] +- **Cache Enabled**: [Yes / No] +- **Debug Mode**: [Yes / No] +- **SEF URLs**: [Yes / No] + +## Checklist +- [ ] I have cleared Joomla cache +- [ ] I have disabled other extensions to test for conflicts +- [ ] I have checked Joomla error logs +- [ ] I have tested with a default Joomla template +- [ ] I have checked browser console for JavaScript errors +- [ ] I have searched for similar issues +- [ ] I am using a supported Joomla version From 8e4680824d3290238d810d74b2c7b28fd08c6d39 Mon Sep 17 00:00:00 2001 From: Jonathan Miller <1+jmiller@noreply.git.mokoconsulting.tech> Date: Sun, 28 Jun 2026 07:20:35 +0000 Subject: [PATCH 32/64] chore: add ADR issue template from Template-Joomla --- .mokogitea/ISSUE_TEMPLATE/adr.md | 110 +++++++++++++++++++++++++++++++ 1 file changed, 110 insertions(+) create mode 100644 .mokogitea/ISSUE_TEMPLATE/adr.md diff --git a/.mokogitea/ISSUE_TEMPLATE/adr.md b/.mokogitea/ISSUE_TEMPLATE/adr.md new file mode 100644 index 0000000..eb40760 --- /dev/null +++ b/.mokogitea/ISSUE_TEMPLATE/adr.md @@ -0,0 +1,110 @@ +--- +name: Architecture Decision Record (ADR) +about: Propose or document an architectural decision +title: '[ADR] ' +labels: 'architecture, decision' +assignees: '' + +--- + + +## ADR Number +ADR-XXXX + +## Status +- [ ] Proposed +- [ ] Accepted +- [ ] Deprecated +- [ ] Superseded by ADR-XXXX + +## Context +Describe the issue or problem that motivates this decision. + +## Decision +State the architecture decision and provide rationale. + +## Consequences +### Positive +- List positive consequences + +### Negative +- List negative consequences or trade-offs + +### Neutral +- List neutral aspects + +## Alternatives Considered +### Alternative 1 +- Description +- Pros +- Cons +- Why not chosen + +### Alternative 2 +- Description +- Pros +- Cons +- Why not chosen + +## Implementation Plan +1. Step 1 +2. Step 2 +3. Step 3 + +## Stakeholders +- **Decision Makers**: @user1, @user2 +- **Consulted**: @user3, @user4 +- **Informed**: team-name + +## Technical Details +### Architecture Diagram +``` +[Add diagram or link] +``` + +### Dependencies +- Dependency 1 +- Dependency 2 + +### Impact Analysis +- **Performance**: [Impact description] +- **Security**: [Impact description] +- **Scalability**: [Impact description] +- **Maintainability**: [Impact description] + +## Testing Strategy +- [ ] Unit tests +- [ ] Integration tests +- [ ] Performance tests +- [ ] Security tests + +## Documentation +- [ ] Architecture documentation updated +- [ ] API documentation updated +- [ ] Developer guide updated +- [ ] Runbook created + +## Migration Path +Describe how to migrate from current state to new architecture. + +## Rollback Plan +Describe how to rollback if issues occur. + +## Timeline +- **Proposal Date**: +- **Decision Date**: +- **Implementation Start**: +- **Expected Completion**: + +## References +- Related ADRs: +- External resources: +- RFCs: + +## Review Checklist +- [ ] Aligns with enterprise architecture principles +- [ ] Security implications reviewed +- [ ] Performance implications reviewed +- [ ] Cost implications reviewed +- [ ] Compliance requirements met +- [ ] Team consensus achieved From 133abdd5825b3beffff0094e8c65fa82192618c7 Mon Sep 17 00:00:00 2001 From: Jonathan Miller <1+jmiller@noreply.git.mokoconsulting.tech> Date: Sun, 28 Jun 2026 07:21:15 +0000 Subject: [PATCH 33/64] chore: add documentation issue template from Template-Joomla --- .mokogitea/ISSUE_TEMPLATE/documentation.md | 52 ++++++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 .mokogitea/ISSUE_TEMPLATE/documentation.md diff --git a/.mokogitea/ISSUE_TEMPLATE/documentation.md b/.mokogitea/ISSUE_TEMPLATE/documentation.md new file mode 100644 index 0000000..ed4dabc --- /dev/null +++ b/.mokogitea/ISSUE_TEMPLATE/documentation.md @@ -0,0 +1,52 @@ +--- +name: Documentation Issue +about: Report an issue with documentation +title: '[DOCS] ' +labels: 'documentation' +assignees: '' + +--- + + +## Documentation Issue + +**Location**: + + +## Issue Type + +- [ ] Typo or grammar error +- [ ] Outdated information +- [ ] Missing documentation +- [ ] Unclear explanation +- [ ] Broken links +- [ ] Missing examples +- [ ] Other (specify below) + +## Description + + +## Current Content + +``` +Current text here +``` + +## Suggested Improvement + +``` +Suggested text here +``` + +## Additional Context + + +## Standards Alignment +- [ ] Follows MokoStandards documentation guidelines +- [ ] Uses en_US/en_GB localization +- [ ] Includes proper SPDX headers where applicable + +## Checklist +- [ ] I have searched for similar documentation issues +- [ ] I have provided a clear description +- [ ] I have suggested an improvement (if applicable) From 397c4e022d2c385ef5fb2804eb27f98226589f2b Mon Sep 17 00:00:00 2001 From: Jonathan Miller <1+jmiller@noreply.git.mokoconsulting.tech> Date: Sun, 28 Jun 2026 07:22:00 +0000 Subject: [PATCH 34/64] chore: add question issue template from Template-Joomla --- .mokogitea/ISSUE_TEMPLATE/question.md | 82 +++++++++++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 .mokogitea/ISSUE_TEMPLATE/question.md diff --git a/.mokogitea/ISSUE_TEMPLATE/question.md b/.mokogitea/ISSUE_TEMPLATE/question.md new file mode 100644 index 0000000..3175013 --- /dev/null +++ b/.mokogitea/ISSUE_TEMPLATE/question.md @@ -0,0 +1,82 @@ +--- +name: Question +about: Ask a question about usage, features, or best practices +title: '[QUESTION] ' +labels: ['question'] +assignees: ['jmiller'] +--- + + +## Question + +**Your question:** + + +## Context + +**What are you trying to accomplish?** + + +**What have you already tried?** + + +**Category**: +- [ ] Script usage +- [ ] Configuration +- [ ] Workflow setup +- [ ] Documentation interpretation +- [ ] Best practices +- [ ] Integration +- [ ] Other: __________ + +## Environment (if relevant) + +**Your setup**: +- Operating System: +- Version: + +## What You've Researched + +**Documentation reviewed**: +- [ ] README.md +- [ ] Project documentation +- [ ] Other (specify): __________ + +**Similar issues/questions found**: +- # +- # + +## Expected Outcome + +**What result are you hoping for?** + + +## Code/Configuration Samples + +**Relevant code or configuration** (if applicable): + +```bash +# Your code here +``` + +## Additional Context + +**Any other relevant information:** + + +**Screenshots** (if helpful): + + +## Urgency + +- [ ] Urgent (blocking work) +- [ ] Normal (can work on other things meanwhile) +- [ ] Low priority (just curious) + +## Checklist + +- [ ] I have searched existing issues and discussions +- [ ] I have reviewed relevant documentation +- [ ] I have provided sufficient context +- [ ] I have included code/configuration samples if relevant +- [ ] This is a genuine question (not a bug report or feature request) From 7892fdb8d90d3e1bdf38bec42c40d5925eda33fb Mon Sep 17 00:00:00 2001 From: Jonathan Miller <1+jmiller@noreply.git.mokoconsulting.tech> Date: Sun, 28 Jun 2026 07:22:53 +0000 Subject: [PATCH 35/64] chore: add RFC issue template from Template-Joomla --- .mokogitea/ISSUE_TEMPLATE/rfc.md | 126 +++++++++++++++++++++++++++++++ 1 file changed, 126 insertions(+) create mode 100644 .mokogitea/ISSUE_TEMPLATE/rfc.md diff --git a/.mokogitea/ISSUE_TEMPLATE/rfc.md b/.mokogitea/ISSUE_TEMPLATE/rfc.md new file mode 100644 index 0000000..6f09af7 --- /dev/null +++ b/.mokogitea/ISSUE_TEMPLATE/rfc.md @@ -0,0 +1,126 @@ +--- +name: Request for Comments (RFC) +about: Propose a significant change for community discussion +title: '[RFC] ' +labels: 'rfc, discussion' +assignees: '' + +--- + + +## RFC Summary +One-paragraph summary of the proposal. + +## Motivation +Why are we doing this? What use cases does it support? What is the expected outcome? + +## Detailed Design +### Overview +Provide a detailed explanation of the proposed change. + +### API Changes (if applicable) +```php +// Before +function oldApi($param1) { } + +// After +function newApi($param1, $param2) { } +``` + +### User Experience Changes +Describe how users will interact with this change. + +### Implementation Approach +High-level implementation strategy. + +## Drawbacks +Why should we *not* do this? + +## Alternatives +What other designs have been considered? What is the impact of not doing this? + +### Alternative 1 +- Description +- Trade-offs + +### Alternative 2 +- Description +- Trade-offs + +## Adoption Strategy +How will existing users adopt this? Is this a breaking change? + +### Migration Guide +```bash +# Steps to migrate +``` + +### Deprecation Timeline +- **Announcement**: +- **Deprecation**: +- **Removal**: + +## Unresolved Questions +- Question 1 +- Question 2 + +## Future Possibilities +What future work does this enable? + +## Impact Assessment +### Performance +Expected performance impact. + +### Security +Security considerations and implications. + +### Compatibility +- **Backward Compatible**: [Yes / No] +- **Breaking Changes**: [List] + +### Maintenance +Long-term maintenance considerations. + +## Community Input +### Stakeholders +- [ ] Core team +- [ ] Module developers +- [ ] End users +- [ ] Enterprise customers + +### Feedback Period +**Duration**: [e.g., 2 weeks] +**Deadline**: [date] + +## Implementation Timeline +### Phase 1: Design +- [ ] RFC discussion +- [ ] Design finalization +- [ ] Approval + +### Phase 2: Implementation +- [ ] Core implementation +- [ ] Tests +- [ ] Documentation + +### Phase 3: Release +- [ ] Beta release +- [ ] Feedback collection +- [ ] Stable release + +## Success Metrics +How will we measure success? +- Metric 1 +- Metric 2 + +## References +- Related RFCs: +- External documentation: +- Prior art: + +## Open Questions for Community +1. Question 1? +2. Question 2? + +--- +**Note**: This RFC is open for community discussion. Please provide feedback in the comments below. From 9b01493ed9655014d74e19b03492b045cd761043 Mon Sep 17 00:00:00 2001 From: Jonathan Miller <1+jmiller@noreply.git.mokoconsulting.tech> Date: Sun, 28 Jun 2026 07:23:56 +0000 Subject: [PATCH 36/64] chore: add security issue template from Template-Joomla --- .mokogitea/ISSUE_TEMPLATE/security.md | 51 +++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 .mokogitea/ISSUE_TEMPLATE/security.md diff --git a/.mokogitea/ISSUE_TEMPLATE/security.md b/.mokogitea/ISSUE_TEMPLATE/security.md new file mode 100644 index 0000000..f57b284 --- /dev/null +++ b/.mokogitea/ISSUE_TEMPLATE/security.md @@ -0,0 +1,51 @@ +--- +name: Security Vulnerability Report +about: Report a security vulnerability (use only for non-critical issues) +title: '[SECURITY] ' +labels: 'security' +assignees: '' + +--- + + +## ⚠️ IMPORTANT: Private Disclosure Required + +**For critical security vulnerabilities, DO NOT use this template.** +Follow the process in [SECURITY.md](../SECURITY.md) for responsible disclosure. + +Use this template only for: +- Security improvements +- Non-critical security suggestions +- Security documentation updates + +--- + +## Security Issue + +**Severity**: + + +## Description + + +## Affected Components + + +## Suggested Mitigation + + +## Standards Reference +Does this relate to security standards in [MokoStandards](https://git.mokoconsulting.tech/MokoConsulting/MokoStandards)? +- [ ] SPDX license identifiers +- [ ] Secret management +- [ ] Dependency security +- [ ] Access control +- [ ] Other: [specify] + +## Additional Context + + +## Checklist +- [ ] This is NOT a critical vulnerability requiring private disclosure +- [ ] I have reviewed the SECURITY.md policy +- [ ] I have provided sufficient detail for evaluation From 08c2974ce7f2010306ed0a208650d9f8b70caf34 Mon Sep 17 00:00:00 2001 From: Jonathan Miller <1+jmiller@noreply.git.mokoconsulting.tech> Date: Sun, 28 Jun 2026 07:24:17 +0000 Subject: [PATCH 37/64] chore: add version issue template from Template-Joomla --- .mokogitea/ISSUE_TEMPLATE/version.md | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 .mokogitea/ISSUE_TEMPLATE/version.md diff --git a/.mokogitea/ISSUE_TEMPLATE/version.md b/.mokogitea/ISSUE_TEMPLATE/version.md new file mode 100644 index 0000000..6328421 --- /dev/null +++ b/.mokogitea/ISSUE_TEMPLATE/version.md @@ -0,0 +1,24 @@ +--- +name: Version Bump +about: Request or track a version change +title: '[VERSION] ' +labels: 'version, type: version' +assignees: 'jmiller' +--- + +## Version Change + +**Current version**: +**Requested version**: +**Change type**: + +## Reason + + + +## Checklist + +- [ ] README.md `VERSION:` field updated +- [ ] CHANGELOG.md entry added +- [ ] Module descriptor version updated (Dolibarr: `$this->version`, Joomla: ``) +- [ ] All file headers will be auto-propagated by `sync-version-on-merge` workflow From cd8ef1c1a698f3a64c857c48251cd0069544810b Mon Sep 17 00:00:00 2001 From: "gitea-actions[bot]" Date: Sun, 28 Jun 2026 07:27:04 +0000 Subject: [PATCH 38/64] chore(version): pre-release bump to 06.00.03-dev [skip ci] --- .mokogitea/workflows/issue-branch.yml | 2 +- source/packages/com_mokosuitefield/mokosuitefield.xml | 2 +- source/packages/plg_system_mokosuitefield/mokosuitefield.xml | 2 +- .../packages/plg_webservices_mokosuitefield/mokosuitefield.xml | 2 +- source/pkg_mokosuitefield.xml | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.mokogitea/workflows/issue-branch.yml b/.mokogitea/workflows/issue-branch.yml index 5e79fd2..3cfa08e 100644 --- a/.mokogitea/workflows/issue-branch.yml +++ b/.mokogitea/workflows/issue-branch.yml @@ -5,7 +5,7 @@ # FILE INFORMATION # DEFGROUP: Gitea.Workflow # INGROUP: mokocli.Automation -# VERSION: 06.00.02 +# VERSION: 06.00.03 # BRIEF: Auto-create feature branch when an issue is opened name: "Universal: Issue Branch" diff --git a/source/packages/com_mokosuitefield/mokosuitefield.xml b/source/packages/com_mokosuitefield/mokosuitefield.xml index dc0df20..cc0afe1 100644 --- a/source/packages/com_mokosuitefield/mokosuitefield.xml +++ b/source/packages/com_mokosuitefield/mokosuitefield.xml @@ -5,7 +5,7 @@ 2026-06-27 Copyright (C) 2026 Moko Consulting. GPL-3.0-or-later - 06.00.02 + 06.00.03 Moko\Component\MokoSuiteField servicessrctmpl diff --git a/source/packages/plg_system_mokosuitefield/mokosuitefield.xml b/source/packages/plg_system_mokosuitefield/mokosuitefield.xml index 77c907d..fe8bc13 100644 --- a/source/packages/plg_system_mokosuitefield/mokosuitefield.xml +++ b/source/packages/plg_system_mokosuitefield/mokosuitefield.xml @@ -8,7 +8,7 @@ GPL-3.0-or-later hello@mokoconsulting.tech https://mokoconsulting.tech - 06.00.02 + 06.00.03 8.3 PLG_SYSTEM_MOKOSUITEFIELD_DESC Moko\Plugin\System\MokoSuiteField diff --git a/source/packages/plg_webservices_mokosuitefield/mokosuitefield.xml b/source/packages/plg_webservices_mokosuitefield/mokosuitefield.xml index 6e365d7..e6353ba 100644 --- a/source/packages/plg_webservices_mokosuitefield/mokosuitefield.xml +++ b/source/packages/plg_webservices_mokosuitefield/mokosuitefield.xml @@ -3,7 +3,7 @@ Web Services - MokoSuite Field mokosuitefield Moko Consulting - 06.00.02 + 06.00.03 GPL-3.0-or-later Moko\Plugin\WebServices\MokoSuiteField srcservices diff --git a/source/pkg_mokosuitefield.xml b/source/pkg_mokosuitefield.xml index a325bd2..ea57ab6 100644 --- a/source/pkg_mokosuitefield.xml +++ b/source/pkg_mokosuitefield.xml @@ -2,7 +2,7 @@ Package - MokoSuite Field mokosuitefield - 06.00.02 + 06.00.03 2026-06-27 Moko Consulting hello@mokoconsulting.tech From 9ba3c26ea2a87e665700f62e23aae325e675f012 Mon Sep 17 00:00:00 2001 From: Jonathan Miller <1+jmiller@noreply.git.mokoconsulting.tech> Date: Sun, 28 Jun 2026 07:27:26 +0000 Subject: [PATCH 39/64] chore: add branch-protection.yml from Template-Joomla --- .mokogitea/branch-protection.yml | 251 +++++++++++++++++++++++++++++++ 1 file changed, 251 insertions(+) create mode 100644 .mokogitea/branch-protection.yml diff --git a/.mokogitea/branch-protection.yml b/.mokogitea/branch-protection.yml new file mode 100644 index 0000000..2dff8b9 --- /dev/null +++ b/.mokogitea/branch-protection.yml @@ -0,0 +1,251 @@ +# Copyright (C) 2026 Moko Consulting +# SPDX-License-Identifier: GPL-3.0-or-later +# FILE INFORMATION +# DEFGROUP: Gitea.Workflow +# INGROUP: moko-platform.Automation +# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform +# PATH: /.gitea/workflows/branch-protection.yml +# BRIEF: Apply standardised branch protection rules to all governed repositories +# +# +========================================================================+ +# | BRANCH PROTECTION SETUP | +# +========================================================================+ +# | | +# | Applies protection rules for: main, dev, rc, beta, alpha | +# | | +# | main — Require PR, block rejected reviews, no force push | +# | dev — Allow push, no force push, no delete | +# | rc — Allow push, no force push, no delete | +# | beta — Allow push, no force push, no delete | +# | alpha — Allow push, no force push, no delete | +# | | +# | jmiller has override authority on all branches. | +# | | +# +========================================================================+ + +name: Branch Protection Setup + +on: + schedule: + - cron: '0 2 * * 1' # Weekly Monday 02:00 UTC + workflow_dispatch: + inputs: + dry_run: + description: 'Preview mode (no changes)' + required: false + type: boolean + default: false + repos: + description: 'Comma-separated repo names (empty = all governed repos)' + required: false + type: string + default: '' + +env: + GITEA_URL: https://git.mokoconsulting.tech + GITEA_ORG: MokoConsulting + +permissions: + contents: read + +jobs: + protect: + name: Apply Branch Protection Rules + runs-on: ubuntu-latest + + steps: + - name: Determine target repos + id: repos + env: + GA_TOKEN: ${{ secrets.GA_TOKEN }} + run: | + API="${GITEA_URL}/api/v1" + + # Platform/standards/infra repos to exclude + EXCLUDE="gitea-org-config org-profile gitea-private .mokogitea-private MokoStandards moko-platform MokoTesting" + EXCLUDE="$EXCLUDE MokoStandards-Template-Client MokoStandards-Template-Dolibarr MokoStandards-Template-Generic MokoStandards-Template-Joomla MokoDoliProjTemplate" + + if [ -n "${{ inputs.repos }}" ]; then + # User-specified repos + REPOS=$(echo "${{ inputs.repos }}" | tr ',' ' ') + else + # Fetch all org repos + PAGE=1 + REPOS="" + while true; do + BATCH=$(curl -sS \ + -H "Authorization: token ${GA_TOKEN}" \ + "${API}/orgs/${GITEA_ORG}/repos?page=${PAGE}&limit=50" \ + | jq -r '.[].name // empty') + [ -z "$BATCH" ] && break + REPOS="$REPOS $BATCH" + PAGE=$((PAGE + 1)) + done + + # Filter out excluded repos + FILTERED="" + for REPO in $REPOS; do + SKIP=false + for EX in $EXCLUDE; do + if [ "$REPO" = "$EX" ]; then + SKIP=true + break + fi + done + if [ "$SKIP" = "false" ]; then + FILTERED="$FILTERED $REPO" + fi + done + REPOS="$FILTERED" + fi + + echo "repos=$REPOS" >> "$GITHUB_OUTPUT" + COUNT=$(echo "$REPOS" | wc -w) + echo "📋 Target repos (${COUNT}): $REPOS" + + - name: Apply protection rules + env: + GA_TOKEN: ${{ secrets.GA_TOKEN }} + DRY_RUN: ${{ inputs.dry_run || 'false' }} + run: | + API="${GITEA_URL}/api/v1" + REPOS="${{ steps.repos.outputs.repos }}" + + SUCCESS=0 + FAILED=0 + SKIPPED=0 + + # ── Rule definitions ────────────────────────────────────── + # Only the CI bot (jmiller token) can push directly. + # All human contributors must use PRs. + # Force push disabled on all branches. + + RULE_MAIN='{ + "rule_name": "main", + "enable_push": true, + "enable_push_whitelist": true, + "push_whitelist_usernames": ["jmiller"], + "enable_force_push": false, + "enable_force_push_allowlist": false, + "force_push_allowlist_usernames": [], + "enable_merge_whitelist": false, + "required_approvals": 0, + "dismiss_stale_approvals": true, + "block_on_rejected_reviews": true, + "block_on_outdated_branch": false, + "priority": 1 + }' + + RULE_DEV='{ + "rule_name": "dev", + "enable_push": true, + "enable_push_whitelist": true, + "push_whitelist_usernames": ["jmiller"], + "enable_force_push": false, + "enable_force_push_allowlist": false, + "force_push_allowlist_usernames": [], + "enable_merge_whitelist": false, + "required_approvals": 0, + "block_on_rejected_reviews": false, + "priority": 2 + }' + + RULE_RC='{ + "rule_name": "rc", + "enable_push": true, + "enable_push_whitelist": true, + "push_whitelist_usernames": ["jmiller"], + "enable_force_push": false, + "enable_force_push_allowlist": false, + "force_push_allowlist_usernames": [], + "enable_merge_whitelist": false, + "required_approvals": 0, + "block_on_rejected_reviews": false, + "priority": 3 + }' + + RULE_BETA='{ + "rule_name": "beta", + "enable_push": true, + "enable_push_whitelist": true, + "push_whitelist_usernames": ["jmiller"], + "enable_force_push": false, + "enable_force_push_allowlist": false, + "force_push_allowlist_usernames": [], + "enable_merge_whitelist": false, + "required_approvals": 0, + "block_on_rejected_reviews": false, + "priority": 4 + }' + + RULE_ALPHA='{ + "rule_name": "alpha", + "enable_push": true, + "enable_push_whitelist": true, + "push_whitelist_usernames": ["jmiller"], + "enable_force_push": false, + "enable_force_push_allowlist": false, + "force_push_allowlist_usernames": [], + "enable_merge_whitelist": false, + "required_approvals": 0, + "block_on_rejected_reviews": false, + "priority": 5 + }' + + RULES=("$RULE_MAIN" "$RULE_DEV" "$RULE_RC" "$RULE_BETA" "$RULE_ALPHA") + RULE_NAMES=("main" "dev" "rc" "beta" "alpha") + + # ── Apply rules to each repo ────────────────────────────── + for REPO in $REPOS; do + echo "" + echo "═══ ${REPO} ═══" + + for i in "${!RULES[@]}"; do + RULE="${RULES[$i]}" + NAME="${RULE_NAMES[$i]}" + + if [ "$DRY_RUN" = "true" ]; then + echo " [DRY RUN] Would apply rule: ${NAME}" + SKIPPED=$((SKIPPED + 1)) + continue + fi + + # Delete existing rule if present (idempotent recreate) + ENCODED_NAME=$(echo "$NAME" | sed 's|/|%2F|g') + curl -sS -o /dev/null -w "" \ + -X DELETE \ + -H "Authorization: token ${GA_TOKEN}" \ + "${API}/repos/${GITEA_ORG}/${REPO}/branch_protections/${ENCODED_NAME}" 2>/dev/null || true + + # Create rule + RESPONSE=$(curl -sS -w "\n%{http_code}" \ + -X POST \ + -H "Authorization: token ${GA_TOKEN}" \ + -H "Content-Type: application/json" \ + -d "$RULE" \ + "${API}/repos/${GITEA_ORG}/${REPO}/branch_protections") + + HTTP=$(echo "$RESPONSE" | tail -1) + BODY=$(echo "$RESPONSE" | sed '$d') + + if [ "$HTTP" = "201" ]; then + echo " ✅ ${NAME}" + SUCCESS=$((SUCCESS + 1)) + else + echo " ❌ ${NAME} (HTTP ${HTTP}): $(echo "$BODY" | jq -r '.message // .' 2>/dev/null | head -1)" + FAILED=$((FAILED + 1)) + fi + done + done + + # ── Summary ─────────────────────────────────────────────── + echo "" + echo "════════════════════════════════════════" + echo " ✅ Success: ${SUCCESS}" + echo " ❌ Failed: ${FAILED}" + echo " ⏭️ Skipped: ${SKIPPED}" + echo "════════════════════════════════════════" + + if [ "$FAILED" -gt 0 ]; then + echo "::warning::${FAILED} rule(s) failed to apply" + fi From c5afe4529d5ec6d90e1f7a40a2f830ed23919d7d Mon Sep 17 00:00:00 2001 From: "gitea-actions[bot]" Date: Sun, 28 Jun 2026 07:32:29 +0000 Subject: [PATCH 40/64] chore(version): pre-release bump to 06.00.04-dev [skip ci] --- .mokogitea/workflows/issue-branch.yml | 2 +- source/packages/com_mokosuitefield/mokosuitefield.xml | 2 +- source/packages/plg_system_mokosuitefield/mokosuitefield.xml | 2 +- .../packages/plg_webservices_mokosuitefield/mokosuitefield.xml | 2 +- source/pkg_mokosuitefield.xml | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.mokogitea/workflows/issue-branch.yml b/.mokogitea/workflows/issue-branch.yml index 3cfa08e..4389656 100644 --- a/.mokogitea/workflows/issue-branch.yml +++ b/.mokogitea/workflows/issue-branch.yml @@ -5,7 +5,7 @@ # FILE INFORMATION # DEFGROUP: Gitea.Workflow # INGROUP: mokocli.Automation -# VERSION: 06.00.03 +# VERSION: 06.00.04 # BRIEF: Auto-create feature branch when an issue is opened name: "Universal: Issue Branch" diff --git a/source/packages/com_mokosuitefield/mokosuitefield.xml b/source/packages/com_mokosuitefield/mokosuitefield.xml index cc0afe1..882d1a8 100644 --- a/source/packages/com_mokosuitefield/mokosuitefield.xml +++ b/source/packages/com_mokosuitefield/mokosuitefield.xml @@ -5,7 +5,7 @@ 2026-06-27 Copyright (C) 2026 Moko Consulting. GPL-3.0-or-later - 06.00.03 + 06.00.04 Moko\Component\MokoSuiteField servicessrctmpl diff --git a/source/packages/plg_system_mokosuitefield/mokosuitefield.xml b/source/packages/plg_system_mokosuitefield/mokosuitefield.xml index fe8bc13..fd18ed1 100644 --- a/source/packages/plg_system_mokosuitefield/mokosuitefield.xml +++ b/source/packages/plg_system_mokosuitefield/mokosuitefield.xml @@ -8,7 +8,7 @@ GPL-3.0-or-later hello@mokoconsulting.tech https://mokoconsulting.tech - 06.00.03 + 06.00.04 8.3 PLG_SYSTEM_MOKOSUITEFIELD_DESC Moko\Plugin\System\MokoSuiteField diff --git a/source/packages/plg_webservices_mokosuitefield/mokosuitefield.xml b/source/packages/plg_webservices_mokosuitefield/mokosuitefield.xml index e6353ba..3f39316 100644 --- a/source/packages/plg_webservices_mokosuitefield/mokosuitefield.xml +++ b/source/packages/plg_webservices_mokosuitefield/mokosuitefield.xml @@ -3,7 +3,7 @@ Web Services - MokoSuite Field mokosuitefield Moko Consulting - 06.00.03 + 06.00.04 GPL-3.0-or-later Moko\Plugin\WebServices\MokoSuiteField srcservices diff --git a/source/pkg_mokosuitefield.xml b/source/pkg_mokosuitefield.xml index ea57ab6..8ecd120 100644 --- a/source/pkg_mokosuitefield.xml +++ b/source/pkg_mokosuitefield.xml @@ -2,7 +2,7 @@ Package - MokoSuite Field mokosuitefield - 06.00.03 + 06.00.04 2026-06-27 Moko Consulting hello@mokoconsulting.tech From ba88d8e3b21f92d6a8cb418a7c29812cd9e7546d Mon Sep 17 00:00:00 2001 From: "gitea-actions[bot]" Date: Sun, 28 Jun 2026 07:37:11 +0000 Subject: [PATCH 41/64] chore(version): pre-release bump to 06.00.05-dev [skip ci] --- .mokogitea/workflows/issue-branch.yml | 2 +- source/packages/com_mokosuitefield/mokosuitefield.xml | 2 +- source/packages/plg_system_mokosuitefield/mokosuitefield.xml | 2 +- .../packages/plg_webservices_mokosuitefield/mokosuitefield.xml | 2 +- source/pkg_mokosuitefield.xml | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.mokogitea/workflows/issue-branch.yml b/.mokogitea/workflows/issue-branch.yml index 4389656..7e78553 100644 --- a/.mokogitea/workflows/issue-branch.yml +++ b/.mokogitea/workflows/issue-branch.yml @@ -5,7 +5,7 @@ # FILE INFORMATION # DEFGROUP: Gitea.Workflow # INGROUP: mokocli.Automation -# VERSION: 06.00.04 +# VERSION: 06.00.05 # BRIEF: Auto-create feature branch when an issue is opened name: "Universal: Issue Branch" diff --git a/source/packages/com_mokosuitefield/mokosuitefield.xml b/source/packages/com_mokosuitefield/mokosuitefield.xml index 882d1a8..51a668b 100644 --- a/source/packages/com_mokosuitefield/mokosuitefield.xml +++ b/source/packages/com_mokosuitefield/mokosuitefield.xml @@ -5,7 +5,7 @@ 2026-06-27 Copyright (C) 2026 Moko Consulting. GPL-3.0-or-later - 06.00.04 + 06.00.05 Moko\Component\MokoSuiteField servicessrctmpl diff --git a/source/packages/plg_system_mokosuitefield/mokosuitefield.xml b/source/packages/plg_system_mokosuitefield/mokosuitefield.xml index fd18ed1..f2c0b33 100644 --- a/source/packages/plg_system_mokosuitefield/mokosuitefield.xml +++ b/source/packages/plg_system_mokosuitefield/mokosuitefield.xml @@ -8,7 +8,7 @@ GPL-3.0-or-later hello@mokoconsulting.tech https://mokoconsulting.tech - 06.00.04 + 06.00.05 8.3 PLG_SYSTEM_MOKOSUITEFIELD_DESC Moko\Plugin\System\MokoSuiteField diff --git a/source/packages/plg_webservices_mokosuitefield/mokosuitefield.xml b/source/packages/plg_webservices_mokosuitefield/mokosuitefield.xml index 3f39316..64aebfe 100644 --- a/source/packages/plg_webservices_mokosuitefield/mokosuitefield.xml +++ b/source/packages/plg_webservices_mokosuitefield/mokosuitefield.xml @@ -3,7 +3,7 @@ Web Services - MokoSuite Field mokosuitefield Moko Consulting - 06.00.04 + 06.00.05 GPL-3.0-or-later Moko\Plugin\WebServices\MokoSuiteField srcservices diff --git a/source/pkg_mokosuitefield.xml b/source/pkg_mokosuitefield.xml index 8ecd120..91c8fce 100644 --- a/source/pkg_mokosuitefield.xml +++ b/source/pkg_mokosuitefield.xml @@ -2,7 +2,7 @@ Package - MokoSuite Field mokosuitefield - 06.00.04 + 06.00.05 2026-06-27 Moko Consulting hello@mokoconsulting.tech From 03d49953ba0c2b489e5ef766e653fcaf7005030a Mon Sep 17 00:00:00 2001 From: "gitea-actions[bot]" Date: Sun, 28 Jun 2026 07:42:22 +0000 Subject: [PATCH 42/64] chore(version): pre-release bump to 06.00.06-dev [skip ci] --- .mokogitea/workflows/issue-branch.yml | 2 +- source/packages/com_mokosuitefield/mokosuitefield.xml | 2 +- source/packages/plg_system_mokosuitefield/mokosuitefield.xml | 2 +- .../packages/plg_webservices_mokosuitefield/mokosuitefield.xml | 2 +- source/pkg_mokosuitefield.xml | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.mokogitea/workflows/issue-branch.yml b/.mokogitea/workflows/issue-branch.yml index 7e78553..0119519 100644 --- a/.mokogitea/workflows/issue-branch.yml +++ b/.mokogitea/workflows/issue-branch.yml @@ -5,7 +5,7 @@ # FILE INFORMATION # DEFGROUP: Gitea.Workflow # INGROUP: mokocli.Automation -# VERSION: 06.00.05 +# VERSION: 06.00.06 # BRIEF: Auto-create feature branch when an issue is opened name: "Universal: Issue Branch" diff --git a/source/packages/com_mokosuitefield/mokosuitefield.xml b/source/packages/com_mokosuitefield/mokosuitefield.xml index 51a668b..afa6aa2 100644 --- a/source/packages/com_mokosuitefield/mokosuitefield.xml +++ b/source/packages/com_mokosuitefield/mokosuitefield.xml @@ -5,7 +5,7 @@ 2026-06-27 Copyright (C) 2026 Moko Consulting. GPL-3.0-or-later - 06.00.05 + 06.00.06 Moko\Component\MokoSuiteField servicessrctmpl diff --git a/source/packages/plg_system_mokosuitefield/mokosuitefield.xml b/source/packages/plg_system_mokosuitefield/mokosuitefield.xml index f2c0b33..ea4e546 100644 --- a/source/packages/plg_system_mokosuitefield/mokosuitefield.xml +++ b/source/packages/plg_system_mokosuitefield/mokosuitefield.xml @@ -8,7 +8,7 @@ GPL-3.0-or-later hello@mokoconsulting.tech https://mokoconsulting.tech - 06.00.05 + 06.00.06 8.3 PLG_SYSTEM_MOKOSUITEFIELD_DESC Moko\Plugin\System\MokoSuiteField diff --git a/source/packages/plg_webservices_mokosuitefield/mokosuitefield.xml b/source/packages/plg_webservices_mokosuitefield/mokosuitefield.xml index 64aebfe..d55990d 100644 --- a/source/packages/plg_webservices_mokosuitefield/mokosuitefield.xml +++ b/source/packages/plg_webservices_mokosuitefield/mokosuitefield.xml @@ -3,7 +3,7 @@ Web Services - MokoSuite Field mokosuitefield Moko Consulting - 06.00.05 + 06.00.06 GPL-3.0-or-later Moko\Plugin\WebServices\MokoSuiteField srcservices diff --git a/source/pkg_mokosuitefield.xml b/source/pkg_mokosuitefield.xml index 91c8fce..68bb4ee 100644 --- a/source/pkg_mokosuitefield.xml +++ b/source/pkg_mokosuitefield.xml @@ -2,7 +2,7 @@ Package - MokoSuite Field mokosuitefield - 06.00.05 + 06.00.06 2026-06-27 Moko Consulting hello@mokoconsulting.tech From 226f048cb9e950c36964c7aa23e521658d95c164 Mon Sep 17 00:00:00 2001 From: "gitea-actions[bot]" Date: Sun, 28 Jun 2026 07:48:22 +0000 Subject: [PATCH 43/64] chore(version): pre-release bump to 06.00.07-dev [skip ci] --- .mokogitea/workflows/issue-branch.yml | 2 +- source/packages/com_mokosuitefield/mokosuitefield.xml | 2 +- source/packages/plg_system_mokosuitefield/mokosuitefield.xml | 2 +- .../packages/plg_webservices_mokosuitefield/mokosuitefield.xml | 2 +- source/pkg_mokosuitefield.xml | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.mokogitea/workflows/issue-branch.yml b/.mokogitea/workflows/issue-branch.yml index 0119519..7cd0e27 100644 --- a/.mokogitea/workflows/issue-branch.yml +++ b/.mokogitea/workflows/issue-branch.yml @@ -5,7 +5,7 @@ # FILE INFORMATION # DEFGROUP: Gitea.Workflow # INGROUP: mokocli.Automation -# VERSION: 06.00.06 +# VERSION: 06.00.07 # BRIEF: Auto-create feature branch when an issue is opened name: "Universal: Issue Branch" diff --git a/source/packages/com_mokosuitefield/mokosuitefield.xml b/source/packages/com_mokosuitefield/mokosuitefield.xml index afa6aa2..2246f6b 100644 --- a/source/packages/com_mokosuitefield/mokosuitefield.xml +++ b/source/packages/com_mokosuitefield/mokosuitefield.xml @@ -5,7 +5,7 @@ 2026-06-27 Copyright (C) 2026 Moko Consulting. GPL-3.0-or-later - 06.00.06 + 06.00.07 Moko\Component\MokoSuiteField servicessrctmpl diff --git a/source/packages/plg_system_mokosuitefield/mokosuitefield.xml b/source/packages/plg_system_mokosuitefield/mokosuitefield.xml index ea4e546..a627316 100644 --- a/source/packages/plg_system_mokosuitefield/mokosuitefield.xml +++ b/source/packages/plg_system_mokosuitefield/mokosuitefield.xml @@ -8,7 +8,7 @@ GPL-3.0-or-later hello@mokoconsulting.tech https://mokoconsulting.tech - 06.00.06 + 06.00.07 8.3 PLG_SYSTEM_MOKOSUITEFIELD_DESC Moko\Plugin\System\MokoSuiteField diff --git a/source/packages/plg_webservices_mokosuitefield/mokosuitefield.xml b/source/packages/plg_webservices_mokosuitefield/mokosuitefield.xml index d55990d..b0dd56c 100644 --- a/source/packages/plg_webservices_mokosuitefield/mokosuitefield.xml +++ b/source/packages/plg_webservices_mokosuitefield/mokosuitefield.xml @@ -3,7 +3,7 @@ Web Services - MokoSuite Field mokosuitefield Moko Consulting - 06.00.06 + 06.00.07 GPL-3.0-or-later Moko\Plugin\WebServices\MokoSuiteField srcservices diff --git a/source/pkg_mokosuitefield.xml b/source/pkg_mokosuitefield.xml index 68bb4ee..f43f14c 100644 --- a/source/pkg_mokosuitefield.xml +++ b/source/pkg_mokosuitefield.xml @@ -2,7 +2,7 @@ Package - MokoSuite Field mokosuitefield - 06.00.06 + 06.00.07 2026-06-27 Moko Consulting hello@mokoconsulting.tech From 3c120d70fabedc2ec5fd889cc7baf1f8e60a6f99 Mon Sep 17 00:00:00 2001 From: "gitea-actions[bot]" Date: Sun, 28 Jun 2026 07:56:02 +0000 Subject: [PATCH 44/64] chore(version): pre-release bump to 06.00.08-dev [skip ci] --- .mokogitea/workflows/issue-branch.yml | 2 +- source/packages/com_mokosuitefield/mokosuitefield.xml | 2 +- source/packages/plg_system_mokosuitefield/mokosuitefield.xml | 2 +- .../packages/plg_webservices_mokosuitefield/mokosuitefield.xml | 2 +- source/pkg_mokosuitefield.xml | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.mokogitea/workflows/issue-branch.yml b/.mokogitea/workflows/issue-branch.yml index 7cd0e27..80507b1 100644 --- a/.mokogitea/workflows/issue-branch.yml +++ b/.mokogitea/workflows/issue-branch.yml @@ -5,7 +5,7 @@ # FILE INFORMATION # DEFGROUP: Gitea.Workflow # INGROUP: mokocli.Automation -# VERSION: 06.00.07 +# VERSION: 06.00.08 # BRIEF: Auto-create feature branch when an issue is opened name: "Universal: Issue Branch" diff --git a/source/packages/com_mokosuitefield/mokosuitefield.xml b/source/packages/com_mokosuitefield/mokosuitefield.xml index 2246f6b..b8f1910 100644 --- a/source/packages/com_mokosuitefield/mokosuitefield.xml +++ b/source/packages/com_mokosuitefield/mokosuitefield.xml @@ -5,7 +5,7 @@ 2026-06-27 Copyright (C) 2026 Moko Consulting. GPL-3.0-or-later - 06.00.07 + 06.00.08 Moko\Component\MokoSuiteField servicessrctmpl diff --git a/source/packages/plg_system_mokosuitefield/mokosuitefield.xml b/source/packages/plg_system_mokosuitefield/mokosuitefield.xml index a627316..fb3f419 100644 --- a/source/packages/plg_system_mokosuitefield/mokosuitefield.xml +++ b/source/packages/plg_system_mokosuitefield/mokosuitefield.xml @@ -8,7 +8,7 @@ GPL-3.0-or-later hello@mokoconsulting.tech https://mokoconsulting.tech - 06.00.07 + 06.00.08 8.3 PLG_SYSTEM_MOKOSUITEFIELD_DESC Moko\Plugin\System\MokoSuiteField diff --git a/source/packages/plg_webservices_mokosuitefield/mokosuitefield.xml b/source/packages/plg_webservices_mokosuitefield/mokosuitefield.xml index b0dd56c..76d04a3 100644 --- a/source/packages/plg_webservices_mokosuitefield/mokosuitefield.xml +++ b/source/packages/plg_webservices_mokosuitefield/mokosuitefield.xml @@ -3,7 +3,7 @@ Web Services - MokoSuite Field mokosuitefield Moko Consulting - 06.00.07 + 06.00.08 GPL-3.0-or-later Moko\Plugin\WebServices\MokoSuiteField srcservices diff --git a/source/pkg_mokosuitefield.xml b/source/pkg_mokosuitefield.xml index f43f14c..422052f 100644 --- a/source/pkg_mokosuitefield.xml +++ b/source/pkg_mokosuitefield.xml @@ -2,7 +2,7 @@ Package - MokoSuite Field mokosuitefield - 06.00.07 + 06.00.08 2026-06-27 Moko Consulting hello@mokoconsulting.tech From 903fa0fe57393b18b56e19e72c2fccc4c815678e Mon Sep 17 00:00:00 2001 From: "gitea-actions[bot]" Date: Sun, 28 Jun 2026 08:00:29 +0000 Subject: [PATCH 45/64] chore(version): pre-release bump to 06.00.09-dev [skip ci] --- .mokogitea/workflows/issue-branch.yml | 2 +- source/packages/com_mokosuitefield/mokosuitefield.xml | 2 +- source/packages/plg_system_mokosuitefield/mokosuitefield.xml | 2 +- .../packages/plg_webservices_mokosuitefield/mokosuitefield.xml | 2 +- source/pkg_mokosuitefield.xml | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.mokogitea/workflows/issue-branch.yml b/.mokogitea/workflows/issue-branch.yml index 80507b1..01a7bde 100644 --- a/.mokogitea/workflows/issue-branch.yml +++ b/.mokogitea/workflows/issue-branch.yml @@ -5,7 +5,7 @@ # FILE INFORMATION # DEFGROUP: Gitea.Workflow # INGROUP: mokocli.Automation -# VERSION: 06.00.08 +# VERSION: 06.00.09 # BRIEF: Auto-create feature branch when an issue is opened name: "Universal: Issue Branch" diff --git a/source/packages/com_mokosuitefield/mokosuitefield.xml b/source/packages/com_mokosuitefield/mokosuitefield.xml index b8f1910..541a45d 100644 --- a/source/packages/com_mokosuitefield/mokosuitefield.xml +++ b/source/packages/com_mokosuitefield/mokosuitefield.xml @@ -5,7 +5,7 @@ 2026-06-27 Copyright (C) 2026 Moko Consulting. GPL-3.0-or-later - 06.00.08 + 06.00.09 Moko\Component\MokoSuiteField servicessrctmpl diff --git a/source/packages/plg_system_mokosuitefield/mokosuitefield.xml b/source/packages/plg_system_mokosuitefield/mokosuitefield.xml index fb3f419..0e7295e 100644 --- a/source/packages/plg_system_mokosuitefield/mokosuitefield.xml +++ b/source/packages/plg_system_mokosuitefield/mokosuitefield.xml @@ -8,7 +8,7 @@ GPL-3.0-or-later hello@mokoconsulting.tech https://mokoconsulting.tech - 06.00.08 + 06.00.09 8.3 PLG_SYSTEM_MOKOSUITEFIELD_DESC Moko\Plugin\System\MokoSuiteField diff --git a/source/packages/plg_webservices_mokosuitefield/mokosuitefield.xml b/source/packages/plg_webservices_mokosuitefield/mokosuitefield.xml index 76d04a3..42f1b92 100644 --- a/source/packages/plg_webservices_mokosuitefield/mokosuitefield.xml +++ b/source/packages/plg_webservices_mokosuitefield/mokosuitefield.xml @@ -3,7 +3,7 @@ Web Services - MokoSuite Field mokosuitefield Moko Consulting - 06.00.08 + 06.00.09 GPL-3.0-or-later Moko\Plugin\WebServices\MokoSuiteField srcservices diff --git a/source/pkg_mokosuitefield.xml b/source/pkg_mokosuitefield.xml index 422052f..393cad7 100644 --- a/source/pkg_mokosuitefield.xml +++ b/source/pkg_mokosuitefield.xml @@ -2,7 +2,7 @@ Package - MokoSuite Field mokosuitefield - 06.00.08 + 06.00.09 2026-06-27 Moko Consulting hello@mokoconsulting.tech From a14dc635d5f9e2922a781c58161a5799c2409245 Mon Sep 17 00:00:00 2001 From: Jonathan Miller <1+jmiller@noreply.git.mokoconsulting.tech> Date: Sun, 28 Jun 2026 19:02:49 +0000 Subject: [PATCH 46/64] feat: add license key warning on install/update --- source/script.php | 71 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 source/script.php diff --git a/source/script.php b/source/script.php new file mode 100644 index 0000000..9ce2d9b --- /dev/null +++ b/source/script.php @@ -0,0 +1,71 @@ +warnMissingLicenseKey(); + } + + private function warnMissingLicenseKey(): void + { + try + { + $db = Factory::getDbo(); + $app = Factory::getApplication(); + + $query = $db->getQuery(true) + ->select([$db->quoteName('update_site_id'), $db->quoteName('extra_query')]) + ->from($db->quoteName('#__update_sites')) + ->where('(' . $db->quoteName('name') . ' LIKE ' . $db->quote('%MokoSuiteField%') + . ' OR ' . $db->quoteName('location') . ' LIKE ' . $db->quote('%MokoSuiteField%') . ')') + ->setLimit(1); + $db->setQuery($query); + $site = $db->loadObject(); + + if ($site) + { + $extraQuery = (string) ($site->extra_query ?? ''); + + if (!empty($extraQuery) && strpos($extraQuery, 'dlid=') !== false) + { + parse_str($extraQuery, $parsed); + + if (!empty($parsed['dlid'])) + { + return; + } + } + + $editUrl = 'index.php?option=com_installer&task=updatesite.edit&update_site_id=' . (int) $site->update_site_id; + } + else + { + $editUrl = 'index.php?option=com_installer&view=updatesites'; + } + + $app->enqueueMessage( + 'Moko Consulting License Key Required — ' + . 'No download key is configured. Updates will not be available until a valid license key is entered. ' + . 'Enter License Key', + 'warning' + ); + } + catch (\Throwable $e) + { + // Silent — avoid breaking install if update_sites query fails + } + } +} From 0855685144fe69dfb539a2ece72d898d31d322ba Mon Sep 17 00:00:00 2001 From: Jonathan Miller <1+jmiller@noreply.git.mokoconsulting.tech> Date: Sun, 28 Jun 2026 19:02:54 +0000 Subject: [PATCH 47/64] feat: add scriptfile to package manifest --- source/pkg_mokosuitefield.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/source/pkg_mokosuitefield.xml b/source/pkg_mokosuitefield.xml index 393cad7..916f841 100644 --- a/source/pkg_mokosuitefield.xml +++ b/source/pkg_mokosuitefield.xml @@ -13,6 +13,7 @@ 8.3 true + script.php plg_system_mokosuitefield.zip From 0451931817c3e175b9961a39b50d1207b487d758 Mon Sep 17 00:00:00 2001 From: "gitea-actions[bot]" Date: Sun, 28 Jun 2026 19:10:26 +0000 Subject: [PATCH 48/64] chore(version): pre-release bump to 06.00.10-dev [skip ci] --- .mokogitea/workflows/issue-branch.yml | 2 +- source/packages/com_mokosuitefield/mokosuitefield.xml | 2 +- source/packages/plg_system_mokosuitefield/mokosuitefield.xml | 2 +- .../packages/plg_webservices_mokosuitefield/mokosuitefield.xml | 2 +- source/pkg_mokosuitefield.xml | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.mokogitea/workflows/issue-branch.yml b/.mokogitea/workflows/issue-branch.yml index 01a7bde..b9e66ff 100644 --- a/.mokogitea/workflows/issue-branch.yml +++ b/.mokogitea/workflows/issue-branch.yml @@ -5,7 +5,7 @@ # FILE INFORMATION # DEFGROUP: Gitea.Workflow # INGROUP: mokocli.Automation -# VERSION: 06.00.09 +# VERSION: 06.00.10 # BRIEF: Auto-create feature branch when an issue is opened name: "Universal: Issue Branch" diff --git a/source/packages/com_mokosuitefield/mokosuitefield.xml b/source/packages/com_mokosuitefield/mokosuitefield.xml index 541a45d..a33a8f7 100644 --- a/source/packages/com_mokosuitefield/mokosuitefield.xml +++ b/source/packages/com_mokosuitefield/mokosuitefield.xml @@ -5,7 +5,7 @@ 2026-06-27 Copyright (C) 2026 Moko Consulting. GPL-3.0-or-later - 06.00.09 + 06.00.10 Moko\Component\MokoSuiteField servicessrctmpl diff --git a/source/packages/plg_system_mokosuitefield/mokosuitefield.xml b/source/packages/plg_system_mokosuitefield/mokosuitefield.xml index 0e7295e..6c6e1d4 100644 --- a/source/packages/plg_system_mokosuitefield/mokosuitefield.xml +++ b/source/packages/plg_system_mokosuitefield/mokosuitefield.xml @@ -8,7 +8,7 @@ GPL-3.0-or-later hello@mokoconsulting.tech https://mokoconsulting.tech - 06.00.09 + 06.00.10 8.3 PLG_SYSTEM_MOKOSUITEFIELD_DESC Moko\Plugin\System\MokoSuiteField diff --git a/source/packages/plg_webservices_mokosuitefield/mokosuitefield.xml b/source/packages/plg_webservices_mokosuitefield/mokosuitefield.xml index 42f1b92..c6c2d38 100644 --- a/source/packages/plg_webservices_mokosuitefield/mokosuitefield.xml +++ b/source/packages/plg_webservices_mokosuitefield/mokosuitefield.xml @@ -3,7 +3,7 @@ Web Services - MokoSuite Field mokosuitefield Moko Consulting - 06.00.09 + 06.00.10 GPL-3.0-or-later Moko\Plugin\WebServices\MokoSuiteField srcservices diff --git a/source/pkg_mokosuitefield.xml b/source/pkg_mokosuitefield.xml index 916f841..b60492d 100644 --- a/source/pkg_mokosuitefield.xml +++ b/source/pkg_mokosuitefield.xml @@ -2,7 +2,7 @@ Package - MokoSuite Field mokosuitefield - 06.00.09 + 06.00.10 2026-06-27 Moko Consulting hello@mokoconsulting.tech From c4d6d9ebd0b69ead103c65b4317600801255b71b Mon Sep 17 00:00:00 2001 From: "gitea-actions[bot]" Date: Sun, 28 Jun 2026 19:10:48 +0000 Subject: [PATCH 49/64] chore(version): pre-release bump to 06.00.11-dev [skip ci] --- .mokogitea/workflows/issue-branch.yml | 2 +- source/packages/com_mokosuitefield/mokosuitefield.xml | 2 +- source/packages/plg_system_mokosuitefield/mokosuitefield.xml | 2 +- .../packages/plg_webservices_mokosuitefield/mokosuitefield.xml | 2 +- source/pkg_mokosuitefield.xml | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.mokogitea/workflows/issue-branch.yml b/.mokogitea/workflows/issue-branch.yml index b9e66ff..a902cf1 100644 --- a/.mokogitea/workflows/issue-branch.yml +++ b/.mokogitea/workflows/issue-branch.yml @@ -5,7 +5,7 @@ # FILE INFORMATION # DEFGROUP: Gitea.Workflow # INGROUP: mokocli.Automation -# VERSION: 06.00.10 +# VERSION: 06.00.11 # BRIEF: Auto-create feature branch when an issue is opened name: "Universal: Issue Branch" diff --git a/source/packages/com_mokosuitefield/mokosuitefield.xml b/source/packages/com_mokosuitefield/mokosuitefield.xml index a33a8f7..da589b0 100644 --- a/source/packages/com_mokosuitefield/mokosuitefield.xml +++ b/source/packages/com_mokosuitefield/mokosuitefield.xml @@ -5,7 +5,7 @@ 2026-06-27 Copyright (C) 2026 Moko Consulting. GPL-3.0-or-later - 06.00.10 + 06.00.11 Moko\Component\MokoSuiteField servicessrctmpl diff --git a/source/packages/plg_system_mokosuitefield/mokosuitefield.xml b/source/packages/plg_system_mokosuitefield/mokosuitefield.xml index 6c6e1d4..6801552 100644 --- a/source/packages/plg_system_mokosuitefield/mokosuitefield.xml +++ b/source/packages/plg_system_mokosuitefield/mokosuitefield.xml @@ -8,7 +8,7 @@ GPL-3.0-or-later hello@mokoconsulting.tech https://mokoconsulting.tech - 06.00.10 + 06.00.11 8.3 PLG_SYSTEM_MOKOSUITEFIELD_DESC Moko\Plugin\System\MokoSuiteField diff --git a/source/packages/plg_webservices_mokosuitefield/mokosuitefield.xml b/source/packages/plg_webservices_mokosuitefield/mokosuitefield.xml index c6c2d38..8f7469d 100644 --- a/source/packages/plg_webservices_mokosuitefield/mokosuitefield.xml +++ b/source/packages/plg_webservices_mokosuitefield/mokosuitefield.xml @@ -3,7 +3,7 @@ Web Services - MokoSuite Field mokosuitefield Moko Consulting - 06.00.10 + 06.00.11 GPL-3.0-or-later Moko\Plugin\WebServices\MokoSuiteField srcservices diff --git a/source/pkg_mokosuitefield.xml b/source/pkg_mokosuitefield.xml index b60492d..a0ea8d6 100644 --- a/source/pkg_mokosuitefield.xml +++ b/source/pkg_mokosuitefield.xml @@ -2,7 +2,7 @@ Package - MokoSuite Field mokosuitefield - 06.00.10 + 06.00.11 2026-06-27 Moko Consulting hello@mokoconsulting.tech From b4ecc999526ca71ddf854570d20f53edc95191ba Mon Sep 17 00:00:00 2001 From: Jonathan Miller <1+jmiller@noreply.git.mokoconsulting.tech> Date: Mon, 29 Jun 2026 15:11:31 +0000 Subject: [PATCH 50/64] chore: migrate update server URLs to MokoGitea --- source/pkg_mokosuitefield.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source/pkg_mokosuitefield.xml b/source/pkg_mokosuitefield.xml index a0ea8d6..1731e08 100644 --- a/source/pkg_mokosuitefield.xml +++ b/source/pkg_mokosuitefield.xml @@ -18,6 +18,6 @@ plg_system_mokosuitefield.zip - https://git.mokoconsulting.tech/MokoConsulting/MokoSuiteField/updates.xml + https://git.mokoconsulting.tech/api/packages/MokoConsulting/generic/MokoSuiteField/latest/updates.xml From a470bca51f67c5dfd92670185c40a1859cea1431 Mon Sep 17 00:00:00 2001 From: "gitea-actions[bot]" Date: Mon, 29 Jun 2026 15:12:12 +0000 Subject: [PATCH 51/64] chore(version): pre-release bump to 06.00.12-dev [skip ci] --- .mokogitea/workflows/issue-branch.yml | 2 +- source/packages/com_mokosuitefield/mokosuitefield.xml | 2 +- source/packages/plg_system_mokosuitefield/mokosuitefield.xml | 2 +- .../packages/plg_webservices_mokosuitefield/mokosuitefield.xml | 2 +- source/pkg_mokosuitefield.xml | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.mokogitea/workflows/issue-branch.yml b/.mokogitea/workflows/issue-branch.yml index a902cf1..cabba95 100644 --- a/.mokogitea/workflows/issue-branch.yml +++ b/.mokogitea/workflows/issue-branch.yml @@ -5,7 +5,7 @@ # FILE INFORMATION # DEFGROUP: Gitea.Workflow # INGROUP: mokocli.Automation -# VERSION: 06.00.11 +# VERSION: 06.00.12 # BRIEF: Auto-create feature branch when an issue is opened name: "Universal: Issue Branch" diff --git a/source/packages/com_mokosuitefield/mokosuitefield.xml b/source/packages/com_mokosuitefield/mokosuitefield.xml index da589b0..f59478f 100644 --- a/source/packages/com_mokosuitefield/mokosuitefield.xml +++ b/source/packages/com_mokosuitefield/mokosuitefield.xml @@ -5,7 +5,7 @@ 2026-06-27 Copyright (C) 2026 Moko Consulting. GPL-3.0-or-later - 06.00.11 + 06.00.12 Moko\Component\MokoSuiteField servicessrctmpl diff --git a/source/packages/plg_system_mokosuitefield/mokosuitefield.xml b/source/packages/plg_system_mokosuitefield/mokosuitefield.xml index 6801552..9c3d57e 100644 --- a/source/packages/plg_system_mokosuitefield/mokosuitefield.xml +++ b/source/packages/plg_system_mokosuitefield/mokosuitefield.xml @@ -8,7 +8,7 @@ GPL-3.0-or-later hello@mokoconsulting.tech https://mokoconsulting.tech - 06.00.11 + 06.00.12 8.3 PLG_SYSTEM_MOKOSUITEFIELD_DESC Moko\Plugin\System\MokoSuiteField diff --git a/source/packages/plg_webservices_mokosuitefield/mokosuitefield.xml b/source/packages/plg_webservices_mokosuitefield/mokosuitefield.xml index 8f7469d..96d9693 100644 --- a/source/packages/plg_webservices_mokosuitefield/mokosuitefield.xml +++ b/source/packages/plg_webservices_mokosuitefield/mokosuitefield.xml @@ -3,7 +3,7 @@ Web Services - MokoSuite Field mokosuitefield Moko Consulting - 06.00.11 + 06.00.12 GPL-3.0-or-later Moko\Plugin\WebServices\MokoSuiteField srcservices diff --git a/source/pkg_mokosuitefield.xml b/source/pkg_mokosuitefield.xml index 1731e08..d85d2ed 100644 --- a/source/pkg_mokosuitefield.xml +++ b/source/pkg_mokosuitefield.xml @@ -2,7 +2,7 @@ Package - MokoSuite Field mokosuitefield - 06.00.11 + 06.00.12 2026-06-27 Moko Consulting hello@mokoconsulting.tech From bd1d9155598eacada147dfadd60fdca55079d238 Mon Sep 17 00:00:00 2001 From: "gitea-actions[bot]" Date: Mon, 29 Jun 2026 15:28:42 +0000 Subject: [PATCH 52/64] chore(version): auto-bump patch 06.00.13-dev [skip ci] --- .mokogitea/workflows/issue-branch.yml | 2 +- source/packages/com_mokosuitefield/mokosuitefield.xml | 2 +- source/packages/plg_system_mokosuitefield/mokosuitefield.xml | 2 +- .../packages/plg_webservices_mokosuitefield/mokosuitefield.xml | 2 +- source/pkg_mokosuitefield.xml | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.mokogitea/workflows/issue-branch.yml b/.mokogitea/workflows/issue-branch.yml index cabba95..94ae363 100644 --- a/.mokogitea/workflows/issue-branch.yml +++ b/.mokogitea/workflows/issue-branch.yml @@ -5,7 +5,7 @@ # FILE INFORMATION # DEFGROUP: Gitea.Workflow # INGROUP: mokocli.Automation -# VERSION: 06.00.12 +# VERSION: 06.00.13 # BRIEF: Auto-create feature branch when an issue is opened name: "Universal: Issue Branch" diff --git a/source/packages/com_mokosuitefield/mokosuitefield.xml b/source/packages/com_mokosuitefield/mokosuitefield.xml index f59478f..b623f95 100644 --- a/source/packages/com_mokosuitefield/mokosuitefield.xml +++ b/source/packages/com_mokosuitefield/mokosuitefield.xml @@ -5,7 +5,7 @@ 2026-06-27 Copyright (C) 2026 Moko Consulting. GPL-3.0-or-later - 06.00.12 + 06.00.13 Moko\Component\MokoSuiteField servicessrctmpl diff --git a/source/packages/plg_system_mokosuitefield/mokosuitefield.xml b/source/packages/plg_system_mokosuitefield/mokosuitefield.xml index 9c3d57e..ca22f27 100644 --- a/source/packages/plg_system_mokosuitefield/mokosuitefield.xml +++ b/source/packages/plg_system_mokosuitefield/mokosuitefield.xml @@ -8,7 +8,7 @@ GPL-3.0-or-later hello@mokoconsulting.tech https://mokoconsulting.tech - 06.00.12 + 06.00.13 8.3 PLG_SYSTEM_MOKOSUITEFIELD_DESC Moko\Plugin\System\MokoSuiteField diff --git a/source/packages/plg_webservices_mokosuitefield/mokosuitefield.xml b/source/packages/plg_webservices_mokosuitefield/mokosuitefield.xml index 96d9693..c4b3b3a 100644 --- a/source/packages/plg_webservices_mokosuitefield/mokosuitefield.xml +++ b/source/packages/plg_webservices_mokosuitefield/mokosuitefield.xml @@ -3,7 +3,7 @@ Web Services - MokoSuite Field mokosuitefield Moko Consulting - 06.00.12 + 06.00.13 GPL-3.0-or-later Moko\Plugin\WebServices\MokoSuiteField srcservices diff --git a/source/pkg_mokosuitefield.xml b/source/pkg_mokosuitefield.xml index d85d2ed..9d4dfe1 100644 --- a/source/pkg_mokosuitefield.xml +++ b/source/pkg_mokosuitefield.xml @@ -2,7 +2,7 @@ Package - MokoSuite Field mokosuitefield - 06.00.12 + 06.00.13 2026-06-27 Moko Consulting hello@mokoconsulting.tech From 289c0b6c17404b731b6975a174a3ff22858ccb5c Mon Sep 17 00:00:00 2001 From: "gitea-actions[bot]" Date: Mon, 29 Jun 2026 15:28:52 +0000 Subject: [PATCH 53/64] chore(version): pre-release bump to 06.00.14-dev [skip ci] --- .mokogitea/workflows/issue-branch.yml | 2 +- source/packages/com_mokosuitefield/mokosuitefield.xml | 2 +- source/packages/plg_system_mokosuitefield/mokosuitefield.xml | 2 +- .../packages/plg_webservices_mokosuitefield/mokosuitefield.xml | 2 +- source/pkg_mokosuitefield.xml | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.mokogitea/workflows/issue-branch.yml b/.mokogitea/workflows/issue-branch.yml index 94ae363..1904498 100644 --- a/.mokogitea/workflows/issue-branch.yml +++ b/.mokogitea/workflows/issue-branch.yml @@ -5,7 +5,7 @@ # FILE INFORMATION # DEFGROUP: Gitea.Workflow # INGROUP: mokocli.Automation -# VERSION: 06.00.13 +# VERSION: 06.00.14 # BRIEF: Auto-create feature branch when an issue is opened name: "Universal: Issue Branch" diff --git a/source/packages/com_mokosuitefield/mokosuitefield.xml b/source/packages/com_mokosuitefield/mokosuitefield.xml index b623f95..2fd8bbb 100644 --- a/source/packages/com_mokosuitefield/mokosuitefield.xml +++ b/source/packages/com_mokosuitefield/mokosuitefield.xml @@ -5,7 +5,7 @@ 2026-06-27 Copyright (C) 2026 Moko Consulting. GPL-3.0-or-later - 06.00.13 + 06.00.14 Moko\Component\MokoSuiteField servicessrctmpl diff --git a/source/packages/plg_system_mokosuitefield/mokosuitefield.xml b/source/packages/plg_system_mokosuitefield/mokosuitefield.xml index ca22f27..e61dfb0 100644 --- a/source/packages/plg_system_mokosuitefield/mokosuitefield.xml +++ b/source/packages/plg_system_mokosuitefield/mokosuitefield.xml @@ -8,7 +8,7 @@ GPL-3.0-or-later hello@mokoconsulting.tech https://mokoconsulting.tech - 06.00.13 + 06.00.14 8.3 PLG_SYSTEM_MOKOSUITEFIELD_DESC Moko\Plugin\System\MokoSuiteField diff --git a/source/packages/plg_webservices_mokosuitefield/mokosuitefield.xml b/source/packages/plg_webservices_mokosuitefield/mokosuitefield.xml index c4b3b3a..0089817 100644 --- a/source/packages/plg_webservices_mokosuitefield/mokosuitefield.xml +++ b/source/packages/plg_webservices_mokosuitefield/mokosuitefield.xml @@ -3,7 +3,7 @@ Web Services - MokoSuite Field mokosuitefield Moko Consulting - 06.00.13 + 06.00.14 GPL-3.0-or-later Moko\Plugin\WebServices\MokoSuiteField srcservices diff --git a/source/pkg_mokosuitefield.xml b/source/pkg_mokosuitefield.xml index 9d4dfe1..539374f 100644 --- a/source/pkg_mokosuitefield.xml +++ b/source/pkg_mokosuitefield.xml @@ -2,7 +2,7 @@ Package - MokoSuite Field mokosuitefield - 06.00.13 + 06.00.14 2026-06-27 Moko Consulting hello@mokoconsulting.tech From 4ba500f6d56c859637049e1ecf0e1645905cc100 Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Tue, 30 Jun 2026 13:15:00 -0500 Subject: [PATCH 54/64] chore: cleanup workflows, add manual dispatch to pre-release - Remove old build.yaml (hardcoded, references updates.xml) - Remove duplicate build-release.yml - Add pre-release.yml with workflow_dispatch for manual triggering - All releases marked as pre-release until stable testing complete --- .gitea/workflows/build.yaml | 33 ---------------- .gitea/workflows/pre-release.yml | 66 ++++++++++++++++++++++++++++++++ 2 files changed, 66 insertions(+), 33 deletions(-) delete mode 100644 .gitea/workflows/build.yaml create mode 100644 .gitea/workflows/pre-release.yml diff --git a/.gitea/workflows/build.yaml b/.gitea/workflows/build.yaml deleted file mode 100644 index 10f5bf5..0000000 --- a/.gitea/workflows/build.yaml +++ /dev/null @@ -1,33 +0,0 @@ -name: Build Package -on: - push: - tags: - - 'v*' - -jobs: - build: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - with: - submodules: recursive - - - name: Build package ZIP - run: | - cd source - # Create individual package ZIPs - for pkg_dir in packages/*/; do - pkg_name=$(basename "$pkg_dir") - cd "$pkg_dir" - zip -r "../../${pkg_name}.zip" . -x "*.git*" - cd ../.. - done - # Create main package ZIP with all sub-packages + manifest - zip -j "pkg_mokosuitefield.zip" pkg_*.xml script.php updates.xml *.zip 2>/dev/null || true - ls -la *.zip - - - name: Create Release - uses: softprops/action-gh-release@v1 - with: - files: source/pkg_mokosuitefield.zip - generate_release_notes: true diff --git a/.gitea/workflows/pre-release.yml b/.gitea/workflows/pre-release.yml new file mode 100644 index 0000000..7e4d40e --- /dev/null +++ b/.gitea/workflows/pre-release.yml @@ -0,0 +1,66 @@ +name: Pre-Release Package + +on: + push: + tags: + - "v*" + workflow_dispatch: + inputs: + version: + description: "Version (e.g. 1.0.0-alpha.1)" + required: true + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Determine version + id: version + run: | + if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then + echo "VERSION=${{ github.event.inputs.version }}" >> $GITHUB_OUTPUT + echo "TAG=v${{ github.event.inputs.version }}" >> $GITHUB_OUTPUT + else + VERSION="${GITHUB_REF#refs/tags/v}" + echo "VERSION=$VERSION" >> $GITHUB_OUTPUT + echo "TAG=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT + fi + + - name: Build package zip + run: | + cd source + for dir in packages/*/; do + pkg_name=$(basename "$dir") + cd "$dir" + zip -r "../../${pkg_name}.zip" . -x "*.git*" + cd ../.. + done + mkdir -p pkg_build + mv *.zip pkg_build/ + cp pkg_*.xml pkg_build/ 2>/dev/null || true + cp script.php pkg_build/ 2>/dev/null || true + cd pkg_build + zip -r "../${{ github.event.repository.name }}-${{ steps.version.outputs.VERSION }}.zip" . + + - name: Create Pre-Release + uses: actions/create-release@v1 + id: create_release + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + tag_name: ${{ steps.version.outputs.TAG }} + release_name: "${{ github.event.repository.name }} ${{ steps.version.outputs.TAG }}" + draft: false + prerelease: true + + - name: Upload Release Asset + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ steps.create_release.outputs.upload_url }} + asset_path: ./source/${{ github.event.repository.name }}-${{ steps.version.outputs.VERSION }}.zip + asset_name: ${{ github.event.repository.name }}-${{ steps.version.outputs.VERSION }}.zip + asset_content_type: application/zip From 96ae9d9fc7d732bde5c2565b5aaa394176661223 Mon Sep 17 00:00:00 2001 From: "gitea-actions[bot]" Date: Tue, 30 Jun 2026 18:15:13 +0000 Subject: [PATCH 55/64] chore(version): pre-release bump to 06.00.15-dev [skip ci] --- .mokogitea/workflows/issue-branch.yml | 2 +- source/packages/com_mokosuitefield/mokosuitefield.xml | 2 +- source/packages/plg_system_mokosuitefield/mokosuitefield.xml | 2 +- .../packages/plg_webservices_mokosuitefield/mokosuitefield.xml | 2 +- source/pkg_mokosuitefield.xml | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.mokogitea/workflows/issue-branch.yml b/.mokogitea/workflows/issue-branch.yml index 1904498..65c7ace 100644 --- a/.mokogitea/workflows/issue-branch.yml +++ b/.mokogitea/workflows/issue-branch.yml @@ -5,7 +5,7 @@ # FILE INFORMATION # DEFGROUP: Gitea.Workflow # INGROUP: mokocli.Automation -# VERSION: 06.00.14 +# VERSION: 06.00.15 # BRIEF: Auto-create feature branch when an issue is opened name: "Universal: Issue Branch" diff --git a/source/packages/com_mokosuitefield/mokosuitefield.xml b/source/packages/com_mokosuitefield/mokosuitefield.xml index 2fd8bbb..edbda75 100644 --- a/source/packages/com_mokosuitefield/mokosuitefield.xml +++ b/source/packages/com_mokosuitefield/mokosuitefield.xml @@ -5,7 +5,7 @@ 2026-06-27 Copyright (C) 2026 Moko Consulting. GPL-3.0-or-later - 06.00.14 + 06.00.15 Moko\Component\MokoSuiteField servicessrctmpl diff --git a/source/packages/plg_system_mokosuitefield/mokosuitefield.xml b/source/packages/plg_system_mokosuitefield/mokosuitefield.xml index e61dfb0..1da5f18 100644 --- a/source/packages/plg_system_mokosuitefield/mokosuitefield.xml +++ b/source/packages/plg_system_mokosuitefield/mokosuitefield.xml @@ -8,7 +8,7 @@ GPL-3.0-or-later hello@mokoconsulting.tech https://mokoconsulting.tech - 06.00.14 + 06.00.15 8.3 PLG_SYSTEM_MOKOSUITEFIELD_DESC Moko\Plugin\System\MokoSuiteField diff --git a/source/packages/plg_webservices_mokosuitefield/mokosuitefield.xml b/source/packages/plg_webservices_mokosuitefield/mokosuitefield.xml index 0089817..d352ce9 100644 --- a/source/packages/plg_webservices_mokosuitefield/mokosuitefield.xml +++ b/source/packages/plg_webservices_mokosuitefield/mokosuitefield.xml @@ -3,7 +3,7 @@ Web Services - MokoSuite Field mokosuitefield Moko Consulting - 06.00.14 + 06.00.15 GPL-3.0-or-later Moko\Plugin\WebServices\MokoSuiteField srcservices diff --git a/source/pkg_mokosuitefield.xml b/source/pkg_mokosuitefield.xml index 539374f..07f4639 100644 --- a/source/pkg_mokosuitefield.xml +++ b/source/pkg_mokosuitefield.xml @@ -2,7 +2,7 @@ Package - MokoSuite Field mokosuitefield - 06.00.14 + 06.00.15 2026-06-27 Moko Consulting hello@mokoconsulting.tech From 07c80b8e5dde1173e8f10a20e6ca9245dfb47b0a Mon Sep 17 00:00:00 2001 From: "gitea-actions[bot]" Date: Tue, 30 Jun 2026 18:30:39 +0000 Subject: [PATCH 56/64] chore(version): pre-release bump to 06.00.19-dev [skip ci] --- .mokogitea/workflows/issue-branch.yml | 2 +- source/packages/com_mokosuitefield/mokosuitefield.xml | 2 +- source/packages/plg_system_mokosuitefield/mokosuitefield.xml | 2 +- .../packages/plg_webservices_mokosuitefield/mokosuitefield.xml | 2 +- source/pkg_mokosuitefield.xml | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.mokogitea/workflows/issue-branch.yml b/.mokogitea/workflows/issue-branch.yml index 65c7ace..8ebe99d 100644 --- a/.mokogitea/workflows/issue-branch.yml +++ b/.mokogitea/workflows/issue-branch.yml @@ -5,7 +5,7 @@ # FILE INFORMATION # DEFGROUP: Gitea.Workflow # INGROUP: mokocli.Automation -# VERSION: 06.00.15 +# VERSION: 06.00.19 # BRIEF: Auto-create feature branch when an issue is opened name: "Universal: Issue Branch" diff --git a/source/packages/com_mokosuitefield/mokosuitefield.xml b/source/packages/com_mokosuitefield/mokosuitefield.xml index edbda75..898aff7 100644 --- a/source/packages/com_mokosuitefield/mokosuitefield.xml +++ b/source/packages/com_mokosuitefield/mokosuitefield.xml @@ -5,7 +5,7 @@ 2026-06-27 Copyright (C) 2026 Moko Consulting. GPL-3.0-or-later - 06.00.15 + 06.00.19 Moko\Component\MokoSuiteField servicessrctmpl diff --git a/source/packages/plg_system_mokosuitefield/mokosuitefield.xml b/source/packages/plg_system_mokosuitefield/mokosuitefield.xml index 1da5f18..532e8f9 100644 --- a/source/packages/plg_system_mokosuitefield/mokosuitefield.xml +++ b/source/packages/plg_system_mokosuitefield/mokosuitefield.xml @@ -8,7 +8,7 @@ GPL-3.0-or-later hello@mokoconsulting.tech https://mokoconsulting.tech - 06.00.15 + 06.00.19 8.3 PLG_SYSTEM_MOKOSUITEFIELD_DESC Moko\Plugin\System\MokoSuiteField diff --git a/source/packages/plg_webservices_mokosuitefield/mokosuitefield.xml b/source/packages/plg_webservices_mokosuitefield/mokosuitefield.xml index d352ce9..c435113 100644 --- a/source/packages/plg_webservices_mokosuitefield/mokosuitefield.xml +++ b/source/packages/plg_webservices_mokosuitefield/mokosuitefield.xml @@ -3,7 +3,7 @@ Web Services - MokoSuite Field mokosuitefield Moko Consulting - 06.00.15 + 06.00.19 GPL-3.0-or-later Moko\Plugin\WebServices\MokoSuiteField srcservices diff --git a/source/pkg_mokosuitefield.xml b/source/pkg_mokosuitefield.xml index 07f4639..aa4b966 100644 --- a/source/pkg_mokosuitefield.xml +++ b/source/pkg_mokosuitefield.xml @@ -2,7 +2,7 @@ Package - MokoSuite Field mokosuitefield - 06.00.15 + 06.00.19 2026-06-27 Moko Consulting hello@mokoconsulting.tech From 0e3c922b7ad0f20fe350368bb210494eaf8a81d6 Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Mon, 6 Jul 2026 11:19:37 -0500 Subject: [PATCH 57/64] fix: honest installer success (gate license/next-steps on child install) The package postflight showed the "License Key Required" / next-steps message even when a bundled child extension failed to sub-install (Joomla only logs those failures). Add a fail-open manifest verifier (missingChildExtensions) that confirms every declared child landed in #__extensions and enqueues an error + returns before the license path when any are missing. Fails open so a transient glitch never fakes a failure. Claude-Session: https://claude.ai/code/session_01B9aZHSWbiiZykJD88pYx8R --- CHANGELOG.md | 3 ++ source/script.php | 95 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 98 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 525c203..7552818 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,9 @@ ## [Unreleased] +### Fixed +- **Honest install success** — the package installer no longer shows the license / next-steps message when a bundled child extension failed to install. A fail-open manifest verifier confirms every child extension landed in `#__extensions` and reports any that are missing. + ### Added - **Repository** -- initial repo creation with scaffolding - **System Plugin** -- Extension class, service provider diff --git a/source/script.php b/source/script.php index 9ce2d9b..50fddcc 100644 --- a/source/script.php +++ b/source/script.php @@ -16,9 +16,104 @@ class Pkg_MokoSuiteFieldInstallerScript { public function postflight(string $type, InstallerAdapter $adapter): void { + // Be honest about success. Joomla's package installer only LOGS a failed child + // sub-install but still runs this postflight, so don't show the license / + // next-steps message if a bundled extension is actually missing. Fails open + // (see missingChildExtensions) so a query/IO glitch never fakes a failure. + $missing = $this->missingChildExtensions($adapter); + + if (!empty($missing)) + { + Factory::getApplication()->enqueueMessage( + '

MokoSuiteField did not install correctly.

' + . '

The following bundled extensions are missing: ' + . htmlspecialchars(implode(', ', $missing), ENT_QUOTES) . '

' + . '

Please uninstall MokoSuiteField and reinstall the full package.

', + 'error' + ); + + return; + } + $this->warnMissingLicenseKey(); } + /** + * Verify every child extension declared in the package manifest actually landed in + * #__extensions. Returns readable labels of any that are missing. + * + * Matching per Joomla's #__extensions uniqueness: type = "type"; element = + * "id" (for plugins, strip a leading plg__); plugins also match + * folder = "group". + * + * FAILS OPEN: any error returns [] so a transient glitch never fakes a failure. + */ + private function missingChildExtensions($adapter): array + { + try + { + $manifest = $adapter->getParent()->getManifest(); + + if (!$manifest || !isset($manifest->files) || !isset($manifest->files->file)) + { + return []; + } + + $db = Factory::getDbo(); + $missing = []; + + foreach ($manifest->files->file as $file) + { + $attrs = $file->attributes(); + $id = isset($attrs['id']) ? (string) $attrs['id'] : ''; + $exType = isset($attrs['type']) ? (string) $attrs['type'] : ''; + + if ($id === '' || $exType === '') + { + continue; + } + + $group = isset($attrs['group']) ? (string) $attrs['group'] : ''; + $element = $id; + + // Plugin element in #__extensions is the id minus any leading plg__. + if ($exType === 'plugin' && $group !== '') + { + $prefix = 'plg_' . $group . '_'; + + if (strpos($element, $prefix) === 0) + { + $element = substr($element, \strlen($prefix)); + } + } + + $query = $db->getQuery(true) + ->select('COUNT(*)') + ->from($db->quoteName('#__extensions')) + ->where($db->quoteName('element') . ' = ' . $db->quote($element)) + ->where($db->quoteName('type') . ' = ' . $db->quote($exType)); + + if ($exType === 'plugin' && $group !== '') + { + $query->where($db->quoteName('folder') . ' = ' . $db->quote($group)); + } + + if ((int) $db->setQuery($query)->loadResult() === 0) + { + $label = trim((string) $file); + $missing[] = $label !== '' ? preg_replace('/\.zip$/i', '', $label) : $id; + } + } + + return $missing; + } + catch (\Throwable $e) + { + // Fail open — never fake a failure on a glitch. + return []; + } + } + private function warnMissingLicenseKey(): void { try From 4212ce1ded78d5de9dc92fede02b83c9de75a05a Mon Sep 17 00:00:00 2001 From: "gitea-actions[bot]" Date: Mon, 6 Jul 2026 16:20:48 +0000 Subject: [PATCH 58/64] chore(version): pre-release bump to 06.00.21-dev [skip ci] --- .mokogitea/workflows/issue-branch.yml | 2 +- source/packages/com_mokosuitefield/mokosuitefield.xml | 2 +- source/packages/plg_system_mokosuitefield/mokosuitefield.xml | 2 +- .../packages/plg_webservices_mokosuitefield/mokosuitefield.xml | 2 +- source/pkg_mokosuitefield.xml | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.mokogitea/workflows/issue-branch.yml b/.mokogitea/workflows/issue-branch.yml index 8ebe99d..64dade7 100644 --- a/.mokogitea/workflows/issue-branch.yml +++ b/.mokogitea/workflows/issue-branch.yml @@ -5,7 +5,7 @@ # FILE INFORMATION # DEFGROUP: Gitea.Workflow # INGROUP: mokocli.Automation -# VERSION: 06.00.19 +# VERSION: 06.00.21 # BRIEF: Auto-create feature branch when an issue is opened name: "Universal: Issue Branch" diff --git a/source/packages/com_mokosuitefield/mokosuitefield.xml b/source/packages/com_mokosuitefield/mokosuitefield.xml index 898aff7..5b08055 100644 --- a/source/packages/com_mokosuitefield/mokosuitefield.xml +++ b/source/packages/com_mokosuitefield/mokosuitefield.xml @@ -5,7 +5,7 @@ 2026-06-27 Copyright (C) 2026 Moko Consulting. GPL-3.0-or-later - 06.00.19 + 06.00.21 Moko\Component\MokoSuiteField servicessrctmpl diff --git a/source/packages/plg_system_mokosuitefield/mokosuitefield.xml b/source/packages/plg_system_mokosuitefield/mokosuitefield.xml index 532e8f9..82ce757 100644 --- a/source/packages/plg_system_mokosuitefield/mokosuitefield.xml +++ b/source/packages/plg_system_mokosuitefield/mokosuitefield.xml @@ -8,7 +8,7 @@ GPL-3.0-or-later hello@mokoconsulting.tech https://mokoconsulting.tech - 06.00.19 + 06.00.21 8.3 PLG_SYSTEM_MOKOSUITEFIELD_DESC Moko\Plugin\System\MokoSuiteField diff --git a/source/packages/plg_webservices_mokosuitefield/mokosuitefield.xml b/source/packages/plg_webservices_mokosuitefield/mokosuitefield.xml index c435113..11f3fa9 100644 --- a/source/packages/plg_webservices_mokosuitefield/mokosuitefield.xml +++ b/source/packages/plg_webservices_mokosuitefield/mokosuitefield.xml @@ -3,7 +3,7 @@ Web Services - MokoSuite Field mokosuitefield Moko Consulting - 06.00.19 + 06.00.21 GPL-3.0-or-later Moko\Plugin\WebServices\MokoSuiteField srcservices diff --git a/source/pkg_mokosuitefield.xml b/source/pkg_mokosuitefield.xml index aa4b966..024506f 100644 --- a/source/pkg_mokosuitefield.xml +++ b/source/pkg_mokosuitefield.xml @@ -2,7 +2,7 @@ Package - MokoSuite Field mokosuitefield - 06.00.19 + 06.00.21 2026-06-27 Moko Consulting hello@mokoconsulting.tech From 8167223c922af6ec744bf93331f7598d490cec32 Mon Sep 17 00:00:00 2001 From: Jonathan Miller <1+jmiller@noreply.git.mokoconsulting.tech> Date: Mon, 6 Jul 2026 22:37:41 +0000 Subject: [PATCH 59/64] feat(menu): add COM_MOKOSUITEFIELD_SHORT short-name constant (#44) Claude-Session: https://claude.ai/code/session_01B9aZHSWbiiZykJD88pYx8R --- .../com_mokosuitefield/language/en-GB/com_mokosuitefield.sys.ini | 1 + 1 file changed, 1 insertion(+) diff --git a/source/components/com_mokosuitefield/language/en-GB/com_mokosuitefield.sys.ini b/source/components/com_mokosuitefield/language/en-GB/com_mokosuitefield.sys.ini index 3e6a97f..2891028 100644 --- a/source/components/com_mokosuitefield/language/en-GB/com_mokosuitefield.sys.ini +++ b/source/components/com_mokosuitefield/language/en-GB/com_mokosuitefield.sys.ini @@ -3,6 +3,7 @@ ; Authored-by: Moko Consulting COM_MOKOSUITEFIELD="MokoSuite Field" +COM_MOKOSUITEFIELD_SHORT="Field" COM_MOKOSUITEFIELD_DESCRIPTION="Field service management for contractors." COM_MOKOSUITEFIELD_MENU_DASHBOARD="Dashboard" COM_MOKOSUITEFIELD_MENU_WORKORDERS="Work Orders" From 552d31aaec8315e3f7c6bdcc506e846416dbf084 Mon Sep 17 00:00:00 2001 From: Jonathan Miller <1+jmiller@noreply.git.mokoconsulting.tech> Date: Mon, 6 Jul 2026 22:37:54 +0000 Subject: [PATCH 60/64] feat(menu): use COM_MOKOSUITEFIELD_SHORT for admin menu label (#44) Claude-Session: https://claude.ai/code/session_01B9aZHSWbiiZykJD88pYx8R --- source/packages/com_mokosuitefield/mokosuitefield.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source/packages/com_mokosuitefield/mokosuitefield.xml b/source/packages/com_mokosuitefield/mokosuitefield.xml index 5b08055..598f85f 100644 --- a/source/packages/com_mokosuitefield/mokosuitefield.xml +++ b/source/packages/com_mokosuitefield/mokosuitefield.xml @@ -9,6 +9,6 @@ Moko\Component\MokoSuiteField servicessrctmpl - COM_MOKOSUITEFIELD + COM_MOKOSUITEFIELD_SHORT From 9309500fe5e63d089c6e77e6fffe7308297ee3c6 Mon Sep 17 00:00:00 2001 From: "gitea-actions[bot]" Date: Mon, 6 Jul 2026 22:40:56 +0000 Subject: [PATCH 61/64] chore(version): pre-release bump to 06.00.22-dev [skip ci] --- .mokogitea/workflows/issue-branch.yml | 2 +- source/packages/com_mokosuitefield/mokosuitefield.xml | 2 +- source/packages/plg_system_mokosuitefield/mokosuitefield.xml | 2 +- .../packages/plg_webservices_mokosuitefield/mokosuitefield.xml | 2 +- source/pkg_mokosuitefield.xml | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.mokogitea/workflows/issue-branch.yml b/.mokogitea/workflows/issue-branch.yml index 64dade7..04a31b0 100644 --- a/.mokogitea/workflows/issue-branch.yml +++ b/.mokogitea/workflows/issue-branch.yml @@ -5,7 +5,7 @@ # FILE INFORMATION # DEFGROUP: Gitea.Workflow # INGROUP: mokocli.Automation -# VERSION: 06.00.21 +# VERSION: 06.00.22 # BRIEF: Auto-create feature branch when an issue is opened name: "Universal: Issue Branch" diff --git a/source/packages/com_mokosuitefield/mokosuitefield.xml b/source/packages/com_mokosuitefield/mokosuitefield.xml index 598f85f..12c64a2 100644 --- a/source/packages/com_mokosuitefield/mokosuitefield.xml +++ b/source/packages/com_mokosuitefield/mokosuitefield.xml @@ -5,7 +5,7 @@ 2026-06-27 Copyright (C) 2026 Moko Consulting. GPL-3.0-or-later - 06.00.21 + 06.00.22 Moko\Component\MokoSuiteField servicessrctmpl diff --git a/source/packages/plg_system_mokosuitefield/mokosuitefield.xml b/source/packages/plg_system_mokosuitefield/mokosuitefield.xml index 82ce757..eb5c3d4 100644 --- a/source/packages/plg_system_mokosuitefield/mokosuitefield.xml +++ b/source/packages/plg_system_mokosuitefield/mokosuitefield.xml @@ -8,7 +8,7 @@ GPL-3.0-or-later hello@mokoconsulting.tech https://mokoconsulting.tech - 06.00.21 + 06.00.22 8.3 PLG_SYSTEM_MOKOSUITEFIELD_DESC Moko\Plugin\System\MokoSuiteField diff --git a/source/packages/plg_webservices_mokosuitefield/mokosuitefield.xml b/source/packages/plg_webservices_mokosuitefield/mokosuitefield.xml index 11f3fa9..95671df 100644 --- a/source/packages/plg_webservices_mokosuitefield/mokosuitefield.xml +++ b/source/packages/plg_webservices_mokosuitefield/mokosuitefield.xml @@ -3,7 +3,7 @@ Web Services - MokoSuite Field mokosuitefield Moko Consulting - 06.00.21 + 06.00.22 GPL-3.0-or-later Moko\Plugin\WebServices\MokoSuiteField srcservices diff --git a/source/pkg_mokosuitefield.xml b/source/pkg_mokosuitefield.xml index 024506f..8f6afac 100644 --- a/source/pkg_mokosuitefield.xml +++ b/source/pkg_mokosuitefield.xml @@ -2,7 +2,7 @@ Package - MokoSuite Field mokosuitefield - 06.00.21 + 06.00.22 2026-06-27 Moko Consulting hello@mokoconsulting.tech From 2585d540188061f426a219150d9e4cff457a6db5 Mon Sep 17 00:00:00 2001 From: "gitea-actions[bot]" Date: Mon, 6 Jul 2026 22:41:25 +0000 Subject: [PATCH 62/64] chore(version): pre-release bump to 06.00.23-dev [skip ci] --- .mokogitea/workflows/issue-branch.yml | 2 +- source/packages/com_mokosuitefield/mokosuitefield.xml | 2 +- source/packages/plg_system_mokosuitefield/mokosuitefield.xml | 2 +- .../packages/plg_webservices_mokosuitefield/mokosuitefield.xml | 2 +- source/pkg_mokosuitefield.xml | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.mokogitea/workflows/issue-branch.yml b/.mokogitea/workflows/issue-branch.yml index 04a31b0..8e808c0 100644 --- a/.mokogitea/workflows/issue-branch.yml +++ b/.mokogitea/workflows/issue-branch.yml @@ -5,7 +5,7 @@ # FILE INFORMATION # DEFGROUP: Gitea.Workflow # INGROUP: mokocli.Automation -# VERSION: 06.00.22 +# VERSION: 06.00.23 # BRIEF: Auto-create feature branch when an issue is opened name: "Universal: Issue Branch" diff --git a/source/packages/com_mokosuitefield/mokosuitefield.xml b/source/packages/com_mokosuitefield/mokosuitefield.xml index 12c64a2..46454fb 100644 --- a/source/packages/com_mokosuitefield/mokosuitefield.xml +++ b/source/packages/com_mokosuitefield/mokosuitefield.xml @@ -5,7 +5,7 @@ 2026-06-27 Copyright (C) 2026 Moko Consulting. GPL-3.0-or-later - 06.00.22 + 06.00.23 Moko\Component\MokoSuiteField servicessrctmpl diff --git a/source/packages/plg_system_mokosuitefield/mokosuitefield.xml b/source/packages/plg_system_mokosuitefield/mokosuitefield.xml index eb5c3d4..ff0fd12 100644 --- a/source/packages/plg_system_mokosuitefield/mokosuitefield.xml +++ b/source/packages/plg_system_mokosuitefield/mokosuitefield.xml @@ -8,7 +8,7 @@ GPL-3.0-or-later hello@mokoconsulting.tech https://mokoconsulting.tech - 06.00.22 + 06.00.23 8.3 PLG_SYSTEM_MOKOSUITEFIELD_DESC Moko\Plugin\System\MokoSuiteField diff --git a/source/packages/plg_webservices_mokosuitefield/mokosuitefield.xml b/source/packages/plg_webservices_mokosuitefield/mokosuitefield.xml index 95671df..33a93d4 100644 --- a/source/packages/plg_webservices_mokosuitefield/mokosuitefield.xml +++ b/source/packages/plg_webservices_mokosuitefield/mokosuitefield.xml @@ -3,7 +3,7 @@ Web Services - MokoSuite Field mokosuitefield Moko Consulting - 06.00.22 + 06.00.23 GPL-3.0-or-later Moko\Plugin\WebServices\MokoSuiteField srcservices diff --git a/source/pkg_mokosuitefield.xml b/source/pkg_mokosuitefield.xml index 8f6afac..f51731d 100644 --- a/source/pkg_mokosuitefield.xml +++ b/source/pkg_mokosuitefield.xml @@ -2,7 +2,7 @@ Package - MokoSuite Field mokosuitefield - 06.00.22 + 06.00.23 2026-06-27 Moko Consulting hello@mokoconsulting.tech From dd7b089d30b1264fba2f49c0226800c06093b516 Mon Sep 17 00:00:00 2001 From: "gitea-actions[bot]" Date: Mon, 6 Jul 2026 22:42:25 +0000 Subject: [PATCH 63/64] chore(version): pre-release bump to 06.00.24-dev [skip ci] --- .mokogitea/workflows/issue-branch.yml | 2 +- source/packages/com_mokosuitefield/mokosuitefield.xml | 2 +- source/packages/plg_system_mokosuitefield/mokosuitefield.xml | 2 +- .../packages/plg_webservices_mokosuitefield/mokosuitefield.xml | 2 +- source/pkg_mokosuitefield.xml | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.mokogitea/workflows/issue-branch.yml b/.mokogitea/workflows/issue-branch.yml index 8e808c0..5aeee64 100644 --- a/.mokogitea/workflows/issue-branch.yml +++ b/.mokogitea/workflows/issue-branch.yml @@ -5,7 +5,7 @@ # FILE INFORMATION # DEFGROUP: Gitea.Workflow # INGROUP: mokocli.Automation -# VERSION: 06.00.23 +# VERSION: 06.00.24 # BRIEF: Auto-create feature branch when an issue is opened name: "Universal: Issue Branch" diff --git a/source/packages/com_mokosuitefield/mokosuitefield.xml b/source/packages/com_mokosuitefield/mokosuitefield.xml index 46454fb..214516c 100644 --- a/source/packages/com_mokosuitefield/mokosuitefield.xml +++ b/source/packages/com_mokosuitefield/mokosuitefield.xml @@ -5,7 +5,7 @@ 2026-06-27 Copyright (C) 2026 Moko Consulting. GPL-3.0-or-later - 06.00.23 + 06.00.24 Moko\Component\MokoSuiteField servicessrctmpl diff --git a/source/packages/plg_system_mokosuitefield/mokosuitefield.xml b/source/packages/plg_system_mokosuitefield/mokosuitefield.xml index ff0fd12..4217e2e 100644 --- a/source/packages/plg_system_mokosuitefield/mokosuitefield.xml +++ b/source/packages/plg_system_mokosuitefield/mokosuitefield.xml @@ -8,7 +8,7 @@ GPL-3.0-or-later hello@mokoconsulting.tech https://mokoconsulting.tech - 06.00.23 + 06.00.24 8.3 PLG_SYSTEM_MOKOSUITEFIELD_DESC Moko\Plugin\System\MokoSuiteField diff --git a/source/packages/plg_webservices_mokosuitefield/mokosuitefield.xml b/source/packages/plg_webservices_mokosuitefield/mokosuitefield.xml index 33a93d4..20d1bb6 100644 --- a/source/packages/plg_webservices_mokosuitefield/mokosuitefield.xml +++ b/source/packages/plg_webservices_mokosuitefield/mokosuitefield.xml @@ -3,7 +3,7 @@ Web Services - MokoSuite Field mokosuitefield Moko Consulting - 06.00.23 + 06.00.24 GPL-3.0-or-later Moko\Plugin\WebServices\MokoSuiteField srcservices diff --git a/source/pkg_mokosuitefield.xml b/source/pkg_mokosuitefield.xml index f51731d..db50df1 100644 --- a/source/pkg_mokosuitefield.xml +++ b/source/pkg_mokosuitefield.xml @@ -2,7 +2,7 @@ Package - MokoSuite Field mokosuitefield - 06.00.23 + 06.00.24 2026-06-27 Moko Consulting hello@mokoconsulting.tech From 196d237e9f1b9c3a8bacfaa93ed72c811eafac2c Mon Sep 17 00:00:00 2001 From: "gitea-actions[bot]" Date: Tue, 7 Jul 2026 10:16:31 +0000 Subject: [PATCH 64/64] chore(version): pre-release bump to 06.00.25-dev [skip ci] --- .mokogitea/workflows/issue-branch.yml | 2 +- source/packages/com_mokosuitefield/mokosuitefield.xml | 2 +- source/packages/plg_system_mokosuitefield/mokosuitefield.xml | 2 +- .../packages/plg_webservices_mokosuitefield/mokosuitefield.xml | 2 +- source/pkg_mokosuitefield.xml | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.mokogitea/workflows/issue-branch.yml b/.mokogitea/workflows/issue-branch.yml index 5aeee64..47d0050 100644 --- a/.mokogitea/workflows/issue-branch.yml +++ b/.mokogitea/workflows/issue-branch.yml @@ -5,7 +5,7 @@ # FILE INFORMATION # DEFGROUP: Gitea.Workflow # INGROUP: mokocli.Automation -# VERSION: 06.00.24 +# VERSION: 06.00.25 # BRIEF: Auto-create feature branch when an issue is opened name: "Universal: Issue Branch" diff --git a/source/packages/com_mokosuitefield/mokosuitefield.xml b/source/packages/com_mokosuitefield/mokosuitefield.xml index 214516c..6574c50 100644 --- a/source/packages/com_mokosuitefield/mokosuitefield.xml +++ b/source/packages/com_mokosuitefield/mokosuitefield.xml @@ -5,7 +5,7 @@ 2026-06-27 Copyright (C) 2026 Moko Consulting. GPL-3.0-or-later - 06.00.24 + 06.00.25 Moko\Component\MokoSuiteField servicessrctmpl diff --git a/source/packages/plg_system_mokosuitefield/mokosuitefield.xml b/source/packages/plg_system_mokosuitefield/mokosuitefield.xml index 4217e2e..e2346ee 100644 --- a/source/packages/plg_system_mokosuitefield/mokosuitefield.xml +++ b/source/packages/plg_system_mokosuitefield/mokosuitefield.xml @@ -8,7 +8,7 @@ GPL-3.0-or-later hello@mokoconsulting.tech https://mokoconsulting.tech - 06.00.24 + 06.00.25 8.3 PLG_SYSTEM_MOKOSUITEFIELD_DESC Moko\Plugin\System\MokoSuiteField diff --git a/source/packages/plg_webservices_mokosuitefield/mokosuitefield.xml b/source/packages/plg_webservices_mokosuitefield/mokosuitefield.xml index 20d1bb6..718dc0d 100644 --- a/source/packages/plg_webservices_mokosuitefield/mokosuitefield.xml +++ b/source/packages/plg_webservices_mokosuitefield/mokosuitefield.xml @@ -3,7 +3,7 @@ Web Services - MokoSuite Field mokosuitefield Moko Consulting - 06.00.24 + 06.00.25 GPL-3.0-or-later Moko\Plugin\WebServices\MokoSuiteField srcservices diff --git a/source/pkg_mokosuitefield.xml b/source/pkg_mokosuitefield.xml index db50df1..e0f9c37 100644 --- a/source/pkg_mokosuitefield.xml +++ b/source/pkg_mokosuitefield.xml @@ -2,7 +2,7 @@ Package - MokoSuite Field mokosuitefield - 06.00.24 + 06.00.25 2026-06-27 Moko Consulting hello@mokoconsulting.tech