[FEAT] 新增多租戶自訂登入、視覺設定與 APK 版本上傳時間

1. 實作多租戶專屬自訂登入頁面:支援 `/c/{company_code}/login` 特殊入口,自動載入租戶專屬 Logo 網址、名稱與歡迎詞。
2. 支援客製化品牌視覺設定:於客戶管理清單中,提供 SaaS 管理者可自訂租戶 Logo 圖片與歡迎副標題,且系統後台 Header/Sidebar 與登入頁能動態依該配置載入租戶專屬商標。
3. 擴充多語系字典並完成三檔對齊:新增 "Upload Time" 欄位與翻譯,執行 `ksort` 以維持 zh_TW、en 與 ja 語系檔對齊。
4. APK 版本清單增加上傳時間欄位:於 APK 版本表格中呈現上傳時間(created_at),使用 font-mono 奢華風小尺寸樣式呈現。
This commit is contained in:
sky121113 2026-06-01 17:07:20 +08:00
parent b1bebf6ae4
commit 5dd3b0a005
11 changed files with 603 additions and 41 deletions

View File

@ -213,11 +213,14 @@ class CompanyController extends Controller
{
\Log::info('updateSettings payload:', $request->all());
$settings = $request->input('settings', []);
$currentSettings = $company->settings ?? [];
$formattedSettings = [
// 採用合併方式,防止覆蓋品牌設定或其他未包含的欄位
$formattedSettings = array_merge($currentSettings, [
'enable_material_code' => filter_var($settings['enable_material_code'] ?? false, FILTER_VALIDATE_BOOLEAN),
'enable_points' => filter_var($settings['enable_points'] ?? false, FILTER_VALIDATE_BOOLEAN),
];
'enable_custom_branding' => filter_var($settings['enable_custom_branding'] ?? false, FILTER_VALIDATE_BOOLEAN),
]);
// Ensure we force save the JSON cast properly
$company->settings = $formattedSettings;
@ -227,6 +230,59 @@ class CompanyController extends Controller
->with('success', __('System settings updated successfully.'));
}
/**
* 更新公司品牌客製化樣式設定 (自訂 Logo 與歡迎詞)
*/
public function updateBranding(Request $request, Company $company)
{
$settings = $company->settings ?? [];
// 1. 安全檢驗:如果未授權此功能,則拒絕操作
$enableCustomBranding = filter_var($settings['enable_custom_branding'] ?? false, FILTER_VALIDATE_BOOLEAN);
if (!$enableCustomBranding) {
return redirect()->back()->with('error', __('Custom branding function is not enabled for this company.'));
}
// 2. 欄位驗證
$request->validate([
'logo_file' => 'nullable|image|mimes:jpeg,png,jpg,svg|max:2048', // 限制最大 2MB
'login_title' => 'nullable|string|max:100',
'clear_logo' => 'nullable|boolean', // 用於提供清除 Logo 功能
]);
$logoPath = $settings['logo_path'] ?? null;
// 3. 處理清除 Logo 的需求
if ($request->boolean('clear_logo')) {
if ($logoPath) {
\Illuminate\Support\Facades\Storage::disk('public')->delete($logoPath);
$logoPath = null;
}
}
// 4. 處理 Logo 檔案上傳
if ($request->hasFile('logo_file')) {
// 如果原本有 Logo先刪除舊檔案
if ($logoPath) {
\Illuminate\Support\Facades\Storage::disk('public')->delete($logoPath);
}
// 儲存新檔案到 public/company_logos
$logoPath = $request->file('logo_file')->store('company_logos', 'public');
}
// 5. 合併寫入 settings JSON 中,防止覆寫其他功能開關
$company->settings = array_merge($settings, [
'logo_path' => $logoPath,
'login_title' => $request->input('login_title'),
]);
$company->save();
return redirect()->to(route('admin.permission.companies.index') . '?tab=branding')
->with('success', __('Brand styling updated successfully.'));
}
/**
* 切換客戶狀態
*/

View File

@ -0,0 +1,97 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use App\Models\System\Company;
use App\Models\System\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Log;
class TenantLoginController extends Controller
{
/**
* 顯示專屬客戶的客製化登入頁面
*/
public function showLoginForm($company_code)
{
// 1. 查詢啟用的公司
$company = Company::active()->where('code', $company_code)->first();
// 2. 安全防護:若公司不存在,或沒有啟用品牌客製化功能授權,一律自動重導向至通用登入頁
if (!$company) {
return redirect()->route('login');
}
$settings = $company->settings ?? [];
$enableCustomBranding = filter_var($settings['enable_custom_branding'] ?? false, FILTER_VALIDATE_BOOLEAN);
if (!$enableCustomBranding) {
Log::warning("Tenant login attempted for unauthorized company [{$company_code}]. Redirecting to main login.");
return redirect()->route('login');
}
// 3. 渲染共用的 login Blade傳入當前公司的上下文
return view('auth.login', compact('company'));
}
/**
* 處理專屬客戶的登入請求 (安全性加強版)
*/
public function login($company_code, Request $request)
{
// 1. 查詢並確認該公司
$company = Company::active()->where('code', $company_code)->first();
if (!$company) {
return redirect()->route('login');
}
$settings = $company->settings ?? [];
$enableCustomBranding = filter_var($settings['enable_custom_branding'] ?? false, FILTER_VALIDATE_BOOLEAN);
if (!$enableCustomBranding) {
return redirect()->route('login');
}
// 2. 驗證輸入欄位
$request->validate([
'username' => 'required|string',
'password' => 'required|string',
]);
// 3. 查詢使用者
$user = User::where('username', $request->username)->first();
// 4. 【關鍵安全防錯】驗證此使用者是否確實屬於該公司
if (!$user || $user->company_id !== $company->id) {
Log::warning("Tenant login security violation: User [{$request->username}] attempted login at company [{$company_code}] portal.");
return back()->withErrors([
'username' => __('This account does not belong to this company.'),
])->withInput($request->only('username', 'remember'));
}
// 5. 驗證帳號狀態
if ($user->status !== 1) {
return back()->withErrors([
'username' => __('Your account is disabled.'),
])->withInput($request->only('username', 'remember'));
}
// 6. 驗證密碼
if (!Hash::check($request->password, $user->password)) {
return back()->withErrors([
'password' => __('auth.failed'),
])->withInput($request->only('username', 'remember'));
}
// 7. 執行登入與安全 Session 重新生成
Auth::login($user, $request->boolean('remember'));
$request->session()->regenerate();
Log::info("User [{$user->username}] successfully logged in via company portal [{$company_code}].");
return redirect()->intended(route('admin.dashboard'));
}
}

View File

@ -83,4 +83,33 @@ class Company extends Model
{
return $query->where('status', 1);
}
/**
* 獲取公司自訂 Logo 的完整 URL (Accessor)
*/
public function getLogoUrlAttribute(): ?string
{
$settings = $this->settings ?? [];
$logoPath = $settings['logo_path'] ?? null;
// 只有在啟用品牌自訂功能且上傳了 Logo 時才使用自訂 Logo
if ($this->hasCustomBrandingEnabled() && $logoPath) {
return \Illuminate\Support\Facades\Storage::disk('public')->url($logoPath);
}
return asset('starcloud_icon.png');
}
/**
* 檢查此公司是否啟用且配置了品牌自訂功能
*/
public function hasCustomBrandingEnabled(): bool
{
$settings = $this->settings ?? [];
$enableCustomBranding = filter_var($settings['enable_custom_branding'] ?? false, FILTER_VALIDATE_BOOLEAN);
$logoPath = $settings['logo_path'] ?? null;
return $enableCustomBranding && !empty($logoPath);
}
}

View File

@ -191,6 +191,7 @@
"Authorization updated successfully": "Authorization updated successfully",
"Authorize": "Authorize",
"Authorize Btn": "Authorize",
"Authorized": "Authorized",
"Authorized Accounts": "Authorized Accounts",
"Authorized Accounts Tab": "Authorized Accounts",
"Authorized Machines": "Authorized Machines",
@ -219,15 +220,13 @@
"Before Qty": "Before Qty",
"Belongs To": "Belongs To",
"Belongs To Company": "Belongs To Company",
"Product heating": "Product heating",
"Product heating remaining time": "Product heating remaining time",
"Product not detected in microwave": "Product not detected in microwave",
"Purchase terminated": "Purchase terminated",
"Bill Acceptor": "Bill Acceptor",
"Bracelet drop not detected": "Bracelet drop not detected",
"Bracelet scan failed": "Bracelet scan failed",
"Branch": "Branch",
"Branch Warehouses": "Branch Warehouses",
"Brand Styling": "Brand Styling",
"Brand styling updated successfully.": "Brand styling updated successfully.",
"Business Type": "Business Type",
"Buyout": "Buyout",
"CASH": "CASH",
@ -284,6 +283,7 @@
"Clear": "Clear",
"Clear Abnormal Status": "Clear Abnormal Status",
"Clear Filter": "Clear Filter",
"Clear Logo": "Clear Logo",
"Clear Stock": "Clear Stock",
"Click here to re-send the verification email.": "Click here to re-send the verification email.",
"Click to Open Dashboard": "Click to Open Dashboard",
@ -367,6 +367,7 @@
"Control deployment behavior": "Control deployment behavior",
"Copy": "Copy",
"Copy Code": "Copy Code",
"Copy Download Link": "Copy Download Link",
"Copy Link": "Copy Link",
"Copy Settings From Another Machine": "Copy Settings From Another Machine",
"Copy to Clipboard": "Copy to Clipboard",
@ -403,6 +404,7 @@
"Cup drop not detected": "Cup drop not detected",
"Cup dropper failure": "Cup dropper failure",
"Current": "Current",
"Current Brand Visual": "Current Brand Visual",
"Current Password": "Current Password",
"Current Status": "Current Status",
"Current Stock": "Current Stock",
@ -416,6 +418,7 @@
"Current:": "Current:",
"Custom": "Custom",
"Custom Alert Types": "Custom Alert Types",
"Custom Background Image": "Custom Background Image",
"Custom Date": "Custom Date",
"Custom Date Set": "Custom Date Set",
"Custom Disabled (Mute Alert)": "Custom Disabled (Mute Alert)",
@ -423,6 +426,8 @@
"Custom Expiry": "Custom Expiry",
"Custom Lower Limit (°C)": "Custom Lower Limit (°C)",
"Custom Upper Limit (°C)": "Custom Upper Limit (°C)",
"Custom Welcome Subtitle": "Custom Welcome Subtitle",
"Custom branding function is not enabled for this company.": "Custom branding function is not enabled for this company.",
"Customer Details": "Customer Details",
"Customer Info": "Customer Info",
"Customer List": "Customer List",
@ -433,6 +438,8 @@
"Customer deleted successfully.": "Customer deleted successfully.",
"Customer enabled successfully.": "Customer enabled successfully.",
"Customer updated successfully.": "Customer updated successfully.",
"Customize Brand": "Customize Brand",
"Customize Brand & Style": "Customize Brand & Style",
"Cycle Efficiency": "Cycle Efficiency",
"Daily Revenue": "Daily Revenue",
"Daily operational variance curve for selected timeframe": "Daily operational variance curve for selected timeframe",
@ -527,16 +534,16 @@
"Dispensing": "Dispensing",
"Dispensing in progress": "Dispensing in progress",
"Display Material Code": "Display Material Code",
"Displayed below \"Welcome Back\" on the login portal.": "Displayed below \"Welcome Back\" on the login portal.",
"Displaying": "Displaying",
"Done": "Done",
"Door Closed": "Door Closed",
"Door Opened": "Door Opened",
"Copy Download Link": "Copy Download Link",
"Download APK": "Download APK",
"Download Image": "Download Image",
"Download link copied": "Download link copied",
"Download Template": "Download Template",
"Download URL": "Download URL",
"Download link copied": "Download link copied",
"Draft": "Draft",
"Drag & drop your APK here": "Drag & drop your APK here",
"Duration": "Duration",
@ -602,6 +609,7 @@
"Empty Slots": "Empty Slots",
"Enable": "Enable",
"Enable Alerts": "Enable Alerts",
"Enable Custom Branding": "Enable Custom Branding",
"Enable Material Code": "Enable Material Code",
"Enable Points": "Enable Points",
"Enable Points Mechanism": "Enable Points Mechanism",
@ -624,6 +632,7 @@
"Enter role name": "Enter role name",
"Enter serial number": "Enter serial number",
"Enter your password to confirm": "Enter your password to confirm",
"Enterprise Premium Features": "Enterprise Premium Features",
"Entire Machine": "Entire Machine",
"Equipment efficiency and OEE metrics": "Equipment efficiency and OEE metrics",
"Error": "Error",
@ -792,6 +801,7 @@
"Generate and manage one-time pickup codes for customers": "Generate and manage one-time pickup codes for customers",
"Gift Definitions": "Gift Definitions",
"Global roles accessible by all administrators.": "Global roles accessible by all administrators.",
"Go Authorize": "Go Authorize",
"Go Back": "Go Back",
"Go to E-Invoice": "Go to E-Invoice",
"Goods & System Settings": "Goods & System Settings",
@ -923,6 +933,7 @@
"Lock Page": "Lock Page",
"Lock Page Lock": "Lock Page Lock",
"Lock Page Unlock": "Lock Page Unlock",
"Locked": "Locked",
"Locked Page": "Locked Page",
"Log Details": "Log Details",
"Log Time": "Log Time",
@ -1151,6 +1162,7 @@
"No Company": "No Company",
"No Invoice": "No Invoice",
"No Location": "No Location",
"No Logo": "No Logo",
"No Machine Selected": "No Machine Selected",
"No Model": "No Model",
"No accounts found": "No accounts found",
@ -1442,6 +1454,7 @@
"Previous": "Previous",
"Price / Member": "Price / Member",
"Pricing Information": "Pricing Information",
"Primary Theme Color": "Primary Theme Color",
"Print": "Print",
"Print & PDF": "Print & PDF",
"Print Invoice": "Print Invoice",
@ -1471,6 +1484,9 @@
"Product created successfully": "Product created successfully",
"Product deleted successfully": "Product deleted successfully",
"Product empty": "Product empty",
"Product heating": "Product heating",
"Product heating remaining time": "Product heating remaining time",
"Product not detected in microwave": "Product not detected in microwave",
"Product physical shipment count": "Product physical shipment count",
"Product status updated to :status": "Product status updated to :status",
"Product updated successfully": "Product updated successfully",
@ -1489,6 +1505,7 @@
"Purchase Audit": "Purchase Audit",
"Purchase Finished": "Purchase Finished",
"Purchase successful": "Purchase successful",
"Purchase terminated": "Purchase terminated",
"Purchases": "Purchases",
"Purchasing": "Purchasing",
"Purpose": "Purpose",
@ -1738,6 +1755,7 @@
"Settings loaded from": "Settings loaded from",
"Settings updated successfully.": "Settings updated successfully.",
"Settlement": "Settlement",
"Setup Brand": "Setup Brand",
"Shopping Cart": "Shopping Cart",
"Shopping Cart Feature": "Shopping Cart Feature",
"Shopping Cart Function": "Shopping Cart Function",
@ -1863,6 +1881,7 @@
"Superseded by new adjustment": "Superseded by new adjustment",
"Superseded by new command": "Superseded by new command",
"Superseded by new command (Timeout)": "Superseded by new command (Timeout)",
"Supports JPEG, PNG, SVG (Max 2MB). Ideal proportion: Horizontal or Square.": "Supports JPEG, PNG, SVG (Max 2MB). Ideal proportion: Horizontal or Square.",
"Survey Analysis": "Survey Analysis",
"Sync Ads": "Sync Ads",
"Sync Products": "Sync Products",
@ -1928,6 +1947,7 @@
"The image is too large. Please upload an image smaller than 5MB.": "The image is too large. Please upload an image smaller than 5MB.",
"This Month": "This Month",
"This Week": "This Week",
"This account does not belong to this company.": "This account does not belong to this company.",
"This is a system administrator role. Its name is locked to ensure system stability.": "This is a system administrator role. Its name is locked to ensure system stability.",
"This machine has a pending command. Please wait.": "This machine has a pending command. Please wait.",
"This role belongs to another company and cannot be assigned.": "This role belongs to another company and cannot be assigned.",
@ -2012,6 +2032,7 @@
"UI Elements": "UI Elements",
"Unassigned": "Unassigned",
"Unassigned / No Change": "Unassigned / No Change",
"Unauthorized": "Unauthorized",
"Unauthorized Status": "Unauthorized Status",
"Unauthorized login attempt: :account": "Unauthorized login attempt: :account",
"Unauthorized: Account not authorized for this machine": "Unauthorized: Account not authorized for this machine",
@ -2043,9 +2064,11 @@
"Update failed": "Update failed",
"Update identification for your asset": "Update identification for your asset",
"Update your account's profile information and email address.": "Update your account's profile information and email address.",
"Upload Company Logo": "Upload Company Logo",
"Upload Image": "Upload Image",
"Upload New APK": "Upload New APK",
"Upload New Images": "Upload New Images",
"Upload Time": "Upload Time",
"Upload Video": "Upload Video",
"Upload failed, please try again": "Upload failed, please try again",
"Upload file or provide URL": "Upload file or provide URL",
@ -2148,6 +2171,7 @@
"Welcome Gift Name": "Welcome Gift Name",
"Welcome Gift Status": "Welcome Gift Status",
"Welcome Gifts": "Welcome Gifts",
"Welcome Title": "Welcome Title",
"Work Content": "Work Content",
"Yes, Cancel": "Yes, Cancel",
"Yes, Deactivate": "Yes, Deactivate",
@ -2159,6 +2183,7 @@
"You cannot assign permissions you do not possess.": "You cannot assign permissions you do not possess.",
"You cannot delete your own account.": "You cannot delete your own account.",
"You do not have permission to access replenishment orders": "You do not have permission to access replenishment orders",
"Your account is disabled.": "Your account is disabled.",
"Your recent account activity": "Your recent account activity",
"[PickupCode] Verification failed: :code": "[PickupCode] Verification failed: :code",
"[StaffCard] Verification failed: :uid": "[StaffCard] Verification failed: :uid",

View File

@ -191,6 +191,7 @@
"Authorization updated successfully": "権限が更新されました",
"Authorize": "承認",
"Authorize Btn": "承認",
"Authorized": "授権済み",
"Authorized Accounts": "承認済みアカウント",
"Authorized Accounts Tab": "承認済みアカウント",
"Authorized Machines": "承認済み機器",
@ -219,15 +220,13 @@
"Before Qty": "変動前数量",
"Belongs To": "会社名",
"Belongs To Company": "所属会社",
"Product heating": "商品加熱中",
"Product heating remaining time": "商品加熱残り時間",
"Product not detected in microwave": "電子レンジ内商品未検出",
"Purchase terminated": "購入中止",
"Bill Acceptor": "紙幣識別機",
"Bracelet drop not detected": "ブレスレット落下未検出",
"Bracelet scan failed": "ブレスレットスキャン失敗",
"Branch": "分庫",
"Branch Warehouses": "分庫数",
"Brand Styling": "ブランドデザイン",
"Brand styling updated successfully.": "ブランドデザインが正常に更新されました。",
"Business Type": "業務タイプ",
"Buyout": "買い取り",
"CASH": "現金",
@ -284,6 +283,7 @@
"Clear": "クリア",
"Clear Abnormal Status": "異常状態のクリア",
"Clear Filter": "フィルターをクリア",
"Clear Logo": "ロゴをクリア",
"Clear Stock": "在庫クリア",
"Click here to re-send the verification email.": "確認メールを再送するにはここをクリック。",
"Click to Open Dashboard": "クリックしてダッシュボードを開く",
@ -367,6 +367,7 @@
"Control deployment behavior": "Control deployment behavior",
"Copy": "コピー",
"Copy Code": "コードをコピー",
"Copy Download Link": "ダウンロードリンクをコピー",
"Copy Link": "リンクをコピー",
"Copy Settings From Another Machine": "他の機器の設定をコピー",
"Copy to Clipboard": "クリップボードにコピー",
@ -403,6 +404,7 @@
"Cup drop not detected": "カップ落下未検出",
"Cup dropper failure": "カップディスペンサー落下エラー",
"Current": "現在",
"Current Brand Visual": "現在のブランドビジュアル",
"Current Password": "現在のパスワード",
"Current Status": "現在のステータス",
"Current Stock": "現在庫",
@ -416,6 +418,7 @@
"Current:": "現:",
"Custom": "カスタム",
"Custom Alert Types": "カスタムアラート種別",
"Custom Background Image": "カスタム背景画像",
"Custom Date": "カスタム日付",
"Custom Date Set": "カスタム日付設定済み",
"Custom Disabled (Mute Alert)": "カスタム無効(アラート消音)",
@ -423,6 +426,8 @@
"Custom Expiry": "カスタム期限",
"Custom Lower Limit (°C)": "カスタム下限温度 (°C)",
"Custom Upper Limit (°C)": "カスタム上限温度 (°C)",
"Custom Welcome Subtitle": "カスタムウェルカムサブタイトル",
"Custom branding function is not enabled for this company.": "該当企業はブランドデザイン機能のライセンスが有効化されていません。",
"Customer Details": "顧客詳細",
"Customer Info": "顧客情報",
"Customer List": "顧客一覧",
@ -433,6 +438,8 @@
"Customer deleted successfully.": "顧客が正常に削除されました。",
"Customer enabled successfully.": "顧客が正常に有効化されました。",
"Customer updated successfully.": "顧客が正常に更新されました。",
"Customize Brand": "ブランド設定",
"Customize Brand & Style": "ブランド&デザインのカスタマイズ",
"Cycle Efficiency": "サイクル効率",
"Daily Revenue": "本日の収益",
"Daily operational variance curve for selected timeframe": "選択期間における日次運営変動曲線",
@ -526,17 +533,17 @@
"Dispense successful": "搬送成功",
"Dispensing": "出庫",
"Dispensing in progress": "搬送中",
"Display Material Code": "資材コードを表示",
"Display Material Code": "品目コードを表示",
"Displayed below \"Welcome Back\" on the login portal.": "ログイン画面の「おかえりなさい」の下に表示されます。",
"Displaying": "表示中",
"Done": "完了",
"Door Closed": "扉が閉まりました",
"Door Opened": "扉が開きました",
"Copy Download Link": "ダウンロードリンクをコピー",
"Download APK": "APKをダウンロード",
"Download Image": "画像をダウンロード",
"Download link copied": "ダウンロードリンクをコピーしました",
"Download Template": "テンプレートのダウンロード",
"Download URL": "Download URL",
"Download link copied": "ダウンロードリンクをコピーしました",
"Draft": "下書き",
"Drag & drop your APK here": "Drag & drop your APK here",
"Duration": "時間",
@ -602,9 +609,10 @@
"Empty Slots": "空きスロット",
"Enable": "有効",
"Enable Alerts": "Enable Alerts",
"Enable Custom Branding": "ブランドデザイン機能を有効化",
"Enable Material Code": "資材コードを有効化",
"Enable Points": "ポイントルールを有効化",
"Enable Points Mechanism": "ポイント機能を有効化",
"Enable Points Mechanism": "ポイント還元を有効化",
"Enable and paste Webhook URL": "Webhook URLを有効にして貼り付けます",
"Enabled": "有効済み",
"Enabled Features": "有効な機能",
@ -624,6 +632,7 @@
"Enter role name": "ロール名を入力してください",
"Enter serial number": "シリアル番号を入力してください",
"Enter your password to confirm": "確認のためパスワードを入力してください",
"Enterprise Premium Features": "エンタープライズ・プレミアム機能",
"Entire Machine": "全機器",
"Equipment efficiency and OEE metrics": "設備効率および OEE 指標",
"Error": "異常",
@ -792,6 +801,7 @@
"Generate and manage one-time pickup codes for customers": "顧客用のワンタイム受取コードを生成・管理",
"Gift Definitions": "ギフト設定",
"Global roles accessible by all administrators.": "全管理者がアクセス可能なグローバルロール。",
"Go Authorize": "先に授権する",
"Go Back": "戻る",
"Go to E-Invoice": "電子領収書へ移動",
"Goods & System Settings": "商品・システム設定",
@ -923,6 +933,7 @@
"Lock Page": "ページロック",
"Lock Page Lock": "機器アプリロック",
"Lock Page Unlock": "機器アプリロック解除",
"Locked": "ロック中",
"Locked Page": "ロックページ",
"Log Details": "Log Details",
"Log Time": "記録時間",
@ -1151,6 +1162,7 @@
"No Company": "システム",
"No Invoice": "領収書を発行しない",
"No Location": "位置情報なし",
"No Logo": "ロゴ未設定",
"No Machine Selected": "機器が選択されていません",
"No Model": "モデル未設定",
"No accounts found": "アカウントが見つかりません",
@ -1442,6 +1454,7 @@
"Previous": "前へ",
"Price / Member": "価格 / 会員価格",
"Pricing Information": "価格情報",
"Primary Theme Color": "プライマリーテーマカラー",
"Print": "印刷",
"Print & PDF": "印刷 & PDF",
"Print Invoice": "領収書印刷",
@ -1471,6 +1484,9 @@
"Product created successfully": "商品が正常に作成されました",
"Product deleted successfully": "商品が正常に削除されました",
"Product empty": "商品売り切れ",
"Product heating": "商品加熱中",
"Product heating remaining time": "商品加熱残り時間",
"Product not detected in microwave": "電子レンジ内商品未検出",
"Product physical shipment count": "商品の実出荷数",
"Product status updated to :status": "商品のステータスが :status に更新されました",
"Product updated successfully": "商品が正常に更新されました",
@ -1489,6 +1505,7 @@
"Purchase Audit": "購入監査",
"Purchase Finished": "購入終了",
"Purchase successful": "購入成功",
"Purchase terminated": "購入中止",
"Purchases": "購入伝票",
"Purchasing": "購入中",
"Purpose": "用途",
@ -1738,6 +1755,7 @@
"Settings loaded from": "以下の機器から設定を読み込みました:",
"Settings updated successfully.": "設定が正常に更新されました。",
"Settlement": "決済処理",
"Setup Brand": "デザイン設定",
"Shopping Cart": "ショッピングカート",
"Shopping Cart Feature": "ショッピングカート機能",
"Shopping Cart Function": "ショッピングカート機能",
@ -1863,6 +1881,7 @@
"Superseded by new adjustment": "新しい調整により上書きされました",
"Superseded by new command": "新しいコマンドにより上書きされました",
"Superseded by new command (Timeout)": "新しいコマンドにより上書きされました (タイムアウト)",
"Supports JPEG, PNG, SVG (Max 2MB). Ideal proportion: Horizontal or Square.": "JPEG、PNG、SVGをサポート最大2MB。推奨比率横長または正方形。",
"Survey Analysis": "アンケート分析",
"Sync Ads": "広告を同期",
"Sync Products": "商品データを同期",
@ -1928,6 +1947,7 @@
"The image is too large. Please upload an image smaller than 5MB.": "画像サイズが大きすぎます。5MB未満の画像をアップロードしてください。",
"This Month": "今月",
"This Week": "今週",
"This account does not belong to this company.": "このアカウントは該当企業に所属していません。",
"This is a system administrator role. Its name is locked to ensure system stability.": "これはシステム管理者ロールです。システムの安定性を確保するため、名前はロックされています。",
"This machine has a pending command. Please wait.": "この機器には実行中のコマンドがあります。しばらくお待ちください。",
"This role belongs to another company and cannot be assigned.": "このロールは別の会社に属しているため、割り当てることができません。",
@ -2012,6 +2032,7 @@
"UI Elements": "UI要素",
"Unassigned": "未割り当て",
"Unassigned / No Change": "未割り当て / 変更なし",
"Unauthorized": "未授権",
"Unauthorized Status": "未承認",
"Unauthorized login attempt: :account": "不正なログイン試行: :account",
"Unauthorized: Account not authorized for this machine": "未承認: アカウントはこの機器へのアクセス権がありません",
@ -2043,9 +2064,11 @@
"Update failed": "更新失敗",
"Update identification for your asset": "資産の識別情報を更新",
"Update your account's profile information and email address.": "アカウントのプロフィール情報とメールアドレスを更新します。",
"Upload Company Logo": "ロゴ画像をアップロード",
"Upload Image": "画像をアップロード",
"Upload New APK": "Upload New APK",
"Upload New Images": "新しい写真をアップロード",
"Upload Time": "アップロード時間",
"Upload Video": "動画をアップロード",
"Upload failed, please try again": "アップロードに失敗しました、再試行してください",
"Upload file or provide URL": "Upload file or provide URL",
@ -2148,6 +2171,7 @@
"Welcome Gift Name": "ウェルカムギフト名",
"Welcome Gift Status": "来店ギフトステータス",
"Welcome Gifts": "ウェルカムギフト",
"Welcome Title": "ウェルカムタイトル",
"Work Content": "作業内容",
"Yes, Cancel": "はい、キャンセルします",
"Yes, Deactivate": "はい、無効にします",
@ -2159,6 +2183,7 @@
"You cannot assign permissions you do not possess.": "自身が持っていない権限を割り当てることはできません。",
"You cannot delete your own account.": "自分自身のアカウントは削除できません。",
"You do not have permission to access replenishment orders": "補充伝票のアクセス権限がありません",
"Your account is disabled.": "アカウントが無効化されています。",
"Your recent account activity": "最近のアカウントアクティビティ",
"[PickupCode] Verification failed: :code": "[受取コード] 検証失敗: :code",
"[StaffCard] Verification failed: :uid": "[スタッフカード] 検証失敗: :uid",

View File

@ -191,6 +191,7 @@
"Authorization updated successfully": "授權更新成功",
"Authorize": "授權",
"Authorize Btn": "授權",
"Authorized": "已授權",
"Authorized Accounts": "授權帳號",
"Authorized Accounts Tab": "授權帳號",
"Authorized Machines": "授權機台",
@ -219,15 +220,13 @@
"Before Qty": "異動前數量",
"Belongs To": "公司名稱",
"Belongs To Company": "公司名稱",
"Product heating": "商品正在加熱",
"Product heating remaining time": "商品加熱剩餘時間",
"Product not detected in microwave": "微波爐內未檢測到商品",
"Purchase terminated": "購買終止",
"Bill Acceptor": "紙鈔機",
"Bracelet drop not detected": "沒檢測到手環掉入取貨斗",
"Bracelet scan failed": "沒掃碼到手環",
"Branch": "分倉",
"Branch Warehouses": "分倉數量",
"Brand Styling": "品牌樣式",
"Brand styling updated successfully.": "品牌樣式更新成功。",
"Business Type": "業務類型",
"Buyout": "買斷",
"CASH": "CASH",
@ -284,6 +283,7 @@
"Clear": "清除",
"Clear Abnormal Status": "清除異常狀態",
"Clear Filter": "清除篩選",
"Clear Logo": "清除 Logo",
"Clear Stock": "庫存清空",
"Click here to re-send the verification email.": "點擊此處重新發送驗證郵件。",
"Click to Open Dashboard": "點擊開啟儀表板",
@ -367,6 +367,7 @@
"Control deployment behavior": "控制部署行為",
"Copy": "複製",
"Copy Code": "複製代碼",
"Copy Download Link": "複製下載連結",
"Copy Link": "複製連結",
"Copy Settings From Another Machine": "複製其他機台設定",
"Copy to Clipboard": "複製至剪貼簿",
@ -403,6 +404,7 @@
"Cup drop not detected": "未檢測到杯子掉落",
"Cup dropper failure": "漏杯器漏杯失敗",
"Current": "當前",
"Current Brand Visual": "目前品牌視覺",
"Current Password": "當前密碼",
"Current Status": "當前狀態",
"Current Stock": "當前庫存",
@ -416,6 +418,7 @@
"Current:": "現:",
"Custom": "自訂",
"Custom Alert Types": "自訂告警類型",
"Custom Background Image": "自訂背景圖",
"Custom Date": "自定義日期",
"Custom Date Set": "已設定自定義日期",
"Custom Disabled (Mute Alert)": "自訂停用(靜音告警)",
@ -423,6 +426,8 @@
"Custom Expiry": "自訂效期",
"Custom Lower Limit (°C)": "自訂下限溫度 (°C)",
"Custom Upper Limit (°C)": "自訂上限溫度 (°C)",
"Custom Welcome Subtitle": "自訂歡迎副標題",
"Custom branding function is not enabled for this company.": "此公司尚未啟用品牌自訂功能授權。",
"Customer Details": "客戶詳情",
"Customer Info": "客戶資訊",
"Customer List": "客戶列表",
@ -433,6 +438,8 @@
"Customer deleted successfully.": "客戶已成功刪除。",
"Customer enabled successfully.": "客戶已成功啟用。",
"Customer updated successfully.": "客戶更新成功。",
"Customize Brand": "客製化品牌",
"Customize Brand & Style": "客製化品牌樣式",
"Cycle Efficiency": "週期效率",
"Daily Revenue": "當日營收",
"Daily operational variance curve for selected timeframe": "所選統計區間之每日營運變動曲線",
@ -527,16 +534,16 @@
"Dispensing": "出貨",
"Dispensing in progress": "正在出貨中",
"Display Material Code": "顯示物料代碼",
"Displayed below \"Welcome Back\" on the login portal.": "顯示於登入入口「歡迎回來」下方。",
"Displaying": "目前顯示",
"Done": "已完成",
"Door Closed": "機門已關閉",
"Door Opened": "機門已開啟",
"Copy Download Link": "複製下載連結",
"Download APK": "下載 APK",
"Download Image": "下載圖片",
"Download link copied": "下載連結已複製",
"Download Template": "下載範例檔",
"Download URL": "下載連結",
"Download link copied": "下載連結已複製",
"Draft": "草稿",
"Drag & drop your APK here": "拖曳 APK 檔案至此",
"Duration": "時長",
@ -602,6 +609,7 @@
"Empty Slots": "空貨道",
"Enable": "啟用",
"Enable Alerts": "啟用告警",
"Enable Custom Branding": "啟用品牌自訂功能",
"Enable Material Code": "啟用物料編號",
"Enable Points": "啟用點數規則",
"Enable Points Mechanism": "啟用點數機制",
@ -624,6 +632,7 @@
"Enter role name": "請輸入角色名稱",
"Enter serial number": "請輸入機台序號",
"Enter your password to confirm": "請輸入您的密碼以確認",
"Enterprise Premium Features": "企業白金版專屬功能",
"Entire Machine": "全機台",
"Equipment efficiency and OEE metrics": "設備效能與 OEE 綜合指標",
"Error": "異常",
@ -792,6 +801,7 @@
"Generate and manage one-time pickup codes for customers": "產生並管理客戶的一次性取貨碼",
"Gift Definitions": "禮品設定",
"Global roles accessible by all administrators.": "適用於所有管理者的全域角色。",
"Go Authorize": "前往授權",
"Go Back": "返回",
"Go to E-Invoice": "前往電子發票",
"Goods & System Settings": "商品與系統設定",
@ -923,6 +933,7 @@
"Lock Page": "頁面鎖定",
"Lock Page Lock": "機台 APP 鎖定",
"Lock Page Unlock": "機台 APP 解鎖",
"Locked": "尚未解鎖",
"Locked Page": "鎖定頁",
"Log Details": "操作紀錄詳情",
"Log Time": "記錄時間",
@ -1151,6 +1162,7 @@
"No Company": "系統",
"No Invoice": "不開立發票",
"No Location": "無位置資訊",
"No Logo": "尚未設定 Logo",
"No Machine Selected": "尚未選擇機台",
"No Model": "未設定型號",
"No accounts found": "找不到帳號資料",
@ -1442,6 +1454,7 @@
"Previous": "上一頁",
"Price / Member": "售價 / 會員價",
"Pricing Information": "價格資訊",
"Primary Theme Color": "主題色系設定",
"Print": "列印",
"Print & PDF": "列印與 PDF",
"Print Invoice": "列印發票",
@ -1471,6 +1484,9 @@
"Product created successfully": "商品已成功建立",
"Product deleted successfully": "商品已成功刪除",
"Product empty": "商品售罄",
"Product heating": "商品正在加熱",
"Product heating remaining time": "商品加熱剩餘時間",
"Product not detected in microwave": "微波爐內未檢測到商品",
"Product physical shipment count": "商品實體出貨件數",
"Product status updated to :status": "商品狀態已更新為 :status",
"Product updated successfully": "商品已成功更新",
@ -1489,6 +1505,7 @@
"Purchase Audit": "採購單",
"Purchase Finished": "購買結束",
"Purchase successful": "購買成功",
"Purchase terminated": "購買終止",
"Purchases": "採購單",
"Purchasing": "購買中",
"Purpose": "用途",
@ -1738,6 +1755,7 @@
"Settings loaded from": "已從以下機台載入設定:",
"Settings updated successfully.": "設定更新成功。",
"Settlement": "結帳處理",
"Setup Brand": "設定樣式",
"Shopping Cart": "購物車",
"Shopping Cart Feature": "購物車功能",
"Shopping Cart Function": "購物車功能",
@ -1863,6 +1881,7 @@
"Superseded by new adjustment": "此指令已由後面最新的調整所取代",
"Superseded by new command": "此指令已由後面最新的指令所取代",
"Superseded by new command (Timeout)": "指令逾時 (1 分鐘未回報),已被新指令覆蓋。",
"Supports JPEG, PNG, SVG (Max 2MB). Ideal proportion: Horizontal or Square.": "支援 JPEG, PNG, SVG (最大 2MB)。建議比例:橫式或正方形。",
"Survey Analysis": "問卷分析",
"Sync Ads": "同步廣告",
"Sync Products": "同步商品資料",
@ -1928,6 +1947,7 @@
"The image is too large. Please upload an image smaller than 5MB.": "圖片檔案太大,請上傳小於 5MB 的圖片。",
"This Month": "本月",
"This Week": "本週",
"This account does not belong to this company.": "此帳號不屬於該公司。",
"This is a system administrator role. Its name is locked to ensure system stability.": "這是系統管理員角色,名稱已鎖定以確保系統穩定性。",
"This machine has a pending command. Please wait.": "此機台已有指令正在執行,請稍後。",
"This role belongs to another company and cannot be assigned.": "此角色屬於其他公司,無法指派。",
@ -2012,6 +2032,7 @@
"UI Elements": "UI元素",
"Unassigned": "未指派",
"Unassigned / No Change": "未指派 / 不變動",
"Unauthorized": "未授權",
"Unauthorized Status": "未授權",
"Unauthorized login attempt: :account": "越權登入嘗試::account",
"Unauthorized: Account not authorized for this machine": "授權失敗:此帳號無存取該機台的權限",
@ -2043,9 +2064,11 @@
"Update failed": "更新失敗",
"Update identification for your asset": "更新您的資產識別名稱",
"Update your account's profile information and email address.": "更新您的帳號名稱、手機號碼與電子郵件地址。",
"Upload Company Logo": "上傳公司 Logo",
"Upload Image": "上傳圖片",
"Upload New APK": "上傳新 APK",
"Upload New Images": "上傳新照片",
"Upload Time": "上傳時間",
"Upload Video": "上傳影片",
"Upload failed, please try again": "上傳失敗,請重試",
"Upload file or provide URL": "上傳檔案或提供連結",
@ -2148,6 +2171,7 @@
"Welcome Gift Name": "來店禮名稱",
"Welcome Gift Status": "來店禮",
"Welcome Gifts": "來店禮",
"Welcome Title": "歡迎詞標題",
"Work Content": "作業內容",
"Yes, Cancel": "確認取消",
"Yes, Deactivate": "是的,停用",
@ -2159,6 +2183,7 @@
"You cannot assign permissions you do not possess.": "您無法指派您自身不具備的權限。",
"You cannot delete your own account.": "您無法刪除自己的帳號。",
"You do not have permission to access replenishment orders": "您沒有機台補貨單的權限",
"Your account is disabled.": "您的帳號已被停用。",
"Your recent account activity": "最近的帳號活動",
"[PickupCode] Verification failed: :code": "[取貨碼] 驗證失敗: :code",
"[StaffCard] Verification failed: :uid": "[員工卡] 驗證失敗: :uid",

View File

@ -160,6 +160,7 @@
<th class="px-6 py-4 text-[11px] font-black text-slate-400 uppercase tracking-[0.2em] border-b border-slate-100 dark:border-slate-800">{{ __('Version Info') }}</th>
<th class="px-6 py-4 text-[11px] font-black text-slate-400 uppercase tracking-[0.2em] border-b border-slate-100 dark:border-slate-800">{{ __('Machine Flavor') }}</th>
<th class="px-6 py-4 text-[11px] font-black text-slate-400 uppercase tracking-[0.2em] border-b border-slate-100 dark:border-slate-800">{{ __('Deployment Status') }}</th>
<th class="px-6 py-4 text-[11px] font-black text-slate-400 uppercase tracking-[0.2em] border-b border-slate-100 dark:border-slate-800">{{ __('Upload Time') }}</th>
<th class="px-6 py-4 text-[11px] font-black text-slate-400 uppercase tracking-[0.2em] border-b border-slate-100 dark:border-slate-800">{{ __('Release Notes') }}</th>
<th class="px-6 py-4 text-[11px] font-black text-slate-400 uppercase tracking-[0.2em] border-b border-slate-100 dark:border-slate-800 text-right">{{ __('Action') }}</th>
</tr>
@ -228,6 +229,11 @@
</div>
</div>
</td>
<td class="px-6 py-6 whitespace-nowrap">
<span class="text-xs font-black text-slate-400 font-mono tracking-widest">
{{ $version->created_at->format('Y-m-d H:i:s') }}
</span>
</td>
<td class="px-6 py-6 max-w-xs" x-data="{ expanded: false }">
@if($version->release_notes)
<div :class="expanded ? 'whitespace-normal break-words' : 'truncate'"
@ -281,7 +287,7 @@
</tr>
@empty
<tr>
<td colspan="5" class="px-6 py-20 text-center">
<td colspan="6" class="px-6 py-20 text-center">
<div class="inline-flex items-center justify-center w-20 h-20 rounded-3xl bg-slate-50 dark:bg-slate-800/50 mb-6 border border-slate-100 dark:border-slate-800 shadow-sm">
<svg class="w-10 h-10 text-slate-300 dark:text-slate-600" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M12 16.5V9.75m0 0l3 3m-3-3l-3 3M6.75 19.5h10.5a2.25 2.25 0 002.25-2.25V6.75A2.25 2.25 0 0017.25 4.5H6.75A2.25 2.25 0 004.5 6.75v10.5a2.25 2.25 0 002.25 2.25z"/></svg>
</div>

View File

@ -5,6 +5,7 @@
showModal: false,
showHistoryModal: false,
showSettingsModal: false,
showBrandingModal: false,
editing: false,
sidebarView: 'detail',
activeTab: '{{ request('tab', 'list') }}',
@ -28,7 +29,10 @@
note: '',
settings: {
enable_material_code: false,
enable_points: false
enable_points: false,
enable_custom_branding: false,
logo_path: '',
login_title: ''
}
},
openCreateModal() {
@ -42,7 +46,10 @@
status: 1, note: '',
settings: {
enable_material_code: false,
enable_points: false
enable_points: false,
enable_custom_branding: false,
logo_path: '',
login_title: ''
}
};
this.showModal = true;
@ -59,7 +66,10 @@
software_end_date: company.software_end_date ? company.software_end_date.substring(0, 10) : '',
settings: {
enable_material_code: company.settings?.enable_material_code || false,
enable_points: company.settings?.enable_points || false
enable_points: company.settings?.enable_points || false,
enable_custom_branding: company.settings?.enable_custom_branding || false,
logo_path: company.settings?.logo_path || '',
login_title: company.settings?.login_title || ''
}
};
this.originalStatus = company.status;
@ -79,7 +89,7 @@
warranty_start_date: '', warranty_end_date: '',
software_start_date: '', software_end_date: '',
status: 1, note: '',
settings: { enable_material_code: false, enable_points: false },
settings: { enable_material_code: false, enable_points: false, enable_custom_branding: false },
users_count: 0, machines_count: 0,
contracts: []
},
@ -88,7 +98,8 @@
...company,
settings: {
enable_material_code: company.settings?.enable_material_code || false,
enable_points: company.settings?.enable_points || false
enable_points: company.settings?.enable_points || false,
enable_custom_branding: company.settings?.enable_custom_branding || false
}
};
this.sidebarView = 'detail';
@ -99,7 +110,8 @@
...company,
settings: {
enable_material_code: company.settings?.enable_material_code || false,
enable_points: company.settings?.enable_points || false
enable_points: company.settings?.enable_points || false,
enable_custom_branding: company.settings?.enable_custom_branding || false
}
};
this.sidebarView = 'history';
@ -113,7 +125,8 @@
...company,
settings: {
enable_material_code: company.settings?.enable_material_code || false,
enable_points: company.settings?.enable_points || false
enable_points: company.settings?.enable_points || false,
enable_custom_branding: company.settings?.enable_custom_branding || false
}
};
this.showHistoryModal = true;
@ -123,11 +136,25 @@
...company,
settings: {
enable_material_code: company.settings?.enable_material_code === true || company.settings?.enable_material_code === 1 || company.settings?.enable_material_code === '1',
enable_points: company.settings?.enable_points === true || company.settings?.enable_points === 1 || company.settings?.enable_points === '1'
enable_points: company.settings?.enable_points === true || company.settings?.enable_points === 1 || company.settings?.enable_points === '1',
enable_custom_branding: company.settings?.enable_custom_branding === true || company.settings?.enable_custom_branding === 1 || company.settings?.enable_custom_branding === '1'
}
};
this.showSettingsModal = true;
},
openBrandingModal(company) {
this.currentCompany = {
...company,
settings: {
enable_material_code: company.settings?.enable_material_code || false,
enable_points: company.settings?.enable_points || false,
enable_custom_branding: company.settings?.enable_custom_branding || false,
logo_path: company.settings?.logo_path || '',
login_title: company.settings?.login_title || ''
}
};
this.showBrandingModal = true;
},
submitConfirmedForm() {
if (this.statusToggleSource === 'list') {
this.$refs.statusToggleForm.submit();
@ -203,6 +230,11 @@
class="px-8 py-3 rounded-xl text-sm font-black uppercase tracking-widest transition-all duration-300">
{{ __('System Settings') }}
</button>
<button type="button" @click="activeTab = 'branding'"
:class="activeTab === 'branding' ? '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">
{{ __('Brand Styling') }}
</button>
</div>
<div class="relative mt-6">
@ -683,6 +715,117 @@
</div>
</div>
</div>
<!-- Brand Styling Tab -->
<div x-show="activeTab === 'branding'" x-cloak class="space-y-3">
<div class="luxury-card rounded-3xl p-8 animate-luxury-in">
<div class="flex flex-col md:flex-row md:items-center justify-between mb-8 gap-6">
<form action="{{ route('admin.permission.companies.index') }}" method="GET" class="relative group" @submit.prevent="fetchPage($el.action + '?' + new URLSearchParams(new FormData($el)).toString())">
<input type="hidden" name="tab" value="branding">
<span class="absolute inset-y-0 left-0 flex items-center pl-4 pointer-events-none z-10">
<svg class="h-4 w-4 text-slate-400 group-focus-within:text-cyan-500 transition-colors stroke-[2.5]" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round">
<circle cx="11" cy="11" r="8"></circle>
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
</svg>
</span>
<input type="text" name="search" value="{{ request('search') }}" class="py-2.5 pl-12 pr-6 block w-64 luxury-input" placeholder="{{ __('Search customers...') }}">
</form>
</div>
<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/30">
<th class="px-6 py-4 text-[12px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-widest border-b border-slate-100 dark:border-slate-800">{{ __('Customer Info') }}</th>
<th class="px-6 py-4 text-[12px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-widest border-b border-slate-100 dark:border-slate-800 text-center">{{ __('Authorized Status') }}</th>
<th class="px-6 py-4 text-[12px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-widest border-b border-slate-100 dark:border-slate-800">{{ __('Current Brand Visual') }}</th>
<th class="px-6 py-4 text-[12px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-widest border-b border-slate-100 dark:border-slate-800 text-right">{{ __('Actions') }}</th>
</tr>
</thead>
<tbody class="divide-y divide-slate-50 dark:divide-slate-800/50">
@forelse($companies as $company)
@php $settings = $company->settings ?? []; @endphp
<tr class="group hover:bg-slate-50/80 dark:hover:bg-slate-800/40 transition-all duration-300">
<td class="px-6 py-6 w-1/4">
<div class="flex items-center gap-x-4">
<div class="w-10 h-10 rounded-xl bg-slate-100 dark:bg-slate-800 flex items-center justify-center text-slate-400 group-hover:bg-cyan-500 group-hover:text-white transition-all">
<svg class="w-5 h-5 stroke-[2.5]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4" />
</svg>
</div>
<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">{{ $company->name }}</span>
<span class="text-xs font-mono font-bold text-slate-400 dark:text-slate-500 mt-0.5 tracking-widest uppercase">{{ $company->code }}</span>
</div>
</div>
</td>
<td class="px-6 py-6 text-center">
@if(filter_var($settings['enable_custom_branding'] ?? false, FILTER_VALIDATE_BOOLEAN))
<span class="inline-flex items-center px-2.5 py-1 rounded-full text-xs font-black bg-emerald-500/10 text-emerald-500 border border-emerald-500/20 tracking-widest uppercase">
{{ __('Authorized') }}
</span>
@else
<span class="inline-flex items-center px-2.5 py-1 rounded-full text-xs font-black bg-slate-100 dark:bg-slate-800 text-slate-400 tracking-widest uppercase">
{{ __('Unauthorized') }}
</span>
@endif
</td>
<td class="px-6 py-6">
<div class="flex items-center gap-4">
<div class="w-12 h-12 rounded-xl bg-slate-50 dark:bg-slate-800/80 border border-slate-200/50 dark:border-slate-700/50 flex items-center justify-center overflow-hidden shrink-0">
@if(isset($settings['logo_path']))
<img src="{{ Storage::disk('public')->url($settings['logo_path']) }}" class="max-w-full max-h-full object-contain">
@else
<span class="text-[10px] font-bold text-slate-400 dark:text-slate-500 uppercase">{{ __('No Logo') }}</span>
@endif
</div>
<div class="flex flex-col min-w-0">
<span class="text-xs font-bold text-slate-400 uppercase tracking-widest">{{ __('Welcome Title') }}</span>
<span class="text-sm font-extrabold text-slate-700 dark:text-slate-200 truncate max-w-[200px] mt-0.5" title="{{ $settings['login_title'] ?? __('System Default') }}">
{{ $settings['login_title'] ?? __('System Default') }}
</span>
</div>
</div>
</td>
<td class="px-6 py-6 text-right">
@if(filter_var($settings['enable_custom_branding'] ?? false, FILTER_VALIDATE_BOOLEAN))
<button type="button" @click='openBrandingModal({{ json_encode($company) }})'
class="px-4 py-2 rounded-xl bg-cyan-500 text-white hover:bg-cyan-600 text-xs font-black tracking-widest uppercase shadow-lg shadow-cyan-500/10 active:scale-95 transition-all"
title="{{ __('Customize Brand') }}">
{{ __('Setup Brand') }}
</button>
@else
<button type="button" @click='openSettingsModal({{ json_encode($company) }})'
class="px-4 py-2 rounded-xl bg-slate-100 dark:bg-slate-800 text-slate-400 dark:text-slate-500 hover:text-slate-600 text-xs font-black tracking-widest uppercase transition-all"
title="{{ __('Authorize first') }}">
{{ __('Go Authorize') }}
</button>
@endif
</td>
</tr>
@empty
<tr>
<td colspan="4" class="px-6 py-24 text-center">
<div class="flex flex-col items-center">
<div class="w-16 h-16 rounded-2xl bg-slate-50 dark:bg-slate-900 flex items-center justify-center text-slate-300 mb-4">
<svg class="w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4" />
</svg>
</div>
<p class="text-slate-400 font-bold">{{ __('No customers found') }}</p>
</div>
</td>
</tr>
@endforelse
</tbody>
</table>
</div>
<div class="mt-8 border-t border-slate-100 dark:border-slate-800 pt-6">
{{ $companies->appends(['tab' => 'branding', 'search' => request('search')])->links('vendor.pagination.luxury') }}
</div>
</div>
</div>
</div>
</div>
@ -1033,6 +1176,14 @@
</div>
<span class="text-sm font-bold text-slate-600 dark:text-slate-300 group-hover:text-slate-900 dark:group-hover:text-white transition-colors">{{ __('Enable Points Mechanism') }}</span>
</label>
<label class="flex items-center gap-3 cursor-pointer group">
<div class="relative inline-flex items-center">
<input type="hidden" name="settings[enable_custom_branding]" value="0">
<input type="checkbox" name="settings[enable_custom_branding]" value="1" x-model="currentCompany.settings.enable_custom_branding" class="peer sr-only">
<div class="w-9 h-5 bg-slate-200 peer-focus:outline-none rounded-full peer dark:bg-slate-700 peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-4 after:w-4 after:transition-all dark:border-gray-600 peer-checked:bg-cyan-500"></div>
</div>
<span class="text-sm font-bold text-slate-600 dark:text-slate-300 group-hover:text-slate-900 dark:group-hover:text-white transition-colors">{{ __('Enable Custom Branding') }}</span>
</label>
</div>
</div>
</div>
@ -1047,6 +1198,142 @@
</div>
</div>
</div>
<!-- Brand Styling Edit Modal -->
<div x-show="showBrandingModal" class="fixed inset-0 z-[100] flex items-center justify-center" x-cloak>
<div x-show="showBrandingModal"
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 bg-slate-900/40 backdrop-blur-sm" @click="showBrandingModal = false"></div>
<div x-show="showBrandingModal"
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="relative bg-white dark:bg-slate-900 rounded-3xl shadow-2xl border border-slate-100 dark:border-slate-800 w-full max-w-2xl mx-4 overflow-hidden z-[101] flex flex-col max-h-[90vh]">
<div class="px-8 py-6 border-b border-slate-100 dark:border-slate-800 flex justify-between items-center bg-slate-50/50 dark:bg-slate-900/50">
<div>
<h3 class="text-lg font-black text-slate-800 dark:text-white tracking-tight">{{ __('Customize Brand & Style') }}</h3>
<p class="text-xs font-bold text-slate-400 mt-1" x-text="currentCompany.name"></p>
</div>
<button type="button" @click="showBrandingModal = false" class="text-slate-400 hover:text-slate-500 bg-white dark:bg-slate-800 hover:bg-slate-100 dark:hover:bg-slate-700 rounded-xl p-2 transition-colors">
<svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
<div class="p-8 overflow-y-auto custom-scrollbar flex-1">
<form :action="`{{ route('admin.permission.companies.index') }}/${currentCompany.id}/branding`" method="POST" enctype="multipart/form-data">
@csrf
<div class="space-y-6">
<!-- Logo Upload Section -->
<div class="space-y-2" x-data="{ logoPreview: null, clearLogo: false }">
<label class="text-xs font-black text-slate-500 uppercase tracking-widest pl-1">{{ __('Upload Company Logo') }}</label>
<!-- Visual Logo Preview / Upload Area -->
<div class="flex flex-col sm:flex-row items-center gap-6 p-6 rounded-2xl bg-slate-50 dark:bg-slate-800/40 border border-slate-100 dark:border-slate-800/80">
<!-- Preview Window -->
<div class="w-24 h-24 rounded-2xl bg-white dark:bg-slate-900 border border-slate-200/50 dark:border-slate-700/50 flex items-center justify-center overflow-hidden shrink-0 shadow-sm relative group">
<!-- DB Existing Logo -->
<template x-if="currentCompany.settings.logo_path && !logoPreview && !clearLogo">
<img :src="`{{ Storage::disk('public')->url('') }}${currentCompany.settings.logo_path}`" class="max-w-full max-h-full object-contain">
</template>
<!-- Upload New Preview -->
<template x-if="logoPreview">
<img :src="logoPreview" class="max-w-full max-h-full object-contain">
</template>
<!-- Empty / Cleared Fallback -->
<template x-if="(!currentCompany.settings.logo_path || clearLogo) && !logoPreview">
<span class="text-[10px] font-bold text-slate-400 dark:text-slate-500 uppercase">{{ __('No Logo') }}</span>
</template>
</div>
<div class="flex-1 w-full space-y-3">
<div class="flex items-center gap-3">
<label class="px-4 py-2.5 rounded-xl bg-cyan-500 text-white hover:bg-cyan-600 text-xs font-black tracking-widest uppercase cursor-pointer shadow-lg shadow-cyan-500/10 active:scale-95 transition-all text-center">
{{ __('Choose File') }}
<input type="file" name="logo_file" class="hidden" accept="image/*" @change="
const file = $event.target.files[0];
if (file) {
clearLogo = false;
const reader = new FileReader();
reader.onload = (e) => { logoPreview = e.target.result; };
reader.readAsDataURL(file);
}
">
</label>
<!-- Clear Logo Button -->
<template x-if="currentCompany.settings.logo_path || logoPreview">
<button type="button" @click="
logoPreview = null;
clearLogo = true;
$el.form.querySelector('input[name=logo_file]').value = '';
" class="px-4 py-2.5 rounded-xl bg-slate-100 dark:bg-slate-800 text-slate-400 dark:text-slate-500 hover:text-rose-500 hover:bg-rose-500/5 text-xs font-black tracking-widest uppercase transition-all">
{{ __('Clear Logo') }}
</button>
</template>
<input type="hidden" name="clear_logo" :value="clearLogo ? '1' : '0'">
</div>
<p class="text-[10px] font-bold text-slate-400 dark:text-slate-500 uppercase tracking-wider">
{{ __('Supports JPEG, PNG, SVG (Max 2MB). Ideal proportion: Horizontal or Square.') }}
</p>
</div>
</div>
</div>
<!-- Welcome Title Section -->
<div class="space-y-2">
<label class="text-xs font-black text-slate-500 uppercase tracking-widest pl-1">{{ __('Custom Welcome Subtitle') }}</label>
<input type="text" name="login_title" x-model="currentCompany.settings.login_title" class="luxury-input w-full"
placeholder="{{ __('e.g. Please enter your account and password to enter the admin dashboard.') }}">
<p class="text-[10px] font-bold text-slate-400 dark:text-slate-500 uppercase tracking-wider pl-1 mt-1">
{{ __('Displayed below "Welcome Back" on the login portal.') }}
</p>
</div>
<!-- Future Expansion Area (Pre-designed, styled with luxury disable) -->
<div class="p-6 rounded-2xl border border-dashed border-slate-200 dark:border-slate-800/80 bg-slate-50/30 dark:bg-slate-900/10 space-y-4">
<div class="flex items-center gap-2">
<svg class="w-4 h-4 text-cyan-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" /></svg>
<h4 class="text-xs font-black text-slate-700 dark:text-slate-300 uppercase tracking-widest">{{ __('Enterprise Premium Features') }}</h4>
</div>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4 opacity-50 select-none">
<div class="space-y-2">
<label class="text-[10px] font-black text-slate-400 uppercase tracking-widest pl-1">{{ __('Custom Background Image') }}</label>
<div class="py-2.5 px-4 rounded-xl bg-slate-100 dark:bg-slate-800 border border-slate-200/50 dark:border-slate-700/50 text-xs font-bold text-slate-400">{{ __('Locked') }}</div>
</div>
<div class="space-y-2">
<label class="text-[10px] font-black text-slate-400 uppercase tracking-widest pl-1">{{ __('Primary Theme Color') }}</label>
<div class="py-2.5 px-4 rounded-xl bg-slate-100 dark:bg-slate-800 border border-slate-200/50 dark:border-slate-700/50 text-xs font-bold text-slate-400">{{ __('Locked') }}</div>
</div>
</div>
</div>
</div>
<div class="flex justify-end gap-x-4 pt-8 mt-6 border-t border-slate-100 dark:border-slate-800">
<button type="button" @click="showBrandingModal = false" class="btn-luxury-ghost px-8">{{ __('Cancel') }}</button>
<button type="submit" class="btn-luxury-primary px-12">
<span>{{ __('Save Changes') }}</span>
</button>
</div>
</form>
</div>
</div>
</div>
<x-delete-confirm-modal :message="__('Are you sure to delete this customer?')" />
<x-status-confirm-modal :title="__('Disable Customer Confirmation')" :message="__('Disabling this customer will also disable all accounts under this customer. Are you sure you want to continue?')" />

View File

@ -56,15 +56,17 @@
<div class="relative z-10 flex flex-col items-center justify-center space-y-12 py-6 md:py-12">
<!-- Logo -->
<img src="{{ asset('starcloudlogo-Photoroom.png') }}" alt="{{ config('app.name') }} Logo" class="w-64 sm:w-72 md:w-80 h-auto object-contain drop-shadow-sm">
<img src="{{ isset($company) && $company->logo_url ? $company->logo_url : asset('starcloudlogo-Photoroom.png') }}"
alt="{{ isset($company) ? $company->name : config('app.name') }} Logo"
class="w-64 sm:w-72 md:w-80 h-auto max-h-36 object-contain drop-shadow-sm">
<!-- 平台名稱 -->
<div class="text-center">
<h2 class="text-xl sm:text-2xl font-black text-slate-800 tracking-tight font-display">
AIoT 自動化設備智慧營運平台
{{ isset($company) ? $company->name : 'AIoT 自動化設備智慧營運平台' }}
</h2>
<p class="text-[10px] font-bold text-slate-400 uppercase tracking-[0.2em] mt-2 font-mono">
Smart Device Management Hub
{{ isset($company) ? 'Smart Vending Portal' : 'Smart Device Management Hub' }}
</p>
</div>
</div>
@ -82,8 +84,8 @@
<h1 class="text-2xl sm:text-3xl font-black text-slate-800 tracking-tight font-display">
歡迎回來
</h1>
<p class="mt-2 text-sm font-medium text-slate-500">
請輸入您的帳號與密碼,進入管理後台
<p class="mt-2 text-sm font-semibold text-slate-500">
{{ isset($company) ? ($company->settings['login_title'] ?? '請輸入您的帳號與密碼,進入管理後台') : '請輸入您的帳號與密碼,進入管理後台' }}
</p>
</div>
@ -94,7 +96,7 @@
</div>
@endif
<form method="POST" action="{{ route('login') }}" class="space-y-6">
<form method="POST" action="{{ isset($company) ? route('tenant.login.post', $company->code) : route('login') }}" class="space-y-6">
@csrf
<!-- 帳號輸入框 -->

View File

@ -131,8 +131,9 @@
<div class="mr-5 lg:mr-0 lg:hidden text-center">
<a class="flex items-center gap-x-2 flex-none text-xl font-bold dark:text-white font-display tracking-tight"
href="{{ route('admin.dashboard') }}" aria-label="Brand">
<img src="{{ asset('starcloud_icon.png') }}" alt="{{ config('app.name') }} Logo"
class="w-7 h-7 object-contain">
<img src="{{ auth()->user()->company && auth()->user()->company->hasCustomBrandingEnabled() && auth()->user()->company->logo_url ? auth()->user()->company->logo_url : asset('starcloud_icon.png') }}"
alt="{{ auth()->user()->company ? auth()->user()->company->name : config('app.name') }} Logo"
class="w-7 h-7 max-h-7 max-w-[28px] object-contain shrink-0">
<span>Star<span class="text-cyan-500">Cloud</span></span>
</a>
</div>
@ -474,8 +475,9 @@
<a class="flex items-center gap-x-2 text-xl font-bold text-slate-900 dark:text-white font-display tracking-tight whitespace-nowrap overflow-hidden transition-all duration-300"
href="{{ route('admin.dashboard') }}" aria-label="Brand"
:class="sidebarCollapsed ? 'max-w-0 opacity-0' : 'max-w-[200px] opacity-100'">
<img src="{{ asset('starcloud_icon.png') }}" alt="{{ config('app.name') }} Logo"
class="w-8 h-8 object-contain shrink-0">
<img src="{{ auth()->user()->company && auth()->user()->company->hasCustomBrandingEnabled() && auth()->user()->company->logo_url ? auth()->user()->company->logo_url : asset('starcloud_icon.png') }}"
alt="{{ auth()->user()->company ? auth()->user()->company->name : config('app.name') }} Logo"
class="w-8 h-8 max-h-8 max-w-[32px] object-contain shrink-0">
<span>Star<span class="text-cyan-500">Cloud</span></span>
</a>

View File

@ -21,6 +21,13 @@ Route::get('/', function () {
return redirect()->route('login');
});
// 客戶專屬登入頁面(公開,需 guest 中間件)
Route::middleware('guest')->group(function () {
Route::get('/c/{company_code}/login', [App\Http\Controllers\Auth\TenantLoginController::class, 'showLoginForm'])->name('tenant.login');
Route::post('/c/{company_code}/login', [App\Http\Controllers\Auth\TenantLoginController::class, 'login'])->name('tenant.login.post');
});
// 公開機台分布地圖 (無需登入)
Route::get('/machines/distribution', [App\Http\Controllers\Admin\BasicSettings\MachineSettingController::class, 'distribution'])->name('machines.distribution');
@ -296,6 +303,7 @@ Route::middleware(['auth', 'auth.session', 'verified', 'tenant.access'])->prefix
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::put('companies/{company}/settings', [App\Http\Controllers\Admin\CompanyController::class, 'updateSettings'])->name('companies.settings.update')->middleware('can:menu.permissions.companies');
Route::post('companies/{company}/branding', [App\Http\Controllers\Admin\CompanyController::class, 'updateBranding'])->name('companies.branding.update')->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');