-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathindex.html
More file actions
350 lines (315 loc) · 61 KB
/
index.html
File metadata and controls
350 lines (315 loc) · 61 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>TaskMaster: Your Life Gamified</title>
<script src="https://cdn.tailwindcss.com"></script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/dayjs@1/dayjs.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/dayjs@1/plugin/relativeTime.js"></script>
<script src="https://cdn.jsdelivr.net/npm/dayjs@1/plugin/customParseFormat.js"></script>
<script src="https://cdn.jsdelivr.net/npm/dayjs@1/plugin/duration.js"></script>
<!-- Sound Library -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/tone/14.7.77/Tone.js"></script>
<script>
dayjs.extend(dayjs_plugin_relativeTime);
dayjs.extend(dayjs_plugin_customParseFormat);
dayjs.extend(dayjs_plugin_duration);
tailwind.config = {
theme: {
extend: {
fontFamily: {
sans: ['Inter', 'sans-serif'],
},
colors: {
'brand-primary': '#1a1a1a',
'brand-secondary': '#2c2c2c',
'brand-accent': '#4f46e5',
'brand-accent-hover': '#4338ca',
'brand-text': '#f5f5f5',
'brand-subtle': '#a3a3a3',
'brand-success': '#22c55e',
'brand-warning': '#facc15',
'brand-danger': '#ef4444',
}
}
}
}
</script>
<style>
body { background-color: #1a1a1a; color: #f5f5f5; }
.modal-backdrop { background-color: rgba(0,0,0,0.7); }
.task-card { background: linear-gradient(145deg, #2e2e2e, #262626); box-shadow: 8px 8px 16px #1e1e1e, -8px -8px 16px #343434; }
.btn { transition: all 0.2s ease-in-out; }
.btn:hover:not(:disabled) { transform: translateY(-2px); box-shadow: 0 4px 8px rgba(0,0,0,0.2); }
.btn:disabled { opacity: 0.5; cursor: not-allowed; }
.tab-active { border-bottom-color: #4f46e5; color: #f5f5f5; }
.tab { border-bottom-color: transparent; color: #a3a3a3; }
.tab:disabled { color: #555; cursor: not-allowed; }
#compliance-bar { transition: width 0.5s ease-in-out; }
</style>
</head>
<body class="antialiased">
<div id="app" class="container mx-auto p-4 md:p-8 max-w-4xl font-sans">
<header class="flex justify-between items-center mb-8">
<h1 class="text-3xl md:text-4xl font-bold text-brand-text">TaskMaster</h1>
<div class="flex items-center space-x-4">
<div id="badge-display" class="text-right text-2xl"></div> <!-- For Perfect Week badge -->
<div id="streak-display" class="text-right"></div>
<div id="points-debt-display" class="text-right">
<p class="text-brand-subtle text-sm">Points</p>
<p id="points-display" class="text-2xl font-bold text-brand-accent">0</p>
</div>
<button id="shop-btn" class="btn bg-brand-secondary p-3 rounded-full hover:bg-gray-700">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 text-brand-text" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 3h2l.4 2M7 13h10l4-8H5.4M7 13L5.4 5M7 13l-2.293 2.293c-.63.63-.184 1.707.707 1.707H17m0 0a2 2 0 100 4 2 2 0 000-4zm-8 2a2 2 0 11-4 0 2 2 0 014 0z" /></svg>
</button>
</div>
</header>
<main id="main-content" class="text-center p-8 rounded-2xl task-card min-h-[300px] flex flex-col justify-center items-center"></main>
<div id="compliance-container" class="mt-8">
<div class="w-full bg-brand-danger rounded-full h-6 border-2 border-gray-600 relative overflow-hidden">
<div id="compliance-bar" class="bg-brand-success h-full rounded-full"></div>
<div id="compliance-text" class="absolute inset-0 flex items-center justify-center text-xs font-bold text-white" style="text-shadow: 1px 1px 2px rgba(0,0,0,0.7);"></div>
</div>
</div>
<footer class="mt-4 flex justify-center">
<button id="manage-btn" class="btn bg-brand-secondary hover:bg-gray-700 text-brand-text font-bold py-3 px-6 rounded-lg shadow-lg">Manage Your Setup</button>
</footer>
</div>
<!-- Modals -->
<div id="sound-modal" class="hidden fixed inset-0 z-50 modal-backdrop flex justify-center items-center p-4"><div class="bg-brand-secondary rounded-2xl shadow-2xl w-full max-w-sm text-center p-8"><h2 class="text-2xl font-bold mb-4">Enable Sound</h2><p class="text-brand-subtle mb-6">Click the button below to enable sound notifications for new tasks.</p><button id="enable-sound-btn" class="btn w-full bg-brand-accent hover:bg-brand-accent-hover text-white font-bold py-3 px-6 rounded-lg">Enable Sound</button></div></div>
<div id="manage-modal" class="hidden fixed inset-0 z-50 overflow-y-auto modal-backdrop flex justify-center items-center p-4"><div class="bg-brand-secondary rounded-2xl shadow-2xl w-full max-w-3xl max-h-[90vh] flex flex-col"><div class="p-6 border-b border-gray-700 flex justify-between items-center"><h2 class="text-2xl font-bold">Manage Your Setup</h2><button class="close-modal-btn text-gray-400 hover:text-white" data-modal="manage-modal">×</button></div><div class="flex border-b border-gray-700 px-6 overflow-x-auto"><button data-tab="tasks" class="tab tab-active text-lg font-semibold py-4 mr-6 flex-shrink-0">Tasks</button><button data-tab="rewards" class="tab text-lg font-semibold py-4 mr-6 flex-shrink-0">Rewards</button><button data-tab="forfeits" class="tab text-lg font-semibold py-4 mr-6 flex-shrink-0">Forfeits</button><button data-tab="schedule" class="tab text-lg font-semibold py-4 mr-6 flex-shrink-0">Schedule</button><button data-tab="stats" class="tab text-lg font-semibold py-4 mr-6 flex-shrink-0">Stats</button><button data-tab="settings" class="tab text-lg font-semibold py-4 flex-shrink-0">Settings</button></div><div class="p-6 overflow-y-auto"><div id="tasks-tab" class="tab-content"></div><div id="rewards-tab" class="tab-content hidden"></div><div id="forfeits-tab" class="tab-content hidden"></div><div id="schedule-tab" class="tab-content hidden"></div><div id="stats-tab" class="tab-content hidden"></div><div id="settings-tab" class="tab-content hidden"></div></div></div></div>
<div id="shop-modal" class="hidden fixed inset-0 z-50 overflow-y-auto modal-backdrop flex justify-center items-center p-4"><div class="bg-brand-secondary rounded-2xl shadow-2xl w-full max-w-md max-h-[90vh] flex flex-col"><div class="p-6 border-b border-gray-700 flex justify-between items-center"><h2 class="text-2xl font-bold">Reward Shop</h2><button class="close-modal-btn text-gray-400 hover:text-white" data-modal="shop-modal">×</button></div><div id="shop-content" class="p-6 overflow-y-auto"></div><div class="p-4 border-t border-gray-700"><button id="log-toggle-btn" class="w-full btn bg-gray-600 hover:bg-gray-500 text-white font-bold py-2 px-4 rounded-lg">View Purchase History</button></div></div></div>
<div id="result-modal" class="hidden fixed inset-0 z-50 overflow-y-auto modal-backdrop flex justify-center items-center p-4"><div class="bg-brand-secondary rounded-2xl shadow-2xl w-full max-w-md text-center p-8"><h2 id="result-title" class="text-2xl font-bold mb-4"></h2><p id="result-message" class="text-brand-subtle mb-6"></p><div id="result-details" class="mb-6"></div><button id="result-ok-btn" class="btn w-full bg-brand-accent hover:bg-brand-accent-hover text-white font-bold py-3 px-6 rounded-lg">OK</button></div></div>
<div id="feedback-modal" class="hidden fixed inset-0 z-50 overflow-y-auto modal-backdrop flex justify-center items-center p-4"><div class="bg-brand-secondary rounded-2xl shadow-2xl w-full max-w-md text-center p-8"><h2 class="text-2xl font-bold mb-4">Forfeit Feedback</h2><p class="text-brand-subtle mb-6">How difficult was that forfeit?</p><div class="flex justify-around"><button data-feedback="hard" class="btn feedback-btn bg-brand-danger hover:bg-red-500 text-white font-bold py-3 px-6 rounded-lg">Hard</button><button data-feedback="ok" class="btn feedback-btn bg-brand-warning hover:bg-yellow-400 text-black font-bold py-3 px-6 rounded-lg">About Right</button><button data-feedback="easy" class="btn feedback-btn bg-brand-success hover:bg-green-500 text-white font-bold py-3 px-6 rounded-lg">Easy</button></div></div></div>
<div id="failure-modal" class="hidden fixed inset-0 z-50 overflow-y-auto modal-backdrop flex justify-center items-center p-4"><div class="bg-brand-secondary rounded-2xl shadow-2xl w-full max-w-md text-center p-8"><h2 class="text-2xl font-bold mb-4 text-brand-danger">FAILURE REPORT</h2><p class="text-brand-subtle mb-6">Why did you fail your task, maggot?</p><div class="flex flex-col space-y-3"><button data-reason="Forgot / Ran out of time" class="btn failure-reason-btn bg-brand-accent hover:bg-brand-accent-hover text-white font-bold py-3 px-6 rounded-lg">Forgot / Ran out of time</button><button data-reason="Task was too difficult" class="btn failure-reason-btn bg-brand-accent hover:bg-brand-accent-hover text-white font-bold py-3 px-6 rounded-lg">Task was too difficult</button><button data-reason="I refused" class="btn failure-reason-btn bg-brand-accent hover:bg-brand-accent-hover text-white font-bold py-3 px-6 rounded-lg">I refused</button></div></div></div>
<div id="edit-modal" class="hidden fixed inset-0 z-50 overflow-y-auto modal-backdrop flex justify-center items-center p-4"><div class="bg-brand-secondary rounded-2xl shadow-2xl w-full max-w-md p-6"><h2 id="edit-modal-title" class="text-2xl font-bold mb-6"></h2><form id="edit-form"></form><div class="mt-6 flex justify-end space-x-4"><button id="edit-cancel-btn" class="btn bg-gray-600 hover:bg-gray-500 text-white font-bold py-2 px-4 rounded-lg">Cancel</button><button id="edit-save-btn" class="btn bg-brand-accent hover:bg-brand-accent-hover text-white font-bold py-2 px-4 rounded-lg">Save</button></div></div></div>
<script type="module">
const App = {
state: {}, timerInterval: null, taskAssignmentInterval: null, interestInterval: null,
getDefaultState() {
return {
version: 1.6, // Version bump for new feature
points: 100, tasksPerDay: 1, targetTasksToday: 1, activeTask: null, lastTaskDate: null,
dailyTasksAssigned: 0, rewardsRedeemedToday: 0, lastPriceCheckDate: null,
saleIsOn: false, rewardLog: [], soundInitialized: false,
taskDurationHours: 2, compliance: 100, compliancePenalty: 10,
skipPasses: 0, totalTasksCompleted: 0,
currentStreak: 0, lastStreakDate: null,
perfectWeekStart: null, hasPerfectWeekBadge: false,
failureLog: [],
debt: null,
schedule: { 0: { start: '00:00', end: '00:00' }, 1: { start: '09:00', end: '17:00' }, 2: { start: '09:00', end: '17:00' }, 3: { start: '09:00', end: '17:00' }, 4: { start: '09:00', end: '17:00' }, 5: { start: '09:00', end: '17:00' }, 6: { start: '00:00', end: '00:00' }, },
tasks: [ { id: 't1', name: 'Pushups', value: 10, unit: 'reps', points: 10, enabled: true, consecutiveSuccesses: 0, scheduledDay: 'any' }, { id: 't2', name: 'Pull ups', value: 5, unit: 'reps', points: 20, enabled: true, consecutiveSuccesses: 0, scheduledDay: 'any' }, ],
rewards: [ { id: 'r1', name: 'Eat Candy', points: 50, enabled: true }, { id: 'r2', name: '30min Me Time', points: 100, enabled: true }, ],
forfeits: {
tasks: [ { id: 'ft1', name: 'Face the corner', enabled: true }, { id: 'ft2', name: 'Face the wall', enabled: true }, ],
positions: [ { id: 'fp1', name: 'Hands in air', enabled: true }, { id: 'fp2', name: 'Hands on hair', enabled: true }, ],
baseDuration: 900,
}
};
},
saveState() { try { localStorage.setItem('taskmaster_state', JSON.stringify(this.state)); } catch (error) { console.error("FATAL: Could not save state!", error); } },
applyLoadedState(loadedState) {
if (!loadedState) {
this.state = this.getDefaultState();
return;
}
let defaults = this.getDefaultState();
const mergedForfeits = { ...defaults.forfeits, ...loadedState.forfeits };
const mergedSchedule = { ...defaults.schedule, ...loadedState.schedule };
const defaultTaskProps = { consecutiveSuccesses: 0, scheduledDay: 'any' };
const mergedTasks = (loadedState.tasks || defaults.tasks).map(task => ({ ...defaultTaskProps, ...task }));
const defaultRewardProps = { enabled: true };
const mergedRewards = (loadedState.rewards || defaults.rewards).map(reward => ({ ...defaultRewardProps, ...reward }));
this.state = {
...defaults,
...loadedState,
forfeits: mergedForfeits,
schedule: mergedSchedule,
tasks: mergedTasks,
rewards: mergedRewards,
version: defaults.version // Force update to latest version
};
if (this.state.forfeits.duration && !this.state.forfeits.baseDuration) {
this.state.forfeits.baseDuration = this.state.forfeits.duration;
}
console.log("Successfully merged loaded state with defaults.");
},
loadState() {
try {
const s = localStorage.getItem('taskmaster_state');
this.applyLoadedState(s ? JSON.parse(s) : null);
} catch (error) {
console.error("Error loading state, resetting to default:", error);
localStorage.removeItem('taskmaster_state');
this.state = this.getDefaultState();
}
},
init() { this.loadState(); this.attachEventListeners(); if (!this.state.soundInitialized) { this.showModal('sound-modal'); } else { this.runStartupLogic(); } },
runStartupLogic() { this.checkDateAndReset(); this.checkStreak(); this.checkAndAdjustPrices(); this.render(); this.startTaskAssignmentChecks(); if(this.state.debt) { this.startInterestCalculation(); } },
attachEventListeners() {
document.getElementById('enable-sound-btn').addEventListener('click', async () => { await Tone.start(); this.state.soundInitialized = true; this.saveState(); this.hideModal('sound-modal'); this.runStartupLogic(); });
document.getElementById('manage-btn').addEventListener('click', () => { this.renderManagement(); this.showModal('manage-modal');});
document.getElementById('shop-btn').addEventListener('click', () => { if (this.state.compliance <= 50 && !this.state.debt) { this.showResult('Shop Privileges Revoked!', `Compliance is at ${this.state.compliance}%. Get it above 50% to regain access.`); } else { this.renderShopModal(true); this.showModal('shop-modal'); } });
document.querySelectorAll('.close-modal-btn').forEach(btn => btn.addEventListener('click', e => this.hideModal(e.target.dataset.modal)));
document.querySelectorAll('#manage-modal .tab').forEach(tab => {
tab.addEventListener('click', (e) => this.switchTab(e.target));
});
document.getElementById('main-content').addEventListener('click', e => { if (e.target.id === 'complete-task-btn') this.completeTask(); if (e.target.id === 'skip-task-btn') this.skipTask(); if (e.target.id === 'complete-forfeit-btn') this.completeForfeit(); if (e.target.id === 'halve-forfeit-btn') this.halveForfeit(); });
document.getElementById('result-ok-btn').addEventListener('click', () => { this.hideModal('result-modal'); this.checkAndAssignTask(); this.render(); });
document.querySelectorAll('.feedback-btn').forEach(btn => btn.addEventListener('click', e => this.handleForfeitFeedback(e.target.dataset.feedback)));
document.querySelectorAll('.failure-reason-btn').forEach(btn => btn.addEventListener('click', e => this.handleFailureReason(e.target.dataset.reason)));
document.getElementById('edit-cancel-btn').addEventListener('click', () => this.hideModal('edit-modal'));
document.getElementById('edit-save-btn').addEventListener('click', () => this.handleSave());
document.getElementById('shop-modal').addEventListener('click', e => { if (e.target.classList.contains('redeem-reward-btn')) this.redeemReward(e); if (e.target.classList.contains('borrow-reward-btn')) this.borrowReward(e); if (e.target.id === 'volunteer-task-btn') this.volunteerForTask(); });
document.getElementById('log-toggle-btn').addEventListener('click', e => { const i = e.target.textContent.includes('History'); this.renderShopModal(!i); });
},
playNotificationSound() { if (this.state.soundInitialized) { const synth = new Tone.Synth().toDestination(); synth.triggerAttackRelease("C5", "8n"); } },
playCompleteSound() { if (!this.state.soundInitialized) return; const synth = new Tone.Synth().toDestination(); const now = Tone.now(); synth.triggerAttackRelease("C5", "8n", now); synth.triggerAttackRelease("G4", "8n", now + 0.3); synth.triggerAttackRelease("G4", "8n", now + 0.6); },
checkDateAndReset() { /* ... unchanged ... */ const today = dayjs().format('YYYY-MM-DD'); if (this.state.lastTaskDate !== today) { if(this.state.lastComplianceCheckDate) { const lastCheck = dayjs(this.state.lastComplianceCheckDate).startOf('day'); const yesterday = dayjs().startOf('day').subtract(1, 'day'); if(lastCheck.isSame(yesterday)) { if (this.state.compliance === 100) { if (!this.state.perfectWeekStart) { this.state.perfectWeekStart = this.state.lastComplianceCheckDate; } if (dayjs(this.state.perfectWeekStart).diff(dayjs(), 'day') >= 7) { this.state.hasPerfectWeekBadge = true; } } else { this.state.perfectWeekStart = null; this.state.hasPerfectWeekBadge = false; } } } this.state.lastComplianceCheckDate = today; this.state.lastTaskDate = today; this.state.dailyTasksAssigned = 0; const baseTasks = this.state.tasksPerDay; const variation = Math.round(baseTasks * 0.2); const randomVariation = Math.floor(Math.random() * (variation * 2 + 1)) - variation; let target = Math.max(1, Math.min(24, baseTasks + randomVariation)); let bonusTasks = 0; if (this.state.compliance <= 80) bonusTasks = 1; if (this.state.compliance <= 60) bonusTasks = 2; if (this.state.compliance <= 40) bonusTasks = 3; if (this.state.compliance <= 20) bonusTasks = 4; if (this.state.compliance <= 0) bonusTasks = 5; target += bonusTasks; this.state.targetTasksToday = Math.min(24, target); console.log(`New day started. Base tasks: ${baseTasks}, Variation: ${randomVariation}, Compliance bonus: ${bonusTasks}. Final target tasks for today: ${this.state.targetTasksToday}`); this.saveState(); } else if (!this.state.targetTasksToday || this.state.targetTasksToday < this.state.tasksPerDay) { const baseTasks = this.state.tasksPerDay; let target = baseTasks; let bonusTasks = 0; if (this.state.compliance <= 80) bonusTasks = 1; if (this.state.compliance <= 60) bonusTasks = 2; if (this.state.compliance <= 40) bonusTasks = 3; if (this.state.compliance <= 20) bonusTasks = 4; if (this.state.compliance <= 0) bonusTasks = 5; target += bonusTasks; this.state.targetTasksToday = Math.min(24, target); console.log(`Recalculated target tasks for today: ${this.state.targetTasksToday}`); this.saveState(); } },
checkAndAdjustPrices() { /* ... unchanged ... */ const t=dayjs().format('YYYY-MM-DD');if(this.state.lastPriceCheckDate&&this.state.lastPriceCheckDate!==t){ if(this.state.rewardsRedeemedToday>=3){this.state.saleIsOn=false;}else if(this.state.rewardsRedeemedToday===0){this.state.saleIsOn=true;}else{this.state.saleIsOn=false;} this.state.rewardsRedeemedToday=0;} this.state.lastPriceCheckDate=t; this.saveState(); },
checkStreak() { /* ... unchanged ... */ if (!this.state.lastStreakDate) return; const today = dayjs().startOf('day'); const lastDate = dayjs(this.state.lastStreakDate).startOf('day'); if (today.diff(lastDate, 'day') > 1) { this.state.currentStreak = 0; } },
isWithinScheduledTime() { /* ... unchanged ... */ const n=dayjs(),d=n.day(),s=this.state.schedule[d]; if(!s||s.start==='00:00'&&s.end==='00:00'){return false;} const t=dayjs(s.start,'HH:mm'),e=dayjs(s.end,'HH:mm'); if(e.isBefore(t)){return n.isAfter(t)||n.isBefore(e);}else{return n.isAfter(t)&&n.isBefore(e);} },
startTaskAssignmentChecks() { /* ... unchanged ... */ clearInterval(this.taskAssignmentInterval); const interval = this.state.debt ? 3000 : 60000; this.taskAssignmentInterval = setInterval(() => { this.checkAndAssignTask(); }, interval); console.log(`Task assignment interval started (checking every ${interval/1000}s).`); },
checkAndAssignTask() { /* ... unchanged ... */ if (this.state.activeTask) return; if (!this.isWithinScheduledTime()) { this.render(); return; } if (this.state.dailyTasksAssigned >= this.state.targetTasksToday) { this.render(); return; } const tasksRemaining = this.state.targetTasksToday - this.state.dailyTasksAssigned; const now = dayjs(); const currentDay = now.day(); const schedule = this.state.schedule[currentDay]; let endTime = dayjs(schedule.end, 'HH:mm'); if (endTime.isBefore(dayjs(schedule.start, 'HH:mm'))) { endTime = endTime.add(1, 'day'); } const minutesRemaining = Math.max(1, endTime.diff(now, 'minute')); const baseProb = tasksRemaining / minutesRemaining; const assignProbability = Math.min(1, (this.state.debt ? 1.0 : baseProb * 2)); if (Math.random() < assignProbability) { this.assignTask(); this.render(); } else { this.render(); } },
assignTask(isVolunteer = false) {
let taskToAssign = null;
if (isVolunteer) {
const allEnabledTasks = this.state.tasks.filter(t => t.enabled);
if (allEnabledTasks.length > 0) {
taskToAssign = allEnabledTasks[Math.floor(Math.random() * allEnabledTasks.length)];
}
} else {
const today = dayjs().day();
const scheduledTasks = this.state.tasks.filter(t => t.enabled && parseInt(t.scheduledDay) === today);
const randomTasks = this.state.tasks.filter(t => t.enabled && t.scheduledDay === 'any');
let scheduledTaskIndex = this.state.dailyTasksAssigned;
if (scheduledTaskIndex < scheduledTasks.length) { taskToAssign = scheduledTasks[scheduledTaskIndex]; }
else if (randomTasks.length > 0) { taskToAssign = randomTasks[Math.floor(Math.random() * randomTasks.length)]; }
}
if (taskToAssign) {
let baseName = taskToAssign.name; let value = taskToAssign.value; let unit = taskToAssign.unit;
let nameString = (unit === 'area') ? `${baseName}` : `${value} ${unit} ${baseName}`;
let points = isVolunteer ? Math.ceil(taskToAssign.points / 2) : taskToAssign.points;
this.state.activeTask = { id: taskToAssign.id, name: nameString, assignedAt: Date.now(), type: 'task', points: points, originalValue: value, baseName: baseName, unit: unit, taskStartTime: Date.now(), isVolunteer: isVolunteer };
if (!isVolunteer) { this.state.dailyTasksAssigned++; }
this.playNotificationSound();
this.saveState();
}
},
volunteerForTask() { /* ... unchanged ... */ if (this.state.activeTask) { this.showResult("Error", "You already have an assignment, maggot!"); return; } this.assignTask(true); this.hideModal('shop-modal'); this.render(); setTimeout(() => { this.showResult("You want work?!", "You got it! Complete this task for half points. Get to it!"); }, 100); },
completeTask() { /* ... (DEBT FIX HERE) ... */
clearInterval(this.timerInterval); this.timerInterval = null;
const task = this.state.activeTask;
let pointsGained = task.points;
let detailsMessage = '';
const totalDuration = this.state.taskDurationHours * 3600000;
const timeTaken = Date.now() - task.taskStartTime;
if (timeTaken <= totalDuration * 0.25) {
pointsGained = Math.round(pointsGained * 1.5); // 1.5x points
detailsMessage += `<p class="text-brand-success mt-2">⚡ SPEED RUN! +50% Points Bonus!</p>`;
}
const originalTask = this.state.tasks.find(t => t.id === task.id);
if(originalTask) { originalTask.consecutiveSuccesses=(originalTask.consecutiveSuccesses||0)+1; if(originalTask.consecutiveSuccesses>=3&&originalTask.unit!=='area'){const o=originalTask.value;originalTask.value=Math.ceil(o*1.1);originalTask.consecutiveSuccesses=0;detailsMessage+=`<p class="text-brand-success mt-2">✨ ${originalTask.name} leveled up! New goal: ${originalTask.value} ${originalTask.unit}.</p>`;} }
const today=dayjs().format('YYYY-MM-DD'),yesterday=dayjs().subtract(1,'day').format('YYYY-MM-DD'); if(this.state.lastStreakDate!==today){ if(this.state.lastStreakDate===yesterday){this.state.currentStreak++;} else{this.state.currentStreak=1;} this.state.lastStreakDate=today; detailsMessage+=`<p class="text-yellow-400 mt-2">🔥 Daily Streak: ${this.state.currentStreak} day(s)!</p>`;}
this.state.totalTasksCompleted++; if(this.state.totalTasksCompleted>0&&this.state.totalTasksCompleted%20===0){this.state.skipPasses++;detailsMessage+=`<p class="text-blue-400 mt-2">🎟️ You earned a Skip Pass!</p>`;}
if (this.state.compliance === 100) { if (!this.state.perfectWeekStart) { this.state.perfectWeekStart = Date.now(); } if (Date.now() - this.state.perfectWeekStart >= 7 * 24 * 60 * 60 * 1000) { if(!this.state.hasPerfectWeekBadge) detailsMessage += `<p class="text-yellow-400 mt-2">🎖️ PERFECT WEEK badge earned!</p>`; this.state.hasPerfectWeekBadge = true; } }
else { this.state.perfectWeekStart = null; this.state.hasPerfectWeekBadge = false; }
let message;
if (this.state.debt) {
let pointsApplied = pointsGained;
this.state.debt.amountOwed -= pointsApplied;
message = `Task complete. You are in DEBT LOCKDOWN. ${pointsApplied} points applied to your debt.`;
if (this.state.debt.amountOwed <= 0) {
const leftoverPoints = Math.abs(this.state.debt.amountOwed);
this.state.points += leftoverPoints;
const settledDebtName = this.state.debt.rewardName;
this.state.debt = null;
clearInterval(this.interestInterval); this.interestInterval = null;
detailsMessage += `<p class="text-brand-success mt-4 font-bold">DEBT SETTLED! Your debt for "${settledDebtName}" is paid off! You earned ${leftoverPoints} points.</p>`;
this.startTaskAssignmentChecks();
}
} else {
this.state.points += pointsGained;
message = `You earned ${pointsGained} points. Compliance is now ${this.state.compliance}%.`;
}
this.state.compliance = Math.min(100, this.state.compliance + 10);
this.state.activeTask = null;
this.saveState();
this.playCompleteSound();
this.render();
setTimeout(() => { // DEFERRED POPUP
this.showResult('Task Complete!', message, detailsMessage);
}, 100);
},
skipTask() { /* ... unchanged ... */ if (this.state.skipPasses > 0) { clearInterval(this.timerInterval); this.timerInterval = null; this.state.skipPasses--; this.state.activeTask = null; this.saveState(); this.showResult('Task Skipped', `You used a skip pass. You have ${this.state.skipPasses} remaining.`); this.render(); } },
failTaskByTimer() { if (!this.state.activeTask || this.state.activeTask.type !== 'task') return; clearInterval(this.timerInterval); this.timerInterval = null; this.state.failureResult = { task: {...this.state.activeTask} }; this.hideModal('result-modal'); this.showModal('failure-modal'); },
handleFailureReason(reason) { /* ... unchanged ... */ const failedTask = this.state.failureResult.task; let detailsMessage = ''; const originalTask = this.state.tasks.find(t => t.id === failedTask.id); if(originalTask) { originalTask.consecutiveSuccesses = 0; if (originalTask.unit !== 'area') { const o=originalTask.value; originalTask.value = Math.max(1, Math.floor(o * 0.9)); detailsMessage = `<p class="text-brand-danger mt-2">🔻 ${originalTask.name} difficulty reduced to ${originalTask.value} ${originalTask.unit}.</p>`; } } const newCompliance = Math.max(0, this.state.compliance - this.state.compliancePenalty); this.state.failureLog.unshift({ taskName: failedTask.name, reason: reason, failedAt: Date.now() }); this.state.compliance = newCompliance; this.state.currentStreak = 0; this.state.hasPerfectWeekBadge = false; this.state.perfectWeekStart = null; const newForfeit = this.calculateForfeit(); let forfeitMessage = ''; if (newCompliance <= 25) { const newForfeit2 = this.calculateForfeit(); this.state.activeTask = newForfeit; if(newForfeit2) this.state.forfeitQueue = [newForfeit2]; forfeitMessage = "Your performance is pathetic. You get TWO forfeits."; } else { this.state.activeTask = newForfeit; this.state.forfeitQueue = []; forfeitMessage = "A forfeit has been assigned."; } this.saveState(); this.hideModal('failure-modal'); this.render(); if (newForfeit) { this.showResult("Task Failed!", `${forfeitMessage} Compliance is now ${this.state.compliance}%. Your streak is broken.`, detailsMessage); } else { this.showResult('Task Failed!', `No forfeits are enabled, you got lucky. Compliance is now ${this.state.compliance}%. Your streak is broken.`, detailsMessage); } },
calculateForfeit() { /* ... unchanged ... */ const t=this.state.forfeits.tasks.filter(t=>t.enabled),p=this.state.forfeits.positions.filter(p=>p.enabled);if(t.length===0||p.length===0){return null;} const r=t[Math.floor(Math.random()*t.length)],o=p[Math.floor(Math.random()*p.length)]; let duration = this.state.forfeits.baseDuration; if (this.state.compliance <= 50) { duration *= 2; } return{id:`f-${Date.now()}`,name:r.name,position:o.name,currentDuration:duration,assignedAt:Date.now(),type:'forfeit', lastPenaltyHour: 0, baseDurationOnAssign: duration}; },
completeForfeit() { /* ... unchanged ... */ clearInterval(this.timerInterval); this.timerInterval = null; this.showModal('feedback-modal'); },
halveForfeit() { /* ... unchanged ... */ if (this.state.skipPasses > 0 && this.state.activeTask && this.state.activeTask.type === 'forfeit') { this.state.skipPasses--; this.state.activeTask.currentDuration = Math.ceil(this.state.activeTask.currentDuration / 2); this.state.activeTask.assignedAt = Date.now(); this.state.activeTask.lastPenaltyHour = 0; this.saveState(); this.render(); } },
handleForfeitFeedback(f){ /* ... unchanged ... */ this.state.forfeits.baseDuration = Math.round(this.state.forfeits.baseDuration * 1.1); let feedbackMsg = `Base forfeit duration increased to ${dayjs.duration(this.state.forfeits.baseDuration, 'seconds').format('m [minutes]')}.`; this.state.activeTask = null; if (this.state.forfeitQueue && this.state.forfeitQueue.length > 0) { this.state.activeTask = this.state.forfeitQueue.shift(); feedbackMsg += " Your next forfeit has been assigned. Move it!"; } this.hideModal('feedback-modal'); this.saveState(); this.showResult('Forfeit Complete', feedbackMsg); this.render(); },
borrowReward(e) { /* ... (DEFERRED POPUP) ... */ if (this.state.debt) { this.showResult('Error', 'You already have an outstanding debt. Pay it off first.'); return; } const { id } = e.target.dataset; const reward = this.state.rewards.find(r => r.id === id); const displayPrice = this.state.saleIsOn ? Math.max(1, Math.round(reward.points * 0.95)) : reward.points; if (reward && this.state.points < displayPrice) { const now = Date.now(); this.state.debt = { rewardId: reward.id, rewardName: reward.name, initialCost: displayPrice, amountOwed: displayPrice - this.state.points, borrowedAt: now, lastInterestCalc: now }; this.state.points = 0; this.saveState(); this.startInterestCalculation(); this.render(); this.hideModal('shop-modal'); setTimeout(() => { this.showResult('Reward Borrowed', `You borrowed "${reward.name}". You now owe ${this.state.debt.amountOwed} points. Interest will accrue hourly.`); }, 100); } },
startInterestCalculation() { /* ... (1% INTEREST) ... */ clearInterval(this.interestInterval); this.interestInterval = setInterval(() => { if (!this.state.debt) { clearInterval(this.interestInterval); return; } const now = Date.now(); const hoursPassed = Math.floor((now - this.state.debt.lastInterestCalc) / 3600000); if (hoursPassed > 0) { for (let i = 0; i < hoursPassed; i++) { this.state.debt.amountOwed = Math.ceil(this.state.debt.amountOwed * 1.01); } this.state.debt.lastInterestCalc = now; this.saveState(); this.renderHeader(); console.log(`Interest applied. New debt: ${this.state.debt.amountOwed}`); } }, 300000); },
checkDebtRepayment() { /* ... unchanged ... */ if (this.state.debt && this.state.points >= this.state.debt.amountOwed) { this.state.points -= this.state.debt.amountOwed; const settledDebtName = this.state.debt.rewardName; this.state.debt = null; clearInterval(this.interestInterval); this.interestInterval = null; this.saveState(); this.showResult('Debt Settled!', `Your debt for "${settledDebtName}" has been paid off!`); this.startTaskAssignmentChecks(); } },
redeemReward(e){ /* ... (DEFERRED POPUP) ... */ const{id:t}=e.target.dataset,r=this.state.rewards.find(r=>r.id===t); const displayPrice = this.state.saleIsOn ? Math.max(1, Math.round(r.points * 0.95)) : r.points; if(r&&this.state.points>=displayPrice){ this.state.points-=displayPrice; this.state.rewardsRedeemedToday++; this.state.rewardLog.unshift({name:r.name,points:displayPrice,redeemedAt:Date.now()}); this.saveState(); this.render(); this.renderShopModal(true); setTimeout(() => { this.showResult('Reward Claimed!',`You redeemed "${r.name}" for ${displayPrice} points.`); }, 100); } },
render() { this.renderMainContent(); this.renderManagement(); this.renderHeader(); this.renderComplianceBar(); },
renderHeader() { /* ... unchanged ... */ const pointsDebtContainer = document.getElementById('points-debt-display'); if (this.state.debt) { pointsDebtContainer.innerHTML = `<p class="text-brand-subtle text-sm">Debt Owed</p><p id="debt-display" class="text-2xl font-bold text-brand-danger">${Math.ceil(this.state.debt.amountOwed)}</p>`; } else { pointsDebtContainer.innerHTML = `<p class="text-brand-subtle text-sm">Points</p><p id="points-display" class="text-2xl font-bold text-brand-accent">${this.state.points}</p>`; } const s = document.getElementById('streak-display'); if (this.state.currentStreak > 0) { s.innerHTML = `<p class="text-brand-subtle text-sm">Streak</p><p class="text-2xl font-bold text-yellow-400">🔥 ${this.state.currentStreak}</p>`; } else { s.innerHTML = ''; } const b = document.getElementById('badge-display'); if (this.state.hasPerfectWeekBadge) { b.innerHTML = '🎖️'; } else { b.innerHTML = ''; } },
renderComplianceAvatar() { /* ... unchanged ... */ const avatar = document.getElementById('compliance-avatar-main'); if (avatar) { let imgNum = 6; if (this.state.compliance >= 100) imgNum = 1; else if (this.state.compliance >= 90) imgNum = 2; else if (this.state.compliance >= 80) imgNum = 3; else if (this.state.compliance >= 70) imgNum = 4; else if (this.state.compliance >= 60) imgNum = 5; avatar.src = `${imgNum}.png`; } },
renderMainContent() { /* ... unchanged ... */ const mainContent = document.getElementById('main-content'); const avatarHtml = `<img id="compliance-avatar-main" src="1.png" alt="Compliance Status" class="h-24 w-24 md:h-32 md:w-32 rounded border-2 border-gray-600 object-cover flex-shrink-0" onerror="this.src='https://placehold.co/128x128/2c2c2c/a3a3a3?text=Err'">`; let contentHtml = ''; if (this.state.activeTask && this.state.activeTask.type === 'task') { contentHtml = this.getTaskHtml(); } else if (this.state.activeTask && this.state.activeTask.type === 'forfeit') { contentHtml = this.getForfeitHtml(); } else { if (!this.isWithinScheduledTime()) { contentHtml = `<div class="text-left flex-grow"><h2 class="text-2xl font-bold text-brand-text mb-2">Outside Active Hours.</h2><p class="text-brand-subtle">Standing by.</p></div>`; } else if (this.state.dailyTasksAssigned >= this.state.targetTasksToday) { contentHtml = `<div class="text-left flex-grow"><h2 class="text-2xl font-bold text-brand-text mb-2">All Tasks Assigned.</h2><p class="text-brand-subtle">Check back tomorrow.</p></div>`; } else { const e=this.state.tasks.filter(t=>t.enabled); if(e.length===0){contentHtml = `<div class="text-left flex-grow"><h2 class="text-2xl font-bold text-brand-warning mb-2">No Tasks Enabled</h2><p class="text-brand-subtle">Go to 'Manage' and add tasks to your roster.</p></div>`;} else{contentHtml = `<div class="text-left flex-grow"><h2 class="text-2xl font-bold text-brand-text mb-2">Await Orders.</h2><p class="text-brand-subtle">Stand by for your next assignment.</p></div>`;} } } mainContent.innerHTML = `<div class="flex items-center space-x-6 w-full">${avatarHtml}${contentHtml}</div>`; this.renderComplianceAvatar(); this.startTimerIfNeeded(); },
getTaskHtml() { /* ... unchanged ... */ const t = this.state.activeTask; const skipButton = this.state.skipPasses > 0 ? `<button id="skip-task-btn" class="btn bg-gray-600 hover:bg-gray-500 text-white font-bold py-3 px-6 rounded-lg text-sm">Skip Task (${this.state.skipPasses})</button>` : ''; return `<div class="text-left flex-grow"> <p class="text-brand-subtle mb-1 text-sm">Your Orders:</p> <h2 class="text-2xl md:text-3xl font-bold text-brand-text mb-3" id="task-name-display">${t.name}</h2> <div class="text-lg text-brand-warning mb-4" id="timer">Calculating...</div> <div class="flex justify-start space-x-4"> <button id="complete-task-btn" class="btn bg-brand-success hover:bg-green-500 text-white font-bold py-3 px-6 rounded-lg text-sm">Complete</button> ${skipButton} </div> </div>`; },
getForfeitHtml() { /* ... unchanged ... */ const f = this.state.activeTask; const skipButton = this.state.skipPasses > 0 ? `<button id="halve-forfeit-btn" class="btn bg-yellow-600 hover:bg-yellow-500 text-white font-bold py-3 px-6 rounded-lg text-sm">Use Skip Pass to Halve Duration (${this.state.skipPasses})</button>` : ''; return `<div class="text-left flex-grow"> <p class="text-brand-subtle mb-1 text-sm">Your Forfeit:</p> <h2 class="text-2xl md:text-3xl font-bold text-brand-danger mb-2">${f.name}</h2> <p class="text-lg text-brand-subtle mb-2">Position: ${f.position}</p> <div class="text-lg text-brand-warning mb-1" id="forfeit-timer">Calculating...</div> <p id="total-forfeit-duration" class="text-xs text-brand-warning mb-4"></p> <div class="flex flex-col sm:flex-row space-y-3 sm:space-y-0 sm:space-x-4"> <button id="complete-forfeit-btn" class="btn bg-brand-accent hover:bg-brand-accent-hover text-white font-bold py-3 px-6 rounded-lg text-sm">Forfeit Completed</button> ${skipButton} </div></div>`; },
startTimerIfNeeded() { /* ... unchanged ... */ clearInterval(this.timerInterval); this.timerInterval = null; const activeTask = this.state.activeTask; if (activeTask?.type === 'task') { const timerEl = document.getElementById('timer'); const taskNameEl = document.getElementById('task-name-display'); if(!timerEl || !taskNameEl) return; const totalDuration = this.state.taskDurationHours * 3600000; const deadline = activeTask.assignedAt + totalDuration; let lastWhistleTime = Date.now(); this.timerInterval = setInterval(() => { const now = Date.now(); const timeLeft = deadline - now; if (timeLeft <= 0) { clearInterval(this.timerInterval); this.failTaskByTimer(); } else { const h=Math.floor(timeLeft/36e5),m=Math.floor(timeLeft%36e5/6e4),c=Math.floor(timeLeft%6e4/1e3); timerEl.textContent=`Time left: ${h}h ${m}m ${c}s`; if (now - lastWhistleTime >= 1800000) { this.playNotificationSound(); lastWhistleTime = now; } if (activeTask.unit !== 'area') { const elapsedPercent = (totalDuration - timeLeft) / totalDuration; const multiplier = 1.0 + elapsedPercent; const newScaledValue = Math.ceil(activeTask.originalValue * multiplier); const newName = `${newScaledValue} ${activeTask.unit} ${activeTask.baseName}`; if (taskNameEl.textContent !== newName) { taskNameEl.textContent = newName; this.state.activeTask.name = newName; } } } }, 1000); } else if (activeTask?.type === 'forfeit') { const f = activeTask; const baseDuration = this.state.forfeits.baseDuration; const timerEl = document.getElementById('forfeit-timer'); const totalDurationEl = document.getElementById('total-forfeit-duration'); if(!timerEl || !totalDurationEl) return; const updateTimerDisplay = () => { const now = Date.now(); let deadline = f.assignedAt + (f.currentDuration * 1000); const timeLeft = deadline - now; const currentTotalDurationStr = dayjs.duration(f.currentDuration, 'seconds').format('H[h] m[m] s[s]'); totalDurationEl.textContent = `(Total required: ${currentTotalDurationStr})`; if (timeLeft > 0) { const h=Math.floor(timeLeft/36e5),m=Math.floor(timeLeft%36e5/6e4),s=Math.floor(timeLeft%6e4/1e3); timerEl.textContent = `Time left: ${h}h ${m}m ${s}s`; timerEl.classList.remove('text-brand-danger'); timerEl.classList.add('text-brand-warning'); } else { const overdue = now - deadline; const oh=Math.floor(overdue/36e5),om=Math.floor(overdue%36e5/6e4),os=Math.floor(overdue%6e4/1E3); timerEl.textContent = `Overdue by: ${oh}h ${om}m ${os}s`; timerEl.classList.add('text-brand-danger'); timerEl.classList.remove('text-brand-warning'); const originalDeadline = f.assignedAt + (baseDuration * 1000); const totalOverdueMillis = Math.max(0, now - originalDeadline); const hoursOverdue = Math.floor(totalOverdueMillis / 3600000); if (hoursOverdue >= (f.lastPenaltyHour || 0) + 1) { const penaltyMultiplier = hoursOverdue; f.currentDuration = baseDuration * (1 + penaltyMultiplier); f.lastPenaltyHour = hoursOverdue; this.saveState(); console.log(`Forfeit duration increased to ${f.currentDuration} seconds`); } } }; updateTimerDisplay(); this.timerInterval = setInterval(updateTimerDisplay, 1000); } },
renderComplianceBar() { /* ... unchanged ... */ const bar = document.getElementById('compliance-bar'); const text = document.getElementById('compliance-text'); if (bar && text) { bar.style.width = `${this.state.compliance}%`; text.textContent = `Compliance: ${this.state.compliance}%`; } },
switchTab(e_target){ /* ... unchanged ... */ document.querySelectorAll('.tab').forEach(t=>t.classList.remove('tab-active')); e_target.classList.add('tab-active'); document.querySelectorAll('.tab-content').forEach(c=>c.classList.add('hidden')); document.getElementById(`${e_target.dataset.tab}-tab`).classList.remove('hidden'); this.renderManagement();},
renderManagement(){ /* ... unchanged ... */ const a=document.querySelector('#manage-modal .tab-active').dataset.tab; if(a==='tasks')this.renderList('tasks',this.state.tasks,['name','value','unit','points','scheduledDay']); if(a==='rewards')this.renderList('rewards',this.state.rewards,['name','points']); if(a==='forfeits')this.renderForfeitsTab(); if(a==='schedule')this.renderScheduleTab(); if(a==='stats')this.renderStatsTab(); if(a==='settings')this.renderSettingsTab(); },
renderShopModal(s){ /* ... (Sale price fix) ... */ const c=document.getElementById('shop-content'), t=document.getElementById('log-toggle-btn'); if(s) { const a = this.state.saleIsOn ? `<div class="bg-green-500/20 border border-green-500 text-green-300 text-center p-3 rounded-lg mb-4">🔥 **REWARD SALE ON!** All prices are 5% off today! 🔥</div>`:''; const hasDebt = !!this.state.debt; const debtMessage = hasDebt ? `<div class="bg-red-500/20 border border-red-500 text-red-300 text-center p-3 rounded-lg mb-4">Shop access suspended until debt of ${Math.ceil(this.state.debt.amountOwed)} points is settled.</div>` : ''; const enabledRewards = this.state.rewards.filter(r => r.enabled); const cheapestRewardCost = enabledRewards.length > 0 ? Math.min(...enabledRewards.map(r => this.state.saleIsOn ? Math.max(1, Math.round(r.points * 0.95)) : r.points)) : Infinity; const i=enabledRewards.map(item=>{ const displayPrice = this.state.saleIsOn ? Math.max(1, Math.round(item.points * 0.95)) : item.points; const n=this.state.points >= displayPrice; const canBorrow = !hasDebt && !n; let buttonHtml = ''; if (n) { buttonHtml = `<button class="btn redeem-reward-btn bg-brand-accent hover:bg-brand-accent-hover text-white font-bold py-1 px-3 rounded-md text-sm" data-id="${item.id}" ${hasDebt ? 'disabled' : ''}>Redeem</button>`; } else if (canBorrow) { buttonHtml = `<button class="btn borrow-reward-btn bg-yellow-600 hover:bg-yellow-500 text-white font-bold py-1 px-3 rounded-md text-sm" data-id="${item.id}">Borrow</button>`; } else { buttonHtml = `<button class="btn bg-gray-600 text-white font-bold py-1 px-3 rounded-md text-sm" data-id="${item.id}" disabled>Unavailable</button>`; } return` <div class="flex items-center justify-between p-3 rounded-lg bg-brand-primary mb-2 ${!n && !canBorrow ? 'opacity-50' : ''}"> <div> <p class="font-semibold">${item.name}</p> <p class="text-sm font-bold ${this.state.saleIsOn?'text-green-400':'text-brand-accent'}">${displayPrice} Points</p> </div> ${buttonHtml} </div>`; }).join(''); let volunteerButtonHtml = ''; if (!hasDebt && this.state.points < cheapestRewardCost) { const canVolunteer = !this.state.activeTask; volunteerButtonHtml = `<div class="text-center p-3 bg-brand-primary rounded-lg mt-6"> <p class="text-brand-subtle mb-3">You can't afford any rewards. Volunteer for an extra task to earn points.</p> <button id="volunteer-task-btn" class="btn w-full bg-brand-warning hover:bg-yellow-400 text-black font-bold py-2 px-4 rounded-lg" ${!canVolunteer ? 'disabled' : ''}> ${canVolunteer ? 'Volunteer for Task (Half Points)' : 'Complete active task first!'} </button> </div>`; } c.innerHTML= debtMessage + a + (i.length > 0 ? i : '<p class="text-brand-subtle">No rewards enabled. Go to Manage -> Rewards.</p>') + volunteerButtonHtml; t.textContent='View Purchase History'; } else { let l='<h3 class="text-lg font-bold mb-4">Purchase History</h3>';if(this.state.rewardLog.length===0){l+='<p class="text-brand-subtle">You haven\'t purchased any rewards yet.</p>';}else{l+=this.state.rewardLog.map(log=>` <div class="flex justify-between items-center p-2 bg-brand-primary rounded-md mb-2"> <div> <p class="font-semibold">${log.name}</p> <p class="text-xs text-brand-subtle">${dayjs(log.redeemedAt).fromNow()}</p> </div> <p class="font-bold text-brand-accent">${log.points}</p> </div>`).join('');}c.innerHTML=l;t.textContent='Back to Shop'; } },
renderList(t,i,f){/* ... unchanged ... */ const c=document.getElementById(`${t}-tab`),h=i.map(item=>{ let details = f.slice(1).map(k=>{ if(k==='scheduledDay'){const days=['Sun','Mon','Tue','Wed','Thu','Fri','Sat']; return `Schedule: ${item[k]==='any'?'Any':days[item[k]]}`;} else { return `${k.charAt(0).toUpperCase()+k.slice(1)}: ${item[k]}`; } }).join(', '); return `<div class="flex items-center justify-between p-3 rounded-lg bg-brand-primary mb-2"><div class="flex items-center"><label class="flex items-center cursor-pointer mr-4"><input type="checkbox" class="form-checkbox h-5 w-5 text-brand-accent bg-gray-800 border-gray-600 rounded focus:ring-brand-accent" data-type="${t}" data-id="${item.id}" ${item.enabled?'checked':''}></label><div><p class="font-semibold ${item.enabled?'':'line-through text-gray-500'}">${item.name}</p><p class="text-sm text-brand-subtle">${details}</p></div></div><div><button class="edit-item-btn text-brand-subtle hover:text-white mr-2" data-type="${t}" data-id="${item.id}">Edit</button><button class="delete-item-btn text-brand-danger hover:text-red-400" data-type="${t}" data-id="${item.id}">Delete</button></div></div>`; }).join('');c.innerHTML=`<div class="mb-6">${h}</div><button class="add-item-btn w-full btn bg-brand-accent hover:bg-brand-accent-hover text-white font-bold py-3 px-4 rounded-lg" data-type="${t}">Add New ${t.slice(0,-1)}</button>`;c.querySelectorAll('.form-checkbox').forEach(b=>b.addEventListener('change',e=>this.toggleEnable(e)));c.querySelectorAll('.delete-item-btn').forEach(b=>b.addEventListener('click',e=>this.deleteItem(e)));c.querySelectorAll('.add-item-btn').forEach(b=>b.addEventListener('click',e=>this.handleAddItem(e)));c.querySelectorAll('.edit-item-btn').forEach(b=>b.addEventListener('click',e=>this.handleEditItem(e)));},
renderForfeitsTab(){/* ... unchanged ... */ const c=document.getElementById('forfeits-tab'),{tasks:t,positions:p}=this.state.forfeits,r=(s,l)=>l.map(i=>`<div class="flex items-center justify-between p-3 rounded-lg bg-brand-primary mb-2"><div class="flex items-center"><label class="flex items-center cursor-pointer mr-4"><input type="checkbox" class="form-checkbox h-5 w-5 text-brand-accent bg-gray-800 border-gray-600 rounded focus:ring-brand-accent" data-type="forfeits" data-subtype="${s}" data-id="${i.id}" ${i.enabled?'checked':''}></label><p class="font-semibold ${i.enabled?'':'line-through text-gray-500'}">${i.name}</p></div><div><button class="delete-item-btn text-brand-danger hover:text-red-400" data-type="forfeits" data-subtype="${s}" data-id="${i.id}">Delete</button></div></div>`).join('');c.innerHTML=`<div class="mb-4"><h3 class="text-lg font-semibold mb-2">Forfeit Tasks</h3>${r('tasks',t)}<button class="add-item-btn w-full btn bg-brand-accent hover:bg-brand-accent-hover text-white font-bold py-2 px-4 rounded-lg mt-2" data-type="forfeits" data-subtype="tasks">Add New Task</button></div><div class="mb-4"><h3 class="text-lg font-semibold mb-2">Forfeit Positions</h3>${r('positions',p)}<button class="add-item-btn w-full btn bg-brand-accent hover:bg-brand-accent-hover text-white font-bold py-2 px-4 rounded-lg mt-2" data-type="forfeits" data-subtype="positions">Add New Position</button></div>`;c.querySelectorAll('.form-checkbox').forEach(b=>b.addEventListener('change',e=>this.toggleEnable(e)));c.querySelectorAll('.delete-item-btn').forEach(b=>b.addEventListener('click',e=>this.deleteItem(e)));c.querySelectorAll('.add-item-btn').forEach(b=>b.addEventListener('click',e=>this.handleAddItem(e)));},
renderScheduleTab() { /* ... unchanged ... */ const c=document.getElementById('schedule-tab'); const days=['Sunday','Monday','Tuesday','Wednesday','Thursday','Friday','Saturday']; let h='<p class="text-brand-subtle mb-4">Set start/end times for tasks each day. Set both to 00:00 to disable tasks.</p>'; days.forEach((day,idx)=>{ const ds=this.state.schedule[idx]||{start:'00:00',end:'00:00'}; h+=`<div class="bg-brand-primary p-4 rounded-lg mb-3"><label class="block text-lg font-semibold mb-2">${day}</label><div class="flex space-x-4"><div class="flex-1"><label class="block text-sm text-brand-subtle mb-1">Start Time</label><input type="time" id="start-${idx}" value="${ds.start}" class="w-full bg-brand-secondary text-white p-2 rounded-md border border-gray-600"></div><div class="flex-1"><label class="block text-sm text-brand-subtle mb-1">End Time</label><input type="time" id="end-${idx}" value="${ds.end}" class="w-full bg-brand-secondary text-white p-2 rounded-md border border-gray-600"></div></div></div>`; }); h+=`<div class="mt-6"><button id="save-schedule-btn" class="w-full btn bg-brand-success hover:bg-green-500 text-white font-bold py-3 px-4 rounded-lg">Save Schedule</button></div>`; c.innerHTML=h; document.getElementById('save-schedule-btn').addEventListener('click',()=>{ days.forEach((_,idx)=>{ const s=document.getElementById(`start-${idx}`).value,e=document.getElementById(`end-${idx}`).value; this.state.schedule[idx]={start:s,end:e}; }); this.saveState(); this.showResult("Schedule Saved","Your task schedule has been updated."); }); },
renderStatsTab() { /* ... unchanged ... */ const container = document.getElementById('stats-tab'); const { failureLog, tasks } = this.state; if (failureLog.length === 0) { container.innerHTML = `<p class="text-brand-subtle">No failure data recorded yet. Keep working.</p>`; return; } const reasonCounts = failureLog.reduce((acc, log) => { acc[log.reason] = (acc[log.reason] || 0) + 1; return acc; }, {}); let reasonHtml = '<h3 class="text-lg font-semibold mb-2">Failure Analysis</h3>'; for (const [reason, count] of Object.entries(reasonCounts)) { reasonHtml += `<div class="flex justify-between p-2 bg-brand-primary rounded mb-2"><span>${reason}</span><span class="font-bold">${count} time(s)</span></div>`; } const taskFailCounts = failureLog.reduce((acc, log) => { acc[log.taskName] = (acc[log.taskName] || 0) + 1; return acc; }, {}); let taskHtml = '<h3 class="text-lg font-semibold mt-6 mb-2">Most Failed Tasks</h3>'; const sortedTasks = Object.entries(taskFailCounts).sort((a, b) => b[1] - a[1]); for (const [taskName, count] of sortedTasks) { taskHtml += `<div class="flex justify-between p-2 bg-brand-primary rounded mb-2"><span>${taskName}</span><span class="font-bold">${count} failure(s)</span></div>`; } container.innerHTML = reasonHtml + taskHtml; },
renderSettingsTab(){/* ... (Lock feature removed) ... */ const c=document.getElementById('settings-tab');c.innerHTML = ` <div class="bg-brand-primary p-4 rounded-lg"><label for="tasks-per-day" class="block text-lg font-semibold mb-2">Tasks Per Day</label><input type="number" id="tasks-per-day" min="1" max="24" value="${this.state.tasksPerDay}" class="w-full bg-brand-secondary text-white p-2 rounded-md border border-gray-600"><p class="text-sm text-brand-subtle mt-2">Set how many tasks you want to be assigned each day (1-24).</p></div> <div class="bg-brand-primary p-4 rounded-lg mt-4"><label for="task-duration" class="block text-lg font-semibold mb-2">Task Duration (hours)</label><input type="number" id="task-duration" min="1" max="24" value="${this.state.taskDurationHours}" class="w-full bg-brand-secondary text-white p-2 rounded-md border border-gray-600"><p class="text-sm text-brand-subtle mt-2">Set how long you have to complete a task.</p></div> <div class="bg-brand-primary p-4 rounded-lg mt-4"><label for="forfeit-duration" class="block text-lg font-semibold mb-2">Base Forfeit Duration (minutes)</label><input type="number" id="forfeit-duration" min="1" value="${this.state.forfeits.baseDuration/60}" class="w-full bg-brand-secondary text-white p-2 rounded-md border border-gray-600"></div> <div class="mt-6"><button id="save-settings-btn" class="w-full btn bg-brand-accent hover:bg-brand-accent-hover text-white font-bold py-3 px-4 rounded-lg">Save Settings</button></div> <hr class="my-6 border-gray-600"> <div class="bg-brand-primary p-4 rounded-lg"> <label class="block text-lg font-semibold mb-2">Backup & Restore</label> <p class="text-sm text-brand-subtle mb-4">Save your settings to a file, or restore them from a backup.</p> <div class="flex space-x-4"> <button id="download-settings-btn" class="flex-1 btn bg-brand-accent hover:bg-brand-accent-hover text-white font-bold py-2 px-4 rounded-lg">Download Settings</a > <label for="upload-settings-input" class="flex-1 btn bg-gray-600 hover:bg-gray-500 text-white font-bold py-2 px-4 rounded-lg cursor-pointer text-center"> Upload Settings </label> <input type="file" id="upload-settings-input" class="hidden" accept=".json"> </div> </div> `; document.getElementById('save-settings-btn').addEventListener('click',()=>{this.state.tasksPerDay=parseInt(document.getElementById('tasks-per-day').value,10);this.state.taskDurationHours=parseInt(document.getElementById('task-duration').value,10);this.state.forfeits.baseDuration=parseInt(document.getElementById('forfeit-duration').value,10)*60;this.saveState();this.showResult("Settings Saved","Your new settings have been applied.");}); document.getElementById('download-settings-btn').addEventListener('click', () => this.handleDownloadSettings()); document.getElementById('upload-settings-input').addEventListener('change', (e) => this.handleUploadSettings(e)); },
toggleEnable(e){/* ... unchanged ... */ const{type:t,id:i,subtype:s}=e.target.dataset;let a;if(t==='forfeits'){a=this.state.forfeits[s].find(item=>item.id===i);}else{a=this.state[t].find(item=>item.id===i);}if(a){a.enabled=e.target.checked;this.saveState();this.renderManagement();}},
deleteItem(e){/* ... unchanged ... */ const{type:t,id:i,subtype:s}=e.target.closest('button').dataset;if(confirm('Are you sure you want to delete this item?')){if(t==='forfeits'){this.state.forfeits[s]=this.state.forfeits[s].filter(item=>item.id!==i);}else{this.state[t]=this.state[t].filter(item=>item.id!==i);}this.saveState();this.renderManagement();}},
handleAddItem(e){/* ... unchanged ... */ const{type:t,subtype:s}=e.target.dataset;this.openEditModal(t,null,s);},
handleEditItem(e){/* ... unchanged ... */ const{type:t,id:i}=e.target.closest('button').dataset;this.openEditModal(t,i);},
openEditModal(t,i,s=null){/* ... unchanged ... */ const n=i===null;let a=n?{}:t==='forfeits'?this.state.forfeits[s].find(item=>item.id===i):this.state[t].find(item=>item.id===i);const o=document.getElementById('edit-form');let d='';let l=`${n?'Add':'Edit'} ${s||t.slice(0,-1)}`;const f={tasks:{name:'text',value:'number',unit:'text',points:'number', scheduledDay:'select'},rewards:{name:'text',points:'number'},forfeits:{name:'text'}};const c=f[t];for(const[k,p]of Object.entries(c)){d+=`<div class="mb-4"><label class="block text-brand-subtle text-sm font-bold mb-2" for="${k}">${k.charAt(0).toUpperCase()+k.slice(1)}</label>`;if(p==='select'){const days=['Any Day (Random)','Sunday','Monday','Tuesday','Wednesday','Thursday','Friday','Saturday'];d+=`<select class="shadow appearance-none border rounded w-full py-2 px-3 bg-brand-primary border-gray-600 text-white" id="${k}">${days.map((day,idx)=>`<option value="${idx===0?'any':idx-1}" ${ (a[k] === (idx === 0 ? 'any' : (idx-1).toString())) ? 'selected':''}>${day}</option>`).join('')}</select>`;}else{d+=`<input class="shadow appearance-none border rounded w-full py-2 px-3 bg-brand-primary border-gray-600 text-white leading-tight focus:outline-none focus:shadow-outline" id="${k}" type="${p}" placeholder="${k}" value="${a[k]||''}">`;}d+='</div>';}o.innerHTML=d;document.getElementById('edit-modal-title').textContent=l;o.dataset.type=t;o.dataset.id=i;if(s)o.dataset.subtype=s;else delete o.dataset.subtype;this.showModal('edit-modal');},
handleSave(){/* ... unchanged ... */ const f=document.getElementById('edit-form'),{type:t,id:i,subtype:s}=f.dataset,n=i==='null';let a=n?{id:`${t.charAt(0)}${Date.now()}`,enabled:true}:{}; if(t==='tasks' && n) a.consecutiveSuccesses=0; if(t==='rewards' && n) a.enabled = true; const d=f.querySelectorAll('input, select');d.forEach(field=>{if(field.type==='number'){a[field.id]=parseInt(field.value,10)||0;}else{a[field.id]=field.value;}});if(n){if(t==='forfeits'){this.state.forfeits[s].push(a);}else{this.state[t].push(a);}}else{let c=t==='forfeits'?this.state.forfeits[s]:this.state[t];const e=c.findIndex(item=>item.id===i);if(e>-1){c[e]={...c[e],...a};}}this.saveState();this.renderManagement();this.hideModal('edit-modal');},
handleDownloadSettings() { /* ... unchanged ... */ try { const dataStr = "data:text/json;charset=utf-8," + encodeURIComponent(JSON.stringify(this.state, null, 2)); const downloadAnchorNode = document.createElement('a'); downloadAnchorNode.setAttribute("href", dataStr); downloadAnchorNode.setAttribute("download", `taskmaster_backup_${dayjs().format('YYYY-MM-DD')}.json`); document.body.appendChild(downloadAnchorNode); downloadAnchorNode.click(); downloadAnchorNode.remove(); } catch (error) { console.error("Error downloading settings:", error); this.showResult("Error", "Could not download settings."); } },
handleUploadSettings(event) {
const file = event.target.files[0];
if (!file) { return; }
if (file.type !== "application/json") {
this.showResult("Error", "Invalid file type. Please upload a .json backup file.");
return;
}
const reader = new FileReader();
reader.onload = (e) => {
try {
this.applyLoadedState(JSON.parse(e.target.result)); // Use smart merge
this.saveState();
this.showResult('Success', 'Settings restored! The app will now reload.');
setTimeout(() => location.reload(), 1500);
} catch (error) {
console.error("Error parsing settings file:", error);
this.showResult("Error", "Invalid or corrupted settings file.");
}
};
reader.readAsText(file);
event.target.value = null; // Clear input to allow re-uploading same file
},
showModal(id){document.getElementById(id).classList.remove('hidden');}, hideModal(id){document.getElementById(id).classList.add('hidden');},
// THIS IS THE FIX. The recursive call is removed.
showResult(t,m,d=''){
document.getElementById('result-title').textContent=t;
document.getElementById('result-message').textContent=m;
document.getElementById('result-details').innerHTML=d;
this.showModal('result-modal'); // Was this.showResult(...)
},
};
document.addEventListener('DOMContentLoaded', () => { App.init(); });
</script>
</body>
</html>