Compare commits
7 Commits
c343df34ee
...
dev
| Author | SHA1 | Date | |
|---|---|---|---|
| daf8b1ebcc | |||
| 8f008ffb61 | |||
| 729890d7c7 | |||
| ad256d3d3b | |||
| 5415b14a53 | |||
| c97776892e | |||
| a599b14df1 |
@@ -12,8 +12,18 @@ description: 本技能規範定義了 Star Cloud 系統中所有機台 (IoT) 與
|
|||||||
- **類型嚴格**:文件定義的類型 (Integer, Float, String) 必須在後端 Model 與前端文件中心嚴格遵守。
|
- **類型嚴格**:文件定義的類型 (Integer, Float, String) 必須在後端 Model 與前端文件中心嚴格遵守。
|
||||||
|
|
||||||
## 2. 身份認證 (Authentication)
|
## 2. 身份認證 (Authentication)
|
||||||
- **Bearer Token**:所有 API 必須在 Header 帶入 Authorization: Bearer <api_token>。
|
本系統採用兩階段認證模式:
|
||||||
- **身分綁定**:後端透過 Token 自動識別 machine_id,禁止在 Body 帶入 machine 或 key 欄位。
|
|
||||||
|
### 2.1 維運人員認證 (User Authentication)
|
||||||
|
- **核發端點**:B000 (登入)。
|
||||||
|
- **使用端點**:B014 (參數下載)。
|
||||||
|
- **方式**:使用 Laravel Sanctum 核發之 **User Token**。
|
||||||
|
- **Header**:`Authorization: Bearer <user_token>`。
|
||||||
|
|
||||||
|
### 2.2 機台通訊認證 (Machine Authentication)
|
||||||
|
- **適用 API**:B010, B012, B013, B600 等後續通訊。
|
||||||
|
- **方式**:使用機台專屬之 **api_token**。
|
||||||
|
- **Header**:`Authorization: Bearer <api_token>`。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -24,6 +34,7 @@ description: 本技能規範定義了 Star Cloud 系統中所有機台 (IoT) 與
|
|||||||
|
|
||||||
- **URL**: POST /api/v1/app/admin/login/B000
|
- **URL**: POST /api/v1/app/admin/login/B000
|
||||||
- **Request Body:**
|
- **Request Body:**
|
||||||
|
|
||||||
| 參數 | 類型 | 必填 | 說明 | 範例 |
|
| 參數 | 類型 | 必填 | 說明 | 範例 |
|
||||||
| :--- | :--- | :--- | :--- | :--- |
|
| :--- | :--- | :--- | :--- | :--- |
|
||||||
| machine | String | 是 | 機台編號 (serial_no) | M-001 |
|
| machine | String | 是 | 機台編號 (serial_no) | M-001 |
|
||||||
@@ -35,9 +46,11 @@ description: 本技能規範定義了 Star Cloud 系統中所有機台 (IoT) 與
|
|||||||
- **Response Body:**
|
- **Response Body:**
|
||||||
> [!IMPORTANT]
|
> [!IMPORTANT]
|
||||||
> 為了相容 Java APP 現有邏輯,這裡嚴格規定成功必須回傳字串 Success。
|
> 為了相容 Java APP 現有邏輯,這裡嚴格規定成功必須回傳字串 Success。
|
||||||
|
|
||||||
| 參數 | 類型 | 說明 | 範例 |
|
| 參數 | 類型 | 說明 | 範例 |
|
||||||
| :--- | :--- | :--- | :--- |
|
| :--- | :--- | :--- | :--- |
|
||||||
| message | String | 驗證結果 (Success 或 Failed) | Success |
|
| message | String | 驗證結果 (Success 或 Failed) | Success |
|
||||||
|
| token | String | **臨時身份認證 Token** (用於 B014) | 1|abcdefg... |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -48,6 +61,7 @@ description: 本技能規範定義了 Star Cloud 系統中所有機台 (IoT) 與
|
|||||||
- **Request Body:** 無 (GET 請求)
|
- **Request Body:** 無 (GET 請求)
|
||||||
|
|
||||||
- **Response Body:**
|
- **Response Body:**
|
||||||
|
|
||||||
| 參數 | 類型 | 說明 | 範例 |
|
| 參數 | 類型 | 說明 | 範例 |
|
||||||
| :--- | :--- | :--- | :--- |
|
| :--- | :--- | :--- | :--- |
|
||||||
| success | Boolean | 請求是否成功 | true |
|
| success | Boolean | 請求是否成功 | true |
|
||||||
@@ -74,12 +88,14 @@ description: 本技能規範定義了 Star Cloud 系統中所有機台 (IoT) 與
|
|||||||
|
|
||||||
- **URL**: PUT /api/v1/app/products/supplementary/B009
|
- **URL**: PUT /api/v1/app/products/supplementary/B009
|
||||||
- **Request Body:**
|
- **Request Body:**
|
||||||
|
|
||||||
| 參數 | 類型 | 必填 | 說明 | 範例 |
|
| 參數 | 類型 | 必填 | 說明 | 範例 |
|
||||||
| :--- | :--- | :--- | :--- | :--- |
|
| :--- | :--- | :--- | :--- | :--- |
|
||||||
| account | String | 是 | 操作人員帳號 | 0999123456 |
|
| account | String | 是 | 操作人員帳號 | 0999123456 |
|
||||||
| data | Array | 是 | 貨道數據陣列 | [{"tid":"1", "t060v00":"1", "num":"10"}] |
|
| data | Array | 是 | 貨道數據陣列 | [{"tid":"1", "t060v00":"1", "num":"10"}] |
|
||||||
|
|
||||||
- **data 陣列內部欄位:**
|
- **data 陣列內部欄位:**
|
||||||
|
|
||||||
| 欄位 | 類型 | 說明 | 範例 |
|
| 欄位 | 類型 | 說明 | 範例 |
|
||||||
| :--- | :--- | :--- | :--- |
|
| :--- | :--- | :--- | :--- |
|
||||||
| tid | Integer | 貨道編號 (Slot No) | 1 |
|
| tid | Integer | 貨道編號 (Slot No) | 1 |
|
||||||
@@ -92,6 +108,7 @@ description: 本技能規範定義了 Star Cloud 系統中所有機台 (IoT) 與
|
|||||||
> 當後端收到 B009 時,會根據 type 自動從該商品的配置中選取 spring_limit 或 track_limit 並自動更新該貨道的 max_stock 欄位。機台端無需手動計算上限。
|
> 當後端收到 B009 時,會根據 type 自動從該商品的配置中選取 spring_limit 或 track_limit 並自動更新該貨道的 max_stock 欄位。機台端無需手動計算上限。
|
||||||
|
|
||||||
- **Response Body (Success 200):**
|
- **Response Body (Success 200):**
|
||||||
|
|
||||||
| 參數 | 類型 | 說明 | 範例 |
|
| 參數 | 類型 | 說明 | 範例 |
|
||||||
| :--- | :--- | :--- | :--- |
|
| :--- | :--- | :--- | :--- |
|
||||||
| success | Boolean | 同步是否成功 | true |
|
| success | Boolean | 同步是否成功 | true |
|
||||||
@@ -107,6 +124,7 @@ description: 本技能規範定義了 Star Cloud 系統中所有機台 (IoT) 與
|
|||||||
- **URL**: POST /api/v1/app/machine/status/B010
|
- **URL**: POST /api/v1/app/machine/status/B010
|
||||||
- **Authentication**: Bearer Token (Header)
|
- **Authentication**: Bearer Token (Header)
|
||||||
- **Request Body:**
|
- **Request Body:**
|
||||||
|
|
||||||
| 參數 | 類型 | 必填 | 說明 | 範例 |
|
| 參數 | 類型 | 必填 | 說明 | 範例 |
|
||||||
| :--- | :--- | :--- | :--- | :--- |
|
| :--- | :--- | :--- | :--- | :--- |
|
||||||
| current_page | Integer | 是 | 當前頁面代碼 (見下表) | 1 |
|
| current_page | Integer | 是 | 當前頁面代碼 (見下表) | 1 |
|
||||||
@@ -119,6 +137,7 @@ description: 本技能規範定義了 Star Cloud 系統中所有機台 (IoT) 與
|
|||||||
| log_payload | Object | 否 | 額外日誌 JSON 對象 | {"code":500} |
|
| log_payload | Object | 否 | 額外日誌 JSON 對象 | {"code":500} |
|
||||||
|
|
||||||
- **Response Body:**
|
- **Response Body:**
|
||||||
|
|
||||||
| 參數 | 類型 | 說明 | 範例 |
|
| 參數 | 類型 | 說明 | 範例 |
|
||||||
| :--- | :--- | :--- | :--- |
|
| :--- | :--- | :--- | :--- |
|
||||||
| success | Boolean | 請求是否處理成功 | true |
|
| success | Boolean | 請求是否處理成功 | true |
|
||||||
@@ -175,3 +194,67 @@ description: 本技能規範定義了 Star Cloud 系統中所有機台 (IoT) 與
|
|||||||
| t060v41 | String | 物料編碼 (Material Code) | SKU-001 |
|
| t060v41 | String | 物料編碼 (Material Code) | SKU-001 |
|
||||||
| spring_limit | Integer | **彈簧貨道上限** (建議使用此欄位) | 10 |
|
| spring_limit | Integer | **彈簧貨道上限** (建議使用此欄位) | 10 |
|
||||||
| track_limit | Integer | **履帶貨道上限** (建議使用此欄位) | 15 |
|
| track_limit | Integer | **履帶貨道上限** (建議使用此欄位) | 15 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3.6 B013: 機台故障與異常狀態上報 (Error/Status Report)
|
||||||
|
用於接收機台發出的即時硬體狀態代碼(如卡貨、門未關),並自動由雲端後端翻譯為易讀日誌。
|
||||||
|
|
||||||
|
- **URL**: POST /api/v1/app/machine/error/B013
|
||||||
|
- **Authentication**: Bearer Token (Header)
|
||||||
|
- **Request Body:**
|
||||||
|
|
||||||
|
| 參數 | 類型 | 必填 | 說明 | 範例 |
|
||||||
|
| :--- | :--- | :--- | :--- | :--- |
|
||||||
|
| tid | Integer | 否 | 涉及之貨道編號 (Slot No) | 12 |
|
||||||
|
| error_code | String | 是 | 硬體狀態代碼 (4 位 16 進位) | "0403" |
|
||||||
|
|
||||||
|
- **回應 (Success 202):**
|
||||||
|
|
||||||
|
| 參數 | 類型 | 說明 | 範例 |
|
||||||
|
| :--- | :--- | :--- | :--- |
|
||||||
|
| success | Boolean | 請求已接收 | true |
|
||||||
|
| code | Integer | 200 | 200 |
|
||||||
|
|
||||||
|
#### B013 硬體代碼對照表 (由 MachineService 自動翻譯)
|
||||||
|
|
||||||
|
| 代碼 | 英文 Key (i18n) | 級別 | 範例繁中翻譯 |
|
||||||
|
| :--- | :--- | :--- | :--- |
|
||||||
|
| **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 | 昇降系統故障 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3.7 B014: 機台參數與金鑰下載 (Config Download)
|
||||||
|
用於機台引導階段 (Provisioning),向雲端請求支付金鑰、發票設定及機台正式 API Token。
|
||||||
|
|
||||||
|
- **URL**: POST /api/v1/app/machine/setting/B014
|
||||||
|
- **Authentication**: **User Token** (Sanctum Header)
|
||||||
|
- **Request Body:**
|
||||||
|
|
||||||
|
| 參數 | 類型 | 必填 | 說明 | 範例 |
|
||||||
|
| :--- | :--- | :--- | :--- | :--- |
|
||||||
|
| machine | String | 是 | 機台編號 (serial_no) | M-001 |
|
||||||
|
|
||||||
|
- **Response Body (Success 200):**
|
||||||
|
|
||||||
|
| 欄位 (Key) | 說明 | 備註 |
|
||||||
|
| :--- | :--- | :--- |
|
||||||
|
| **t050v01** | 機台序號 | 即 machine_id |
|
||||||
|
| **api_token** | **機台正式 Token** | 初始化後應存於本地,後續 API 認證用 |
|
||||||
|
| **t050v41** | 玉山特店編號 | ESUN Merchant ID |
|
||||||
|
| **t050v42** | 玉山終端編號 | ESUN Terminal ID |
|
||||||
|
| **t050v43** | 玉山 Hash Key | ESUN Hash |
|
||||||
|
| **t050v34** | 發票特店 ID | Invoice Merchant ID |
|
||||||
|
| **t050v35** | 發票 Hash Key | Invoice Key |
|
||||||
|
| **t050v36** | 發票 Hash IV | Invoice IV |
|
||||||
|
| **TP_APP_ID** | 趨勢支付 AppID | TrendPay ID |
|
||||||
|
| **TP_APP_KEY** | 趨勢支付 Key | TrendPay Key |
|
||||||
|
|
||||||
|
> [!CAUTION]
|
||||||
|
> **安全性規範**:B014 會回傳敏感金鑰與正式 Token,背景必須強制進行 RBAC 校驗。只有當前登入的人員具備該機台管理權限時,後端才允許發放資料。
|
||||||
|
|||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -19,5 +19,6 @@ yarn-error.log
|
|||||||
/.vscode
|
/.vscode
|
||||||
/docs/API
|
/docs/API
|
||||||
/docs/*.xlsx
|
/docs/*.xlsx
|
||||||
|
/docs/pptx
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -21,7 +21,9 @@ class AdvertisementController extends AdminController
|
|||||||
|
|
||||||
// Tab 1: 廣告列表
|
// Tab 1: 廣告列表
|
||||||
$advertisements = Advertisement::with('company')->latest()->paginate(10);
|
$advertisements = Advertisement::with('company')->latest()->paginate(10);
|
||||||
$allAds = Advertisement::active()->get();
|
|
||||||
|
// Tab 2: 機台廣告設置 (所需資料) - 隱藏已過期的廣告
|
||||||
|
$allAds = Advertisement::playing()->get();
|
||||||
|
|
||||||
// Tab 2: 機台廣告設置 (所需資料)
|
// Tab 2: 機台廣告設置 (所需資料)
|
||||||
// 取得使用者有權限的機台列表 (已透過 Global Scope 過濾)
|
// 取得使用者有權限的機台列表 (已透過 Global Scope 過濾)
|
||||||
@@ -54,6 +56,8 @@ class AdvertisementController extends AdminController
|
|||||||
$request->type === 'image' ? 'max:10240' : 'max:51200', // Image 10MB, Video 50MB
|
$request->type === 'image' ? 'max:10240' : 'max:51200', // Image 10MB, Video 50MB
|
||||||
],
|
],
|
||||||
'company_id' => 'nullable|exists:companies,id',
|
'company_id' => 'nullable|exists:companies,id',
|
||||||
|
'start_at' => 'nullable|date',
|
||||||
|
'end_at' => 'nullable|date|after_or_equal:start_at',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$user = auth()->user();
|
$user = auth()->user();
|
||||||
@@ -71,13 +75,15 @@ class AdvertisementController extends AdminController
|
|||||||
$companyId = $user->company_id;
|
$companyId = $user->company_id;
|
||||||
}
|
}
|
||||||
|
|
||||||
Advertisement::create([
|
$advertisement = Advertisement::create([
|
||||||
'company_id' => $companyId,
|
'company_id' => $companyId,
|
||||||
'name' => $request->name,
|
'name' => $request->name,
|
||||||
'type' => $request->type,
|
'type' => $request->type,
|
||||||
'duration' => (int) $request->duration,
|
'duration' => (int) $request->duration,
|
||||||
'url' => Storage::disk('public')->url($path),
|
'url' => Storage::disk('public')->url($path),
|
||||||
'is_active' => true,
|
'is_active' => true,
|
||||||
|
'start_at' => $request->start_at,
|
||||||
|
'end_at' => $request->end_at,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if ($request->wantsJson()) {
|
if ($request->wantsJson()) {
|
||||||
@@ -99,6 +105,8 @@ class AdvertisementController extends AdminController
|
|||||||
'duration' => 'required|in:15,30,60',
|
'duration' => 'required|in:15,30,60',
|
||||||
'is_active' => 'boolean',
|
'is_active' => 'boolean',
|
||||||
'company_id' => 'nullable|exists:companies,id',
|
'company_id' => 'nullable|exists:companies,id',
|
||||||
|
'start_at' => 'nullable|date',
|
||||||
|
'end_at' => 'nullable|date|after_or_equal:start_at',
|
||||||
];
|
];
|
||||||
|
|
||||||
if ($request->hasFile('file')) {
|
if ($request->hasFile('file')) {
|
||||||
@@ -111,7 +119,7 @@ class AdvertisementController extends AdminController
|
|||||||
|
|
||||||
$request->validate($rules);
|
$request->validate($rules);
|
||||||
|
|
||||||
$data = $request->only(['name', 'type', 'duration']);
|
$data = $request->only(['name', 'type', 'duration', 'start_at', 'end_at']);
|
||||||
$data['is_active'] = $request->has('is_active');
|
$data['is_active'] = $request->has('is_active');
|
||||||
|
|
||||||
$user = auth()->user();
|
$user = auth()->user();
|
||||||
@@ -150,7 +158,7 @@ class AdvertisementController extends AdminController
|
|||||||
return redirect()->back()->with('success', __('Advertisement updated successfully.'));
|
return redirect()->back()->with('success', __('Advertisement updated successfully.'));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function destroy(Advertisement $advertisement)
|
public function destroy(Request $request, Advertisement $advertisement)
|
||||||
{
|
{
|
||||||
// 檢查是否有機台正投放中
|
// 檢查是否有機台正投放中
|
||||||
if ($advertisement->machineAdvertisements()->exists()) {
|
if ($advertisement->machineAdvertisements()->exists()) {
|
||||||
|
|||||||
@@ -14,7 +14,8 @@ class CompanyController extends Controller
|
|||||||
*/
|
*/
|
||||||
public function index(Request $request)
|
public function index(Request $request)
|
||||||
{
|
{
|
||||||
$query = Company::query()->withCount(['users', 'machines']);
|
$query = Company::query()->withCount(['users', 'machines'])
|
||||||
|
->with(['contracts.creator:id,name']);
|
||||||
|
|
||||||
// 搜尋
|
// 搜尋
|
||||||
if ($search = $request->input('search')) {
|
if ($search = $request->input('search')) {
|
||||||
@@ -55,6 +56,10 @@ class CompanyController extends Controller
|
|||||||
'contact_email' => 'nullable|email|max:255',
|
'contact_email' => 'nullable|email|max:255',
|
||||||
'start_date' => 'required|date',
|
'start_date' => 'required|date',
|
||||||
'end_date' => 'nullable|date',
|
'end_date' => 'nullable|date',
|
||||||
|
'warranty_start_date' => 'nullable|date',
|
||||||
|
'warranty_end_date' => 'nullable|date',
|
||||||
|
'software_start_date' => 'nullable|date',
|
||||||
|
'software_end_date' => 'nullable|date',
|
||||||
'status' => 'required|boolean',
|
'status' => 'required|boolean',
|
||||||
'note' => 'nullable|string',
|
'note' => 'nullable|string',
|
||||||
'settings' => 'nullable|array',
|
'settings' => 'nullable|array',
|
||||||
@@ -83,11 +88,28 @@ class CompanyController extends Controller
|
|||||||
'contact_email' => $validated['contact_email'] ?? null,
|
'contact_email' => $validated['contact_email'] ?? null,
|
||||||
'start_date' => $validated['start_date'] ?? null,
|
'start_date' => $validated['start_date'] ?? null,
|
||||||
'end_date' => $validated['end_date'] ?? null,
|
'end_date' => $validated['end_date'] ?? null,
|
||||||
|
'warranty_start_date' => $validated['warranty_start_date'] ?? null,
|
||||||
|
'warranty_end_date' => $validated['warranty_end_date'] ?? null,
|
||||||
|
'software_start_date' => $validated['software_start_date'] ?? null,
|
||||||
|
'software_end_date' => $validated['software_end_date'] ?? null,
|
||||||
'status' => $validated['status'],
|
'status' => $validated['status'],
|
||||||
'note' => $validated['note'] ?? null,
|
'note' => $validated['note'] ?? null,
|
||||||
'settings' => $validated['settings'] ?? [],
|
'settings' => $validated['settings'] ?? [],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
// 記錄合約歷程
|
||||||
|
$company->contracts()->create([
|
||||||
|
'type' => $company->original_type,
|
||||||
|
'start_date' => $company->start_date,
|
||||||
|
'end_date' => $company->end_date,
|
||||||
|
'warranty_start_date' => $company->warranty_start_date,
|
||||||
|
'warranty_end_date' => $company->warranty_end_date,
|
||||||
|
'software_start_date' => $company->software_start_date,
|
||||||
|
'software_end_date' => $company->software_end_date,
|
||||||
|
'note' => __('Initial contract registration'),
|
||||||
|
'creator_id' => auth()->id(),
|
||||||
|
]);
|
||||||
|
|
||||||
// 如果有填寫帳號資訊,則建立管理員帳號
|
// 如果有填寫帳號資訊,則建立管理員帳號
|
||||||
if (!empty($validated['admin_username']) && !empty($validated['admin_password'])) {
|
if (!empty($validated['admin_username']) && !empty($validated['admin_password'])) {
|
||||||
$user = \App\Models\System\User::create([
|
$user = \App\Models\System\User::create([
|
||||||
@@ -143,6 +165,10 @@ class CompanyController extends Controller
|
|||||||
'contact_email' => 'nullable|email|max:255',
|
'contact_email' => 'nullable|email|max:255',
|
||||||
'start_date' => 'required|date',
|
'start_date' => 'required|date',
|
||||||
'end_date' => 'nullable|date',
|
'end_date' => 'nullable|date',
|
||||||
|
'warranty_start_date' => 'nullable|date',
|
||||||
|
'warranty_end_date' => 'nullable|date',
|
||||||
|
'software_start_date' => 'nullable|date',
|
||||||
|
'software_end_date' => 'nullable|date',
|
||||||
'status' => 'required|boolean',
|
'status' => 'required|boolean',
|
||||||
'note' => 'nullable|string',
|
'note' => 'nullable|string',
|
||||||
'settings' => 'nullable|array',
|
'settings' => 'nullable|array',
|
||||||
@@ -154,7 +180,22 @@ class CompanyController extends Controller
|
|||||||
$validated['settings']['enable_points'] = filter_var($validated['settings']['enable_points'] ?? false, FILTER_VALIDATE_BOOLEAN);
|
$validated['settings']['enable_points'] = filter_var($validated['settings']['enable_points'] ?? false, FILTER_VALIDATE_BOOLEAN);
|
||||||
}
|
}
|
||||||
|
|
||||||
$company->update($validated);
|
DB::transaction(function () use ($validated, $company) {
|
||||||
|
$company->update($validated);
|
||||||
|
|
||||||
|
// 記錄合約歷程
|
||||||
|
$company->contracts()->create([
|
||||||
|
'type' => $company->current_type,
|
||||||
|
'start_date' => $company->start_date,
|
||||||
|
'end_date' => $company->end_date,
|
||||||
|
'warranty_start_date' => $company->warranty_start_date,
|
||||||
|
'warranty_end_date' => $company->warranty_end_date,
|
||||||
|
'software_start_date' => $company->software_start_date,
|
||||||
|
'software_end_date' => $company->software_end_date,
|
||||||
|
'note' => $validated['note'] ?? __('Contract information updated'),
|
||||||
|
'creator_id' => auth()->id(),
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
// 分支邏輯:若停用客戶,連帶停用其所有帳號
|
// 分支邏輯:若停用客戶,連帶停用其所有帳號
|
||||||
if ($validated['status'] == 0) {
|
if ($validated['status'] == 0) {
|
||||||
|
|||||||
@@ -71,17 +71,21 @@ class MachineController extends AdminController
|
|||||||
*/
|
*/
|
||||||
public function logsAjax(Request $request, Machine $machine)
|
public function logsAjax(Request $request, Machine $machine)
|
||||||
{
|
{
|
||||||
$per_page = $request->input('per_page', 10);
|
$per_page = $request->input('per_page', 20);
|
||||||
|
|
||||||
$startDate = $request->get('start_date', now()->format('Y-m-d'));
|
$startDate = $request->get('start_date');
|
||||||
$endDate = $request->get('end_date', now()->format('Y-m-d'));
|
$endDate = $request->get('end_date');
|
||||||
|
|
||||||
$logs = $machine->logs()
|
$logs = $machine->logs()
|
||||||
->when($request->level, function ($query, $level) {
|
->when($request->level, function ($query, $level) {
|
||||||
return $query->where('level', $level);
|
return $query->where('level', $level);
|
||||||
})
|
})
|
||||||
->whereDate('created_at', '>=', $startDate)
|
->when($startDate, function ($query, $start) {
|
||||||
->whereDate('created_at', '<=', $endDate)
|
return $query->where('created_at', '>=', str_replace('T', ' ', $start));
|
||||||
|
})
|
||||||
|
->when($endDate, function ($query, $end) {
|
||||||
|
return $query->where('created_at', '<=', str_replace('T', ' ', $end));
|
||||||
|
})
|
||||||
->when($request->type, function ($query, $type) {
|
->when($request->type, function ($query, $type) {
|
||||||
return $query->where('type', $type);
|
return $query->where('type', $type);
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -318,7 +318,11 @@ class PermissionController extends Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ($user->isSystemAdmin() && $request->filled('company_id')) {
|
if ($user->isSystemAdmin() && $request->filled('company_id')) {
|
||||||
$query->where('company_id', $request->company_id);
|
if ($request->company_id === 'system') {
|
||||||
|
$query->whereNull('company_id');
|
||||||
|
} else {
|
||||||
|
$query->where('company_id', $request->company_id);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$per_page = $request->input('per_page', 10);
|
$per_page = $request->input('per_page', 10);
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ use App\Models\System\User;
|
|||||||
use App\Models\Machine\Machine;
|
use App\Models\Machine\Machine;
|
||||||
use Illuminate\Support\Facades\Hash;
|
use Illuminate\Support\Facades\Hash;
|
||||||
use Illuminate\Support\Facades\Log;
|
use Illuminate\Support\Facades\Log;
|
||||||
|
use App\Jobs\Machine\ProcessStateLog;
|
||||||
|
|
||||||
class MachineAuthController extends Controller
|
class MachineAuthController extends Controller
|
||||||
{
|
{
|
||||||
@@ -20,30 +21,14 @@ class MachineAuthController extends Controller
|
|||||||
{
|
{
|
||||||
// 1. 驗證欄位 (相容舊版 Java App 發送的 JSON 格式)
|
// 1. 驗證欄位 (相容舊版 Java App 發送的 JSON 格式)
|
||||||
$validated = $request->validate([
|
$validated = $request->validate([
|
||||||
'machine' => 'required|string',
|
'machine' => 'required|string',
|
||||||
'Su_Account' => 'required|string',
|
'Su_Account' => 'required|string',
|
||||||
'Su_Password' => 'required|string',
|
'Su_Password' => 'required|string',
|
||||||
'ip' => 'nullable|string',
|
'ip' => 'nullable|string',
|
||||||
'type' => 'nullable|string',
|
'type' => 'nullable|string',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// 2. 透過帳號尋找使用者 (允許使用 username 或 email)
|
// 2. 取得機台物件 (需優先於帳密驗證,以便記錄日誌到正確機台)
|
||||||
$user = User::where('username', $validated['Su_Account'])
|
|
||||||
->orWhere('email', $validated['Su_Account'])
|
|
||||||
->first();
|
|
||||||
|
|
||||||
// 若無此帳號或密碼錯誤
|
|
||||||
if (!$user || !Hash::check($validated['Su_Password'], $user->password)) {
|
|
||||||
Log::warning("B000 機台登入失敗: 帳密錯誤", [
|
|
||||||
'account' => $validated['Su_Account'],
|
|
||||||
'machine' => $validated['machine']
|
|
||||||
]);
|
|
||||||
// 按現行設定,Java 端只認 Success 字串,其餘視為帳密錯誤
|
|
||||||
return response()->json(['message' => 'Failed']);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. 取得機台物件
|
|
||||||
// 因為此 API 無狀態 (沒有登入 session),為了避免被 global scope 擋住,直接取消所有 scope 來撈取
|
|
||||||
$machine = Machine::withoutGlobalScopes()->where('serial_no', $validated['machine'])->first();
|
$machine = Machine::withoutGlobalScopes()->where('serial_no', $validated['machine'])->first();
|
||||||
|
|
||||||
if (!$machine) {
|
if (!$machine) {
|
||||||
@@ -53,40 +38,82 @@ class MachineAuthController extends Controller
|
|||||||
return response()->json(['message' => 'Failed']);
|
return response()->json(['message' => 'Failed']);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. RBAC 權限驗證 (遵循多租戶與機台授權規範)
|
// 3. 透過帳號尋找使用者 (允許使用 username 或 email)
|
||||||
|
$user = User::where('username', $validated['Su_Account'])
|
||||||
|
->orWhere('email', $validated['Su_Account'])
|
||||||
|
->first();
|
||||||
|
|
||||||
|
// 4. 驗證密碼
|
||||||
|
if (!$user || !Hash::check($validated['Su_Password'], $user->password)) {
|
||||||
|
Log::warning("B000 機台登入失敗: 帳密錯誤", [
|
||||||
|
'account' => $validated['Su_Account'],
|
||||||
|
'machine' => $validated['machine']
|
||||||
|
]);
|
||||||
|
|
||||||
|
// 寫入機台日誌
|
||||||
|
ProcessStateLog::dispatch(
|
||||||
|
$machine->id,
|
||||||
|
$machine->company_id,
|
||||||
|
__("Login failed: :account", ['account' => $validated['Su_Account']]),
|
||||||
|
'warning',
|
||||||
|
[],
|
||||||
|
'login'
|
||||||
|
);
|
||||||
|
|
||||||
|
return response()->json(['message' => 'Failed']);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. RBAC 權限驗證 (遵循多租戶與機台授權規範)
|
||||||
|
$isAuthorized = false;
|
||||||
if ($user->isSystemAdmin()) {
|
if ($user->isSystemAdmin()) {
|
||||||
// [系統管理員] : 擁有最高權限,可登入平台下轄所有機台,直接放行
|
$isAuthorized = true;
|
||||||
|
|
||||||
} elseif ($user->is_admin) {
|
} elseif ($user->is_admin) {
|
||||||
// [公司管理員] : 不需要檢查 machine_user 表,但【必須驗證】該機台是否隸屬於他的公司
|
if ($machine->company_id === $user->company_id) {
|
||||||
if ($machine->company_id !== $user->company_id) {
|
$isAuthorized = true;
|
||||||
Log::warning("B000 機台登入失敗: 企圖越權登入其他公司的機台", [
|
|
||||||
'user_id' => $user->id,
|
|
||||||
'user_company' => $user->company_id,
|
|
||||||
'machine_company' => $machine->company_id
|
|
||||||
]);
|
|
||||||
return response()->json(['message' => 'Forbidden']);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
// [一般租戶帳號] : (包括補貨員等)必須嚴格檢查該帳號有沒有被分配到這台機器的 machine_user 關聯授權
|
if ($user->machines()->where('machine_id', $machine->id)->exists()) {
|
||||||
if (!$user->machines()->where('machine_id', $machine->id)->exists()) {
|
$isAuthorized = true;
|
||||||
Log::warning("B000 機台登入失敗: 該帳號沒有此機台的授權", [
|
|
||||||
'user_id' => $user->id,
|
|
||||||
'machine_id' => $machine->id
|
|
||||||
]);
|
|
||||||
return response()->json(['message' => 'Forbidden']);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 5. 驗證完美通過!回傳固定字串 Success 讓 Java 端放行
|
if (!$isAuthorized) {
|
||||||
|
Log::warning("B000 機台登入失敗: 權限不足", [
|
||||||
|
'user_id' => $user->id,
|
||||||
|
'machine_id' => $machine->id
|
||||||
|
]);
|
||||||
|
|
||||||
|
ProcessStateLog::dispatch(
|
||||||
|
$machine->id,
|
||||||
|
$machine->company_id,
|
||||||
|
__("Unauthorized login attempt: :account", ['account' => $user->username]),
|
||||||
|
'warning',
|
||||||
|
[],
|
||||||
|
'login'
|
||||||
|
);
|
||||||
|
|
||||||
|
return response()->json(['message' => 'Forbidden']);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 6. 驗證完美通過!
|
||||||
Log::info("B000 機台登入成功", [
|
Log::info("B000 機台登入成功", [
|
||||||
'account' => $user->username,
|
'account' => $user->username,
|
||||||
'machine' => $machine->serial_no
|
'machine' => $machine->serial_no
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
// 寫入成功登入日誌
|
||||||
|
ProcessStateLog::dispatch(
|
||||||
|
$machine->id,
|
||||||
|
$machine->company_id,
|
||||||
|
__("User logged in: :name", ['name' => $user->name ?? $user->username]),
|
||||||
|
'info',
|
||||||
|
[],
|
||||||
|
'login'
|
||||||
|
);
|
||||||
|
|
||||||
return response()->json([
|
return response()->json([
|
||||||
'message' => 'Success'
|
'message' => 'Success',
|
||||||
|
'token' => $user->createToken('technician-setup', ['*'], now()->addHours(8))->plainTextToken
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,8 +9,11 @@ use App\Models\System\User;
|
|||||||
use App\Jobs\Machine\ProcessHeartbeat;
|
use App\Jobs\Machine\ProcessHeartbeat;
|
||||||
use App\Jobs\Machine\ProcessTimerStatus;
|
use App\Jobs\Machine\ProcessTimerStatus;
|
||||||
use App\Jobs\Machine\ProcessCoinInventory;
|
use App\Jobs\Machine\ProcessCoinInventory;
|
||||||
|
use App\Jobs\Machine\ProcessMachineError;
|
||||||
|
use App\Jobs\Machine\ProcessStateLog;
|
||||||
use Illuminate\Support\Arr;
|
use Illuminate\Support\Arr;
|
||||||
use Illuminate\Support\Facades\Validator;
|
use Illuminate\Support\Facades\Validator;
|
||||||
|
use Illuminate\Support\Facades\Cache;
|
||||||
|
|
||||||
class MachineController extends Controller
|
class MachineController extends Controller
|
||||||
{
|
{
|
||||||
@@ -22,6 +25,69 @@ class MachineController extends Controller
|
|||||||
$machine = $request->get('machine');
|
$machine = $request->get('machine');
|
||||||
$data = $request->except(['machine', 'key']); // 排除 Middleware 注入的 Model 物件與認證 key
|
$data = $request->except(['machine', 'key']); // 排除 Middleware 注入的 Model 物件與認證 key
|
||||||
|
|
||||||
|
// === 狀態異動觸發 (Redis 快取免查 DB) ===
|
||||||
|
$cacheKey = "machine:{$machine->serial_no}:state";
|
||||||
|
$oldState = Cache::get($cacheKey);
|
||||||
|
|
||||||
|
$currentPage = $data['current_page'] ?? null;
|
||||||
|
$doorStatus = $data['door_status'] ?? null;
|
||||||
|
$firmwareVersion = $data['firmware_version'] ?? null;
|
||||||
|
$model = $data['model'] ?? null;
|
||||||
|
|
||||||
|
if ($currentPage !== null || $doorStatus !== null || $firmwareVersion !== null || $model !== null) {
|
||||||
|
// 更新目前狀態到 Redis (保存 1 天)
|
||||||
|
$newState = $oldState ?? [];
|
||||||
|
if ($currentPage !== null) $newState['current_page'] = $currentPage;
|
||||||
|
if ($doorStatus !== null) $newState['door_status'] = $doorStatus;
|
||||||
|
if ($firmwareVersion !== null) $newState['firmware_version'] = $firmwareVersion;
|
||||||
|
if ($model !== null) $newState['model'] = $model;
|
||||||
|
|
||||||
|
Cache::put($cacheKey, $newState, 86400);
|
||||||
|
|
||||||
|
// 若有歷史紀錄才進行比對 (避開 Cache Miss 造成的雪崩)
|
||||||
|
if ($oldState !== null) {
|
||||||
|
// 1. 判斷頁面是否變更
|
||||||
|
if ($currentPage !== null && (string)$currentPage !== (string)($oldState['current_page'] ?? '')) {
|
||||||
|
// 只記錄「絕對狀態」,配合 lang 中 "Page X" 的翻譯
|
||||||
|
ProcessStateLog::dispatch($machine->id, $machine->company_id, "Page {$currentPage}", 'info');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 判斷門禁是否變更 (0: 關閉, 1: 開啟)
|
||||||
|
if ($doorStatus !== null && (string)$doorStatus !== (string)($oldState['door_status'] ?? '')) {
|
||||||
|
$doorMessage = $doorStatus == 1 ? "Door Opened" : "Door Closed";
|
||||||
|
$doorLevel = 'info'; // 不論開關門皆為 info,避免觸發異常狀態
|
||||||
|
ProcessStateLog::dispatch($machine->id, $machine->company_id, $doorMessage, $doorLevel);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 判斷韌體版本是否變更
|
||||||
|
if ($firmwareVersion !== null && (string)$firmwareVersion !== (string)($oldState['firmware_version'] ?? '')) {
|
||||||
|
$oldVersion = $oldState['firmware_version'] ?? 'Unknown';
|
||||||
|
// 直接在 Controller 進行翻譯並填值,確保儲存到 DB 的是最終正確字串
|
||||||
|
$versionMessage = __("Firmware updated to :version", ['version' => $firmwareVersion]);
|
||||||
|
ProcessStateLog::dispatch(
|
||||||
|
$machine->id,
|
||||||
|
$machine->company_id,
|
||||||
|
$versionMessage,
|
||||||
|
'info',
|
||||||
|
['old' => $oldVersion, 'new' => $firmwareVersion]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. 判斷型號是否變更
|
||||||
|
if ($model !== null && (string)$model !== (string)($oldState['model'] ?? '')) {
|
||||||
|
$oldModel = $oldState['model'] ?? 'Unknown';
|
||||||
|
$modelMessage = __("Model changed to :model", ['model' => $model]);
|
||||||
|
ProcessStateLog::dispatch(
|
||||||
|
$machine->id,
|
||||||
|
$machine->company_id,
|
||||||
|
$modelMessage,
|
||||||
|
'info',
|
||||||
|
['old' => $oldModel, 'new' => $model]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 異步處理狀態更新
|
// 異步處理狀態更新
|
||||||
ProcessHeartbeat::dispatch($machine->serial_no, $data);
|
ProcessHeartbeat::dispatch($machine->serial_no, $data);
|
||||||
|
|
||||||
@@ -207,7 +273,7 @@ class MachineController extends Controller
|
|||||||
$advertisements = \App\Models\Machine\MachineAdvertisement::where('machine_id', $machine->id)
|
$advertisements = \App\Models\Machine\MachineAdvertisement::where('machine_id', $machine->id)
|
||||||
->with([
|
->with([
|
||||||
'advertisement' => function ($query) {
|
'advertisement' => function ($query) {
|
||||||
$query->active();
|
$query->playing();
|
||||||
}
|
}
|
||||||
])
|
])
|
||||||
->get()
|
->get()
|
||||||
@@ -372,4 +438,104 @@ class MachineController extends Controller
|
|||||||
'data' => $products
|
'data' => $products
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* B013: Report Machine Hardware Error/Status (Asynchronous)
|
||||||
|
*/
|
||||||
|
public function reportError(Request $request)
|
||||||
|
{
|
||||||
|
$machine = $request->get('machine');
|
||||||
|
$data = $request->only(['tid', 'error_code']);
|
||||||
|
|
||||||
|
// 異步分派處理 (Dispatch to queue)
|
||||||
|
ProcessMachineError::dispatch($machine->serial_no, $data);
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'success' => true,
|
||||||
|
'code' => 200,
|
||||||
|
'message' => 'Error report accepted',
|
||||||
|
], 202); // 202 Accepted
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* B014: Download Machine Settings & Config (Synchronous, Requires User Auth)
|
||||||
|
* 用於機台引導階段,同步金流、發票與機台專屬 API Token。
|
||||||
|
*/
|
||||||
|
public function getSettings(Request $request)
|
||||||
|
{
|
||||||
|
$serialNo = $request->input('machine');
|
||||||
|
$user = $request->user();
|
||||||
|
|
||||||
|
// 1. 查找機台 (忽略全局範圍以進行認領)
|
||||||
|
$machine = Machine::withoutGlobalScopes()
|
||||||
|
->with(['paymentConfig', 'company'])
|
||||||
|
->where('serial_no', $serialNo)
|
||||||
|
->first();
|
||||||
|
|
||||||
|
if (!$machine) {
|
||||||
|
return response()->json([
|
||||||
|
'success' => false,
|
||||||
|
'code' => 404,
|
||||||
|
'message' => 'Machine not found'
|
||||||
|
], 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 權限加強驗證 (RBAC)
|
||||||
|
$isAuthorized = false;
|
||||||
|
if ($user->isSystemAdmin()) {
|
||||||
|
$isAuthorized = true;
|
||||||
|
} elseif ($machine->company_id === $user->company_id) {
|
||||||
|
// 公司管理員或已授權員工才能存取
|
||||||
|
if ($user->is_admin || $user->machines()->where('machine_id', $machine->id)->exists()) {
|
||||||
|
$isAuthorized = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$isAuthorized) {
|
||||||
|
return response()->json([
|
||||||
|
'success' => false,
|
||||||
|
'code' => 403,
|
||||||
|
'message' => 'Forbidden: You do not have permission to configure this machine'
|
||||||
|
], 403);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 獲取關聯設定
|
||||||
|
$paymentSettings = $machine->paymentConfig->settings ?? [];
|
||||||
|
$companySettings = $machine->company->settings ?? [];
|
||||||
|
|
||||||
|
// 4. 映射 App 預期欄位 (嚴格遵守 HttpAPI.java 結構)
|
||||||
|
$data = [
|
||||||
|
't050v01' => $machine->serial_no,
|
||||||
|
'api_token' => $machine->api_token, // 向 App 核發正式通訊 Token
|
||||||
|
|
||||||
|
// 玉山支付
|
||||||
|
't050v41' => $paymentSettings['esun_store_id'] ?? '',
|
||||||
|
't050v42' => $paymentSettings['esun_term_id'] ?? '',
|
||||||
|
't050v43' => $paymentSettings['esun_hash'] ?? '',
|
||||||
|
|
||||||
|
// 電子發票 (綠界)
|
||||||
|
't050v34' => $companySettings['invoice_merchant_id'] ?? '',
|
||||||
|
't050v35' => $companySettings['invoice_hash_key'] ?? '',
|
||||||
|
't050v36' => $companySettings['invoice_hash_iv'] ?? '',
|
||||||
|
't050v38' => $companySettings['invoice_email'] ?? '',
|
||||||
|
|
||||||
|
// 趨勢支付 (TrendPay/Greenpay)
|
||||||
|
'TP_APP_ID' => $paymentSettings['tp_app_id'] ?? '',
|
||||||
|
'TP_APP_KEY' => $paymentSettings['tp_app_key'] ?? '',
|
||||||
|
'TP_PARTNER_KEY' => $paymentSettings['tp_partner_key'] ?? '',
|
||||||
|
|
||||||
|
// 各類行動支付特店 ID
|
||||||
|
'TP_LINE_MERCHANT_ID' => $paymentSettings['tp_line_merchant_id'] ?? '',
|
||||||
|
'TP_PS_MERCHANT_ID' => $paymentSettings['tp_ps_merchant_id'] ?? '',
|
||||||
|
'TP_EASY_MERCHANT_ID' => $paymentSettings['tp_easy_merchant_id'] ?? '',
|
||||||
|
'TP_PI_MERCHANT_ID' => $paymentSettings['tp_pi_merchant_id'] ?? '',
|
||||||
|
'TP_JKO_MERCHANT_ID' => $paymentSettings['tp_jko_merchant_id'] ?? '',
|
||||||
|
];
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'success' => true,
|
||||||
|
'code' => 200,
|
||||||
|
'data' => [$data] // App 預期的是包含單一物件的陣列
|
||||||
|
]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
40
app/Jobs/Machine/ProcessMachineError.php
Normal file
40
app/Jobs/Machine/ProcessMachineError.php
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Jobs\Machine;
|
||||||
|
|
||||||
|
use App\Models\Machine\Machine;
|
||||||
|
use App\Services\Machine\MachineService;
|
||||||
|
use Illuminate\Bus\Queueable;
|
||||||
|
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||||
|
use Illuminate\Foundation\Bus\Dispatchable;
|
||||||
|
use Illuminate\Queue\InteractsWithQueue;
|
||||||
|
use Illuminate\Queue\SerializesModels;
|
||||||
|
|
||||||
|
class ProcessMachineError implements ShouldQueue
|
||||||
|
{
|
||||||
|
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||||
|
|
||||||
|
protected $serialNo;
|
||||||
|
protected $data;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new job instance.
|
||||||
|
*/
|
||||||
|
public function __construct(string $serialNo, array $data)
|
||||||
|
{
|
||||||
|
$this->serialNo = $serialNo;
|
||||||
|
$this->data = $data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute the job.
|
||||||
|
*/
|
||||||
|
public function handle(MachineService $service): void
|
||||||
|
{
|
||||||
|
$machine = Machine::where('serial_no', $this->serialNo)->first();
|
||||||
|
|
||||||
|
if ($machine) {
|
||||||
|
$service->recordErrorLog($machine, $this->data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
56
app/Jobs/Machine/ProcessStateLog.php
Normal file
56
app/Jobs/Machine/ProcessStateLog.php
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Jobs\Machine;
|
||||||
|
|
||||||
|
use App\Models\Machine\MachineLog;
|
||||||
|
use Illuminate\Bus\Queueable;
|
||||||
|
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||||
|
use Illuminate\Foundation\Bus\Dispatchable;
|
||||||
|
use Illuminate\Queue\InteractsWithQueue;
|
||||||
|
use Illuminate\Queue\SerializesModels;
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
|
|
||||||
|
class ProcessStateLog implements ShouldQueue
|
||||||
|
{
|
||||||
|
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||||
|
|
||||||
|
protected $machineId;
|
||||||
|
protected $companyId;
|
||||||
|
protected $message;
|
||||||
|
protected $level;
|
||||||
|
protected $type;
|
||||||
|
protected $context;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new job instance.
|
||||||
|
*/
|
||||||
|
public function __construct(int $machineId, ?int $companyId, string $message, string $level = 'info', array $context = [], string $type = 'status')
|
||||||
|
{
|
||||||
|
$this->machineId = $machineId;
|
||||||
|
$this->companyId = $companyId;
|
||||||
|
$this->message = $message;
|
||||||
|
$this->level = $level;
|
||||||
|
$this->context = $context;
|
||||||
|
$this->type = $type;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute the job.
|
||||||
|
*/
|
||||||
|
public function handle(): void
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
MachineLog::create([
|
||||||
|
'machine_id' => $this->machineId,
|
||||||
|
'company_id' => $this->companyId,
|
||||||
|
'type' => $this->type,
|
||||||
|
'level' => $this->level,
|
||||||
|
'message' => $this->message,
|
||||||
|
'context' => $this->context,
|
||||||
|
]);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
Log::error("Failed to create state log for machine {$this->machineId}: " . $e->getMessage());
|
||||||
|
throw $e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -24,6 +24,33 @@ class MachineLog extends Model
|
|||||||
'context' => 'array',
|
'context' => 'array',
|
||||||
];
|
];
|
||||||
|
|
||||||
|
protected $appends = [
|
||||||
|
'translated_message',
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 動態重組翻譯後的訊息
|
||||||
|
*/
|
||||||
|
public function getTranslatedMessageAttribute(): string
|
||||||
|
{
|
||||||
|
$context = $this->context;
|
||||||
|
|
||||||
|
// 若 context 中已有翻譯標籤 (B013 封裝),則進行動態重組
|
||||||
|
if (isset($context['translated_label'])) {
|
||||||
|
$label = __($context['translated_label']);
|
||||||
|
$tid = $context['tid'] ?? null;
|
||||||
|
$code = $context['raw_code'] ?? '0000';
|
||||||
|
|
||||||
|
if ($tid) {
|
||||||
|
return __('Slot') . " {$tid}: {$label} (Code: {$code})";
|
||||||
|
}
|
||||||
|
return "{$label} (Code: {$code})";
|
||||||
|
}
|
||||||
|
|
||||||
|
// 預設退回原始 message (支援歷史資料的翻譯判定與佔位符替換)
|
||||||
|
return __($this->message, $context ?? []);
|
||||||
|
}
|
||||||
|
|
||||||
public function machine()
|
public function machine()
|
||||||
{
|
{
|
||||||
return $this->belongsTo(Machine::class);
|
return $this->belongsTo(Machine::class);
|
||||||
|
|||||||
@@ -18,11 +18,15 @@ class Advertisement extends Model
|
|||||||
'duration',
|
'duration',
|
||||||
'url',
|
'url',
|
||||||
'is_active',
|
'is_active',
|
||||||
|
'start_at',
|
||||||
|
'end_at',
|
||||||
];
|
];
|
||||||
|
|
||||||
protected $casts = [
|
protected $casts = [
|
||||||
'duration' => 'integer',
|
'duration' => 'integer',
|
||||||
'is_active' => 'boolean',
|
'is_active' => 'boolean',
|
||||||
|
'start_at' => 'datetime',
|
||||||
|
'end_at' => 'datetime',
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -48,4 +52,21 @@ class Advertisement extends Model
|
|||||||
{
|
{
|
||||||
return $query->where('is_active', true);
|
return $query->where('is_active', true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scope a query to only include advertisements that should be playing now.
|
||||||
|
*/
|
||||||
|
public function scopePlaying($query)
|
||||||
|
{
|
||||||
|
$now = now();
|
||||||
|
return $query->where('is_active', true)
|
||||||
|
->where(function ($q) use ($now) {
|
||||||
|
$q->whereNull('start_at')
|
||||||
|
->orWhere('start_at', '<=', $now);
|
||||||
|
})
|
||||||
|
->where(function ($q) use ($now) {
|
||||||
|
$q->whereNull('end_at')
|
||||||
|
->orWhere('end_at', '>=', $now);
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,17 +25,33 @@ class Company extends Model
|
|||||||
'status',
|
'status',
|
||||||
'start_date',
|
'start_date',
|
||||||
'end_date',
|
'end_date',
|
||||||
|
'warranty_start_date',
|
||||||
|
'warranty_end_date',
|
||||||
|
'software_start_date',
|
||||||
|
'software_end_date',
|
||||||
'note',
|
'note',
|
||||||
'settings',
|
'settings',
|
||||||
];
|
];
|
||||||
|
|
||||||
protected $casts = [
|
protected $casts = [
|
||||||
'start_date' => 'date',
|
'start_date' => 'date:Y-m-d',
|
||||||
'end_date' => 'date',
|
'end_date' => 'date:Y-m-d',
|
||||||
|
'warranty_start_date' => 'date:Y-m-d',
|
||||||
|
'warranty_end_date' => 'date:Y-m-d',
|
||||||
|
'software_start_date' => 'date:Y-m-d',
|
||||||
|
'software_end_date' => 'date:Y-m-d',
|
||||||
'status' => 'integer',
|
'status' => 'integer',
|
||||||
'settings' => 'array',
|
'settings' => 'array',
|
||||||
];
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the contract history for the company.
|
||||||
|
*/
|
||||||
|
public function contracts(): HasMany
|
||||||
|
{
|
||||||
|
return $this->hasMany(CompanyContract::class)->latest();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the users for the company.
|
* Get the users for the company.
|
||||||
*/
|
*/
|
||||||
|
|||||||
50
app/Models/System/CompanyContract.php
Normal file
50
app/Models/System/CompanyContract.php
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models\System;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
|
||||||
|
class CompanyContract extends Model
|
||||||
|
{
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'company_id',
|
||||||
|
'type',
|
||||||
|
'start_date',
|
||||||
|
'end_date',
|
||||||
|
'warranty_start_date',
|
||||||
|
'warranty_end_date',
|
||||||
|
'software_start_date',
|
||||||
|
'software_end_date',
|
||||||
|
'note',
|
||||||
|
'creator_id',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected $casts = [
|
||||||
|
'start_date' => 'date:Y-m-d',
|
||||||
|
'end_date' => 'date:Y-m-d',
|
||||||
|
'warranty_start_date' => 'date:Y-m-d',
|
||||||
|
'warranty_end_date' => 'date:Y-m-d',
|
||||||
|
'software_start_date' => 'date:Y-m-d',
|
||||||
|
'software_end_date' => 'date:Y-m-d',
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the company that owns the contract.
|
||||||
|
*/
|
||||||
|
public function company(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Company::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the user who created the record.
|
||||||
|
*/
|
||||||
|
public function creator(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(User::class, 'creator_id');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,6 +10,56 @@ use Carbon\Carbon;
|
|||||||
|
|
||||||
class MachineService
|
class MachineService
|
||||||
{
|
{
|
||||||
|
/**
|
||||||
|
* B013: 硬體狀態代碼對照表 (Hardware Status Code Mapping)
|
||||||
|
*/
|
||||||
|
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'],
|
||||||
|
|
||||||
|
// 貨道狀態類 (Prefix: 02 - SLOT_STATUS)
|
||||||
|
'0201' => ['label' => 'Slot normal', 'level' => 'info'],
|
||||||
|
'0202' => ['label' => 'Product empty', 'level' => 'warning'],
|
||||||
|
'0203' => ['label' => 'Slot empty', 'level' => 'warning'],
|
||||||
|
'0206' => ['label' => 'Slot not closed', 'level' => 'warning'],
|
||||||
|
'0207' => ['label' => 'Slot motor error (0207)', 'level' => 'error'],
|
||||||
|
'0208' => ['label' => 'Slot motor error (0208)', 'level' => 'error'],
|
||||||
|
'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'],
|
||||||
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update machine heartbeat and status.
|
* Update machine heartbeat and status.
|
||||||
*
|
*
|
||||||
@@ -178,6 +228,36 @@ class MachineService
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* B013: Record machine hardware error/status log with auto-translation.
|
||||||
|
*
|
||||||
|
* @param Machine $machine
|
||||||
|
* @param array $data
|
||||||
|
* @return MachineLog
|
||||||
|
*/
|
||||||
|
public function recordErrorLog(Machine $machine, array $data): MachineLog
|
||||||
|
{
|
||||||
|
$errorCode = $data['error_code'] ?? '0000';
|
||||||
|
$mapping = self::ERROR_CODE_MAP[$errorCode] ?? ['label' => 'Unknown Status', 'level' => 'error'];
|
||||||
|
|
||||||
|
$slotNo = $data['tid'] ?? null;
|
||||||
|
$label = $mapping['label'];
|
||||||
|
|
||||||
|
// 儲存原始英文格式作為 DB 備用,前端顯示會優先使用 model accessor 的動態翻譯內容
|
||||||
|
$message = $slotNo ? "Slot {$slotNo}: {$label} (Code: {$errorCode})" : "{$label} (Code: {$errorCode})";
|
||||||
|
|
||||||
|
return $machine->logs()->create([
|
||||||
|
'company_id' => $machine->company_id,
|
||||||
|
'type' => 'submachine',
|
||||||
|
'level' => $mapping['level'],
|
||||||
|
'message' => $message,
|
||||||
|
'context' => array_merge($data, [
|
||||||
|
'translated_label' => $label,
|
||||||
|
'raw_code' => $errorCode
|
||||||
|
]),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update machine slot stock (single slot).
|
* Update machine slot stock (single slot).
|
||||||
* Legacy support for recordLog (Existing code).
|
* Legacy support for recordLog (Existing code).
|
||||||
|
|||||||
@@ -8,6 +8,115 @@ return [
|
|||||||
[
|
[
|
||||||
'name' => '機台核心通訊 (IoT Core)',
|
'name' => '機台核心通訊 (IoT Core)',
|
||||||
'apis' => [
|
'apis' => [
|
||||||
|
[
|
||||||
|
'name' => 'B000: 維運人員登入認證 (Technician Login)',
|
||||||
|
'slug' => 'b000-tech-login',
|
||||||
|
'method' => 'POST',
|
||||||
|
'path' => '/api/v1/app/admin/login/B000',
|
||||||
|
'description' => '機台啟動引導的第一步。維運人員輸入個人帳密與機台編號進行認證,成功後核發臨時 Sanctum Token 供後續 B014 下載敏感設定使用。',
|
||||||
|
'headers' => [
|
||||||
|
'Content-Type' => 'application/json',
|
||||||
|
],
|
||||||
|
'parameters' => [
|
||||||
|
'username' => [
|
||||||
|
'type' => 'string',
|
||||||
|
'required' => true,
|
||||||
|
'description' => '維運人員帳號',
|
||||||
|
'example' => 'admin_test'
|
||||||
|
],
|
||||||
|
'password' => [
|
||||||
|
'type' => 'string',
|
||||||
|
'required' => true,
|
||||||
|
'description' => '維運人員密碼',
|
||||||
|
'example' => 'password123'
|
||||||
|
],
|
||||||
|
'machine' => [
|
||||||
|
'type' => 'string',
|
||||||
|
'required' => true,
|
||||||
|
'description' => '機台序號 (Serial No)',
|
||||||
|
'example' => 'SN202604130001'
|
||||||
|
],
|
||||||
|
],
|
||||||
|
'response_parameters' => [
|
||||||
|
'message' => [
|
||||||
|
'type' => 'string',
|
||||||
|
'description' => '回應訊息',
|
||||||
|
'example' => 'Success'
|
||||||
|
],
|
||||||
|
'token' => [
|
||||||
|
'type' => 'string',
|
||||||
|
'description' => '臨時身份認證 Token (Sanctum)',
|
||||||
|
'example' => '1|abcdefg...'
|
||||||
|
],
|
||||||
|
],
|
||||||
|
'request' => [
|
||||||
|
'username' => 'admin_test',
|
||||||
|
'password' => 'password123',
|
||||||
|
'machine' => 'SN202604130001'
|
||||||
|
],
|
||||||
|
'response' => [
|
||||||
|
'message' => 'Success',
|
||||||
|
'token' => '1|abcdefg...'
|
||||||
|
],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'name' => 'B014: 機台參數與金鑰下載 (Config Download)',
|
||||||
|
'slug' => 'b014-config-download',
|
||||||
|
'method' => 'POST',
|
||||||
|
'path' => '/api/v1/app/machine/setting/B014',
|
||||||
|
'description' => '機台引導階段的第二步。在人員登入後,透過此介面下載金流金鑰、電子發票設定與機台專屬通訊 Token。',
|
||||||
|
'headers' => [
|
||||||
|
'Authorization' => 'Bearer <user_token>',
|
||||||
|
'Content-Type' => 'application/json',
|
||||||
|
],
|
||||||
|
'parameters' => [
|
||||||
|
'machine' => [
|
||||||
|
'type' => 'string',
|
||||||
|
'required' => true,
|
||||||
|
'description' => '機台序號',
|
||||||
|
'example' => 'SN202604130001'
|
||||||
|
],
|
||||||
|
],
|
||||||
|
'response_parameters' => [
|
||||||
|
'success' => [
|
||||||
|
'type' => 'boolean',
|
||||||
|
'description' => '是否成功',
|
||||||
|
'example' => true
|
||||||
|
],
|
||||||
|
'data' => [
|
||||||
|
'type' => 'array',
|
||||||
|
'description' => '配置物件陣列。包含:t050v01 (序號), api_token (通訊 Token), t050v41~43 (玉山設定), t050v34~38 (發票設定), TP_... (趨勢/手機支付設定)',
|
||||||
|
'example' => [
|
||||||
|
[
|
||||||
|
't050v01' => 'SN202604130001',
|
||||||
|
'api_token' => 'mac_token_...',
|
||||||
|
't050v41' => '80812345',
|
||||||
|
't050v34' => '2000132',
|
||||||
|
'TP_APP_ID' => 'GP_001'
|
||||||
|
]
|
||||||
|
]
|
||||||
|
],
|
||||||
|
],
|
||||||
|
'request' => [
|
||||||
|
'machine' => 'SN202604130001'
|
||||||
|
],
|
||||||
|
'response' => [
|
||||||
|
'success' => true,
|
||||||
|
'code' => 200,
|
||||||
|
'data' => [
|
||||||
|
[
|
||||||
|
't050v01' => 'SN202604130001',
|
||||||
|
'api_token' => 'mac_token_...',
|
||||||
|
't050v41' => '80812345',
|
||||||
|
't050v42' => '9001',
|
||||||
|
't050v43' => 'hash_key',
|
||||||
|
't050v34' => '2000132',
|
||||||
|
'TP_APP_ID' => 'GP_001'
|
||||||
|
]
|
||||||
|
]
|
||||||
|
],
|
||||||
|
'notes' => '此 API 受 auth:sanctum 保護,必須在 Header 帶上從 B000 取得的 Token。'
|
||||||
|
],
|
||||||
[
|
[
|
||||||
'name' => 'B005: 廣告清單同步 (Ad Sync)',
|
'name' => 'B005: 廣告清單同步 (Ad Sync)',
|
||||||
'slug' => 'b005-ad-sync',
|
'slug' => 'b005-ad-sync',
|
||||||
@@ -302,6 +411,54 @@ return [
|
|||||||
]
|
]
|
||||||
],
|
],
|
||||||
'notes' => '運作邏輯 (Client-side Logic): GET 執行全量同步,App 應於收到成功回應後,先執行 deleteAll() 再進行 insertAll()。PATCH 執行增量更新,App 僅對記憶體中的既存商品進行欄位值覆蓋 (Patching)。'
|
'notes' => '運作邏輯 (Client-side Logic): GET 執行全量同步,App 應於收到成功回應後,先執行 deleteAll() 再進行 insertAll()。PATCH 執行增量更新,App 僅對記憶體中的既存商品進行欄位值覆蓋 (Patching)。'
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'name' => 'B013: 機台故障與異常狀態上報 (Error/Status Report)',
|
||||||
|
'slug' => 'b013-error-report',
|
||||||
|
'method' => 'POST',
|
||||||
|
'path' => '/api/v1/app/machine/error/B013',
|
||||||
|
'description' => '用於接收機台發出的即時硬體狀態代碼(如卡貨、門未關)。身份由 Bearer Token 識別,回傳成功代表伺服器已將任務列入異步隊列處理。',
|
||||||
|
'headers' => [
|
||||||
|
'Authorization' => 'Bearer <api_token>',
|
||||||
|
'Content-Type' => 'application/json',
|
||||||
|
],
|
||||||
|
'parameters' => [
|
||||||
|
'tid' => [
|
||||||
|
'type' => 'integer',
|
||||||
|
'required' => false,
|
||||||
|
'description' => '涉及到之具體貨道編號 (Slot No)',
|
||||||
|
'example' => 12
|
||||||
|
],
|
||||||
|
'error_code' => [
|
||||||
|
'type' => 'string',
|
||||||
|
'required' => true,
|
||||||
|
'description' => '硬體狀態代碼 (4 位 16 進位字串)',
|
||||||
|
'example' => '0403'
|
||||||
|
],
|
||||||
|
],
|
||||||
|
'response_parameters' => [
|
||||||
|
'success' => [
|
||||||
|
'type' => 'boolean',
|
||||||
|
'description' => '請求是否已成功接收',
|
||||||
|
'example' => true
|
||||||
|
],
|
||||||
|
'code' => [
|
||||||
|
'type' => 'integer',
|
||||||
|
'description' => '內部業務狀態碼',
|
||||||
|
'example' => 200
|
||||||
|
],
|
||||||
|
],
|
||||||
|
'request' => [
|
||||||
|
'tid' => 12,
|
||||||
|
'error_code' => '0403',
|
||||||
|
],
|
||||||
|
'response' => [
|
||||||
|
'success' => true,
|
||||||
|
'code' => 200,
|
||||||
|
'message' => 'Error report accepted'
|
||||||
|
],
|
||||||
|
'notes' => '硬體代碼對照表見後端 MachineService::ERROR_CODE_MAP 定義。
|
||||||
|
0402: 出貨成功, 0403: 貨道卡貨, 0202: 貨道缺貨, 0415: 取貨門異常...等。'
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::table('companies', function (Blueprint $table) {
|
||||||
|
$table->date('warranty_start_date')->nullable()->after('end_date')->comment('保固起始日');
|
||||||
|
$table->date('warranty_end_date')->nullable()->after('warranty_start_date')->comment('保固結束日');
|
||||||
|
$table->date('software_start_date')->nullable()->after('warranty_end_date')->comment('軟體服務起始日');
|
||||||
|
$table->date('software_end_date')->nullable()->after('software_start_date')->comment('軟體服務結束日');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('companies', function (Blueprint $table) {
|
||||||
|
$table->dropColumn([
|
||||||
|
'warranty_start_date',
|
||||||
|
'warranty_end_date',
|
||||||
|
'software_start_date',
|
||||||
|
'software_end_date'
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::create('company_contracts', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->foreignId('company_id')->constrained()->onDelete('cascade');
|
||||||
|
$table->string('type')->comment('buyout, lease');
|
||||||
|
$table->date('start_date')->nullable();
|
||||||
|
$table->date('end_date')->nullable();
|
||||||
|
$table->date('warranty_start_date')->nullable();
|
||||||
|
$table->date('warranty_end_date')->nullable();
|
||||||
|
$table->date('software_start_date')->nullable();
|
||||||
|
$table->date('software_end_date')->nullable();
|
||||||
|
$table->text('note')->nullable();
|
||||||
|
$table->foreignId('creator_id')->nullable()->constrained('users');
|
||||||
|
$table->timestamps();
|
||||||
|
|
||||||
|
$table->index(['company_id', 'created_at']);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('company_contracts');
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::table('advertisements', function (Blueprint $table) {
|
||||||
|
$table->dateTime('start_at')->nullable()->after('url')->comment('發布時間');
|
||||||
|
$table->dateTime('end_at')->nullable()->after('start_at')->comment('下架時間');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('advertisements', function (Blueprint $table) {
|
||||||
|
$table->dropColumn(['start_at', 'end_at']);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -4,6 +4,67 @@
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## 🔐 B000: 維運人員登入認證 (Technician Login)
|
||||||
|
機台引導階段 (Provisioning) 的第一步,用於核發臨時身份 Token 以便後續下載敏感設定。
|
||||||
|
|
||||||
|
### 1. API 資訊
|
||||||
|
- **Endpoint**: `POST /api/v1/app/admin/login/B000`
|
||||||
|
- **認證方式**: 無 (需傳入 `username`, `password`, `machine`)
|
||||||
|
- **回應內容**: `token` (Sanctum Token)
|
||||||
|
|
||||||
|
### 2. 回應範例
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"message": "Success",
|
||||||
|
"token": "3|abcdef1234567890..."
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔑 B014: 機台參數與金鑰下載 (Config Download)
|
||||||
|
下載機台運作所需的支付金鑰、電子發票設定與正式通訊 Token。
|
||||||
|
|
||||||
|
### 1. API 資訊
|
||||||
|
- **Endpoint**: `POST /api/v1/app/machine/setting/B014`
|
||||||
|
- **認證方式**: **Bearer Token** (需帶上 B000 取得的 Token)
|
||||||
|
- **Header**: `Authorization: Bearer {token}`
|
||||||
|
|
||||||
|
### 2. 請求參數
|
||||||
|
- `machine`: 機台序號 (Serial No)
|
||||||
|
|
||||||
|
### 3. 回應規格 (欄位映射)
|
||||||
|
| 欄位 | 說明 | 來源範例 |
|
||||||
|
| :--- | :--- | :--- |
|
||||||
|
| `t050v01` | 機台序號 | `SN2026041301` |
|
||||||
|
| `api_token` | **機台正式 Token** | 後續 B010/B600 認證用 |
|
||||||
|
| `t050v41` | 玉山特店編號 | `ESUN_STORE_ID` |
|
||||||
|
| `t050v43` | 玉山 Hash Key | `ESUN_HASH` |
|
||||||
|
| `t050v34` | 發票特店 ID | `INV_MID` |
|
||||||
|
| `TP_APP_ID` | 趨勢支付 AppID | `TP_APP_ID` |
|
||||||
|
|
||||||
|
### 4. 回應範例 (JSON)
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"code": 200,
|
||||||
|
"data": [
|
||||||
|
{
|
||||||
|
"t050v01": "SN2026041301",
|
||||||
|
"api_token": "mac_token_...",
|
||||||
|
"t050v41": "8081234567",
|
||||||
|
"t050v42": "9001",
|
||||||
|
"t050v43": "password123",
|
||||||
|
"t050v34": "2000132",
|
||||||
|
"TP_APP_ID": "GREEN_001",
|
||||||
|
"...": "..."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## 🟢 B010: 心跳上報與狀態同步 (Heartbeat & Status)
|
## 🟢 B010: 心跳上報與狀態同步 (Heartbeat & Status)
|
||||||
機台定時(建議每 5-10 秒)上送,用於確認連線狀態、溫度及門禁狀態。
|
機台定時(建議每 5-10 秒)上送,用於確認連線狀態、溫度及門禁狀態。
|
||||||
|
|
||||||
|
|||||||
43
docs/future_todo.md
Normal file
43
docs/future_todo.md
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
# Star Cloud 近期開發待辦清單 (Target Roadmap)
|
||||||
|
|
||||||
|
本文件列出了 Star Cloud 系統近期優先開發的功能模組,旨在強化系統的營運溝通能力與非同步處理效率。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🟢 核心開發階段:全域工具列與通訊系統
|
||||||
|
*本階段為目前唯一開發重心*
|
||||||
|
|
||||||
|
### 1. 全域工具列升級 (Header Toolbar)
|
||||||
|
| 功能項目 | 具體描述 | 預計開發時間 |
|
||||||
|
| :--- | :--- | :--- |
|
||||||
|
| **☁️ 下載任務中心** | 整合 Redis Queue 處理耗時報表匯出。商戶點擊匯出後背景執行,完成後透過 Header 圖示點擊下載。 | 2 天 |
|
||||||
|
| **🔔 通知中心** | 串接 Laravel Database Notifications,顯示系統消息、機台警告與業務通知,帶有紅點提示。 | 1 天 |
|
||||||
|
| **❓ 幫助/客服中心** | 於 Header 置入問號圖示,點擊觸發側邊抽屜 (Offcanvas),展示 FAQ 與客服聯繫窗口。 | 0.5 天 |
|
||||||
|
| **🎭 帳號切換與身分模擬** | **整合於頭像下拉選單**:支援「系統管理員切換租戶」與「租戶管理員切換子帳號」,提供顯眼的頂部模擬狀態橫幅。 | 1.5 天 |
|
||||||
|
|
||||||
|
### 2. 公告與溝通系統 (Communication System)
|
||||||
|
| 功能項目 | 具體描述 | 預計開發時間 |
|
||||||
|
| :--- | :--- | :--- |
|
||||||
|
| **📢 系統公告管理** | 建立後台發布介面,支援針對全體或特定租戶發布「一般」或「重要」公告。 | 1.5 天 |
|
||||||
|
| **🛡️ 登錄強制公告** | 實作具備「滑動解鎖」功能的彈窗。使用者必須將公告滑到底部,解鎖按鈕後才能進入 Dashboard。 | 1 天 |
|
||||||
|
|
||||||
|
### 3. 儀表板優化 (Dashboard Enhancement)
|
||||||
|
| 功能項目 | 具體描述 | 預計開發時間 |
|
||||||
|
| :--- | :--- | :--- |
|
||||||
|
| **🚀 儀表板快捷入口** | 在儀表板頂部加入一排快捷圖示(如:機台管理、訂單查詢、會員中心),方便商戶快速跳轉核心功能。 | 0.5 天 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🟡 第二階段:進階行銷與營運工具
|
||||||
|
*優先順序:中 | 預計總工時:約 5 個開發日*
|
||||||
|
|
||||||
|
| 功能項目 | 具體描述 | 預計開發時間 |
|
||||||
|
| :--- | :--- | :--- |
|
||||||
|
| **🎁 互動盲盒抽獎** | **後台端**:實作中獎機率配置、獎項庫存管理、活動排程。**終端 API**:提供給機台大螢幕 H5/React 遊戲呼叫的開獎與配置介面。 | 4 天 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📝 實作標準
|
||||||
|
1. **UI/UX**: 必須符合 `ui-minimal-luxury` 規範(Outfit 字體、青色點綴、柔和投影)。
|
||||||
|
2. **安全性**: 權限控制必須嚴格過濾 `company_id`,公告需支援「已讀紀錄」追蹤。
|
||||||
|
3. **效能**: 下載中心必須使用非同步隊列,嚴禁在 Request 週期內執行耗時匯出。
|
||||||
126
lang/en.json
126
lang/en.json
@@ -17,7 +17,7 @@
|
|||||||
"Account updated successfully.": "Account updated successfully.",
|
"Account updated successfully.": "Account updated successfully.",
|
||||||
"Account:": "Account:",
|
"Account:": "Account:",
|
||||||
"accounts": "Account Management",
|
"accounts": "Account Management",
|
||||||
"Accounts / Machines": "Accounts / Machines",
|
"Accounts \/ Machines": "Accounts \/ Machines",
|
||||||
"Action": "Action",
|
"Action": "Action",
|
||||||
"Actions": "Actions",
|
"Actions": "Actions",
|
||||||
"Active": "Active",
|
"Active": "Active",
|
||||||
@@ -51,7 +51,7 @@
|
|||||||
"Advertisement Management": "Advertisement Management",
|
"Advertisement Management": "Advertisement Management",
|
||||||
"Advertisement updated successfully": "Advertisement updated successfully",
|
"Advertisement updated successfully": "Advertisement updated successfully",
|
||||||
"Advertisement updated successfully.": "Advertisement updated successfully.",
|
"Advertisement updated successfully.": "Advertisement updated successfully.",
|
||||||
"Advertisement Video/Image": "Advertisement Video/Image",
|
"Advertisement Video\/Image": "Advertisement Video\/Image",
|
||||||
"Affiliated Company": "Affiliated Company",
|
"Affiliated Company": "Affiliated Company",
|
||||||
"Affiliated Unit": "Company Name",
|
"Affiliated Unit": "Company Name",
|
||||||
"Affiliation": "Company Name",
|
"Affiliation": "Company Name",
|
||||||
@@ -127,7 +127,7 @@
|
|||||||
"Back to List": "Back to List",
|
"Back to List": "Back to List",
|
||||||
"Badge Settings": "Badge Settings",
|
"Badge Settings": "Badge Settings",
|
||||||
"Barcode": "Barcode",
|
"Barcode": "Barcode",
|
||||||
"Barcode / Material": "Barcode / Material",
|
"Barcode \/ Material": "Barcode \/ Material",
|
||||||
"Basic Information": "Basic Information",
|
"Basic Information": "Basic Information",
|
||||||
"Basic Settings": "Basic Settings",
|
"Basic Settings": "Basic Settings",
|
||||||
"Basic Specifications": "Basic Specifications",
|
"Basic Specifications": "Basic Specifications",
|
||||||
@@ -164,7 +164,7 @@
|
|||||||
"Change": "Change",
|
"Change": "Change",
|
||||||
"Change Stock": "Change Stock",
|
"Change Stock": "Change Stock",
|
||||||
"Channel Limits": "Channel Limits",
|
"Channel Limits": "Channel Limits",
|
||||||
"Channel Limits (Track/Spring)": "Channel Limits (Track/Spring)",
|
"Channel Limits (Track\/Spring)": "Channel Limits (Track\/Spring)",
|
||||||
"Channel Limits Configuration": "Channel Limits Configuration",
|
"Channel Limits Configuration": "Channel Limits Configuration",
|
||||||
"ChannelId": "ChannelId",
|
"ChannelId": "ChannelId",
|
||||||
"ChannelSecret": "ChannelSecret",
|
"ChannelSecret": "ChannelSecret",
|
||||||
@@ -277,7 +277,7 @@
|
|||||||
"Dispensing": "Dispensing",
|
"Dispensing": "Dispensing",
|
||||||
"Duration": "Duration",
|
"Duration": "Duration",
|
||||||
"Duration (Seconds)": "Duration (Seconds)",
|
"Duration (Seconds)": "Duration (Seconds)",
|
||||||
"e.g. 500ml / 300g": "e.g. 500ml / 300g",
|
"e.g. 500ml \/ 300g": "e.g. 500ml \/ 300g",
|
||||||
"e.g. John Doe": "e.g. John Doe",
|
"e.g. John Doe": "e.g. John Doe",
|
||||||
"e.g. johndoe": "e.g. johndoe",
|
"e.g. johndoe": "e.g. johndoe",
|
||||||
"e.g. Taiwan Star": "e.g. Taiwan Star",
|
"e.g. Taiwan Star": "e.g. Taiwan Star",
|
||||||
@@ -315,7 +315,7 @@
|
|||||||
"Enable Material Code": "Enable Material Code",
|
"Enable Material Code": "Enable Material Code",
|
||||||
"Enable Points": "Enable Points",
|
"Enable Points": "Enable Points",
|
||||||
"Enabled": "Enabled",
|
"Enabled": "Enabled",
|
||||||
"Enabled/Disabled": "Enabled/Disabled",
|
"Enabled\/Disabled": "Enabled\/Disabled",
|
||||||
"End Date": "End Date",
|
"End Date": "End Date",
|
||||||
"Engineer": "Engineer",
|
"Engineer": "Engineer",
|
||||||
"English": "English",
|
"English": "English",
|
||||||
@@ -341,7 +341,7 @@
|
|||||||
"Execution Time": "Execution Time",
|
"Execution Time": "Execution Time",
|
||||||
"Exp": "Exp",
|
"Exp": "Exp",
|
||||||
"Expired": "Expired",
|
"Expired": "Expired",
|
||||||
"Expired / Disabled": "Expired / Disabled",
|
"Expired \/ Disabled": "Expired \/ Disabled",
|
||||||
"Expiring": "Expiring",
|
"Expiring": "Expiring",
|
||||||
"Expiry": "Expiry",
|
"Expiry": "Expiry",
|
||||||
"Expiry Date": "Expiry Date",
|
"Expiry Date": "Expiry Date",
|
||||||
@@ -570,7 +570,7 @@
|
|||||||
"Monthly cumulative revenue overview": "Monthly cumulative revenue overview",
|
"Monthly cumulative revenue overview": "Monthly cumulative revenue overview",
|
||||||
"Monthly Transactions": "Monthly Transactions",
|
"Monthly Transactions": "Monthly Transactions",
|
||||||
"Multilingual Names": "Multilingual Names",
|
"Multilingual Names": "Multilingual Names",
|
||||||
"N/A": "N/A",
|
"N\/A": "N\/A",
|
||||||
"Name": "Name",
|
"Name": "Name",
|
||||||
"Name in English": "Name in English",
|
"Name in English": "Name in English",
|
||||||
"Name in Japanese": "Name in Japanese",
|
"Name in Japanese": "Name in Japanese",
|
||||||
@@ -713,7 +713,7 @@
|
|||||||
"Position": "Position",
|
"Position": "Position",
|
||||||
"Preview": "Preview",
|
"Preview": "Preview",
|
||||||
"Previous": "Previous",
|
"Previous": "Previous",
|
||||||
"Price / Member": "Price / Member",
|
"Price \/ Member": "Price \/ Member",
|
||||||
"Pricing Information": "Pricing Information",
|
"Pricing Information": "Pricing Information",
|
||||||
"Product Count": "Product Count",
|
"Product Count": "Product Count",
|
||||||
"Product created successfully": "Product created successfully",
|
"Product created successfully": "Product created successfully",
|
||||||
@@ -825,7 +825,7 @@
|
|||||||
"Scale level and access control": "層級與存取控制",
|
"Scale level and access control": "層級與存取控制",
|
||||||
"Scan this code to quickly access the maintenance form for this device.": "Scan this code to quickly access the maintenance form for this device.",
|
"Scan this code to quickly access the maintenance form for this device.": "Scan this code to quickly access the maintenance form for this device.",
|
||||||
"Search accounts...": "Search accounts...",
|
"Search accounts...": "Search accounts...",
|
||||||
"Search by name or S/N...": "Search by name or S/N...",
|
"Search by name or S\/N...": "Search by name or S\/N...",
|
||||||
"Search cargo lane": "Search cargo lane",
|
"Search cargo lane": "Search cargo lane",
|
||||||
"Search Company Title...": "Search Company Title...",
|
"Search Company Title...": "Search Company Title...",
|
||||||
"Search company...": "Search company...",
|
"Search company...": "Search company...",
|
||||||
@@ -880,7 +880,6 @@
|
|||||||
"Showing :from to :to of :total items": "Showing :from to :to of :total items",
|
"Showing :from to :to of :total items": "Showing :from to :to of :total items",
|
||||||
"Sign in to your account": "Sign in to your account",
|
"Sign in to your account": "Sign in to your account",
|
||||||
"Signed in as": "Signed in as",
|
"Signed in as": "Signed in as",
|
||||||
"Slot": "Slot",
|
|
||||||
"Slot Mechanism (default: Conveyor, check for Spring)": "Slot Mechanism (default: Conveyor, check for Spring)",
|
"Slot Mechanism (default: Conveyor, check for Spring)": "Slot Mechanism (default: Conveyor, check for Spring)",
|
||||||
"Slot No": "Slot No",
|
"Slot No": "Slot No",
|
||||||
"Slot Status": "Slot Status",
|
"Slot Status": "Slot Status",
|
||||||
@@ -902,7 +901,7 @@
|
|||||||
"Start Date": "Start Date",
|
"Start Date": "Start Date",
|
||||||
"Statistics": "Statistics",
|
"Statistics": "Statistics",
|
||||||
"Status": "Status",
|
"Status": "Status",
|
||||||
"Status / Temp / Sub / Card / Scan": "Status / Temp / Sub / Card / Scan",
|
"Status \/ Temp \/ Sub \/ Card \/ Scan": "Status \/ Temp \/ Sub \/ Card \/ Scan",
|
||||||
"Stock": "Stock",
|
"Stock": "Stock",
|
||||||
"Stock & Expiry": "Stock & Expiry",
|
"Stock & Expiry": "Stock & Expiry",
|
||||||
"Stock & Expiry Management": "Stock & Expiry Management",
|
"Stock & Expiry Management": "Stock & Expiry Management",
|
||||||
@@ -1052,5 +1051,106 @@
|
|||||||
"You cannot delete your own account.": "You cannot delete your own account.",
|
"You cannot delete your own account.": "You cannot delete your own account.",
|
||||||
"Your email address is unverified.": "Your email address is unverified.",
|
"Your email address is unverified.": "Your email address is unverified.",
|
||||||
"Your recent account activity": "Your recent account activity",
|
"Your recent account activity": "Your recent account activity",
|
||||||
"待填寫": "待填寫"
|
"待填寫": "待填寫",
|
||||||
|
"Dispensing in progress": "Dispensing in progress",
|
||||||
|
"Dispense successful": "Dispense successful",
|
||||||
|
"Slot jammed": "Slot jammed",
|
||||||
|
"Motor not stopped": "Motor not stopped",
|
||||||
|
"Slot not found": "Slot not found",
|
||||||
|
"Dispense error (0407)": "Dispense error (0407)",
|
||||||
|
"Dispense error (0408)": "Dispense error (0408)",
|
||||||
|
"Dispense error (0409)": "Dispense error (0409)",
|
||||||
|
"Dispense error (040A)": "Dispense error (040A)",
|
||||||
|
"Elevator rising": "Elevator rising",
|
||||||
|
"Elevator descending": "Elevator descending",
|
||||||
|
"Elevator rise error": "Elevator rise error",
|
||||||
|
"Elevator descent error": "Elevator descent error",
|
||||||
|
"Pickup door closed": "Pickup door closed",
|
||||||
|
"Pickup door error": "Pickup door error",
|
||||||
|
"Delivery door opened": "Delivery door opened",
|
||||||
|
"Delivery door open error": "Delivery door open error",
|
||||||
|
"Delivering product": "Delivering product",
|
||||||
|
"Delivery door closed": "Delivery door closed",
|
||||||
|
"Delivery door close error": "Delivery door close error",
|
||||||
|
"Hopper empty": "Hopper empty",
|
||||||
|
"Hopper overheated": "Hopper overheated",
|
||||||
|
"Hopper heating timeout": "Hopper heating timeout",
|
||||||
|
"Hopper error (0424)": "Hopper error (0424)",
|
||||||
|
"Microwave door opened": "Microwave door opened",
|
||||||
|
"Microwave door error": "Microwave door error",
|
||||||
|
"Dispense stopped": "Dispense stopped",
|
||||||
|
"Slot normal": "Slot normal",
|
||||||
|
"Product empty": "Product empty",
|
||||||
|
"Slot empty": "Slot empty",
|
||||||
|
"Slot not closed": "Slot not closed",
|
||||||
|
"Slot motor error (0207)": "Slot motor error (0207)",
|
||||||
|
"Slot motor error (0208)": "Slot motor error (0208)",
|
||||||
|
"Slot motor error (0209)": "Slot motor error (0209)",
|
||||||
|
"Hopper empty (0212)": "Hopper empty (0212)",
|
||||||
|
"Machine normal": "Machine normal",
|
||||||
|
"Elevator sensor error": "Elevator sensor error",
|
||||||
|
"Pickup door not closed": "Pickup door not closed",
|
||||||
|
"Elevator failure": "Elevator failure",
|
||||||
|
"Slot": "Slot",
|
||||||
|
"Page 0": "Offline",
|
||||||
|
"Page 1": "Home",
|
||||||
|
"Page 2": "Vending",
|
||||||
|
"Page 3": "Admin",
|
||||||
|
"Page 4": "Restock",
|
||||||
|
"Page 5": "Tutorial",
|
||||||
|
"Page 6": "Purchasing",
|
||||||
|
"Page 7": "Locked",
|
||||||
|
"Page 60": "Dispense Success",
|
||||||
|
"Page 61": "Slot Test",
|
||||||
|
"Page 62": "Payment Selection",
|
||||||
|
"Page 63": "Waiting for Payment",
|
||||||
|
"Page 64": "Dispensing",
|
||||||
|
"Page 65": "Receipt",
|
||||||
|
"Page 66": "Passcode",
|
||||||
|
"Page 67": "Pickup Code",
|
||||||
|
"Page 68": "Message",
|
||||||
|
"Page 69": "Purchase Cancelled",
|
||||||
|
"Page 610": "Purchase Ended",
|
||||||
|
"Page 611": "Store Gift",
|
||||||
|
"Page 612": "Dispense Failed",
|
||||||
|
"Door Opened": "Door Opened",
|
||||||
|
"Door Closed": "Door Closed",
|
||||||
|
"Firmware updated to :version": "Firmware updated to :version",
|
||||||
|
"Model changed to :model": "Model changed to :model",
|
||||||
|
"User logged in: :name": "User logged in: :name",
|
||||||
|
"Login failed: :account": "Login failed: :account",
|
||||||
|
"Unauthorized login attempt: :account": "Unauthorized login attempt: :account",
|
||||||
|
"Contract Model": "Contract Model",
|
||||||
|
"Warranty Service": "Warranty Service",
|
||||||
|
"Software Service": "Software Service",
|
||||||
|
"Modification History": "Modification History",
|
||||||
|
"No history records": "No history records",
|
||||||
|
"Initial contract registration": "Initial contract registration",
|
||||||
|
"Contract information updated": "Contract information updated",
|
||||||
|
"Warranty Start": "Warranty Start",
|
||||||
|
"Warranty End": "Warranty End",
|
||||||
|
"Software Start": "Software Start",
|
||||||
|
"Software End": "Software End",
|
||||||
|
"Contract History": "Contract History",
|
||||||
|
"Unlimited": "Unlimited",
|
||||||
|
"Change Note": "Change Note",
|
||||||
|
"Log Time": "Log Time",
|
||||||
|
"Service Periods": "Service Periods",
|
||||||
|
"Creator": "Creator",
|
||||||
|
"View Full History": "View Full History",
|
||||||
|
"Contract Start": "Contract Start",
|
||||||
|
"Contract End": "Contract End",
|
||||||
|
"Contract History Detail": "Contract History Detail",
|
||||||
|
"by": "by",
|
||||||
|
"Service Terms": "Service Periods",
|
||||||
|
"Contract": "Contract",
|
||||||
|
"Warranty": "Warranty",
|
||||||
|
"Software": "Software",
|
||||||
|
"Schedule": "Schedule",
|
||||||
|
"Immediate": "Immediate",
|
||||||
|
"Indefinite": "Indefinite",
|
||||||
|
"Ongoing": "Ongoing",
|
||||||
|
"Waiting": "Waiting",
|
||||||
|
"Publish Time": "Publish Time",
|
||||||
|
"Expired Time": "Expired Time"
|
||||||
}
|
}
|
||||||
126
lang/ja.json
126
lang/ja.json
@@ -17,7 +17,7 @@
|
|||||||
"Account updated successfully.": "アカウントが正常に更新されました。",
|
"Account updated successfully.": "アカウントが正常に更新されました。",
|
||||||
"Account:": "アカウント:",
|
"Account:": "アカウント:",
|
||||||
"accounts": "アカウント管理",
|
"accounts": "アカウント管理",
|
||||||
"Accounts / Machines": "アカウント / 機体",
|
"Accounts \/ Machines": "アカウント \/ 機体",
|
||||||
"Action": "操作",
|
"Action": "操作",
|
||||||
"Actions": "操作",
|
"Actions": "操作",
|
||||||
"Active": "有効",
|
"Active": "有効",
|
||||||
@@ -51,7 +51,7 @@
|
|||||||
"Advertisement Management": "広告管理",
|
"Advertisement Management": "広告管理",
|
||||||
"Advertisement updated successfully": "広告の更新に成功しました",
|
"Advertisement updated successfully": "広告の更新に成功しました",
|
||||||
"Advertisement updated successfully.": "広告の更新に成功しました。",
|
"Advertisement updated successfully.": "広告の更新に成功しました。",
|
||||||
"Advertisement Video/Image": "広告動画/画像",
|
"Advertisement Video\/Image": "広告動画\/画像",
|
||||||
"Affiliated Company": "所属会社",
|
"Affiliated Company": "所属会社",
|
||||||
"Affiliated Unit": "所属会社",
|
"Affiliated Unit": "所属会社",
|
||||||
"Affiliation": "所属会社",
|
"Affiliation": "所属会社",
|
||||||
@@ -127,7 +127,7 @@
|
|||||||
"Back to List": "リストに戻る",
|
"Back to List": "リストに戻る",
|
||||||
"Badge Settings": "バッジ設定",
|
"Badge Settings": "バッジ設定",
|
||||||
"Barcode": "バーコード",
|
"Barcode": "バーコード",
|
||||||
"Barcode / Material": "バーコード / 素材",
|
"Barcode \/ Material": "バーコード \/ 素材",
|
||||||
"Basic Information": "基本情報",
|
"Basic Information": "基本情報",
|
||||||
"Basic Settings": "基本設定",
|
"Basic Settings": "基本設定",
|
||||||
"Basic Specifications": "基本仕様",
|
"Basic Specifications": "基本仕様",
|
||||||
@@ -164,7 +164,7 @@
|
|||||||
"Change": "変更",
|
"Change": "変更",
|
||||||
"Change Stock": "在庫変更",
|
"Change Stock": "在庫変更",
|
||||||
"Channel Limits": "チャネル制限",
|
"Channel Limits": "チャネル制限",
|
||||||
"Channel Limits (Track/Spring)": "チャネル上限(トラック/スプリング)",
|
"Channel Limits (Track\/Spring)": "チャネル上限(トラック\/スプリング)",
|
||||||
"Channel Limits Configuration": "チャネル制限設定",
|
"Channel Limits Configuration": "チャネル制限設定",
|
||||||
"ChannelId": "チャネルID",
|
"ChannelId": "チャネルID",
|
||||||
"ChannelSecret": "チャネルシークレット",
|
"ChannelSecret": "チャネルシークレット",
|
||||||
@@ -277,7 +277,7 @@
|
|||||||
"Dispensing": "払い出し中",
|
"Dispensing": "払い出し中",
|
||||||
"Duration": "再生時間",
|
"Duration": "再生時間",
|
||||||
"Duration (Seconds)": "再生時間(秒)",
|
"Duration (Seconds)": "再生時間(秒)",
|
||||||
"e.g. 500ml / 300g": "例: 500ml / 300g",
|
"e.g. 500ml \/ 300g": "例: 500ml \/ 300g",
|
||||||
"e.g. John Doe": "例:山田太郎",
|
"e.g. John Doe": "例:山田太郎",
|
||||||
"e.g. johndoe": "例:yamadataro",
|
"e.g. johndoe": "例:yamadataro",
|
||||||
"e.g. Taiwan Star": "例:Taiwan Star",
|
"e.g. Taiwan Star": "例:Taiwan Star",
|
||||||
@@ -315,7 +315,7 @@
|
|||||||
"Enable Material Code": "品目コードを有効にする",
|
"Enable Material Code": "品目コードを有効にする",
|
||||||
"Enable Points": "ポイントを有効にする",
|
"Enable Points": "ポイントを有効にする",
|
||||||
"Enabled": "有効",
|
"Enabled": "有効",
|
||||||
"Enabled/Disabled": "有効/無効",
|
"Enabled\/Disabled": "有効\/無効",
|
||||||
"End Date": "終了日",
|
"End Date": "終了日",
|
||||||
"Engineer": "エンジニア",
|
"Engineer": "エンジニア",
|
||||||
"English": "英語",
|
"English": "英語",
|
||||||
@@ -341,7 +341,7 @@
|
|||||||
"Execution Time": "実行時間",
|
"Execution Time": "実行時間",
|
||||||
"Exp": "有効期限",
|
"Exp": "有効期限",
|
||||||
"Expired": "期限切れ",
|
"Expired": "期限切れ",
|
||||||
"Expired / Disabled": "期限切れ / 無効",
|
"Expired \/ Disabled": "期限切れ \/ 無効",
|
||||||
"Expiring": "期限間近",
|
"Expiring": "期限間近",
|
||||||
"Expiry": "期限",
|
"Expiry": "期限",
|
||||||
"Expiry Date": "有効期限",
|
"Expiry Date": "有効期限",
|
||||||
@@ -569,7 +569,7 @@
|
|||||||
"Monthly cumulative revenue overview": "月間累積収益の概要",
|
"Monthly cumulative revenue overview": "月間累積収益の概要",
|
||||||
"Monthly Transactions": "月間取引数",
|
"Monthly Transactions": "月間取引数",
|
||||||
"Multilingual Names": "多言語名称",
|
"Multilingual Names": "多言語名称",
|
||||||
"N/A": "N/A",
|
"N\/A": "N\/A",
|
||||||
"Name": "名前",
|
"Name": "名前",
|
||||||
"Name in English": "英語名",
|
"Name in English": "英語名",
|
||||||
"Name in Japanese": "日本語名",
|
"Name in Japanese": "日本語名",
|
||||||
@@ -630,7 +630,7 @@
|
|||||||
"OEE.Hours": "時間",
|
"OEE.Hours": "時間",
|
||||||
"OEE.Orders": "注文数",
|
"OEE.Orders": "注文数",
|
||||||
"OEE.Sales": "売上高",
|
"OEE.Sales": "売上高",
|
||||||
"of": "/",
|
"of": "\/",
|
||||||
"Offline": "オフライン",
|
"Offline": "オフライン",
|
||||||
"Offline Machines": "オフライン機体",
|
"Offline Machines": "オフライン機体",
|
||||||
"Once your account is deleted, all of its resources and data will be permanently deleted. Before deleting your account, please download any data or information that you wish to retain.": "アカウントを削除すると、そのすべてのリソースとデータが完全に削除されます。削除する前に、保存しておきたいデータをダウンロードしてください。",
|
"Once your account is deleted, all of its resources and data will be permanently deleted. Before deleting your account, please download any data or information that you wish to retain.": "アカウントを削除すると、そのすべてのリソースとデータが完全に削除されます。削除する前に、保存しておきたいデータをダウンロードしてください。",
|
||||||
@@ -712,7 +712,7 @@
|
|||||||
"Position": "位置",
|
"Position": "位置",
|
||||||
"Preview": "プレビュー",
|
"Preview": "プレビュー",
|
||||||
"Previous": "前へ",
|
"Previous": "前へ",
|
||||||
"Price / Member": "価格 / 会員",
|
"Price \/ Member": "価格 \/ 会員",
|
||||||
"Pricing Information": "価格情報",
|
"Pricing Information": "価格情報",
|
||||||
"Product Count": "商品数",
|
"Product Count": "商品数",
|
||||||
"Product created successfully": "商品が正常に作成されました",
|
"Product created successfully": "商品が正常に作成されました",
|
||||||
@@ -824,7 +824,7 @@
|
|||||||
"Scale level and access control": "規模レベルとアクセス制御",
|
"Scale level and access control": "規模レベルとアクセス制御",
|
||||||
"Scan this code to quickly access the maintenance form for this device.": "このコードをスキャンして、端末のメンテナンスフォームに素早くアクセスします。",
|
"Scan this code to quickly access the maintenance form for this device.": "このコードをスキャンして、端末のメンテナンスフォームに素早くアクセスします。",
|
||||||
"Search accounts...": "アカウントを検索...",
|
"Search accounts...": "アカウントを検索...",
|
||||||
"Search by name or S/N...": "名称または製造番号で検索...",
|
"Search by name or S\/N...": "名称または製造番号で検索...",
|
||||||
"Search cargo lane": "貨道を検索",
|
"Search cargo lane": "貨道を検索",
|
||||||
"Search Company Title...": "会社名を検索...",
|
"Search Company Title...": "会社名を検索...",
|
||||||
"Search company...": "会社を検索...",
|
"Search company...": "会社を検索...",
|
||||||
@@ -901,7 +901,7 @@
|
|||||||
"Start Date": "開始日",
|
"Start Date": "開始日",
|
||||||
"Statistics": "統計",
|
"Statistics": "統計",
|
||||||
"Status": "ステータス",
|
"Status": "ステータス",
|
||||||
"Status / Temp / Sub / Card / Scan": "状態 / 温度 / 子機 / カード / スキャン",
|
"Status \/ Temp \/ Sub \/ Card \/ Scan": "状態 \/ 温度 \/ 子機 \/ カード \/ スキャン",
|
||||||
"Stock": "在庫",
|
"Stock": "在庫",
|
||||||
"Stock & Expiry": "在庫と消費期限",
|
"Stock & Expiry": "在庫と消費期限",
|
||||||
"Stock & Expiry Management": "在庫・期限管理",
|
"Stock & Expiry Management": "在庫・期限管理",
|
||||||
@@ -1051,5 +1051,105 @@
|
|||||||
"You cannot delete your own account.": "自分自身のアカウントを削除することはできません。",
|
"You cannot delete your own account.": "自分自身のアカウントを削除することはできません。",
|
||||||
"Your email address is unverified.": "メールアドレスが未認証です。",
|
"Your email address is unverified.": "メールアドレスが未認証です。",
|
||||||
"Your recent account activity": "最近のアカウントアクティビティ",
|
"Your recent account activity": "最近のアカウントアクティビティ",
|
||||||
"待填寫": "待填寫"
|
"待填寫": "待填寫",
|
||||||
|
"Dispensing in progress": "商品搬送中",
|
||||||
|
"Dispense successful": "搬送成功",
|
||||||
|
"Slot jammed": "貨道詰まり (K-PDT)",
|
||||||
|
"Motor not stopped": "モーター停止異常",
|
||||||
|
"Slot not found": "指定貨道が見つかりません",
|
||||||
|
"Dispense error (0407)": "搬送異常 (0407)",
|
||||||
|
"Dispense error (0408)": "搬送異常 (0408)",
|
||||||
|
"Dispense error (0409)": "搬送異常 (0409)",
|
||||||
|
"Dispense error (040A)": "搬送異常 (040A)",
|
||||||
|
"Elevator rising": "昇降機上昇中",
|
||||||
|
"Elevator descending": "昇降機下降中",
|
||||||
|
"Elevator rise error": "昇降機上昇異常",
|
||||||
|
"Elevator descent error": "昇降機下降異常",
|
||||||
|
"Pickup door closed": "取出口ドア閉鎖",
|
||||||
|
"Pickup door error": "取出口ドア異常",
|
||||||
|
"Delivery door opened": "配送口ドア開放",
|
||||||
|
"Delivery door open error": "配送口ドア開放異常",
|
||||||
|
"Delivering product": "商品配送中",
|
||||||
|
"Delivery door closed": "配送口ドア閉鎖",
|
||||||
|
"Delivery door close error": "配送口ドア閉鎖異常",
|
||||||
|
"Hopper empty": "ホッパー空",
|
||||||
|
"Hopper overheated": "ホッパー過熱",
|
||||||
|
"Hopper heating timeout": "ホッパー加熱タイムアウト",
|
||||||
|
"Hopper error (0424)": "ホッパー異常 (0424)",
|
||||||
|
"Microwave door opened": "電子レンジドア開放",
|
||||||
|
"Microwave door error": "電子レンジドア異常",
|
||||||
|
"Dispense stopped": "搬送停止",
|
||||||
|
"Slot normal": "貨道正常",
|
||||||
|
"Product empty": "品切れ (PDT_EMPTY)",
|
||||||
|
"Slot empty": "貨道空 (SLOT_EMPTY)",
|
||||||
|
"Slot not closed": "貨道未閉鎖",
|
||||||
|
"Slot motor error (0207)": "貨道モーター故障 (0207)",
|
||||||
|
"Slot motor error (0208)": "貨道モーター故障 (0208)",
|
||||||
|
"Slot motor error (0209)": "貨道モーター故障 (0209)",
|
||||||
|
"Hopper empty (0212)": "ホッパー空 (0212)",
|
||||||
|
"Machine normal": "システム正常",
|
||||||
|
"Elevator sensor error": "昇降センサー異常",
|
||||||
|
"Pickup door not closed": "取出口ドア未閉鎖",
|
||||||
|
"Elevator failure": "昇降システム故障",
|
||||||
|
"Page 0": "オフライン",
|
||||||
|
"Page 1": "ホーム",
|
||||||
|
"Page 2": "販売ページ",
|
||||||
|
"Page 3": "管理ページ",
|
||||||
|
"Page 4": "補充ページ",
|
||||||
|
"Page 5": "チュートリアル",
|
||||||
|
"Page 6": "購入中",
|
||||||
|
"Page 7": "ロック中",
|
||||||
|
"Page 60": "排出成功",
|
||||||
|
"Page 61": "スロットテスト",
|
||||||
|
"Page 62": "支払い方法選択",
|
||||||
|
"Page 63": "支払い待ち",
|
||||||
|
"Page 64": "排出中",
|
||||||
|
"Page 65": "レシート",
|
||||||
|
"Page 66": "パスコード",
|
||||||
|
"Page 67": "受取コード",
|
||||||
|
"Page 68": "メッセージ",
|
||||||
|
"Page 69": "購入キャンセル",
|
||||||
|
"Page 610": "購入完了",
|
||||||
|
"Page 611": "来店ギフト",
|
||||||
|
"Page 612": "排出失敗",
|
||||||
|
"Door Opened": "機ドア開放",
|
||||||
|
"Door Closed": "機ドア閉鎖",
|
||||||
|
"Firmware updated to :version": "ファームウェア更新::version",
|
||||||
|
"Model changed to :model": "モデル変更::model",
|
||||||
|
"User logged in: :name": "ユーザーログイン::name",
|
||||||
|
"Login failed: :account": "ログイン失敗::account",
|
||||||
|
"Unauthorized login attempt: :account": "不許可のログイン試行::account",
|
||||||
|
"Contract Model": "契約モデル",
|
||||||
|
"Warranty Service": "保証サービス",
|
||||||
|
"Software Service": "ソフトウェアサービス",
|
||||||
|
"Modification History": "変更履歴",
|
||||||
|
"No history records": "履歴なし",
|
||||||
|
"Initial contract registration": "初期契約登録",
|
||||||
|
"Contract information updated": "契約情報更新",
|
||||||
|
"Warranty Start": "保証開始",
|
||||||
|
"Warranty End": "保証終了",
|
||||||
|
"Software Start": "ソフト開始",
|
||||||
|
"Software End": "ソフト終了",
|
||||||
|
"Contract History": "契約履歴",
|
||||||
|
"Unlimited": "無期限",
|
||||||
|
"Change Note": "変更メモ",
|
||||||
|
"Log Time": "記録時間",
|
||||||
|
"Service Periods": "サービス期間",
|
||||||
|
"Creator": "作成者",
|
||||||
|
"View Full History": "全履歴を表示",
|
||||||
|
"Contract Start": "契約開始",
|
||||||
|
"Contract End": "契約終了",
|
||||||
|
"Contract History Detail": "契約履歴の詳細",
|
||||||
|
"by": "作成者:",
|
||||||
|
"Service Terms": "サービス期間",
|
||||||
|
"Contract": "契約",
|
||||||
|
"Warranty": "保証",
|
||||||
|
"Software": "ソフトウェア",
|
||||||
|
"Schedule": "スケジュール設定",
|
||||||
|
"Immediate": "即時",
|
||||||
|
"Indefinite": "無期限",
|
||||||
|
"Ongoing": "進行中",
|
||||||
|
"Waiting": "待機中",
|
||||||
|
"Publish Time": "公開時間",
|
||||||
|
"Expired Time": "終了時間"
|
||||||
}
|
}
|
||||||
128
lang/zh_TW.json
128
lang/zh_TW.json
@@ -17,7 +17,7 @@
|
|||||||
"Account updated successfully.": "帳號已成功更新。",
|
"Account updated successfully.": "帳號已成功更新。",
|
||||||
"Account:": "帳號:",
|
"Account:": "帳號:",
|
||||||
"accounts": "帳號管理",
|
"accounts": "帳號管理",
|
||||||
"Accounts / Machines": "帳號 / 機台",
|
"Accounts \/ Machines": "帳號 \/ 機台",
|
||||||
"Action": "操作",
|
"Action": "操作",
|
||||||
"Actions": "操作",
|
"Actions": "操作",
|
||||||
"Active": "使用中",
|
"Active": "使用中",
|
||||||
@@ -51,7 +51,7 @@
|
|||||||
"Advertisement Management": "廣告管理",
|
"Advertisement Management": "廣告管理",
|
||||||
"Advertisement updated successfully": "廣告更新成功",
|
"Advertisement updated successfully": "廣告更新成功",
|
||||||
"Advertisement updated successfully.": "廣告更新成功。",
|
"Advertisement updated successfully.": "廣告更新成功。",
|
||||||
"Advertisement Video/Image": "廣告影片/圖片",
|
"Advertisement Video\/Image": "廣告影片\/圖片",
|
||||||
"Affiliated Company": "公司名稱",
|
"Affiliated Company": "公司名稱",
|
||||||
"Affiliated Unit": "公司名稱",
|
"Affiliated Unit": "公司名稱",
|
||||||
"Affiliation": "所屬單位",
|
"Affiliation": "所屬單位",
|
||||||
@@ -127,7 +127,7 @@
|
|||||||
"Back to List": "返回列表",
|
"Back to List": "返回列表",
|
||||||
"Badge Settings": "識別證",
|
"Badge Settings": "識別證",
|
||||||
"Barcode": "條碼",
|
"Barcode": "條碼",
|
||||||
"Barcode / Material": "條碼 / 物料編碼",
|
"Barcode \/ Material": "條碼 \/ 物料編碼",
|
||||||
"Basic Information": "基本資訊",
|
"Basic Information": "基本資訊",
|
||||||
"Basic Settings": "基本設定",
|
"Basic Settings": "基本設定",
|
||||||
"Basic Specifications": "基本規格",
|
"Basic Specifications": "基本規格",
|
||||||
@@ -164,7 +164,7 @@
|
|||||||
"Change": "更換",
|
"Change": "更換",
|
||||||
"Change Stock": "零錢庫存",
|
"Change Stock": "零錢庫存",
|
||||||
"Channel Limits": "貨道上限",
|
"Channel Limits": "貨道上限",
|
||||||
"Channel Limits (Track/Spring)": "貨道上限 (履帶/彈簧)",
|
"Channel Limits (Track\/Spring)": "貨道上限 (履帶\/彈簧)",
|
||||||
"Channel Limits Configuration": "貨道上限配置",
|
"Channel Limits Configuration": "貨道上限配置",
|
||||||
"ChannelId": "ChannelId",
|
"ChannelId": "ChannelId",
|
||||||
"ChannelSecret": "ChannelSecret",
|
"ChannelSecret": "ChannelSecret",
|
||||||
@@ -277,7 +277,7 @@
|
|||||||
"Dispensing": "出貨",
|
"Dispensing": "出貨",
|
||||||
"Duration": "時長",
|
"Duration": "時長",
|
||||||
"Duration (Seconds)": "播放秒數",
|
"Duration (Seconds)": "播放秒數",
|
||||||
"e.g. 500ml / 300g": "例如:500ml / 300g",
|
"e.g. 500ml \/ 300g": "例如:500ml \/ 300g",
|
||||||
"e.g. John Doe": "例如:張曉明",
|
"e.g. John Doe": "例如:張曉明",
|
||||||
"e.g. johndoe": "例如:xiaoming",
|
"e.g. johndoe": "例如:xiaoming",
|
||||||
"e.g. Taiwan Star": "例如:台灣之星",
|
"e.g. Taiwan Star": "例如:台灣之星",
|
||||||
@@ -315,7 +315,7 @@
|
|||||||
"Enable Material Code": "啟用物料編號",
|
"Enable Material Code": "啟用物料編號",
|
||||||
"Enable Points": "啟用點數規則",
|
"Enable Points": "啟用點數規則",
|
||||||
"Enabled": "已啟用",
|
"Enabled": "已啟用",
|
||||||
"Enabled/Disabled": "啟用/停用",
|
"Enabled\/Disabled": "啟用\/停用",
|
||||||
"End Date": "截止日",
|
"End Date": "截止日",
|
||||||
"Engineer": "維修人員",
|
"Engineer": "維修人員",
|
||||||
"English": "英文",
|
"English": "英文",
|
||||||
@@ -341,7 +341,7 @@
|
|||||||
"Execution Time": "執行時間",
|
"Execution Time": "執行時間",
|
||||||
"Exp": "效期",
|
"Exp": "效期",
|
||||||
"Expired": "已過期",
|
"Expired": "已過期",
|
||||||
"Expired / Disabled": "已過期 / 停用",
|
"Expired \/ Disabled": "已過期 \/ 停用",
|
||||||
"Expiring": "效期將屆",
|
"Expiring": "效期將屆",
|
||||||
"Expiry": "效期",
|
"Expiry": "效期",
|
||||||
"Expiry Date": "有效日期",
|
"Expiry Date": "有效日期",
|
||||||
@@ -570,7 +570,7 @@
|
|||||||
"Monthly cumulative revenue overview": "本月累計營收概況",
|
"Monthly cumulative revenue overview": "本月累計營收概況",
|
||||||
"Monthly Transactions": "本月交易統計",
|
"Monthly Transactions": "本月交易統計",
|
||||||
"Multilingual Names": "多語系名稱",
|
"Multilingual Names": "多語系名稱",
|
||||||
"N/A": "不適用",
|
"N\/A": "不適用",
|
||||||
"Name": "名稱",
|
"Name": "名稱",
|
||||||
"Name in English": "英文名稱",
|
"Name in English": "英文名稱",
|
||||||
"Name in Japanese": "日文名稱",
|
"Name in Japanese": "日文名稱",
|
||||||
@@ -677,7 +677,7 @@
|
|||||||
"Pending": "等待機台領取",
|
"Pending": "等待機台領取",
|
||||||
"pending": "等待機台領取",
|
"pending": "等待機台領取",
|
||||||
"Performance": "效能 (Performance)",
|
"Performance": "效能 (Performance)",
|
||||||
"Permanent": "永久授權",
|
"Permanent": "永久",
|
||||||
"Permanently Delete Account": "永久刪除帳號",
|
"Permanently Delete Account": "永久刪除帳號",
|
||||||
"Permission Settings": "權限設定",
|
"Permission Settings": "權限設定",
|
||||||
"Permissions": "權限",
|
"Permissions": "權限",
|
||||||
@@ -713,7 +713,7 @@
|
|||||||
"Position": "投放位置",
|
"Position": "投放位置",
|
||||||
"Preview": "預覽",
|
"Preview": "預覽",
|
||||||
"Previous": "上一頁",
|
"Previous": "上一頁",
|
||||||
"Price / Member": "售價 / 會員價",
|
"Price \/ Member": "售價 \/ 會員價",
|
||||||
"Pricing Information": "價格資訊",
|
"Pricing Information": "價格資訊",
|
||||||
"Product Count": "商品數量",
|
"Product Count": "商品數量",
|
||||||
"Product created successfully": "商品已成功建立",
|
"Product created successfully": "商品已成功建立",
|
||||||
@@ -825,7 +825,7 @@
|
|||||||
"Scale level and access control": "層級與存取控制",
|
"Scale level and access control": "層級與存取控制",
|
||||||
"Scan this code to quickly access the maintenance form for this device.": "掃描此 QR Code 即可快速進入此設備的維修單填寫頁面。",
|
"Scan this code to quickly access the maintenance form for this device.": "掃描此 QR Code 即可快速進入此設備的維修單填寫頁面。",
|
||||||
"Search accounts...": "搜尋帳號...",
|
"Search accounts...": "搜尋帳號...",
|
||||||
"Search by name or S/N...": "搜尋名稱或序號...",
|
"Search by name or S\/N...": "搜尋名稱或序號...",
|
||||||
"Search cargo lane": "搜尋貨道編號或商品名稱",
|
"Search cargo lane": "搜尋貨道編號或商品名稱",
|
||||||
"Search Company Title...": "搜尋公司名稱...",
|
"Search Company Title...": "搜尋公司名稱...",
|
||||||
"Search company...": "搜尋公司...",
|
"Search company...": "搜尋公司...",
|
||||||
@@ -880,7 +880,6 @@
|
|||||||
"Showing :from to :to of :total items": "顯示第 :from 到 :to 項,共 :total 項",
|
"Showing :from to :to of :total items": "顯示第 :from 到 :to 項,共 :total 項",
|
||||||
"Sign in to your account": "隨時隨地掌控您的業務。",
|
"Sign in to your account": "隨時隨地掌控您的業務。",
|
||||||
"Signed in as": "登入身份",
|
"Signed in as": "登入身份",
|
||||||
"Slot": "貨道",
|
|
||||||
"Slot Mechanism (default: Conveyor, check for Spring)": "貨道機制 (預設履帶,勾選為彈簧)",
|
"Slot Mechanism (default: Conveyor, check for Spring)": "貨道機制 (預設履帶,勾選為彈簧)",
|
||||||
"Slot No": "貨道編號",
|
"Slot No": "貨道編號",
|
||||||
"Slot Status": "貨道效期",
|
"Slot Status": "貨道效期",
|
||||||
@@ -902,7 +901,7 @@
|
|||||||
"Start Date": "起始日",
|
"Start Date": "起始日",
|
||||||
"Statistics": "數據統計",
|
"Statistics": "數據統計",
|
||||||
"Status": "狀態",
|
"Status": "狀態",
|
||||||
"Status / Temp / Sub / Card / Scan": "狀態 / 溫度 / 下位機 / 刷卡機 / 掃碼機",
|
"Status \/ Temp \/ Sub \/ Card \/ Scan": "狀態 \/ 溫度 \/ 下位機 \/ 刷卡機 \/ 掃碼機",
|
||||||
"Stock": "庫存",
|
"Stock": "庫存",
|
||||||
"Stock & Expiry": "庫存與效期",
|
"Stock & Expiry": "庫存與效期",
|
||||||
"Stock & Expiry Management": "庫存與效期管理",
|
"Stock & Expiry Management": "庫存與效期管理",
|
||||||
@@ -1052,5 +1051,106 @@
|
|||||||
"You cannot delete your own account.": "您無法刪除自己的帳號。",
|
"You cannot delete your own account.": "您無法刪除自己的帳號。",
|
||||||
"Your email address is unverified.": "您的電子郵件地址尚未驗證。",
|
"Your email address is unverified.": "您的電子郵件地址尚未驗證。",
|
||||||
"Your recent account activity": "最近的帳號活動",
|
"Your recent account activity": "最近的帳號活動",
|
||||||
"待填寫": "待填寫"
|
"待填寫": "待填寫",
|
||||||
|
"Dispensing in progress": "正在出貨中",
|
||||||
|
"Dispense successful": "出貨成功",
|
||||||
|
"Slot jammed": "貨道卡貨 (K-PDT)",
|
||||||
|
"Motor not stopped": "電機未停止",
|
||||||
|
"Slot not found": "找不到指定貨道",
|
||||||
|
"Dispense error (0407)": "出貨過程異常 (0407)",
|
||||||
|
"Dispense error (0408)": "出貨過程異常 (0408)",
|
||||||
|
"Dispense error (0409)": "出貨過程異常 (0409)",
|
||||||
|
"Dispense error (040A)": "出貨過程異常 (040A)",
|
||||||
|
"Elevator rising": "升降平台上升中",
|
||||||
|
"Elevator descending": "升降平台下降中",
|
||||||
|
"Elevator rise error": "升降平台上升異常",
|
||||||
|
"Elevator descent error": "升降平台下降異常",
|
||||||
|
"Pickup door closed": "取貨門已關閉",
|
||||||
|
"Pickup door error": "取貨門運作異常",
|
||||||
|
"Delivery door opened": "送貨門開啟",
|
||||||
|
"Delivery door open error": "送貨門開啟異常",
|
||||||
|
"Delivering product": "正在送出商品",
|
||||||
|
"Delivery door closed": "送貨門關閉",
|
||||||
|
"Delivery door close error": "送貨門關閉異常",
|
||||||
|
"Hopper empty": "料斗箱空",
|
||||||
|
"Hopper overheated": "料斗箱過熱",
|
||||||
|
"Hopper heating timeout": "料斗箱加熱逾時",
|
||||||
|
"Hopper error (0424)": "料斗箱異常 (0424)",
|
||||||
|
"Microwave door opened": "微波爐門開啟",
|
||||||
|
"Microwave door error": "微波爐門異常",
|
||||||
|
"Dispense stopped": "出貨停止",
|
||||||
|
"Slot normal": "貨道正常",
|
||||||
|
"Product empty": "貨道缺貨 (PDT_EMPTY)",
|
||||||
|
"Slot empty": "貨道空 (SLOT_EMPTY)",
|
||||||
|
"Slot not closed": "貨道未關閉",
|
||||||
|
"Slot motor error (0207)": "貨道電機故障 (0207)",
|
||||||
|
"Slot motor error (0208)": "貨道電機故障 (0208)",
|
||||||
|
"Slot motor error (0209)": "貨道電機故障 (0209)",
|
||||||
|
"Hopper empty (0212)": "料斗空 (0212)",
|
||||||
|
"Machine normal": "機台系統正常",
|
||||||
|
"Elevator sensor error": "升降箱感測異常",
|
||||||
|
"Pickup door not closed": "取貨門未關閉",
|
||||||
|
"Elevator failure": "升降系統故障",
|
||||||
|
"Slot": "貨道",
|
||||||
|
"Page 0": "離線",
|
||||||
|
"Page 1": "主頁面",
|
||||||
|
"Page 2": "販賣頁",
|
||||||
|
"Page 3": "管理頁",
|
||||||
|
"Page 4": "補貨頁",
|
||||||
|
"Page 5": "教學頁",
|
||||||
|
"Page 6": "購買中",
|
||||||
|
"Page 7": "鎖定頁",
|
||||||
|
"Page 60": "出貨成功",
|
||||||
|
"Page 61": "貨道測試",
|
||||||
|
"Page 62": "付款選擇",
|
||||||
|
"Page 63": "等待付款",
|
||||||
|
"Page 64": "出貨",
|
||||||
|
"Page 65": "收據簽單",
|
||||||
|
"Page 66": "通行碼",
|
||||||
|
"Page 67": "取貨碼",
|
||||||
|
"Page 68": "訊息顯示",
|
||||||
|
"Page 69": "取消購買",
|
||||||
|
"Page 610": "購買結束",
|
||||||
|
"Page 611": "來店禮",
|
||||||
|
"Page 612": "出貨失敗",
|
||||||
|
"Door Opened": "機門已開啟",
|
||||||
|
"Door Closed": "機門已關閉",
|
||||||
|
"Firmware updated to :version": "韌體版本更新::version",
|
||||||
|
"Model changed to :model": "型號變更::model",
|
||||||
|
"User logged in: :name": "使用者登入::name",
|
||||||
|
"Login failed: :account": "登入失敗::account",
|
||||||
|
"Unauthorized login attempt: :account": "越權登入嘗試::account",
|
||||||
|
"Contract Model": "合約模式",
|
||||||
|
"Warranty Service": "保固服務",
|
||||||
|
"Software Service": "軟體服務",
|
||||||
|
"Modification History": "異動歷程",
|
||||||
|
"No history records": "尚無歷史紀錄",
|
||||||
|
"Initial contract registration": "初始合約註冊",
|
||||||
|
"Contract information updated": "合約資訊已更新",
|
||||||
|
"Warranty Start": "保固起始",
|
||||||
|
"Warranty End": "保固結束",
|
||||||
|
"Software Start": "軟體起始",
|
||||||
|
"Software End": "軟體結束",
|
||||||
|
"Contract History": "合約歷程",
|
||||||
|
"Unlimited": "無限期",
|
||||||
|
"Change Note": "異動備註",
|
||||||
|
"Log Time": "記錄時間",
|
||||||
|
"Service Periods": "服務區間",
|
||||||
|
"Creator": "建立者",
|
||||||
|
"View Full History": "查看完整歷程",
|
||||||
|
"Contract Start": "合約起始",
|
||||||
|
"Contract End": "合約結束",
|
||||||
|
"Contract History Detail": "合約歷程詳情",
|
||||||
|
"by": "由",
|
||||||
|
"Service Terms": "服務期程",
|
||||||
|
"Contract": "合約",
|
||||||
|
"Warranty": "保固",
|
||||||
|
"Software": "軟體",
|
||||||
|
"Schedule": "排程區間",
|
||||||
|
"Immediate": "立即",
|
||||||
|
"Indefinite": "無限期",
|
||||||
|
"Ongoing": "進行中",
|
||||||
|
"Waiting": "等待中",
|
||||||
|
"Publish Time": "發布時間",
|
||||||
|
"Expired Time": "下架時間"
|
||||||
}
|
}
|
||||||
7
package-lock.json
generated
7
package-lock.json
generated
@@ -5,6 +5,7 @@
|
|||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"flatpickr": "^4.6.13",
|
||||||
"pptxgenjs": "^4.0.1"
|
"pptxgenjs": "^4.0.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -1506,6 +1507,12 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/flatpickr": {
|
||||||
|
"version": "4.6.13",
|
||||||
|
"resolved": "https://registry.npmjs.org/flatpickr/-/flatpickr-4.6.13.tgz",
|
||||||
|
"integrity": "sha512-97PMG/aywoYpB4IvbvUJi0RQi8vearvU0oov1WW3k0WZPBMrTQVqekSX5CjSG/M4Q3i6A/0FKXC7RyAoAUUSPw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/follow-redirects": {
|
"node_modules/follow-redirects": {
|
||||||
"version": "1.15.11",
|
"version": "1.15.11",
|
||||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
|
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
|
||||||
|
|||||||
@@ -19,6 +19,7 @@
|
|||||||
"vite": "^5.0.0"
|
"vite": "^5.0.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"flatpickr": "^4.6.13",
|
||||||
"pptxgenjs": "^4.0.1"
|
"pptxgenjs": "^4.0.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
@import 'flatpickr/dist/flatpickr.min.css';
|
||||||
|
|
||||||
@tailwind base;
|
@tailwind base;
|
||||||
@tailwind components;
|
@tailwind components;
|
||||||
@tailwind utilities;
|
@tailwind utilities;
|
||||||
@@ -155,6 +157,17 @@
|
|||||||
[x-cloak] {
|
[x-cloak] {
|
||||||
display: none !important;
|
display: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Hide scrollbar for Chrome, Safari and Opera */
|
||||||
|
.hide-scrollbar::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hide scrollbar for IE, Edge and Firefox */
|
||||||
|
.hide-scrollbar {
|
||||||
|
-ms-overflow-style: none; /* IE and Edge */
|
||||||
|
scrollbar-width: none; /* Firefox */
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@layer components {
|
@layer components {
|
||||||
@@ -317,4 +330,204 @@
|
|||||||
.luxury-select-sm .hs-select-toggle {
|
.luxury-select-sm .hs-select-toggle {
|
||||||
@apply py-2.5 text-sm !important;
|
@apply py-2.5 text-sm !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
/* Flatpickr Luxury Theme Overrides */
|
||||||
|
.flatpickr-calendar {
|
||||||
|
border: 1px solid #e2e8f0 !important;
|
||||||
|
border-radius: 1.25rem !important;
|
||||||
|
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25) !important;
|
||||||
|
background: #ffffff !important;
|
||||||
|
padding: 4px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .flatpickr-calendar {
|
||||||
|
background: #1e293b !important;
|
||||||
|
border-color: #334155 !important;
|
||||||
|
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flatpickr-day {
|
||||||
|
color: #475569 !important;
|
||||||
|
border-radius: 12px !important;
|
||||||
|
font-weight: 500 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .flatpickr-day {
|
||||||
|
color: #cbd5e1 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flatpickr-day.prevMonthDay,
|
||||||
|
.flatpickr-day.nextMonthDay {
|
||||||
|
color: #cbd5e1 !important;
|
||||||
|
opacity: 0.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .flatpickr-day.prevMonthDay,
|
||||||
|
.dark .flatpickr-day.nextMonthDay {
|
||||||
|
color: #475569 !important;
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flatpickr-day.today {
|
||||||
|
border-color: #06b6d4 !important;
|
||||||
|
color: #06b6d4 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flatpickr-day.selected {
|
||||||
|
background: linear-gradient(135deg, #06b6d4, #3b82f6) !important;
|
||||||
|
border-color: transparent !important;
|
||||||
|
color: white !important;
|
||||||
|
box-shadow: 0 8px 15px -3px rgba(6, 182, 212, 0.4) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flatpickr-day:not(.selected):hover {
|
||||||
|
background: #f1f5f9 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .flatpickr-day:not(.selected):hover {
|
||||||
|
background: #334155 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Weekdays & Header */
|
||||||
|
.flatpickr-weekday {
|
||||||
|
color: #94a3b8 !important;
|
||||||
|
font-weight: 800 !important;
|
||||||
|
font-size: 11px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .flatpickr-weekday {
|
||||||
|
color: #475569 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flatpickr-months .flatpickr-month {
|
||||||
|
color: #1e293b !important;
|
||||||
|
fill: currentColor !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .flatpickr-months .flatpickr-month {
|
||||||
|
color: #f8fafc !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flatpickr-prev-month,
|
||||||
|
.flatpickr-next-month {
|
||||||
|
color: #1e293b !important;
|
||||||
|
fill: currentColor !important;
|
||||||
|
@apply transition-colors duration-200 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .flatpickr-prev-month,
|
||||||
|
.dark .flatpickr-next-month {
|
||||||
|
color: #cbd5e1 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flatpickr-prev-month:hover,
|
||||||
|
.flatpickr-next-month:hover {
|
||||||
|
color: #06b6d4 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flatpickr-current-month .flatpickr-monthDropdown-months {
|
||||||
|
font-weight: 800 !important;
|
||||||
|
@apply bg-transparent dark:bg-slate-800 text-slate-900 dark:text-slate-100 !important;
|
||||||
|
border: none !important;
|
||||||
|
border-radius: 6px !important;
|
||||||
|
padding: 2px 4px !important;
|
||||||
|
cursor: pointer !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flatpickr-monthDropdown-month {
|
||||||
|
@apply bg-white dark:bg-slate-800 text-slate-900 dark:text-slate-100 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Time Section */
|
||||||
|
.flatpickr-time {
|
||||||
|
border-top: 1px solid #f1f5f9 !important;
|
||||||
|
margin-top: 8px !important;
|
||||||
|
padding-top: 8px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .flatpickr-time {
|
||||||
|
border-top-color: #334155 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flatpickr-time input {
|
||||||
|
color: #1e293b !important;
|
||||||
|
font-weight: 800 !important;
|
||||||
|
border-radius: 8px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .flatpickr-time input {
|
||||||
|
color: #f8fafc !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flatpickr-time input:hover,
|
||||||
|
.flatpickr-time input:focus {
|
||||||
|
background: #f1f5f9 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .flatpickr-time input:hover,
|
||||||
|
.dark .flatpickr-time input:focus {
|
||||||
|
background: #334155 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .flatpickr-time .flatpickr-am-pm {
|
||||||
|
color: #f8fafc !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Time Stepper Arrows & Separator */
|
||||||
|
.flatpickr-time .numInputWrapper span.arrowUp:after {
|
||||||
|
border-bottom-color: #94a3b8 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flatpickr-time .numInputWrapper span.arrowUp:hover:after {
|
||||||
|
border-bottom-color: #06b6d4 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flatpickr-time .numInputWrapper span.arrowDown:after {
|
||||||
|
border-top-color: #94a3b8 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flatpickr-time .numInputWrapper span.arrowDown:hover:after {
|
||||||
|
border-top-color: #06b6d4 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .flatpickr-time .numInputWrapper span.arrowUp:after {
|
||||||
|
border-bottom-color: #64748b !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .flatpickr-time .numInputWrapper span.arrowUp:hover:after {
|
||||||
|
border-bottom-color: #06b6d4 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .flatpickr-time .numInputWrapper span.arrowDown:after {
|
||||||
|
border-top-color: #64748b !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .flatpickr-time .numInputWrapper span.arrowDown:hover:after {
|
||||||
|
border-top-color: #06b6d4 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flatpickr-time .numInputWrapper span {
|
||||||
|
border-color: #f1f5f9 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .flatpickr-time .numInputWrapper span {
|
||||||
|
border-color: #334155 !important;
|
||||||
|
background: transparent !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .flatpickr-time .numInputWrapper span:hover {
|
||||||
|
background: #334155 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flatpickr-time .flatpickr-time-separator {
|
||||||
|
color: #94a3b8 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .flatpickr-time .flatpickr-time-separator {
|
||||||
|
color: #64748b !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flatpickr-weekdays {
|
||||||
|
background: transparent !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,11 +3,32 @@ import './bootstrap';
|
|||||||
import Alpine from 'alpinejs';
|
import Alpine from 'alpinejs';
|
||||||
import collapse from '@alpinejs/collapse';
|
import collapse from '@alpinejs/collapse';
|
||||||
|
|
||||||
|
// 初始化 Preline UI
|
||||||
|
import 'preline';
|
||||||
|
|
||||||
|
// 引入 Flatpickr 與語系
|
||||||
|
import flatpickr from "flatpickr";
|
||||||
|
import { MandarinTraditional } from "flatpickr/dist/l10n/zh-tw.js";
|
||||||
|
import { Japanese } from "flatpickr/dist/l10n/ja.js";
|
||||||
|
|
||||||
|
const docLang = document.documentElement.lang.toLowerCase();
|
||||||
|
|
||||||
|
if (docLang.includes('zh')) {
|
||||||
|
flatpickr.localize(MandarinTraditional);
|
||||||
|
window.flatpickrLocale = MandarinTraditional;
|
||||||
|
} else if (docLang.includes('ja')) {
|
||||||
|
flatpickr.localize(Japanese);
|
||||||
|
window.flatpickrLocale = Japanese;
|
||||||
|
} else {
|
||||||
|
// English is the default in flatpickr
|
||||||
|
window.flatpickrLocale = 'default';
|
||||||
|
}
|
||||||
|
|
||||||
|
window.flatpickr = flatpickr;
|
||||||
|
|
||||||
Alpine.plugin(collapse);
|
Alpine.plugin(collapse);
|
||||||
|
|
||||||
window.Alpine = Alpine;
|
window.Alpine = Alpine;
|
||||||
|
|
||||||
|
// 確保其他套件都初始化完成後再啟動 Alpine
|
||||||
Alpine.start();
|
Alpine.start();
|
||||||
|
|
||||||
// 初始化 Preline UI
|
|
||||||
import 'preline';
|
|
||||||
|
|||||||
@@ -71,6 +71,7 @@ $baseRoute = 'admin.data-config.advertisements';
|
|||||||
@endif
|
@endif
|
||||||
<th class="px-6 py-4 text-xs font-bold text-slate-500 dark:text-slate-400 uppercase tracking-[0.15em] border-b border-slate-100 dark:border-slate-800 text-center">{{ __('Type') }}</th>
|
<th class="px-6 py-4 text-xs font-bold text-slate-500 dark:text-slate-400 uppercase tracking-[0.15em] border-b border-slate-100 dark:border-slate-800 text-center">{{ __('Type') }}</th>
|
||||||
<th class="px-6 py-4 text-xs font-bold text-slate-500 dark:text-slate-400 uppercase tracking-[0.15em] border-b border-slate-100 dark:border-slate-800 text-center">{{ __('Duration') }}</th>
|
<th class="px-6 py-4 text-xs font-bold text-slate-500 dark:text-slate-400 uppercase tracking-[0.15em] border-b border-slate-100 dark:border-slate-800 text-center">{{ __('Duration') }}</th>
|
||||||
|
<th class="px-6 py-4 text-xs font-bold text-slate-500 dark:text-slate-400 uppercase tracking-[0.15em] border-b border-slate-100 dark:border-slate-800 text-center">{{ __('Schedule') }}</th>
|
||||||
<th class="px-6 py-4 text-xs font-bold text-slate-500 dark:text-slate-400 uppercase tracking-[0.15em] border-b border-slate-100 dark:border-slate-800 text-center">{{ __('Status') }}</th>
|
<th class="px-6 py-4 text-xs font-bold text-slate-500 dark:text-slate-400 uppercase tracking-[0.15em] border-b border-slate-100 dark:border-slate-800 text-center">{{ __('Status') }}</th>
|
||||||
<th class="px-6 py-4 text-xs font-bold text-slate-500 dark:text-slate-400 uppercase tracking-[0.15em] border-b border-slate-100 dark:border-slate-800 text-right">{{ __('Actions') }}</th>
|
<th class="px-6 py-4 text-xs font-bold text-slate-500 dark:text-slate-400 uppercase tracking-[0.15em] border-b border-slate-100 dark:border-slate-800 text-right">{{ __('Actions') }}</th>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -104,15 +105,34 @@ $baseRoute = 'admin.data-config.advertisements';
|
|||||||
{{ __($ad->type) }}
|
{{ __($ad->type) }}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td class="px-6 py-4 text-center whitespace-nowrap text-sm font-black text-slate-700 dark:text-slate-200">
|
<td class="px-6 py-4 text-center whitespace-nowrap text-sm font-mono font-bold text-slate-700 dark:text-slate-200">
|
||||||
{{ $ad->duration }}s
|
{{ $ad->duration }}s
|
||||||
</td>
|
</td>
|
||||||
|
<td class="px-6 py-4 text-center whitespace-nowrap">
|
||||||
|
<div class="flex flex-col items-center gap-0.5">
|
||||||
|
<span class="text-[11px] font-mono font-bold text-slate-500 dark:text-slate-400 uppercase tracking-tight">{{ __('From') }}: {{ $ad->start_at?->format('Y-m-d H:i') ?? __('Immediate') }}</span>
|
||||||
|
<span class="text-[11px] font-mono font-bold text-slate-500 dark:text-slate-400 uppercase tracking-tight">{{ __('To') }}: {{ $ad->end_at?->format('Y-m-d H:i') ?? __('Indefinite') }}</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
<td class="px-6 py-4 text-center">
|
<td class="px-6 py-4 text-center">
|
||||||
@if($ad->is_active)
|
@php
|
||||||
<span class="inline-flex items-center px-3 py-1 rounded-full text-[11px] font-black bg-emerald-500/10 text-emerald-500 border border-emerald-500/20 tracking-widest uppercase">{{ __('Active') }}</span>
|
$now = now();
|
||||||
@else
|
$isStarted = !$ad->start_at || $ad->start_at <= $now;
|
||||||
<span class="inline-flex items-center px-3 py-1 rounded-full text-[11px] font-black bg-rose-500/10 text-rose-500 border border-rose-500/20 tracking-widest uppercase">{{ __('Disabled') }}</span>
|
$isExpired = $ad->end_at && $ad->end_at < $now;
|
||||||
@endif
|
$isPlaying = $ad->is_active && $isStarted && !$isExpired;
|
||||||
|
@endphp
|
||||||
|
|
||||||
|
<div class="flex flex-col items-center gap-1">
|
||||||
|
@if(!$ad->is_active)
|
||||||
|
<span class="inline-flex items-center px-3 py-1 rounded-full text-[10px] font-black bg-slate-500/10 text-slate-500 border border-slate-500/20 tracking-widest uppercase">{{ __('Disabled') }}</span>
|
||||||
|
@elseif($isExpired)
|
||||||
|
<span class="inline-flex items-center px-3 py-1 rounded-full text-[10px] font-black bg-rose-500/10 text-rose-500 border border-rose-500/20 tracking-widest uppercase">{{ __('Expired') }}</span>
|
||||||
|
@elseif(!$isStarted)
|
||||||
|
<span class="inline-flex items-center px-3 py-1 rounded-full text-[10px] font-black bg-amber-500/10 text-amber-500 border border-amber-500/20 tracking-widest uppercase">{{ __('Waiting') }}</span>
|
||||||
|
@else
|
||||||
|
<span class="inline-flex items-center px-3 py-1 rounded-full text-[10px] font-black bg-emerald-500/10 text-emerald-500 border border-emerald-500/20 tracking-widest uppercase">{{ __('Ongoing') }}</span>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td class="px-6 py-4 text-right">
|
<td class="px-6 py-4 text-right">
|
||||||
<div class="flex justify-end items-center gap-2">
|
<div class="flex justify-end items-center gap-2">
|
||||||
@@ -211,7 +231,7 @@ $baseRoute = 'admin.data-config.advertisements';
|
|||||||
</button>
|
</button>
|
||||||
<div class="flex-1 min-w-0 flex flex-col justify-center cursor-pointer group-hover:text-cyan-500 transition-colors" @click="openPreview(assign.advertisement)">
|
<div class="flex-1 min-w-0 flex flex-col justify-center cursor-pointer group-hover:text-cyan-500 transition-colors" @click="openPreview(assign.advertisement)">
|
||||||
<p class="text-xs font-black text-slate-700 dark:text-white truncate transition-colors" x-text="assign.advertisement.name"></p>
|
<p class="text-xs font-black text-slate-700 dark:text-white truncate transition-colors" x-text="assign.advertisement.name"></p>
|
||||||
<p class="text-[10px] font-bold text-slate-400 uppercase tracking-tighter mt-0.5" x-text="assign.advertisement.duration + 's'"></p>
|
<p class="text-[11px] font-mono font-bold text-slate-400 uppercase tracking-tight mt-0.5" x-text="assign.advertisement.duration + 's'"></p>
|
||||||
</div>
|
</div>
|
||||||
<button @click="removeAssignment(assign.id)" class="p-1.5 text-slate-300 hover:text-rose-500 transition-colors">
|
<button @click="removeAssignment(assign.id)" class="p-1.5 text-slate-300 hover:text-rose-500 transition-colors">
|
||||||
<svg class="size-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5"><path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" /></svg>
|
<svg class="size-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5"><path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" /></svg>
|
||||||
@@ -324,7 +344,7 @@ $baseRoute = 'admin.data-config.advertisements';
|
|||||||
<span class="w-1.5 h-1.5 rounded-full bg-white/20"></span>
|
<span class="w-1.5 h-1.5 rounded-full bg-white/20"></span>
|
||||||
<span class="text-cyan-400 font-black tracking-widest text-xs uppercase" x-text="(currentSequenceIndex + 1) + ' / ' + sequenceAds.length"></span>
|
<span class="text-cyan-400 font-black tracking-widest text-xs uppercase" x-text="(currentSequenceIndex + 1) + ' / ' + sequenceAds.length"></span>
|
||||||
<span class="w-1.5 h-1.5 rounded-full bg-white/20"></span>
|
<span class="w-1.5 h-1.5 rounded-full bg-white/20"></span>
|
||||||
<span class="text-white/80 font-bold tracking-widest text-xs tabular-nums" x-text="Math.ceil(sequenceRemainingTime) + 's'"></span>
|
<span class="text-white/80 font-mono font-bold tracking-tight text-xs tabular-nums" x-text="Math.ceil(sequenceRemainingTime) + 's'"></span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
@@ -385,7 +405,9 @@ $baseRoute = 'admin.data-config.advertisements';
|
|||||||
type: 'image',
|
type: 'image',
|
||||||
duration: 15,
|
duration: 15,
|
||||||
is_active: true,
|
is_active: true,
|
||||||
url: ''
|
url: '',
|
||||||
|
start_at: '',
|
||||||
|
end_at: ''
|
||||||
},
|
},
|
||||||
|
|
||||||
// Assign Modal
|
// Assign Modal
|
||||||
@@ -604,6 +626,17 @@ $baseRoute = 'admin.data-config.advertisements';
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
formatDateForInput(dateStr) {
|
||||||
|
if (!dateStr) return '';
|
||||||
|
const date = new Date(dateStr);
|
||||||
|
const year = date.getFullYear();
|
||||||
|
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||||||
|
const day = String(date.getDate()).padStart(2, '0');
|
||||||
|
const hours = String(date.getHours()).padStart(2, '0');
|
||||||
|
const minutes = String(date.getMinutes()).padStart(2, '0');
|
||||||
|
return `${year}/${month}/${day} ${hours}:${minutes}`;
|
||||||
|
},
|
||||||
|
|
||||||
async submitAssignment() {
|
async submitAssignment() {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(this.urls.assign, {
|
const response = await fetch(this.urls.assign, {
|
||||||
@@ -650,6 +683,14 @@ $baseRoute = 'admin.data-config.advertisements';
|
|||||||
if (document.querySelector('#ad_company_select')) {
|
if (document.querySelector('#ad_company_select')) {
|
||||||
window.HSSelect.getInstance('#ad_company_select')?.setValue(this.adForm.company_id || '');
|
window.HSSelect.getInstance('#ad_company_select')?.setValue(this.adForm.company_id || '');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 確保 Flatpickr 實例同步顯示目前的時間值
|
||||||
|
if (this.$refs.startAtPicker?._flatpickr) {
|
||||||
|
this.$refs.startAtPicker._flatpickr.setDate(this.adForm.start_at);
|
||||||
|
}
|
||||||
|
if (this.$refs.endAtPicker?._flatpickr) {
|
||||||
|
this.$refs.endAtPicker._flatpickr.setDate(this.adForm.end_at);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -685,7 +726,7 @@ $baseRoute = 'admin.data-config.advertisements';
|
|||||||
|
|
||||||
openAddModal() {
|
openAddModal() {
|
||||||
this.adFormMode = 'add';
|
this.adFormMode = 'add';
|
||||||
this.adForm = { id: null, company_id: '', name: '', type: 'image', duration: 15, is_active: true, url: '' };
|
this.adForm = { id: null, company_id: '', name: '', type: 'image', duration: 15, is_active: true, url: '', start_at: '', end_at: '' };
|
||||||
this.fileName = '';
|
this.fileName = '';
|
||||||
this.mediaPreview = null;
|
this.mediaPreview = null;
|
||||||
this.isAdModalOpen = true;
|
this.isAdModalOpen = true;
|
||||||
@@ -693,7 +734,11 @@ $baseRoute = 'admin.data-config.advertisements';
|
|||||||
|
|
||||||
openEditModal(ad) {
|
openEditModal(ad) {
|
||||||
this.adFormMode = 'edit';
|
this.adFormMode = 'edit';
|
||||||
this.adForm = { ...ad };
|
this.adForm = {
|
||||||
|
...ad,
|
||||||
|
start_at: this.formatDateForInput(ad.start_at),
|
||||||
|
end_at: this.formatDateForInput(ad.end_at)
|
||||||
|
};
|
||||||
this.fileName = '';
|
this.fileName = '';
|
||||||
this.mediaPreview = ad.url; // Use existing URL as preview
|
this.mediaPreview = ad.url; // Use existing URL as preview
|
||||||
this.isAdModalOpen = true;
|
this.isAdModalOpen = true;
|
||||||
|
|||||||
@@ -94,6 +94,51 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Scheduling -->
|
||||||
|
<div class="grid grid-cols-2 gap-4">
|
||||||
|
<div class="space-y-2">
|
||||||
|
<label class="text-xs font-black text-slate-500 dark:text-slate-400 uppercase tracking-widest ml-1">
|
||||||
|
{{ __('Publish Time') }}
|
||||||
|
</label>
|
||||||
|
<div class="relative group/input">
|
||||||
|
<input type="text" name="start_at" x-ref="startAtPicker" x-model="adForm.start_at"
|
||||||
|
x-init="flatpickr($refs.startAtPicker, {
|
||||||
|
enableTime: true,
|
||||||
|
dateFormat: 'Y/m/d H:i',
|
||||||
|
time_24hr: true,
|
||||||
|
locale: window.flatpickrLocale,
|
||||||
|
onClose: (selectedDates, dateStr) => { adForm.start_at = dateStr; }
|
||||||
|
})"
|
||||||
|
class="w-full h-12 bg-slate-50 dark:bg-slate-800/50 border-none rounded-xl px-4 pr-10 text-sm font-bold text-slate-800 dark:text-white focus:ring-2 focus:ring-cyan-500/20 transition-all placeholder:text-slate-400"
|
||||||
|
placeholder="YYYY/MM/DD HH:MM">
|
||||||
|
<div class="absolute right-4 top-1/2 -translate-y-1/2 text-slate-400 group-hover/input:text-cyan-500 transition-colors pointer-events-none">
|
||||||
|
<svg class="size-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" 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>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="space-y-2">
|
||||||
|
<label class="text-xs font-black text-slate-500 dark:text-slate-400 uppercase tracking-widest ml-1">
|
||||||
|
{{ __('Expired Time') }}
|
||||||
|
</label>
|
||||||
|
<div class="relative group/input">
|
||||||
|
<input type="text" name="end_at" x-ref="endAtPicker" x-model="adForm.end_at"
|
||||||
|
x-init="flatpickr($refs.endAtPicker, {
|
||||||
|
enableTime: true,
|
||||||
|
dateFormat: 'Y/m/d H:i',
|
||||||
|
time_24hr: true,
|
||||||
|
locale: window.flatpickrLocale,
|
||||||
|
onClose: (selectedDates, dateStr) => { adForm.end_at = dateStr; }
|
||||||
|
})"
|
||||||
|
class="w-full h-12 bg-slate-50 dark:bg-slate-800/50 border-none rounded-xl px-4 pr-10 text-sm font-bold text-slate-800 dark:text-white focus:ring-2 focus:ring-cyan-500/20 transition-all placeholder:text-slate-400"
|
||||||
|
placeholder="YYYY/MM/DD HH:MM">
|
||||||
|
<div class="absolute right-4 top-1/2 -translate-y-1/2 text-slate-400 group-hover/input:text-cyan-500 transition-colors pointer-events-none">
|
||||||
|
<svg class="size-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><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>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- File Upload (Luxury UI Pattern) -->
|
<!-- File Upload (Luxury UI Pattern) -->
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
<label class="text-xs font-black text-slate-500 dark:text-slate-400 uppercase tracking-widest ml-1">
|
<label class="text-xs font-black text-slate-500 dark:text-slate-400 uppercase tracking-widest ml-1">
|
||||||
|
|||||||
@@ -2,8 +2,11 @@
|
|||||||
|
|
||||||
@section('content')
|
@section('content')
|
||||||
<div class="space-y-6" x-data="{
|
<div class="space-y-6" x-data="{
|
||||||
|
|
||||||
showModal: false,
|
showModal: false,
|
||||||
|
showHistoryModal: false,
|
||||||
editing: false,
|
editing: false,
|
||||||
|
sidebarView: 'detail',
|
||||||
currentCompany: {
|
currentCompany: {
|
||||||
id: '',
|
id: '',
|
||||||
name: '',
|
name: '',
|
||||||
@@ -16,6 +19,10 @@
|
|||||||
contact_email: '',
|
contact_email: '',
|
||||||
start_date: '',
|
start_date: '',
|
||||||
end_date: '',
|
end_date: '',
|
||||||
|
warranty_start_date: '',
|
||||||
|
warranty_end_date: '',
|
||||||
|
software_start_date: '',
|
||||||
|
software_end_date: '',
|
||||||
status: 1,
|
status: 1,
|
||||||
note: '',
|
note: '',
|
||||||
settings: {
|
settings: {
|
||||||
@@ -28,7 +35,10 @@
|
|||||||
this.currentCompany = {
|
this.currentCompany = {
|
||||||
id: '', name: '', code: '', original_type: 'lease', current_type: 'lease',
|
id: '', name: '', code: '', original_type: 'lease', current_type: 'lease',
|
||||||
tax_id: '', contact_name: '', contact_phone: '',
|
tax_id: '', contact_name: '', contact_phone: '',
|
||||||
contact_email: '', start_date: '', end_date: '', status: 1, note: '',
|
contact_email: '', start_date: '', end_date: '',
|
||||||
|
warranty_start_date: '', warranty_end_date: '',
|
||||||
|
software_start_date: '', software_end_date: '',
|
||||||
|
status: 1, note: '',
|
||||||
settings: { enable_material_code: false, enable_points: false }
|
settings: { enable_material_code: false, enable_points: false }
|
||||||
};
|
};
|
||||||
this.showModal = true;
|
this.showModal = true;
|
||||||
@@ -39,6 +49,10 @@
|
|||||||
...company,
|
...company,
|
||||||
start_date: company.start_date ? company.start_date.substring(0, 10) : '',
|
start_date: company.start_date ? company.start_date.substring(0, 10) : '',
|
||||||
end_date: company.end_date ? company.end_date.substring(0, 10) : '',
|
end_date: company.end_date ? company.end_date.substring(0, 10) : '',
|
||||||
|
warranty_start_date: company.warranty_start_date ? company.warranty_start_date.substring(0, 10) : '',
|
||||||
|
warranty_end_date: company.warranty_end_date ? company.warranty_end_date.substring(0, 10) : '',
|
||||||
|
software_start_date: company.software_start_date ? company.software_start_date.substring(0, 10) : '',
|
||||||
|
software_end_date: company.software_end_date ? company.software_end_date.substring(0, 10) : '',
|
||||||
settings: {
|
settings: {
|
||||||
enable_material_code: company.settings?.enable_material_code || false,
|
enable_material_code: company.settings?.enable_material_code || false,
|
||||||
enable_points: company.settings?.enable_points || false
|
enable_points: company.settings?.enable_points || false
|
||||||
@@ -57,9 +71,13 @@
|
|||||||
detailCompany: {
|
detailCompany: {
|
||||||
id: '', name: '', code: '', original_type: 'lease', current_type: 'lease',
|
id: '', name: '', code: '', original_type: 'lease', current_type: 'lease',
|
||||||
tax_id: '', contact_name: '', contact_phone: '', contact_email: '',
|
tax_id: '', contact_name: '', contact_phone: '', contact_email: '',
|
||||||
start_date: '', end_date: '', status: 1, note: '',
|
start_date: '', end_date: '',
|
||||||
|
warranty_start_date: '', warranty_end_date: '',
|
||||||
|
software_start_date: '', software_end_date: '',
|
||||||
|
status: 1, note: '',
|
||||||
settings: { enable_material_code: false, enable_points: false },
|
settings: { enable_material_code: false, enable_points: false },
|
||||||
users_count: 0, machines_count: 0
|
users_count: 0, machines_count: 0,
|
||||||
|
contracts: []
|
||||||
},
|
},
|
||||||
openDetailSidebar(company) {
|
openDetailSidebar(company) {
|
||||||
this.detailCompany = {
|
this.detailCompany = {
|
||||||
@@ -69,15 +87,40 @@
|
|||||||
enable_points: company.settings?.enable_points || false
|
enable_points: company.settings?.enable_points || false
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
this.sidebarView = 'detail';
|
||||||
this.showDetail = true;
|
this.showDetail = true;
|
||||||
},
|
},
|
||||||
|
openHistorySidebar(company) {
|
||||||
|
this.detailCompany = {
|
||||||
|
...company,
|
||||||
|
settings: {
|
||||||
|
enable_material_code: company.settings?.enable_material_code || false,
|
||||||
|
enable_points: company.settings?.enable_points || false
|
||||||
|
}
|
||||||
|
};
|
||||||
|
this.sidebarView = 'history';
|
||||||
|
this.showDetail = true;
|
||||||
|
},
|
||||||
|
openFullHistory() {
|
||||||
|
this.showHistoryModal = true;
|
||||||
|
},
|
||||||
|
openHistory(company) {
|
||||||
|
this.detailCompany = {
|
||||||
|
...company,
|
||||||
|
settings: {
|
||||||
|
enable_material_code: company.settings?.enable_material_code || false,
|
||||||
|
enable_points: company.settings?.enable_points || false
|
||||||
|
}
|
||||||
|
};
|
||||||
|
this.showHistoryModal = true;
|
||||||
|
},
|
||||||
submitConfirmedForm() {
|
submitConfirmedForm() {
|
||||||
if (this.statusToggleSource === 'list') {
|
if (this.statusToggleSource === 'list') {
|
||||||
this.$refs.statusToggleForm.submit();
|
this.$refs.statusToggleForm.submit();
|
||||||
} else {
|
} else {
|
||||||
this.$refs.companyForm.submit();
|
this.$refs.companyForm.submit();
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
}">
|
}">
|
||||||
<!-- Header -->
|
<!-- Header -->
|
||||||
<div class="flex flex-col md:flex-row md:items-center justify-between gap-6">
|
<div class="flex flex-col md:flex-row md:items-center justify-between gap-6">
|
||||||
@@ -164,7 +207,7 @@
|
|||||||
{{ __('Accounts / Machines') }}</th>
|
{{ __('Accounts / Machines') }}</th>
|
||||||
<th
|
<th
|
||||||
class="px-6 py-4 text-[12px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-widest border-b border-slate-100 dark:border-slate-800 text-center">
|
class="px-6 py-4 text-[12px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-widest border-b border-slate-100 dark:border-slate-800 text-center">
|
||||||
{{ __('Contract Period') }}</th>
|
{{ __('Service Terms') }}</th>
|
||||||
<th
|
<th
|
||||||
class="px-6 py-4 text-[12px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-widest border-b border-slate-100 dark:border-slate-800 text-right">
|
class="px-6 py-4 text-[12px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-widest border-b border-slate-100 dark:border-slate-800 text-right">
|
||||||
{{ __('Actions') }}</th>
|
{{ __('Actions') }}</th>
|
||||||
@@ -235,20 +278,53 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td class="px-6 py-6 text-center text-slate-500 dark:text-slate-400">
|
<td class="px-6 py-6 border-b border-slate-50 dark:border-slate-800/50">
|
||||||
<div class="flex flex-col items-center gap-1.5 font-mono">
|
<div class="flex flex-col gap-2.5 min-w-[200px] mx-auto w-fit">
|
||||||
<div class="flex items-center gap-2 min-w-[130px] justify-center">
|
<!-- Contract Period (Only for Lease) -->
|
||||||
<span class="text-[10px] font-bold text-slate-400 uppercase tracking-tighter">{{ __('From:') }}</span>
|
@if($company->current_type === 'lease')
|
||||||
<span class="text-[13px] font-bold tracking-tighter text-slate-600 dark:text-slate-300">
|
<div class="flex items-center gap-3 group/term">
|
||||||
{{ $company->start_date ? $company->start_date->format('Y-m-d') : '--' }}
|
<span class="px-2 py-0.5 rounded-md bg-blue-500/10 text-blue-600 text-[10px] font-black border border-blue-500/20 group-hover/term:bg-blue-500 group-hover/term:text-white transition-all tracking-wider whitespace-nowrap min-w-[42px] text-center">
|
||||||
|
{{ __('Contract') }}
|
||||||
</span>
|
</span>
|
||||||
|
<div class="flex items-center gap-1.5 font-mono text-[11px] font-bold">
|
||||||
|
<span class="text-slate-400">{{ $company->start_date ? $company->start_date->format('Y-m-d') : '--' }}</span>
|
||||||
|
<span class="text-slate-300">~</span>
|
||||||
|
<span class="{{ $company->end_date && $company->end_date->isPast() ? 'text-rose-500 shadow-sm shadow-rose-500/10' : 'text-slate-600 dark:text-slate-300' }}">
|
||||||
|
{{ $company->end_date ? $company->end_date->format('Y-m-d') : __('Permanent') }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-2 min-w-[130px] justify-center">
|
@endif
|
||||||
<span class="text-[10px] font-bold text-slate-400 uppercase tracking-tighter">{{ __('To:') }}</span>
|
|
||||||
<span class="text-[13px] font-bold tracking-tighter {{ $company->end_date && $company->end_date->isPast() ? 'text-rose-500' : 'text-slate-800 dark:text-slate-200' }}">
|
@if($company->current_type === 'buyout')
|
||||||
{{ $company->end_date ? $company->end_date->format('Y-m-d') : __('Permanent') }}
|
<!-- Warranty Period -->
|
||||||
|
<div class="flex items-center gap-3 group/term">
|
||||||
|
<span class="px-2 py-0.5 rounded-md bg-amber-500/10 text-amber-600 text-[10px] font-black border border-amber-500/20 group-hover/term:bg-amber-500 group-hover/term:text-white transition-all tracking-wider whitespace-nowrap min-w-[42px] text-center">
|
||||||
|
{{ __('Warranty') }}
|
||||||
</span>
|
</span>
|
||||||
|
<div class="flex items-center gap-1.5 font-mono text-[11px] font-bold">
|
||||||
|
<span class="text-slate-400">{{ $company->warranty_start_date ? $company->warranty_start_date->format('Y-m-d') : '--' }}</span>
|
||||||
|
<span class="text-slate-300">~</span>
|
||||||
|
<span class="{{ $company->warranty_end_date && $company->warranty_end_date->isPast() ? 'text-rose-500 shadow-sm shadow-rose-500/10' : 'text-slate-500 dark:text-slate-400' }}">
|
||||||
|
{{ $company->warranty_end_date ? $company->warranty_end_date->format('Y-m-d') : '--' }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Software Period -->
|
||||||
|
<div class="flex items-center gap-3 group/term">
|
||||||
|
<span class="px-2 py-0.5 rounded-md bg-purple-500/10 text-purple-600 text-[10px] font-black border border-purple-500/20 group-hover/term:bg-purple-500 group-hover/term:text-white transition-all tracking-wider whitespace-nowrap min-w-[42px] text-center">
|
||||||
|
{{ __('Software') }}
|
||||||
|
</span>
|
||||||
|
<div class="flex items-center gap-1.5 font-mono text-[11px] font-bold">
|
||||||
|
<span class="text-slate-400">{{ $company->software_start_date ? $company->software_start_date->format('Y-m-d') : '--' }}</span>
|
||||||
|
<span class="text-slate-300">~</span>
|
||||||
|
<span class="{{ $company->software_end_date && $company->software_end_date->isPast() ? 'text-rose-500 shadow-sm shadow-rose-500/10' : 'text-slate-500 dark:text-slate-400' }}">
|
||||||
|
{{ $company->software_end_date ? $company->software_end_date->format('Y-m-d') : '--' }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td class="px-6 py-6 text-right">
|
<td class="px-6 py-6 text-right">
|
||||||
@@ -447,11 +523,11 @@
|
|||||||
<input type="text" name="tax_id" x-model="currentCompany.tax_id"
|
<input type="text" name="tax_id" x-model="currentCompany.tax_id"
|
||||||
class="luxury-input w-full">
|
class="luxury-input w-full">
|
||||||
</div>
|
</div>
|
||||||
<div class="grid grid-cols-2 gap-3">
|
<div class="grid grid-cols-2 gap-3" x-show="currentCompany.current_type === 'lease'">
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
<label class="text-xs font-black text-slate-500 uppercase tracking-widest pl-1">{{
|
<label class="text-xs font-black text-slate-500 uppercase tracking-widest pl-1">{{
|
||||||
__('Start Date') }} <span class="text-rose-500 ml-0.5">*</span></label>
|
__('Start Date') }} <span class="text-rose-500 ml-0.5">*</span></label>
|
||||||
<input type="date" name="start_date" x-model="currentCompany.start_date" required
|
<input type="date" name="start_date" x-model="currentCompany.start_date" :required="currentCompany.current_type === 'lease'"
|
||||||
class="luxury-input w-full px-2">
|
class="luxury-input w-full px-2">
|
||||||
</div>
|
</div>
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
@@ -462,6 +538,38 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Buyout Specific Dates -->
|
||||||
|
<div x-show="currentCompany.current_type === 'buyout'" x-transition class="space-y-4 pt-2">
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
|
<div class="grid grid-cols-2 gap-3">
|
||||||
|
<div class="space-y-2">
|
||||||
|
<label class="text-[10px] font-black text-amber-600 uppercase tracking-widest pl-1">{{ __('Warranty Start') }}</label>
|
||||||
|
<input type="date" name="warranty_start_date" x-model="currentCompany.warranty_start_date"
|
||||||
|
class="luxury-input w-full px-2 border-amber-100 dark:border-amber-900/30">
|
||||||
|
</div>
|
||||||
|
<div class="space-y-2">
|
||||||
|
<label class="text-[10px] font-black text-amber-600 uppercase tracking-widest pl-1">{{ __('Warranty End') }}</label>
|
||||||
|
<input type="date" name="warranty_end_date" x-model="currentCompany.warranty_end_date"
|
||||||
|
class="luxury-input w-full px-2 border-amber-100 dark:border-amber-900/30">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="grid grid-cols-2 gap-3">
|
||||||
|
<div class="space-y-2">
|
||||||
|
<label class="text-[10px] font-black text-indigo-600 uppercase tracking-widest pl-1">{{ __('Software Start') }}</label>
|
||||||
|
<input type="date" name="software_start_date" x-model="currentCompany.software_start_date"
|
||||||
|
class="luxury-input w-full px-2 border-indigo-100 dark:border-indigo-900/30">
|
||||||
|
</div>
|
||||||
|
<div class="space-y-2">
|
||||||
|
<label class="text-[10px] font-black text-indigo-600 uppercase tracking-widest pl-1">{{ __('Software End') }}</label>
|
||||||
|
<input type="date" name="software_end_date" x-model="currentCompany.software_end_date"
|
||||||
|
class="luxury-input w-full px-2 border-indigo-100 dark:border-indigo-900/30">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Keep hidden start_date for buyout as well if needed by backend -->
|
||||||
|
<input type="hidden" name="start_date" :value="currentCompany.start_date" x-if="currentCompany.current_type === 'buyout' && !currentCompany.start_date">
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Admin Account Section (Account Creation) - Only show when creating -->
|
<!-- Admin Account Section (Account Creation) - Only show when creating -->
|
||||||
@@ -652,7 +760,7 @@
|
|||||||
<!-- Header -->
|
<!-- Header -->
|
||||||
<div class="px-8 py-6 border-b border-slate-100 dark:border-slate-800 flex items-center justify-between bg-white dark:bg-slate-900 sticky top-0 z-10">
|
<div class="px-8 py-6 border-b border-slate-100 dark:border-slate-800 flex items-center justify-between bg-white dark:bg-slate-900 sticky top-0 z-10">
|
||||||
<div>
|
<div>
|
||||||
<h2 class="text-xl font-black text-slate-800 dark:text-white tracking-tight">{{ __('Customer Details') }}</h2>
|
<h2 class="text-xl font-black text-slate-800 dark:text-white tracking-tight" x-text="sidebarView === 'history' ? '{{ __('Contract History Detail') }}' : '{{ __('Customer Details') }}'"></h2>
|
||||||
<div class="flex items-center gap-2 mt-1">
|
<div class="flex items-center gap-2 mt-1">
|
||||||
<p class="text-xs font-bold text-slate-400 uppercase tracking-[0.2em]" x-text="detailCompany.name"></p>
|
<p class="text-xs font-bold text-slate-400 uppercase tracking-[0.2em]" x-text="detailCompany.name"></p>
|
||||||
<span class="text-xs font-mono font-black text-cyan-500 px-1.5 py-0.5 bg-cyan-500/10 rounded" x-text="detailCompany.code"></span>
|
<span class="text-xs font-mono font-black text-cyan-500 px-1.5 py-0.5 bg-cyan-500/10 rounded" x-text="detailCompany.code"></span>
|
||||||
@@ -666,7 +774,19 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Body -->
|
<!-- Body -->
|
||||||
<div class="flex-1 overflow-y-auto px-8 py-8 space-y-8 custom-scrollbar">
|
<div class="flex-1 overflow-y-auto px-8 pt-4 pb-8 space-y-5 custom-scrollbar">
|
||||||
|
<!-- Tab Switcher -->
|
||||||
|
<div class="flex gap-1 p-1 bg-slate-100 dark:bg-slate-800/60 rounded-xl">
|
||||||
|
<button @click="sidebarView = 'detail'"
|
||||||
|
:class="sidebarView === 'detail' ? 'bg-white dark:bg-slate-700 text-slate-800 dark:text-white shadow-sm' : 'text-slate-400 hover:text-slate-600'"
|
||||||
|
class="flex-1 py-2.5 px-3 text-xs font-black uppercase tracking-[0.15em] rounded-lg transition-all">{{ __('Customer Details') }}</button>
|
||||||
|
<button @click="sidebarView = 'history'"
|
||||||
|
:class="sidebarView === 'history' ? 'bg-white dark:bg-slate-700 text-slate-800 dark:text-white shadow-sm' : 'text-slate-400 hover:text-slate-600'"
|
||||||
|
class="flex-1 py-2.5 px-3 text-xs font-black uppercase tracking-[0.15em] rounded-lg transition-all">{{ __('Contract History Detail') }}</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Detail View -->
|
||||||
|
<div x-show="sidebarView === 'detail'" class="space-y-8">
|
||||||
<!-- Validity & Status Section -->
|
<!-- Validity & Status Section -->
|
||||||
<section class="space-y-4">
|
<section class="space-y-4">
|
||||||
<h3 class="text-xs font-black text-emerald-500 uppercase tracking-[0.3em]">{{ __('Account Status') }}</h3>
|
<h3 class="text-xs font-black text-emerald-500 uppercase tracking-[0.3em]">{{ __('Account Status') }}</h3>
|
||||||
@@ -690,38 +810,56 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Business Type -->
|
<!-- Business Type -->
|
||||||
<div class="bg-slate-50 dark:bg-slate-800/40 p-5 rounded-2xl border border-slate-100 dark:border-slate-800/80">
|
<div class="bg-slate-50 dark:bg-slate-800/40 p-5 rounded-2xl border border-slate-100 dark:border-slate-800/80 space-y-4">
|
||||||
<h4 class="text-[10px] font-black text-indigo-500 uppercase tracking-[0.2em] mb-4">{{ __('Business Type') }}</h4>
|
<div class="flex justify-between items-start pb-3 border-b border-slate-100/50 dark:border-slate-800/50">
|
||||||
<div class="space-y-4">
|
<div>
|
||||||
<div class="flex items-center justify-between">
|
<h4 class="text-[10px] font-black text-indigo-500 uppercase tracking-[0.2em]">{{ __('Contract Model') }}</h4>
|
||||||
<span class="text-[11px] font-bold text-slate-400">{{ __('Original:') }}</span>
|
|
||||||
<span class="px-2.5 py-1 rounded-lg text-[11px] font-bold uppercase tracking-widest"
|
|
||||||
:class="detailCompany.original_type === 'buyout' ? 'bg-amber-500/10 text-amber-600' : 'bg-blue-500/10 text-blue-600'"
|
|
||||||
x-text="detailCompany.original_type === 'buyout' ? '{{ __('Buyout') }}' : '{{ __('Lease') }}'"></span>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center justify-between">
|
|
||||||
<span class="text-[11px] font-bold text-slate-400">{{ __('Current:') }}</span>
|
|
||||||
<span class="px-2.5 py-1 rounded-lg text-[11px] font-bold uppercase tracking-widest"
|
|
||||||
:class="detailCompany.current_type === 'buyout' ? 'bg-amber-500/10 text-amber-600 border border-amber-500/20' : 'bg-blue-500/10 text-blue-600 border border-blue-500/20'"
|
|
||||||
x-text="detailCompany.current_type === 'buyout' ? '{{ __('Buyout') }}' : '{{ __('Lease') }}'"></span>
|
|
||||||
</div>
|
</div>
|
||||||
|
<span :class="detailCompany.current_type === 'lease' ? 'text-blue-500 bg-blue-500/10' : 'text-amber-500 bg-amber-500/10'"
|
||||||
|
class="text-[10px] font-black uppercase tracking-wider px-2.5 py-1 rounded-lg">
|
||||||
|
<span x-text="detailCompany.current_type === 'lease' ? '{{ __('Lease') }}' : '{{ __('Buyout') }}'"></span>
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Lease Info Display -->
|
||||||
|
<template x-if="detailCompany.current_type === 'lease'">
|
||||||
|
<div class="grid grid-cols-2 gap-4">
|
||||||
|
<div class="space-y-1">
|
||||||
|
<p class="text-[9px] font-black text-slate-400 uppercase tracking-widest">{{ __('Start Date') }}</p>
|
||||||
|
<p class="text-xs font-black text-slate-700 dark:text-slate-200" x-text="detailCompany.start_date?.substring(0, 10) || '-'"></p>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-1">
|
||||||
|
<p class="text-[9px] font-black text-slate-400 uppercase tracking-widest">{{ __('End Date') }}</p>
|
||||||
|
<p class="text-xs font-black text-slate-800 dark:text-white" x-text="detailCompany.end_date?.substring(0, 10) || '{{ __('Unlimited') }}'"></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- Buyout Info Display -->
|
||||||
|
<template x-if="detailCompany.current_type === 'buyout'">
|
||||||
|
<div class="space-y-4">
|
||||||
|
<div class="grid grid-cols-1 gap-3">
|
||||||
|
<div class="p-3 bg-amber-50/30 dark:bg-amber-500/5 rounded-xl border border-amber-100/50 dark:border-amber-500/10">
|
||||||
|
<p class="text-[9px] font-black text-amber-600 uppercase tracking-widest">{{ __('Warranty Service') }}</p>
|
||||||
|
<div class="flex items-center gap-2 mt-1">
|
||||||
|
<span class="text-xs font-black text-slate-700 dark:text-slate-200" x-text="detailCompany.warranty_start_date?.substring(0, 10) || '-'"></span>
|
||||||
|
<span class="text-slate-400">~</span>
|
||||||
|
<span class="text-xs font-black text-slate-800 dark:text-white" x-text="detailCompany.warranty_end_date?.substring(0, 10) || '-'"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="p-3 bg-indigo-50/30 dark:bg-indigo-500/5 rounded-xl border border-indigo-100/50 dark:border-indigo-500/10">
|
||||||
|
<p class="text-[9px] font-black text-indigo-600 uppercase tracking-widest">{{ __('Software Service') }}</p>
|
||||||
|
<div class="flex items-center gap-2 mt-1">
|
||||||
|
<span class="text-xs font-black text-slate-700 dark:text-slate-200" x-text="detailCompany.software_start_date?.substring(0, 10) || '-'"></span>
|
||||||
|
<span class="text-slate-400">~</span>
|
||||||
|
<span class="text-xs font-black text-slate-800 dark:text-white" x-text="detailCompany.software_end_date?.substring(0, 10) || '-'"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Contract Period -->
|
|
||||||
<div class="bg-slate-50 dark:bg-slate-800/40 p-5 rounded-2xl border border-slate-100 dark:border-slate-800/80">
|
|
||||||
<h4 class="text-[10px] font-black text-indigo-500 uppercase tracking-[0.2em] mb-4">{{ __('Contract Period') }}</h4>
|
|
||||||
<div class="space-y-3 font-mono">
|
|
||||||
<div class="flex items-center gap-3">
|
|
||||||
<span class="text-[10px] font-bold text-slate-400 uppercase tracking-tighter min-w-[32px]">{{ __('From:') }}</span>
|
|
||||||
<div class="text-[13px] font-bold tracking-tighter text-slate-700 dark:text-slate-200" x-text="detailCompany.start_date || '--'"></div>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center gap-3">
|
|
||||||
<span class="text-[10px] font-bold text-slate-400 uppercase tracking-tighter min-w-[32px]">{{ __('To:') }}</span>
|
|
||||||
<div class="text-[13px] font-bold tracking-tighter text-slate-800 dark:text-white" :class="detailCompany.end_date_expired ? 'text-rose-500' : ''" x-text="detailCompany.end_date || '{{ __('Permanent') }}'"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
@@ -803,6 +941,82 @@
|
|||||||
<p class="text-sm font-bold text-slate-600 dark:text-slate-400 leading-relaxed italic" x-text="detailCompany.note"></p>
|
<p class="text-sm font-bold text-slate-600 dark:text-slate-400 leading-relaxed italic" x-text="detailCompany.note"></p>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- History View -->
|
||||||
|
<div x-show="sidebarView === 'history'" class="space-y-6">
|
||||||
|
<template x-for="(contract, index) in (detailCompany.contracts || [])" :key="contract.id">
|
||||||
|
<div class="bg-slate-50/50 dark:bg-slate-800/30 rounded-2xl border border-slate-100 dark:border-slate-800/80 overflow-hidden">
|
||||||
|
<!-- Card Header -->
|
||||||
|
<div class="px-5 py-3 bg-white dark:bg-slate-800/50 border-b border-slate-100 dark:border-slate-800 flex justify-between items-center">
|
||||||
|
<div class="flex items-center gap-2.5">
|
||||||
|
<div class="size-7 rounded-full bg-slate-100 dark:bg-slate-700 flex items-center justify-center text-[9px] font-black text-slate-500" x-text="'#' + (detailCompany.contracts.length - index)"></div>
|
||||||
|
<div>
|
||||||
|
<p class="text-[10px] font-black text-slate-700 dark:text-slate-200" x-text="new Date(contract.created_at).toLocaleString()"></p>
|
||||||
|
<div class="flex items-center gap-1.5 mt-0.5">
|
||||||
|
<span :class="contract.type === 'lease' ? 'text-blue-500 bg-blue-500/10' : 'text-amber-500 bg-amber-500/10'"
|
||||||
|
class="text-[8px] font-black uppercase tracking-wider px-1.5 py-0.5 rounded"
|
||||||
|
x-text="contract.type === 'lease' ? '{{ __('Lease') }}' : '{{ __('Buyout') }}'"></span>
|
||||||
|
<span class="text-[9px] text-slate-400">{{ __('by') }} <span class="text-slate-600 dark:text-slate-300" x-text="contract.creator?.name || 'System'"></span></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<template x-if="index === 0">
|
||||||
|
<span class="px-2 py-0.5 bg-emerald-500 text-white text-[8px] font-black uppercase tracking-widest rounded-full">{{ __('Current') }}</span>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Card Body -->
|
||||||
|
<div class="p-5 space-y-4">
|
||||||
|
<!-- Lease dates -->
|
||||||
|
<template x-if="contract.type === 'lease'">
|
||||||
|
<div class="grid grid-cols-2 gap-4">
|
||||||
|
<div class="space-y-1.5">
|
||||||
|
<p class="text-[10px] font-black text-slate-400 uppercase tracking-widest">{{ __('Contract Start') }}</p>
|
||||||
|
<p class="text-sm font-black text-slate-700 dark:text-white font-mono" x-text="contract.start_date?.substring(0, 10) || '-'"></p>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-1.5">
|
||||||
|
<p class="text-[10px] font-black text-slate-400 uppercase tracking-widest">{{ __('Contract End') }}</p>
|
||||||
|
<p class="text-sm font-black text-slate-700 dark:text-white font-mono" x-text="contract.end_date?.substring(0, 10) || '{{ __('Unlimited') }}'"></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<!-- Buyout dates -->
|
||||||
|
<template x-if="contract.type === 'buyout'">
|
||||||
|
<div class="grid grid-cols-1 gap-4">
|
||||||
|
<div class="p-4 bg-amber-50/40 dark:bg-amber-500/5 rounded-2xl border border-amber-100/50 dark:border-amber-500/10">
|
||||||
|
<p class="text-[10px] font-black text-amber-600 uppercase tracking-widest mb-2">{{ __('Warranty Service') }}</p>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span class="text-sm font-black text-slate-700 dark:text-slate-200 font-mono" x-text="contract.warranty_start_date?.substring(0, 10) || '-'"></span>
|
||||||
|
<span class="text-slate-400 font-bold">~</span>
|
||||||
|
<span class="text-sm font-black text-slate-800 dark:text-white font-mono" x-text="contract.warranty_end_date?.substring(0, 10) || '-'"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="p-4 bg-indigo-50/40 dark:bg-indigo-500/5 rounded-2xl border border-indigo-100/50 dark:border-indigo-500/10">
|
||||||
|
<p class="text-[10px] font-black text-indigo-600 uppercase tracking-widest mb-2">{{ __('Software Service') }}</p>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span class="text-sm font-black text-slate-700 dark:text-slate-200 font-mono" x-text="contract.software_start_date?.substring(0, 10) || '-'"></span>
|
||||||
|
<span class="text-slate-400 font-bold">~</span>
|
||||||
|
<span class="text-sm font-black text-slate-800 dark:text-white font-mono" x-text="contract.software_end_date?.substring(0, 10) || '-'"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<!-- Note -->
|
||||||
|
<div class="pt-2 border-t border-slate-100/50 dark:border-slate-800/50" x-show="contract.note">
|
||||||
|
<p class="text-[9px] font-bold text-slate-400 italic" x-text="contract.note"></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div x-show="!(detailCompany.contracts || []).length" class="py-12 text-center">
|
||||||
|
<div class="size-14 rounded-2xl bg-slate-50 dark:bg-slate-800/50 flex items-center justify-center mx-auto mb-3 border border-slate-100 dark:border-slate-800">
|
||||||
|
<svg class="size-7 text-slate-300" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M12 6.042A8.967 8.967 0 006 3.75c-1.052 0-2.062.18-3 .512v14.25A8.987 8.987 0 016 18c2.305 0 4.408.867 6 2.292m0-14.25a8.966 8.966 0 016-2.292c1.052 0 2.062.18 3 .512v14.25A8.987 8.987 0 0018 18c-2.305 0-4.408.867-6 2.292m0-14.25v14.25" /></svg>
|
||||||
|
</div>
|
||||||
|
<p class="text-xs font-black text-slate-400 uppercase tracking-[0.2em]">{{ __('No history records') }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Footer -->
|
<!-- Footer -->
|
||||||
@@ -815,7 +1029,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
|||||||
@@ -95,7 +95,10 @@ $roleSelectConfig = [
|
|||||||
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
|
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
|
||||||
</svg>
|
</svg>
|
||||||
</span>
|
</span>
|
||||||
<input type="text" name="search" value="{{ request('search') }}" class="py-2.5 pl-12 pr-6 block w-full md:w-80 luxury-input" placeholder="{{ __('Search roles...') }}">
|
<input type="text" name="search" value="{{ request('search') }}"
|
||||||
|
class="py-2.5 pl-12 pr-6 block w-full md:w-80 luxury-input"
|
||||||
|
placeholder="{{ __('Search roles...') }}"
|
||||||
|
@keydown.enter="$el.form.submit()">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@if(auth()->user()->isSystemAdmin())
|
@if(auth()->user()->isSystemAdmin())
|
||||||
@@ -106,6 +109,7 @@ $roleSelectConfig = [
|
|||||||
</div>
|
</div>
|
||||||
@endif
|
@endif
|
||||||
<input type="hidden" name="per_page" value="{{ request('per_page', 10) }}">
|
<input type="hidden" name="per_page" value="{{ request('per_page', 10) }}">
|
||||||
|
<button type="submit" class="hidden"></button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<div class="overflow-x-auto">
|
<div class="overflow-x-auto">
|
||||||
@@ -211,7 +215,8 @@ $roleSelectConfig = [
|
|||||||
</span>
|
</span>
|
||||||
<input type="text" name="search" value="{{ request('search') }}"
|
<input type="text" name="search" value="{{ request('search') }}"
|
||||||
class="py-2.5 pl-12 pr-6 block w-full md:w-80 luxury-input"
|
class="py-2.5 pl-12 pr-6 block w-full md:w-80 luxury-input"
|
||||||
placeholder="{{ __('Search users...') }}">
|
placeholder="{{ __('Search users...') }}"
|
||||||
|
@keydown.enter="$el.form.submit()">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@if(auth()->user()->isSystemAdmin())
|
@if(auth()->user()->isSystemAdmin())
|
||||||
@@ -222,6 +227,7 @@ $roleSelectConfig = [
|
|||||||
</div>
|
</div>
|
||||||
@endif
|
@endif
|
||||||
<input type="hidden" name="per_page" value="{{ request('per_page', 10) }}">
|
<input type="hidden" name="per_page" value="{{ request('per_page', 10) }}">
|
||||||
|
<button type="submit" class="hidden"></button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<div class="overflow-x-auto">
|
<div class="overflow-x-auto">
|
||||||
|
|||||||
@@ -24,17 +24,17 @@
|
|||||||
selectedMachine: null,
|
selectedMachine: null,
|
||||||
slots: [],
|
slots: [],
|
||||||
inventorySlots: [],
|
inventorySlots: [],
|
||||||
|
currentPage: 1,
|
||||||
|
lastPage: 1,
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
const d = new Date();
|
const now = new Date();
|
||||||
const today = [
|
const pad = (n) => String(n).padStart(2, '0');
|
||||||
d.getFullYear(),
|
const formatDate = (date, time) => `${date.getFullYear()}/${pad(date.getMonth() + 1)}/${pad(date.getDate())} ${time}`;
|
||||||
String(d.getMonth() + 1).padStart(2, '0'),
|
|
||||||
String(d.getDate()).padStart(2, '0')
|
this.startDate = formatDate(now, '00:00');
|
||||||
].join('-');
|
this.endDate = formatDate(now, '23:59');
|
||||||
this.startDate = today;
|
this.$watch('activeTab', () => this.fetchLogs(1));
|
||||||
this.endDate = today;
|
|
||||||
this.$watch('activeTab', () => this.fetchLogs());
|
|
||||||
},
|
},
|
||||||
|
|
||||||
async openLogPanel(id, sn, name) {
|
async openLogPanel(id, sn, name) {
|
||||||
@@ -54,15 +54,21 @@
|
|||||||
},
|
},
|
||||||
|
|
||||||
|
|
||||||
async fetchLogs() {
|
async fetchLogs(page = 1) {
|
||||||
this.loading = true;
|
this.loading = true;
|
||||||
|
this.currentPage = page;
|
||||||
try {
|
try {
|
||||||
let url = '/admin/machines/' + this.currentMachineId + '/logs-ajax?type=' + this.activeTab;
|
let url = '/admin/machines/' + this.currentMachineId + '/logs-ajax?type=' + this.activeTab + '&page=' + page;
|
||||||
if (this.startDate) url += '&start_date=' + this.startDate;
|
if (this.startDate) url += '&start_date=' + this.startDate;
|
||||||
if (this.endDate) url += '&end_date=' + this.endDate;
|
if (this.endDate) url += '&end_date=' + this.endDate;
|
||||||
|
|
||||||
const res = await fetch(url);
|
const res = await fetch(url);
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
if (data.success) this.logs = data.data.data || data.data || [];
|
if (data.success) {
|
||||||
|
this.logs = data.data || [];
|
||||||
|
this.currentPage = data.pagination.current_page;
|
||||||
|
this.lastPage = data.pagination.last_page;
|
||||||
|
}
|
||||||
} catch (e) { console.error('fetchLogs error:', e); }
|
} catch (e) { console.error('fetchLogs error:', e); }
|
||||||
finally { this.loading = false; }
|
finally { this.loading = false; }
|
||||||
},
|
},
|
||||||
@@ -100,6 +106,13 @@
|
|||||||
finally { this.inventoryLoading = false; }
|
finally { this.inventoryLoading = false; }
|
||||||
},
|
},
|
||||||
|
|
||||||
|
formatDateTime(dateStr) {
|
||||||
|
if (!dateStr) return '--';
|
||||||
|
const d = new Date(dateStr);
|
||||||
|
const pad = (n) => String(n).padStart(2, '0');
|
||||||
|
return `${d.getFullYear()}/${pad(d.getMonth() + 1)}/${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
|
||||||
|
},
|
||||||
|
|
||||||
getSlotColorClass(slot) {
|
getSlotColorClass(slot) {
|
||||||
if (!slot.expiry_date) return 'bg-slate-50/50 dark:bg-slate-800/50 text-slate-400 border-slate-200/60 dark:border-slate-700/50';
|
if (!slot.expiry_date) return 'bg-slate-50/50 dark:bg-slate-800/50 text-slate-400 border-slate-200/60 dark:border-slate-700/50';
|
||||||
const todayStr = new Date().toISOString().split('T')[0];
|
const todayStr = new Date().toISOString().split('T')[0];
|
||||||
@@ -390,19 +403,35 @@
|
|||||||
<label
|
<label
|
||||||
class="text-[10px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-[0.1em] whitespace-nowrap">{{
|
class="text-[10px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-[0.1em] whitespace-nowrap">{{
|
||||||
__('From') }}</label>
|
__('From') }}</label>
|
||||||
<input type="date" x-model="startDate" @change="fetchLogs()"
|
<input type="text" x-ref="startDatePicker" x-model="startDate"
|
||||||
class="luxury-input text-[11px] h-9 sm:h-8 py-0 w-full sm:w-32 bg-white dark:bg-slate-800 border-slate-200 dark:border-slate-700">
|
x-init="flatpickr($refs.startDatePicker, {
|
||||||
|
enableTime: true,
|
||||||
|
dateFormat: 'Y/m/d H:i',
|
||||||
|
time_24hr: true,
|
||||||
|
locale: window.flatpickrLocale,
|
||||||
|
defaultDate: startDate,
|
||||||
|
onClose: (selectedDates, dateStr) => { startDate = dateStr; fetchLogs(1); }
|
||||||
|
})"
|
||||||
|
class="luxury-input text-[11px] h-9 sm:h-8 py-0 w-full sm:w-44 bg-white dark:bg-slate-800 border-slate-200 dark:border-slate-700">
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col sm:flex-row sm:items-center gap-1.5 sm:gap-2">
|
<div class="flex flex-col sm:flex-row sm:items-center gap-1.5 sm:gap-2">
|
||||||
<label
|
<label
|
||||||
class="text-[10px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-[0.1em] whitespace-nowrap">{{
|
class="text-[10px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-[0.1em] whitespace-nowrap">{{
|
||||||
__('To') }}</label>
|
__('To') }}</label>
|
||||||
<input type="date" x-model="endDate" @change="fetchLogs()"
|
<input type="text" x-ref="endDatePicker" x-model="endDate"
|
||||||
class="luxury-input text-[11px] h-9 sm:h-8 py-0 w-full sm:w-32 bg-white dark:bg-slate-800 border-slate-200 dark:border-slate-700">
|
x-init="flatpickr($refs.endDatePicker, {
|
||||||
|
enableTime: true,
|
||||||
|
dateFormat: 'Y/m/d H:i',
|
||||||
|
time_24hr: true,
|
||||||
|
locale: window.flatpickrLocale,
|
||||||
|
defaultDate: endDate,
|
||||||
|
onClose: (selectedDates, dateStr) => { endDate = dateStr; fetchLogs(1); }
|
||||||
|
})"
|
||||||
|
class="luxury-input text-[11px] h-9 sm:h-8 py-0 w-full sm:w-44 bg-white dark:bg-slate-800 border-slate-200 dark:border-slate-700">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex justify-start sm:justify-end">
|
<div class="flex justify-start sm:justify-end">
|
||||||
<button @click="startDate = ''; endDate = ''; fetchLogs()"
|
<button @click="startDate = ''; endDate = ''; fetchLogs(1)"
|
||||||
class="text-[10px] font-bold text-cyan-600 dark:text-cyan-400 uppercase tracking-widest hover:text-cyan-500 transition-colors flex items-center gap-1.5">
|
class="text-[10px] font-bold text-cyan-600 dark:text-cyan-400 uppercase tracking-widest hover:text-cyan-500 transition-colors flex items-center gap-1.5">
|
||||||
<svg class="w-3 h-3" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
<svg class="w-3 h-3" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
||||||
stroke-width="2.5">
|
stroke-width="2.5">
|
||||||
@@ -419,7 +448,7 @@
|
|||||||
<!-- Body / Navigation Tabs -->
|
<!-- Body / Navigation Tabs -->
|
||||||
<div class="flex-1 flex flex-col min-h-0">
|
<div class="flex-1 flex flex-col min-h-0">
|
||||||
<div
|
<div
|
||||||
class="border-b border-slate-200 dark:border-slate-800 bg-white dark:bg-slate-900 sticky top-0 z-10 px-6 sm:px-8 overflow-x-auto hide-scrollbar">
|
class="border-b border-slate-200 dark:border-slate-800 bg-white dark:bg-slate-900 sticky top-0 z-10 px-6 sm:px-8 overflow-x-auto overflow-y-hidden hide-scrollbar">
|
||||||
<nav class="-mb-px flex space-x-6 sm:space-x-8" aria-label="Tabs">
|
<nav class="-mb-px flex space-x-6 sm:space-x-8" aria-label="Tabs">
|
||||||
<button @click="activeTab = 'status'"
|
<button @click="activeTab = 'status'"
|
||||||
:class="{'border-cyan-500 text-cyan-600 dark:text-cyan-400': activeTab === 'status', 'border-transparent text-slate-500 hover:text-slate-700 hover:border-slate-300 dark:hover:text-slate-300': activeTab !== 'status'}"
|
:class="{'border-cyan-500 text-cyan-600 dark:text-cyan-400': activeTab === 'status', 'border-transparent text-slate-500 hover:text-slate-700 hover:border-slate-300 dark:hover:text-slate-300': activeTab !== 'status'}"
|
||||||
@@ -486,7 +515,7 @@
|
|||||||
class="group hover:bg-slate-50/50 dark:hover:bg-slate-800/30 transition-all">
|
class="group hover:bg-slate-50/50 dark:hover:bg-slate-800/30 transition-all">
|
||||||
<td class="px-6 py-4">
|
<td class="px-6 py-4">
|
||||||
<div class="text-[12px] font-bold text-slate-600 dark:text-slate-300"
|
<div class="text-[12px] font-bold text-slate-600 dark:text-slate-300"
|
||||||
x-text="new Date(log.created_at).toLocaleString()">
|
x-text="formatDateTime(log.created_at)">
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td class="px-6 py-4 text-center">
|
<td class="px-6 py-4 text-center">
|
||||||
@@ -501,7 +530,7 @@
|
|||||||
</td>
|
</td>
|
||||||
<td class="px-6 py-4">
|
<td class="px-6 py-4">
|
||||||
<p class="text-[13px] font-medium text-slate-700 dark:text-slate-200 truncate max-w-md"
|
<p class="text-[13px] font-medium text-slate-700 dark:text-slate-200 truncate max-w-md"
|
||||||
:title="log.message" x-text="log.message"></p>
|
:title="log.translated_message || log.message" x-text="log.translated_message || log.message"></p>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</template>
|
</template>
|
||||||
@@ -517,7 +546,7 @@
|
|||||||
<div class="flex items-center justify-between mb-2">
|
<div class="flex items-center justify-between mb-2">
|
||||||
<span
|
<span
|
||||||
class="text-[10px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-widest"
|
class="text-[10px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-widest"
|
||||||
x-text="new Date(log.created_at).toLocaleString()"></span>
|
x-text="formatDateTime(log.created_at)"></span>
|
||||||
<span :class="{
|
<span :class="{
|
||||||
'bg-cyan-500/10 text-cyan-600 dark:text-cyan-400 border-cyan-500/20': log.level === 'info',
|
'bg-cyan-500/10 text-cyan-600 dark:text-cyan-400 border-cyan-500/20': log.level === 'info',
|
||||||
'bg-amber-500/10 text-amber-600 dark:text-amber-400 border-amber-500/20': log.level === 'warning',
|
'bg-amber-500/10 text-amber-600 dark:text-amber-400 border-amber-500/20': log.level === 'warning',
|
||||||
@@ -528,7 +557,7 @@
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<p class="text-[13px] font-bold text-slate-700 dark:text-slate-200 line-clamp-3 leading-relaxed"
|
<p class="text-[13px] font-bold text-slate-700 dark:text-slate-200 line-clamp-3 leading-relaxed"
|
||||||
x-text="log.message"></p>
|
x-text="log.translated_message || log.message"></p>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
@@ -554,6 +583,33 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<!-- Pagination Footer -->
|
||||||
|
<div x-show="logs.length > 0" class="px-6 py-4 bg-slate-50/50 dark:bg-slate-800/30 border-t border-slate-200 dark:border-slate-800 flex items-center justify-between">
|
||||||
|
<div class="flex items-center gap-4">
|
||||||
|
<button @click="fetchLogs(currentPage - 1)"
|
||||||
|
:disabled="currentPage <= 1"
|
||||||
|
class="p-2 rounded-xl bg-white dark:bg-slate-800 border border-slate-200 dark:border-slate-700 text-slate-400 disabled:opacity-30 disabled:cursor-not-allowed hover:text-cyan-500 hover:border-cyan-500/30 transition-all shadow-sm">
|
||||||
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M15 19l-7-7 7-7" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div class="flex items-center gap-1.5">
|
||||||
|
<span class="text-[10px] font-black text-cyan-600 dark:text-cyan-400 uppercase tracking-widest" x-text="currentPage"></span>
|
||||||
|
<span class="text-[10px] font-black text-slate-300 dark:text-slate-600 uppercase">/</span>
|
||||||
|
<span class="text-[10px] font-black text-slate-500 dark:text-slate-400 uppercase tracking-widest" x-text="lastPage"></span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button @click="fetchLogs(currentPage + 1)"
|
||||||
|
:disabled="currentPage >= lastPage"
|
||||||
|
class="p-2 rounded-xl bg-white dark:bg-slate-800 border border-slate-200 dark:border-slate-700 text-slate-400 disabled:opacity-30 disabled:cursor-not-allowed hover:text-cyan-500 hover:border-cyan-500/30 transition-all shadow-sm">
|
||||||
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M9 5l7 7-7 7" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -61,7 +61,10 @@
|
|||||||
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
|
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
|
||||||
</svg>
|
</svg>
|
||||||
</span>
|
</span>
|
||||||
<input type="text" name="search" value="{{ request('search') }}" class="py-2.5 pl-12 pr-6 block w-full md:w-80 luxury-input" placeholder="{{ __('Search roles...') }}">
|
<input type="text" name="search" value="{{ request('search') }}"
|
||||||
|
class="py-2.5 pl-12 pr-6 block w-full md:w-80 luxury-input"
|
||||||
|
placeholder="{{ __('Search roles...') }}"
|
||||||
|
@keydown.enter="$el.form.submit()">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@if(auth()->user()->isSystemAdmin())
|
@if(auth()->user()->isSystemAdmin())
|
||||||
@@ -79,6 +82,7 @@
|
|||||||
</div>
|
</div>
|
||||||
@endif
|
@endif
|
||||||
<input type="hidden" name="per_page" value="{{ request('per_page', 10) }}">
|
<input type="hidden" name="per_page" value="{{ request('per_page', 10) }}">
|
||||||
|
<button type="submit" class="hidden"></button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -51,6 +51,9 @@ Route::prefix('v1')->middleware(['throttle:api'])->group(function () {
|
|||||||
// 機台管理員 B000 登入驗證 (由於此階段機台未帶 Token 無法通過 iot.auth)
|
// 機台管理員 B000 登入驗證 (由於此階段機台未帶 Token 無法通過 iot.auth)
|
||||||
Route::prefix('app')->group(function () {
|
Route::prefix('app')->group(function () {
|
||||||
Route::post('admin/login/B000', [\App\Http\Controllers\Api\V1\App\MachineAuthController::class, 'loginB000'])->middleware('throttle:30,1');
|
Route::post('admin/login/B000', [\App\Http\Controllers\Api\V1\App\MachineAuthController::class, 'loginB000'])->middleware('throttle:30,1');
|
||||||
|
|
||||||
|
// 機台啟動引導與參數下載 (需人員登入 Token)
|
||||||
|
Route::middleware('auth:sanctum')->post('machine/setting/B014', [App\Http\Controllers\Api\V1\App\MachineController::class, 'getSettings']);
|
||||||
});
|
});
|
||||||
|
|
||||||
Route::prefix('app')->middleware(['iot.auth', 'throttle:100,1'])->group(function () {
|
Route::prefix('app')->middleware(['iot.auth', 'throttle:100,1'])->group(function () {
|
||||||
@@ -68,6 +71,9 @@ Route::prefix('v1')->middleware(['throttle:api'])->group(function () {
|
|||||||
// 統一商品主檔 API (B012 整合版)
|
// 統一商品主檔 API (B012 整合版)
|
||||||
Route::match(['get', 'patch'], 'machine/products/B012', [App\Http\Controllers\Api\V1\App\MachineController::class, 'getProducts']);
|
Route::match(['get', 'patch'], 'machine/products/B012', [App\Http\Controllers\Api\V1\App\MachineController::class, 'getProducts']);
|
||||||
|
|
||||||
|
// 機台故障與異常上報 (B013)
|
||||||
|
Route::post('machine/error/B013', [App\Http\Controllers\Api\V1\App\MachineController::class, 'reportError']);
|
||||||
|
|
||||||
// 交易、發票與出貨 (B600, B601, B602)
|
// 交易、發票與出貨 (B600, B601, B602)
|
||||||
Route::post('machine/restock/B018', [App\Http\Controllers\Api\V1\App\MachineController::class, 'recordRestock']);
|
Route::post('machine/restock/B018', [App\Http\Controllers\Api\V1\App\MachineController::class, 'recordRestock']);
|
||||||
Route::post('B600', [App\Http\Controllers\Api\V1\App\TransactionController::class, 'store']);
|
Route::post('B600', [App\Http\Controllers\Api\V1\App\TransactionController::class, 'store']);
|
||||||
|
|||||||
Reference in New Issue
Block a user