@@ -1,4 +1,4 @@ | |||||
import { serverFetchJson } from "@/app/utils/fetchUtil"; | |||||
import { serverFetchJson, serverFetchString } from "@/app/utils/fetchUtil"; | |||||
import { BASE_API_URL } from "@/config/api"; | import { BASE_API_URL } from "@/config/api"; | ||||
import { cache } from "react"; | import { cache } from "react"; | ||||
import "server-only"; | import "server-only"; | ||||
@@ -88,3 +88,9 @@ export const fetchCustomerTypes = cache(async () => { | |||||
next: { tags: ["customerTypes"] }, | next: { tags: ["customerTypes"] }, | ||||
}); | }); | ||||
}); | }); | ||||
export const getMaxCustomerCode = cache(async () => { | |||||
return serverFetchString<string>(`${BASE_API_URL}/customer/getMaxCode`, { | |||||
next: { tags: ["customerMaxCode"] }, | |||||
}); | |||||
}); |
@@ -151,10 +151,12 @@ export interface ProjectMonthlyReportFilter { | |||||
export interface LastModifiedReportFilter { | export interface LastModifiedReportFilter { | ||||
date: string; | date: string; | ||||
dayRange: number; | |||||
} | } | ||||
export interface LastModifiedReportRequest { | export interface LastModifiedReportRequest { | ||||
dateString: string; | dateString: string; | ||||
dayRange: number; | |||||
} | } | ||||
export interface ExportCurrentStaffInfoRequest { | export interface ExportCurrentStaffInfoRequest { | ||||
@@ -45,6 +45,24 @@ export interface Props extends Omit<ModalProps, "children"> { | |||||
modalSx?: SxProps; | modalSx?: SxProps; | ||||
} | } | ||||
type DatetypeOption = { | |||||
value: string; | |||||
label: string; | |||||
unit: 'week' | 'month' | 'year'; | |||||
step: number; | |||||
}; | |||||
const datetypeOptions: DatetypeOption[] = [ | |||||
{ value: "weekly", label: "Weekly", unit: "week", step: 1 }, | |||||
{ value: "biweekly", label: "Bi-Weekly", unit: "week", step: 2 }, | |||||
{ value: "monthly", label: "Monthly", unit: "month", step: 1 }, | |||||
// { value: "bimonthly", label: "Every two months from", unit: "month", step: 2 }, | |||||
{ value: "quarterly", label: "Quarterly", unit: "month", step: 3 }, | |||||
{ value: "half-year", label: "Half Year", unit: "month", step: 6 }, | |||||
// { value: "yearly", label: "Yearly from", unit: "year", step: 1 }, | |||||
{ value: "fixed", label: "Fixed", unit: "month", step: 0 }, // unit/step not used for fixed | |||||
]; | |||||
const modalSx: SxProps = { | const modalSx: SxProps = { | ||||
position: "absolute", | position: "absolute", | ||||
top: "50%", | top: "50%", | ||||
@@ -99,6 +117,9 @@ const BulkAddPaymentModal: React.FC<Props> = ({ | |||||
const amountForLastItem = truncateMoney( | const amountForLastItem = truncateMoney( | ||||
amountToDivide - dividedAmount * (numberOfEntries - 1), | amountToDivide - dividedAmount * (numberOfEntries - 1), | ||||
)!; | )!; | ||||
const datetypeOption = datetypeOptions.find(r => r.value === dateType); | |||||
return Array(numberOfEntries) | return Array(numberOfEntries) | ||||
.fill(undefined) | .fill(undefined) | ||||
.map((_, index) => { | .map((_, index) => { | ||||
@@ -106,8 +127,8 @@ const BulkAddPaymentModal: React.FC<Props> = ({ | |||||
dateType === "fixed" | dateType === "fixed" | ||||
? dateReference | ? dateReference | ||||
: dateReference.add( | : dateReference.add( | ||||
index, | |||||
dateType === "monthly" ? "month" : "week", | |||||
index * (datetypeOption?.step ?? 0), | |||||
datetypeOption?.unit ?? "month", | |||||
); | ); | ||||
return { | return { | ||||
@@ -194,9 +215,14 @@ const BulkAddPaymentModal: React.FC<Props> = ({ | |||||
{...field} | {...field} | ||||
error={Boolean(formState.errors.dateType)} | error={Boolean(formState.errors.dateType)} | ||||
> | > | ||||
<MenuItem value="monthly">{t("Monthly from")}</MenuItem> | |||||
{datetypeOptions.map(option => ( | |||||
<MenuItem key={option.value} value={option.value}> | |||||
{t(option.label)} | |||||
</MenuItem> | |||||
))} | |||||
{/* <MenuItem value="monthly">{t("Monthly from")}</MenuItem> | |||||
<MenuItem value="weekly">{t("Weekly from")}</MenuItem> | <MenuItem value="weekly">{t("Weekly from")}</MenuItem> | ||||
<MenuItem value="fixed">{t("Fixed")}</MenuItem> | |||||
<MenuItem value="fixed">{t("Fixed")}</MenuItem> */} | |||||
</Select> | </Select> | ||||
)} | )} | ||||
rules={{ | rules={{ | ||||
@@ -28,6 +28,7 @@ import { differenceBy } from "lodash"; | |||||
export interface Props { | export interface Props { | ||||
subsidiaries: Subsidiary[], | subsidiaries: Subsidiary[], | ||||
customerTypes: CustomerType[], | customerTypes: CustomerType[], | ||||
maxCustomerCode: string | |||||
} | } | ||||
const hasErrorsInTab = ( | const hasErrorsInTab = ( | ||||
@@ -45,6 +46,7 @@ const hasErrorsInTab = ( | |||||
const CustomerSave: React.FC<Props> = ({ | const CustomerSave: React.FC<Props> = ({ | ||||
subsidiaries, | subsidiaries, | ||||
customerTypes, | customerTypes, | ||||
maxCustomerCode | |||||
}) => { | }) => { | ||||
const [serverError, setServerError] = useState(""); | const [serverError, setServerError] = useState(""); | ||||
const [tabIndex, setTabIndex] = useState(0); | const [tabIndex, setTabIndex] = useState(0); | ||||
@@ -58,7 +60,7 @@ const CustomerSave: React.FC<Props> = ({ | |||||
try { | try { | ||||
const defaultCustomer = { | const defaultCustomer = { | ||||
id: null, | id: null, | ||||
code: "", | |||||
code: maxCustomerCode ?? "", | |||||
name: "", | name: "", | ||||
brNo: null, | brNo: null, | ||||
address: null, | address: null, | ||||
@@ -2,7 +2,7 @@ | |||||
// import CreateProject from "./CreateProject"; | // import CreateProject from "./CreateProject"; | ||||
// import { fetchProjectCategories } from "@/app/api/projects"; | // import { fetchProjectCategories } from "@/app/api/projects"; | ||||
// import { fetchTeamLeads } from "@/app/api/staff"; | // import { fetchTeamLeads } from "@/app/api/staff"; | ||||
import { fetchCustomerTypes, fetchAllSubsidiaries } from "@/app/api/customer"; | |||||
import { fetchCustomerTypes, fetchAllSubsidiaries, getMaxCustomerCode } from "@/app/api/customer"; | |||||
import CustomerSave from "./CustomerSave"; | import CustomerSave from "./CustomerSave"; | ||||
// type Props = { | // type Props = { | ||||
@@ -14,14 +14,15 @@ import CustomerSave from "./CustomerSave"; | |||||
const CustomerSaveWrapper: React.FC = async () => { | const CustomerSaveWrapper: React.FC = async () => { | ||||
// const { params } = props | // const { params } = props | ||||
// console.log(params) | // console.log(params) | ||||
const [subsidiaries, customerTypes] = | |||||
const [subsidiaries, customerTypes, maxCustomerCode] = | |||||
await Promise.all([ | await Promise.all([ | ||||
fetchAllSubsidiaries(), | fetchAllSubsidiaries(), | ||||
fetchCustomerTypes(), | fetchCustomerTypes(), | ||||
getMaxCustomerCode() | |||||
]); | ]); | ||||
return ( | return ( | ||||
<CustomerSave subsidiaries={subsidiaries} customerTypes={customerTypes} /> | |||||
<CustomerSave subsidiaries={subsidiaries} customerTypes={customerTypes} maxCustomerCode={maxCustomerCode} /> | |||||
); | ); | ||||
}; | }; | ||||
@@ -65,8 +65,8 @@ const CustomerSearch: React.FC<Props> = ({ customers, abilities }) => { | |||||
buttonIcon: <EditNote />, | buttonIcon: <EditNote />, | ||||
isHidden: !maintainClient | isHidden: !maintainClient | ||||
}, | }, | ||||
{ name: "code", label: t("Customer Code") }, | |||||
{ name: "name", label: t("Customer Name") }, | |||||
{ name: "code", label: t("Customer Code"), sortable: true }, | |||||
{ name: "name", label: t("Customer Name"), sortable: true }, | |||||
{ | { | ||||
name: "id", | name: "id", | ||||
label: t("Delete"), | label: t("Delete"), | ||||
@@ -94,7 +94,7 @@ const CustomerSearch: React.FC<Props> = ({ customers, abilities }) => { | |||||
}} | }} | ||||
onReset={onReset} | onReset={onReset} | ||||
/> | /> | ||||
<SearchResults items={filteredCustomers} columns={columns} /> | |||||
<SearchResults items={filteredCustomers} columns={columns} autoRedirectToFirstPage={true}/> | |||||
</> | </> | ||||
); | ); | ||||
}; | }; | ||||
@@ -33,6 +33,17 @@ const GenerateLastModifiedReport: React.FC<Props> = ({ projects, companyHolidays | |||||
paramName: "date", | paramName: "date", | ||||
type: "date", | type: "date", | ||||
}, | }, | ||||
{ | |||||
label: t("Range"), | |||||
paramName: "dayRange", | |||||
type: "autocomplete", | |||||
options: [ | |||||
{label: t("30 Days"), value: 30}, | |||||
{label: t("60 Days"), value: 60}, | |||||
{label: t("90 Days"), value: 90}, | |||||
], | |||||
needAll: false | |||||
} | |||||
], | ], | ||||
[t] | [t] | ||||
); | ); | ||||
@@ -51,7 +62,8 @@ const GenerateLastModifiedReport: React.FC<Props> = ({ projects, companyHolidays | |||||
let postData = { | let postData = { | ||||
dateString: dayjs().format("YYYY-MM-DD").toString(), | dateString: dayjs().format("YYYY-MM-DD").toString(), | ||||
holidays: uniqueHoliday | |||||
holidays: uniqueHoliday, | |||||
dayRange: query.dayRange | |||||
}; | }; | ||||
console.log(query.date.length > 0) | console.log(query.date.length > 0) | ||||
if (query.date.length > 0) { | if (query.date.length > 0) { | ||||
@@ -32,6 +32,8 @@ interface BaseColumn<T extends ResultWithId> { | |||||
needTranslation?: boolean; | needTranslation?: boolean; | ||||
type?: string; | type?: string; | ||||
isHidden?: boolean; | isHidden?: boolean; | ||||
sortable?: boolean; // NEW | |||||
sortFn?: (a: T, b: T) => number; // NEW (optional custom sort) | |||||
} | } | ||||
interface ColumnWithAction<T extends ResultWithId> extends BaseColumn<T> { | interface ColumnWithAction<T extends ResultWithId> extends BaseColumn<T> { | ||||
@@ -158,6 +160,45 @@ function SearchResults<T extends ResultWithId>({ | |||||
return column.underline ?? "always"; | return column.underline ?? "always"; | ||||
}; | }; | ||||
const [orderByCol, setOrderByCol] = useState<keyof T | null>(null); | |||||
const [order, setOrder] = useState<'asc' | 'desc'>('asc'); | |||||
const handleSort = (col: Column<T>) => { | |||||
if (!('sortable' in col) || !col.sortable) return; | |||||
if (orderByCol === col.name) { | |||||
setOrder(prev => (prev === 'asc' ? 'desc' : 'asc')); | |||||
} else { | |||||
setOrderByCol(col.name); | |||||
setOrder('asc'); | |||||
} | |||||
}; | |||||
const sortedItems = React.useMemo(() => { | |||||
if (!orderByCol) return items; | |||||
const column = columns.find(col => col.name === orderByCol); | |||||
if (!column) return items; | |||||
let sorted = [...items]; | |||||
if (column.sortFn) { | |||||
sorted.sort((a, b) => | |||||
order === 'asc' ? column.sortFn!(a, b) : column.sortFn!(b, a) | |||||
); | |||||
} else { | |||||
sorted.sort((a, b) => { | |||||
const aValue = a[orderByCol]; | |||||
const bValue = b[orderByCol]; | |||||
if (aValue == null) return 1; | |||||
if (bValue == null) return -1; | |||||
if (aValue < bValue) return order === 'asc' ? -1 : 1; | |||||
if (aValue > bValue) return order === 'asc' ? 1 : -1; | |||||
return 0; | |||||
}); | |||||
} | |||||
return sorted; | |||||
}, [items, orderByCol, order, columns]); | |||||
// type OrderProps = Record<keyof T, Boolean> | // type OrderProps = Record<keyof T, Boolean> | ||||
// const [sortedItems, setSortedItems] = useState(items) | // const [sortedItems, setSortedItems] = useState(items) | ||||
// const [orderProps, setOrderProps] = useState<OrderProps>(() => { | // const [orderProps, setOrderProps] = useState<OrderProps>(() => { | ||||
@@ -209,15 +250,31 @@ function SearchResults<T extends ResultWithId>({ | |||||
<TableHead> | <TableHead> | ||||
<TableRow> | <TableRow> | ||||
{columns.filter(item => item.isHidden !== true).map((column, idx) => ( | {columns.filter(item => item.isHidden !== true).map((column, idx) => ( | ||||
<TableCell key={`${column.name.toString()}${idx}`}> | |||||
{column?.type === "money" ? <div style={{display: "flex", justifyContent: "flex-end"}}>{column.label}</div> : column.label} | |||||
<TableCell | |||||
key={`${column.name.toString()}${idx}`} | |||||
onClick={() => handleSort(column)} | |||||
style={{ cursor: column.sortable ? 'pointer' : undefined }} | |||||
> | |||||
<span style={{ display: "flex", alignItems: "center" }}> | |||||
{column?.type === "money" ? ( | |||||
<div style={{display: "flex", justifyContent: "flex-end", width: "100%"}}> | |||||
{column.label} | |||||
</div> | |||||
) : ( | |||||
column.label | |||||
)} | |||||
{column.sortable && ( | |||||
orderByCol === column.name ? | |||||
(order === 'asc' ? <ArrowUp fontSize="small" /> : <ArrowDown fontSize="small" />) | |||||
: <ArrowUp style={{ opacity: 0.3 }} fontSize="small" /> | |||||
)} | |||||
</span> | |||||
</TableCell> | </TableCell> | ||||
))} | ))} | ||||
</TableRow> | </TableRow> | ||||
</TableHead> | </TableHead> | ||||
<TableBody> | <TableBody> | ||||
{items | |||||
{sortedItems | |||||
.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage) | .slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage) | ||||
.map((item) => { | .map((item) => { | ||||
return ( | return ( | ||||