Compare commits

...

29 Commits

Author SHA1 Message Date
bd29410191 deletet不需要的deploy
All checks were successful
Koori-ERP-Deploy-System / deploy-demo (push) Successful in 40s
Koori-ERP-Deploy-System / deploy-production (push) Has been skipped
2026-01-06 15:50:23 +08:00
be315a76cc 修正CICD 2026-01-06 15:49:11 +08:00
fad74df6ac 更新採購單跟商品資料一些bug
All checks were successful
Koori-ERP-Deploy-System / deploy-demo (push) Has been skipped
Koori-ERP-Deploy-System / deploy-production (push) Successful in 52s
2026-01-06 15:45:13 +08:00
7160a7e780 test
All checks were successful
Koori-ERP-Deploy-System / deploy-demo (push) Has been skipped
Koori-ERP-Deploy-System / deploy-production (push) Successful in 10m24s
2026-01-06 14:21:13 +08:00
3e28067c97 tt
All checks were successful
Koori-ERP-Deploy-System / deploy-demo (push) Has been skipped
Koori-ERP-Deploy-System / deploy-production (push) Successful in 47s
2026-01-06 14:15:47 +08:00
fd3ddd0bac mama
All checks were successful
Koori-ERP-Deploy-System / deploy-demo (push) Has been skipped
Koori-ERP-Deploy-System / deploy-production (push) Successful in 57s
2026-01-06 14:12:23 +08:00
5797ff118d OK1
All checks were successful
Koori-ERP-Deploy-System / deploy-demo (push) Has been skipped
Koori-ERP-Deploy-System / deploy-production (push) Successful in 1m41s
2026-01-06 14:03:56 +08:00
41d5e8e7fc mama
All checks were successful
Koori-ERP-Deploy-System / deploy-demo (push) Has been skipped
Koori-ERP-Deploy-System / deploy-production (push) Successful in 1m7s
2026-01-06 13:59:12 +08:00
f4ca6b09e8 mana
Some checks failed
Koori-ERP-Deploy-System / deploy-demo (push) Has been skipped
Koori-ERP-Deploy-System / deploy-production (push) Failing after 1m7s
2026-01-06 13:56:33 +08:00
1c8c3009ec ggg
All checks were successful
Koori-ERP-Deploy-System / deploy-demo (push) Has been skipped
Koori-ERP-Deploy-System / deploy-production (push) Successful in 57s
2026-01-06 13:51:51 +08:00
315cce467e OK
Some checks failed
Koori-ERP-Deploy-System / deploy-demo (push) Has been skipped
Koori-ERP-Deploy-System / deploy-production (push) Failing after 1m0s
2026-01-06 13:48:13 +08:00
001ba33335 OK
All checks were successful
Koori-ERP-Deploy-System / deploy-demo (push) Has been skipped
Koori-ERP-Deploy-System / deploy-production (push) Successful in 1m2s
2026-01-06 13:46:10 +08:00
6209b28345 f
Some checks failed
Koori-ERP-Deploy-System / deploy-demo (push) Has been skipped
Koori-ERP-Deploy-System / deploy-production (push) Failing after 1m3s
2026-01-06 13:43:51 +08:00
1759fceaed m
All checks were successful
Koori-ERP-Deploy-System / deploy-demo (push) Has been skipped
Koori-ERP-Deploy-System / deploy-production (push) Successful in 53s
2026-01-06 13:41:27 +08:00
54d36f51e7 main test
All checks were successful
Koori-ERP-Deploy-System / deploy-demo (push) Has been skipped
Koori-ERP-Deploy-System / deploy-production (push) Successful in 44s
2026-01-06 13:38:38 +08:00
981d887ae8 main test
All checks were successful
Koori-ERP-Deploy-System / deploy-demo (push) Has been skipped
Koori-ERP-Deploy-System / deploy-production (push) Successful in 58s
2026-01-06 13:32:14 +08:00
8e91f28ef4 main
All checks were successful
Koori-ERP-Deploy-System / deploy-demo (push) Has been skipped
Koori-ERP-Deploy-System / deploy-production (push) Successful in 59s
2026-01-06 13:26:40 +08:00
fbcdcd05b0 main go
All checks were successful
Koori-ERP-Deploy-System / deploy-demo (push) Has been skipped
Koori-ERP-Deploy-System / deploy-production (push) Successful in 41s
2026-01-06 13:21:31 +08:00
8d838ee6f6 main ok
All checks were successful
Koori-ERP-Deploy-System / deploy-demo (push) Has been skipped
Koori-ERP-Deploy-System / deploy-production (push) Successful in 1m6s
2026-01-06 12:46:56 +08:00
fd86ae0153 main oo
Some checks failed
Koori-ERP-Deploy-System / deploy-demo (push) Has been skipped
Koori-ERP-Deploy-System / deploy-production (push) Failing after 59s
2026-01-06 11:58:59 +08:00
cdf434d63c main OK
Some checks failed
Koori-ERP-Deploy-System / deploy-demo (push) Has been skipped
Koori-ERP-Deploy-System / deploy-production (push) Failing after 53s
2026-01-06 11:56:14 +08:00
ccdbe48b88 main gotest
Some checks failed
Koori-ERP-Deploy-System / deploy-demo (push) Has been skipped
Koori-ERP-Deploy-System / deploy-production (push) Failing after 1m7s
2026-01-06 11:48:31 +08:00
564c6588c1 main ii
Some checks failed
Koori-ERP-Deploy-System / deploy-demo (push) Has been skipped
Koori-ERP-Deploy-System / deploy-production (push) Failing after 9m30s
2026-01-06 11:22:02 +08:00
21d0ea4cc2 main push
Some checks failed
Koori-ERP-Deploy-System / deploy-demo (push) Has been skipped
Koori-ERP-Deploy-System / deploy-production (push) Failing after 29s
2026-01-06 11:11:10 +08:00
6b0f3c9bcd main test
Some checks failed
Koori-ERP-Deploy-System / deploy-demo (push) Has been skipped
Koori-ERP-Deploy-System / deploy-production (push) Failing after 7m55s
2026-01-06 10:58:04 +08:00
0aaa761a47 123
Some checks failed
Koori-ERP-Deploy-System / deploy-demo (push) Has been skipped
Koori-ERP-Deploy-System / deploy-production (push) Has been cancelled
2026-01-06 10:47:28 +08:00
d683861233 test
Some checks failed
Koori-ERP-Deploy-System / deploy-demo (push) Has been skipped
Koori-ERP-Deploy-System / deploy-production (push) Failing after 31s
2026-01-06 10:43:53 +08:00
b240877d40 rrr
Some checks failed
Koori-ERP-Deploy-System / deploy-demo (push) Has been skipped
Koori-ERP-Deploy-System / deploy-production (push) Failing after 7s
2026-01-06 10:32:10 +08:00
e31715becb main test
Some checks failed
Koori-ERP-Deploy-System / deploy-demo (push) Has been skipped
Koori-ERP-Deploy-System / deploy-production (push) Failing after 27s
2026-01-06 10:30:13 +08:00
13 changed files with 244 additions and 163 deletions

View File

@@ -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

View File

@@ -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 .
# # 1. 確保 .env 存在 (建議正式機手動維護 .env不隨 git 連動) WWWGROUP=1000 WWWUSER=1000 docker compose up -d --build --wait
# if [ ! -f .env ]; then cp .env.example .env; fi echo "容器狀態:" && docker ps --filter "name=koori-erp-laravel"
# # 2. 啟動容器 # 3. 處理後端與前端依賴(這時網站可能因為沒 vendor 呈現 500/502
# docker compose up -d --build - name: Step 3 - Composer & NPM Build
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 && composer install --no-dev --optimize-autoloader &&
# php artisan view:cache npm install &&
# " npm run build
"
# 4. 處理資料庫與 Laravel 快取
- name: Step 4 - Database & Optimization
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 "
php artisan migrate --force &&
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 "部署完成!"

View File

@@ -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
``` ```
啟動後,您可以透過以下連結瀏覽專案: 啟動後,您可以透過以下連結瀏覽專案:

View File

@@ -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,

View File

@@ -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

View File

@@ -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');
}
} }
} }

View File

@@ -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">
value={data.base_unit} <Input
onValueChange={(value) => setData("base_unit", value)} id="base_unit"
> value={data.base_unit}
<SelectTrigger id="base_unit" className={errors.base_unit ? "border-red-500" : ""}> onChange={(e) => setData("base_unit", e.target.value)}
<SelectValue /> placeholder="可輸入或選擇..."
</SelectTrigger> className={errors.base_unit ? "border-red-500 flex-1" : "flex-1"}
<SelectContent> />
<SelectItem value="kg"> (kg)</SelectItem> <DropdownMenu>
<SelectItem value="g"> (g)</SelectItem> <DropdownMenuTrigger asChild>
<SelectItem value="l"> (l)</SelectItem> <Button variant="outline" size="icon" className="shrink-0">
<SelectItem value="ml"> (ml)</SelectItem> <ChevronDown className="h-4 w-4" />
<SelectItem value="個"></SelectItem> </Button>
<SelectItem value="支"></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>
</SelectContent> ))}
</Select> </DropdownMenuContent>
</DropdownMenu>
</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}
@@ -270,6 +279,6 @@ export default function ProductDialog({
</DialogFooter> </DialogFooter>
</form> </form>
</DialogContent> </DialogContent>
</Dialog> </Dialog >
); );
} }

View File

@@ -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 ${
? "border-amber-400 bg-amber-50 focus-visible:ring-amber-500" // 如果有數量但沒有單價,顯示錯誤樣式
: "border-gray-200" 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-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>

View File

@@ -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>

View File

@@ -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;
} }

View File

@@ -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;
} }

View File

@@ -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);
} }

View File

@@ -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 () {