feat: garden dashboard widgets and manifest completion (task 75)
- getCareDueWidgetRows query: schedule rows due within N days, filtered by optional container list, ordered by next_due_at - getGardenOverviewStats query: plant/container/overdue counts + next care - CareDueWidget: compact list sorted by urgency, inline log-care button, truncates at 10 rows with View all link - GardenOverviewWidget: stat row (plants, containers, overdue) + next care line + garden link - plant-widget.tsx server component: InlineLogButton uses form action to call logCare without client JS or navigation - manifest: garden.care-due and garden.overview widgets registered; resolveConfigOptions returns container list for care-due config picker - quickAdds: icons updated (leaf/box/droplets), log-care URL -> /garden?logCare=1
This commit is contained in:
@@ -492,3 +492,128 @@ export async function getCareDueSoon(
|
||||
|
||||
return rows;
|
||||
}
|
||||
|
||||
// ─── Widget queries ───────────────────────────────────────────────────────────
|
||||
|
||||
export type CareDueWidgetRow = {
|
||||
plantId: string;
|
||||
plantName: string;
|
||||
scheduleId: string;
|
||||
careType: string;
|
||||
nextDueAt: Date | null;
|
||||
daysUntilDue: number;
|
||||
};
|
||||
|
||||
export async function getCareDueWidgetRows(
|
||||
householdId: string,
|
||||
containerIds: "all" | string[],
|
||||
daysAhead: number,
|
||||
): Promise<CareDueWidgetRow[]> {
|
||||
const cutoff = new Date();
|
||||
cutoff.setDate(cutoff.getDate() + daysAhead);
|
||||
|
||||
const baseConditions = [
|
||||
eq(gardenCareSchedules.householdId, householdId),
|
||||
eq(gardenCareSchedules.enabled, true),
|
||||
lte(gardenCareSchedules.nextDueAt, cutoff),
|
||||
];
|
||||
|
||||
const containerFilter =
|
||||
containerIds !== "all" && containerIds.length > 0
|
||||
? sql`${gardenPlants.containerId} = ANY(ARRAY[${sql.join(
|
||||
containerIds.map((id) => sql`${id}::uuid`),
|
||||
sql`, `,
|
||||
)}])`
|
||||
: undefined;
|
||||
|
||||
const where =
|
||||
containerFilter !== undefined
|
||||
? and(...baseConditions, containerFilter)
|
||||
: and(...baseConditions);
|
||||
|
||||
const rows = await db
|
||||
.select({
|
||||
plantId: gardenPlants.id,
|
||||
plantName: gardenPlants.name,
|
||||
scheduleId: gardenCareSchedules.id,
|
||||
careType: gardenCareSchedules.careType,
|
||||
nextDueAt: gardenCareSchedules.nextDueAt,
|
||||
})
|
||||
.from(gardenCareSchedules)
|
||||
.innerJoin(gardenPlants, eq(gardenCareSchedules.plantId, gardenPlants.id))
|
||||
.where(where)
|
||||
.orderBy(gardenCareSchedules.nextDueAt);
|
||||
|
||||
const now = Date.now();
|
||||
return rows.map((r) => ({
|
||||
plantId: r.plantId,
|
||||
plantName: r.plantName,
|
||||
scheduleId: r.scheduleId,
|
||||
careType: r.careType,
|
||||
nextDueAt: r.nextDueAt,
|
||||
daysUntilDue: r.nextDueAt
|
||||
? Math.ceil((r.nextDueAt.getTime() - now) / (1000 * 60 * 60 * 24))
|
||||
: 0,
|
||||
}));
|
||||
}
|
||||
|
||||
export type GardenOverviewStats = {
|
||||
plantCount: number;
|
||||
containerCount: number;
|
||||
overdueCount: number;
|
||||
nextCare: { plantName: string; careType: string; daysUntilDue: number } | null;
|
||||
};
|
||||
|
||||
export async function getGardenOverviewStats(householdId: string): Promise<GardenOverviewStats> {
|
||||
const [plantRow, containerRow] = await Promise.all([
|
||||
db
|
||||
.select({ count: sql<number>`count(*)::int` })
|
||||
.from(gardenPlants)
|
||||
.where(eq(gardenPlants.householdId, householdId)),
|
||||
db
|
||||
.select({ count: sql<number>`count(*)::int` })
|
||||
.from(gardenContainers)
|
||||
.where(eq(gardenContainers.householdId, householdId)),
|
||||
]);
|
||||
|
||||
const plantCount = plantRow[0]?.count ?? 0;
|
||||
const containerCount = containerRow[0]?.count ?? 0;
|
||||
|
||||
const overdueRows = await db
|
||||
.select({ count: sql<number>`count(*)::int` })
|
||||
.from(gardenCareSchedules)
|
||||
.where(
|
||||
and(
|
||||
eq(gardenCareSchedules.householdId, householdId),
|
||||
eq(gardenCareSchedules.enabled, true),
|
||||
lte(gardenCareSchedules.nextDueAt, sql`now()`),
|
||||
),
|
||||
);
|
||||
const overdueCount = overdueRows[0]?.count ?? 0;
|
||||
|
||||
const nextRows = await db
|
||||
.select({
|
||||
plantName: gardenPlants.name,
|
||||
careType: gardenCareSchedules.careType,
|
||||
nextDueAt: gardenCareSchedules.nextDueAt,
|
||||
})
|
||||
.from(gardenCareSchedules)
|
||||
.innerJoin(gardenPlants, eq(gardenCareSchedules.plantId, gardenPlants.id))
|
||||
.where(
|
||||
and(eq(gardenCareSchedules.householdId, householdId), eq(gardenCareSchedules.enabled, true)),
|
||||
)
|
||||
.orderBy(gardenCareSchedules.nextDueAt)
|
||||
.limit(1);
|
||||
|
||||
const next = nextRows[0];
|
||||
const now = Date.now();
|
||||
const nextCare = next?.nextDueAt
|
||||
? {
|
||||
plantName: next.plantName,
|
||||
careType: next.careType,
|
||||
daysUntilDue: Math.ceil((next.nextDueAt.getTime() - now) / (1000 * 60 * 60 * 24)),
|
||||
}
|
||||
: null;
|
||||
|
||||
return { plantCount, containerCount, overdueCount, nextCare };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user