Files
star-erp/resources/js/Pages/Landlord/Tenant/Branding.tsx
sky121113 55272d5d43
All checks were successful
Koori-ERP-Deploy-System / deploy-demo (push) Successful in 47s
Koori-ERP-Deploy-System / deploy-production (push) Has been skipped
feat: 新增租戶品牌客製化系統(Logo、主色系)、修正 hardcoded 顏色為 CSS 變數
2026-01-16 14:36:24 +08:00

197 lines
8.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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>
);
}