import axios from 'axios'; import { useRouter } from 'next/router'; import { useEffect, useState, useRef } from 'react'; import { toast } from 'react-hot-toast'; import { EllipsisVerticalIcon, MagnifyingGlassIcon, ChevronDownIcon, } from '@heroicons/react/24/outline'; import useAuth from '@/core/hooks/useAuth'; import { downloadPurchaseOrder, downloadQuotation, } from '../utils/transactions'; import useTransactions from '../hooks/useTransactions'; import currencyFormat from '@/core/utils/currencyFormat'; import cancelTransactionApi from '../api/cancelTransactionApi'; import TransactionStatusBadge from './TransactionStatusBadge'; import Spinner from '@/core/components/elements/Spinner/Spinner'; import Link from '@/core/components/elements/Link/Link'; import BottomPopup from '@/core/components/elements/Popup/BottomPopup'; import Pagination from '@/core/components/elements/Pagination/Pagination'; import { toQuery } from 'lodash-contrib'; import _ from 'lodash'; import Alert from '@/core/components/elements/Alert/Alert'; import MobileView from '@/core/components/views/MobileView'; import DesktopView from '@/core/components/views/DesktopView'; import Menu from '@/lib/auth/components/Menu'; import * as XLSX from 'xlsx'; import getSite from '../api/listSiteApi'; import transactionsApi from '../api/transactionsApi'; import { motion } from 'framer-motion'; import Image from '@/core/components/elements/Image/Image'; import { upsertUserCart } from '~/services/cart'; import { useProductCartContext } from '@/contexts/ProductCartContext'; import { Swiper, SwiperSlide } from 'swiper/react'; import { Navigation } from 'swiper'; import 'swiper/css'; import 'swiper/css/navigation'; import { Calendar } from 'lucide-react'; import { DateRangePicker } from 'react-date-range'; import 'react-date-range/dist/styles.css'; import 'react-date-range/dist/theme/default.css'; const Transactions = ({ context = '' }) => { const auth = useAuth(); const router = useRouter(); const { q = '', page = 1, site = null, limit = 15, status = 'all', sort = 'none', startDate = null, endDate = new Date(), } = router.query; const { setRefreshCart } = useProductCartContext(); const [inputQuery, setInputQuery] = useState(q); const [cachedAllData, setCachedAllData] = useState(null); // Simpan data "All" const [currentData, setCurrentData] = useState([]); // Data yang ditampilkan const [toOthers, setToOthers] = useState(null); const [toCancel, setToCancel] = useState(null); const [listSites, setListSites] = useState([]); const [isOpen, setIsOpen] = useState(false); const [siteFilter, setSiteFilter] = useState(site); const [pageNew, setPageNew] = useState(page); const [limitNew, setLimitNew] = useState(limit); const [statusNew, setStatusNew] = useState(status); const [sortNew, setSortNew] = useState(sort); const [contextNew, setcontextNew] = useState(router.query.context || context); const [isOpenCalender, setIsOpenCalender] = useState(false); const calendarRef = useRef(null); const isUnpaid = (s) => ['belum_bayar'].includes(String(s || '').toLowerCase()); // loading id utk tombol lanjutkan transaksi const [contLoadingId, setContLoadingId] = useState(null); const parseDate = (date) => { if (!date || date === 'null') return null; if (date instanceof Date) return date; const [day, month, year] = date.split('/').map(Number); return new Date(year, month - 1, day); }; const [state, setState] = useState([ { startDate: startDate != null || 'null' ? parseDate(startDate) : null, endDate: startDate == null ? endDate : parseDate(endDate), key: 'selection', }, ]); const query = { name: q, offset: (pageNew - 1) * limitNew, context: contextNew, limit: limitNew, status: statusNew, sort: sortNew, startDate: state[0].startDate ? state[0].startDate.toLocaleDateString('id-ID') : null, endDate: state[0]?.endDate?.toLocaleDateString('id-ID'), site: siteFilter || (auth?.webRole === null && auth?.site ? auth.site : null), }; const statuses = [ { id: 'all', label: 'Semua' }, { id: 'quotation', label: 'Pending Quotation' }, { id: 'belum_bayar', label: 'Belum Bayar' }, { id: 'diproses', label: 'Pesanan Diproses' }, { id: 'dikemas', label: 'Pesanan Dikemas' }, { id: 'partial', label: 'Dikirim Sebagian' }, { id: 'dikirim', label: 'Pesanan Dikirim' }, { id: 'selesai', label: 'Pesanan Selesai' }, { id: 'cancel', label: 'Pesanan Dibatalkan' }, ]; const contextLabelMap = { draft: 'Pending Quotation', waiting: 'Pesanan Diproses', sale: 'Pesanan Dikemas', partial_shipping: 'Dikirim Sebagian', shipping: 'Pesanan Dikirim', done: 'Pesanan Selesai', cancel: 'Pesanan Dibatalkan', belum_bayar: 'Belum Bayar', }; const sortes = [ { id: 'none', label: 'Urutkan' }, { id: 'asc', label: 'dari yang terkecil' }, { id: 'desc', label: 'dari yang terbesar' }, ]; const { transactions } = useTransactions({ query }); const fetchSite = async () => { const site = await getSite(); setListSites(site.sites); }; const submitCancelTransaction = async () => { const isCancelled = await cancelTransactionApi({ transaction: toCancel }); if (isCancelled) { toast.success('Berhasil batalkan transaksi'); transactions.refetch(); } setToCancel(null); }; const pageCount = Math.ceil( (transactions?.data?.saleOrderTotal || 0) / (limitNew || 1) ); const handleSubmit = (e) => { e.preventDefault(); const queryParams = {}; if (inputQuery) queryParams.q = inputQuery; if (siteFilter) queryParams.site = siteFilter; router.push({ pathname: router.pathname, query: queryParams }); }; const handleSiteFilterChange = (e) => { setSiteFilter(e.target.value); const queryParams = {}; if (inputQuery) queryParams.q = inputQuery; if (e.target.value) queryParams.site = e.target.value; router.push({ pathname: router.pathname, query: queryParams }); }; const exportToExcel = (data, siteFilter) => { const fieldsToExport = [ 'No. Transaksi', 'No. PO', 'Tanggal', 'Created By', 'Salesperson', 'Total', 'Status', ]; const rowsToExport = []; (data || []).forEach((saleOrder) => { const row = { 'No. Transaksi': saleOrder.name, 'No. PO': saleOrder.purchaseOrderName || '-', Tanggal: saleOrder.dateOrder || '-', 'Created By': saleOrder.address?.customer?.name || '-', Salesperson: saleOrder.sales, Total: currencyFormat(saleOrder.amountTotal), Status: contextLabelMap[saleOrder.status] || saleOrder.status, }; if (siteFilter) row['Site'] = siteFilter; rowsToExport.push(row); }); const worksheet = XLSX.utils.json_to_sheet(rowsToExport, { header: fieldsToExport, }); const workbook = XLSX.utils.book_new(); XLSX.utils.book_append_sheet(workbook, worksheet, 'Sheet1'); XLSX.writeFile(workbook, 'transactions.xlsx'); }; const getAllData = async () => { const qobj = { name: q, offset: (pageNew - 1) * limitNew, limit: limitNew, context: contextNew[statusNew] || 'all', sort: sortNew, startDate: state[0]?.startDate ? state[0].startDate.toLocaleDateString('id-ID') : null, endDate: state[0]?.endDate ? state[0].endDate.toLocaleDateString('id-ID') : null, site: siteFilter || (auth?.webRole === null && auth?.site ? auth.site : null), }; const queryString = toQuery(qobj); const data = await transactionsApi({ query: queryString }); return data; }; const handleExportCSV = async () => { const dataToExport = await getAllData(); const fieldsToExport = [ 'No. Transaksi', 'No. PO', 'Tanggal', 'Created By', 'Salesperson', 'Total', 'Status', ]; const rowsToExport = dataToExport?.saleOrders?.map((saleOrder) => { const row = [ saleOrder.name, saleOrder.purchaseOrderName || '-', saleOrder.dateOrder || '-', saleOrder.address?.customer?.name || '-', saleOrder.sales, currencyFormat(saleOrder.amountTotal), (contextLabelMap[saleOrder.status] || saleOrder.status || '').replace( /,/g, ' ' ), ]; if (siteFilter) row.push((siteFilter || '').replace(/,/g, ' ')); return row.join(','); }) || []; const csvContent = 'data:text/csv;charset=utf-8,' + [fieldsToExport.join(','), ...rowsToExport].join('\n'); const encodedUri = encodeURI(csvContent); const link = document.createElement('a'); link.setAttribute('href', encodedUri); link.setAttribute('download', 'transactions.csv'); document.body.appendChild(link); link.click(); document.body.removeChild(link); }; const handleExportExcel = async () => { const dataToExport = await getAllData(); exportToExcel(dataToExport?.saleOrders, siteFilter); }; const handleDownload = (format) => { if (format === 'csv') handleExportCSV(); else if (format === 'xlsx') handleExportExcel(); setIsOpen(false); }; useEffect(() => { const handleClickOutside = (event) => { if (calendarRef.current && !calendarRef.current.contains(event.target)) { setIsOpenCalender(false); } }; document.addEventListener('mousedown', handleClickOutside); return () => document.removeEventListener('mousedown', handleClickOutside); }, []); const startItem = 1 + (pageNew - 1) * limitNew; const endItem = Math.min( limitNew * pageNew, transactions?.data?.saleOrderTotal || 0 ); useEffect(() => { fetchSite(); }, []); const handleBuyBack = async (products) => { try { const results = await Promise.all( (products || []).map((product) => upsertUserCart({ userId: auth.id, type: 'product', id: product.id, qty: product.quantity, selected: true, source: 'add_to_cart', qtyAppend: true, }).catch((error) => ({ error, product })) ) ); const failed = results.filter((r) => r && r.error); if (failed.length > 0) { toast.error(`${failed.length} produk gagal ditambahkan ke keranjang`); if (failed.length < (products || []).length) { toast.success( `${ (products || []).length - failed.length } produk berhasil ditambahkan` ); setRefreshCart(true); router.push('/shop/cart'); } return; } setRefreshCart(true); toast.success('Semua produk berhasil ditambahkan ke keranjang'); router.push('/shop/cart'); } catch { toast.error('Terjadi kesalahan saat menambahkan produk ke keranjang'); } }; const handleStatusChange = async (status) => { setStatusNew(status); setPageNew(1); if (status === 'all' && cachedAllData) { setCurrentData(cachedAllData); return; } const data = await fetchSite(status, 1); if (status === 'all') { setCachedAllData(data); } setCurrentData(data); }; const handleReset = () => { setState([{ startDate: null, endDate: new Date(), key: 'selection' }]); setIsOpenCalender(false); router.push(`${router.pathname}`); }; const formatDate = (dateString) => { const months = [ 'Januari', 'Februari', 'Maret', 'April', 'Mei', 'Juni', 'Juli', 'Agustus', 'September', 'Oktober', 'November', 'Desember', ]; const [day, month, year] = (dateString || '').split('/'); if (!day || !month || !year) return dateString || '-'; return `${day} ${months[parseInt(month, 10) - 1]} ${year}`; }; // ==== Lanjutkan Transaksi (tanpa endpoint baru) ==== const handleContinuePayment = async (saleOrder) => { try { setContLoadingId(saleOrder.id); const base = (process.env.NEXT_PUBLIC_ODOO_API_HOST || '').replace( /\/$/, '' ); const token = auth?.token; const partnerId = auth?.partnerId; // 1. TRIGGER GENERATE + GET URL const { data: response } = await axios.get( `${base}/api/v1/partner/${partnerId}/sale_order/${saleOrder.id}`, { params: { ensure_payment_link: 1, ts: Date.now() }, headers: { Token: token, 'Cache-Control': 'no-cache' }, timeout: 10000, } ); // 2. EKSTRAK URL let paymentUrl = response?.result?.payment_summary?.redirect_url || response?.data?.result?.payment_summary?.redirect_url; // 3. JIKA DAPAT URL, BUKA if (paymentUrl) { window.location.href = paymentUrl; toast.success('Membuka halaman pembayaran…'); return; } // 4. FALLBACK: COBA TANPA ensure_payment_link try { const { data: fallbackResponse } = await axios.get( `${base}/api/v1/partner/${partnerId}/sale_order/${saleOrder.id}`, { headers: { Token: token }, timeout: 5000 } ); const fallbackUrl = fallbackResponse?.result?.payment_summary?.redirect_url || fallbackResponse?.data?.result?.payment_summary?.redirect_url; if (fallbackUrl) { window.location.href = fallbackUrl; toast.success('Membuka halaman pembayaran…'); return; } } catch (fallbackError) { // Continue to next fallback } // 5. ULTIMATE FALLBACK: PAKAI URL DARI DATA LAMA const existingUrl = saleOrder?.paymentSummary?.redirectUrl || saleOrder?.payment_summary?.redirect_url; if (existingUrl) { window.open(existingUrl, '_blank', 'noopener,noreferrer'); toast.success('Membuka halaman pembayaran…'); } else { toast.error('Link pembayaran tidak ditemukan. Silakan coba lagi.'); } } catch (error) { toast.error( error.response?.data?.description || 'Gagal memproses pembayaran' ); } finally { setContLoadingId(null); } }; return ( <> {/* ===== MOBILE ===== */}
{isOpenCalender && (
setState([item.selection])} showSelectionPreview={false} maxDate={new Date()} moveRangeOnFirstSelection={false} months={1} ranges={state} className='w-full' />
)}
{transactions.isLoading && (
)} {!transactions.isLoading && transactions.data?.saleOrders?.length === 0 && ( Tidak ada transaksi )} {transactions.data?.saleOrders?.map((saleOrder, index) => (

{saleOrder.name}

{formatDate(saleOrder.dateOrder.split(' ')[0]) || '-'}
setToOthers(saleOrder)} />
{saleOrder.products[0]?.name}

Nomor PO: {saleOrder.purchaseOrderName || '-'}

{saleOrder.products[0]?.parent?.name}

{saleOrder.products[0]?.quantity} x{' '} {currencyFormat( saleOrder.products[0]?.price?.priceDiscount )}

{saleOrder.products?.length > 1 && (
{saleOrder.products .slice(1, 4) .map((product, idx) => ( {product?.name} ))} {saleOrder.products.length > 4 ? ( +{saleOrder.products.length - 4} lihat semua produk ) : ( Lihat semua produk )}
)}
Pesanan dibuat oleh:

{saleOrder.address.customer?.name || '-'}

Total Harga

{currencyFormat(saleOrder.amountTotal)}

{/* Beli Lagi hanya muncul jika status bukan unpaid */} {!isUnpaid(saleOrder.status) && ( )} {/* Bayar Sekarang hanya kalau eligible */} {saleOrder?.eligibleContinue && ( )}
))} setToOthers(null)} > {transactions.data?.status === 'draft' && ( <> )}
setToCancel(null)} title='Batalkan Transaksi' >
Apakah anda yakin membatalkan transaksi{' '} {toCancel?.name}?
{/* ===== DESKTOP ===== */}

Daftar Transaksi{' '} {transactions?.data?.saleOrders ? `(${transactions?.data?.saleOrderTotal})` : ''}

{isOpen && ( )}
Status
{statuses.map((status) => ( ))}
{listSites?.length > 0 ? ( ) : (
)}
setInputQuery(e.target.value)} />

Menampilkan {startItem}- {endItem ? endItem : transactions?.data?.saleOrderTotal ? transactions?.data?.saleOrderTotal : limitNew * pageNew}{' '} dari{' '} {transactions?.data?.saleOrderTotal ? transactions?.data?.saleOrderTotal : limitNew * pageNew}

{isOpenCalender && (
setState([item.selection])} showSelectionPreview={false} maxDate={new Date()} moveRangeOnFirstSelection={false} months={1} ranges={state} className='w-full' />
)}
{!transactions.isLoading && transactions?.data?.saleOrders?.length == 0 && (

Tidak Ada Transaksi

)} {transactions.isLoading && (
)} {!transactions.isLoading && transactions && transactions.data?.saleOrders?.length > 0 && (
{transactions.data.saleOrders.map((saleOrder, index) => (

{saleOrder.name}

Salesperson:{' '} {saleOrder.sales}

Tanggal Pesanan:{' '} {formatDate( saleOrder.dateOrder.split(' ')[0] ) || '-'}

{saleOrder.products[0]?.name}

Nomor PO: {saleOrder.purchaseOrderName || '-'}

{saleOrder.products[0]?.parent?.name}

{saleOrder.products[0]?.quantity} x{' '} {currencyFormat( saleOrder.products[0]?.price ?.priceDiscount )}

{saleOrder.products?.length > 1 && (
{saleOrder.products .slice(1, 4) .map((product, idx) => ( {product?.name} ))} {saleOrder.products.length > 4 ? ( +{saleOrder.products.length - 4}{' '} lihat semua produk ) : ( Lihat semua produk )}
)}
Pesanan dibuat oleh:

{saleOrder.address.customer?.name || '-'}

Total Harga

{currencyFormat(saleOrder.amountTotal)}

{!isUnpaid(saleOrder.status) && ( )} {/* Bayar Sekarang: hanya kalau eligible */} {saleOrder?.eligibleContinue && ( )}
))}
)}
); }; export default Transactions;