Sync time entries with invoice lines and harden auth for mobile.

Link clocked time to invoice items with bidirectional sync, add entry editing on web, broaden session cookie detection for Expo clients, and handle API rate limits without signing users out.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-28 21:44:19 -04:00
co-authored by Cursor
parent c267b3e1c8
commit 31151e7f39
19 changed files with 849 additions and 111 deletions
+2 -2
View File
@@ -1,4 +1,4 @@
import { NextRequest, NextResponse } from "next/server";
import { type NextRequest, NextResponse } from "next/server";
import { eq } from "drizzle-orm";
import { getOptionalServerSession } from "~/lib/auth-server";
import { getObject } from "~/lib/object-storage";
@@ -20,7 +20,7 @@ export async function GET(
with: { expense: true },
});
if (!receipt || receipt.expense.createdById !== session.user.id) {
if (receipt?.expense.createdById !== session.user.id) {
return NextResponse.json({ error: "Not found" }, { status: 404 });
}
+10 -3
View File
@@ -39,10 +39,17 @@ export function SignInForm({ allowRegistration }: SignInFormProps) {
setLoading(false);
if (error) {
const message = error.message?.toLowerCase() ?? "";
const rateLimited =
error.status === 429 ||
message.includes("too many") ||
message.includes("rate limit");
toast.error(
error.message && error.message !== "Required"
? error.message
: "Invalid email or password",
rateLimited
? "Too many sign-in attempts. Please wait a moment and try again."
: error.message && error.message !== "Required"
? error.message
: "Invalid email or password",
);
return;
}
+1 -7
View File
@@ -1,6 +1,6 @@
"use client";
import { useEffect, useMemo, useState } from "react";
import { useMemo, useState } from "react";
import { api } from "~/trpc/react";
import { DashboardPageHeader } from "~/components/layout/page-header";
import { DashboardPage, dashboardStatGridClass } from "~/components/layout/dashboard-page";
@@ -116,12 +116,6 @@ export default function ExpensesPage() {
[businesses],
);
useEffect(() => {
if (!open || dialogMode !== "create" || !defaultBusinessId || form.businessId)
return;
setForm((prev) => ({ ...prev, businessId: defaultBusinessId }));
}, [open, dialogMode, defaultBusinessId, form.businessId]);
const create = api.expenses.create.useMutation({
onSuccess: (expense) => {
if (!expense) return;