[FEAT] 新增機台日誌溫度趨勢圖與同步指令日誌補全
1. 修改 ProcessCommandAck.php,將「同步商品」與「同步廣告」指令加入日誌紀錄白名單,確保同步結果正確寫入機台日誌。 2. 在 MachineController 新增 temperatureAjax API,從機台日誌中提取歷史溫度數據供圖表使用。 3. 在 web.php 註冊溫度數據 AJAX 查詢路由。 4. 在機台管理頁面 (index.blade.php) 的日誌面板中整合 ApexCharts,並於「機台狀態」分頁內嵌入溫度趨勢曲線圖。 5. 更新 Alpine.js 邏輯,實現圖表與日期篩選的同步連動,並優化面板展開時的圖表渲染。 6. 新增繁體中文、英文、日文的「溫度趨勢」翻譯標籤,並修復語言檔中的重複 key 與翻譯內容。
This commit is contained in:
parent
db2fd734ee
commit
a16b0363a8
@ -237,4 +237,34 @@ class MachineController extends AdminController
|
|||||||
'message' => __('All issues marked as resolved.')
|
'message' => __('All issues marked as resolved.')
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AJAX: 取得機台溫度歷史紀錄 (供圖表使用)
|
||||||
|
*/
|
||||||
|
public function temperatureAjax(Request $request, Machine $machine)
|
||||||
|
{
|
||||||
|
$startDate = $request->get('start_date');
|
||||||
|
$endDate = $request->get('end_date');
|
||||||
|
|
||||||
|
$logs = $machine->logs()
|
||||||
|
->where('message', 'Temperature reported: :temp°C')
|
||||||
|
->when($startDate, function ($query, $start) {
|
||||||
|
return $query->where('created_at', '>=', str_replace('T', ' ', $start));
|
||||||
|
})
|
||||||
|
->when($endDate, function ($query, $end) {
|
||||||
|
return $query->where('created_at', '<=', str_replace('T', ' ', $end));
|
||||||
|
})
|
||||||
|
->oldest()
|
||||||
|
->get();
|
||||||
|
|
||||||
|
$chartData = [
|
||||||
|
'labels' => $logs->pluck('created_at')->map(fn($date) => $date->format('Y-m-d H:i:s'))->toArray(),
|
||||||
|
'values' => $logs->map(fn($log) => (int) ($log->context['temp'] ?? 0))->toArray(),
|
||||||
|
];
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'success' => true,
|
||||||
|
'data' => $chartData
|
||||||
|
]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -134,7 +134,7 @@ class ProcessCommandAck implements ShouldQueue
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
// 記錄維護類指令到機台日誌 (MachineLog)
|
// 記錄維護類指令到機台日誌 (MachineLog)
|
||||||
if (in_array($command->command_type, ['reboot', 'reboot_card', 'lock', 'unlock', 'checkout', 'change', 'reload_stock'], true)) {
|
if (in_array($command->command_type, ['reboot', 'reboot_card', 'lock', 'unlock', 'checkout', 'change', 'reload_stock', 'update_products', 'update_ads'], true)) {
|
||||||
$msgKey = $status === 'success' ? 'log.command.success' : 'log.command.failed';
|
$msgKey = $status === 'success' ? 'log.command.success' : 'log.command.failed';
|
||||||
$level = $status === 'success' ? 'info' : 'warning';
|
$level = $status === 'success' ? 'info' : 'warning';
|
||||||
|
|
||||||
|
|||||||
@ -1607,6 +1607,7 @@
|
|||||||
"Taxation System": "Taxation System",
|
"Taxation System": "Taxation System",
|
||||||
"Temperature": "Temperature",
|
"Temperature": "Temperature",
|
||||||
"Temperature reported: :temp°C": "Temperature reported: :temp°C",
|
"Temperature reported: :temp°C": "Temperature reported: :temp°C",
|
||||||
|
"Temperature Trend": "Temperature Trend",
|
||||||
"Temperature updated to :temp°C": "Temperature updated to :temp°C",
|
"Temperature updated to :temp°C": "Temperature updated to :temp°C",
|
||||||
"Tenant": "Tenant",
|
"Tenant": "Tenant",
|
||||||
"TermID": "TermID",
|
"TermID": "TermID",
|
||||||
@ -1827,6 +1828,8 @@
|
|||||||
"command.reboot_card": "Reboot Card Reader",
|
"command.reboot_card": "Reboot Card Reader",
|
||||||
"command.reload_stock": "Sync Stock",
|
"command.reload_stock": "Sync Stock",
|
||||||
"command.unlock": "Unlock",
|
"command.unlock": "Unlock",
|
||||||
|
"command.update_ads": "Sync Ads",
|
||||||
|
"command.update_products": "Sync Products",
|
||||||
"companies": "Customer Management",
|
"companies": "Customer Management",
|
||||||
"completed": "Pickup Completed",
|
"completed": "Pickup Completed",
|
||||||
"consume": "Pickup Successful",
|
"consume": "Pickup Successful",
|
||||||
|
|||||||
@ -1606,7 +1606,8 @@
|
|||||||
"Tax System": "税務システム",
|
"Tax System": "税務システム",
|
||||||
"Taxation System": "税務システム",
|
"Taxation System": "税務システム",
|
||||||
"Temperature": "温度",
|
"Temperature": "温度",
|
||||||
"Temperature reported: :temp°C": "温度報告: :temp°C",
|
"Temperature reported: :temp°C": "温度が報告されました: :temp°C",
|
||||||
|
"Temperature Trend": "温度傾向",
|
||||||
"Temperature updated to :temp°C": "温度が :temp°C に更新されました",
|
"Temperature updated to :temp°C": "温度が :temp°C に更新されました",
|
||||||
"Tenant": "顧客 (テナント)",
|
"Tenant": "顧客 (テナント)",
|
||||||
"TermID": "端末ID (TermID)",
|
"TermID": "端末ID (TermID)",
|
||||||
@ -1827,6 +1828,8 @@
|
|||||||
"command.reboot_card": "カードリーダー再起動",
|
"command.reboot_card": "カードリーダー再起動",
|
||||||
"command.reload_stock": "在庫同期",
|
"command.reload_stock": "在庫同期",
|
||||||
"command.unlock": "ロック解除",
|
"command.unlock": "ロック解除",
|
||||||
|
"command.update_ads": "広告同期",
|
||||||
|
"command.update_products": "商品同期",
|
||||||
"companies": "顧客管理",
|
"companies": "顧客管理",
|
||||||
"completed": "受取完了",
|
"completed": "受取完了",
|
||||||
"consume": "受取成功",
|
"consume": "受取成功",
|
||||||
|
|||||||
@ -1043,7 +1043,6 @@
|
|||||||
"Normal": "正常",
|
"Normal": "正常",
|
||||||
"Not Used": "不使用",
|
"Not Used": "不使用",
|
||||||
"Not Used Description": "不使用第三方支付介接",
|
"Not Used Description": "不使用第三方支付介接",
|
||||||
"Normal": "正常",
|
|
||||||
"Note": "備註",
|
"Note": "備註",
|
||||||
"Note (optional)": "備註 (選填)",
|
"Note (optional)": "備註 (選填)",
|
||||||
"Notes": "備註",
|
"Notes": "備註",
|
||||||
@ -1630,6 +1629,7 @@
|
|||||||
"Taxation System": "稅務系統",
|
"Taxation System": "稅務系統",
|
||||||
"Temperature": "溫度",
|
"Temperature": "溫度",
|
||||||
"Temperature reported: :temp°C": "回報溫度 : :temp°C",
|
"Temperature reported: :temp°C": "回報溫度 : :temp°C",
|
||||||
|
"Temperature Trend": "溫度趨勢",
|
||||||
"Temperature updated to :temp°C": "溫度已更新為::temp°C",
|
"Temperature updated to :temp°C": "溫度已更新為::temp°C",
|
||||||
"Tenant": "客戶",
|
"Tenant": "客戶",
|
||||||
"TermID": "終端代號 (TermID)",
|
"TermID": "終端代號 (TermID)",
|
||||||
@ -1856,6 +1856,8 @@
|
|||||||
"command.reboot_card": "讀卡機重啟",
|
"command.reboot_card": "讀卡機重啟",
|
||||||
"command.reload_stock": "同步庫存",
|
"command.reload_stock": "同步庫存",
|
||||||
"command.unlock": "解鎖",
|
"command.unlock": "解鎖",
|
||||||
|
"command.update_ads": "同步廣告",
|
||||||
|
"command.update_products": "同步商品",
|
||||||
"companies": "客戶管理",
|
"companies": "客戶管理",
|
||||||
"company_id": "公司",
|
"company_id": "公司",
|
||||||
"completed": "取貨完成",
|
"completed": "取貨完成",
|
||||||
|
|||||||
@ -54,6 +54,8 @@
|
|||||||
lastPage: 1,
|
lastPage: 1,
|
||||||
totalLogs: 0,
|
totalLogs: 0,
|
||||||
|
|
||||||
|
temperatureChart: null,
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
const pad = (n) => String(n).padStart(2, '0');
|
const pad = (n) => String(n).padStart(2, '0');
|
||||||
@ -62,6 +64,10 @@
|
|||||||
this.startDate = formatDate(now, '00:00');
|
this.startDate = formatDate(now, '00:00');
|
||||||
this.endDate = formatDate(now, '23:59');
|
this.endDate = formatDate(now, '23:59');
|
||||||
this.$watch('activeTab', () => this.fetchLogs(1));
|
this.$watch('activeTab', () => this.fetchLogs(1));
|
||||||
|
|
||||||
|
// 監聽日期變動,同步更新圖表 (僅在當前為狀態分頁時)
|
||||||
|
this.$watch('startDate', () => { if(this.activeTab === 'status') this.fetchTemperatureData(); });
|
||||||
|
this.$watch('endDate', () => { if(this.activeTab === 'status') this.fetchTemperatureData(); });
|
||||||
},
|
},
|
||||||
|
|
||||||
async openLogPanel(id, sn, name) {
|
async openLogPanel(id, sn, name) {
|
||||||
@ -72,6 +78,97 @@
|
|||||||
this.showLogPanel = true;
|
this.showLogPanel = true;
|
||||||
this.activeTab = 'status';
|
this.activeTab = 'status';
|
||||||
await this.fetchLogs();
|
await this.fetchLogs();
|
||||||
|
await this.fetchTemperatureData();
|
||||||
|
},
|
||||||
|
|
||||||
|
async fetchTemperatureData() {
|
||||||
|
if (this.activeTab !== 'status') return;
|
||||||
|
try {
|
||||||
|
let url = `/admin/machines/${this.currentMachineId}/temperature-ajax?`;
|
||||||
|
if (this.startDate) url += '&start_date=' + this.startDate;
|
||||||
|
if (this.endDate) url += '&end_date=' + this.endDate;
|
||||||
|
|
||||||
|
const res = await fetch(url);
|
||||||
|
const data = await res.json();
|
||||||
|
if (data.success) {
|
||||||
|
this.initTemperatureChart(data.data);
|
||||||
|
}
|
||||||
|
} catch (e) { console.error('fetchTemperatureData error:', e); }
|
||||||
|
},
|
||||||
|
|
||||||
|
initTemperatureChart(chartData) {
|
||||||
|
this.$nextTick(() => {
|
||||||
|
const chartEl = document.querySelector("#temperature-chart");
|
||||||
|
if (!chartEl) return;
|
||||||
|
|
||||||
|
const isDark = document.documentElement.classList.contains('dark');
|
||||||
|
|
||||||
|
const options = {
|
||||||
|
series: [{
|
||||||
|
name: "{{ __('Temperature') }}",
|
||||||
|
data: chartData.values
|
||||||
|
}],
|
||||||
|
chart: {
|
||||||
|
type: 'area',
|
||||||
|
height: 200,
|
||||||
|
toolbar: { show: false },
|
||||||
|
zoom: { enabled: false },
|
||||||
|
animations: { enabled: true },
|
||||||
|
background: 'transparent'
|
||||||
|
},
|
||||||
|
colors: ['#06b6d4'],
|
||||||
|
fill: {
|
||||||
|
type: 'gradient',
|
||||||
|
gradient: {
|
||||||
|
shadeIntensity: 1,
|
||||||
|
opacityFrom: 0.45,
|
||||||
|
opacityTo: 0.05,
|
||||||
|
stops: [20, 100]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
dataLabels: { enabled: false },
|
||||||
|
stroke: {
|
||||||
|
curve: 'smooth',
|
||||||
|
width: 3
|
||||||
|
},
|
||||||
|
grid: {
|
||||||
|
borderColor: isDark ? '#1e293b' : '#f1f5f9',
|
||||||
|
strokeDashArray: 4,
|
||||||
|
padding: { left: 10, right: 10 }
|
||||||
|
},
|
||||||
|
xaxis: {
|
||||||
|
categories: chartData.labels,
|
||||||
|
labels: {
|
||||||
|
show: true,
|
||||||
|
style: { colors: '#94a3b8', fontSize: '10px', fontWeight: 600 },
|
||||||
|
formatter: (val) => {
|
||||||
|
if (!val) return '';
|
||||||
|
return val.split(' ')[1].substring(0, 5); // 僅顯示 HH:mm
|
||||||
|
}
|
||||||
|
},
|
||||||
|
axisBorder: { show: false },
|
||||||
|
axisTicks: { show: false },
|
||||||
|
tooltip: { enabled: false }
|
||||||
|
},
|
||||||
|
yaxis: {
|
||||||
|
labels: {
|
||||||
|
style: { colors: '#94a3b8', fontSize: '10px', fontWeight: 600 },
|
||||||
|
formatter: (val) => val + '°C'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
tooltip: {
|
||||||
|
theme: isDark ? 'dark' : 'light',
|
||||||
|
x: { formatter: (val) => val }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (this.temperatureChart) {
|
||||||
|
this.temperatureChart.updateOptions(options);
|
||||||
|
} else {
|
||||||
|
this.temperatureChart = new ApexCharts(chartEl, options);
|
||||||
|
this.temperatureChart.render();
|
||||||
|
}
|
||||||
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
openEditModal(id, name) {
|
openEditModal(id, name) {
|
||||||
@ -540,7 +637,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<!-- Offcanvas Log Panel -->
|
<!-- Offcanvas Log Panel -->
|
||||||
<div x-show="showLogPanel" class="fixed inset-0 z-[100] overflow-hidden" style="display: none;"
|
<div x-show="showLogPanel" class="fixed inset-0 z-[100] overflow-hidden" style="display: none;"
|
||||||
aria-labelledby="slide-over-title" role="dialog" aria-modal="true">
|
aria-labelledby="slide-over-title" role="dialog" aria-modal="true">
|
||||||
@ -738,6 +834,21 @@
|
|||||||
{{ __('Loading Data') }}...</p>
|
{{ __('Loading Data') }}...</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Temperature Trend Chart -->
|
||||||
|
<div x-show="activeTab === 'status'"
|
||||||
|
class="mb-6 p-4 rounded-2xl bg-slate-50/50 dark:bg-white/[0.02] border border-slate-100 dark:border-slate-800/50">
|
||||||
|
<div class="flex items-center justify-between mb-4 px-2">
|
||||||
|
<h3 class="text-[11px] font-black text-slate-400 uppercase tracking-[0.2em] flex items-center gap-2">
|
||||||
|
<span class="w-1.5 h-1.5 rounded-full bg-cyan-500 shadow-[0_0_8px_rgba(6,182,212,0.5)]"></span>
|
||||||
|
{{ __('Temperature Trend') }}
|
||||||
|
</h3>
|
||||||
|
<span class="text-[10px] font-bold text-slate-400" x-show="logs.length > 0">
|
||||||
|
<span x-text="currentMachineName"></span> @ <span x-text="startDate.split(' ')[0]"></span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div id="temperature-chart" class="min-h-[200px] w-full"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Logs Container -->
|
<!-- Logs Container -->
|
||||||
<div x-show="activeTab !== 'expiry'"
|
<div x-show="activeTab !== 'expiry'"
|
||||||
class="luxury-card border border-slate-200 dark:border-slate-800 rounded-2xl overflow-hidden shadow-sm">
|
class="luxury-card border border-slate-200 dark:border-slate-800 rounded-2xl overflow-hidden shadow-sm">
|
||||||
@ -1206,4 +1317,8 @@
|
|||||||
message="{{ __('Are you sure you want to resolve all recent issues for this machine?') }}"
|
message="{{ __('Are you sure you want to resolve all recent issues for this machine?') }}"
|
||||||
confirmText="{{ __('Confirm') }}" cancelText="{{ __('Cancel') }}" confirmColor="rose" />
|
confirmText="{{ __('Confirm') }}" cancelText="{{ __('Cancel') }}" confirmColor="rose" />
|
||||||
|
|
||||||
@endsection
|
@endsection
|
||||||
|
|
||||||
|
@push('scripts')
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/apexcharts"></script>
|
||||||
|
@endpush
|
||||||
@ -54,6 +54,7 @@ Route::middleware(['auth', 'verified', 'tenant.access'])->prefix('admin')->name(
|
|||||||
Route::get('/{machine}/slots-ajax', [App\Http\Controllers\Admin\MachineController::class, 'slotsAjax'])->name('slots-ajax');
|
Route::get('/{machine}/slots-ajax', [App\Http\Controllers\Admin\MachineController::class, 'slotsAjax'])->name('slots-ajax');
|
||||||
Route::post('/{machine}/slots/expiry', [App\Http\Controllers\Admin\MachineController::class, 'updateSlotExpiry'])->name('slots.expiry.update');
|
Route::post('/{machine}/slots/expiry', [App\Http\Controllers\Admin\MachineController::class, 'updateSlotExpiry'])->name('slots.expiry.update');
|
||||||
Route::get('/{machine}/logs-ajax', [App\Http\Controllers\Admin\MachineController::class, 'logsAjax'])->name('logs-ajax');
|
Route::get('/{machine}/logs-ajax', [App\Http\Controllers\Admin\MachineController::class, 'logsAjax'])->name('logs-ajax');
|
||||||
|
Route::get('/{machine}/temperature-ajax', [App\Http\Controllers\Admin\MachineController::class, 'temperatureAjax'])->name('temperature-ajax');
|
||||||
Route::post('/{machine}/resolve-logs', [App\Http\Controllers\Admin\MachineController::class, 'resolveLogs'])->name('resolve-logs');
|
Route::post('/{machine}/resolve-logs', [App\Http\Controllers\Admin\MachineController::class, 'resolveLogs'])->name('resolve-logs');
|
||||||
});
|
});
|
||||||
Route::resource('machines', App\Http\Controllers\Admin\MachineController::class);
|
Route::resource('machines', App\Http\Controllers\Admin\MachineController::class);
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user