Compare commits
29 Commits
6631f64e4b
...
bd29410191
| Author | SHA1 | Date | |
|---|---|---|---|
| bd29410191 | |||
| be315a76cc | |||
| fad74df6ac | |||
| 7160a7e780 | |||
| 3e28067c97 | |||
| fd3ddd0bac | |||
| 5797ff118d | |||
| 41d5e8e7fc | |||
| f4ca6b09e8 | |||
| 1c8c3009ec | |||
| 315cce467e | |||
| 001ba33335 | |||
| 6209b28345 | |||
| 1759fceaed | |||
| 54d36f51e7 | |||
| 981d887ae8 | |||
| 8e91f28ef4 | |||
| fbcdcd05b0 | |||
| 8d838ee6f6 | |||
| fd86ae0153 | |||
| cdf434d63c | |||
| ccdbe48b88 | |||
| 564c6588c1 | |||
| 21d0ea4cc2 | |||
| 6b0f3c9bcd | |||
| 0aaa761a47 | |||
| d683861233 | |||
| b240877d40 | |||
| e31715becb |
@@ -1,54 +0,0 @@
|
|||||||
# name: Koori-ERP-Demo-Deploy
|
|
||||||
# on:
|
|
||||||
# push:
|
|
||||||
# branches:
|
|
||||||
# - demo
|
|
||||||
|
|
||||||
# jobs:
|
|
||||||
# sync-update:
|
|
||||||
# runs-on: ubuntu-latest
|
|
||||||
# steps:
|
|
||||||
# - name: 1. Checkout New Code
|
|
||||||
# uses: actions/checkout@v3
|
|
||||||
# with:
|
|
||||||
# github-server-url: http://192.168.0.103:3000
|
|
||||||
# repository: ${{ gitea.repository }}
|
|
||||||
# # - name: 1. Checkout New Code
|
|
||||||
# # run: |
|
|
||||||
# # # 進入工作目錄並直接用 git 抓 code,完全不需要 Node
|
|
||||||
# # rm -rf ./*
|
|
||||||
# # git clone -b main http://server:3000/${{ gitea.repository }}.git .
|
|
||||||
|
|
||||||
# - name: 2. Sync Files to Running Container
|
|
||||||
# run: |
|
|
||||||
# # A. 執行複製
|
|
||||||
# cp .env.example .env
|
|
||||||
# sed -i "s|APP_KEY=.*|APP_KEY=${{ secrets.APP_KEY }}|g" .env
|
|
||||||
# # B. 確保容器環境是最新的
|
|
||||||
# # --wait 會確保容器真的跑起來了才執行下一步
|
|
||||||
# docker compose up -d --build --force-recreate --wait
|
|
||||||
|
|
||||||
# # C. 執行精簡化複製 (關鍵優化!)
|
|
||||||
# # 排除 .git, node_modules, vendor 這三大黑洞
|
|
||||||
# tar --exclude='.git' \
|
|
||||||
# --exclude='node_modules' \
|
|
||||||
# --exclude='vendor' \
|
|
||||||
# -cf - . | docker exec -i koori-erp-laravel tar -xf - -C /var/www/html
|
|
||||||
|
|
||||||
# docker exec koori-erp-laravel chown -R 1000:1000 /var/www/html
|
|
||||||
|
|
||||||
|
|
||||||
# - name: 3. Backend & Frontend Build
|
|
||||||
# run: |
|
|
||||||
# docker exec -u 1000:1000 -w /var/www/html koori-erp-laravel sh -c "
|
|
||||||
# composer install --optimize-autoloader &&
|
|
||||||
# npm install &&
|
|
||||||
# npm run build &&
|
|
||||||
# php artisan migrate --force &&
|
|
||||||
# php artisan optimize:clear
|
|
||||||
# "
|
|
||||||
|
|
||||||
# - name: 4. Final Permission Fix
|
|
||||||
# run: |
|
|
||||||
# # 統一修正權限
|
|
||||||
# docker exec koori-erp-laravel chmod -R 775 /var/www/html/storage /var/www/html/bootstrap/cache
|
|
||||||
@@ -31,48 +31,85 @@ jobs:
|
|||||||
docker exec koori-erp-laravel chmod -R 775 /var/www/html/storage /var/www/html/bootstrap/cache
|
docker exec koori-erp-laravel chmod -R 775 /var/www/html/storage /var/www/html/bootstrap/cache
|
||||||
|
|
||||||
# --- 2. 正式環境部署 (erp.koori.tw:2224) ---
|
# --- 2. 正式環境部署 (erp.koori.tw:2224) ---
|
||||||
# deploy-production:
|
deploy-production:
|
||||||
# if: github.ref == 'refs/heads/main'
|
if: github.ref == 'refs/heads/main'
|
||||||
# runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
# steps:
|
steps:
|
||||||
# - name: Checkout Code
|
- name: Checkout Code
|
||||||
# uses: actions/checkout@v3
|
uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
github-server-url: http://192.168.0.103:3000
|
||||||
|
repository: ${{ github.repository }}
|
||||||
|
|
||||||
# # 使用 rsync 透過 2224 Port 推送代碼
|
- name: Step 1 - Push Code to Production
|
||||||
# - name: Push Code to Production
|
run: |
|
||||||
# run: |
|
apt-get update && apt-get install -y rsync openssh-client
|
||||||
# # 注意:這裡的 -e 指定了 ssh port 2224
|
mkdir -p ~/.ssh
|
||||||
# rsync -avz --delete \
|
echo "${{ secrets.PROD_SSH_KEY }}" > ~/.ssh/id_rsa_prod
|
||||||
# --exclude='.git' \
|
chmod 600 ~/.ssh/id_rsa_prod
|
||||||
# --exclude='node_modules' \
|
rsync -avz --delete \
|
||||||
# --exclude='vendor' \
|
--exclude='.git' \
|
||||||
# -e "ssh -p 2224 -o StrictHostKeyChecking=no" \
|
--exclude='.env' \
|
||||||
# ./ root@erp.koori.tw:/var/www/koori-erp-prod/
|
--exclude='node_modules' \
|
||||||
|
--exclude='vendor' \
|
||||||
|
-e "ssh -p 2224 -i ~/.ssh/id_rsa_prod -o StrictHostKeyChecking=no" \
|
||||||
|
./ root@erp.koori.tw:/var/www/koori-erp-prod/
|
||||||
|
rm ~/.ssh/id_rsa_prod
|
||||||
|
|
||||||
# # 遠端執行 Docker 指令
|
# 2. 啟動或重建容器(502 最容易發生在這裡的瞬間)
|
||||||
# - name: Remote Docker Commands
|
- name: Step 2 - Container Up & Health Check
|
||||||
# uses: appleboy/ssh-action@master
|
uses: appleboy/ssh-action@master
|
||||||
# with:
|
with:
|
||||||
# host: erp.koori.tw
|
host: erp.koori.tw
|
||||||
# port: 2224 # <--- 這裡指定了 2224 Port
|
port: 2224
|
||||||
# username: root
|
username: root
|
||||||
# key: ${{ secrets.PROD_SSH_KEY }}
|
key: ${{ secrets.PROD_SSH_KEY }}
|
||||||
# script: |
|
script: |
|
||||||
# cd /var/www/koori-erp-prod
|
cd /var/www/koori-erp-prod
|
||||||
|
chown -R 1000:1000 .
|
||||||
|
WWWGROUP=1000 WWWUSER=1000 docker compose up -d --build --wait
|
||||||
|
echo "容器狀態:" && docker ps --filter "name=koori-erp-laravel"
|
||||||
|
|
||||||
# # 1. 確保 .env 存在 (建議正式機手動維護 .env,不隨 git 連動)
|
# 3. 處理後端與前端依賴(這時網站可能因為沒 vendor 呈現 500/502)
|
||||||
# if [ ! -f .env ]; then cp .env.example .env; fi
|
- name: Step 3 - Composer & NPM Build
|
||||||
|
uses: appleboy/ssh-action@master
|
||||||
|
with:
|
||||||
|
host: erp.koori.tw
|
||||||
|
port: 2224
|
||||||
|
username: root
|
||||||
|
key: ${{ secrets.PROD_SSH_KEY }}
|
||||||
|
script: |
|
||||||
|
docker exec -u 1000:1000 -w /var/www/html koori-erp-laravel sh -c "
|
||||||
|
composer install --no-dev --optimize-autoloader &&
|
||||||
|
npm install &&
|
||||||
|
npm run build
|
||||||
|
"
|
||||||
|
|
||||||
# # 2. 啟動容器
|
# 4. 處理資料庫與 Laravel 快取
|
||||||
# docker compose up -d --build
|
- name: Step 4 - Database & Optimization
|
||||||
|
uses: appleboy/ssh-action@master
|
||||||
# # 3. 執行 Laravel 正式環境優化流程
|
with:
|
||||||
# docker exec -u 1000:1000 koori-erp-laravel-prod sh -c "
|
host: erp.koori.tw
|
||||||
# composer install --no-dev --optimize-autoloader &&
|
port: 2224
|
||||||
# npm install &&
|
username: root
|
||||||
# npm run build &&
|
key: ${{ secrets.PROD_SSH_KEY }}
|
||||||
# php artisan migrate --force &&
|
script: |
|
||||||
# php artisan config:cache &&
|
docker exec -u 1000:1000 -w /var/www/html koori-erp-laravel sh -c "
|
||||||
# php artisan route:cache &&
|
php artisan migrate --force &&
|
||||||
# php artisan view:cache
|
php artisan optimize:clear &&
|
||||||
# "
|
php artisan optimize &&
|
||||||
|
php artisan view:cache
|
||||||
|
"
|
||||||
|
# 5. 最後權限修正與重啟(一發入魂,解決 502)
|
||||||
|
- name: Step 5 - Final Permission & Service Restart
|
||||||
|
uses: appleboy/ssh-action@master
|
||||||
|
with:
|
||||||
|
host: erp.koori.tw
|
||||||
|
port: 2224
|
||||||
|
username: root
|
||||||
|
key: ${{ secrets.PROD_SSH_KEY }}
|
||||||
|
script: |
|
||||||
|
docker exec koori-erp-laravel chmod -R 775 /var/www/html/storage /var/www/html/bootstrap/cache
|
||||||
|
echo "正在進行最後重啟以確保服務生效..."
|
||||||
|
# docker restart koori-erp-laravel
|
||||||
|
echo "部署完成!"
|
||||||
@@ -54,7 +54,7 @@ docker exec -it koori-erp-laravel.test-1 php artisan migrate --seed
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker exec -it koori-erp-laravel.test-1 npm install
|
docker exec -it koori-erp-laravel.test-1 npm install
|
||||||
docker exec -it koori-erp-laravel.test-1 npm run build
|
docker exec -it koori-erp-laravel.test-1 npm run dev
|
||||||
```
|
```
|
||||||
|
|
||||||
啟動後,您可以透過以下連結瀏覽專案:
|
啟動後,您可以透過以下連結瀏覽專案:
|
||||||
|
|||||||
@@ -62,7 +62,10 @@ class PurchaseOrderController extends Controller
|
|||||||
return [
|
return [
|
||||||
'productId' => (string) $product->id,
|
'productId' => (string) $product->id,
|
||||||
'productName' => $product->name,
|
'productName' => $product->name,
|
||||||
'unit' => $product->base_unit,
|
'unit' => $product->purchase_unit ?: ($product->large_unit ?: $product->base_unit), // 優先使用採購單位 > 大單位 > 基本單位
|
||||||
|
'base_unit' => $product->base_unit,
|
||||||
|
'purchase_unit' => $product->purchase_unit ?: $product->large_unit, // 若無採購單位,預設為大單位
|
||||||
|
'conversion_rate' => (float) $product->conversion_rate,
|
||||||
'lastPrice' => (float) ($product->pivot->last_price ?? 0),
|
'lastPrice' => (float) ($product->pivot->last_price ?? 0),
|
||||||
];
|
];
|
||||||
})
|
})
|
||||||
@@ -173,6 +176,23 @@ class PurchaseOrderController extends Controller
|
|||||||
{
|
{
|
||||||
$order = PurchaseOrder::with(['vendor', 'warehouse', 'user', 'items.product'])->findOrFail($id);
|
$order = PurchaseOrder::with(['vendor', 'warehouse', 'user', 'items.product'])->findOrFail($id);
|
||||||
|
|
||||||
|
// Transform items to include product details needed for frontend calculation
|
||||||
|
$order->items->transform(function ($item) {
|
||||||
|
$product = $item->product;
|
||||||
|
if ($product) {
|
||||||
|
// 手動附加 productName 和 unit (因為已從 $appends 移除)
|
||||||
|
$item->productName = $product->name;
|
||||||
|
$item->productId = $product->id;
|
||||||
|
$item->base_unit = $product->base_unit;
|
||||||
|
$item->purchase_unit = $product->purchase_unit ?: $product->large_unit; // Fallback logic same as Create
|
||||||
|
$item->conversion_rate = (float) $product->conversion_rate;
|
||||||
|
// 優先使用採購單位 > 大單位 > 基本單位
|
||||||
|
$item->unit = $product->purchase_unit ?: ($product->large_unit ?: $product->base_unit);
|
||||||
|
$item->unitPrice = (float) $item->unit_price;
|
||||||
|
}
|
||||||
|
return $item;
|
||||||
|
});
|
||||||
|
|
||||||
return Inertia::render('PurchaseOrder/Show', [
|
return Inertia::render('PurchaseOrder/Show', [
|
||||||
'order' => $order
|
'order' => $order
|
||||||
]);
|
]);
|
||||||
@@ -190,7 +210,10 @@ class PurchaseOrderController extends Controller
|
|||||||
return [
|
return [
|
||||||
'productId' => (string) $product->id,
|
'productId' => (string) $product->id,
|
||||||
'productName' => $product->name,
|
'productName' => $product->name,
|
||||||
'unit' => $product->base_unit,
|
'unit' => $product->purchase_unit ?: ($product->large_unit ?: $product->base_unit),
|
||||||
|
'base_unit' => $product->base_unit,
|
||||||
|
'purchase_unit' => $product->purchase_unit ?: $product->large_unit,
|
||||||
|
'conversion_rate' => (float) $product->conversion_rate,
|
||||||
'lastPrice' => (float) ($product->pivot->last_price ?? 0),
|
'lastPrice' => (float) ($product->pivot->last_price ?? 0),
|
||||||
];
|
];
|
||||||
})
|
})
|
||||||
@@ -204,6 +227,23 @@ class PurchaseOrderController extends Controller
|
|||||||
];
|
];
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Transform items for frontend form
|
||||||
|
$order->items->transform(function ($item) {
|
||||||
|
$product = $item->product;
|
||||||
|
if ($product) {
|
||||||
|
// 手動附加所有必要的屬性 (因為已從 $appends 移除)
|
||||||
|
$item->productId = (string) $product->id; // Ensure consistent ID type
|
||||||
|
$item->productName = $product->name;
|
||||||
|
$item->base_unit = $product->base_unit;
|
||||||
|
$item->purchase_unit = $product->purchase_unit ?: $product->large_unit;
|
||||||
|
$item->conversion_rate = (float) $product->conversion_rate;
|
||||||
|
// 優先使用採購單位 > 大單位 > 基本單位
|
||||||
|
$item->unit = $product->purchase_unit ?: ($product->large_unit ?: $product->base_unit);
|
||||||
|
$item->unitPrice = (float) $item->unit_price;
|
||||||
|
}
|
||||||
|
return $item;
|
||||||
|
});
|
||||||
|
|
||||||
return Inertia::render('PurchaseOrder/Create', [
|
return Inertia::render('PurchaseOrder/Create', [
|
||||||
'order' => $order,
|
'order' => $order,
|
||||||
'suppliers' => $vendors,
|
'suppliers' => $vendors,
|
||||||
|
|||||||
@@ -26,31 +26,25 @@ class PurchaseOrderItem extends Model
|
|||||||
'received_quantity' => 'decimal:2',
|
'received_quantity' => 'decimal:2',
|
||||||
];
|
];
|
||||||
|
|
||||||
protected $appends = [
|
// 移除 $appends 以避免自動附加導致的錯誤
|
||||||
'productName',
|
// 這些屬性將在 Controller 中需要時手動附加
|
||||||
'unit',
|
// 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
|
public function getProductNameAttribute(): string
|
||||||
{
|
{
|
||||||
return $this->product ? $this->product->name : '';
|
return $this->product?->name ?? '';
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getUnitAttribute(): string
|
public function getUnitAttribute(): string
|
||||||
{
|
{
|
||||||
return $this->product ? $this->product->base_unit : '';
|
// 優先使用採購單位 > 大單位 > 基本單位
|
||||||
|
// 與 PurchaseOrderController 的邏輯保持一致
|
||||||
|
if (!$this->product) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->product->purchase_unit
|
||||||
|
?: ($this->product->large_unit ?: $this->product->base_unit);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function purchaseOrder(): BelongsTo
|
public function purchaseOrder(): BelongsTo
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
namespace App\Providers;
|
namespace App\Providers;
|
||||||
|
|
||||||
use Illuminate\Support\ServiceProvider;
|
use Illuminate\Support\ServiceProvider;
|
||||||
|
use Illuminate\Support\Facades\URL;
|
||||||
|
|
||||||
class AppServiceProvider extends ServiceProvider
|
class AppServiceProvider extends ServiceProvider
|
||||||
{
|
{
|
||||||
@@ -19,6 +20,9 @@ class AppServiceProvider extends ServiceProvider
|
|||||||
*/
|
*/
|
||||||
public function boot(): void
|
public function boot(): void
|
||||||
{
|
{
|
||||||
//
|
// 如果是在正式環境,強制轉為 https
|
||||||
|
if (config('app.env') === 'production') {
|
||||||
|
URL::forceScheme('https');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,6 +21,13 @@ import {
|
|||||||
import { useForm } from "@inertiajs/react";
|
import { useForm } from "@inertiajs/react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import type { Product, Category } from "@/Pages/Product/Index";
|
import type { Product, Category } from "@/Pages/Product/Index";
|
||||||
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
} from "@/Components/ui/dropdown-menu";
|
||||||
|
import { ChevronDown } from "lucide-react";
|
||||||
|
|
||||||
interface ProductDialogProps {
|
interface ProductDialogProps {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
@@ -41,7 +48,7 @@ export default function ProductDialog({
|
|||||||
category_id: "",
|
category_id: "",
|
||||||
brand: "",
|
brand: "",
|
||||||
specification: "",
|
specification: "",
|
||||||
base_unit: "kg",
|
base_unit: "公斤",
|
||||||
large_unit: "",
|
large_unit: "",
|
||||||
conversion_rate: "",
|
conversion_rate: "",
|
||||||
purchase_unit: "",
|
purchase_unit: "",
|
||||||
@@ -184,32 +191,34 @@ export default function ProductDialog({
|
|||||||
<Label htmlFor="base_unit">
|
<Label htmlFor="base_unit">
|
||||||
基本庫存單位 <span className="text-red-500">*</span>
|
基本庫存單位 <span className="text-red-500">*</span>
|
||||||
</Label>
|
</Label>
|
||||||
<Select
|
<div className="flex gap-2">
|
||||||
|
<Input
|
||||||
|
id="base_unit"
|
||||||
value={data.base_unit}
|
value={data.base_unit}
|
||||||
onValueChange={(value) => setData("base_unit", value)}
|
onChange={(e) => setData("base_unit", e.target.value)}
|
||||||
>
|
placeholder="可輸入或選擇..."
|
||||||
<SelectTrigger id="base_unit" className={errors.base_unit ? "border-red-500" : ""}>
|
className={errors.base_unit ? "border-red-500 flex-1" : "flex-1"}
|
||||||
<SelectValue />
|
/>
|
||||||
</SelectTrigger>
|
<DropdownMenu>
|
||||||
<SelectContent>
|
<DropdownMenuTrigger asChild>
|
||||||
<SelectItem value="kg">公斤 (kg)</SelectItem>
|
<Button variant="outline" size="icon" className="shrink-0">
|
||||||
<SelectItem value="g">公克 (g)</SelectItem>
|
<ChevronDown className="h-4 w-4" />
|
||||||
<SelectItem value="l">公升 (l)</SelectItem>
|
</Button>
|
||||||
<SelectItem value="ml">毫升 (ml)</SelectItem>
|
</DropdownMenuTrigger>
|
||||||
<SelectItem value="個">個</SelectItem>
|
<DropdownMenuContent align="end">
|
||||||
<SelectItem value="支">支</SelectItem>
|
{["公斤", "公克", "公升", "毫升", "個", "支", "包", "罐", "瓶", "箱", "袋"].map((u) => (
|
||||||
<SelectItem value="包">包</SelectItem>
|
<DropdownMenuItem key={u} onClick={() => setData("base_unit", u)}>
|
||||||
<SelectItem value="罐">罐</SelectItem>
|
{u}
|
||||||
<SelectItem value="瓶">瓶</SelectItem>
|
</DropdownMenuItem>
|
||||||
<SelectItem value="箱">箱</SelectItem>
|
))}
|
||||||
<SelectItem value="袋">袋</SelectItem>
|
</DropdownMenuContent>
|
||||||
</SelectContent>
|
</DropdownMenu>
|
||||||
</Select>
|
</div>
|
||||||
{errors.base_unit && <p className="text-sm text-red-500">{errors.base_unit}</p>}
|
{errors.base_unit && <p className="text-sm text-red-500">{errors.base_unit}</p>}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="large_unit">大單位 (進貨單位)</Label>
|
<Label htmlFor="large_unit">大單位</Label>
|
||||||
<Input
|
<Input
|
||||||
id="large_unit"
|
id="large_unit"
|
||||||
value={data.large_unit}
|
value={data.large_unit}
|
||||||
|
|||||||
@@ -46,11 +46,12 @@ export function PurchaseOrderItemsTable({
|
|||||||
<Table>
|
<Table>
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow className="bg-gray-50 hover:bg-gray-50">
|
<TableRow className="bg-gray-50 hover:bg-gray-50">
|
||||||
<TableHead className="w-[30%] text-left">商品名稱</TableHead>
|
<TableHead className="w-[25%] text-left">商品名稱</TableHead>
|
||||||
<TableHead className="w-[15%] text-left">數量</TableHead>
|
<TableHead className="w-[10%] text-left">數量</TableHead>
|
||||||
<TableHead className="w-[10%] text-left">單位</TableHead>
|
<TableHead className="w-[10%] text-left">採購單位</TableHead>
|
||||||
<TableHead className="w-[20%] text-left">預估單價</TableHead>
|
<TableHead className="w-[15%] text-left">換算基本單位</TableHead>
|
||||||
<TableHead className="w-[20%] text-left">小計</TableHead>
|
<TableHead className="w-[15%] text-left">預估單價</TableHead>
|
||||||
|
<TableHead className="w-[15%] text-left">小計</TableHead>
|
||||||
{!isReadOnly && <TableHead className="w-[5%]"></TableHead>}
|
{!isReadOnly && <TableHead className="w-[5%]"></TableHead>}
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
@@ -58,7 +59,7 @@ export function PurchaseOrderItemsTable({
|
|||||||
{items.length === 0 ? (
|
{items.length === 0 ? (
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell
|
<TableCell
|
||||||
colSpan={isReadOnly ? 5 : 6}
|
colSpan={isReadOnly ? 6 : 7}
|
||||||
className="text-center text-gray-400 py-12 italic"
|
className="text-center text-gray-400 py-12 italic"
|
||||||
>
|
>
|
||||||
{isDisabled ? "請先選擇供應商後才能新增商品" : "尚未新增任何商品項"}
|
{isDisabled ? "請先選擇供應商後才能新增商品" : "尚未新增任何商品項"}
|
||||||
@@ -115,11 +116,20 @@ export function PurchaseOrderItemsTable({
|
|||||||
)}
|
)}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
|
|
||||||
{/* 單位 */}
|
{/* 採購單位 */}
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<span className="text-gray-500 font-medium">{item.unit || "-"}</span>
|
<span className="text-gray-500 font-medium">{item.unit || "-"}</span>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
|
|
||||||
|
{/* 換算基本單位 */}
|
||||||
|
<TableCell>
|
||||||
|
<span className="text-gray-500 font-medium">
|
||||||
|
{item.conversion_rate && item.base_unit
|
||||||
|
? `${parseFloat((item.quantity * item.conversion_rate).toFixed(2))} ${item.base_unit}`
|
||||||
|
: "-"}
|
||||||
|
</span>
|
||||||
|
</TableCell>
|
||||||
|
|
||||||
{/* 單價 */}
|
{/* 單價 */}
|
||||||
<TableCell className="text-left">
|
<TableCell className="text-left">
|
||||||
{isReadOnly ? (
|
{isReadOnly ? (
|
||||||
@@ -135,12 +145,23 @@ export function PurchaseOrderItemsTable({
|
|||||||
onItemChange?.(index, "unitPrice", Number(e.target.value))
|
onItemChange?.(index, "unitPrice", Number(e.target.value))
|
||||||
}
|
}
|
||||||
disabled={isDisabled}
|
disabled={isDisabled}
|
||||||
className={`h-10 text-left w-32 ${isPriceAlert(item.unitPrice, item.previousPrice)
|
className={`h-10 text-left w-32 ${
|
||||||
|
// 如果有數量但沒有單價,顯示錯誤樣式
|
||||||
|
item.quantity > 0 && (!item.unitPrice || item.unitPrice <= 0)
|
||||||
|
? "border-red-400 bg-red-50 focus-visible:ring-red-500"
|
||||||
|
: isPriceAlert(item.unitPrice, item.previousPrice)
|
||||||
? "border-amber-400 bg-amber-50 focus-visible:ring-amber-500"
|
? "border-amber-400 bg-amber-50 focus-visible:ring-amber-500"
|
||||||
: "border-gray-200"
|
: "border-gray-200"
|
||||||
}`}
|
}`}
|
||||||
/>
|
/>
|
||||||
{isPriceAlert(item.unitPrice, item.previousPrice) && (
|
{/* 錯誤提示:有數量但沒有單價 */}
|
||||||
|
{item.quantity > 0 && (!item.unitPrice || item.unitPrice <= 0) && (
|
||||||
|
<p className="text-[10px] text-red-600 font-medium">
|
||||||
|
❌ 請填寫預估單價
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
{/* 價格警示:單價高於上次 */}
|
||||||
|
{item.unitPrice > 0 && isPriceAlert(item.unitPrice, item.previousPrice) && (
|
||||||
<p className="text-[10px] text-amber-600 font-medium animate-pulse">
|
<p className="text-[10px] text-amber-600 font-medium animate-pulse">
|
||||||
⚠️ 高於上次: {formatCurrency(item.previousPrice || 0)}
|
⚠️ 高於上次: {formatCurrency(item.previousPrice || 0)}
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -60,8 +60,8 @@ export default function CreatePurchaseOrder({
|
|||||||
setStatus,
|
setStatus,
|
||||||
} = usePurchaseOrderForm({ order, suppliers });
|
} = usePurchaseOrderForm({ order, suppliers });
|
||||||
|
|
||||||
|
|
||||||
const totalAmount = calculateTotalAmount(items);
|
const totalAmount = calculateTotalAmount(items);
|
||||||
const isValid = validatePurchaseOrder(String(supplierId), expectedDate, items);
|
|
||||||
|
|
||||||
const handleSave = () => {
|
const handleSave = () => {
|
||||||
if (!warehouseId) {
|
if (!warehouseId) {
|
||||||
@@ -84,9 +84,23 @@ export default function CreatePurchaseOrder({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 檢查是否有數量大於 0 的項目
|
||||||
|
const itemsWithQuantity = items.filter(item => item.quantity > 0);
|
||||||
|
if (itemsWithQuantity.length === 0) {
|
||||||
|
toast.error("請填寫有效的採購數量(必須大於 0)");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 檢查有數量的項目是否都有填寫單價
|
||||||
|
const itemsWithoutPrice = itemsWithQuantity.filter(item => !item.unitPrice || item.unitPrice <= 0);
|
||||||
|
if (itemsWithoutPrice.length > 0) {
|
||||||
|
toast.error("請填寫所有商品的預估單價(必須大於 0)");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const validItems = filterValidItems(items);
|
const validItems = filterValidItems(items);
|
||||||
if (validItems.length === 0) {
|
if (validItems.length === 0) {
|
||||||
toast.error("請填寫有效的採購數量(必須大於 0)");
|
toast.error("請確保所有商品都有填寫數量和單價");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -107,7 +121,14 @@ export default function CreatePurchaseOrder({
|
|||||||
router.put(`/purchase-orders/${order.id}`, data, {
|
router.put(`/purchase-orders/${order.id}`, data, {
|
||||||
onSuccess: () => toast.success("採購單已更新"),
|
onSuccess: () => toast.success("採購單已更新"),
|
||||||
onError: (errors) => {
|
onError: (errors) => {
|
||||||
toast.error("更新失敗,請檢查輸入內容");
|
// 顯示更詳細的錯誤訊息
|
||||||
|
if (errors.items) {
|
||||||
|
toast.error("商品資料有誤,請檢查數量和單價是否正確填寫");
|
||||||
|
} else if (errors.error) {
|
||||||
|
toast.error(errors.error);
|
||||||
|
} else {
|
||||||
|
toast.error("更新失敗,請檢查輸入內容");
|
||||||
|
}
|
||||||
console.error(errors);
|
console.error(errors);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -115,10 +136,12 @@ export default function CreatePurchaseOrder({
|
|||||||
router.post("/purchase-orders", data, {
|
router.post("/purchase-orders", data, {
|
||||||
onSuccess: () => toast.success("採購單已成功建立"),
|
onSuccess: () => toast.success("採購單已成功建立"),
|
||||||
onError: (errors) => {
|
onError: (errors) => {
|
||||||
if (errors.error) {
|
if (errors.items) {
|
||||||
|
toast.error("商品資料有誤,請檢查數量和單價是否正確填寫");
|
||||||
|
} else if (errors.error) {
|
||||||
toast.error(errors.error);
|
toast.error(errors.error);
|
||||||
} else {
|
} else {
|
||||||
toast.error("建立失敗,請檢查輸入內容");
|
toast.error("建立失敗,請檢查輸入內容");
|
||||||
}
|
}
|
||||||
console.error(errors);
|
console.error(errors);
|
||||||
}
|
}
|
||||||
@@ -127,7 +150,6 @@ export default function CreatePurchaseOrder({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const hasSupplier = !!supplierId;
|
const hasSupplier = !!supplierId;
|
||||||
const canSave = isValid && !!warehouseId && items.length > 0;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AuthenticatedLayout>
|
<AuthenticatedLayout>
|
||||||
|
|||||||
@@ -76,6 +76,9 @@ export function usePurchaseOrderForm({ order, suppliers }: UsePurchaseOrderFormP
|
|||||||
if (product) {
|
if (product) {
|
||||||
newItems[index].productName = product.productName;
|
newItems[index].productName = product.productName;
|
||||||
newItems[index].unit = product.unit;
|
newItems[index].unit = product.unit;
|
||||||
|
newItems[index].base_unit = product.base_unit;
|
||||||
|
newItems[index].purchase_unit = product.purchase_unit;
|
||||||
|
newItems[index].conversion_rate = product.conversion_rate;
|
||||||
newItems[index].unitPrice = product.lastPrice;
|
newItems[index].unitPrice = product.lastPrice;
|
||||||
newItems[index].previousPrice = product.lastPrice;
|
newItems[index].previousPrice = product.lastPrice;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,6 +22,9 @@ export interface PurchaseOrderItem {
|
|||||||
productName: string;
|
productName: string;
|
||||||
quantity: number;
|
quantity: number;
|
||||||
unit: string;
|
unit: string;
|
||||||
|
base_unit?: string; // 基本庫存單位
|
||||||
|
purchase_unit?: string; // 採購單位
|
||||||
|
conversion_rate?: number;// 換算率
|
||||||
unitPrice: number;
|
unitPrice: number;
|
||||||
previousPrice?: number;
|
previousPrice?: number;
|
||||||
subtotal: number;
|
subtotal: number;
|
||||||
@@ -77,6 +80,9 @@ export interface CommonProduct {
|
|||||||
productId: string;
|
productId: string;
|
||||||
productName: string;
|
productName: string;
|
||||||
unit: string;
|
unit: string;
|
||||||
|
base_unit?: string;
|
||||||
|
purchase_unit?: string;
|
||||||
|
conversion_rate?: number;
|
||||||
lastPrice: number;
|
lastPrice: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -76,8 +76,8 @@ export function validatePurchaseOrder(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 過濾有效項目(數量大於 0)
|
* 過濾有效項目(數量和單價都必須大於 0)
|
||||||
*/
|
*/
|
||||||
export function filterValidItems(items: PurchaseOrderItem[]): PurchaseOrderItem[] {
|
export function filterValidItems(items: PurchaseOrderItem[]): PurchaseOrderItem[] {
|
||||||
return items.filter((item) => item.quantity > 0);
|
return items.filter((item) => item.quantity > 0 && item.unitPrice > 0);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,7 +27,6 @@ Route::resource('warehouses', \App\Http\Controllers\WarehouseController::class);
|
|||||||
|
|
||||||
// 庫存管理
|
// 庫存管理
|
||||||
Route::get('warehouses/{warehouse}/inventory', [\App\Http\Controllers\InventoryController::class, 'index'])->name('warehouses.inventory.index');
|
Route::get('warehouses/{warehouse}/inventory', [\App\Http\Controllers\InventoryController::class, 'index'])->name('warehouses.inventory.index');
|
||||||
Route::put('warehouses/{warehouse}/inventory/{product}', [\App\Http\Controllers\InventoryController::class, 'update'])->name('warehouses.inventory.update');
|
|
||||||
|
|
||||||
// 安全庫存管理
|
// 安全庫存管理
|
||||||
Route::prefix('warehouses/{warehouse}/safety-stock-settings')->name('warehouses.safety-stock.')->group(function () {
|
Route::prefix('warehouses/{warehouse}/safety-stock-settings')->name('warehouses.safety-stock.')->group(function () {
|
||||||
|
|||||||
Reference in New Issue
Block a user