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