feat: Enhance task filtering, smart scheduling, audit logs and translations
continuous-integration/drone/push Build is passing
continuous-integration/drone/push Build is passing
This commit is contained in:
+82
-40
@@ -160,17 +160,31 @@ CRITICAL: After executing tools, provide a concise summary of your actions.`;
|
||||
return { success: false, error: "Task not found." };
|
||||
}
|
||||
|
||||
const user = await this.storage.getUser(userId);
|
||||
if (!user) {
|
||||
return { success: false, error: "User not found." };
|
||||
}
|
||||
|
||||
// 1. Determine Context (Work vs Personal)
|
||||
let domain = "neutral";
|
||||
if (task.labelId) {
|
||||
const label = await this.storage.getLabel(task.labelId);
|
||||
if (label) {
|
||||
domain = label.domain; // 'work', 'personal', 'neutral'
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Get Availability Config
|
||||
// Fallback to old workHours if availability is missing (backward compatibility)
|
||||
const availability = user.availability || {
|
||||
work: user.workHours || { start: "09:00", end: "17:00", days: [1, 2, 3, 4, 5] },
|
||||
personal: { start: "18:00", end: "22:00", days: [1, 2, 3, 4, 5, 0, 6] }
|
||||
};
|
||||
|
||||
const startAfter = startAfterStr ? new Date(startAfterStr) : new Date();
|
||||
const durationMins = task.estimatedDuration || 60; // Default to 1h if not set
|
||||
const durationMins = task.estimatedDuration || 60;
|
||||
|
||||
const workStartHour = 9;
|
||||
// PRIORITY LOGIC: High priority tasks can be scheduled until 20:00 (8 PM)
|
||||
const workEndHour = task.priority === 'high' ? 20 : 18;
|
||||
|
||||
let scheduledDate: Date | null = null;
|
||||
|
||||
// PLANNED TIME LOGIC: If startDate is set, do not schedule before it.
|
||||
// If starteAfterStr is provided (e.g. "tomorrow"), use the max of both.
|
||||
// PLANNED TIME LOGIC
|
||||
let effectiveStart = startAfter;
|
||||
if (task.startDate) {
|
||||
const plannedStart = new Date(task.startDate);
|
||||
@@ -180,25 +194,35 @@ CRITICAL: After executing tools, provide a concise summary of your actions.`;
|
||||
}
|
||||
|
||||
let currentDay = new Date(effectiveStart);
|
||||
let scheduledDate: Date | null = null;
|
||||
|
||||
// Reset to next slot if passed
|
||||
if (currentDay.getHours() >= workEndHour) {
|
||||
currentDay.setDate(currentDay.getDate() + 1);
|
||||
currentDay.setHours(workStartHour, 0, 0, 0);
|
||||
} else if (currentDay.getHours() < workStartHour) {
|
||||
currentDay.setHours(workStartHour, 0, 0, 0);
|
||||
}
|
||||
// Helper to check if a specific time is within available hours
|
||||
const isTimeAvailable = (date: Date): boolean => {
|
||||
const day = date.getDay();
|
||||
const timeStr = date.toLocaleTimeString('en-US', { hour12: false, hour: '2-digit', minute: '2-digit' });
|
||||
|
||||
for (let dayOffset = 0; dayOffset < 3; dayOffset++) { // Look ahead 3 days
|
||||
// Checks
|
||||
const inSchedule = (sched: { start: string, end: string, days: number[] }) => {
|
||||
if (!sched.days.includes(day)) return false;
|
||||
return timeStr >= sched.start && timeStr < sched.end;
|
||||
};
|
||||
|
||||
if (domain === 'work') return inSchedule(availability.work);
|
||||
if (domain === 'personal') return inSchedule(availability.personal);
|
||||
|
||||
// Neutral: Available in either
|
||||
return inSchedule(availability.work) || inSchedule(availability.personal);
|
||||
};
|
||||
|
||||
// Look ahead 7 days
|
||||
for (let dayOffset = 0; dayOffset < 7; dayOffset++) {
|
||||
const dayStart = new Date(currentDay);
|
||||
dayStart.setHours(workStartHour, 0, 0, 0);
|
||||
dayStart.setHours(0, 0, 0, 0);
|
||||
const dayEnd = new Date(currentDay);
|
||||
dayEnd.setHours(workEndHour, 0, 0, 0);
|
||||
dayEnd.setHours(23, 59, 59, 999);
|
||||
|
||||
// Get all tasks for this day that have a due date (and time)
|
||||
// Fetch tasks for collision detection
|
||||
const allTasks = await this.storage.searchTasks("", userId);
|
||||
|
||||
// Filter for tasks on this day
|
||||
const dayTasks = allTasks.filter(t => {
|
||||
if (!t.dueDate) return false;
|
||||
const d = new Date(t.dueDate);
|
||||
@@ -207,25 +231,44 @@ CRITICAL: After executing tools, provide a concise summary of your actions.`;
|
||||
d.getFullYear() === currentDay.getFullYear();
|
||||
});
|
||||
|
||||
// Find gaps
|
||||
// Sort by time
|
||||
dayTasks.sort((a, b) => (a.dueDate!.getTime() - b.dueDate!.getTime()));
|
||||
|
||||
// Check slots
|
||||
// Start checking from 'currentDay' time (if today) or 9am
|
||||
// Iterate through the day in 15min chunks
|
||||
// Start from 'now' if checking today, otherwise start of day
|
||||
let attemptTime = new Date(currentDay);
|
||||
if (attemptTime < dayStart) attemptTime = dayStart;
|
||||
if (attemptTime < dayStart) attemptTime = dayStart; // Should not happen due to setHours logic but safety
|
||||
|
||||
while (attemptTime.getTime() + (durationMins * 60000) <= dayEnd.getTime()) {
|
||||
const attemptEnd = new Date(attemptTime.getTime() + (durationMins * 60000));
|
||||
// Advance to next 15m slot if needed
|
||||
const remainder = attemptTime.getMinutes() % 15;
|
||||
if (remainder !== 0) {
|
||||
attemptTime.setMinutes(attemptTime.getMinutes() + (15 - remainder));
|
||||
}
|
||||
attemptTime.setSeconds(0, 0);
|
||||
|
||||
// Check collision
|
||||
// Loop until end of day
|
||||
while (attemptTime < dayEnd) {
|
||||
// 1. Check if this START time is within allowed hours
|
||||
if (!isTimeAvailable(attemptTime)) {
|
||||
attemptTime.setMinutes(attemptTime.getMinutes() + 15);
|
||||
continue;
|
||||
}
|
||||
|
||||
// 2. Check if the END time is within allowed hours (don't span into offline time)
|
||||
const attemptEndTime = new Date(attemptTime.getTime() + durationMins * 60000);
|
||||
// We check the end time loosely, or strictly? Strictly ensures we don't work late.
|
||||
// But simplified: check if end is also available (or roughly available)
|
||||
// Let's check the End Time as well.
|
||||
// Note: If schedule is 9-5 and 6-10, a task could technically span 4:30-5:30 if we strictly check 'inSchedule' for all points.
|
||||
// Simplification: Check Start and End.
|
||||
if (!isTimeAvailable(new Date(attemptEndTime.getTime() - 1))) { // Check just before end
|
||||
attemptTime.setMinutes(attemptTime.getMinutes() + 15);
|
||||
continue;
|
||||
}
|
||||
|
||||
// 3. Collision Check
|
||||
const hasCollision = dayTasks.some(t => {
|
||||
const tStart = new Date(t.dueDate!);
|
||||
const tDuration = t.estimatedDuration || 60;
|
||||
const tEnd = new Date(tStart.getTime() + (tDuration * 60000));
|
||||
|
||||
return (attemptTime < tEnd && attemptEnd > tStart);
|
||||
return (attemptTime < tEnd && attemptEndTime > tStart);
|
||||
});
|
||||
|
||||
if (!hasCollision) {
|
||||
@@ -233,15 +276,14 @@ CRITICAL: After executing tools, provide a concise summary of your actions.`;
|
||||
break;
|
||||
}
|
||||
|
||||
// specific increment? 30 mins
|
||||
attemptTime = new Date(attemptTime.getTime() + 30 * 60000);
|
||||
attemptTime.setMinutes(attemptTime.getMinutes() + 15);
|
||||
}
|
||||
|
||||
if (scheduledDate) break;
|
||||
|
||||
// Move to next day
|
||||
// Prepare next day
|
||||
currentDay.setDate(currentDay.getDate() + 1);
|
||||
currentDay.setHours(workStartHour, 0, 0, 0);
|
||||
currentDay.setHours(0, 0, 0, 0); // Start at midnight
|
||||
}
|
||||
|
||||
if (scheduledDate) {
|
||||
@@ -249,10 +291,10 @@ CRITICAL: After executing tools, provide a concise summary of your actions.`;
|
||||
return {
|
||||
success: true,
|
||||
scheduledDate: scheduledDate.toISOString(),
|
||||
message: `Scheduled for ${scheduledDate.toLocaleDateString()} at ${scheduledDate.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}.`
|
||||
message: `Scheduled for ${scheduledDate.toLocaleDateString()} at ${scheduledDate.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })} (${domain} time).`
|
||||
};
|
||||
} else {
|
||||
return { success: false, error: "Could not find a free slot in the next 3 days." };
|
||||
return { success: false, error: "Could not find a free slot in the next 7 days." };
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user