mirror of
https://gitlab.com/mbugroup/lti-web-client.git
synced 2026-05-20 05:22:02 +00:00
fix(FE): adjust ui debt supplier pixel perfect figma
This commit is contained in:
@@ -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
@@ -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 && (
|
||||
|
||||
@@ -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',
|
||||
}
|
||||
)
|
||||
);
|
||||
Reference in New Issue
Block a user