From 4ffc736fea04b395756f0500d06e013591d5b623 Mon Sep 17 00:00:00 2001 From: Mathieu Date: Wed, 19 Nov 2025 13:34:08 +0100 Subject: [PATCH] - Rename application from "Worker Presence Tracker" to "Worker Presence Manager" in UI texts and metadata. - Add settings management feature to dynamically update total points and sprint days. - Refactor forms and modals for better user feedback, validation, and usability. --- README.md | 2 +- app/layout.tsx | 2 +- app/page.tsx | 313 ++++++++++++++++++++++++++++++++++++++++--------- db.json | 7 +- lib/api.ts | 24 ++++ 5 files changed, 289 insertions(+), 59 deletions(-) diff --git a/README.md b/README.md index e16b7a1..86aa9a5 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Worker Presence Tracker +# Worker Presence Manager A Next.js application for tracking worker presence with a point system. diff --git a/app/layout.tsx b/app/layout.tsx index 865fa20..66c4286 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -2,7 +2,7 @@ import type { Metadata } from "next"; import "./globals.css"; export const metadata: Metadata = { - title: "Worker Presence Tracker", + title: "Worker Presence Manager", description: "Track worker presence with point system", }; diff --git a/app/page.tsx b/app/page.tsx index 0af3813..56d2256 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,22 +1,30 @@ 'use client'; import { useState, useEffect } from 'react'; -import { workersAPI, Worker } from '@/lib/api'; +import { workersAPI, Worker, settingsAPI, Settings } from '@/lib/api'; export default function Home() { const [workers, setWorkers] = useState([]); const [newWorkerName, setNewWorkerName] = useState(''); const [newPresenceDays, setNewPresenceDays] = useState(''); + const [nameError, setNameError] = useState(false); + const [daysError, setDaysError] = useState(false); const [editingId, setEditingId] = useState(null); const [editingDays, setEditingDays] = useState(''); const [loading, setLoading] = useState(false); + const [settings, setSettings] = useState({ id: 1, totalPoints: 13, totalDays: 10 }); + const [showSettings, setShowSettings] = useState(false); + const [showResetConfirm, setShowResetConfirm] = useState(false); + const [editTotalPoints, setEditTotalPoints] = useState(''); + const [editTotalDays, setEditTotalDays] = useState(''); - const TOTAL_POINTS = 13; - const TOTAL_DAYS = 10; + const TOTAL_POINTS = settings.totalPoints; + const TOTAL_DAYS = settings.totalDays; const POINTS_PER_DAY = TOTAL_POINTS / TOTAL_DAYS; useEffect(() => { loadWorkers(); + loadSettings(); }, []); const loadWorkers = async () => { @@ -29,12 +37,68 @@ export default function Home() { } }; + const loadSettings = async () => { + try { + const data = await settingsAPI.get(); + setSettings(data); + } catch (error) { + console.error('Failed to load settings:', error); + } + }; + + const openSettings = () => { + setEditTotalPoints(settings.totalPoints.toString()); + setEditTotalDays(settings.totalDays.toString()); + setShowSettings(true); + }; + + const closeSettings = () => { + setShowSettings(false); + setEditTotalPoints(''); + setEditTotalDays(''); + }; + + const saveSettings = async () => { + const points = parseInt(editTotalPoints); + const days = parseInt(editTotalDays); + + if (isNaN(points) || points <= 0) { + alert('Please enter a valid number of points greater than 0'); + return; + } + + if (isNaN(days) || days <= 0) { + alert('Please enter a valid number of days greater than 0'); + return; + } + + try { + setLoading(true); + const updatedSettings = await settingsAPI.update({ totalPoints: points, totalDays: days }); + setSettings(updatedSettings); + closeSettings(); + } catch (error) { + console.error('Failed to save settings:', error); + alert('Failed to save settings'); + } finally { + setLoading(false); + } + }; + const addWorker = async () => { - if (!newWorkerName.trim() || !newPresenceDays.trim()) return; + // Validate required fields + const isNameEmpty = !newWorkerName.trim(); + const isDaysEmpty = !newPresenceDays.trim(); + + setNameError(isNameEmpty); + setDaysError(isDaysEmpty); + + if (isNameEmpty || isDaysEmpty) return; const days = parseInt(newPresenceDays); if (isNaN(days) || days < 0 || days > TOTAL_DAYS) { alert(`Please enter a valid number of days between 0 and ${TOTAL_DAYS}`); + setDaysError(true); return; } @@ -47,6 +111,8 @@ export default function Home() { await loadWorkers(); setNewWorkerName(''); setNewPresenceDays(''); + setNameError(false); + setDaysError(false); } catch (error) { console.error('Failed to add worker:', error); alert('Failed to add worker'); @@ -99,13 +165,10 @@ export default function Home() { } }; - const resetAllPresences = async () => { - if (!confirm('Are you sure you want to reset all worker presences to 0?')) { - return; - } - + const confirmResetAllPresences = async () => { try { setLoading(true); + setShowResetConfirm(false); await workersAPI.resetAllPresences(); await loadWorkers(); } catch (error) { @@ -129,20 +192,18 @@ export default function Home() {
-

Worker Presence Tracker

+

Worker Presence Manager

- Sprint duration: 2 weeks (10 working days) | Total points per worker: {TOTAL_POINTS} + Sprint duration: {TOTAL_DAYS} working days | Total points per worker: {TOTAL_POINTS}

- {workers.length > 0 && ( - - )} +
{/* Add Worker Form */} @@ -153,18 +214,32 @@ export default function Home() { type="text" placeholder="Worker name" value={newWorkerName} - onChange={(e) => setNewWorkerName(e.target.value)} - className="flex-1 px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500" + onChange={(e) => { + setNewWorkerName(e.target.value); + if (nameError) setNameError(false); + }} + className={`flex-1 px-4 py-2 border rounded-lg focus:outline-none focus:ring-2 ${ + nameError + ? 'border-red-500 focus:ring-red-500' + : 'border-gray-300 focus:ring-blue-500' + }`} onKeyPress={(e) => e.key === 'Enter' && addWorker()} /> setNewPresenceDays(e.target.value)} + onChange={(e) => { + setNewPresenceDays(e.target.value); + if (daysError) setDaysError(false); + }} min="0" - max="10" - className="w-full sm:w-48 px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500" + max={TOTAL_DAYS} + className={`w-full sm:w-48 px-4 py-2 border rounded-lg focus:outline-none focus:ring-2 ${ + daysError + ? 'border-red-500 focus:ring-red-500' + : 'border-gray-300 focus:ring-blue-500' + }`} onKeyPress={(e) => e.key === 'Enter' && addWorker()} />
)} @@ -286,31 +367,43 @@ export default function Home() { {/* Summary */}
-
-
- Total Workers: - {workers.length} -
-
- Avg Presence Days: - - {workers.length > 0 - ? (workers.reduce((sum, w) => sum + w.presenceDays, 0) / workers.length).toFixed(1) - : '0'} - -
-
- Total Presence Days: - - {workers.reduce((sum, w) => sum + w.presenceDays, 0)} - -
-
- Total Remaining Points: - - {workers.reduce((sum, w) => sum + calculatePoints(w.presenceDays), 0).toFixed(2)} - +
+
+
+ Total Workers: + {workers.length} +
+
+ Avg Presence Days: + + {workers.length > 0 + ? (workers.reduce((sum, w) => sum + w.presenceDays, 0) / workers.length).toFixed(1) + : '0'} + +
+
+ Total Presence Days: + + {workers.reduce((sum, w) => sum + w.presenceDays, 0)} + +
+
+ Total Points in the sprint: + + {workers.reduce((sum, w) => sum + calculatePoints(w.presenceDays), 0).toFixed(2)} + +
+
@@ -324,13 +417,121 @@ export default function Home() {

How it works:

    -
  • • Each worker has {TOTAL_POINTS} points for a 2-week sprint ({TOTAL_DAYS} working days)
  • +
  • • Each worker has {TOTAL_POINTS} points for a sprint ({TOTAL_DAYS} working days)
  • • Each presence day equals {POINTS_PER_DAY.toFixed(2)} points
  • • Points Earned = Presence Days × {POINTS_PER_DAY.toFixed(2)}
  • • Points Used = {TOTAL_POINTS} - Points Earned (days not present)
  • • Remaining Points = Points Earned
+ + {/* Settings Modal */} + {showSettings && ( +
+
+

Settings

+ +
+
+ + setEditTotalPoints(e.target.value)} + min="1" + className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500" + placeholder="e.g., 13" + /> +

+ Total points allocated to each worker for the sprint +

+
+ +
+ + setEditTotalDays(e.target.value)} + min="1" + className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500" + placeholder="e.g., 10" + /> +

+ Total working days in the sprint +

+
+ +
+

+ Points per day:{' '} + {editTotalPoints && editTotalDays && parseInt(editTotalPoints) > 0 && parseInt(editTotalDays) > 0 + ? (parseInt(editTotalPoints) / parseInt(editTotalDays)).toFixed(2) + : 'N/A'} +

+
+
+ +
+ + +
+
+
+ )} + + {/* Reset Confirmation Modal */} + {showResetConfirm && ( +
+
+
+
+ + + +
+

Reset All Presences

+
+ +

+ Are you sure you want to reset all worker presences to 0? This action cannot be undone. +

+ +
+ + +
+
+
+ )}
); diff --git a/db.json b/db.json index 5ba8598..cfcb5b3 100644 --- a/db.json +++ b/db.json @@ -5,5 +5,10 @@ "presenceDays": 0, "id": 1 } - ] + ], + "settings": { + "id": 1, + "totalPoints": 13, + "totalDays": 10 + } } \ No newline at end of file diff --git a/lib/api.ts b/lib/api.ts index b673d81..0252fce 100644 --- a/lib/api.ts +++ b/lib/api.ts @@ -8,6 +8,12 @@ export interface Worker { presenceDays: number; } +export interface Settings { + id: number; + totalPoints: number; + totalDays: number; +} + export const workersAPI = { async getAll(): Promise { const response = await fetch(`${API_URL}/workers`); @@ -51,3 +57,21 @@ export const workersAPI = { ); }, }; + +export const settingsAPI = { + async get(): Promise { + const response = await fetch(`${API_URL}/settings`); + if (!response.ok) throw new Error('Failed to fetch settings'); + return response.json(); + }, + + async update(settings: Partial): Promise { + const response = await fetch(`${API_URL}/settings`, { + method: 'PATCH', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(settings), + }); + if (!response.ok) throw new Error('Failed to update settings'); + return response.json(); + }, +};