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() ?: []; + } +}