diff --git a/src/app/closing/detail/page.tsx b/src/app/closing/detail/page.tsx index 1b4ebc45..62f3fa20 100644 --- a/src/app/closing/detail/page.tsx +++ b/src/app/closing/detail/page.tsx @@ -24,6 +24,11 @@ const ClosingDetailPage = () => { () => ClosingApi.getPenjualan(Number(closingId)) ); + const { data: hppEkspedisiData, isLoading: isLoadingHppEkspedisi } = useSWR( + closingId ? `hpp-ekspedisi-${closingId}` : null, + () => ClosingApi.getHppEkspedisi(Number(closingId)) + ); + if (!closingId) { router.back(); @@ -39,7 +44,7 @@ const ClosingDetailPage = () => { return; } - const isLoading = isLoadingClosing || isLoadingSales; + const isLoading = isLoadingClosing || isLoadingSales || isLoadingHppEkspedisi; return (
@@ -50,6 +55,11 @@ const ClosingDetailPage = () => { id={Number(closingId)} initialValue={closing.data} salesData={isResponseSuccess(salesData) ? salesData.data : undefined} + hppExpeditionData={ + isResponseSuccess(hppEkspedisiData) + ? hppEkspedisiData.data + : undefined + } /> )}
diff --git a/src/components/Dropdown.tsx b/src/components/Dropdown.tsx new file mode 100644 index 00000000..5bfa7a7d --- /dev/null +++ b/src/components/Dropdown.tsx @@ -0,0 +1,114 @@ +import React, { ReactNode, useState, useRef } from 'react'; + +import { cn } from '@/lib/helper'; + +export interface DropdownProps { + trigger: ReactNode; + children: ReactNode; + className?: { + wrapper?: string; + trigger?: string; + content?: string; + }; + align?: 'start' | 'center' | 'end'; + direction?: 'top' | 'bottom' | 'left' | 'right'; + hover?: boolean; + defaultOpen?: boolean; + open?: boolean; + close?: boolean; + controlled?: boolean; +} + +const Dropdown = ({ + trigger, + children, + className, + align, + direction, + hover, + defaultOpen = false, + open, + close, + controlled = false, +}: DropdownProps) => { + const [isOpen, setIsOpen] = useState(defaultOpen); + const dropdownRef = useRef(null); + + const toggleDropdown = () => { + if (!controlled) { + const newState = !isOpen; + setIsOpen(newState); + } + }; + + const getWrapperClasses = () => { + const openState = controlled ? open : isOpen; + + return cn( + 'dropdown', + { + 'dropdown-start': align === 'start', + 'dropdown-center': align === 'center', + 'dropdown-end': align === 'end', + 'dropdown-top': direction === 'top', + 'dropdown-bottom': direction === 'bottom', + 'dropdown-left': direction === 'left', + 'dropdown-right': direction === 'right', + 'dropdown-hover': hover, + 'dropdown-open': openState && !close, + 'dropdown-close': close, + }, + className?.wrapper + ); + }; + + const getTriggerClasses = () => { + return cn(className?.trigger); + }; + + const getContentClasses = () => { + return cn( + 'dropdown-content z-[9999] shadow-sm bg-base-100 rounded-box', + className?.content + ); + }; + + if (controlled) { + return ( +
+ {trigger} + {open && !close && ( +
+ {children} +
+ )} +
+ ); + } + + return ( +
+
{ + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + toggleDropdown(); + } + }} + > + {trigger} +
+ {!close && ( +
+ {children} +
+ )} +
+ ); +}; + +export default Dropdown; diff --git a/src/components/Navbar.tsx b/src/components/Navbar.tsx index bee92a57..280217a0 100644 --- a/src/components/Navbar.tsx +++ b/src/components/Navbar.tsx @@ -7,7 +7,7 @@ import { Icon } from '@iconify/react'; import Menu from '@/components/menu/Menu'; import MenuItem from '@/components/menu/MenuItem'; import Button from '@/components/Button'; -import Dropdown from '@/components/dropdown/Dropdown'; +import Dropdown from '@/components/Dropdown'; import { useAuth } from '@/services/hooks/useAuth'; import { AuthApi } from '@/services/api/auth'; @@ -54,7 +54,8 @@ const Navbar = ({ title, toggleSidebar }: NavbarProps) => {
@@ -62,9 +63,11 @@ const Navbar = ({ title, toggleSidebar }: NavbarProps) => {
} - contentClassName='w-52 mt-3' + className={{ + content: 'w-52 mt-3', + }} > - + diff --git a/src/components/dropdown/Dropdown.tsx b/src/components/dropdown/Dropdown.tsx deleted file mode 100644 index 4489231d..00000000 --- a/src/components/dropdown/Dropdown.tsx +++ /dev/null @@ -1,116 +0,0 @@ -'use client'; - -import { ReactNode, useRef, useEffect, useState } from 'react'; -import { cn } from '@/lib/helper'; - -interface DropdownProps { - trigger: ReactNode; - children: ReactNode; - position?: - | 'top' - | 'bottom' - | 'left' - | 'right' - | 'top-start' - | 'top-end' - | 'bottom-start' - | 'bottom-end' - | 'left-start' - | 'left-end' - | 'right-start' - | 'right-end'; - align?: 'start' | 'center' | 'end'; - hover?: boolean; - className?: string; - contentClassName?: string; -} - -const Dropdown = ({ - trigger, - children, - position = 'bottom', - align = 'start', - hover = false, - className, - contentClassName, -}: DropdownProps) => { - const [isOpen, setIsOpen] = useState(false); - const dropdownRef = useRef(null); - - // Handle click outside to close dropdown - useEffect(() => { - const handleClickOutside = (event: MouseEvent) => { - if ( - dropdownRef.current && - !dropdownRef.current.contains(event.target as Node) - ) { - setIsOpen(false); - } - }; - - if (isOpen) { - document.addEventListener('mousedown', handleClickOutside); - } - - return () => { - document.removeEventListener('mousedown', handleClickOutside); - }; - }, [isOpen]); - - // Build position classes - const getPositionClasses = () => { - const classes: string[] = []; - - // Handle combined positions like 'top-start' - if (position.includes('-')) { - const [pos, al] = position.split('-'); - classes.push(`dropdown-${pos}`); - classes.push(`dropdown-${al}`); - } else { - classes.push(`dropdown-${position}`); - if (align !== 'start') { - classes.push(`dropdown-${align}`); - } - } - - return classes.join(' '); - }; - - const handleToggle = (e: React.MouseEvent) => { - e.preventDefault(); - e.stopPropagation(); - // alert('clicked'); - setIsOpen(!isOpen); - }; - - return ( -
- {/* Trigger Button */} -
- {trigger} -
- - {/* Dropdown Content - Only render when open */} - {isOpen && ( -
setIsOpen(false)} // Close on item click - > - {children} -
- )} -
- ); -}; - -export default Dropdown; diff --git a/src/components/pages/closing/ClosingDetail.tsx b/src/components/pages/closing/ClosingDetail.tsx index fd88fa49..336047e2 100644 --- a/src/components/pages/closing/ClosingDetail.tsx +++ b/src/components/pages/closing/ClosingDetail.tsx @@ -10,22 +10,26 @@ import ClosingGeneralInformationTable from '@/components/pages/closing/ClosingGe import { ClosingGeneralInformation, BaseClosingSales, + ClosingHppExpedition, } from '@/types/api/closing'; import ClosingSapronakTabContent from './ClosingSapronakTabContent'; import ClosingSapronakCalculationTabContent from '@/components/pages/closing/ClosingSapronakCalculationTabContent'; import ClosingOverheadTabContent from '@/components/pages/closing/ClosingOverheadTabContent'; import SalesReportTable from './sale/SalesReportTable'; +import HppExpeditionReportTable from './hpp-ekspedisi/HppExpeditionReportTable'; interface ClosingDetailProps { id: number; initialValue?: ClosingGeneralInformation; salesData?: BaseClosingSales; + hppExpeditionData?: ClosingHppExpedition; } const ClosingDetail: React.FC = ({ id, initialValue, salesData, + hppExpeditionData, }) => { const [activeTab, setActiveTab] = useState('sapronak'); @@ -54,7 +58,7 @@ const ClosingDetail: React.FC = ({ { id: 'hppEkspedisi', label: 'HPP Ekspedisi', - content: 'HPP Ekspedisi', + content: , }, { id: 'dataProduksi', diff --git a/src/components/pages/closing/hpp-ekspedisi/HppExpeditionReportTable.tsx b/src/components/pages/closing/hpp-ekspedisi/HppExpeditionReportTable.tsx new file mode 100644 index 00000000..f683ec58 --- /dev/null +++ b/src/components/pages/closing/hpp-ekspedisi/HppExpeditionReportTable.tsx @@ -0,0 +1,110 @@ +'use client'; + +import React, { useMemo } from 'react'; +import { ColumnDef } from '@tanstack/react-table'; +import Table from '@/components/Table'; +import Card from '@/components/Card'; +import { formatCurrency } from '@/lib/helper'; +import { BaseHppExpedition, BaseExpeditionCost } from '@/types/api/closing'; + +interface HppExpeditionReportTableProps { + type?: 'detail'; + initialValues?: BaseHppExpedition; +} + +const HppExpeditionReportTable = ({ + type = 'detail', + initialValues, +}: HppExpeditionReportTableProps) => { + const costOfRevenueExpeditionData: BaseExpeditionCost[] = useMemo(() => { + return initialValues?.expedition_costs || []; + }, [initialValues]); + + const totals = useMemo(() => { + const totalHpp = initialValues?.total_hpp_amount || 0; + + return { + totalHpp, + }; + }, [initialValues]); + + const costOfRevenueExpeditionColumns: ColumnDef[] = + useMemo( + () => [ + { + id: 'id', + accessorKey: 'id', + header: 'No', + cell: (props) => { + return
{props.row.index + 1}
; + }, + footer: () => ( +
+ Total HPP Ekspedisi +
+ ), + }, + { + id: 'expedition_vendor_name', + accessorKey: 'expedition_vendor_name', + header: 'Nama Ekspedisi', + cell: (props) => props.getValue() || '-', + }, + { + id: 'hpp_amount', + accessorKey: 'hpp_amount', + header: 'HPP Ekspedisi', + cell: (props) => { + const value = props.getValue() as number; + return
{formatCurrency(value)}
; + }, + footer: () => ( +
+ {formatCurrency(totals.totalHpp)} +
+ ), + }, + ], + [totals] + ); + + return ( + <> +
+
+

HPP Ekspedisi

+ + 0} + className={{ + tableWrapperClassName: 'overflow-x-auto', + tableClassName: 'w-full table-auto text-sm', + headerRowClassName: 'border-b border-b-gray-200', + headerColumnClassName: + 'px-4 py-3 text-xs font-semibold text-gray-500 last:flex last:flex-row last:justify-end whitespace-nowrap', + bodyRowClassName: + 'hover:bg-gray-50 transition-colors border-b border-l border-r border-b-gray-200 border-l-gray-200 border-r-gray-200', + bodyColumnClassName: + 'px-4 py-3 text-xs text-gray-900 whitespace-nowrap', + tableFooterClassName: + 'bg-gray-100 font-semibold border border-gray-200', + footerRowClassName: 'border-t-2 border-gray-300', + footerColumnClassName: + 'px-4 py-3 text-xs text-gray-900 whitespace-nowrap', + }} + /> + + + + + ); +}; + +export default HppExpeditionReportTable; diff --git a/src/services/api/closing.ts b/src/services/api/closing.ts index 0a6ef40c..e0b0bf89 100644 --- a/src/services/api/closing.ts +++ b/src/services/api/closing.ts @@ -8,7 +8,9 @@ import { ClosingOutgoingSapronak, ClosingOverhead, ClosingSapronakCalculation, + ClosingHppExpedition, } from '@/types/api/closing'; +import { httpClient, httpClientFetcher } from '@/services/http/client'; import { BaseApiResponse } from '@/types/api/api-general'; import { dummyGetAllFetcher, @@ -19,7 +21,6 @@ import { dummyGetPerhitunganSapronak, dummyGetOverhead, } from '@/dummy/closing.dummy'; -import { httpClient, httpClientFetcher } from '@/services/http/client'; import { ClosingSales } from '@/types/api/closing'; export class ClosingApiService extends BaseApiService { @@ -194,6 +195,24 @@ export class ClosingApiService extends BaseApiService { return undefined; } } + + async getHppEkspedisi( + id: number + ): Promise | undefined> { + try { + const getHppEkspedisiPath = `${this.basePath}/${id}/expedition-hpp`; + const getHppEkspedisiRes = + await httpClient>( + getHppEkspedisiPath + ); + return getHppEkspedisiRes; + } catch (error) { + if (axios.isAxiosError>(error)) { + return error.response?.data; + } + return undefined; + } + } } export const ClosingApi = new ClosingApiService('/closings'); diff --git a/src/types/api/closing.d.ts b/src/types/api/closing.d.ts index baf4c7aa..bc3cb0bc 100644 --- a/src/types/api/closing.d.ts +++ b/src/types/api/closing.d.ts @@ -78,6 +78,7 @@ export type ClosingIncomingSapronak = { }; export type ClosingOutgoingSapronak = ClosingIncomingSapronak; +export type ClosingSales = BaseMetadata & BaseClosingSales; // ====== PERHITUNGAN SAPRONAK ====== @@ -141,4 +142,16 @@ export type OverheadTotal = { actual_total_amount: number; cost_per_bird: number; }; -export type ClosingSales = BaseMetadata & BaseClosingSales; + +export type BaseExpeditionCost = { + id: number; + expedition_vendor_name: string; + hpp_amount: number; +}; + +export type BaseHppExpedition = { + expedition_costs: BaseExpeditionCost[]; + total_hpp_amount: number; +}; + +export type ClosingHppExpedition = BaseMetadata & BaseHppExpedition;