feat: 新增租戶品牌客製化系統(Logo、主色系)、修正 hardcoded 顏色為 CSS 變數
This commit is contained in:
@@ -92,6 +92,26 @@ class TenantController extends Controller
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 顯示租戶樣式管理頁面
|
||||||
|
*/
|
||||||
|
public function showBranding(Tenant $tenant)
|
||||||
|
{
|
||||||
|
$logoUrl = null;
|
||||||
|
if (isset($tenant->branding['logo_path'])) {
|
||||||
|
$logoUrl = \Storage::url($tenant->branding['logo_path']);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Inertia::render('Landlord/Tenant/Branding', [
|
||||||
|
'tenant' => [
|
||||||
|
'id' => $tenant->id,
|
||||||
|
'name' => $tenant->name ?? $tenant->id,
|
||||||
|
'branding' => $tenant->branding ?? [],
|
||||||
|
],
|
||||||
|
'logo_url' => $logoUrl,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 顯示編輯租戶表單
|
* 顯示編輯租戶表單
|
||||||
*/
|
*/
|
||||||
@@ -171,4 +191,44 @@ class TenantController extends Controller
|
|||||||
|
|
||||||
return back()->with('success', "域名 {$domainName} 已移除!");
|
return back()->with('success', "域名 {$domainName} 已移除!");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新租戶品牌樣式設定
|
||||||
|
*/
|
||||||
|
public function updateBranding(Request $request, Tenant $tenant)
|
||||||
|
{
|
||||||
|
$validated = $request->validate([
|
||||||
|
'logo' => 'nullable|image|max:2048',
|
||||||
|
'primary_color' => 'required|regex:/^#[0-9A-Fa-f]{6}$/',
|
||||||
|
'text_color' => 'nullable|regex:/^#[0-9A-Fa-f]{6}$/',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$branding = $tenant->branding ?? [];
|
||||||
|
|
||||||
|
// 處理 Logo 上傳
|
||||||
|
if ($request->hasFile('logo')) {
|
||||||
|
// 刪除舊 Logo
|
||||||
|
if (isset($branding['logo_path'])) {
|
||||||
|
\Storage::disk('public')->delete($branding['logo_path']);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 儲存新 Logo
|
||||||
|
$path = $request->file('logo')->store('tenant-logos', 'public');
|
||||||
|
$branding['logo_path'] = $path;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新主色系
|
||||||
|
$branding['primary_color'] = $validated['primary_color'];
|
||||||
|
|
||||||
|
// 如果有傳入字體顏色則更新,否則保留原值(或預設值)
|
||||||
|
if (isset($validated['text_color'])) {
|
||||||
|
$branding['text_color'] = $validated['text_color'];
|
||||||
|
} elseif (!isset($branding['text_color'])) {
|
||||||
|
$branding['text_color'] = '#1a1a1a';
|
||||||
|
}
|
||||||
|
|
||||||
|
$tenant->update(['branding' => $branding]);
|
||||||
|
|
||||||
|
return redirect()->back()->with('success', '樣式設定已更新');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -55,6 +55,23 @@ class HandleInertiaRequests extends Middleware
|
|||||||
'success' => $request->session()->get('success'),
|
'success' => $request->session()->get('success'),
|
||||||
'error' => $request->session()->get('error'),
|
'error' => $request->session()->get('error'),
|
||||||
],
|
],
|
||||||
|
'branding' => function () {
|
||||||
|
$tenant = tenancy()->tenant;
|
||||||
|
if (!$tenant) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$logoUrl = null;
|
||||||
|
if (isset($tenant->branding['logo_path'])) {
|
||||||
|
$logoUrl = \Storage::url($tenant->branding['logo_path']);
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'logo_url' => $logoUrl,
|
||||||
|
'primary_color' => $tenant->branding['primary_color'] ?? '#01ab83',
|
||||||
|
'text_color' => $tenant->branding['text_color'] ?? '#1a1a1a',
|
||||||
|
];
|
||||||
|
},
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
BIN
public/favicon-landlord.png
Normal file
BIN
public/favicon-landlord.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 176 KiB |
BIN
public/images/star-erp-icon.png
Normal file
BIN
public/images/star-erp-icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 176 KiB |
@@ -1,6 +1,22 @@
|
|||||||
import { ImgHTMLAttributes } from 'react';
|
import { ImgHTMLAttributes } from 'react';
|
||||||
|
import { usePage } from '@inertiajs/react';
|
||||||
|
import { PageProps } from '@/types/global';
|
||||||
|
|
||||||
export default function ApplicationLogo(props: ImgHTMLAttributes<HTMLImageElement>) {
|
export default function ApplicationLogo(props: ImgHTMLAttributes<HTMLImageElement>) {
|
||||||
|
const { branding } = usePage<PageProps>().props;
|
||||||
|
|
||||||
|
// 如果有自訂 Logo,優先使用
|
||||||
|
if (branding?.logo_url) {
|
||||||
|
return (
|
||||||
|
<img
|
||||||
|
{...props}
|
||||||
|
src={branding.logo_url}
|
||||||
|
alt="Logo"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 預設 Logo
|
||||||
return (
|
return (
|
||||||
<img
|
<img
|
||||||
{...props}
|
{...props}
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ const BreadcrumbNav = ({ items, className }: BreadcrumbNavProps) => {
|
|||||||
<BreadcrumbLink asChild>
|
<BreadcrumbLink asChild>
|
||||||
<Link
|
<Link
|
||||||
href={item.href}
|
href={item.href}
|
||||||
className="text-[#01ab83] hover:text-[#018a6a] font-medium transition-colors"
|
className="text-primary-main hover:text-primary-dark font-medium transition-colors"
|
||||||
>
|
>
|
||||||
{item.label}
|
{item.label}
|
||||||
</Link>
|
</Link>
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ import {
|
|||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { toast, Toaster } from "sonner";
|
import { toast, Toaster } from "sonner";
|
||||||
import { useState, useEffect, useMemo } from "react";
|
import { useState, useEffect, useMemo } from "react";
|
||||||
import { Link, usePage } from "@inertiajs/react";
|
import { Link, usePage, Head } from "@inertiajs/react";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import BreadcrumbNav, { BreadcrumbItemType } from "@/Components/shared/BreadcrumbNav";
|
import BreadcrumbNav, { BreadcrumbItemType } from "@/Components/shared/BreadcrumbNav";
|
||||||
import {
|
import {
|
||||||
@@ -33,6 +33,7 @@ import {
|
|||||||
} from "@/Components/ui/dropdown-menu";
|
} from "@/Components/ui/dropdown-menu";
|
||||||
import { usePermission } from "@/hooks/usePermission";
|
import { usePermission } from "@/hooks/usePermission";
|
||||||
import ApplicationLogo from "@/Components/ApplicationLogo";
|
import ApplicationLogo from "@/Components/ApplicationLogo";
|
||||||
|
import { generateLightestColor, generateLightColor, generateDarkColor, generateActiveColor } from "@/utils/colorUtils";
|
||||||
|
|
||||||
interface MenuItem {
|
interface MenuItem {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -328,6 +329,18 @@ export default function AuthenticatedLayout({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex min-h-screen bg-slate-50">
|
<div className="flex min-h-screen bg-slate-50">
|
||||||
|
<Head>
|
||||||
|
<link rel="icon" type="image/png" href="/favicon.png" />
|
||||||
|
<style>{`
|
||||||
|
:root {
|
||||||
|
--primary-main: ${(props as any).branding?.primary_color || '#01ab83'};
|
||||||
|
--primary-dark: ${generateDarkColor((props as any).branding?.primary_color || '#01ab83')};
|
||||||
|
--primary-light: ${generateLightColor((props as any).branding?.primary_color || '#01ab83')};
|
||||||
|
--primary-lightest: ${generateLightestColor((props as any).branding?.primary_color || '#01ab83')};
|
||||||
|
--button-main-active: ${generateActiveColor((props as any).branding?.primary_color || '#01ab83')};
|
||||||
|
}
|
||||||
|
`}</style>
|
||||||
|
</Head>
|
||||||
{/* Mobile Header -> Global Header */}
|
{/* Mobile Header -> Global Header */}
|
||||||
<header className="fixed top-0 left-0 right-0 h-16 bg-white border-b border-slate-200 z-[60] flex items-center justify-between px-4 transition-all duration-300">
|
<header className="fixed top-0 left-0 right-0 h-16 bg-white border-b border-slate-200 z-[60] flex items-center justify-between px-4 transition-all duration-300">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
@@ -395,7 +408,7 @@ export default function AuthenticatedLayout({
|
|||||||
{!isCollapsed && (
|
{!isCollapsed && (
|
||||||
<Link href="/" className="flex items-center gap-2 group">
|
<Link href="/" className="flex items-center gap-2 group">
|
||||||
<ApplicationLogo className="w-8 h-8 rounded-lg object-contain group-hover:scale-110 transition-transform" />
|
<ApplicationLogo className="w-8 h-8 rounded-lg object-contain group-hover:scale-110 transition-transform" />
|
||||||
<span className="font-extrabold text-[#01ab83] text-lg tracking-tight">小小冰室 ERP</span>
|
<span className="font-extrabold text-primary-main text-lg tracking-tight">小小冰室 ERP</span>
|
||||||
</Link>
|
</Link>
|
||||||
)}
|
)}
|
||||||
{isCollapsed && (
|
{isCollapsed && (
|
||||||
@@ -444,7 +457,7 @@ export default function AuthenticatedLayout({
|
|||||||
<div className="h-16 flex items-center justify-between px-6 border-b border-slate-100">
|
<div className="h-16 flex items-center justify-between px-6 border-b border-slate-100">
|
||||||
<Link href="/" className="flex items-center gap-2">
|
<Link href="/" className="flex items-center gap-2">
|
||||||
<ApplicationLogo className="w-8 h-8 rounded-lg object-contain" />
|
<ApplicationLogo className="w-8 h-8 rounded-lg object-contain" />
|
||||||
<span className="font-extrabold text-[#01ab83] text-lg">小小冰室 ERP</span>
|
<span className="font-extrabold text-primary-main text-lg">小小冰室 ERP</span>
|
||||||
</Link>
|
</Link>
|
||||||
<button onClick={() => setIsMobileOpen(false)} className="p-2 text-slate-400">
|
<button onClick={() => setIsMobileOpen(false)} className="p-2 text-slate-400">
|
||||||
<X className="h-5 w-5" />
|
<X className="h-5 w-5" />
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Link, usePage } from "@inertiajs/react";
|
import { Link, usePage, Head } from "@inertiajs/react";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import {
|
import {
|
||||||
Building2,
|
Building2,
|
||||||
@@ -8,6 +8,8 @@ import {
|
|||||||
Menu,
|
Menu,
|
||||||
X,
|
X,
|
||||||
Settings,
|
Settings,
|
||||||
|
PanelLeftClose,
|
||||||
|
PanelLeftOpen,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { Toaster } from "sonner";
|
import { Toaster } from "sonner";
|
||||||
import {
|
import {
|
||||||
@@ -18,7 +20,7 @@ import {
|
|||||||
DropdownMenuSeparator,
|
DropdownMenuSeparator,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from "@/Components/ui/dropdown-menu";
|
} from "@/Components/ui/dropdown-menu";
|
||||||
import { useState } from "react";
|
import { useState, useEffect } from "react";
|
||||||
|
|
||||||
interface LandlordLayoutProps {
|
interface LandlordLayoutProps {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
@@ -46,9 +48,24 @@ export default function LandlordLayout({ children, title }: LandlordLayoutProps)
|
|||||||
];
|
];
|
||||||
|
|
||||||
const [isSidebarOpen, setIsSidebarOpen] = useState(false);
|
const [isSidebarOpen, setIsSidebarOpen] = useState(false);
|
||||||
|
const [isCollapsed, setIsCollapsed] = useState(() => {
|
||||||
|
if (typeof window !== "undefined") {
|
||||||
|
return localStorage.getItem("landlord-sidebar-collapsed") === "true";
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
localStorage.setItem("landlord-sidebar-collapsed", String(isCollapsed));
|
||||||
|
}, [isCollapsed]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex min-h-screen bg-slate-50 relative">
|
<div className="flex min-h-screen bg-slate-50 relative">
|
||||||
|
<Head>
|
||||||
|
<link rel="icon" type="image/png" href="/favicon-landlord.png" />
|
||||||
|
<link rel="shortcut icon" href="/favicon-landlord.png" type="image/png" />
|
||||||
|
<link rel="apple-touch-icon" href="/favicon-landlord.png" />
|
||||||
|
</Head>
|
||||||
{/* Sidebar Overlay for mobile */}
|
{/* Sidebar Overlay for mobile */}
|
||||||
{isSidebarOpen && (
|
{isSidebarOpen && (
|
||||||
<div
|
<div
|
||||||
@@ -59,46 +76,75 @@ export default function LandlordLayout({ children, title }: LandlordLayoutProps)
|
|||||||
|
|
||||||
{/* Sidebar */}
|
{/* Sidebar */}
|
||||||
<aside className={cn(
|
<aside className={cn(
|
||||||
"fixed left-0 top-0 bottom-0 w-64 bg-slate-900 text-white flex flex-col z-50 transition-transform duration-300 lg:translate-x-0",
|
"fixed left-0 top-0 bottom-0 bg-slate-900 text-white flex flex-col z-50 transition-all duration-300 ease-in-out lg:translate-x-0",
|
||||||
isSidebarOpen ? "translate-x-0" : "-translate-x-full"
|
isSidebarOpen ? "translate-x-0" : "-translate-x-full",
|
||||||
|
isCollapsed ? "lg:w-20" : "lg:w-64"
|
||||||
)}>
|
)}>
|
||||||
<div className="h-16 flex items-center justify-between px-6 border-b border-slate-800">
|
<div className={cn(
|
||||||
|
"h-16 flex items-center border-b border-slate-800 transition-all",
|
||||||
|
isCollapsed ? "justify-center px-0" : "justify-between px-6"
|
||||||
|
)}>
|
||||||
<Link href="/landlord" className="flex items-center gap-2">
|
<Link href="/landlord" className="flex items-center gap-2">
|
||||||
<div className="w-8 h-8 bg-primary-main rounded-lg flex items-center justify-center">
|
<div className="w-8 h-8 bg-primary-main rounded-lg flex items-center justify-center shrink-0">
|
||||||
<Building2 className="w-5 h-5 text-white" />
|
<Building2 className="w-5 h-5 text-white" />
|
||||||
</div>
|
</div>
|
||||||
<span className="font-bold text-lg">中央後台</span>
|
{!isCollapsed && <span className="font-bold text-lg whitespace-nowrap">中央後台</span>}
|
||||||
</Link>
|
</Link>
|
||||||
<button
|
{!isCollapsed && (
|
||||||
className="lg:hidden text-slate-400 hover:text-white"
|
<button
|
||||||
onClick={() => setIsSidebarOpen(false)}
|
className="lg:hidden text-slate-400 hover:text-white"
|
||||||
>
|
onClick={() => setIsSidebarOpen(false)}
|
||||||
<X className="w-6 h-6" />
|
>
|
||||||
</button>
|
<X className="w-6 h-6" />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<nav className="flex-1 p-4 space-y-1 overflow-y-auto">
|
<nav className="flex-1 p-3 space-y-1 overflow-y-auto overflow-x-hidden">
|
||||||
{menuItems.map((item) => (
|
{menuItems.map((item) => (
|
||||||
<Link
|
<Link
|
||||||
key={item.href}
|
key={item.href}
|
||||||
href={item.href}
|
href={item.href}
|
||||||
onClick={() => setIsSidebarOpen(false)}
|
onClick={() => setIsSidebarOpen(false)}
|
||||||
|
title={isCollapsed ? item.label : ""}
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex items-center gap-3 px-3 py-2.5 rounded-lg transition-colors",
|
"flex items-center rounded-lg transition-all",
|
||||||
|
isCollapsed ? "justify-center px-0 h-10 w-10 mx-auto" : "gap-3 px-3 py-2.5",
|
||||||
item.active
|
item.active
|
||||||
? "bg-primary-main text-white"
|
? "bg-primary-main text-white"
|
||||||
: "text-slate-400 hover:bg-slate-800 hover:text-white"
|
: "text-slate-400 hover:bg-slate-800 hover:text-white"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<item.icon className="w-5 h-5" />
|
<item.icon className="w-5 h-5 shrink-0" />
|
||||||
<span className="font-medium">{item.label}</span>
|
{!isCollapsed && <span className="font-medium whitespace-nowrap">{item.label}</span>}
|
||||||
</Link>
|
</Link>
|
||||||
))}
|
))}
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
|
<div className="p-4 border-t border-slate-800 hidden lg:block">
|
||||||
|
<button
|
||||||
|
onClick={() => setIsCollapsed(!isCollapsed)}
|
||||||
|
className={cn(
|
||||||
|
"w-full flex items-center transition-colors text-slate-400 hover:text-white hover:bg-slate-800 rounded-lg p-2",
|
||||||
|
isCollapsed ? "justify-center" : "gap-3"
|
||||||
|
)}
|
||||||
|
title={isCollapsed ? "展開側邊欄" : "收合側邊欄"}
|
||||||
|
>
|
||||||
|
{isCollapsed ? <PanelLeftOpen className="w-5 h-5" /> : (
|
||||||
|
<>
|
||||||
|
<PanelLeftClose className="w-5 h-5" />
|
||||||
|
<span className="text-sm font-medium">收合側邊欄</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
{/* Main Content */}
|
{/* Main Content */}
|
||||||
<main className="flex-1 lg:ml-64 w-full">
|
<main className={cn(
|
||||||
|
"flex-1 flex flex-col transition-all duration-300 min-h-screen",
|
||||||
|
isCollapsed ? "lg:ml-20" : "lg:ml-64"
|
||||||
|
)}>
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<header className="h-16 bg-white border-b border-slate-200 flex items-center justify-between px-4 lg:px-6">
|
<header className="h-16 bg-white border-b border-slate-200 flex items-center justify-between px-4 lg:px-6">
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
|
|||||||
@@ -107,7 +107,7 @@ export default function RoleCreate({ groupedPermissions }: Props) {
|
|||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-2xl font-bold text-grey-0 flex items-center gap-2">
|
<h1 className="text-2xl font-bold text-grey-0 flex items-center gap-2">
|
||||||
<Shield className="h-6 w-6 text-[#01ab83]" />
|
<Shield className="h-6 w-6 text-primary-main" />
|
||||||
建立新角色
|
建立新角色
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-gray-500 mt-1">
|
<p className="text-gray-500 mt-1">
|
||||||
@@ -179,7 +179,7 @@ export default function RoleCreate({ groupedPermissions }: Props) {
|
|||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => toggleGroup(group.permissions)}
|
onClick={() => toggleGroup(group.permissions)}
|
||||||
className="text-xs h-7 text-[#01ab83] hover:text-[#01ab83] hover:bg-[#01ab83]/10"
|
className="text-xs h-7 text-primary-main hover:text-primary-main hover:bg-primary-main/10"
|
||||||
>
|
>
|
||||||
{allGroupSelected ? '取消全選' : '全選'}
|
{allGroupSelected ? '取消全選' : '全選'}
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -114,7 +114,7 @@ export default function RoleEdit({ role, groupedPermissions, currentPermissions
|
|||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-2xl font-bold text-grey-0 flex items-center gap-2">
|
<h1 className="text-2xl font-bold text-grey-0 flex items-center gap-2">
|
||||||
<Shield className="h-6 w-6 text-[#01ab83]" />
|
<Shield className="h-6 w-6 text-primary-main" />
|
||||||
編輯角色
|
編輯角色
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-gray-500 mt-1">
|
<p className="text-gray-500 mt-1">
|
||||||
@@ -193,7 +193,7 @@ export default function RoleEdit({ role, groupedPermissions, currentPermissions
|
|||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => toggleGroup(group.permissions)}
|
onClick={() => toggleGroup(group.permissions)}
|
||||||
className="text-xs h-7 text-[#01ab83] hover:text-[#01ab83] hover:bg-[#01ab83]/10"
|
className="text-xs h-7 text-primary-main hover:text-primary-main hover:bg-primary-main/10"
|
||||||
>
|
>
|
||||||
{allGroupSelected ? '取消全選' : '全選'}
|
{allGroupSelected ? '取消全選' : '全選'}
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ export default function RoleIndex({ roles }: Props) {
|
|||||||
<div className="flex items-center justify-between mb-6">
|
<div className="flex items-center justify-between mb-6">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-2xl font-bold text-grey-0 flex items-center gap-2">
|
<h1 className="text-2xl font-bold text-grey-0 flex items-center gap-2">
|
||||||
<Shield className="h-6 w-6 text-[#01ab83]" />
|
<Shield className="h-6 w-6 text-primary-main" />
|
||||||
角色與權限
|
角色與權限
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-gray-500 mt-1">
|
<p className="text-gray-500 mt-1">
|
||||||
@@ -123,7 +123,7 @@ export default function RoleIndex({ roles }: Props) {
|
|||||||
className={cn(
|
className={cn(
|
||||||
"flex items-center justify-center gap-1 w-full h-full py-2 rounded-md transition-colors",
|
"flex items-center justify-center gap-1 w-full h-full py-2 rounded-md transition-colors",
|
||||||
role.users_count > 0
|
role.users_count > 0
|
||||||
? "text-[#01ab83] hover:bg-[#01ab83]/10 font-bold"
|
? "text-primary-main hover:bg-primary-main/10 font-bold"
|
||||||
: "text-gray-400 cursor-default"
|
: "text-gray-400 cursor-default"
|
||||||
)}
|
)}
|
||||||
title={role.users_count > 0 ? "點擊查看成員名單" : ""}
|
title={role.users_count > 0 ? "點擊查看成員名單" : ""}
|
||||||
@@ -177,7 +177,7 @@ export default function RoleIndex({ roles }: Props) {
|
|||||||
<DialogContent className="sm:max-w-md">
|
<DialogContent className="sm:max-w-md">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle className="flex items-center gap-2">
|
<DialogTitle className="flex items-center gap-2">
|
||||||
<Users className="h-5 w-5 text-[#01ab83]" />
|
<Users className="h-5 w-5 text-primary-main" />
|
||||||
{selectedRole?.display_name} - 成員名單
|
{selectedRole?.display_name} - 成員名單
|
||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
<DialogDescription>
|
<DialogDescription>
|
||||||
@@ -204,7 +204,7 @@ export default function RoleIndex({ roles }: Props) {
|
|||||||
</div>
|
</div>
|
||||||
<Link
|
<Link
|
||||||
href={route('users.edit', user.id)}
|
href={route('users.edit', user.id)}
|
||||||
className="text-xs text-[#01ab83] hover:underline font-medium"
|
className="text-xs text-primary-main hover:underline font-medium"
|
||||||
>
|
>
|
||||||
查看帳號
|
查看帳號
|
||||||
</Link>
|
</Link>
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ export default function UserCreate({ roles }: Props) {
|
|||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-2xl font-bold text-grey-0 flex items-center gap-2">
|
<h1 className="text-2xl font-bold text-grey-0 flex items-center gap-2">
|
||||||
<Users className="h-6 w-6 text-[#01ab83]" />
|
<Users className="h-6 w-6 text-primary-main" />
|
||||||
新增使用者
|
新增使用者
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-gray-500 mt-1">
|
<p className="text-gray-500 mt-1">
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ export default function UserEdit({ user, roles, currentRoles }: Props) {
|
|||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-2xl font-bold text-grey-0 flex items-center gap-2">
|
<h1 className="text-2xl font-bold text-grey-0 flex items-center gap-2">
|
||||||
<Users className="h-6 w-6 text-[#01ab83]" />
|
<Users className="h-6 w-6 text-primary-main" />
|
||||||
編輯使用者
|
編輯使用者
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-gray-500 mt-1">
|
<p className="text-gray-500 mt-1">
|
||||||
|
|||||||
@@ -86,7 +86,7 @@ export default function UserIndex({ users, filters }: Props) {
|
|||||||
<div className="flex items-center justify-between mb-6">
|
<div className="flex items-center justify-between mb-6">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-2xl font-bold text-grey-0 flex items-center gap-2">
|
<h1 className="text-2xl font-bold text-grey-0 flex items-center gap-2">
|
||||||
<Users className="h-6 w-6 text-[#01ab83]" />
|
<Users className="h-6 w-6 text-primary-main" />
|
||||||
使用者管理
|
使用者管理
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-gray-500 mt-1">
|
<p className="text-gray-500 mt-1">
|
||||||
|
|||||||
@@ -40,7 +40,9 @@ export default function Login() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen flex items-center justify-center bg-gray-50 relative overflow-hidden">
|
<div className="min-h-screen flex items-center justify-center bg-gray-50 relative overflow-hidden">
|
||||||
<Head title="登入" />
|
<Head title="登入">
|
||||||
|
<link rel="icon" type="image/png" href="/favicon.png" />
|
||||||
|
</Head>
|
||||||
|
|
||||||
{/* 動態背景裝飾 */}
|
{/* 動態背景裝飾 */}
|
||||||
<div className="absolute top-0 -left-4 w-72 h-72 bg-purple-300 rounded-full mix-blend-multiply filter blur-xl opacity-70 animate-blob"></div>
|
<div className="absolute top-0 -left-4 w-72 h-72 bg-purple-300 rounded-full mix-blend-multiply filter blur-xl opacity-70 animate-blob"></div>
|
||||||
@@ -91,7 +93,7 @@ export default function Login() {
|
|||||||
/>
|
/>
|
||||||
<div className={cn(
|
<div className={cn(
|
||||||
"w-9 h-5 rounded-full shadow-inner transition-colors duration-300 ease-in-out",
|
"w-9 h-5 rounded-full shadow-inner transition-colors duration-300 ease-in-out",
|
||||||
data.rememberUsername ? "bg-[#01ab83]" : "bg-gray-300"
|
data.rememberUsername ? "bg-primary-main" : "bg-gray-300"
|
||||||
)}></div>
|
)}></div>
|
||||||
<div className={cn(
|
<div className={cn(
|
||||||
"absolute left-0.5 top-0.5 w-4 h-4 bg-white rounded-full shadow transition-transform duration-300 ease-in-out",
|
"absolute left-0.5 top-0.5 w-4 h-4 bg-white rounded-full shadow transition-transform duration-300 ease-in-out",
|
||||||
@@ -112,7 +114,7 @@ export default function Login() {
|
|||||||
/>
|
/>
|
||||||
<div className={cn(
|
<div className={cn(
|
||||||
"w-9 h-5 rounded-full shadow-inner transition-colors duration-300 ease-in-out",
|
"w-9 h-5 rounded-full shadow-inner transition-colors duration-300 ease-in-out",
|
||||||
data.remember ? "bg-[#01ab83]" : "bg-gray-300"
|
data.remember ? "bg-primary-main" : "bg-gray-300"
|
||||||
)}></div>
|
)}></div>
|
||||||
<div className={cn(
|
<div className={cn(
|
||||||
"absolute left-0.5 top-0.5 w-4 h-4 bg-white rounded-full shadow transition-transform duration-300 ease-in-out",
|
"absolute left-0.5 top-0.5 w-4 h-4 bg-white rounded-full shadow transition-transform duration-300 ease-in-out",
|
||||||
@@ -123,7 +125,7 @@ export default function Login() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
className="w-full h-11 text-base bg-[#01ab83] hover:bg-[#018a6a] transition-all shadow-lg hover:shadow-xl"
|
className="w-full h-11 text-base bg-primary-main hover:bg-primary-dark transition-all shadow-lg hover:shadow-xl"
|
||||||
disabled={processing}
|
disabled={processing}
|
||||||
>
|
>
|
||||||
{processing ? "登入中..." : "登入系統"}
|
{processing ? "登入中..." : "登入系統"}
|
||||||
|
|||||||
@@ -31,9 +31,9 @@ export default function Dashboard({ stats }: Props) {
|
|||||||
{
|
{
|
||||||
label: '商品總數',
|
label: '商品總數',
|
||||||
value: stats.productsCount,
|
value: stats.productsCount,
|
||||||
icon: <Package className="h-6 w-6 text-[#01ab83]" />,
|
icon: <Package className="h-6 w-6 text-primary-main" />,
|
||||||
description: '目前系統中的商品種類',
|
description: '目前系統中的商品種類',
|
||||||
color: 'bg-[#01ab83]/10',
|
color: 'bg-primary-main/10',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: '合作廠商',
|
label: '合作廠商',
|
||||||
@@ -80,7 +80,7 @@ export default function Dashboard({ stats }: Props) {
|
|||||||
<div className="p-8 max-w-7xl mx-auto">
|
<div className="p-8 max-w-7xl mx-auto">
|
||||||
<div className="mb-8">
|
<div className="mb-8">
|
||||||
<h1 className="text-2xl font-bold text-grey-0 flex items-center gap-2">
|
<h1 className="text-2xl font-bold text-grey-0 flex items-center gap-2">
|
||||||
<TrendingUp className="h-6 w-6 text-[#01ab83]" />
|
<TrendingUp className="h-6 w-6 text-primary-main" />
|
||||||
系統總覽
|
系統總覽
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-gray-500 mt-1">歡迎回來,這是您的小小冰室 ERP 營運數據概況。</p>
|
<p className="text-gray-500 mt-1">歡迎回來,這是您的小小冰室 ERP 營運數據概況。</p>
|
||||||
@@ -113,7 +113,7 @@ export default function Dashboard({ stats }: Props) {
|
|||||||
{/* 警示與通知 */}
|
{/* 警示與通知 */}
|
||||||
<div className="lg:col-span-1 space-y-6">
|
<div className="lg:col-span-1 space-y-6">
|
||||||
<h2 className="text-xl font-bold text-grey-0 flex items-center gap-2">
|
<h2 className="text-xl font-bold text-grey-0 flex items-center gap-2">
|
||||||
<TrendingUp className="h-5 w-5 text-[#01ab83]" />
|
<TrendingUp className="h-5 w-5 text-primary-main" />
|
||||||
即時動態
|
即時動態
|
||||||
</h2>
|
</h2>
|
||||||
<div className="bg-white rounded-2xl border border-grey-4 shadow-sm divide-y divide-grey-4">
|
<div className="bg-white rounded-2xl border border-grey-4 shadow-sm divide-y divide-grey-4">
|
||||||
@@ -159,12 +159,12 @@ export default function Dashboard({ stats }: Props) {
|
|||||||
<h2 className="text-xl font-bold text-grey-0">快速操作</h2>
|
<h2 className="text-xl font-bold text-grey-0">快速操作</h2>
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
<Link href="/products" className="group h-full">
|
<Link href="/products" className="group h-full">
|
||||||
<div className="bg-white p-6 rounded-2xl border border-grey-4 shadow-sm hover:border-[#01ab83] transition-all h-full flex flex-col justify-between">
|
<div className="bg-white p-6 rounded-2xl border border-grey-4 shadow-sm hover:border-primary-main transition-all h-full flex flex-col justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h3 className="font-bold text-grey-0 mb-1 group-hover:text-[#01ab83]">商品管理</h3>
|
<h3 className="font-bold text-grey-0 mb-1 group-hover:text-primary-main">商品管理</h3>
|
||||||
<p className="text-sm text-grey-2">查看並編輯所有商品資料與單位換算。</p>
|
<p className="text-sm text-grey-2">查看並編輯所有商品資料與單位換算。</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-4 flex items-center text-xs font-bold text-[#01ab83] group-hover:gap-2 transition-all">
|
<div className="mt-4 flex items-center text-xs font-bold text-primary-main group-hover:gap-2 transition-all">
|
||||||
即刻前往 <ChevronRight className="h-3 w-3" />
|
即刻前往 <ChevronRight className="h-3 w-3" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -27,17 +27,20 @@ export default function LandlordLogin() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen flex items-center justify-center bg-slate-900 relative overflow-hidden">
|
<div className="min-h-screen flex items-center justify-center bg-slate-900 relative overflow-hidden">
|
||||||
<Head title="總後台登入" />
|
<Head title="總後台登入">
|
||||||
|
<link rel="icon" type="image/png" href="/favicon-landlord.png" />
|
||||||
|
<link rel="shortcut icon" href="/favicon-landlord.png" type="image/png" />
|
||||||
|
<link rel="apple-touch-icon" href="/favicon-landlord.png" />
|
||||||
|
</Head>
|
||||||
|
|
||||||
{/* 深色背景裝飾 */}
|
{/* 深色背景裝飾 */}
|
||||||
<div className="absolute top-0 -left-4 w-96 h-96 bg-indigo-900/30 rounded-full mix-blend-screen filter blur-3xl opacity-50 animate-blob"></div>
|
<div className="absolute top-0 -left-4 w-96 h-96 bg-indigo-900/30 rounded-full mix-blend-screen filter blur-3xl opacity-50 animate-blob"></div>
|
||||||
<div className="absolute bottom-0 -right-4 w-96 h-96 bg-blue-900/30 rounded-full mix-blend-screen filter blur-3xl opacity-50 animate-blob animation-delay-2000"></div>
|
<div className="absolute bottom-0 -right-4 w-96 h-96 bg-blue-900/30 rounded-full mix-blend-screen filter blur-3xl opacity-50 animate-blob animation-delay-2000"></div>
|
||||||
|
|
||||||
<div className="w-full max-w-md p-8 relative z-10">
|
<div className="w-full max-w-md p-8 relative z-10">
|
||||||
<div className="flex flex-col items-center mb-8">
|
<div className="flex flex-col items-center mb-12">
|
||||||
{/* 使用不同風格的 Logo 或純文字 */}
|
<div className="text-white text-5xl font-bold tracking-wider mb-3">Star ERP</div>
|
||||||
<div className="text-white text-3xl font-bold tracking-wider mb-2">Star ERP</div>
|
<div className="text-slate-400 text-sm tracking-widest uppercase font-semibold">中央管理系統</div>
|
||||||
<div className="text-slate-400 text-sm tracking-widest uppercase">中央管理系統</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="glass-panel p-8 rounded-xl shadow-2xl bg-slate-800/50 backdrop-blur-md border border-slate-700/50">
|
<div className="glass-panel p-8 rounded-xl shadow-2xl bg-slate-800/50 backdrop-blur-md border border-slate-700/50">
|
||||||
|
|||||||
196
resources/js/Pages/Landlord/Tenant/Branding.tsx
Normal file
196
resources/js/Pages/Landlord/Tenant/Branding.tsx
Normal file
@@ -0,0 +1,196 @@
|
|||||||
|
import LandlordLayout from "@/Layouts/LandlordLayout";
|
||||||
|
import { Link, useForm } from "@inertiajs/react";
|
||||||
|
import { Button } from "@/Components/ui/button";
|
||||||
|
import { Input } from "@/Components/ui/input";
|
||||||
|
import { Label } from "@/Components/ui/label";
|
||||||
|
import { Card } from "@/Components/ui/card";
|
||||||
|
import { ArrowLeft, LayoutDashboard } from "lucide-react";
|
||||||
|
import { FormEventHandler, useState } from "react";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import { generateLightestColor } from "@/utils/colorUtils";
|
||||||
|
|
||||||
|
interface Tenant {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
branding?: {
|
||||||
|
logo_path?: string;
|
||||||
|
primary_color?: string;
|
||||||
|
text_color?: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface BrandingProps {
|
||||||
|
tenant: Tenant;
|
||||||
|
logo_url?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Branding({ tenant, logo_url }: BrandingProps) {
|
||||||
|
const { data, setData, post, processing, errors } = useForm({
|
||||||
|
logo: null as File | null,
|
||||||
|
primary_color: tenant.branding?.primary_color || '#01ab83',
|
||||||
|
});
|
||||||
|
|
||||||
|
const [logoPreview, setLogoPreview] = useState<string | null>(logo_url || null);
|
||||||
|
|
||||||
|
const handleLogoChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const file = e.target.files?.[0];
|
||||||
|
if (file) {
|
||||||
|
setData('logo', file);
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onloadend = () => {
|
||||||
|
setLogoPreview(reader.result as string);
|
||||||
|
};
|
||||||
|
reader.readAsDataURL(file);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const submit: FormEventHandler = (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
post(route('landlord.tenants.branding.update', tenant.id), {
|
||||||
|
onSuccess: () => {
|
||||||
|
toast.success('樣式設定已更新');
|
||||||
|
},
|
||||||
|
onError: () => {
|
||||||
|
toast.error('更新失敗,請檢查輸入');
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<LandlordLayout title="客戶樣式管理">
|
||||||
|
<div className="space-y-6 p-6">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<Link href={route('landlord.tenants.show', tenant.id)}>
|
||||||
|
<Button variant="outline" size="sm">
|
||||||
|
<ArrowLeft className="w-4 h-4 mr-2" />
|
||||||
|
返回
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
<div>
|
||||||
|
<h2 className="text-2xl font-bold text-slate-900">
|
||||||
|
客戶樣式管理
|
||||||
|
</h2>
|
||||||
|
<p className="text-sm text-slate-500 mt-1">
|
||||||
|
{tenant.name}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Branding Form */}
|
||||||
|
<Card className="p-6">
|
||||||
|
<form onSubmit={submit} className="space-y-6">
|
||||||
|
{/* Logo Upload */}
|
||||||
|
<div>
|
||||||
|
<Label htmlFor="logo" className="text-base font-semibold">
|
||||||
|
客戶 Logo
|
||||||
|
</Label>
|
||||||
|
<p className="text-sm text-slate-500 mb-3">
|
||||||
|
上傳客戶專屬 Logo,建議尺寸 200×200 px,最大 2MB
|
||||||
|
</p>
|
||||||
|
<Input
|
||||||
|
id="logo"
|
||||||
|
type="file"
|
||||||
|
accept="image/*"
|
||||||
|
onChange={handleLogoChange}
|
||||||
|
className="cursor-pointer"
|
||||||
|
/>
|
||||||
|
{errors.logo && (
|
||||||
|
<p className="text-sm text-red-600 mt-1">{errors.logo}</p>
|
||||||
|
)}
|
||||||
|
{logoPreview && (
|
||||||
|
<div className="mt-4 p-4 bg-slate-50 rounded-lg border border-slate-200">
|
||||||
|
<p className="text-sm text-slate-600 mb-2">預覽:</p>
|
||||||
|
<img
|
||||||
|
src={logoPreview}
|
||||||
|
alt="Logo Preview"
|
||||||
|
className="h-20 w-20 object-contain rounded-lg border border-slate-300"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Primary Color */}
|
||||||
|
<div>
|
||||||
|
<Label htmlFor="primary_color" className="text-base font-semibold">
|
||||||
|
主色系
|
||||||
|
</Label>
|
||||||
|
<p className="text-sm text-slate-500 mb-3">
|
||||||
|
設定客戶後台的主要品牌色(按鈕、連結等)
|
||||||
|
</p>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<Input
|
||||||
|
id="primary_color"
|
||||||
|
type="color"
|
||||||
|
value={data.primary_color}
|
||||||
|
onChange={(e) => setData('primary_color', e.target.value)}
|
||||||
|
className="w-16 h-10 cursor-pointer"
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
value={data.primary_color}
|
||||||
|
onChange={(e) => setData('primary_color', e.target.value)}
|
||||||
|
pattern="^#[0-9A-Fa-f]{6}$"
|
||||||
|
className="w-32"
|
||||||
|
placeholder="#01ab83"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{errors.primary_color && (
|
||||||
|
<p className="text-sm text-red-600 mt-1">{errors.primary_color}</p>
|
||||||
|
)}
|
||||||
|
{/* Preview */}
|
||||||
|
<div className="mt-4 p-4 bg-slate-50 rounded-lg border border-slate-200">
|
||||||
|
<p className="text-sm text-slate-600 mb-3">預覽效果:</p>
|
||||||
|
<div className="flex flex-wrap gap-4">
|
||||||
|
{/* 按鈕預覽 */}
|
||||||
|
<div>
|
||||||
|
<p className="text-xs text-slate-500 mb-2">按鈕</p>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
style={{ backgroundColor: data.primary_color }}
|
||||||
|
className="text-white"
|
||||||
|
>
|
||||||
|
預覽按鈕
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
{/* 側邊欄選中狀態預覽 */}
|
||||||
|
<div>
|
||||||
|
<p className="text-xs text-slate-500 mb-2">側邊欄選中狀態</p>
|
||||||
|
<div
|
||||||
|
className="flex items-center gap-2 px-3 py-2 rounded-lg"
|
||||||
|
style={{
|
||||||
|
backgroundColor: generateLightestColor(data.primary_color),
|
||||||
|
color: data.primary_color
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<LayoutDashboard className="w-5 h-5" />
|
||||||
|
<span className="font-medium">儀表板</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Submit Button */}
|
||||||
|
<div className="flex justify-end gap-3 pt-4 border-t">
|
||||||
|
<Link href={route('landlord.tenants.show', tenant.id)}>
|
||||||
|
<Button type="button" variant="outline">
|
||||||
|
取消
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
disabled={processing}
|
||||||
|
className="bg-primary-main hover:bg-primary-dark"
|
||||||
|
>
|
||||||
|
{processing ? '儲存中...' : '儲存樣式設定'}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</LandlordLayout>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -64,12 +64,20 @@ export default function TenantShow({ tenant }: Props) {
|
|||||||
<h1 className="text-2xl font-bold text-slate-900">{tenant.name}</h1>
|
<h1 className="text-2xl font-bold text-slate-900">{tenant.name}</h1>
|
||||||
<p className="text-slate-500 mt-1">客戶 ID: {tenant.id}</p>
|
<p className="text-slate-500 mt-1">客戶 ID: {tenant.id}</p>
|
||||||
</div>
|
</div>
|
||||||
<Link
|
<div className="flex gap-2">
|
||||||
href={`/landlord/tenants/${tenant.id}/edit`}
|
<Link
|
||||||
className="bg-slate-100 hover:bg-slate-200 text-slate-700 px-4 py-2 rounded-lg transition-colors"
|
href={`/landlord/tenants/${tenant.id}/branding`}
|
||||||
>
|
className="bg-primary-main hover:bg-primary-dark text-white px-4 py-2 rounded-lg transition-colors"
|
||||||
編輯
|
>
|
||||||
</Link>
|
樣式管理
|
||||||
|
</Link>
|
||||||
|
<Link
|
||||||
|
href={`/landlord/tenants/${tenant.id}/edit`}
|
||||||
|
className="bg-slate-100 hover:bg-slate-200 text-slate-700 px-4 py-2 rounded-lg transition-colors"
|
||||||
|
>
|
||||||
|
編輯
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Info Card */}
|
{/* Info Card */}
|
||||||
|
|||||||
@@ -178,7 +178,7 @@ export default function ProductManagement({ products, categories, units, filters
|
|||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="mb-6">
|
<div className="mb-6">
|
||||||
<h1 className="text-2xl font-bold text-grey-0 flex items-center gap-2">
|
<h1 className="text-2xl font-bold text-grey-0 flex items-center gap-2">
|
||||||
<Package className="h-6 w-6 text-[#01ab83]" />
|
<Package className="h-6 w-6 text-primary-main" />
|
||||||
商品資料管理
|
商品資料管理
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-gray-500 mt-1">管理小小冰室原物料與成品資料</p>
|
<p className="text-gray-500 mt-1">管理小小冰室原物料與成品資料</p>
|
||||||
|
|||||||
@@ -174,7 +174,7 @@ export default function CreatePurchaseOrder({
|
|||||||
|
|
||||||
<div className="mb-6">
|
<div className="mb-6">
|
||||||
<h1 className="text-2xl font-bold text-grey-0 flex items-center gap-2">
|
<h1 className="text-2xl font-bold text-grey-0 flex items-center gap-2">
|
||||||
<ShoppingCart className="h-6 w-6 text-[#01ab83]" />
|
<ShoppingCart className="h-6 w-6 text-primary-main" />
|
||||||
{order ? "編輯採購單" : "建立採購單"}
|
{order ? "編輯採購單" : "建立採購單"}
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-gray-500 mt-1">
|
<p className="text-gray-500 mt-1">
|
||||||
|
|||||||
@@ -109,7 +109,7 @@ export default function PurchaseOrderIndex({ orders, filters, warehouses }: Prop
|
|||||||
<div className="flex items-center justify-between mb-6">
|
<div className="flex items-center justify-between mb-6">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-2xl font-bold text-grey-0 flex items-center gap-2">
|
<h1 className="text-2xl font-bold text-grey-0 flex items-center gap-2">
|
||||||
<ShoppingCart className="h-6 w-6 text-[#01ab83]" />
|
<ShoppingCart className="h-6 w-6 text-primary-main" />
|
||||||
採購單管理
|
採購單管理
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-gray-500 mt-1">
|
<p className="text-gray-500 mt-1">
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ export default function ViewPurchaseOrderPage({ order }: Props) {
|
|||||||
<div className="flex items-center justify-between mb-6">
|
<div className="flex items-center justify-between mb-6">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-2xl font-bold text-grey-0 flex items-center gap-2">
|
<h1 className="text-2xl font-bold text-grey-0 flex items-center gap-2">
|
||||||
<ShoppingCart className="h-6 w-6 text-[#01ab83]" />
|
<ShoppingCart className="h-6 w-6 text-primary-main" />
|
||||||
查看採購單
|
查看採購單
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-gray-500 mt-1">單號:{order.poNumber}</p>
|
<p className="text-gray-500 mt-1">單號:{order.poNumber}</p>
|
||||||
|
|||||||
2
resources/js/Pages/Vendor/Index.tsx
vendored
2
resources/js/Pages/Vendor/Index.tsx
vendored
@@ -146,7 +146,7 @@ export default function VendorManagement({ vendors, filters }: PageProps) {
|
|||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="mb-6">
|
<div className="mb-6">
|
||||||
<h1 className="text-2xl font-bold text-grey-0 flex items-center gap-2">
|
<h1 className="text-2xl font-bold text-grey-0 flex items-center gap-2">
|
||||||
<Contact2 className="h-6 w-6 text-[#01ab83]" />
|
<Contact2 className="h-6 w-6 text-primary-main" />
|
||||||
廠商資料管理
|
廠商資料管理
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-gray-500 mt-1">管理 ERP 系統供應商與聯絡資訊</p>
|
<p className="text-gray-500 mt-1">管理 ERP 系統供應商與聯絡資訊</p>
|
||||||
|
|||||||
2
resources/js/Pages/Vendor/Show.tsx
vendored
2
resources/js/Pages/Vendor/Show.tsx
vendored
@@ -143,7 +143,7 @@ export default function VendorShow({ vendor, products }: ShowProps) {
|
|||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-2xl font-bold text-grey-0 flex items-center gap-2">
|
<h1 className="text-2xl font-bold text-grey-0 flex items-center gap-2">
|
||||||
<Contact2 className="h-6 w-6 text-[#01ab83]" />
|
<Contact2 className="h-6 w-6 text-primary-main" />
|
||||||
廠商詳細資訊
|
廠商詳細資訊
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-gray-500 mt-1">查看並管理供應商的詳細資料與供貨商品</p>
|
<p className="text-gray-500 mt-1">查看並管理供應商的詳細資料與供貨商品</p>
|
||||||
|
|||||||
@@ -184,7 +184,7 @@ export default function AddInventoryPage({ warehouse, products }: Props) {
|
|||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-2xl font-bold text-grey-0 flex items-center gap-2">
|
<h1 className="text-2xl font-bold text-grey-0 flex items-center gap-2">
|
||||||
<Boxes className="h-6 w-6 text-[#01ab83]" />
|
<Boxes className="h-6 w-6 text-primary-main" />
|
||||||
新增庫存(手動入庫)
|
新增庫存(手動入庫)
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-gray-500 mt-1">
|
<p className="text-gray-500 mt-1">
|
||||||
|
|||||||
@@ -91,7 +91,7 @@ export default function EditInventory({ warehouse, inventory, transactions = []
|
|||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-2xl font-bold text-grey-0 flex items-center gap-2">
|
<h1 className="text-2xl font-bold text-grey-0 flex items-center gap-2">
|
||||||
<Boxes className="h-6 w-6 text-[#01ab83]" />
|
<Boxes className="h-6 w-6 text-primary-main" />
|
||||||
編輯庫存品項
|
編輯庫存品項
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-gray-500 mt-1">
|
<p className="text-gray-500 mt-1">
|
||||||
|
|||||||
@@ -111,7 +111,7 @@ export default function WarehouseIndex({ warehouses, filters }: PageProps) {
|
|||||||
{/* 頁面標題 */}
|
{/* 頁面標題 */}
|
||||||
<div className="mb-6">
|
<div className="mb-6">
|
||||||
<h1 className="text-2xl font-bold text-grey-0 flex items-center gap-2">
|
<h1 className="text-2xl font-bold text-grey-0 flex items-center gap-2">
|
||||||
<WarehouseIcon className="h-6 w-6 text-[#01ab83]" />
|
<WarehouseIcon className="h-6 w-6 text-primary-main" />
|
||||||
倉庫管理
|
倉庫管理
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-gray-500 mt-1">
|
<p className="text-gray-500 mt-1">
|
||||||
|
|||||||
@@ -109,7 +109,7 @@ export default function WarehouseInventoryPage({
|
|||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-2xl font-bold text-grey-0 flex items-center gap-2">
|
<h1 className="text-2xl font-bold text-grey-0 flex items-center gap-2">
|
||||||
<Boxes className="h-6 w-6 text-[#01ab83]" />
|
<Boxes className="h-6 w-6 text-primary-main" />
|
||||||
庫存管理 - {warehouse.name}
|
庫存管理 - {warehouse.name}
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-gray-500 mt-1">查看並管理此倉庫內的商品庫存數量與批號資訊</p>
|
<p className="text-gray-500 mt-1">查看並管理此倉庫內的商品庫存數量與批號資訊</p>
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ export default function InventoryHistory({ warehouse, inventory, transactions }:
|
|||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-2xl font-bold text-grey-0 flex items-center gap-2">
|
<h1 className="text-2xl font-bold text-grey-0 flex items-center gap-2">
|
||||||
<History className="h-6 w-6 text-[#01ab83]" />
|
<History className="h-6 w-6 text-primary-main" />
|
||||||
庫存異動紀錄
|
庫存異動紀錄
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-gray-500 mt-1">
|
<p className="text-gray-500 mt-1">
|
||||||
|
|||||||
@@ -120,7 +120,7 @@ export default function SafetyStockPage({
|
|||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-2xl font-bold text-grey-0 flex items-center gap-2">
|
<h1 className="text-2xl font-bold text-grey-0 flex items-center gap-2">
|
||||||
<Shield className="h-6 w-6 text-[#01ab83]" />
|
<Shield className="h-6 w-6 text-primary-main" />
|
||||||
安全庫存設定 - {warehouse.name}
|
安全庫存設定 - {warehouse.name}
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-gray-500 mt-1">
|
<p className="text-gray-500 mt-1">
|
||||||
|
|||||||
7
resources/js/types/global.d.ts
vendored
7
resources/js/types/global.d.ts
vendored
@@ -11,6 +11,12 @@ export interface AuthUser {
|
|||||||
permissions: string[];
|
permissions: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface Branding {
|
||||||
|
logo_url?: string | null;
|
||||||
|
primary_color?: string;
|
||||||
|
text_color?: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface PageProps {
|
export interface PageProps {
|
||||||
auth: {
|
auth: {
|
||||||
user: AuthUser | null;
|
user: AuthUser | null;
|
||||||
@@ -19,6 +25,7 @@ export interface PageProps {
|
|||||||
success?: string;
|
success?: string;
|
||||||
error?: string;
|
error?: string;
|
||||||
};
|
};
|
||||||
|
branding?: Branding | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
|
|||||||
129
resources/js/utils/colorUtils.ts
Normal file
129
resources/js/utils/colorUtils.ts
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
/**
|
||||||
|
* 調整顏色亮度
|
||||||
|
* @param hex 原始顏色的 HEX 值(例如 "#01ab83")
|
||||||
|
* @param percent 調整百分比,正值變亮,負值變暗
|
||||||
|
* @returns 調整後的 HEX 色碼
|
||||||
|
*/
|
||||||
|
export function adjustBrightness(hex: string, percent: number): string {
|
||||||
|
// 移除 # 符號
|
||||||
|
const cleanHex = hex.replace('#', '');
|
||||||
|
|
||||||
|
// 解析 RGB
|
||||||
|
const num = parseInt(cleanHex, 16);
|
||||||
|
const r = (num >> 16) & 0xFF;
|
||||||
|
const g = (num >> 8) & 0xFF;
|
||||||
|
const b = num & 0xFF;
|
||||||
|
|
||||||
|
// 計算調整後的 RGB(確保在 0-255 範圍內)
|
||||||
|
const newR = Math.min(255, Math.max(0, r + percent * 2.55));
|
||||||
|
const newG = Math.min(255, Math.max(0, g + percent * 2.55));
|
||||||
|
const newB = Math.min(255, Math.max(0, b + percent * 2.55));
|
||||||
|
|
||||||
|
// 轉回 HEX
|
||||||
|
const result = ((Math.round(newR) << 16) | (Math.round(newG) << 8) | Math.round(newB))
|
||||||
|
.toString(16)
|
||||||
|
.padStart(6, '0');
|
||||||
|
|
||||||
|
return `#${result}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成非常淺的背景色(類似原本的 primary-lightest: #e6f7f3)
|
||||||
|
* 這個函式會保留原色的色調,但大幅提高亮度和降低飽和度
|
||||||
|
* @param hex 原始顏色的 HEX 值
|
||||||
|
* @returns 淺色背景的 HEX 色碼
|
||||||
|
*/
|
||||||
|
export function generateLightestColor(hex: string): string {
|
||||||
|
const cleanHex = hex.replace('#', '');
|
||||||
|
const num = parseInt(cleanHex, 16);
|
||||||
|
const r = (num >> 16) & 0xFF;
|
||||||
|
const g = (num >> 8) & 0xFF;
|
||||||
|
const b = num & 0xFF;
|
||||||
|
|
||||||
|
// 混合白色來創造非常淺的色調(90% 白色 + 10% 原色)
|
||||||
|
const mixRatio = 0.1;
|
||||||
|
const newR = Math.round(255 * (1 - mixRatio) + r * mixRatio);
|
||||||
|
const newG = Math.round(255 * (1 - mixRatio) + g * mixRatio);
|
||||||
|
const newB = Math.round(255 * (1 - mixRatio) + b * mixRatio);
|
||||||
|
|
||||||
|
const result = ((newR << 16) | (newG << 8) | newB)
|
||||||
|
.toString(16)
|
||||||
|
.padStart(6, '0');
|
||||||
|
|
||||||
|
return `#${result}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成淺色(類似原本的 primary-light: #33bc9a)
|
||||||
|
* @param hex 原始顏色的 HEX 值
|
||||||
|
* @returns 淺色的 HEX 色碼
|
||||||
|
*/
|
||||||
|
export function generateLightColor(hex: string): string {
|
||||||
|
const cleanHex = hex.replace('#', '');
|
||||||
|
const num = parseInt(cleanHex, 16);
|
||||||
|
const r = (num >> 16) & 0xFF;
|
||||||
|
const g = (num >> 8) & 0xFF;
|
||||||
|
const b = num & 0xFF;
|
||||||
|
|
||||||
|
// 混合白色(70% 原色 + 30% 白色)
|
||||||
|
const mixRatio = 0.7;
|
||||||
|
const newR = Math.round(r * mixRatio + 255 * (1 - mixRatio));
|
||||||
|
const newG = Math.round(g * mixRatio + 255 * (1 - mixRatio));
|
||||||
|
const newB = Math.round(b * mixRatio + 255 * (1 - mixRatio));
|
||||||
|
|
||||||
|
const result = ((newR << 16) | (newG << 8) | newB)
|
||||||
|
.toString(16)
|
||||||
|
.padStart(6, '0');
|
||||||
|
|
||||||
|
return `#${result}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成深色(類似原本的 primary-dark: #018a6a)
|
||||||
|
* @param hex 原始顏色的 HEX 值
|
||||||
|
* @returns 深色的 HEX 色碼
|
||||||
|
*/
|
||||||
|
export function generateDarkColor(hex: string): string {
|
||||||
|
const cleanHex = hex.replace('#', '');
|
||||||
|
const num = parseInt(cleanHex, 16);
|
||||||
|
const r = (num >> 16) & 0xFF;
|
||||||
|
const g = (num >> 8) & 0xFF;
|
||||||
|
const b = num & 0xFF;
|
||||||
|
|
||||||
|
// 降低亮度(80% 原色)
|
||||||
|
const factor = 0.8;
|
||||||
|
const newR = Math.round(r * factor);
|
||||||
|
const newG = Math.round(g * factor);
|
||||||
|
const newB = Math.round(b * factor);
|
||||||
|
|
||||||
|
const result = ((newR << 16) | (newG << 8) | newB)
|
||||||
|
.toString(16)
|
||||||
|
.padStart(6, '0');
|
||||||
|
|
||||||
|
return `#${result}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成按鈕 active 狀態的顏色(比 dark 更深,約 60% 原色)
|
||||||
|
* @param hex 原始顏色的 HEX 值
|
||||||
|
* @returns 更深色的 HEX 色碼
|
||||||
|
*/
|
||||||
|
export function generateActiveColor(hex: string): string {
|
||||||
|
const cleanHex = hex.replace('#', '');
|
||||||
|
const num = parseInt(cleanHex, 16);
|
||||||
|
const r = (num >> 16) & 0xFF;
|
||||||
|
const g = (num >> 8) & 0xFF;
|
||||||
|
const b = num & 0xFF;
|
||||||
|
|
||||||
|
// 降低亮度(60% 原色)
|
||||||
|
const factor = 0.6;
|
||||||
|
const newR = Math.round(r * factor);
|
||||||
|
const newG = Math.round(g * factor);
|
||||||
|
const newB = Math.round(b * factor);
|
||||||
|
|
||||||
|
const result = ((newR << 16) | (newG << 8) | newB)
|
||||||
|
.toString(16)
|
||||||
|
.padStart(6, '0');
|
||||||
|
|
||||||
|
return `#${result}`;
|
||||||
|
}
|
||||||
@@ -6,9 +6,6 @@
|
|||||||
|
|
||||||
<title inertia>{{ config('app.name', 'Laravel') }}</title>
|
<title inertia>{{ config('app.name', 'Laravel') }}</title>
|
||||||
|
|
||||||
<link rel="icon" href="/favicon.ico">
|
|
||||||
<link rel="icon" type="image/png" href="/favicon.png">
|
|
||||||
|
|
||||||
<!-- Fonts -->
|
<!-- Fonts -->
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
|
|||||||
@@ -30,4 +30,8 @@ Route::prefix('landlord')->name('landlord.')->middleware(['web', 'auth', \App\Ht
|
|||||||
// 租戶域名管理
|
// 租戶域名管理
|
||||||
Route::post('tenants/{tenant}/domains', [TenantController::class, 'addDomain'])->name('tenants.domains.store');
|
Route::post('tenants/{tenant}/domains', [TenantController::class, 'addDomain'])->name('tenants.domains.store');
|
||||||
Route::delete('tenants/{tenant}/domains/{domain}', [TenantController::class, 'removeDomain'])->name('tenants.domains.destroy');
|
Route::delete('tenants/{tenant}/domains/{domain}', [TenantController::class, 'removeDomain'])->name('tenants.domains.destroy');
|
||||||
|
|
||||||
|
// 租戶樣式管理
|
||||||
|
Route::get('tenants/{tenant}/branding', [TenantController::class, 'showBranding'])->name('tenants.branding');
|
||||||
|
Route::post('tenants/{tenant}/branding', [TenantController::class, 'updateBranding'])->name('tenants.branding.update');
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user