| @@ -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 ( | ||||