Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
159 changes: 159 additions & 0 deletions src/app/api/schedule-utils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
import { schedule, monthAdjustments } from '../../../config';

/**
* Apply monthly adjustments to a schedule entry
* @param {Object} entry - Schedule entry with hour, minute, mode
* @param {number} month - Month (0-11)
* @returns {Object} Adjusted schedule entry with adjustedHour and adjustedMinute
*/
export function applyMonthlyAdjustment(entry, month) {
const monthAdjustment = monthAdjustments[month] || 1;
const totalMinutes = (entry.hour * 60 + entry.minute) * monthAdjustment;
const adjustedHour = Math.floor(totalMinutes / 60);
const adjustedMinute = Math.floor(totalMinutes % 60);

return {
...entry,
adjustedHour,
adjustedMinute,
monthAdjustment
};
}

/**
* Get the adjusted schedule for a given date
* @param {Date} date - Date to get schedule for
* @returns {Array} Array of adjusted schedule entries
*/
export function getAdjustedSchedule(date = new Date()) {
const month = date.getMonth();
return schedule.map(entry => applyMonthlyAdjustment(entry, month));
}

/**
* Find the next scheduled change from a given time
* @param {Date} currentTime - Current time to calculate from
* @returns {Object|null} Next schedule change or null if no more changes today
*/
export function getNextScheduledChange(currentTime = new Date()) {
const adjustedSchedule = getAdjustedSchedule(currentTime);
const currentHour = currentTime.getHours();
const currentMinute = currentTime.getMinutes();

// Find the next schedule entry that hasn't occurred yet today
for (let entry of adjustedSchedule) {
const { adjustedHour, adjustedMinute } = entry;

// Check if this schedule time is in the future
if (adjustedHour > currentHour || (adjustedHour === currentHour && adjustedMinute > currentMinute)) {
// Calculate time until this change
const changeTime = new Date(currentTime);
changeTime.setHours(adjustedHour, adjustedMinute, 0, 0);

const timeUntilChange = changeTime.getTime() - currentTime.getTime();
const minutesUntil = Math.floor(timeUntilChange / (1000 * 60));
const hoursUntil = Math.floor(minutesUntil / 60);
const remainingMinutes = minutesUntil % 60;

return {
...entry,
changeTime,
timeUntilChange,
minutesUntil,
hoursUntil,
remainingMinutes,
isToday: true
};
}
}

// If no more changes today, find the first change tomorrow
const tomorrow = new Date(currentTime);
tomorrow.setDate(tomorrow.getDate() + 1);
tomorrow.setHours(0, 0, 0, 0);

const tomorrowSchedule = getAdjustedSchedule(tomorrow);
if (tomorrowSchedule.length > 0) {
const firstEntry = tomorrowSchedule[0];
const changeTime = new Date(tomorrow);
changeTime.setHours(firstEntry.adjustedHour, firstEntry.adjustedMinute, 0, 0);

const timeUntilChange = changeTime.getTime() - currentTime.getTime();
const minutesUntil = Math.floor(timeUntilChange / (1000 * 60));
const hoursUntil = Math.floor(minutesUntil / 60);
const remainingMinutes = minutesUntil % 60;

return {
...firstEntry,
changeTime,
timeUntilChange,
minutesUntil,
hoursUntil,
remainingMinutes,
isToday: false
};
}

return null;
}

/**
* Get all upcoming schedule changes for the current day
* @param {Date} currentTime - Current time to calculate from
* @returns {Array} Array of upcoming schedule changes
*/
export function getUpcomingChanges(currentTime = new Date()) {
const adjustedSchedule = getAdjustedSchedule(currentTime);
const currentHour = currentTime.getHours();
const currentMinute = currentTime.getMinutes();

return adjustedSchedule
.filter(entry => {
const { adjustedHour, adjustedMinute } = entry;
return adjustedHour > currentHour || (adjustedHour === currentHour && adjustedMinute > currentMinute);
})
.map(entry => {
const changeTime = new Date(currentTime);
changeTime.setHours(entry.adjustedHour, entry.adjustedMinute, 0, 0);

const timeUntilChange = changeTime.getTime() - currentTime.getTime();
const minutesUntil = Math.floor(timeUntilChange / (1000 * 60));
const hoursUntil = Math.floor(minutesUntil / 60);
const remainingMinutes = minutesUntil % 60;

return {
...entry,
changeTime,
timeUntilChange,
minutesUntil,
hoursUntil,
remainingMinutes
};
});
}

/**
* Get the current active mode based on schedule (mirrors scheduler logic)
* @param {Date} currentTime - Current time to calculate from
* @returns {string} Current mode name
*/
export function getCurrentScheduledMode(currentTime = new Date()) {
const adjustedSchedule = getAdjustedSchedule(currentTime);
const currentHour = currentTime.getHours();
const currentMinute = currentTime.getMinutes();

let mode = 'Off'; // Default mode

// Find the most recent schedule entry that has already occurred
for (let entry of adjustedSchedule) {
const { adjustedHour, adjustedMinute } = entry;

// Check if current time is past the adjusted schedule time
if (adjustedHour < currentHour || (adjustedHour === currentHour && adjustedMinute <= currentMinute)) {
mode = entry.mode;
}
}

return mode;
}

26 changes: 26 additions & 0 deletions src/app/api/schedule/route.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { NextResponse } from 'next/server';
import { getNextScheduledChange, getUpcomingChanges, getCurrentScheduledMode } from '../schedule-utils';
import { handleSuccess, handleServerError } from '../utils';

export async function GET() {
try {
const currentTime = new Date();
const nextChange = getNextScheduledChange(currentTime);
const upcomingChanges = getUpcomingChanges(currentTime);
const currentScheduledMode = getCurrentScheduledMode(currentTime);

const data = {
currentTime: currentTime.toISOString(),
currentScheduledMode,
nextChange,
upcomingChanges,
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone
};

return handleSuccess(data);
} catch (error) {
console.error('Error in schedule API:', error);
return handleServerError(error);
}
}

21 changes: 19 additions & 2 deletions src/app/api/state/route.js
Original file line number Diff line number Diff line change
@@ -1,15 +1,32 @@
import { NextResponse } from 'next/server';
import * as scheduler from '../scheduler';
import { modes } from '../../../../config';
import { getNextScheduledChange, getUpcomingChanges } from '../schedule-utils';
import { handleSuccess, handleServerError } from '../utils';

export async function GET() {
try {
let mode = scheduler.getMode();
let override = scheduler.getOverride();
const data = { mode, modes, override, state: modes[mode] };

// Add schedule information
const currentTime = new Date();
const nextChange = getNextScheduledChange(currentTime);
const upcomingChanges = getUpcomingChanges(currentTime);

const data = {
mode,
modes,
override,
state: modes[mode],
schedule: {
nextChange,
upcomingChanges,
currentTime: currentTime.toISOString()
}
};
return handleSuccess(data);
} catch (error) {
return handleServerError(error);
}
}
}
81 changes: 76 additions & 5 deletions src/app/page.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import { useEffect, useState } from 'react';
import ModeSwitch from '../components/ModeSwitch';
import { formatTimeUntil, formatTime12Hour, getShortTimeUntil } from '../utils/time-formatting';

// Icons for different equipment types
const EquipmentIcon = ({ type, active }) => {
Expand Down Expand Up @@ -179,17 +180,87 @@ export default function Home() {
<h3 className="text-md font-medium text-gray-800">Next Scheduled Change</h3>
</div>
<div className="text-md">
{/* This would need actual schedule data to display accurately */}
<div className="flex items-center">
<span className="font-medium text-gray-700">Coming soon...</span>
<span className="ml-2 text-xs bg-gray-200 text-gray-600 py-1 px-2 rounded-full">Feature in development</span>
</div>
{controlState?.schedule?.nextChange ? (
<div className="flex flex-col space-y-1">
<div className="flex items-center justify-between">
<span className="font-medium text-gray-700">
{controlState.schedule.nextChange.mode}
</span>
<span className="text-sm text-gray-500">
{formatTime12Hour(new Date(controlState.schedule.nextChange.changeTime))}
</span>
</div>
<div className="text-sm text-blue-600 font-medium">
{formatTimeUntil(
controlState.schedule.nextChange.minutesUntil,
controlState.schedule.nextChange.hoursUntil,
controlState.schedule.nextChange.remainingMinutes
)}
</div>
{!controlState.schedule.nextChange.isToday && (
<span className="text-xs bg-orange-100 text-orange-600 py-1 px-2 rounded-full inline-block w-fit">
Tomorrow
</span>
)}
</div>
) : (
<div className="flex items-center">
<span className="font-medium text-gray-700">No more changes scheduled today</span>
</div>
)}
</div>
</div>
</div>
</div>
</div>

{/* Upcoming Actions Section */}
{controlState?.schedule?.upcomingChanges && controlState.schedule.upcomingChanges.length > 0 && (
<div className="bg-white rounded-lg shadow-md overflow-hidden mb-6">
<div className="bg-gradient-to-r from-purple-700 to-purple-500 text-white px-4 py-3">
<div className="flex items-center">
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" />
</svg>
<h2 className="text-lg font-medium">Today's Upcoming Actions</h2>
</div>
</div>
<div className="p-5">
<div className="space-y-3">
{controlState.schedule.upcomingChanges.map((change, index) => (
<div
key={index}
className="flex items-center justify-between p-3 bg-gray-50 rounded-lg border border-gray-200 hover:bg-gray-100 transition-colors duration-200"
>
<div className="flex items-center">
<div className="w-3 h-3 bg-purple-500 rounded-full mr-3"></div>
<div>
<div className="font-medium text-gray-800">{change.mode}</div>
<div className="text-sm text-gray-500">
{formatTime12Hour(new Date(change.changeTime))}
</div>
</div>
</div>
<div className="text-right">
<div className="text-sm font-medium text-purple-600">
{getShortTimeUntil(change.minutesUntil)}
</div>
<div className="text-xs text-gray-500">
{change.minutesUntil < 60 ? 'soon' : 'later'}
</div>
</div>
</div>
))}
</div>
{controlState.schedule.upcomingChanges.length === 0 && (
<div className="text-center py-4 text-gray-500">
No more scheduled changes today
</div>
)}
</div>
</div>
)}

<div className="bg-white rounded-lg shadow-md overflow-hidden mb-6">
<div className="bg-gradient-to-r from-blue-700 to-blue-500 text-white px-4 py-3">
<div className="flex items-center">
Expand Down
75 changes: 75 additions & 0 deletions src/app/utils/time-formatting.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
/**
* Format a time difference into a human-readable string
* @param {number} minutesUntil - Minutes until the event
* @param {number} hoursUntil - Hours until the event
* @param {number} remainingMinutes - Remaining minutes after hours
* @returns {string} Formatted time string
*/
export function formatTimeUntil(minutesUntil, hoursUntil, remainingMinutes) {
if (minutesUntil < 1) {
return 'in less than 1 minute';
} else if (minutesUntil < 60) {
return `in ${minutesUntil} minute${minutesUntil === 1 ? '' : 's'}`;
} else if (hoursUntil === 1 && remainingMinutes === 0) {
return 'in 1 hour';
} else if (remainingMinutes === 0) {
return `in ${hoursUntil} hours`;
} else if (hoursUntil === 1) {
return `in 1 hour ${remainingMinutes} minute${remainingMinutes === 1 ? '' : 's'}`;
} else {
return `in ${hoursUntil} hours ${remainingMinutes} minute${remainingMinutes === 1 ? '' : 's'}`;
}
}

/**
* Format a Date object into a 12-hour time string
* @param {Date} date - Date to format
* @returns {string} Formatted time string (e.g., "4:30 PM")
*/
export function formatTime12Hour(date) {
return date.toLocaleTimeString('en-US', {
hour: 'numeric',
minute: '2-digit',
hour12: true
});
}

/**
* Format a time for display with both absolute time and relative time
* @param {Object} scheduleEntry - Schedule entry with time information
* @param {boolean} isToday - Whether the change is today or tomorrow
* @returns {string} Formatted time string
*/
export function formatScheduleTime(scheduleEntry, isToday = true) {
const { changeTime, minutesUntil, hoursUntil, remainingMinutes } = scheduleEntry;
const absoluteTime = formatTime12Hour(changeTime);
const relativeTime = formatTimeUntil(minutesUntil, hoursUntil, remainingMinutes);

if (isToday) {
return `${absoluteTime} (${relativeTime})`;
} else {
return `tomorrow at ${absoluteTime} (${relativeTime})`;
}
}

/**
* Get a short relative time description
* @param {number} minutesUntil - Minutes until the event
* @returns {string} Short time description
*/
export function getShortTimeUntil(minutesUntil) {
if (minutesUntil < 1) {
return '< 1m';
} else if (minutesUntil < 60) {
return `${minutesUntil}m`;
} else {
const hours = Math.floor(minutesUntil / 60);
const minutes = minutesUntil % 60;
if (minutes === 0) {
return `${hours}h`;
} else {
return `${hours}h ${minutes}m`;
}
}
}

Loading