Export Salary Temaplet, Todo: template with data in DBtags/Baseline_30082024_FRONTEND_UAT
@@ -1,8 +1,9 @@ | |||||
"use server" | "use server" | ||||
import { serverFetchJson } from "@/app/utils/fetchUtil"; | |||||
import { serverFetchBlob, 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 { FileResponse } from "../reports/actions"; | |||||
export interface comboProp { | export interface comboProp { | ||||
id: any; | id: any; | ||||
@@ -17,4 +18,31 @@ export const fetchSalaryCombo = cache(async () => { | |||||
return serverFetchJson<combo>(`${BASE_API_URL}/salarys/combo`, { | return serverFetchJson<combo>(`${BASE_API_URL}/salarys/combo`, { | ||||
next: { tags: ["salary"] }, | next: { tags: ["salary"] }, | ||||
}); | }); | ||||
}); | |||||
}); | |||||
export const importSalarys = async (data: FormData) => { | |||||
console.log("----------------",data) | |||||
const importSalarys = await serverFetchString<String>( | |||||
`${BASE_API_URL}/salarys/import`, | |||||
{ | |||||
method: "POST", | |||||
body: data, | |||||
// headers: { "Content-Type": "multipart/form-data" }, | |||||
}, | |||||
); | |||||
return importSalarys; | |||||
}; | |||||
export const exportSalary = async () => { | |||||
const reportBlob = await serverFetchBlob<FileResponse>( | |||||
`${BASE_API_URL}/salarys/export`, | |||||
{ | |||||
method: "POST", | |||||
// body: JSON.stringify(data), | |||||
headers: { "Content-Type": "application/json" }, | |||||
}, | |||||
); | |||||
return reportBlob | |||||
}; |
@@ -27,7 +27,7 @@ export const serverFetch: typeof fetch = async (input, init) => { | |||||
? { | ? { | ||||
Authorization: `Bearer ${accessToken}`, | Authorization: `Bearer ${accessToken}`, | ||||
Accept: | Accept: | ||||
"application/json, application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", | |||||
"application/json, application/vnd.openxmlformats-officedocument.spreadsheetml.sheet, multipart/form-data", | |||||
} | } | ||||
: {}), | : {}), | ||||
}, | }, | ||||
@@ -71,6 +71,25 @@ export async function serverFetchWithNoContent(...args: FetchParams) { | |||||
} | } | ||||
} | } | ||||
export async function serverFetchString<T>(...args: FetchParams) { | |||||
const response = await serverFetch(...args); | |||||
if (response.ok) { | |||||
return response.text() as T; | |||||
} else { | |||||
switch (response.status) { | |||||
case 401: | |||||
signOutUser(); | |||||
default: | |||||
console.error(await response.text()); | |||||
throw new ServerFetchError( | |||||
"Something went wrong fetching data in server.", | |||||
response, | |||||
); | |||||
} | |||||
} | |||||
} | |||||
export async function serverFetchBlob<T>(...args: FetchParams) { | export async function serverFetchBlob<T>(...args: FetchParams) { | ||||
const response = await serverFetch(...args); | const response = await serverFetch(...args); | ||||
@@ -30,6 +30,7 @@ const pathToLabelMap: { [path: string]: string } = { | |||||
"/settings/position/new": "Create Position", | "/settings/position/new": "Create Position", | ||||
"/settings/salarys": "Salary", | "/settings/salarys": "Salary", | ||||
"/analytics/ProjectCashFlowReport": "Project Cash Flow Report", | "/analytics/ProjectCashFlowReport": "Project Cash Flow Report", | ||||
"/settings/holiday": "Holiday", | |||||
}; | }; | ||||
const Breadcrumb = () => { | const Breadcrumb = () => { | ||||
@@ -200,8 +200,9 @@ const CompanyHoliday: React.FC<Props> = ({ holidays }) => { | |||||
end: "dayGridMonth listMonth" | end: "dayGridMonth listMonth" | ||||
}} | }} | ||||
buttonText={{ | buttonText={{ | ||||
month: t("Month view"), | |||||
list: t("List View") | |||||
month: t("Calender View"), | |||||
list: t("List View"), | |||||
today: t("Today") | |||||
}} | }} | ||||
/> | /> | ||||
<CompanyHolidayDialog | <CompanyHolidayDialog | ||||
@@ -50,10 +50,10 @@ const CompanyHolidayDialog: React.FC<CompanyHolidayDialogProps> = ({ open, onClo | |||||
<Grid item xs={12}> | <Grid item xs={12}> | ||||
<TextField | <TextField | ||||
disabled={!editable} | disabled={!editable} | ||||
label={t("title")} | |||||
label={t("Description")} | |||||
fullWidth | fullWidth | ||||
{...register("name", { | {...register("name", { | ||||
required: "title required!", | |||||
required: "Description required!", | |||||
})} | })} | ||||
error={Boolean(errors.name)} | error={Boolean(errors.name)} | ||||
/> | /> | ||||
@@ -70,7 +70,7 @@ const DepartmentSearch: React.FC<Props> = ({ departments }) => { | |||||
onClick: onDeleteClick, | onClick: onDeleteClick, | ||||
buttonIcon: <DeleteIcon />, | buttonIcon: <DeleteIcon />, | ||||
color: "error" | color: "error" | ||||
}, | |||||
}, | |||||
], | ], | ||||
[t, onProjectClick], | [t, onProjectClick], | ||||
); | ); | ||||
@@ -7,6 +7,11 @@ import SearchResults, { Column } from "../SearchResults"; | |||||
import EditNote from "@mui/icons-material/EditNote"; | import EditNote from "@mui/icons-material/EditNote"; | ||||
import { SalaryResult } from "@/app/api/salarys"; | import { SalaryResult } from "@/app/api/salarys"; | ||||
import { convertLocaleStringToNumber } from "@/app/utils/formatUtil" | import { convertLocaleStringToNumber } from "@/app/utils/formatUtil" | ||||
import { Button, ButtonGroup, Stack } from "@mui/material"; | |||||
import FileDownloadIcon from '@mui/icons-material/FileDownload'; | |||||
import FileUploadIcon from '@mui/icons-material/FileUpload'; | |||||
import { exportSalary, importSalarys } from "@/app/api/salarys/actions"; | |||||
import { downloadFile } from "@/app/utils/commonUtil"; | |||||
interface Props { | interface Props { | ||||
salarys: SalaryResult[]; | salarys: SalaryResult[]; | ||||
@@ -32,8 +37,47 @@ const SalarySearch: React.FC<Props> = ({ salarys }) => { | |||||
setFilteredSalarys(salarys); | setFilteredSalarys(salarys); | ||||
}, [salarys]); | }, [salarys]); | ||||
const onSalaryClick = useCallback((project: SalaryResult) => { | |||||
console.log(project); | |||||
const onSalaryClick = useCallback((salary: SalaryResult) => { | |||||
console.log(salary); | |||||
}, []); | |||||
const handleImportClick = useCallback(async (event:any) => { | |||||
// console.log(event) | |||||
try { | |||||
const file = event.target.files[0]; | |||||
if (!file) { | |||||
console.log('No file selected'); | |||||
return; | |||||
} | |||||
if (file.type !== 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet') { | |||||
console.log('Invalid file format. Only XLSX files are allowed.'); | |||||
return; | |||||
} | |||||
const formData = new FormData(); | |||||
formData.append('multipartFileList', file); | |||||
const response = await importSalarys(formData); | |||||
if (response === "OK") { | |||||
window.location.reload() | |||||
} | |||||
} catch (err) { | |||||
console.log(err) | |||||
return false | |||||
} | |||||
}, []); | |||||
const handleExportClick = useCallback(async (event:any) => { | |||||
// console.log(event); | |||||
const response = await exportSalary() | |||||
if (response) { | |||||
downloadFile(new Uint8Array(response.blobValue), response.filename!!) | |||||
} | |||||
}, []); | }, []); | ||||
const columns = useMemo<Column<SalaryResult>[]>( | const columns = useMemo<Column<SalaryResult>[]>( | ||||
@@ -54,6 +98,28 @@ const SalarySearch: React.FC<Props> = ({ salarys }) => { | |||||
return ( | return ( | ||||
<> | <> | ||||
<Stack | |||||
direction="row" | |||||
justifyContent="right" | |||||
flexWrap="wrap" | |||||
spacing={2} | |||||
> | |||||
<ButtonGroup variant="contained"> | |||||
<Button startIcon={<FileUploadIcon />} variant="contained" component="label"> | |||||
<input | |||||
id='importExcel' | |||||
type='file' | |||||
accept='.xlsx, .csv' | |||||
hidden | |||||
onChange={(event) => {handleImportClick(event)}} | |||||
/> | |||||
{t("Import Salary")} | |||||
</Button> | |||||
<Button startIcon={<FileDownloadIcon />} onClick={handleExportClick} component="label" variant="contained"> | |||||
{t("Export Salary")} | |||||
</Button> | |||||
</ButtonGroup> | |||||
</Stack> | |||||
<SearchBox | <SearchBox | ||||
criteria={searchCriteria} | criteria={searchCriteria} | ||||
onSearch={(query) => { | onSearch={(query) => { | ||||
@@ -8,21 +8,21 @@ interface SubComponents { | |||||
Loading: typeof SalarySearchLoading; | Loading: typeof SalarySearchLoading; | ||||
} | } | ||||
function calculateHourlyRate(loweLimit: number, upperLimit: number, numOfWorkingDay: number, workingHour: number){ | |||||
const hourlyRate = (loweLimit + upperLimit)/2/numOfWorkingDay/workingHour | |||||
return hourlyRate.toLocaleString() | |||||
} | |||||
// function calculateHourlyRate(loweLimit: number, upperLimit: number, numOfWorkingDay: number, workingHour: number){ | |||||
// const hourlyRate = (loweLimit + upperLimit)/2/numOfWorkingDay/workingHour | |||||
// return hourlyRate.toLocaleString() | |||||
// } | |||||
const SalarySearchWrapper: React.FC & SubComponents = async () => { | const SalarySearchWrapper: React.FC & SubComponents = async () => { | ||||
const Salarys = await fetchSalarys(); | const Salarys = await fetchSalarys(); | ||||
// const Salarys:any[] = [] | // const Salarys:any[] = [] | ||||
const salarysWithHourlyRate = Salarys.map((salary) => { | const salarysWithHourlyRate = Salarys.map((salary) => { | ||||
const hourlyRate = calculateHourlyRate(Number(salary.lowerLimit), Number(salary.upperLimit),22, 8) | |||||
// const hourlyRate = calculateHourlyRate(Number(salary.lowerLimit), Number(salary.upperLimit),22, 8) | |||||
return { | return { | ||||
...salary, | ...salary, | ||||
upperLimit: salary.upperLimit.toLocaleString(), | upperLimit: salary.upperLimit.toLocaleString(), | ||||
lowerLimit: salary.lowerLimit.toLocaleString(), | lowerLimit: salary.lowerLimit.toLocaleString(), | ||||
hourlyRate: hourlyRate | |||||
hourlyRate: salary.hourlyRate.toLocaleString(), | |||||
} | } | ||||
}) | }) | ||||
// console.log(salarysWithHourlyRate) | // console.log(salarysWithHourlyRate) | ||||