[RELEASE] 晉升 demo 到 main 正式上線環境

1. 新增 OTA 預約排程功能:建立 `ota_schedules` 資料表、`OtaSchedule` 模型與 `ota:process-schedules` 定時背景指令。
2. 實現機台部署狀態顯示 (Tooltip):新增「部署狀態」一欄,在加載與刷新時以原生 Tooltip 100% 穩定展示待執行與已完成之機台清單。
3. 優合 Flatpickr 時間選擇器 UI:整合日曆圖標並支援動態 locale 語言切換,統一顯示標籤為「選擇更新時間」。
4. 移除更新模式 (強制更新) 限制:自上傳與控制中完全拔除 `is_forced` 的開關與顯示。
5. 完備三語系同步:新增與對齊中、英、日三語系 `lang/*.json` 語系檔,並完成字母排序。
This commit is contained in:
sky121113 2026-05-29 10:58:30 +08:00
commit fce59eb19b
19 changed files with 968 additions and 149 deletions

View File

@ -129,17 +129,90 @@ Mqtt3Client client = MqttClient.builder()
```
#### B013 硬體代碼對照表 (由 MachineService 自動翻譯)
> [!IMPORTANT]
> **出貨結果判定**:僅 `0402` (出貨成功) 與 `0424` (購買成功) 視為成功,其餘出貨結果碼均應觸發退款。
> 完整代碼定義於 `MachineService::ERROR_CODE_MAP`
**基本出貨流程:**
| 代碼 | 英文 Key (i18n) | 級別 | 範例繁中翻譯 |
| :--- | :--- | :--- | :--- |
| **0402** | Dispense successful | info | 出貨成功 |
| **0401** | Dispensing in progress | info | 正在出貨 |
| **0402** | Dispense successful | info | ✅ 出貨成功 |
| **0403** | Slot jammed | error | **貨道卡貨** (重大異常) |
| **0202** | Product empty | warning | 貨道缺貨 |
| **0412** | Elevator rise error | error | 昇降機上升異常 |
| **0415** | Pickup door error | error | 取貨門異常 |
| **5402** | Pickup door not closed | warning | **取貨門未關** (警告) |
| **5403** | Elevator failure | error | 昇降系統故障 |
| **0404** | Motor not stopped | warning | 電機未正常停止 |
| **0405** | Motor not found | error | 電機不存在 |
| **0406** | Motor not found (0406) | error | 電機不存在 (同 0405) |
| **0407** | Elevator failure | error | 昇降機故障 |
*(註:`error_code` 應遵循此表定義,雲端將根據 `i18n` 自動轉換。`tid` 為貨道或任務 ID。)*
**昇降機系統:**
| 代碼 | 英文 Key (i18n) | 級別 | 範例繁中翻譯 |
| :--- | :--- | :--- | :--- |
| **040A** | Elevator belt self-check error | error | 昇降機皮帶自檢錯誤 |
| **0410** | Elevator rising | info | 昇降機正在上升 |
| **0411** | Elevator descending | info | 昇降機正在下降 |
| **0412** | Elevator rise error | error | 昇降機上升錯誤 |
| **0413** | Elevator descent error | error | 昇降機下降錯誤 |
| **0425** | Elevator support rod return error | error | 昇降台撐桿回位錯誤 |
| **0428** | Elevator support rod push error | error | 昇降台撐桿推出錯誤 |
| **0429** | Elevator enter microwave error | error | 昇降台進微波爐錯誤 |
| **0430** | Elevator exit microwave error | error | 昇降台出微波爐錯誤 |
**微波爐取貨口門:**
| 代碼 | 英文 Key (i18n) | 級別 | 範例繁中翻譯 |
| :--- | :--- | :--- | :--- |
| **0414** | Microwave pickup door closing | info | 正在關閉微波爐取貨口門 |
| **0415** | Microwave pickup door close error | error | 微波爐取貨口門關閉錯誤 |
| **0426** | Microwave pickup door opening | info | 正在打開微波爐取貨口門 |
| **0427** | Microwave pickup door open error | error | 微波爐取貨口門打開錯誤 |
**微波爐進貨口門:**
| 代碼 | 英文 Key (i18n) | 級別 | 範例繁中翻譯 |
| :--- | :--- | :--- | :--- |
| **0416** | Microwave delivery door opening | info | 正在打開微波爐進貨口門 |
| **0417** | Microwave delivery door open error | error | 微波爐進貨口門打開錯誤 |
| **0418** | Pushing bento into microwave | info | 正在將便當推入微波爐 |
| **0419** | Microwave delivery door closing | info | 正在關閉微波爐進貨口門 |
| **0420** | Microwave delivery door close error | error | 微波爐進貨口門關閉錯誤 |
**微波爐內部:**
| 代碼 | 英文 Key (i18n) | 級別 | 範例繁中翻譯 |
| :--- | :--- | :--- | :--- |
| **0421** | Bento not detected in microwave | error | 微波爐內沒檢測到便當 |
| **0422** | Bento heating | info | 便當正在加熱 |
| **0423** | Bento heating remaining time | info | 便當加熱剩餘時間秒 |
| **0424** | Purchase successful | info | ✅ 購買成功 |
| **0431** | Microwave push rod push error | error | 微波爐內推桿推出錯誤 |
| **0432** | Microwave push rod retract error | error | 微波爐內推桿收回錯誤 |
**鮮奶機專用:**
| 代碼 | 英文 Key (i18n) | 級別 | 範例繁中翻譯 |
| :--- | :--- | :--- | :--- |
| **040B** | Milk dispense timeout | error | 按鍵出奶超時 |
| **040C** | Cup dropper failure | error | 漏杯器漏杯失敗 |
| **040D** | Cup drop not detected | error | 未檢測到杯子掉落 |
**手環機專用:**
| 代碼 | 英文 Key (i18n) | 級別 | 範例繁中翻譯 |
| :--- | :--- | :--- | :--- |
| **040E** | Bracelet scan failed | error | 沒掃碼到手環 |
| **040F** | Bracelet drop not detected | error | 沒檢測到手環掉入取貨斗 |
**取貨口門 / 購物車模式 / 液體機:**
| 代碼 | 英文 Key (i18n) | 級別 | 範例繁中翻譯 |
| :--- | :--- | :--- | :--- |
| **0433** | Pickup door opened (cart mode) | info | 取貨口門打開成功 (購物車) |
| **0434** | Pickup door closed (cart mode) | info | 取貨口門關閉成功 (購物車) |
| **0435** | Liquid dispense paused | warning | 暫停出液 (液體機) |
| **0441** | Pickup door unlocked | info | 取貨口門開鎖上報 |
| **04FF** | Bento purchase terminated | error | 便當購買終止 |
### 3.3 交易生命週期上報 (Transaction) - `machine/{serial_no}/transaction`

View File

@ -0,0 +1,119 @@
<?php
namespace App\Console\Commands\Machine;
use Illuminate\Console\Command;
use App\Models\Machine\OtaSchedule;
use App\Models\Machine\Machine;
use App\Models\Machine\RemoteCommand;
use App\Services\Machine\MqttService;
use Exception;
use Illuminate\Support\Facades\Log;
class ProcessOtaSchedules extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'ota:process-schedules';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Process scheduled OTA firmware updates and send MQTT commands to target machines';
/**
* Execute the console command.
*/
public function handle(MqttService $mqttService)
{
$this->info('[' . now()->toDateTimeString() . '] Starting to process scheduled OTA updates...');
// 撈取狀態為待發送且時間已到的排程
$schedules = OtaSchedule::where('status', 'pending')
->where('scheduled_at', '<=', now())
->with('apkVersion')
->get();
if ($schedules->isEmpty()) {
$this->info('No pending OTA schedules to process.');
return 0;
}
$this->info("Found {$schedules->count()} pending OTA schedule(s) to process.");
foreach ($schedules as $schedule) {
$this->info("Processing OTA Schedule ID: {$schedule->id} (APK: {$schedule->apkVersion->version_name})");
// 標記為處理中,防止重複執行
$schedule->update(['status' => 'processing']);
$apkVersion = $schedule->apkVersion;
try {
// 根據 target_type 撈取目標機台
if ($schedule->target_type === 'all') {
$machines = Machine::all();
} else {
$machineIds = $schedule->target_value ?? [];
$machines = Machine::whereIn('id', $machineIds)->get();
}
if ($machines->isEmpty()) {
$this->warn("No target machines found for OTA Schedule ID: {$schedule->id}. Skipping.");
$schedule->update(['status' => 'completed']);
continue;
}
$successCount = 0;
foreach ($machines as $machine) {
// 1. 先建立 RemoteCommand 紀錄
$command = RemoteCommand::create([
'machine_id' => $machine->id,
'user_id' => $schedule->user_id,
'command_type' => 'update_app',
'status' => 'pending',
'payload' => [
'url' => $apkVersion->url,
'version' => $apkVersion->version_name,
'version_code' => $apkVersion->version_code,
'is_forced' => $apkVersion->is_forced,
],
]);
// 2. 推播 MQTT 更新指令
$pushed = $mqttService->pushCommand(
$machine->serial_no,
'update_app',
$command->payload,
(string) $command->id
);
if ($pushed) {
$successCount++;
} else {
$command->update(['status' => 'failed']);
$this->error("Failed to push MQTT OTA command to machine: {$machine->serial_no}");
}
}
$schedule->update(['status' => 'completed']);
$this->info("Successfully processed OTA Schedule ID: {$schedule->id}. MQTT deployed to {$successCount}/{$machines->count()} machines.");
} catch (Exception $e) {
Log::error("Error processing OTA Schedule ID {$schedule->id}: " . $e->getMessage(), [
'exception' => $e
]);
$schedule->update(['status' => 'failed']);
$this->error("Error processing OTA Schedule ID {$schedule->id}: " . $e->getMessage());
}
}
$this->info('OTA schedules processing completed.');
return 0;
}
}

View File

@ -13,6 +13,7 @@ class Kernel extends ConsoleKernel
protected function schedule(Schedule $schedule): void
{
// $schedule->command('inspire')->hourly();
$schedule->command('ota:process-schedules')->everyMinute();
}
/**

View File

@ -15,7 +15,39 @@ class ApkVersionController extends AdminController
{
public function index(): View
{
$versions = ApkVersion::orderBy('version_code', 'desc')->paginate(10);
// 在進入 APK 版本清單時,同步觸發已到期排程的執行(確保無 cron 的開發/測試環境也能 100% 執行)
try {
\Illuminate\Support\Facades\Artisan::call('ota:process-schedules');
} catch (\Exception $e) {
\Illuminate\Support\Facades\Log::error('Auto-trigger scheduled OTA failed: ' . $e->getMessage());
}
$versions = ApkVersion::latest()->paginate(10);
// 統計並附屬機台部署狀態
$commands = \App\Models\Machine\RemoteCommand::where('command_type', 'update_app')
->with('machine')
->get();
foreach ($versions as $version) {
$pending = [];
$completed = [];
foreach ($commands as $cmd) {
$cmdVersionCode = $cmd->payload['version_code'] ?? null;
if ($cmdVersionCode == $version->version_code) {
if ($cmd->status === 'completed' || $cmd->status === 'success') {
$completed[] = $cmd->machine;
} elseif ($cmd->status === 'pending' || $cmd->status === 'processing') {
$pending[] = $cmd->machine;
}
}
}
$version->pending_machines = collect($pending)->filter()->unique('id');
$version->completed_machines = collect($completed)->filter()->unique('id');
}
// 用於 OTA 下發時讓使用者選擇的機台列表
$machines = Machine::all();
return view('admin.basic-settings.apk-versions.index', compact('versions', 'machines'));
@ -35,7 +67,6 @@ class ApkVersionController extends AdminController
'apk_file' => 'required_without:download_url|nullable|file|max:102400', // 上傳限制 100MB
'download_url' => 'required_without:apk_file|nullable|url|max:500',
'release_notes' => 'nullable|string',
'is_forced' => 'nullable|boolean',
]);
$filePath = null;
@ -50,7 +81,7 @@ class ApkVersionController extends AdminController
'file_path' => $filePath,
'download_url' => $request->download_url,
'release_notes' => $request->release_notes,
'is_forced' => $request->boolean('is_forced'),
'is_forced' => false,
]);
return redirect()->route('admin.basic-settings.apk-versions.index')
@ -76,8 +107,24 @@ class ApkVersionController extends AdminController
$request->validate([
'machine_ids' => 'required|array',
'machine_ids.*' => 'exists:machines,id',
'scheduled_at' => 'nullable|date|after:now',
]);
// 如果設定了預約發送時間
if ($request->filled('scheduled_at')) {
\App\Models\Machine\OtaSchedule::create([
'apk_version_id' => $apkVersion->id,
'scheduled_at' => $request->scheduled_at,
'target_type' => 'custom',
'target_value' => $request->machine_ids,
'status' => 'pending',
'user_id' => auth()->id(),
]);
return redirect()->route('admin.basic-settings.apk-versions.index')
->with('success', __('OTA update scheduled successfully.'));
}
$machines = Machine::whereIn('id', $request->machine_ids)->get();
$successCount = 0;

View File

@ -0,0 +1,35 @@
<?php
namespace App\Models\Machine;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class OtaSchedule extends Model
{
use HasFactory;
protected $fillable = [
'apk_version_id',
'scheduled_at',
'target_type',
'target_value',
'status',
'user_id',
];
protected $casts = [
'scheduled_at' => 'datetime',
'target_value' => 'array',
];
public function apkVersion()
{
return $this->belongsTo(ApkVersion::class);
}
public function user()
{
return $this->belongsTo(\App\Models\System\User::class);
}
}

View File

@ -82,6 +82,7 @@ class Order extends Model
34 => __('PlusPay'),
40 => __('Member Verify Pickup'),
41 => __('Staff Card'),
42 => __('Pickup Voucher'),
50 => __('Offline + LINE Pay'),
51 => __('Offline + JKO Pay'),
52 => __('Offline + Easy Wallet'),

View File

@ -16,38 +16,82 @@ class MachineService
{
/**
* B013: 硬體狀態代碼對照表 (Hardware Status Code Mapping)
*
* 代碼格式Command 0x04 + Status Byte例如 0x04+0x03 = '0403'
* 出貨結果判定:僅 0402 (出貨成功) 0424 (便當取出成功) 視為成功,
* 其餘出貨結果碼均應觸發退款。
*
* 參考文件:廠商硬體通訊協議 Command(0x04) 狀態定義
*/
public const ERROR_CODE_MAP = [
// 出貨狀態類 (Prefix: 04 - BUY_STATUS)
'0401' => ['label' => 'Dispensing in progress', 'level' => 'info'],
'0402' => ['label' => 'Dispense successful', 'level' => 'info'],
'0403' => ['label' => 'Slot jammed', 'level' => 'error'],
'0404' => ['label' => 'Motor not stopped', 'level' => 'warning'],
'0406' => ['label' => 'Slot not found', 'level' => 'error'],
'0407' => ['label' => 'Dispense error (0407)', 'level' => 'error'],
'0408' => ['label' => 'Dispense error (0408)', 'level' => 'error'],
'0409' => ['label' => 'Dispense error (0409)', 'level' => 'error'],
'040A' => ['label' => 'Dispense error (040A)', 'level' => 'error'],
'0410' => ['label' => 'Elevator rising', 'level' => 'info'],
'0411' => ['label' => 'Elevator descending', 'level' => 'info'],
'0412' => ['label' => 'Elevator rise error', 'level' => 'error'],
'0413' => ['label' => 'Elevator descent error', 'level' => 'error'],
'0414' => ['label' => 'Pickup door closed', 'level' => 'info'],
'0415' => ['label' => 'Pickup door error', 'level' => 'error'],
'0416' => ['label' => 'Delivery door opened', 'level' => 'info'],
'0417' => ['label' => 'Delivery door open error', 'level' => 'error'],
'0418' => ['label' => 'Delivering product', 'level' => 'info'],
'0419' => ['label' => 'Delivery door closed', 'level' => 'info'],
'0420' => ['label' => 'Delivery door close error', 'level' => 'error'],
'0421' => ['label' => 'Hopper empty', 'level' => 'warning'],
'0422' => ['label' => 'Hopper overheated', 'level' => 'warning'],
'0423' => ['label' => 'Hopper heating timeout', 'level' => 'error'],
'0424' => ['label' => 'Hopper error (0424)', 'level' => 'error'],
'0426' => ['label' => 'Microwave door opened', 'level' => 'info'],
'0427' => ['label' => 'Microwave door error', 'level' => 'error'],
'04FF' => ['label' => 'Dispense stopped', 'level' => 'info'],
// ═══════════════════════════════════════════════════════════════
// 出貨與物理動作類 (Command 0x04 - BUY_STATUS)
// ═══════════════════════════════════════════════════════════════
// 貨道狀態類 (Prefix: 02 - SLOT_STATUS)
// --- 基本出貨流程 ---
'0401' => ['label' => 'Dispensing in progress', 'level' => 'info'], // 正在出貨
'0402' => ['label' => 'Dispense successful', 'level' => 'info'], // ✅ 出貨成功
'0403' => ['label' => 'Slot jammed', 'level' => 'error'], // 貨道卡貨 (重大異常)
'0404' => ['label' => 'Motor not stopped', 'level' => 'warning'], // 電機未正常停止
'0405' => ['label' => 'Motor not found', 'level' => 'error'], // 電機不存在
'0406' => ['label' => 'Motor not found (0406)', 'level' => 'error'], // 電機不存在 (同 0405)
'0407' => ['label' => 'Elevator failure', 'level' => 'error'], // 昇降機故障
// --- 昇降機系統 ---
'040A' => ['label' => 'Elevator belt self-check error', 'level' => 'error'], // 昇降機皮帶自檢錯誤
'0410' => ['label' => 'Elevator rising', 'level' => 'info'], // 昇降機正在上升
'0411' => ['label' => 'Elevator descending', 'level' => 'info'], // 昇降機正在下降
'0412' => ['label' => 'Elevator rise error', 'level' => 'error'], // 昇降機上升錯誤
'0413' => ['label' => 'Elevator descent error', 'level' => 'error'], // 昇降機下降錯誤
'0425' => ['label' => 'Elevator support rod return error', 'level' => 'error'], // 昇降台撐桿回位錯誤
'0428' => ['label' => 'Elevator support rod push error', 'level' => 'error'], // 昇降台撐桿推出錯誤
'0429' => ['label' => 'Elevator enter microwave error', 'level' => 'error'], // 昇降台進微波爐錯誤
'0430' => ['label' => 'Elevator exit microwave error', 'level' => 'error'], // 昇降台出微波爐錯誤
// --- 微波爐取貨口門 ---
'0414' => ['label' => 'Microwave pickup door closing', 'level' => 'info'], // 正在關閉微波爐取貨口門
'0415' => ['label' => 'Microwave pickup door close error', 'level' => 'error'], // 微波爐取貨口門關閉錯誤
'0426' => ['label' => 'Microwave pickup door opening', 'level' => 'info'], // 正在打開微波爐取貨口門
'0427' => ['label' => 'Microwave pickup door open error', 'level' => 'error'], // 微波爐取貨口門打開錯誤
// --- 微波爐進貨口門 ---
'0416' => ['label' => 'Microwave delivery door opening', 'level' => 'info'], // 正在打開微波爐進貨口門
'0417' => ['label' => 'Microwave delivery door open error', 'level' => 'error'], // 微波爐進貨口門打開錯誤
'0418' => ['label' => 'Pushing bento into microwave', 'level' => 'info'], // 正在將便當推入微波爐
'0419' => ['label' => 'Microwave delivery door closing', 'level' => 'info'], // 正在關閉微波爐進貨口門
'0420' => ['label' => 'Microwave delivery door close error', 'level' => 'error'], // 微波爐進貨口門關閉錯誤
// --- 微波爐內部 ---
'0421' => ['label' => 'Bento not detected in microwave', 'level' => 'error'], // 微波爐內沒檢測到便當
'0422' => ['label' => 'Bento heating', 'level' => 'info'], // 便當正在加熱
'0423' => ['label' => 'Bento heating remaining time', 'level' => 'info'], // 便當加熱剩餘時間 (貨道號=秒數)
'0424' => ['label' => 'Purchase successful', 'level' => 'info'], // ✅ 購買成功 (原 Bento ready for pickup)
'0431' => ['label' => 'Microwave push rod push error', 'level' => 'error'], // 微波爐內推桿推出錯誤
'0432' => ['label' => 'Microwave push rod retract error', 'level' => 'error'], // 微波爐內推桿收回錯誤
// --- 鮮奶機專用 ---
'040B' => ['label' => 'Milk dispense timeout', 'level' => 'error'], // 按鍵出奶超時 (鮮奶機)
'040C' => ['label' => 'Cup dropper failure', 'level' => 'error'], // 漏杯器漏杯失敗 (鮮奶機)
'040D' => ['label' => 'Cup drop not detected', 'level' => 'error'], // 未檢測到杯子掉落 (鮮奶機)
// --- 手環機專用 ---
'040E' => ['label' => 'Bracelet scan failed', 'level' => 'error'], // 沒掃碼到手環
'040F' => ['label' => 'Bracelet drop not detected', 'level' => 'error'], // 沒檢測到手環掉入取貨斗
// --- 取貨口門 (購物車模式) ---
'0433' => ['label' => 'Pickup door opened (cart mode)', 'level' => 'info'], // 取貨口門打開成功 (購物車購買)
'0434' => ['label' => 'Pickup door closed (cart mode)', 'level' => 'info'], // 取貨口門關閉成功 (購物車購買)
'0441' => ['label' => 'Pickup door unlocked', 'level' => 'info'], // 取貨口門開鎖上報
// --- 液體機專用 ---
'0435' => ['label' => 'Liquid dispense paused', 'level' => 'warning'], // 暫停出液
// --- 終止 ---
'04FF' => ['label' => 'Bento purchase terminated', 'level' => 'error'], // 便當購買終止
// ═══════════════════════════════════════════════════════════════
// 貨道與馬達狀態類 (Prefix: 02 - SLOT_STATUS)
// ═══════════════════════════════════════════════════════════════
'0201' => ['label' => 'Slot normal', 'level' => 'info'],
'0202' => ['label' => 'Product empty', 'level' => 'warning'],
'0203' => ['label' => 'Slot empty', 'level' => 'warning'],
@ -58,13 +102,24 @@ class MachineService
'0209' => ['label' => 'Slot motor error (0209)', 'level' => 'error'],
'0212' => ['label' => 'Hopper empty (0212)', 'level' => 'warning'],
// ═══════════════════════════════════════════════════════════════
// 機台整體狀態類 (Prefix: 54 - MACHINE_STATUS)
// ═══════════════════════════════════════════════════════════════
'5400' => ['label' => 'Machine normal', 'level' => 'info'],
'5401' => ['label' => 'Elevator sensor error', 'level' => 'error'],
'5402' => ['label' => 'Pickup door not closed', 'level' => 'warning'],
'5403' => ['label' => 'Elevator failure', 'level' => 'error'],
];
/**
* 出貨成功結果碼 (Dispense Success Codes)
*
* 僅以下代碼視為出貨成功,其餘出貨結果碼均應觸發退款。
* - 0402: 一般出貨成功
* - 0424: 便當取出成功 (微波爐機型)
*/
public const DISPENSE_SUCCESS_CODES = ['0402', '0424'];
/**
* Update machine heartbeat and status.
*

View File

@ -8,16 +8,7 @@ use Illuminate\Support\Facades\Log;
class DiscordWebhookService
{
// B013 硬體代碼中文翻譯對照
protected array $errorCodes = [
'0402' => '出貨成功',
'0403' => '貨道卡貨 (重大異常)',
'0202' => '貨道缺貨',
'0412' => '昇降機上升異常',
'0415' => '取貨門異常',
'5402' => '取貨門未關 (警告)',
'5403' => '昇降系統故障',
];
// B013 硬體代碼翻譯統一由 MachineService::ERROR_CODE_MAP 管理 (Single Source of Truth)
/**
* 發送測試通知

View File

@ -593,7 +593,7 @@ return [
'order.original_amount' => ['type' => 'numeric', 'description' => '(選填) 原始標價金額', 'required' => false],
'order.discount_amount' => ['type' => 'numeric', 'description' => '(選填) 折扣金額,預設 0', 'required' => false],
'order.points_used' => ['type' => 'integer', 'description' => '(選填) 使用點數,預設 0', 'required' => false],
'order.payment_type' => ['type' => 'integer', 'description' => "(必填) 支付類型代碼:\n1:信用卡, 2:悠遊卡/一卡通, 3:掃碼支付, 4:紙鈔機, 5:通行碼, 6:取貨碼, 7:來店禮, 8:問卷, 9:零錢, 41:員工卡\n21~25:線下付款+X (21:線下+1, 22:線下+2, 23:線下+3, 24:線下+4, 25:線下+9)。\n30:LINE Pay, 31:街口, 32:悠遊付, 33:Pi, 34:全盈+。\n40:會員驗證取貨商品。\n50~54:線下付款+TapPay (50:線下+30, 51:線下+31, 52:線下+32, 53:線下+33, 54:線下+34)。\n60:點數/優惠卷全額折抵。61~69:會員+X (61:會員+1...69:會員+9)。\n90~94:會員+TapPay (90:會員+30...94:會員+34)。\n100:遠端出貨。"],
'order.payment_type' => ['type' => 'integer', 'description' => "(必填) 支付類型代碼:\n1:信用卡, 2:悠遊卡/一卡通, 3:掃碼支付, 4:紙鈔機, 5:通行碼, 6:取貨碼, 7:來店禮, 8:問卷, 9:零錢, 41:員工卡, 42:取物單\n21~25:線下付款+X (21:線下+1, 22:線下+2, 23:線下+3, 24:線下+4, 25:線下+9)。\n30:LINE Pay, 31:街口, 32:悠遊付, 33:Pi, 34:全盈+。\n40:會員驗證取貨商品。\n50~54:線下付款+TapPay (50:線下+30, 51:線下+31, 52:線下+32, 53:線下+33, 54:線下+34)。\n60:點數/優惠卷全額折抵。61~69:會員+X (61:會員+1...69:會員+9)。\n90~94:會員+TapPay (90:會員+30...94:會員+34)。\n100:遠端出貨。"],
'order.payment_status' => ['type' => 'integer', 'description' => '(必填) 支付狀態 (1:成功)'],
'order.code_id' => ['type' => 'string', 'description' => '(選填) 閉環勾稽用的代碼 ID (payment_type 為 5, 6, 41 時必填)', 'required' => false],
'order.payment_request' => ['type' => 'string', 'description' => '(選填) 金流請求原始字串', 'required' => false],

View File

@ -0,0 +1,26 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration {
public function up(): void
{
Schema::create('ota_schedules', function (Blueprint $table) {
$table->id();
$table->foreignId('apk_version_id')->constrained('apk_versions')->onDelete('cascade');
$table->dateTime('scheduled_at');
$table->string('target_type')->default('custom'); // 'all', 'custom'
$table->json('target_value')->nullable(); // 當 target_type = 'custom' 時,儲存 [1, 2, 3] 機台 ID 的 JSON 陣列
$table->string('status')->default('pending'); // 'pending', 'processing', 'completed', 'failed'
$table->foreignId('user_id')->nullable()->constrained('users')->onDelete('set null'); // 建立排程的使用者
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('ota_schedules');
}
};

44
docs/workflow-commands.md Normal file
View File

@ -0,0 +1,44 @@
# Workflow Commands
統一入口:`./workflow`
## 查看可用指令
```bash
./workflow list
```
## 執行指令
```bash
./workflow run <name> [args...]
```
## 指令對照
- `load-ids`
- 載入 `~/.gemini/AGENTS.md``~/.gemini/GEMINI.md` 內容。
- `daily-work-log`
- 輸出今日 commit`git log --since="midnight" ...`
- `hotfix-prod <hotfix-name>`
- 從 `main` 建立 `hotfix/<name>`
- `demo-day-pptx`
- 輸出流程提醒(先做 implementation_plan 再做 PPTX
- `start-task <feat|fix|refactor|docs> <task-name-kebab> [--stash]`
- 從最新 `dev` 建立任務分支,必要時先 stash 再 pop。
- `promote-env <dev|demo> <demo|main> [--force-main]`
- 支援 `dev -> demo``demo -> main`
- 會先列出 log 與敏感檔案,再 merge/push最後切回 `dev`
- `monthly-report <year> <month>`
- 產生月報草稿檔案:`docs/monthly-report-YYYY-MM.md`。
- `now-push <target-branch> <prefix> "<summary>" ["detail1" ...]`
- `prefix`: `FIX|FEAT|DOCS|STYLE|REFACTOR`
- 自動 `git add -A`、commit、push 到目標分支。

View File

@ -5,8 +5,8 @@
"30s": "30s",
"60 Seconds": "60 Seconds",
"60s": "60s",
":count staff cards imported successfully": ":count staff cards imported successfully",
":count products imported successfully": ":count products imported successfully",
":count staff cards imported successfully": ":count staff cards imported successfully",
"A new verification link has been sent to your email address.": "A new verification link has been sent to your email address.",
"A sync command was recently sent. Please wait 1 minute.": "A sync command was recently sent. Please wait 1 minute.",
"AI Prediction": "AI Prediction",
@ -219,7 +219,13 @@
"Before Qty": "Before Qty",
"Belongs To": "Belongs To",
"Belongs To Company": "Belongs To Company",
"Bento heating": "Bento heating",
"Bento heating remaining time": "Bento heating remaining time",
"Bento not detected in microwave": "Bento not detected in microwave",
"Bento purchase terminated": "Bento purchase terminated",
"Bill Acceptor": "Bill Acceptor",
"Bracelet drop not detected": "Bracelet drop not detected",
"Bracelet scan failed": "Bracelet scan failed",
"Branch": "Branch",
"Branch Warehouses": "Branch Warehouses",
"Business Type": "Business Type",
@ -257,11 +263,11 @@
"Cash Module Function": "Cash Module Function",
"Category": "Category",
"Category Management": "Category Management",
"Category not found: :category": "Category not found: :category",
"Category Name": "Category Name",
"Category Name (en)": "Category Name (en)",
"Category Name (ja)": "Category Name (ja)",
"Category Name (zh_TW)": "Category Name (zh_TW)",
"Category not found: :category": "Category not found: :category",
"Change": "Change",
"Change Note": "Change Note",
"Change Stock": "Change Stock",
@ -394,6 +400,8 @@
"Critical": "Critical",
"Critical failures requiring immediate hands-on attention.": "Critical failures requiring immediate hands-on attention.",
"Cross-Company / Headquarter Global Stats": "Cross-Company / Headquarter Global Stats",
"Cup drop not detected": "Cup drop not detected",
"Cup dropper failure": "Cup dropper failure",
"Current": "Current",
"Current Password": "Current Password",
"Current Status": "Current Status",
@ -468,6 +476,7 @@
"Delta": "Delta",
"Deploy Update": "Deploy Update",
"Deploy version": "Deploy version",
"Deployment Status": "Deployment Status",
"Deposit Bonus": "Deposit Bonus",
"Describe the changes in this version...": "Describe the changes in this version...",
"Describe the repair or maintenance status...": "Describe the repair or maintenance status...",
@ -519,6 +528,7 @@
"Dispensing in progress": "Dispensing in progress",
"Display Material Code": "Display Material Code",
"Displaying": "Displaying",
"Done": "Done",
"Door Closed": "Door Closed",
"Door Opened": "Door Opened",
"Download Image": "Download Image",
@ -569,12 +579,17 @@
"Effective Alert Types": "Effective Alert Types",
"Electronic Invoice": "Electronic Invoice",
"Electronic Invoices": "Electronic Invoices",
"Elevator belt self-check error": "Elevator belt self-check error",
"Elevator descending": "Elevator descending",
"Elevator descent error": "Elevator descent error",
"Elevator enter microwave error": "Elevator enter microwave error",
"Elevator exit microwave error": "Elevator exit microwave error",
"Elevator failure": "Elevator failure",
"Elevator rise error": "Elevator rise error",
"Elevator rising": "Elevator rising",
"Elevator sensor error": "Elevator sensor error",
"Elevator support rod push error": "Elevator support rod push error",
"Elevator support rod return error": "Elevator support rod return error",
"Email": "Email",
"Emblem / Label": "Emblem / Label",
"Employee": "Employee",
@ -814,8 +829,8 @@
"Import Products": "Import Products",
"Import Staff Cards": "Import Staff Cards",
"Import failed": "Import failed",
"Imported with errors. Please check the console.": "Imported with errors. Please check the console.",
"Important Notice": "Important Notice",
"Imported with errors. Please check the console.": "Imported with errors. Please check the console.",
"In Stock": "In Stock",
"Inactive": "Inactive",
"Includes Credit Card/Mobile Pay": "Includes Credit Card/Mobile Pay",
@ -874,6 +889,7 @@
"Last Week": "Last Week",
"Latitude": "Latitude",
"Lease": "Lease",
"Leave blank to deploy immediately, or set a future time to schedule a background release.": "Leave blank to deploy immediately, or set a future time to schedule a background release.",
"Leave empty for permanent code": "Leave empty for permanent code",
"Leave empty or 0 for permanent code": "Leave empty or 0 for permanent code",
"Leave empty to inherit company settings": "Leave empty to inherit company settings",
@ -890,6 +906,7 @@
"LinePay Payment": "LinePay Payment",
"Link": "Link",
"Link Copied": "Link Copied",
"Liquid dispense paused": "Liquid dispense paused",
"Live Fleet Updates": "Live Fleet Updates",
"Loading Cabinet...": "Loading Cabinet...",
"Loading Data": "Loading Data",
@ -1070,8 +1087,19 @@
"Message": "Message",
"Message Content": "Message Content",
"Message Display": "Message Display",
"Microwave delivery door close error": "Microwave delivery door close error",
"Microwave delivery door closing": "Microwave delivery door closing",
"Microwave delivery door open error": "Microwave delivery door open error",
"Microwave delivery door opening": "Microwave delivery door opening",
"Microwave door error": "Microwave door error",
"Microwave door opened": "Microwave door opened",
"Microwave pickup door close error": "Microwave pickup door close error",
"Microwave pickup door closing": "Microwave pickup door closing",
"Microwave pickup door open error": "Microwave pickup door open error",
"Microwave pickup door opening": "Microwave pickup door opening",
"Microwave push rod push error": "Microwave push rod push error",
"Microwave push rod retract error": "Microwave push rod retract error",
"Milk dispense timeout": "Milk dispense timeout",
"Min 8 characters": "Min 8 characters",
"Model": "Model",
"Model Default": "Model Default",
@ -1087,6 +1115,8 @@
"Monthly Cumulative Revenue": "Monthly Cumulative Revenue",
"Monthly Transactions": "Monthly Transactions",
"Monthly cumulative revenue overview": "Monthly cumulative revenue overview",
"Motor not found": "Motor not found",
"Motor not found (0406)": "Motor not found (0406)",
"Motor not stopped": "Motor not stopped",
"Movement History": "Movement History",
"Movement Logs": "Movement Logs",
@ -1147,6 +1177,7 @@
"No machines available": "No machines available",
"No machines available in this company.": "No machines available in this company.",
"No machines found": "No machines found",
"No machines in this status": "No machines in this status",
"No machines match your criteria": "No machines match your criteria",
"No maintenance records found": "No maintenance records found",
"No matching logs found": "No matching logs found",
@ -1204,6 +1235,7 @@
"OTA Update Deployment": "OTA Update Deployment",
"OTA command sent to %d devices successfully.": "OTA command sent to %d devices successfully.",
"OTA firmware update and version control": "OTA firmware update and version control",
"OTA update scheduled successfully.": "OTA update scheduled successfully.",
"Off": "Off",
"Offline": "Offline",
"Offline + 1": "Offline + 1",
@ -1353,13 +1385,19 @@
"Pickup Code": "Pickup Code",
"Pickup Code (8 Digits)": "Pickup Code (8 Digits)",
"Pickup Codes": "Pickup Codes",
"Pickup Recipient (Prescription Slip)": "Pickup Recipient (Prescription Slip)",
"Pickup Recipient Info": "Pickup Recipient Info",
"Pickup Ticket": "Pickup Ticket",
"Pickup Voucher": "Pickup Voucher",
"Pickup code cancelled.": "Pickup code cancelled.",
"Pickup code generated: :code": "Pickup code generated: :code",
"Pickup code updated.": "Pickup code updated.",
"Pickup door closed": "Pickup door closed",
"Pickup door closed (cart mode)": "Pickup door closed (cart mode)",
"Pickup door error": "Pickup door error",
"Pickup door not closed": "Pickup door not closed",
"Pickup door opened (cart mode)": "Pickup door opened (cart mode)",
"Pickup door unlocked": "Pickup door unlocked",
"Platform Operator": "Platform Operator",
"Playback Order": "Playback Order",
"Please Select Slot first": "Please Select Slot first",
@ -1405,6 +1443,7 @@
"Print Notice": "Print Notice",
"Print Order": "Print Order",
"Print Time": "Print Time",
"Processing...": "Processing...",
"Product": "Product",
"Product / Slot": "Product / Slot",
"Product / Stock": "Product / Stock",
@ -1444,10 +1483,12 @@
"Publish Time": "Publish Time",
"Purchase Audit": "Purchase Audit",
"Purchase Finished": "Purchase Finished",
"Purchase successful": "Purchase successful",
"Purchases": "Purchases",
"Purchasing": "Purchasing",
"Purpose": "Purpose",
"Push Status": "Push Status",
"Pushing bento into microwave": "Pushing bento into microwave",
"QR": "QR",
"QR CODE": "QR CODE",
"QR Code": "QR Code",
@ -1467,6 +1508,8 @@
"Quick replenishment from this view": "Quick replenishment from this view",
"Quick search...": "Quick search...",
"Rank": "Rank",
"Ready": "Ready",
"Ready to Execute": "Ready to Execute",
"Ready to print": "Ready to print",
"Real-time OEE analysis awaits": "Real-time OEE analysis awaits",
"Real-time Operation Logs (Last 50)": "Real-time Operation Logs (Last 50)",
@ -1594,6 +1637,7 @@
"Scan to authorize testing or maintenance": "Scan to authorize testing or maintenance",
"Scan to pick up your product": "Scan to pick up your product",
"Schedule": "Schedule",
"Schedule Release Time (Optional)": "Schedule Release Time (Optional)",
"Search": "Search",
"Search Company Title...": "Search Company Title...",
"Search Company...": "Search Company...",
@ -1659,6 +1703,7 @@
"Select Slot...": "Select Slot...",
"Select Target Company": "Select Target Company",
"Select Target Slot": "Select Target Slot",
"Select Update Time (Optional)": "Select Update Time (Optional)",
"Select Warehouse": "Select Warehouse",
"Select a machine to copy settings from...": "Select a machine to copy settings from...",
"Select a machine to deep dive": "Select a machine to deep dive",
@ -1668,6 +1713,7 @@
"Select an asset from the left to start analysis": "Select an asset from the left to start analysis",
"Select date to sync data": "Select date to sync data",
"Select flavor...": "Select flavor...",
"Select future time...": "Select update time...",
"Select...": "Select...",
"Selected": "Selected",
"Selected Date": "Selected Date",
@ -1750,7 +1796,6 @@
"Standby Ad": "Standby Ad",
"Start Date": "Start Date",
"Start Delivery": "Start Delivery",
"Processing...": "Processing...",
"Start Import": "Start Import",
"Start Time": "Start Time",
"Statistics": "Statistics",
@ -1980,6 +2025,7 @@
"Unpublish": "Unpublish",
"Update": "Update",
"Update Authorization": "Update Authorization",
"Update Completed": "Update Completed",
"Update Customer": "Update Customer",
"Update Mode": "Update Mode",
"Update Pass Code": "Update Pass Code",

View File

@ -5,6 +5,7 @@
"30s": "30秒",
"60 Seconds": "60秒",
"60s": "60秒",
":count products imported successfully": ":count products imported successfully",
":count staff cards imported successfully": ":count 枚のスタッフカードが正常にインポートされました",
"A new verification link has been sent to your email address.": "新しい確認リンクがメールアドレスに送信されました。",
"A sync command was recently sent. Please wait 1 minute.": "先ほど同期コマンドが送信されました。1分間お待ちください。",
@ -218,7 +219,13 @@
"Before Qty": "変動前数量",
"Belongs To": "会社名",
"Belongs To Company": "所属会社",
"Bento heating": "お弁当加熱中",
"Bento heating remaining time": "お弁当加熱残り時間",
"Bento not detected in microwave": "電子レンジ内お弁当未検出",
"Bento purchase terminated": "お弁当購入中止",
"Bill Acceptor": "紙幣識別機",
"Bracelet drop not detected": "ブレスレット落下未検出",
"Bracelet scan failed": "ブレスレットスキャン失敗",
"Branch": "分庫",
"Branch Warehouses": "分庫数",
"Business Type": "業務タイプ",
@ -260,6 +267,7 @@
"Category Name (en)": "カテゴリ名 (英語)",
"Category Name (ja)": "カテゴリ名 (日本語)",
"Category Name (zh_TW)": "カテゴリ名 (繁体字中国語)",
"Category not found: :category": "Category not found: :category",
"Change": "変更",
"Change Note": "変動備考",
"Change Stock": "お釣り在庫",
@ -392,6 +400,8 @@
"Critical": "深刻",
"Critical failures requiring immediate hands-on attention.": "即座に対処が必要な重大な障害。",
"Cross-Company / Headquarter Global Stats": "複数会社間 / 本部グローバル統計",
"Cup drop not detected": "カップ落下未検出",
"Cup dropper failure": "カップディスペンサー落下エラー",
"Current": "現在",
"Current Password": "現在のパスワード",
"Current Status": "現在のステータス",
@ -466,6 +476,7 @@
"Delta": "変動量",
"Deploy Update": "Deploy Update",
"Deploy version": "Deploy version",
"Deployment Status": "配信ステータス",
"Deposit Bonus": "チャージボーナス",
"Describe the changes in this version...": "Describe the changes in this version...",
"Describe the repair or maintenance status...": "修理やメンテナンスの状況を説明してください...",
@ -512,11 +523,12 @@
"Dispense error (0409)": "出庫プロセス異常 (0409)",
"Dispense error (040A)": "出庫プロセス異常 (040A)",
"Dispense stopped": "出庫停止",
"Dispense successful": "出庫成功",
"Dispense successful": "搬送成功",
"Dispensing": "出庫",
"Dispensing in progress": "出庫作業中",
"Dispensing in progress": "搬送中",
"Display Material Code": "資材コードを表示",
"Displaying": "表示中",
"Done": "完了",
"Door Closed": "扉が閉まりました",
"Door Opened": "扉が開きました",
"Download Image": "画像をダウンロード",
@ -567,12 +579,17 @@
"Effective Alert Types": "有効なアラート種別",
"Electronic Invoice": "電子領収書",
"Electronic Invoices": "電子領収書",
"Elevator descending": "昇降プラットフォーム下降中",
"Elevator descent error": "昇降プラットフォーム下降異常",
"Elevator failure": "昇降システム故障",
"Elevator rise error": "昇降プラットフォーム上昇異常",
"Elevator rising": "昇降プラットフォーム上昇中",
"Elevator sensor error": "昇降箱センサー異常",
"Elevator belt self-check error": "エレベーターベルト自己診断エラー",
"Elevator descending": "エレベーター下降中",
"Elevator descent error": "エレベーター下降エラー",
"Elevator enter microwave error": "エレベーター電子レンジ進入エラー",
"Elevator exit microwave error": "エレベーター電子レンジ退出エラー",
"Elevator failure": "エレベーター故障",
"Elevator rise error": "エレベーター上昇エラー",
"Elevator rising": "エレベーター上昇中",
"Elevator sensor error": "エレベーターセンサーエラー",
"Elevator support rod push error": "エレベーター支柱押し出しエラー",
"Elevator support rod return error": "エレベーター支柱復帰エラー",
"Email": "メールアドレス",
"Emblem / Label": "エンブレム / ラベル",
"Employee": "従業員",
@ -809,9 +826,11 @@
"Image Downloaded": "画像をダウンロードしました",
"Immediate": "即時",
"Import Excel": "Excel インポート",
"Import Products": "Import Products",
"Import Staff Cards": "スタッフカードのインポート",
"Import failed": "インポート失敗",
"Important Notice": "Important Notice",
"Imported with errors. Please check the console.": "Imported with errors. Please check the console.",
"In Stock": "現在庫",
"Inactive": "停止中",
"Includes Credit Card/Mobile Pay": "クレジット/QR決済を含む",
@ -870,6 +889,7 @@
"Last Week": "先週",
"Latitude": "緯度",
"Lease": "リース",
"Leave blank to deploy immediately, or set a future time to schedule a background release.": "空欄のままにすると即時配信されます。未来の日時を指定すると背景で自動予約配信されます。",
"Leave empty for permanent code": "空欄で無期限コードになります",
"Leave empty or 0 for permanent code": "空欄または0で無期限コードになります",
"Leave empty to inherit company settings": "空白のままにすると会社のグローバル設定を継承します",
@ -886,6 +906,7 @@
"LinePay Payment": "LinePay決済",
"Link": "リンク",
"Link Copied": "リンクをコピーしました",
"Liquid dispense paused": "液体抽出一時停止",
"Live Fleet Updates": "リアルタイム更新",
"Loading Cabinet...": "機器キャビネットを読み込み中...",
"Loading Data": "データを読み込み中",
@ -975,7 +996,7 @@
"Machine model deleted successfully.": "機器モデルが正常に削除されました。",
"Machine model saved successfully.": "機器モデルが正常に追加されました。",
"Machine model updated successfully.": "機器モデルが正常に更新されました。",
"Machine normal": "機器システム正常",
"Machine normal": "デバイス正常",
"Machine not found": "機器が見つかりません",
"Machine permissions updated successfully.": "機器権限が正常に更新されました。",
"Machine settings updated successfully.": "機器設定が正常に更新されました。",
@ -1066,8 +1087,19 @@
"Message": "メッセージ",
"Message Content": "メッセージ内容",
"Message Display": "メッセージ表示",
"Microwave delivery door close error": "電子レンジ搬入口扉閉鎖エラー",
"Microwave delivery door closing": "電子レンジ搬入口扉閉鎖中",
"Microwave delivery door open error": "電子レンジ搬入口扉開放エラー",
"Microwave delivery door opening": "電子レンジ搬入口扉開放中",
"Microwave door error": "電子レンジ扉エラー",
"Microwave door opened": "電子レンジ扉開放",
"Microwave pickup door close error": "電子レンジ取り出し口扉閉鎖エラー",
"Microwave pickup door closing": "電子レンジ取り出し口扉閉鎖中",
"Microwave pickup door open error": "電子レンジ取り出し口扉開放エラー",
"Microwave pickup door opening": "電子レンジ取り出し口扉開放中",
"Microwave push rod push error": "電子レンジ内プッシャー押し出しエラー",
"Microwave push rod retract error": "電子レンジ内プッシャー回収エラー",
"Milk dispense timeout": "牛乳抽出タイムアウト",
"Min 8 characters": "最低 8文字",
"Model": "機器モデル",
"Model Default": "モデルデフォルト",
@ -1083,7 +1115,9 @@
"Monthly Cumulative Revenue": "今月の累計収益",
"Monthly Transactions": "今月の取引額",
"Monthly cumulative revenue overview": "今月の累計収益の概要",
"Motor not stopped": "モーターが停止していません",
"Motor not found": "モーター未検出",
"Motor not found (0406)": "モーター未検出 (0406)",
"Motor not stopped": "モーター異常停止",
"Movement History": "変動履歴",
"Movement Logs": "変動ログ",
"Multilingual Names": "多言語名",
@ -1143,6 +1177,7 @@
"No machines available": "利用可能な機器がありません",
"No machines available in this company.": "この会社で利用可能な機器がありません。",
"No machines found": "機器が見つかりません",
"No machines in this status": "このステータスのデバイスはありません",
"No machines match your criteria": "No machines match your criteria",
"No maintenance records found": "メンテナンス記録が見つかりません",
"No matching logs found": "一致するログが見つかりません",
@ -1200,6 +1235,7 @@
"OTA Update Deployment": "OTA Update Deployment",
"OTA command sent to %d devices successfully.": "OTA command sent to %d devices successfully.",
"OTA firmware update and version control": "OTA firmware update and version control",
"OTA update scheduled successfully.": "OTA予約更新スケジュールが正常に作成されました。",
"Off": "オフ",
"Offline": "オフライン",
"Offline + 1": "オフライン + 1",
@ -1225,6 +1261,7 @@
"Online Machines": "オンライン機器",
"Online Only": "Online Only",
"Online Status": "オンラインステータス",
"Only .xlsx files are supported (Max 5MB)": "Only .xlsx files are supported (Max 5MB)",
"Only .xlsx, .xls, .csv files are supported (Max 5MB)": ".xlsx, .xls, .csv ファイルのみサポートされています (最大 5MB)",
"Only machines under the same company can be cloned for security.": "セキュリティのため、同じ会社の機器設定のみコピーできます。",
"Only system roles can be assigned to platform administrative accounts.": "プラットフォーム管理アカウントにはシステムロールのみを割り当てることができます。",
@ -1348,13 +1385,19 @@
"Pickup Code": "受取コード",
"Pickup Code (8 Digits)": "受取コード (8桁)",
"Pickup Codes": "受取コード",
"Pickup Recipient (Prescription Slip)": "受取者(処方箋)",
"Pickup Recipient Info": "受取者情報",
"Pickup Ticket": "商品受取チケット",
"Pickup Voucher": "受取票",
"Pickup code cancelled.": "受取コードがキャンセルされました。",
"Pickup code generated: :code": "受取コードが生成されました: :code",
"Pickup code updated.": "受取コードが更新されました。",
"Pickup door closed": "受取扉が閉まりました",
"Pickup door closed (cart mode)": "取り出し口扉閉鎖成功",
"Pickup door error": "受取扉エラー",
"Pickup door not closed": "受取扉が閉まっていません",
"Pickup door not closed": "取り出し口扉未閉鎖",
"Pickup door opened (cart mode)": "取り出し口扉開放成功",
"Pickup door unlocked": "取り出し口扉開錠",
"Platform Operator": "プラットフォーム事業者",
"Playback Order": "再生順序",
"Please Select Slot first": "先にスロットを選択してください",
@ -1400,6 +1443,7 @@
"Print Notice": "印刷時の注意",
"Print Order": "伝票印刷",
"Print Time": "印刷日時",
"Processing...": "Processing...",
"Product": "商品",
"Product / Slot": "商品 / スロット",
"Product / Stock": "商品 / 在庫",
@ -1421,7 +1465,7 @@
"Product Status": "商品ステータス",
"Product created successfully": "商品が正常に作成されました",
"Product deleted successfully": "商品が正常に削除されました",
"Product empty": "スロットが空です (PDT_EMPTY)",
"Product empty": "商品売り切れ",
"Product physical shipment count": "商品の実出荷数",
"Product status updated to :status": "商品のステータスが :status に更新されました",
"Product updated successfully": "商品が正常に更新されました",
@ -1439,10 +1483,12 @@
"Publish Time": "公開時間",
"Purchase Audit": "購入監査",
"Purchase Finished": "購入終了",
"Purchase successful": "購入成功",
"Purchases": "購入伝票",
"Purchasing": "購入中",
"Purpose": "用途",
"Push Status": "アラート状態",
"Pushing bento into microwave": "お弁当を電子レンジに投入中",
"QR": "QR",
"QR CODE": "QRコード",
"QR Code": "QRコード",
@ -1462,6 +1508,8 @@
"Quick replenishment from this view": "この画面からクイック補充",
"Quick search...": "クイック検索...",
"Rank": "順位",
"Ready": "準備中",
"Ready to Execute": "コマンド実行待ちデバイス",
"Ready to print": "印刷準備完了",
"Real-time OEE analysis awaits": "リアルタイムOEE分析待機中",
"Real-time Operation Logs (Last 50)": "リアルタイム操作ログ (直近50件)",
@ -1589,6 +1637,7 @@
"Scan to authorize testing or maintenance": "スキャンしてテストまたはメンテナンスを承認します",
"Scan to pick up your product": "スキャンして商品をお受け取りください",
"Schedule": "スケジュール",
"Schedule Release Time (Optional)": "予約配信日時(任意)",
"Search": "検索",
"Search Company Title...": "会社名を検索...",
"Search Company...": "会社を検索...",
@ -1654,6 +1703,7 @@
"Select Slot...": "スロットを選択...",
"Select Target Company": "対象の会社を選択",
"Select Target Slot": "対象のスロットを選択",
"Select Update Time (Optional)": "更新時間を選択してください(任意)",
"Select Warehouse": "倉庫を選択",
"Select a machine to copy settings from...": "設定のコピー元機器を選択...",
"Select a machine to deep dive": "詳細分析する機器を選択",
@ -1663,6 +1713,7 @@
"Select an asset from the left to start analysis": "左側から機器を選択して分析を開始",
"Select date to sync data": "データを同期する日付を選択",
"Select flavor...": "Select flavor...",
"Select future time...": "配信日時を選択してください...",
"Select...": "選択してください...",
"Selected": "選択中",
"Selected Date": "選択日",
@ -1699,16 +1750,16 @@
"Slot No": "スロット番号",
"Slot Status": "スロット期限ステータス",
"Slot Test": "スロットテスト",
"Slot empty": "スロットが空です (SLOT_EMPTY)",
"Slot jammed": "スロット詰まり (K-PDT)",
"Slot motor error (0207)": "スロットモーター異常 (0207)",
"Slot motor error (0208)": "スロットモーター異常 (0208)",
"Slot motor error (0209)": "スロットモーター異常 (0209)",
"Slot normal": "スロット正常",
"Slot not closed": "スロットが閉まっていません",
"Slot empty": "チャンネル在庫切れ",
"Slot jammed": "商品詰まり",
"Slot motor error (0207)": "チャンネルモーターエラー (0207)",
"Slot motor error (0208)": "チャンネルモーターエラー (0208)",
"Slot motor error (0209)": "チャンネルモーターエラー (0209)",
"Slot normal": "チャンネル正常",
"Slot not closed": "チャンネル未閉鎖",
"Slot not found": "指定されたスロットが見つかりません",
"Slot report synchronized success": "スロットレポートの同期に成功しました",
"Slot sensor blocked": "スロットセンサーが塞がれています",
"Slot sensor blocked": "チャンネルセンサー遮蔽",
"Slot updated successfully.": "スロットが正常に更新されました。",
"Slots": "スロット数",
"Slots need replenishment": "補充が必要なスロット",
@ -1974,6 +2025,7 @@
"Unpublish": "非公開",
"Update": "更新",
"Update Authorization": "権限更新",
"Update Completed": "アップデート完了デバイス",
"Update Customer": "顧客更新",
"Update Mode": "Update Mode",
"Update Pass Code": "パスコード更新",

View File

@ -5,8 +5,8 @@
"30s": "30秒",
"60 Seconds": "60 秒",
"60s": "60秒",
":count staff cards imported successfully": "成功匯入 :count 張員工識別卡",
":count products imported successfully": "成功匯入 :count 筆商品",
":count staff cards imported successfully": "成功匯入 :count 張員工識別卡",
"A new verification link has been sent to your email address.": "已將新的驗證連結發送至您的電子郵件地址。",
"A sync command was recently sent. Please wait 1 minute.": "近期已發送過同步指令,請等待 1 分鐘後再試",
"AI Prediction": "AI智能預測",
@ -219,7 +219,13 @@
"Before Qty": "異動前數量",
"Belongs To": "公司名稱",
"Belongs To Company": "公司名稱",
"Bento heating": "便當正在加熱",
"Bento heating remaining time": "便當加熱剩餘時間",
"Bento not detected in microwave": "微波爐內沒檢測到便當",
"Bento purchase terminated": "便當購買終止",
"Bill Acceptor": "紙鈔機",
"Bracelet drop not detected": "沒檢測到手環掉入取貨斗",
"Bracelet scan failed": "沒掃碼到手環",
"Branch": "分倉",
"Branch Warehouses": "分倉數量",
"Business Type": "業務類型",
@ -257,11 +263,11 @@
"Cash Module Function": "現金模組功能",
"Category": "類別",
"Category Management": "分類管理",
"Category not found: :category": "找不到商品分類::category",
"Category Name": "分類名稱",
"Category Name (en)": "分類名稱 (英文)",
"Category Name (ja)": "分類名稱 (日文)",
"Category Name (zh_TW)": "分類名稱 (繁體中文)",
"Category not found: :category": "找不到商品分類::category",
"Change": "更換",
"Change Note": "異動備註",
"Change Stock": "零錢庫存",
@ -394,6 +400,8 @@
"Critical": "嚴重",
"Critical failures requiring immediate hands-on attention.": "需要立即處理的關鍵故障。",
"Cross-Company / Headquarter Global Stats": "跨公司 / 總部全域統計",
"Cup drop not detected": "未檢測到杯子掉落",
"Cup dropper failure": "漏杯器漏杯失敗",
"Current": "當前",
"Current Password": "當前密碼",
"Current Status": "當前狀態",
@ -468,6 +476,7 @@
"Delta": "變化量",
"Deploy Update": "部署更新",
"Deploy version": "部署版本",
"Deployment Status": "部署狀態",
"Deposit Bonus": "儲值回饋",
"Describe the changes in this version...": "描述此版本的變更...",
"Describe the repair or maintenance status...": "請描述維修或保養狀況...",
@ -519,6 +528,7 @@
"Dispensing in progress": "正在出貨中",
"Display Material Code": "顯示物料代碼",
"Displaying": "目前顯示",
"Done": "已完成",
"Door Closed": "機門已關閉",
"Door Opened": "機門已開啟",
"Download Image": "下載圖片",
@ -569,12 +579,17 @@
"Effective Alert Types": "目前生效的告警類型",
"Electronic Invoice": "電子發票",
"Electronic Invoices": "電子發票",
"Elevator descending": "升降平台下降中",
"Elevator descent error": "升降平台下降異常",
"Elevator failure": "升降系統故障",
"Elevator rise error": "升降平台上升異常",
"Elevator rising": "升降平台上升中",
"Elevator sensor error": "升降箱感測異常",
"Elevator belt self-check error": "昇降機皮帶自檢錯誤",
"Elevator descending": "昇降機正在下降",
"Elevator descent error": "昇降機下降錯誤",
"Elevator enter microwave error": "昇降台進微波爐錯誤",
"Elevator exit microwave error": "昇降台出微波爐錯誤",
"Elevator failure": "昇降機故障",
"Elevator rise error": "昇降機上升錯誤",
"Elevator rising": "昇降機正在上升",
"Elevator sensor error": "昇降機感測器錯誤",
"Elevator support rod push error": "昇降台撐桿推出錯誤",
"Elevator support rod return error": "昇降台撐桿回位錯誤",
"Email": "電子郵件",
"Emblem / Label": "標章 / 標籤",
"Employee": "員工",
@ -794,7 +809,7 @@
"Helper": "小幫手",
"Home Page": "主頁面",
"Hopper empty": "料斗箱空",
"Hopper empty (0212)": "斗空 (0212)",
"Hopper empty (0212)": "漏斗缺料 (0212)",
"Hopper error (0424)": "料斗箱異常 (0424)",
"Hopper heating timeout": "料斗箱加熱逾時",
"Hopper overheated": "料斗箱過熱",
@ -814,8 +829,8 @@
"Import Products": "匯入商品",
"Import Staff Cards": "匯入員工識別卡",
"Import failed": "匯入失敗",
"Imported with errors. Please check the console.": "部分資料匯入失敗,請查看瀏覽器主控台。",
"Important Notice": "重要通知",
"Imported with errors. Please check the console.": "部分資料匯入失敗,請查看瀏覽器主控台。",
"In Stock": "當前庫存",
"Inactive": "已停用",
"Includes Credit Card/Mobile Pay": "含信用卡/行動支付",
@ -874,6 +889,7 @@
"Last Week": "上週",
"Latitude": "緯度",
"Lease": "租賃",
"Leave blank to deploy immediately, or set a future time to schedule a background release.": "留空將立即部署更新,或設定未來時間由背景自動排程發布。",
"Leave empty for permanent code": "留空表示永久有效",
"Leave empty or 0 for permanent code": "留空或輸入 0 則為永久代碼",
"Leave empty to inherit company settings": "留空則繼承公司全域設定",
@ -890,6 +906,7 @@
"LinePay Payment": "LinePay 支付",
"Link": "連結",
"Link Copied": "連結已複製",
"Liquid dispense paused": "暫停出液",
"Live Fleet Updates": "即時數據更新",
"Loading Cabinet...": "正在載入機台貨盤...",
"Loading Data": "載入資料中",
@ -979,7 +996,7 @@
"Machine model deleted successfully.": "機台型號已成功刪除。",
"Machine model saved successfully.": "機台型號已成功新增。",
"Machine model updated successfully.": "機台型號已成功更新。",
"Machine normal": "機台系統正常",
"Machine normal": "機台正常",
"Machine not found": "找不到機台",
"Machine permissions updated successfully.": "機台權限已成功更新。",
"Machine settings updated successfully.": "機台設定已成功更新。",
@ -1070,8 +1087,19 @@
"Message": "訊息",
"Message Content": "日誌內容",
"Message Display": "訊息顯示",
"Microwave delivery door close error": "微波爐進貨口門關閉錯誤",
"Microwave delivery door closing": "正在關閉微波爐進貨口門",
"Microwave delivery door open error": "微波爐進貨口門打開錯誤",
"Microwave delivery door opening": "正在打開微波爐進貨口門",
"Microwave door error": "微波爐門異常",
"Microwave door opened": "微波爐門開啟",
"Microwave pickup door close error": "微波爐取貨口門關閉錯誤",
"Microwave pickup door closing": "正在關閉微波爐取貨口門",
"Microwave pickup door open error": "微波爐取貨口門打開錯誤",
"Microwave pickup door opening": "正在打開微波爐取貨口門",
"Microwave push rod push error": "微波爐內推桿推出錯誤",
"Microwave push rod retract error": "微波爐內推桿收回錯誤",
"Milk dispense timeout": "按鍵出奶超時",
"Min 8 characters": "至少 8 個字元",
"Model": "機台型號",
"Model Default": "型號預設",
@ -1087,7 +1115,9 @@
"Monthly Cumulative Revenue": "本月累計營收",
"Monthly Transactions": "本月交易統計",
"Monthly cumulative revenue overview": "本月累計營收概況",
"Motor not stopped": "電機未停止",
"Motor not found": "電機不存在",
"Motor not found (0406)": "電機不存在 (0406)",
"Motor not stopped": "電機未正常停止",
"Movement History": "異動紀錄",
"Movement Logs": "異動紀錄",
"Multilingual Names": "多語系名稱",
@ -1147,6 +1177,7 @@
"No machines available": "目前沒有可供分配的機台",
"No machines available in this company.": "此客戶目前沒有可供分配的機台。",
"No machines found": "未找到機台",
"No machines in this status": "此狀態下無設備",
"No machines match your criteria": "沒有符合條件的機台",
"No maintenance records found": "找不到維修紀錄",
"No matching logs found": "找不到符合條件的日誌",
@ -1204,6 +1235,7 @@
"OTA Update Deployment": "OTA 更新部署",
"OTA command sent to %d devices successfully.": "OTA 指令已成功發送至 %d 台設備。",
"OTA firmware update and version control": "OTA 韌體更新與版本控制",
"OTA update scheduled successfully.": "OTA 預約更新排程已建立成功。",
"Off": "關閉",
"Offline": "離線",
"Offline + 1": "離線 + 1",
@ -1353,13 +1385,19 @@
"Pickup Code": "取貨碼",
"Pickup Code (8 Digits)": "取貨碼 (8 位數)",
"Pickup Codes": "取貨碼",
"Pickup Recipient (Prescription Slip)": "取物人(領藥單)",
"Pickup Recipient Info": "取物人資訊",
"Pickup Ticket": "商品領取券",
"Pickup Voucher": "取物單",
"Pickup code cancelled.": "取貨碼已取消",
"Pickup code generated: :code": "已生成取貨碼::code",
"Pickup code updated.": "取貨碼已更新",
"Pickup door closed": "取貨門已關閉",
"Pickup door closed (cart mode)": "取貨口門關閉成功",
"Pickup door error": "取貨門運作異常",
"Pickup door not closed": "取貨門未關閉",
"Pickup door opened (cart mode)": "取貨口門打開成功",
"Pickup door unlocked": "取貨口門開鎖",
"Platform Operator": "平台營運商",
"Playback Order": "播放順序",
"Please Select Slot first": "請先選擇貨道",
@ -1405,6 +1443,7 @@
"Print Notice": "列印提示",
"Print Order": "列印單據",
"Print Time": "列印時間",
"Processing...": "處理中...",
"Product": "商品",
"Product / Slot": "商品 / 貨道",
"Product / Stock": "商品 / 庫存",
@ -1426,7 +1465,7 @@
"Product Status": "商品狀態",
"Product created successfully": "商品已成功建立",
"Product deleted successfully": "商品已成功刪除",
"Product empty": "貨道缺貨 (PDT_EMPTY)",
"Product empty": "商品售罄",
"Product physical shipment count": "商品實體出貨件數",
"Product status updated to :status": "商品狀態已更新為 :status",
"Product updated successfully": "商品已成功更新",
@ -1444,10 +1483,12 @@
"Publish Time": "發布時間",
"Purchase Audit": "採購單",
"Purchase Finished": "購買結束",
"Purchase successful": "購買成功",
"Purchases": "採購單",
"Purchasing": "購買中",
"Purpose": "用途",
"Push Status": "告警狀態",
"Pushing bento into microwave": "正在將便當推入微波爐",
"QR": "二維碼",
"QR CODE": "二維碼",
"QR Code": "QR Code",
@ -1467,6 +1508,8 @@
"Quick replenishment from this view": "從此畫面快速補貨",
"Quick search...": "快速搜尋...",
"Rank": "排行",
"Ready": "準備中",
"Ready to Execute": "準備要執行指令的設備",
"Ready to print": "準備列印",
"Real-time OEE analysis awaits": "即時 OEE 分析預備中",
"Real-time Operation Logs (Last 50)": "即時操作日誌 (最後 50 筆)",
@ -1594,6 +1637,7 @@
"Scan to authorize testing or maintenance": "掃描以進行測試或維護授權",
"Scan to pick up your product": "掃描以領取您的商品",
"Schedule": "排程區間",
"Schedule Release Time (Optional)": "預約發布時間(選填)",
"Search": "搜尋",
"Search Company Title...": "搜尋公司名稱...",
"Search Company...": "搜尋公司...",
@ -1659,6 +1703,7 @@
"Select Slot...": "選擇貨道...",
"Select Target Company": "選擇目標公司",
"Select Target Slot": "選擇目標貨道",
"Select Update Time (Optional)": "選擇更新時間(選填)",
"Select Warehouse": "選擇倉庫",
"Select a machine to copy settings from...": "選擇要複製設定的來源機台...",
"Select a machine to deep dive": "請選擇機台以開始深度分析",
@ -1668,6 +1713,7 @@
"Select an asset from the left to start analysis": "選擇左側機台以開始分析數據",
"Select date to sync data": "選擇日期以同步數據",
"Select flavor...": "選擇風味...",
"Select future time...": "選擇更新時間",
"Select...": "請選擇...",
"Selected": "已選擇",
"Selected Date": "查詢日期",
@ -1704,16 +1750,16 @@
"Slot No": "貨道編號",
"Slot Status": "貨道效期",
"Slot Test": "貨道測試",
"Slot empty": "貨道空 (SLOT_EMPTY)",
"Slot jammed": "貨道卡貨 (K-PDT)",
"Slot motor error (0207)": "貨道電機故障 (0207)",
"Slot motor error (0208)": "貨道電機故障 (0208)",
"Slot motor error (0209)": "貨道電機故障 (0209)",
"Slot empty": "貨道缺貨",
"Slot jammed": "貨道卡貨",
"Slot motor error (0207)": "貨道馬達錯誤 (0207)",
"Slot motor error (0208)": "貨道馬達錯誤 (0208)",
"Slot motor error (0209)": "貨道馬達錯誤 (0209)",
"Slot normal": "貨道正常",
"Slot not closed": "貨道未關閉",
"Slot not found": "找不到指定貨道",
"Slot report synchronized success": "貨道狀態同步成功",
"Slot sensor blocked": "貨道口有物",
"Slot sensor blocked": "貨道感測器受阻",
"Slot updated successfully.": "貨道更新成功。",
"Slots": "貨道數",
"Slots need replenishment": "需要補貨的貨道",
@ -1750,7 +1796,6 @@
"Standby Ad": "待機廣告",
"Start Date": "起始日",
"Start Delivery": "開始配送",
"Processing...": "處理中...",
"Start Import": "開始匯入",
"Start Time": "開始時間",
"Statistics": "數據統計",
@ -1980,6 +2025,7 @@
"Unpublish": "下架",
"Update": "編輯",
"Update Authorization": "更新授權",
"Update Completed": "已經完成更新指令的設備",
"Update Customer": "更新客戶",
"Update Mode": "更新模式",
"Update Pass Code": "更新通行碼",

View File

@ -173,49 +173,15 @@
</div>
</div>
{{-- 更新模式卡片 --}}
<div class="luxury-card rounded-3xl p-8 animate-luxury-in group/card" style="animation-delay: 100ms">
<div class="flex items-center gap-5 mb-8">
<div class="w-10 h-10 rounded-xl bg-gradient-to-br from-amber-500/10 to-orange-500/10 flex items-center justify-center text-amber-500 border border-amber-500/20 shadow-lg shadow-amber-500/5 group-hover/card:scale-110 transition-transform duration-500">
<svg class="w-5 h-5 stroke-[2.5]" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126zM12 15.75h.007v.008H12v-.008z"/></svg>
{{-- 注意事項卡片 --}}
<div class="luxury-card rounded-3xl p-8 animate-luxury-in group/card bg-amber-500/5 dark:bg-amber-500/5 border border-amber-500/15" style="animation-delay: 100ms">
<div class="flex items-start gap-4">
<div class="w-10 h-10 rounded-xl bg-amber-500/10 flex items-center justify-center text-amber-500 border border-amber-500/20 shadow-lg shadow-amber-500/5 shrink-0">
<svg class="w-5 h-5 stroke-[2.5]" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m9-.75a9 9 0 11-18 0 9 9 0 0118 0zm-9 3.75h.008v.008H12v-.008z"/></svg>
</div>
<div>
<h3 class="text-lg font-black text-slate-800 dark:text-white tracking-tight font-display uppercase">{{ __('Update Mode') }}</h3>
<p class="text-[10px] font-bold text-slate-400 dark:text-slate-500 uppercase tracking-widest mt-0.5">{{ __('Control deployment behavior') }}</p>
</div>
</div>
{{-- 強制更新開關 --}}
<div class="flex items-center justify-between p-5 rounded-2xl border transition-all duration-300"
:class="isForced
? 'bg-rose-500/10 dark:bg-rose-500/10 border-rose-500/20 dark:border-rose-500/30 text-rose-600 dark:text-rose-400 shadow-sm shadow-rose-500/5'
: 'bg-slate-50/80 dark:bg-slate-800/40 border-slate-100 dark:border-slate-800/80 text-slate-700 dark:text-slate-200'">
<div class="flex items-center gap-4">
<div class="w-10 h-10 rounded-xl flex items-center justify-center transition-all duration-300"
:class="isForced ? 'bg-rose-500/10 text-rose-500' : 'bg-slate-100 dark:bg-slate-800 text-slate-400'">
<svg class="w-5 h-5 stroke-[2.5]" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M3.75 13.5l10.5-11.25L12 10.5h8.25L9.75 21.75 12 13.5H3.75z"/></svg>
</div>
<div>
<p class="text-sm font-extrabold transition-colors"
:class="isForced ? 'text-rose-600 dark:text-rose-400' : 'text-slate-700 dark:text-slate-200'">{{ __('Forced Update') }}</p>
<p class="text-[10px] font-bold text-slate-400 dark:text-slate-500 tracking-wide mt-0.5">{{ __('During OTA deployment, the device will install directly in the background without user confirmation.') }}</p>
</div>
</div>
<label class="relative inline-flex items-center cursor-pointer shrink-0">
<input type="hidden" name="is_forced" value="0">
<input type="checkbox" name="is_forced" value="1" x-model="isForced" class="sr-only peer" {{ old('is_forced') ? 'checked' : '' }}>
<div class="w-11 h-6 bg-slate-200 dark:bg-slate-700 peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-slate-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all dark:border-slate-600 peer-checked:bg-rose-500"></div>
</label>
</div>
{{-- 注意事項 --}}
<div class="mt-6 p-5 rounded-2xl bg-amber-500/10 border border-amber-500/20 shadow-sm shadow-amber-500/5">
<div class="flex items-start gap-4">
<svg class="w-5 h-5 stroke-[2.5] text-amber-500 shrink-0 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m9-.75a9 9 0 11-18 0 9 9 0 0118 0zm-9 3.75h.008v.008H12v-.008z"/></svg>
<div class="text-xs font-bold text-amber-800 dark:text-amber-300 tracking-wide leading-relaxed">
<p class="text-sm font-black uppercase tracking-widest mb-1.5 flex items-center gap-1">{{ __('Important Notice') }}</p>
<p class="font-bold opacity-90 leading-relaxed">{{ __('Version Code must be strictly greater than the previous release, otherwise the OTA silent installation on the device will fail with INSTALL_FAILED_ALREADY_EXISTS.') }}</p>
</div>
<div class="text-xs font-bold text-amber-800 dark:text-amber-300 tracking-wide leading-relaxed">
<p class="text-sm font-black uppercase tracking-widest mb-1.5 flex items-center gap-1 font-display">{{ __('Important Notice') }}</p>
<p class="font-bold opacity-90 leading-relaxed">{{ __('Version Code must be strictly greater than the previous release, otherwise the OTA silent installation on the device will fail with INSTALL_FAILED_ALREADY_EXISTS.') }}</p>
</div>
</div>
</div>
@ -230,7 +196,6 @@
uploadMode: 'file',
fileName: '',
isDragging: false,
isForced: false,
versionName: '{{ old('version_name', '') }}',
versionCode: '{{ old('version_code', '') }}',

View File

@ -43,6 +43,9 @@
this.searchMachineQuery = '';
this.onlyOnlineMachines = false;
this.isPushModalOpen = true;
// 清空預約時間
const scheduleInput = document.getElementById('scheduled_at');
if (scheduleInput) scheduleInput.value = '';
},
get filteredMachines() {
@ -143,7 +146,7 @@
<tr class="bg-slate-50/50 dark:bg-slate-900/10">
<th class="px-6 py-4 text-[11px] font-black text-slate-400 uppercase tracking-[0.2em] border-b border-slate-100 dark:border-slate-800">{{ __('Version Info') }}</th>
<th class="px-6 py-4 text-[11px] font-black text-slate-400 uppercase tracking-[0.2em] border-b border-slate-100 dark:border-slate-800">{{ __('Machine Flavor') }}</th>
<th class="px-6 py-4 text-[11px] font-black text-slate-400 uppercase tracking-[0.2em] border-b border-slate-100 dark:border-slate-800">{{ __('Update Mode') }}</th>
<th class="px-6 py-4 text-[11px] font-black text-slate-400 uppercase tracking-[0.2em] border-b border-slate-100 dark:border-slate-800">{{ __('Deployment Status') }}</th>
<th class="px-6 py-4 text-[11px] font-black text-slate-400 uppercase tracking-[0.2em] border-b border-slate-100 dark:border-slate-800">{{ __('Release Notes') }}</th>
<th class="px-6 py-4 text-[11px] font-black text-slate-400 uppercase tracking-[0.2em] border-b border-slate-100 dark:border-slate-800 text-right">{{ __('Action') }}</th>
</tr>
@ -171,16 +174,46 @@
{{ $version->flavor }}
</span>
</td>
<td class="px-6 py-6">
@if($version->is_forced)
<span class="px-2.5 py-1 rounded-lg text-xs font-bold border border-rose-100 dark:border-rose-900/30 bg-rose-50 dark:bg-rose-900/20 text-rose-600 dark:text-rose-400 tracking-widest uppercase">
{{ __('Forced Update') }}
</span>
@else
<span class="px-2.5 py-1 rounded-lg text-xs font-bold border border-slate-200 dark:border-slate-700 bg-slate-100 dark:bg-slate-800 text-slate-500 dark:text-slate-400 tracking-widest uppercase">
{{ __('Optional Update') }}
</span>
@endif
<td class="px-6 py-6 whitespace-nowrap">
<div class="flex items-center gap-3">
<!-- 準備中 -->
@php
$pendingTooltip = __('Ready to Execute') . ":\n";
if ($version->pending_machines->isEmpty()) {
$pendingTooltip .= "" . __('No machines in this status');
} else {
foreach($version->pending_machines as $m) {
$pendingTooltip .= "{$m->name} ({$m->serial_no})\n";
}
}
$pendingTooltip = trim($pendingTooltip);
@endphp
<div class="inline-block tooltip cursor-help" title="{{ $pendingTooltip }}">
<span class="px-2.5 py-1 rounded-lg text-xs font-bold border border-amber-100 dark:border-amber-900/30 bg-amber-50 dark:bg-amber-900/20 text-amber-600 dark:text-amber-400 tracking-widest inline-flex items-center gap-1.5">
<span class="w-1.5 h-1.5 rounded-full bg-amber-500 animate-pulse"></span>
{{ __('Ready') }}: {{ $version->pending_machines->count() }}
</span>
</div>
<!-- 已完成 -->
@php
$completedTooltip = __('Update Completed') . ":\n";
if ($version->completed_machines->isEmpty()) {
$completedTooltip .= "" . __('No machines in this status');
} else {
foreach($version->completed_machines as $m) {
$completedTooltip .= "{$m->name} ({$m->serial_no})\n";
}
}
$completedTooltip = trim($completedTooltip);
@endphp
<div class="inline-block tooltip cursor-help" title="{{ $completedTooltip }}">
<span class="px-2.5 py-1 rounded-lg text-xs font-bold border border-emerald-100 dark:border-emerald-900/30 bg-emerald-50 dark:bg-emerald-900/20 text-emerald-600 dark:text-emerald-400 tracking-widest inline-flex items-center gap-1.5">
<span class="w-1.5 h-1.5 rounded-full bg-emerald-500"></span>
{{ __('Done') }}: {{ $version->completed_machines->count() }}
</span>
</div>
</div>
</td>
<td class="px-6 py-6 max-w-xs truncate">
<span class="text-sm font-medium text-slate-500 dark:text-slate-400 leading-relaxed">
@ -307,6 +340,42 @@
<!-- 表單與機台選擇清單 -->
<form :action="pushActionUrl" method="POST" id="ota-push-form">
@csrf
<!-- 預約排程時間設定 -->
<div class="mb-6 p-4 rounded-2xl border border-slate-100 dark:border-slate-800 bg-slate-50/50 dark:bg-slate-950/20">
<div class="flex flex-col md:flex-row md:items-center justify-between gap-4">
<div class="flex-1">
<label for="scheduled_at" class="block text-xs font-black text-slate-500 dark:text-slate-400 uppercase tracking-widest mb-1.5">
{{ __('Select Update Time (Optional)') }}
</label>
<p class="text-[10px] text-slate-400 dark:text-slate-500 leading-normal">
{{ __('Leave blank to deploy immediately, or set a future time to schedule a background release.') }}
</p>
</div>
<div class="w-full md:w-64 group">
<div class="relative">
<span class="absolute inset-y-0 left-0 flex items-center pl-4 pointer-events-none z-10 text-slate-400 group-focus-within:text-cyan-500 transition-colors">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" />
</svg>
</span>
<input type="text"
id="scheduled_at"
name="scheduled_at"
x-init="flatpickr($el, {
enableTime: true,
dateFormat: 'Y-m-d H:i',
time_24hr: true,
locale: '{{ app()->getLocale() === 'zh_TW' ? 'zh_tw' : (app()->getLocale() === 'ja' ? 'ja' : 'en') }}',
disableMobile: true
})"
class="luxury-input py-2.5 pl-12 pr-4 block w-full text-sm font-bold tracking-tight cursor-pointer"
placeholder="{{ __('Select future time...') }}">
</div>
</div>
</div>
</div>
<div class="border border-slate-100 dark:border-slate-800/80 rounded-2xl overflow-hidden">
<table class="w-full text-left">
<thead class="bg-slate-50/50 dark:bg-slate-950/40">

View File

@ -114,6 +114,21 @@
</section>
@endif
@if($order->payment_type == 42 && !empty($order->member_barcode))
<section class="space-y-4">
<div class="flex items-center gap-2 px-1">
<div class="w-1.5 h-4 bg-cyan-500 rounded-full"></div>
<h3 class="text-sm font-black text-slate-800 dark:text-white uppercase tracking-widest">{{ __('Pickup Recipient Info') }}</h3>
</div>
<div class="grid grid-cols-2 gap-8 p-8 bg-slate-50/50 dark:bg-slate-800/30 rounded-[2rem] border border-slate-100 dark:border-slate-800">
<div class="space-y-1 col-span-2">
<p class="text-[10px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-widest">{{ __('Pickup Recipient (Prescription Slip)') }}</p>
<p class="text-sm font-extrabold text-slate-700 dark:text-slate-200">{{ $order->member_barcode }}</p>
</div>
</div>
</section>
@endif
{{-- 商品明細 --}}
<section class="space-y-4">
<div class="flex items-center justify-between px-1">

View File

@ -243,6 +243,14 @@
{{ $order->staffCardLog->staffCard->name }} ({{ $order->staffCardLog->staffCard->employee_id }})
</span>
@endif
@if($order->payment_type == 42 && !empty($order->member_barcode))
<span class="text-[10px] text-cyan-600 dark:text-cyan-400 font-extrabold mt-1.5 flex items-center gap-1">
<svg class="w-3.5 h-3.5 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5">
<path stroke-linecap="round" stroke-linejoin="round" d="M15 9h3.75M15 12h3.75M15 15h3.75M4.5 19.5h15a2.25 2.25 0 002.25-2.25V6.75A2.25 2.25 0 0019.5 4.5h-15a2.25 2.25 0 00-2.25 2.25v10.5A2.25 2.25 0 004.5 19.5zm6-10.125a1.875 1.875 0 11-3.75 0 1.875 1.875 0 013.75 0zm1.294 6.336a6.721 6.721 0 01-3.17.789 6.721 6.721 0 01-3.168-.789 3.376 3.376 0 016.338 0z" />
</svg>
{{ $order->member_barcode }}
</span>
@endif
</div>
</td>
<td class="px-6 py-6 whitespace-nowrap">
@ -380,6 +388,14 @@
{{ $order->staffCardLog->staffCard->name }} ({{ $order->staffCardLog->staffCard->employee_id }})
</div>
@endif
@if($order->payment_type == 42 && !empty($order->member_barcode))
<div class="text-[10px] text-cyan-600 dark:text-cyan-400 font-extrabold mt-1 flex items-center gap-1">
<svg class="w-3.5 h-3.5 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5">
<path stroke-linecap="round" stroke-linejoin="round" d="M15 9h3.75M15 12h3.75M15 15h3.75M4.5 19.5h15a2.25 2.25 0 002.25-2.25V6.75A2.25 2.25 0 0019.5 4.5h-15a2.25 2.25 0 00-2.25 2.25v10.5A2.25 2.25 0 004.5 19.5zm6-10.125a1.875 1.875 0 11-3.75 0 1.875 1.875 0 013.75 0zm1.294 6.336a6.721 6.721 0 01-3.17.789 6.721 6.721 0 01-3.168-.789 3.376 3.376 0 016.338 0z" />
</svg>
{{ $order->member_barcode }}
</div>
@endif
</div>
<div>
<p class="text-[10px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-widest mb-1">{{
@ -422,4 +438,4 @@
{{-- Pagination --}}
<div class="mt-8 border-t border-slate-100/50 dark:border-slate-800/50 pt-6">
{{ $orders->links('vendor.pagination.luxury', ['page_param' => 'orders_page']) }}
</div>
</div>

218
workflow Executable file
View File

@ -0,0 +1,218 @@
#!/usr/bin/env bash
set -euo pipefail
WF_DIR="${HOME}/.gemini/config/global_workflows"
SELF_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
usage() {
cat <<'EOF'
Usage:
./workflow list
./workflow run <name> [args...]
Workflows:
load-ids
daily-work-log
hotfix-prod <hotfix-name>
demo-day-pptx
start-task <feat|fix|refactor|docs> <task-name-kebab> [--stash]
promote-env <dev|demo> <demo|main> [--force-main]
monthly-report <year> <month>
now-push <target-branch> <prefix:FIX|FEAT|DOCS|STYLE|REFACTOR> "<summary>" ["detail 1" "detail 2"...]
EOF
}
require_repo() {
git -C "${SELF_DIR}" rev-parse --is-inside-work-tree >/dev/null
}
ensure_dev_window_for_main() {
local now_dow now_hm
now_dow="$(date +%u)" # 1..7 = Mon..Sun
now_hm="$(date +%H%M)"
if [[ "${now_dow}" -gt 4 || "${now_hm}" -gt 1200 ]]; then
echo "WARNING: 現在不在標準上線時段(週一至週四 12:00 前)。"
if [[ "${1:-}" != "--force-main" ]]; then
echo "中止。若確認要繼續,請加上 --force-main。"
exit 1
fi
fi
}
cmd_list() {
ls -1 "${WF_DIR}" 2>/dev/null | sed 's/\.md$//'
}
cmd_load_ids() {
sed -n '1,120p' "${HOME}/.gemini/AGENTS.md"
echo
sed -n '1,120p' "${HOME}/.gemini/GEMINI.md"
echo
echo "身份與全域規範已加載完成。"
}
cmd_daily_work_log() {
require_repo
git -C "${SELF_DIR}" log --since="midnight" --pretty=format:"%h - %s (%an)"
}
cmd_hotfix_prod() {
require_repo
local name="${1:-}"
if [[ -z "${name}" ]]; then
echo "hotfix-prod 需要 hotfix 名稱。"
exit 1
fi
git -C "${SELF_DIR}" checkout main
git -C "${SELF_DIR}" pull origin main
git -C "${SELF_DIR}" checkout -b "hotfix/${name}"
echo "已建立分支 hotfix/${name}。完成修復後請自行合併到 main再回補 demo/dev。"
}
cmd_demo_day_pptx() {
cat <<'EOF'
demo-day-pptx 已觸發:
1) 先產出 implementation_plan.md7-9頁給你確認
2) 確認後再執行 PPTX 產製
EOF
}
cmd_start_task() {
require_repo
local kind="${1:-}" name="${2:-}" stash_opt="${3:-}"
if [[ ! "${kind}" =~ ^(feat|fix|refactor|docs)$ ]] || [[ -z "${name}" ]]; then
echo "用法:./workflow run start-task <feat|fix|refactor|docs> <task-name-kebab> [--stash]"
exit 1
fi
if [[ -n "$(git -C "${SELF_DIR}" status --porcelain)" && "${stash_opt}" == "--stash" ]]; then
git -C "${SELF_DIR}" stash push -u -m "workflow:start-task"
fi
git -C "${SELF_DIR}" checkout dev
git -C "${SELF_DIR}" pull origin dev
git -C "${SELF_DIR}" checkout -b "${kind}/${name}"
if git -C "${SELF_DIR}" stash list | rg -q "workflow:start-task"; then
git -C "${SELF_DIR}" stash pop || true
fi
echo "已切到分支 ${kind}/${name}"
}
cmd_promote_env() {
require_repo
local src="${1:-}" dst="${2:-}" force_main="${3:-}"
if [[ -z "${src}" || -z "${dst}" ]]; then
echo "用法:./workflow run promote-env <dev|demo> <demo|main> [--force-main]"
exit 1
fi
if [[ "${src}" == "dev" && "${dst}" != "demo" ]]; then
echo "僅允許 dev -> demo。"
exit 1
fi
if [[ "${src}" == "demo" && "${dst}" != "main" ]]; then
echo "僅允許 demo -> main。"
exit 1
fi
if [[ "${dst}" == "main" ]]; then
ensure_dev_window_for_main "${force_main:-}"
fi
echo "=== 待晉升提交 ==="
git -C "${SELF_DIR}" log "${dst}..${src}" --oneline || true
echo
echo "=== 敏感檔案檢查 ==="
git -C "${SELF_DIR}" diff --name-only "${dst}...${src}" | rg "^(database/migrations/|\.env\.example|rbac-rules\.md)" || true
git -C "${SELF_DIR}" checkout "${dst}"
git -C "${SELF_DIR}" pull origin "${dst}"
git -C "${SELF_DIR}" merge "${src}" --no-ff -m "[PROMOTE] ${src} -> ${dst}"
git -C "${SELF_DIR}" push origin "${dst}"
git -C "${SELF_DIR}" checkout dev
}
cmd_monthly_report() {
local year="${1:-}" month="${2:-}"
if [[ ! "${year}" =~ ^[0-9]{4}$ ]] || [[ ! "${month}" =~ ^[0-9]{1,2}$ ]]; then
echo "用法:./workflow run monthly-report <year> <month>"
exit 1
fi
local out="${SELF_DIR}/docs/monthly-report-${year}-$(printf "%02d" "${month}").md"
mkdir -p "${SELF_DIR}/docs"
cat > "${out}" <<EOF
# Monthly Report Draft (${year}-$(printf "%02d" "${month}"))
## Data Source
- Notion Work Log (to be fetched)
- Git commits for the target month
## Slide Plan
1. 封面與概要
2. 月初至月底時序成果
3. 關鍵技術專題
4. 下月展望
EOF
echo "已建立草稿:${out}"
}
cmd_now_push() {
require_repo
local target="${1:-}" prefix="${2:-}" summary="${3:-}"
shift 3 || true
if [[ -z "${target}" || -z "${prefix}" || -z "${summary}" ]]; then
echo '用法:./workflow run now-push <target-branch> <prefix:FIX|FEAT|DOCS|STYLE|REFACTOR> "<summary>" ["detail 1" ...]'
exit 1
fi
local p="[${prefix}]"
if [[ ! "${p}" =~ ^\[(FIX|FEAT|DOCS|STYLE|REFACTOR)\]$ ]]; then
echo "prefix 僅能為 FIX/FEAT/DOCS/STYLE/REFACTOR"
exit 1
fi
if [[ "${target}" == "main" ]]; then
ensure_dev_window_for_main "${4:-}"
fi
local msg="${p} ${summary}"
if [[ "$#" -gt 0 ]]; then
msg+=$'\n\n'
local i=1
for item in "$@"; do
msg+="${i}. ${item}"$'\n'
i=$((i+1))
done
fi
git -C "${SELF_DIR}" add -A
git -C "${SELF_DIR}" commit -m "${msg}"
local cur
cur="$(git -C "${SELF_DIR}" branch --show-current)"
git -C "${SELF_DIR}" push origin "${cur}:${target}"
}
main() {
local action="${1:-}"
case "${action}" in
list) cmd_list ;;
run)
local name="${2:-}"
shift 2 || true
case "${name}" in
load-ids) cmd_load_ids "$@" ;;
daily-work-log) cmd_daily_work_log "$@" ;;
hotfix-prod) cmd_hotfix_prod "$@" ;;
demo-day-pptx) cmd_demo_day_pptx "$@" ;;
start-task) cmd_start_task "$@" ;;
promote-env) cmd_promote_env "$@" ;;
monthly-report) cmd_monthly_report "$@" ;;
now-push) cmd_now_push "$@" ;;
*) usage; exit 1 ;;
esac
;;
*) usage; exit 1 ;;
esac
}
main "$@"