[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.')
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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)
|
||||
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';
|
||||
$level = $status === 'success' ? 'info' : 'warning';
|
||||
|
||||
|
||||
@ -1607,6 +1607,7 @@
|
||||
"Taxation System": "Taxation System",
|
||||
"Temperature": "Temperature",
|
||||
"Temperature reported: :temp°C": "Temperature reported: :temp°C",
|
||||
"Temperature Trend": "Temperature Trend",
|
||||
"Temperature updated to :temp°C": "Temperature updated to :temp°C",
|
||||
"Tenant": "Tenant",
|
||||
"TermID": "TermID",
|
||||
@ -1827,6 +1828,8 @@
|
||||
"command.reboot_card": "Reboot Card Reader",
|
||||
"command.reload_stock": "Sync Stock",
|
||||
"command.unlock": "Unlock",
|
||||
"command.update_ads": "Sync Ads",
|
||||
"command.update_products": "Sync Products",
|
||||
"companies": "Customer Management",
|
||||
"completed": "Pickup Completed",
|
||||
"consume": "Pickup Successful",
|
||||
|
||||
@ -1606,7 +1606,8 @@
|
||||
"Tax System": "税務システム",
|
||||
"Taxation System": "税務システム",
|
||||
"Temperature": "温度",
|
||||
"Temperature reported: :temp°C": "温度報告: :temp°C",
|
||||
"Temperature reported: :temp°C": "温度が報告されました: :temp°C",
|
||||
"Temperature Trend": "温度傾向",
|
||||
"Temperature updated to :temp°C": "温度が :temp°C に更新されました",
|
||||
"Tenant": "顧客 (テナント)",
|
||||
"TermID": "端末ID (TermID)",
|
||||
@ -1827,6 +1828,8 @@
|
||||
"command.reboot_card": "カードリーダー再起動",
|
||||
"command.reload_stock": "在庫同期",
|
||||
"command.unlock": "ロック解除",
|
||||
"command.update_ads": "広告同期",
|
||||
"command.update_products": "商品同期",
|
||||
"companies": "顧客管理",
|
||||
"completed": "受取完了",
|
||||
"consume": "受取成功",
|
||||
|
||||
@ -1043,7 +1043,6 @@
|
||||
"Normal": "正常",
|
||||
"Not Used": "不使用",
|
||||
"Not Used Description": "不使用第三方支付介接",
|
||||
"Normal": "正常",
|
||||
"Note": "備註",
|
||||
"Note (optional)": "備註 (選填)",
|
||||
"Notes": "備註",
|
||||
@ -1630,6 +1629,7 @@
|
||||
"Taxation System": "稅務系統",
|
||||
"Temperature": "溫度",
|
||||
"Temperature reported: :temp°C": "回報溫度 : :temp°C",
|
||||
"Temperature Trend": "溫度趨勢",
|
||||
"Temperature updated to :temp°C": "溫度已更新為::temp°C",
|
||||
"Tenant": "客戶",
|
||||
"TermID": "終端代號 (TermID)",
|
||||
@ -1856,6 +1856,8 @@
|
||||
"command.reboot_card": "讀卡機重啟",
|
||||
"command.reload_stock": "同步庫存",
|
||||
"command.unlock": "解鎖",
|
||||
"command.update_ads": "同步廣告",
|
||||
"command.update_products": "同步商品",
|
||||
"companies": "客戶管理",
|
||||
"company_id": "公司",
|
||||
"completed": "取貨完成",
|
||||
|
||||
@ -54,6 +54,8 @@
|
||||
lastPage: 1,
|
||||
totalLogs: 0,
|
||||
|
||||
temperatureChart: null,
|
||||
|
||||
init() {
|
||||
const now = new Date();
|
||||
const pad = (n) => String(n).padStart(2, '0');
|
||||
@ -62,6 +64,10 @@
|
||||
this.startDate = formatDate(now, '00:00');
|
||||
this.endDate = formatDate(now, '23:59');
|
||||
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) {
|
||||
@ -72,6 +78,97 @@
|
||||
this.showLogPanel = true;
|
||||
this.activeTab = 'status';
|
||||
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) {
|
||||
@ -540,7 +637,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Offcanvas Log Panel -->
|
||||
<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">
|
||||
@ -738,6 +834,21 @@
|
||||
{{ __('Loading Data') }}...</p>
|
||||
</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 -->
|
||||
<div x-show="activeTab !== 'expiry'"
|
||||
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?') }}"
|
||||
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::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}/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::resource('machines', App\Http\Controllers\Admin\MachineController::class);
|
||||
|
||||
Loading…
Reference in New Issue
Block a user