diff --git a/src/app/api/history/route.ts b/src/app/api/history/route.ts index c983f59..df60508 100644 --- a/src/app/api/history/route.ts +++ b/src/app/api/history/route.ts @@ -1,23 +1,25 @@ "use server"; import { NextResponse } from 'next/server'; -import { legacyGetHistory } from '~/server/functions'; +import { getHistory } from '~/server/functions'; export const GET = async (request: Request) => { try { const url = new URL(request.url); const apiKey = url.searchParams.get('apikey'); const page = Number(url.searchParams.get('page')) || 1; - - if (apiKey !== 'zAf4vYVN2pszrK') { - return NextResponse.json({ message: 'Unauthorized' }, { status: 401 }); - } - - const perPage = 50; // You can adjust the perPage value as needed - const historyData = await legacyGetHistory(page, perPage); - + if (apiKey !== process.env.API_KEY) + return NextResponse.json( + { message: 'Unauthorized' }, + { status: 401 } + ); + const perPage = 50; + const historyData = await getHistory(page, perPage); return NextResponse.json(historyData, { status: 200 }); } catch (error) { console.error('Error fetching history data:', error); - return NextResponse.json({ message: 'Internal server error' }, { status: 500 }); + return NextResponse.json( + { message: 'Internal server error' }, + { status: 500 } + ); } }; diff --git a/src/app/api/technicians/route.ts b/src/app/api/technicians/route.ts index 020e911..53d4c15 100644 --- a/src/app/api/technicians/route.ts +++ b/src/app/api/technicians/route.ts @@ -1,6 +1,6 @@ "use server"; import { NextResponse } from 'next/server'; -import { legacyGetEmployees } from '~/server/functions'; +import { getEmployees } from '~/server/functions'; type Technician = { name: string; @@ -12,22 +12,25 @@ export const GET = async (request: Request) => { try { const url = new URL(request.url); const apiKey = url.searchParams.get('apikey'); - - if (apiKey !== 'zAf4vYVN2pszrK') { - return NextResponse.json({ message: 'Unauthorized' }, { status: 401 }); - } - - const employees = await legacyGetEmployees(); - + if (apiKey !== process.env.API_KEY) + return NextResponse.json( + { message: 'Unauthorized' }, + { status: 401 } + ); + const employees = await getEmployees(); + // Necessary because I haven't updated the iOS app + // yet to expect updatedAt rather than time const formattedEmployees = employees.map((employee: Technician) => ({ name: employee.name, status: employee.status, time: employee.updatedAt })); - return NextResponse.json(formattedEmployees, { status: 200 }); } catch (error) { console.error('Error fetching employees:', error); - return NextResponse.json({ message: 'Internal server error' }, { status: 500 }); + return NextResponse.json( + { message: 'Internal server error' }, + { status: 500 } + ); } }; diff --git a/src/app/api/update_technicians/route.ts b/src/app/api/update_technicians/route.ts index 25a6f5a..91eacf1 100644 --- a/src/app/api/update_technicians/route.ts +++ b/src/app/api/update_technicians/route.ts @@ -1,8 +1,7 @@ "use server"; import { NextResponse } from 'next/server'; -import { legacyUpdateEmployeeStatusByName } from '~/server/functions'; +import { updateEmployeeStatusByName } from '~/server/functions'; -// Define the Technician type directly in the file interface Technician { name: string; status: string; @@ -11,37 +10,42 @@ interface Technician { // Type guard to check if an object is a Technician const isTechnician = (technician: unknown): technician is Technician => { if (typeof technician !== 'object' || technician === null) return false; - return 'name' in technician && typeof (technician as Technician).name === 'string' && - 'status' in technician && typeof (technician as Technician).status === 'string'; + return 'name' in technician && + typeof (technician as Technician).name === 'string' && + 'status' in technician && + typeof (technician as Technician).status === 'string'; }; export const POST = async (request: Request) => { try { const url = new URL(request.url); const apiKey = url.searchParams.get('apikey'); - - if (apiKey !== 'zAf4vYVN2pszrK') { + if (apiKey !== process.env.API_KEY) return NextResponse.json({ message: 'Unauthorized' }, { status: 401 }); - } - const body: unknown = await request.json(); - // Validate the body and its technicians property - if (typeof body !== 'object' || body === null || !Array.isArray((body as { technicians?: unknown[] }).technicians)) { - return NextResponse.json({ message: 'Invalid input: expecting an array of technicians.' }, { status: 400 }); - } - + if (typeof body !== 'object' || body === null || + !Array.isArray((body as { technicians?: unknown[] }).technicians)) + return NextResponse.json( + { message: 'Invalid input: expecting an array of technicians.' }, + { status: 400 } + ); const technicians = (body as { technicians: unknown[] }).technicians; - - if (!technicians.every(isTechnician)) { - return NextResponse.json({ message: 'Invalid input: missing name or status for a technician.' }, { status: 400 }); - } - - await legacyUpdateEmployeeStatusByName(technicians); - - return NextResponse.json({ message: 'Technicians updated successfully.' }, { status: 200 }); + if (!technicians.every(isTechnician)) + return NextResponse.json( + { message: 'Invalid input: missing name or status for a technician.' }, + { status: 400 } + ); + await updateEmployeeStatusByName(technicians); + return NextResponse.json( + { message: 'Technicians updated successfully.' }, + { status: 200 } + ); } catch (error) { console.error('Error updating technicians:', error); - return NextResponse.json({ message: 'Internal server error' }, { status: 500 }); + return NextResponse.json( + { message: 'Internal server error' }, + { status: 500 } + ); } }; diff --git a/src/app/api/v2/get_employees/route.ts b/src/app/api/v2/get_employees/route.ts index eb8f238..d7e06ff 100644 --- a/src/app/api/v2/get_employees/route.ts +++ b/src/app/api/v2/get_employees/route.ts @@ -1,5 +1,4 @@ "use server"; - import { NextResponse } from 'next/server'; import { getEmployees } from '~/server/functions'; import { auth } from '~/auth'; diff --git a/src/app/api/v2/update_status/route.ts b/src/app/api/v2/update_status/route.ts index e837ba0..3ad344d 100644 --- a/src/app/api/v2/update_status/route.ts +++ b/src/app/api/v2/update_status/route.ts @@ -12,19 +12,27 @@ type UpdateStatusBody = { export const POST = async (req: NextRequest) => { const session = await auth(); if (!session) - return NextResponse.json({ message: 'Unauthorized' }, { status: 401 }); - + return NextResponse.json( + { message: 'Unauthorized' }, + { status: 401 } + ); const { employeeIds, newStatus } = await req.json() as UpdateStatusBody; - - if (!Array.isArray(employeeIds) || typeof newStatus !== 'string') { - return NextResponse.json({ message: 'Invalid input' }, { status: 400 }); - } - + if (!Array.isArray(employeeIds) || typeof newStatus !== 'string') + return NextResponse.json( + { message: 'Invalid input' }, + { status: 400 } + ); try { await updateEmployeeStatus(employeeIds, newStatus); - return NextResponse.json({ message: 'Status updated successfully' }, { status: 200 }); + return NextResponse.json( + { message: 'Status updated successfully' }, + { status: 200 } + ); } catch (error) { console.error('Error updating status:', error); - return NextResponse.json({ message: 'Internal server error' }, { status: 500 }); + return NextResponse.json( + { message: 'Internal server error' }, + { status: 500 } + ); } }; diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 2ad8d53..291b59c 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -7,7 +7,8 @@ import Sign_Out from "~/components/auth/Sign_Out"; import { type Metadata } from "next"; export const metadata: Metadata = { title: "Tech Tracker", - description: "App used by COG IT employees to update their status throughout the day.", + description: "App used by COG IT employees to \ + update their status throughout the day.", icons: [ { rel: 'icon', diff --git a/src/app/page.tsx b/src/app/page.tsx index b80b434..87551f0 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -9,8 +9,8 @@ export default async function HomePage() { return } else { return ( -
+
diff --git a/src/components/ui/TT_Header.tsx b/src/components/ui/TT_Header.tsx index f8cb092..63dcc0f 100644 --- a/src/components/ui/TT_Header.tsx +++ b/src/components/ui/TT_Header.tsx @@ -3,15 +3,15 @@ import Image from "next/image"; export default function TT_Header() { return (
-
+
Tech Tracker Logo -

+ font-bold pl-2 md:pl-12 text-transparent bg-clip-text"> Tech Tracker

diff --git a/src/components/ui/Table.tsx b/src/components/ui/Table.tsx index 773613c..2ce1c4b 100644 --- a/src/components/ui/Table.tsx +++ b/src/components/ui/Table.tsx @@ -19,19 +19,7 @@ export default function Table({ employees }: { employees: Employee[] }) { const [employeeStatus, setStatus] = useState(''); const [employeeData, setEmployeeData] = useState(employees); - useEffect(() => { - if (status !== "loading") { - setLoading(false); - } - }, [status]); - - - useEffect(() => { - // Refresh employee data if needed after state updates - setEmployeeData(employees); - }, [employees]); - - const fetchEmployees = useCallback(async (): Promise => { + const fetch_employees = useCallback(async (): Promise => { const res = await fetch('/api/v2/get_employees', { method: 'GET', headers: { @@ -41,28 +29,39 @@ export default function Table({ employees }: { employees: Employee[] }) { return res.json() as Promise; }, []); - useEffect(() => { - const fetchAndUpdateEmployees = async () => { - const updatedEmployees = await fetchEmployees(); - setEmployeeData(updatedEmployees); - }; - - fetchAndUpdateEmployees() - .catch((error) => { - console.error('Error fetching employees:', error); - }); - - const intervalId = setInterval(() => { - (async () => { - await fetchAndUpdateEmployees(); - })() - .catch((error) => { - console.error('Error fetching employees:', error); + const update_status = async () => { + if (!session) { + alert("You must be signed in to update status."); + return; + } + // If no employee is selected and status is not empty + if (selectedIds.length === 0 && employeeStatus.trim() !== '') { + const cur_user = employees.find(employee => employee.name === session.user?.name); + if (cur_user) { + await fetch('/api/v2/update_status', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${process.env.API_KEY}` + }, + body: JSON.stringify({ employeeIds: [cur_user.id], newStatus: employeeStatus }), + }); + } + } else if (employeeStatus.trim() !== '') { + await fetch('/api/v2/update_status', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${process.env.API_KEY}` + }, + body: JSON.stringify({ employeeIds: selectedIds, newStatus: employeeStatus }), }); - }, 10000); // Poll every 10 seconds - - return () => clearInterval(intervalId); // Clear interval on component unmount - }, [fetchEmployees]); + } + const updatedEmployees = await fetch_employees(); + setEmployeeData(updatedEmployees); + setSelectedIds([]); + setStatus(''); + }; const handleCheckboxChange = (id: number) => { setSelectedIds((prevSelected) => @@ -82,59 +81,18 @@ export default function Table({ employees }: { employees: Employee[] }) { } }; - useEffect(() => { - if (selectedIds.length === employeeData.length && employeeData.length > 0) { - setSelectAll(true); - } else { - setSelectAll(false); - } - }, [selectedIds, employeeData]); - const handleStatusChange = (e: React.ChangeEvent) => { setStatus(e.target.value); }; -const handleSubmit = async () => { - if (!session) { - alert("You must be signed in to update status."); - return; - } - // If no employee is selected and status is not empty - if (selectedIds.length === 0 && employeeStatus.trim() !== '') { - const cur_user = employees.find(employee => employee.name === session.user?.name); - if (cur_user) { - await fetch('/api/v2/update_status', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'Authorization': `Bearer ${process.env.API_KEY}` - }, - body: JSON.stringify({ employeeIds: [cur_user.id], newStatus: employeeStatus }), - }); - } - } else if (employeeStatus.trim() !== '') { - await fetch('/api/v2/update_status', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'Authorization': `Bearer ${process.env.API_KEY}` - }, - body: JSON.stringify({ employeeIds: selectedIds, newStatus: employeeStatus }), - }); - } - // Optionally refresh data on the client-side after update - const updatedEmployees = await fetchEmployees(); - setEmployeeData(updatedEmployees); - setSelectedIds([]); - setStatus(''); -}; - - const handleKeyPress = async (e: React.KeyboardEvent) => { + const handleKeyDown = async (e: React.KeyboardEvent) => { if (e.key === 'Enter') { - await handleSubmit(); + await update_status(); + // if key is i then focus text input } }; + // Format time for display const formatTime = (timestamp: Date) => { const date = new Date(timestamp); const time = date.toLocaleTimeString('en-US', { @@ -145,64 +103,121 @@ const handleSubmit = async () => { const month = date.toLocaleString('default', { month: 'long' }); return `${time} - ${month} ${day}`; }; + + // Loading bar while we wait for auth + useEffect(() => { + if (status !== "loading") { + setLoading(false); + } + }, [status]); + + // Refresh employee data if needed after state updates + useEffect(() => { + setEmployeeData(employees); + }, [employees]); + + // Fetch employees from the server every 10 seconds + useEffect(() => { + const fetchAndUpdateEmployees = async () => { + const updatedEmployees = await fetch_employees(); + setEmployeeData(updatedEmployees); + }; + fetchAndUpdateEmployees() + .catch((error) => { + console.error('Error fetching employees:', error); + }); + const intervalId = setInterval(() => { + (async () => { + await fetchAndUpdateEmployees(); + })() + .catch((error) => { + console.error('Error fetching employees:', error); + }); + }, 10000); // Poll every 10 seconds + + return () => clearInterval(intervalId); // Clear interval on component unmount + }, [fetch_employees]); + + // Handle checkbox changes + useEffect(() => { + if (selectedIds.length === employeeData.length && employeeData.length > 0) { + setSelectAll(true); + } else { + setSelectAll(false); + } + }, [selectedIds, employeeData]); + if (loading) return ; - return ( -
- - - - - - - - - - - {employeeData.map((employee) => ( - - + {employeeData.map((employee) => ( + + + + + + + ))} + +
- - NameStatusUpdated At
+ else { + return ( +
+ + + + - - + + + + - ))} - -
handleCheckboxChange(employee.id)} + className="m-auto cursor-pointer transform scale-150" + checked={selectAll} + onChange={handleSelectAllChange} /> - - {employee.name}{employee.status}{formatTime(employee.updatedAt)}NameStatusUpdated At
-
- - + +
+ handleCheckboxChange(employee.id)} + /> + + {employee.name} + + {employee.status} + + {formatTime(employee.updatedAt)} +
+
+ + +
-
- ); + ); + } } diff --git a/src/server/functions.ts b/src/server/functions.ts index 8ed40b2..df494e1 100644 --- a/src/server/functions.ts +++ b/src/server/functions.ts @@ -2,37 +2,29 @@ import "server-only"; import { db } from "~/server/db"; import { sql } from "drizzle-orm"; -// Function to Get Employees export const getEmployees = async () => { return await db.query.users.findMany({ orderBy: (model, { asc }) => asc(model.id), }); }; -// Uncomment this and change updatedAt below if using localhost and you want correct time. -// I dont know why it is like this. -//const convertToUTC = (date: Date) => { - //return new Date(date.setHours(date.getUTCHours())+ 5); -//}; - -// Function to Update Employee Status using Raw SQL -export const updateEmployeeStatus = async (employeeIds: string[], newStatus: string) => { +// Update Employee Status uses Raw SQL because Drizzle ORM doesn't support +// update with MySQL +export const updateEmployeeStatus = + async (employeeIds: string[], newStatus: string) => { try { // Convert array of ids to a format suitable for SQL query (comma-separated string) const idList = employeeIds.map(id => parseInt(id, 10)); - //const updatedAt = convertToUTC(new Date()); - const updatedAt = new Date(); // Do not change for PROD! It acts different on PROD - - // Prepare the query using drizzle-orm's template-like syntax for escaping variables + let updatedAt = new Date(); + // Not sure why but localhost is off by 5 hours + if (process.env.NODE_ENV === 'development') + updatedAt = new Date(updatedAt.setHours(updatedAt.getUTCHours())+ 5); const query = sql` UPDATE users SET status = ${newStatus}, updatedAt = ${updatedAt} WHERE id IN ${idList} `; - - // Execute the query await db.execute(query); - return { success: true }; } catch (error) { console.error("Error updating employee status:", error); @@ -40,15 +32,32 @@ export const updateEmployeeStatus = async (employeeIds: string[], newStatus: str } }; -// Legacy Functions for Legacy API for iOS App +// Function to Update Employee Status by Name using Raw SQL +export const updateEmployeeStatusByName = + async (technicians:{ name: string, status: string }[]) => { + try { + for (const technician of technicians) { + const { name, status } = technician; + const query = sql` + UPDATE users + SET status = ${status}, updatedAt = ${new Date()} + WHERE name = ${name} + `; + await db.execute(query); + } + return { success: true }; + } catch (error) { + console.error("Error updating employee status by name:", error); + throw new Error("Failed to update status by name"); + } +}; -// Type definitions +// Type definitions for Paginated History API interface HistoryEntry { name: string; status: string; time: Date; } - interface PaginatedHistory { data: HistoryEntry[]; meta: { @@ -59,25 +68,9 @@ interface PaginatedHistory { } } -export const legacyGetEmployees = async () => { - const employees = await db.query.users.findMany({ - orderBy: (model, { asc }) => asc(model.id), - }); - if (employees.length === 0) { - return []; - } - for (const employee of employees) { - const date = new Date(employee.updatedAt); - employee.updatedAt = date; - } - return employees; -}; - -// Function to Get History Data with Pagination using Raw SQL -export const legacyGetHistory = async (page: number, perPage: number): Promise => { +export const getHistory = + async (page: number, perPage: number): Promise => { const offset = (page - 1) * perPage; - - // Raw SQL queries const historyQuery = sql` SELECT u.name, h.status, h.updatedAt FROM history h @@ -85,31 +78,26 @@ export const legacyGetHistory = async (page: number, perPage: number): Promise

({ name: row.name, status: row.status, time: new Date(row.updatedAt), })); - return { data: formattedResults, meta: { @@ -121,25 +109,3 @@ export const legacyGetHistory = async (page: number, perPage: number): Promise

{ - try { - // Prepare and execute the queries for each technician - for (const technician of technicians) { - const { name, status } = technician; - const utcdate: Date = new Date(); - const query = sql` - UPDATE users - SET status = ${status}, updatedAt = ${utcdate} - WHERE name = ${name} - `; - - await db.execute(query); - } - - return { success: true }; - } catch (error) { - console.error("Error updating employee status by name:", error); - throw new Error("Failed to update status by name"); - } -};