fix(FE): adjust ui debt supplier pixel perfect figma

This commit is contained in:
randy-ar
2026-01-29 18:24:43 +07:00
parent 49e843f3b2
commit c7818cefbb
7 changed files with 214 additions and 84 deletions
+5
View File
@@ -123,6 +123,10 @@ const Card = ({
return cn(baseClasses, 'p-6', className?.body);
};
const getCollapsibleClasses = () => {
return cn('', className?.collapsible);
};
const getTitleClasses = () => {
const sizeClasses = {
sm: 'text-lg',
@@ -213,6 +217,7 @@ const Card = ({
titleClassName='w-full cursor-pointer'
contentClassName='p-0'
fullWidth={true}
className={getCollapsibleClasses()}
>
{cardContent}
</Collapse>
+23 -12
View File
@@ -25,8 +25,10 @@ export interface TabsProps
wrapper?: string;
tab?: string;
content?: string;
tabHeaderWrapper?: string;
};
onTabChange?: (tabId: string) => void;
sideContent?: ReactNode;
}
const Tabs = ({
@@ -38,6 +40,7 @@ const Tabs = ({
activeTabId: controlledActiveId,
className,
onTabChange,
sideContent,
...props
}: TabsProps) => {
// State internal hanya dipakai kalau `activeTabId` (controlled) tidak diset
@@ -59,6 +62,7 @@ const Tabs = ({
wrapper: wrapperClassName,
tab: tabClassName,
content: contentClassName,
tabHeaderWrapper: tabHeaderWrapperClassName,
} = typeof className === 'object'
? className
: { wrapper: className, tab: undefined };
@@ -102,6 +106,10 @@ const Tabs = ({
tabClassName
);
const getSideContentClasses = () => {
return cn('flex flex-row', tabHeaderWrapperClassName);
};
const activeContent = tabs.find((tab) => tab.id === activeTabId)?.content;
return (
@@ -112,18 +120,21 @@ const Tabs = ({
typeof className === 'string' ? className : containerClassName
)}
>
<div role='tablist' className={getTabsClasses()}>
{tabs.map(({ id, label, disabled }) => (
<button
key={id}
role='tab'
className={getTabClasses(id === activeTabId, disabled)}
onClick={() => !disabled && handleTabChange(id)}
disabled={disabled}
>
{label}
</button>
))}
<div className={getSideContentClasses()}>
<div role='tablist' className={getTabsClasses()}>
{tabs.map(({ id, label, disabled }) => (
<button
key={id}
role='tab'
className={getTabClasses(id === activeTabId, disabled)}
onClick={() => !disabled && handleTabChange(id)}
disabled={disabled}
>
{label}
</button>
))}
</div>
{sideContent && sideContent}
</div>
{activeContent && (
+4 -4
View File
@@ -19,11 +19,11 @@ const ButtonFilter = ({ values, onClick, ...props }: ButtonFilterProps) => {
variant='outline'
color='none'
className={cn(
'padding-[12px] rounded-[8px] max-h-[40px] font-semibold text-[14px] gap-[6px]',
'rounded-lg max-h-10 font-semibold text-sm gap-1.5',
'text-sm text-base-content/50 border border-base-content/10 shadow-button-soft',
getFilledFormikValuesCount(values) > 0
? 'border-primary-gradient !rounded-[8px]'
: '!rounded-[8px]',
? 'border-primary-gradient text-primary rounded-lg!'
: 'rounded-lg',
props.className
)}
>
@@ -37,7 +37,7 @@ const ButtonFilter = ({ values, onClick, ...props }: ButtonFilterProps) => {
/>
Filter
{getFilledFormikValuesCount(values) > 0 && (
<span className='w-[20px] h-[20px] text-white bg-[#FF3535] rounded-[8px] border-[1px] border-base-300 flex items-center justify-center text-xs'>
<span className='w-5 h-5 text-white bg-[#FF3535] rounded-lg border border-base-300 flex items-center justify-center text-xs'>
{getFilledFormikValuesCount(values)}
</span>
)}
@@ -226,7 +226,7 @@ const DashboardProduction = () => {
variant='outline'
color='none'
className={cn(
'p-2 rounded-lg font-semibold text-sm gap-1.5',
'rounded-lg font-semibold text-sm gap-1.5',
'text-sm text-base-content/50 border border-base-content/10 shadow-button-soft'
)}
>
@@ -1,28 +1,43 @@
'use client';
import { useState } from 'react';
import Tabs from '@/components/Tabs';
import CustomerPaymentTab from '@/components/pages/report/finance/tab/CustomerPaymentTab';
import DebtSupplierTab from '@/components/pages/report/finance/tab/DebtSupplierTab';
import { useFinanceTabStore } from '@/stores/finance-tab/finance-tab.store';
const FinanceTabs = () => {
const [activeTabId, setActiveTabId] = useState<string>('1');
const tabActions = useFinanceTabStore((state) => state.tabActions);
const tabs = [
{
id: '1',
label: 'Rekapitulasi Hutang Ke Supplier',
content: <DebtSupplierTab />,
content: <DebtSupplierTab tabId={'1'} />,
},
{
id: '2',
label: 'Kontrol Pembayaran Customer',
content: <CustomerPaymentTab />,
},
];
return (
<section className='w-full p-4'>
<Tabs tabs={tabs} variant='lifted' />
<section className='w-full'>
<Tabs
tabs={tabs}
variant='boxed'
activeTabId={activeTabId}
onTabChange={setActiveTabId}
className={{
tabHeaderWrapper:
'justify-between items-center p-3 border-b border-base-content/10',
tab: 'w-fit',
content: 'p-0 m-0',
}}
sideContent={tabActions[activeTabId] || null}
/>
</section>
);
};
@@ -22,7 +22,7 @@ import { generateDebtSupplierExcel } from '@/components/pages/report/finance/exp
import { generateDebtSupplierPDF } from '@/components/pages/report/finance/export/DebtSupllierExportPDF';
import { Icon } from '@iconify/react';
import { ColumnDef } from '@tanstack/react-table';
import { useCallback, useMemo, useState } from 'react';
import { useCallback, useEffect, useMemo, useState } from 'react';
import toast from 'react-hot-toast';
import useSWR from 'swr';
import { DebtSupplierApi } from '@/services/api/report/debt-supplier';
@@ -37,6 +37,7 @@ import { Color } from '@/types/theme';
import { Supplier } from '@/types/api/master-data/supplier';
import SelectInputCheckbox from '@/components/input/SelectInputCheckbox';
import SelectInputRadio from '@/components/input/SelectInputRadio';
import { useFinanceTabStore } from '@/stores/finance-tab/finance-tab.store';
const dueStatus: Record<string, Color> = {
'Sudah Jatuh Tempo': 'error',
@@ -75,7 +76,11 @@ const getPillBadge = (
);
};
const DebtSupplierTab = () => {
interface DebtSupplierTabProps {
tabId: string;
}
const DebtSupplierTab = ({ tabId }: DebtSupplierTabProps) => {
// ===== STATE MANAGEMENT =====
const [isPdfExportLoading, setIsPdfExportLoading] = useState(false);
const [isExcelExportLoading, setIsExcelExportLoading] = useState(false);
@@ -271,6 +276,77 @@ const DebtSupplierTab = () => {
}
}, [debtSupplierExport]);
// ===== REGISTER TAB ACTIONS TO STORE =====
const setTabActions = useFinanceTabStore((state) => state.setTabActions);
const clearTabActions = useFinanceTabStore((state) => state.clearTabActions);
useEffect(() => {
setTabActions(
tabId,
<div className='flex flex-row gap-3 '>
<ButtonFilter
values={formik.values}
onClick={handleFilterModalOpen}
variant='outline'
className='px-3 py-2.5'
/>
<Dropdown
trigger={
<Button
variant='outline'
color='none'
isLoading={isAnyExportLoading}
className={cn(
'px-3 py-2.5',
'rounded-lg font-semibold text-sm gap-1.5',
'text-sm text-base-content/50 border border-base-content/10 shadow-button-soft'
)}
>
<Icon icon='heroicons:cloud-arrow-down' width={20} height={20} />
Export
<div className='w-6.5 h-5 flex items-center justify-center border-l border-base-content/10'>
<Icon width={14} height={14} icon='heroicons:chevron-down' />
</div>
</Button>
}
align='end'
className={{
content:
'mt-1 p-0 w-full shadow-button-soft border border-base-content/10 rounded-lg',
}}
>
<Menu className='p-0 w-full'>
<MenuItem
className='text-sm p-3'
title='Excel'
onClick={handleExportExcel}
/>
<MenuItem
className='text-sm p-3'
title='PDF'
onClick={handleExportPdf}
/>
</Menu>
</Dropdown>
</div>
);
}, [
tabId,
formik.values,
isAnyExportLoading,
handleExportExcel,
handleExportPdf,
setTabActions,
]);
// Cleanup on unmount
useEffect(() => {
return () => {
clearTabActions(tabId);
};
}, [tabId, clearTabActions]);
const getTableColumns = (supplier: DebtSupplier): ColumnDef<DebtRow>[] => [
{
id: 'no',
@@ -478,41 +554,9 @@ const DebtSupplierTab = () => {
];
return (
<>
<div className='w-full p-0 sm:p-4 flex flex-col gap-4'>
<Card
subtitle='Laporan > Rekapitulasi Hutang ke Supplier'
className={{ wrapper: 'w-full', body: 'p-1!' }}
>
<div className='mb-4 flex justify-end gap-2 [&_button]:px-4'>
<ButtonFilter
values={formik.values}
onClick={handleFilterModalOpen}
variant='outline'
/>
<Dropdown
trigger={
<Button variant='outline' isLoading={isAnyExportLoading}>
<Icon
icon='heroicons:cloud-arrow-down'
width={18}
height={18}
/>
Export
</Button>
}
align='end'
>
<Menu>
<MenuItem title='Excel' onClick={handleExportExcel} />
<MenuItem title='PDF' onClick={handleExportPdf} />
</Menu>
</Dropdown>
</div>
</Card>
<div className='w-full p-0 sm:p-3 flex flex-col gap-3'>
{!isSubmitted ? (
<div className='mt-6 text-center text-gray-500'>
<div className='mt-6 text-center text-base-content/50'>
Silakan klik tombol Filter untuk mengatur filter dan menampilkan
data.
</div>
@@ -521,7 +565,7 @@ const DebtSupplierTab = () => {
<span className='loading loading-spinner loading-xl' />
</div>
) : data.length === 0 ? (
<div className='mt-6 text-center text-gray-500'>
<div className='mt-6 text-center text-base-content/50'>
Tidak ada data yang dapat ditampilkan...
</div>
) : (
@@ -531,10 +575,11 @@ const DebtSupplierTab = () => {
key={supplierReport.supplier.id}
title={supplierReport.supplier.name}
className={{
wrapper: 'w-full !rounded-lg',
body: 'p-0 rounded-lg',
wrapper: 'w-full rounded-lg border-none',
body: 'p-0',
title:
'ps-2 pt-1 pb-1 font-normal text-md bg-primary text-white',
'px-2 py-1.5 font-normal text-sm bg-primary text-white',
collapsible: 'rounded-lg',
}}
variant='bordered'
collapsible={true}
@@ -551,8 +596,9 @@ const DebtSupplierTab = () => {
pageSize={supplierReport.rows.length + 1}
renderFooter={supplierReport.rows.length > 0}
className={{
containerClassName: 'w-full',
tableWrapperClassName: 'overflow-x-auto',
containerClassName: 'w-full mb-0',
tableWrapperClassName:
'overflow-x-auto rounded-tr-none rounded-tl-none',
headerColumnClassName: cn(
TABLE_DEFAULT_STYLING.headerColumnClassName,
'whitespace-nowrap'
@@ -617,33 +663,34 @@ const DebtSupplierTab = () => {
ref={filterModal.ref}
className={{
modal: 'p-0',
modalBox: 'p-0 rounded-2xl xl:max-w-4/12 max-w-sm',
modalBox: 'p-0 rounded-[0.875rem] xl:max-w-4/12 max-w-sm',
}}
>
<form
className='space-y-6'
onSubmit={formik.handleSubmit}
onReset={formik.handleReset}
>
<form onSubmit={formik.handleSubmit} onReset={formik.handleReset}>
{/* Modal Header */}
<div className='flex items-center justify-between gap-2 py-3 border-b border-gray-300 px-4'>
<div className='flex items-center justify-between gap-2 border-b border-base-content/10 p-4'>
<div className='flex items-center gap-2 text-primary'>
<Icon icon='heroicons:funnel' width={20} height={20} />
<h3 className='font-semibold'>Filter Data</h3>
<h3 className='font-medium text-sm'>Filter Data</h3>
</div>
<Button
variant='link'
type='button'
onClick={filterModal.closeModal}
className='text-gray-500 hover:text-gray-700 transition-colors cursor-pointer'
className='text-base-content/50 hover:text-base-content transition-colors cursor-pointer'
>
<Icon icon='heroicons:x-mark' width={20} height={20} />
</Button>
</div>
<div className='space-y-4 px-4'>
<div className='grid grid-cols-1 sm:grid-cols-2 sm:gap-4'>
<div>
{/* Modal Body */}
<div className='p-4 flex flex-col gap-1.5'>
<div>
<label className='block text-xs font-semibold text-base-content py-2'>
Tanggal
</label>
<div className='flex flex-row gap-1.5 items-center justify-between'>
<DateInput
label='Tanggal'
name='startDate'
value={formik.values.startDate || ''}
onChange={(e) => {
@@ -655,11 +702,8 @@ const DebtSupplierTab = () => {
}
errorMessage={formik.errors.startDate}
/>
</div>
<div className='mt-auto'>
<hr className='w-full max-w-3 h-px border-base-content/10'></hr>
<DateInput
label=' '
name='endDate'
value={formik.values.endDate || ''}
onChange={(e) => {
@@ -730,15 +774,19 @@ const DebtSupplierTab = () => {
</div>
{/* Action Buttons */}
<div className='flex justify-between gap-4 py-4 mt-8 border-t border-gray-300 bg-gray-100'>
<div className='flex justify-between items-center gap-4 p-4 border-t border-gray-300 bg-gray-100'>
<Button
variant='soft'
className='ms-4 min-w-36 rounded-lg'
color='none'
className='rounded-lg text-base-content/65 bg-transparent border-none hover:bg-base-content/10 hover:text-base-content/65 transition-colors px-3 py-2'
type='reset'
>
Reset Filter
</Button>
<Button className='me-4 min-w-36 rounded-lg' type='submit'>
<Button
className='min-w-40 text-sm rounded-lg py-3 text-white'
type='submit'
>
Apply Filter
</Button>
</div>
@@ -0,0 +1,51 @@
'use client';
import { ReactNode } from 'react';
import { create } from 'zustand';
import { devtools } from 'zustand/middleware';
export type FinanceTabActionsSlice = {
// State - actions per tab ID
tabActions: Record<string, ReactNode>;
// Actions
setTabActions: (tabId: string, actions: ReactNode) => void;
clearTabActions: (tabId: string) => void;
clearAllTabActions: () => void;
};
export const useFinanceTabStore = create<FinanceTabActionsSlice>()(
devtools(
(set) => ({
tabActions: {},
setTabActions: (tabId, actions) =>
set(
(state) => ({
tabActions: {
...state.tabActions,
[tabId]: actions,
},
}),
false,
'setTabActions'
),
clearTabActions: (tabId) =>
set(
(state) => {
const { [tabId]: _, ...rest } = state.tabActions;
return { tabActions: rest };
},
false,
'clearTabActions'
),
clearAllTabActions: () =>
set({ tabActions: {} }, false, 'clearAllTabActions'),
}),
{
name: 'FinanceTabStore',
}
)
);