feat: TechnicianSkillHelper — skill matrix, best-match dispatch, cert expiry tracking
This commit is contained in:
@@ -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() ?: [];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user