Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3f73e680f0 | |||
| 353ed547f7 | |||
| c9d529f412 | |||
| 78c41a70ca | |||
| a772f4b872 | |||
| e6ba7ade71 | |||
| f9eb1153e6 | |||
| 0e9f4888c9 | |||
| c72f679cfb |
@@ -0,0 +1,76 @@
|
||||
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
# 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
|
||||
@@ -5,7 +5,7 @@
|
||||
# FILE INFORMATION
|
||||
# DEFGROUP: Gitea.Workflow
|
||||
# INGROUP: mokocli.Automation
|
||||
# VERSION: 01.05.00
|
||||
# VERSION: 01.06.00
|
||||
# BRIEF: Auto-create feature branch when an issue is opened
|
||||
|
||||
name: "Universal: Issue Branch"
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
<?php
|
||||
namespace Moko\Plugin\System\MokoSuiteField\Helper;
|
||||
|
||||
defined('_JEXEC') or die;
|
||||
|
||||
use Joomla\CMS\Factory;
|
||||
use Joomla\Database\DatabaseInterface;
|
||||
|
||||
/**
|
||||
* Technician skill matrix — certifications, skill-based dispatch matching, expiry tracking.
|
||||
*/
|
||||
class TechnicianSkillHelper
|
||||
{
|
||||
/**
|
||||
* Get skill matrix for all active technicians.
|
||||
*/
|
||||
public static function getSkillMatrix(): array
|
||||
{
|
||||
$db = Factory::getContainer()->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,7 +2,7 @@
|
||||
<extension type="package" method="upgrade">
|
||||
<name>Package - MokoSuite Field</name>
|
||||
<packagename>mokosuitefield</packagename>
|
||||
<version>01.05.00</version>
|
||||
<version>01.06.00</version>
|
||||
<creationDate>2026-06-12</creationDate>
|
||||
<author>Moko Consulting</author>
|
||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||
|
||||
Reference in New Issue
Block a user