feat: PartsHelper #17

Merged
jmiller merged 1 commits from dev into main 2026-06-20 18:52:22 +00:00
@@ -0,0 +1,107 @@
<?php
namespace Moko\Plugin\System\MokoSuiteField\Helper;
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\Database\DatabaseInterface;
/**
* Parts management — common parts lookup, usage tracking, reorder alerts, cost tracking.
*/
class PartsHelper
{
/**
* Get frequently used parts by trade for quick-add on work orders.
*/
public static function getCommonParts(string $trade = '', int $limit = 20): array
{
$db = Factory::getContainer()->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];
}
}