527 lines
30 KiB
TypeScript
527 lines
30 KiB
TypeScript
'use client'
|
|
|
|
import React from 'react'
|
|
import PageLayout from '../../components/PageLayout'
|
|
import InvoiceDetailModal from './components/InvoiceDetailModal'
|
|
import { type AdminInvoice } from './hooks/getInvoices'
|
|
import { useFinanceManagementPageState } from './hooks/useFinanceManagementPageState'
|
|
|
|
function getStatusBadgeClass(status?: string) {
|
|
if (status === 'paid') return 'bg-green-100 text-green-700'
|
|
if (status === 'issued') return 'bg-indigo-100 text-indigo-700'
|
|
if (status === 'draft') return 'bg-slate-100 text-slate-700'
|
|
if (status === 'overdue') return 'bg-red-100 text-red-700'
|
|
return 'bg-amber-100 text-amber-700'
|
|
}
|
|
|
|
function getStatusLabel(t: (key: string) => string, status?: string) {
|
|
if (status === 'draft') return t('autofix.k5f6d9f11')
|
|
if (status === 'issued') return t('autofix.kdc8f2ab2')
|
|
if (status === 'paid') return t('autofix.k9d5b2d74')
|
|
if (status === 'overdue') return t('autofix.k2f44ec11')
|
|
if (status === 'canceled') return t('autofix.kcf31ed66')
|
|
return status
|
|
}
|
|
|
|
function FinanceInvoiceActions({
|
|
invoice,
|
|
pdfLoading,
|
|
onViewPdf,
|
|
onOpenDetails,
|
|
t,
|
|
}: {
|
|
invoice: AdminInvoice
|
|
pdfLoading: string | number | null
|
|
onViewPdf: (invoice: AdminInvoice) => void
|
|
onOpenDetails: (invoice: AdminInvoice) => void
|
|
t: (key: string) => string
|
|
}) {
|
|
return (
|
|
<td className="px-3 py-2 space-x-2">
|
|
<button
|
|
onClick={() => onViewPdf(invoice)}
|
|
disabled={pdfLoading === invoice.id || !invoice.pdf_storage_key}
|
|
className="text-xs rounded-lg border border-slate-200 px-2.5 py-1.5 hover:bg-slate-50 disabled:opacity-50 disabled:cursor-not-allowed"
|
|
>
|
|
{pdfLoading === invoice.id ? t('autofix.k79d12c2e') : t('autofix.kfbe29d11')}
|
|
</button>
|
|
<button
|
|
onClick={() => onOpenDetails(invoice)}
|
|
className="text-xs rounded-lg border border-slate-200 px-2.5 py-1.5 hover:bg-slate-50"
|
|
>
|
|
{t('autofix.kf67200af')}
|
|
</button>
|
|
</td>
|
|
)
|
|
}
|
|
|
|
export default function FinanceManagementPage() {
|
|
const {
|
|
t,
|
|
router,
|
|
rates,
|
|
vatLoading,
|
|
vatError,
|
|
timeframe,
|
|
setTimeframe,
|
|
billFilter,
|
|
setBillFilter,
|
|
diagLoading,
|
|
diagError,
|
|
diagData,
|
|
selectedInvoice,
|
|
setSelectedInvoice,
|
|
detailModalOpen,
|
|
setDetailModalOpen,
|
|
emailDialogOpen,
|
|
setEmailDialogOpen,
|
|
reportEmail,
|
|
setReportEmail,
|
|
sendingReport,
|
|
reportMsg,
|
|
setReportMsg,
|
|
invLoading,
|
|
invError,
|
|
reload,
|
|
totals,
|
|
filteredBills,
|
|
exportBills,
|
|
runPoolCheck,
|
|
exportInvoice,
|
|
pdfLoading,
|
|
uploadModalOpen,
|
|
setUploadModalOpen,
|
|
uploadForm,
|
|
setUploadForm,
|
|
uploadFile,
|
|
setUploadFile,
|
|
uploading,
|
|
uploadError,
|
|
setUploadError,
|
|
viewInvoicePdf,
|
|
submitUploadInvoice,
|
|
sendEmailReport,
|
|
uploadPreview,
|
|
} = useFinanceManagementPageState()
|
|
|
|
return (
|
|
<PageLayout contentClassName="flex-1 relative w-full">
|
|
<div className="min-h-screen bg-[radial-gradient(circle_at_top_left,rgba(251,191,36,0.10),transparent_22%),radial-gradient(circle_at_top_right,rgba(56,189,248,0.10),transparent_24%),linear-gradient(180deg,#f8fafc_0%,#f8fafc_50%,#eef2ff_100%)]">
|
|
<div className="max-w-[1820px] mx-auto px-4 sm:px-6 xl:px-10 py-8 space-y-5">
|
|
<header className="rounded-[28px] border border-white/80 bg-white/90 p-8 shadow-[0_24px_70px_-40px_rgba(15,23,42,0.3)] backdrop-blur">
|
|
<div className="inline-flex items-center rounded-full border border-slate-200 bg-white px-3 py-1 text-[11px] font-semibold uppercase tracking-[0.28em] text-slate-500">
|
|
{t('autofix.k8070cd52')}
|
|
</div>
|
|
<h1 className="mt-3 text-3xl sm:text-4xl font-extrabold text-slate-900 tracking-tight break-words">{t('autofix.k777299de')}</h1>
|
|
<p className="text-sm sm:text-base text-slate-600 mt-2 break-words">{t('autofix.k01ad6d49')}</p>
|
|
</header>
|
|
|
|
<section className="grid [grid-template-columns:repeat(auto-fit,minmax(16rem,1fr))] gap-4">
|
|
<div className="rounded-3xl border border-white/80 bg-white/90 p-5 shadow-[0_24px_70px_-44px_rgba(15,23,42,0.3)] backdrop-blur">
|
|
<div className="text-xs text-slate-500 mb-1">{t('autofix.k73f7184d')}</div>
|
|
<div className="text-2xl font-semibold text-slate-900">EUR {totals.totalAll.toFixed(2)}</div>
|
|
</div>
|
|
<div className="rounded-3xl border border-white/80 bg-white/90 p-5 shadow-[0_24px_70px_-44px_rgba(15,23,42,0.3)] backdrop-blur">
|
|
<div className="text-xs text-slate-500 mb-1">{t('autofix.k9b3082af')}</div>
|
|
<div className="text-2xl font-semibold text-slate-900">EUR {totals.totalRange.toFixed(2)}</div>
|
|
</div>
|
|
<div className="rounded-3xl border border-white/80 bg-white/90 p-5 shadow-[0_24px_70px_-44px_rgba(15,23,42,0.3)] backdrop-blur">
|
|
<div className="text-xs text-slate-500 mb-1">{t('autofix.k9f4ec5e2')}</div>
|
|
<div className="text-2xl font-semibold text-slate-900">{filteredBills.length}</div>
|
|
</div>
|
|
<div className="rounded-3xl border border-white/80 bg-white/90 p-5 shadow-[0_24px_70px_-44px_rgba(15,23,42,0.3)] backdrop-blur">
|
|
<div className="text-xs text-slate-500 mb-1">{t('autofix.kafb65833')}</div>
|
|
<select
|
|
value={timeframe}
|
|
onChange={(event) => setTimeframe(event.target.value as '7d' | '30d' | '90d' | 'ytd')}
|
|
className="mt-2 w-full rounded-lg border border-slate-200 px-3 py-2 text-sm focus:ring-2 focus:ring-slate-900 focus:border-transparent"
|
|
>
|
|
<option value="7d">{t('autofix.k502a0057')}</option>
|
|
<option value="30d">{t('autofix.k5f74c123')}</option>
|
|
<option value="90d">{t('autofix.k915115a9')}</option>
|
|
<option value="ytd">{t('autofix.k0f5d95a1')}</option>
|
|
</select>
|
|
</div>
|
|
</section>
|
|
|
|
<section className="rounded-[28px] border border-white/80 bg-white/90 p-8 shadow-[0_24px_70px_-40px_rgba(15,23,42,0.3)] backdrop-blur space-y-3">
|
|
<div className="flex flex-wrap items-center justify-between gap-3">
|
|
<div>
|
|
<h2 className="text-lg font-semibold text-slate-900">{t('autofix.kf2180ff6')}</h2>
|
|
<p className="text-xs text-slate-600">{t('autofix.k5ce7a5b0')}</p>
|
|
</div>
|
|
<button
|
|
onClick={() => router.push('/admin/finance-management/vat-edit')}
|
|
className="rounded-xl bg-slate-900 px-4 py-2 text-sm font-semibold text-white shadow hover:bg-slate-800 transition"
|
|
>
|
|
{t('autofix.k4191cdba')}
|
|
</button>
|
|
</div>
|
|
<div className="text-sm text-slate-700">
|
|
{vatLoading && t('autofix.ka5d50257')}
|
|
{vatError && <span className="text-red-600">{vatError}</span>}
|
|
{!vatLoading && !vatError && (
|
|
<>
|
|
{t('autofix.k3e4a95bc').replace('{count}', String(rates.length)).replace('{examples}', rates.slice(0, 5).map((rate) => rate.country_code).join(', '))}
|
|
</>
|
|
)}
|
|
</div>
|
|
</section>
|
|
|
|
<section className="rounded-[28px] border border-white/80 bg-white/90 p-8 shadow-[0_24px_70px_-40px_rgba(15,23,42,0.3)] backdrop-blur space-y-4">
|
|
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
|
<h2 className="text-lg font-semibold text-slate-900">{t('autofix.k21f123af')}</h2>
|
|
<div className="flex flex-wrap gap-2 text-sm">
|
|
<button
|
|
onClick={() => {
|
|
setUploadError(null)
|
|
setUploadModalOpen(true)
|
|
}}
|
|
className="rounded-xl bg-slate-900 px-3 py-2 text-white font-medium hover:bg-slate-800 transition"
|
|
>
|
|
{t('autofix.kec5a5357')}
|
|
</button>
|
|
<button
|
|
onClick={() => {
|
|
setReportMsg(null)
|
|
setEmailDialogOpen(true)
|
|
}}
|
|
className="rounded-xl border border-sky-200 bg-sky-50 px-3 py-2 text-sky-900 font-medium hover:bg-sky-100 transition"
|
|
>
|
|
{t('autofix.kfdcad59b')}
|
|
</button>
|
|
<button onClick={() => exportBills('csv')} className="rounded-xl border border-slate-200 px-3 py-2 hover:bg-slate-50 transition">{t('autofix.k4c5e8e87')}</button>
|
|
<button onClick={() => exportBills('pdf')} className="rounded-xl border border-slate-200 px-3 py-2 hover:bg-slate-50 transition">{t('autofix.k4c5ecd73')}</button>
|
|
<button onClick={reload} className="rounded-xl border border-slate-200 px-3 py-2 hover:bg-slate-50 transition">{t('autofix.kddf7ca98')}</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="grid gap-3 lg:grid-cols-4 text-sm">
|
|
<input
|
|
placeholder={t('autofix.k8bb2fe26')}
|
|
value={billFilter.query}
|
|
onChange={(event) => setBillFilter((current) => ({ ...current, query: event.target.value }))}
|
|
className="rounded-lg border border-slate-200 px-3 py-2 focus:ring-2 focus:ring-slate-900 focus:border-transparent"
|
|
/>
|
|
<select
|
|
value={billFilter.status}
|
|
onChange={(event) => setBillFilter((current) => ({ ...current, status: event.target.value }))}
|
|
className="rounded-lg border border-slate-200 px-3 py-2 focus:ring-2 focus:ring-slate-900 focus:border-transparent"
|
|
>
|
|
<option value="all">{t('autofix.kec99a6cc')}</option>
|
|
<option value="draft">{t('autofix.k5f6d9f11')}</option>
|
|
<option value="issued">{t('autofix.kdc8f2ab2')}</option>
|
|
<option value="paid">{t('autofix.k9d5b2d74')}</option>
|
|
<option value="overdue">{t('autofix.k2f44ec11')}</option>
|
|
<option value="canceled">{t('autofix.kcf31ed66')}</option>
|
|
</select>
|
|
<input
|
|
type="date"
|
|
value={billFilter.from}
|
|
onChange={(event) => setBillFilter((current) => ({ ...current, from: event.target.value }))}
|
|
className="rounded-lg border border-slate-200 px-3 py-2 focus:ring-2 focus:ring-slate-900 focus:border-transparent"
|
|
/>
|
|
<input
|
|
type="date"
|
|
value={billFilter.to}
|
|
onChange={(event) => setBillFilter((current) => ({ ...current, to: event.target.value }))}
|
|
className="rounded-lg border border-slate-200 px-3 py-2 focus:ring-2 focus:ring-slate-900 focus:border-transparent"
|
|
/>
|
|
</div>
|
|
|
|
<div className="overflow-x-auto rounded-2xl border border-slate-200/70 bg-white/70 p-1">
|
|
{reportMsg && (
|
|
<div className={`rounded-xl border px-3 py-2 text-sm mb-3 ${reportMsg.type === 'success' ? 'border-green-200 bg-green-50 text-green-700' : 'border-red-200 bg-red-50 text-red-700'}`}>
|
|
{reportMsg.text}
|
|
</div>
|
|
)}
|
|
|
|
{invError && (
|
|
<div className="rounded-xl border border-red-200 bg-red-50 px-3 py-2 text-sm text-red-700 mb-3">
|
|
{invError}
|
|
</div>
|
|
)}
|
|
|
|
{(diagLoading || diagError || diagData) && (
|
|
<div className="rounded-xl border border-sky-200 bg-sky-50/60 px-3 py-3 text-sm mb-3">
|
|
{diagLoading && <div className="text-sky-800">{t('autofix.k37d7b9c4')}</div>}
|
|
{!diagLoading && diagError && <div className="text-red-700">{diagError}</div>}
|
|
{!diagLoading && !diagError && diagData && (
|
|
<div className="space-y-2">
|
|
<div className="text-sky-900 font-semibold">
|
|
{t('autofix.kf6a5a971').replace('{invoice}', String(diagData.invoice_id ?? '—'))}
|
|
</div>
|
|
<div className="text-slate-700">
|
|
{t('autofix.k81c0b74b')}
|
|
<span className="font-medium">{diagData.ok ? t('autofix.kaf7e90cc') : t('autofix.k6ba7f5b1')}</span>
|
|
{t('autofix.k77049179')}
|
|
<span className="font-mono">{diagData.reason}</span>
|
|
</div>
|
|
|
|
{diagData.ok && (
|
|
<div className="text-slate-700">
|
|
{t('autofix.k4968eb2a')}
|
|
<span className="font-medium">{diagData.abonement_id}</span>
|
|
{t('autofix.kfaa8fc4a')}
|
|
<span className="font-medium">{diagData.will_book_count}</span>
|
|
{t('autofix.kd2e5e813')}
|
|
<span className="font-medium">{diagData.already_booked_count}</span>
|
|
</div>
|
|
)}
|
|
|
|
{Array.isArray(diagData.candidates) && diagData.candidates.length > 0 && (
|
|
<div className="overflow-x-auto">
|
|
<table className="min-w-full text-xs">
|
|
<thead>
|
|
<tr className="text-left text-sky-900">
|
|
<th className="pr-3 py-1">{t('autofix.kf1b73a92')}</th>
|
|
<th className="pr-3 py-1">{t('autofix.k2f9cd1e0')}</th>
|
|
<th className="pr-3 py-1">{t('autofix.k1ddc3f42')}</th>
|
|
<th className="pr-3 py-1">{t('autofix.kdb79aa30')}</th>
|
|
<th className="pr-3 py-1">{t('autofix.k93e61ad1')}</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{diagData.candidates.map((candidate: any) => (
|
|
<tr key={`${candidate.pool_id}-${candidate.coffee_table_id}`}>
|
|
<td className="pr-3 py-1">{candidate.pool_name}</td>
|
|
<td className="pr-3 py-1">#{candidate.coffee_table_id}</td>
|
|
<td className="pr-3 py-1">{candidate.capsules_count}</td>
|
|
<td className="pr-3 py-1">EUR {Number(candidate.amount_gross ?? candidate.amount_net ?? 0).toFixed(2)}</td>
|
|
<td className="pr-3 py-1">{candidate.already_booked ? t('common.yes') : t('common.no')}</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
<table className="min-w-full text-sm rounded-xl overflow-hidden">
|
|
<thead>
|
|
<tr className="bg-slate-50 text-left text-slate-900">
|
|
<th className="px-3 py-2 font-semibold">{t('autofix.kf8f0c1f3')}</th>
|
|
<th className="px-3 py-2 font-semibold">{t('autofix.kf2b5c1a6')}</th>
|
|
<th className="px-3 py-2 font-semibold">{t('autofix.kd4af6368')}</th>
|
|
<th className="px-3 py-2 font-semibold">{t('autofix.k867f8265')}</th>
|
|
<th className="px-3 py-2 font-semibold">{t('autofix.k762eef76')}</th>
|
|
<th className="px-3 py-2 font-semibold">{t('autofix.k81c0b74b')}</th>
|
|
<th className="px-3 py-2 font-semibold">{t('autofix.k0afbbac4')}</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody className="divide-y divide-slate-100">
|
|
{invLoading ? (
|
|
<>
|
|
<tr><td colSpan={7} className="px-3 py-3"><div className="h-4 w-40 bg-slate-200 animate-pulse rounded" /></td></tr>
|
|
<tr><td colSpan={7} className="px-3 py-3"><div className="h-4 w-3/4 bg-slate-200 animate-pulse rounded" /></td></tr>
|
|
</>
|
|
) : filteredBills.length === 0 ? (
|
|
<tr>
|
|
<td colSpan={7} className="px-3 py-4 text-center text-slate-500">{t('autofix.kbdb02e32')}</td>
|
|
</tr>
|
|
) : (
|
|
filteredBills.map((invoice) => (
|
|
<tr key={invoice.id} className="border-b border-slate-100 last:border-0">
|
|
<td className="px-3 py-2">{invoice.invoice_number ?? invoice.id}</td>
|
|
<td className="px-3 py-2">{invoice.buyer_name ?? '—'}</td>
|
|
<td className="px-3 py-2">{invoice.issued_at ? new Date(invoice.issued_at).toLocaleDateString() : '—'}</td>
|
|
<td className="px-3 py-2">
|
|
{(() => {
|
|
if (!invoice.due_at) return <span className="text-slate-400">—</span>
|
|
|
|
const due = new Date(invoice.due_at)
|
|
const now = new Date()
|
|
const diffDays = Math.ceil((due.getTime() - now.getTime()) / (1000 * 60 * 60 * 24))
|
|
|
|
let cls = 'bg-green-100 text-green-700'
|
|
if (invoice.status === 'paid') cls = 'bg-green-100 text-green-700'
|
|
else if (diffDays < 0) cls = 'bg-red-100 text-red-700'
|
|
else if (diffDays <= 3) cls = 'bg-red-100 text-red-700'
|
|
else if (diffDays <= 7) cls = 'bg-amber-100 text-amber-700'
|
|
|
|
return <span className={`rounded-full px-2 py-0.5 text-xs font-semibold ${cls}`}>{due.toLocaleDateString()}</span>
|
|
})()}
|
|
</td>
|
|
<td className="px-3 py-2">
|
|
EUR {Number(invoice.total_gross ?? 0).toFixed(2)} <span className="text-xs text-slate-500">{invoice.currency ?? 'EUR'}</span>
|
|
</td>
|
|
<td className="px-3 py-2">
|
|
<span className={`rounded-full px-2 py-0.5 text-xs font-semibold ${getStatusBadgeClass(invoice.status ?? '')}`}>
|
|
{getStatusLabel(t, invoice.status ?? '')}
|
|
</span>
|
|
</td>
|
|
<FinanceInvoiceActions
|
|
invoice={invoice}
|
|
pdfLoading={pdfLoading}
|
|
onViewPdf={viewInvoicePdf}
|
|
onOpenDetails={(value) => {
|
|
setSelectedInvoice(value)
|
|
setDetailModalOpen(true)
|
|
}}
|
|
t={t}
|
|
/>
|
|
</tr>
|
|
))
|
|
)}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
{selectedInvoice && (
|
|
<InvoiceDetailModal
|
|
invoice={selectedInvoice}
|
|
open={detailModalOpen}
|
|
onClose={() => {
|
|
setDetailModalOpen(false)
|
|
setTimeout(() => setSelectedInvoice(null), 200)
|
|
}}
|
|
onStatusChanged={reload}
|
|
onRunPoolCheck={(id) => {
|
|
setDetailModalOpen(false)
|
|
runPoolCheck(id)
|
|
}}
|
|
onExport={(invoice) => exportInvoice(invoice)}
|
|
/>
|
|
)}
|
|
|
|
{uploadModalOpen && (
|
|
<div className="fixed inset-0 z-50 flex items-center justify-center bg-slate-900/35 backdrop-blur-sm">
|
|
<div className="w-full max-w-2xl rounded-[28px] border border-white/80 bg-white/95 p-6 shadow-[0_24px_70px_-30px_rgba(15,23,42,0.35)] overflow-y-auto max-h-[90vh]">
|
|
<h3 className="text-lg font-semibold text-slate-900 mb-4">{t('autofix.kec5a5357')}</h3>
|
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3 text-sm">
|
|
<div>
|
|
<label className="block text-xs font-medium text-slate-700 mb-1">{t('autofix.kf2b5c1a6')}</label>
|
|
<input className="w-full rounded-lg border border-slate-200 px-3 py-2" value={uploadForm.buyer_name} onChange={(event) => setUploadForm((current) => ({ ...current, buyer_name: event.target.value }))} placeholder={t('autofix.k1882bd75')} />
|
|
</div>
|
|
<div>
|
|
<label className="block text-xs font-medium text-slate-700 mb-1">{t('autofix.k48852b8d')}</label>
|
|
<input type="email" className="w-full rounded-lg border border-slate-200 px-3 py-2" value={uploadForm.buyer_email} onChange={(event) => setUploadForm((current) => ({ ...current, buyer_email: event.target.value }))} placeholder={t('autofix.kf8c220d3')} />
|
|
</div>
|
|
<div>
|
|
<label className="block text-xs font-medium text-slate-700 mb-1">{t('autofix.kba8ee9b1')}</label>
|
|
<input className="w-full rounded-lg border border-slate-200 px-3 py-2" value={uploadForm.buyer_street} onChange={(event) => setUploadForm((current) => ({ ...current, buyer_street: event.target.value }))} placeholder={t('autofix.k81c7c2f2')} />
|
|
</div>
|
|
<div>
|
|
<label className="block text-xs font-medium text-slate-700 mb-1">{t('autofix.kc9d9d15d')}</label>
|
|
<input className="w-full rounded-lg border border-slate-200 px-3 py-2" value={uploadForm.buyer_postal_code} onChange={(event) => setUploadForm((current) => ({ ...current, buyer_postal_code: event.target.value }))} placeholder="8010" />
|
|
</div>
|
|
<div>
|
|
<label className="block text-xs font-medium text-slate-700 mb-1">{t('autofix.k5d52917f')}</label>
|
|
<input className="w-full rounded-lg border border-slate-200 px-3 py-2" value={uploadForm.buyer_city} onChange={(event) => setUploadForm((current) => ({ ...current, buyer_city: event.target.value }))} placeholder="Graz" />
|
|
</div>
|
|
<div>
|
|
<label className="block text-xs font-medium text-slate-700 mb-1">{t('autofix.k9e39e560')}</label>
|
|
<input className="w-full rounded-lg border border-slate-200 px-3 py-2" value={uploadForm.buyer_country} onChange={(event) => setUploadForm((current) => ({ ...current, buyer_country: event.target.value }))} placeholder="Austria" />
|
|
</div>
|
|
<div>
|
|
<label className="block text-xs font-medium text-slate-700 mb-1">{t('autofix.k002455d8')}<span className="text-red-500">*</span></label>
|
|
<input type="number" step="0.01" min="0" className="w-full rounded-lg border border-slate-200 px-3 py-2" value={uploadForm.total_gross} onChange={(event) => setUploadForm((current) => ({ ...current, total_gross: event.target.value }))} placeholder="0.00" />
|
|
</div>
|
|
<div>
|
|
<label className="block text-xs font-medium text-slate-700 mb-1">{t('autofix.k57d5f250')}</label>
|
|
<input type="number" step="0.01" min="0" max="100" className="w-full rounded-lg border border-slate-200 px-3 py-2" value={uploadForm.vat_rate} onChange={(event) => setUploadForm((current) => ({ ...current, vat_rate: event.target.value }))} placeholder="20" />
|
|
</div>
|
|
|
|
<div className="sm:col-span-2 grid grid-cols-2 gap-3">
|
|
<div className="rounded-lg bg-slate-50 border border-slate-100 px-3 py-2">
|
|
<div className="text-xs text-slate-500 mb-0.5">{t('autofix.k1f5a403a')}</div>
|
|
<div className="font-semibold text-slate-800">{uploadForm.currency} {uploadPreview.net.toFixed(2)}</div>
|
|
</div>
|
|
<div className="rounded-lg bg-slate-50 border border-slate-100 px-3 py-2">
|
|
<div className="text-xs text-slate-500 mb-0.5">{t('autofix.k089e8c08')}</div>
|
|
<div className="font-semibold text-slate-800">{uploadForm.currency} {uploadPreview.tax.toFixed(2)}</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-xs font-medium text-slate-700 mb-1">{t('autofix.k3466b0e0')}</label>
|
|
<select className="w-full rounded-lg border border-slate-200 px-3 py-2" value={uploadForm.currency} onChange={(event) => setUploadForm((current) => ({ ...current, currency: event.target.value }))}>
|
|
<option value="EUR">EUR</option>
|
|
<option value="CHF">CHF</option>
|
|
<option value="USD">USD</option>
|
|
</select>
|
|
</div>
|
|
<div>
|
|
<label className="block text-xs font-medium text-slate-700 mb-1">{t('autofix.k81c0b74b')}</label>
|
|
<select className="w-full rounded-lg border border-slate-200 px-3 py-2" value={uploadForm.status} onChange={(event) => setUploadForm((current) => ({ ...current, status: event.target.value }))}>
|
|
<option value="issued">{t('autofix.kdc8f2ab2')}</option>
|
|
<option value="paid">{t('autofix.k9d5b2d74')}</option>
|
|
<option value="draft">{t('autofix.k5f6d9f11')}</option>
|
|
<option value="overdue">{t('autofix.k2f44ec11')}</option>
|
|
</select>
|
|
</div>
|
|
<div>
|
|
<label className="block text-xs font-medium text-slate-700 mb-1">{t('autofix.kd4af6368')}</label>
|
|
<input type="date" className="w-full rounded-lg border border-slate-200 px-3 py-2" value={uploadForm.issued_at} onChange={(event) => setUploadForm((current) => ({ ...current, issued_at: event.target.value }))} />
|
|
</div>
|
|
<div>
|
|
<label className="block text-xs font-medium text-slate-700 mb-1">{t('autofix.k867f8265')}</label>
|
|
<input type="date" className="w-full rounded-lg border border-slate-200 px-3 py-2" value={uploadForm.due_at} onChange={(event) => setUploadForm((current) => ({ ...current, due_at: event.target.value }))} />
|
|
</div>
|
|
<div className="sm:col-span-2">
|
|
<label className="block text-xs font-medium text-slate-700 mb-1">{t('autofix.kd6024811')}</label>
|
|
<input
|
|
type="file"
|
|
accept="application/pdf"
|
|
className="w-full text-sm text-slate-700 file:mr-3 file:rounded-lg file:border-0 file:bg-sky-50 file:px-3 file:py-2 file:text-sky-900 file:font-medium hover:file:bg-sky-100"
|
|
onChange={(event) => setUploadFile(event.target.files?.[0] ?? null)}
|
|
/>
|
|
{uploadFile && <p className="mt-1 text-xs text-slate-500">{uploadFile.name}</p>}
|
|
</div>
|
|
</div>
|
|
|
|
{uploadError && <div className="mt-3 rounded-xl border border-red-200 bg-red-50 px-3 py-2 text-sm text-red-700">{uploadError}</div>}
|
|
|
|
<div className="mt-5 flex items-center justify-end gap-2">
|
|
<button onClick={() => { setUploadModalOpen(false); setUploadError(null) }} disabled={uploading} className="rounded-xl border border-slate-200 px-4 py-2 text-sm text-slate-700 hover:bg-slate-50 disabled:opacity-60">{t('common.cancel')}</button>
|
|
<button onClick={submitUploadInvoice} disabled={uploading} className="rounded-xl bg-slate-900 px-4 py-2 text-sm font-semibold text-white shadow hover:bg-slate-800 disabled:opacity-60 disabled:cursor-not-allowed">
|
|
{uploading ? t('autofix.k3bc9a0f1') : t('autofix.k1139753d')}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{emailDialogOpen && (
|
|
<div className="fixed inset-0 z-50 flex items-center justify-center bg-slate-900/35 backdrop-blur-sm">
|
|
<div className="w-full max-w-md rounded-[28px] border border-white/80 bg-white/95 p-6 shadow-[0_24px_70px_-30px_rgba(15,23,42,0.35)]">
|
|
<h3 className="text-lg font-semibold text-slate-900 mb-1">{t('autofix.kfdcad59b')}</h3>
|
|
<div className="text-xs text-amber-700 bg-amber-50 border border-amber-200 rounded-lg px-3 py-2 mb-4">
|
|
{t('autofix.k45c3fd51').replace('{paid}', t('autofix.k9d5b2d74').toLowerCase())}
|
|
{(billFilter.from || billFilter.to) && (
|
|
<span> {t('autofix.kdd22a5f2').replace('{from}', billFilter.from || '…').replace('{to}', billFilter.to || '…')}</span>
|
|
)}
|
|
</div>
|
|
|
|
<label className="block text-sm font-medium text-slate-700 mb-1">{t('autofix.kd56a13f2')}</label>
|
|
<input
|
|
type="email"
|
|
value={reportEmail}
|
|
onChange={(event) => setReportEmail(event.target.value)}
|
|
placeholder={t('autofix.k51ee3aae')}
|
|
className="w-full rounded-lg border border-slate-200 px-3 py-2 text-sm text-slate-900 placeholder:text-slate-400 focus:ring-2 focus:ring-slate-900 focus:border-transparent"
|
|
autoFocus
|
|
onKeyDown={(event) => {
|
|
if (event.key === 'Enter' && !sendingReport) sendEmailReport()
|
|
}}
|
|
/>
|
|
|
|
<div className="mt-4 flex items-center justify-end gap-2">
|
|
<button onClick={() => { setEmailDialogOpen(false); setReportEmail('') }} disabled={sendingReport} className="rounded-xl border border-slate-200 px-4 py-2 text-sm text-slate-700 hover:bg-slate-50 disabled:opacity-60">{t('common.cancel')}</button>
|
|
<button onClick={sendEmailReport} disabled={sendingReport || !reportEmail.trim()} className="rounded-xl bg-slate-900 px-4 py-2 text-sm font-semibold text-white shadow hover:bg-slate-800 disabled:opacity-60 disabled:cursor-not-allowed">
|
|
{sendingReport ? t('autofix.k795911e8') : t('autofix.kf6f9b3c0')}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</section>
|
|
</div>
|
|
</div>
|
|
</PageLayout>
|
|
)
|
|
}
|