feat: 新增租戶品牌客製化系統(Logo、主色系)、修正 hardcoded 顏色為 CSS 變數
All checks were successful
Koori-ERP-Deploy-System / deploy-demo (push) Successful in 47s
Koori-ERP-Deploy-System / deploy-production (push) Has been skipped

This commit is contained in:
2026-01-16 14:36:24 +08:00
parent a2c99e3a36
commit 55272d5d43
36 changed files with 568 additions and 70 deletions

View File

@@ -107,7 +107,7 @@ export default function RoleCreate({ groupedPermissions }: Props) {
<div className="flex items-center justify-between">
<div>
<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>
<p className="text-gray-500 mt-1">
@@ -179,7 +179,7 @@ export default function RoleCreate({ groupedPermissions }: Props) {
variant="ghost"
size="sm"
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 ? '取消全選' : '全選'}
</Button>

View File

@@ -114,7 +114,7 @@ export default function RoleEdit({ role, groupedPermissions, currentPermissions
<div className="flex items-center justify-between">
<div>
<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>
<p className="text-gray-500 mt-1">
@@ -193,7 +193,7 @@ export default function RoleEdit({ role, groupedPermissions, currentPermissions
variant="ghost"
size="sm"
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 ? '取消全選' : '全選'}
</Button>

View File

@@ -68,7 +68,7 @@ export default function RoleIndex({ roles }: Props) {
<div className="flex items-center justify-between mb-6">
<div>
<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>
<p className="text-gray-500 mt-1">
@@ -123,7 +123,7 @@ export default function RoleIndex({ roles }: Props) {
className={cn(
"flex items-center justify-center gap-1 w-full h-full py-2 rounded-md transition-colors",
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"
)}
title={role.users_count > 0 ? "點擊查看成員名單" : ""}
@@ -177,7 +177,7 @@ export default function RoleIndex({ roles }: Props) {
<DialogContent className="sm:max-w-md">
<DialogHeader>
<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} -
</DialogTitle>
<DialogDescription>
@@ -204,7 +204,7 @@ export default function RoleIndex({ roles }: Props) {
</div>
<Link
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>

View File

@@ -55,7 +55,7 @@ export default function UserCreate({ roles }: Props) {
<div className="flex items-center justify-between">
<div>
<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>
<p className="text-gray-500 mt-1">

View File

@@ -69,7 +69,7 @@ export default function UserEdit({ user, roles, currentRoles }: Props) {
<div className="flex items-center justify-between">
<div>
<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>
<p className="text-gray-500 mt-1">

View File

@@ -86,7 +86,7 @@ export default function UserIndex({ users, filters }: Props) {
<div className="flex items-center justify-between mb-6">
<div>
<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>
<p className="text-gray-500 mt-1">

View File

@@ -40,7 +40,9 @@ export default function Login() {
return (
<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>
@@ -91,7 +93,7 @@ export default function Login() {
/>
<div className={cn(
"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 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",
@@ -112,7 +114,7 @@ export default function Login() {
/>
<div className={cn(
"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 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",
@@ -123,7 +125,7 @@ export default function Login() {
</div>
<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}
>
{processing ? "登入中..." : "登入系統"}

View File

@@ -31,9 +31,9 @@ export default function Dashboard({ stats }: Props) {
{
label: '商品總數',
value: stats.productsCount,
icon: <Package className="h-6 w-6 text-[#01ab83]" />,
icon: <Package className="h-6 w-6 text-primary-main" />,
description: '目前系統中的商品種類',
color: 'bg-[#01ab83]/10',
color: 'bg-primary-main/10',
},
{
label: '合作廠商',
@@ -80,7 +80,7 @@ export default function Dashboard({ stats }: Props) {
<div className="p-8 max-w-7xl mx-auto">
<div className="mb-8">
<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>
<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">
<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>
<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>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<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>
<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>
</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" />
</div>
</div>

View File

@@ -27,17 +27,20 @@ export default function LandlordLogin() {
return (
<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 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="flex flex-col items-center mb-8">
{/* 使用不同風格的 Logo 或純文字 */}
<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"></div>
<div className="flex flex-col items-center mb-12">
<div className="text-white text-5xl font-bold tracking-wider mb-3">Star ERP</div>
<div className="text-slate-400 text-sm tracking-widest uppercase font-semibold"></div>
</div>
<div className="glass-panel p-8 rounded-xl shadow-2xl bg-slate-800/50 backdrop-blur-md border border-slate-700/50">

View 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>
);
}

View File

@@ -64,12 +64,20 @@ export default function TenantShow({ tenant }: Props) {
<h1 className="text-2xl font-bold text-slate-900">{tenant.name}</h1>
<p className="text-slate-500 mt-1"> ID: {tenant.id}</p>
</div>
<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 className="flex gap-2">
<Link
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
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>
{/* Info Card */}

View File

@@ -178,7 +178,7 @@ export default function ProductManagement({ products, categories, units, filters
{/* Header */}
<div className="mb-6">
<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>
<p className="text-gray-500 mt-1"></p>

View File

@@ -174,7 +174,7 @@ export default function CreatePurchaseOrder({
<div className="mb-6">
<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 ? "編輯採購單" : "建立採購單"}
</h1>
<p className="text-gray-500 mt-1">

View File

@@ -109,7 +109,7 @@ export default function PurchaseOrderIndex({ orders, filters, warehouses }: Prop
<div className="flex items-center justify-between mb-6">
<div>
<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>
<p className="text-gray-500 mt-1">

View File

@@ -38,7 +38,7 @@ export default function ViewPurchaseOrderPage({ order }: Props) {
<div className="flex items-center justify-between mb-6">
<div>
<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>
<p className="text-gray-500 mt-1">{order.poNumber}</p>

View File

@@ -146,7 +146,7 @@ export default function VendorManagement({ vendors, filters }: PageProps) {
{/* Header */}
<div className="mb-6">
<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>
<p className="text-gray-500 mt-1"> ERP </p>

View File

@@ -143,7 +143,7 @@ export default function VendorShow({ vendor, products }: ShowProps) {
<div className="flex items-center justify-between">
<div>
<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>
<p className="text-gray-500 mt-1"></p>

View File

@@ -184,7 +184,7 @@ export default function AddInventoryPage({ warehouse, products }: Props) {
<div className="flex items-center justify-between">
<div>
<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>
<p className="text-gray-500 mt-1">

View File

@@ -91,7 +91,7 @@ export default function EditInventory({ warehouse, inventory, transactions = []
<div className="flex items-center justify-between">
<div>
<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>
<p className="text-gray-500 mt-1">

View File

@@ -111,7 +111,7 @@ export default function WarehouseIndex({ warehouses, filters }: PageProps) {
{/* 頁面標題 */}
<div className="mb-6">
<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>
<p className="text-gray-500 mt-1">

View File

@@ -109,7 +109,7 @@ export default function WarehouseInventoryPage({
<div className="flex items-center justify-between">
<div>
<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}
</h1>
<p className="text-gray-500 mt-1"></p>

View File

@@ -37,7 +37,7 @@ export default function InventoryHistory({ warehouse, inventory, transactions }:
<div className="flex items-center justify-between">
<div>
<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>
<p className="text-gray-500 mt-1">

View File

@@ -120,7 +120,7 @@ export default function SafetyStockPage({
<div className="flex items-center justify-between">
<div>
<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}
</h1>
<p className="text-gray-500 mt-1">