fix: email-based user lookup, slot controls, sub-hour toggle
API routes (goal, weather, calendar/sync, calendar/events): - All user lookups now use session email instead of session ID so stale JWTs after DB restore/migration no longer break requests (P2003 FK error) WeeklyView QuickSettings sidebar: - effectiveCellDuration now uses local cellDuration state as fallback instead of stale profile.cellDuration — slot buttons now actually update the time grid - Added 20m slot option to all three button groups (sidebar, header, mobile) - Added :15/:30/:45 sub-hour slots toggle to QuickSettings sidebar - Slot controls now highlight using effectiveCellDuration (per-view aware) - Someday and All-day toggles now use effective value, not global profile v1.81.15
This commit is contained in:
parent
2d785ea2bf
commit
09caf5ffa0
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "my-weekly-todo-list",
|
"name": "my-weekly-todo-list",
|
||||||
"version": "1.81.14",
|
"version": "1.81.15",
|
||||||
"description": "A web-based weekly task management application that organizes to-dos and calendar events in a single, intuitive weekly view",
|
"description": "A web-based weekly task management application that organizes to-dos and calendar events in a single, intuitive weekly view",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@ -5,10 +5,10 @@ import { prisma } from '@/lib/prisma';
|
|||||||
import { createCalendarEvent, updateCalendarEvent, deleteCalendarEvent, CalendarConnection } from '@/lib/calendar-events';
|
import { createCalendarEvent, updateCalendarEvent, deleteCalendarEvent, CalendarConnection } from '@/lib/calendar-events';
|
||||||
import { upsertCachedEvent, deleteCachedEvent } from '@/lib/calendar-cache';
|
import { upsertCachedEvent, deleteCachedEvent } from '@/lib/calendar-cache';
|
||||||
|
|
||||||
// Helper to find connection by calendarId
|
// Helper to find connection by calendarId (look up by email to avoid stale session IDs)
|
||||||
async function findConnectionForCalendar(userId: string, calendarId: string) {
|
async function findConnectionForCalendar(email: string, calendarId: string) {
|
||||||
const user = await prisma.user.findUnique({
|
const user = await prisma.user.findUnique({
|
||||||
where: { id: userId },
|
where: { email },
|
||||||
include: { calendarConnections: true }
|
include: { calendarConnections: true }
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -9,8 +9,7 @@ import { runSyncRules } from '@/lib/calendar-cross-sync';
|
|||||||
export async function POST(request: NextRequest) {
|
export async function POST(request: NextRequest) {
|
||||||
try {
|
try {
|
||||||
const session = await getServerSession(authOptions);
|
const session = await getServerSession(authOptions);
|
||||||
const userId = (session?.user as any)?.id;
|
if (!session?.user?.email) {
|
||||||
if (!userId) {
|
|
||||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -20,18 +19,11 @@ export async function POST(request: NextRequest) {
|
|||||||
const timeMinDate = new Date(timeMin ?? Date.now());
|
const timeMinDate = new Date(timeMin ?? Date.now());
|
||||||
const timeMaxDate = new Date(timeMax ?? Date.now() + 7 * 24 * 60 * 60 * 1000);
|
const timeMaxDate = new Date(timeMax ?? Date.now() + 7 * 24 * 60 * 60 * 1000);
|
||||||
|
|
||||||
let user = await prisma.user.findUnique({
|
// Look up by email — session ID can be stale after DB restore/migration
|
||||||
where: { id: userId },
|
const user = await prisma.user.findUnique({
|
||||||
include: { calendarConnections: true },
|
|
||||||
});
|
|
||||||
|
|
||||||
// Fallback: session ID may be stale (e.g. after DB restore) — try by email
|
|
||||||
if (!user && session?.user?.email) {
|
|
||||||
user = await prisma.user.findUnique({
|
|
||||||
where: { email: session.user.email },
|
where: { email: session.user.email },
|
||||||
include: { calendarConnections: true },
|
include: { calendarConnections: true },
|
||||||
}) ?? null;
|
});
|
||||||
}
|
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return NextResponse.json({ error: 'User not found' }, { status: 404 });
|
return NextResponse.json({ error: 'User not found' }, { status: 404 });
|
||||||
|
|||||||
@ -10,15 +10,10 @@ export const dynamic = 'force-dynamic';
|
|||||||
export async function GET(req: Request) {
|
export async function GET(req: Request) {
|
||||||
try {
|
try {
|
||||||
const session = await getServerSession(authOptions);
|
const session = await getServerSession(authOptions);
|
||||||
if (!session || !session.user) {
|
if (!session?.user?.email) {
|
||||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||||
}
|
}
|
||||||
|
|
||||||
const userId = (session.user as any).id;
|
|
||||||
if (!userId) {
|
|
||||||
return NextResponse.json({ error: 'User ID missing from session' }, { status: 400 });
|
|
||||||
}
|
|
||||||
|
|
||||||
const { searchParams } = new URL(req.url);
|
const { searchParams } = new URL(req.url);
|
||||||
const weekStartParam = searchParams.get('weekStart');
|
const weekStartParam = searchParams.get('weekStart');
|
||||||
|
|
||||||
@ -29,10 +24,11 @@ export async function GET(req: Request) {
|
|||||||
const date = new Date(weekStartParam);
|
const date = new Date(weekStartParam);
|
||||||
date.setUTCHours(0, 0, 0, 0);
|
date.setUTCHours(0, 0, 0, 0);
|
||||||
|
|
||||||
// Fetch user preferences
|
// Look up by email — session ID can be stale after DB restore
|
||||||
const user = await prisma.user.findUnique({
|
const user = await prisma.user.findUnique({
|
||||||
where: { id: userId },
|
where: { email: session.user.email },
|
||||||
select: {
|
select: {
|
||||||
|
id: true,
|
||||||
goalFallbackType: true,
|
goalFallbackType: true,
|
||||||
goalDefaultSentence: true,
|
goalDefaultSentence: true,
|
||||||
language: true,
|
language: true,
|
||||||
@ -40,6 +36,9 @@ export async function GET(req: Request) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (!user) return NextResponse.json({ error: 'User not found' }, { status: 404 });
|
||||||
|
const userId = user.id;
|
||||||
|
|
||||||
// 1. Check if user has a custom set goal for THIS week specifically
|
// 1. Check if user has a custom set goal for THIS week specifically
|
||||||
const goal = await prisma.weeklyGoal.findUnique({
|
const goal = await prisma.weeklyGoal.findUnique({
|
||||||
where: {
|
where: {
|
||||||
@ -134,14 +133,13 @@ export async function POST(req: Request) {
|
|||||||
export async function PUT(req: Request) {
|
export async function PUT(req: Request) {
|
||||||
try {
|
try {
|
||||||
const session = await getServerSession(authOptions);
|
const session = await getServerSession(authOptions);
|
||||||
if (!session || !session.user) {
|
if (!session?.user?.email) {
|
||||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||||
}
|
}
|
||||||
|
|
||||||
const userId = (session.user as any).id;
|
const dbUser = await prisma.user.findUnique({ where: { email: session.user.email }, select: { id: true } });
|
||||||
if (!userId) {
|
if (!dbUser) return NextResponse.json({ error: 'User not found' }, { status: 404 });
|
||||||
return NextResponse.json({ error: 'User ID missing from session' }, { status: 400 });
|
const userId = dbUser.id;
|
||||||
}
|
|
||||||
|
|
||||||
const { weekStart, text } = await req.json();
|
const { weekStart, text } = await req.json();
|
||||||
|
|
||||||
|
|||||||
@ -11,13 +11,12 @@ const CACHE_TTL_MS = 15 * 60 * 1000; // 15 minutes
|
|||||||
|
|
||||||
export async function GET(request: NextRequest) {
|
export async function GET(request: NextRequest) {
|
||||||
const session = await getServerSession(authOptions);
|
const session = await getServerSession(authOptions);
|
||||||
const userId = (session?.user as any)?.id;
|
if (!session?.user?.email) {
|
||||||
if (!userId) {
|
|
||||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||||
}
|
}
|
||||||
|
|
||||||
const user = await prisma.user.findUnique({
|
const user = await prisma.user.findUnique({
|
||||||
where: { id: userId },
|
where: { email: session.user.email },
|
||||||
select: { weatherEnabled: true, weatherLat: true, weatherLon: true },
|
select: { weatherEnabled: true, weatherLat: true, weatherLon: true },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -1040,7 +1040,7 @@ export default function WeeklyView() {
|
|||||||
const effectiveShowAllDay = getEffective("showAllDayEvents", profile.showAllDayEvents ?? true);
|
const effectiveShowAllDay = getEffective("showAllDayEvents", profile.showAllDayEvents ?? true);
|
||||||
const effectiveAllDayPosition = getEffective("allDayPosition", profile.allDayPosition ?? "above") || "above";
|
const effectiveAllDayPosition = getEffective("allDayPosition", profile.allDayPosition ?? "above") || "above";
|
||||||
const effectiveShowCompletedTasks = getEffective("showCompletedTasks", profile.showCompletedTasks !== false);
|
const effectiveShowCompletedTasks = getEffective("showCompletedTasks", profile.showCompletedTasks !== false);
|
||||||
const effectiveCellDuration = getEffective("cellDuration", profile.cellDuration ?? 30) as CellDuration;
|
const effectiveCellDuration = getEffective("cellDuration", cellDuration) as CellDuration;
|
||||||
const effectiveStartHour = getEffective("startHour", profile.startHour ?? 8) ?? 8;
|
const effectiveStartHour = getEffective("startHour", profile.startHour ?? 8) ?? 8;
|
||||||
const effectiveEndHour = getEffective("endHour", profile.endHour ?? 18) ?? 18;
|
const effectiveEndHour = getEffective("endHour", profile.endHour ?? 18) ?? 18;
|
||||||
|
|
||||||
@ -5444,22 +5444,30 @@ export default function WeeklyView() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Slot Duration (only with time grid) */}
|
{/* Slot Duration + sub-hour slots (only with time grid) */}
|
||||||
{profile.showTimeGrid && (
|
{profile.showTimeGrid && (
|
||||||
<div style={{ display: "flex", flexDirection: "column", gap: "6px" }}>
|
<div style={{ display: "flex", flexDirection: "column", gap: "6px" }}>
|
||||||
<span style={{ fontSize: "0.7rem", fontWeight: 600, color: darkMode ? "#9ca3af" : "#6b7280" }}>
|
<span style={{ fontSize: "0.7rem", fontWeight: 600, color: darkMode ? "#9ca3af" : "#6b7280" }}>
|
||||||
{profile.language === "de" ? "Zeitfenster" : "Slot"}
|
{profile.language === "de" ? "Zeitfenster" : "Slot"}
|
||||||
</span>
|
</span>
|
||||||
<div style={{ display: "flex", gap: "4px" }}>
|
<div style={{ display: "flex", gap: "4px" }}>
|
||||||
{[15, 30, 60].map((d) => (
|
{([15, 20, 30, 60] as CellDuration[]).map((d) => (
|
||||||
<button key={d} onClick={() => { setCellDuration(d as CellDuration); saveSetting("cellDuration", d); }}
|
<button key={d} onClick={() => { setCellDuration(d); saveSetting("cellDuration", d); }}
|
||||||
style={{ flex: 1, padding: "5px 0", fontSize: "0.8rem", borderRadius: "6px", border: "none", cursor: "pointer", fontWeight: cellDuration === d ? 700 : 400, background: cellDuration === d ? (darkMode ? "#374151" : "#333") : (darkMode ? "#1f2937" : "#e5e7eb"), color: cellDuration === d ? "#fff" : (darkMode ? "#9ca3af" : "#6b7280") }}>
|
style={{ flex: 1, padding: "5px 0", fontSize: "0.8rem", borderRadius: "6px", border: "none", cursor: "pointer", fontWeight: effectiveCellDuration === d ? 700 : 400, background: effectiveCellDuration === d ? (darkMode ? "#374151" : "#333") : (darkMode ? "#1f2937" : "#e5e7eb"), color: effectiveCellDuration === d ? "#fff" : (darkMode ? "#9ca3af" : "#6b7280") }}>
|
||||||
{d}m
|
{d}m
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
{profile.showTimeGrid && (
|
||||||
|
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}>
|
||||||
|
<span style={{ fontSize: "0.75rem", fontWeight: 600, color: darkMode ? "#9ca3af" : "#6b7280" }}>{profile.language === "de" ? ":15/:30/:45" : ":15/:30/:45"}</span>
|
||||||
|
<button onClick={() => { const v = !effectiveShowSubHourSlots; setShowSubHourSlots(v); saveSetting("showSubHourSlots", v); }} style={{ background: "none", border: "none", cursor: "pointer", padding: "2px", color: darkMode ? "#9ca3af" : "#6b7280" }}>
|
||||||
|
{effectiveShowSubHourSlots ? <Eye size={16} /> : <EyeOff size={16} />}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Text size */}
|
{/* Text size */}
|
||||||
<div style={{ display: "flex", flexDirection: "column", gap: "6px" }}>
|
<div style={{ display: "flex", flexDirection: "column", gap: "6px" }}>
|
||||||
@ -5482,13 +5490,13 @@ export default function WeeklyView() {
|
|||||||
{/* Toggle switches */}
|
{/* Toggle switches */}
|
||||||
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}>
|
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}>
|
||||||
<span style={{ fontSize: "0.75rem", fontWeight: 600, color: darkMode ? "#9ca3af" : "#6b7280" }}>{profile.language === "de" ? "Irgendwann" : "Someday"}</span>
|
<span style={{ fontSize: "0.75rem", fontWeight: 600, color: darkMode ? "#9ca3af" : "#6b7280" }}>{profile.language === "de" ? "Irgendwann" : "Someday"}</span>
|
||||||
<button onClick={() => { const v = !profile.showSomeday; setShowSomeday(v); saveSetting("showSomeday", v); }} style={{ background: "none", border: "none", cursor: "pointer", padding: "2px", color: darkMode ? "#9ca3af" : "#6b7280" }}>
|
<button onClick={() => { const v = !effectiveShowSomeday; setShowSomeday(v); saveSetting("showSomeday", v); }} style={{ background: "none", border: "none", cursor: "pointer", padding: "2px", color: darkMode ? "#9ca3af" : "#6b7280" }}>
|
||||||
{effectiveShowSomeday ? <Eye size={16} /> : <EyeOff size={16} />}
|
{effectiveShowSomeday ? <Eye size={16} /> : <EyeOff size={16} />}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}>
|
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}>
|
||||||
<span style={{ fontSize: "0.75rem", fontWeight: 600, color: darkMode ? "#9ca3af" : "#6b7280" }}>{profile.language === "de" ? "Ganztägig" : "All-day"}</span>
|
<span style={{ fontSize: "0.75rem", fontWeight: 600, color: darkMode ? "#9ca3af" : "#6b7280" }}>{profile.language === "de" ? "Ganztägig" : "All-day"}</span>
|
||||||
<button onClick={() => { const v = !profile.showAllDayEvents; setShowAllDay(v); saveSetting("showAllDayEvents", v); }} style={{ background: "none", border: "none", cursor: "pointer", padding: "2px", color: darkMode ? "#9ca3af" : "#6b7280" }}>
|
<button onClick={() => { const v = !effectiveShowAllDay; setShowAllDay(v); saveSetting("showAllDayEvents", v); }} style={{ background: "none", border: "none", cursor: "pointer", padding: "2px", color: darkMode ? "#9ca3af" : "#6b7280" }}>
|
||||||
{effectiveShowAllDay ? <Eye size={16} /> : <EyeOff size={16} />}
|
{effectiveShowAllDay ? <Eye size={16} /> : <EyeOff size={16} />}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@ -5738,11 +5746,11 @@ export default function WeeklyView() {
|
|||||||
title="Slot Duration"
|
title="Slot Duration"
|
||||||
>
|
>
|
||||||
<Clock size={16} className="text-gray-500 mr-1" />
|
<Clock size={16} className="text-gray-500 mr-1" />
|
||||||
{[15, 30, 60].map((duration) => (
|
{([15, 20, 30, 60] as CellDuration[]).map((duration) => (
|
||||||
<button
|
<button
|
||||||
key={duration}
|
key={duration}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setCellDuration(duration as CellDuration);
|
setCellDuration(duration);
|
||||||
saveSetting("cellDuration", duration);
|
saveSetting("cellDuration", duration);
|
||||||
}}
|
}}
|
||||||
className={`px-2 py-0.5 text-xs rounded transition-colors ${effectiveCellDuration === duration ? "bg-white shadow-sm font-bold text-black" : "text-gray-500 hover:text-gray-900 hover:bg-gray-200 dark:hover:bg-gray-700"}`}
|
className={`px-2 py-0.5 text-xs rounded transition-colors ${effectiveCellDuration === duration ? "bg-white shadow-sm font-bold text-black" : "text-gray-500 hover:text-gray-900 hover:bg-gray-200 dark:hover:bg-gray-700"}`}
|
||||||
@ -5986,10 +5994,10 @@ export default function WeeklyView() {
|
|||||||
{profile.showTimeGrid && (
|
{profile.showTimeGrid && (
|
||||||
<div style={{ padding: "6px 12px", display: "flex", alignItems: "center", gap: "6px" }}>
|
<div style={{ padding: "6px 12px", display: "flex", alignItems: "center", gap: "6px" }}>
|
||||||
<span style={{ fontSize: "12px", color: "#888", marginRight: "4px" }}>{profile.language === "de" ? "Slot" : "Slot"}:</span>
|
<span style={{ fontSize: "12px", color: "#888", marginRight: "4px" }}>{profile.language === "de" ? "Slot" : "Slot"}:</span>
|
||||||
{[15, 30, 60].map((duration) => (
|
{([15, 20, 30, 60] as CellDuration[]).map((duration) => (
|
||||||
<button
|
<button
|
||||||
key={duration}
|
key={duration}
|
||||||
onClick={() => { setCellDuration(duration as CellDuration); saveSetting("cellDuration", duration); }}
|
onClick={() => { setCellDuration(duration); saveSetting("cellDuration", duration); }}
|
||||||
style={{ padding: "2px 8px", fontSize: "12px", borderRadius: "4px", border: "none", cursor: "pointer", fontWeight: effectiveCellDuration === duration ? 700 : 400, background: effectiveCellDuration === duration ? "var(--bg-secondary, #e5e7eb)" : "transparent", color: "inherit" }}
|
style={{ padding: "2px 8px", fontSize: "12px", borderRadius: "4px", border: "none", cursor: "pointer", fontWeight: effectiveCellDuration === duration ? 700 : 400, background: effectiveCellDuration === duration ? "var(--bg-secondary, #e5e7eb)" : "transparent", color: "inherit" }}
|
||||||
>
|
>
|
||||||
{duration}m
|
{duration}m
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user