Files
star-erp/resources/js/Pages/AccountPayable/Show.tsx
sky121113 e406ecd63d
All checks were successful
ERP-Deploy-Demo / deploy-demo (push) Successful in 1m17s
feat: 實作應付帳款與銷售訂單權限管理與進貨單權限修正
2026-02-24 17:29:09 +08:00

408 lines
22 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useState } from 'react';
import { Head, Link, useForm } from '@inertiajs/react';
import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout';
import { Button } from '@/Components/ui/button';
import { ExternalLink, ArrowLeft, Wallet, FileText, CheckCircle } from 'lucide-react';
import { StatusBadge } from '@/Components/shared/StatusBadge';
import { formatDate } from '@/lib/date';
import { Badge } from '@/Components/ui/badge';
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, DialogDescription } from '@/Components/ui/dialog';
import { Input } from '@/Components/ui/input';
import { Label } from '@/Components/ui/label';
import { toast } from 'sonner';
import { Can } from '@/Components/Permission/Can';
const getStatusBadgeVariant = (status: string) => {
switch (status) {
case 'pending': return 'warning';
case 'partially_paid': return 'info';
case 'paid': return 'success';
case 'cancelled': return 'destructive';
default: return 'neutral';
}
};
const getStatusLabel = (status: string) => {
switch (status) {
case 'pending': return '待付款';
case 'partially_paid': return '部分付款';
case 'paid': return '已結清';
case 'cancelled': return '已作廢';
default: return status;
}
};
export default function AccountPayableShow({ payable }: any) {
const [invoiceDialogOpen, setInvoiceDialogOpen] = useState(false);
const [paymentDialogOpen, setPaymentDialogOpen] = useState(false);
const invoiceForm = useForm({
invoice_number: payable.invoice_number || '',
invoice_date: payable.invoice_date || '',
});
const paymentForm = useForm({
payment_method: payable.payment_method || 'bank_transfer',
paid_at: payable.paid_at ? payable.paid_at.split('T')[0] : new Date().toISOString().split('T')[0],
payment_note: payable.payment_note || '',
});
const handleInvoiceSubmit = (e: React.FormEvent) => {
e.preventDefault();
invoiceForm.post(route('account-payables.invoice', payable.id), {
preserveScroll: true,
onSuccess: () => {
setInvoiceDialogOpen(false);
toast.success('發票資訊已更新');
},
onError: (errors) => {
toast.error(Object.values(errors)[0] as string || '更新失敗');
}
});
};
const handlePaymentSubmit = (e: React.FormEvent) => {
e.preventDefault();
paymentForm.post(route('account-payables.pay', payable.id), {
preserveScroll: true,
onSuccess: () => {
setPaymentDialogOpen(false);
toast.success('帳款已成功標記為已付款');
},
onError: (errors) => {
toast.error(Object.values(errors)[0] as string || '標記失敗');
}
});
};
return (
<AuthenticatedLayout
breadcrumbs={[
{ label: '財務管理', href: '#' },
{ label: '應付帳款', href: route('account-payables.index') },
{ label: `詳情: ${payable.document_number}` }
]}
>
<Head title={`應付帳款 - ${payable.document_number}`} />
<div className="container mx-auto p-6 max-w-7xl">
{/* Back Button */}
<div className="mb-6">
<Link href={route('account-payables.index')}>
<Button
variant="outline"
className="gap-2 button-outlined-primary"
>
<ArrowLeft className="h-4 w-4" />
</Button>
</Link>
</div>
{/* 頁面標題與操作 */}
<div className="flex items-start justify-between mb-6">
<div>
<h1 className="text-2xl font-bold text-grey-0 flex items-center gap-2">
<Wallet className="h-6 w-6 text-primary-main" />
{payable.document_number}
</h1>
<div className="flex items-center gap-2 mt-1">
{/* @ts-ignore */}
<StatusBadge variant={getStatusBadgeVariant(payable.status)}>
{getStatusLabel(payable.status)}
</StatusBadge>
<span className="text-gray-500 text-sm">
{formatDate(payable.created_at)}
</span>
</div>
</div>
<div className="flex items-center gap-2">
<Can permission="account_payables.edit">
<Button
variant="outline"
className="gap-2 button-outlined-primary"
onClick={() => setInvoiceDialogOpen(true)}
>
<FileText className="h-4 w-4" />
</Button>
</Can>
{payable.status !== 'paid' && (
<Can permission="account_payables.pay">
<Button
className="gap-2 button-filled-primary"
onClick={() => setPaymentDialogOpen(true)}
>
<CheckCircle className="h-4 w-4" />
</Button>
</Can>
)}
</div>
</div>
<div className="space-y-6">
{/* 基本資料 */}
<div className="bg-white rounded-lg shadow-sm border p-6">
<h2 className="text-lg font-semibold text-gray-800 mb-4"></h2>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<div>
<span className="text-sm text-gray-500"></span>
<p className="font-medium text-gray-800 mt-1">
{payable.vendor?.name || '未知供應商'}
</p>
</div>
<div>
<span className="text-sm text-gray-500"></span>
<p className="font-medium text-gray-800 mt-1">
${parseFloat(payable.total_amount).toLocaleString()}
</p>
</div>
<div>
<span className="text-sm text-gray-500"></span>
<p className="font-medium text-gray-800 mt-1">
{formatDate(payable.due_date)}
</p>
</div>
<div>
<span className="text-sm text-gray-500"></span>
<p className="font-medium text-gray-800 mt-1">
{payable.creator?.name || '-'}
</p>
</div>
<div>
<span className="text-sm text-gray-500"></span>
<p className="font-medium text-gray-800 mt-1">
{formatDate(payable.created_at)}
</p>
</div>
{payable.remarks && (
<div className="md:col-span-3">
<span className="text-sm text-gray-500"></span>
<p className="font-medium text-gray-800 mt-1">
{payable.remarks}
</p>
</div>
)}
</div>
</div>
{/* 來源關聯 */}
<div className="bg-white rounded-lg shadow-sm border p-6">
<h2 className="text-lg font-semibold text-gray-800 mb-4"></h2>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<div>
<span className="text-sm text-gray-500"></span>
<p className="font-medium text-gray-800 mt-1">
{payable.source_document_type === 'goods_receipt' ? (
<Badge variant="outline" className="font-normal"></Badge>
) : (
payable.source_document_type || '-'
)}
</p>
</div>
<div>
<span className="text-sm text-gray-500"></span>
<div className="flex items-center gap-2 mt-1">
<p className="font-medium text-gray-800">
{payable.source_document_code || payable.source_document_id || '-'}
</p>
{payable.source_document_type === 'goods_receipt' && payable.source_document_id && (
<Link
href={route('goods-receipts.show', [payable.source_document_id])}
className="text-primary-main hover:underline flex items-center gap-1 text-sm font-medium"
>
<ExternalLink className="h-3 w-3" />
</Link>
)}
</div>
</div>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{/* 發票資訊 */}
<div className="bg-primary-main/5 rounded-xl border border-primary-main/20 p-6">
<div className="flex justify-between items-center mb-4">
<h2 className="text-lg font-semibold text-primary-main flex items-center gap-2">
<FileText className="h-5 w-5 text-primary-main" />
</h2>
<Can permission="account_payables.edit">
<Button
variant="ghost"
size="sm"
className="text-primary-main hover:text-primary-dark hover:bg-primary-main/10"
onClick={() => setInvoiceDialogOpen(true)}
>
{payable.invoice_number ? '修改' : '填寫'}
</Button>
</Can>
</div>
<div className="space-y-4">
<div>
<span className="text-sm text-primary-main/70"></span>
<p className="font-medium text-primary-dark mt-1">
{payable.invoice_number || <span className="opacity-70 italic"></span>}
</p>
</div>
<div>
<span className="text-sm text-primary-main/70"></span>
<p className="font-medium text-primary-dark mt-1">
{payable.invoice_date ? formatDate(payable.invoice_date) : '-'}
</p>
</div>
</div>
</div>
{/* 付款資訊 */}
{payable.status === 'paid' && (
<div className="bg-white rounded-lg shadow-sm border p-6">
<h2 className="text-lg font-semibold text-gray-800 mb-4 flex items-center gap-2">
<CheckCircle className="h-5 w-5 text-success-main" />
</h2>
<div className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div>
<span className="text-sm text-gray-500"></span>
<p className="font-medium text-success-main mt-1">
</p>
</div>
<div>
<span className="text-sm text-gray-500"></span>
<p className="font-medium text-gray-800 mt-1">
{payable.payment_method === 'cash' && '現金'}
{payable.payment_method === 'bank_transfer' && '銀行轉帳'}
{payable.payment_method === 'check' && '支票'}
{payable.payment_method === 'credit_card' && '信用卡'}
{!['cash', 'bank_transfer', 'check', 'credit_card'].includes(payable.payment_method) && (payable.payment_method || '-')}
</p>
</div>
<div>
<span className="text-sm text-gray-500"></span>
<p className="font-medium text-gray-800 mt-1">
{payable.paid_at ? formatDate(payable.paid_at) : '-'}
</p>
</div>
</div>
{payable.payment_note && (
<div>
<span className="text-sm text-gray-500"></span>
<p className="font-medium text-gray-800 mt-1">
{payable.payment_note}
</p>
</div>
)}
</div>
</div>
)}
</div>
</div>
{/* 發票對話框 */}
<Dialog open={invoiceDialogOpen} onOpenChange={setInvoiceDialogOpen}>
<DialogContent>
<form onSubmit={handleInvoiceSubmit}>
<DialogHeader>
<DialogTitle></DialogTitle>
<DialogDescription>
</DialogDescription>
</DialogHeader>
<div className="space-y-4 py-4">
<div className="space-y-2">
<Label htmlFor="invoice_number"></Label>
<Input
id="invoice_number"
value={invoiceForm.data.invoice_number}
onChange={e => invoiceForm.setData('invoice_number', e.target.value)}
placeholder="例如: AB12345678"
/>
{invoiceForm.errors.invoice_number && <p className="text-sm text-destructive">{invoiceForm.errors.invoice_number}</p>}
</div>
<div className="space-y-2">
<Label htmlFor="invoice_date"></Label>
<Input
id="invoice_date"
type="date"
value={invoiceForm.data.invoice_date}
onChange={e => invoiceForm.setData('invoice_date', e.target.value)}
/>
{invoiceForm.errors.invoice_date && <p className="text-sm text-destructive">{invoiceForm.errors.invoice_date}</p>}
</div>
</div>
<DialogFooter>
<Button type="button" variant="outline" className="button-outlined-primary" onClick={() => setInvoiceDialogOpen(false)}></Button>
<Button type="submit" disabled={invoiceForm.processing} className="button-filled-primary"></Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
{/* 付款對話框 */}
<Dialog open={paymentDialogOpen} onOpenChange={setPaymentDialogOpen}>
<DialogContent>
<form onSubmit={handlePaymentSubmit}>
<DialogHeader>
<DialogTitle></DialogTitle>
<DialogDescription>
</DialogDescription>
</DialogHeader>
<div className="space-y-4 py-4">
<div className="space-y-2">
<Label htmlFor="payment_method"></Label>
<select
id="payment_method"
className="flex h-10 w-full items-center justify-between rounded-md border border-neutral-200 bg-white px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-primary-main focus:border-transparent text-gray-700"
value={paymentForm.data.payment_method}
onChange={(e) => paymentForm.setData('payment_method', e.target.value)}
required
>
<option value="bank_transfer">/</option>
<option value="cash"></option>
<option value="check"></option>
<option value="credit_card"></option>
<option value="other"></option>
</select>
{paymentForm.errors.payment_method && <p className="text-sm text-destructive">{paymentForm.errors.payment_method}</p>}
</div>
<div className="space-y-2">
<Label htmlFor="paid_at"></Label>
<Input
id="paid_at"
type="date"
value={paymentForm.data.paid_at}
onChange={e => paymentForm.setData('paid_at', e.target.value)}
required
/>
{paymentForm.errors.paid_at && <p className="text-sm text-destructive">{paymentForm.errors.paid_at}</p>}
</div>
<div className="space-y-2">
<Label htmlFor="payment_note"> ()</Label>
<Input
id="payment_note"
value={paymentForm.data.payment_note}
onChange={e => paymentForm.setData('payment_note', e.target.value)}
placeholder="例如: 匯款帳號後五碼、支票號碼..."
/>
{paymentForm.errors.payment_note && <p className="text-sm text-destructive">{paymentForm.errors.payment_note}</p>}
</div>
</div>
<DialogFooter>
<Button type="button" variant="outline" className="button-outlined-primary" onClick={() => setPaymentDialogOpen(false)}></Button>
<Button type="submit" disabled={paymentForm.processing} className="button-filled-primary"></Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
</div>
</AuthenticatedLayout>
);
}