| @@ -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 { cache } from "react"; | |||
| import "server-only"; | |||
| @@ -88,3 +88,9 @@ export const fetchCustomerTypes = cache(async () => { | |||
| 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 { | |||
| date: string; | |||
| dayRange: number; | |||
| } | |||
| export interface LastModifiedReportRequest { | |||
| dateString: string; | |||
| dayRange: number; | |||
| } | |||
| export interface ExportCurrentStaffInfoRequest { | |||
| @@ -45,6 +45,24 @@ export interface Props extends Omit<ModalProps, "children"> { | |||
| 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 = { | |||
| position: "absolute", | |||
| top: "50%", | |||
| @@ -99,6 +117,9 @@ const BulkAddPaymentModal: React.FC<Props> = ({ | |||
| const amountForLastItem = truncateMoney( | |||
| amountToDivide - dividedAmount * (numberOfEntries - 1), | |||
| )!; | |||
| const datetypeOption = datetypeOptions.find(r => r.value === dateType); | |||
| return Array(numberOfEntries) | |||
| .fill(undefined) | |||
| .map((_, index) => { | |||
| @@ -106,8 +127,8 @@ const BulkAddPaymentModal: React.FC<Props> = ({ | |||
| dateType === "fixed" | |||
| ? dateReference | |||
| : dateReference.add( | |||
| index, | |||
| dateType === "monthly" ? "month" : "week", | |||
| index * (datetypeOption?.step ?? 0), | |||
| datetypeOption?.unit ?? "month", | |||
| ); | |||
| return { | |||
| @@ -194,9 +215,14 @@ const BulkAddPaymentModal: React.FC<Props> = ({ | |||
| {...field} | |||
| 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="fixed">{t("Fixed")}</MenuItem> | |||
| <MenuItem value="fixed">{t("Fixed")}</MenuItem> */} | |||
| </Select> | |||
| )} | |||
| rules={{ | |||
| @@ -28,6 +28,7 @@ import { differenceBy } from "lodash"; | |||
| export interface Props { | |||
| subsidiaries: Subsidiary[], | |||
| customerTypes: CustomerType[], | |||
| maxCustomerCode: string | |||
| } | |||
| const hasErrorsInTab = ( | |||
| @@ -45,6 +46,7 @@ const hasErrorsInTab = ( | |||
| const CustomerSave: React.FC<Props> = ({ | |||
| subsidiaries, | |||
| customerTypes, | |||
| maxCustomerCode | |||
| }) => { | |||
| const [serverError, setServerError] = useState(""); | |||
| const [tabIndex, setTabIndex] = useState(0); | |||
| @@ -58,7 +60,7 @@ const CustomerSave: React.FC<Props> = ({ | |||
| try { | |||
| const defaultCustomer = { | |||
| id: null, | |||
| code: "", | |||
| code: maxCustomerCode ?? "", | |||
| name: "", | |||
| brNo: null, | |||
| address: null, | |||
| @@ -2,7 +2,7 @@ | |||
| // import CreateProject from "./CreateProject"; | |||
| // import { fetchProjectCategories } from "@/app/api/projects"; | |||
| // 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"; | |||
| // type Props = { | |||
| @@ -14,14 +14,15 @@ import CustomerSave from "./CustomerSave"; | |||
| const CustomerSaveWrapper: React.FC = async () => { | |||
| // const { params } = props | |||
| // console.log(params) | |||
| const [subsidiaries, customerTypes] = | |||
| const [subsidiaries, customerTypes, maxCustomerCode] = | |||
| await Promise.all([ | |||
| fetchAllSubsidiaries(), | |||
| fetchCustomerTypes(), | |||
| getMaxCustomerCode() | |||
| ]); | |||
| 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 />, | |||
| 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", | |||
| label: t("Delete"), | |||
| @@ -94,7 +94,7 @@ const CustomerSearch: React.FC<Props> = ({ customers, abilities }) => { | |||
| }} | |||
| 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", | |||
| 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] | |||
| ); | |||
| @@ -51,7 +62,8 @@ const GenerateLastModifiedReport: React.FC<Props> = ({ projects, companyHolidays | |||
| let postData = { | |||
| dateString: dayjs().format("YYYY-MM-DD").toString(), | |||
| holidays: uniqueHoliday | |||
| holidays: uniqueHoliday, | |||
| dayRange: query.dayRange | |||
| }; | |||
| console.log(query.date.length > 0) | |||
| if (query.date.length > 0) { | |||
| @@ -32,6 +32,8 @@ interface BaseColumn<T extends ResultWithId> { | |||
| needTranslation?: boolean; | |||
| type?: string; | |||
| isHidden?: boolean; | |||
| sortable?: boolean; // NEW | |||
| sortFn?: (a: T, b: T) => number; // NEW (optional custom sort) | |||
| } | |||
| interface ColumnWithAction<T extends ResultWithId> extends BaseColumn<T> { | |||
| @@ -158,6 +160,45 @@ function SearchResults<T extends ResultWithId>({ | |||
| 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> | |||
| // const [sortedItems, setSortedItems] = useState(items) | |||
| // const [orderProps, setOrderProps] = useState<OrderProps>(() => { | |||
| @@ -209,15 +250,31 @@ function SearchResults<T extends ResultWithId>({ | |||
| <TableHead> | |||
| <TableRow> | |||
| {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> | |||
| ))} | |||
| </TableRow> | |||
| </TableHead> | |||
| <TableBody> | |||
| {items | |||
| {sortedItems | |||
| .slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage) | |||
| .map((item) => { | |||
| return ( | |||