Browse Source

update

tags/Baseline_30082024_FRONTEND_UAT
MSI\derek 1 year ago
parent
commit
133bedcc40
7 changed files with 153 additions and 50 deletions
  1. +9
    -0
      src/app/api/user/actions.ts
  2. +15
    -0
      src/app/api/user/index.ts
  3. +41
    -10
      src/components/EditUser/EditUser.tsx
  4. +3
    -4
      src/components/EditUser/EditUserWrapper.tsx
  5. +3
    -5
      src/components/EditUser/UserDetail.tsx
  6. +15
    -7
      src/components/GenerateMonthlyWorkHoursReport/GenerateMonthlyWorkHoursReport.tsx
  7. +67
    -24
      src/components/SearchBox/SearchBox.tsx

+ 9
- 0
src/app/api/user/actions.ts View File

@@ -11,6 +11,7 @@ export interface UserInputs {
email?: string;
addAuthIds?: number[];
removeAuthIds?: number[];
password?: string;
}

export interface PasswordInputs {
@@ -50,4 +51,12 @@ export const changePassword = async (data: any) => {
body: JSON.stringify(data),
headers: { "Content-Type": "application/json" },
});
};

export const adminChangePassword = async (data: any) => {
return serverFetchWithNoContent(`${BASE_API_URL}/user/admin-change-password`, {
method: "PATCH",
body: JSON.stringify(data),
headers: { "Content-Type": "application/json" },
});
};

+ 15
- 0
src/app/api/user/index.ts View File

@@ -34,6 +34,15 @@ export interface UserDetail {
auths: any[]
}

export type passwordRule = {
min: number;
max: number;
number: boolean;
upperEng: boolean;
lowerEng: boolean;
specialChar: boolean;
}

export const preloadUser = () => {
fetchUser();
};
@@ -52,4 +61,10 @@ export interface UserDetail {
return serverFetchJson<UserResult[]>(`${BASE_API_URL}/user/${id}`, {
next: { tags: ["user"] },
});
});

export const fetchPwRules = cache(async () => {
return serverFetchJson<passwordRule>(`${BASE_API_URL}/user/password-rule`, {
next: { tags: ["pwRule"] },
});
});

+ 41
- 10
src/components/EditUser/EditUser.tsx View File

@@ -26,17 +26,19 @@ import {
} from "react-hook-form";
import { Check, Close, Error, RestartAlt } from "@mui/icons-material";
import { StaffResult } from "@/app/api/staff";
import { UserInputs, editUser, fetchUserDetails } from "@/app/api/user/actions";
import { UserInputs, adminChangePassword, editUser, fetchUserDetails } from "@/app/api/user/actions";
import UserDetail from "./UserDetail";
import { UserResult } from "@/app/api/user";
import { UserResult, passwordRule } from "@/app/api/user";
import { auth, fetchAuth } from "@/app/api/group/actions";
import AuthAllocation from "./AuthAllocation";

interface Props {
// users: UserResult[]
}
rules: passwordRule
}

const EditUser: React.FC<Props> = async ({ }) => {
const EditUser: React.FC<Props> = async ({
rules
}) => {
const { t } = useTranslation();
const formProps = useForm<UserInputs>();
const searchParams = useSearchParams();
@@ -53,11 +55,11 @@ const EditUser: React.FC<Props> = async ({ }) => {
},
[]
);
console.log(rules);

const errors = formProps.formState.errors;

const fetchUserDetail = async () => {
console.log(id);
try {
// fetch user info
const userDetail = await fetchUserDetails(id);
@@ -112,16 +114,45 @@ const EditUser: React.FC<Props> = async ({ }) => {
const onSubmit = useCallback<SubmitHandler<UserInputs>>(
async (data) => {
try {
let haveError = false
let regex_pw = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$/
let pw = ''
if (data.password && data.password.length > 0) {
pw = data.password
if (pw.length < rules.min) {
haveError = true
formProps.setError("password", { message: t("The password requires 8-20 characters."), type: "required" })
}
if (pw.length > rules.max) {
haveError = true
formProps.setError("password", { message: t("The password requires 8-20 characters."), type: "required" })
}
if (!regex_pw.test(pw)) {
haveError = true
formProps.setError("password", { message: "A combination of uppercase letters, lowercase letters, numbers, and symbols is required.", type: "required" })
}
}
console.log(data);
const tempData = {
const userData = {
name: data.name,
email: data.email,
email: '',
locked: false,
addAuthIds: data.addAuthIds || [],
removeAuthIds: data.removeAuthIds || [],
}
console.log(tempData);
await editUser(id, tempData);
const pwData = {
id: id,
password: "",
newPassword: pw
}
console.log(userData);
if (haveError) {
return
}
await editUser(id, userData);
if (data.password && data.password.length > 0) {
await adminChangePassword(pwData);
}
router.replace("/settings/staff");
} catch (e) {
console.log(e);


+ 3
- 4
src/components/EditUser/EditUserWrapper.tsx View File

@@ -5,7 +5,7 @@ import EditUserLoading from "./EditUserLoading";
import { useSearchParams } from "next/navigation";
import { fetchTeam, fetchTeamDetail } from "@/app/api/team";
import { fetchStaff } from "@/app/api/staff";
import { fetchUser, fetchUserDetail } from "@/app/api/user";
import { fetchPwRules, fetchUser, fetchUserDetail } from "@/app/api/user";

interface SubComponents {
Loading: typeof EditUserLoading;
@@ -17,10 +17,9 @@ interface Props {
const EditUserWrapper: React.FC<Props> & SubComponents = async ({
// id
}) => {
// const users = await fetchUser()
// const userDetail = await fetchUserDetail(id)
const pwRule = await fetchPwRules()

return <EditUser />
return <EditUser rules={pwRule} />
};

EditUserWrapper.Loading = EditUserLoading;


+ 3
- 5
src/components/EditUser/UserDetail.tsx View File

@@ -47,12 +47,10 @@ const UserDetail: React.FC<Props> = ({
</Grid>
<Grid item xs={6}>
<TextField
label={t("email")}
label={t("password")}
fullWidth
{...register("email", {
required: "email required!",
})}
error={Boolean(errors.email)}
{...register("password")}
error={Boolean(errors.password)}
/>
</Grid>
</Grid>


+ 15
- 7
src/components/GenerateMonthlyWorkHoursReport/GenerateMonthlyWorkHoursReport.tsx View File

@@ -24,11 +24,18 @@ const GenerateMonthlyWorkHoursReport: React.FC<Props> = ({ staffs }) => {

const searchCriteria: Criterion<SearchParamNames>[] = useMemo(
() => [
{ label: t("Staff"),
paramName: "staff",
type: "select",
options: staffCombo,
needAll: false},
{
label: t("Staff"),
paramName: "staff",
type: "select",
options: staffCombo,
needAll: false
},
{
label: t("date"),
paramName: "date",
type: "monthYear",
},
],
[t],
);
@@ -38,9 +45,10 @@ return (
<SearchBox
criteria={searchCriteria}
onSearch={async (query: any) => {
if (query.staff.length > 0 && query.staff.toLocaleLowerCase() !== "all") {
console.log(query)
if (query.staff.length > 0 && query.staff.toLocaleLowerCase() !== "all" && query.date.length < 0) {
const index = staffCombo.findIndex(staff => staff === query.staff)
const response = await fetchMonthlyWorkHoursReport({ id: staffs[index].id, yearMonth: "2023-03" })
const response = await fetchMonthlyWorkHoursReport({ id: staffs[index].id, yearMonth: query.date })
if (response) {
downloadFile(new Uint8Array(response.blobValue), response.filename!!)
}


+ 67
- 24
src/components/SearchBox/SearchBox.tsx View File

@@ -21,6 +21,7 @@ import { DatePicker } from "@mui/x-date-pickers/DatePicker";
import { LocalizationProvider } from "@mui/x-date-pickers/LocalizationProvider";
import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs";
import { Box } from "@mui/material";
import { DateCalendar } from "@mui/x-date-pickers";

interface BaseCriterion<T extends string> {
label: string;
@@ -43,10 +44,15 @@ interface DateRangeCriterion<T extends string> extends BaseCriterion<T> {
type: "dateRange";
}

interface MonthYearCriterion<T extends string> extends BaseCriterion<T> {
type: "monthYear";
}

export type Criterion<T extends string> =
| TextCriterion<T>
| SelectCriterion<T>
| DateRangeCriterion<T>;
| DateRangeCriterion<T>
| MonthYearCriterion<T>;

interface Props<T extends string> {
criteria: Criterion<T>[];
@@ -66,19 +72,19 @@ function SearchBox<T extends string>({
(acc, c) => {
return { ...acc, [c.paramName]: c.type === "select" ? "All" : "" };
},
{} as Record<T, string>,
{} as Record<T, string>
),
[criteria],
[criteria]
);
const [inputs, setInputs] = useState(defaultInputs);
const makeInputChangeHandler = useCallback(
(paramName: T): React.ChangeEventHandler<HTMLInputElement> => {
return (e) => {
setInputs((i) => ({ ...i, [paramName]: e.target.value }));
};
},
[],
[]
);

const makeSelectChangeHandler = useCallback((paramName: T) => {
@@ -93,6 +99,13 @@ function SearchBox<T extends string>({
};
}, []);

const makeMonthYearChangeHandler = useCallback((paramName: T) => {
return (e: any) => {
console.log(dayjs(e).format("YYYY-MM"))
setInputs((i) => ({ ...i, [paramName]: dayjs(e).format("YYYY-MM") }));
};
}, []);

const makeDateToChangeHandler = useCallback((paramName: T) => {
return (e: any) => {
setInputs((i) => ({
@@ -135,7 +148,9 @@ function SearchBox<T extends string>({
onChange={makeSelectChangeHandler(c.paramName)}
value={inputs[c.paramName]}
>
{!(c.needAll === false) && <MenuItem value={"All"}>{t("All")}</MenuItem>}
{!(c.needAll === false) && (
<MenuItem value={"All"}>{t("All")}</MenuItem>
)}
{c.options.map((option, index) => (
<MenuItem key={`${option}-${index}`} value={option}>
{t(option)}
@@ -144,6 +159,26 @@ function SearchBox<T extends string>({
</Select>
</FormControl>
)}
{c.type === "monthYear" && (
<LocalizationProvider
dateAdapter={AdapterDayjs}
// TODO: Should maybe use a custom adapterLocale here to support YYYY-MM-DD
adapterLocale="zh-hk"
>
<Box display="flex">
<DateCalendar
views={["month", "year"]}
openTo="month"
onChange={makeMonthYearChangeHandler(c.paramName)}
value={
inputs[c.paramName]
? dayjs(inputs[c.paramName])
: null
}
/>
</Box>
</LocalizationProvider>
)}
{c.type === "dateRange" && (
<LocalizationProvider
dateAdapter={AdapterDayjs}
@@ -155,7 +190,11 @@ function SearchBox<T extends string>({
<DatePicker
label={c.label}
onChange={makeDateChangeHandler(c.paramName)}
value={inputs[c.paramName] ? dayjs(inputs[c.paramName]) : null}
value={
inputs[c.paramName]
? dayjs(inputs[c.paramName])
: null
}
/>
</FormControl>
<Box
@@ -170,7 +209,11 @@ function SearchBox<T extends string>({
<DatePicker
label={c.label2}
onChange={makeDateToChangeHandler(c.paramName)}
value={inputs[c.paramName.concat("To") as T] ? dayjs(inputs[c.paramName.concat("To") as T]) : null}
value={
inputs[c.paramName.concat("To") as T]
? dayjs(inputs[c.paramName.concat("To") as T])
: null
}
/>
</FormControl>
</Box>
@@ -180,22 +223,22 @@ function SearchBox<T extends string>({
);
})}
</Grid>
<CardActions sx={{ justifyContent: "flex-end" }}>
<Button
variant="text"
startIcon={<RestartAlt />}
onClick={handleReset}
>
{t("Reset")}
</Button>
<Button
variant="outlined"
startIcon={<Search />}
onClick={handleSearch}
>
{t("Search")}
</Button>
</CardActions>
<CardActions sx={{ justifyContent: "flex-end" }}>
<Button
variant="text"
startIcon={<RestartAlt />}
onClick={handleReset}
>
{t("Reset")}
</Button>
<Button
variant="outlined"
startIcon={<Search />}
onClick={handleSearch}
>
{t("Search")}
</Button>
</CardActions>
</CardContent>
</Card>
);


Loading…
Cancel
Save