Compare commits
3 Commits
746eeb6f01
...
bb78a432f5
| Author | SHA1 | Date | |
|---|---|---|---|
| bb78a432f5 | |||
| 0d720f3515 | |||
| 2e71a1cb29 |
@@ -19,6 +19,7 @@ class TenantController extends Controller
|
||||
return [
|
||||
'id' => $tenant->id,
|
||||
'name' => $tenant->name ?? $tenant->id,
|
||||
'short_name' => $tenant->short_name ?? null,
|
||||
'email' => $tenant->email ?? null,
|
||||
'is_active' => $tenant->is_active ?? true,
|
||||
'created_at' => $tenant->created_at->format('Y-m-d H:i'),
|
||||
@@ -47,6 +48,7 @@ class TenantController extends Controller
|
||||
$validated = $request->validate([
|
||||
'id' => ['required', 'string', 'max:50', 'alpha_dash', Rule::unique('tenants', 'id')],
|
||||
'name' => ['required', 'string', 'max:100'],
|
||||
'short_name' => ['nullable', 'string', 'max:50'],
|
||||
'email' => ['nullable', 'email', 'max:100'],
|
||||
'domain' => ['nullable', 'string', 'max:100'],
|
||||
]);
|
||||
@@ -54,6 +56,7 @@ class TenantController extends Controller
|
||||
$tenant = Tenant::create([
|
||||
'id' => $validated['id'],
|
||||
'name' => $validated['name'],
|
||||
'short_name' => $validated['short_name'] ?? null,
|
||||
'email' => $validated['email'] ?? null,
|
||||
'is_active' => true,
|
||||
'branding' => [
|
||||
@@ -85,6 +88,7 @@ class TenantController extends Controller
|
||||
'tenant' => [
|
||||
'id' => $tenant->id,
|
||||
'name' => $tenant->name ?? $tenant->id,
|
||||
'short_name' => $tenant->short_name ?? null,
|
||||
'email' => $tenant->email ?? null,
|
||||
'is_active' => $tenant->is_active ?? true,
|
||||
'created_at' => $tenant->created_at->format('Y-m-d H:i'),
|
||||
@@ -128,6 +132,7 @@ class TenantController extends Controller
|
||||
'tenant' => [
|
||||
'id' => $tenant->id,
|
||||
'name' => $tenant->name ?? $tenant->id,
|
||||
'short_name' => $tenant->short_name ?? null,
|
||||
'email' => $tenant->email ?? null,
|
||||
'is_active' => $tenant->is_active ?? true,
|
||||
],
|
||||
@@ -143,6 +148,7 @@ class TenantController extends Controller
|
||||
|
||||
$validated = $request->validate([
|
||||
'name' => ['required', 'string', 'max:100'],
|
||||
'short_name' => ['nullable', 'string', 'max:50'],
|
||||
'email' => ['nullable', 'email', 'max:100'],
|
||||
'is_active' => ['boolean'],
|
||||
]);
|
||||
|
||||
@@ -64,25 +64,30 @@ class HandleInertiaRequests extends Middleware
|
||||
],
|
||||
'branding' => function () {
|
||||
$tenant = tenancy()->tenant;
|
||||
if (!$tenant) {
|
||||
// 中央後台預設 Branding
|
||||
return [
|
||||
'logo_url' => \Storage::url('defaults/logo.png'), // 中央後台也使用預設 Logo
|
||||
'primary_color' => '#4F46E5',
|
||||
'text_color' => '#1a1a1a',
|
||||
];
|
||||
}
|
||||
|
||||
// 決定名稱顯示邏輯
|
||||
$fullName = $tenant ? ($tenant->name ?? 'Star ERP') : 'Star ERP 中央後台';
|
||||
$shortName = $tenant ? ($tenant->short_name ?? $fullName) : 'Start ERP';
|
||||
|
||||
$logoUrl = null;
|
||||
if (isset($tenant->branding['logo_path'])) {
|
||||
if ($tenant && isset($tenant->branding['logo_path'])) {
|
||||
$logoUrl = \Storage::url($tenant->branding['logo_path']);
|
||||
} elseif (!$tenant) {
|
||||
$logoUrl = \Storage::url('defaults/logo.png');
|
||||
}
|
||||
|
||||
return [
|
||||
$brandingData = [
|
||||
'name' => $fullName,
|
||||
'short_name' => $shortName,
|
||||
'logo_url' => $logoUrl,
|
||||
'primary_color' => $tenant->branding['primary_color'] ?? '#01ab83',
|
||||
'primary_color' => $tenant->branding['primary_color'] ?? ($tenant ? '#01ab83' : '#4F46E5'),
|
||||
'text_color' => $tenant->branding['text_color'] ?? '#1a1a1a',
|
||||
];
|
||||
|
||||
// 同步分享給 Blade View (給 app.blade.php 使用 Favicon)
|
||||
\Illuminate\Support\Facades\View::share('branding', $brandingData);
|
||||
|
||||
return $brandingData;
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
@@ -34,7 +34,21 @@ class InventoryTransferOrder extends Model
|
||||
|
||||
static::creating(function ($model) {
|
||||
if (empty($model->doc_no)) {
|
||||
$model->doc_no = 'TRF-' . date('YmdHis') . '-' . rand(100, 999);
|
||||
$today = date('Ymd');
|
||||
$prefix = 'TRF' . $today;
|
||||
|
||||
$lastDoc = static::where('doc_no', 'like', $prefix . '%')
|
||||
->orderBy('doc_no', 'desc')
|
||||
->first();
|
||||
|
||||
if ($lastDoc) {
|
||||
$lastNumber = substr($lastDoc->doc_no, -2);
|
||||
$nextNumber = str_pad((int)$lastNumber + 1, 2, '0', STR_PAD_LEFT);
|
||||
} else {
|
||||
$nextNumber = '01';
|
||||
}
|
||||
|
||||
$model->doc_no = $prefix . $nextNumber;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -176,6 +176,13 @@ export default function ProductDialog({
|
||||
id="barcode"
|
||||
value={data.barcode}
|
||||
onChange={(e) => setData("barcode", e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
// 掃描後自動跳轉到下一個欄位(品牌)
|
||||
document.getElementById('brand')?.focus();
|
||||
}
|
||||
}}
|
||||
placeholder="輸入條碼或自動生成"
|
||||
className={`flex-1 ${errors.barcode ? "border-red-500" : ""}`}
|
||||
/>
|
||||
|
||||
@@ -454,7 +454,7 @@ export default function AuthenticatedLayout({
|
||||
</button>
|
||||
<Link href="/" className="flex items-center gap-2">
|
||||
<ApplicationLogo className="w-8 h-8 rounded-lg object-contain" />
|
||||
<span className="font-bold text-slate-900">小小冰室 ERP</span>
|
||||
<span className="font-bold text-slate-900">{branding?.short_name || '小小冰室'} ERP</span>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
@@ -510,7 +510,7 @@ export default function AuthenticatedLayout({
|
||||
{!isCollapsed && (
|
||||
<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" />
|
||||
<span className="font-extrabold text-primary-main text-lg tracking-tight">小小冰室 ERP</span>
|
||||
<span className="font-extrabold text-primary-main text-lg tracking-tight">{branding?.short_name || '小小冰室'} ERP</span>
|
||||
</Link>
|
||||
)}
|
||||
{isCollapsed && (
|
||||
@@ -559,7 +559,7 @@ export default function AuthenticatedLayout({
|
||||
<div className="h-16 flex items-center justify-between px-6 border-b border-slate-100">
|
||||
<Link href="/" className="flex items-center gap-2">
|
||||
<ApplicationLogo className="w-8 h-8 rounded-lg object-contain" />
|
||||
<span className="font-extrabold text-primary-main text-lg">小小冰室 ERP</span>
|
||||
<span className="font-extrabold text-primary-main text-lg">{branding?.short_name || '小小冰室'} ERP</span>
|
||||
</Link>
|
||||
<button onClick={() => setIsMobileOpen(false)} className="p-2 text-slate-400">
|
||||
<X className="h-5 w-5" />
|
||||
@@ -588,7 +588,7 @@ export default function AuthenticatedLayout({
|
||||
{children}
|
||||
</div>
|
||||
<footer className="mt-auto py-6 text-center text-sm text-slate-400">
|
||||
Copyright © {new Date().getFullYear()} 小小冰室. All rights reserved. Design by 星科技
|
||||
Copyright © {new Date().getFullYear()} {branding?.name || '小小冰室'}. All rights reserved. Design by 星科技
|
||||
</footer>
|
||||
<Toaster richColors closeButton position="top-center" />
|
||||
</main>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Head, useForm } from "@inertiajs/react";
|
||||
import { Head, useForm, usePage } from "@inertiajs/react";
|
||||
import { PageProps } from "@/types/global";
|
||||
import { FormEventHandler, useEffect } from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Button } from "@/Components/ui/button";
|
||||
@@ -8,6 +9,7 @@ import InputError from "../../Components/InputError";
|
||||
import ApplicationLogo from "../../Components/ApplicationLogo";
|
||||
|
||||
export default function Login() {
|
||||
const { props } = usePage<PageProps>();
|
||||
const { data, setData, post, processing, errors, reset } = useForm({
|
||||
username: localStorage.getItem("saved_username") || "",
|
||||
password: "",
|
||||
@@ -134,7 +136,7 @@ export default function Login() {
|
||||
</div>
|
||||
|
||||
<p className="text-center text-gray-400 text-sm mt-8">
|
||||
© 2026 小小冰室. All rights reserved.
|
||||
© {new Date().getFullYear()} {props.branding?.name || '小小冰室'}. All rights reserved.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -6,6 +6,7 @@ export default function TenantCreate() {
|
||||
const { data, setData, post, processing, errors } = useForm({
|
||||
id: "",
|
||||
name: "",
|
||||
short_name: "",
|
||||
email: "",
|
||||
domain: "",
|
||||
});
|
||||
@@ -55,6 +56,21 @@ export default function TenantCreate() {
|
||||
{errors.name && <p className="mt-1 text-sm text-red-500">{errors.name}</p>}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 mb-2">
|
||||
客戶簡稱
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={data.short_name}
|
||||
onChange={(e) => setData("short_name", e.target.value)}
|
||||
placeholder="例如:小冰"
|
||||
className="w-full px-4 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-primary-main focus:border-primary-main"
|
||||
/>
|
||||
<p className="mt-1 text-sm text-slate-500">選填</p>
|
||||
{errors.short_name && <p className="mt-1 text-sm text-red-500">{errors.short_name}</p>}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 mb-2">
|
||||
聯絡信箱
|
||||
|
||||
@@ -5,6 +5,7 @@ import { FormEvent } from "react";
|
||||
interface Tenant {
|
||||
id: string;
|
||||
name: string;
|
||||
short_name: string | null;
|
||||
email: string | null;
|
||||
is_active: boolean;
|
||||
}
|
||||
@@ -16,6 +17,7 @@ interface Props {
|
||||
export default function TenantEdit({ tenant }: Props) {
|
||||
const { data, setData, put, processing, errors } = useForm({
|
||||
name: tenant.name,
|
||||
short_name: tenant.short_name || "",
|
||||
email: tenant.email || "",
|
||||
is_active: tenant.is_active,
|
||||
});
|
||||
@@ -62,6 +64,19 @@ export default function TenantEdit({ tenant }: Props) {
|
||||
{errors.name && <p className="mt-1 text-sm text-red-500">{errors.name}</p>}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 mb-2">
|
||||
客戶簡稱
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={data.short_name}
|
||||
onChange={(e) => setData("short_name", e.target.value)}
|
||||
className="w-full px-4 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-primary-main focus:border-primary-main"
|
||||
/>
|
||||
<p className="mt-1 text-sm text-slate-500">選填</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 mb-2">
|
||||
聯絡信箱
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
interface Tenant {
|
||||
id: string;
|
||||
name: string;
|
||||
short_name: string | null;
|
||||
email: string | null;
|
||||
is_active: boolean;
|
||||
created_at: string;
|
||||
@@ -67,6 +68,9 @@ export default function TenantIndex({ tenants }: Props) {
|
||||
<th className="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase">
|
||||
名稱
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase">
|
||||
簡稱
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase">
|
||||
域名
|
||||
</th>
|
||||
@@ -84,7 +88,7 @@ export default function TenantIndex({ tenants }: Props) {
|
||||
<tbody className="divide-y divide-slate-100">
|
||||
{tenants.length === 0 ? (
|
||||
<tr>
|
||||
<td colSpan={6} className="px-6 py-12 text-center text-slate-500">
|
||||
<td colSpan={7} className="px-6 py-12 text-center text-slate-500">
|
||||
尚無客戶資料,請點擊「新增客戶」建立第一個客戶
|
||||
</td>
|
||||
</tr>
|
||||
@@ -102,6 +106,9 @@ export default function TenantIndex({ tenants }: Props) {
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-6 py-4 text-sm text-slate-700">
|
||||
{tenant.short_name || '-'}
|
||||
</td>
|
||||
<td className="px-6 py-4">
|
||||
{tenant.domains.length > 0 ? (
|
||||
<div className="flex items-center gap-1 flex-wrap">
|
||||
|
||||
@@ -11,6 +11,7 @@ interface Domain {
|
||||
interface Tenant {
|
||||
id: string;
|
||||
name: string;
|
||||
short_name: string | null;
|
||||
email: string | null;
|
||||
is_active: boolean;
|
||||
created_at: string;
|
||||
@@ -97,6 +98,10 @@ export default function TenantShow({ tenant }: Props) {
|
||||
</span>
|
||||
</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt className="text-sm text-slate-500">客戶簡稱</dt>
|
||||
<dd className="mt-1 text-slate-900">{tenant.short_name || "-"}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt className="text-sm text-slate-500">聯絡信箱</dt>
|
||||
<dd className="mt-1 text-slate-900">{tenant.email || "-"}</dd>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
*/
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import { Plus, Factory, Search, RotateCcw, Eye, Pencil, Trash2 } from 'lucide-react';
|
||||
import { Plus, Factory, Search, Eye, Pencil, Trash2 } from 'lucide-react';
|
||||
import { Button } from "@/Components/ui/button";
|
||||
import AuthenticatedLayout from "@/Layouts/AuthenticatedLayout";
|
||||
import { Head, router, Link } from "@inertiajs/react";
|
||||
@@ -12,7 +12,7 @@ import { getBreadcrumbs } from "@/utils/breadcrumb";
|
||||
import { Can } from "@/Components/Permission/Can";
|
||||
import { SearchableSelect } from "@/Components/ui/searchable-select";
|
||||
import { Input } from "@/Components/ui/input";
|
||||
import { Label } from "@/Components/ui/label";
|
||||
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
@@ -80,11 +80,7 @@ export default function ProductionIndex({ productionOrders, filters }: Props) {
|
||||
);
|
||||
};
|
||||
|
||||
const handleReset = () => {
|
||||
setSearch("");
|
||||
setStatus("all");
|
||||
router.get(route('production-orders.index'));
|
||||
};
|
||||
|
||||
|
||||
const handlePerPageChange = (value: string) => {
|
||||
setPerPage(value);
|
||||
@@ -113,70 +109,79 @@ export default function ProductionIndex({ productionOrders, filters }: Props) {
|
||||
記錄生產過程,追蹤原物料使用與成品入庫
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Can permission="production_orders.create">
|
||||
<Button
|
||||
onClick={handleNavigateToCreate}
|
||||
className="gap-2 button-filled-primary"
|
||||
>
|
||||
<Plus className="h-4 w-4" />
|
||||
建立生產單
|
||||
</Button>
|
||||
</Can>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
{/* 篩選區塊 */}
|
||||
<div className="bg-white rounded-lg shadow-sm border border-gray-200 mb-6 overflow-hidden">
|
||||
<div className="p-5">
|
||||
<div className="grid grid-cols-1 md:grid-cols-12 gap-4">
|
||||
<div className="md:col-span-8 space-y-1">
|
||||
<Label className="text-xs font-medium text-grey-2">關鍵字搜尋</Label>
|
||||
<div className="relative">
|
||||
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 h-4 w-4" />
|
||||
<Input
|
||||
placeholder="搜尋生產單號、批號、商品名稱..."
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
className="pl-10 h-9 block"
|
||||
onKeyDown={(e) => e.key === 'Enter' && handleFilter()}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="md:col-span-4 space-y-1">
|
||||
<Label className="text-xs font-medium text-grey-2">狀態</Label>
|
||||
<Select value={status} onValueChange={setStatus}>
|
||||
<SelectTrigger className="h-9 text-sm">
|
||||
<SelectValue placeholder="選擇狀態" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">全部狀態</SelectItem>
|
||||
<SelectItem value="draft">草稿</SelectItem>
|
||||
<SelectItem value="completed">已完成</SelectItem>
|
||||
<SelectItem value="cancelled">已取消</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
{/* Toolbar */}
|
||||
<div className="bg-white rounded-lg shadow-sm border p-4 mb-6">
|
||||
<div className="flex flex-col md:flex-row gap-4">
|
||||
{/* Search */}
|
||||
<div className="flex-1 relative">
|
||||
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 h-4 w-4" />
|
||||
<Input
|
||||
placeholder="搜尋生產單號、批號、商品名稱..."
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
className="pl-10 pr-10 h-9"
|
||||
onKeyDown={(e) => e.key === 'Enter' && handleFilter()}
|
||||
/>
|
||||
{search && (
|
||||
<button
|
||||
onClick={() => {
|
||||
setSearch("");
|
||||
router.get(route('production-orders.index'), { ...filters, search: "" }, { preserveState: true, replace: true });
|
||||
}}
|
||||
className="absolute right-3 top-1/2 transform -translate-y-1/2 text-gray-400 hover:text-gray-600"
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-end px-5 py-4 bg-gray-50/50 border-t border-gray-100 gap-3">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={handleReset}
|
||||
className="button-outlined-primary h-9 gap-2"
|
||||
{/* Status Filter */}
|
||||
<Select
|
||||
value={status}
|
||||
onValueChange={(val) => {
|
||||
setStatus(val);
|
||||
router.get(
|
||||
route('production-orders.index'),
|
||||
{ ...filters, status: val === 'all' ? undefined : val },
|
||||
{ preserveState: true, replace: true }
|
||||
);
|
||||
}}
|
||||
>
|
||||
<RotateCcw className="h-4 w-4" />
|
||||
重置
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleFilter}
|
||||
className="button-filled-primary h-9 px-6 gap-2"
|
||||
>
|
||||
<Search className="h-4 w-4" />
|
||||
搜尋
|
||||
</Button>
|
||||
<SelectTrigger className="w-full md:w-[150px] h-9 text-sm">
|
||||
<SelectValue placeholder="選擇狀態" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">全部狀態</SelectItem>
|
||||
<SelectItem value="draft">草稿</SelectItem>
|
||||
<SelectItem value="completed">已完成</SelectItem>
|
||||
<SelectItem value="cancelled">已取消</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
{/* Action Buttons */}
|
||||
<div className="flex gap-2 w-full md:w-auto">
|
||||
<Button
|
||||
variant="outline"
|
||||
className="button-outlined-primary"
|
||||
onClick={handleFilter}
|
||||
>
|
||||
<Search className="w-4 h-4 mr-2" />
|
||||
搜尋
|
||||
</Button>
|
||||
|
||||
<Can permission="production_orders.create">
|
||||
<Button
|
||||
onClick={handleNavigateToCreate}
|
||||
className="gap-2 button-filled-primary"
|
||||
>
|
||||
<Plus className="h-4 w-4" />
|
||||
建立生產單
|
||||
</Button>
|
||||
</Can>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
*/
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import { Plus, Search, RotateCcw, Pencil, Trash2, BookOpen, Eye } from 'lucide-react';
|
||||
import { Plus, Search, Pencil, Trash2, BookOpen, Eye } from 'lucide-react';
|
||||
import { Button } from "@/Components/ui/button";
|
||||
import AuthenticatedLayout from "@/Layouts/AuthenticatedLayout";
|
||||
import { Head, router, Link } from "@inertiajs/react";
|
||||
@@ -11,7 +11,7 @@ import Pagination from "@/Components/shared/Pagination";
|
||||
import { getBreadcrumbs } from "@/utils/breadcrumb";
|
||||
import { SearchableSelect } from "@/Components/ui/searchable-select";
|
||||
import { Input } from "@/Components/ui/input";
|
||||
import { Label } from "@/Components/ui/label";
|
||||
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/Components/ui/table";
|
||||
import { Badge } from "@/Components/ui/badge";
|
||||
import { Can } from "@/Components/Permission/Can";
|
||||
@@ -82,10 +82,7 @@ export default function RecipeIndex({ recipes, filters }: Props) {
|
||||
);
|
||||
};
|
||||
|
||||
const handleReset = () => {
|
||||
setSearch("");
|
||||
router.get(route('recipes.index'));
|
||||
};
|
||||
|
||||
|
||||
const handlePerPageChange = (value: string) => {
|
||||
setPerPage(value);
|
||||
@@ -130,54 +127,55 @@ export default function RecipeIndex({ recipes, filters }: Props) {
|
||||
管理產品的標準生產配方與用量
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Can permission="recipes.create">
|
||||
<Link href={route('recipes.create')}>
|
||||
<Button className="gap-2 button-filled-primary">
|
||||
<Plus className="h-4 w-4" />
|
||||
新增配方
|
||||
</Button>
|
||||
</Link>
|
||||
</Can>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
{/* 篩選區塊 */}
|
||||
<div className="bg-white rounded-lg shadow-sm border border-gray-200 mb-6 overflow-hidden">
|
||||
<div className="p-5">
|
||||
<div className="grid grid-cols-1 md:grid-cols-12 gap-4">
|
||||
<div className="md:col-span-12 space-y-1">
|
||||
<Label className="text-xs font-medium text-grey-2">關鍵字搜尋</Label>
|
||||
<div className="relative">
|
||||
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 h-4 w-4" />
|
||||
<Input
|
||||
placeholder="搜尋配方代號、名稱、產品名稱..."
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
className="pl-10 h-9 block"
|
||||
onKeyDown={(e) => e.key === 'Enter' && handleFilter()}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/* Toolbar */}
|
||||
<div className="bg-white rounded-lg shadow-sm border p-4 mb-6">
|
||||
<div className="flex flex-col md:flex-row gap-4">
|
||||
{/* Search */}
|
||||
<div className="flex-1 relative">
|
||||
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 h-4 w-4" />
|
||||
<Input
|
||||
placeholder="搜尋配方代號、名稱、產品名稱..."
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
className="pl-10 pr-10 h-9"
|
||||
onKeyDown={(e) => e.key === 'Enter' && handleFilter()}
|
||||
/>
|
||||
{search && (
|
||||
<button
|
||||
onClick={() => {
|
||||
setSearch("");
|
||||
router.get(route('recipes.index'), { ...filters, search: "" }, { preserveState: true, replace: true });
|
||||
}}
|
||||
className="absolute right-3 top-1/2 transform -translate-y-1/2 text-gray-400 hover:text-gray-600"
|
||||
>
|
||||
<Trash2 className="h-4 w-4" /> {/* Using Trash2/X as clear icon, need to check imports. Inventory used X. */}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-end px-5 py-4 bg-gray-50/50 border-t border-gray-100 gap-3">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={handleReset}
|
||||
className="button-outlined-primary h-9 gap-2"
|
||||
>
|
||||
<RotateCcw className="h-4 w-4" />
|
||||
重置
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleFilter}
|
||||
className="button-filled-primary h-9 px-6 gap-2"
|
||||
>
|
||||
<Search className="h-4 w-4" />
|
||||
搜尋
|
||||
</Button>
|
||||
{/* Action Buttons */}
|
||||
<div className="flex gap-2 w-full md:w-auto">
|
||||
<Button
|
||||
variant="outline"
|
||||
className="button-outlined-primary"
|
||||
onClick={handleFilter}
|
||||
>
|
||||
<Search className="w-4 h-4 mr-2" />
|
||||
搜尋
|
||||
</Button>
|
||||
|
||||
<Can permission="recipes.create">
|
||||
<Link href={route('recipes.create')}>
|
||||
<Button className="button-filled-primary">
|
||||
<Plus className="w-4 h-4 mr-2" />
|
||||
新增配方
|
||||
</Button>
|
||||
</Link>
|
||||
</Can>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
3
resources/js/types/global.d.ts
vendored
3
resources/js/types/global.d.ts
vendored
@@ -12,6 +12,8 @@ export interface AuthUser {
|
||||
}
|
||||
|
||||
export interface Branding {
|
||||
name?: string;
|
||||
short_name?: string;
|
||||
logo_url?: string | null;
|
||||
primary_color?: string;
|
||||
text_color?: string;
|
||||
@@ -26,6 +28,7 @@ export interface PageProps {
|
||||
error?: string;
|
||||
};
|
||||
branding?: Branding | null;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
<title inertia>{{ config('app.name', 'Laravel') }}</title>
|
||||
|
||||
<!-- Fonts -->
|
||||
<link rel="icon" type="image/png" href="{{ $branding['logo_url'] ?? '/favicon.png' }}">
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+TC:wght@400;500;700&display=swap" rel="stylesheet">
|
||||
|
||||
Reference in New Issue
Block a user