I think finished?

This commit is contained in:
Leons Aleksandrovs
2025-07-02 22:25:03 +03:00
parent acb81d671d
commit 46dd731eab
9 changed files with 129 additions and 27 deletions

Binary file not shown.

View File

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

View File

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

View File

@@ -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>
); );
} }

View File

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

View 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>
);
}

View File

@@ -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>
); );

View File

@@ -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[];
}

View File

@@ -0,0 +1,3 @@
export function capitalizeFirstLetter(val: string) {
return String(val).charAt(0).toUpperCase() + String(val).slice(1);
}