planning-fight/app/page.tsx

635 lines
34 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, settingsAPI, Settings } from '@/lib/api';
export default function Home() {
const [workers, setWorkers] = useState<Worker[]>([]);
const [newWorkerName, setNewWorkerName] = useState('');
const [newPresenceDays, setNewPresenceDays] = useState('');
const [nameError, setNameError] = useState(false);
const [daysError, setDaysError] = useState(false);
const [editingId, setEditingId] = useState<string | null>(null);
const [editingDays, setEditingDays] = useState('');
const [loading, setLoading] = useState(false);
const [settings, setSettings] = useState<Settings>({ 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 [darkMode, setDarkMode] = useState(false);
const [mounted, setMounted] = useState(false);
const TOTAL_POINTS = settings.totalPoints;
const TOTAL_DAYS = settings.totalDays;
const POINTS_PER_DAY = TOTAL_POINTS / TOTAL_DAYS;
useEffect(() => {
setMounted(true);
loadWorkers();
loadSettings();
// Load dark mode preference from localStorage
const savedDarkMode = localStorage.getItem('darkMode');
if (savedDarkMode !== null) {
setDarkMode(savedDarkMode === 'true');
}
}, []);
useEffect(() => {
if (!mounted) return;
// Apply dark mode class to document root
if (darkMode) {
document.documentElement.classList.add('dark');
} else {
document.documentElement.classList.remove('dark');
}
// Save preference to localStorage
localStorage.setItem('darkMode', darkMode.toString());
}, [darkMode, mounted]);
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 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 () => {
// 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;
}
try {
setLoading(true);
await workersAPI.create({
name: newWorkerName.trim(),
presenceDays: days,
});
await loadWorkers();
setNewWorkerName('');
setNewPresenceDays('');
setNameError(false);
setDaysError(false);
} 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 confirmResetAllPresences = async () => {
try {
setLoading(true);
setShowResetConfirm(false);
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-slate-50 via-white to-slate-100 dark:from-slate-900 dark:via-slate-800 dark:to-slate-900 p-8 transition-colors">
<div className="max-w-6xl mx-auto">
<div className="flex justify-between items-start mb-8">
<div>
<h1 className="text-4xl font-bold text-slate-900 dark:text-slate-100 mb-2">Worker Presence Manager</h1>
<p className="text-slate-600 dark:text-slate-400 text-lg">
Sprint duration: <span className="font-semibold text-slate-700 dark:text-slate-300">{TOTAL_DAYS}</span> working days | Total points per worker: <span className="font-semibold text-slate-700 dark:text-slate-300">{TOTAL_POINTS}</span>
</p>
</div>
<button
onClick={openSettings}
disabled={loading}
className="px-5 py-2.5 bg-slate-700 dark:bg-slate-600 text-white rounded-lg hover:bg-slate-800 dark:hover:bg-slate-700 transition-all shadow-sm hover:shadow-md font-medium disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-2"
>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className="w-5 h-5">
<path strokeLinecap="round" strokeLinejoin="round" d="M9.594 3.94c.09-.542.56-.94 1.11-.94h2.593c.55 0 1.02.398 1.11.94l.213 1.281c.063.374.313.686.645.87.074.04.147.083.22.127.325.196.72.257 1.075.124l1.217-.456a1.125 1.125 0 0 1 1.37.49l1.296 2.247a1.125 1.125 0 0 1-.26 1.431l-1.003.827c-.293.241-.438.613-.43.992a7.723 7.723 0 0 1 0 .255c-.008.378.137.75.43.991l1.004.827c.424.35.534.955.26 1.43l-1.298 2.247a1.125 1.125 0 0 1-1.369.491l-1.217-.456c-.355-.133-.75-.072-1.076.124a6.47 6.47 0 0 1-.22.128c-.331.183-.581.495-.644.869l-.213 1.281c-.09.543-.56.94-1.11.94h-2.594c-.55 0-1.019-.398-1.11-.94l-.213-1.281c-.062-.374-.312-.686-.644-.87a6.52 6.52 0 0 1-.22-.127c-.325-.196-.72-.257-1.076-.124l-1.217.456a1.125 1.125 0 0 1-1.369-.49l-1.297-2.247a1.125 1.125 0 0 1 .26-1.431l1.004-.827c.292-.24.437-.613.43-.991a6.932 6.932 0 0 1 0-.255c.007-.38-.138-.751-.43-.992l-1.004-.827a1.125 1.125 0 0 1-.26-1.43l1.297-2.247a1.125 1.125 0 0 1 1.37-.491l1.216.456c.356.133.751.072 1.076-.124.072-.044.146-.086.22-.128.332-.183.582-.495.644-.869l.214-1.28Z" />
<path strokeLinecap="round" strokeLinejoin="round" d="M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z" />
</svg>
Settings
</button>
</div>
{/* Add Worker Form */}
<div className="bg-white dark:bg-slate-800 rounded-xl shadow-sm border border-slate-200 dark:border-slate-700 p-6 mb-8">
<h2 className="text-xl font-semibold text-slate-800 dark:text-slate-200 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);
if (nameError) setNameError(false);
}}
className={`flex-1 px-4 py-2.5 border rounded-lg focus:outline-none focus:ring-2 transition-all bg-white dark:bg-slate-900 text-slate-900 dark:text-slate-100 placeholder:text-slate-400 dark:placeholder:text-slate-500 ${
nameError
? 'border-red-400 focus:ring-red-400 focus:border-red-400'
: 'border-slate-300 dark:border-slate-600 focus:ring-indigo-500 focus:border-indigo-500'
}`}
onKeyPress={(e) => e.key === 'Enter' && addWorker()}
/>
<input
type="number"
placeholder={`Presence days (0-${TOTAL_DAYS})`}
value={newPresenceDays}
onChange={(e) => {
setNewPresenceDays(e.target.value);
if (daysError) setDaysError(false);
}}
min="0"
max={TOTAL_DAYS}
className={`w-full sm:w-48 px-4 py-2.5 border rounded-lg focus:outline-none focus:ring-2 transition-all bg-white dark:bg-slate-900 text-slate-900 dark:text-slate-100 placeholder:text-slate-400 dark:placeholder:text-slate-500 ${
daysError
? 'border-red-400 focus:ring-red-400 focus:border-red-400'
: 'border-slate-300 dark:border-slate-600 focus:ring-indigo-500 focus:border-indigo-500'
}`}
onKeyPress={(e) => e.key === 'Enter' && addWorker()}
/>
<button
onClick={addWorker}
disabled={loading}
className="px-6 py-2.5 bg-indigo-600 dark:bg-indigo-500 text-white rounded-lg hover:bg-indigo-700 dark:hover:bg-indigo-600 transition-all shadow-sm hover:shadow-md font-medium disabled:opacity-50 disabled:cursor-not-allowed"
>
Add
</button>
</div>
</div>
{/* Workers List */}
{workers.length > 0 ? (
<div className="bg-white dark:bg-slate-800 rounded-xl shadow-sm border border-slate-200 dark:border-slate-700 overflow-hidden">
<div className="overflow-x-auto">
<table className="w-full">
<thead className="bg-slate-50 dark:bg-slate-900 border-b border-slate-200 dark:border-slate-700">
<tr>
<th className="px-6 py-4 text-left text-xs font-semibold text-slate-600 dark:text-slate-400 uppercase tracking-wider">
Worker Name
</th>
<th className="px-6 py-4 text-left text-xs font-semibold text-slate-600 dark:text-slate-400 uppercase tracking-wider">
Presence Days
</th>
<th className="px-6 py-4 text-left text-xs font-semibold text-slate-600 dark:text-slate-400 uppercase tracking-wider">
Points Earned
</th>
<th className="px-6 py-4 text-left text-xs font-semibold text-slate-600 dark:text-slate-400 uppercase tracking-wider">
Points Used
</th>
<th className="px-6 py-4 text-left text-xs font-semibold text-slate-600 dark:text-slate-400 uppercase tracking-wider">
Remaining Points
</th>
<th className="px-6 py-4 text-right text-xs font-semibold text-slate-600 dark:text-slate-400 uppercase tracking-wider">
Actions
</th>
</tr>
</thead>
<tbody className="bg-white dark:bg-slate-800 divide-y divide-slate-100 dark:divide-slate-700">
{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-slate-50 dark:hover:bg-slate-700/50 transition-colors">
<td className="px-6 py-4 whitespace-nowrap text-sm font-semibold text-slate-900 dark:text-slate-100">
{worker.name}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-slate-700 dark:text-slate-300">
{isEditing ? (
<input
type="number"
value={editingDays}
onChange={(e) => setEditingDays(e.target.value)}
min="0"
max={TOTAL_DAYS}
className="w-20 px-2 py-1.5 border border-indigo-300 dark:border-indigo-600 rounded-lg focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 transition-all bg-white dark:bg-slate-900 text-slate-900 dark:text-slate-100"
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-emerald-600 dark:text-emerald-400 font-semibold">
{pointsEarned.toFixed(2)}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-rose-600 dark:text-rose-400 font-semibold">
{pointsUsed.toFixed(2)}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-slate-700 dark:text-slate-300 font-semibold">
{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="p-2 text-emerald-600 dark:text-emerald-400 hover:text-emerald-700 dark:hover:text-emerald-300 hover:bg-emerald-50 dark:hover:bg-emerald-900/30 rounded-lg transition-all"
title="Save"
>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={2} stroke="currentColor" className="w-5 h-5">
<path strokeLinecap="round" strokeLinejoin="round" d="m4.5 12.75 6 6 9-13.5" />
</svg>
</button>
<button
onClick={cancelEditing}
className="p-2 text-slate-600 dark:text-slate-400 hover:text-slate-700 dark:hover:text-slate-300 hover:bg-slate-100 dark:hover:bg-slate-700 rounded-lg transition-all"
title="Cancel"
>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={2} stroke="currentColor" className="w-5 h-5">
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18 18 6M6 6l12 12" />
</svg>
</button>
</div>
) : (
<div className="flex gap-2 justify-end">
<button
onClick={() => startEditing(worker)}
className="p-2 text-indigo-600 dark:text-indigo-400 hover:text-indigo-700 dark:hover:text-indigo-300 hover:bg-indigo-50 dark:hover:bg-indigo-900/30 rounded-lg transition-all"
title="Edit"
>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className="w-5 h-5">
<path strokeLinecap="round" strokeLinejoin="round" d="m16.862 4.487 1.687-1.688a1.875 1.875 0 1 1 2.652 2.652L10.582 16.07a4.5 4.5 0 0 1-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 0 1 1.13-1.897l8.932-8.931Zm0 0L19.5 7.125M18 14v4.75A2.25 2.25 0 0 1 15.75 21H5.25A2.25 2.25 0 0 1 3 18.75V8.25A2.25 2.25 0 0 1 5.25 6H10" />
</svg>
</button>
<button
onClick={() => removeWorker(worker.id)}
className="p-2 text-rose-600 dark:text-rose-400 hover:text-rose-700 dark:hover:text-rose-300 hover:bg-rose-50 dark:hover:bg-rose-900/30 rounded-lg transition-all"
title="Remove"
>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className="w-5 h-5">
<path strokeLinecap="round" strokeLinejoin="round" d="m14.74 9-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 0 1-2.244 2.077H8.084a2.25 2.25 0 0 1-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 0 0-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 0 1 3.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 0 0-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 0 0-7.5 0" />
</svg>
</button>
</div>
)}
</td>
</tr>
);
})}
</tbody>
</table>
</div>
{/* Summary */}
<div className="bg-slate-50 dark:bg-slate-900 px-6 py-5 border-t border-slate-200 dark:border-slate-700">
<div className="flex flex-wrap items-center justify-between gap-4">
<div className="flex flex-wrap gap-6 text-sm">
<div>
<span className="text-slate-600 dark:text-slate-400">Total Workers: </span>
<span className="font-semibold text-slate-900 dark:text-slate-100">{workers.length}</span>
</div>
<div>
<span className="text-slate-600 dark:text-slate-400">Avg Presence Days: </span>
<span className="font-semibold text-slate-900 dark:text-slate-100">
{workers.length > 0
? (workers.reduce((sum, w) => sum + w.presenceDays, 0) / workers.length).toFixed(1)
: '0'}
</span>
</div>
<div>
<span className="text-slate-600 dark:text-slate-400">Total Presence Days: </span>
<span className="font-semibold text-slate-900 dark:text-slate-100">
{workers.reduce((sum, w) => sum + w.presenceDays, 0)}
</span>
</div>
</div>
<div>
<span className="text-slate-600 dark:text-slate-400">Total Points in the sprint: </span>
<span className="font-semibold text-indigo-600 dark:text-indigo-400">
{workers.reduce((sum, w) => sum + calculatePoints(w.presenceDays), 0).toFixed(2)}
</span>
</div>
<button
onClick={() => setShowResetConfirm(true)}
disabled={loading}
className="p-2.5 bg-amber-600 dark:bg-amber-500 text-white rounded-lg hover:bg-amber-700 dark:hover:bg-amber-600 transition-all shadow-sm hover:shadow-md disabled:opacity-50 disabled:cursor-not-allowed"
title="Reset All Presences"
>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className="w-5 h-5">
<path strokeLinecap="round" strokeLinejoin="round" d="M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0 3.181 3.183a8.25 8.25 0 0 0 13.803-3.7M4.031 9.865a8.25 8.25 0 0 1 13.803-3.7l3.181 3.182m0-4.991v4.99" />
</svg>
</button>
</div>
</div>
</div>
) : (
<div className="bg-white dark:bg-slate-800 rounded-xl shadow-sm border border-slate-200 dark:border-slate-700 p-12 text-center">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className="w-16 h-16 mx-auto mb-4 text-slate-300 dark:text-slate-600">
<path strokeLinecap="round" strokeLinejoin="round" d="M18 18.72a9.094 9.094 0 0 0 3.741-.479 3 3 0 0 0-4.682-2.72m.94 3.198.001.031c0 .225-.012.447-.037.666A11.944 11.944 0 0 1 12 21c-2.17 0-4.207-.576-5.963-1.584A6.062 6.062 0 0 1 6 18.719m12 0a5.971 5.971 0 0 0-.941-3.197m0 0A5.995 5.995 0 0 0 12 12.75a5.995 5.995 0 0 0-5.058 2.772m0 0a3 3 0 0 0-4.681 2.72 8.986 8.986 0 0 0 3.74.477m.94-3.197a5.971 5.971 0 0 0-.94 3.197M15 6.75a3 3 0 1 1-6 0 3 3 0 0 1 6 0Zm6 3a2.25 2.25 0 1 1-4.5 0 2.25 2.25 0 0 1 4.5 0Zm-13.5 0a2.25 2.25 0 1 1-4.5 0 2.25 2.25 0 0 1 4.5 0Z" />
</svg>
<p className="text-slate-500 dark:text-slate-400 text-lg">No workers added yet. Add your first worker above.</p>
</div>
)}
{/* Info Box */}
<div className="mt-8 bg-gradient-to-br from-indigo-50 to-purple-50 dark:from-indigo-950/50 dark:to-purple-950/50 border border-indigo-100 dark:border-indigo-900 rounded-xl p-6 shadow-sm">
<h3 className="font-semibold text-indigo-900 dark:text-indigo-300 mb-3 flex items-center gap-2">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className="w-5 h-5">
<path strokeLinecap="round" strokeLinejoin="round" d="m11.25 11.25.041-.02a.75.75 0 0 1 1.063.852l-.708 2.836a.75.75 0 0 0 1.063.853l.041-.021M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Zm-9-3.75h.008v.008H12V8.25Z" />
</svg>
How it works:
</h3>
<ul className="text-sm text-indigo-800 dark:text-indigo-300 space-y-2">
<li className="flex items-start gap-2">
<span className="text-indigo-400 dark:text-indigo-500 mt-0.5"></span>
<span>Each worker has {TOTAL_POINTS} points for a sprint ({TOTAL_DAYS} working days)</span>
</li>
<li className="flex items-start gap-2">
<span className="text-indigo-400 dark:text-indigo-500 mt-0.5"></span>
<span>Each presence day equals {POINTS_PER_DAY.toFixed(2)} points</span>
</li>
<li className="flex items-start gap-2">
<span className="text-indigo-400 dark:text-indigo-500 mt-0.5"></span>
<span>Points Earned = Presence Days × {POINTS_PER_DAY.toFixed(2)}</span>
</li>
<li className="flex items-start gap-2">
<span className="text-indigo-400 dark:text-indigo-500 mt-0.5"></span>
<span>Points Used = {TOTAL_POINTS} - Points Earned (days not present)</span>
</li>
<li className="flex items-start gap-2">
<span className="text-indigo-400 dark:text-indigo-500 mt-0.5"></span>
<span>Remaining Points = Points Earned</span>
</li>
</ul>
</div>
{/* Settings Modal */}
{showSettings && (
<div className="fixed inset-0 bg-black bg-opacity-50 backdrop-blur-sm flex items-center justify-center p-4 z-50">
<div className="bg-white dark:bg-slate-800 rounded-2xl shadow-2xl max-w-md w-full p-6">
<h2 className="text-2xl font-bold text-slate-800 dark:text-slate-100 mb-6">Settings</h2>
<div className="space-y-5">
{/* Dark Mode Toggle */}
<div className="pb-5 border-b border-slate-200 dark:border-slate-700">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="p-2 bg-slate-100 dark:bg-slate-700 rounded-lg">
{darkMode ? (
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className="w-5 h-5 text-slate-700 dark:text-slate-300">
<path strokeLinecap="round" strokeLinejoin="round" d="M21.752 15.002A9.72 9.72 0 0 1 18 15.75c-5.385 0-9.75-4.365-9.75-9.75 0-1.33.266-2.597.748-3.752A9.753 9.753 0 0 0 3 11.25C3 16.635 7.365 21 12.75 21a9.753 9.753 0 0 0 9.002-5.998Z" />
</svg>
) : (
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className="w-5 h-5 text-slate-700 dark:text-slate-300">
<path strokeLinecap="round" strokeLinejoin="round" d="M12 3v2.25m6.364.386-1.591 1.591M21 12h-2.25m-.386 6.364-1.591-1.591M12 18.75V21m-4.773-4.227-1.591 1.591M5.25 12H3m4.227-4.773L5.636 5.636M15.75 12a3.75 3.75 0 1 1-7.5 0 3.75 3.75 0 0 1 7.5 0Z" />
</svg>
)}
</div>
<div>
<span className="block text-sm font-semibold text-slate-700 dark:text-slate-200">Dark Mode</span>
<span className="text-xs text-slate-500 dark:text-slate-400">Toggle theme appearance</span>
</div>
</div>
<button
type="button"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
setDarkMode(!darkMode);
}}
className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 dark:focus:ring-offset-slate-800 ${
darkMode ? 'bg-indigo-600' : 'bg-slate-300'
}`}
>
<span
className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${
darkMode ? 'translate-x-6' : 'translate-x-1'
}`}
/>
</button>
</div>
</div>
<div>
<label className="block text-sm font-semibold text-slate-700 dark:text-slate-300 mb-2">
Points per Worker per Sprint
</label>
<input
type="number"
value={editTotalPoints}
onChange={(e) => setEditTotalPoints(e.target.value)}
min="1"
className="w-full px-4 py-2.5 border border-slate-300 dark:border-slate-600 rounded-lg focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 transition-all bg-white dark:bg-slate-900 text-slate-900 dark:text-slate-100 placeholder:text-slate-400 dark:placeholder:text-slate-500"
placeholder="e.g., 13"
/>
<p className="mt-2 text-sm text-slate-500 dark:text-slate-400">
Total points allocated to each worker for the sprint
</p>
</div>
<div>
<label className="block text-sm font-semibold text-slate-700 dark:text-slate-300 mb-2">
Number of Days in Sprint
</label>
<input
type="number"
value={editTotalDays}
onChange={(e) => setEditTotalDays(e.target.value)}
min="1"
className="w-full px-4 py-2.5 border border-slate-300 dark:border-slate-600 rounded-lg focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 transition-all bg-white dark:bg-slate-900 text-slate-900 dark:text-slate-100 placeholder:text-slate-400 dark:placeholder:text-slate-500"
placeholder="e.g., 10"
/>
<p className="mt-2 text-sm text-slate-500 dark:text-slate-400">
Total working days in the sprint
</p>
</div>
<div className="bg-indigo-50 dark:bg-indigo-950/50 border border-indigo-100 dark:border-indigo-900 rounded-lg p-4 text-sm text-slate-700 dark:text-slate-300">
<p>
<strong className="text-indigo-900 dark:text-indigo-300">Points per day:</strong>{' '}
<span className="font-semibold text-indigo-700 dark:text-indigo-400">
{editTotalPoints && editTotalDays && parseInt(editTotalPoints) > 0 && parseInt(editTotalDays) > 0
? (parseInt(editTotalPoints) / parseInt(editTotalDays)).toFixed(2)
: 'N/A'}
</span>
</p>
</div>
</div>
<div className="flex gap-3 mt-6">
<button
onClick={saveSettings}
disabled={loading}
className="flex-1 px-4 py-2.5 bg-indigo-600 dark:bg-indigo-500 text-white rounded-lg hover:bg-indigo-700 dark:hover:bg-indigo-600 transition-all shadow-sm hover:shadow-md font-medium disabled:opacity-50 disabled:cursor-not-allowed"
>
Save
</button>
<button
onClick={closeSettings}
disabled={loading}
className="flex-1 px-4 py-2.5 bg-slate-200 dark:bg-slate-700 text-slate-800 dark:text-slate-200 rounded-lg hover:bg-slate-300 dark:hover:bg-slate-600 transition-all font-medium disabled:opacity-50 disabled:cursor-not-allowed"
>
Cancel
</button>
</div>
</div>
</div>
)}
{/* Reset Confirmation Modal */}
{showResetConfirm && (
<div className="fixed inset-0 bg-black bg-opacity-50 backdrop-blur-sm flex items-center justify-center p-4 z-50">
<div className="bg-white dark:bg-slate-800 rounded-2xl shadow-2xl max-w-md w-full p-6">
<div className="flex items-center gap-3 mb-4">
<div className="p-3 bg-amber-100 dark:bg-amber-900/50 rounded-full">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className="w-6 h-6 text-amber-600 dark:text-amber-400">
<path strokeLinecap="round" strokeLinejoin="round" d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126ZM12 15.75h.007v.008H12v-.008Z" />
</svg>
</div>
<h2 className="text-xl font-bold text-slate-800 dark:text-slate-100">Reset All Presences</h2>
</div>
<p className="text-slate-600 dark:text-slate-400 mb-6">
Are you sure you want to reset all worker presences to 0? This action cannot be undone.
</p>
<div className="flex gap-3">
<button
onClick={() => setShowResetConfirm(false)}
disabled={loading}
className="flex-1 px-4 py-2.5 bg-slate-200 dark:bg-slate-700 text-slate-800 dark:text-slate-200 rounded-lg hover:bg-slate-300 dark:hover:bg-slate-600 transition-all font-medium disabled:opacity-50 disabled:cursor-not-allowed"
>
Cancel
</button>
<button
onClick={confirmResetAllPresences}
disabled={loading}
className="flex-1 px-4 py-2.5 bg-amber-600 dark:bg-amber-500 text-white rounded-lg hover:bg-amber-700 dark:hover:bg-amber-600 transition-all shadow-sm hover:shadow-md font-medium disabled:opacity-50 disabled:cursor-not-allowed"
>
Reset All
</button>
</div>
</div>
</div>
)}
</div>
</main>
);
}