planning-fight/app/page.tsx
2025-11-04 15:18:19 +01:00

337 lines
13 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

'use client';
import { useState, useEffect } from 'react';
import { workersAPI, Worker } from '@/lib/api';
export default function Home() {
const [workers, setWorkers] = useState<Worker[]>([]);
const [newWorkerName, setNewWorkerName] = useState('');
const [newPresenceDays, setNewPresenceDays] = useState('');
const [editingId, setEditingId] = useState<string | null>(null);
const [editingDays, setEditingDays] = useState('');
const [loading, setLoading] = useState(false);
const TOTAL_POINTS = 13;
const TOTAL_DAYS = 10;
const POINTS_PER_DAY = TOTAL_POINTS / TOTAL_DAYS;
useEffect(() => {
loadWorkers();
}, []);
const loadWorkers = async () => {
try {
const data = await workersAPI.getAll();
setWorkers(data);
} catch (error) {
console.error('Failed to load workers:', error);
alert('Failed to load workers. Make sure json-server is running.');
}
};
const addWorker = async () => {
if (!newWorkerName.trim() || !newPresenceDays.trim()) 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}`);
return;
}
try {
setLoading(true);
await workersAPI.create({
name: newWorkerName.trim(),
presenceDays: days,
});
await loadWorkers();
setNewWorkerName('');
setNewPresenceDays('');
} catch (error) {
console.error('Failed to add worker:', error);
alert('Failed to add worker');
} finally {
setLoading(false);
}
};
const removeWorker = async (id: string) => {
try {
setLoading(true);
await workersAPI.delete(id);
await loadWorkers();
} catch (error) {
console.error('Failed to remove worker:', error);
alert('Failed to remove worker');
} finally {
setLoading(false);
}
};
const startEditing = (worker: Worker) => {
setEditingId(worker.id);
setEditingDays(worker.presenceDays.toString());
};
const cancelEditing = () => {
setEditingId(null);
setEditingDays('');
};
const saveEditing = async (id: string) => {
const days = parseInt(editingDays);
if (isNaN(days) || days < 0 || days > TOTAL_DAYS) {
alert(`Please enter a valid number of days between 0 and ${TOTAL_DAYS}`);
return;
}
try {
setLoading(true);
await workersAPI.update(id, { presenceDays: days });
await loadWorkers();
setEditingId(null);
setEditingDays('');
} catch (error) {
console.error('Failed to update worker:', error);
alert('Failed to update worker');
} finally {
setLoading(false);
}
};
const resetAllPresences = async () => {
if (!confirm('Are you sure you want to reset all worker presences to 0?')) {
return;
}
try {
setLoading(true);
await workersAPI.resetAllPresences();
await loadWorkers();
} catch (error) {
console.error('Failed to reset presences:', error);
alert('Failed to reset presences');
} finally {
setLoading(false);
}
};
const calculatePoints = (days: number): number => {
return days * POINTS_PER_DAY;
};
const calculateUsedPoints = (days: number): number => {
return TOTAL_POINTS - calculatePoints(days);
};
return (
<main className="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-100 p-8">
<div className="max-w-4xl mx-auto">
<div className="flex justify-between items-start mb-8">
<div>
<h1 className="text-4xl font-bold text-gray-800 mb-2">Worker Presence Tracker</h1>
<p className="text-gray-600">
Sprint duration: 2 weeks (10 working days) | Total points per worker: {TOTAL_POINTS}
</p>
</div>
{workers.length > 0 && (
<button
onClick={resetAllPresences}
disabled={loading}
className="px-4 py-2 bg-orange-600 text-white rounded-lg hover:bg-orange-700 transition-colors font-medium disabled:opacity-50 disabled:cursor-not-allowed"
>
Reset All Presences
</button>
)}
</div>
{/* Add Worker Form */}
<div className="bg-white rounded-lg shadow-md p-6 mb-8">
<h2 className="text-xl font-semibold text-gray-800 mb-4">Add Worker</h2>
<div className="flex flex-col sm:flex-row gap-3">
<input
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"
onKeyPress={(e) => e.key === 'Enter' && addWorker()}
/>
<input
type="number"
placeholder="Presence days (0-10)"
value={newPresenceDays}
onChange={(e) => setNewPresenceDays(e.target.value)}
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"
onKeyPress={(e) => e.key === 'Enter' && addWorker()}
/>
<button
onClick={addWorker}
disabled={loading}
className="px-6 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors font-medium disabled:opacity-50 disabled:cursor-not-allowed"
>
Add
</button>
</div>
</div>
{/* Workers List */}
{workers.length > 0 ? (
<div className="bg-white rounded-lg shadow-md overflow-hidden">
<div className="overflow-x-auto">
<table className="w-full">
<thead className="bg-gray-50 border-b border-gray-200">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Worker Name
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Presence Days
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Points Earned
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Points Used
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Remaining Points
</th>
<th className="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">
Actions
</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{workers.map((worker) => {
const pointsEarned = calculatePoints(worker.presenceDays);
const pointsUsed = calculateUsedPoints(worker.presenceDays);
const remainingPoints = TOTAL_POINTS - pointsUsed;
const isEditing = editingId === worker.id;
return (
<tr key={worker.id} className="hover:bg-gray-50">
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">
{worker.name}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-700">
{isEditing ? (
<input
type="number"
value={editingDays}
onChange={(e) => setEditingDays(e.target.value)}
min="0"
max="10"
className="w-20 px-2 py-1 border border-blue-300 rounded focus:outline-none focus:ring-2 focus:ring-blue-500"
onKeyPress={(e) => {
if (e.key === 'Enter') saveEditing(worker.id);
if (e.key === 'Escape') cancelEditing();
}}
autoFocus
/>
) : (
`${worker.presenceDays} / ${TOTAL_DAYS}`
)}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-green-600 font-medium">
{pointsEarned.toFixed(2)}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-red-600 font-medium">
{pointsUsed.toFixed(2)}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-700 font-medium">
{remainingPoints.toFixed(2)} / {TOTAL_POINTS}
</td>
<td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
{isEditing ? (
<div className="flex gap-2 justify-end">
<button
onClick={() => saveEditing(worker.id)}
className="text-green-600 hover:text-green-900 transition-colors"
>
Save
</button>
<button
onClick={cancelEditing}
className="text-gray-600 hover:text-gray-900 transition-colors"
>
Cancel
</button>
</div>
) : (
<div className="flex gap-2 justify-end">
<button
onClick={() => startEditing(worker)}
className="text-blue-600 hover:text-blue-900 transition-colors"
>
Edit
</button>
<button
onClick={() => removeWorker(worker.id)}
className="text-red-600 hover:text-red-900 transition-colors"
>
Remove
</button>
</div>
)}
</td>
</tr>
);
})}
</tbody>
</table>
</div>
{/* Summary */}
<div className="bg-gray-50 px-6 py-4 border-t border-gray-200">
<div className="flex flex-wrap gap-6 text-sm">
<div>
<span className="text-gray-600">Total Workers: </span>
<span className="font-semibold text-gray-900">{workers.length}</span>
</div>
<div>
<span className="text-gray-600">Avg Presence Days: </span>
<span className="font-semibold text-gray-900">
{workers.length > 0
? (workers.reduce((sum, w) => sum + w.presenceDays, 0) / workers.length).toFixed(1)
: '0'}
</span>
</div>
<div>
<span className="text-gray-600">Total Presence Days: </span>
<span className="font-semibold text-gray-900">
{workers.reduce((sum, w) => sum + w.presenceDays, 0)}
</span>
</div>
<div>
<span className="text-gray-600">Total Remaining Points: </span>
<span className="font-semibold text-blue-600">
{workers.reduce((sum, w) => sum + calculatePoints(w.presenceDays), 0).toFixed(2)}
</span>
</div>
</div>
</div>
</div>
) : (
<div className="bg-white rounded-lg shadow-md p-12 text-center">
<p className="text-gray-500 text-lg">No workers added yet. Add your first worker above.</p>
</div>
)}
{/* Info Box */}
<div className="mt-8 bg-blue-50 border border-blue-200 rounded-lg p-4">
<h3 className="font-semibold text-blue-900 mb-2">How it works:</h3>
<ul className="text-sm text-blue-800 space-y-1">
<li> Each worker has {TOTAL_POINTS} points for a 2-week sprint ({TOTAL_DAYS} working days)</li>
<li> Each presence day equals {POINTS_PER_DAY.toFixed(2)} points</li>
<li> Points Earned = Presence Days × {POINTS_PER_DAY.toFixed(2)}</li>
<li> Points Used = {TOTAL_POINTS} - Points Earned (days not present)</li>
<li> Remaining Points = Points Earned</li>
</ul>
</div>
</div>
</main>
);
}