Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add search skills page to allow search users by their skills #233

Merged
merged 1 commit into from
Nov 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/app/_domain/interfaces/Skill.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export type UserSkill = {
id?: number;
lastAppliedYear: number;
level: skillLevelKeys;
skill: Skill;
skillId?: string | number;
userId?: number;
yearsOfExperience: number;
Expand Down
2 changes: 1 addition & 1 deletion src/app/_presenters/data/userSkills/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,4 @@ export const updateUserSkills = async (
});

return data.message;
};
};
1 change: 1 addition & 0 deletions src/app/_presenters/data/userSkills/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export const fromApiParser = (data: any): UserSkill => {
yearsOfExperience: data.years_of_experience,
lastAppliedYear: data.last_applied_in_year,
level: data.level,
skill: data.skill,
};
};

Expand Down
4 changes: 3 additions & 1 deletion src/app/_presenters/data/users/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,14 @@ export const getUser = async (id: number | string): Promise<User> => {

export const getUsers = async (
onlyActive = true,
onlyInternal = false
onlyInternal = false,
skills = ""
): Promise<User[]> => {
const { data } = await backstageApiClient.get(`/users.json`, {
params: {
only_active: onlyActive,
only_internal: onlyInternal,
filter_by_skills: skills,
},
});
return data.map(fromApiParser);
Expand Down
2 changes: 1 addition & 1 deletion src/app/_presenters/data/users/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ export const fromApiParser = (user: FromApiUser): User => {
country: country,
internal: user.internal,
professionId: user.profession_id,
servicesIdentifiers: user_service_identifiers.map(
servicesIdentifiers: user_service_identifiers?.map(
(service: ApiServiceIdentifier) => ({
id: service.id,
customer: customerFromApiParser(service.customer),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ const BasicInfo: React.FC<Props> = ({ project, onSave, onDelete }) => {
const defaultProject = {
name: "",
customer: !isCustomersLoading ? customers[0] : undefined,
slackChannel: !isChannelsLoading ? channels[0].id : undefined,
slackChannel: !isChannelsLoading ? channels[0]?.id : undefined,
billable: false,
logoUrl: "",
logoBackgroundColor: "",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ const useChannelsController = (customer: Customer | undefined | null) => {
}, [isError, customer, showAlert]);

return {
channels: data,
channels: data ?? [],
isLoading,
};
};
Expand Down
93 changes: 93 additions & 0 deletions src/app/users/reports/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
"use client";
import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown';
import KeyboardArrowUpIcon from '@mui/icons-material/KeyboardArrowUp';
import { Grid, IconButton, Paper, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, Typography } from "@mui/material";
import { Fragment } from 'react';

import Loading from "@/components/Loading";

import Footer from "./presenters/components/Footer";
import SkillsList from './presenters/components/SkillsList';
import SkillsSearch from './presenters/components/SkillsSearch';
import useReportsController from "./presenters/controllers/useReportsController";

const UsersDashboard = () => {
const {
users,
onSearch,
query,
setQuery,
isLoading,
onExpand,
selectedUser,
userSkills,
onKeyPress
} = useReportsController();

if (isLoading) {
return <Loading />;
}

return (
<Grid container>
<Grid item xs={12} padding={5}>
<Grid xs={12} justifyContent={"center"} display="flex" mb={3}>
<Typography variant={"h2"} ml={2}>
Skills Search
</Typography>
</Grid>
<Grid xs={12} display={"flex"} justifyContent={"center"}>
<Grid md={5} xs={12}>
<SkillsSearch
onSearch={onSearch}
query={query}
setQuery={setQuery}
onKeyPress={onKeyPress}
/>
</Grid>
</Grid>
<Grid container justifyContent={"space-around"} display="flex" mt={5}>
<Grid item xs={12} md={12} pb={12}>
<TableContainer component={Paper}>
<Table sx={{ minWidth: 650 }} aria-label="simple table">
<TableHead>
<TableRow>
<TableCell />
<TableCell component="th" scope="row">Users</TableCell>
</TableRow>
</TableHead>
<TableBody>
{users?.map((user) => (
<Fragment key={user.id}>
<TableRow>
<TableCell>
<IconButton
aria-label="expand row"
size="small"
onClick={() => onExpand(selectedUser == user.id ? null : user.id!.toString())}
>
{selectedUser == user.id ? <KeyboardArrowUpIcon /> : <KeyboardArrowDownIcon />}
</IconButton>
</TableCell>
<TableCell>{user.fullName}</TableCell>
<TableCell>{user.email}</TableCell>
</TableRow>
<SkillsList
user={user}
userSkills={userSkills}
selectedUser={selectedUser}
/>
</Fragment>
))}
</TableBody>
</Table>
</TableContainer>
</Grid>
</Grid>
<Footer />
</Grid>
</Grid>
);
};

export default UsersDashboard;
38 changes: 38 additions & 0 deletions src/app/users/reports/presenters/components/Footer/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { Icon, Typography } from "@mui/material";
import Link from "next/link";

import Box from "@/components/Box";

const Footer = () => {
return (
<Box
mt={2}
display="flex"
justifyContent="center"
alignItems="center"
flexWrap="wrap"
color="text"
>
<Typography variant="caption" fontWeight="medium">
&copy; {new Date().getFullYear()}, made with
</Typography>
<Typography variant="caption" fontWeight="medium">
<Box color="text" mb={-0.5} mx={0.25}>
<Icon color="error" fontSize="inherit">
favorite
</Icon>
</Box>
</Typography>
<Typography variant="caption" fontWeight="medium">
by
</Typography>
<Typography variant="caption" fontWeight="medium">
<Link href={"https://codelitt.com"} target="_blank">
&nbsp;Codelitt&nbsp;
</Link>
</Typography>
</Box>
);
};

export default Footer;
36 changes: 36 additions & 0 deletions src/app/users/reports/presenters/components/SkillsList/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { Chip, Collapse, TableCell, TableRow, Typography } from "@mui/material";

import { UserSkill } from "@/app/_domain/interfaces/Skill";
import { User } from "@/app/_domain/interfaces/User";
import Box from "@/components/Box";

type Props = {
selectedUser: string | null;
user: User;
userSkills: UserSkill[] | undefined;
};

const SkillsList = ({ selectedUser, userSkills, user }: Props) => {
return (
<TableRow>
<TableCell style={{ paddingBottom: 0, paddingTop: 0 }} colSpan={6}>
<Collapse in={selectedUser == user.id} timeout="auto" unmountOnExit>
<Box sx={{ margin: 1 }}>
<Typography variant="h6" gutterBottom component="div">
Skills:
</Typography>
{userSkills?.map(({ id, skill }) => (
<Chip
key={id}
label={skill.name}
style={{ marginRight: 5 }}
/>
))}
</Box>
</Collapse>
</TableCell>
</TableRow>
);
};

export default SkillsList;
42 changes: 42 additions & 0 deletions src/app/users/reports/presenters/components/SkillsSearch/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import CodeIcon from '@mui/icons-material/Code';
import SearchIcon from '@mui/icons-material/Search';
import { IconButton, InputBase, Paper } from "@mui/material";

type Props = {
onKeyPress: (e: React.KeyboardEvent<HTMLInputElement>) => void;
onSearch: () => void;
query: string;
setQuery: (query: string) => void;
};

const SkillsSearch = ({ onSearch, query, setQuery, onKeyPress }: Props) => {
return (
<Paper
component="div"
sx={{ p: '2px 4px', display: 'flex', alignItems: 'center' }}
>
<IconButton sx={{ p: '10px' }} aria-label="menu">
<CodeIcon />
</IconButton>
<InputBase
sx={{ ml: 1, flex: 1 }}
placeholder="e.g. React, Node, Python"
inputProps={{ 'aria-label': 'search skills' }}
value={query}
onChange={(e) => setQuery(e.target.value)}
onKeyUp={onKeyPress}
autoFocus
/>
<IconButton
type="button"
sx={{ p: '10px' }}
aria-label="search"
onClick={onSearch}
>
<SearchIcon />
</IconButton>
</Paper>
);
};

export default SkillsSearch;
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { useQuery } from "@tanstack/react-query";
import { useSearchParams } from "next/navigation";
import { useEffect, useState } from "react";

import tanstackKeys from "@/app/_domain/enums/tanstackKeys";
import { useAppStore } from "@/app/_presenters/data/store/store";
import { getUsers } from "@/app/_presenters/data/users";
import { getUserSkills } from "@/app/_presenters/data/userSkills";

const useReportsController = () => {
const params = useSearchParams();
const authKey = params.get("authKey") as string;

const { setProjectAuthKey, projectAuthKey } = useAppStore();
const [query, setQuery] = useState<string>("");
const [selectedUser, setSelectedUser] = useState<string | null>(null);

const { data: users, isLoading, refetch } = useQuery({
queryKey: [tanstackKeys.Users, authKey],
queryFn: () => getUsers(true, false, query),
enabled: !!projectAuthKey,
retry: false,
});

const { data: userSkills } = useQuery({
queryKey: [tanstackKeys.UserSkills, selectedUser],
queryFn: () => getUserSkills(selectedUser!),
enabled: !!selectedUser,
retry: false,
});

const onSearch = () => {
setSelectedUser(null);
refetch();
}

const onExpand = (userId: string | null) => {
setSelectedUser(userId);
}

const onKeyPress = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === "Enter") {
e.preventDefault();
onSearch();
}
}

useEffect(() => {
if (authKey) {
setProjectAuthKey(authKey as string);
}
}, [authKey]);

return {
users,
isLoading,
query,
setQuery,
onSearch,
userSkills,
onExpand,
selectedUser,
onKeyPress
};
};

export default useReportsController;
Loading