diff --git a/.gemini/antigravity/skills/activity-logging/SKILL.md b/.gemini/antigravity/skills/activity-logging/SKILL.md new file mode 100644 index 0000000..854df5a --- /dev/null +++ b/.gemini/antigravity/skills/activity-logging/SKILL.md @@ -0,0 +1,158 @@ +--- +name: 操作紀錄實作規範 +description: 規範系統內 Activity Log 的實作標準,包含後端資料過濾、快照策略、與前端顯示邏輯。 +--- + +# 操作紀錄實作規範 + +本文件說明如何在開發新功能時,依據系統規範實作 `spatie/laravel-activitylog` 操作紀錄,確保資料儲存效率與前端顯示一致性。 + +## 1. 後端實作標準 (Backend) + +所有 Model 之操作紀錄應遵循「僅儲存變動資料」與「保留關鍵快照」兩大原則。 + +### 1.1 啟用 Activity Log + +在 Model 中引用 `LogsActivity` trait 並實作 `getActivitylogOptions` 方法。 + +```php +use Spatie\Activitylog\Traits\LogsActivity; +use Spatie\Activitylog\LogOptions; + +class Product extends Model +{ + use LogsActivity; + + public function getActivitylogOptions(): LogOptions + { + return LogOptions::defaults() + ->logAll() + ->logOnlyDirty() // ✅ 關鍵:只記錄有變動的欄位 + ->dontSubmitEmptyLogs(); // 若無變動則不記錄 + } +} +``` + +### 1.2 手動記錄 (Manual Logging) + +若需在 Controller 手動記錄(例如需客製化邏輯),**必須**自行實作變動過濾,不可直接儲存所有屬性。 + +**錯誤範例 (Do NOT do this):** +```php +// ❌ 錯誤:這會導致每次更新都記錄所有欄位,即使它們沒變 +activity() + ->withProperties(['attributes' => $newAttributes, 'old' => $oldAttributes]) + ->log('updated'); +``` + +**正確範例 (Do this):** +```php +// ✅ 正確:自行比對差異,只存變動值 +$changedAttributes = []; +$changedOldAttributes = []; + +foreach ($newAttributes as $key => $value) { + if ($value != ($oldAttributes[$key] ?? null)) { + $changedAttributes[$key] = $value; + $changedOldAttributes[$key] = $oldAttributes[$key] ?? null; + } +} + +if (!empty($changedAttributes)) { + activity() + ->withProperties(['attributes' => $changedAttributes, 'old' => $changedOldAttributes]) + ->log('updated'); +} +``` + +### 1.3 快照策略 (Snapshot Strategy) + +為確保資料被刪除後仍能辨識操作對象,**必須**在 `properties.snapshot` 中儲存關鍵識別資訊(如名稱、代號、類別名稱)。 + +**主要方式:使用 `tapActivity` (推薦)** + +```php +public function tapActivity(\Spatie\Activitylog\Contracts\Activity $activity, string $eventName) +{ + $properties = $activity->properties; + $snapshot = $properties['snapshot'] ?? []; + + // 保存關鍵關聯名稱 (避免關聯資料刪除後 ID 失效) + $snapshot['category_name'] = $this->category ? $this->category->name : null; + $snapshot['po_number'] = $this->code; // 儲存單號 + + // 保存自身名稱 (Context) + $snapshot['name'] = $this->name; + + $properties['snapshot'] = $snapshot; + $activity->properties = $properties; +} +``` + +## 2. 顯示名稱映射 (UI Mapping) + +### 2.1 對象名稱映射 (Mapping) + +需在 `ActivityLogController.php` 中設定 Model 與中文名稱的對應,讓前端列表能顯示中文對象(如「公共事業費」而非 `UtilityFee`)。 + +**位置**: `app/Http/Controllers/Admin/ActivityLogController.php` + +```php +protected function getSubjectMap() +{ + return [ + 'App\Modules\Inventory\Models\Product' => '商品', + 'App\Modules\Finance\Models\UtilityFee' => '公共事業費', // ✅ 新增映射 + ]; +} +``` + +### 2.2 欄位名稱中文化 (Field Translation) + +需在前端 `ActivityDetailDialog` 中設定欄位名稱的中文翻譯。 + +**位置**: `resources/js/Components/ActivityLog/ActivityDetailDialog.tsx` + +```typescript +const fieldLabels: Record = { + // ... 既有欄位 + 'transaction_date': '費用日期', + 'category': '費用類別', + 'amount': '金額', +}; +``` + +## 3. 前端顯示邏輯 (Frontend) + +### 3.1 列表描述生成 (Description Generation) + +前端 `LogTable.tsx` 會依據 `properties.snapshot` 中的欄位自動組建描述(例如:「Admin 新增 電話費 公共事業費」)。 + +若您的 Model 使用了特殊的識別欄位(例如 `category`),**必須**將其加入 `nameParams` 陣列中。 + +**位置**: `resources/js/Components/ActivityLog/LogTable.tsx` + +```typescript +const nameParams = [ + 'po_number', 'name', 'code', + 'category_name', + 'category' // ✅ 確保加入此欄位,前端才能抓到 $snapshot['category'] +]; +``` + +### 3.2 詳情過濾邏輯 + +前端 `ActivityDetailDialog` 已內建智慧過濾邏輯: +- **Created**: 顯示初始化欄位。 +- **Updated**: **僅顯示有變動的欄位** (由 `isChanged` 判斷)。 +- **Deleted**: 顯示刪除前的完整資料。 + +開發者僅需確保傳入的 `attributes` 與 `old` 資料結構正確,過濾邏輯會自動運作。 + +## 檢核清單 + +- [ ] **Backend**: Model 是否已設定 `logOnlyDirty` 或手動實作過濾? +- [ ] **Backend**: 是否已透過 `tapActivity` 或手動方式記錄 Snapshot(關鍵名稱)? +- [ ] **Backend**: 是否已在 `ActivityLogController` 加入 Model 中文名稱映射? +- [ ] **Frontend**: 是否已在 `ActivityDetailDialog` 加入欄位中文翻譯? +- [ ] **Frontend**: 若使用特殊識別欄位,是否已加入 `LogTable` 的 `nameParams`? diff --git a/.gemini/antigravity/skills/permission-management/SKILL.md b/.gemini/antigravity/skills/permission-management/SKILL.md new file mode 100644 index 0000000..78c311d --- /dev/null +++ b/.gemini/antigravity/skills/permission-management/SKILL.md @@ -0,0 +1,140 @@ +--- +name: 權限管理與實作規範 +description: 為新功能實作權限控制的完整流程規範,包含後端 Seeder 設定、Middleware 路由保護與前端權限判斷。 +--- + +# 權限管理與實作規範 + +本文件說明如何在新增功能時,一併實作完整的權限控制機制。專案採用 `spatie/laravel-permission` 套件進行權限管理。 + +## 1. 定義權限 (Backend) + +所有權限皆定義於 `database/seeders/PermissionSeeder.php`。 + +### 步驟: + +1. 開啟 `database/seeders/PermissionSeeder.php`。 +2. 在 `$permissions` 陣列中新增功能對應的權限字串。 + * **命名慣例**:`{resource}.{action}` (例如:`system.view_logs`, `products.create`) + * 常用動作:`view`, `create`, `edit`, `delete`, `publish`, `export` +3. 在下方「角色分配」區段,將新權限分配給適合的角色。 + * `super-admin`:通常擁有所有權限(程式碼中 `Permission::all()` 自動涵蓋,無需手動新增)。 + * `admin`:通常擁有大部分權限。 + * 其他角色 (`warehouse-manager`, `purchaser`, `viewer`):依業務邏輯分配。 + +### 範例: + +```php +// 1. 新增權限字串 +$permissions = [ + // ... 現有權限 + 'system.view_logs', // 新增:檢視系統日誌 +]; + +// ... + +// 2. 分配給角色 +$admin->givePermissionTo([ + // ... 現有權限 + 'system.view_logs', +]); +``` + +## 2. 套用資料庫變更 + +修改 Seeder 後,必須重新執行 Seeder 以將權限寫入資料庫。 + +```bash +# 對於所有租戶執行 Seeder (開發環境) +php artisan tenants:seed --class=PermissionSeeder +``` + +## 3. 路由保護 (Backend Middleware) + +在 `routes/web.php` 中,使用 `permission:{name}` middleware 保護路由。 + +### 範例: + +```php +// 單一權限保護 +Route::get('/logs', [LogController::class, 'index']) + ->middleware('permission:system.view_logs') + ->name('logs.index'); + +// 路由群組保護 +Route::middleware('permission:products.view')->group(function () { + // ... +}); + +// 多重權限 (OR 邏輯:有其一即可) +Route::middleware('permission:products.create|products.edit')->group(function () { + // ... +}); +``` + +## 4. 前端權限判斷 (React Component) + +使用自訂 Hook `usePermission` 來控制 UI 元素的顯示(例如:隱藏沒有權限的按鈕)。 + +### 引入 Hook: + +```tsx +import { usePermission } from "@/hooks/usePermission"; +``` + +### 使用方式: + +```tsx +export default function ProductIndex() { + const { can } = usePermission(); + + return ( +
+

商品列表

+ + {/* 只有擁有 create 權限才顯示按鈕 */} + {can('products.create') && ( + + )} + + {/* 組合判斷 */} + {can('products.edit') && } +
+ ); +} +``` + +### 權限 Hook 介面說明: + +- `can(permission: string)`: 檢查當前使用者是否擁有指定權限。 +- `canAny(permissions: string[])`: 檢查當前使用者是否擁有陣列中**任一**權限。 +- `hasRole(role: string)`: 檢查當前使用者是否擁有指定角色。 + +## 5. 配置權限群組名稱 (Backend UI Config) + +為了讓新權限在「角色與權限」管理介面中顯示正確的中文分組標題,需修改 Controller 設定。 + +### 步驟: + +1. 開啟 `app/Http/Controllers/Admin/RoleController.php`。 +2. 找到 `getGroupedPermissions` 方法。 +3. 在 `$groupDefinitions` 陣列中,新增 `{resource}` 對應的中文名稱。 + +### 範例: + +```php +$groupDefinitions = [ + 'products' => '商品資料管理', + // ... + 'utility_fees' => '公共事業費管理', // 新增此行 +]; +``` + +## 檢核清單 + +- [ ] `PermissionSeeder.php` 已新增權限字串。 +- [ ] `PermissionSeeder.php` 已將新權限分配給對應角色。 +- [ ] 已執行 `php artisan tenants:seed --class=PermissionSeeder` 更新資料庫。 +- [ ] `RoleController.php` 已新增權限群組的中文名稱映射。 +- [ ] 後端路由 (`routes/web.php`) 已加上 middleware 保護。 +- [ ] 前端頁面/按鈕已使用 `usePermission` 進行顯示控制。 diff --git a/.gemini/antigravity/skills/ui-consistency/SKILL.md b/.gemini/antigravity/skills/ui-consistency/SKILL.md new file mode 100644 index 0000000..e5ef3bd --- /dev/null +++ b/.gemini/antigravity/skills/ui-consistency/SKILL.md @@ -0,0 +1,990 @@ +--- +name: 客戶端後台 UI 統一規範 +description: 確保 Star ERP 客戶端(租戶端)後台所有頁面的 UI 元件保持統一的樣式與行為 +--- + +# 客戶端後台 UI 統一規範 + +## 概述 + +本技能提供 Star ERP 系統**客戶端(租戶端)後台**的 UI 統一性規範,確保所有頁面使用一致的元件、樣式類別、圖標和佈局模式。 + +> **適用範圍**:本規範適用於租戶端後台(使用 `AuthenticatedLayout` 的頁面),**不適用於**中央管理後台(`LandlordLayout`)。 + +## 核心原則 + +1. **使用統一的 UI 組件庫**:優先使用 `@/Components/ui/` 中的 47 個元件 +2. **遵循既定的樣式類別**:使用 `app.css` 中定義的自定義按鈕類別 +3. **統一的圖標系統**:全面使用 `lucide-react` 圖標 +4. **一致的佈局模式**:表格、分頁、操作按鈕等保持相同結構 +5. **權限控制**:所有操作按鈕必須使用 `` 元件包裹 + +--- + +## 1. 專案結構 + +### 1.1 關鍵目錄 + +``` +resources/ +├── css/ +│ └── app.css # 全域樣式與設計 Token +├── js/ +│ ├── Components/ +│ │ ├── ui/ # 47 個基礎 UI 元件 (shadcn/ui) +│ │ ├── shared/ # 共用業務元件 (Pagination, BreadcrumbNav 等) +│ │ └── Permission/ # 權限控制元件 (Can, HasRole, CanAll) +│ ├── Layouts/ +│ │ ├── AuthenticatedLayout.tsx # 客戶端後台佈局 ⬅️ 本規範適用 +│ │ └── LandlordLayout.tsx # 中央管理後台佈局 +│ └── Pages/ # 頁面元件 +``` + +### 1.2 可用 UI 元件清單 + +``` +accordion, alert, alert-dialog, avatar, badge, breadcrumb, button, +calendar, card, carousel, chart, checkbox, collapsible, command, +context-menu, dialog, drawer, dropdown-menu, form, hover-card, +input, input-otp, label, menubar, navigation-menu, pagination, +popover, progress, radio-group, resizable, scroll-area, +searchable-select, select, separator, sheet, sidebar, skeleton, +slider, sonner, switch, table, tabs, textarea, toggle, toggle-group, +tooltip +``` + +--- + +## 2. 色彩系統 + +### 2.1 主題色 (Primary) - **動態租戶品牌色** + +> **注意**:主題色會根據租戶設定(Branding)動態改變,**嚴禁**在程式碼中 Hardcode 色碼(如 `#01ab83`)。 +> 請務必使用 Tailwind Utility Class 或 CSS 變數。 + +| Tailwind Class | CSS Variable | 說明 | +|----------------|--------------|------| +| `*-primary-main` | `--primary-main` | **主色**:與租戶設定一致(預設綠色),用於主要按鈕、連結、強調文字 | +| `*-primary-dark` | `--primary-dark` | **深色**:系統自動計算,用於 Hover 狀態 | +| `*-primary-light` | `--primary-light` | **淺色**:系統自動計算,用於次要強調 | +| `*-primary-lightest` | `--primary-lightest` | **最淺色**:系統自動計算,用於背景底色、Active 狀態 | + +**運作機制**: +`AuthenticatedLayout` 會根據後端回傳的 `branding` 資料,自動注入 CSS 變數覆寫預設值。 + +```tsx +// ✅ 正確:使用 Tailwind Class +
...
+ +// ✅ 正確:使用 CSS 變數 (自定義樣式時) +
...
+ +// ❌ 錯誤:寫死色碼 (會導致租戶無法換色) +
...
+``` + +### 2.2 灰階 (Grey Scale) + +```css +--grey-0: #1a1a1a; /* 深黑 - 標題文字 */ +--grey-1: #4a4a4a; /* 深灰 - 主要內文 */ +--grey-2: #6b6b6b; /* 中灰 - 次要內文、Placeholder */ +--grey-3: #9e9e9e; /* 淺灰 - 禁用文字、輔助說明 */ +--grey-4: #e0e0e0; /* 極淺灰 - 邊框、分隔線 */ +--grey-5: #fff; /* 白色 - 背景、按鈕文字 */ +``` + +### 2.3 狀態色 (State Colors) + +```css +--other-success: #01ab83; /* 成功 - 同主題色 */ +--other-error: #dc2626; /* 錯誤 - 刪除、警示 */ +--other-warning: #f59e0b; /* 警告 - 提醒、注意 */ +--other-info: #3b82f6; /* 資訊 - 說明、提示 */ +``` + +--- + +## 3. 按鈕規範 + +### 3.1 按鈕樣式類別 + +專案在 `resources/css/app.css` 中定義了統一的按鈕樣式,**必須**使用這些類別: + +#### Filled 按鈕(實心按鈕)— 用於主要操作 + +```tsx +// ✅ 主要操作按鈕(綠色主題色)- 新增、儲存、確認 + + +// ✅ 成功操作 + + +// ✅ 資訊操作(用於系統提示、說明等非業務主流程) + + +// ✅ 警告操作 + + +// ✅ 錯誤/刪除操作(AlertDialog 內確認按鈕) + +``` + +#### Outlined 按鈕(邊框按鈕)— 用於次要操作 + +```tsx +// ✅ 編輯按鈕(表格操作列) + + +// ✅ 刪除按鈕(表格操作列) + +``` + +#### Text 按鈕(文字按鈕) + +```tsx + +``` + +### 3.2 按鈕大小 + +| Size | 高度 | 使用情境 | +|------|------|----------| +| `size="sm"` | h-8 | 表格操作列、緊湊佈局 | +| `size="default"` | h-9 | 一般操作、表單提交 | +| `size="lg"` | h-10 | 主要 CTA、頁面主操作 | +| `size="icon"` | 9×9 | 純圖標按鈕 | + +### 3.3 常見操作按鈕模式 + +#### 頁面頂部新增按鈕 + +```tsx + + + + + +``` + +#### 表格操作列檢視按鈕 + +```tsx + + + + + +``` + +#### 表格操作列編輯按鈕 + +```tsx + + + + + +``` + +#### 表格操作列刪除按鈕(帶確認對話框) + +```tsx + + + + + + + + 確認刪除 + + 確定要刪除「{item.name}」嗎?此操作無法復原。 + + + + 取消 + handleDelete(item.id)} + className="bg-red-600 hover:bg-red-700" + > + 刪除 + + + + + +``` + +### 3.4 返回按鈕規範 + +詳情頁面(如:查看庫存、進貨單詳情)的返回按鈕應統一放置於 **頁面標題上方**,並採用「**圖標 + 文字**」的 Outlined 樣式。 + +**樣式規格**: +- **位置**:標題區域上方 (`mb-6`),獨立於標題列 +- **樣式**:`variant="outline"` + `className="gap-2 button-outlined-primary"` +- **圖標**:`` +- **文字**:清楚說明返回目的地,例如「返回倉庫管理」、「返回列表」 + +```tsx +
+ + + +
+``` + +--- + +## 4. 圖標規範 + +### 4.1 統一使用 lucide-react + +**統一使用 `lucide-react`**,禁止使用其他圖標庫(如 FontAwesome、Material Icons、react-icons 等)。 + +### 4.2 圖標尺寸標準 + +| 尺寸 | 類別 | 使用情境 | +|------|------|----------| +| 小型 | `h-3 w-3` | Badge 內、小文字旁 | +| 標準 | `h-4 w-4` | 按鈕內、表格操作 | +| 標題 | `h-5 w-5` | 側邊欄選單 | +| 大型 | `h-6 w-6` | 頁面標題 | + +### 4.3 常用操作圖標映射 + +| 操作 | 圖標組件 | 使用情境 | +|------|----------|----------| +| 新增 | `` | 新增按鈕 | +| 編輯 | `` | 編輯按鈕 | +| 刪除 | `` | 刪除按鈕 | +| 查看 | `` | 查看詳情 | +| 搜尋 | `` | 搜尋欄位 | +| 篩選 | `` | 篩選功能 | +| 下載 | `` | 下載/匯出 | +| 上傳 | `` | 上傳/匯入 | +| 設定 | `` | 設定功能 | +| 複製 | `` | 複製內容 | +| 郵件 | `` | Email 顯示 | +| 使用者 | ``, `` | 使用者管理 | +| 權限 | `` | 角色/權限 | +| 排序 | ``, ``, `` | 表格排序 | +| 儀表板 | `` | 首頁/總覽 | +| 商品 | `` | 商品管理 | +| 倉庫 | `` | 倉庫管理 | +| 廠商 | ``, `` | 廠商管理 | +| 採購 | `` | 採購管理 | + +### 4.4 圖標使用範例 + +```tsx +import { Plus, Pencil, Trash2, Users } from 'lucide-react'; + +// 頁面標題 +

+ + 使用者管理 +

+ +// 按鈕內圖標(圖標在左,帶文字) + + +// 純圖標按鈕(表格操作列) + +``` + +--- + +## 5. 表格規範 + +### 5.1 表格容器 + +```tsx +
+ + {/* 表格內容 */} +
+
+``` + +### 5.2 表格標題列 + +```tsx + + + # + 名稱 + 操作 + + +``` + +**關鍵要點**: +- 使用 `bg-gray-50` 背景色 +- 序號欄位固定寬度 `w-[50px]` 並置中 +- 操作欄位置中顯示 + +### 5.3 表格主體 + +```tsx + + {items.length === 0 ? ( + + + 無符合條件的資料 + + + ) : ( + items.map((item, index) => ( + + + {startIndex + index} + + {/* 其他欄位 */} + +
+ {/* 操作按鈕 */} +
+
+
+ )) + )} +
+``` + +**關鍵要點**: +- 空狀態訊息使用置中、灰色文字 +- 序號欄使用 `text-gray-500 font-medium text-center` +- 操作欄使用 `flex items-center justify-center gap-2` 排列按鈕 + +### 5.4 欄位排序規範 + +當表格需要支援排序時,請遵循以下模式: + +1. **圖標邏輯**: + * 未排序:`ArrowUpDown` (class: `text-muted-foreground`) + * 升冪 (asc):`ArrowUp` (class: `text-primary`) + * 降冪 (desc):`ArrowDown` (class: `text-primary`) +2. **結構**:在 `TableHead` 內使用 `button` 元素。 +3. **後端配合**:後端 Controller **必須** 處理 `sort_by` 與 `sort_order` 參數。 + +```tsx +// 1. 定義 Helper Component (在元件內部) +const SortIcon = ({ field }: { field: string }) => { + if (filters.sort_by !== field) { + return ; + } + if (filters.sort_order === "asc") { + return ; + } + return ; +}; + +// 2. 表格標題應用 + + + + +// 3. 排序處理函式 (三態切換:未排序 -> 升冪 -> 降冪 -> 未排序) +const handleSort = (field: string) => { + let newSortBy: string | undefined = field; + let newSortOrder: 'asc' | 'desc' | undefined = 'asc'; + + if (filters.sort_by === field) { + if (filters.sort_order === 'asc') { + newSortOrder = 'desc'; + } else { + // desc -> reset (回到預設排序) + newSortBy = undefined; + newSortOrder = undefined; + } + } + + router.get( + route(route().current()!), + { ...filters, sort_by: newSortBy, sort_order: newSortOrder }, + { preserveState: true, replace: true } + ); +}; +``` + +--- + +## 6. 分頁規範 + +### 6.1 統一分頁元件 + +使用 `@/Components/shared/Pagination` 元件: + +```tsx +import Pagination from "@/Components/shared/Pagination"; +import { SearchableSelect } from "@/Components/ui/searchable-select"; + +// 在表格下方 +
+
+ 每頁顯示 + + +
+ +
+``` + +### 6.2 每頁筆數狀態管理 + +```tsx +const [perPage, setPerPage] = useState(filters.per_page || "10"); + +const handlePerPageChange = (value: string) => { + setPerPage(value); + router.get( + route('resource.index'), + { per_page: value }, + { preserveState: false, replace: true, preserveScroll: true } + ); +}; +``` + +--- + +## 7. Badge 與狀態顯示 + +### 7.1 基本 Badge + +```tsx +import { Badge } from "@/Components/ui/badge"; + +// Outline 樣式(最常用) +{item.category?.name || '-'} + +// 預設樣式(主題色背景) +啟用中 + +// 錯誤樣式 +停用 +``` + +### 7.2 角色顯示(特殊樣式) + +```tsx +
+ {user.roles.map(role => ( +
+
+ {role.name === 'super-admin' && } + + {role.display_name} + +
+
+ ))} +
+``` + +--- + +## 8. 頁面佈局規範 + +### 8.1 頁面結構 + +```tsx +export default function ResourceIndex() { + return ( + + + +
+ {/* 頁面頭部 */} + {/* 主要內容 */} + {/* 分頁元件 */} +
+
+ ); +} +``` + +### 8.2 標準頁面頭部 + +```tsx +
+
+

+ + 頁面標題 +

+

+ 頁面說明文字 +

+
+ + + + + +
+``` + +--- + +## 9. 權限控制規範 + +### 9.1 使用 Can 元件 + +**所有**涉及權限的 UI 元素都必須使用 `` 元件包裹: + +```tsx +import { Can } from "@/Components/Permission/Can"; + + + {/* 新增按鈕 */} + + + + {/* 編輯按鈕 */} + + + + {/* 刪除按鈕 */} + +``` + +### 9.2 權限命名規範 + +遵循 `resource.action` 格式: + +- `resource.view`:查看列表/詳情 +- `resource.create`:新增 +- `resource.edit`:編輯 +- `resource.delete`:刪除 + +### 9.3 多權限判斷 + +```tsx +// 滿足任一權限即可 + +
管理操作
+
+ +// 必須滿足所有權限 +import { CanAll } from "@/Components/Permission/Can"; + + + + +``` + +--- + +## 10. 通知訊息規範 + +### 10.1 使用 Toast 通知 + +使用 `sonner` 的 `toast` 進行通知: + +```tsx +import { toast } from 'sonner'; + +// 成功訊息 +toast.success('操作成功'); + +// 錯誤訊息 +toast.error('操作失敗'); + +// 資訊訊息 +toast.info('提示訊息'); + +// 警告訊息 +toast.warning('警告訊息'); +``` + +### 10.2 常見操作的 Toast 訊息 + +```tsx +// 新增成功 +router.post(route('resource.store'), data, { + onSuccess: () => toast.success('新增成功'), + onError: () => toast.error('新增失敗,請檢查輸入內容'), +}); + +// 更新成功 +router.put(route('resource.update', id), data, { + onSuccess: () => toast.success('更新成功'), + onError: () => toast.error('更新失敗'), +}); + +// 刪除成功 +router.delete(route('resource.destroy', id), { + onSuccess: () => toast.success('已刪除'), + onError: () => toast.error('刪除失敗,請檢查權限'), +}); +``` + +--- + +## 11. 表單規範 + +### 11.1 表單容器 + +```tsx +
+
+ {/* 表單欄位 */} +
+
+``` + +### 11.2 表單欄位 + +```tsx +
+ + setData("field", 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" + /> + {errors.field &&

{errors.field}

} +
+``` + +### 11.3 下拉選單 + +使用 `SearchableSelect` 元件: + +```tsx +import { SearchableSelect } from "@/Components/ui/searchable-select"; + + setData("category_id", value)} + options={categories.map(cat => ({ label: cat.name, value: String(cat.id) }))} + placeholder="請選擇分類" + searchThreshold={10} // 超過 10 個選項才顯示搜尋框 +/> +``` + +--- + +## 11.4 對話框 (Dialog) 滾動與佈局 + +當對話框內容可能超出螢幕高度時(如長表單或詳細資料),**請勿使用 `ScrollArea`**,應直接在 `DialogContent` 使用原生的 `overflow-y-auto`。 + +**原因**:`ScrollArea` 在 Flex 佈局計算高度時容易失效或導致雙重滾動條。以及與原生捲動行為不一致。 + +```tsx +// ❌ 錯誤:使用 ScrollArea 或固定高度計算 + + + {/* 內容 */} + + + +// ✅ 正確:直接使用 overflow-y-auto 與 max-h + + ... +
+ {/* 內容會自動滾動 */} +
+ ... +
+``` + +--- + +## 11.5 輸入框尺寸 (Input Sizes) + +為確保介面整齊與統一,所有表單輸入元件標準高度應為 **`h-9`** (36px),與標準按鈕尺寸對齊。 + +- **Input**: 預設即為 `h-9` (由 `py-1` 與 `text-sm` 組合而成) +- **Select / SearchableSelect**: 必須確保 Trigger 按鈕高度為 `h-9` +- **禁止使用**: 除非有特殊設計需求,否則避免使用 `h-10` (40px) 或其他非標準高度。 + +## 11.6 日期輸入框樣式 (Date Input Style) + +日期輸入框應採用「**左側裝飾圖示 + 右側原生操作**」的配置,以保持視覺一致性並保留瀏覽器原生便利性。 + +**樣式規格**: +1. **容器**: 使用 `relative` 定位。 +2. **圖標**: 使用 `Calendar` 圖標,放置於絕對位置 `absolute left-2.5 top-2.5`,顏色 `text-gray-400`,並設定 `pointer-events-none` 避免干擾點擊。 +3. **輸入框**: 設定 `pl-9` (左內距) 以避開圖示,並使用原生 `type="date"` 或 `type="datetime-local"`。 + +```tsx +import { Calendar } from "lucide-react"; +import { Input } from "@/Components/ui/input"; + +
+ + setDate(e.target.value)} + /> +
+``` + +## 11.7 搜尋選單樣式 (SearchableSelect Style) + +`SearchableSelect` 元件在表單或篩選列中使用時,高度必須設定為 `h-9` 以與輸入框對齊。 + +```tsx + +``` + +## 11.8 篩選列規範 (Filter Bar Norms) + +列表頁面的篩選區域(Filter Bar)應遵循以下規範以節省空間並保持層級清晰: + +1. **標籤文字 (Labels)**: 使用 **`text-xs`** (`12px`) 大小,顏色建議使用 `text-gray-500` 或 `text-grey-2`。這與一般表單 (`text-sm`) 不同,目的是降低篩選列的視覺權重。 +2. **輸入元件高度**: 統一使用 **`h-9`** (`36px`)。 +3. **佈局**: + - **容器內距**: 統一使用 **`p-5`** (`20px`)。 + - **Grid 間距**: 建議使用 **`gap-4`** (`16px`) 或 `gap-6` (`24px`),但同一專案內需統一。本專案推薦 **`gap-4`**。 + - **垂直間距**: Label 與 Input 之間使用 **`space-y-1`** (`4px`)。 + - **排版**: 建議使用 Grid 系統 (`grid-cols-12`) 進行排版。 + +```tsx +
+ + +
+``` + +4. **操作按鈕區 (Action Bar)**: + - **位置**: 位於篩選列最下方。 + - **樣式**: 統一使用 `flex items-center justify-end border-t border-grey-4 pt-5 gap-3`。 + - **說明**: `border-grey-4` 為標準通用邊框色,`pt-5` 與容器 padding (`p-5`) 呼應,維持視覺平衡。 + +5. **收合模式 (Collapsible Mode)**: + - **目的**: 節省垂直空間,預設隱藏較佔空間與低頻使用的篩選器(如日期區間)。 + - **實作**: + - 預設狀態:若無相關篩選值,則預設為 **收合 (Collapsed)**。 + - 切換按鈕:位於 Action Bar 左側 (`mr-auto`)。 + - 樣式:Ghost Button + `ChevronDown`/`ChevronUp` Icon + 提示圓點 (Indicator)。 + - **邏輯**: 若載入頁面時已有被隱藏的篩選值 (e.g. `date_start`),則強制 **展開 (Expanded)** 或顯示提示。 + +--- + +## 12. 檢查清單 + +在開發或審查頁面時,請確認以下項目: + +### ✅ 按鈕 +- [ ] 使用 `button-filled-*` 或 `button-outlined-*` 類別 +- [ ] 主要操作使用 `button-filled-primary` +- [ ] 編輯操作使用 `button-outlined-primary` +- [ ] 刪除操作使用 `button-outlined-error` +- [ ] 按鈕尺寸正確(sm/default/lg) +- [ ] 包含適當的圖標 + +### ✅ 圖標 +- [ ] 全部使用 `lucide-react` +- [ ] 尺寸正確(h-3/h-4/h-5/h-6) +- [ ] 顏色與上下文一致 + +### ✅ 表格 +- [ ] 使用 `@/Components/ui/table` 元件 +- [ ] 有 `bg-white rounded-xl border` 容器 +- [ ] 標題列有 `bg-gray-50` 背景 +- [ ] 序號欄固定寬度並置中 +- [ ] 操作欄使用 `flex justify-center gap-2` +- [ ] 空狀態訊息置中顯示 + +### ✅ 分頁 +- [ ] 使用 `@/Components/shared/Pagination` +- [ ] 有每頁筆數選擇器(10/20/50/100) + +### ✅ 權限 +- [ ] 所有操作按鈕都用 `` 包裹 +- [ ] 權限命名符合 `resource.action` 格式 + +### ✅ 通知 +- [ ] 使用 `toast` 提供操作反饋 +- [ ] 成功/錯誤訊息明確 + +### ✅ 整體 +- [ ] 頁面有標準頭部(標題 + 圖標 + 說明 + 新增按鈕) +- [ ] 容器寬度使用 `max-w-7xl` +- [ ] 使用正確的佈局(`AuthenticatedLayout`) + +--- + +## 13. 常見錯誤與修正 + +### ❌ 錯誤:自定義按鈕樣式 + +```tsx +// ❌ 錯誤 + + +// ✅ 正確 + +``` + +### ❌ 錯誤:混用圖標庫 + +```tsx +// ❌ 錯誤 +import { FaEdit } from 'react-icons/fa'; + + +// ✅ 正確 +import { Pencil } from 'lucide-react'; + +``` + +### ❌ 錯誤:操作欄未置中 + +```tsx +// ❌ 錯誤 + + + + + +// ✅ 正確 + +
+ + +
+
+``` + +### ❌ 錯誤:缺少權限控制 + +```tsx +// ❌ 錯誤 + + +// ✅ 正確 + + + +``` + +--- + +## 14. 參考範例 + +以下頁面展示了完整的 UI 統一性實踐: + +- **使用者管理**:`resources/js/Pages/Admin/User/Index.tsx` +- **角色管理**:`resources/js/Pages/Admin/Role/Index.tsx` +- **產品管理**:`resources/js/Pages/Product/Index.tsx` +- **倉庫管理**:`resources/js/Pages/Warehouse/Index.tsx` + +--- + +## 總結 + +遵循本規範可確保: + +1. ✅ **視覺一致性**:所有頁面看起來像同一個系統 +2. ✅ **維護效率**:使用統一組件,修改一處即可影響全局 +3. ✅ **開發速度**:有明確的模式可循,減少決策時間 +4. ✅ **使用者體驗**:一致的互動模式降低學習成本 +5. ✅ **安全性**:統一的權限控制確保資料安全 + +當你在開發或審查 Star ERP 客戶端後台的 UI 時,請務必參考此規範! diff --git a/.gitea/workflows/deploy.yaml b/.gitea/workflows/deploy.yaml index 92e189e..e7c044d 100644 --- a/.gitea/workflows/deploy.yaml +++ b/.gitea/workflows/deploy.yaml @@ -102,6 +102,7 @@ jobs: # 3. Laravel 初始化與優化 php artisan storage:link && php artisan migrate --force && + php artisan tenants:migrate --force && php artisan db:seed --force && php artisan optimize:clear && php artisan optimize && @@ -195,6 +196,7 @@ jobs: php artisan storage:link && php artisan migrate --force && + php artisan tenants:migrate --force && php artisan optimize:clear && php artisan optimize && php artisan view:cache diff --git a/app/Modules/Inventory/Controllers/ProductController.php b/app/Modules/Inventory/Controllers/ProductController.php index 99685dc..23f4912 100644 --- a/app/Modules/Inventory/Controllers/ProductController.php +++ b/app/Modules/Inventory/Controllers/ProductController.php @@ -25,6 +25,7 @@ class ProductController extends Controller $query->where(function ($q) use ($search) { $q->where('name', 'like', "%{$search}%") ->orWhere('code', 'like', "%{$search}%") + ->orWhere('barcode', 'like', "%{$search}%") ->orWhere('brand', 'like', "%{$search}%"); }); } @@ -66,6 +67,7 @@ class ProductController extends Controller return (object) [ 'id' => (string) $product->id, 'code' => $product->code, + 'barcode' => $product->barcode, 'name' => $product->name, 'categoryId' => $product->category_id, 'category' => $product->category ? (object) [ @@ -110,6 +112,7 @@ class ProductController extends Controller { $validated = $request->validate([ 'code' => 'required|string|max:2|unique:products,code', + 'barcode' => 'required|string|unique:products,barcode', 'name' => 'required|string|max:255', 'category_id' => 'required|exists:categories,id', 'brand' => 'nullable|string|max:255', @@ -123,6 +126,8 @@ class ProductController extends Controller 'code.required' => '商品代號為必填', 'code.max' => '商品代號最多 2 碼', 'code.unique' => '商品代號已存在', + 'barcode.required' => '條碼編號為必填', + 'barcode.unique' => '條碼編號已存在', 'name.required' => '商品名稱為必填', 'category_id.required' => '請選擇分類', 'category_id.exists' => '所選分類不存在', @@ -145,6 +150,7 @@ class ProductController extends Controller { $validated = $request->validate([ 'code' => 'required|string|max:2|unique:products,code,' . $product->id, + 'barcode' => 'required|string|unique:products,barcode,' . $product->id, 'name' => 'required|string|max:255', 'category_id' => 'required|exists:categories,id', 'brand' => 'nullable|string|max:255', @@ -157,6 +163,8 @@ class ProductController extends Controller 'code.required' => '商品代號為必填', 'code.max' => '商品代號最多 2 碼', 'code.unique' => '商品代號已存在', + 'barcode.required' => '條碼編號為必填', + 'barcode.unique' => '條碼編號已存在', 'name.required' => '商品名稱為必填', 'category_id.required' => '請選擇分類', 'category_id.exists' => '所選分類不存在', diff --git a/app/Modules/Inventory/Models/Product.php b/app/Modules/Inventory/Models/Product.php index ce8f790..03b364d 100644 --- a/app/Modules/Inventory/Models/Product.php +++ b/app/Modules/Inventory/Models/Product.php @@ -17,6 +17,7 @@ class Product extends Model protected $fillable = [ 'code', + 'barcode', 'name', 'category_id', 'brand', diff --git a/app/Modules/Production/Controllers/ProductionOrderController.php b/app/Modules/Production/Controllers/ProductionOrderController.php index 862523a..0ea7135 100644 --- a/app/Modules/Production/Controllers/ProductionOrderController.php +++ b/app/Modules/Production/Controllers/ProductionOrderController.php @@ -8,20 +8,28 @@ use App\Modules\Production\Models\ProductionOrder; use App\Modules\Production\Models\ProductionOrderItem; use App\Modules\Inventory\Contracts\InventoryServiceInterface; use App\Modules\Core\Contracts\CoreServiceInterface; +use App\Modules\Procurement\Contracts\ProcurementServiceInterface; use Illuminate\Http\Request; use Illuminate\Support\Facades\DB; use Inertia\Inertia; use Inertia\Response; + class ProductionOrderController extends Controller { protected $inventoryService; protected $coreService; + protected $procurementService; - public function __construct(InventoryServiceInterface $inventoryService, CoreServiceInterface $coreService) + public function __construct( + InventoryServiceInterface $inventoryService, + CoreServiceInterface $coreService, + ProcurementServiceInterface $procurementService + ) { $this->inventoryService = $inventoryService; $this->coreService = $coreService; + $this->procurementService = $procurementService; } /** @@ -37,9 +45,6 @@ class ProductionOrderController extends Controller if ($request->filled('search')) { $search = $request->search; $query->where(function ($q) use ($search) { - $q->where('code', 'like', "%{$search}%") - ->orWhere('output_batch_number', 'like', "%{$search}%"); - // 若要搜尋產品名稱,現在需先從 Inventory 查出 IDs $q->where('code', 'like', "%{$search}%") ->orWhere('output_batch_number', 'like', "%{$search}%"); // 若要搜尋產品名稱,現在需先從 Inventory 查出 IDs @@ -205,15 +210,29 @@ class ProductionOrderController extends Controller // 手動水和明細資料 $items = $productionOrder->items; $inventoryIds = $items->pluck('inventory_id')->unique()->filter()->toArray(); + + // 修正: 移除跨模組關聯 sourcePurchaseOrder.vendor $inventories = $this->inventoryService->getInventoriesByIds( $inventoryIds, - ['product.baseUnit', 'sourcePurchaseOrder.vendor'] + ['product.baseUnit'] )->keyBy('id'); + // 手動載入 Purchase Orders + $poIds = $inventories->pluck('source_purchase_order_id')->unique()->filter()->toArray(); + $purchaseOrders = collect(); + if (!empty($poIds)) { + $purchaseOrders = $this->procurementService->getPurchaseOrdersByIds($poIds, ['vendor'])->keyBy('id'); + } + $units = $this->inventoryService->getUnits()->keyBy('id'); foreach ($items as $item) { $item->inventory = $inventories->get($item->inventory_id); + if ($item->inventory) { + // 手動掛載 PO + $poId = $item->inventory->source_purchase_order_id; + $item->inventory->sourcePurchaseOrder = $purchaseOrders->get($poId); + } $item->unit = $units->get($item->unit_id); } diff --git a/app/Modules/Production/Controllers/RecipeController.php b/app/Modules/Production/Controllers/RecipeController.php index b91165b..10dff11 100644 --- a/app/Modules/Production/Controllers/RecipeController.php +++ b/app/Modules/Production/Controllers/RecipeController.php @@ -188,4 +188,118 @@ class RecipeController extends Controller $recipe->delete(); return redirect()->back()->with('success', '配方已刪除'); } + + /** + * 獲取配方詳細資料 (API) + */ + /** + * 獲取配方詳細資料 (API) + */ + public function show(Recipe $recipe) + { + // Manual Hydration for strict modularity + $recipe->product = $this->inventoryService->getProduct($recipe->product_id); + + $items = $recipe->items; + $productIds = $items->pluck('product_id')->unique()->toArray(); + $products = $this->inventoryService->getProductsByIds($productIds)->keyBy('id'); + $units = $this->inventoryService->getUnits()->keyBy('id'); + + foreach ($items as $item) { + $item->product = $products->get($item->product_id); + $item->unit = $units->get($item->unit_id); + } + + return response()->json($recipe); + } + + /** + * 獲取商品最新有效配方 (API) + */ + public function getLatestByProduct($productId) + { + // 放寬條件,只要 product_id 相符就抓最新的 + $recipe = Recipe::where('product_id', (int)$productId) + ->orderBy('created_at', 'desc') + ->first(); + + if (!$recipe) { + return response()->json(null); + } + + // Load items with product info + $items = $recipe->items; + $productIds = $items->pluck('product_id')->unique()->toArray(); + $products = $this->inventoryService->getProductsByIds($productIds)->keyBy('id'); + + $formattedItems = $items->map(function ($item) use ($products) { + $product = $products->get($item->product_id); + return [ + 'product_id' => $item->product_id, + 'product_name' => $product->name ?? '未知商品', + 'product_code' => $product->code ?? '', + 'quantity' => $item->quantity, + 'unit_id' => $item->unit_id, + 'unit_name' => $product->baseUnit->name ?? '', + ]; + }); + + return response()->json([ + 'id' => $recipe->id, + 'name' => $recipe->name, + 'code' => $recipe->code, + 'yield_quantity' => $recipe->yield_quantity, + 'items' => $formattedItems, + ]); + } + /** + * 獲取商品所有有效配方列表 (API) + */ + public function getByProduct($productId) + { + $recipes = Recipe::where('product_id', (int)$productId) + ->where('is_active', true) + ->orderBy('created_at', 'desc') + ->get(); + + if ($recipes->isEmpty()) { + return response()->json([]); + } + + // 預先載入必要的關聯與數據 + // 為了效能,我們只在列表顯示基本資訊,詳細 Item 資料等選中後再透過 getLatestByProduct (或是重構為 getDetails) 獲取 + // 不過為了前端方便,若配方不多,直接回傳完整結構也可以。 + // 這裡選擇回傳完整結構,因為配方通常不會太多 + + $recipes->load('items'); + + // 收集所有 recipe items 中的 product ids + $allProductIds = $recipes->pluck('items')->flatten()->pluck('product_id')->unique()->toArray(); + $products = $this->inventoryService->getProductsByIds($allProductIds)->keyBy('id'); + + $result = $recipes->map(function ($recipe) use ($products) { + $formattedItems = $recipe->items->map(function ($item) use ($products) { + $product = $products->get($item->product_id); + return [ + 'product_id' => $item->product_id, + 'product_name' => $product->name ?? '未知商品', + 'product_code' => $product->code ?? '', + 'quantity' => $item->quantity, + 'unit_id' => $item->unit_id, + 'unit_name' => $product->baseUnit->name ?? '', + ]; + }); + + return [ + 'id' => $recipe->id, + 'name' => $recipe->name, + 'code' => $recipe->code, + 'yield_quantity' => $recipe->yield_quantity, + 'items' => $formattedItems, + 'created_at' => $recipe->created_at->toIso8601String(), + ]; + }); + + return response()->json($result); + } } diff --git a/app/Modules/Production/Models/RecipeItem.php b/app/Modules/Production/Models/RecipeItem.php index 32520cb..b7c3572 100644 --- a/app/Modules/Production/Models/RecipeItem.php +++ b/app/Modules/Production/Models/RecipeItem.php @@ -27,5 +27,13 @@ class RecipeItem extends Model return $this->belongsTo(Recipe::class); } + public function product() + { + return $this->belongsTo(\App\Modules\Inventory\Models\Product::class); + } + public function unit() + { + return $this->belongsTo(\App\Modules\Inventory\Models\Unit::class); + } } diff --git a/app/Modules/Production/Routes/web.php b/app/Modules/Production/Routes/web.php index 56cdc08..d7c47d5 100644 --- a/app/Modules/Production/Routes/web.php +++ b/app/Modules/Production/Routes/web.php @@ -29,4 +29,10 @@ Route::middleware('auth')->group(function () { Route::get('/api/production/warehouses/{warehouse}/inventories', [ProductionOrderController::class, 'getWarehouseInventories']) ->middleware('permission:production_orders.create') ->name('api.production.warehouses.inventories'); + + Route::get('/api/production/recipes/latest-by-product/{productId}', [RecipeController::class, 'getLatestByProduct']) + ->name('api.production.recipes.latest-by-product'); + + Route::get('/api/production/recipes/by-product/{productId}', [RecipeController::class, 'getByProduct']) + ->name('api.production.recipes.by-product'); }); diff --git a/database/migrations/tenant/2026_01_29_145025_add_barcode_to_products_table.php b/database/migrations/tenant/2026_01_29_145025_add_barcode_to_products_table.php new file mode 100644 index 0000000..eb02b46 --- /dev/null +++ b/database/migrations/tenant/2026_01_29_145025_add_barcode_to_products_table.php @@ -0,0 +1,28 @@ +string('barcode')->nullable()->unique()->index()->after('code')->comment('條碼編號'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('products', function (Blueprint $table) { + $table->dropColumn('barcode'); + }); + } +}; diff --git a/package-lock.json b/package-lock.json index 29748af..045eeca 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,6 +17,7 @@ "@radix-ui/react-radio-group": "^1.3.8", "@radix-ui/react-scroll-area": "^1.2.10", "@radix-ui/react-select": "^2.2.6", + "@radix-ui/react-separator": "^1.1.8", "@radix-ui/react-slot": "^1.2.4", "@radix-ui/react-tooltip": "^1.2.8", "@types/lodash": "^4.17.21", @@ -75,7 +76,6 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz", "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", @@ -1672,6 +1672,52 @@ } } }, + "node_modules/@radix-ui/react-separator": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.8.tgz", + "integrity": "sha512-sDvqVY4itsKwwSMEe0jtKgfTh+72Sy3gPmQpjqcQneqQ4PFmr/1I0YA+2/puilhggCe2gJcx5EBAYFkWkdpa5g==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-primitive": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.4.tgz", + "integrity": "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-slot": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.4.tgz", @@ -2539,7 +2585,6 @@ "integrity": "sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA==", "devOptional": true, "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~7.16.0" } @@ -2550,7 +2595,6 @@ "integrity": "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==", "devOptional": true, "license": "MIT", - "peer": true, "dependencies": { "csstype": "^3.2.2" } @@ -2561,7 +2605,6 @@ "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", "devOptional": true, "license": "MIT", - "peer": true, "peerDependencies": { "@types/react": "^19.2.0" } @@ -2669,7 +2712,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -2882,8 +2924,7 @@ "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/date-fns": { "version": "4.1.0", @@ -3762,7 +3803,6 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -3824,7 +3864,6 @@ "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", "license": "MIT", - "peer": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -3837,7 +3876,6 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", "license": "MIT", - "peer": true, "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" @@ -4330,7 +4368,6 @@ "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.0.tgz", "integrity": "sha512-dZwN5L1VlUBewiP6H9s2+B3e3Jg96D0vzN+Ry73sOefebhYr9f94wwkMNN/9ouoU8pV1BqA1d1zGk8928cx0rg==", "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", diff --git a/package.json b/package.json index 02c53e8..3b7d43f 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,7 @@ "@radix-ui/react-radio-group": "^1.3.8", "@radix-ui/react-scroll-area": "^1.2.10", "@radix-ui/react-select": "^2.2.6", + "@radix-ui/react-separator": "^1.1.8", "@radix-ui/react-slot": "^1.2.4", "@radix-ui/react-tooltip": "^1.2.8", "@types/lodash": "^4.17.21", diff --git a/resources/js/Components/Product/ProductDialog.tsx b/resources/js/Components/Product/ProductDialog.tsx index 932a787..73fdcdb 100644 --- a/resources/js/Components/Product/ProductDialog.tsx +++ b/resources/js/Components/Product/ProductDialog.tsx @@ -1,4 +1,5 @@ import { useEffect } from "react"; +import { Wand2 } from "lucide-react"; import { Dialog, DialogContent, @@ -36,6 +37,7 @@ export default function ProductDialog({ }: ProductDialogProps) { const { data, setData, post, put, processing, errors, reset, clearErrors } = useForm({ code: "", + barcode: "", name: "", category_id: "", brand: "", @@ -52,6 +54,7 @@ export default function ProductDialog({ if (product) { setData({ code: product.code, + barcode: product.barcode || "", name: product.name, category_id: product.categoryId.toString(), brand: product.brand || "", @@ -99,6 +102,11 @@ export default function ProductDialog({ } }; + const generateRandomBarcode = () => { + const randomDigits = Math.floor(Math.random() * 9000000000) + 1000000000; + setData("barcode", randomDigits.toString()); + }; + return ( @@ -159,6 +167,32 @@ export default function ProductDialog({ {errors.code &&

{errors.code}

} +
+ +
+ setData("barcode", e.target.value)} + placeholder="輸入條碼或自動生成" + className={`flex-1 ${errors.barcode ? "border-red-500" : ""}`} + /> + +
+ {errors.barcode &&

{errors.barcode}

} +
+
# - - - + 條碼編號
{item.batch_number || '-'} - {item.system_qty.toFixed(2)} + {item.system_qty.toFixed(0)} {isCompleted ? ( {item.counted_qty} ) : ( updateItem(index, 'counted_qty', e.target.value)} onWheel={(e: any) => e.target.blur()} @@ -284,7 +284,7 @@ export default function Show({ doc }: any) { : 'text-red-600' }`}> {formItem.counted_qty !== '' && formItem.counted_qty !== null - ? diff.toFixed(2) + ? diff.toFixed(0) : '-'} diff --git a/resources/js/Pages/Product/Index.tsx b/resources/js/Pages/Product/Index.tsx index 548cc5e..a27506a 100644 --- a/resources/js/Pages/Product/Index.tsx +++ b/resources/js/Pages/Product/Index.tsx @@ -22,6 +22,7 @@ export interface Category { export interface Product { id: string; code: string; + barcode?: string; name: string; categoryId: number; category?: Category; diff --git a/resources/js/Pages/Production/Create.tsx b/resources/js/Pages/Production/Create.tsx index 59beba4..60cc11e 100644 --- a/resources/js/Pages/Production/Create.tsx +++ b/resources/js/Pages/Production/Create.tsx @@ -4,11 +4,11 @@ */ import { useState, useEffect } from "react"; -import { Factory, Plus, Trash2, ArrowLeft, Save, Calendar, AlertCircle } from 'lucide-react'; +import { Factory, Plus, Trash2, ArrowLeft, Save, Calendar } from 'lucide-react'; import { Button } from "@/Components/ui/button"; import AuthenticatedLayout from "@/Layouts/AuthenticatedLayout"; import { Head, router, useForm } from "@inertiajs/react"; -import toast, { Toaster } from 'react-hot-toast'; +import { toast } from "sonner"; import { getBreadcrumbs } from "@/utils/breadcrumb"; import { SearchableSelect } from "@/Components/ui/searchable-select"; import { Input } from "@/Components/ui/input"; @@ -90,12 +90,17 @@ export default function ProductionCreate({ products, warehouses }: Props) { const [bomItems, setBomItems] = useState([]); + // 多配方支援 + const [recipes, setRecipes] = useState([]); + const [selectedRecipeId, setSelectedRecipeId] = useState(""); + const { data, setData, processing, errors } = useForm({ product_id: "", warehouse_id: "", output_quantity: "", output_batch_number: "", - output_box_count: "", + // 移除 Box Count UI + // 移除相關邏輯 production_date: new Date().toISOString().split('T')[0], expiry_date: "", remark: "", @@ -244,34 +249,116 @@ export default function ProductionCreate({ products, warehouses }: Props) { }))); }, [bomItems]); - // 自動產生成品批號(當選擇商品或日期變動時) + // 應用配方到表單 (獨立函式) + const applyRecipe = (recipe: any) => { + if (!recipe || !recipe.items) return; + + const yieldQty = parseFloat(recipe.yield_quantity || "0") || 1; + // 自動帶入配方標準產量 + setData('output_quantity', String(yieldQty)); + const ratio = 1; + + const newBomItems: BomItem[] = recipe.items.map((item: any) => { + const baseQty = parseFloat(item.quantity || "0"); + const calculatedQty = (baseQty * ratio).toFixed(4); // 保持精度 + + return { + inventory_id: "", + quantity_used: String(calculatedQty), + unit_id: String(item.unit_id), + ui_warehouse_id: selectedWarehouse || "", // 自動帶入目前選擇的倉庫 + ui_product_id: String(item.product_id), + ui_product_name: item.product_name, + ui_batch_number: "", + ui_available_qty: 0, + ui_input_quantity: String(calculatedQty), + ui_selected_unit: 'base', + ui_base_unit_name: item.unit_name, + ui_base_unit_id: item.unit_id, + ui_conversion_rate: 1, + }; + }); + setBomItems(newBomItems); + + // 若有選倉庫,預先載入庫存資料以供選擇 + if (selectedWarehouse) { + fetchWarehouseInventory(selectedWarehouse); + } + + toast.success(`已自動載入配方: ${recipe.name}`, { + description: `標準產量: ${yieldQty} 份` + }); + }; + + // 當手動切換配方時 + useEffect(() => { + if (!selectedRecipeId) return; + const targetRecipe = recipes.find(r => String(r.id) === selectedRecipeId); + if (targetRecipe) { + applyRecipe(targetRecipe); + } + }, [selectedRecipeId]); + + // 自動產生成品批號與載入配方 useEffect(() => { if (!data.product_id) return; + // 1. 自動產生成品批號 const product = products.find(p => String(p.id) === data.product_id); - if (!product) return; + if (product) { + const datePart = data.production_date; + const dateFormatted = datePart.replace(/-/g, ''); + const originCountry = 'TW'; + const warehouseId = selectedWarehouse || (warehouses.length > 0 ? String(warehouses[0].id) : '1'); - const datePart = data.production_date; // YYYY-MM-DD - const dateFormatted = datePart.replace(/-/g, ''); - const originCountry = 'TW'; + fetch(`/api/warehouses/${warehouseId}/inventory/batches/${product.id}?originCountry=${originCountry}&arrivalDate=${datePart}`) + .then(res => res.json()) + .then(result => { + const seq = result.nextSequence || '01'; + const suggested = `${product.code}-${originCountry}-${dateFormatted}-${seq}`; + setData('output_batch_number', suggested); + }) + .catch(() => { + const suggested = `${product.code}-${originCountry}-${dateFormatted}-01`; + setData('output_batch_number', suggested); + }); + } - // 呼叫 API 取得下一組流水號 - // 複用庫存批號 API,但這裡可能沒有選 warehouse,所以用第一個預設 - const warehouseId = selectedWarehouse || (warehouses.length > 0 ? String(warehouses[0].id) : '1'); + // 2. 自動載入配方列表 + const fetchRecipes = async () => { + try { + // 改為抓取所有配方 + const res = await fetch(route('api.production.recipes.by-product', data.product_id)); + const recipesData = await res.json(); - fetch(`/api/warehouses/${warehouseId}/inventory/batches/${product.id}?originCountry=${originCountry}&arrivalDate=${datePart}`) - .then(res => res.json()) - .then(result => { - const seq = result.nextSequence || '01'; - const suggested = `${product.code}-${originCountry}-${dateFormatted}-${seq}`; - setData('output_batch_number', suggested); - }) - .catch(() => { - // Fallback:若 API 失敗,使用預設 01 - const suggested = `${product.code}-${originCountry}-${dateFormatted}-01`; - setData('output_batch_number', suggested); - }); - }, [data.product_id, data.production_date]); + if (Array.isArray(recipesData) && recipesData.length > 0) { + setRecipes(recipesData); + // 預設選取最新的 (第一個) + const latest = recipesData[0]; + setSelectedRecipeId(String(latest.id)); + } else { + // 若無配方 + setRecipes([]); + setSelectedRecipeId(""); + setBomItems([]); // 清空 BOM + } + } catch (e) { + console.error("Failed to fetch recipes", e); + setRecipes([]); + setBomItems([]); + } + }; + fetchRecipes(); + }, [data.product_id]); + + // 當生產數量變動時,如果是從配方載入的,則按比例更新用量 + useEffect(() => { + if (bomItems.length > 0 && data.output_quantity) { + // 這個部位比較複雜,因為使用者可能已經手動修改過或是選了批號 + // 目前先保持簡單:如果使用者改了產出量,我們不強行覆蓋已經選好批號的明細,避免困擾 + // 但如果是剛載入(inventory_id 為空),可以考慮連動?暫時先不處理以維護操作穩定性 + } + }, [data.output_quantity]); // 提交表單 const submit = (status: 'draft' | 'completed') => { @@ -286,12 +373,9 @@ export default function ProductionCreate({ products, warehouses }: Props) { if (bomItems.length === 0) missingFields.push('原物料明細'); if (missingFields.length > 0) { - toast.error( -
- 請填寫必要欄位 - 缺漏:{missingFields.join('、')} -
- ); + toast.error("請填寫必要欄位", { + description: `缺漏:${missingFields.join('、')}` + }); return; } } @@ -313,12 +397,9 @@ export default function ProductionCreate({ products, warehouses }: Props) { }, { onError: (errors) => { const errorCount = Object.keys(errors).length; - toast.error( -
- 建立失敗,請檢查表單 - 共有 {errorCount} 個欄位有誤,請修正後再試 -
- ); + toast.error("建立失敗,請檢查表單", { + description: `共有 ${errorCount} 個欄位有誤,請修正後再試` + }); } }); }; @@ -331,7 +412,7 @@ export default function ProductionCreate({ products, warehouses }: Props) { return ( - +
@@ -394,6 +475,28 @@ export default function ProductionCreate({ products, warehouses }: Props) { className="w-full h-9" /> {errors.product_id &&

{errors.product_id}

} + + {/* 配方選擇 (放在成品商品底下) */} + {recipes.length > 0 && ( +
+
+ + + 切換將重置明細 + +
+ ({ + label: `${r.name} (${r.code})`, + value: String(r.id), + }))} + placeholder="選擇配方" + className="w-full h-9" + /> +
+ )}
@@ -420,15 +523,7 @@ export default function ProductionCreate({ products, warehouses }: Props) { {errors.output_batch_number &&

{errors.output_batch_number}

}
-
- - setData('output_box_count', e.target.value)} - placeholder="例如: 10" - className="h-9" - /> -
+
diff --git a/resources/js/Pages/Production/Recipe/Components/RecipeDetailModal.tsx b/resources/js/Pages/Production/Recipe/Components/RecipeDetailModal.tsx new file mode 100644 index 0000000..765cf52 --- /dev/null +++ b/resources/js/Pages/Production/Recipe/Components/RecipeDetailModal.tsx @@ -0,0 +1,166 @@ +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, +} from "@/Components/ui/dialog"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/Components/ui/table"; +import { Badge } from "@/Components/ui/badge"; +import { Loader2, Package, Calendar, Clock, BookOpen } from "lucide-react"; + +interface RecipeDetailModalProps { + isOpen: boolean; + onClose: () => void; + recipe: any | null; // Detailed recipe object with items + isLoading?: boolean; +} + +export function RecipeDetailModal({ isOpen, onClose, recipe, isLoading }: RecipeDetailModalProps) { + if (!isOpen) return null; + + return ( + + + +
+ + 配方明細 + + {recipe && ( + + {recipe.is_active ? "啟用中" : "已停用"} + + )} +
+ + {/* 現代化元數據條 */} + {recipe && ( +
+
+ + {recipe.code} +
+
+ + 建立於 {new Date(recipe.created_at).toLocaleDateString()} +
+
+ + 更新於 {new Date(recipe.updated_at).toLocaleDateString()} +
+
+ )} +
+ +
+ {isLoading ? ( +
+ +
+ ) : recipe ? ( +
+ {/* 基本資訊區塊 */} +
+ + + + 欄位 + 內容 + + + + + 配方名稱 + {recipe.name} + + + 對應成品 + +
+ {recipe.product?.name || '-'} + {recipe.product?.code} +
+
+
+ + 標準產量 + + {Number(recipe.yield_quantity).toLocaleString()} {recipe.product?.base_unit?.name || '份'} + + + {recipe.description && ( + + 備註說明 + + {recipe.description} + + + )} +
+
+
+ + {/* BOM 表格區塊 */} +
+

+ + 原物料清單 (BOM) +

+
+ + + + 原物料名稱 / 料號 + 標準用量 + 單位 + 備註 + + + + {recipe.items?.length > 0 ? ( + recipe.items.map((item: any, index: number) => ( + + +
+ {item.product?.name || 'Unknown'} + {item.product?.code} +
+
+ + {Number(item.quantity).toLocaleString()} + + + {item.unit?.name || '-'} + + + {item.remark || '-'} + +
+ )) + ) : ( + + + 此配方尚未設定原物料 + + + )} +
+
+
+
+
+ ) : ( +
無法載入配方資料
+ )} +
+
+
+ ); +} diff --git a/resources/js/Pages/Production/Recipe/Index.tsx b/resources/js/Pages/Production/Recipe/Index.tsx index b584ba0..a8d3d30 100644 --- a/resources/js/Pages/Production/Recipe/Index.tsx +++ b/resources/js/Pages/Production/Recipe/Index.tsx @@ -3,7 +3,7 @@ */ import { useState, useEffect } from "react"; -import { Plus, Search, RotateCcw, Pencil, Trash2, BookOpen } from 'lucide-react'; +import { Plus, Search, RotateCcw, 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"; @@ -15,6 +15,8 @@ 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"; +import { RecipeDetailModal } from "./Components/RecipeDetailModal"; +import axios from 'axios'; import { AlertDialog, AlertDialogAction, @@ -59,6 +61,11 @@ export default function RecipeIndex({ recipes, filters }: Props) { const [search, setSearch] = useState(filters.search || ""); const [perPage, setPerPage] = useState(filters.per_page || "10"); + // View Modal State + const [viewRecipe, setViewRecipe] = useState(null); + const [isViewModalOpen, setIsViewModalOpen] = useState(false); + const [isViewLoading, setIsViewLoading] = useState(false); + useEffect(() => { setSearch(filters.search || ""); setPerPage(filters.per_page || "10"); @@ -95,6 +102,20 @@ export default function RecipeIndex({ recipes, filters }: Props) { } }; + const handleView = async (id: number) => { + setIsViewModalOpen(true); + setIsViewLoading(true); + setViewRecipe(null); + try { + const response = await axios.get(route('recipes.show', id)); + setViewRecipe(response.data); + } catch (error) { + console.error("Failed to load recipe details", error); + } finally { + setIsViewLoading(false); + } + }; + return ( @@ -171,7 +192,7 @@ export default function RecipeIndex({ recipes, filters }: Props) { 標準產量 狀態 更新時間 - 操作 + 操作 @@ -221,6 +242,17 @@ export default function RecipeIndex({ recipes, filters }: Props) {
+ + +
+ + setIsViewModalOpen(false)} + recipe={viewRecipe} + isLoading={isViewLoading} + />
); diff --git a/resources/js/Pages/Warehouse/Index.tsx b/resources/js/Pages/Warehouse/Index.tsx index e566804..f75e1c0 100644 --- a/resources/js/Pages/Warehouse/Index.tsx +++ b/resources/js/Pages/Warehouse/Index.tsx @@ -130,7 +130,7 @@ export default function WarehouseIndex({ warehouses, totals, filters }: PageProp
可用庫存總計 - + {totals.available_stock.toLocaleString()}
diff --git a/resources/js/ziggy.js b/resources/js/ziggy.js index f553902..c40d670 100644 --- a/resources/js/ziggy.js +++ b/resources/js/ziggy.js @@ -1,4 +1,4 @@ -const Ziggy = {"url":"http:\/\/star-erp.test:8080","port":8080,"defaults":{},"routes":{"stancl.tenancy.asset":{"uri":"tenancy\/assets\/{path?}","methods":["GET","HEAD"],"wheres":{"path":"(.*)"},"parameters":["path"]},"login":{"uri":"login","methods":["GET","HEAD"]},"logout":{"uri":"logout","methods":["POST"]},"dashboard":{"uri":"\/","methods":["GET","HEAD"]},"profile.edit":{"uri":"profile","methods":["GET","HEAD"]},"profile.update":{"uri":"profile","methods":["PATCH"]},"profile.password":{"uri":"profile\/password","methods":["PUT"]},"roles.index":{"uri":"admin\/roles","methods":["GET","HEAD"]},"roles.create":{"uri":"admin\/roles\/create","methods":["GET","HEAD"]},"roles.store":{"uri":"admin\/roles","methods":["POST"]},"roles.edit":{"uri":"admin\/roles\/{role}\/edit","methods":["GET","HEAD"],"parameters":["role"]},"roles.update":{"uri":"admin\/roles\/{role}","methods":["PUT"],"parameters":["role"]},"roles.destroy":{"uri":"admin\/roles\/{role}","methods":["DELETE"],"parameters":["role"]},"users.index":{"uri":"admin\/users","methods":["GET","HEAD"]},"users.create":{"uri":"admin\/users\/create","methods":["GET","HEAD"]},"users.store":{"uri":"admin\/users","methods":["POST"]},"users.edit":{"uri":"admin\/users\/{user}\/edit","methods":["GET","HEAD"],"parameters":["user"]},"users.update":{"uri":"admin\/users\/{user}","methods":["PUT"],"parameters":["user"]},"users.destroy":{"uri":"admin\/users\/{user}","methods":["DELETE"],"parameters":["user"]},"activity-logs.index":{"uri":"admin\/activity-logs","methods":["GET","HEAD"]},"utility-fees.index":{"uri":"utility-fees","methods":["GET","HEAD"]},"utility-fees.store":{"uri":"utility-fees","methods":["POST"]},"utility-fees.update":{"uri":"utility-fees\/{utility_fee}","methods":["PUT"],"parameters":["utility_fee"],"bindings":{"utility_fee":"id"}},"utility-fees.destroy":{"uri":"utility-fees\/{utility_fee}","methods":["DELETE"],"parameters":["utility_fee"],"bindings":{"utility_fee":"id"}},"accounting.report":{"uri":"accounting-report","methods":["GET","HEAD"]},"accounting.export":{"uri":"accounting-report\/export","methods":["GET","HEAD"]},"categories.index":{"uri":"categories","methods":["GET","HEAD"]},"categories.store":{"uri":"categories","methods":["POST"]},"categories.update":{"uri":"categories\/{category}","methods":["PUT"],"parameters":["category"],"bindings":{"category":"id"}},"categories.destroy":{"uri":"categories\/{category}","methods":["DELETE"],"parameters":["category"],"bindings":{"category":"id"}},"units.store":{"uri":"units","methods":["POST"]},"units.update":{"uri":"units\/{unit}","methods":["PUT"],"parameters":["unit"],"bindings":{"unit":"id"}},"units.destroy":{"uri":"units\/{unit}","methods":["DELETE"],"parameters":["unit"],"bindings":{"unit":"id"}},"products.index":{"uri":"products","methods":["GET","HEAD"]},"products.store":{"uri":"products","methods":["POST"]},"products.update":{"uri":"products\/{product}","methods":["PUT"],"parameters":["product"],"bindings":{"product":"id"}},"products.destroy":{"uri":"products\/{product}","methods":["DELETE"],"parameters":["product"],"bindings":{"product":"id"}},"warehouses.index":{"uri":"warehouses","methods":["GET","HEAD"]},"warehouses.store":{"uri":"warehouses","methods":["POST"]},"warehouses.update":{"uri":"warehouses\/{warehouse}","methods":["PUT"],"parameters":["warehouse"],"bindings":{"warehouse":"id"}},"warehouses.destroy":{"uri":"warehouses\/{warehouse}","methods":["DELETE"],"parameters":["warehouse"],"bindings":{"warehouse":"id"}},"warehouses.inventory.index":{"uri":"warehouses\/{warehouse}\/inventory","methods":["GET","HEAD"],"parameters":["warehouse"],"bindings":{"warehouse":"id"}},"warehouses.inventory.history":{"uri":"warehouses\/{warehouse}\/inventory-history","methods":["GET","HEAD"],"parameters":["warehouse"],"bindings":{"warehouse":"id"}},"warehouses.inventory.create":{"uri":"warehouses\/{warehouse}\/inventory\/create","methods":["GET","HEAD"],"parameters":["warehouse"],"bindings":{"warehouse":"id"}},"warehouses.inventory.store":{"uri":"warehouses\/{warehouse}\/inventory","methods":["POST"],"parameters":["warehouse"],"bindings":{"warehouse":"id"}},"warehouses.inventory.edit":{"uri":"warehouses\/{warehouse}\/inventory\/{inventoryId}\/edit","methods":["GET","HEAD"],"parameters":["warehouse","inventoryId"],"bindings":{"warehouse":"id"}},"warehouses.inventory.update":{"uri":"warehouses\/{warehouse}\/inventory\/{inventoryId}","methods":["PUT"],"parameters":["warehouse","inventoryId"],"bindings":{"warehouse":"id"}},"warehouses.inventory.destroy":{"uri":"warehouses\/{warehouse}\/inventory\/{inventoryId}","methods":["DELETE"],"parameters":["warehouse","inventoryId"],"bindings":{"warehouse":"id"}},"api.warehouses.inventory.batches":{"uri":"api\/warehouses\/{warehouse}\/inventory\/batches\/{productId}","methods":["GET","HEAD"],"parameters":["warehouse","productId"],"bindings":{"warehouse":"id"}},"warehouses.safety-stock.index":{"uri":"warehouses\/{warehouse}\/safety-stock","methods":["GET","HEAD"],"parameters":["warehouse"],"bindings":{"warehouse":"id"}},"warehouses.safety-stock.store":{"uri":"warehouses\/{warehouse}\/safety-stock","methods":["POST"],"parameters":["warehouse"],"bindings":{"warehouse":"id"}},"warehouses.safety-stock.update":{"uri":"warehouses\/{warehouse}\/safety-stock\/{safetyStock}","methods":["PUT"],"parameters":["warehouse","safetyStock"],"bindings":{"warehouse":"id","safetyStock":"id"}},"warehouses.safety-stock.destroy":{"uri":"warehouses\/{warehouse}\/safety-stock\/{safetyStock}","methods":["DELETE"],"parameters":["warehouse","safetyStock"],"bindings":{"warehouse":"id","safetyStock":"id"}},"inventory.count.index":{"uri":"inventory\/count-docs","methods":["GET","HEAD"]},"inventory.count.show":{"uri":"inventory\/count-docs\/{doc}","methods":["GET","HEAD"],"parameters":["doc"],"bindings":{"doc":"id"}},"inventory.count.store":{"uri":"inventory\/count-docs","methods":["POST"]},"inventory.count.update":{"uri":"inventory\/count-docs\/{doc}","methods":["PUT"],"parameters":["doc"],"bindings":{"doc":"id"}},"inventory.count.destroy":{"uri":"inventory\/count-docs\/{doc}","methods":["DELETE"],"parameters":["doc"],"bindings":{"doc":"id"}},"inventory.count.print":{"uri":"inventory\/count-docs\/{doc}\/print","methods":["GET","HEAD"],"parameters":["doc"],"bindings":{"doc":"id"}},"inventory.adjust.index":{"uri":"inventory\/adjust-docs","methods":["GET","HEAD"]},"inventory.adjust.pending-counts":{"uri":"inventory\/adjust-docs\/get-pending-counts","methods":["GET","HEAD"]},"inventory.adjust.show":{"uri":"inventory\/adjust-docs\/{doc}","methods":["GET","HEAD"],"parameters":["doc"],"bindings":{"doc":"id"}},"inventory.adjust.store":{"uri":"inventory\/adjust-docs","methods":["POST"]},"inventory.adjust.update":{"uri":"inventory\/adjust-docs\/{doc}","methods":["PUT"],"parameters":["doc"],"bindings":{"doc":"id"}},"inventory.adjust.destroy":{"uri":"inventory\/adjust-docs\/{doc}","methods":["DELETE"],"parameters":["doc"],"bindings":{"doc":"id"}},"inventory.transfer.index":{"uri":"inventory\/transfer-orders","methods":["GET","HEAD"]},"inventory.transfer.show":{"uri":"inventory\/transfer-orders\/{order}","methods":["GET","HEAD"],"parameters":["order"],"bindings":{"order":"id"}},"inventory.transfer.store":{"uri":"inventory\/transfer-orders","methods":["POST"]},"inventory.transfer.update":{"uri":"inventory\/transfer-orders\/{order}","methods":["PUT"],"parameters":["order"],"bindings":{"order":"id"}},"inventory.transfer.destroy":{"uri":"inventory\/transfer-orders\/{order}","methods":["DELETE"],"parameters":["order"],"bindings":{"order":"id"}},"api.warehouses.inventories":{"uri":"api\/warehouses\/{warehouse}\/inventories","methods":["GET","HEAD"],"parameters":["warehouse"],"bindings":{"warehouse":"id"}},"goods-receipts.index":{"uri":"goods-receipts","methods":["GET","HEAD"]},"goods-receipts.create":{"uri":"goods-receipts\/create","methods":["GET","HEAD"]},"goods-receipts.show":{"uri":"goods-receipts\/{goods_receipt}","methods":["GET","HEAD"],"parameters":["goods_receipt"]},"goods-receipts.store":{"uri":"goods-receipts","methods":["POST"]},"goods-receipts.search-pos":{"uri":"api\/goods-receipts\/search-pos","methods":["GET","HEAD"]},"goods-receipts.search-products":{"uri":"api\/goods-receipts\/search-products","methods":["GET","HEAD"]},"goods-receipts.search-vendors":{"uri":"api\/goods-receipts\/search-vendors","methods":["GET","HEAD"]},"vendors.index":{"uri":"vendors","methods":["GET","HEAD"]},"vendors.show":{"uri":"vendors\/{vendor}","methods":["GET","HEAD"],"parameters":["vendor"],"bindings":{"vendor":"id"}},"vendors.store":{"uri":"vendors","methods":["POST"]},"vendors.update":{"uri":"vendors\/{vendor}","methods":["PUT"],"parameters":["vendor"],"bindings":{"vendor":"id"}},"vendors.destroy":{"uri":"vendors\/{vendor}","methods":["DELETE"],"parameters":["vendor"],"bindings":{"vendor":"id"}},"vendors.products.store":{"uri":"vendors\/{vendor}\/products","methods":["POST"],"parameters":["vendor"],"bindings":{"vendor":"id"}},"vendors.products.update":{"uri":"vendors\/{vendor}\/products\/{product}","methods":["PUT"],"parameters":["vendor","product"],"bindings":{"vendor":"id"}},"vendors.products.destroy":{"uri":"vendors\/{vendor}\/products\/{product}","methods":["DELETE"],"parameters":["vendor","product"],"bindings":{"vendor":"id"}},"purchase-orders.index":{"uri":"purchase-orders","methods":["GET","HEAD"]},"purchase-orders.create":{"uri":"purchase-orders\/create","methods":["GET","HEAD"]},"purchase-orders.store":{"uri":"purchase-orders","methods":["POST"]},"purchase-orders.show":{"uri":"purchase-orders\/{id}","methods":["GET","HEAD"],"parameters":["id"]},"purchase-orders.edit":{"uri":"purchase-orders\/{id}\/edit","methods":["GET","HEAD"],"parameters":["id"]},"purchase-orders.update":{"uri":"purchase-orders\/{id}","methods":["PUT"],"parameters":["id"]},"purchase-orders.destroy":{"uri":"purchase-orders\/{id}","methods":["DELETE"],"parameters":["id"]},"recipes.index":{"uri":"recipes","methods":["GET","HEAD"]},"recipes.create":{"uri":"recipes\/create","methods":["GET","HEAD"]},"recipes.store":{"uri":"recipes","methods":["POST"]},"recipes.show":{"uri":"recipes\/{recipe}","methods":["GET","HEAD"],"parameters":["recipe"]},"recipes.edit":{"uri":"recipes\/{recipe}\/edit","methods":["GET","HEAD"],"parameters":["recipe"],"bindings":{"recipe":"id"}},"recipes.update":{"uri":"recipes\/{recipe}","methods":["PUT","PATCH"],"parameters":["recipe"],"bindings":{"recipe":"id"}},"recipes.destroy":{"uri":"recipes\/{recipe}","methods":["DELETE"],"parameters":["recipe"],"bindings":{"recipe":"id"}},"production-orders.index":{"uri":"production-orders","methods":["GET","HEAD"]},"production-orders.create":{"uri":"production-orders\/create","methods":["GET","HEAD"]},"production-orders.store":{"uri":"production-orders","methods":["POST"]},"production-orders.show":{"uri":"production-orders\/{productionOrder}","methods":["GET","HEAD"],"parameters":["productionOrder"],"bindings":{"productionOrder":"id"}},"production-orders.edit":{"uri":"production-orders\/{productionOrder}\/edit","methods":["GET","HEAD"],"parameters":["productionOrder"],"bindings":{"productionOrder":"id"}},"production-orders.update":{"uri":"production-orders\/{productionOrder}","methods":["PUT"],"parameters":["productionOrder"],"bindings":{"productionOrder":"id"}},"api.production.warehouses.inventories":{"uri":"api\/production\/warehouses\/{warehouse}\/inventories","methods":["GET","HEAD"],"parameters":["warehouse"]},"storage.local":{"uri":"storage\/{path}","methods":["GET","HEAD"],"wheres":{"path":".*"},"parameters":["path"]},"landlord.dashboard":{"uri":"landlord","methods":["GET","HEAD"]},"landlord.tenants.index":{"uri":"landlord\/tenants","methods":["GET","HEAD"]},"landlord.tenants.create":{"uri":"landlord\/tenants\/create","methods":["GET","HEAD"]},"landlord.tenants.store":{"uri":"landlord\/tenants","methods":["POST"]},"landlord.tenants.show":{"uri":"landlord\/tenants\/{tenant}","methods":["GET","HEAD"],"parameters":["tenant"]},"landlord.tenants.edit":{"uri":"landlord\/tenants\/{tenant}\/edit","methods":["GET","HEAD"],"parameters":["tenant"]},"landlord.tenants.update":{"uri":"landlord\/tenants\/{tenant}","methods":["PUT","PATCH"],"parameters":["tenant"]},"landlord.tenants.destroy":{"uri":"landlord\/tenants\/{tenant}","methods":["DELETE"],"parameters":["tenant"]},"landlord.profile.edit":{"uri":"landlord\/profile","methods":["GET","HEAD"]},"landlord.profile.update":{"uri":"landlord\/profile","methods":["PATCH"]},"landlord.profile.password":{"uri":"landlord\/profile\/password","methods":["PUT"]},"landlord.tenants.domains.store":{"uri":"landlord\/tenants\/{tenant}\/domains","methods":["POST"],"parameters":["tenant"]},"landlord.tenants.domains.destroy":{"uri":"landlord\/tenants\/{tenant}\/domains\/{domain}","methods":["DELETE"],"parameters":["tenant","domain"]},"landlord.tenants.branding":{"uri":"landlord\/tenants\/{tenant}\/branding","methods":["GET","HEAD"],"parameters":["tenant"],"bindings":{"tenant":"id"}},"landlord.tenants.branding.update":{"uri":"landlord\/tenants\/{tenant}\/branding","methods":["POST"],"parameters":["tenant"],"bindings":{"tenant":"id"}}}}; +const Ziggy = {"url":"http:\/\/star-erp.test:8080","port":8080,"defaults":{},"routes":{"stancl.tenancy.asset":{"uri":"tenancy\/assets\/{path?}","methods":["GET","HEAD"],"wheres":{"path":"(.*)"},"parameters":["path"]},"login":{"uri":"login","methods":["GET","HEAD"]},"logout":{"uri":"logout","methods":["POST"]},"dashboard":{"uri":"\/","methods":["GET","HEAD"]},"profile.edit":{"uri":"profile","methods":["GET","HEAD"]},"profile.update":{"uri":"profile","methods":["PATCH"]},"profile.password":{"uri":"profile\/password","methods":["PUT"]},"roles.index":{"uri":"admin\/roles","methods":["GET","HEAD"]},"roles.create":{"uri":"admin\/roles\/create","methods":["GET","HEAD"]},"roles.store":{"uri":"admin\/roles","methods":["POST"]},"roles.edit":{"uri":"admin\/roles\/{role}\/edit","methods":["GET","HEAD"],"parameters":["role"]},"roles.update":{"uri":"admin\/roles\/{role}","methods":["PUT"],"parameters":["role"]},"roles.destroy":{"uri":"admin\/roles\/{role}","methods":["DELETE"],"parameters":["role"]},"users.index":{"uri":"admin\/users","methods":["GET","HEAD"]},"users.create":{"uri":"admin\/users\/create","methods":["GET","HEAD"]},"users.store":{"uri":"admin\/users","methods":["POST"]},"users.edit":{"uri":"admin\/users\/{user}\/edit","methods":["GET","HEAD"],"parameters":["user"]},"users.update":{"uri":"admin\/users\/{user}","methods":["PUT"],"parameters":["user"]},"users.destroy":{"uri":"admin\/users\/{user}","methods":["DELETE"],"parameters":["user"]},"activity-logs.index":{"uri":"admin\/activity-logs","methods":["GET","HEAD"]},"utility-fees.index":{"uri":"utility-fees","methods":["GET","HEAD"]},"utility-fees.store":{"uri":"utility-fees","methods":["POST"]},"utility-fees.update":{"uri":"utility-fees\/{utility_fee}","methods":["PUT"],"parameters":["utility_fee"],"bindings":{"utility_fee":"id"}},"utility-fees.destroy":{"uri":"utility-fees\/{utility_fee}","methods":["DELETE"],"parameters":["utility_fee"],"bindings":{"utility_fee":"id"}},"accounting.report":{"uri":"accounting-report","methods":["GET","HEAD"]},"accounting.export":{"uri":"accounting-report\/export","methods":["GET","HEAD"]},"categories.index":{"uri":"categories","methods":["GET","HEAD"]},"categories.store":{"uri":"categories","methods":["POST"]},"categories.update":{"uri":"categories\/{category}","methods":["PUT"],"parameters":["category"],"bindings":{"category":"id"}},"categories.destroy":{"uri":"categories\/{category}","methods":["DELETE"],"parameters":["category"],"bindings":{"category":"id"}},"units.store":{"uri":"units","methods":["POST"]},"units.update":{"uri":"units\/{unit}","methods":["PUT"],"parameters":["unit"],"bindings":{"unit":"id"}},"units.destroy":{"uri":"units\/{unit}","methods":["DELETE"],"parameters":["unit"],"bindings":{"unit":"id"}},"products.index":{"uri":"products","methods":["GET","HEAD"]},"products.store":{"uri":"products","methods":["POST"]},"products.update":{"uri":"products\/{product}","methods":["PUT"],"parameters":["product"],"bindings":{"product":"id"}},"products.destroy":{"uri":"products\/{product}","methods":["DELETE"],"parameters":["product"],"bindings":{"product":"id"}},"warehouses.index":{"uri":"warehouses","methods":["GET","HEAD"]},"warehouses.store":{"uri":"warehouses","methods":["POST"]},"warehouses.update":{"uri":"warehouses\/{warehouse}","methods":["PUT"],"parameters":["warehouse"],"bindings":{"warehouse":"id"}},"warehouses.destroy":{"uri":"warehouses\/{warehouse}","methods":["DELETE"],"parameters":["warehouse"],"bindings":{"warehouse":"id"}},"warehouses.inventory.index":{"uri":"warehouses\/{warehouse}\/inventory","methods":["GET","HEAD"],"parameters":["warehouse"],"bindings":{"warehouse":"id"}},"warehouses.inventory.history":{"uri":"warehouses\/{warehouse}\/inventory-history","methods":["GET","HEAD"],"parameters":["warehouse"],"bindings":{"warehouse":"id"}},"warehouses.inventory.create":{"uri":"warehouses\/{warehouse}\/inventory\/create","methods":["GET","HEAD"],"parameters":["warehouse"],"bindings":{"warehouse":"id"}},"warehouses.inventory.store":{"uri":"warehouses\/{warehouse}\/inventory","methods":["POST"],"parameters":["warehouse"],"bindings":{"warehouse":"id"}},"warehouses.inventory.edit":{"uri":"warehouses\/{warehouse}\/inventory\/{inventoryId}\/edit","methods":["GET","HEAD"],"parameters":["warehouse","inventoryId"],"bindings":{"warehouse":"id"}},"warehouses.inventory.update":{"uri":"warehouses\/{warehouse}\/inventory\/{inventoryId}","methods":["PUT"],"parameters":["warehouse","inventoryId"],"bindings":{"warehouse":"id"}},"warehouses.inventory.destroy":{"uri":"warehouses\/{warehouse}\/inventory\/{inventoryId}","methods":["DELETE"],"parameters":["warehouse","inventoryId"],"bindings":{"warehouse":"id"}},"api.warehouses.inventory.batches":{"uri":"api\/warehouses\/{warehouse}\/inventory\/batches\/{productId}","methods":["GET","HEAD"],"parameters":["warehouse","productId"],"bindings":{"warehouse":"id"}},"warehouses.safety-stock.index":{"uri":"warehouses\/{warehouse}\/safety-stock","methods":["GET","HEAD"],"parameters":["warehouse"],"bindings":{"warehouse":"id"}},"warehouses.safety-stock.store":{"uri":"warehouses\/{warehouse}\/safety-stock","methods":["POST"],"parameters":["warehouse"],"bindings":{"warehouse":"id"}},"warehouses.safety-stock.update":{"uri":"warehouses\/{warehouse}\/safety-stock\/{safetyStock}","methods":["PUT"],"parameters":["warehouse","safetyStock"],"bindings":{"warehouse":"id","safetyStock":"id"}},"warehouses.safety-stock.destroy":{"uri":"warehouses\/{warehouse}\/safety-stock\/{safetyStock}","methods":["DELETE"],"parameters":["warehouse","safetyStock"],"bindings":{"warehouse":"id","safetyStock":"id"}},"inventory.count.index":{"uri":"inventory\/count-docs","methods":["GET","HEAD"]},"inventory.count.show":{"uri":"inventory\/count-docs\/{doc}","methods":["GET","HEAD"],"parameters":["doc"],"bindings":{"doc":"id"}},"inventory.count.store":{"uri":"inventory\/count-docs","methods":["POST"]},"inventory.count.update":{"uri":"inventory\/count-docs\/{doc}","methods":["PUT"],"parameters":["doc"],"bindings":{"doc":"id"}},"inventory.count.destroy":{"uri":"inventory\/count-docs\/{doc}","methods":["DELETE"],"parameters":["doc"],"bindings":{"doc":"id"}},"inventory.count.reopen":{"uri":"inventory\/count-docs\/{doc}\/reopen","methods":["PUT"],"parameters":["doc"],"bindings":{"doc":"id"}},"inventory.count.print":{"uri":"inventory\/count-docs\/{doc}\/print","methods":["GET","HEAD"],"parameters":["doc"],"bindings":{"doc":"id"}},"inventory.adjust.index":{"uri":"inventory\/adjust-docs","methods":["GET","HEAD"]},"inventory.adjust.pending-counts":{"uri":"inventory\/adjust-docs\/get-pending-counts","methods":["GET","HEAD"]},"inventory.adjust.show":{"uri":"inventory\/adjust-docs\/{doc}","methods":["GET","HEAD"],"parameters":["doc"],"bindings":{"doc":"id"}},"inventory.adjust.store":{"uri":"inventory\/adjust-docs","methods":["POST"]},"inventory.adjust.update":{"uri":"inventory\/adjust-docs\/{doc}","methods":["PUT"],"parameters":["doc"],"bindings":{"doc":"id"}},"inventory.adjust.destroy":{"uri":"inventory\/adjust-docs\/{doc}","methods":["DELETE"],"parameters":["doc"],"bindings":{"doc":"id"}},"inventory.transfer.index":{"uri":"inventory\/transfer-orders","methods":["GET","HEAD"]},"inventory.transfer.show":{"uri":"inventory\/transfer-orders\/{order}","methods":["GET","HEAD"],"parameters":["order"],"bindings":{"order":"id"}},"inventory.transfer.store":{"uri":"inventory\/transfer-orders","methods":["POST"]},"inventory.transfer.update":{"uri":"inventory\/transfer-orders\/{order}","methods":["PUT"],"parameters":["order"],"bindings":{"order":"id"}},"inventory.transfer.destroy":{"uri":"inventory\/transfer-orders\/{order}","methods":["DELETE"],"parameters":["order"],"bindings":{"order":"id"}},"api.warehouses.inventories":{"uri":"api\/warehouses\/{warehouse}\/inventories","methods":["GET","HEAD"],"parameters":["warehouse"],"bindings":{"warehouse":"id"}},"goods-receipts.index":{"uri":"goods-receipts","methods":["GET","HEAD"]},"goods-receipts.create":{"uri":"goods-receipts\/create","methods":["GET","HEAD"]},"goods-receipts.show":{"uri":"goods-receipts\/{goods_receipt}","methods":["GET","HEAD"],"parameters":["goods_receipt"]},"goods-receipts.store":{"uri":"goods-receipts","methods":["POST"]},"goods-receipts.search-pos":{"uri":"api\/goods-receipts\/search-pos","methods":["GET","HEAD"]},"goods-receipts.search-products":{"uri":"api\/goods-receipts\/search-products","methods":["GET","HEAD"]},"goods-receipts.search-vendors":{"uri":"api\/goods-receipts\/search-vendors","methods":["GET","HEAD"]},"vendors.index":{"uri":"vendors","methods":["GET","HEAD"]},"vendors.show":{"uri":"vendors\/{vendor}","methods":["GET","HEAD"],"parameters":["vendor"],"bindings":{"vendor":"id"}},"vendors.store":{"uri":"vendors","methods":["POST"]},"vendors.update":{"uri":"vendors\/{vendor}","methods":["PUT"],"parameters":["vendor"],"bindings":{"vendor":"id"}},"vendors.destroy":{"uri":"vendors\/{vendor}","methods":["DELETE"],"parameters":["vendor"],"bindings":{"vendor":"id"}},"vendors.products.store":{"uri":"vendors\/{vendor}\/products","methods":["POST"],"parameters":["vendor"],"bindings":{"vendor":"id"}},"vendors.products.update":{"uri":"vendors\/{vendor}\/products\/{product}","methods":["PUT"],"parameters":["vendor","product"],"bindings":{"vendor":"id"}},"vendors.products.destroy":{"uri":"vendors\/{vendor}\/products\/{product}","methods":["DELETE"],"parameters":["vendor","product"],"bindings":{"vendor":"id"}},"purchase-orders.index":{"uri":"purchase-orders","methods":["GET","HEAD"]},"purchase-orders.create":{"uri":"purchase-orders\/create","methods":["GET","HEAD"]},"purchase-orders.store":{"uri":"purchase-orders","methods":["POST"]},"purchase-orders.show":{"uri":"purchase-orders\/{id}","methods":["GET","HEAD"],"parameters":["id"]},"purchase-orders.edit":{"uri":"purchase-orders\/{id}\/edit","methods":["GET","HEAD"],"parameters":["id"]},"purchase-orders.update":{"uri":"purchase-orders\/{id}","methods":["PUT"],"parameters":["id"]},"purchase-orders.destroy":{"uri":"purchase-orders\/{id}","methods":["DELETE"],"parameters":["id"]},"recipes.index":{"uri":"recipes","methods":["GET","HEAD"]},"recipes.create":{"uri":"recipes\/create","methods":["GET","HEAD"]},"recipes.store":{"uri":"recipes","methods":["POST"]},"recipes.show":{"uri":"recipes\/{recipe}","methods":["GET","HEAD"],"parameters":["recipe"]},"recipes.edit":{"uri":"recipes\/{recipe}\/edit","methods":["GET","HEAD"],"parameters":["recipe"],"bindings":{"recipe":"id"}},"recipes.update":{"uri":"recipes\/{recipe}","methods":["PUT","PATCH"],"parameters":["recipe"],"bindings":{"recipe":"id"}},"recipes.destroy":{"uri":"recipes\/{recipe}","methods":["DELETE"],"parameters":["recipe"],"bindings":{"recipe":"id"}},"production-orders.index":{"uri":"production-orders","methods":["GET","HEAD"]},"production-orders.create":{"uri":"production-orders\/create","methods":["GET","HEAD"]},"production-orders.store":{"uri":"production-orders","methods":["POST"]},"production-orders.show":{"uri":"production-orders\/{productionOrder}","methods":["GET","HEAD"],"parameters":["productionOrder"],"bindings":{"productionOrder":"id"}},"production-orders.edit":{"uri":"production-orders\/{productionOrder}\/edit","methods":["GET","HEAD"],"parameters":["productionOrder"],"bindings":{"productionOrder":"id"}},"production-orders.update":{"uri":"production-orders\/{productionOrder}","methods":["PUT"],"parameters":["productionOrder"],"bindings":{"productionOrder":"id"}},"api.production.warehouses.inventories":{"uri":"api\/production\/warehouses\/{warehouse}\/inventories","methods":["GET","HEAD"],"parameters":["warehouse"]},"api.production.recipes.latest-by-product":{"uri":"api\/production\/recipes\/latest-by-product\/{productId}","methods":["GET","HEAD"],"parameters":["productId"]},"api.production.recipes.by-product":{"uri":"api\/production\/recipes\/by-product\/{productId}","methods":["GET","HEAD"],"parameters":["productId"]},"storage.local":{"uri":"storage\/{path}","methods":["GET","HEAD"],"wheres":{"path":".*"},"parameters":["path"]},"landlord.dashboard":{"uri":"landlord","methods":["GET","HEAD"]},"landlord.tenants.index":{"uri":"landlord\/tenants","methods":["GET","HEAD"]},"landlord.tenants.create":{"uri":"landlord\/tenants\/create","methods":["GET","HEAD"]},"landlord.tenants.store":{"uri":"landlord\/tenants","methods":["POST"]},"landlord.tenants.show":{"uri":"landlord\/tenants\/{tenant}","methods":["GET","HEAD"],"parameters":["tenant"]},"landlord.tenants.edit":{"uri":"landlord\/tenants\/{tenant}\/edit","methods":["GET","HEAD"],"parameters":["tenant"]},"landlord.tenants.update":{"uri":"landlord\/tenants\/{tenant}","methods":["PUT","PATCH"],"parameters":["tenant"]},"landlord.tenants.destroy":{"uri":"landlord\/tenants\/{tenant}","methods":["DELETE"],"parameters":["tenant"]},"landlord.profile.edit":{"uri":"landlord\/profile","methods":["GET","HEAD"]},"landlord.profile.update":{"uri":"landlord\/profile","methods":["PATCH"]},"landlord.profile.password":{"uri":"landlord\/profile\/password","methods":["PUT"]},"landlord.tenants.domains.store":{"uri":"landlord\/tenants\/{tenant}\/domains","methods":["POST"],"parameters":["tenant"]},"landlord.tenants.domains.destroy":{"uri":"landlord\/tenants\/{tenant}\/domains\/{domain}","methods":["DELETE"],"parameters":["tenant","domain"]},"landlord.tenants.branding":{"uri":"landlord\/tenants\/{tenant}\/branding","methods":["GET","HEAD"],"parameters":["tenant"],"bindings":{"tenant":"id"}},"landlord.tenants.branding.update":{"uri":"landlord\/tenants\/{tenant}\/branding","methods":["POST"],"parameters":["tenant"],"bindings":{"tenant":"id"}}}}; if (typeof window !== 'undefined' && typeof window.Ziggy !== 'undefined') { Object.assign(Ziggy.routes, window.Ziggy.routes); }