From f9eb1153e6b3cd449bf169502433d9accb6b271f Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Sun, 21 Jun 2026 08:49:40 -0500 Subject: [PATCH 1/5] =?UTF-8?q?feat:=20TechnicianSkillHelper=20=E2=80=94?= =?UTF-8?q?=20skill=20matrix,=20best-match=20dispatch,=20cert=20expiry=20t?= =?UTF-8?q?racking?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/Helper/TechnicianSkillHelper.php | 97 +++++++++++++++++++ 1 file changed, 97 insertions(+) create mode 100644 source/packages/plg_system_mokosuitefield/src/Helper/TechnicianSkillHelper.php diff --git a/source/packages/plg_system_mokosuitefield/src/Helper/TechnicianSkillHelper.php b/source/packages/plg_system_mokosuitefield/src/Helper/TechnicianSkillHelper.php new file mode 100644 index 0000000..bbc95d1 --- /dev/null +++ b/source/packages/plg_system_mokosuitefield/src/Helper/TechnicianSkillHelper.php @@ -0,0 +1,97 @@ +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() ?: []; + } +} -- 2.52.0 From e6ba7ade7173f11084b42f22d6e63f63dd4faf5f Mon Sep 17 00:00:00 2001 From: "gitea-actions[bot]" Date: Sun, 21 Jun 2026 13:49:51 +0000 Subject: [PATCH 2/5] chore(version): auto-bump patch 01.05.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 33cf916..3aea2aa 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.05.00 +# VERSION: 01.05.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 b9da687..1ea2670 100644 --- a/source/pkg_mokosuitefield.xml +++ b/source/pkg_mokosuitefield.xml @@ -2,7 +2,7 @@ Package - MokoSuite Field mokosuitefield - 01.05.00 + 01.05.01 2026-06-12 Moko Consulting hello@mokoconsulting.tech -- 2.52.0 From a772f4b87244460d4c24e48d1aa0b9dd0db327dc Mon Sep 17 00:00:00 2001 From: "gitea-actions[bot]" Date: Sun, 21 Jun 2026 13:50:00 +0000 Subject: [PATCH 3/5] chore(version): pre-release bump to 01.05.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 3aea2aa..7d01a20 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.05.01 +# VERSION: 01.05.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 1ea2670..52d11bd 100644 --- a/source/pkg_mokosuitefield.xml +++ b/source/pkg_mokosuitefield.xml @@ -2,7 +2,7 @@ Package - MokoSuite Field mokosuitefield - 01.05.01 + 01.05.02 2026-06-12 Moko Consulting hello@mokoconsulting.tech -- 2.52.0 From 78c41a70ca14289e38498d2d96cbdd063ea31ea9 Mon Sep 17 00:00:00 2001 From: Jonathan Miller <1+jmiller@noreply.git.mokoconsulting.tech> Date: Sun, 21 Jun 2026 06:36:05 +0000 Subject: [PATCH 4/5] chore: sync composer-publish.yml from Template-Generic [skip ci] --- .mokogitea/workflows/composer-publish.yml | 76 +++++++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100644 .mokogitea/workflows/composer-publish.yml diff --git a/.mokogitea/workflows/composer-publish.yml b/.mokogitea/workflows/composer-publish.yml new file mode 100644 index 0000000..03735c9 --- /dev/null +++ b/.mokogitea/workflows/composer-publish.yml @@ -0,0 +1,76 @@ +# 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 -- 2.52.0 From c9d529f412b1883288d00a68e7dfb20b4dd146f0 Mon Sep 17 00:00:00 2001 From: Jonathan Miller <1+jmiller@noreply.git.mokoconsulting.tech> Date: Sun, 21 Jun 2026 06:36:06 +0000 Subject: [PATCH 5/5] chore: sync issue-branch.yml from Template-Generic [skip ci] --- .mokogitea/workflows/issue-branch.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.mokogitea/workflows/issue-branch.yml b/.mokogitea/workflows/issue-branch.yml index 7d01a20..75a6963 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.05.02 +# VERSION: 01.00.00 # BRIEF: Auto-create feature branch when an issue is opened name: "Universal: Issue Branch" -- 2.52.0