diff --git a/app/Http/Controllers/Admin/CompanyController.php b/app/Http/Controllers/Admin/CompanyController.php index 88a21ed..1e89316 100644 --- a/app/Http/Controllers/Admin/CompanyController.php +++ b/app/Http/Controllers/Admin/CompanyController.php @@ -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.')); + } + + /** * 切換客戶狀態 */ diff --git a/app/Http/Controllers/Auth/TenantLoginController.php b/app/Http/Controllers/Auth/TenantLoginController.php new file mode 100644 index 0000000..62c55fc --- /dev/null +++ b/app/Http/Controllers/Auth/TenantLoginController.php @@ -0,0 +1,97 @@ +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')); + } +} diff --git a/app/Models/System/Company.php b/app/Models/System/Company.php index 20f5a09..e120595 100644 --- a/app/Models/System/Company.php +++ b/app/Models/System/Company.php @@ -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); + } } + diff --git a/lang/en.json b/lang/en.json index 13976f9..4f4ce76 100644 --- a/lang/en.json +++ b/lang/en.json @@ -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", diff --git a/lang/ja.json b/lang/ja.json index ddb7f19..6e5c78b 100644 --- a/lang/ja.json +++ b/lang/ja.json @@ -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", diff --git a/lang/zh_TW.json b/lang/zh_TW.json index 4b94763..1fc6b22 100644 --- a/lang/zh_TW.json +++ b/lang/zh_TW.json @@ -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", diff --git a/resources/views/admin/basic-settings/apk-versions/index.blade.php b/resources/views/admin/basic-settings/apk-versions/index.blade.php index 2ffb154..e81a9c6 100644 --- a/resources/views/admin/basic-settings/apk-versions/index.blade.php +++ b/resources/views/admin/basic-settings/apk-versions/index.blade.php @@ -160,6 +160,7 @@ {{ __('Version Info') }} {{ __('Machine Flavor') }} {{ __('Deployment Status') }} + {{ __('Upload Time') }} {{ __('Release Notes') }} {{ __('Action') }} @@ -228,6 +229,11 @@ + + + {{ $version->created_at->format('Y-m-d H:i:s') }} + + @if($version->release_notes)
@empty - +
diff --git a/resources/views/admin/companies/index.blade.php b/resources/views/admin/companies/index.blade.php index 23c55c6..f0374c1 100644 --- a/resources/views/admin/companies/index.blade.php +++ b/resources/views/admin/companies/index.blade.php @@ -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') }} +
@@ -683,6 +715,117 @@
+ + +
+
+
+
+ + + + + + + + +
+
+ +
+ + + + + + + + + + + @forelse($companies as $company) + @php $settings = $company->settings ?? []; @endphp + + + + + + + @empty + + + + @endforelse + +
{{ __('Customer Info') }}{{ __('Authorized Status') }}{{ __('Current Brand Visual') }}{{ __('Actions') }}
+
+
+ + + +
+
+ {{ $company->name }} + {{ $company->code }} +
+
+
+ @if(filter_var($settings['enable_custom_branding'] ?? false, FILTER_VALIDATE_BOOLEAN)) + + {{ __('Authorized') }} + + @else + + {{ __('Unauthorized') }} + + @endif + +
+
+ @if(isset($settings['logo_path'])) + + @else + {{ __('No Logo') }} + @endif +
+
+ {{ __('Welcome Title') }} + + {{ $settings['login_title'] ?? __('System Default') }} + +
+
+
+ @if(filter_var($settings['enable_custom_branding'] ?? false, FILTER_VALIDATE_BOOLEAN)) + + @else + + @endif +
+
+
+ + + +
+

{{ __('No customers found') }}

+
+
+
+ +
+ {{ $companies->appends(['tab' => 'branding', 'search' => request('search')])->links('vendor.pagination.luxury') }} +
+
+
@@ -1033,6 +1176,14 @@ {{ __('Enable Points Mechanism') }} + @@ -1047,6 +1198,142 @@ + + +
+
+ +
+ +
+
+

{{ __('Customize Brand & Style') }}

+

+
+ +
+ +
+
+ @csrf + +
+ +
+ + + +
+ +
+ + + + + + + + +
+ +
+
+ + + + + + +
+

+ {{ __('Supports JPEG, PNG, SVG (Max 2MB). Ideal proportion: Horizontal or Square.') }} +

+
+
+
+ + +
+ + +

+ {{ __('Displayed below "Welcome Back" on the login portal.') }} +

+
+ + +
+
+ +

{{ __('Enterprise Premium Features') }}

+
+
+
+ +
{{ __('Locked') }}
+
+
+ +
{{ __('Locked') }}
+
+
+
+
+ +
+ + +
+
+
+
+
diff --git a/resources/views/auth/login.blade.php b/resources/views/auth/login.blade.php index 3359ae1..6af7ece 100644 --- a/resources/views/auth/login.blade.php +++ b/resources/views/auth/login.blade.php @@ -56,15 +56,17 @@
- {{ config('app.name') }} Logo + {{ isset($company) ? $company->name : config('app.name') }} Logo

- AIoT 自動化設備智慧營運平台 + {{ isset($company) ? $company->name : 'AIoT 自動化設備智慧營運平台' }}

- Smart Device Management Hub + {{ isset($company) ? 'Smart Vending Portal' : 'Smart Device Management Hub' }}

@@ -82,8 +84,8 @@

歡迎回來

-

- 請輸入您的帳號與密碼,進入管理後台 +

+ {{ isset($company) ? ($company->settings['login_title'] ?? '請輸入您的帳號與密碼,進入管理後台') : '請輸入您的帳號與密碼,進入管理後台' }}

@@ -94,7 +96,7 @@ @endif -
+ @csrf diff --git a/resources/views/layouts/admin.blade.php b/resources/views/layouts/admin.blade.php index 165bfc3..d610adc 100644 --- a/resources/views/layouts/admin.blade.php +++ b/resources/views/layouts/admin.blade.php @@ -131,8 +131,9 @@ @@ -474,8 +475,9 @@ - {{ config('app.name') }} Logo + {{ auth()->user()->company ? auth()->user()->company->name : config('app.name') }} Logo StarCloud diff --git a/routes/web.php b/routes/web.php index abc0dec..8ad5fa2 100644 --- a/routes/web.php +++ b/routes/web.php @@ -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');