feat(procurement): 統一採購單按鈕樣式與術語更名為「作廢」,並加強權限控管
This commit is contained in:
@@ -10,7 +10,7 @@ import { Textarea } from "@/Components/ui/textarea";
|
||||
import { Alert, AlertDescription } from "@/Components/ui/alert";
|
||||
import { SearchableSelect } from "@/Components/ui/searchable-select";
|
||||
import AuthenticatedLayout from "@/Layouts/AuthenticatedLayout";
|
||||
import { Head, Link, router } from "@inertiajs/react";
|
||||
import { Head, Link, router, usePage } from "@inertiajs/react";
|
||||
import { PurchaseOrderItemsTable } from "@/Components/PurchaseOrder/PurchaseOrderItemsTable";
|
||||
import type { PurchaseOrder, Supplier } from "@/types/purchase-order";
|
||||
import type { Warehouse } from "@/types/requester";
|
||||
@@ -21,8 +21,9 @@ import {
|
||||
getTodayDate,
|
||||
formatCurrency,
|
||||
} from "@/utils/purchase-order";
|
||||
import { STATUS_OPTIONS } from "@/constants/purchase-order";
|
||||
import { STATUS_CONFIG, MANUAL_STATUS_OPTIONS } from "@/constants/purchase-order";
|
||||
import { toast } from "sonner";
|
||||
import { Can } from "@/Components/Permission/Can";
|
||||
import { getCreateBreadcrumbs, getEditBreadcrumbs } from "@/utils/breadcrumb";
|
||||
|
||||
interface Props {
|
||||
@@ -36,6 +37,17 @@ export default function CreatePurchaseOrder({
|
||||
suppliers,
|
||||
warehouses,
|
||||
}: Props) {
|
||||
const { auth } = usePage<any>().props;
|
||||
const permissions = auth.user?.permissions || [];
|
||||
const isSuperAdmin = auth.user?.roles?.some((r: any) => r.name === 'super-admin');
|
||||
|
||||
const canApprove = isSuperAdmin || permissions.includes('purchase_orders.approve');
|
||||
const canCreate = isSuperAdmin || permissions.includes('purchase_orders.create');
|
||||
const canEdit = isSuperAdmin || permissions.includes('purchase_orders.edit');
|
||||
|
||||
// 儲存權限判斷
|
||||
const canSave = order ? canEdit : canCreate;
|
||||
|
||||
const {
|
||||
supplierId,
|
||||
expectedDate,
|
||||
@@ -273,12 +285,26 @@ export default function CreatePurchaseOrder({
|
||||
{order && (
|
||||
<div className="space-y-3">
|
||||
<label className="text-sm font-bold text-gray-700">狀態</label>
|
||||
<SearchableSelect
|
||||
value={status}
|
||||
onValueChange={(v) => setStatus(v as any)}
|
||||
options={STATUS_OPTIONS.map((opt) => ({ label: opt.label, value: opt.value }))}
|
||||
placeholder="選擇狀態"
|
||||
/>
|
||||
<Can permission="purchase_orders.approve">
|
||||
<SearchableSelect
|
||||
value={status}
|
||||
onValueChange={(v) => setStatus(v as any)}
|
||||
options={MANUAL_STATUS_OPTIONS}
|
||||
placeholder="選擇狀態"
|
||||
/>
|
||||
</Can>
|
||||
<div className="!mt-1">
|
||||
{!canApprove && (
|
||||
<>
|
||||
<div className="px-3 py-2 bg-gray-50 border rounded-md text-sm text-gray-600">
|
||||
{STATUS_CONFIG[status as keyof typeof STATUS_CONFIG]?.label || status}
|
||||
</div>
|
||||
<p className="text-[10px] text-gray-400 mt-1 italic">
|
||||
* 您沒有權限在此修改狀態,請使用詳情頁面的動作按鈕進行操作。
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -454,9 +480,11 @@ export default function CreatePurchaseOrder({
|
||||
</Button>
|
||||
</Link>
|
||||
<Button
|
||||
size="lg"
|
||||
className="bg-primary hover:bg-primary/90 text-white px-12 h-14 rounded-xl shadow-lg shadow-primary/20 text-lg font-bold transition-all hover:scale-[1.02] active:scale-[0.98]"
|
||||
size="xl"
|
||||
className="bg-primary hover:bg-primary/90 text-white shadow-primary/20"
|
||||
onClick={handleSave}
|
||||
disabled={!canSave}
|
||||
title={!canSave ? "您沒有執行此動作的權限" : ""}
|
||||
>
|
||||
{order ? "更新採購單" : "確認發布採購單"}
|
||||
</Button>
|
||||
|
||||
@@ -23,7 +23,7 @@ import {
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/Components/ui/select";
|
||||
import { STATUS_OPTIONS } from "@/constants/purchase-order";
|
||||
import { MANUAL_STATUS_OPTIONS } from "@/constants/purchase-order";
|
||||
|
||||
interface Props {
|
||||
orders: {
|
||||
@@ -177,7 +177,7 @@ export default function PurchaseOrderIndex({ orders, filters, warehouses }: Prop
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">全部狀態</SelectItem>
|
||||
{STATUS_OPTIONS.map((option) => (
|
||||
{MANUAL_STATUS_OPTIONS.map((option) => (
|
||||
<SelectItem key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</SelectItem>
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
* 查看採購單詳情頁面
|
||||
*/
|
||||
|
||||
import { ArrowLeft, ShoppingCart } from "lucide-react";
|
||||
import { ArrowLeft, ShoppingCart, Send, CheckCircle, XCircle, RotateCcw } from "lucide-react";
|
||||
import { Button } from "@/Components/ui/button";
|
||||
import AuthenticatedLayout from "@/Layouts/AuthenticatedLayout";
|
||||
import { Head, Link } from "@inertiajs/react";
|
||||
import { Head, Link, useForm, usePage, router } from "@inertiajs/react";
|
||||
import { StatusProgressBar } from "@/Components/PurchaseOrder/StatusProgressBar";
|
||||
import PurchaseOrderStatusBadge from "@/Components/PurchaseOrder/PurchaseOrderStatusBadge";
|
||||
import CopyButton from "@/Components/shared/CopyButton";
|
||||
@@ -13,6 +13,8 @@ import { PurchaseOrderItemsTable } from "@/Components/PurchaseOrder/PurchaseOrde
|
||||
import type { PurchaseOrder } from "@/types/purchase-order";
|
||||
import { formatCurrency, formatDateTime } from "@/utils/format";
|
||||
import { getShowBreadcrumbs } from "@/utils/breadcrumb";
|
||||
import { toast } from "sonner";
|
||||
import { PageProps } from "@/types/global";
|
||||
|
||||
interface Props {
|
||||
order: PurchaseOrder;
|
||||
@@ -44,11 +46,6 @@ export default function ViewPurchaseOrderPage({ order }: Props) {
|
||||
<p className="text-gray-500 mt-1">單號:{order.poNumber}</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<Link href={`/purchase-orders/${order.id}/edit`}>
|
||||
<Button variant="outline" className="button-outlined-primary">
|
||||
編輯採購單
|
||||
</Button>
|
||||
</Link>
|
||||
<PurchaseOrderStatusBadge status={order.status} />
|
||||
</div>
|
||||
</div>
|
||||
@@ -171,9 +168,111 @@ export default function ViewPurchaseOrderPage({ order }: Props) {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 操作按鈕 (底部) */}
|
||||
<div className="flex justify-end pt-4">
|
||||
<PurchaseOrderActions order={order} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</AuthenticatedLayout>
|
||||
);
|
||||
}
|
||||
|
||||
function PurchaseOrderActions({ order }: { order: PurchaseOrder }) {
|
||||
const { auth } = usePage<PageProps>().props;
|
||||
const permissions = auth.user?.permissions || [];
|
||||
|
||||
const { processing } = useForm({
|
||||
status: order.status,
|
||||
});
|
||||
|
||||
const handleUpdateStatus = (newStatus: string, actionName: string) => {
|
||||
const formData = {
|
||||
vendor_id: order.supplierId,
|
||||
warehouse_id: order.warehouse_id,
|
||||
order_date: order.orderDate,
|
||||
expected_delivery_date: order.expectedDate ? new Date(order.expectedDate).toISOString().split('T')[0] : null,
|
||||
items: order.items.map((item: any) => ({
|
||||
productId: item.productId,
|
||||
quantity: item.quantity,
|
||||
unitId: item.unitId,
|
||||
subtotal: item.subtotal,
|
||||
})),
|
||||
tax_amount: order.taxAmount,
|
||||
status: newStatus,
|
||||
remark: order.remark || "",
|
||||
};
|
||||
|
||||
router.patch(route('purchase-orders.update', order.id), formData, {
|
||||
onSuccess: () => toast.success(`採購單已${actionName === '取消' ? '作廢' : actionName}`),
|
||||
onError: (errors: any) => {
|
||||
console.error("Status Update Error:", errors);
|
||||
toast.error(errors.error || "操作失敗");
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// 權限判斷 (包含超級管理員檢查)
|
||||
const isSuperAdmin = auth.user?.roles?.some((r: any) => r.name === 'super-admin');
|
||||
const canApprove = isSuperAdmin || permissions.includes('purchase_orders.approve');
|
||||
const canCancel = isSuperAdmin || permissions.includes('purchase_orders.cancel');
|
||||
const canEdit = isSuperAdmin || permissions.includes('purchase_orders.edit');
|
||||
const canView = isSuperAdmin || permissions.includes('purchase_orders.view');
|
||||
|
||||
// 送審權限:擁有檢視或編輯權限的人都可以送審
|
||||
const canSubmit = canEdit || canView;
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-4">
|
||||
{['draft', 'pending', 'approved'].includes(order.status) && canCancel && (
|
||||
<Button
|
||||
onClick={() => handleUpdateStatus('cancelled', '作廢')}
|
||||
disabled={processing}
|
||||
variant="outline"
|
||||
size="xl"
|
||||
className="button-outlined-error shadow-red-200/20 border-red-600 text-red-600 hover:bg-red-50"
|
||||
>
|
||||
<XCircle className="h-5 w-5" /> 作廢
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{order.status === 'pending' && canApprove && (
|
||||
<Button
|
||||
onClick={() => handleUpdateStatus('draft', '退回')}
|
||||
disabled={processing}
|
||||
variant="outline"
|
||||
size="xl"
|
||||
className="button-outlined-warning shadow-amber-200/20"
|
||||
>
|
||||
<RotateCcw className="h-5 w-5" /> 退回
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<div className="flex-1" />
|
||||
|
||||
{order.status === 'draft' && canSubmit && (
|
||||
<Button
|
||||
onClick={() => handleUpdateStatus('pending', '送出審核')}
|
||||
disabled={processing}
|
||||
size="xl"
|
||||
className="button-filled-primary shadow-primary/20"
|
||||
>
|
||||
<Send className="h-5 w-5" /> 送審
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{order.status === 'pending' && canApprove && (
|
||||
<Button
|
||||
onClick={() => handleUpdateStatus('approved', '核准')}
|
||||
disabled={processing}
|
||||
size="xl"
|
||||
className="button-filled-primary shadow-primary/20"
|
||||
>
|
||||
<CheckCircle className="h-5 w-5" /> 核准
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user