I think finished?
This commit is contained in:
Binary file not shown.
@@ -6,7 +6,6 @@ conn = get_db()
|
|||||||
def get_users(search):
|
def get_users(search):
|
||||||
# Get search param
|
# Get search param
|
||||||
search_param = f"%{search}%"
|
search_param = f"%{search}%"
|
||||||
print(search_param)
|
|
||||||
|
|
||||||
# Generate cursor
|
# Generate cursor
|
||||||
cursor = conn.cursor()
|
cursor = conn.cursor()
|
||||||
@@ -25,29 +24,26 @@ def get_attendance(id):
|
|||||||
# Set up query for worked hours, group by and ordered by date
|
# Set up query for worked hours, group by and ordered by date
|
||||||
cursor = conn.cursor()
|
cursor = conn.cursor()
|
||||||
cursor.execute("""
|
cursor.execute("""
|
||||||
SELECT date, sum(hours_worked) as hours_worked, e.username
|
SELECT date, sum(hours_worked) as hours_worked
|
||||||
FROM Attendance a
|
FROM Attendance
|
||||||
JOIN Employees e ON a.employee_id = e.id
|
WHERE employee_id = ?
|
||||||
WHERE a.employee_id = ?
|
|
||||||
GROUP BY date
|
GROUP BY date
|
||||||
ORDER BY date DESC
|
ORDER BY date DESC
|
||||||
""", (id,))
|
""", (id,))
|
||||||
data = cursor.fetchall()
|
attendance = cursor.fetchall()
|
||||||
|
|
||||||
# Check if there's any attendance
|
# Fetch username from database
|
||||||
if not data:
|
# I am not using table JOIN because when theres no attendance, the query returns no rows (and shows user doesnt exist)
|
||||||
|
cursor.execute("SELECT username FROM Employees WHERE id = ?", (id,))
|
||||||
|
username = cursor.fetchone()
|
||||||
|
|
||||||
|
# Check if user exists
|
||||||
|
if not username:
|
||||||
return {"username": None, "attendance": []}
|
return {"username": None, "attendance": []}
|
||||||
|
|
||||||
# Remove repetetive username, and return clean api response
|
|
||||||
username = data[0]["username"] # get username from first row
|
|
||||||
attendance = [
|
|
||||||
{"date": row["date"], "hours_worked": row["hours_worked"]}
|
|
||||||
for row in data
|
|
||||||
]
|
|
||||||
|
|
||||||
# Return clean api response
|
# Return clean api response
|
||||||
return {
|
return {
|
||||||
"username": username,
|
"username": username[0],
|
||||||
"attendance": attendance
|
"attendance": attendance
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -19,6 +19,6 @@ app.add_middleware(
|
|||||||
def list_users(s: str = None):
|
def list_users(s: str = None):
|
||||||
return get_users(s)
|
return get_users(s)
|
||||||
|
|
||||||
@app.get("/{eployee_id}")
|
@app.get("/{employee_id}")
|
||||||
def list_attendance(eployee_id: int):
|
def list_attendance(employee_id: int):
|
||||||
return get_attendance(eployee_id)
|
return get_attendance(employee_id)
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
import { API_BASE_URL } from "@/consts";
|
import { API_BASE_URL } from "@/consts";
|
||||||
import type { User } from "@/types/api";
|
import type { User } from "@/types/api";
|
||||||
import { useQuery, type UseQueryResult } from "@tanstack/react-query";
|
import { useQuery, type UseQueryResult } from "@tanstack/react-query";
|
||||||
|
import { Link } from "@tanstack/react-router";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
function RenderList({ list }: { list: UseQueryResult<User[]> }) {
|
function RenderList({ list }: { list: UseQueryResult<User[]> }) {
|
||||||
// Base classes for each row
|
// Base classes for each row
|
||||||
const baseRowClass = "text-xl p-2 px-4 rounded capitalize bg-panel/75";
|
const baseRowClass = "flex text-xl p-2 px-4 rounded capitalize bg-panel/75";
|
||||||
|
|
||||||
// Display loading and error states
|
// Display loading and error states
|
||||||
if (list.isLoading) return <div className={`${baseRowClass} text-gray-400`}>Loading...</div>;
|
if (list.isLoading) return <div className={`${baseRowClass} text-gray-400`}>Loading...</div>;
|
||||||
@@ -14,7 +15,13 @@ function RenderList({ list }: { list: UseQueryResult<User[]> }) {
|
|||||||
// Render all users to the list
|
// Render all users to the list
|
||||||
return list.data?.map((u) => {
|
return list.data?.map((u) => {
|
||||||
return (
|
return (
|
||||||
<p className={`${baseRowClass} transition cursor-pointer hover:bg-panel hover:text-accent`}>{u.username}</p>
|
<Link
|
||||||
|
to="/$userId"
|
||||||
|
params={{ userId: u.id.toString() }}
|
||||||
|
className={`${baseRowClass} transition cursor-pointer hover:bg-panel hover:text-accent`}
|
||||||
|
>
|
||||||
|
{u.username}
|
||||||
|
</Link>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -45,9 +52,9 @@ export default function UsersTable() {
|
|||||||
className="bg-panel/75 transition focus:bg-panel border border-transparent outline-none focus:border-accent rounded px-4 py-2 mt-8 w-full "
|
className="bg-panel/75 transition focus:bg-panel border border-transparent outline-none focus:border-accent rounded px-4 py-2 mt-8 w-full "
|
||||||
onChange={(e) => setSearch(e.target.value)}
|
onChange={(e) => setSearch(e.target.value)}
|
||||||
/>
|
/>
|
||||||
<ul className="space-y-2 mt-2">
|
<div className="flex flex-col gap-2 mt-2">
|
||||||
<RenderList list={users} />
|
<RenderList list={users} />
|
||||||
</ul>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,8 +9,14 @@
|
|||||||
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
|
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
|
||||||
|
|
||||||
import { Route as rootRouteImport } from './routes/__root'
|
import { Route as rootRouteImport } from './routes/__root'
|
||||||
|
import { Route as UserIdRouteImport } from './routes/$userId'
|
||||||
import { Route as IndexRouteImport } from './routes/index'
|
import { Route as IndexRouteImport } from './routes/index'
|
||||||
|
|
||||||
|
const UserIdRoute = UserIdRouteImport.update({
|
||||||
|
id: '/$userId',
|
||||||
|
path: '/$userId',
|
||||||
|
getParentRoute: () => rootRouteImport,
|
||||||
|
} as any)
|
||||||
const IndexRoute = IndexRouteImport.update({
|
const IndexRoute = IndexRouteImport.update({
|
||||||
id: '/',
|
id: '/',
|
||||||
path: '/',
|
path: '/',
|
||||||
@@ -19,28 +25,39 @@ const IndexRoute = IndexRouteImport.update({
|
|||||||
|
|
||||||
export interface FileRoutesByFullPath {
|
export interface FileRoutesByFullPath {
|
||||||
'/': typeof IndexRoute
|
'/': typeof IndexRoute
|
||||||
|
'/$userId': typeof UserIdRoute
|
||||||
}
|
}
|
||||||
export interface FileRoutesByTo {
|
export interface FileRoutesByTo {
|
||||||
'/': typeof IndexRoute
|
'/': typeof IndexRoute
|
||||||
|
'/$userId': typeof UserIdRoute
|
||||||
}
|
}
|
||||||
export interface FileRoutesById {
|
export interface FileRoutesById {
|
||||||
__root__: typeof rootRouteImport
|
__root__: typeof rootRouteImport
|
||||||
'/': typeof IndexRoute
|
'/': typeof IndexRoute
|
||||||
|
'/$userId': typeof UserIdRoute
|
||||||
}
|
}
|
||||||
export interface FileRouteTypes {
|
export interface FileRouteTypes {
|
||||||
fileRoutesByFullPath: FileRoutesByFullPath
|
fileRoutesByFullPath: FileRoutesByFullPath
|
||||||
fullPaths: '/'
|
fullPaths: '/' | '/$userId'
|
||||||
fileRoutesByTo: FileRoutesByTo
|
fileRoutesByTo: FileRoutesByTo
|
||||||
to: '/'
|
to: '/' | '/$userId'
|
||||||
id: '__root__' | '/'
|
id: '__root__' | '/' | '/$userId'
|
||||||
fileRoutesById: FileRoutesById
|
fileRoutesById: FileRoutesById
|
||||||
}
|
}
|
||||||
export interface RootRouteChildren {
|
export interface RootRouteChildren {
|
||||||
IndexRoute: typeof IndexRoute
|
IndexRoute: typeof IndexRoute
|
||||||
|
UserIdRoute: typeof UserIdRoute
|
||||||
}
|
}
|
||||||
|
|
||||||
declare module '@tanstack/react-router' {
|
declare module '@tanstack/react-router' {
|
||||||
interface FileRoutesByPath {
|
interface FileRoutesByPath {
|
||||||
|
'/$userId': {
|
||||||
|
id: '/$userId'
|
||||||
|
path: '/$userId'
|
||||||
|
fullPath: '/$userId'
|
||||||
|
preLoaderRoute: typeof UserIdRouteImport
|
||||||
|
parentRoute: typeof rootRouteImport
|
||||||
|
}
|
||||||
'/': {
|
'/': {
|
||||||
id: '/'
|
id: '/'
|
||||||
path: '/'
|
path: '/'
|
||||||
@@ -53,6 +70,7 @@ declare module '@tanstack/react-router' {
|
|||||||
|
|
||||||
const rootRouteChildren: RootRouteChildren = {
|
const rootRouteChildren: RootRouteChildren = {
|
||||||
IndexRoute: IndexRoute,
|
IndexRoute: IndexRoute,
|
||||||
|
UserIdRoute: UserIdRoute,
|
||||||
}
|
}
|
||||||
export const routeTree = rootRouteImport
|
export const routeTree = rootRouteImport
|
||||||
._addFileChildren(rootRouteChildren)
|
._addFileChildren(rootRouteChildren)
|
||||||
|
|||||||
67
frontend/src/routes/$userId.tsx
Normal file
67
frontend/src/routes/$userId.tsx
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
import MainContainer from "@/components/MainContainer";
|
||||||
|
import { API_BASE_URL } from "@/consts";
|
||||||
|
import type { UserInfo } from "@/types/api";
|
||||||
|
import { capitalizeFirstLetter } from "@/utils/capitalizeFirstLetter";
|
||||||
|
import { useQuery, type UseQueryResult } from "@tanstack/react-query";
|
||||||
|
import { createFileRoute, useParams } from "@tanstack/react-router";
|
||||||
|
|
||||||
|
export const Route = createFileRoute("/$userId")({
|
||||||
|
component: RouteComponent,
|
||||||
|
});
|
||||||
|
|
||||||
|
function DisplayUserName({ info }: { info: UseQueryResult<UserInfo> }) {
|
||||||
|
// Base classes for headers
|
||||||
|
const baseClasses = "text-4xl font-bold text-center";
|
||||||
|
|
||||||
|
// Display loading and error states
|
||||||
|
if (info.isLoading) return <h1 className={`${baseClasses} text-gray-400`}>Loading user info ...</h1>;
|
||||||
|
if (info.isError) return <h1 className={`${baseClasses} text-red-400`}>Error: {info.error.message}</h1>;
|
||||||
|
|
||||||
|
// Check if user is null
|
||||||
|
if (!info.data?.username) return <h1 className={`${baseClasses} text-red-400`}>User not found</h1>;
|
||||||
|
|
||||||
|
return <h1 className={`${baseClasses} text-accent`}>{capitalizeFirstLetter(info.data.username)}</h1>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function DisplayUserTable({ info }: { info: UseQueryResult<UserInfo> }) {
|
||||||
|
// Base classes for headers
|
||||||
|
const baseClasses = "p-2 text-center";
|
||||||
|
|
||||||
|
if (!info.data) return; // return nothing when no data
|
||||||
|
if (!info.data.username) return; // Return nothing when user is null
|
||||||
|
if (info.data.attendance.length === 0) return <p className={`text-gray-400 text-center`}>No attendance</p>; // Empty attendance, return msg
|
||||||
|
|
||||||
|
// Display attendance in a table
|
||||||
|
return (
|
||||||
|
<table className="w-full bg-panel rounded">
|
||||||
|
<thead>
|
||||||
|
<th className={`${baseClasses}`}>Date</th>
|
||||||
|
<th className={`${baseClasses}`}>Hours worked</th>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{info.data.attendance.map((a) => (
|
||||||
|
<tr key={a.date} className="odd:bg-body/50">
|
||||||
|
<td className={`${baseClasses}`}>{a.date}</td>
|
||||||
|
<td className={`${baseClasses}`}>{a.hours_worked}</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function RouteComponent() {
|
||||||
|
const { userId } = Route.useParams();
|
||||||
|
|
||||||
|
// Define url for user info fetch
|
||||||
|
const userInfoURL = new URL(`${API_BASE_URL}/${userId}`);
|
||||||
|
const fetchUserInfo = () => fetch(userInfoURL).then((res) => res.json());
|
||||||
|
const userInfo = useQuery<UserInfo>({ queryKey: ["user", userId], queryFn: fetchUserInfo });
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MainContainer className="pt-8 space-y-4">
|
||||||
|
<DisplayUserName info={userInfo} />
|
||||||
|
<DisplayUserTable info={userInfo} />
|
||||||
|
</MainContainer>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -9,7 +9,7 @@ export const Route = createFileRoute("/")({
|
|||||||
function App() {
|
function App() {
|
||||||
return (
|
return (
|
||||||
<MainContainer className="pt-8">
|
<MainContainer className="pt-8">
|
||||||
<h1 className="text-4xl font-bold text-center text-accent">Darbinieki</h1>
|
<h1 className="text-4xl font-bold text-center text-accent">Employees</h1>
|
||||||
<UsersTable />
|
<UsersTable />
|
||||||
</MainContainer>
|
</MainContainer>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,3 +1,14 @@
|
|||||||
export interface User {
|
export interface User {
|
||||||
|
id: number;
|
||||||
username: string;
|
username: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface Attendance {
|
||||||
|
date: string;
|
||||||
|
hours_worked: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UserInfo {
|
||||||
|
username: string | null;
|
||||||
|
attendance: Attendance[];
|
||||||
|
}
|
||||||
|
|||||||
3
frontend/src/utils/capitalizeFirstLetter.ts
Normal file
3
frontend/src/utils/capitalizeFirstLetter.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export function capitalizeFirstLetter(val: string) {
|
||||||
|
return String(val).charAt(0).toUpperCase() + String(val).slice(1);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user