Files

380 lines
21 KiB
TypeScript
Raw Permalink 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 flex-col md:flex-row md:items-start justify-between gap-4 mb-6">
<div className="space-y-2">
<div className="flex items-center gap-2">
<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>
{/* @ts-ignore */}
<StatusBadge variant={getStatusBadgeVariant(payable.status)}>
{getStatusLabel(payable.status)}
</StatusBadge>
</div>
<p className="text-sm text-gray-500 font-medium flex flex-wrap items-center gap-2">
: {payable.vendor?.name || '未知供應商'} <span className="mx-1">|</span>
: {payable.creator?.name || "-"} <span className="mx-1">|</span>
: {formatDate(payable.due_date)} <span className="mx-1">|</span>
: {formatDate(payable.created_at)}
</p>
</div>
<div className="flex flex-wrap 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-bold text-gray-900 mb-6 border-b pb-4"></h2>
<div className="grid grid-cols-1 md:grid-cols-3 gap-x-8 gap-y-6">
<div>
<span className="text-sm text-gray-500 block mb-1"></span>
<p className="font-medium text-gray-800">
${parseFloat(payable.total_amount).toLocaleString()}
</p>
</div>
<div>
<span className="text-sm text-gray-500 block mb-1"></span>
<p className="font-medium text-gray-800">
{payable.source_document_type === 'goods_receipt' ? (
<Badge variant="outline" className="font-normal border-gray-300"></Badge>
) : (
payable.source_document_type || '-'
)}
</p>
</div>
<div>
<span className="text-sm text-gray-500 block mb-1"></span>
<div className="flex items-center gap-2">
<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]) + `?from=account-payables&from_id=${payable.id}&from_label=${payable.document_number}`}
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>
{payable.remarks && (
<div className="mt-8 pt-6 border-t border-gray-100">
<span className="text-sm text-gray-500 block mb-2"></span>
<p className="text-sm text-gray-700 bg-gray-50 p-4 rounded-lg leading-relaxed">
{payable.remarks}
</p>
</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>
);
}