[FIX] 整合機台效期管理功能並優化 UI 比例
All checks were successful
star-cloud-deploy-demo / deploy-demo (push) Successful in 1m2s
All checks were successful
star-cloud-deploy-demo / deploy-demo (push) Successful in 1m2s
- 修正 Alpine.js 作用域問題,恢復效期編輯彈窗功能 - 整合機台日誌與效期管理至主列表頁 (Index) - 優化大螢幕貨道格線佈局,解決日期折行問題 - 縮小彈窗字體與內距,調整為極簡奢華風 UI - 新增貨道效期與批號欄位之 Migration 與模型關聯 - 補齊中、英、日三語系翻譯檔
This commit is contained in:
@@ -21,12 +21,20 @@ class MachineService
|
||||
return DB::transaction(function () use ($serialNo, $data) {
|
||||
$machine = Machine::where('serial_no', $serialNo)->firstOrFail();
|
||||
|
||||
// 參數相容性處理 (Mapping legacy fields to new fields)
|
||||
$temperature = $data['temperature'] ?? $machine->temperature;
|
||||
$currentPage = $data['current_page'] ?? $data['M_Stus2'] ?? $machine->current_page;
|
||||
$doorStatus = $data['door_status'] ?? $data['door'] ?? $machine->door_status;
|
||||
$firmwareVersion = $data['firmware_version'] ?? $data['M_Ver'] ?? $machine->firmware_version;
|
||||
$model = $data['model'] ?? $data['M_Stus'] ?? $machine->model;
|
||||
|
||||
$updateData = [
|
||||
'status' => 'online',
|
||||
'temperature' => $data['temperature'] ?? $machine->temperature,
|
||||
'current_page' => $data['current_page'] ?? $machine->current_page,
|
||||
'door_status' => $data['door_status'] ?? $machine->door_status,
|
||||
'firmware_version' => $data['firmware_version'] ?? $machine->firmware_version,
|
||||
'temperature' => $temperature,
|
||||
'current_page' => $currentPage,
|
||||
'door_status' => $doorStatus,
|
||||
'firmware_version' => $firmwareVersion,
|
||||
'model' => $model,
|
||||
'last_heartbeat_at' => now(),
|
||||
];
|
||||
|
||||
@@ -35,9 +43,11 @@ class MachineService
|
||||
// Record log if provided
|
||||
if (!empty($data['log'])) {
|
||||
$machine->logs()->create([
|
||||
'company_id' => $machine->company_id,
|
||||
'type' => 'status',
|
||||
'level' => $data['log_level'] ?? 'info',
|
||||
'message' => $data['log'],
|
||||
'payload' => $data['log_payload'] ?? null,
|
||||
'context' => $data['log_payload'] ?? null,
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -46,17 +56,43 @@ class MachineService
|
||||
}
|
||||
|
||||
/**
|
||||
* Update machine slot stock.
|
||||
* Sync machine slots based on replenishment report.
|
||||
*
|
||||
* @param Machine $machine
|
||||
* @param array $slotsData
|
||||
*/
|
||||
public function updateSlotStock(Machine $machine, int $slotNo, int $stock): void
|
||||
public function syncSlots(Machine $machine, array $slotsData): void
|
||||
{
|
||||
$machine->slots()->where('slot_no', $slotNo)->update([
|
||||
'stock' => $stock,
|
||||
'last_restocked_at' => now(),
|
||||
]);
|
||||
DB::transaction(function () use ($machine, $slotsData) {
|
||||
foreach ($slotsData as $slotData) {
|
||||
$slotNo = $slotData['slot_no'] ?? null;
|
||||
if (!$slotNo) continue;
|
||||
|
||||
$existingSlot = $machine->slots()->where('slot_no', $slotNo)->first();
|
||||
|
||||
$updateData = [
|
||||
'product_id' => $slotData['product_id'] ?? null,
|
||||
'stock' => $slotData['stock'] ?? 0,
|
||||
'capacity' => $slotData['capacity'] ?? ($existingSlot->capacity ?? 10),
|
||||
'price' => $slotData['price'] ?? ($existingSlot->price ?? 0),
|
||||
'last_restocked_at' => now(),
|
||||
];
|
||||
|
||||
// 如果商品變了,或者這是一次明確的補貨回報,清空效期等待管理員更新
|
||||
// 這裡我們暫定只要有 report 進來,就需要重新確認效期
|
||||
$updateData['expiry_date'] = null;
|
||||
|
||||
if ($existingSlot) {
|
||||
$existingSlot->update($updateData);
|
||||
} else {
|
||||
$machine->slots()->create(array_merge($updateData, ['slot_no' => $slotNo]));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Update machine slot stock (single slot).
|
||||
* Legacy support for recordLog (Existing code).
|
||||
*/
|
||||
public function recordLog(int $machineId, array $data): MachineLog
|
||||
@@ -66,7 +102,129 @@ class MachineService
|
||||
return $machine->logs()->create([
|
||||
'level' => $data['level'] ?? 'info',
|
||||
'message' => $data['message'],
|
||||
'payload' => $data['context'] ?? null,
|
||||
'context' => $data['context'] ?? null,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get machine utilization and OEE statistics.
|
||||
*/
|
||||
public function getUtilizationStats(Machine $machine, string $date): array
|
||||
{
|
||||
$start = Carbon::parse($date)->startOfDay();
|
||||
$end = Carbon::parse($date)->endOfDay();
|
||||
|
||||
// 1. Availability: Based on heartbeat logs (status type)
|
||||
// Assume online if heartbeat within 6 minutes
|
||||
$logs = $machine->logs()
|
||||
->where('type', 'status')
|
||||
->whereBetween('created_at', [$start, $end])
|
||||
->orderBy('created_at')
|
||||
->get();
|
||||
|
||||
$onlineMinutes = 0;
|
||||
$lastLogTime = null;
|
||||
|
||||
foreach ($logs as $log) {
|
||||
$currentTime = Carbon::parse($log->created_at);
|
||||
if ($lastLogTime) {
|
||||
$diff = $currentTime->diffInMinutes($lastLogTime);
|
||||
if ($diff <= 6) {
|
||||
$onlineMinutes += $diff;
|
||||
}
|
||||
}
|
||||
$lastLogTime = $currentTime;
|
||||
}
|
||||
|
||||
$totalMinutes = 24 * 60;
|
||||
$availability = ($totalMinutes > 0) ? min(100, ($onlineMinutes / $totalMinutes) * 100) : 0;
|
||||
|
||||
// 2. Performance: Sales Count (B600)
|
||||
// Target: 2 sales per hour (48/day)
|
||||
$salesCount = $machine->logs()
|
||||
->where('message', 'like', '%B600%')
|
||||
->whereBetween('created_at', [$start, $end])
|
||||
->count();
|
||||
|
||||
$targetSales = 48;
|
||||
$performance = ($targetSales > 0) ? min(100, ($salesCount / $targetSales) * 100) : 0;
|
||||
|
||||
// 3. Quality: Success Rate
|
||||
// Exclude failed dispense (B130)
|
||||
$errorCount = $machine->logs()
|
||||
->where('message', 'like', '%B130%')
|
||||
->whereBetween('created_at', [$start, $end])
|
||||
->count();
|
||||
|
||||
$totalAttempts = $salesCount + $errorCount;
|
||||
$quality = ($totalAttempts > 0) ? (($salesCount / $totalAttempts) * 100) : 100;
|
||||
|
||||
// Combined OEE
|
||||
$oee = ($availability / 100) * ($performance / 100) * ($quality / 100) * 100;
|
||||
|
||||
return [
|
||||
'overview' => [
|
||||
'availability' => round($availability, 2),
|
||||
'performance' => round($performance, 2),
|
||||
'quality' => round($quality, 2),
|
||||
'oee' => round($oee, 2),
|
||||
'onlineHours' => round($onlineMinutes / 60, 2),
|
||||
'salesCount' => $salesCount,
|
||||
'errorCount' => $errorCount,
|
||||
],
|
||||
'chart' => [
|
||||
'uptime' => $this->formatUptimeTimeline($logs, $start, $end),
|
||||
'sales' => $this->formatSalesTimeline($machine, $start, $end)
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
private function formatUptimeTimeline($logs, $start, $end)
|
||||
{
|
||||
$data = [];
|
||||
if ($logs->isEmpty()) return $data;
|
||||
|
||||
$lastLog = null;
|
||||
$currentRangeStart = null;
|
||||
|
||||
foreach ($logs as $log) {
|
||||
$logTime = Carbon::parse($log->created_at);
|
||||
if (!$currentRangeStart) {
|
||||
$currentRangeStart = $logTime;
|
||||
} else {
|
||||
$diff = $logTime->diffInMinutes(Carbon::parse($lastLog->created_at));
|
||||
if ($diff > 10) { // Interruption > 10 mins
|
||||
$data[] = [
|
||||
'x' => 'Uptime',
|
||||
'y' => [$currentRangeStart->getTimestamp() * 1000, Carbon::parse($lastLog->created_at)->getTimestamp() * 1000],
|
||||
'fillColor' => '#06b6d4'
|
||||
];
|
||||
$currentRangeStart = $logTime;
|
||||
}
|
||||
}
|
||||
$lastLog = $log;
|
||||
}
|
||||
|
||||
if ($currentRangeStart && $lastLog) {
|
||||
$data[] = [
|
||||
'x' => 'Uptime',
|
||||
'y' => [$currentRangeStart->getTimestamp() * 1000, Carbon::parse($lastLog->created_at)->getTimestamp() * 1000],
|
||||
'fillColor' => '#06b6d4'
|
||||
];
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
private function formatSalesTimeline($machine, $start, $end)
|
||||
{
|
||||
return $machine->logs()
|
||||
->where('message', 'like', '%B600%')
|
||||
->whereBetween('created_at', [$start, $end])
|
||||
->get()
|
||||
->map(function($log) {
|
||||
return [Carbon::parse($log->created_at)->getTimestamp() * 1000, 1];
|
||||
})
|
||||
->toArray();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user