first commit
This commit is contained in:
74
.agent/rules/framework.md
Normal file
74
.agent/rules/framework.md
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
---
|
||||||
|
trigger: always_on
|
||||||
|
---
|
||||||
|
|
||||||
|
---
|
||||||
|
trigger: always_on
|
||||||
|
---
|
||||||
|
預設專案運行於 WSL2 的 Laravel Sail (Docker) 環境。
|
||||||
|
開發框架規範說明書:ERP 系統 (koori-erp)
|
||||||
|
1. 專案概述
|
||||||
|
目標: 打造一個強大且穩定的 ERP 後台管理系統。
|
||||||
|
|
||||||
|
核心架構: 採用 單體式架構配現代化前端 (Monolith with a Modern Frontend)。使用 Laravel、Inertia.js 及 React。
|
||||||
|
|
||||||
|
工作流程: 將 UI/UX 設計師提供的 React 原始碼,透過 Inertia.js 整合進 Laravel 環境中。
|
||||||
|
|
||||||
|
2. 技術棧 (Tech Stack)
|
||||||
|
後端: PHP 8.5 / Laravel 12
|
||||||
|
|
||||||
|
前端橋樑: Inertia.js (不使用傳統 RESTful API 串接頁面,改用 Inertia 協議)
|
||||||
|
|
||||||
|
前端庫: React (以 Functional Components 與 Hooks 為主)
|
||||||
|
|
||||||
|
樣式處理: Tailwind CSS (確保與 UI/UX 設計稿完全一致)
|
||||||
|
|
||||||
|
資料庫: MySQL 8.0
|
||||||
|
|
||||||
|
開發環境: Laravel Sail (Docker / WSL2)
|
||||||
|
|
||||||
|
未來擴充: 針對高併發或跨平台模組,預留 Golang 微服務接口。
|
||||||
|
|
||||||
|
3. 目錄結構與慣例
|
||||||
|
3.1 後端 (Laravel)
|
||||||
|
Controllers: 必須回傳 Inertia::render() 來渲染頁面。
|
||||||
|
|
||||||
|
Models: 嚴格執行型別標註,使用 Eloquent 進行資料庫操作。
|
||||||
|
|
||||||
|
Routes: 統一在 routes/web.php 定義 Inertia 路由。
|
||||||
|
|
||||||
|
3.2 前端 (React)
|
||||||
|
Pages (頁面): 位於 resources/js/Pages/。每個檔案代表一個完整的路由視圖。
|
||||||
|
|
||||||
|
Components (組件): 位於 resources/js/Components/。存放由 UI/UX 團隊提供的可重複使用 UI 元件。
|
||||||
|
|
||||||
|
Layouts (版面): 位於 resources/js/Layouts/。定義 ERP 的通用版面(例如:包含側邊欄 Sidebar 與導覽列 Navbar 的後台主框架)。
|
||||||
|
|
||||||
|
4. 整合指南 (UI/UX 轉換至 Laravel)
|
||||||
|
組件遷移: 將 UI/UX 的 React 原始碼移入 resources/js/ 時,應進行「原子化」拆解,提高元件複用率。
|
||||||
|
|
||||||
|
資料傳遞: 透過 Laravel Controller 的 props 傳送動態資料給 React。除非是後續的異步請求,否則避免在 React 初次渲染時使用 axios 抓取資料,應優先使用 Inertia 的資料流。
|
||||||
|
|
||||||
|
狀態管理: 優先使用 Inertia 內建的狀態管理與 useForm hook 處理表單提交。
|
||||||
|
|
||||||
|
5. 開發標準 (Coding Standards)
|
||||||
|
命名規範:
|
||||||
|
|
||||||
|
Controllers: PascalCaseController.php
|
||||||
|
|
||||||
|
React Components: PascalCase.jsx
|
||||||
|
|
||||||
|
Routes: kebab-case (小寫橫線分隔)
|
||||||
|
|
||||||
|
回傳格式: 所有的前後端溝通需維持一致的 JSON 結構,特別是驗證錯誤 (Validation Errors) 與閃存訊息 (Flash Messages)。
|
||||||
|
|
||||||
|
6. AI 協作規則 (給 Antigravity AI)
|
||||||
|
角色設定: 你是一位專業的全端開發工程師助手。
|
||||||
|
|
||||||
|
代碼生成指令:
|
||||||
|
|
||||||
|
所有的解釋說明請使用 繁體中文。
|
||||||
|
|
||||||
|
生成 React 組件時,必須符合專案現有的 Tailwind CSS 配置。
|
||||||
|
|
||||||
|
必須考慮 ERP 邏輯(例如:權限判斷、操作日誌、資料完整性)。
|
||||||
27
.devcontainer/devcontainer.json
Normal file
27
.devcontainer/devcontainer.json
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
// https://aka.ms/devcontainer.json
|
||||||
|
{
|
||||||
|
"name": "Existing Docker Compose (Extend)",
|
||||||
|
"dockerComposeFile": [
|
||||||
|
"../compose.yaml"
|
||||||
|
],
|
||||||
|
"service": "laravel.test",
|
||||||
|
"workspaceFolder": "/var/www/html",
|
||||||
|
"customizations": {
|
||||||
|
"vscode": {
|
||||||
|
"extensions": [
|
||||||
|
// "laravel.vscode-laravel",
|
||||||
|
// "mikestead.dotenv",
|
||||||
|
// "amiralizadeh9480.laravel-extra-intellisense",
|
||||||
|
// "ryannaddy.laravel-artisan",
|
||||||
|
// "onecentlin.laravel5-snippets",
|
||||||
|
// "onecentlin.laravel-blade"
|
||||||
|
],
|
||||||
|
"settings": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"remoteUser": "sail",
|
||||||
|
"postCreateCommand": "chown -R 1000:1000 /var/www/html 2>/dev/null || true"
|
||||||
|
// "forwardPorts": [],
|
||||||
|
// "runServices": [],
|
||||||
|
// "shutdownAction": "none",
|
||||||
|
}
|
||||||
18
.editorconfig
Normal file
18
.editorconfig
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
charset = utf-8
|
||||||
|
end_of_line = lf
|
||||||
|
indent_size = 4
|
||||||
|
indent_style = space
|
||||||
|
insert_final_newline = true
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
|
||||||
|
[*.md]
|
||||||
|
trim_trailing_whitespace = false
|
||||||
|
|
||||||
|
[*.{yml,yaml}]
|
||||||
|
indent_size = 2
|
||||||
|
|
||||||
|
[compose.yaml]
|
||||||
|
indent_size = 4
|
||||||
65
.env.example
Normal file
65
.env.example
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
APP_NAME=Laravel
|
||||||
|
APP_ENV=local
|
||||||
|
APP_KEY=
|
||||||
|
APP_DEBUG=true
|
||||||
|
APP_URL=http://localhost
|
||||||
|
|
||||||
|
APP_LOCALE=en
|
||||||
|
APP_FALLBACK_LOCALE=en
|
||||||
|
APP_FAKER_LOCALE=en_US
|
||||||
|
|
||||||
|
APP_MAINTENANCE_DRIVER=file
|
||||||
|
# APP_MAINTENANCE_STORE=database
|
||||||
|
|
||||||
|
# PHP_CLI_SERVER_WORKERS=4
|
||||||
|
|
||||||
|
BCRYPT_ROUNDS=12
|
||||||
|
|
||||||
|
LOG_CHANNEL=stack
|
||||||
|
LOG_STACK=single
|
||||||
|
LOG_DEPRECATIONS_CHANNEL=null
|
||||||
|
LOG_LEVEL=debug
|
||||||
|
|
||||||
|
DB_CONNECTION=sqlite
|
||||||
|
# DB_HOST=127.0.0.1
|
||||||
|
# DB_PORT=3306
|
||||||
|
# DB_DATABASE=laravel
|
||||||
|
# DB_USERNAME=root
|
||||||
|
# DB_PASSWORD=
|
||||||
|
|
||||||
|
SESSION_DRIVER=database
|
||||||
|
SESSION_LIFETIME=120
|
||||||
|
SESSION_ENCRYPT=false
|
||||||
|
SESSION_PATH=/
|
||||||
|
SESSION_DOMAIN=null
|
||||||
|
|
||||||
|
BROADCAST_CONNECTION=log
|
||||||
|
FILESYSTEM_DISK=local
|
||||||
|
QUEUE_CONNECTION=database
|
||||||
|
|
||||||
|
CACHE_STORE=database
|
||||||
|
# CACHE_PREFIX=
|
||||||
|
|
||||||
|
MEMCACHED_HOST=127.0.0.1
|
||||||
|
|
||||||
|
REDIS_CLIENT=phpredis
|
||||||
|
REDIS_HOST=127.0.0.1
|
||||||
|
REDIS_PASSWORD=null
|
||||||
|
REDIS_PORT=6379
|
||||||
|
|
||||||
|
MAIL_MAILER=log
|
||||||
|
MAIL_SCHEME=null
|
||||||
|
MAIL_HOST=127.0.0.1
|
||||||
|
MAIL_PORT=2525
|
||||||
|
MAIL_USERNAME=null
|
||||||
|
MAIL_PASSWORD=null
|
||||||
|
MAIL_FROM_ADDRESS="hello@example.com"
|
||||||
|
MAIL_FROM_NAME="${APP_NAME}"
|
||||||
|
|
||||||
|
AWS_ACCESS_KEY_ID=
|
||||||
|
AWS_SECRET_ACCESS_KEY=
|
||||||
|
AWS_DEFAULT_REGION=us-east-1
|
||||||
|
AWS_BUCKET=
|
||||||
|
AWS_USE_PATH_STYLE_ENDPOINT=false
|
||||||
|
|
||||||
|
VITE_APP_NAME="${APP_NAME}"
|
||||||
11
.gitattributes
vendored
Normal file
11
.gitattributes
vendored
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
* text=auto eol=lf
|
||||||
|
|
||||||
|
*.blade.php diff=html
|
||||||
|
*.css diff=css
|
||||||
|
*.html diff=html
|
||||||
|
*.md diff=markdown
|
||||||
|
*.php diff=php
|
||||||
|
|
||||||
|
/.github export-ignore
|
||||||
|
CHANGELOG.md export-ignore
|
||||||
|
.styleci.yml export-ignore
|
||||||
24
.gitignore
vendored
Normal file
24
.gitignore
vendored
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
*.log
|
||||||
|
.DS_Store
|
||||||
|
.env
|
||||||
|
.env.backup
|
||||||
|
.env.production
|
||||||
|
.phpactor.json
|
||||||
|
.phpunit.result.cache
|
||||||
|
/.fleet
|
||||||
|
/.idea
|
||||||
|
/.nova
|
||||||
|
/.phpunit.cache
|
||||||
|
/.vscode
|
||||||
|
/.zed
|
||||||
|
/auth.json
|
||||||
|
/node_modules
|
||||||
|
/public/build
|
||||||
|
/public/hot
|
||||||
|
/public/storage
|
||||||
|
/storage/*.key
|
||||||
|
/storage/pail
|
||||||
|
/vendor
|
||||||
|
Homestead.json
|
||||||
|
Homestead.yaml
|
||||||
|
Thumbs.db
|
||||||
81
README.md
Normal file
81
README.md
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
# Koori ERP
|
||||||
|
|
||||||
|
本專案是一個基於 Laravel 12, Inertia.js (React) 與 Tailwind CSS 開發的 ERP 系統。
|
||||||
|
|
||||||
|
## 開發環境需求
|
||||||
|
|
||||||
|
- **WSL2** (Windows 建議環境)
|
||||||
|
- **Docker Desktop** 或 **Docker Engine**
|
||||||
|
- **PHP 8.5+** (本地端若需執行基礎 composer 指令,或直接使用 Sail 容器)
|
||||||
|
- **Node.js 20+**
|
||||||
|
|
||||||
|
## 啟動步驟
|
||||||
|
|
||||||
|
本專案使用 [Laravel Sail](https://laravel.com/docs/12.x/sail) 作為 Docker 開發環境。
|
||||||
|
|
||||||
|
### 1. 安裝依賴 (初次啟動)
|
||||||
|
|
||||||
|
如果您是第一次 clone 專案,請先安裝 PHP 與 JS 依賴:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 安裝 PHP 依賴 (透過暫時的容器)
|
||||||
|
docker run --rm \
|
||||||
|
-u "$(id -u):$(id -g)" \
|
||||||
|
-v "$(pwd):/var/www/html" \
|
||||||
|
-w /var/www/html \
|
||||||
|
laravelsail/php84-composer:latest \
|
||||||
|
composer install --ignore-platform-reqs
|
||||||
|
|
||||||
|
# 初始化 .env 檔案
|
||||||
|
cp .env.example .env
|
||||||
|
|
||||||
|
# 生成 App Key
|
||||||
|
./vendor/bin/sail artisan key:generate
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 啟動 Docker 容器
|
||||||
|
|
||||||
|
在專案根目錄執行:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 背景執行容器
|
||||||
|
./vendor/bin/sail up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 資料庫遷移與初始化
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./vendor/bin/sail artisan migrate
|
||||||
|
# (選填) 如果有種子資料
|
||||||
|
# ./vendor/bin/sail artisan db:seed
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. 啟動前端開發伺服器 (Vite)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./vendor/bin/sail npm install
|
||||||
|
./vendor/bin/sail npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
啟動後,您可以透過以下連結瀏覽專案:
|
||||||
|
- **後台網址**: [http://localhost](http://localhost)
|
||||||
|
- **Vite 伺服器**: [http://localhost:5174](http://localhost:5174)
|
||||||
|
|
||||||
|
## 常用 Sail 指令
|
||||||
|
|
||||||
|
- **停止服務**: `./vendor/bin/sail stop`
|
||||||
|
- **執行 Artisan 指令**: `./vendor/bin/sail artisan ...`
|
||||||
|
- **執行 Composer 指令**: `./vendor/bin/sail composer ...`
|
||||||
|
- **執行測試**: `./vendor/bin/sail test`
|
||||||
|
|
||||||
|
## 技術棧
|
||||||
|
|
||||||
|
- **Backend**: Laravel 12
|
||||||
|
- **Frontend**: React (Functional Components) via Inertia.js
|
||||||
|
- **Styling**: Tailwind CSS
|
||||||
|
- **Database**: MySQL 8.0
|
||||||
|
- **Cache/Session**: Redis
|
||||||
|
|
||||||
|
## 開發規範
|
||||||
|
|
||||||
|
請參考專案內的開發文件或 AI 指導規則,確保 UI/UX 元件與後端邏輯符合專案架構。
|
||||||
54
app/Http/Controllers/CategoryController.php
Normal file
54
app/Http/Controllers/CategoryController.php
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Models\Category;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
|
class CategoryController extends Controller
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Store a newly created resource in storage.
|
||||||
|
*/
|
||||||
|
public function store(Request $request)
|
||||||
|
{
|
||||||
|
$validated = $request->validate([
|
||||||
|
'name' => 'required|string|max:255|unique:categories,name',
|
||||||
|
]);
|
||||||
|
|
||||||
|
Category::create([
|
||||||
|
'name' => $validated['name'],
|
||||||
|
'is_active' => true,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return redirect()->back()->with('success', '分類已建立');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the specified resource in storage.
|
||||||
|
*/
|
||||||
|
public function update(Request $request, Category $category)
|
||||||
|
{
|
||||||
|
$validated = $request->validate([
|
||||||
|
'name' => 'required|string|max:255|unique:categories,name,' . $category->id,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$category->update($validated);
|
||||||
|
|
||||||
|
return redirect()->back()->with('success', '分類已更新');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove the specified resource from storage.
|
||||||
|
*/
|
||||||
|
public function destroy(Category $category)
|
||||||
|
{
|
||||||
|
if ($category->products()->count() > 0) {
|
||||||
|
return redirect()->back()->with('error', '該分類下尚有商品,無法刪除');
|
||||||
|
}
|
||||||
|
|
||||||
|
$category->delete();
|
||||||
|
|
||||||
|
return redirect()->back()->with('success', '分類已刪除');
|
||||||
|
}
|
||||||
|
}
|
||||||
8
app/Http/Controllers/Controller.php
Normal file
8
app/Http/Controllers/Controller.php
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
abstract class Controller
|
||||||
|
{
|
||||||
|
//
|
||||||
|
}
|
||||||
325
app/Http/Controllers/InventoryController.php
Normal file
325
app/Http/Controllers/InventoryController.php
Normal file
@@ -0,0 +1,325 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
|
class InventoryController extends Controller
|
||||||
|
{
|
||||||
|
public function index(\Illuminate\Http\Request $request, \App\Models\Warehouse $warehouse)
|
||||||
|
{
|
||||||
|
$warehouse->load([
|
||||||
|
'inventories.product.category',
|
||||||
|
'inventories.lastIncomingTransaction',
|
||||||
|
'inventories.lastOutgoingTransaction'
|
||||||
|
]);
|
||||||
|
$allProducts = \App\Models\Product::with('category')->get();
|
||||||
|
|
||||||
|
// 1. 準備 availableProducts
|
||||||
|
$availableProducts = $allProducts->map(function ($product) {
|
||||||
|
return [
|
||||||
|
'id' => (string) $product->id, // Frontend expects string
|
||||||
|
'name' => $product->name,
|
||||||
|
'type' => $product->category ? $product->category->name : '其他', // 暫時用 Category Name 當 Type
|
||||||
|
];
|
||||||
|
});
|
||||||
|
|
||||||
|
// 2. 準備 inventories (模擬批號)
|
||||||
|
// 2. 準備 inventories
|
||||||
|
// 資料庫結構為 (warehouse_id, product_id) 唯一,故為扁平列表
|
||||||
|
$inventories = $warehouse->inventories->map(function ($inv) {
|
||||||
|
return [
|
||||||
|
'id' => (string) $inv->id,
|
||||||
|
'warehouseId' => (string) $inv->warehouse_id,
|
||||||
|
'productId' => (string) $inv->product_id,
|
||||||
|
'productName' => $inv->product->name,
|
||||||
|
'productCode' => $inv->product->code ?? 'N/A',
|
||||||
|
'unit' => $inv->product->base_unit ?? '個',
|
||||||
|
'quantity' => (float) $inv->quantity,
|
||||||
|
'safetyStock' => $inv->safety_stock !== null ? (float) $inv->safety_stock : null,
|
||||||
|
'status' => '正常', // 前端會根據 quantity 與 safetyStock 重算,但後端亦可提供基礎狀態
|
||||||
|
'batchNumber' => 'BATCH-' . $inv->id, // DB 無批號,暫時模擬,某些 UI 可能還會用到
|
||||||
|
'expiryDate' => '2099-12-31', // DB 無效期,暫時模擬
|
||||||
|
'lastInboundDate' => $inv->lastIncomingTransaction ? ($inv->lastIncomingTransaction->actual_time ? $inv->lastIncomingTransaction->actual_time->format('Y-m-d') : $inv->lastIncomingTransaction->created_at->format('Y-m-d')) : null,
|
||||||
|
'lastOutboundDate' => $inv->lastOutgoingTransaction ? ($inv->lastOutgoingTransaction->actual_time ? $inv->lastOutgoingTransaction->actual_time->format('Y-m-d') : $inv->lastOutgoingTransaction->created_at->format('Y-m-d')) : null,
|
||||||
|
];
|
||||||
|
});
|
||||||
|
|
||||||
|
// 3. 準備 safetyStockSettings
|
||||||
|
$safetyStockSettings = $warehouse->inventories->filter(function($inv) {
|
||||||
|
return !is_null($inv->safety_stock);
|
||||||
|
})->map(function ($inv) {
|
||||||
|
return [
|
||||||
|
'id' => 'ss-' . $inv->id,
|
||||||
|
'warehouseId' => (string) $inv->warehouse_id,
|
||||||
|
'productId' => (string) $inv->product_id,
|
||||||
|
'productName' => $inv->product->name,
|
||||||
|
'productType' => $inv->product->category ? $inv->product->category->name : '其他',
|
||||||
|
'safetyStock' => (float) $inv->safety_stock,
|
||||||
|
'createdAt' => $inv->created_at->toIso8601String(),
|
||||||
|
'updatedAt' => $inv->updated_at->toIso8601String(),
|
||||||
|
];
|
||||||
|
})->values();
|
||||||
|
|
||||||
|
return \Inertia\Inertia::render('Warehouse/Inventory', [
|
||||||
|
'warehouse' => $warehouse,
|
||||||
|
'inventories' => $inventories,
|
||||||
|
'safetyStockSettings' => $safetyStockSettings,
|
||||||
|
'availableProducts' => $availableProducts,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function create(\App\Models\Warehouse $warehouse)
|
||||||
|
{
|
||||||
|
// 取得所有商品供前端選單使用
|
||||||
|
$products = \App\Models\Product::select('id', 'name', 'base_unit')->get()->map(function ($product) {
|
||||||
|
return [
|
||||||
|
'id' => (string) $product->id,
|
||||||
|
'name' => $product->name,
|
||||||
|
'unit' => $product->base_unit,
|
||||||
|
];
|
||||||
|
});
|
||||||
|
|
||||||
|
return \Inertia\Inertia::render('Warehouse/AddInventory', [
|
||||||
|
'warehouse' => $warehouse,
|
||||||
|
'products' => $products,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function store(\Illuminate\Http\Request $request, \App\Models\Warehouse $warehouse)
|
||||||
|
{
|
||||||
|
$validated = $request->validate([
|
||||||
|
'inboundDate' => 'required|date',
|
||||||
|
'reason' => 'required|string',
|
||||||
|
'notes' => 'nullable|string',
|
||||||
|
'items' => 'required|array|min:1',
|
||||||
|
'items.*.productId' => 'required|exists:products,id',
|
||||||
|
'items.*.quantity' => 'required|numeric|min:0.01',
|
||||||
|
]);
|
||||||
|
|
||||||
|
return \Illuminate\Support\Facades\DB::transaction(function () use ($validated, $warehouse) {
|
||||||
|
foreach ($validated['items'] as $item) {
|
||||||
|
// 取得或建立庫存紀錄
|
||||||
|
$inventory = $warehouse->inventories()->firstOrCreate(
|
||||||
|
['product_id' => $item['productId']],
|
||||||
|
['quantity' => 0, 'safety_stock' => null]
|
||||||
|
);
|
||||||
|
|
||||||
|
$currentQty = $inventory->quantity;
|
||||||
|
$newQty = $currentQty + $item['quantity'];
|
||||||
|
|
||||||
|
// 更新庫存
|
||||||
|
$inventory->update(['quantity' => $newQty]);
|
||||||
|
|
||||||
|
// 寫入異動紀錄
|
||||||
|
$inventory->transactions()->create([
|
||||||
|
'type' => '手動入庫',
|
||||||
|
'quantity' => $item['quantity'],
|
||||||
|
'balance_before' => $currentQty,
|
||||||
|
'balance_after' => $newQty,
|
||||||
|
'reason' => $validated['reason'] . ($validated['notes'] ? ' - ' . $validated['notes'] : ''),
|
||||||
|
'actual_time' => $validated['inboundDate'],
|
||||||
|
'user_id' => auth()->id(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return redirect()->route('warehouses.inventory.index', $warehouse->id)
|
||||||
|
->with('success', '庫存記錄已儲存成功');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function edit(\App\Models\Warehouse $warehouse, $inventoryId)
|
||||||
|
{
|
||||||
|
// 取得庫存紀錄,包含商品資訊與異動紀錄 (含經手人)
|
||||||
|
// 這裡如果是 Mock 的 ID (mock-inv-1),需要特殊處理
|
||||||
|
if (str_starts_with($inventoryId, 'mock-inv-')) {
|
||||||
|
return redirect()->back()->with('error', '無法編輯範例資料');
|
||||||
|
}
|
||||||
|
|
||||||
|
$inventory = \App\Models\Inventory::with(['product', 'transactions' => function($query) {
|
||||||
|
$query->orderBy('actual_time', 'desc')->orderBy('id', 'desc');
|
||||||
|
}, 'transactions.user'])->findOrFail($inventoryId);
|
||||||
|
|
||||||
|
// 轉換為前端需要的格式
|
||||||
|
$inventoryData = [
|
||||||
|
'id' => (string) $inventory->id,
|
||||||
|
'warehouseId' => (string) $inventory->warehouse_id,
|
||||||
|
'productId' => (string) $inventory->product_id,
|
||||||
|
'productName' => $inventory->product->name,
|
||||||
|
'quantity' => (float) $inventory->quantity,
|
||||||
|
'batchNumber' => 'BATCH-' . $inventory->id, // Mock
|
||||||
|
'expiryDate' => '2099-12-31', // Mock
|
||||||
|
'lastInboundDate' => $inventory->updated_at->format('Y-m-d'),
|
||||||
|
'lastOutboundDate' => null,
|
||||||
|
];
|
||||||
|
|
||||||
|
// 整理異動紀錄
|
||||||
|
$transactions = $inventory->transactions->map(function ($tx) {
|
||||||
|
return [
|
||||||
|
'id' => (string) $tx->id,
|
||||||
|
'type' => $tx->type,
|
||||||
|
'quantity' => (float) $tx->quantity,
|
||||||
|
'balanceAfter' => (float) $tx->balance_after,
|
||||||
|
'reason' => $tx->reason,
|
||||||
|
'userName' => $tx->user ? $tx->user->name : '系統',
|
||||||
|
'actualTime' => $tx->actual_time ? $tx->actual_time->format('Y-m-d H:i') : $tx->created_at->format('Y-m-d H:i'),
|
||||||
|
];
|
||||||
|
});
|
||||||
|
|
||||||
|
return \Inertia\Inertia::render('Warehouse/EditInventory', [
|
||||||
|
'warehouse' => $warehouse,
|
||||||
|
'inventory' => $inventoryData,
|
||||||
|
'transactions' => $transactions,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function update(\Illuminate\Http\Request $request, \App\Models\Warehouse $warehouse, $inventoryId)
|
||||||
|
{
|
||||||
|
// 若是 product ID (舊邏輯),先轉為 inventory
|
||||||
|
// 但新路由我們傳的是 inventory ID
|
||||||
|
// 為了相容,我們先判斷 $inventoryId 是 inventory ID
|
||||||
|
|
||||||
|
$inventory = \App\Models\Inventory::find($inventoryId);
|
||||||
|
|
||||||
|
// 如果找不到 (可能是舊路由傳 product ID)
|
||||||
|
if (!$inventory) {
|
||||||
|
$inventory = $warehouse->inventories()->where('product_id', $inventoryId)->first();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$inventory) {
|
||||||
|
return redirect()->back()->with('error', '找不到庫存紀錄');
|
||||||
|
}
|
||||||
|
|
||||||
|
$validated = $request->validate([
|
||||||
|
'quantity' => 'required|numeric|min:0',
|
||||||
|
// 以下欄位改為 nullable,支援新表單
|
||||||
|
'type' => 'nullable|string',
|
||||||
|
'operation' => 'nullable|in:add,subtract,set',
|
||||||
|
'reason' => 'nullable|string',
|
||||||
|
'notes' => 'nullable|string',
|
||||||
|
// 新增日期欄位驗證 (雖然暫不儲存到 DB)
|
||||||
|
'batchNumber' => 'nullable|string',
|
||||||
|
'expiryDate' => 'nullable|date',
|
||||||
|
'lastInboundDate' => 'nullable|date',
|
||||||
|
'lastOutboundDate' => 'nullable|date',
|
||||||
|
]);
|
||||||
|
|
||||||
|
return \Illuminate\Support\Facades\DB::transaction(function () use ($validated, $inventory) {
|
||||||
|
$currentQty = $inventory->quantity;
|
||||||
|
$newQty = $validated['quantity'];
|
||||||
|
|
||||||
|
// 判斷操作模式
|
||||||
|
if (isset($validated['operation'])) {
|
||||||
|
$changeQty = 0;
|
||||||
|
switch ($validated['operation']) {
|
||||||
|
case 'add':
|
||||||
|
$changeQty = $validated['quantity'];
|
||||||
|
$newQty = $currentQty + $changeQty;
|
||||||
|
break;
|
||||||
|
case 'subtract':
|
||||||
|
$changeQty = -$validated['quantity'];
|
||||||
|
$newQty = $currentQty + $changeQty;
|
||||||
|
break;
|
||||||
|
case 'set':
|
||||||
|
$changeQty = $newQty - $currentQty;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 來自編輯頁面,直接 Set
|
||||||
|
$changeQty = $newQty - $currentQty;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新庫存
|
||||||
|
$inventory->update(['quantity' => $newQty]);
|
||||||
|
|
||||||
|
// 異動類型映射
|
||||||
|
$type = $validated['type'] ?? 'adjustment';
|
||||||
|
$typeMapping = [
|
||||||
|
'adjustment' => '盤點調整',
|
||||||
|
'purchase_in' => '採購進貨',
|
||||||
|
'sales_out' => '銷售出庫',
|
||||||
|
'return_in' => '退貨入庫',
|
||||||
|
'return_out' => '退貨出庫',
|
||||||
|
'transfer_in' => '撥補入庫',
|
||||||
|
'transfer_out' => '撥補出庫',
|
||||||
|
];
|
||||||
|
$chineseType = $typeMapping[$type] ?? $type;
|
||||||
|
|
||||||
|
// 如果是編輯頁面來的,可能沒有 type,預設為 "盤點調整" 或 "手動編輯"
|
||||||
|
if (!isset($validated['type'])) {
|
||||||
|
$chineseType = '手動編輯';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 寫入異動紀錄
|
||||||
|
// 如果數量沒變,是否要寫紀錄?通常編輯頁面按儲存可能只改了其他欄位(如果有)
|
||||||
|
// 但因為我們目前只存 quantity,如果 quantity 沒變,可以不寫異動,或者寫一筆 0 的異動代表更新屬性
|
||||||
|
if (abs($changeQty) > 0.0001) {
|
||||||
|
$inventory->transactions()->create([
|
||||||
|
'type' => $chineseType,
|
||||||
|
'quantity' => $changeQty,
|
||||||
|
'balance_before' => $currentQty,
|
||||||
|
'balance_after' => $newQty,
|
||||||
|
'reason' => ($validated['reason'] ?? '編輯頁面更新') . ($validated['notes'] ?? ''),
|
||||||
|
'actual_time' => now(), // 手動調整設定為當下
|
||||||
|
'user_id' => auth()->id(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return redirect()->route('warehouses.inventory.index', $inventory->warehouse_id)
|
||||||
|
->with('success', '庫存資料已更新');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function destroy(\App\Models\Warehouse $warehouse, $inventoryId)
|
||||||
|
{
|
||||||
|
$inventory = \App\Models\Inventory::findOrFail($inventoryId);
|
||||||
|
|
||||||
|
// 歸零異動
|
||||||
|
if ($inventory->quantity > 0) {
|
||||||
|
$inventory->transactions()->create([
|
||||||
|
'type' => '手動編輯',
|
||||||
|
'quantity' => -$inventory->quantity,
|
||||||
|
'balance_before' => $inventory->quantity,
|
||||||
|
'balance_after' => 0,
|
||||||
|
'reason' => '刪除庫存品項',
|
||||||
|
'actual_time' => now(),
|
||||||
|
'user_id' => auth()->id(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$inventory->delete();
|
||||||
|
|
||||||
|
return redirect()->route('warehouses.inventory.index', $warehouse->id)
|
||||||
|
->with('success', '庫存品項已刪除');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function history(\App\Models\Warehouse $warehouse, $inventoryId)
|
||||||
|
{
|
||||||
|
$inventory = \App\Models\Inventory::with(['product', 'transactions' => function($query) {
|
||||||
|
$query->orderBy('actual_time', 'desc')->orderBy('id', 'desc');
|
||||||
|
}, 'transactions.user'])->findOrFail($inventoryId);
|
||||||
|
|
||||||
|
$transactions = $inventory->transactions->map(function ($tx) {
|
||||||
|
return [
|
||||||
|
'id' => (string) $tx->id,
|
||||||
|
'type' => $tx->type,
|
||||||
|
'quantity' => (float) $tx->quantity,
|
||||||
|
'balanceAfter' => (float) $tx->balance_after,
|
||||||
|
'reason' => $tx->reason,
|
||||||
|
'userName' => $tx->user ? $tx->user->name : '系統',
|
||||||
|
'actualTime' => $tx->actual_time ? $tx->actual_time->format('Y-m-d H:i') : $tx->created_at->format('Y-m-d H:i'),
|
||||||
|
];
|
||||||
|
});
|
||||||
|
|
||||||
|
return \Inertia\Inertia::render('Warehouse/InventoryHistory', [
|
||||||
|
'warehouse' => $warehouse,
|
||||||
|
'inventory' => [
|
||||||
|
'id' => (string) $inventory->id,
|
||||||
|
'productName' => $inventory->product->name,
|
||||||
|
'productCode' => $inventory->product->code,
|
||||||
|
'quantity' => (float) $inventory->quantity,
|
||||||
|
],
|
||||||
|
'transactions' => $transactions
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
129
app/Http/Controllers/ProductController.php
Normal file
129
app/Http/Controllers/ProductController.php
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Models\Product;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Inertia\Inertia;
|
||||||
|
use Inertia\Response;
|
||||||
|
|
||||||
|
class ProductController extends Controller
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Display a listing of the resource.
|
||||||
|
*/
|
||||||
|
public function index(Request $request): Response
|
||||||
|
{
|
||||||
|
$query = Product::with('category');
|
||||||
|
|
||||||
|
if ($request->filled('search')) {
|
||||||
|
$search = $request->search;
|
||||||
|
$query->where(function ($q) use ($search) {
|
||||||
|
$q->where('name', 'like', "%{$search}%")
|
||||||
|
->orWhere('code', 'like', "%{$search}%")
|
||||||
|
->orWhere('brand', 'like', "%{$search}%");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($request->filled('category_id') && $request->category_id !== 'all') {
|
||||||
|
$query->where('category_id', $request->category_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
$perPage = $request->input('per_page', 10);
|
||||||
|
if (!in_array($perPage, [10, 20, 50, 100])) {
|
||||||
|
$perPage = 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
$sortField = $request->input('sort_field', 'id');
|
||||||
|
$sortDirection = $request->input('sort_direction', 'desc');
|
||||||
|
|
||||||
|
// Define allowed sort fields to prevent SQL injection
|
||||||
|
$allowedSorts = ['id', 'code', 'name', 'category_id', 'base_unit', 'conversion_rate'];
|
||||||
|
if (!in_array($sortField, $allowedSorts)) {
|
||||||
|
$sortField = 'id';
|
||||||
|
}
|
||||||
|
if (!in_array(strtolower($sortDirection), ['asc', 'desc'])) {
|
||||||
|
$sortDirection = 'desc';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle relation sorting (category name) separately if needed, or simple join
|
||||||
|
if ($sortField === 'category_id') {
|
||||||
|
// Join categories for sorting by name? Or just by ID?
|
||||||
|
// Simple approach: sort by ID for now, or join if user wants name sort.
|
||||||
|
// Let's assume standard field sorting first.
|
||||||
|
$query->orderBy('category_id', $sortDirection);
|
||||||
|
} else {
|
||||||
|
$query->orderBy($sortField, $sortDirection);
|
||||||
|
}
|
||||||
|
|
||||||
|
$products = $query->paginate($perPage)->withQueryString();
|
||||||
|
|
||||||
|
$categories = \App\Models\Category::where('is_active', true)->get();
|
||||||
|
|
||||||
|
return Inertia::render('Product/Index', [
|
||||||
|
'products' => $products,
|
||||||
|
'categories' => $categories,
|
||||||
|
'filters' => $request->only(['search', 'category_id', 'per_page', 'sort_field', 'sort_direction']),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Store a newly created resource in storage.
|
||||||
|
*/
|
||||||
|
public function store(Request $request)
|
||||||
|
{
|
||||||
|
$validated = $request->validate([
|
||||||
|
'name' => 'required|string|max:255',
|
||||||
|
'category_id' => 'required|exists:categories,id',
|
||||||
|
'brand' => 'nullable|string|max:255',
|
||||||
|
'specification' => 'nullable|string',
|
||||||
|
'base_unit' => 'required|string|max:50',
|
||||||
|
'large_unit' => 'nullable|string|max:50',
|
||||||
|
'conversion_rate' => 'required_with:large_unit|nullable|numeric|min:0.0001',
|
||||||
|
'purchase_unit' => 'nullable|string|max:50',
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Auto-generate code
|
||||||
|
$prefix = 'P';
|
||||||
|
$lastProduct = Product::withTrashed()->latest('id')->first();
|
||||||
|
$nextId = $lastProduct ? $lastProduct->id + 1 : 1;
|
||||||
|
$code = $prefix . str_pad($nextId, 5, '0', STR_PAD_LEFT);
|
||||||
|
|
||||||
|
$validated['code'] = $code;
|
||||||
|
|
||||||
|
$product = Product::create($validated);
|
||||||
|
|
||||||
|
return redirect()->back()->with('success', '商品已建立');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the specified resource in storage.
|
||||||
|
*/
|
||||||
|
public function update(Request $request, Product $product)
|
||||||
|
{
|
||||||
|
$validated = $request->validate([
|
||||||
|
'name' => 'required|string|max:255',
|
||||||
|
'category_id' => 'required|exists:categories,id',
|
||||||
|
'brand' => 'nullable|string|max:255',
|
||||||
|
'specification' => 'nullable|string',
|
||||||
|
'base_unit' => 'required|string|max:50',
|
||||||
|
'large_unit' => 'nullable|string|max:50',
|
||||||
|
'conversion_rate' => 'required_with:large_unit|nullable|numeric|min:0.0001',
|
||||||
|
'purchase_unit' => 'nullable|string|max:50',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$product->update($validated);
|
||||||
|
|
||||||
|
return redirect()->back()->with('success', '商品已更新');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove the specified resource from storage.
|
||||||
|
*/
|
||||||
|
public function destroy(Product $product)
|
||||||
|
{
|
||||||
|
$product->delete();
|
||||||
|
|
||||||
|
return redirect()->back()->with('success', '商品已刪除');
|
||||||
|
}
|
||||||
|
}
|
||||||
259
app/Http/Controllers/PurchaseOrderController.php
Normal file
259
app/Http/Controllers/PurchaseOrderController.php
Normal file
@@ -0,0 +1,259 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Models\PurchaseOrder;
|
||||||
|
use App\Models\Vendor;
|
||||||
|
use App\Models\Warehouse;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Inertia\Inertia;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
|
||||||
|
class PurchaseOrderController extends Controller
|
||||||
|
{
|
||||||
|
public function index(Request $request)
|
||||||
|
{
|
||||||
|
$query = PurchaseOrder::with(['vendor', 'warehouse', 'user']);
|
||||||
|
|
||||||
|
// Search
|
||||||
|
if ($request->search) {
|
||||||
|
$query->where(function($q) use ($request) {
|
||||||
|
$q->where('code', 'like', "%{$request->search}%")
|
||||||
|
->orWhereHas('vendor', function($vq) use ($request) {
|
||||||
|
$vq->where('name', 'like', "%{$request->search}%");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filters
|
||||||
|
if ($request->status && $request->status !== 'all') {
|
||||||
|
$query->where('status', $request->status);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($request->warehouse_id && $request->warehouse_id !== 'all') {
|
||||||
|
$query->where('warehouse_id', $request->warehouse_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sorting
|
||||||
|
$sortField = $request->sort_field ?? 'id';
|
||||||
|
$sortDirection = $request->sort_direction ?? 'desc';
|
||||||
|
$allowedSortFields = ['id', 'code', 'status', 'total_amount', 'created_at', 'expected_delivery_date'];
|
||||||
|
|
||||||
|
if (in_array($sortField, $allowedSortFields)) {
|
||||||
|
$query->orderBy($sortField, $sortDirection);
|
||||||
|
}
|
||||||
|
|
||||||
|
$orders = $query->paginate(15)->withQueryString();
|
||||||
|
|
||||||
|
return Inertia::render('PurchaseOrder/Index', [
|
||||||
|
'orders' => $orders,
|
||||||
|
'filters' => $request->only(['search', 'status', 'warehouse_id', 'sort_field', 'sort_direction']),
|
||||||
|
'warehouses' => Warehouse::all(['id', 'name']),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function create()
|
||||||
|
{
|
||||||
|
$vendors = Vendor::with('products')->get()->map(function ($vendor) {
|
||||||
|
return [
|
||||||
|
'id' => (string) $vendor->id,
|
||||||
|
'name' => $vendor->name,
|
||||||
|
'commonProducts' => $vendor->products->map(function ($product) {
|
||||||
|
return [
|
||||||
|
'productId' => (string) $product->id,
|
||||||
|
'productName' => $product->name,
|
||||||
|
'unit' => $product->base_unit,
|
||||||
|
'lastPrice' => (float) ($product->pivot->last_price ?? 0),
|
||||||
|
];
|
||||||
|
})
|
||||||
|
];
|
||||||
|
});
|
||||||
|
|
||||||
|
$warehouses = Warehouse::all()->map(function ($w) {
|
||||||
|
return [
|
||||||
|
'id' => (string) $w->id,
|
||||||
|
'name' => $w->name,
|
||||||
|
];
|
||||||
|
});
|
||||||
|
|
||||||
|
return Inertia::render('PurchaseOrder/Create', [
|
||||||
|
'suppliers' => $vendors,
|
||||||
|
'warehouses' => $warehouses,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function store(Request $request)
|
||||||
|
{
|
||||||
|
$validated = $request->validate([
|
||||||
|
'vendor_id' => 'required|exists:vendors,id',
|
||||||
|
'warehouse_id' => 'required|exists:warehouses,id',
|
||||||
|
'expected_delivery_date' => 'nullable|date',
|
||||||
|
'remark' => 'nullable|string',
|
||||||
|
'items' => 'required|array|min:1',
|
||||||
|
'items.*.productId' => 'required|exists:products,id',
|
||||||
|
'items.*.quantity' => 'required|numeric|min:0.01',
|
||||||
|
'items.*.unitPrice' => 'required|numeric|min:0',
|
||||||
|
]);
|
||||||
|
|
||||||
|
try {
|
||||||
|
DB::beginTransaction();
|
||||||
|
|
||||||
|
// 生成單號:YYYYMMDD001
|
||||||
|
$today = now()->format('Ymd');
|
||||||
|
$lastOrder = PurchaseOrder::where('code', 'like', $today . '%')
|
||||||
|
->lockForUpdate() // 鎖定以避免並發衝突
|
||||||
|
->orderBy('code', 'desc')
|
||||||
|
->first();
|
||||||
|
|
||||||
|
if ($lastOrder) {
|
||||||
|
// 取得最後 3 碼序號並加 1
|
||||||
|
$lastSequence = intval(substr($lastOrder->code, -3));
|
||||||
|
$sequence = str_pad($lastSequence + 1, 3, '0', STR_PAD_LEFT);
|
||||||
|
} else {
|
||||||
|
$sequence = '001';
|
||||||
|
}
|
||||||
|
$code = $today . $sequence;
|
||||||
|
|
||||||
|
$totalAmount = 0;
|
||||||
|
foreach ($validated['items'] as $item) {
|
||||||
|
$totalAmount += $item['quantity'] * $item['unitPrice'];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Simple tax calculation (e.g., 5%)
|
||||||
|
$taxAmount = round($totalAmount * 0.05, 2);
|
||||||
|
$grandTotal = $totalAmount + $taxAmount;
|
||||||
|
|
||||||
|
$order = PurchaseOrder::create([
|
||||||
|
'code' => $code,
|
||||||
|
'vendor_id' => $validated['vendor_id'],
|
||||||
|
'warehouse_id' => $validated['warehouse_id'],
|
||||||
|
'user_id' => auth()->id() ?? 1, // Fallback for dev if not using auth
|
||||||
|
'status' => 'draft',
|
||||||
|
'expected_delivery_date' => $validated['expected_delivery_date'],
|
||||||
|
'total_amount' => $totalAmount,
|
||||||
|
'tax_amount' => $taxAmount,
|
||||||
|
'grand_total' => $grandTotal,
|
||||||
|
'remark' => $validated['remark'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
foreach ($validated['items'] as $item) {
|
||||||
|
$order->items()->create([
|
||||||
|
'product_id' => $item['productId'],
|
||||||
|
'quantity' => $item['quantity'],
|
||||||
|
'unit_price' => $item['unitPrice'],
|
||||||
|
'subtotal' => $item['quantity'] * $item['unitPrice'],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
DB::commit();
|
||||||
|
|
||||||
|
return redirect()->route('purchase-orders.index')->with('success', '採購單已成功建立');
|
||||||
|
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
DB::rollBack();
|
||||||
|
return back()->withErrors(['error' => '建立失敗:' . $e->getMessage()]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function show($id)
|
||||||
|
{
|
||||||
|
$order = PurchaseOrder::with(['vendor', 'warehouse', 'user', 'items.product'])->findOrFail($id);
|
||||||
|
|
||||||
|
return Inertia::render('PurchaseOrder/Show', [
|
||||||
|
'order' => $order
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function edit($id)
|
||||||
|
{
|
||||||
|
$order = PurchaseOrder::with(['items.product'])->findOrFail($id);
|
||||||
|
|
||||||
|
$vendors = Vendor::with('products')->get()->map(function ($vendor) {
|
||||||
|
return [
|
||||||
|
'id' => (string) $vendor->id,
|
||||||
|
'name' => $vendor->name,
|
||||||
|
'commonProducts' => $vendor->products->map(function ($product) {
|
||||||
|
return [
|
||||||
|
'productId' => (string) $product->id,
|
||||||
|
'productName' => $product->name,
|
||||||
|
'unit' => $product->base_unit,
|
||||||
|
'lastPrice' => (float) ($product->pivot->last_price ?? 0),
|
||||||
|
];
|
||||||
|
})
|
||||||
|
];
|
||||||
|
});
|
||||||
|
|
||||||
|
$warehouses = Warehouse::all()->map(function ($w) {
|
||||||
|
return [
|
||||||
|
'id' => (string) $w->id,
|
||||||
|
'name' => $w->name,
|
||||||
|
];
|
||||||
|
});
|
||||||
|
|
||||||
|
return Inertia::render('PurchaseOrder/Create', [
|
||||||
|
'order' => $order,
|
||||||
|
'suppliers' => $vendors,
|
||||||
|
'warehouses' => $warehouses,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function update(Request $request, $id)
|
||||||
|
{
|
||||||
|
$order = PurchaseOrder::findOrFail($id);
|
||||||
|
|
||||||
|
$validated = $request->validate([
|
||||||
|
'vendor_id' => 'required|exists:vendors,id',
|
||||||
|
'warehouse_id' => 'required|exists:warehouses,id',
|
||||||
|
'expected_delivery_date' => 'nullable|date',
|
||||||
|
'remark' => 'nullable|string',
|
||||||
|
'status' => 'required|string|in:draft,pending,processing,shipping,confirming,completed,cancelled',
|
||||||
|
'items' => 'required|array|min:1',
|
||||||
|
'items.*.productId' => 'required|exists:products,id',
|
||||||
|
'items.*.quantity' => 'required|numeric|min:0.01',
|
||||||
|
'items.*.unitPrice' => 'required|numeric|min:0',
|
||||||
|
]);
|
||||||
|
|
||||||
|
try {
|
||||||
|
DB::beginTransaction();
|
||||||
|
|
||||||
|
$totalAmount = 0;
|
||||||
|
foreach ($validated['items'] as $item) {
|
||||||
|
$totalAmount += $item['quantity'] * $item['unitPrice'];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Simple tax calculation (e.g., 5%)
|
||||||
|
$taxAmount = round($totalAmount * 0.05, 2);
|
||||||
|
$grandTotal = $totalAmount + $taxAmount;
|
||||||
|
|
||||||
|
$order->update([
|
||||||
|
'vendor_id' => $validated['vendor_id'],
|
||||||
|
'warehouse_id' => $validated['warehouse_id'],
|
||||||
|
'expected_delivery_date' => $validated['expected_delivery_date'],
|
||||||
|
'total_amount' => $totalAmount,
|
||||||
|
'tax_amount' => $taxAmount,
|
||||||
|
'grand_total' => $grandTotal,
|
||||||
|
'remark' => $validated['remark'],
|
||||||
|
'status' => $validated['status'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Sync items
|
||||||
|
$order->items()->delete();
|
||||||
|
foreach ($validated['items'] as $item) {
|
||||||
|
$order->items()->create([
|
||||||
|
'product_id' => $item['productId'],
|
||||||
|
'quantity' => $item['quantity'],
|
||||||
|
'unit_price' => $item['unitPrice'],
|
||||||
|
'subtotal' => $item['quantity'] * $item['unitPrice'],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
DB::commit();
|
||||||
|
|
||||||
|
return redirect()->route('purchase-orders.index')->with('success', '採購單已更新');
|
||||||
|
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
DB::rollBack();
|
||||||
|
return back()->withErrors(['error' => '更新失敗:' . $e->getMessage()]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
123
app/Http/Controllers/SafetyStockController.php
Normal file
123
app/Http/Controllers/SafetyStockController.php
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Models\Warehouse;
|
||||||
|
use App\Models\Inventory;
|
||||||
|
use App\Models\Product;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Inertia\Inertia;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
|
||||||
|
class SafetyStockController extends Controller
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* 顯示安全庫存設定頁面
|
||||||
|
*/
|
||||||
|
public function index(Warehouse $warehouse)
|
||||||
|
{
|
||||||
|
$warehouse->load(['inventories.product.category']);
|
||||||
|
|
||||||
|
$allProducts = Product::with('category')->get();
|
||||||
|
|
||||||
|
// 準備可選商品列表
|
||||||
|
$availableProducts = $allProducts->map(function ($product) {
|
||||||
|
return [
|
||||||
|
'id' => (string) $product->id,
|
||||||
|
'name' => $product->name,
|
||||||
|
'type' => $product->category ? $product->category->name : '其他',
|
||||||
|
'unit' => $product->base_unit,
|
||||||
|
];
|
||||||
|
});
|
||||||
|
|
||||||
|
// 準備現有庫存列表 (用於狀態計算)
|
||||||
|
$inventories = $warehouse->inventories->map(function ($inv) {
|
||||||
|
return [
|
||||||
|
'id' => (string) $inv->id,
|
||||||
|
'productId' => (string) $inv->product_id,
|
||||||
|
'quantity' => (float) $inv->quantity,
|
||||||
|
'safetyStock' => (float) $inv->safety_stock,
|
||||||
|
];
|
||||||
|
});
|
||||||
|
|
||||||
|
// 準備安全庫存設定列表
|
||||||
|
$safetyStockSettings = $warehouse->inventories->filter(function($inv) {
|
||||||
|
return !is_null($inv->safety_stock);
|
||||||
|
})->map(function ($inv) {
|
||||||
|
return [
|
||||||
|
'id' => (string) $inv->id,
|
||||||
|
'warehouseId' => (string) $inv->warehouse_id,
|
||||||
|
'productId' => (string) $inv->product_id,
|
||||||
|
'productName' => $inv->product->name,
|
||||||
|
'productType' => $inv->product->category ? $inv->product->category->name : '其他',
|
||||||
|
'safetyStock' => (float) $inv->safety_stock,
|
||||||
|
'unit' => $inv->product->base_unit,
|
||||||
|
'updatedAt' => $inv->updated_at->toIso8601String(),
|
||||||
|
];
|
||||||
|
})->values();
|
||||||
|
|
||||||
|
|
||||||
|
return Inertia::render('Warehouse/SafetyStockSettings', [
|
||||||
|
'warehouse' => $warehouse,
|
||||||
|
'safetyStockSettings' => $safetyStockSettings,
|
||||||
|
'inventories' => $inventories,
|
||||||
|
'availableProducts' => $availableProducts,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量儲存安全庫存設定
|
||||||
|
*/
|
||||||
|
public function store(Request $request, Warehouse $warehouse)
|
||||||
|
{
|
||||||
|
$validated = $request->validate([
|
||||||
|
'settings' => 'required|array|min:1',
|
||||||
|
'settings.*.productId' => 'required|exists:products,id',
|
||||||
|
'settings.*.quantity' => 'required|numeric|min:0',
|
||||||
|
]);
|
||||||
|
|
||||||
|
DB::transaction(function () use ($validated, $warehouse) {
|
||||||
|
foreach ($validated['settings'] as $item) {
|
||||||
|
Inventory::updateOrCreate(
|
||||||
|
[
|
||||||
|
'warehouse_id' => $warehouse->id,
|
||||||
|
'product_id' => $item['productId'],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'safety_stock' => $item['quantity'],
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return redirect()->back()->with('success', '安全庫存設定已更新');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新單筆安全庫存設定
|
||||||
|
*/
|
||||||
|
public function update(Request $request, Warehouse $warehouse, Inventory $inventory)
|
||||||
|
{
|
||||||
|
$validated = $request->validate([
|
||||||
|
'safetyStock' => 'required|numeric|min:0',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$inventory->update([
|
||||||
|
'safety_stock' => $validated['safetyStock'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
return redirect()->back()->with('success', '安全庫存已更新');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 刪除 (歸零) 安全庫存設定
|
||||||
|
*/
|
||||||
|
public function destroy(Warehouse $warehouse, Inventory $inventory)
|
||||||
|
{
|
||||||
|
$inventory->update([
|
||||||
|
'safety_stock' => null,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return redirect()->back()->with('success', '安全庫存設定已移除');
|
||||||
|
}
|
||||||
|
}
|
||||||
115
app/Http/Controllers/TransferOrderController.php
Normal file
115
app/Http/Controllers/TransferOrderController.php
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Models\Inventory;
|
||||||
|
use App\Models\Warehouse;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use Illuminate\Validation\ValidationException;
|
||||||
|
|
||||||
|
class TransferOrderController extends Controller
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* 儲存撥補單(建立調撥單並執行庫存轉移)
|
||||||
|
*/
|
||||||
|
public function store(Request $request)
|
||||||
|
{
|
||||||
|
$validated = $request->validate([
|
||||||
|
'sourceWarehouseId' => 'required|exists:warehouses,id',
|
||||||
|
'targetWarehouseId' => 'required|exists:warehouses,id|different:sourceWarehouseId',
|
||||||
|
'productId' => 'required|exists:products,id',
|
||||||
|
'quantity' => 'required|numeric|min:0.01',
|
||||||
|
'transferDate' => 'required|date',
|
||||||
|
'status' => 'required|in:待處理,處理中,已完成,已取消', // 目前僅支援立即完成或單純記錄
|
||||||
|
'notes' => 'nullable|string',
|
||||||
|
'batchNumber' => 'nullable|string', // 暫時接收,雖然 DB 可能沒存
|
||||||
|
]);
|
||||||
|
|
||||||
|
return DB::transaction(function () use ($validated) {
|
||||||
|
// 1. 檢查來源倉庫庫存
|
||||||
|
$sourceInventory = Inventory::where('warehouse_id', $validated['sourceWarehouseId'])
|
||||||
|
->where('product_id', $validated['productId'])
|
||||||
|
->first();
|
||||||
|
|
||||||
|
if (!$sourceInventory || $sourceInventory->quantity < $validated['quantity']) {
|
||||||
|
throw ValidationException::withMessages([
|
||||||
|
'quantity' => ['來源倉庫庫存不足'],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 獲取或建立目標倉庫庫存
|
||||||
|
$targetInventory = Inventory::firstOrCreate(
|
||||||
|
[
|
||||||
|
'warehouse_id' => $validated['targetWarehouseId'],
|
||||||
|
'product_id' => $validated['productId'],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'quantity' => 0,
|
||||||
|
'safety_stock' => null, // 預設為 null (未設定),而非 0
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
$sourceWarehouse = Warehouse::find($validated['sourceWarehouseId']);
|
||||||
|
$targetWarehouse = Warehouse::find($validated['targetWarehouseId']);
|
||||||
|
|
||||||
|
// 3. 執行庫存轉移 (扣除來源)
|
||||||
|
$oldSourceQty = $sourceInventory->quantity;
|
||||||
|
$newSourceQty = $oldSourceQty - $validated['quantity'];
|
||||||
|
$sourceInventory->update(['quantity' => $newSourceQty]);
|
||||||
|
|
||||||
|
// 記錄來源異動
|
||||||
|
$sourceInventory->transactions()->create([
|
||||||
|
'type' => '撥補出庫',
|
||||||
|
'quantity' => -$validated['quantity'],
|
||||||
|
'balance_before' => $oldSourceQty,
|
||||||
|
'balance_after' => $newSourceQty,
|
||||||
|
'reason' => "撥補至 {$targetWarehouse->name}" . ($validated['notes'] ? " ({$validated['notes']})" : ""),
|
||||||
|
'actual_time' => $validated['transferDate'],
|
||||||
|
'user_id' => auth()->id(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
// 4. 執行庫存轉移 (增加目標)
|
||||||
|
$oldTargetQty = $targetInventory->quantity;
|
||||||
|
$newTargetQty = $oldTargetQty + $validated['quantity'];
|
||||||
|
$targetInventory->update(['quantity' => $newTargetQty]);
|
||||||
|
|
||||||
|
// 記錄目標異動
|
||||||
|
$targetInventory->transactions()->create([
|
||||||
|
'type' => '撥補入庫',
|
||||||
|
'quantity' => $validated['quantity'],
|
||||||
|
'balance_before' => $oldTargetQty,
|
||||||
|
'balance_after' => $newTargetQty,
|
||||||
|
'reason' => "來自 {$sourceWarehouse->name} 的撥補" . ($validated['notes'] ? " ({$validated['notes']})" : ""),
|
||||||
|
'actual_time' => $validated['transferDate'],
|
||||||
|
'user_id' => auth()->id(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
// TODO: 未來若有獨立的 TransferOrder 模型,可在此建立紀錄
|
||||||
|
|
||||||
|
return redirect()->back()->with('success', '撥補單已建立且庫存已轉移');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 獲取特定倉庫的庫存列表 (API)
|
||||||
|
*/
|
||||||
|
public function getWarehouseInventories(Warehouse $warehouse)
|
||||||
|
{
|
||||||
|
$inventories = $warehouse->inventories()
|
||||||
|
->with(['product:id,name,base_unit,category_id', 'product.category'])
|
||||||
|
->where('quantity', '>', 0) // 只回傳有庫存的
|
||||||
|
->get()
|
||||||
|
->map(function ($inv) {
|
||||||
|
return [
|
||||||
|
'productId' => (string) $inv->product_id,
|
||||||
|
'productName' => $inv->product->name,
|
||||||
|
'batchNumber' => 'BATCH-' . $inv->id, // 模擬批號
|
||||||
|
'availableQty' => (float) $inv->quantity,
|
||||||
|
'unit' => $inv->product->base_unit,
|
||||||
|
];
|
||||||
|
});
|
||||||
|
|
||||||
|
return response()->json($inventories);
|
||||||
|
}
|
||||||
|
}
|
||||||
124
app/Http/Controllers/VendorController.php
Normal file
124
app/Http/Controllers/VendorController.php
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Models\Vendor;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
|
class VendorController extends Controller
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Display a listing of the resource.
|
||||||
|
*/
|
||||||
|
public function index(\Illuminate\Http\Request $request): \Inertia\Response
|
||||||
|
{
|
||||||
|
$query = Vendor::query();
|
||||||
|
|
||||||
|
if ($request->filled('search')) {
|
||||||
|
$search = $request->search;
|
||||||
|
$query->where(function ($q) use ($search) {
|
||||||
|
$q->where('name', 'like', "%{$search}%")
|
||||||
|
->orWhere('code', 'like', "%{$search}%")
|
||||||
|
->orWhere('tax_id', 'like', "%{$search}%")
|
||||||
|
->orWhere('owner', 'like', "%{$search}%")
|
||||||
|
->orWhere('contact_name', 'like', "%{$search}%");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
$sortField = $request->input('sort_field', 'id');
|
||||||
|
$sortDirection = $request->input('sort_direction', 'desc');
|
||||||
|
|
||||||
|
$allowedSorts = ['id', 'code', 'name', 'owner', 'contact_name', 'phone'];
|
||||||
|
if (!in_array($sortField, $allowedSorts)) {
|
||||||
|
$sortField = 'id';
|
||||||
|
}
|
||||||
|
if (!in_array(strtolower($sortDirection), ['asc', 'desc'])) {
|
||||||
|
$sortDirection = 'desc';
|
||||||
|
}
|
||||||
|
|
||||||
|
$vendors = $query->orderBy($sortField, $sortDirection)
|
||||||
|
->paginate(10)
|
||||||
|
->withQueryString();
|
||||||
|
|
||||||
|
return \Inertia\Inertia::render('Vendor/Index', [
|
||||||
|
'vendors' => $vendors,
|
||||||
|
'filters' => $request->only(['search', 'sort_field', 'sort_direction']),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display the specified resource.
|
||||||
|
*/
|
||||||
|
public function show(Vendor $vendor): \Inertia\Response
|
||||||
|
{
|
||||||
|
$vendor->load('products');
|
||||||
|
return \Inertia\Inertia::render('Vendor/Show', [
|
||||||
|
'vendor' => $vendor,
|
||||||
|
'products' => \App\Models\Product::all(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Store a newly created resource in storage.
|
||||||
|
*/
|
||||||
|
public function store(\Illuminate\Http\Request $request)
|
||||||
|
{
|
||||||
|
$validated = $request->validate([
|
||||||
|
'name' => 'required|string|max:255',
|
||||||
|
'short_name' => 'nullable|string|max:255',
|
||||||
|
'tax_id' => 'nullable|string|max:8',
|
||||||
|
'owner' => 'nullable|string|max:255',
|
||||||
|
'contact_name' => 'nullable|string|max:255',
|
||||||
|
'tel' => 'nullable|string|max:50',
|
||||||
|
'phone' => 'nullable|string|max:50',
|
||||||
|
'email' => 'nullable|email|max:255',
|
||||||
|
'address' => 'nullable|string',
|
||||||
|
'remark' => 'nullable|string',
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Auto-generate code
|
||||||
|
$prefix = 'V';
|
||||||
|
$lastVendor = Vendor::latest('id')->first();
|
||||||
|
$nextId = $lastVendor ? $lastVendor->id + 1 : 1;
|
||||||
|
$code = $prefix . str_pad($nextId, 5, '0', STR_PAD_LEFT);
|
||||||
|
|
||||||
|
$validated['code'] = $code;
|
||||||
|
|
||||||
|
Vendor::create($validated);
|
||||||
|
|
||||||
|
return redirect()->back()->with('success', '廠商已建立');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the specified resource in storage.
|
||||||
|
*/
|
||||||
|
public function update(\Illuminate\Http\Request $request, Vendor $vendor)
|
||||||
|
{
|
||||||
|
$validated = $request->validate([
|
||||||
|
'name' => 'required|string|max:255',
|
||||||
|
'short_name' => 'nullable|string|max:255',
|
||||||
|
'tax_id' => 'nullable|string|max:8',
|
||||||
|
'owner' => 'nullable|string|max:255',
|
||||||
|
'contact_name' => 'nullable|string|max:255',
|
||||||
|
'tel' => 'nullable|string|max:50',
|
||||||
|
'phone' => 'nullable|string|max:50',
|
||||||
|
'email' => 'nullable|email|max:255',
|
||||||
|
'address' => 'nullable|string',
|
||||||
|
'remark' => 'nullable|string',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$vendor->update($validated);
|
||||||
|
|
||||||
|
return redirect()->back()->with('success', '廠商資料已更新');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove the specified resource from storage.
|
||||||
|
*/
|
||||||
|
public function destroy(Vendor $vendor)
|
||||||
|
{
|
||||||
|
$vendor->delete();
|
||||||
|
|
||||||
|
return redirect()->back()->with('success', '廠商已刪除');
|
||||||
|
}
|
||||||
|
}
|
||||||
58
app/Http/Controllers/VendorProductController.php
Normal file
58
app/Http/Controllers/VendorProductController.php
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Models\Vendor;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
|
|
||||||
|
class VendorProductController extends Controller
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* 新增供貨商品 (Attach)
|
||||||
|
*/
|
||||||
|
public function store(Request $request, Vendor $vendor)
|
||||||
|
{
|
||||||
|
$validated = $request->validate([
|
||||||
|
'product_id' => 'required|exists:products,id',
|
||||||
|
'last_price' => 'nullable|numeric|min:0',
|
||||||
|
]);
|
||||||
|
|
||||||
|
// 檢查是否已存在
|
||||||
|
if ($vendor->products()->where('product_id', $validated['product_id'])->exists()) {
|
||||||
|
return redirect()->back()->with('error', '該商品已在供貨清單中');
|
||||||
|
}
|
||||||
|
|
||||||
|
$vendor->products()->attach($validated['product_id'], [
|
||||||
|
'last_price' => $validated['last_price'] ?? null
|
||||||
|
]);
|
||||||
|
|
||||||
|
return redirect()->back()->with('success', '供貨商品已新增');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新供貨商品資訊 (Update Pivot)
|
||||||
|
*/
|
||||||
|
public function update(Request $request, Vendor $vendor, $productId)
|
||||||
|
{
|
||||||
|
$validated = $request->validate([
|
||||||
|
'last_price' => 'nullable|numeric|min:0',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$vendor->products()->updateExistingPivot($productId, [
|
||||||
|
'last_price' => $validated['last_price'] ?? null
|
||||||
|
]);
|
||||||
|
|
||||||
|
return redirect()->back()->with('success', '供貨資訊已更新');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 移除供貨商品 (Detach)
|
||||||
|
*/
|
||||||
|
public function destroy(Vendor $vendor, $productId)
|
||||||
|
{
|
||||||
|
$vendor->products()->detach($productId);
|
||||||
|
|
||||||
|
return redirect()->back()->with('success', '供貨商品已移除');
|
||||||
|
}
|
||||||
|
}
|
||||||
80
app/Http/Controllers/WarehouseController.php
Normal file
80
app/Http/Controllers/WarehouseController.php
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
|
use App\Models\Warehouse;
|
||||||
|
|
||||||
|
use Inertia\Inertia;
|
||||||
|
|
||||||
|
class WarehouseController extends Controller
|
||||||
|
{
|
||||||
|
public function index(Request $request)
|
||||||
|
{
|
||||||
|
$query = Warehouse::query();
|
||||||
|
|
||||||
|
if ($request->has('search')) {
|
||||||
|
$search = $request->input('search');
|
||||||
|
$query->where(function ($q) use ($search) {
|
||||||
|
$q->where('name', 'like', "%{$search}%")
|
||||||
|
->orWhere('code', 'like', "%{$search}%");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
$warehouses = $query->withSum('inventories as total_quantity', 'quantity')
|
||||||
|
->withCount(['inventories as low_stock_count' => function ($query) {
|
||||||
|
$query->whereColumn('quantity', '<', 'safety_stock');
|
||||||
|
}])
|
||||||
|
->orderBy('created_at', 'desc')
|
||||||
|
->paginate(10)
|
||||||
|
->withQueryString();
|
||||||
|
|
||||||
|
return Inertia::render('Warehouse/Index', [
|
||||||
|
'warehouses' => $warehouses,
|
||||||
|
'filters' => $request->only(['search']),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function store(Request $request)
|
||||||
|
{
|
||||||
|
$validated = $request->validate([
|
||||||
|
'name' => 'required|string|max:50',
|
||||||
|
'address' => 'nullable|string|max:255',
|
||||||
|
'description' => 'nullable|string',
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Auto-generate code
|
||||||
|
$prefix = 'WH';
|
||||||
|
$lastWarehouse = Warehouse::latest('id')->first();
|
||||||
|
$nextId = $lastWarehouse ? $lastWarehouse->id + 1 : 1;
|
||||||
|
$code = $prefix . str_pad($nextId, 3, '0', STR_PAD_LEFT);
|
||||||
|
|
||||||
|
$validated['code'] = $code;
|
||||||
|
|
||||||
|
Warehouse::create($validated);
|
||||||
|
|
||||||
|
return redirect()->back()->with('success', '倉庫已建立');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function update(Request $request, Warehouse $warehouse)
|
||||||
|
{
|
||||||
|
$validated = $request->validate([
|
||||||
|
'name' => 'required|string|max:50',
|
||||||
|
'address' => 'nullable|string|max:255',
|
||||||
|
'description' => 'nullable|string',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$warehouse->update($validated);
|
||||||
|
|
||||||
|
return redirect()->back()->with('success', '倉庫資訊已更新');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function destroy(Warehouse $warehouse)
|
||||||
|
{
|
||||||
|
// 真實刪除
|
||||||
|
$warehouse->delete();
|
||||||
|
|
||||||
|
return redirect()->back()->with('success', '倉庫已刪除');
|
||||||
|
}
|
||||||
|
}
|
||||||
43
app/Http/Middleware/HandleInertiaRequests.php
Normal file
43
app/Http/Middleware/HandleInertiaRequests.php
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Middleware;
|
||||||
|
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Inertia\Middleware;
|
||||||
|
|
||||||
|
class HandleInertiaRequests extends Middleware
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The root template that's loaded on the first page visit.
|
||||||
|
*
|
||||||
|
* @see https://inertiajs.com/server-side-setup#root-template
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $rootView = 'app';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines the current asset version.
|
||||||
|
*
|
||||||
|
* @see https://inertiajs.com/asset-versioning
|
||||||
|
*/
|
||||||
|
public function version(Request $request): ?string
|
||||||
|
{
|
||||||
|
return parent::version($request);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Define the props that are shared by default.
|
||||||
|
*
|
||||||
|
* @see https://inertiajs.com/shared-data
|
||||||
|
*
|
||||||
|
* @return array<string, mixed>
|
||||||
|
*/
|
||||||
|
public function share(Request $request): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
...parent::share($request),
|
||||||
|
//
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
30
app/Models/Category.php
Normal file
30
app/Models/Category.php
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||||
|
|
||||||
|
class Category extends Model
|
||||||
|
{
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'name',
|
||||||
|
'description',
|
||||||
|
'is_active',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected $casts = [
|
||||||
|
'is_active' => 'boolean',
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the products for the category.
|
||||||
|
*/
|
||||||
|
public function products(): HasMany
|
||||||
|
{
|
||||||
|
return $this->hasMany(Product::class);
|
||||||
|
}
|
||||||
|
}
|
||||||
55
app/Models/Inventory.php
Normal file
55
app/Models/Inventory.php
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class Inventory extends Model
|
||||||
|
{
|
||||||
|
/** @use HasFactory<\Database\Factories\InventoryFactory> */
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'warehouse_id',
|
||||||
|
'product_id',
|
||||||
|
'quantity',
|
||||||
|
'safety_stock',
|
||||||
|
'location',
|
||||||
|
];
|
||||||
|
|
||||||
|
public function warehouse(): \Illuminate\Database\Eloquent\Relations\BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Warehouse::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function product(): \Illuminate\Database\Eloquent\Relations\BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Product::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function transactions(): \Illuminate\Database\Eloquent\Relations\HasMany
|
||||||
|
{
|
||||||
|
return $this->hasMany(InventoryTransaction::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function lastOutgoingTransaction()
|
||||||
|
{
|
||||||
|
return $this->hasOne(InventoryTransaction::class)->ofMany([
|
||||||
|
'actual_time' => 'max',
|
||||||
|
'id' => 'max',
|
||||||
|
], function ($query) {
|
||||||
|
$query->where('quantity', '<', 0);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function lastIncomingTransaction()
|
||||||
|
{
|
||||||
|
return $this->hasOne(InventoryTransaction::class)->ofMany([
|
||||||
|
'actual_time' => 'max',
|
||||||
|
'id' => 'max',
|
||||||
|
], function ($query) {
|
||||||
|
$query->where('quantity', '>', 0);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
46
app/Models/InventoryTransaction.php
Normal file
46
app/Models/InventoryTransaction.php
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use App\Models\Inventory;
|
||||||
|
use App\Models\User;
|
||||||
|
|
||||||
|
class InventoryTransaction extends Model
|
||||||
|
{
|
||||||
|
/** @use HasFactory<\Database\Factories\InventoryTransactionFactory> */
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'inventory_id',
|
||||||
|
'type',
|
||||||
|
'quantity',
|
||||||
|
'balance_before',
|
||||||
|
'balance_after',
|
||||||
|
'reason',
|
||||||
|
'reference_type',
|
||||||
|
'reference_id',
|
||||||
|
'user_id',
|
||||||
|
'actual_time',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected $casts = [
|
||||||
|
'actual_time' => 'datetime',
|
||||||
|
];
|
||||||
|
|
||||||
|
public function inventory(): \Illuminate\Database\Eloquent\Relations\BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Inventory::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function user(): \Illuminate\Database\Eloquent\Relations\BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(User::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function reference(): \Illuminate\Database\Eloquent\Relations\MorphTo
|
||||||
|
{
|
||||||
|
return $this->morphTo();
|
||||||
|
}
|
||||||
|
}
|
||||||
54
app/Models/Product.php
Normal file
54
app/Models/Product.php
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
|
||||||
|
class Product extends Model
|
||||||
|
{
|
||||||
|
use HasFactory, SoftDeletes;
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'code',
|
||||||
|
'name',
|
||||||
|
'category_id',
|
||||||
|
'brand',
|
||||||
|
'specification',
|
||||||
|
'base_unit',
|
||||||
|
'large_unit',
|
||||||
|
'conversion_rate',
|
||||||
|
'purchase_unit',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected $casts = [
|
||||||
|
'conversion_rate' => 'decimal:4',
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the category that owns the product.
|
||||||
|
*/
|
||||||
|
public function category(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Category::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function vendors(): \Illuminate\Database\Eloquent\Relations\BelongsToMany
|
||||||
|
{
|
||||||
|
return $this->belongsToMany(Vendor::class)->withPivot('last_price')->withTimestamps();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function inventories(): \Illuminate\Database\Eloquent\Relations\HasMany
|
||||||
|
{
|
||||||
|
return $this->hasMany(Inventory::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function warehouses(): \Illuminate\Database\Eloquent\Relations\BelongsToMany
|
||||||
|
{
|
||||||
|
return $this->belongsToMany(Warehouse::class, 'inventories')
|
||||||
|
->withPivot(['quantity', 'safety_stock', 'location'])
|
||||||
|
->withTimestamps();
|
||||||
|
}
|
||||||
|
}
|
||||||
104
app/Models/PurchaseOrder.php
Normal file
104
app/Models/PurchaseOrder.php
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||||
|
|
||||||
|
class PurchaseOrder extends Model
|
||||||
|
{
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'code',
|
||||||
|
'vendor_id',
|
||||||
|
'warehouse_id',
|
||||||
|
'user_id',
|
||||||
|
'status',
|
||||||
|
'expected_delivery_date',
|
||||||
|
'total_amount',
|
||||||
|
'tax_amount',
|
||||||
|
'grand_total',
|
||||||
|
'remark',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected $casts = [
|
||||||
|
'expected_delivery_date' => 'date',
|
||||||
|
'total_amount' => 'decimal:2',
|
||||||
|
'tax_amount' => 'decimal:2',
|
||||||
|
'grand_total' => 'decimal:2',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected $appends = [
|
||||||
|
'poNumber',
|
||||||
|
'supplierId',
|
||||||
|
'supplierName',
|
||||||
|
'expectedDate',
|
||||||
|
'totalAmount',
|
||||||
|
'createdBy',
|
||||||
|
'warehouse_name',
|
||||||
|
'createdAt',
|
||||||
|
];
|
||||||
|
|
||||||
|
public function getCreatedAtAttribute()
|
||||||
|
{
|
||||||
|
return $this->attributes['created_at'];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getPoNumberAttribute(): string
|
||||||
|
{
|
||||||
|
return $this->code;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getSupplierIdAttribute(): string
|
||||||
|
{
|
||||||
|
return (string) $this->vendor_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getSupplierNameAttribute(): string
|
||||||
|
{
|
||||||
|
return $this->vendor ? $this->vendor->name : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getExpectedDateAttribute(): ?string
|
||||||
|
{
|
||||||
|
return $this->expected_delivery_date ? $this->expected_delivery_date->format('Y-m-d') : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getTotalAmountAttribute(): float
|
||||||
|
{
|
||||||
|
return (float) ($this->attributes['total_amount'] ?? 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getCreatedByAttribute(): string
|
||||||
|
{
|
||||||
|
return $this->user ? $this->user->name : '系統';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getWarehouseNameAttribute(): string
|
||||||
|
{
|
||||||
|
return $this->warehouse ? $this->warehouse->name : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function vendor(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Vendor::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function warehouse(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Warehouse::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function user(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(User::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function items(): HasMany
|
||||||
|
{
|
||||||
|
return $this->hasMany(PurchaseOrderItem::class);
|
||||||
|
}
|
||||||
|
}
|
||||||
65
app/Models/PurchaseOrderItem.php
Normal file
65
app/Models/PurchaseOrderItem.php
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
|
||||||
|
class PurchaseOrderItem extends Model
|
||||||
|
{
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'purchase_order_id',
|
||||||
|
'product_id',
|
||||||
|
'quantity',
|
||||||
|
'unit_price',
|
||||||
|
'subtotal',
|
||||||
|
'received_quantity',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected $casts = [
|
||||||
|
'quantity' => 'decimal:2',
|
||||||
|
'unit_price' => 'decimal:2',
|
||||||
|
'subtotal' => 'decimal:2',
|
||||||
|
'received_quantity' => 'decimal:2',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected $appends = [
|
||||||
|
'productName',
|
||||||
|
'unit',
|
||||||
|
'productId',
|
||||||
|
'unitPrice',
|
||||||
|
];
|
||||||
|
|
||||||
|
public function getProductIdAttribute(): string
|
||||||
|
{
|
||||||
|
return (string) $this->attributes['product_id'];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getUnitPriceAttribute(): float
|
||||||
|
{
|
||||||
|
return (float) $this->attributes['unit_price'];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getProductNameAttribute(): string
|
||||||
|
{
|
||||||
|
return $this->product ? $this->product->name : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getUnitAttribute(): string
|
||||||
|
{
|
||||||
|
return $this->product ? $this->product->base_unit : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function purchaseOrder(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(PurchaseOrder::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function product(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Product::class);
|
||||||
|
}
|
||||||
|
}
|
||||||
48
app/Models/User.php
Normal file
48
app/Models/User.php
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
// use Illuminate\Contracts\Auth\MustVerifyEmail;
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||||
|
use Illuminate\Notifications\Notifiable;
|
||||||
|
|
||||||
|
class User extends Authenticatable
|
||||||
|
{
|
||||||
|
/** @use HasFactory<\Database\Factories\UserFactory> */
|
||||||
|
use HasFactory, Notifiable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The attributes that are mass assignable.
|
||||||
|
*
|
||||||
|
* @var list<string>
|
||||||
|
*/
|
||||||
|
protected $fillable = [
|
||||||
|
'name',
|
||||||
|
'email',
|
||||||
|
'password',
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The attributes that should be hidden for serialization.
|
||||||
|
*
|
||||||
|
* @var list<string>
|
||||||
|
*/
|
||||||
|
protected $hidden = [
|
||||||
|
'password',
|
||||||
|
'remember_token',
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the attributes that should be cast.
|
||||||
|
*
|
||||||
|
* @return array<string, string>
|
||||||
|
*/
|
||||||
|
protected function casts(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'email_verified_at' => 'datetime',
|
||||||
|
'password' => 'hashed',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
35
app/Models/Vendor.php
Normal file
35
app/Models/Vendor.php
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||||
|
|
||||||
|
class Vendor extends Model
|
||||||
|
{
|
||||||
|
protected $fillable = [
|
||||||
|
'code',
|
||||||
|
'name',
|
||||||
|
'short_name',
|
||||||
|
'tax_id',
|
||||||
|
'owner',
|
||||||
|
'contact_name',
|
||||||
|
'tel',
|
||||||
|
'phone',
|
||||||
|
'email',
|
||||||
|
'address',
|
||||||
|
'remark'
|
||||||
|
];
|
||||||
|
public function products(): BelongsToMany
|
||||||
|
{
|
||||||
|
return $this->belongsToMany(Product::class, 'product_vendor')
|
||||||
|
->withPivot('last_price')
|
||||||
|
->withTimestamps();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function purchaseOrders(): HasMany
|
||||||
|
{
|
||||||
|
return $this->hasMany(PurchaseOrder::class);
|
||||||
|
}
|
||||||
|
}
|
||||||
36
app/Models/Warehouse.php
Normal file
36
app/Models/Warehouse.php
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class Warehouse extends Model
|
||||||
|
{
|
||||||
|
/** @use HasFactory<\Database\Factories\WarehouseFactory> */
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'code',
|
||||||
|
'name',
|
||||||
|
'address',
|
||||||
|
'description',
|
||||||
|
];
|
||||||
|
|
||||||
|
public function inventories(): \Illuminate\Database\Eloquent\Relations\HasMany
|
||||||
|
{
|
||||||
|
return $this->hasMany(Inventory::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function purchaseOrders(): \Illuminate\Database\Eloquent\Relations\HasMany
|
||||||
|
{
|
||||||
|
return $this->hasMany(PurchaseOrder::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function products(): \Illuminate\Database\Eloquent\Relations\BelongsToMany
|
||||||
|
{
|
||||||
|
return $this->belongsToMany(Product::class, 'inventories')
|
||||||
|
->withPivot(['quantity', 'safety_stock', 'location'])
|
||||||
|
->withTimestamps();
|
||||||
|
}
|
||||||
|
}
|
||||||
24
app/Providers/AppServiceProvider.php
Normal file
24
app/Providers/AppServiceProvider.php
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Providers;
|
||||||
|
|
||||||
|
use Illuminate\Support\ServiceProvider;
|
||||||
|
|
||||||
|
class AppServiceProvider extends ServiceProvider
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Register any application services.
|
||||||
|
*/
|
||||||
|
public function register(): void
|
||||||
|
{
|
||||||
|
//
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bootstrap any application services.
|
||||||
|
*/
|
||||||
|
public function boot(): void
|
||||||
|
{
|
||||||
|
//
|
||||||
|
}
|
||||||
|
}
|
||||||
18
artisan
Executable file
18
artisan
Executable file
@@ -0,0 +1,18 @@
|
|||||||
|
#!/usr/bin/env php
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Application;
|
||||||
|
use Symfony\Component\Console\Input\ArgvInput;
|
||||||
|
|
||||||
|
define('LARAVEL_START', microtime(true));
|
||||||
|
|
||||||
|
// Register the Composer autoloader...
|
||||||
|
require __DIR__.'/vendor/autoload.php';
|
||||||
|
|
||||||
|
// Bootstrap Laravel and handle the command...
|
||||||
|
/** @var Application $app */
|
||||||
|
$app = require_once __DIR__.'/bootstrap/app.php';
|
||||||
|
|
||||||
|
$status = $app->handleCommand(new ArgvInput);
|
||||||
|
|
||||||
|
exit($status);
|
||||||
20
bootstrap/app.php
Normal file
20
bootstrap/app.php
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Application;
|
||||||
|
use Illuminate\Foundation\Configuration\Exceptions;
|
||||||
|
use Illuminate\Foundation\Configuration\Middleware;
|
||||||
|
|
||||||
|
return Application::configure(basePath: dirname(__DIR__))
|
||||||
|
->withRouting(
|
||||||
|
web: __DIR__.'/../routes/web.php',
|
||||||
|
commands: __DIR__.'/../routes/console.php',
|
||||||
|
health: '/up',
|
||||||
|
)
|
||||||
|
->withMiddleware(function (Middleware $middleware): void {
|
||||||
|
$middleware->web(append: [
|
||||||
|
\App\Http\Middleware\HandleInertiaRequests::class,
|
||||||
|
]);
|
||||||
|
})
|
||||||
|
->withExceptions(function (Exceptions $exceptions): void {
|
||||||
|
//
|
||||||
|
})->create();
|
||||||
2
bootstrap/cache/.gitignore
vendored
Normal file
2
bootstrap/cache/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
*
|
||||||
|
!.gitignore
|
||||||
5
bootstrap/providers.php
Normal file
5
bootstrap/providers.php
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
return [
|
||||||
|
App\Providers\AppServiceProvider::class,
|
||||||
|
];
|
||||||
82
compose.yaml
Normal file
82
compose.yaml
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
services:
|
||||||
|
laravel.test:
|
||||||
|
build:
|
||||||
|
context: './docker/8.5'
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
args:
|
||||||
|
WWWGROUP: '${WWWGROUP}'
|
||||||
|
image: 'sail-8.5/app'
|
||||||
|
extra_hosts:
|
||||||
|
- 'host.docker.internal:host-gateway'
|
||||||
|
ports:
|
||||||
|
- '${APP_PORT:-80}:80'
|
||||||
|
- '${VITE_PORT:-5173}:${VITE_PORT:-5173}'
|
||||||
|
environment:
|
||||||
|
WWWUSER: '${WWWUSER}'
|
||||||
|
LARAVEL_SAIL: 1
|
||||||
|
XDEBUG_MODE: '${SAIL_XDEBUG_MODE:-off}'
|
||||||
|
XDEBUG_CONFIG: '${SAIL_XDEBUG_CONFIG:-client_host=host.docker.internal}'
|
||||||
|
IGNITION_LOCAL_SITES_PATH: '${PWD}'
|
||||||
|
volumes:
|
||||||
|
- '.:/var/www/html'
|
||||||
|
networks:
|
||||||
|
- sail
|
||||||
|
depends_on:
|
||||||
|
- mysql
|
||||||
|
- redis
|
||||||
|
# - mailpit
|
||||||
|
mysql:
|
||||||
|
image: 'mysql/mysql-server:8.0'
|
||||||
|
ports:
|
||||||
|
- '${FORWARD_DB_PORT:-3306}:3306'
|
||||||
|
environment:
|
||||||
|
MYSQL_ROOT_PASSWORD: '${DB_PASSWORD}'
|
||||||
|
MYSQL_ROOT_HOST: '%'
|
||||||
|
MYSQL_DATABASE: '${DB_DATABASE}'
|
||||||
|
MYSQL_USER: '${DB_USERNAME}'
|
||||||
|
MYSQL_PASSWORD: '${DB_PASSWORD}'
|
||||||
|
MYSQL_ALLOW_EMPTY_PASSWORD: 1
|
||||||
|
MYSQL_EXTRA_OPTIONS: '${MYSQL_EXTRA_OPTIONS:-}'
|
||||||
|
volumes:
|
||||||
|
- 'sail-mysql:/var/lib/mysql'
|
||||||
|
- './docker/mysql/create-testing-database.sh:/docker-entrypoint-initdb.d/10-create-testing-database.sh'
|
||||||
|
networks:
|
||||||
|
- sail
|
||||||
|
healthcheck:
|
||||||
|
test:
|
||||||
|
- CMD
|
||||||
|
- mysqladmin
|
||||||
|
- ping
|
||||||
|
- '-p${DB_PASSWORD}'
|
||||||
|
retries: 3
|
||||||
|
timeout: 5s
|
||||||
|
redis:
|
||||||
|
image: 'redis:alpine'
|
||||||
|
ports:
|
||||||
|
- '${FORWARD_REDIS_PORT:-6379}:6379'
|
||||||
|
volumes:
|
||||||
|
- 'sail-redis:/data'
|
||||||
|
networks:
|
||||||
|
- sail
|
||||||
|
healthcheck:
|
||||||
|
test:
|
||||||
|
- CMD
|
||||||
|
- redis-cli
|
||||||
|
- ping
|
||||||
|
retries: 3
|
||||||
|
timeout: 5s
|
||||||
|
# mailpit:
|
||||||
|
# image: 'axllent/mailpit:latest'
|
||||||
|
# ports:
|
||||||
|
# - '${FORWARD_MAILPIT_PORT:-1025}:1025'
|
||||||
|
# - '${FORWARD_MAILPIT_DASHBOARD_PORT:-8025}:8025'
|
||||||
|
# networks:
|
||||||
|
# - sail
|
||||||
|
networks:
|
||||||
|
sail:
|
||||||
|
driver: bridge
|
||||||
|
volumes:
|
||||||
|
sail-mysql:
|
||||||
|
driver: local
|
||||||
|
sail-redis:
|
||||||
|
driver: local
|
||||||
91
composer.json
Normal file
91
composer.json
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://getcomposer.org/schema.json",
|
||||||
|
"name": "laravel/laravel",
|
||||||
|
"type": "project",
|
||||||
|
"description": "The skeleton application for the Laravel framework.",
|
||||||
|
"keywords": [
|
||||||
|
"laravel",
|
||||||
|
"framework"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"require": {
|
||||||
|
"php": "^8.2",
|
||||||
|
"inertiajs/inertia-laravel": "^2.0",
|
||||||
|
"laravel/framework": "^12.0",
|
||||||
|
"laravel/tinker": "^2.10.1",
|
||||||
|
"tightenco/ziggy": "^2.6"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"fakerphp/faker": "^1.23",
|
||||||
|
"laravel/pail": "^1.2.2",
|
||||||
|
"laravel/pint": "^1.24",
|
||||||
|
"laravel/sail": "^1.41",
|
||||||
|
"mockery/mockery": "^1.6",
|
||||||
|
"nunomaduro/collision": "^8.6",
|
||||||
|
"phpunit/phpunit": "^11.5.3"
|
||||||
|
},
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"App\\": "app/",
|
||||||
|
"Database\\Factories\\": "database/factories/",
|
||||||
|
"Database\\Seeders\\": "database/seeders/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"autoload-dev": {
|
||||||
|
"psr-4": {
|
||||||
|
"Tests\\": "tests/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"setup": [
|
||||||
|
"composer install",
|
||||||
|
"@php -r \"file_exists('.env') || copy('.env.example', '.env');\"",
|
||||||
|
"@php artisan key:generate",
|
||||||
|
"@php artisan migrate --force",
|
||||||
|
"npm install",
|
||||||
|
"npm run build"
|
||||||
|
],
|
||||||
|
"dev": [
|
||||||
|
"Composer\\Config::disableProcessTimeout",
|
||||||
|
"npx concurrently -c \"#93c5fd,#c4b5fd,#fb7185,#fdba74\" \"php artisan serve\" \"php artisan queue:listen --tries=1\" \"php artisan pail --timeout=0\" \"npm run dev\" --names=server,queue,logs,vite --kill-others"
|
||||||
|
],
|
||||||
|
"test": [
|
||||||
|
"@php artisan config:clear --ansi",
|
||||||
|
"@php artisan test"
|
||||||
|
],
|
||||||
|
"post-autoload-dump": [
|
||||||
|
"Illuminate\\Foundation\\ComposerScripts::postAutoloadDump",
|
||||||
|
"@php artisan package:discover --ansi"
|
||||||
|
],
|
||||||
|
"post-update-cmd": [
|
||||||
|
"@php artisan vendor:publish --tag=laravel-assets --ansi --force"
|
||||||
|
],
|
||||||
|
"post-root-package-install": [
|
||||||
|
"@php -r \"file_exists('.env') || copy('.env.example', '.env');\""
|
||||||
|
],
|
||||||
|
"post-create-project-cmd": [
|
||||||
|
"@php artisan key:generate --ansi",
|
||||||
|
"@php -r \"file_exists('database/database.sqlite') || touch('database/database.sqlite');\"",
|
||||||
|
"@php artisan migrate --graceful --ansi"
|
||||||
|
],
|
||||||
|
"pre-package-uninstall": [
|
||||||
|
"Illuminate\\Foundation\\ComposerScripts::prePackageUninstall"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"extra": {
|
||||||
|
"laravel": {
|
||||||
|
"dont-discover": []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"config": {
|
||||||
|
"optimize-autoloader": true,
|
||||||
|
"preferred-install": "dist",
|
||||||
|
"sort-packages": true,
|
||||||
|
"allow-plugins": {
|
||||||
|
"pestphp/pest-plugin": true,
|
||||||
|
"php-http/discovery": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"minimum-stability": "stable",
|
||||||
|
"prefer-stable": true
|
||||||
|
}
|
||||||
8513
composer.lock
generated
Normal file
8513
composer.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
126
config/app.php
Normal file
126
config/app.php
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
return [
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Application Name
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This value is the name of your application, which will be used when the
|
||||||
|
| framework needs to place the application's name in a notification or
|
||||||
|
| other UI elements where an application name needs to be displayed.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'name' => env('APP_NAME', 'Laravel'),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Application Environment
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This value determines the "environment" your application is currently
|
||||||
|
| running in. This may determine how you prefer to configure various
|
||||||
|
| services the application utilizes. Set this in your ".env" file.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'env' => env('APP_ENV', 'production'),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Application Debug Mode
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| When your application is in debug mode, detailed error messages with
|
||||||
|
| stack traces will be shown on every error that occurs within your
|
||||||
|
| application. If disabled, a simple generic error page is shown.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'debug' => (bool) env('APP_DEBUG', false),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Application URL
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This URL is used by the console to properly generate URLs when using
|
||||||
|
| the Artisan command line tool. You should set this to the root of
|
||||||
|
| the application so that it's available within Artisan commands.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'url' => env('APP_URL', 'http://localhost'),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Application Timezone
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Here you may specify the default timezone for your application, which
|
||||||
|
| will be used by the PHP date and date-time functions. The timezone
|
||||||
|
| is set to "UTC" by default as it is suitable for most use cases.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'timezone' => 'Asia/Taipei',
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Application Locale Configuration
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| The application locale determines the default locale that will be used
|
||||||
|
| by Laravel's translation / localization methods. This option can be
|
||||||
|
| set to any locale for which you plan to have translation strings.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'locale' => env('APP_LOCALE', 'en'),
|
||||||
|
|
||||||
|
'fallback_locale' => env('APP_FALLBACK_LOCALE', 'en'),
|
||||||
|
|
||||||
|
'faker_locale' => env('APP_FAKER_LOCALE', 'en_US'),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Encryption Key
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This key is utilized by Laravel's encryption services and should be set
|
||||||
|
| to a random, 32 character string to ensure that all encrypted values
|
||||||
|
| are secure. You should do this prior to deploying the application.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'cipher' => 'AES-256-CBC',
|
||||||
|
|
||||||
|
'key' => env('APP_KEY'),
|
||||||
|
|
||||||
|
'previous_keys' => [
|
||||||
|
...array_filter(
|
||||||
|
explode(',', (string) env('APP_PREVIOUS_KEYS', ''))
|
||||||
|
),
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Maintenance Mode Driver
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| These configuration options determine the driver used to determine and
|
||||||
|
| manage Laravel's "maintenance mode" status. The "cache" driver will
|
||||||
|
| allow maintenance mode to be controlled across multiple machines.
|
||||||
|
|
|
||||||
|
| Supported drivers: "file", "cache"
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'maintenance' => [
|
||||||
|
'driver' => env('APP_MAINTENANCE_DRIVER', 'file'),
|
||||||
|
'store' => env('APP_MAINTENANCE_STORE', 'database'),
|
||||||
|
],
|
||||||
|
|
||||||
|
];
|
||||||
115
config/auth.php
Normal file
115
config/auth.php
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
return [
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Authentication Defaults
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This option defines the default authentication "guard" and password
|
||||||
|
| reset "broker" for your application. You may change these values
|
||||||
|
| as required, but they're a perfect start for most applications.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'defaults' => [
|
||||||
|
'guard' => env('AUTH_GUARD', 'web'),
|
||||||
|
'passwords' => env('AUTH_PASSWORD_BROKER', 'users'),
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Authentication Guards
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Next, you may define every authentication guard for your application.
|
||||||
|
| Of course, a great default configuration has been defined for you
|
||||||
|
| which utilizes session storage plus the Eloquent user provider.
|
||||||
|
|
|
||||||
|
| All authentication guards have a user provider, which defines how the
|
||||||
|
| users are actually retrieved out of your database or other storage
|
||||||
|
| system used by the application. Typically, Eloquent is utilized.
|
||||||
|
|
|
||||||
|
| Supported: "session"
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'guards' => [
|
||||||
|
'web' => [
|
||||||
|
'driver' => 'session',
|
||||||
|
'provider' => 'users',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| User Providers
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| All authentication guards have a user provider, which defines how the
|
||||||
|
| users are actually retrieved out of your database or other storage
|
||||||
|
| system used by the application. Typically, Eloquent is utilized.
|
||||||
|
|
|
||||||
|
| If you have multiple user tables or models you may configure multiple
|
||||||
|
| providers to represent the model / table. These providers may then
|
||||||
|
| be assigned to any extra authentication guards you have defined.
|
||||||
|
|
|
||||||
|
| Supported: "database", "eloquent"
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'providers' => [
|
||||||
|
'users' => [
|
||||||
|
'driver' => 'eloquent',
|
||||||
|
'model' => env('AUTH_MODEL', App\Models\User::class),
|
||||||
|
],
|
||||||
|
|
||||||
|
// 'users' => [
|
||||||
|
// 'driver' => 'database',
|
||||||
|
// 'table' => 'users',
|
||||||
|
// ],
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Resetting Passwords
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| These configuration options specify the behavior of Laravel's password
|
||||||
|
| reset functionality, including the table utilized for token storage
|
||||||
|
| and the user provider that is invoked to actually retrieve users.
|
||||||
|
|
|
||||||
|
| The expiry time is the number of minutes that each reset token will be
|
||||||
|
| considered valid. This security feature keeps tokens short-lived so
|
||||||
|
| they have less time to be guessed. You may change this as needed.
|
||||||
|
|
|
||||||
|
| The throttle setting is the number of seconds a user must wait before
|
||||||
|
| generating more password reset tokens. This prevents the user from
|
||||||
|
| quickly generating a very large amount of password reset tokens.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'passwords' => [
|
||||||
|
'users' => [
|
||||||
|
'provider' => 'users',
|
||||||
|
'table' => env('AUTH_PASSWORD_RESET_TOKEN_TABLE', 'password_reset_tokens'),
|
||||||
|
'expire' => 60,
|
||||||
|
'throttle' => 60,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Password Confirmation Timeout
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Here you may define the number of seconds before a password confirmation
|
||||||
|
| window expires and users are asked to re-enter their password via the
|
||||||
|
| confirmation screen. By default, the timeout lasts for three hours.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'password_timeout' => env('AUTH_PASSWORD_TIMEOUT', 10800),
|
||||||
|
|
||||||
|
];
|
||||||
117
config/cache.php
Normal file
117
config/cache.php
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
|
||||||
|
return [
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Default Cache Store
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This option controls the default cache store that will be used by the
|
||||||
|
| framework. This connection is utilized if another isn't explicitly
|
||||||
|
| specified when running a cache operation inside the application.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'default' => env('CACHE_STORE', 'database'),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Cache Stores
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Here you may define all of the cache "stores" for your application as
|
||||||
|
| well as their drivers. You may even define multiple stores for the
|
||||||
|
| same cache driver to group types of items stored in your caches.
|
||||||
|
|
|
||||||
|
| Supported drivers: "array", "database", "file", "memcached",
|
||||||
|
| "redis", "dynamodb", "octane",
|
||||||
|
| "failover", "null"
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'stores' => [
|
||||||
|
|
||||||
|
'array' => [
|
||||||
|
'driver' => 'array',
|
||||||
|
'serialize' => false,
|
||||||
|
],
|
||||||
|
|
||||||
|
'database' => [
|
||||||
|
'driver' => 'database',
|
||||||
|
'connection' => env('DB_CACHE_CONNECTION'),
|
||||||
|
'table' => env('DB_CACHE_TABLE', 'cache'),
|
||||||
|
'lock_connection' => env('DB_CACHE_LOCK_CONNECTION'),
|
||||||
|
'lock_table' => env('DB_CACHE_LOCK_TABLE'),
|
||||||
|
],
|
||||||
|
|
||||||
|
'file' => [
|
||||||
|
'driver' => 'file',
|
||||||
|
'path' => storage_path('framework/cache/data'),
|
||||||
|
'lock_path' => storage_path('framework/cache/data'),
|
||||||
|
],
|
||||||
|
|
||||||
|
'memcached' => [
|
||||||
|
'driver' => 'memcached',
|
||||||
|
'persistent_id' => env('MEMCACHED_PERSISTENT_ID'),
|
||||||
|
'sasl' => [
|
||||||
|
env('MEMCACHED_USERNAME'),
|
||||||
|
env('MEMCACHED_PASSWORD'),
|
||||||
|
],
|
||||||
|
'options' => [
|
||||||
|
// Memcached::OPT_CONNECT_TIMEOUT => 2000,
|
||||||
|
],
|
||||||
|
'servers' => [
|
||||||
|
[
|
||||||
|
'host' => env('MEMCACHED_HOST', '127.0.0.1'),
|
||||||
|
'port' => env('MEMCACHED_PORT', 11211),
|
||||||
|
'weight' => 100,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
|
||||||
|
'redis' => [
|
||||||
|
'driver' => 'redis',
|
||||||
|
'connection' => env('REDIS_CACHE_CONNECTION', 'cache'),
|
||||||
|
'lock_connection' => env('REDIS_CACHE_LOCK_CONNECTION', 'default'),
|
||||||
|
],
|
||||||
|
|
||||||
|
'dynamodb' => [
|
||||||
|
'driver' => 'dynamodb',
|
||||||
|
'key' => env('AWS_ACCESS_KEY_ID'),
|
||||||
|
'secret' => env('AWS_SECRET_ACCESS_KEY'),
|
||||||
|
'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
|
||||||
|
'table' => env('DYNAMODB_CACHE_TABLE', 'cache'),
|
||||||
|
'endpoint' => env('DYNAMODB_ENDPOINT'),
|
||||||
|
],
|
||||||
|
|
||||||
|
'octane' => [
|
||||||
|
'driver' => 'octane',
|
||||||
|
],
|
||||||
|
|
||||||
|
'failover' => [
|
||||||
|
'driver' => 'failover',
|
||||||
|
'stores' => [
|
||||||
|
'database',
|
||||||
|
'array',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Cache Key Prefix
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| When utilizing the APC, database, memcached, Redis, and DynamoDB cache
|
||||||
|
| stores, there might be other applications using the same cache. For
|
||||||
|
| that reason, you may prefix every cache key to avoid collisions.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'prefix' => env('CACHE_PREFIX', Str::slug((string) env('APP_NAME', 'laravel')).'-cache-'),
|
||||||
|
|
||||||
|
];
|
||||||
183
config/database.php
Normal file
183
config/database.php
Normal file
@@ -0,0 +1,183 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
|
||||||
|
return [
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Default Database Connection Name
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Here you may specify which of the database connections below you wish
|
||||||
|
| to use as your default connection for database operations. This is
|
||||||
|
| the connection which will be utilized unless another connection
|
||||||
|
| is explicitly specified when you execute a query / statement.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'default' => env('DB_CONNECTION', 'sqlite'),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Database Connections
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Below are all of the database connections defined for your application.
|
||||||
|
| An example configuration is provided for each database system which
|
||||||
|
| is supported by Laravel. You're free to add / remove connections.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'connections' => [
|
||||||
|
|
||||||
|
'sqlite' => [
|
||||||
|
'driver' => 'sqlite',
|
||||||
|
'url' => env('DB_URL'),
|
||||||
|
'database' => env('DB_DATABASE', database_path('database.sqlite')),
|
||||||
|
'prefix' => '',
|
||||||
|
'foreign_key_constraints' => env('DB_FOREIGN_KEYS', true),
|
||||||
|
'busy_timeout' => null,
|
||||||
|
'journal_mode' => null,
|
||||||
|
'synchronous' => null,
|
||||||
|
'transaction_mode' => 'DEFERRED',
|
||||||
|
],
|
||||||
|
|
||||||
|
'mysql' => [
|
||||||
|
'driver' => 'mysql',
|
||||||
|
'url' => env('DB_URL'),
|
||||||
|
'host' => env('DB_HOST', '127.0.0.1'),
|
||||||
|
'port' => env('DB_PORT', '3306'),
|
||||||
|
'database' => env('DB_DATABASE', 'laravel'),
|
||||||
|
'username' => env('DB_USERNAME', 'root'),
|
||||||
|
'password' => env('DB_PASSWORD', ''),
|
||||||
|
'unix_socket' => env('DB_SOCKET', ''),
|
||||||
|
'charset' => env('DB_CHARSET', 'utf8mb4'),
|
||||||
|
'collation' => env('DB_COLLATION', 'utf8mb4_unicode_ci'),
|
||||||
|
'prefix' => '',
|
||||||
|
'prefix_indexes' => true,
|
||||||
|
'strict' => true,
|
||||||
|
'engine' => null,
|
||||||
|
'options' => extension_loaded('pdo_mysql') ? array_filter([
|
||||||
|
(PHP_VERSION_ID >= 80500 ? \Pdo\Mysql::ATTR_SSL_CA : \PDO::MYSQL_ATTR_SSL_CA) => env('MYSQL_ATTR_SSL_CA'),
|
||||||
|
]) : [],
|
||||||
|
],
|
||||||
|
|
||||||
|
'mariadb' => [
|
||||||
|
'driver' => 'mariadb',
|
||||||
|
'url' => env('DB_URL'),
|
||||||
|
'host' => env('DB_HOST', '127.0.0.1'),
|
||||||
|
'port' => env('DB_PORT', '3306'),
|
||||||
|
'database' => env('DB_DATABASE', 'laravel'),
|
||||||
|
'username' => env('DB_USERNAME', 'root'),
|
||||||
|
'password' => env('DB_PASSWORD', ''),
|
||||||
|
'unix_socket' => env('DB_SOCKET', ''),
|
||||||
|
'charset' => env('DB_CHARSET', 'utf8mb4'),
|
||||||
|
'collation' => env('DB_COLLATION', 'utf8mb4_unicode_ci'),
|
||||||
|
'prefix' => '',
|
||||||
|
'prefix_indexes' => true,
|
||||||
|
'strict' => true,
|
||||||
|
'engine' => null,
|
||||||
|
'options' => extension_loaded('pdo_mysql') ? array_filter([
|
||||||
|
(PHP_VERSION_ID >= 80500 ? \Pdo\Mysql::ATTR_SSL_CA : \PDO::MYSQL_ATTR_SSL_CA) => env('MYSQL_ATTR_SSL_CA'),
|
||||||
|
]) : [],
|
||||||
|
],
|
||||||
|
|
||||||
|
'pgsql' => [
|
||||||
|
'driver' => 'pgsql',
|
||||||
|
'url' => env('DB_URL'),
|
||||||
|
'host' => env('DB_HOST', '127.0.0.1'),
|
||||||
|
'port' => env('DB_PORT', '5432'),
|
||||||
|
'database' => env('DB_DATABASE', 'laravel'),
|
||||||
|
'username' => env('DB_USERNAME', 'root'),
|
||||||
|
'password' => env('DB_PASSWORD', ''),
|
||||||
|
'charset' => env('DB_CHARSET', 'utf8'),
|
||||||
|
'prefix' => '',
|
||||||
|
'prefix_indexes' => true,
|
||||||
|
'search_path' => 'public',
|
||||||
|
'sslmode' => 'prefer',
|
||||||
|
],
|
||||||
|
|
||||||
|
'sqlsrv' => [
|
||||||
|
'driver' => 'sqlsrv',
|
||||||
|
'url' => env('DB_URL'),
|
||||||
|
'host' => env('DB_HOST', 'localhost'),
|
||||||
|
'port' => env('DB_PORT', '1433'),
|
||||||
|
'database' => env('DB_DATABASE', 'laravel'),
|
||||||
|
'username' => env('DB_USERNAME', 'root'),
|
||||||
|
'password' => env('DB_PASSWORD', ''),
|
||||||
|
'charset' => env('DB_CHARSET', 'utf8'),
|
||||||
|
'prefix' => '',
|
||||||
|
'prefix_indexes' => true,
|
||||||
|
// 'encrypt' => env('DB_ENCRYPT', 'yes'),
|
||||||
|
// 'trust_server_certificate' => env('DB_TRUST_SERVER_CERTIFICATE', 'false'),
|
||||||
|
],
|
||||||
|
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Migration Repository Table
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This table keeps track of all the migrations that have already run for
|
||||||
|
| your application. Using this information, we can determine which of
|
||||||
|
| the migrations on disk haven't actually been run on the database.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'migrations' => [
|
||||||
|
'table' => 'migrations',
|
||||||
|
'update_date_on_publish' => true,
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Redis Databases
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Redis is an open source, fast, and advanced key-value store that also
|
||||||
|
| provides a richer body of commands than a typical key-value system
|
||||||
|
| such as Memcached. You may define your connection settings here.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'redis' => [
|
||||||
|
|
||||||
|
'client' => env('REDIS_CLIENT', 'phpredis'),
|
||||||
|
|
||||||
|
'options' => [
|
||||||
|
'cluster' => env('REDIS_CLUSTER', 'redis'),
|
||||||
|
'prefix' => env('REDIS_PREFIX', Str::slug((string) env('APP_NAME', 'laravel')).'-database-'),
|
||||||
|
'persistent' => env('REDIS_PERSISTENT', false),
|
||||||
|
],
|
||||||
|
|
||||||
|
'default' => [
|
||||||
|
'url' => env('REDIS_URL'),
|
||||||
|
'host' => env('REDIS_HOST', '127.0.0.1'),
|
||||||
|
'username' => env('REDIS_USERNAME'),
|
||||||
|
'password' => env('REDIS_PASSWORD'),
|
||||||
|
'port' => env('REDIS_PORT', '6379'),
|
||||||
|
'database' => env('REDIS_DB', '0'),
|
||||||
|
'max_retries' => env('REDIS_MAX_RETRIES', 3),
|
||||||
|
'backoff_algorithm' => env('REDIS_BACKOFF_ALGORITHM', 'decorrelated_jitter'),
|
||||||
|
'backoff_base' => env('REDIS_BACKOFF_BASE', 100),
|
||||||
|
'backoff_cap' => env('REDIS_BACKOFF_CAP', 1000),
|
||||||
|
],
|
||||||
|
|
||||||
|
'cache' => [
|
||||||
|
'url' => env('REDIS_URL'),
|
||||||
|
'host' => env('REDIS_HOST', '127.0.0.1'),
|
||||||
|
'username' => env('REDIS_USERNAME'),
|
||||||
|
'password' => env('REDIS_PASSWORD'),
|
||||||
|
'port' => env('REDIS_PORT', '6379'),
|
||||||
|
'database' => env('REDIS_CACHE_DB', '1'),
|
||||||
|
'max_retries' => env('REDIS_MAX_RETRIES', 3),
|
||||||
|
'backoff_algorithm' => env('REDIS_BACKOFF_ALGORITHM', 'decorrelated_jitter'),
|
||||||
|
'backoff_base' => env('REDIS_BACKOFF_BASE', 100),
|
||||||
|
'backoff_cap' => env('REDIS_BACKOFF_CAP', 1000),
|
||||||
|
],
|
||||||
|
|
||||||
|
],
|
||||||
|
|
||||||
|
];
|
||||||
80
config/filesystems.php
Normal file
80
config/filesystems.php
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
return [
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Default Filesystem Disk
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Here you may specify the default filesystem disk that should be used
|
||||||
|
| by the framework. The "local" disk, as well as a variety of cloud
|
||||||
|
| based disks are available to your application for file storage.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'default' => env('FILESYSTEM_DISK', 'local'),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Filesystem Disks
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Below you may configure as many filesystem disks as necessary, and you
|
||||||
|
| may even configure multiple disks for the same driver. Examples for
|
||||||
|
| most supported storage drivers are configured here for reference.
|
||||||
|
|
|
||||||
|
| Supported drivers: "local", "ftp", "sftp", "s3"
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'disks' => [
|
||||||
|
|
||||||
|
'local' => [
|
||||||
|
'driver' => 'local',
|
||||||
|
'root' => storage_path('app/private'),
|
||||||
|
'serve' => true,
|
||||||
|
'throw' => false,
|
||||||
|
'report' => false,
|
||||||
|
],
|
||||||
|
|
||||||
|
'public' => [
|
||||||
|
'driver' => 'local',
|
||||||
|
'root' => storage_path('app/public'),
|
||||||
|
'url' => env('APP_URL').'/storage',
|
||||||
|
'visibility' => 'public',
|
||||||
|
'throw' => false,
|
||||||
|
'report' => false,
|
||||||
|
],
|
||||||
|
|
||||||
|
's3' => [
|
||||||
|
'driver' => 's3',
|
||||||
|
'key' => env('AWS_ACCESS_KEY_ID'),
|
||||||
|
'secret' => env('AWS_SECRET_ACCESS_KEY'),
|
||||||
|
'region' => env('AWS_DEFAULT_REGION'),
|
||||||
|
'bucket' => env('AWS_BUCKET'),
|
||||||
|
'url' => env('AWS_URL'),
|
||||||
|
'endpoint' => env('AWS_ENDPOINT'),
|
||||||
|
'use_path_style_endpoint' => env('AWS_USE_PATH_STYLE_ENDPOINT', false),
|
||||||
|
'throw' => false,
|
||||||
|
'report' => false,
|
||||||
|
],
|
||||||
|
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Symbolic Links
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Here you may configure the symbolic links that will be created when the
|
||||||
|
| `storage:link` Artisan command is executed. The array keys should be
|
||||||
|
| the locations of the links and the values should be their targets.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'links' => [
|
||||||
|
public_path('storage') => storage_path('app/public'),
|
||||||
|
],
|
||||||
|
|
||||||
|
];
|
||||||
132
config/logging.php
Normal file
132
config/logging.php
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Monolog\Handler\NullHandler;
|
||||||
|
use Monolog\Handler\StreamHandler;
|
||||||
|
use Monolog\Handler\SyslogUdpHandler;
|
||||||
|
use Monolog\Processor\PsrLogMessageProcessor;
|
||||||
|
|
||||||
|
return [
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Default Log Channel
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This option defines the default log channel that is utilized to write
|
||||||
|
| messages to your logs. The value provided here should match one of
|
||||||
|
| the channels present in the list of "channels" configured below.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'default' => env('LOG_CHANNEL', 'stack'),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Deprecations Log Channel
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This option controls the log channel that should be used to log warnings
|
||||||
|
| regarding deprecated PHP and library features. This allows you to get
|
||||||
|
| your application ready for upcoming major versions of dependencies.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'deprecations' => [
|
||||||
|
'channel' => env('LOG_DEPRECATIONS_CHANNEL', 'null'),
|
||||||
|
'trace' => env('LOG_DEPRECATIONS_TRACE', false),
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Log Channels
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Here you may configure the log channels for your application. Laravel
|
||||||
|
| utilizes the Monolog PHP logging library, which includes a variety
|
||||||
|
| of powerful log handlers and formatters that you're free to use.
|
||||||
|
|
|
||||||
|
| Available drivers: "single", "daily", "slack", "syslog",
|
||||||
|
| "errorlog", "monolog", "custom", "stack"
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'channels' => [
|
||||||
|
|
||||||
|
'stack' => [
|
||||||
|
'driver' => 'stack',
|
||||||
|
'channels' => explode(',', (string) env('LOG_STACK', 'single')),
|
||||||
|
'ignore_exceptions' => false,
|
||||||
|
],
|
||||||
|
|
||||||
|
'single' => [
|
||||||
|
'driver' => 'single',
|
||||||
|
'path' => storage_path('logs/laravel.log'),
|
||||||
|
'level' => env('LOG_LEVEL', 'debug'),
|
||||||
|
'replace_placeholders' => true,
|
||||||
|
],
|
||||||
|
|
||||||
|
'daily' => [
|
||||||
|
'driver' => 'daily',
|
||||||
|
'path' => storage_path('logs/laravel.log'),
|
||||||
|
'level' => env('LOG_LEVEL', 'debug'),
|
||||||
|
'days' => env('LOG_DAILY_DAYS', 14),
|
||||||
|
'replace_placeholders' => true,
|
||||||
|
],
|
||||||
|
|
||||||
|
'slack' => [
|
||||||
|
'driver' => 'slack',
|
||||||
|
'url' => env('LOG_SLACK_WEBHOOK_URL'),
|
||||||
|
'username' => env('LOG_SLACK_USERNAME', 'Laravel Log'),
|
||||||
|
'emoji' => env('LOG_SLACK_EMOJI', ':boom:'),
|
||||||
|
'level' => env('LOG_LEVEL', 'critical'),
|
||||||
|
'replace_placeholders' => true,
|
||||||
|
],
|
||||||
|
|
||||||
|
'papertrail' => [
|
||||||
|
'driver' => 'monolog',
|
||||||
|
'level' => env('LOG_LEVEL', 'debug'),
|
||||||
|
'handler' => env('LOG_PAPERTRAIL_HANDLER', SyslogUdpHandler::class),
|
||||||
|
'handler_with' => [
|
||||||
|
'host' => env('PAPERTRAIL_URL'),
|
||||||
|
'port' => env('PAPERTRAIL_PORT'),
|
||||||
|
'connectionString' => 'tls://'.env('PAPERTRAIL_URL').':'.env('PAPERTRAIL_PORT'),
|
||||||
|
],
|
||||||
|
'processors' => [PsrLogMessageProcessor::class],
|
||||||
|
],
|
||||||
|
|
||||||
|
'stderr' => [
|
||||||
|
'driver' => 'monolog',
|
||||||
|
'level' => env('LOG_LEVEL', 'debug'),
|
||||||
|
'handler' => StreamHandler::class,
|
||||||
|
'handler_with' => [
|
||||||
|
'stream' => 'php://stderr',
|
||||||
|
],
|
||||||
|
'formatter' => env('LOG_STDERR_FORMATTER'),
|
||||||
|
'processors' => [PsrLogMessageProcessor::class],
|
||||||
|
],
|
||||||
|
|
||||||
|
'syslog' => [
|
||||||
|
'driver' => 'syslog',
|
||||||
|
'level' => env('LOG_LEVEL', 'debug'),
|
||||||
|
'facility' => env('LOG_SYSLOG_FACILITY', LOG_USER),
|
||||||
|
'replace_placeholders' => true,
|
||||||
|
],
|
||||||
|
|
||||||
|
'errorlog' => [
|
||||||
|
'driver' => 'errorlog',
|
||||||
|
'level' => env('LOG_LEVEL', 'debug'),
|
||||||
|
'replace_placeholders' => true,
|
||||||
|
],
|
||||||
|
|
||||||
|
'null' => [
|
||||||
|
'driver' => 'monolog',
|
||||||
|
'handler' => NullHandler::class,
|
||||||
|
],
|
||||||
|
|
||||||
|
'emergency' => [
|
||||||
|
'path' => storage_path('logs/laravel.log'),
|
||||||
|
],
|
||||||
|
|
||||||
|
],
|
||||||
|
|
||||||
|
];
|
||||||
118
config/mail.php
Normal file
118
config/mail.php
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
return [
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Default Mailer
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This option controls the default mailer that is used to send all email
|
||||||
|
| messages unless another mailer is explicitly specified when sending
|
||||||
|
| the message. All additional mailers can be configured within the
|
||||||
|
| "mailers" array. Examples of each type of mailer are provided.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'default' => env('MAIL_MAILER', 'log'),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Mailer Configurations
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Here you may configure all of the mailers used by your application plus
|
||||||
|
| their respective settings. Several examples have been configured for
|
||||||
|
| you and you are free to add your own as your application requires.
|
||||||
|
|
|
||||||
|
| Laravel supports a variety of mail "transport" drivers that can be used
|
||||||
|
| when delivering an email. You may specify which one you're using for
|
||||||
|
| your mailers below. You may also add additional mailers if needed.
|
||||||
|
|
|
||||||
|
| Supported: "smtp", "sendmail", "mailgun", "ses", "ses-v2",
|
||||||
|
| "postmark", "resend", "log", "array",
|
||||||
|
| "failover", "roundrobin"
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'mailers' => [
|
||||||
|
|
||||||
|
'smtp' => [
|
||||||
|
'transport' => 'smtp',
|
||||||
|
'scheme' => env('MAIL_SCHEME'),
|
||||||
|
'url' => env('MAIL_URL'),
|
||||||
|
'host' => env('MAIL_HOST', '127.0.0.1'),
|
||||||
|
'port' => env('MAIL_PORT', 2525),
|
||||||
|
'username' => env('MAIL_USERNAME'),
|
||||||
|
'password' => env('MAIL_PASSWORD'),
|
||||||
|
'timeout' => null,
|
||||||
|
'local_domain' => env('MAIL_EHLO_DOMAIN', parse_url((string) env('APP_URL', 'http://localhost'), PHP_URL_HOST)),
|
||||||
|
],
|
||||||
|
|
||||||
|
'ses' => [
|
||||||
|
'transport' => 'ses',
|
||||||
|
],
|
||||||
|
|
||||||
|
'postmark' => [
|
||||||
|
'transport' => 'postmark',
|
||||||
|
// 'message_stream_id' => env('POSTMARK_MESSAGE_STREAM_ID'),
|
||||||
|
// 'client' => [
|
||||||
|
// 'timeout' => 5,
|
||||||
|
// ],
|
||||||
|
],
|
||||||
|
|
||||||
|
'resend' => [
|
||||||
|
'transport' => 'resend',
|
||||||
|
],
|
||||||
|
|
||||||
|
'sendmail' => [
|
||||||
|
'transport' => 'sendmail',
|
||||||
|
'path' => env('MAIL_SENDMAIL_PATH', '/usr/sbin/sendmail -bs -i'),
|
||||||
|
],
|
||||||
|
|
||||||
|
'log' => [
|
||||||
|
'transport' => 'log',
|
||||||
|
'channel' => env('MAIL_LOG_CHANNEL'),
|
||||||
|
],
|
||||||
|
|
||||||
|
'array' => [
|
||||||
|
'transport' => 'array',
|
||||||
|
],
|
||||||
|
|
||||||
|
'failover' => [
|
||||||
|
'transport' => 'failover',
|
||||||
|
'mailers' => [
|
||||||
|
'smtp',
|
||||||
|
'log',
|
||||||
|
],
|
||||||
|
'retry_after' => 60,
|
||||||
|
],
|
||||||
|
|
||||||
|
'roundrobin' => [
|
||||||
|
'transport' => 'roundrobin',
|
||||||
|
'mailers' => [
|
||||||
|
'ses',
|
||||||
|
'postmark',
|
||||||
|
],
|
||||||
|
'retry_after' => 60,
|
||||||
|
],
|
||||||
|
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Global "From" Address
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| You may wish for all emails sent by your application to be sent from
|
||||||
|
| the same address. Here you may specify a name and address that is
|
||||||
|
| used globally for all emails that are sent by your application.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'from' => [
|
||||||
|
'address' => env('MAIL_FROM_ADDRESS', 'hello@example.com'),
|
||||||
|
'name' => env('MAIL_FROM_NAME', 'Example'),
|
||||||
|
],
|
||||||
|
|
||||||
|
];
|
||||||
129
config/queue.php
Normal file
129
config/queue.php
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
return [
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Default Queue Connection Name
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Laravel's queue supports a variety of backends via a single, unified
|
||||||
|
| API, giving you convenient access to each backend using identical
|
||||||
|
| syntax for each. The default queue connection is defined below.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'default' => env('QUEUE_CONNECTION', 'database'),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Queue Connections
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Here you may configure the connection options for every queue backend
|
||||||
|
| used by your application. An example configuration is provided for
|
||||||
|
| each backend supported by Laravel. You're also free to add more.
|
||||||
|
|
|
||||||
|
| Drivers: "sync", "database", "beanstalkd", "sqs", "redis",
|
||||||
|
| "deferred", "background", "failover", "null"
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'connections' => [
|
||||||
|
|
||||||
|
'sync' => [
|
||||||
|
'driver' => 'sync',
|
||||||
|
],
|
||||||
|
|
||||||
|
'database' => [
|
||||||
|
'driver' => 'database',
|
||||||
|
'connection' => env('DB_QUEUE_CONNECTION'),
|
||||||
|
'table' => env('DB_QUEUE_TABLE', 'jobs'),
|
||||||
|
'queue' => env('DB_QUEUE', 'default'),
|
||||||
|
'retry_after' => (int) env('DB_QUEUE_RETRY_AFTER', 90),
|
||||||
|
'after_commit' => false,
|
||||||
|
],
|
||||||
|
|
||||||
|
'beanstalkd' => [
|
||||||
|
'driver' => 'beanstalkd',
|
||||||
|
'host' => env('BEANSTALKD_QUEUE_HOST', 'localhost'),
|
||||||
|
'queue' => env('BEANSTALKD_QUEUE', 'default'),
|
||||||
|
'retry_after' => (int) env('BEANSTALKD_QUEUE_RETRY_AFTER', 90),
|
||||||
|
'block_for' => 0,
|
||||||
|
'after_commit' => false,
|
||||||
|
],
|
||||||
|
|
||||||
|
'sqs' => [
|
||||||
|
'driver' => 'sqs',
|
||||||
|
'key' => env('AWS_ACCESS_KEY_ID'),
|
||||||
|
'secret' => env('AWS_SECRET_ACCESS_KEY'),
|
||||||
|
'prefix' => env('SQS_PREFIX', 'https://sqs.us-east-1.amazonaws.com/your-account-id'),
|
||||||
|
'queue' => env('SQS_QUEUE', 'default'),
|
||||||
|
'suffix' => env('SQS_SUFFIX'),
|
||||||
|
'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
|
||||||
|
'after_commit' => false,
|
||||||
|
],
|
||||||
|
|
||||||
|
'redis' => [
|
||||||
|
'driver' => 'redis',
|
||||||
|
'connection' => env('REDIS_QUEUE_CONNECTION', 'default'),
|
||||||
|
'queue' => env('REDIS_QUEUE', 'default'),
|
||||||
|
'retry_after' => (int) env('REDIS_QUEUE_RETRY_AFTER', 90),
|
||||||
|
'block_for' => null,
|
||||||
|
'after_commit' => false,
|
||||||
|
],
|
||||||
|
|
||||||
|
'deferred' => [
|
||||||
|
'driver' => 'deferred',
|
||||||
|
],
|
||||||
|
|
||||||
|
'background' => [
|
||||||
|
'driver' => 'background',
|
||||||
|
],
|
||||||
|
|
||||||
|
'failover' => [
|
||||||
|
'driver' => 'failover',
|
||||||
|
'connections' => [
|
||||||
|
'database',
|
||||||
|
'deferred',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Job Batching
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| The following options configure the database and table that store job
|
||||||
|
| batching information. These options can be updated to any database
|
||||||
|
| connection and table which has been defined by your application.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'batching' => [
|
||||||
|
'database' => env('DB_CONNECTION', 'sqlite'),
|
||||||
|
'table' => 'job_batches',
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Failed Queue Jobs
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| These options configure the behavior of failed queue job logging so you
|
||||||
|
| can control how and where failed jobs are stored. Laravel ships with
|
||||||
|
| support for storing failed jobs in a simple file or in a database.
|
||||||
|
|
|
||||||
|
| Supported drivers: "database-uuids", "dynamodb", "file", "null"
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'failed' => [
|
||||||
|
'driver' => env('QUEUE_FAILED_DRIVER', 'database-uuids'),
|
||||||
|
'database' => env('DB_CONNECTION', 'sqlite'),
|
||||||
|
'table' => 'failed_jobs',
|
||||||
|
],
|
||||||
|
|
||||||
|
];
|
||||||
38
config/services.php
Normal file
38
config/services.php
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
return [
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Third Party Services
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This file is for storing the credentials for third party services such
|
||||||
|
| as Mailgun, Postmark, AWS and more. This file provides the de facto
|
||||||
|
| location for this type of information, allowing packages to have
|
||||||
|
| a conventional file to locate the various service credentials.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'postmark' => [
|
||||||
|
'key' => env('POSTMARK_API_KEY'),
|
||||||
|
],
|
||||||
|
|
||||||
|
'resend' => [
|
||||||
|
'key' => env('RESEND_API_KEY'),
|
||||||
|
],
|
||||||
|
|
||||||
|
'ses' => [
|
||||||
|
'key' => env('AWS_ACCESS_KEY_ID'),
|
||||||
|
'secret' => env('AWS_SECRET_ACCESS_KEY'),
|
||||||
|
'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
|
||||||
|
],
|
||||||
|
|
||||||
|
'slack' => [
|
||||||
|
'notifications' => [
|
||||||
|
'bot_user_oauth_token' => env('SLACK_BOT_USER_OAUTH_TOKEN'),
|
||||||
|
'channel' => env('SLACK_BOT_USER_DEFAULT_CHANNEL'),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
|
||||||
|
];
|
||||||
217
config/session.php
Normal file
217
config/session.php
Normal file
@@ -0,0 +1,217 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
|
||||||
|
return [
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Default Session Driver
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This option determines the default session driver that is utilized for
|
||||||
|
| incoming requests. Laravel supports a variety of storage options to
|
||||||
|
| persist session data. Database storage is a great default choice.
|
||||||
|
|
|
||||||
|
| Supported: "file", "cookie", "database", "memcached",
|
||||||
|
| "redis", "dynamodb", "array"
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'driver' => env('SESSION_DRIVER', 'database'),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Session Lifetime
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Here you may specify the number of minutes that you wish the session
|
||||||
|
| to be allowed to remain idle before it expires. If you want them
|
||||||
|
| to expire immediately when the browser is closed then you may
|
||||||
|
| indicate that via the expire_on_close configuration option.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'lifetime' => (int) env('SESSION_LIFETIME', 120),
|
||||||
|
|
||||||
|
'expire_on_close' => env('SESSION_EXPIRE_ON_CLOSE', false),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Session Encryption
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This option allows you to easily specify that all of your session data
|
||||||
|
| should be encrypted before it's stored. All encryption is performed
|
||||||
|
| automatically by Laravel and you may use the session like normal.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'encrypt' => env('SESSION_ENCRYPT', false),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Session File Location
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| When utilizing the "file" session driver, the session files are placed
|
||||||
|
| on disk. The default storage location is defined here; however, you
|
||||||
|
| are free to provide another location where they should be stored.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'files' => storage_path('framework/sessions'),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Session Database Connection
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| When using the "database" or "redis" session drivers, you may specify a
|
||||||
|
| connection that should be used to manage these sessions. This should
|
||||||
|
| correspond to a connection in your database configuration options.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'connection' => env('SESSION_CONNECTION'),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Session Database Table
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| When using the "database" session driver, you may specify the table to
|
||||||
|
| be used to store sessions. Of course, a sensible default is defined
|
||||||
|
| for you; however, you're welcome to change this to another table.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'table' => env('SESSION_TABLE', 'sessions'),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Session Cache Store
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| When using one of the framework's cache driven session backends, you may
|
||||||
|
| define the cache store which should be used to store the session data
|
||||||
|
| between requests. This must match one of your defined cache stores.
|
||||||
|
|
|
||||||
|
| Affects: "dynamodb", "memcached", "redis"
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'store' => env('SESSION_STORE'),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Session Sweeping Lottery
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Some session drivers must manually sweep their storage location to get
|
||||||
|
| rid of old sessions from storage. Here are the chances that it will
|
||||||
|
| happen on a given request. By default, the odds are 2 out of 100.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'lottery' => [2, 100],
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Session Cookie Name
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Here you may change the name of the session cookie that is created by
|
||||||
|
| the framework. Typically, you should not need to change this value
|
||||||
|
| since doing so does not grant a meaningful security improvement.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'cookie' => env(
|
||||||
|
'SESSION_COOKIE',
|
||||||
|
Str::slug((string) env('APP_NAME', 'laravel')).'-session'
|
||||||
|
),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Session Cookie Path
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| The session cookie path determines the path for which the cookie will
|
||||||
|
| be regarded as available. Typically, this will be the root path of
|
||||||
|
| your application, but you're free to change this when necessary.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'path' => env('SESSION_PATH', '/'),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Session Cookie Domain
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This value determines the domain and subdomains the session cookie is
|
||||||
|
| available to. By default, the cookie will be available to the root
|
||||||
|
| domain without subdomains. Typically, this shouldn't be changed.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'domain' => env('SESSION_DOMAIN'),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| HTTPS Only Cookies
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| By setting this option to true, session cookies will only be sent back
|
||||||
|
| to the server if the browser has a HTTPS connection. This will keep
|
||||||
|
| the cookie from being sent to you when it can't be done securely.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'secure' => env('SESSION_SECURE_COOKIE'),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| HTTP Access Only
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Setting this value to true will prevent JavaScript from accessing the
|
||||||
|
| value of the cookie and the cookie will only be accessible through
|
||||||
|
| the HTTP protocol. It's unlikely you should disable this option.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'http_only' => env('SESSION_HTTP_ONLY', true),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Same-Site Cookies
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This option determines how your cookies behave when cross-site requests
|
||||||
|
| take place, and can be used to mitigate CSRF attacks. By default, we
|
||||||
|
| will set this value to "lax" to permit secure cross-site requests.
|
||||||
|
|
|
||||||
|
| See: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#samesitesamesite-value
|
||||||
|
|
|
||||||
|
| Supported: "lax", "strict", "none", null
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'same_site' => env('SESSION_SAME_SITE', 'lax'),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Partitioned Cookies
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Setting this value to true will tie the cookie to the top-level site for
|
||||||
|
| a cross-site context. Partitioned cookies are accepted by the browser
|
||||||
|
| when flagged "secure" and the Same-Site attribute is set to "none".
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'partitioned' => env('SESSION_PARTITIONED_COOKIE', false),
|
||||||
|
|
||||||
|
];
|
||||||
1
database/.gitignore
vendored
Normal file
1
database/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
*.sqlite*
|
||||||
70
database/factories/ProductFactory.php
Normal file
70
database/factories/ProductFactory.php
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Database\Factories;
|
||||||
|
|
||||||
|
use App\Models\Category;
|
||||||
|
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Product>
|
||||||
|
*/
|
||||||
|
class ProductFactory extends Factory
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Define the model's default state.
|
||||||
|
*
|
||||||
|
* @return array<string, mixed>
|
||||||
|
*/
|
||||||
|
public function definition(): array
|
||||||
|
{
|
||||||
|
// 定義豐富的中文測試資料庫
|
||||||
|
$productNames = [
|
||||||
|
'高山烏龍茶', '茉莉綠茶', '特級紅茶', '四季春茶', '阿薩姆紅茶', '錫蘭紅茶', '普洱茶',
|
||||||
|
'珍珠粉圓', '波霸粉圓', '椰果', '仙草凍', '愛玉凍', '布丁', '紅豆', '綠豆',
|
||||||
|
'果糖', '黑糖漿', '蜂蜜', '蔗糖液', '煉乳',
|
||||||
|
'奶精粉', '鮮奶油', '全脂鮮乳', '低脂鮮乳', '燕麥奶',
|
||||||
|
'檸檬原汁', '百香果漿', '芒果果泥', '草莓果醬', '柳橙濃縮汁',
|
||||||
|
'塑膠杯 (700cc)', '紙杯 (500cc)', '封口膜', '粗吸管', '細吸管', '杯蓋',
|
||||||
|
'外帶提袋 (單杯)', '外帶提袋 (雙杯)', '吸管套', '杯架'
|
||||||
|
];
|
||||||
|
|
||||||
|
$brands = [
|
||||||
|
'統一', '光泉', '味全', '雀巢', '立頓', '天仁', '台糖', '義美', '開元', '瑞穗', '六甲田莊', '南亞塑膠'
|
||||||
|
];
|
||||||
|
|
||||||
|
$specs = [
|
||||||
|
'業務用 5kg裝', '100入/袋', '業務號 3L', '特級品', 'A級', '家庭號 2L', '10kg/箱', '500g/罐'
|
||||||
|
];
|
||||||
|
|
||||||
|
$units = ['包', '罐', '瓶', '箱', '袋', '個', '支', '公斤', '公升'];
|
||||||
|
$baseUnit = $this->faker->randomElement($units);
|
||||||
|
|
||||||
|
$largeUnits = ['箱', '袋', '桶', '磨', '組'];
|
||||||
|
$largeUnit = $this->faker->randomElement($largeUnits);
|
||||||
|
|
||||||
|
$purchaseUnits = ['箱', '棧板', '車'];
|
||||||
|
$purchaseUnit = $this->faker->randomElement($purchaseUnits);
|
||||||
|
|
||||||
|
// 隨機取得一個 Category,若沒有則略過
|
||||||
|
$category = Category::inRandomOrder()->first();
|
||||||
|
|
||||||
|
// 為了讓名稱不重複,可以加個隨機形容詞或編號,或者直接用 faker 但指定語系 (若 env 不能改)
|
||||||
|
// 這裡我們混搭:隨機選一個品名 + 隨機品牌 + 隨機規格,組成較真實的商品描述概念
|
||||||
|
|
||||||
|
$name = $this->faker->randomElement($productNames);
|
||||||
|
$brand = $this->faker->randomElement($brands);
|
||||||
|
$spec = $this->faker->randomElement($specs);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'code' => strtoupper($this->faker->unique()->bothify('??###')),
|
||||||
|
'name' => $name . ' (' . $brand . ')', // 例如:高山烏龍茶 (天仁)
|
||||||
|
'category_id' => $category ? $category->id : null,
|
||||||
|
'brand' => $brand,
|
||||||
|
'specification' => $spec,
|
||||||
|
'base_unit' => $baseUnit,
|
||||||
|
'large_unit' => $largeUnit,
|
||||||
|
'conversion_rate' => $this->faker->numberBetween(10, 1000),
|
||||||
|
'purchase_unit' => $purchaseUnit,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
44
database/factories/UserFactory.php
Normal file
44
database/factories/UserFactory.php
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Database\Factories;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||||
|
use Illuminate\Support\Facades\Hash;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\User>
|
||||||
|
*/
|
||||||
|
class UserFactory extends Factory
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The current password being used by the factory.
|
||||||
|
*/
|
||||||
|
protected static ?string $password;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Define the model's default state.
|
||||||
|
*
|
||||||
|
* @return array<string, mixed>
|
||||||
|
*/
|
||||||
|
public function definition(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'name' => fake()->name(),
|
||||||
|
'email' => fake()->unique()->safeEmail(),
|
||||||
|
'email_verified_at' => now(),
|
||||||
|
'password' => static::$password ??= Hash::make('password'),
|
||||||
|
'remember_token' => Str::random(10),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicate that the model's email address should be unverified.
|
||||||
|
*/
|
||||||
|
public function unverified(): static
|
||||||
|
{
|
||||||
|
return $this->state(fn (array $attributes) => [
|
||||||
|
'email_verified_at' => null,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
49
database/migrations/0001_01_01_000000_create_users_table.php
Normal file
49
database/migrations/0001_01_01_000000_create_users_table.php
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::create('users', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->string('name');
|
||||||
|
$table->string('email')->unique();
|
||||||
|
$table->timestamp('email_verified_at')->nullable();
|
||||||
|
$table->string('password');
|
||||||
|
$table->rememberToken();
|
||||||
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
|
||||||
|
Schema::create('password_reset_tokens', function (Blueprint $table) {
|
||||||
|
$table->string('email')->primary();
|
||||||
|
$table->string('token');
|
||||||
|
$table->timestamp('created_at')->nullable();
|
||||||
|
});
|
||||||
|
|
||||||
|
Schema::create('sessions', function (Blueprint $table) {
|
||||||
|
$table->string('id')->primary();
|
||||||
|
$table->foreignId('user_id')->nullable()->index();
|
||||||
|
$table->string('ip_address', 45)->nullable();
|
||||||
|
$table->text('user_agent')->nullable();
|
||||||
|
$table->longText('payload');
|
||||||
|
$table->integer('last_activity')->index();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('users');
|
||||||
|
Schema::dropIfExists('password_reset_tokens');
|
||||||
|
Schema::dropIfExists('sessions');
|
||||||
|
}
|
||||||
|
};
|
||||||
35
database/migrations/0001_01_01_000001_create_cache_table.php
Normal file
35
database/migrations/0001_01_01_000001_create_cache_table.php
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::create('cache', function (Blueprint $table) {
|
||||||
|
$table->string('key')->primary();
|
||||||
|
$table->mediumText('value');
|
||||||
|
$table->integer('expiration');
|
||||||
|
});
|
||||||
|
|
||||||
|
Schema::create('cache_locks', function (Blueprint $table) {
|
||||||
|
$table->string('key')->primary();
|
||||||
|
$table->string('owner');
|
||||||
|
$table->integer('expiration');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('cache');
|
||||||
|
Schema::dropIfExists('cache_locks');
|
||||||
|
}
|
||||||
|
};
|
||||||
57
database/migrations/0001_01_01_000002_create_jobs_table.php
Normal file
57
database/migrations/0001_01_01_000002_create_jobs_table.php
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::create('jobs', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->string('queue')->index();
|
||||||
|
$table->longText('payload');
|
||||||
|
$table->unsignedTinyInteger('attempts');
|
||||||
|
$table->unsignedInteger('reserved_at')->nullable();
|
||||||
|
$table->unsignedInteger('available_at');
|
||||||
|
$table->unsignedInteger('created_at');
|
||||||
|
});
|
||||||
|
|
||||||
|
Schema::create('job_batches', function (Blueprint $table) {
|
||||||
|
$table->string('id')->primary();
|
||||||
|
$table->string('name');
|
||||||
|
$table->integer('total_jobs');
|
||||||
|
$table->integer('pending_jobs');
|
||||||
|
$table->integer('failed_jobs');
|
||||||
|
$table->longText('failed_job_ids');
|
||||||
|
$table->mediumText('options')->nullable();
|
||||||
|
$table->integer('cancelled_at')->nullable();
|
||||||
|
$table->integer('created_at');
|
||||||
|
$table->integer('finished_at')->nullable();
|
||||||
|
});
|
||||||
|
|
||||||
|
Schema::create('failed_jobs', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->string('uuid')->unique();
|
||||||
|
$table->text('connection');
|
||||||
|
$table->text('queue');
|
||||||
|
$table->longText('payload');
|
||||||
|
$table->longText('exception');
|
||||||
|
$table->timestamp('failed_at')->useCurrent();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('jobs');
|
||||||
|
Schema::dropIfExists('job_batches');
|
||||||
|
Schema::dropIfExists('failed_jobs');
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::create('categories', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->string('name')->unique();
|
||||||
|
$table->string('description')->nullable();
|
||||||
|
$table->boolean('is_active')->default(true);
|
||||||
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('categories');
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::create('products', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->string('code')->unique()->comment('商品編號');
|
||||||
|
$table->string('name')->comment('商品名稱');
|
||||||
|
$table->foreignId('category_id')->constrained('categories')->onDelete('restrict')->comment('商品分類');
|
||||||
|
$table->string('brand')->nullable()->comment('品牌');
|
||||||
|
$table->text('specification')->nullable()->comment('規格描述');
|
||||||
|
|
||||||
|
// Unit conversion
|
||||||
|
$table->string('base_unit')->comment('基本庫存單位 (e.g. g, ml)');
|
||||||
|
$table->string('large_unit')->nullable()->comment('大單位 (e.g. 桶, 箱)');
|
||||||
|
$table->decimal('conversion_rate', 10, 4)->default(1)->comment('換算率 (1 大單位 = ? 基本單位)');
|
||||||
|
$table->string('purchase_unit')->nullable()->comment('採購單位');
|
||||||
|
|
||||||
|
$table->timestamps();
|
||||||
|
$table->softDeletes();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('products');
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::create('vendors', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->string('code')->unique()->comment('廠商編號');
|
||||||
|
$table->string('name')->comment('廠商名稱');
|
||||||
|
$table->string('short_name')->nullable()->comment('廠商簡稱');
|
||||||
|
$table->string('tax_id')->nullable()->comment('統一編號');
|
||||||
|
$table->string('owner')->nullable()->comment('負責人');
|
||||||
|
$table->string('contact_name')->nullable()->comment('聯絡人');
|
||||||
|
$table->string('tel')->nullable()->comment('市話');
|
||||||
|
$table->string('phone')->nullable()->comment('連絡電話');
|
||||||
|
$table->string('email')->nullable()->comment('電子郵件');
|
||||||
|
$table->text('address')->nullable()->comment('地址');
|
||||||
|
$table->text('remark')->nullable()->comment('備註');
|
||||||
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('vendors');
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::table('vendors', function (Blueprint $table) {
|
||||||
|
$table->string('code', 50)->change();
|
||||||
|
$table->string('name', 100)->change();
|
||||||
|
$table->string('short_name', 50)->nullable()->change();
|
||||||
|
$table->string('tax_id', 20)->nullable()->change();
|
||||||
|
$table->string('owner', 50)->nullable()->change();
|
||||||
|
$table->string('contact_name', 50)->nullable()->change();
|
||||||
|
$table->string('tel', 20)->nullable()->change();
|
||||||
|
$table->string('phone', 20)->nullable()->change();
|
||||||
|
});
|
||||||
|
|
||||||
|
Schema::table('products', function (Blueprint $table) {
|
||||||
|
$table->string('code', 50)->change();
|
||||||
|
$table->string('name', 100)->change();
|
||||||
|
$table->string('brand', 50)->nullable()->change();
|
||||||
|
$table->string('base_unit', 20)->change();
|
||||||
|
$table->string('large_unit', 20)->nullable()->change();
|
||||||
|
$table->string('purchase_unit', 20)->nullable()->change();
|
||||||
|
});
|
||||||
|
|
||||||
|
Schema::table('categories', function (Blueprint $table) {
|
||||||
|
$table->string('name', 50)->change();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('vendors', function (Blueprint $table) {
|
||||||
|
$table->string('code', 255)->change();
|
||||||
|
$table->string('name', 255)->change();
|
||||||
|
$table->string('short_name', 255)->nullable()->change();
|
||||||
|
$table->string('tax_id', 255)->nullable()->change();
|
||||||
|
$table->string('owner', 255)->nullable()->change();
|
||||||
|
$table->string('contact_name', 255)->nullable()->change();
|
||||||
|
$table->string('tel', 255)->nullable()->change();
|
||||||
|
$table->string('phone', 255)->nullable()->change();
|
||||||
|
});
|
||||||
|
|
||||||
|
Schema::table('products', function (Blueprint $table) {
|
||||||
|
$table->string('code', 255)->change();
|
||||||
|
$table->string('name', 255)->change();
|
||||||
|
$table->string('brand', 255)->nullable()->change();
|
||||||
|
$table->string('base_unit', 255)->change();
|
||||||
|
$table->string('large_unit', 255)->nullable()->change();
|
||||||
|
$table->string('purchase_unit', 255)->nullable()->change();
|
||||||
|
});
|
||||||
|
|
||||||
|
Schema::table('categories', function (Blueprint $table) {
|
||||||
|
$table->string('name', 255)->change();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
// 倉庫基本資料
|
||||||
|
Schema::create('warehouses', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->string('code', 50)->unique()->comment('倉庫編號');
|
||||||
|
$table->string('name', 50)->comment('倉庫名稱');
|
||||||
|
$table->string('address')->nullable()->comment('地址');
|
||||||
|
$table->text('description')->nullable()->comment('描述');
|
||||||
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 商品庫存詳情
|
||||||
|
Schema::create('inventories', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->foreignId('warehouse_id')->constrained()->onDelete('cascade');
|
||||||
|
$table->foreignId('product_id')->constrained()->onDelete('restrict');
|
||||||
|
$table->decimal('quantity', 10, 2)->default(0)->comment('當前庫存數量');
|
||||||
|
$table->decimal('safety_stock', 10, 2)->default(0)->nullable()->comment('安全存量');
|
||||||
|
$table->string('location', 50)->nullable()->comment('儲位/貨架號');
|
||||||
|
$table->timestamps();
|
||||||
|
|
||||||
|
$table->unique(['warehouse_id', 'product_id'], 'warehouse_product_unique');
|
||||||
|
});
|
||||||
|
|
||||||
|
// 庫存異動紀錄
|
||||||
|
Schema::create('inventory_transactions', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->foreignId('inventory_id')->constrained()->onDelete('cascade');
|
||||||
|
$table->string('type', 20)->comment('異動類型: purchase_in, sales_out, adjustment, transfer_in, transfer_out');
|
||||||
|
$table->decimal('quantity', 10, 2)->comment('異動數量 (+/-)');
|
||||||
|
$table->decimal('balance_before', 10, 2)->comment('異動前餘額');
|
||||||
|
$table->decimal('balance_after', 10, 2)->comment('異動後餘額');
|
||||||
|
$table->string('reason')->nullable()->comment('異動原因/備註');
|
||||||
|
$table->nullableMorphs('reference'); // reference_type, reference_id
|
||||||
|
$table->foreignId('user_id')->nullable()->constrained()->onDelete('set null')->comment('操作人員');
|
||||||
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('inventory_transactions');
|
||||||
|
Schema::dropIfExists('inventories');
|
||||||
|
Schema::dropIfExists('warehouses');
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::table('inventory_transactions', function (Blueprint $table) {
|
||||||
|
$table->string('type', 20)->comment('異動類型: 採購進貨, 銷售出庫, 盤點調整, 撥補入庫, 撥補出庫, 手動入庫')->change();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('inventory_transactions', function (Blueprint $table) {
|
||||||
|
$table->string('type', 20)->comment('異動類型: purchase_in, sales_out, adjustment, transfer_in, transfer_out')->change();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::table('inventory_transactions', function (Blueprint $table) {
|
||||||
|
$table->dateTime('actual_time')->nullable()->after('reason')->comment('實際異動發生時間');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('inventory_transactions', function (Blueprint $table) {
|
||||||
|
$table->dropColumn('actual_time');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::create('product_vendor', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->foreignId('vendor_id')->constrained()->cascadeOnDelete();
|
||||||
|
$table->foreignId('product_id')->constrained()->cascadeOnDelete();
|
||||||
|
$table->decimal('last_price', 10, 2)->nullable()->comment('最近一次進價');
|
||||||
|
$table->timestamps();
|
||||||
|
|
||||||
|
// 確保同一個廠商對同一個商品只有一筆記錄
|
||||||
|
$table->unique(['vendor_id', 'product_id']);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('product_vendor');
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::table('inventories', function (Blueprint $table) {
|
||||||
|
// Change default value of safety_stock to NULL
|
||||||
|
$table->decimal('safety_stock', 10, 2)->nullable()->default(null)->change();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('inventories', function (Blueprint $table) {
|
||||||
|
// Revert default value to 0
|
||||||
|
$table->decimal('safety_stock', 10, 2)->nullable()->default(0)->change();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::create('purchase_orders', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->string('code')->unique()->comment('採購單號');
|
||||||
|
$table->foreignId('vendor_id')->constrained()->onDelete('restrict');
|
||||||
|
$table->foreignId('warehouse_id')->nullable()->constrained()->onDelete('set null');
|
||||||
|
$table->foreignId('user_id')->constrained()->onDelete('restrict');
|
||||||
|
$table->string('status')->default('draft')->comment('草稿, 待審核, 處理中, 運送中, 待確認, 已完成');
|
||||||
|
$table->date('expected_delivery_date')->nullable();
|
||||||
|
$table->decimal('total_amount', 12, 2)->default(0);
|
||||||
|
$table->decimal('tax_amount', 12, 2)->default(0);
|
||||||
|
$table->decimal('grand_total', 12, 2)->default(0);
|
||||||
|
$table->text('remark')->nullable();
|
||||||
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('purchase_orders');
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::create('purchase_order_items', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->foreignId('purchase_order_id')->constrained()->onDelete('cascade');
|
||||||
|
$table->foreignId('product_id')->constrained()->onDelete('restrict');
|
||||||
|
$table->decimal('quantity', 10, 2);
|
||||||
|
$table->decimal('unit_price', 10, 2);
|
||||||
|
$table->decimal('subtotal', 12, 2);
|
||||||
|
$table->decimal('received_quantity', 10, 2)->default(0);
|
||||||
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('purchase_order_items');
|
||||||
|
}
|
||||||
|
};
|
||||||
47
database/seeders/CategorySeeder.php
Normal file
47
database/seeders/CategorySeeder.php
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Database\Seeders;
|
||||||
|
|
||||||
|
use App\Models\Category;
|
||||||
|
use Illuminate\Database\Seeder;
|
||||||
|
|
||||||
|
class CategorySeeder extends Seeder
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the database seeds.
|
||||||
|
*/
|
||||||
|
public function run(): void
|
||||||
|
{
|
||||||
|
$categories = [
|
||||||
|
[
|
||||||
|
'name' => '原物料',
|
||||||
|
'description' => '製作飲品或餐點的基礎原料',
|
||||||
|
'is_active' => true,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'name' => '半成品',
|
||||||
|
'description' => '已經過初步加工的原料',
|
||||||
|
'is_active' => true,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'name' => '包材',
|
||||||
|
'description' => '杯子、吸管、封膜等消耗品',
|
||||||
|
'is_active' => true,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'name' => '設備',
|
||||||
|
'description' => '店面或廚房使用的機器設備',
|
||||||
|
'is_active' => true,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'name' => '清潔用品',
|
||||||
|
'description' => '清潔與環境維護用品',
|
||||||
|
'is_active' => true,
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($categories as $category) {
|
||||||
|
Category::create($category);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
30
database/seeders/DatabaseSeeder.php
Normal file
30
database/seeders/DatabaseSeeder.php
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Database\Seeders;
|
||||||
|
|
||||||
|
use App\Models\User;
|
||||||
|
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
|
||||||
|
use Illuminate\Database\Seeder;
|
||||||
|
|
||||||
|
class DatabaseSeeder extends Seeder
|
||||||
|
{
|
||||||
|
use WithoutModelEvents;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Seed the application's database.
|
||||||
|
*/
|
||||||
|
public function run(): void
|
||||||
|
{
|
||||||
|
// User::factory(10)->create();
|
||||||
|
|
||||||
|
User::factory()->create([
|
||||||
|
'name' => 'Test User',
|
||||||
|
'email' => 'test@example.com',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->call([
|
||||||
|
CategorySeeder::class,
|
||||||
|
ProductSeeder::class,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
88
database/seeders/ProductSeeder.php
Normal file
88
database/seeders/ProductSeeder.php
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Database\Seeders;
|
||||||
|
|
||||||
|
use App\Models\Category;
|
||||||
|
use App\Models\Product;
|
||||||
|
use Illuminate\Database\Seeder;
|
||||||
|
|
||||||
|
class ProductSeeder extends Seeder
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the database seeds.
|
||||||
|
*/
|
||||||
|
public function run(): void
|
||||||
|
{
|
||||||
|
// 取得分類 ID
|
||||||
|
$rawMaterialId = Category::where('name', '原物料')->first()->id;
|
||||||
|
$packagingId = Category::where('name', '包材')->first()->id;
|
||||||
|
|
||||||
|
$products = [
|
||||||
|
// 原物料
|
||||||
|
[
|
||||||
|
'code' => 'RM001',
|
||||||
|
'name' => '二砂糖',
|
||||||
|
'category_id' => $rawMaterialId,
|
||||||
|
'brand' => '台糖',
|
||||||
|
'specification' => '業務用 50kg/袋',
|
||||||
|
'base_unit' => 'g',
|
||||||
|
'large_unit' => '袋',
|
||||||
|
'conversion_rate' => 50000,
|
||||||
|
'purchase_unit' => '袋',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'code' => 'RM002',
|
||||||
|
'name' => '錫蘭紅茶',
|
||||||
|
'category_id' => $rawMaterialId,
|
||||||
|
'brand' => '天仁',
|
||||||
|
'specification' => '特級茶葉',
|
||||||
|
'base_unit' => 'g',
|
||||||
|
'large_unit' => '箱',
|
||||||
|
'conversion_rate' => 30000, // 假設一箱 30kg
|
||||||
|
'purchase_unit' => '箱',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'code' => 'RM003',
|
||||||
|
'name' => '鮮乳',
|
||||||
|
'category_id' => $rawMaterialId,
|
||||||
|
'brand' => '光泉',
|
||||||
|
'specification' => '業務用鮮乳 1公升',
|
||||||
|
'base_unit' => 'ml',
|
||||||
|
'large_unit' => '瓶',
|
||||||
|
'conversion_rate' => 960, // 960ml
|
||||||
|
'purchase_unit' => '瓶',
|
||||||
|
],
|
||||||
|
// 包材
|
||||||
|
[
|
||||||
|
'code' => 'PC001',
|
||||||
|
'name' => 'PP飲料杯 700cc',
|
||||||
|
'category_id' => $packagingId,
|
||||||
|
'brand' => '南亞',
|
||||||
|
'specification' => '透明, 95口徑',
|
||||||
|
'base_unit' => '個',
|
||||||
|
'large_unit' => '箱',
|
||||||
|
'conversion_rate' => 1000, // 1箱1000個
|
||||||
|
'purchase_unit' => '箱',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'code' => 'PC002',
|
||||||
|
'name' => '粗吸管',
|
||||||
|
'category_id' => $packagingId,
|
||||||
|
'brand' => '',
|
||||||
|
'specification' => '獨立包裝, 12mm',
|
||||||
|
'base_unit' => '支',
|
||||||
|
'large_unit' => '包',
|
||||||
|
'conversion_rate' => 100, // 1包100支
|
||||||
|
'purchase_unit' => '箱', // 假設採購單位是箱,這裡可能需要邏輯調整,但先跟著轉換率
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($products as $product) {
|
||||||
|
// 使用 firstOrCreate 避免重複建立
|
||||||
|
Product::firstOrCreate(['code' => $product['code']], $product);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 額外隨機建立 25 筆商品以測試分頁
|
||||||
|
Product::factory()->count(25)->create();
|
||||||
|
}
|
||||||
|
}
|
||||||
38
database/seeders/VendorSeeder.php
Normal file
38
database/seeders/VendorSeeder.php
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Database\Seeders;
|
||||||
|
|
||||||
|
use App\Models\Vendor;
|
||||||
|
use Illuminate\Database\Seeder;
|
||||||
|
|
||||||
|
class VendorSeeder extends Seeder
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the database seeds.
|
||||||
|
*/
|
||||||
|
public function run(): void
|
||||||
|
{
|
||||||
|
$vendors = [
|
||||||
|
[
|
||||||
|
'code' => 'V001',
|
||||||
|
'name' => '萬源食糧行',
|
||||||
|
'contact_name' => '王老闆',
|
||||||
|
'phone' => '02-23456789',
|
||||||
|
'address' => '台北市萬華區某路123號',
|
||||||
|
'tax_id' => '12345678',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'code' => 'V002',
|
||||||
|
'name' => '新竹農產批發',
|
||||||
|
'contact_name' => '李經理',
|
||||||
|
'phone' => '03-5566778',
|
||||||
|
'address' => '新竹市北區某路456號',
|
||||||
|
'tax_id' => '87654321',
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($vendors as $vendor) {
|
||||||
|
Vendor::create($vendor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
71
docker/8.0/Dockerfile
Normal file
71
docker/8.0/Dockerfile
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
FROM ubuntu:24.04
|
||||||
|
|
||||||
|
LABEL maintainer="Taylor Otwell"
|
||||||
|
|
||||||
|
ARG WWWGROUP
|
||||||
|
ARG NODE_VERSION=24
|
||||||
|
ARG POSTGRES_VERSION=18
|
||||||
|
|
||||||
|
WORKDIR /var/www/html
|
||||||
|
|
||||||
|
ENV DEBIAN_FRONTEND=noninteractive
|
||||||
|
ENV TZ=UTC
|
||||||
|
ENV SUPERVISOR_PHP_COMMAND="/usr/bin/php -d variables_order=EGPCS /var/www/html/artisan serve --host=0.0.0.0 --port=80"
|
||||||
|
ENV SUPERVISOR_PHP_USER="sail"
|
||||||
|
|
||||||
|
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
|
||||||
|
|
||||||
|
RUN echo "Acquire::http::Pipeline-Depth 0;" > /etc/apt/apt.conf.d/99custom && \
|
||||||
|
echo "Acquire::http::No-Cache true;" >> /etc/apt/apt.conf.d/99custom && \
|
||||||
|
echo "Acquire::BrokenProxy true;" >> /etc/apt/apt.conf.d/99custom
|
||||||
|
|
||||||
|
RUN apt-get update && apt-get upgrade -y \
|
||||||
|
&& mkdir -p /etc/apt/keyrings \
|
||||||
|
&& apt-get install -y gnupg gosu curl ca-certificates zip unzip git supervisor sqlite3 libcap2-bin libpng-dev python3 dnsutils librsvg2-bin fswatch ffmpeg nano \
|
||||||
|
&& curl -sS 'https://keyserver.ubuntu.com/pks/lookup?op=get&search=0xb8dc7e53946656efbce4c1dd71daeaab4ad4cab6' | gpg --dearmor | tee /usr/share/keyrings/ppa_ondrej_php.gpg > /dev/null \
|
||||||
|
&& echo "deb [signed-by=/usr/share/keyrings/ppa_ondrej_php.gpg] https://ppa.launchpadcontent.net/ondrej/php/ubuntu noble main" > /etc/apt/sources.list.d/ppa_ondrej_php.list \
|
||||||
|
&& apt-get update \
|
||||||
|
&& apt-get install -y libgd3 php8.0-cli php8.0-dev \
|
||||||
|
php8.0-pgsql php8.0-sqlite3 php8.0-gd php8.0-imagick \
|
||||||
|
php8.0-curl php8.0-memcached php8.0-mongodb \
|
||||||
|
php8.0-imap php8.0-mysql php8.0-mbstring \
|
||||||
|
php8.0-xml php8.0-zip php8.0-bcmath php8.0-soap \
|
||||||
|
php8.0-intl php8.0-readline php8.0-pcov \
|
||||||
|
php8.0-msgpack php8.0-igbinary php8.0-ldap \
|
||||||
|
php8.0-redis php8.0-swoole php8.0-xdebug \
|
||||||
|
&& curl -sLS https://getcomposer.org/installer | php -- --install-dir=/usr/bin/ --filename=composer \
|
||||||
|
&& curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg \
|
||||||
|
&& echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_$NODE_VERSION.x nodistro main" > /etc/apt/sources.list.d/nodesource.list \
|
||||||
|
&& apt-get update \
|
||||||
|
&& apt-get install -y nodejs \
|
||||||
|
&& npm install -g npm \
|
||||||
|
&& npm install -g bun \
|
||||||
|
&& curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | gpg --dearmor | tee /usr/share/keyrings/yarnkey.gpg >/dev/null \
|
||||||
|
&& echo "deb [signed-by=/usr/share/keyrings/yarnkey.gpg] https://dl.yarnpkg.com/debian/ stable main" > /etc/apt/sources.list.d/yarn.list \
|
||||||
|
&& curl -sS https://www.postgresql.org/media/keys/ACCC4CF8.asc | gpg --dearmor | tee /usr/share/keyrings/pgdg.gpg >/dev/null \
|
||||||
|
&& echo "deb [signed-by=/usr/share/keyrings/pgdg.gpg] http://apt.postgresql.org/pub/repos/apt noble-pgdg main" > /etc/apt/sources.list.d/pgdg.list \
|
||||||
|
&& apt-get update \
|
||||||
|
&& apt-get install -y yarn \
|
||||||
|
&& apt-get install -y mysql-client \
|
||||||
|
&& apt-get install -y postgresql-client-$POSTGRES_VERSION \
|
||||||
|
&& apt-get -y autoremove \
|
||||||
|
&& apt-get clean \
|
||||||
|
&& rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
|
||||||
|
|
||||||
|
RUN update-alternatives --set php /usr/bin/php8.0
|
||||||
|
|
||||||
|
RUN setcap "cap_net_bind_service=+ep" /usr/bin/php8.0
|
||||||
|
|
||||||
|
RUN userdel -r ubuntu
|
||||||
|
RUN groupadd --force -g $WWWGROUP sail
|
||||||
|
RUN useradd -ms /bin/bash --no-user-group -g $WWWGROUP -u 1337 sail
|
||||||
|
RUN git config --global --add safe.directory /var/www/html
|
||||||
|
|
||||||
|
COPY start-container /usr/local/bin/start-container
|
||||||
|
COPY supervisord.conf /etc/supervisor/conf.d/supervisord.conf
|
||||||
|
COPY php.ini /etc/php/8.0/cli/conf.d/99-sail.ini
|
||||||
|
RUN chmod +x /usr/local/bin/start-container
|
||||||
|
|
||||||
|
EXPOSE 80/tcp
|
||||||
|
|
||||||
|
ENTRYPOINT ["start-container"]
|
||||||
5
docker/8.0/php.ini
Normal file
5
docker/8.0/php.ini
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
[PHP]
|
||||||
|
post_max_size = 100M
|
||||||
|
upload_max_filesize = 100M
|
||||||
|
variables_order = EGPCS
|
||||||
|
pcov.directory = .
|
||||||
26
docker/8.0/start-container
Normal file
26
docker/8.0/start-container
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
if [ "$SUPERVISOR_PHP_USER" != "root" ] && [ "$SUPERVISOR_PHP_USER" != "sail" ]; then
|
||||||
|
echo "You should set SUPERVISOR_PHP_USER to either 'sail' or 'root'."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ! -z "$WWWUSER" ]; then
|
||||||
|
usermod -u $WWWUSER sail
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ! -d /.composer ]; then
|
||||||
|
mkdir /.composer
|
||||||
|
fi
|
||||||
|
|
||||||
|
chmod -R ugo+rw /.composer
|
||||||
|
|
||||||
|
if [ $# -gt 0 ]; then
|
||||||
|
if [ "$SUPERVISOR_PHP_USER" = "root" ]; then
|
||||||
|
exec "$@"
|
||||||
|
else
|
||||||
|
exec gosu $WWWUSER "$@"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
exec /usr/bin/supervisord -c /etc/supervisor/conf.d/supervisord.conf
|
||||||
|
fi
|
||||||
14
docker/8.0/supervisord.conf
Normal file
14
docker/8.0/supervisord.conf
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
[supervisord]
|
||||||
|
nodaemon=true
|
||||||
|
user=root
|
||||||
|
logfile=/var/log/supervisor/supervisord.log
|
||||||
|
pidfile=/var/run/supervisord.pid
|
||||||
|
|
||||||
|
[program:php]
|
||||||
|
command=%(ENV_SUPERVISOR_PHP_COMMAND)s
|
||||||
|
user=%(ENV_SUPERVISOR_PHP_USER)s
|
||||||
|
environment=LARAVEL_SAIL="1"
|
||||||
|
stdout_logfile=/dev/stdout
|
||||||
|
stdout_logfile_maxbytes=0
|
||||||
|
stderr_logfile=/dev/stderr
|
||||||
|
stderr_logfile_maxbytes=0
|
||||||
70
docker/8.1/Dockerfile
Normal file
70
docker/8.1/Dockerfile
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
FROM ubuntu:24.04
|
||||||
|
|
||||||
|
LABEL maintainer="Taylor Otwell"
|
||||||
|
|
||||||
|
ARG WWWGROUP
|
||||||
|
ARG NODE_VERSION=24
|
||||||
|
ARG POSTGRES_VERSION=18
|
||||||
|
|
||||||
|
WORKDIR /var/www/html
|
||||||
|
|
||||||
|
ENV DEBIAN_FRONTEND=noninteractive
|
||||||
|
ENV TZ=UTC
|
||||||
|
ENV SUPERVISOR_PHP_COMMAND="/usr/bin/php -d variables_order=EGPCS /var/www/html/artisan serve --host=0.0.0.0 --port=80"
|
||||||
|
ENV SUPERVISOR_PHP_USER="sail"
|
||||||
|
|
||||||
|
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
|
||||||
|
|
||||||
|
RUN echo "Acquire::http::Pipeline-Depth 0;" > /etc/apt/apt.conf.d/99custom && \
|
||||||
|
echo "Acquire::http::No-Cache true;" >> /etc/apt/apt.conf.d/99custom && \
|
||||||
|
echo "Acquire::BrokenProxy true;" >> /etc/apt/apt.conf.d/99custom
|
||||||
|
|
||||||
|
RUN apt-get update && apt-get upgrade -y \
|
||||||
|
&& mkdir -p /etc/apt/keyrings \
|
||||||
|
&& apt-get install -y gnupg gosu curl ca-certificates zip unzip git supervisor sqlite3 libcap2-bin libpng-dev python3 dnsutils librsvg2-bin fswatch ffmpeg nano \
|
||||||
|
&& curl -sS 'https://keyserver.ubuntu.com/pks/lookup?op=get&search=0xb8dc7e53946656efbce4c1dd71daeaab4ad4cab6' | gpg --dearmor | tee /usr/share/keyrings/ppa_ondrej_php.gpg > /dev/null \
|
||||||
|
&& echo "deb [signed-by=/usr/share/keyrings/ppa_ondrej_php.gpg] https://ppa.launchpadcontent.net/ondrej/php/ubuntu noble main" > /etc/apt/sources.list.d/ppa_ondrej_php.list \
|
||||||
|
&& apt-get update \
|
||||||
|
&& apt-get install -y libgd3 php8.1-cli php8.1-dev \
|
||||||
|
php8.1-pgsql php8.1-sqlite3 php8.1-gd php8.1-imagick \
|
||||||
|
php8.1-curl php8.1-mongodb \
|
||||||
|
php8.1-imap php8.1-mysql php8.1-mbstring \
|
||||||
|
php8.1-xml php8.1-zip php8.1-bcmath php8.1-soap \
|
||||||
|
php8.1-intl php8.1-readline \
|
||||||
|
php8.1-ldap \
|
||||||
|
php8.1-msgpack php8.1-igbinary php8.1-redis php8.1-swoole \
|
||||||
|
php8.1-memcached php8.1-pcov php8.1-xdebug \
|
||||||
|
&& curl -sLS https://getcomposer.org/installer | php -- --install-dir=/usr/bin/ --filename=composer \
|
||||||
|
&& curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg \
|
||||||
|
&& echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_$NODE_VERSION.x nodistro main" > /etc/apt/sources.list.d/nodesource.list \
|
||||||
|
&& apt-get update \
|
||||||
|
&& apt-get install -y nodejs \
|
||||||
|
&& npm install -g npm \
|
||||||
|
&& npm install -g bun \
|
||||||
|
&& curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | gpg --dearmor | tee /usr/share/keyrings/yarn.gpg >/dev/null \
|
||||||
|
&& echo "deb [signed-by=/usr/share/keyrings/yarn.gpg] https://dl.yarnpkg.com/debian/ stable main" > /etc/apt/sources.list.d/yarn.list \
|
||||||
|
&& curl -sS https://www.postgresql.org/media/keys/ACCC4CF8.asc | gpg --dearmor | tee /usr/share/keyrings/pgdg.gpg >/dev/null \
|
||||||
|
&& echo "deb [signed-by=/usr/share/keyrings/pgdg.gpg] http://apt.postgresql.org/pub/repos/apt noble-pgdg main" > /etc/apt/sources.list.d/pgdg.list \
|
||||||
|
&& apt-get update \
|
||||||
|
&& apt-get install -y yarn \
|
||||||
|
&& apt-get install -y mysql-client \
|
||||||
|
&& apt-get install -y postgresql-client-$POSTGRES_VERSION \
|
||||||
|
&& apt-get -y autoremove \
|
||||||
|
&& apt-get clean \
|
||||||
|
&& rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
|
||||||
|
|
||||||
|
RUN setcap "cap_net_bind_service=+ep" /usr/bin/php8.1
|
||||||
|
|
||||||
|
RUN userdel -r ubuntu
|
||||||
|
RUN groupadd --force -g $WWWGROUP sail
|
||||||
|
RUN useradd -ms /bin/bash --no-user-group -g $WWWGROUP -u 1337 sail
|
||||||
|
RUN git config --global --add safe.directory /var/www/html
|
||||||
|
|
||||||
|
COPY start-container /usr/local/bin/start-container
|
||||||
|
COPY supervisord.conf /etc/supervisor/conf.d/supervisord.conf
|
||||||
|
COPY php.ini /etc/php/8.1/cli/conf.d/99-sail.ini
|
||||||
|
RUN chmod +x /usr/local/bin/start-container
|
||||||
|
|
||||||
|
EXPOSE 80/tcp
|
||||||
|
|
||||||
|
ENTRYPOINT ["start-container"]
|
||||||
5
docker/8.1/php.ini
Normal file
5
docker/8.1/php.ini
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
[PHP]
|
||||||
|
post_max_size = 100M
|
||||||
|
upload_max_filesize = 100M
|
||||||
|
variables_order = EGPCS
|
||||||
|
pcov.directory = .
|
||||||
26
docker/8.1/start-container
Normal file
26
docker/8.1/start-container
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
if [ "$SUPERVISOR_PHP_USER" != "root" ] && [ "$SUPERVISOR_PHP_USER" != "sail" ]; then
|
||||||
|
echo "You should set SUPERVISOR_PHP_USER to either 'sail' or 'root'."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ! -z "$WWWUSER" ]; then
|
||||||
|
usermod -u $WWWUSER sail
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ! -d /.composer ]; then
|
||||||
|
mkdir /.composer
|
||||||
|
fi
|
||||||
|
|
||||||
|
chmod -R ugo+rw /.composer
|
||||||
|
|
||||||
|
if [ $# -gt 0 ]; then
|
||||||
|
if [ "$SUPERVISOR_PHP_USER" = "root" ]; then
|
||||||
|
exec "$@"
|
||||||
|
else
|
||||||
|
exec gosu $WWWUSER "$@"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
exec /usr/bin/supervisord -c /etc/supervisor/conf.d/supervisord.conf
|
||||||
|
fi
|
||||||
14
docker/8.1/supervisord.conf
Normal file
14
docker/8.1/supervisord.conf
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
[supervisord]
|
||||||
|
nodaemon=true
|
||||||
|
user=root
|
||||||
|
logfile=/var/log/supervisor/supervisord.log
|
||||||
|
pidfile=/var/run/supervisord.pid
|
||||||
|
|
||||||
|
[program:php]
|
||||||
|
command=%(ENV_SUPERVISOR_PHP_COMMAND)s
|
||||||
|
user=%(ENV_SUPERVISOR_PHP_USER)s
|
||||||
|
environment=LARAVEL_SAIL="1"
|
||||||
|
stdout_logfile=/dev/stdout
|
||||||
|
stdout_logfile_maxbytes=0
|
||||||
|
stderr_logfile=/dev/stderr
|
||||||
|
stderr_logfile_maxbytes=0
|
||||||
71
docker/8.2/Dockerfile
Normal file
71
docker/8.2/Dockerfile
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
FROM ubuntu:24.04
|
||||||
|
|
||||||
|
LABEL maintainer="Taylor Otwell"
|
||||||
|
|
||||||
|
ARG WWWGROUP
|
||||||
|
ARG NODE_VERSION=24
|
||||||
|
ARG POSTGRES_VERSION=18
|
||||||
|
|
||||||
|
WORKDIR /var/www/html
|
||||||
|
|
||||||
|
ENV DEBIAN_FRONTEND=noninteractive
|
||||||
|
ENV TZ=UTC
|
||||||
|
ENV SUPERVISOR_PHP_COMMAND="/usr/bin/php -d variables_order=EGPCS /var/www/html/artisan serve --host=0.0.0.0 --port=80"
|
||||||
|
ENV SUPERVISOR_PHP_USER="sail"
|
||||||
|
|
||||||
|
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
|
||||||
|
|
||||||
|
RUN echo "Acquire::http::Pipeline-Depth 0;" > /etc/apt/apt.conf.d/99custom && \
|
||||||
|
echo "Acquire::http::No-Cache true;" >> /etc/apt/apt.conf.d/99custom && \
|
||||||
|
echo "Acquire::BrokenProxy true;" >> /etc/apt/apt.conf.d/99custom
|
||||||
|
|
||||||
|
RUN apt-get update && apt-get upgrade -y \
|
||||||
|
&& mkdir -p /etc/apt/keyrings \
|
||||||
|
&& apt-get install -y gnupg gosu curl ca-certificates zip unzip git supervisor sqlite3 libcap2-bin libpng-dev python3 dnsutils librsvg2-bin fswatch ffmpeg nano \
|
||||||
|
&& curl -sS 'https://keyserver.ubuntu.com/pks/lookup?op=get&search=0xb8dc7e53946656efbce4c1dd71daeaab4ad4cab6' | gpg --dearmor | tee /etc/apt/keyrings/ppa_ondrej_php.gpg > /dev/null \
|
||||||
|
&& echo "deb [signed-by=/etc/apt/keyrings/ppa_ondrej_php.gpg] https://ppa.launchpadcontent.net/ondrej/php/ubuntu noble main" > /etc/apt/sources.list.d/ppa_ondrej_php.list \
|
||||||
|
&& apt-get update \
|
||||||
|
&& apt-get install -y libgd3 php8.2-cli php8.2-dev \
|
||||||
|
php8.2-pgsql php8.2-sqlite3 php8.2-gd php8.2-imagick \
|
||||||
|
php8.2-curl php8.2-mongodb \
|
||||||
|
php8.2-imap php8.2-mysql php8.2-mbstring \
|
||||||
|
php8.2-xml php8.2-zip php8.2-bcmath php8.2-soap \
|
||||||
|
php8.2-intl php8.2-readline \
|
||||||
|
php8.2-ldap \
|
||||||
|
php8.2-msgpack php8.2-igbinary php8.2-redis php8.2-swoole \
|
||||||
|
php8.2-memcached php8.2-pcov php8.2-xdebug \
|
||||||
|
&& curl -sLS https://getcomposer.org/installer | php -- --install-dir=/usr/bin/ --filename=composer \
|
||||||
|
&& curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg \
|
||||||
|
&& echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_$NODE_VERSION.x nodistro main" > /etc/apt/sources.list.d/nodesource.list \
|
||||||
|
&& apt-get update \
|
||||||
|
&& apt-get install -y nodejs \
|
||||||
|
&& npm install -g npm \
|
||||||
|
&& npm install -g pnpm \
|
||||||
|
&& npm install -g bun \
|
||||||
|
&& curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | gpg --dearmor | tee /etc/apt/keyrings/yarn.gpg >/dev/null \
|
||||||
|
&& echo "deb [signed-by=/etc/apt/keyrings/yarn.gpg] https://dl.yarnpkg.com/debian/ stable main" > /etc/apt/sources.list.d/yarn.list \
|
||||||
|
&& curl -sS https://www.postgresql.org/media/keys/ACCC4CF8.asc | gpg --dearmor | tee /etc/apt/keyrings/pgdg.gpg >/dev/null \
|
||||||
|
&& echo "deb [signed-by=/etc/apt/keyrings/pgdg.gpg] http://apt.postgresql.org/pub/repos/apt noble-pgdg main" > /etc/apt/sources.list.d/pgdg.list \
|
||||||
|
&& apt-get update \
|
||||||
|
&& apt-get install -y yarn \
|
||||||
|
&& apt-get install -y mysql-client \
|
||||||
|
&& apt-get install -y postgresql-client-$POSTGRES_VERSION \
|
||||||
|
&& apt-get -y autoremove \
|
||||||
|
&& apt-get clean \
|
||||||
|
&& rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
|
||||||
|
|
||||||
|
RUN setcap "cap_net_bind_service=+ep" /usr/bin/php8.2
|
||||||
|
|
||||||
|
RUN userdel -r ubuntu
|
||||||
|
RUN groupadd --force -g $WWWGROUP sail
|
||||||
|
RUN useradd -ms /bin/bash --no-user-group -g $WWWGROUP -u 1337 sail
|
||||||
|
RUN git config --global --add safe.directory /var/www/html
|
||||||
|
|
||||||
|
COPY start-container /usr/local/bin/start-container
|
||||||
|
COPY supervisord.conf /etc/supervisor/conf.d/supervisord.conf
|
||||||
|
COPY php.ini /etc/php/8.2/cli/conf.d/99-sail.ini
|
||||||
|
RUN chmod +x /usr/local/bin/start-container
|
||||||
|
|
||||||
|
EXPOSE 80/tcp
|
||||||
|
|
||||||
|
ENTRYPOINT ["start-container"]
|
||||||
5
docker/8.2/php.ini
Normal file
5
docker/8.2/php.ini
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
[PHP]
|
||||||
|
post_max_size = 100M
|
||||||
|
upload_max_filesize = 100M
|
||||||
|
variables_order = EGPCS
|
||||||
|
pcov.directory = .
|
||||||
26
docker/8.2/start-container
Normal file
26
docker/8.2/start-container
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
if [ "$SUPERVISOR_PHP_USER" != "root" ] && [ "$SUPERVISOR_PHP_USER" != "sail" ]; then
|
||||||
|
echo "You should set SUPERVISOR_PHP_USER to either 'sail' or 'root'."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ! -z "$WWWUSER" ]; then
|
||||||
|
usermod -u $WWWUSER sail
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ! -d /.composer ]; then
|
||||||
|
mkdir /.composer
|
||||||
|
fi
|
||||||
|
|
||||||
|
chmod -R ugo+rw /.composer
|
||||||
|
|
||||||
|
if [ $# -gt 0 ]; then
|
||||||
|
if [ "$SUPERVISOR_PHP_USER" = "root" ]; then
|
||||||
|
exec "$@"
|
||||||
|
else
|
||||||
|
exec gosu $WWWUSER "$@"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
exec /usr/bin/supervisord -c /etc/supervisor/conf.d/supervisord.conf
|
||||||
|
fi
|
||||||
14
docker/8.2/supervisord.conf
Normal file
14
docker/8.2/supervisord.conf
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
[supervisord]
|
||||||
|
nodaemon=true
|
||||||
|
user=root
|
||||||
|
logfile=/var/log/supervisor/supervisord.log
|
||||||
|
pidfile=/var/run/supervisord.pid
|
||||||
|
|
||||||
|
[program:php]
|
||||||
|
command=%(ENV_SUPERVISOR_PHP_COMMAND)s
|
||||||
|
user=%(ENV_SUPERVISOR_PHP_USER)s
|
||||||
|
environment=LARAVEL_SAIL="1"
|
||||||
|
stdout_logfile=/dev/stdout
|
||||||
|
stdout_logfile_maxbytes=0
|
||||||
|
stderr_logfile=/dev/stderr
|
||||||
|
stderr_logfile_maxbytes=0
|
||||||
74
docker/8.3/Dockerfile
Normal file
74
docker/8.3/Dockerfile
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
FROM ubuntu:24.04
|
||||||
|
|
||||||
|
LABEL maintainer="Taylor Otwell"
|
||||||
|
|
||||||
|
ARG WWWGROUP
|
||||||
|
ARG NODE_VERSION=24
|
||||||
|
ARG MYSQL_CLIENT="mysql-client"
|
||||||
|
ARG POSTGRES_VERSION=18
|
||||||
|
|
||||||
|
WORKDIR /var/www/html
|
||||||
|
|
||||||
|
ENV DEBIAN_FRONTEND=noninteractive
|
||||||
|
ENV TZ=UTC
|
||||||
|
ENV SUPERVISOR_PHP_COMMAND="/usr/bin/php -d variables_order=EGPCS /var/www/html/artisan serve --host=0.0.0.0 --port=80"
|
||||||
|
ENV SUPERVISOR_PHP_USER="sail"
|
||||||
|
ENV PLAYWRIGHT_BROWSERS_PATH=0
|
||||||
|
|
||||||
|
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
|
||||||
|
|
||||||
|
RUN echo "Acquire::http::Pipeline-Depth 0;" > /etc/apt/apt.conf.d/99custom && \
|
||||||
|
echo "Acquire::http::No-Cache true;" >> /etc/apt/apt.conf.d/99custom && \
|
||||||
|
echo "Acquire::BrokenProxy true;" >> /etc/apt/apt.conf.d/99custom
|
||||||
|
|
||||||
|
RUN apt-get update && apt-get upgrade -y \
|
||||||
|
&& mkdir -p /etc/apt/keyrings \
|
||||||
|
&& apt-get install -y gnupg gosu curl ca-certificates zip unzip git supervisor sqlite3 libcap2-bin libpng-dev python3 dnsutils librsvg2-bin fswatch ffmpeg nano \
|
||||||
|
&& curl -sS 'https://keyserver.ubuntu.com/pks/lookup?op=get&search=0xb8dc7e53946656efbce4c1dd71daeaab4ad4cab6' | gpg --dearmor | tee /etc/apt/keyrings/ppa_ondrej_php.gpg > /dev/null \
|
||||||
|
&& echo "deb [signed-by=/etc/apt/keyrings/ppa_ondrej_php.gpg] https://ppa.launchpadcontent.net/ondrej/php/ubuntu noble main" > /etc/apt/sources.list.d/ppa_ondrej_php.list \
|
||||||
|
&& apt-get update \
|
||||||
|
&& apt-get install -y libgd3 php8.3-cli php8.3-dev \
|
||||||
|
php8.3-pgsql php8.3-sqlite3 php8.3-gd \
|
||||||
|
php8.3-curl php8.3-mongodb \
|
||||||
|
php8.3-imap php8.3-mysql php8.3-mbstring \
|
||||||
|
php8.3-xml php8.3-zip php8.3-bcmath php8.3-soap \
|
||||||
|
php8.3-intl php8.3-readline \
|
||||||
|
php8.3-ldap \
|
||||||
|
php8.3-msgpack php8.3-igbinary php8.3-redis \
|
||||||
|
php8.3-memcached php8.3-pcov php8.3-imagick php8.3-xdebug php8.3-swoole \
|
||||||
|
&& curl -sLS https://getcomposer.org/installer | php -- --install-dir=/usr/bin/ --filename=composer \
|
||||||
|
&& curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg \
|
||||||
|
&& echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_$NODE_VERSION.x nodistro main" > /etc/apt/sources.list.d/nodesource.list \
|
||||||
|
&& apt-get update \
|
||||||
|
&& apt-get install -y nodejs \
|
||||||
|
&& npm install -g npm \
|
||||||
|
&& npm install -g pnpm \
|
||||||
|
&& npm install -g bun \
|
||||||
|
&& npx playwright install-deps \
|
||||||
|
&& curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | gpg --dearmor | tee /etc/apt/keyrings/yarn.gpg >/dev/null \
|
||||||
|
&& echo "deb [signed-by=/etc/apt/keyrings/yarn.gpg] https://dl.yarnpkg.com/debian/ stable main" > /etc/apt/sources.list.d/yarn.list \
|
||||||
|
&& curl -sS https://www.postgresql.org/media/keys/ACCC4CF8.asc | gpg --dearmor | tee /etc/apt/keyrings/pgdg.gpg >/dev/null \
|
||||||
|
&& echo "deb [signed-by=/etc/apt/keyrings/pgdg.gpg] http://apt.postgresql.org/pub/repos/apt noble-pgdg main" > /etc/apt/sources.list.d/pgdg.list \
|
||||||
|
&& apt-get update \
|
||||||
|
&& apt-get install -y yarn \
|
||||||
|
&& apt-get install -y $MYSQL_CLIENT \
|
||||||
|
&& apt-get install -y postgresql-client-$POSTGRES_VERSION \
|
||||||
|
&& apt-get -y autoremove \
|
||||||
|
&& apt-get clean \
|
||||||
|
&& rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
|
||||||
|
|
||||||
|
RUN setcap "cap_net_bind_service=+ep" /usr/bin/php8.3
|
||||||
|
|
||||||
|
RUN userdel -r ubuntu
|
||||||
|
RUN groupadd --force -g $WWWGROUP sail
|
||||||
|
RUN useradd -ms /bin/bash --no-user-group -g $WWWGROUP -u 1337 sail
|
||||||
|
RUN git config --global --add safe.directory /var/www/html
|
||||||
|
|
||||||
|
COPY start-container /usr/local/bin/start-container
|
||||||
|
COPY supervisord.conf /etc/supervisor/conf.d/supervisord.conf
|
||||||
|
COPY php.ini /etc/php/8.3/cli/conf.d/99-sail.ini
|
||||||
|
RUN chmod +x /usr/local/bin/start-container
|
||||||
|
|
||||||
|
EXPOSE 80/tcp
|
||||||
|
|
||||||
|
ENTRYPOINT ["start-container"]
|
||||||
5
docker/8.3/php.ini
Normal file
5
docker/8.3/php.ini
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
[PHP]
|
||||||
|
post_max_size = 100M
|
||||||
|
upload_max_filesize = 100M
|
||||||
|
variables_order = EGPCS
|
||||||
|
pcov.directory = .
|
||||||
26
docker/8.3/start-container
Normal file
26
docker/8.3/start-container
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
if [ "$SUPERVISOR_PHP_USER" != "root" ] && [ "$SUPERVISOR_PHP_USER" != "sail" ]; then
|
||||||
|
echo "You should set SUPERVISOR_PHP_USER to either 'sail' or 'root'."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ! -z "$WWWUSER" ]; then
|
||||||
|
usermod -u $WWWUSER sail
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ! -d /.composer ]; then
|
||||||
|
mkdir /.composer
|
||||||
|
fi
|
||||||
|
|
||||||
|
chmod -R ugo+rw /.composer
|
||||||
|
|
||||||
|
if [ $# -gt 0 ]; then
|
||||||
|
if [ "$SUPERVISOR_PHP_USER" = "root" ]; then
|
||||||
|
exec "$@"
|
||||||
|
else
|
||||||
|
exec gosu $WWWUSER "$@"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
exec /usr/bin/supervisord -c /etc/supervisor/conf.d/supervisord.conf
|
||||||
|
fi
|
||||||
14
docker/8.3/supervisord.conf
Normal file
14
docker/8.3/supervisord.conf
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
[supervisord]
|
||||||
|
nodaemon=true
|
||||||
|
user=root
|
||||||
|
logfile=/var/log/supervisor/supervisord.log
|
||||||
|
pidfile=/var/run/supervisord.pid
|
||||||
|
|
||||||
|
[program:php]
|
||||||
|
command=%(ENV_SUPERVISOR_PHP_COMMAND)s
|
||||||
|
user=%(ENV_SUPERVISOR_PHP_USER)s
|
||||||
|
environment=LARAVEL_SAIL="1"
|
||||||
|
stdout_logfile=/dev/stdout
|
||||||
|
stdout_logfile_maxbytes=0
|
||||||
|
stderr_logfile=/dev/stderr
|
||||||
|
stderr_logfile_maxbytes=0
|
||||||
74
docker/8.4/Dockerfile
Normal file
74
docker/8.4/Dockerfile
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
FROM ubuntu:24.04
|
||||||
|
|
||||||
|
LABEL maintainer="Taylor Otwell"
|
||||||
|
|
||||||
|
ARG WWWGROUP
|
||||||
|
ARG NODE_VERSION=24
|
||||||
|
ARG MYSQL_CLIENT="mysql-client"
|
||||||
|
ARG POSTGRES_VERSION=18
|
||||||
|
|
||||||
|
WORKDIR /var/www/html
|
||||||
|
|
||||||
|
ENV DEBIAN_FRONTEND=noninteractive
|
||||||
|
ENV TZ=UTC
|
||||||
|
ENV SUPERVISOR_PHP_COMMAND="/usr/bin/php -d variables_order=EGPCS /var/www/html/artisan serve --host=0.0.0.0 --port=80"
|
||||||
|
ENV SUPERVISOR_PHP_USER="sail"
|
||||||
|
ENV PLAYWRIGHT_BROWSERS_PATH=0
|
||||||
|
|
||||||
|
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
|
||||||
|
|
||||||
|
RUN echo "Acquire::http::Pipeline-Depth 0;" > /etc/apt/apt.conf.d/99custom && \
|
||||||
|
echo "Acquire::http::No-Cache true;" >> /etc/apt/apt.conf.d/99custom && \
|
||||||
|
echo "Acquire::BrokenProxy true;" >> /etc/apt/apt.conf.d/99custom
|
||||||
|
|
||||||
|
RUN apt-get update && apt-get upgrade -y \
|
||||||
|
&& mkdir -p /etc/apt/keyrings \
|
||||||
|
&& apt-get install -y gnupg gosu curl ca-certificates zip unzip git supervisor sqlite3 libcap2-bin libpng-dev python3 dnsutils librsvg2-bin fswatch ffmpeg nano \
|
||||||
|
&& curl -sS 'https://keyserver.ubuntu.com/pks/lookup?op=get&search=0xb8dc7e53946656efbce4c1dd71daeaab4ad4cab6' | gpg --dearmor | tee /etc/apt/keyrings/ppa_ondrej_php.gpg > /dev/null \
|
||||||
|
&& echo "deb [signed-by=/etc/apt/keyrings/ppa_ondrej_php.gpg] https://ppa.launchpadcontent.net/ondrej/php/ubuntu noble main" > /etc/apt/sources.list.d/ppa_ondrej_php.list \
|
||||||
|
&& apt-get update \
|
||||||
|
&& apt-get install -y libgd3 php8.4-cli php8.4-dev \
|
||||||
|
php8.4-pgsql php8.4-sqlite3 php8.4-gd \
|
||||||
|
php8.4-curl php8.4-mongodb \
|
||||||
|
php8.4-imap php8.4-mysql php8.4-mbstring \
|
||||||
|
php8.4-xml php8.4-zip php8.4-bcmath php8.4-soap \
|
||||||
|
php8.4-intl php8.4-readline \
|
||||||
|
php8.4-ldap \
|
||||||
|
php8.4-msgpack php8.4-igbinary php8.4-redis php8.4-swoole \
|
||||||
|
php8.4-memcached php8.4-pcov php8.4-imagick php8.4-xdebug \
|
||||||
|
&& curl -sLS https://getcomposer.org/installer | php -- --install-dir=/usr/bin/ --filename=composer \
|
||||||
|
&& curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg \
|
||||||
|
&& echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_$NODE_VERSION.x nodistro main" > /etc/apt/sources.list.d/nodesource.list \
|
||||||
|
&& apt-get update \
|
||||||
|
&& apt-get install -y nodejs \
|
||||||
|
&& npm install -g npm \
|
||||||
|
&& npm install -g pnpm \
|
||||||
|
&& npm install -g bun \
|
||||||
|
&& npx playwright install-deps \
|
||||||
|
&& curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | gpg --dearmor | tee /etc/apt/keyrings/yarn.gpg >/dev/null \
|
||||||
|
&& echo "deb [signed-by=/etc/apt/keyrings/yarn.gpg] https://dl.yarnpkg.com/debian/ stable main" > /etc/apt/sources.list.d/yarn.list \
|
||||||
|
&& curl -sS https://www.postgresql.org/media/keys/ACCC4CF8.asc | gpg --dearmor | tee /etc/apt/keyrings/pgdg.gpg >/dev/null \
|
||||||
|
&& echo "deb [signed-by=/etc/apt/keyrings/pgdg.gpg] http://apt.postgresql.org/pub/repos/apt noble-pgdg main" > /etc/apt/sources.list.d/pgdg.list \
|
||||||
|
&& apt-get update \
|
||||||
|
&& apt-get install -y yarn \
|
||||||
|
&& apt-get install -y $MYSQL_CLIENT \
|
||||||
|
&& apt-get install -y postgresql-client-$POSTGRES_VERSION \
|
||||||
|
&& apt-get -y autoremove \
|
||||||
|
&& apt-get clean \
|
||||||
|
&& rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
|
||||||
|
|
||||||
|
RUN setcap "cap_net_bind_service=+ep" /usr/bin/php8.4
|
||||||
|
|
||||||
|
RUN userdel -r ubuntu
|
||||||
|
RUN groupadd --force -g $WWWGROUP sail
|
||||||
|
RUN useradd -ms /bin/bash --no-user-group -g $WWWGROUP -u 1337 sail
|
||||||
|
RUN git config --global --add safe.directory /var/www/html
|
||||||
|
|
||||||
|
COPY start-container /usr/local/bin/start-container
|
||||||
|
COPY supervisord.conf /etc/supervisor/conf.d/supervisord.conf
|
||||||
|
COPY php.ini /etc/php/8.4/cli/conf.d/99-sail.ini
|
||||||
|
RUN chmod +x /usr/local/bin/start-container
|
||||||
|
|
||||||
|
EXPOSE 80/tcp
|
||||||
|
|
||||||
|
ENTRYPOINT ["start-container"]
|
||||||
5
docker/8.4/php.ini
Normal file
5
docker/8.4/php.ini
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
[PHP]
|
||||||
|
post_max_size = 100M
|
||||||
|
upload_max_filesize = 100M
|
||||||
|
variables_order = EGPCS
|
||||||
|
pcov.directory = .
|
||||||
26
docker/8.4/start-container
Normal file
26
docker/8.4/start-container
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
if [ "$SUPERVISOR_PHP_USER" != "root" ] && [ "$SUPERVISOR_PHP_USER" != "sail" ]; then
|
||||||
|
echo "You should set SUPERVISOR_PHP_USER to either 'sail' or 'root'."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ! -z "$WWWUSER" ]; then
|
||||||
|
usermod -u $WWWUSER sail
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ! -d /.composer ]; then
|
||||||
|
mkdir /.composer
|
||||||
|
fi
|
||||||
|
|
||||||
|
chmod -R ugo+rw /.composer
|
||||||
|
|
||||||
|
if [ $# -gt 0 ]; then
|
||||||
|
if [ "$SUPERVISOR_PHP_USER" = "root" ]; then
|
||||||
|
exec "$@"
|
||||||
|
else
|
||||||
|
exec gosu $WWWUSER "$@"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
exec /usr/bin/supervisord -c /etc/supervisor/conf.d/supervisord.conf
|
||||||
|
fi
|
||||||
14
docker/8.4/supervisord.conf
Normal file
14
docker/8.4/supervisord.conf
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
[supervisord]
|
||||||
|
nodaemon=true
|
||||||
|
user=root
|
||||||
|
logfile=/var/log/supervisor/supervisord.log
|
||||||
|
pidfile=/var/run/supervisord.pid
|
||||||
|
|
||||||
|
[program:php]
|
||||||
|
command=%(ENV_SUPERVISOR_PHP_COMMAND)s
|
||||||
|
user=%(ENV_SUPERVISOR_PHP_USER)s
|
||||||
|
environment=LARAVEL_SAIL="1"
|
||||||
|
stdout_logfile=/dev/stdout
|
||||||
|
stdout_logfile_maxbytes=0
|
||||||
|
stderr_logfile=/dev/stderr
|
||||||
|
stderr_logfile_maxbytes=0
|
||||||
92
docker/8.5/Dockerfile
Normal file
92
docker/8.5/Dockerfile
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
FROM ubuntu:24.04
|
||||||
|
|
||||||
|
LABEL maintainer="Taylor Otwell"
|
||||||
|
|
||||||
|
ARG WWWGROUP
|
||||||
|
ARG NODE_VERSION=24
|
||||||
|
ARG MYSQL_CLIENT="mysql-client"
|
||||||
|
ARG POSTGRES_VERSION=18
|
||||||
|
|
||||||
|
WORKDIR /var/www/html
|
||||||
|
|
||||||
|
ENV DEBIAN_FRONTEND=noninteractive
|
||||||
|
ENV TZ=UTC
|
||||||
|
ENV SUPERVISOR_PHP_COMMAND="/usr/bin/php -d variables_order=EGPCS /var/www/html/artisan serve --host=0.0.0.0 --port=80"
|
||||||
|
ENV SUPERVISOR_PHP_USER="sail"
|
||||||
|
ENV PLAYWRIGHT_BROWSERS_PATH=0
|
||||||
|
|
||||||
|
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
|
||||||
|
|
||||||
|
RUN echo "Acquire::http::Pipeline-Depth 0;" > /etc/apt/apt.conf.d/99custom && \
|
||||||
|
echo "Acquire::http::No-Cache true;" >> /etc/apt/apt.conf.d/99custom && \
|
||||||
|
echo "Acquire::BrokenProxy true;" >> /etc/apt/apt.conf.d/99custom
|
||||||
|
|
||||||
|
RUN apt-get update && apt-get upgrade -y \
|
||||||
|
&& mkdir -p /etc/apt/keyrings \
|
||||||
|
&& apt-get install -y gnupg gosu curl ca-certificates zip unzip git supervisor sqlite3 libcap2-bin libpng-dev python3 dnsutils librsvg2-bin fswatch ffmpeg nano \
|
||||||
|
&& curl -sS 'https://keyserver.ubuntu.com/pks/lookup?op=get&search=0xb8dc7e53946656efbce4c1dd71daeaab4ad4cab6' | gpg --dearmor | tee /etc/apt/keyrings/ppa_ondrej_php.gpg > /dev/null \
|
||||||
|
&& echo "deb [signed-by=/etc/apt/keyrings/ppa_ondrej_php.gpg] https://ppa.launchpadcontent.net/ondrej/php/ubuntu noble main" > /etc/apt/sources.list.d/ppa_ondrej_php.list \
|
||||||
|
&& apt-get update \
|
||||||
|
&& apt-get install -y \
|
||||||
|
libgd3 \
|
||||||
|
php8.5-cli \
|
||||||
|
php8.5-dev \
|
||||||
|
php8.5-pgsql \
|
||||||
|
php8.5-sqlite3 \
|
||||||
|
php8.5-gd \
|
||||||
|
php8.5-curl \
|
||||||
|
php8.5-mongodb \
|
||||||
|
php8.5-imap \
|
||||||
|
php8.5-mysql \
|
||||||
|
php8.5-mbstring \
|
||||||
|
php8.5-xml \
|
||||||
|
php8.5-zip \
|
||||||
|
php8.5-bcmath \
|
||||||
|
php8.5-soap \
|
||||||
|
php8.5-intl \
|
||||||
|
php8.5-readline \
|
||||||
|
php8.5-ldap \
|
||||||
|
php8.5-msgpack \
|
||||||
|
php8.5-igbinary \
|
||||||
|
php8.5-redis \
|
||||||
|
#php8.5-swoole \
|
||||||
|
php8.5-memcached \
|
||||||
|
php8.5-pcov \
|
||||||
|
php8.5-imagick \
|
||||||
|
php8.5-xdebug \
|
||||||
|
&& curl -sLS https://getcomposer.org/installer | php -- --install-dir=/usr/bin/ --filename=composer \
|
||||||
|
&& curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg \
|
||||||
|
&& echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_$NODE_VERSION.x nodistro main" > /etc/apt/sources.list.d/nodesource.list \
|
||||||
|
&& apt-get update \
|
||||||
|
&& apt-get install -y nodejs \
|
||||||
|
&& npm install -g npm \
|
||||||
|
&& npm install -g pnpm \
|
||||||
|
&& npm install -g bun \
|
||||||
|
&& npx playwright install-deps \
|
||||||
|
&& curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | gpg --dearmor | tee /etc/apt/keyrings/yarn.gpg >/dev/null \
|
||||||
|
&& echo "deb [signed-by=/etc/apt/keyrings/yarn.gpg] https://dl.yarnpkg.com/debian/ stable main" > /etc/apt/sources.list.d/yarn.list \
|
||||||
|
&& curl -sS https://www.postgresql.org/media/keys/ACCC4CF8.asc | gpg --dearmor | tee /etc/apt/keyrings/pgdg.gpg >/dev/null \
|
||||||
|
&& echo "deb [signed-by=/etc/apt/keyrings/pgdg.gpg] http://apt.postgresql.org/pub/repos/apt noble-pgdg main" > /etc/apt/sources.list.d/pgdg.list \
|
||||||
|
&& apt-get update \
|
||||||
|
&& apt-get install -y yarn \
|
||||||
|
&& apt-get install -y $MYSQL_CLIENT \
|
||||||
|
&& apt-get install -y postgresql-client-$POSTGRES_VERSION \
|
||||||
|
&& apt-get -y autoremove \
|
||||||
|
&& apt-get clean \
|
||||||
|
&& rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
|
||||||
|
|
||||||
|
RUN setcap "cap_net_bind_service=+ep" /usr/bin/php8.5
|
||||||
|
|
||||||
|
RUN userdel -r ubuntu
|
||||||
|
RUN groupadd --force -g $WWWGROUP sail
|
||||||
|
RUN useradd -ms /bin/bash --no-user-group -g $WWWGROUP -u 1337 sail
|
||||||
|
RUN git config --global --add safe.directory /var/www/html
|
||||||
|
|
||||||
|
COPY start-container /usr/local/bin/start-container
|
||||||
|
COPY supervisord.conf /etc/supervisor/conf.d/supervisord.conf
|
||||||
|
COPY php.ini /etc/php/8.5/cli/conf.d/99-sail.ini
|
||||||
|
RUN chmod +x /usr/local/bin/start-container
|
||||||
|
|
||||||
|
EXPOSE 80/tcp
|
||||||
|
|
||||||
|
ENTRYPOINT ["start-container"]
|
||||||
5
docker/8.5/php.ini
Normal file
5
docker/8.5/php.ini
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
[PHP]
|
||||||
|
post_max_size = 100M
|
||||||
|
upload_max_filesize = 100M
|
||||||
|
variables_order = EGPCS
|
||||||
|
pcov.directory = .
|
||||||
26
docker/8.5/start-container
Normal file
26
docker/8.5/start-container
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
if [ "$SUPERVISOR_PHP_USER" != "root" ] && [ "$SUPERVISOR_PHP_USER" != "sail" ]; then
|
||||||
|
echo "You should set SUPERVISOR_PHP_USER to either 'sail' or 'root'."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ! -z "$WWWUSER" ]; then
|
||||||
|
usermod -u $WWWUSER sail
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ! -d /.composer ]; then
|
||||||
|
mkdir /.composer
|
||||||
|
fi
|
||||||
|
|
||||||
|
chmod -R ugo+rw /.composer
|
||||||
|
|
||||||
|
if [ $# -gt 0 ]; then
|
||||||
|
if [ "$SUPERVISOR_PHP_USER" = "root" ]; then
|
||||||
|
exec "$@"
|
||||||
|
else
|
||||||
|
exec gosu $WWWUSER "$@"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
exec /usr/bin/supervisord -c /etc/supervisor/conf.d/supervisord.conf
|
||||||
|
fi
|
||||||
14
docker/8.5/supervisord.conf
Normal file
14
docker/8.5/supervisord.conf
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
[supervisord]
|
||||||
|
nodaemon=true
|
||||||
|
user=root
|
||||||
|
logfile=/var/log/supervisor/supervisord.log
|
||||||
|
pidfile=/var/run/supervisord.pid
|
||||||
|
|
||||||
|
[program:php]
|
||||||
|
command=%(ENV_SUPERVISOR_PHP_COMMAND)s
|
||||||
|
user=%(ENV_SUPERVISOR_PHP_USER)s
|
||||||
|
environment=LARAVEL_SAIL="1"
|
||||||
|
stdout_logfile=/dev/stdout
|
||||||
|
stdout_logfile_maxbytes=0
|
||||||
|
stderr_logfile=/dev/stderr
|
||||||
|
stderr_logfile_maxbytes=0
|
||||||
11
docker/mariadb/create-testing-database.sh
Normal file
11
docker/mariadb/create-testing-database.sh
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
/usr/bin/mariadb --user=root --password="$MYSQL_ROOT_PASSWORD" <<-EOSQL
|
||||||
|
CREATE DATABASE IF NOT EXISTS testing;
|
||||||
|
EOSQL
|
||||||
|
|
||||||
|
if [ -n "$MYSQL_USER" ]; then
|
||||||
|
/usr/bin/mariadb --user=root --password="$MYSQL_ROOT_PASSWORD" <<-EOSQL
|
||||||
|
GRANT ALL PRIVILEGES ON \`testing%\`.* TO '$MYSQL_USER'@'%';
|
||||||
|
EOSQL
|
||||||
|
fi
|
||||||
11
docker/mysql/create-testing-database.sh
Normal file
11
docker/mysql/create-testing-database.sh
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
mysql --user=root --password="$MYSQL_ROOT_PASSWORD" <<-EOSQL
|
||||||
|
CREATE DATABASE IF NOT EXISTS testing;
|
||||||
|
EOSQL
|
||||||
|
|
||||||
|
if [ -n "$MYSQL_USER" ]; then
|
||||||
|
mysql --user=root --password="$MYSQL_ROOT_PASSWORD" <<-EOSQL
|
||||||
|
GRANT ALL PRIVILEGES ON \`testing%\`.* TO '$MYSQL_USER'@'%';
|
||||||
|
EOSQL
|
||||||
|
fi
|
||||||
2
docker/pgsql/create-testing-database.sql
Normal file
2
docker/pgsql/create-testing-database.sql
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
SELECT 'CREATE DATABASE testing'
|
||||||
|
WHERE NOT EXISTS (SELECT FROM pg_database WHERE datname = 'testing')\gexec
|
||||||
4410
package-lock.json
generated
Normal file
4410
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
47
package.json
Normal file
47
package.json
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://www.schemastore.org/package.json",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"build": "vite build",
|
||||||
|
"dev": "vite"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@tailwindcss/vite": "^4.0.0",
|
||||||
|
"@types/node": "^25.0.3",
|
||||||
|
"@types/react": "^19.2.7",
|
||||||
|
"@types/react-dom": "^19.2.3",
|
||||||
|
"axios": "^1.11.0",
|
||||||
|
"concurrently": "^9.0.1",
|
||||||
|
"laravel-vite-plugin": "^2.0.0",
|
||||||
|
"tailwindcss": "^4.0.0",
|
||||||
|
"typescript": "^5.9.3",
|
||||||
|
"vite": "^7.0.7"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@inertiajs/react": "^2.3.4",
|
||||||
|
"@radix-ui/react-alert-dialog": "^1.1.15",
|
||||||
|
"@radix-ui/react-checkbox": "^1.3.3",
|
||||||
|
"@radix-ui/react-collapsible": "^1.1.12",
|
||||||
|
"@radix-ui/react-dialog": "^1.1.15",
|
||||||
|
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
||||||
|
"@radix-ui/react-label": "^2.1.8",
|
||||||
|
"@radix-ui/react-popover": "^1.1.15",
|
||||||
|
"@radix-ui/react-scroll-area": "^1.2.10",
|
||||||
|
"@radix-ui/react-select": "^2.2.6",
|
||||||
|
"@radix-ui/react-slot": "^1.2.4",
|
||||||
|
"@radix-ui/react-tooltip": "^1.2.8",
|
||||||
|
"@types/lodash": "^4.17.21",
|
||||||
|
"@vitejs/plugin-react": "^5.1.2",
|
||||||
|
"class-variance-authority": "^0.7.1",
|
||||||
|
"clsx": "^2.1.1",
|
||||||
|
"cmdk": "^1.1.1",
|
||||||
|
"jsbarcode": "^3.12.1",
|
||||||
|
"lodash": "^4.17.21",
|
||||||
|
"lucide-react": "^0.562.0",
|
||||||
|
"react": "^18.3.1",
|
||||||
|
"react-dom": "^18.3.1",
|
||||||
|
"sonner": "^2.0.7",
|
||||||
|
"tailwind-merge": "^3.4.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
34
phpunit.xml
Normal file
34
phpunit.xml
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"
|
||||||
|
bootstrap="vendor/autoload.php"
|
||||||
|
colors="true"
|
||||||
|
>
|
||||||
|
<testsuites>
|
||||||
|
<testsuite name="Unit">
|
||||||
|
<directory>tests/Unit</directory>
|
||||||
|
</testsuite>
|
||||||
|
<testsuite name="Feature">
|
||||||
|
<directory>tests/Feature</directory>
|
||||||
|
</testsuite>
|
||||||
|
</testsuites>
|
||||||
|
<source>
|
||||||
|
<include>
|
||||||
|
<directory>app</directory>
|
||||||
|
</include>
|
||||||
|
</source>
|
||||||
|
<php>
|
||||||
|
<env name="APP_ENV" value="testing"/>
|
||||||
|
<env name="APP_MAINTENANCE_DRIVER" value="file"/>
|
||||||
|
<env name="BCRYPT_ROUNDS" value="4"/>
|
||||||
|
<env name="BROADCAST_CONNECTION" value="null"/>
|
||||||
|
<env name="CACHE_STORE" value="array"/>
|
||||||
|
<env name="DB_DATABASE" value="testing"/>
|
||||||
|
<env name="MAIL_MAILER" value="array"/>
|
||||||
|
<env name="QUEUE_CONNECTION" value="sync"/>
|
||||||
|
<env name="SESSION_DRIVER" value="array"/>
|
||||||
|
<env name="PULSE_ENABLED" value="false"/>
|
||||||
|
<env name="TELESCOPE_ENABLED" value="false"/>
|
||||||
|
<env name="NIGHTWATCH_ENABLED" value="false"/>
|
||||||
|
</php>
|
||||||
|
</phpunit>
|
||||||
25
public/.htaccess
Normal file
25
public/.htaccess
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
<IfModule mod_rewrite.c>
|
||||||
|
<IfModule mod_negotiation.c>
|
||||||
|
Options -MultiViews -Indexes
|
||||||
|
</IfModule>
|
||||||
|
|
||||||
|
RewriteEngine On
|
||||||
|
|
||||||
|
# Handle Authorization Header
|
||||||
|
RewriteCond %{HTTP:Authorization} .
|
||||||
|
RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]
|
||||||
|
|
||||||
|
# Handle X-XSRF-Token Header
|
||||||
|
RewriteCond %{HTTP:x-xsrf-token} .
|
||||||
|
RewriteRule .* - [E=HTTP_X_XSRF_TOKEN:%{HTTP:X-XSRF-Token}]
|
||||||
|
|
||||||
|
# Redirect Trailing Slashes If Not A Folder...
|
||||||
|
RewriteCond %{REQUEST_FILENAME} !-d
|
||||||
|
RewriteCond %{REQUEST_URI} (.+)/$
|
||||||
|
RewriteRule ^ %1 [L,R=301]
|
||||||
|
|
||||||
|
# Send Requests To Front Controller...
|
||||||
|
RewriteCond %{REQUEST_FILENAME} !-d
|
||||||
|
RewriteCond %{REQUEST_FILENAME} !-f
|
||||||
|
RewriteRule ^ index.php [L]
|
||||||
|
</IfModule>
|
||||||
0
public/favicon.ico
Normal file
0
public/favicon.ico
Normal file
BIN
public/images/barcode-placeholder.png
Normal file
BIN
public/images/barcode-placeholder.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.5 KiB |
BIN
public/images/barcode-sample.png
Normal file
BIN
public/images/barcode-sample.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.1 KiB |
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user