Compare commits
2 Commits
54d62c5378
...
e27eee78f5
| Author | SHA1 | Date | |
|---|---|---|---|
| e27eee78f5 | |||
| 759fae4380 |
167
app/Http/Controllers/Admin/ProductCategoryController.php
Normal file
167
app/Http/Controllers/Admin/ProductCategoryController.php
Normal file
@@ -0,0 +1,167 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Admin;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Models\Product\ProductCategory;
|
||||||
|
use App\Models\System\Translation;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
|
||||||
|
class ProductCategoryController extends Controller
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* 顯示商品分類清單 (主要用於 AJAX 或內嵌在商品管理頁面)
|
||||||
|
*/
|
||||||
|
public function index(Request $request)
|
||||||
|
{
|
||||||
|
$user = auth()->user();
|
||||||
|
$query = ProductCategory::with(['translations']);
|
||||||
|
|
||||||
|
if ($user->isSystemAdmin() && $request->filled('company_id')) {
|
||||||
|
$query->where('company_id', $request->company_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
$categories = $query->latest()->get();
|
||||||
|
|
||||||
|
if ($request->wantsJson()) {
|
||||||
|
return response()->json([
|
||||||
|
'success' => true,
|
||||||
|
'data' => $categories
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return redirect()->route('admin.data-config.products.index', ['tab' => 'categories']);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 儲存新分類
|
||||||
|
*/
|
||||||
|
public function store(Request $request)
|
||||||
|
{
|
||||||
|
$validated = $request->validate([
|
||||||
|
'names.zh_TW' => 'required|string|max:255',
|
||||||
|
'names.en' => 'nullable|string|max:255',
|
||||||
|
'names.ja' => 'nullable|string|max:255',
|
||||||
|
'company_id' => 'nullable|exists:companies,id',
|
||||||
|
]);
|
||||||
|
|
||||||
|
try {
|
||||||
|
DB::beginTransaction();
|
||||||
|
|
||||||
|
$dictKey = Str::uuid()->toString();
|
||||||
|
$company_id = (auth()->user()->isSystemAdmin() && $request->filled('company_id'))
|
||||||
|
? $request->company_id
|
||||||
|
: auth()->user()->company_id;
|
||||||
|
|
||||||
|
// 儲存多語系翻譯
|
||||||
|
foreach ($request->names as $locale => $value) {
|
||||||
|
if (empty($value)) continue;
|
||||||
|
Translation::withoutGlobalScopes()->create([
|
||||||
|
'group' => 'category',
|
||||||
|
'key' => $dictKey,
|
||||||
|
'locale' => $locale,
|
||||||
|
'value' => $value,
|
||||||
|
'company_id' => $company_id,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$category = ProductCategory::create([
|
||||||
|
'company_id' => $company_id,
|
||||||
|
'name' => $request->names['zh_TW'] ?? (collect($request->names)->first() ?? 'Untitled'),
|
||||||
|
'name_dictionary_key' => $dictKey,
|
||||||
|
]);
|
||||||
|
|
||||||
|
DB::commit();
|
||||||
|
|
||||||
|
return redirect()->back()->with('success', __('Category created successfully'));
|
||||||
|
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
DB::rollBack();
|
||||||
|
return redirect()->back()->with('error', $e->getMessage())->withInput();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新分類
|
||||||
|
*/
|
||||||
|
public function update(Request $request, $id)
|
||||||
|
{
|
||||||
|
$category = ProductCategory::findOrFail($id);
|
||||||
|
|
||||||
|
$validated = $request->validate([
|
||||||
|
'names.zh_TW' => 'required|string|max:255',
|
||||||
|
'names.en' => 'nullable|string|max:255',
|
||||||
|
'names.ja' => 'nullable|string|max:255',
|
||||||
|
]);
|
||||||
|
|
||||||
|
try {
|
||||||
|
DB::beginTransaction();
|
||||||
|
|
||||||
|
$dictKey = $category->name_dictionary_key ?: Str::uuid()->toString();
|
||||||
|
$company_id = $category->company_id;
|
||||||
|
|
||||||
|
foreach ($request->names as $locale => $value) {
|
||||||
|
if (empty($value)) {
|
||||||
|
Translation::withoutGlobalScopes()->where([
|
||||||
|
'group' => 'category',
|
||||||
|
'key' => $dictKey,
|
||||||
|
'locale' => $locale
|
||||||
|
])->delete();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
Translation::withoutGlobalScopes()->updateOrCreate(
|
||||||
|
[
|
||||||
|
'group' => 'category',
|
||||||
|
'key' => $dictKey,
|
||||||
|
'locale' => $locale,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'value' => $value,
|
||||||
|
'company_id' => $company_id,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$category->update([
|
||||||
|
'name' => $request->names['zh_TW'] ?? $category->name,
|
||||||
|
'name_dictionary_key' => $dictKey,
|
||||||
|
]);
|
||||||
|
|
||||||
|
DB::commit();
|
||||||
|
|
||||||
|
return redirect()->back()->with('success', __('Category updated successfully'));
|
||||||
|
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
DB::rollBack();
|
||||||
|
return redirect()->back()->with('error', $e->getMessage())->withInput();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 刪除分類
|
||||||
|
*/
|
||||||
|
public function destroy($id)
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$category = ProductCategory::findOrFail($id);
|
||||||
|
|
||||||
|
// 檢查是否已有商品使用此分類
|
||||||
|
if ($category->products()->count() > 0) {
|
||||||
|
return redirect()->back()->with('error', __('Cannot delete category that has products. Please move products first.'));
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($category->name_dictionary_key) {
|
||||||
|
Translation::withoutGlobalScopes()->where('group', 'category')->where('key', $category->name_dictionary_key)->delete();
|
||||||
|
}
|
||||||
|
|
||||||
|
$category->delete();
|
||||||
|
|
||||||
|
return redirect()->back()->with('success', __('Category deleted successfully'));
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
return redirect()->back()->with('error', $e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -19,7 +19,7 @@ class ProductController extends Controller
|
|||||||
public function index(Request $request)
|
public function index(Request $request)
|
||||||
{
|
{
|
||||||
$user = auth()->user();
|
$user = auth()->user();
|
||||||
$query = Product::with(['category', 'translations', 'company']);
|
$query = Product::with(['category.translations', 'translations', 'company']);
|
||||||
|
|
||||||
// 搜尋
|
// 搜尋
|
||||||
if ($request->filled('search')) {
|
if ($request->filled('search')) {
|
||||||
@@ -47,7 +47,7 @@ class ProductController extends Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
$products = $query->latest()->paginate($per_page)->withQueryString();
|
$products = $query->latest()->paginate($per_page)->withQueryString();
|
||||||
$categories = ProductCategory::all();
|
$categories = ProductCategory::with('translations')->get();
|
||||||
$companies = $user->isSystemAdmin() ? Company::all() : collect();
|
$companies = $user->isSystemAdmin() ? Company::all() : collect();
|
||||||
|
|
||||||
// 系統管理員在過濾特定公司時,應顯示該公司的功能開關 (如物料代碼、點數規則)
|
// 系統管理員在過濾特定公司時,應顯示該公司的功能開關 (如物料代碼、點數規則)
|
||||||
@@ -68,7 +68,7 @@ class ProductController extends Controller
|
|||||||
public function create(Request $request)
|
public function create(Request $request)
|
||||||
{
|
{
|
||||||
$user = auth()->user();
|
$user = auth()->user();
|
||||||
$categories = ProductCategory::all();
|
$categories = ProductCategory::with('translations')->get();
|
||||||
$companies = $user->isSystemAdmin() ? Company::all() : collect();
|
$companies = $user->isSystemAdmin() ? Company::all() : collect();
|
||||||
|
|
||||||
// If system admin, check if company_id is provided in URL to get settings
|
// If system admin, check if company_id is provided in URL to get settings
|
||||||
@@ -94,7 +94,7 @@ class ProductController extends Controller
|
|||||||
->where('key', $product->name_dictionary_key)
|
->where('key', $product->name_dictionary_key)
|
||||||
->get()
|
->get()
|
||||||
);
|
);
|
||||||
$categories = ProductCategory::all();
|
$categories = ProductCategory::with('translations')->get();
|
||||||
$companies = $user->isSystemAdmin() ? Company::all() : collect();
|
$companies = $user->isSystemAdmin() ? Company::all() : collect();
|
||||||
|
|
||||||
// Use the product's company settings for editing
|
// Use the product's company settings for editing
|
||||||
|
|||||||
@@ -17,6 +17,11 @@ class ProductCategory extends Model
|
|||||||
'name_dictionary_key',
|
'name_dictionary_key',
|
||||||
];
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 自動附加到 JSON/陣列輸出
|
||||||
|
*/
|
||||||
|
protected $appends = ['localized_name'];
|
||||||
|
|
||||||
public function products()
|
public function products()
|
||||||
{
|
{
|
||||||
return $this->hasMany(Product::class, 'category_id');
|
return $this->hasMany(Product::class, 'category_id');
|
||||||
@@ -30,4 +35,26 @@ class ProductCategory extends Model
|
|||||||
return $this->hasMany(\App\Models\System\Translation::class, 'key', 'name_dictionary_key')
|
return $this->hasMany(\App\Models\System\Translation::class, 'key', 'name_dictionary_key')
|
||||||
->where('group', 'category');
|
->where('group', 'category');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 取得當前語系的商品名稱。
|
||||||
|
* 回退順序:當前語系 → zh_TW → name 欄位
|
||||||
|
*/
|
||||||
|
public function getLocalizedNameAttribute(): string
|
||||||
|
{
|
||||||
|
if ($this->relationLoaded('translations') && $this->translations->isNotEmpty()) {
|
||||||
|
$locale = app()->getLocale();
|
||||||
|
// 先找當前語系
|
||||||
|
$translation = $this->translations->firstWhere('locale', $locale);
|
||||||
|
if ($translation) {
|
||||||
|
return $translation->value;
|
||||||
|
}
|
||||||
|
// 回退至 zh_TW
|
||||||
|
$fallback = $this->translations->firstWhere('locale', 'zh_TW');
|
||||||
|
if ($fallback) {
|
||||||
|
return $fallback->value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $this->name ?? '';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
33
lang/en.json
33
lang/en.json
@@ -10,16 +10,17 @@
|
|||||||
"APP Version": "APP Version",
|
"APP Version": "APP Version",
|
||||||
"APP_ID": "APP_ID",
|
"APP_ID": "APP_ID",
|
||||||
"APP_KEY": "APP_KEY",
|
"APP_KEY": "APP_KEY",
|
||||||
"Account": "帳號",
|
"Account": "Account",
|
||||||
"Account :name status has been changed to :status.": "Account :name status has been changed to :status.",
|
"Account :name status has been changed to :status.": "Account :name status has been changed to :status.",
|
||||||
|
"Account Info": "Account Info",
|
||||||
"Account Management": "Account Management",
|
"Account Management": "Account Management",
|
||||||
"Account Name": "帳號姓名",
|
"Account Name": "Account Name",
|
||||||
"Account Settings": "Account Settings",
|
"Account Settings": "Account Settings",
|
||||||
"Account Status": "Account Status",
|
"Account Status": "Account Status",
|
||||||
"Account created successfully.": "Account created successfully.",
|
"Account created successfully.": "Account created successfully.",
|
||||||
"Account deleted successfully.": "Account deleted successfully.",
|
"Account deleted successfully.": "Account deleted successfully.",
|
||||||
"Account updated successfully.": "Account updated successfully.",
|
"Account updated successfully.": "Account updated successfully.",
|
||||||
"Account:": "帳號:",
|
"Account:": "Account:",
|
||||||
"Accounts / Machines": "Accounts / Machines",
|
"Accounts / Machines": "Accounts / Machines",
|
||||||
"Action": "Action",
|
"Action": "Action",
|
||||||
"Actions": "Actions",
|
"Actions": "Actions",
|
||||||
@@ -37,10 +38,11 @@
|
|||||||
"Admin display name": "Admin display name",
|
"Admin display name": "Admin display name",
|
||||||
"Administrator": "Administrator",
|
"Administrator": "Administrator",
|
||||||
"Advertisement Management": "Advertisement Management",
|
"Advertisement Management": "Advertisement Management",
|
||||||
|
"Affiliated Company": "Affiliated Company",
|
||||||
"Affiliated Unit": "Company Name",
|
"Affiliated Unit": "Company Name",
|
||||||
"Affiliation": "Company Name",
|
"Affiliation": "Company Name",
|
||||||
"Alert Summary": "Alert Summary",
|
"Alert Summary": "Alert Summary",
|
||||||
"Alerts": "中心告警",
|
"Alerts": "Alerts",
|
||||||
"Alerts Pending": "Alerts Pending",
|
"Alerts Pending": "Alerts Pending",
|
||||||
"All": "All",
|
"All": "All",
|
||||||
"All Affiliations": "All Companies",
|
"All Affiliations": "All Companies",
|
||||||
@@ -203,13 +205,24 @@
|
|||||||
"EASY_MERCHANT_ID": "EASY_MERCHANT_ID",
|
"EASY_MERCHANT_ID": "EASY_MERCHANT_ID",
|
||||||
"ECPay Invoice": "ECPay Invoice",
|
"ECPay Invoice": "ECPay Invoice",
|
||||||
"ECPay Invoice Settings Description": "ECPay Electronic Invoice Settings",
|
"ECPay Invoice Settings Description": "ECPay Electronic Invoice Settings",
|
||||||
|
"Type to search or leave blank for system defaults.": "Type to search or leave blank for system defaults.",
|
||||||
|
"Select Company (Default: System)": "Select Company (Default: System)",
|
||||||
|
"Search Company Title...": "Search Company Title...",
|
||||||
|
"System Default (Common)": "System Default (Common)",
|
||||||
|
"Category Name (zh_TW)": "Category Name (Traditional Chinese)",
|
||||||
|
"Category Name (en)": "Category Name (English)",
|
||||||
|
"Category Name (ja)": "Category Name (Japanese)",
|
||||||
|
"e.g., Beverage": "e.g., Beverage",
|
||||||
|
"e.g., Drinks": "e.g., Drinks",
|
||||||
|
"e.g., お飲み物": "e.g., O-Nomimono",
|
||||||
"Edit": "Edit",
|
"Edit": "Edit",
|
||||||
|
"Edit Category": "Edit Category",
|
||||||
"Edit Account": "Edit Account",
|
"Edit Account": "Edit Account",
|
||||||
"Edit Customer": "Edit Customer",
|
"Edit Customer": "Edit Customer",
|
||||||
"Edit Expiry": "Edit Expiry",
|
"Edit Expiry": "Edit Expiry",
|
||||||
"Edit Machine": "Edit Machine",
|
"Edit Machine": "Edit Machine",
|
||||||
"Edit Machine Model": "Edit Machine Model",
|
"Edit Machine Model": "Edit Machine Model",
|
||||||
"Edit Machine Settings": "編輯機台設定",
|
"Edit Machine Settings": "Edit Machine Settings",
|
||||||
"Edit Payment Config": "Edit Payment Config",
|
"Edit Payment Config": "Edit Payment Config",
|
||||||
"Edit Product": "Edit Product",
|
"Edit Product": "Edit Product",
|
||||||
"Edit Role": "Edit Role",
|
"Edit Role": "Edit Role",
|
||||||
@@ -867,5 +880,13 @@
|
|||||||
"Ad Settings": "Ad Settings",
|
"Ad Settings": "Ad Settings",
|
||||||
"System Default (All Companies)": "System Default (All Companies)",
|
"System Default (All Companies)": "System Default (All Companies)",
|
||||||
"No materials available": "No materials available",
|
"No materials available": "No materials available",
|
||||||
"Search...": "Search..."
|
"Search...": "Search...",
|
||||||
|
"Add Category": "Add Category",
|
||||||
|
"Category Management": "Category Management",
|
||||||
|
"Category Name": "Category Name",
|
||||||
|
"Manage your catalog, categories, and inventory settings.": "Manage your catalog, categories, and inventory settings.",
|
||||||
|
"Multilingual Names": "Multilingual Names",
|
||||||
|
"Barcode / Material": "Barcode / Material",
|
||||||
|
"Product List": "Product List",
|
||||||
|
"Product Count": "Product Count"
|
||||||
}
|
}
|
||||||
25
lang/ja.json
25
lang/ja.json
@@ -96,6 +96,10 @@
|
|||||||
"Buyout": "買取",
|
"Buyout": "買取",
|
||||||
"Cancel": "キャンセル",
|
"Cancel": "キャンセル",
|
||||||
"Cancel Purchase": "購入キャンセル",
|
"Cancel Purchase": "購入キャンセル",
|
||||||
|
"Category": "カテゴリ",
|
||||||
|
"Category Name (zh_TW)": "カテゴリ名 (中国語(繁体字))",
|
||||||
|
"Category Name (en)": "カテゴリ名 (英語)",
|
||||||
|
"Category Name (ja)": "カテゴリ名 (日本語)",
|
||||||
"Cannot Delete Role": "ロールを削除できません",
|
"Cannot Delete Role": "ロールを削除できません",
|
||||||
"Cannot change Super Admin status.": "スーパー管理者のステータスは変更できません。",
|
"Cannot change Super Admin status.": "スーパー管理者のステータスは変更できません。",
|
||||||
"Cannot delete company with active accounts.": "有効なアカウントを持つ顧客を削除できません。",
|
"Cannot delete company with active accounts.": "有効なアカウントを持つ顧客を削除できません。",
|
||||||
@@ -105,7 +109,6 @@
|
|||||||
"Card Reader No": "カードリーダー番号",
|
"Card Reader No": "カードリーダー番号",
|
||||||
"Card Reader Restart": "カードリーダー再起動",
|
"Card Reader Restart": "カードリーダー再起動",
|
||||||
"Card Reader Seconds": "カードリーダー秒数",
|
"Card Reader Seconds": "カードリーダー秒数",
|
||||||
"Category": "カテゴリ",
|
|
||||||
"Change": "変更",
|
"Change": "変更",
|
||||||
"Change Stock": "小銭在庫",
|
"Change Stock": "小銭在庫",
|
||||||
"Channel Limits": "スロット上限",
|
"Channel Limits": "スロット上限",
|
||||||
@@ -195,7 +198,14 @@
|
|||||||
"Dispense Failed": "出庫失敗",
|
"Dispense Failed": "出庫失敗",
|
||||||
"Dispense Success": "出庫成功",
|
"Dispense Success": "出庫成功",
|
||||||
"Dispensing": "出庫中",
|
"Dispensing": "出庫中",
|
||||||
"E.SUN QR Scan": "玉山銀行 QR スキャン",
|
"Type to search or leave blank for system defaults.": "キーワードで検索するか、システムデフォルトの場合は空白のままにします。",
|
||||||
|
"Select Company (Default: System)": "会社を選択 (デフォルト:システム)",
|
||||||
|
"Search Company Title...": "会社名を検索...",
|
||||||
|
"System Default (Common)": "システムデフォルト (共通)",
|
||||||
|
"e.g., Beverage": "例:飲料",
|
||||||
|
"e.g., Drinks": "例:飲料 (英語)",
|
||||||
|
"e.g., お飲み物": "例:お飲み物",
|
||||||
|
"E.SUN QR Scan": "玉山QR決済",
|
||||||
"E.SUN QR Scan Settings Description": "玉山銀行 QR スキャン決済設定",
|
"E.SUN QR Scan Settings Description": "玉山銀行 QR スキャン決済設定",
|
||||||
"EASY_MERCHANT_ID": "悠遊付 加盟店ID",
|
"EASY_MERCHANT_ID": "悠遊付 加盟店ID",
|
||||||
"ECPay Invoice": "ECPay 電子発票",
|
"ECPay Invoice": "ECPay 電子発票",
|
||||||
@@ -870,5 +880,14 @@
|
|||||||
"Ad Settings": "広告設定",
|
"Ad Settings": "広告設定",
|
||||||
"System Default (All Companies)": "システムデフォルト(すべての会社)",
|
"System Default (All Companies)": "システムデフォルト(すべての会社)",
|
||||||
"No materials available": "利用可能な素材がありません",
|
"No materials available": "利用可能な素材がありません",
|
||||||
"Search...": "検索..."
|
"Search...": "検索...",
|
||||||
|
"Add Category": "新しいカテゴリー",
|
||||||
|
"Category Management": "カテゴリー管理",
|
||||||
|
"Category Name": "カテゴリー名",
|
||||||
|
"Edit Category": "カテゴリー編集",
|
||||||
|
"Manage your catalog, categories, and inventory settings.": "型録、カテゴリー、および在庫設定を管理します。",
|
||||||
|
"Multilingual Names": "多言語名",
|
||||||
|
"Barcode / Material": "バーコード / 材料",
|
||||||
|
"Product List": "商品リスト",
|
||||||
|
"Product Count": "商品数"
|
||||||
}
|
}
|
||||||
@@ -26,6 +26,7 @@
|
|||||||
"Actions": "操作",
|
"Actions": "操作",
|
||||||
"Active": "使用中",
|
"Active": "使用中",
|
||||||
"Active Status": "啟用狀態",
|
"Active Status": "啟用狀態",
|
||||||
|
"Add Category": "新增分類",
|
||||||
"Add Account": "新增帳號",
|
"Add Account": "新增帳號",
|
||||||
"Add Customer": "新增客戶",
|
"Add Customer": "新增客戶",
|
||||||
"Add Machine": "新增機台",
|
"Add Machine": "新增機台",
|
||||||
@@ -40,6 +41,7 @@
|
|||||||
"Admin display name": "管理員顯示名稱",
|
"Admin display name": "管理員顯示名稱",
|
||||||
"Administrator": "管理員",
|
"Administrator": "管理員",
|
||||||
"Advertisement Management": "廣告管理",
|
"Advertisement Management": "廣告管理",
|
||||||
|
"Affiliated Company": "公司名稱",
|
||||||
"Affiliated Unit": "公司名稱",
|
"Affiliated Unit": "公司名稱",
|
||||||
"Affiliation": "所屬單位",
|
"Affiliation": "所屬單位",
|
||||||
"Alert Summary": "告警摘要",
|
"Alert Summary": "告警摘要",
|
||||||
@@ -111,6 +113,9 @@
|
|||||||
"Card Reader Restart": "卡機重啟",
|
"Card Reader Restart": "卡機重啟",
|
||||||
"Card Reader Seconds": "刷卡機秒數",
|
"Card Reader Seconds": "刷卡機秒數",
|
||||||
"Category": "類別",
|
"Category": "類別",
|
||||||
|
"Category Name (zh_TW)": "分類名稱 (繁體中文)",
|
||||||
|
"Category Name (en)": "分類名稱 (英文)",
|
||||||
|
"Category Name (ja)": "分類名稱 (日文)",
|
||||||
"Change": "更換",
|
"Change": "更換",
|
||||||
"Change Stock": "零錢庫存",
|
"Change Stock": "零錢庫存",
|
||||||
"Channel Limits": "貨道上限",
|
"Channel Limits": "貨道上限",
|
||||||
@@ -201,15 +206,22 @@
|
|||||||
"Disable Product Confirmation": "停用商品確認",
|
"Disable Product Confirmation": "停用商品確認",
|
||||||
"Disabled": "已停用",
|
"Disabled": "已停用",
|
||||||
"Discord Notifications": "Discord通知",
|
"Discord Notifications": "Discord通知",
|
||||||
"Dispense Failed": "出貨失敗",
|
|
||||||
"Dispense Success": "出貨成功",
|
"Dispense Success": "出貨成功",
|
||||||
"Dispensing": "出貨",
|
"Dispensing": "出貨",
|
||||||
|
"Type to search or leave blank for system defaults.": "輸入關鍵字搜尋,或留空以使用系統預設。",
|
||||||
|
"Select Company (Default: System)": "選擇公司 (預設:系統)",
|
||||||
|
"Search Company Title...": "搜尋公司名稱...",
|
||||||
|
"System Default (Common)": "系統預設 (通用)",
|
||||||
|
"e.g., Beverage": "例如:飲料",
|
||||||
|
"e.g., Drinks": "例如:Drinks",
|
||||||
|
"e.g., お飲み物": "例如:お飲み物",
|
||||||
"E.SUN QR Scan": "玉山銀行標籤支付",
|
"E.SUN QR Scan": "玉山銀行標籤支付",
|
||||||
"E.SUN QR Scan Settings Description": "玉山銀行掃碼支付設定",
|
"E.SUN QR Scan Settings Description": "玉山銀行掃碼支付設定",
|
||||||
"EASY_MERCHANT_ID": "悠遊付 商店代號",
|
"EASY_MERCHANT_ID": "悠遊付 商店代號",
|
||||||
"ECPay Invoice": "綠界電子發票",
|
"ECPay Invoice": "綠界電子發票",
|
||||||
"ECPay Invoice Settings Description": "綠界科技電子發票設定",
|
"ECPay Invoice Settings Description": "綠界科技電子發票設定",
|
||||||
"Edit": "編輯",
|
"Edit": "編輯",
|
||||||
|
"Edit Category": "編輯分類",
|
||||||
"Edit Account": "編輯帳號",
|
"Edit Account": "編輯帳號",
|
||||||
"Edit Customer": "編輯客戶",
|
"Edit Customer": "編輯客戶",
|
||||||
"Edit Expiry": "編輯效期",
|
"Edit Expiry": "編輯效期",
|
||||||
@@ -510,6 +522,8 @@
|
|||||||
"Product Info": "商品資訊",
|
"Product Info": "商品資訊",
|
||||||
"Product Management": "商品管理",
|
"Product Management": "商品管理",
|
||||||
"Product Name (Multilingual)": "商品名稱 (多語系)",
|
"Product Name (Multilingual)": "商品名稱 (多語系)",
|
||||||
|
"Product Count": "商品數量",
|
||||||
|
"Product List": "商品清單",
|
||||||
"Product Reports": "商品報表",
|
"Product Reports": "商品報表",
|
||||||
"Product Status": "商品狀態",
|
"Product Status": "商品狀態",
|
||||||
"Product created successfully": "商品已成功建立",
|
"Product created successfully": "商品已成功建立",
|
||||||
@@ -798,6 +812,9 @@
|
|||||||
"menu.machines": "機台管理",
|
"menu.machines": "機台管理",
|
||||||
"menu.machines.list": "機台列表",
|
"menu.machines.list": "機台列表",
|
||||||
"menu.machines.maintenance": "維修管理單",
|
"menu.machines.maintenance": "維修管理單",
|
||||||
|
"Manage your catalog, categories, and inventory settings.": "管理您的商品型錄、分類及庫存設定。",
|
||||||
|
"Multilingual Names": "多語系名稱",
|
||||||
|
"Barcode / Material": "條碼 / 物料編碼",
|
||||||
"menu.machines.permissions": "機台權限",
|
"menu.machines.permissions": "機台權限",
|
||||||
"menu.machines.utilization": "機台嫁動率",
|
"menu.machines.utilization": "機台嫁動率",
|
||||||
"menu.members": "會員管理",
|
"menu.members": "會員管理",
|
||||||
@@ -894,5 +911,6 @@
|
|||||||
"Ad Settings": "廣告設置",
|
"Ad Settings": "廣告設置",
|
||||||
"System Default (All Companies)": "系統預設 (所有公司)",
|
"System Default (All Companies)": "系統預設 (所有公司)",
|
||||||
"No materials available": "沒有可用的素材",
|
"No materials available": "沒有可用的素材",
|
||||||
"Search...": "搜尋..."
|
"Search...": "搜尋...",
|
||||||
|
"Category Management": "分類管理"
|
||||||
}
|
}
|
||||||
164
package-lock.json
generated
164
package-lock.json
generated
@@ -1,9 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "html",
|
"name": "star-cloud",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
|
"dependencies": {
|
||||||
|
"pptxgenjs": "^4.0.1"
|
||||||
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@alpinejs/collapse": "^3.15.3",
|
"@alpinejs/collapse": "^3.15.3",
|
||||||
"@tailwindcss/forms": "^0.5.2",
|
"@tailwindcss/forms": "^0.5.2",
|
||||||
@@ -871,6 +874,7 @@
|
|||||||
"integrity": "sha512-/VNHWYhNu+BS7ktbYoVGrCmsXDh+chFMaONMwGNdIBcFHrWqk2jY8fNyr3DLdtQUIalvkPfM554ZSFa3dm3nxQ==",
|
"integrity": "sha512-/VNHWYhNu+BS7ktbYoVGrCmsXDh+chFMaONMwGNdIBcFHrWqk2jY8fNyr3DLdtQUIalvkPfM554ZSFa3dm3nxQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"funding": {
|
"funding": {
|
||||||
"type": "github",
|
"type": "github",
|
||||||
"url": "https://github.com/sponsors/Fuzzyma"
|
"url": "https://github.com/sponsors/Fuzzyma"
|
||||||
@@ -896,6 +900,7 @@
|
|||||||
"integrity": "sha512-qkMgso1sd2hXKd1FZ1weO7ANq12sNmQJeGDjs46QwDVsxSRcHmvWKL2NDF7Yimpwf3sl5esOLkPqtV2bQ3v/Jg==",
|
"integrity": "sha512-qkMgso1sd2hXKd1FZ1weO7ANq12sNmQJeGDjs46QwDVsxSRcHmvWKL2NDF7Yimpwf3sl5esOLkPqtV2bQ3v/Jg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 14.18"
|
"node": ">= 14.18"
|
||||||
},
|
},
|
||||||
@@ -930,6 +935,15 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/node": {
|
||||||
|
"version": "22.19.15",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.15.tgz",
|
||||||
|
"integrity": "sha512-F0R/h2+dsy5wJAUe3tAU6oqa2qbWY5TpNfL/RGmo1y38hiyO1w3x2jPtt76wmuaJI4DQnOBu21cNXQ2STIUUWg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"undici-types": "~6.21.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@vue/reactivity": {
|
"node_modules/@vue/reactivity": {
|
||||||
"version": "3.1.5",
|
"version": "3.1.5",
|
||||||
"resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.1.5.tgz",
|
"resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.1.5.tgz",
|
||||||
@@ -1123,6 +1137,7 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"baseline-browser-mapping": "^2.8.25",
|
"baseline-browser-mapping": "^2.8.25",
|
||||||
"caniuse-lite": "^1.0.30001754",
|
"caniuse-lite": "^1.0.30001754",
|
||||||
@@ -1243,6 +1258,12 @@
|
|||||||
"node": ">= 6"
|
"node": ">= 6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/core-util-is": {
|
||||||
|
"version": "1.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
|
||||||
|
"integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/cssesc": {
|
"node_modules/cssesc": {
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
|
||||||
@@ -1669,6 +1690,39 @@
|
|||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/https": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/https/-/https-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-4EC57ddXrkaF0x83Oj8sM6SLQHAWXw90Skqu2M4AEWENZ3F02dFJE/GARA8igO79tcgYqGrD7ae4f5L3um2lgg==",
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
|
"node_modules/image-size": {
|
||||||
|
"version": "1.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/image-size/-/image-size-1.2.1.tgz",
|
||||||
|
"integrity": "sha512-rH+46sQJ2dlwfjfhCyNx5thzrv+dtmBIhPHk0zgRUukHzZ/kRueTJXoYYsclBaKcSMBWuGbOFXtioLpzTb5euw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"queue": "6.0.2"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"image-size": "bin/image-size.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=16.x"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/immediate": {
|
||||||
|
"version": "3.0.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz",
|
||||||
|
"integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/inherits": {
|
||||||
|
"version": "2.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
|
||||||
|
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
"node_modules/is-binary-path": {
|
"node_modules/is-binary-path": {
|
||||||
"version": "2.1.0",
|
"version": "2.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
|
||||||
@@ -1731,12 +1785,19 @@
|
|||||||
"node": ">=0.12.0"
|
"node": ">=0.12.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/isarray": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/jiti": {
|
"node_modules/jiti": {
|
||||||
"version": "1.21.7",
|
"version": "1.21.7",
|
||||||
"resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz",
|
"resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz",
|
||||||
"integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==",
|
"integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"bin": {
|
"bin": {
|
||||||
"jiti": "bin/jiti.js"
|
"jiti": "bin/jiti.js"
|
||||||
}
|
}
|
||||||
@@ -1748,6 +1809,18 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/jszip": {
|
||||||
|
"version": "3.10.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz",
|
||||||
|
"integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==",
|
||||||
|
"license": "(MIT OR GPL-3.0-or-later)",
|
||||||
|
"dependencies": {
|
||||||
|
"lie": "~3.3.0",
|
||||||
|
"pako": "~1.0.2",
|
||||||
|
"readable-stream": "~2.3.6",
|
||||||
|
"setimmediate": "^1.0.5"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/just-extend": {
|
"node_modules/just-extend": {
|
||||||
"version": "5.1.1",
|
"version": "5.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/just-extend/-/just-extend-5.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/just-extend/-/just-extend-5.1.1.tgz",
|
||||||
@@ -1775,6 +1848,15 @@
|
|||||||
"vite": "^5.0.0 || ^6.0.0"
|
"vite": "^5.0.0 || ^6.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/lie": {
|
||||||
|
"version": "3.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz",
|
||||||
|
"integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"immediate": "~3.0.5"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/lilconfig": {
|
"node_modules/lilconfig": {
|
||||||
"version": "3.1.3",
|
"version": "3.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz",
|
||||||
@@ -1947,6 +2029,12 @@
|
|||||||
"node": ">= 6"
|
"node": ">= 6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/pako": {
|
||||||
|
"version": "1.0.11",
|
||||||
|
"resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz",
|
||||||
|
"integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==",
|
||||||
|
"license": "(MIT AND Zlib)"
|
||||||
|
},
|
||||||
"node_modules/path-parse": {
|
"node_modules/path-parse": {
|
||||||
"version": "1.0.7",
|
"version": "1.0.7",
|
||||||
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
|
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
|
||||||
@@ -2014,6 +2102,7 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"nanoid": "^3.3.11",
|
"nanoid": "^3.3.11",
|
||||||
"picocolors": "^1.1.1",
|
"picocolors": "^1.1.1",
|
||||||
@@ -2157,6 +2246,18 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/pptxgenjs": {
|
||||||
|
"version": "4.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/pptxgenjs/-/pptxgenjs-4.0.1.tgz",
|
||||||
|
"integrity": "sha512-TeJISr8wouAuXw4C1F/mC33xbZs/FuEG6nH9FG1Zj+nuPcGMP5YRHl6X+j3HSUnS1f3at6k75ZZXPMZlA5Lj9A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/node": "^22.8.1",
|
||||||
|
"https": "^1.0.0",
|
||||||
|
"image-size": "^1.2.1",
|
||||||
|
"jszip": "^3.10.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/preline": {
|
"node_modules/preline": {
|
||||||
"version": "3.2.3",
|
"version": "3.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/preline/-/preline-3.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/preline/-/preline-3.2.3.tgz",
|
||||||
@@ -2172,6 +2273,12 @@
|
|||||||
"vanilla-calendar-pro": "^3.0.4"
|
"vanilla-calendar-pro": "^3.0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/process-nextick-args": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/proxy-from-env": {
|
"node_modules/proxy-from-env": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
||||||
@@ -2179,6 +2286,15 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/queue": {
|
||||||
|
"version": "6.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/queue/-/queue-6.0.2.tgz",
|
||||||
|
"integrity": "sha512-iHZWu+q3IdFZFX36ro/lKBkSvfkztY5Y7HMiPlOUjhupPcG2JMfst2KKEpu5XndviX/3UhFbRngUPNKtgvtZiA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"inherits": "~2.0.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/queue-microtask": {
|
"node_modules/queue-microtask": {
|
||||||
"version": "1.2.3",
|
"version": "1.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
|
||||||
@@ -2210,6 +2326,21 @@
|
|||||||
"pify": "^2.3.0"
|
"pify": "^2.3.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/readable-stream": {
|
||||||
|
"version": "2.3.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
|
||||||
|
"integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"core-util-is": "~1.0.0",
|
||||||
|
"inherits": "~2.0.3",
|
||||||
|
"isarray": "~1.0.0",
|
||||||
|
"process-nextick-args": "~2.0.0",
|
||||||
|
"safe-buffer": "~5.1.1",
|
||||||
|
"string_decoder": "~1.1.1",
|
||||||
|
"util-deprecate": "~1.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/readdirp": {
|
"node_modules/readdirp": {
|
||||||
"version": "3.6.0",
|
"version": "3.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
|
||||||
@@ -2321,6 +2452,18 @@
|
|||||||
"queue-microtask": "^1.2.2"
|
"queue-microtask": "^1.2.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/safe-buffer": {
|
||||||
|
"version": "5.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
|
||||||
|
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/setimmediate": {
|
||||||
|
"version": "1.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz",
|
||||||
|
"integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/source-map-js": {
|
"node_modules/source-map-js": {
|
||||||
"version": "1.2.1",
|
"version": "1.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
||||||
@@ -2331,6 +2474,15 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/string_decoder": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"safe-buffer": "~5.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/sucrase": {
|
"node_modules/sucrase": {
|
||||||
"version": "3.35.1",
|
"version": "3.35.1",
|
||||||
"resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz",
|
"resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz",
|
||||||
@@ -2373,6 +2525,7 @@
|
|||||||
"integrity": "sha512-6A2rnmW5xZMdw11LYjhcI5846rt9pbLSabY5XPxo+XWdxwZaFEn47Go4NzFiHu9sNNmr/kXivP1vStfvMaK1GQ==",
|
"integrity": "sha512-6A2rnmW5xZMdw11LYjhcI5846rt9pbLSabY5XPxo+XWdxwZaFEn47Go4NzFiHu9sNNmr/kXivP1vStfvMaK1GQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@alloc/quick-lru": "^5.2.0",
|
"@alloc/quick-lru": "^5.2.0",
|
||||||
"arg": "^5.0.2",
|
"arg": "^5.0.2",
|
||||||
@@ -2469,6 +2622,7 @@
|
|||||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
},
|
},
|
||||||
@@ -2496,6 +2650,12 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "Apache-2.0"
|
"license": "Apache-2.0"
|
||||||
},
|
},
|
||||||
|
"node_modules/undici-types": {
|
||||||
|
"version": "6.21.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
||||||
|
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/update-browserslist-db": {
|
"node_modules/update-browserslist-db": {
|
||||||
"version": "1.1.4",
|
"version": "1.1.4",
|
||||||
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.4.tgz",
|
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.4.tgz",
|
||||||
@@ -2531,7 +2691,6 @@
|
|||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||||
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
|
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/vanilla-calendar-pro": {
|
"node_modules/vanilla-calendar-pro": {
|
||||||
@@ -2551,6 +2710,7 @@
|
|||||||
"integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==",
|
"integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"esbuild": "^0.21.3",
|
"esbuild": "^0.21.3",
|
||||||
"postcss": "^8.4.43",
|
"postcss": "^8.4.43",
|
||||||
|
|||||||
@@ -17,5 +17,8 @@
|
|||||||
"preline": "^3.2.3",
|
"preline": "^3.2.3",
|
||||||
"tailwindcss": "^3.1.0",
|
"tailwindcss": "^3.1.0",
|
||||||
"vite": "^5.0.0"
|
"vite": "^5.0.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"pptxgenjs": "^4.0.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
135
pptx_gen.cjs
Normal file
135
pptx_gen.cjs
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
const pptxgen = require("pptxgenjs");
|
||||||
|
const fs = require("fs");
|
||||||
|
|
||||||
|
const pres = new pptxgen();
|
||||||
|
pres.layout = "LAYOUT_16x9";
|
||||||
|
pres.title = "Star Cloud Demo Day - Technical Edition";
|
||||||
|
pres.author = "許家偉";
|
||||||
|
|
||||||
|
// --- Theme Colors ---
|
||||||
|
const COLORS = {
|
||||||
|
bg: "0F172A",
|
||||||
|
cardBg: "1E293B",
|
||||||
|
highlight: "2DD4BF",
|
||||||
|
secondary: "7DD3FC",
|
||||||
|
text: "FFFFFF",
|
||||||
|
muted: "94A3B8"
|
||||||
|
};
|
||||||
|
|
||||||
|
const FONTS = {
|
||||||
|
header: "Georgia",
|
||||||
|
body: "Calibri"
|
||||||
|
};
|
||||||
|
|
||||||
|
const makeShadow = (opacity = 0.2) => ({
|
||||||
|
type: "outer",
|
||||||
|
blur: 8,
|
||||||
|
offset: 3,
|
||||||
|
angle: 135,
|
||||||
|
color: "000000",
|
||||||
|
opacity: opacity
|
||||||
|
});
|
||||||
|
|
||||||
|
// --- Slide 1: Title ---
|
||||||
|
const slide1 = pres.addSlide();
|
||||||
|
slide1.background = { color: COLORS.bg };
|
||||||
|
slide1.addShape(pres.shapes.RECTANGLE, { x: 0, y: 0, w: 0.15, h: "100%", fill: { color: COLORS.highlight } });
|
||||||
|
slide1.addText("Star Cloud Demo Day", { x: 1.0, y: 1.8, w: 8, h: 1, fontSize: 48, fontFace: FONTS.header, color: COLORS.text, bold: true, margin: 0 });
|
||||||
|
slide1.addText("2026-03-25 ~ 2026-03-31 成果發表", { x: 1.0, y: 2.8, w: 8, h: 0.5, fontSize: 24, fontFace: FONTS.body, color: COLORS.secondary, italic: true });
|
||||||
|
slide1.addShape(pres.shapes.RECTANGLE, { x: 7.5, y: 4.5, w: 2, h: 0.6, fill: { color: COLORS.highlight, transparency: 10 }, shadow: makeShadow() });
|
||||||
|
slide1.addText("演講者:許家偉", { x: 7.5, y: 4.5, w: 2, h: 0.6, fontSize: 14, fontFace: FONTS.body, color: COLORS.text, align: "center", valign: "middle", bold: true });
|
||||||
|
|
||||||
|
// --- Slide 2: Overview ---
|
||||||
|
const slide2 = pres.addSlide();
|
||||||
|
slide2.background = { color: COLORS.bg };
|
||||||
|
slide2.addText("本週進度總覽 / Overview", { x: 0.5, y: 0.4, w: 9, h: 0.6, fontSize: 32, fontFace: FONTS.header, color: COLORS.highlight, bold: true });
|
||||||
|
slide2.addShape(pres.shapes.LINE, { x: 2, y: 3, w: 6, h: 0, line: { color: COLORS.highlight, width: 4 } });
|
||||||
|
const timelineItems = [
|
||||||
|
{ title: "機台權限管理", desc: "模組獨立化與效能優化" },
|
||||||
|
{ title: "商品管理整合", desc: "狀態整合與多語系修復" },
|
||||||
|
{ title: "廣告隔離機制", desc: "多租戶素材歸屬強化" }
|
||||||
|
];
|
||||||
|
timelineItems.forEach((item, idx) => {
|
||||||
|
const x = 2 + (idx * 3);
|
||||||
|
slide2.addShape(pres.shapes.OVAL, { x: x - 0.2, y: 2.8, w: 0.4, h: 0.4, fill: { color: COLORS.secondary }, line: { color: COLORS.text, width: 2 } });
|
||||||
|
slide2.addText(item.title, { x: x - 1, y: 3.3, w: 2, h: 0.4, fontSize: 18, fontFace: FONTS.header, color: COLORS.text, align: "center", bold: true });
|
||||||
|
slide2.addText(item.desc, { x: x - 1, y: 3.7, w: 2, h: 0.6, fontSize: 12, fontFace: FONTS.body, color: COLORS.muted, align: "center" });
|
||||||
|
});
|
||||||
|
|
||||||
|
// --- Slide 3: Highlight 1 - Machine Permissions (WITH EAGER LOADING) ---
|
||||||
|
const slide3 = pres.addSlide();
|
||||||
|
slide3.background = { color: COLORS.bg };
|
||||||
|
slide3.addText("亮點一:機台權限管理獨立化", { x: 0.5, y: 0.4, w: 9, h: 0.6, fontSize: 28, fontFace: FONTS.header, color: COLORS.highlight, bold: true });
|
||||||
|
|
||||||
|
const features = [
|
||||||
|
{ title: "直覺檢索", text: "管理員可快速檢索、分配所屬帳號對應的機台清單。" },
|
||||||
|
{ title: "即時過濾", text: "透過 Alpine.js 實作,輸入關鍵字即動態無刷新過濾。" },
|
||||||
|
{ title: "效能關鍵:Eager Loading", text: "使用 with('machines') 解決 N+1 問題,萬筆資料秒開。" }
|
||||||
|
];
|
||||||
|
|
||||||
|
features.forEach((f, idx) => {
|
||||||
|
const y = 1.2 + (idx * 1.3);
|
||||||
|
slide3.addShape(pres.shapes.RECTANGLE, { x: 0.5, y: y, w: 4, h: 1.1, fill: { color: COLORS.cardBg }, line: { color: COLORS.secondary, width: 1 }, shadow: makeShadow() });
|
||||||
|
slide3.addText(f.title, { x: 0.7, y: y + 0.1, w: 3.5, h: 0.3, fontSize: 16, fontFace: FONTS.header, color: COLORS.highlight, bold: true });
|
||||||
|
slide3.addText(f.text, { x: 0.7, y: y + 0.45, w: 3.5, h: 0.5, fontSize: 12, fontFace: FONTS.body, color: COLORS.text });
|
||||||
|
});
|
||||||
|
|
||||||
|
slide3.addImage({
|
||||||
|
path: "/home/mama/.gemini/antigravity/brain/58a170d0-7144-4e9f-9396-3e753a0bf69a/machine_permissions_table_1774944648298.png",
|
||||||
|
x: 4.8, y: 1.2, w: 4.8, h: 3.7, sizing: { type: 'contain' }, shadow: makeShadow(0.3)
|
||||||
|
});
|
||||||
|
|
||||||
|
// --- Slide 4: Highlight 2 ---
|
||||||
|
const slide4 = pres.addSlide();
|
||||||
|
slide4.background = { color: COLORS.bg };
|
||||||
|
slide4.addText("亮點二:商品管理整合與 UX 完善", { x: 0.5, y: 0.4, w: 9, h: 0.6, fontSize: 28, fontFace: FONTS.header, color: COLORS.highlight, bold: true });
|
||||||
|
const comparison = [
|
||||||
|
{ title: "功能整合 (Integrated)", items: ["• 「商品狀態」納入主流程", "• 減少跳轉,提升管理效率"] },
|
||||||
|
{ title: "細節優化 (Enhanced)", items: ["• 修復多語系(ZH/EN/JA)存取故障", "• 密碼欄位新增顯隱切換按鈕"] }
|
||||||
|
];
|
||||||
|
comparison.forEach((c, idx) => {
|
||||||
|
const x = 0.5 + (idx * 4.75);
|
||||||
|
slide4.addShape(pres.shapes.RECTANGLE, { x: x, y: 1.2, w: 4.25, h: 3.5, fill: { color: COLORS.cardBg }, line: { color: COLORS.highlight, width: 2 }, shadow: makeShadow(0.2) });
|
||||||
|
slide4.addText(c.title, { x: x + 0.2, y: 1.4, w: 3.8, h: 0.4, fontSize: 18, fontFace: FONTS.header, color: COLORS.highlight, bold: true, align: "center" });
|
||||||
|
slide4.addText(c.items.map(i => ({ text: i, options: { breakLine: true, bullet: true } })), { x: x + 0.3, y: 2.0, w: 3.6, h: 2.5, fontSize: 14, fontFace: FONTS.body, color: COLORS.text, margin: 0 });
|
||||||
|
});
|
||||||
|
|
||||||
|
// --- Slide 5: Highlight 3 ---
|
||||||
|
const slide5 = pres.addSlide();
|
||||||
|
slide5.background = { color: COLORS.bg };
|
||||||
|
slide5.addText("亮點三:多租戶廣告素材隔離機制", { x: 1.0, y: 0.4, w: 8, h: 1, fontSize: 32, fontFace: FONTS.header, color: COLORS.highlight, bold: true, align: "center" });
|
||||||
|
slide5.addShape(pres.shapes.OVAL, { x: 4.25, y: 2.0, w: 1.5, h: 1.5, fill: { color: COLORS.highlight, transparency: 30 }, line: { color: COLORS.secondary, width: 2 } });
|
||||||
|
slide5.addText("安全性隔離\n(Shield)", { x: 4.25, y: 2.5, w: 1.5, h: 0.5, fontSize: 14, fontFace: FONTS.header, color: COLORS.text, align: "center", bold: true });
|
||||||
|
const isolations = [
|
||||||
|
{ x: 1, y: 1.8, label: "公司 A\n廣告素材", color: "A8DADC" },
|
||||||
|
{ x: 7.5, y: 1.8, label: "公司 B\n廣告素材", color: "A8DADC" },
|
||||||
|
{ x: 1, y: 3.8, label: "公司 C\n廣告素材", color: "A8DADC" },
|
||||||
|
{ x: 7.5, y: 3.8, label: "公司 D\n廣告素材", color: "A8DADC" }
|
||||||
|
];
|
||||||
|
isolations.forEach(i => {
|
||||||
|
slide5.addShape(pres.shapes.ROUNDED_RECTANGLE, { x: i.x, y: i.y, w: 1.5, h: 0.8, fill: { color: COLORS.cardBg }, line: { color: COLORS.secondary, width: 1 }, rectRadius: 0.1 });
|
||||||
|
slide5.addText(i.label, { x: i.x, y: i.y, w: 1.5, h: 0.8, fontSize: 12, fontFace: FONTS.body, color: COLORS.text, align: "center", valign: "middle" });
|
||||||
|
slide5.addShape(pres.shapes.LINE, { x: i.x + (i.x < 5 ? 1.5 : 0), y: i.y + 0.4, w: i.x < 5 ? (4.25 - (i.x + 1.5)) : (7.5 - (i.x + 1.5)), h: 2.75 - (i.y + 0.4), line: { color: COLORS.secondary, width: 1, dashType: "dash" } });
|
||||||
|
});
|
||||||
|
slide5.addText("嚴格驗證 company_id,確保素材 100% 歸屬隔離。", { x: 0.5, y: 5.0, w: 9, h: 0.4, fontSize: 14, fontFace: FONTS.body, color: COLORS.muted, align: "center", italic: true });
|
||||||
|
|
||||||
|
// --- Slide 6: Future Roadmap ---
|
||||||
|
const slide6 = pres.addSlide();
|
||||||
|
slide6.background = { color: COLORS.bg };
|
||||||
|
slide6.addText("未來計畫 / Roadmap", { x: 0.5, y: 0.4, w: 9, h: 1, fontSize: 36, fontFace: FONTS.header, color: COLORS.highlight, bold: true, align: "center" });
|
||||||
|
const roadmap = [
|
||||||
|
{ icon: "Optim", title: "操作優化", text: "針對樂樂反饋優化操作流程與介面體驗。" },
|
||||||
|
{ icon: "Sync", title: "API 對接", text: "與 Terry 配合,實現機台通訊穩定與對接。" },
|
||||||
|
{ icon: "Remote", title: "遠端管理", text: "實作遠端控制與異常即時監控監管。" }
|
||||||
|
];
|
||||||
|
roadmap.forEach((r, idx) => {
|
||||||
|
const x = 0.5 + (idx * 3.1);
|
||||||
|
slide6.addShape(pres.shapes.RECTANGLE, { x: x, y: 2.0, w: 2.8, h: 2.5, fill: { color: COLORS.highlight, transparency: 85 }, line: { color: COLORS.highlight, width: 2 }, shadow: makeShadow() });
|
||||||
|
slide6.addText(r.title, { x: x, y: 2.3, w: 2.8, h: 0.5, fontSize: 20, fontFace: FONTS.header, color: COLORS.highlight, bold: true, align: "center" });
|
||||||
|
slide6.addText(r.text, { x: x + 0.2, y: 3.0, w: 2.4, h: 1.2, fontSize: 14, fontFace: FONTS.body, color: COLORS.text, align: "center" });
|
||||||
|
});
|
||||||
|
slide6.addText("Star Cloud 將持續進化,打造最領先的智能雲端平台。🚀", { x: 0.5, y: 5.0, w: 9, h: 0.4, fontSize: 14, fontFace: FONTS.body, color: COLORS.secondary, align: "center", bold: true });
|
||||||
|
|
||||||
|
pres.writeFile({ fileName: "star-cloud-demo-20260331.pptx" })
|
||||||
|
.then(fileName => console.log(`Presentation created: ${fileName}`))
|
||||||
|
.catch(err => { console.error("Error creating presentation:", err); process.exit(1); });
|
||||||
@@ -10,18 +10,18 @@
|
|||||||
|
|
||||||
<div class="fixed inset-0 bg-slate-900/60 backdrop-blur-sm"></div>
|
<div class="fixed inset-0 bg-slate-900/60 backdrop-blur-sm"></div>
|
||||||
|
|
||||||
<div class="flex items-center justify-center min-h-screen p-4">
|
<div class="flex items-center justify-center min-h-screen p-4 sm:p-0">
|
||||||
<div class="relative w-full max-w-xl bg-white dark:bg-slate-900 rounded-[2rem] shadow-2xl border border-slate-200 dark:border-white/10 overflow-hidden animate-luxury-in"
|
<div class="relative inline-block px-8 py-10 text-left align-bottom transition-all transform luxury-card rounded-3xl dark:bg-slate-900 border-slate-200/50 dark:border-slate-700/50 shadow-2xl sm:my-8 sm:align-middle sm:max-w-xl sm:w-full overflow-visible animate-luxury-in"
|
||||||
@click.away="isAdModalOpen = false">
|
@click.away="isAdModalOpen = false">
|
||||||
|
|
||||||
<!-- Modal Header -->
|
<!-- Modal Header -->
|
||||||
<div class="bg-slate-50/50 dark:bg-slate-800/50 px-8 py-5 border-b border-slate-100 dark:border-white/5 flex items-center justify-between">
|
<div class="flex justify-between items-center mb-8">
|
||||||
<div>
|
<div>
|
||||||
<h3 class="text-xl font-black text-slate-800 dark:text-white uppercase tracking-tight" x-text="adFormMode === 'add' ? '{{ __("Add Advertisement") }}' : '{{ __("Edit Advertisement") }}'"></h3>
|
<h3 class="text-2xl font-black text-slate-800 dark:text-white font-display tracking-tight" x-text="adFormMode === 'add' ? '{{ __("Add Advertisement") }}' : '{{ __("Edit Advertisement") }}'"></h3>
|
||||||
<p class="text-[10px] font-bold text-slate-400 uppercase tracking-widest mt-1">{{ __('Manage your ad material details') }}</p>
|
<p class="text-[10px] font-bold text-slate-400 uppercase tracking-widest mt-1">{{ __('Manage your ad material details') }}</p>
|
||||||
</div>
|
</div>
|
||||||
<button @click="isAdModalOpen = false" class="p-2 text-slate-400 hover:text-cyan-500 transition-colors">
|
<button @click="isAdModalOpen = false" class="text-slate-400 hover:text-slate-600 dark:hover:text-slate-200 transition-colors">
|
||||||
<svg class="size-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" /></svg>
|
<svg class="size-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M6 18L18 6M6 6l12 12" /></svg>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -29,7 +29,7 @@
|
|||||||
method="POST"
|
method="POST"
|
||||||
enctype="multipart/form-data"
|
enctype="multipart/form-data"
|
||||||
@submit.prevent="submitAdForm"
|
@submit.prevent="submitAdForm"
|
||||||
class="px-8 pt-4 pb-8 space-y-4">
|
class="space-y-6">
|
||||||
@csrf
|
@csrf
|
||||||
<template x-if="adFormMode === 'edit'">
|
<template x-if="adFormMode === 'edit'">
|
||||||
@method('PUT')
|
@method('PUT')
|
||||||
|
|||||||
@@ -10,22 +10,22 @@
|
|||||||
|
|
||||||
<div class="fixed inset-0 bg-slate-900/60 backdrop-blur-sm"></div>
|
<div class="fixed inset-0 bg-slate-900/60 backdrop-blur-sm"></div>
|
||||||
|
|
||||||
<div class="flex items-center justify-center min-h-screen p-4">
|
<div class="flex items-center justify-center min-h-screen p-4 sm:p-0">
|
||||||
<div class="relative w-full max-w-lg bg-white dark:bg-slate-900 rounded-[2rem] shadow-2xl border border-slate-200 dark:border-white/10 overflow-visible animate-luxury-in"
|
<div class="relative inline-block px-8 py-10 text-left align-bottom transition-all transform luxury-card rounded-3xl dark:bg-slate-900 border-slate-200/50 dark:border-slate-700/50 shadow-2xl sm:my-8 sm:align-middle sm:max-w-lg sm:w-full overflow-visible animate-luxury-in"
|
||||||
@click.away="isAssignModalOpen = false">
|
@click.away="isAssignModalOpen = false">
|
||||||
|
|
||||||
<!-- Modal Header -->
|
<!-- Modal Header -->
|
||||||
<div class="bg-slate-50/50 dark:bg-slate-800/50 rounded-t-[2rem] px-8 py-6 border-b border-slate-100 dark:border-white/5 flex items-center justify-between">
|
<div class="flex justify-between items-center mb-8">
|
||||||
<div>
|
<div>
|
||||||
<h3 class="text-xl font-black text-slate-800 dark:text-white uppercase tracking-tight">{{ __('Assign Advertisement') }}</h3>
|
<h3 class="text-2xl font-black text-slate-800 dark:text-white font-display tracking-tight">{{ __('Assign Advertisement') }}</h3>
|
||||||
<p class="text-[10px] font-bold text-slate-400 uppercase tracking-widest mt-1">{{ __('Select a material to play on this machine') }}</p>
|
<p class="text-[10px] font-bold text-slate-400 uppercase tracking-widest mt-1">{{ __('Select a material to play on this machine') }}</p>
|
||||||
</div>
|
</div>
|
||||||
<button @click="isAssignModalOpen = false" class="p-2 text-slate-400 hover:text-cyan-500 transition-colors">
|
<button @click="isAssignModalOpen = false" class="text-slate-400 hover:text-slate-600 dark:hover:text-slate-200 transition-colors">
|
||||||
<svg class="size-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" /></svg>
|
<svg class="size-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M6 18L18 6M6 6l12 12" /></svg>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form @submit.prevent="submitAssignment" class="p-8 space-y-6">
|
<form @submit.prevent="submitAssignment" class="space-y-6">
|
||||||
<!-- Machine & Position Info (Read-only) -->
|
<!-- Machine & Position Info (Read-only) -->
|
||||||
<div class="grid grid-cols-2 gap-4">
|
<div class="grid grid-cols-2 gap-4">
|
||||||
<div class="p-4 bg-slate-50 dark:bg-slate-800/50 rounded-2xl border border-slate-100 dark:border-white/5">
|
<div class="p-4 bg-slate-50 dark:bg-slate-800/50 rounded-2xl border border-slate-100 dark:border-white/5">
|
||||||
|
|||||||
@@ -170,13 +170,7 @@
|
|||||||
<x-searchable-select id="product-category" name="category_id" x-model="formData.category_id" :placeholder="__('Uncategorized')">
|
<x-searchable-select id="product-category" name="category_id" x-model="formData.category_id" :placeholder="__('Uncategorized')">
|
||||||
<option value="">{{ __('Uncategorized') }}</option>
|
<option value="">{{ __('Uncategorized') }}</option>
|
||||||
@foreach($categories as $category)
|
@foreach($categories as $category)
|
||||||
@php
|
<option value="{{ $category->id }}" data-title="{{ $category->localized_name }}">{{ $category->localized_name }}</option>
|
||||||
$catName = $category->name;
|
|
||||||
if ($category->name_dictionary_key) {
|
|
||||||
$catName = __($category->name_dictionary_key);
|
|
||||||
}
|
|
||||||
@endphp
|
|
||||||
<option value="{{ $category->id }}" data-title="{{ $catName }}">{{ $catName }}</option>
|
|
||||||
@endforeach
|
@endforeach
|
||||||
</x-searchable-select>
|
</x-searchable-select>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -185,13 +185,7 @@
|
|||||||
<x-searchable-select id="product-category" name="category_id" x-model="formData.category_id" :placeholder="__('Uncategorized')">
|
<x-searchable-select id="product-category" name="category_id" x-model="formData.category_id" :placeholder="__('Uncategorized')">
|
||||||
<option value="">{{ __('Uncategorized') }}</option>
|
<option value="">{{ __('Uncategorized') }}</option>
|
||||||
@foreach($categories as $category)
|
@foreach($categories as $category)
|
||||||
@php
|
<option value="{{ $category->id }}" {{ $product->category_id == $category->id ? 'selected' : '' }} data-title="{{ $category->localized_name }}">{{ $category->localized_name }}</option>
|
||||||
$catName = $category->name;
|
|
||||||
if ($category->name_dictionary_key) {
|
|
||||||
$catName = __($category->name_dictionary_key);
|
|
||||||
}
|
|
||||||
@endphp
|
|
||||||
<option value="{{ $category->id }}" {{ $product->category_id == $category->id ? 'selected' : '' }} data-title="{{ $catName }}">{{ $catName }}</option>
|
|
||||||
@endforeach
|
@endforeach
|
||||||
</x-searchable-select>
|
</x-searchable-select>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ $roleSelectConfig = [
|
|||||||
@endphp
|
@endphp
|
||||||
|
|
||||||
@section('content')
|
@section('content')
|
||||||
<div class="space-y-6 pb-20"
|
<div class="space-y-2 pb-20"
|
||||||
x-data="productManager"
|
x-data="productManager"
|
||||||
data-categories="{{ json_encode($categories) }}"
|
data-categories="{{ json_encode($categories) }}"
|
||||||
data-settings="{{ json_encode($companySettings) }}"
|
data-settings="{{ json_encode($companySettings) }}"
|
||||||
@@ -24,7 +24,7 @@ $roleSelectConfig = [
|
|||||||
data-index-url="{{ route($baseRoute . '.index') }}">
|
data-index-url="{{ route($baseRoute . '.index') }}">
|
||||||
|
|
||||||
<!-- 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-4">
|
||||||
<div>
|
<div>
|
||||||
<h1 class="text-3xl font-black text-slate-800 dark:text-white font-display tracking-tight">{{ __('Product Management') }}</h1>
|
<h1 class="text-3xl font-black text-slate-800 dark:text-white font-display tracking-tight">{{ __('Product Management') }}</h1>
|
||||||
<p class="text-sm font-bold text-slate-500 dark:text-slate-400 mt-1 uppercase tracking-widest">
|
<p class="text-sm font-bold text-slate-500 dark:text-slate-400 mt-1 uppercase tracking-widest">
|
||||||
@@ -32,18 +32,47 @@ $roleSelectConfig = [
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-3">
|
||||||
<a href="{{ route($baseRoute . '.create') }}" class="btn-luxury-primary">
|
<template x-if="activeTab === 'products'">
|
||||||
<svg class="size-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5">
|
<a href="{{ route($baseRoute . '.create') }}" class="btn-luxury-primary transition-all duration-300">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15" />
|
<svg class="size-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5">
|
||||||
</svg>
|
<path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15" />
|
||||||
<span>{{ __('Add Product') }}</span>
|
</svg>
|
||||||
</a>
|
<span>{{ __('Add Product') }}</span>
|
||||||
|
</a>
|
||||||
|
</template>
|
||||||
|
<template x-if="activeTab === 'categories'">
|
||||||
|
<button @click="openCategoryModal()" type="button" class="btn-luxury-primary transition-all duration-300 bg-emerald-600 hover:bg-emerald-700 shadow-emerald-500/20">
|
||||||
|
<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="M12 4.5v15m7.5-7.5h-15" />
|
||||||
|
</svg>
|
||||||
|
<span>{{ __('Add Category') }}</span>
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Tabs Navigation -->
|
||||||
|
<div class="flex items-center gap-1 p-1.5 bg-slate-100 dark:bg-slate-900/50 rounded-2xl w-fit border border-slate-200/50 dark:border-slate-800/50" aria-label="Tabs">
|
||||||
|
<button type="button"
|
||||||
|
@click="activeTab = 'products'"
|
||||||
|
:class="activeTab === 'products' ? 'bg-white dark:bg-slate-800 text-cyan-600 dark:text-cyan-400 shadow-sm shadow-cyan-500/10' : 'text-slate-400 hover:text-slate-600 dark:hover:text-slate-200'"
|
||||||
|
class="px-8 py-3 rounded-xl text-sm font-black uppercase tracking-widest transition-all duration-300">
|
||||||
|
{{ __('Product List') }}
|
||||||
|
</button>
|
||||||
|
<button type="button"
|
||||||
|
@click="activeTab = 'categories'"
|
||||||
|
:class="activeTab === 'categories' ? 'bg-white dark:bg-slate-800 text-cyan-600 dark:text-cyan-400 shadow-sm shadow-cyan-500/10' : 'text-slate-400 hover:text-slate-600 dark:hover:text-slate-200'"
|
||||||
|
class="px-8 py-3 rounded-xl text-sm font-black uppercase tracking-widest transition-all duration-300">
|
||||||
|
{{ __('Category Management') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Main Content Card -->
|
<!-- Tab Contents -->
|
||||||
<div class="luxury-card rounded-3xl p-8 animate-luxury-in">
|
<div class="mt-6">
|
||||||
|
|
||||||
|
|
||||||
|
<!-- Products Tab -->
|
||||||
|
<div x-show="activeTab === 'products'" class="luxury-card rounded-3xl p-8 animate-luxury-in" x-transition:enter="transition ease-out duration-300" x-transition:enter-start="opacity-0 translate-y-4" x-transition:enter-end="opacity-100 translate-y-0" x-cloak>
|
||||||
<!-- Filters & Search -->
|
<!-- Filters & Search -->
|
||||||
<form action="{{ route($routeName) }}" method="GET" class="flex flex-col md:flex-row md:items-center gap-4 mb-10">
|
<form action="{{ route($routeName) }}" method="GET" class="flex flex-col md:flex-row md:items-center gap-4 mb-10">
|
||||||
<div class="relative group">
|
<div class="relative group">
|
||||||
@@ -68,6 +97,7 @@ $roleSelectConfig = [
|
|||||||
<thead>
|
<thead>
|
||||||
<tr class="bg-slate-50/50 dark:bg-slate-900/10">
|
<tr class="bg-slate-50/50 dark:bg-slate-900/10">
|
||||||
<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">{{ __('Product Info') }}</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">{{ __('Product Info') }}</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">{{ __('Barcode') }}</th>
|
||||||
@if(auth()->user()->isSystemAdmin())
|
@if(auth()->user()->isSystemAdmin())
|
||||||
<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">{{ __('Company') }}</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">{{ __('Company') }}</th>
|
||||||
@endif
|
@endif
|
||||||
@@ -91,30 +121,21 @@ $roleSelectConfig = [
|
|||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col">
|
<div class="flex flex-col">
|
||||||
<span class="text-base font-extrabold text-slate-800 dark:text-slate-100 group-hover:text-cyan-600 dark:group-hover:text-cyan-400 transition-colors">{{ $product->localized_name }}</span>
|
<span class="text-base font-extrabold text-slate-800 dark:text-slate-100 group-hover:text-cyan-600 dark:group-hover:text-cyan-400 transition-colors">{{ $product->localized_name }}</span>
|
||||||
<div class="flex items-center gap-2 mt-0.5">
|
<div class="flex flex-wrap items-center gap-1.5 mt-1">
|
||||||
@php
|
@php
|
||||||
$catName = $product->category->name ?? __('Uncategorized');
|
$catName = $product->category->localized_name ?? __('Uncategorized');
|
||||||
if ($product->category && $product->category->name_dictionary_key) {
|
|
||||||
$translatedCat = __($product->category->name_dictionary_key);
|
|
||||||
if ($translatedCat !== $product->category->name_dictionary_key) {
|
|
||||||
$catName = $translatedCat;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@endphp
|
@endphp
|
||||||
<span class="text-[10px] font-bold text-slate-400 dark:text-slate-500 uppercase tracking-widest bg-slate-100 dark:bg-slate-800 px-1.5 py-0.5 rounded transition-colors group-hover:text-slate-600 dark:group-hover:text-slate-300">{{ $catName }}</span>
|
<span class="text-[10px] font-bold text-slate-400 dark:text-slate-500 uppercase tracking-widest bg-slate-100 dark:bg-slate-800 px-1.5 py-0.5 rounded transition-colors group-hover:text-slate-600 dark:group-hover:text-slate-300">{{ $catName }}</span>
|
||||||
@if($companySettings['enable_material_code'] ?? false)
|
@if(($companySettings['enable_material_code'] ?? false) && isset($product->metadata['material_code']))
|
||||||
<div class="flex items-center gap-1.5 bg-slate-100/50 dark:bg-slate-800/50 px-2 py-0.5 rounded-md border border-slate-100 dark:border-slate-800">
|
<span class="text-[10px] font-bold text-emerald-500/80 uppercase tracking-widest bg-emerald-500/10 px-1.5 py-0.5 rounded border border-emerald-500/20">#{{ $product->metadata['material_code'] }}</span>
|
||||||
<span class="text-[10px] font-mono font-bold text-cyan-500 tracking-tighter">{{ $product->barcode }}</span>
|
|
||||||
<span class="h-1 w-1 rounded-full bg-slate-300 dark:bg-slate-700"></span>
|
|
||||||
<span class="text-[10px] font-mono font-bold text-emerald-500 tracking-tighter">{{ $product->metadata['material_code'] ?? '-' }}</span>
|
|
||||||
</div>
|
|
||||||
@else
|
|
||||||
<span class="text-[10px] font-mono font-bold text-cyan-500 tracking-tighter transition-colors group-hover:text-cyan-600 dark:group-hover:text-cyan-400">{{ $product->barcode }}</span>
|
|
||||||
@endif
|
@endif
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
|
<td class="px-6 py-6 whitespace-nowrap">
|
||||||
|
<span class="text-sm font-mono font-black text-slate-700 dark:text-slate-200 tracking-tight">{{ $product->barcode ?: '-' }}</span>
|
||||||
|
</td>
|
||||||
@if(auth()->user()->isSystemAdmin())
|
@if(auth()->user()->isSystemAdmin())
|
||||||
<td class="px-6 py-6 text-center">
|
<td class="px-6 py-6 text-center">
|
||||||
<span class="text-xs font-bold text-slate-600 dark:text-slate-400 group-hover:text-slate-900 dark:group-hover:text-slate-200 transition-colors">{{ $product->company->name ?? '-' }}</span>
|
<span class="text-xs font-bold text-slate-600 dark:text-slate-400 group-hover:text-slate-900 dark:group-hover:text-slate-200 transition-colors">{{ $product->company->name ?? '-' }}</span>
|
||||||
@@ -174,8 +195,62 @@ $roleSelectConfig = [
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Pagination -->
|
<!-- Pagination -->
|
||||||
<div class="mt-10 pt-6 border-t border-slate-100 dark:border-slate-800/50">
|
<div class="mt-8">
|
||||||
{{ $products->links('vendor.pagination.luxury') }}
|
{{ $products->links() }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Category Tab -->
|
||||||
|
<div x-show="activeTab === 'categories'" class="luxury-card rounded-3xl p-8 animate-luxury-in" x-transition:enter="transition ease-out duration-300" x-transition:enter-start="opacity-0 translate-y-4" x-transition:enter-end="opacity-100 translate-y-0" x-cloak>
|
||||||
|
<div class="overflow-x-auto">
|
||||||
|
<table class="w-full text-left border-separate border-spacing-y-0">
|
||||||
|
<thead>
|
||||||
|
<tr class="bg-slate-50/50 dark:bg-slate-900/10">
|
||||||
|
<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">{{ __('Category Name') }}</th>
|
||||||
|
@if(auth()->user()->isSystemAdmin())
|
||||||
|
<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">{{ __('Company') }}</th>
|
||||||
|
@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-right">{{ __('Actions') }}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="divide-y divide-slate-50 dark:divide-slate-800/80">
|
||||||
|
@forelse($categories as $category)
|
||||||
|
<tr class="group hover:bg-slate-50/80 dark:hover:bg-slate-800/40 transition-all duration-300">
|
||||||
|
<td class="px-6 py-6">
|
||||||
|
<span class="text-base font-extrabold text-slate-800 dark:text-slate-100 transition-colors group-hover:text-cyan-600">
|
||||||
|
{{ $category->localized_name }}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
@if(auth()->user()->isSystemAdmin())
|
||||||
|
<td class="px-6 py-6 text-center">
|
||||||
|
<span class="text-xs font-bold text-slate-600 dark:text-slate-400">{{ $category->company->name ?? __('System Default') }}</span>
|
||||||
|
</td>
|
||||||
|
@endif
|
||||||
|
<td class="px-6 py-6 text-right">
|
||||||
|
<div class="flex justify-end items-center gap-2">
|
||||||
|
<button type="button" @click="openCategoryModal(@js($category))" class="p-2.5 rounded-lg bg-slate-50 dark:bg-slate-800 text-slate-400 hover:text-cyan-500 hover:bg-cyan-500/5 transition-all border border-transparent hover:border-cyan-500/20" title="{{ __('Edit') }}">
|
||||||
|
<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="m16.862 4.487 1.687-1.688a1.875 1.875 0 1 1 2.652 2.652L10.582 16.07a4.5 4.5 0 0 1-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 0 1 1.13-1.897l8.932-8.931Zm0 0L19.5 7.125M18 14v4.75A2.25 2.25 0 0 1 15.75 21H5.25A2.25 2.25 0 0 1 3 18.75V8.25A2.25 2.25 0 0 1 5.25 6H10" /></svg>
|
||||||
|
</button>
|
||||||
|
<button type="button" @click="confirmDelete('{{ route('admin.data-config.product-categories.destroy', $category->id) }}')" class="p-2.5 rounded-lg bg-slate-50 dark:bg-slate-800 text-slate-400 hover:text-rose-500 hover:bg-rose-500/5 transition-all border border-transparent hover:border-rose-500/20" title="{{ __('Delete') }}">
|
||||||
|
<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="m14.74 9l-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 0 1-2.244 2.077H8.084a2.25 2.25 0 0 1-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 0 0-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 0 1 3.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 0 0-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 0 0-7.5 0" /></svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
@empty
|
||||||
|
<tr class="animate-luxury-in">
|
||||||
|
<td colspan="{{ auth()->user()->isSystemAdmin() ? 3 : 2 }}" class="px-6 py-20 text-center">
|
||||||
|
<div class="flex flex-col items-center justify-center space-y-4">
|
||||||
|
<div class="w-16 h-16 rounded-3xl bg-slate-50 dark:bg-slate-800/50 flex items-center justify-center text-slate-300 dark:text-slate-700 shadow-inner">
|
||||||
|
<svg class="size-8" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M20.25 7.5l-.625 10.632a2.25 2.25 0 01-2.247 2.118H6.622a2.25 2.25 0 01-2.247-2.118L3.75 7.5M10 11.25h4M3.375 7.5h17.25c.621 0 1.125-.504 1.125-1.125v-1.5c0-.621-.504-1.125-1.125-1.125H3.375c-.621 0-1.125.504-1.125 1.125v1.5c0 .621.504 1.125 1.125 1.125z" /></svg>
|
||||||
|
</div>
|
||||||
|
<p class="text-slate-400 font-bold tracking-widest text-sm uppercase">{{ __('No categories found.') }}</p>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
@endforelse
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -203,6 +278,86 @@ $roleSelectConfig = [
|
|||||||
@method('DELETE')
|
@method('DELETE')
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
<!-- Category Modal -->
|
||||||
|
<div x-show="isCategoryModalOpen" class="fixed inset-0 z-[110] overflow-y-auto" x-cloak>
|
||||||
|
<div class="flex items-center justify-center min-h-screen px-4 pt-4 pb-20 text-center sm:block sm:p-0">
|
||||||
|
<div x-show="isCategoryModalOpen" x-transition:enter="ease-out duration-300" x-transition:enter-start="opacity-0" x-transition:enter-end="opacity-100" x-transition:leave="ease-in duration-200" x-transition:leave-start="opacity-100" x-transition:leave-end="opacity-0" class="fixed inset-0 transition-opacity bg-slate-900/60 backdrop-blur-sm" @click="isCategoryModalOpen = false"></div>
|
||||||
|
|
||||||
|
<span class="hidden sm:inline-block sm:align-middle sm:h-screen">​</span>
|
||||||
|
|
||||||
|
<div x-show="isCategoryModalOpen" x-transition:enter="ease-out duration-300" x-transition:enter-start="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95" x-transition:enter-end="opacity-100 translate-y-0 sm:scale-100" x-transition:leave="ease-in duration-200" x-transition:leave-start="opacity-100 translate-y-0 sm:scale-100" x-transition:leave-end="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95" class="inline-block w-full max-w-lg p-8 my-8 overflow-hidden text-left align-middle transition-all transform bg-white dark:bg-slate-900 shadow-2xl rounded-3xl border border-slate-100 dark:border-white/10">
|
||||||
|
<div class="flex justify-between items-center mb-8">
|
||||||
|
<h3 class="text-2xl font-black text-slate-800 dark:text-white font-display tracking-tight" x-text="categoryModalMode === 'create' ? '{{ __('Add Category') }}' : '{{ __('Edit Category') }}'"></h3>
|
||||||
|
<button @click="isCategoryModalOpen = false" class="text-slate-400 hover:text-slate-600 dark:hover:text-slate-200 transition-colors">
|
||||||
|
<svg class="size-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" /></svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form :action="categoryFormAction" method="POST" class="space-y-6">
|
||||||
|
@csrf
|
||||||
|
<template x-if="categoryModalMode === 'edit'">
|
||||||
|
<input type="hidden" name="_method" value="PUT">
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div class="space-y-6">
|
||||||
|
<!-- 1. Company Selection (If Admin) -->
|
||||||
|
@if(auth()->user()->isSystemAdmin())
|
||||||
|
<div class="p-6 bg-slate-50 dark:bg-slate-800/30 rounded-3xl border border-slate-100 dark:border-white/5 space-y-3">
|
||||||
|
<label class="block text-sm font-black text-slate-700 dark:text-slate-200 px-1">{{ __('Affiliated Company') }}</label>
|
||||||
|
|
||||||
|
<!-- Searchable Select Wrapper -->
|
||||||
|
<div id="category_company_select_wrapper" class="relative">
|
||||||
|
<!-- Will be hydrated by JS -->
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="text-[10px] font-bold text-slate-400 uppercase tracking-widest px-1">{{ __('Type to search or leave blank for system defaults.') }}</p>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
<!-- 2. Multilingual Names -->
|
||||||
|
<div class="space-y-5 px-1">
|
||||||
|
<!-- zh_TW -->
|
||||||
|
<div class="space-y-2">
|
||||||
|
<label class="flex items-center gap-2 text-[10px] font-black text-slate-400 uppercase tracking-[0.2em]">
|
||||||
|
{{ __('Category Name (zh_TW)') }} <span class="text-rose-500">*</span>
|
||||||
|
</label>
|
||||||
|
<input type="text" name="names[zh_TW]" x-model="categoryFormFields.names.zh_TW" class="luxury-input w-full focus:ring-emerald-500/20 focus:border-emerald-500" placeholder="{{ __('e.g., Beverage') }}" required>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- en -->
|
||||||
|
<div class="space-y-2">
|
||||||
|
<label class="flex items-center gap-2 text-[10px] font-black text-slate-400 uppercase tracking-[0.2em]">
|
||||||
|
{{ __('Category Name (en)') }}
|
||||||
|
</label>
|
||||||
|
<input type="text" name="names[en]" x-model="categoryFormFields.names.en" class="luxury-input w-full" placeholder="{{ __('e.g., Drinks') }}">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ja -->
|
||||||
|
<div class="space-y-2">
|
||||||
|
<label class="flex items-center gap-2 text-[10px] font-black text-slate-400 uppercase tracking-[0.2em]">
|
||||||
|
{{ __('Category Name (ja)') }}
|
||||||
|
</label>
|
||||||
|
<input type="text" name="names[ja]" x-model="categoryFormFields.names.ja" class="luxury-input w-full" placeholder="{{ __('e.g., お飲み物') }}">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center justify-end gap-4 mt-12 pt-6 border-t border-slate-100 dark:border-white/5">
|
||||||
|
<button type="button" @click="isCategoryModalOpen = false"
|
||||||
|
class="px-6 py-2.5 text-sm font-black text-slate-400 hover:text-slate-600 dark:hover:text-slate-200 uppercase tracking-widest transition-all">
|
||||||
|
{{ __('Cancel') }}
|
||||||
|
</button>
|
||||||
|
<button type="submit"
|
||||||
|
class="btn-luxury-primary px-10 py-3 shadow-lg"
|
||||||
|
:class="categoryModalMode === 'create' ? 'bg-emerald-600 hover:bg-emerald-700 shadow-emerald-500/20' : 'bg-cyan-600 hover:bg-cyan-700 shadow-cyan-500/20'">
|
||||||
|
<span x-text="categoryModalMode === 'create' ? '{{ __('Create') }}' : '{{ __('Save Changes') }}'"></span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Product Detail Slide-over -->
|
<!-- Product Detail Slide-over -->
|
||||||
<div x-show="isDetailOpen"
|
<div x-show="isDetailOpen"
|
||||||
class="fixed inset-0 z-[100] overflow-hidden"
|
class="fixed inset-0 z-[100] overflow-hidden"
|
||||||
@@ -407,10 +562,18 @@ $roleSelectConfig = [
|
|||||||
isDetailOpen: false,
|
isDetailOpen: false,
|
||||||
isImageZoomed: false,
|
isImageZoomed: false,
|
||||||
isStatusConfirmOpen: false,
|
isStatusConfirmOpen: false,
|
||||||
|
isCategoryModalOpen: false,
|
||||||
|
activeTab: '{{ request("tab", "products") }}',
|
||||||
|
categoryModalMode: 'create',
|
||||||
|
categoryFormAction: '',
|
||||||
deleteFormAction: '',
|
deleteFormAction: '',
|
||||||
toggleFormAction: '',
|
toggleFormAction: '',
|
||||||
selectedProduct: null,
|
selectedProduct: null,
|
||||||
categories: [],
|
categories: [],
|
||||||
|
categoryFormFields: {
|
||||||
|
names: { zh_TW: '', en: '', ja: '' },
|
||||||
|
company_id: ''
|
||||||
|
},
|
||||||
|
|
||||||
submitConfirmedForm() {
|
submitConfirmedForm() {
|
||||||
this.$refs.statusToggleForm.submit();
|
this.$refs.statusToggleForm.submit();
|
||||||
@@ -418,6 +581,73 @@ $roleSelectConfig = [
|
|||||||
|
|
||||||
init() {
|
init() {
|
||||||
this.categories = JSON.parse(this.$el.dataset.categories || '[]');
|
this.categories = JSON.parse(this.$el.dataset.categories || '[]');
|
||||||
|
this.companies = @js($companies);
|
||||||
|
|
||||||
|
// Watch for category modal opening to sync searchable select
|
||||||
|
this.$watch('isCategoryModalOpen', (value) => {
|
||||||
|
if (value && document.getElementById('category_company_select_wrapper')) {
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.updateCategoryCompanySelect();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
updateCategoryCompanySelect() {
|
||||||
|
const wrapper = document.getElementById('category_company_select_wrapper');
|
||||||
|
if (!wrapper) return;
|
||||||
|
wrapper.innerHTML = '';
|
||||||
|
|
||||||
|
const selectEl = document.createElement('select');
|
||||||
|
selectEl.name = 'company_id';
|
||||||
|
const uniqueId = 'cat-company-' + Date.now();
|
||||||
|
selectEl.id = uniqueId;
|
||||||
|
selectEl.className = 'hidden';
|
||||||
|
|
||||||
|
const config = {
|
||||||
|
"placeholder": "{{ __('Select Company (Default: System)') }}",
|
||||||
|
"hasSearch": true,
|
||||||
|
"searchPlaceholder": "{{ __('Search Company Title...') }}",
|
||||||
|
"isHidePlaceholder": false,
|
||||||
|
"searchClasses": "block w-[calc(100%-16px)] mx-2 py-2 px-3 text-sm border-slate-200 dark:border-white/10 rounded-lg focus:border-cyan-500 focus:ring-cyan-500 bg-slate-50 dark:bg-slate-900/50 dark:text-slate-200 placeholder:text-slate-400 dark:placeholder:text-slate-500",
|
||||||
|
"searchWrapperClasses": "sticky top-0 bg-white/95 dark:bg-slate-900/95 backdrop-blur-md p-2 z-10",
|
||||||
|
"toggleClasses": "hs-select-toggle luxury-select-toggle",
|
||||||
|
"dropdownClasses": "hs-select-menu w-full bg-white dark:bg-slate-900 border border-slate-200 dark:border-white/10 rounded-xl shadow-[0_20px_50px_rgba(0,0,0,0.3)] mt-2 z-[100] animate-luxury-in",
|
||||||
|
"optionClasses": "hs-select-option py-2.5 px-3 mb-0.5 text-sm text-slate-800 dark:text-slate-300 cursor-pointer hover:bg-slate-100 dark:hover:bg-cyan-500/10 dark:hover:text-cyan-400 rounded-lg flex items-center justify-between transition-all duration-300",
|
||||||
|
"optionTemplate": '<div class="flex items-center justify-between w-full"><span data-title></span><span class="hs-select-active-indicator hidden text-cyan-500"><svg class="w-4 h-4" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"></polyline></svg></span></div>'
|
||||||
|
};
|
||||||
|
|
||||||
|
selectEl.setAttribute('data-hs-select', JSON.stringify(config));
|
||||||
|
|
||||||
|
// Default System option
|
||||||
|
const defaultOpt = document.createElement('option');
|
||||||
|
defaultOpt.value = "";
|
||||||
|
defaultOpt.textContent = "{{ __('System Default (Common)') }}";
|
||||||
|
defaultOpt.setAttribute('data-title', defaultOpt.textContent);
|
||||||
|
if (!this.categoryFormFields.company_id) defaultOpt.selected = true;
|
||||||
|
selectEl.appendChild(defaultOpt);
|
||||||
|
|
||||||
|
// Company options
|
||||||
|
this.companies.forEach(company => {
|
||||||
|
const opt = document.createElement('option');
|
||||||
|
opt.value = company.id;
|
||||||
|
opt.textContent = company.name;
|
||||||
|
opt.setAttribute('data-title', company.name);
|
||||||
|
if (String(this.categoryFormFields.company_id) === String(company.id)) opt.selected = true;
|
||||||
|
selectEl.appendChild(opt);
|
||||||
|
});
|
||||||
|
|
||||||
|
wrapper.appendChild(selectEl);
|
||||||
|
|
||||||
|
selectEl.addEventListener('change', (e) => {
|
||||||
|
this.categoryFormFields.company_id = e.target.value;
|
||||||
|
});
|
||||||
|
|
||||||
|
this.$nextTick(() => {
|
||||||
|
if (window.HSStaticMethods && window.HSStaticMethods.autoInit) {
|
||||||
|
window.HSStaticMethods.autoInit(['select']);
|
||||||
|
}
|
||||||
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
confirmDelete(action) {
|
confirmDelete(action) {
|
||||||
@@ -430,6 +660,28 @@ $roleSelectConfig = [
|
|||||||
this.isDetailOpen = true;
|
this.isDetailOpen = true;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
openCategoryModal(category = null) {
|
||||||
|
if (category) {
|
||||||
|
this.categoryModalMode = 'edit';
|
||||||
|
this.categoryFormAction = `{{ url('admin/data-config/product-categories') }}/${category.id}`;
|
||||||
|
this.categoryFormFields.names = { zh_TW: category.name || '', en: category.name || '', ja: category.name || '' };
|
||||||
|
if (category.translations && category.translations.length > 0) {
|
||||||
|
category.translations.forEach(t => {
|
||||||
|
if (this.categoryFormFields.names.hasOwnProperty(t.locale)) {
|
||||||
|
this.categoryFormFields.names[t.locale] = t.value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
this.categoryFormFields.company_id = category.company_id || '';
|
||||||
|
} else {
|
||||||
|
this.categoryModalMode = 'create';
|
||||||
|
this.categoryFormAction = `{{ route('admin.data-config.product-categories.store') }}`;
|
||||||
|
this.categoryFormFields.names = { zh_TW: '', en: '', ja: '' };
|
||||||
|
this.categoryFormFields.company_id = '';
|
||||||
|
}
|
||||||
|
this.isCategoryModalOpen = true;
|
||||||
|
},
|
||||||
|
|
||||||
getCategoryName(id) {
|
getCategoryName(id) {
|
||||||
const category = this.categories.find(c => c.id == id);
|
const category = this.categories.find(c => c.id == id);
|
||||||
return category ? (category.name || "{{ __('Uncategorized') }}") : "{{ __('Uncategorized') }}";
|
return category ? (category.name || "{{ __('Uncategorized') }}") : "{{ __('Uncategorized') }}";
|
||||||
|
|||||||
411
routes/web.php
411
routes/web.php
@@ -4,18 +4,18 @@ use App\Http\Controllers\ProfileController;
|
|||||||
use Illuminate\Support\Facades\Route;
|
use Illuminate\Support\Facades\Route;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
| Web Routes
|
| Web Routes
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
|
||||||
| Here is where you can register web routes for your application. These
|
| Here is where you can register web routes for your application. These
|
||||||
| routes are loaded by the RouteServiceProvider and all of them will
|
| routes are loaded by the RouteServiceProvider and all of them will
|
||||||
| be assigned to the "web" middleware group. Make something great!
|
| be assigned to the "web" middleware group. Make something great!
|
||||||
|
|
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// Multi-language switch
|
// Multi-language switch
|
||||||
Route::get('lang/{locale}', [App\Http\Controllers\System\LanguageController::class , 'switch'])->name('lang.switch');
|
Route::get('lang/{locale}', [App\Http\Controllers\System\LanguageController::class, 'switch'])->name('lang.switch');
|
||||||
|
|
||||||
Route::get('/', function () {
|
Route::get('/', function () {
|
||||||
return redirect()->route('login');
|
return redirect()->route('login');
|
||||||
@@ -27,7 +27,7 @@ Route::get('/dashboard', function () {
|
|||||||
|
|
||||||
Route::middleware(['auth', 'verified', 'tenant.access'])->prefix('admin')->name('admin.')->group(function () {
|
Route::middleware(['auth', 'verified', 'tenant.access'])->prefix('admin')->name('admin.')->group(function () {
|
||||||
// 1. 儀表板
|
// 1. 儀表板
|
||||||
Route::get('/dashboard', [App\Http\Controllers\Admin\DashboardController::class , 'index'])->name('dashboard');
|
Route::get('/dashboard', [App\Http\Controllers\Admin\DashboardController::class, 'index'])->name('dashboard');
|
||||||
|
|
||||||
// 2. 會員管理
|
// 2. 會員管理
|
||||||
Route::resource('members', App\Http\Controllers\MemberController::class)->only(['index']);
|
Route::resource('members', App\Http\Controllers\MemberController::class)->only(['index']);
|
||||||
@@ -36,217 +36,206 @@ Route::middleware(['auth', 'verified', 'tenant.access'])->prefix('admin')->name(
|
|||||||
Route::resource('point-rules', App\Http\Controllers\Admin\PointRuleController::class)->except(['show', 'create', 'edit']);
|
Route::resource('point-rules', App\Http\Controllers\Admin\PointRuleController::class)->except(['show', 'create', 'edit']);
|
||||||
Route::resource('gift-definitions', App\Http\Controllers\Admin\GiftDefinitionController::class)->except(['show', 'create', 'edit']);
|
Route::resource('gift-definitions', App\Http\Controllers\Admin\GiftDefinitionController::class)->except(['show', 'create', 'edit']);
|
||||||
|
|
||||||
Route::prefix('machines')->name('machines.')->group(function () {
|
// 3. 機台管理
|
||||||
Route::get('/permissions', [App\Http\Controllers\Admin\Machine\MachinePermissionController::class, 'index'])->name('permissions')->middleware('can:menu.machines.permissions');
|
Route::prefix('machines')->name('machines.')->group(function () {
|
||||||
Route::get('/permissions/accounts/{user}', [App\Http\Controllers\Admin\Machine\MachinePermissionController::class, 'getAccountMachines'])->name('permissions.accounts.get');
|
Route::get('/permissions', [App\Http\Controllers\Admin\Machine\MachinePermissionController::class, 'index'])->name('permissions')->middleware('can:menu.machines.permissions');
|
||||||
Route::post('/permissions/accounts/{user}', [App\Http\Controllers\Admin\Machine\MachinePermissionController::class, 'syncAccountMachines'])->name('permissions.accounts.sync');
|
Route::get('/permissions/accounts/{user}', [App\Http\Controllers\Admin\Machine\MachinePermissionController::class, 'getAccountMachines'])->name('permissions.accounts.get');
|
||||||
|
Route::post('/permissions/accounts/{user}', [App\Http\Controllers\Admin\Machine\MachinePermissionController::class, 'syncAccountMachines'])->name('permissions.accounts.sync');
|
||||||
|
|
||||||
Route::get('/utilization', [App\Http\Controllers\Admin\MachineController::class , 'utilization'])->name('utilization');
|
Route::get('/utilization', [App\Http\Controllers\Admin\MachineController::class, 'utilization'])->name('utilization');
|
||||||
Route::get('/utilization-ajax/{id?}', [App\Http\Controllers\Admin\MachineController::class, 'utilizationData'])->name('utilization-ajax');
|
Route::get('/utilization-ajax/{id?}', [App\Http\Controllers\Admin\MachineController::class, 'utilizationData'])->name('utilization-ajax');
|
||||||
Route::get('/{machine}/slots-ajax', [App\Http\Controllers\Admin\MachineController::class, 'slotsAjax'])->name('slots-ajax');
|
Route::get('/{machine}/slots-ajax', [App\Http\Controllers\Admin\MachineController::class, 'slotsAjax'])->name('slots-ajax');
|
||||||
Route::post('/{machine}/slots/expiry', [App\Http\Controllers\Admin\MachineController::class, 'updateSlotExpiry'])->name('slots.expiry.update');
|
Route::post('/{machine}/slots/expiry', [App\Http\Controllers\Admin\MachineController::class, 'updateSlotExpiry'])->name('slots.expiry.update');
|
||||||
Route::get('/{machine}/logs-ajax', [App\Http\Controllers\Admin\MachineController::class, 'logsAjax'])->name('logs-ajax');
|
Route::get('/{machine}/logs-ajax', [App\Http\Controllers\Admin\MachineController::class, 'logsAjax'])->name('logs-ajax');
|
||||||
});
|
});
|
||||||
Route::resource('machines', App\Http\Controllers\Admin\MachineController::class);
|
Route::resource('machines', App\Http\Controllers\Admin\MachineController::class);
|
||||||
|
|
||||||
// 維修管理
|
// 維修管理
|
||||||
Route::prefix('maintenance')->name('maintenance.')->middleware('can:menu.machines.maintenance')->group(function () {
|
Route::prefix('maintenance')->name('maintenance.')->middleware('can:menu.machines.maintenance')->group(function () {
|
||||||
Route::get('/', [App\Http\Controllers\Admin\MaintenanceController::class, 'index'])->name('index');
|
Route::get('/', [App\Http\Controllers\Admin\MaintenanceController::class, 'index'])->name('index');
|
||||||
Route::get('/create/{serial_no?}', [App\Http\Controllers\Admin\MaintenanceController::class, 'create'])->name('create');
|
Route::get('/create/{serial_no?}', [App\Http\Controllers\Admin\MaintenanceController::class, 'create'])->name('create');
|
||||||
Route::post('/', [App\Http\Controllers\Admin\MaintenanceController::class, 'store'])->name('store');
|
Route::post('/', [App\Http\Controllers\Admin\MaintenanceController::class, 'store'])->name('store');
|
||||||
});
|
|
||||||
|
|
||||||
// 4. APP管理
|
|
||||||
Route::prefix('app')->name('app.')->group(function () {
|
|
||||||
Route::get('/ui-elements', [App\Http\Controllers\Admin\AppConfigController::class , 'uiElements'])->name('ui-elements');
|
|
||||||
Route::get('/helper', [App\Http\Controllers\Admin\AppConfigController::class , 'helper'])->name('helper');
|
|
||||||
Route::get('/questionnaire', [App\Http\Controllers\Admin\AppConfigController::class , 'questionnaire'])->name('questionnaire');
|
|
||||||
Route::get('/games', [App\Http\Controllers\Admin\AppConfigController::class , 'games'])->name('games');
|
|
||||||
Route::get('/timer', [App\Http\Controllers\Admin\AppConfigController::class , 'timer'])->name('timer');
|
|
||||||
}
|
|
||||||
);
|
|
||||||
Route::get('/app-configs', [App\Http\Controllers\Admin\AppConfigController::class , 'index'])->name('app-configs.index');
|
|
||||||
Route::put('/app-configs', [App\Http\Controllers\Admin\AppConfigController::class , 'update'])->name('app-configs.update');
|
|
||||||
|
|
||||||
// 5. 倉庫管理
|
|
||||||
Route::prefix('warehouses')->name('warehouses.')->group(function () {
|
|
||||||
Route::get('/', [App\Http\Controllers\Admin\WarehouseController::class , 'index'])->name('index');
|
|
||||||
Route::get('/personal', [App\Http\Controllers\Admin\WarehouseController::class , 'personal'])->name('personal');
|
|
||||||
Route::get('/stock-management', [App\Http\Controllers\Admin\WarehouseController::class , 'stockManagement'])->name('stock-management');
|
|
||||||
Route::get('/transfers', [App\Http\Controllers\Admin\WarehouseController::class , 'transfers'])->name('transfers');
|
|
||||||
Route::get('/purchases', [App\Http\Controllers\Admin\WarehouseController::class , 'purchases'])->name('purchases');
|
|
||||||
Route::get('/replenishments', [App\Http\Controllers\Admin\WarehouseController::class , 'replenishments'])->name('replenishments');
|
|
||||||
Route::get('/replenishment-records', [App\Http\Controllers\Admin\WarehouseController::class , 'replenishmentRecords'])->name('replenishment-records');
|
|
||||||
Route::get('/replenishment-records-all', [App\Http\Controllers\Admin\WarehouseController::class , 'replenishmentRecordsAll'])->name('replenishment-records-all');
|
|
||||||
Route::get('/machine-stock', [App\Http\Controllers\Admin\WarehouseController::class , 'machineStock'])->name('machine-stock');
|
|
||||||
Route::get('/staff-stock', [App\Http\Controllers\Admin\WarehouseController::class , 'staffStock'])->name('staff-stock');
|
|
||||||
Route::get('/returns', [App\Http\Controllers\Admin\WarehouseController::class , 'returns'])->name('returns');
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
// 6. 銷售管理
|
|
||||||
Route::prefix('sales')->name('sales.')->group(function () {
|
|
||||||
Route::get('/', [App\Http\Controllers\Admin\SalesController::class , 'index'])->name('index');
|
|
||||||
Route::get('/pickup-codes', [App\Http\Controllers\Admin\SalesController::class , 'pickupCodes'])->name('pickup-codes');
|
|
||||||
Route::get('/orders', [App\Http\Controllers\Admin\SalesController::class , 'orders'])->name('orders');
|
|
||||||
Route::get('/promotions', [App\Http\Controllers\Admin\SalesController::class , 'promotions'])->name('promotions');
|
|
||||||
Route::get('/pass-codes', [App\Http\Controllers\Admin\SalesController::class , 'passCodes'])->name('pass-codes');
|
|
||||||
Route::get('/store-gifts', [App\Http\Controllers\Admin\SalesController::class , 'storeGifts'])->name('store-gifts');
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
// 7. 分析管理
|
|
||||||
Route::prefix('analysis')->name('analysis.')->group(function () {
|
|
||||||
Route::get('/change-stock', [App\Http\Controllers\Admin\AnalysisController::class , 'changeStock'])->name('change-stock');
|
|
||||||
Route::get('/machine-reports', [App\Http\Controllers\Admin\AnalysisController::class , 'machineReports'])->name('machine-reports');
|
|
||||||
Route::get('/product-reports', [App\Http\Controllers\Admin\AnalysisController::class , 'productReports'])->name('product-reports');
|
|
||||||
Route::get('/survey-analysis', [App\Http\Controllers\Admin\AnalysisController::class , 'surveyAnalysis'])->name('survey-analysis');
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
// 8. 稽核管理
|
|
||||||
Route::prefix('audit')->name('audit.')->group(function () {
|
|
||||||
Route::get('/purchases', [App\Http\Controllers\Admin\AuditController::class , 'purchases'])->name('purchases');
|
|
||||||
Route::get('/transfers', [App\Http\Controllers\Admin\AuditController::class , 'transfers'])->name('transfers');
|
|
||||||
Route::get('/replenishments', [App\Http\Controllers\Admin\AuditController::class , 'replenishments'])->name('replenishments');
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
// 9. 資料設定
|
|
||||||
Route::prefix('data-config')->name('data-config.')->group(function () {
|
|
||||||
Route::middleware('can:menu.data-config.products')->group(function () {
|
|
||||||
Route::resource('products', App\Http\Controllers\Admin\ProductController::class)->except(['show']);
|
|
||||||
Route::patch('/products/{id}/toggle-status', [App\Http\Controllers\Admin\ProductController::class, 'toggleStatus'])->name('products.status.toggle');
|
|
||||||
});
|
|
||||||
|
|
||||||
// 廣告管理 (Advertisement Management)
|
|
||||||
Route::middleware('can:menu.data-config.advertisements')->group(function () {
|
|
||||||
Route::resource('advertisements', App\Http\Controllers\Admin\AdvertisementController::class)->except(['show', 'create', 'edit']);
|
|
||||||
Route::get('/advertisements/machine/{machine}', [App\Http\Controllers\Admin\AdvertisementController::class, 'getMachineAds'])->name('advertisements.machine.get');
|
|
||||||
Route::post('/advertisements/assign', [App\Http\Controllers\Admin\AdvertisementController::class, 'assign'])->name('advertisements.assign');
|
|
||||||
Route::post('/advertisements/assignments/reorder', [App\Http\Controllers\Admin\AdvertisementController::class, 'reorderAssignments'])->name('advertisements.assignments.reorder');
|
|
||||||
Route::delete('/advertisements/assignment/{id}', [App\Http\Controllers\Admin\AdvertisementController::class, 'removeAssignment'])->name('advertisements.assignment.remove');
|
|
||||||
});
|
|
||||||
|
|
||||||
Route::get('/sub-accounts', [App\Http\Controllers\Admin\PermissionController::class , 'accounts'])->name('sub-accounts')->middleware('can:menu.data-config.sub-accounts');
|
|
||||||
Route::patch('/sub-accounts/{id}/toggle-status', [App\Http\Controllers\Admin\PermissionController::class, 'toggleAccountStatus'])->name('sub-accounts.status.toggle')->middleware('can:menu.data-config.sub-accounts');
|
|
||||||
Route::post('/sub-accounts', [App\Http\Controllers\Admin\PermissionController::class , 'storeAccount'])->name('sub-accounts.store')->middleware('can:menu.data-config.sub-accounts');
|
|
||||||
Route::put('/sub-accounts/{id}', [App\Http\Controllers\Admin\PermissionController::class , 'updateAccount'])->name('sub-accounts.update')->middleware('can:menu.data-config.sub-accounts');
|
|
||||||
Route::delete('/sub-accounts/{id}', [App\Http\Controllers\Admin\PermissionController::class , 'destroyAccount'])->name('sub-accounts.destroy')->middleware('can:menu.data-config.sub-accounts');
|
|
||||||
Route::get('/sub-account-roles', [App\Http\Controllers\Admin\PermissionController::class , 'roles'])->name('sub-account-roles')->middleware('can:menu.data-config.sub-account-roles');
|
|
||||||
Route::get('/sub-account-roles/create', [App\Http\Controllers\Admin\PermissionController::class , 'createRole'])->name('sub-account-roles.create')->middleware('can:menu.data-config.sub-account-roles');
|
|
||||||
Route::get('/sub-account-roles/{id}/edit', [App\Http\Controllers\Admin\PermissionController::class , 'editRole'])->name('sub-account-roles.edit')->middleware('can:menu.data-config.sub-account-roles');
|
|
||||||
Route::post('/sub-account-roles', [App\Http\Controllers\Admin\PermissionController::class , 'storeRole'])->name('sub-account-roles.store')->middleware('can:menu.data-config.sub-account-roles');
|
|
||||||
Route::put('/sub-account-roles/{id}', [App\Http\Controllers\Admin\PermissionController::class , 'updateRole'])->name('sub-account-roles.update')->middleware('can:menu.data-config.sub-account-roles');
|
|
||||||
Route::delete('/sub-account-roles/{id}', [App\Http\Controllers\Admin\PermissionController::class , 'destroyRole'])->name('sub-account-roles.destroy')->middleware('can:menu.data-config.sub-account-roles');
|
|
||||||
Route::get('/points', [App\Http\Controllers\Admin\DataConfigController::class , 'points'])->name('points');
|
|
||||||
Route::get('/badges', [App\Http\Controllers\Admin\DataConfigController::class , 'badges'])->name('badges');
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
// 10. 遠端管理
|
|
||||||
Route::prefix('remote')->name('remote.')->group(function () {
|
|
||||||
Route::get('/stock', [App\Http\Controllers\Admin\RemoteController::class , 'stock'])->name('stock');
|
|
||||||
Route::get('/restart', [App\Http\Controllers\Admin\RemoteController::class , 'restart'])->name('restart');
|
|
||||||
Route::get('/restart-card-reader', [App\Http\Controllers\Admin\RemoteController::class , 'restartCardReader'])->name('restart-card-reader');
|
|
||||||
Route::get('/checkout', [App\Http\Controllers\Admin\RemoteController::class , 'checkout'])->name('checkout');
|
|
||||||
Route::get('/lock', [App\Http\Controllers\Admin\RemoteController::class , 'lock'])->name('lock');
|
|
||||||
Route::get('/change', [App\Http\Controllers\Admin\RemoteController::class , 'change'])->name('change');
|
|
||||||
Route::get('/dispense', [App\Http\Controllers\Admin\RemoteController::class , 'dispense'])->name('dispense');
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
// 11. Line管理
|
|
||||||
Route::prefix('line')->name('line.')->group(function () {
|
|
||||||
Route::get('/members', [App\Http\Controllers\Admin\LineController::class , 'members'])->name('members');
|
|
||||||
Route::get('/machines', [App\Http\Controllers\Admin\LineController::class , 'machines'])->name('machines');
|
|
||||||
Route::get('/products', [App\Http\Controllers\Admin\LineController::class , 'products'])->name('products');
|
|
||||||
Route::get('/official-account', [App\Http\Controllers\Admin\LineController::class , 'officialAccount'])->name('official-account');
|
|
||||||
Route::get('/orders', [App\Http\Controllers\Admin\LineController::class , 'orders'])->name('orders');
|
|
||||||
Route::get('/coupons', [App\Http\Controllers\Admin\LineController::class , 'coupons'])->name('coupons');
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
// 12. 預約系統
|
|
||||||
Route::prefix('reservation')->name('reservation.')->group(function () {
|
|
||||||
Route::get('/members', [App\Http\Controllers\Admin\ReservationController::class , 'members'])->name('members');
|
|
||||||
Route::get('/stores', [App\Http\Controllers\Admin\ReservationController::class , 'stores'])->name('stores');
|
|
||||||
Route::get('/time-slots', [App\Http\Controllers\Admin\ReservationController::class , 'timeSlots'])->name('time-slots');
|
|
||||||
Route::get('/venues', [App\Http\Controllers\Admin\ReservationController::class , 'venues'])->name('venues');
|
|
||||||
Route::get('/coupons', [App\Http\Controllers\Admin\ReservationController::class , 'coupons'])->name('coupons');
|
|
||||||
Route::get('/reservations', [App\Http\Controllers\Admin\ReservationController::class , 'reservations'])->name('reservations');
|
|
||||||
Route::get('/orders', [App\Http\Controllers\Admin\ReservationController::class , 'orders'])->name('orders');
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
// 13. 特殊權限管理
|
|
||||||
Route::prefix('special-permission')->name('special-permission.')->group(function () {
|
|
||||||
Route::get('/clear-stock', [App\Http\Controllers\Admin\SpecialPermissionController::class , 'clearStock'])->name('clear-stock');
|
|
||||||
Route::get('/apk-versions', [App\Http\Controllers\Admin\SpecialPermissionController::class , 'apkVersions'])->name('apk-versions');
|
|
||||||
Route::get('/discord-notifications', [App\Http\Controllers\Admin\SpecialPermissionController::class , 'discordNotifications'])->name('discord-notifications');
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
// 14. 基本設定
|
|
||||||
Route::prefix('basic-settings')->name('basic-settings.')->group(function () {
|
|
||||||
// 機台設定
|
|
||||||
Route::prefix('machines')->name('machines.')->middleware('can:menu.basic.machines')->group(function () {
|
|
||||||
// 機台照片獨立更新
|
|
||||||
Route::patch('{machine}/photos', [App\Http\Controllers\Admin\BasicSettings\MachinePhotoController::class, 'update'])->name('photos.update');
|
|
||||||
|
|
||||||
Route::get('/', [App\Http\Controllers\Admin\BasicSettings\MachineSettingController::class, 'index'])->name('index');
|
|
||||||
Route::get('/{machine}/edit', [App\Http\Controllers\Admin\BasicSettings\MachineSettingController::class, 'edit'])->name('edit');
|
|
||||||
Route::put('/{machine}', [App\Http\Controllers\Admin\BasicSettings\MachineSettingController::class, 'update'])->name('update');
|
|
||||||
Route::post('/', [App\Http\Controllers\Admin\BasicSettings\MachineSettingController::class, 'store'])->name('store');
|
|
||||||
Route::post('/{machine}/regenerate-token', [App\Http\Controllers\Admin\BasicSettings\MachineSettingController::class, 'regenerateToken'])->name('regenerate-token');
|
|
||||||
|
|
||||||
Route::post('/{machine}/regenerate-token', [App\Http\Controllers\Admin\BasicSettings\MachineSettingController::class, 'regenerateToken'])->name('regenerate-token');
|
|
||||||
});
|
|
||||||
|
|
||||||
// 客戶金流設定
|
|
||||||
Route::resource('payment-configs', App\Http\Controllers\Admin\BasicSettings\PaymentConfigController::class)->except(['show'])->middleware('can:menu.basic.payment-configs');
|
|
||||||
|
|
||||||
// 機台型號設定
|
|
||||||
Route::resource('machine-models', App\Http\Controllers\Admin\BasicSettings\MachineModelController::class)->except(['show']);
|
|
||||||
|
|
||||||
// QR Code 生成
|
|
||||||
Route::get('qr-code', [App\Http\Controllers\Admin\QrCodeController::class, 'generate'])->name('qr-code');
|
|
||||||
});
|
|
||||||
|
|
||||||
// 15. 權限設定
|
|
||||||
Route::prefix('permission')->name('permission.')->group(function () {
|
|
||||||
Route::patch('companies/{company}/toggle-status', [App\Http\Controllers\Admin\CompanyController::class, 'toggleStatus'])->name('companies.status.toggle')->middleware('can:menu.permissions.companies');
|
|
||||||
Route::resource('companies', App\Http\Controllers\Admin\CompanyController::class)->except(['show', 'create', 'edit'])->middleware('can:menu.permissions.companies');
|
|
||||||
Route::get('/accounts', [App\Http\Controllers\Admin\PermissionController::class , 'accounts'])->name('accounts')->middleware('can:menu.permissions.accounts');
|
|
||||||
Route::patch('/accounts/{id}/toggle-status', [App\Http\Controllers\Admin\PermissionController::class, 'toggleAccountStatus'])->name('accounts.status.toggle')->middleware('can:menu.permissions.accounts');
|
|
||||||
Route::post('/accounts', [App\Http\Controllers\Admin\PermissionController::class , 'storeAccount'])->name('accounts.store')->middleware('can:menu.permissions.accounts');
|
|
||||||
Route::put('/accounts/{id}', [App\Http\Controllers\Admin\PermissionController::class , 'updateAccount'])->name('accounts.update')->middleware('can:menu.permissions.accounts');
|
|
||||||
Route::delete('/accounts/{id}', [App\Http\Controllers\Admin\PermissionController::class , 'destroyAccount'])->name('accounts.destroy')->middleware('can:menu.permissions.accounts');
|
|
||||||
Route::get('/roles', [App\Http\Controllers\Admin\PermissionController::class , 'roles'])->name('roles')->middleware('can:menu.permissions.roles');
|
|
||||||
Route::get('/roles/create', [App\Http\Controllers\Admin\PermissionController::class , 'createRole'])->name('roles.create')->middleware('can:menu.permissions.roles');
|
|
||||||
Route::get('/roles/{id}/edit', [App\Http\Controllers\Admin\PermissionController::class , 'editRole'])->name('roles.edit')->middleware('can:menu.permissions.roles');
|
|
||||||
Route::post('/roles', [App\Http\Controllers\Admin\PermissionController::class , 'storeRole'])->name('roles.store')->middleware('can:menu.permissions.roles');
|
|
||||||
Route::put('/roles/{id}', [App\Http\Controllers\Admin\PermissionController::class , 'updateRole'])->name('roles.update')->middleware('can:menu.permissions.roles');
|
|
||||||
Route::delete('/roles/{id}', [App\Http\Controllers\Admin\PermissionController::class , 'destroyRole'])->name('roles.destroy');
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
// 主題設定
|
|
||||||
Route::post('/theme', [App\Http\Controllers\Admin\ThemeController::class , 'update'])->name('theme.update');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 4. APP管理
|
||||||
|
Route::prefix('app')->name('app.')->group(function () {
|
||||||
|
Route::get('/ui-elements', [App\Http\Controllers\Admin\AppConfigController::class, 'uiElements'])->name('ui-elements');
|
||||||
|
Route::get('/helper', [App\Http\Controllers\Admin\AppConfigController::class, 'helper'])->name('helper');
|
||||||
|
Route::get('/questionnaire', [App\Http\Controllers\Admin\AppConfigController::class, 'questionnaire'])->name('questionnaire');
|
||||||
|
Route::get('/games', [App\Http\Controllers\Admin\AppConfigController::class, 'games'])->name('games');
|
||||||
|
Route::get('/timer', [App\Http\Controllers\Admin\AppConfigController::class, 'timer'])->name('timer');
|
||||||
|
});
|
||||||
|
Route::get('/app-configs', [App\Http\Controllers\Admin\AppConfigController::class, 'index'])->name('app-configs.index');
|
||||||
|
Route::put('/app-configs', [App\Http\Controllers\Admin\AppConfigController::class, 'update'])->name('app-configs.update');
|
||||||
|
|
||||||
|
// 5. 倉庫管理
|
||||||
|
Route::prefix('warehouses')->name('warehouses.')->group(function () {
|
||||||
|
Route::get('/', [App\Http\Controllers\Admin\WarehouseController::class, 'index'])->name('index');
|
||||||
|
Route::get('/personal', [App\Http\Controllers\Admin\WarehouseController::class, 'personal'])->name('personal');
|
||||||
|
Route::get('/stock-management', [App\Http\Controllers\Admin\WarehouseController::class, 'stockManagement'])->name('stock-management');
|
||||||
|
Route::get('/transfers', [App\Http\Controllers\Admin\WarehouseController::class, 'transfers'])->name('transfers');
|
||||||
|
Route::get('/purchases', [App\Http\Controllers\Admin\WarehouseController::class, 'purchases'])->name('purchases');
|
||||||
|
Route::get('/replenishments', [App\Http\Controllers\Admin\WarehouseController::class, 'replenishments'])->name('replenishments');
|
||||||
|
Route::get('/replenishment-records', [App\Http\Controllers\Admin\WarehouseController::class, 'replenishmentRecords'])->name('replenishment-records');
|
||||||
|
Route::get('/replenishment-records-all', [App\Http\Controllers\Admin\WarehouseController::class, 'replenishmentRecordsAll'])->name('replenishment-records-all');
|
||||||
|
Route::get('/machine-stock', [App\Http\Controllers\Admin\WarehouseController::class, 'machineStock'])->name('machine-stock');
|
||||||
|
Route::get('/staff-stock', [App\Http\Controllers\Admin\WarehouseController::class, 'staffStock'])->name('staff-stock');
|
||||||
|
Route::get('/returns', [App\Http\Controllers\Admin\WarehouseController::class, 'returns'])->name('returns');
|
||||||
|
});
|
||||||
|
|
||||||
|
// 6. 銷售管理
|
||||||
|
Route::prefix('sales')->name('sales.')->group(function () {
|
||||||
|
Route::get('/', [App\Http\Controllers\Admin\SalesController::class, 'index'])->name('index');
|
||||||
|
Route::get('/pickup-codes', [App\Http\Controllers\Admin\SalesController::class, 'pickupCodes'])->name('pickup-codes');
|
||||||
|
Route::get('/orders', [App\Http\Controllers\Admin\SalesController::class, 'orders'])->name('orders');
|
||||||
|
Route::get('/promotions', [App\Http\Controllers\Admin\SalesController::class, 'promotions'])->name('promotions');
|
||||||
|
Route::get('/pass-codes', [App\Http\Controllers\Admin\SalesController::class, 'passCodes'])->name('pass-codes');
|
||||||
|
Route::get('/store-gifts', [App\Http\Controllers\Admin\SalesController::class, 'storeGifts'])->name('store-gifts');
|
||||||
|
});
|
||||||
|
|
||||||
|
// 7. 分析管理
|
||||||
|
Route::prefix('analysis')->name('analysis.')->group(function () {
|
||||||
|
Route::get('/change-stock', [App\Http\Controllers\Admin\AnalysisController::class, 'changeStock'])->name('change-stock');
|
||||||
|
Route::get('/machine-reports', [App\Http\Controllers\Admin\AnalysisController::class, 'machineReports'])->name('machine-reports');
|
||||||
|
Route::get('/product-reports', [App\Http\Controllers\Admin\AnalysisController::class, 'productReports'])->name('product-reports');
|
||||||
|
Route::get('/survey-analysis', [App\Http\Controllers\Admin\AnalysisController::class, 'surveyAnalysis'])->name('survey-analysis');
|
||||||
|
});
|
||||||
|
|
||||||
|
// 8. 稽核管理
|
||||||
|
Route::prefix('audit')->name('audit.')->group(function () {
|
||||||
|
Route::get('/purchases', [App\Http\Controllers\Admin\AuditController::class, 'purchases'])->name('purchases');
|
||||||
|
Route::get('/transfers', [App\Http\Controllers\Admin\AuditController::class, 'transfers'])->name('transfers');
|
||||||
|
Route::get('/replenishments', [App\Http\Controllers\Admin\AuditController::class, 'replenishments'])->name('replenishments');
|
||||||
|
});
|
||||||
|
|
||||||
|
// 9. 資料設定
|
||||||
|
Route::prefix('data-config')->name('data-config.')->group(function () {
|
||||||
|
Route::middleware('can:menu.data-config.products')->group(function () {
|
||||||
|
Route::resource('products', App\Http\Controllers\Admin\ProductController::class)->except(['show']);
|
||||||
|
Route::patch('/products/{id}/toggle-status', [App\Http\Controllers\Admin\ProductController::class, 'toggleStatus'])->name('products.status.toggle');
|
||||||
|
Route::resource('product-categories', App\Http\Controllers\Admin\ProductCategoryController::class)->except(['show', 'create', 'edit']);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 廣告管理 (Advertisement Management)
|
||||||
|
Route::middleware('can:menu.data-config.advertisements')->group(function () {
|
||||||
|
Route::resource('advertisements', App\Http\Controllers\Admin\AdvertisementController::class)->except(['show', 'create', 'edit']);
|
||||||
|
Route::get('/advertisements/machine/{machine}', [App\Http\Controllers\Admin\AdvertisementController::class, 'getMachineAds'])->name('advertisements.machine.get');
|
||||||
|
Route::post('/advertisements/assign', [App\Http\Controllers\Admin\AdvertisementController::class, 'assign'])->name('advertisements.assign');
|
||||||
|
Route::post('/advertisements/assignments/reorder', [App\Http\Controllers\Admin\AdvertisementController::class, 'reorderAssignments'])->name('advertisements.assignments.reorder');
|
||||||
|
Route::delete('/advertisements/assignment/{id}', [App\Http\Controllers\Admin\AdvertisementController::class, 'removeAssignment'])->name('advertisements.assignment.remove');
|
||||||
|
});
|
||||||
|
|
||||||
|
Route::get('/sub-accounts', [App\Http\Controllers\Admin\PermissionController::class, 'accounts'])->name('sub-accounts')->middleware('can:menu.data-config.sub-accounts');
|
||||||
|
Route::patch('/sub-accounts/{id}/toggle-status', [App\Http\Controllers\Admin\PermissionController::class, 'toggleAccountStatus'])->name('sub-accounts.status.toggle')->middleware('can:menu.data-config.sub-accounts');
|
||||||
|
Route::post('/sub-accounts', [App\Http\Controllers\Admin\PermissionController::class, 'storeAccount'])->name('sub-accounts.store')->middleware('can:menu.data-config.sub-accounts');
|
||||||
|
Route::put('/sub-accounts/{id}', [App\Http\Controllers\Admin\PermissionController::class, 'updateAccount'])->name('sub-accounts.update')->middleware('can:menu.data-config.sub-accounts');
|
||||||
|
Route::delete('/sub-accounts/{id}', [App\Http\Controllers\Admin\PermissionController::class, 'destroyAccount'])->name('sub-accounts.destroy')->middleware('can:menu.data-config.sub-accounts');
|
||||||
|
Route::get('/sub-account-roles', [App\Http\Controllers\Admin\PermissionController::class, 'roles'])->name('sub-account-roles')->middleware('can:menu.data-config.sub-account-roles');
|
||||||
|
Route::get('/sub-account-roles/create', [App\Http\Controllers\Admin\PermissionController::class, 'createRole'])->name('sub-account-roles.create')->middleware('can:menu.data-config.sub-account-roles');
|
||||||
|
Route::get('/sub-account-roles/{id}/edit', [App\Http\Controllers\Admin\PermissionController::class, 'editRole'])->name('sub-account-roles.edit')->middleware('can:menu.data-config.sub-account-roles');
|
||||||
|
Route::post('/sub-account-roles', [App\Http\Controllers\Admin\PermissionController::class, 'storeRole'])->name('sub-account-roles.store')->middleware('can:menu.data-config.sub-account-roles');
|
||||||
|
Route::put('/sub-account-roles/{id}', [App\Http\Controllers\Admin\PermissionController::class, 'updateRole'])->name('sub-account-roles.update')->middleware('can:menu.data-config.sub-account-roles');
|
||||||
|
Route::delete('/sub-account-roles/{id}', [App\Http\Controllers\Admin\PermissionController::class, 'destroyRole'])->name('sub-account-roles.destroy')->middleware('can:menu.data-config.sub-account-roles');
|
||||||
|
Route::get('/points', [App\Http\Controllers\Admin\DataConfigController::class, 'points'])->name('points');
|
||||||
|
Route::get('/badges', [App\Http\Controllers\Admin\DataConfigController::class, 'badges'])->name('badges');
|
||||||
|
});
|
||||||
|
|
||||||
|
// 10. 遠端管理
|
||||||
|
Route::prefix('remote')->name('remote.')->group(function () {
|
||||||
|
Route::get('/stock', [App\Http\Controllers\Admin\RemoteController::class, 'stock'])->name('stock');
|
||||||
|
Route::get('/restart', [App\Http\Controllers\Admin\RemoteController::class, 'restart'])->name('restart');
|
||||||
|
Route::get('/restart-card-reader', [App\Http\Controllers\Admin\RemoteController::class, 'restartCardReader'])->name('restart-card-reader');
|
||||||
|
Route::get('/checkout', [App\Http\Controllers\Admin\RemoteController::class, 'checkout'])->name('checkout');
|
||||||
|
Route::get('/lock', [App\Http\Controllers\Admin\RemoteController::class, 'lock'])->name('lock');
|
||||||
|
Route::get('/change', [App\Http\Controllers\Admin\RemoteController::class, 'change'])->name('change');
|
||||||
|
Route::get('/dispense', [App\Http\Controllers\Admin\RemoteController::class, 'dispense'])->name('dispense');
|
||||||
|
});
|
||||||
|
|
||||||
|
// 11. Line管理
|
||||||
|
Route::prefix('line')->name('line.')->group(function () {
|
||||||
|
Route::get('/members', [App\Http\Controllers\Admin\LineController::class, 'members'])->name('members');
|
||||||
|
Route::get('/machines', [App\Http\Controllers\Admin\LineController::class, 'machines'])->name('machines');
|
||||||
|
Route::get('/products', [App\Http\Controllers\Admin\LineController::class, 'products'])->name('products');
|
||||||
|
Route::get('/official-account', [App\Http\Controllers\Admin\LineController::class, 'officialAccount'])->name('official-account');
|
||||||
|
Route::get('/orders', [App\Http\Controllers\Admin\LineController::class, 'orders'])->name('orders');
|
||||||
|
Route::get('/coupons', [App\Http\Controllers\Admin\LineController::class, 'coupons'])->name('coupons');
|
||||||
|
});
|
||||||
|
|
||||||
|
// 12. 預約系統
|
||||||
|
Route::prefix('reservation')->name('reservation.')->group(function () {
|
||||||
|
Route::get('/members', [App\Http\Controllers\Admin\ReservationController::class, 'members'])->name('members');
|
||||||
|
Route::get('/stores', [App\Http\Controllers\Admin\ReservationController::class, 'stores'])->name('stores');
|
||||||
|
Route::get('/time-slots', [App\Http\Controllers\Admin\ReservationController::class, 'timeSlots'])->name('time-slots');
|
||||||
|
Route::get('/venues', [App\Http\Controllers\Admin\ReservationController::class, 'venues'])->name('venues');
|
||||||
|
Route::get('/coupons', [App\Http\Controllers\Admin\ReservationController::class, 'coupons'])->name('coupons');
|
||||||
|
Route::get('/reservations', [App\Http\Controllers\Admin\ReservationController::class, 'reservations'])->name('reservations');
|
||||||
|
Route::get('/orders', [App\Http\Controllers\Admin\ReservationController::class, 'orders'])->name('orders');
|
||||||
|
});
|
||||||
|
|
||||||
|
// 13. 特殊權限管理
|
||||||
|
Route::prefix('special-permission')->name('special-permission.')->group(function () {
|
||||||
|
Route::get('/clear-stock', [App\Http\Controllers\Admin\SpecialPermissionController::class, 'clearStock'])->name('clear-stock');
|
||||||
|
Route::get('/apk-versions', [App\Http\Controllers\Admin\SpecialPermissionController::class, 'apkVersions'])->name('apk-versions');
|
||||||
|
Route::get('/discord-notifications', [App\Http\Controllers\Admin\SpecialPermissionController::class, 'discordNotifications'])->name('discord-notifications');
|
||||||
|
});
|
||||||
|
|
||||||
|
// 14. 基本設定
|
||||||
|
Route::prefix('basic-settings')->name('basic-settings.')->group(function () {
|
||||||
|
// 機台設定
|
||||||
|
Route::prefix('machines')->name('machines.')->middleware('can:menu.basic.machines')->group(function () {
|
||||||
|
// 機台照片獨立更新
|
||||||
|
Route::patch('{machine}/photos', [App\Http\Controllers\Admin\BasicSettings\MachinePhotoController::class, 'update'])->name('photos.update');
|
||||||
|
|
||||||
|
Route::get('/', [App\Http\Controllers\Admin\BasicSettings\MachineSettingController::class, 'index'])->name('index');
|
||||||
|
Route::get('/{machine}/edit', [App\Http\Controllers\Admin\BasicSettings\MachineSettingController::class, 'edit'])->name('edit');
|
||||||
|
Route::put('/{machine}', [App\Http\Controllers\Admin\BasicSettings\MachineSettingController::class, 'update'])->name('update');
|
||||||
|
Route::post('/', [App\Http\Controllers\Admin\BasicSettings\MachineSettingController::class, 'store'])->name('store');
|
||||||
|
Route::post('/{machine}/regenerate-token', [App\Http\Controllers\Admin\BasicSettings\MachineSettingController::class, 'regenerateToken'])->name('regenerate-token');
|
||||||
|
});
|
||||||
|
|
||||||
|
// 客戶金流設定
|
||||||
|
Route::resource('payment-configs', App\Http\Controllers\Admin\BasicSettings\PaymentConfigController::class)->except(['show'])->middleware('can:menu.basic.payment-configs');
|
||||||
|
|
||||||
|
// 機台型號設定
|
||||||
|
Route::resource('machine-models', App\Http\Controllers\Admin\BasicSettings\MachineModelController::class)->except(['show']);
|
||||||
|
|
||||||
|
// QR Code 生成
|
||||||
|
Route::get('qr-code', [App\Http\Controllers\Admin\QrCodeController::class, 'generate'])->name('qr-code');
|
||||||
|
});
|
||||||
|
|
||||||
|
// 15. 權限設定
|
||||||
|
Route::prefix('permission')->name('permission.')->group(function () {
|
||||||
|
Route::patch('companies/{company}/toggle-status', [App\Http\Controllers\Admin\CompanyController::class, 'toggleStatus'])->name('companies.status.toggle')->middleware('can:menu.permissions.companies');
|
||||||
|
Route::resource('companies', App\Http\Controllers\Admin\CompanyController::class)->except(['show', 'create', 'edit'])->middleware('can:menu.permissions.companies');
|
||||||
|
Route::get('/accounts', [App\Http\Controllers\Admin\PermissionController::class, 'accounts'])->name('accounts')->middleware('can:menu.permissions.accounts');
|
||||||
|
Route::patch('/accounts/{id}/toggle-status', [App\Http\Controllers\Admin\PermissionController::class, 'toggleAccountStatus'])->name('accounts.status.toggle')->middleware('can:menu.permissions.accounts');
|
||||||
|
Route::post('/accounts', [App\Http\Controllers\Admin\PermissionController::class, 'storeAccount'])->name('accounts.store')->middleware('can:menu.permissions.accounts');
|
||||||
|
Route::put('/accounts/{id}', [App\Http\Controllers\Admin\PermissionController::class, 'updateAccount'])->name('accounts.update')->middleware('can:menu.permissions.accounts');
|
||||||
|
Route::delete('/accounts/{id}', [App\Http\Controllers\Admin\PermissionController::class, 'destroyAccount'])->name('accounts.destroy')->middleware('can:menu.permissions.accounts');
|
||||||
|
Route::get('/roles', [App\Http\Controllers\Admin\PermissionController::class, 'roles'])->name('roles')->middleware('can:menu.permissions.roles');
|
||||||
|
Route::get('/roles/create', [App\Http\Controllers\Admin\PermissionController::class, 'createRole'])->name('roles.create')->middleware('can:menu.permissions.roles');
|
||||||
|
Route::get('/roles/{id}/edit', [App\Http\Controllers\Admin\PermissionController::class, 'editRole'])->name('roles.edit')->middleware('can:menu.permissions.roles');
|
||||||
|
Route::post('/roles', [App\Http\Controllers\Admin\PermissionController::class, 'storeRole'])->name('roles.store')->middleware('can:menu.permissions.roles');
|
||||||
|
Route::put('/roles/{id}', [App\Http\Controllers\Admin\PermissionController::class, 'updateRole'])->name('roles.update')->middleware('can:menu.permissions.roles');
|
||||||
|
Route::delete('/roles/{id}', [App\Http\Controllers\Admin\PermissionController::class, 'destroyRole'])->name('roles.destroy');
|
||||||
|
});
|
||||||
|
|
||||||
|
// 主題設定
|
||||||
|
Route::post('/theme', [App\Http\Controllers\Admin\ThemeController::class, 'update'])->name('theme.update');
|
||||||
|
});
|
||||||
|
|
||||||
Route::middleware('auth')->group(function () {
|
Route::middleware('auth')->group(function () {
|
||||||
Route::get('/profile', [ProfileController::class , 'edit'])->name('profile.edit');
|
Route::get('/profile', [ProfileController::class, 'edit'])->name('profile.edit');
|
||||||
Route::patch('/profile', [ProfileController::class , 'update'])->name('profile.update');
|
Route::patch('/profile', [ProfileController::class, 'update'])->name('profile.update');
|
||||||
Route::post("/profile/avatar", [ProfileController::class , "updateAvatar"])->name("profile.avatar");
|
Route::post("/profile/avatar", [ProfileController::class, "updateAvatar"])->name("profile.avatar");
|
||||||
});
|
});
|
||||||
|
|
||||||
require __DIR__ . '/auth.php';
|
require __DIR__ . '/auth.php';
|
||||||
|
|
||||||
// 測試路由 (需非正式環境或有特別權限控管)
|
// 測試路由 (需非正式環境或有特別權限控管)
|
||||||
Route::prefix('test')->name('test.')->group(function () {
|
Route::prefix('test')->name('test.')->group(function () {
|
||||||
Route::get('/social-login', [App\Http\Controllers\SocialLoginTestController::class , 'index'])->name('social-login');
|
Route::get('/social-login', [App\Http\Controllers\SocialLoginTestController::class, 'index'])->name('social-login');
|
||||||
Route::get('/line/callback', [App\Http\Controllers\SocialLoginTestController::class , 'lineCallback'])->name('line.callback');
|
Route::get('/line/callback', [App\Http\Controllers\SocialLoginTestController::class, 'lineCallback'])->name('line.callback');
|
||||||
});
|
});
|
||||||
// 公開 API 文件 (無需登入)
|
// 公開 API 文件 (無需登入)
|
||||||
Route::get('/api/docs', [App\Http\Controllers\ApiDocsController::class, 'index'])->name('api.docs');
|
Route::get('/api/docs', [App\Http\Controllers\ApiDocsController::class, 'index'])->name('api.docs');
|
||||||
|
|||||||
Reference in New Issue
Block a user