Phase 8
This commit is contained in:
parent
029940b079
commit
e396f5088c
36 changed files with 4199 additions and 22 deletions
1062
packages/web/src/app/(dashboard)/meal-plans/page.tsx
Normal file
1062
packages/web/src/app/(dashboard)/meal-plans/page.tsx
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -71,6 +71,13 @@ const NAV: NavItem[] = [
|
|||
section: 'Medicines',
|
||||
},
|
||||
{ id: 'settings', label: 'Settings', href: '/settings', icon: 'settings' },
|
||||
{
|
||||
id: 'meal-plans',
|
||||
label: 'Meal Planner',
|
||||
href: '/meal-plans',
|
||||
icon: 'calendar',
|
||||
section: 'Food',
|
||||
},
|
||||
{
|
||||
id: 'recipes',
|
||||
label: 'Recipes',
|
||||
|
|
|
|||
|
|
@ -34,7 +34,10 @@ export type IconName =
|
|||
| 'fridge'
|
||||
| 'box'
|
||||
| 'yen'
|
||||
| 'zap';
|
||||
| 'zap'
|
||||
| 'chevronLeft'
|
||||
| 'chevronRight'
|
||||
| 'target';
|
||||
|
||||
interface IconProps {
|
||||
name: IconName;
|
||||
|
|
@ -206,6 +209,15 @@ const PATHS: Record<IconName, React.ReactNode> = {
|
|||
),
|
||||
yen: <path d="M5 4l7 9 7-9M7 13h10M7 17h10M12 13v7" />,
|
||||
zap: <path d="M13 2L4 14h7l-1 8 9-12h-7z" />,
|
||||
chevronLeft: <path d="M15 18l-6-6 6-6" />,
|
||||
chevronRight: <path d="M9 18l6-6-6-6" />,
|
||||
target: (
|
||||
<>
|
||||
<circle cx="12" cy="12" r="10" />
|
||||
<circle cx="12" cy="12" r="6" />
|
||||
<circle cx="12" cy="12" r="2" />
|
||||
</>
|
||||
),
|
||||
};
|
||||
|
||||
const svgProps: Omit<SVGProps<SVGSVGElement>, 'width' | 'height'> = {
|
||||
|
|
|
|||
131
packages/web/src/services/meal-plans.ts
Normal file
131
packages/web/src/services/meal-plans.ts
Normal file
|
|
@ -0,0 +1,131 @@
|
|||
import { apiClient } from './api-client';
|
||||
import type { z } from 'zod/v4';
|
||||
import type {
|
||||
MealPlanResponseSchema,
|
||||
MealPlanListResponseSchema,
|
||||
CreateMealPlanInput,
|
||||
UpdateMealPlanInput,
|
||||
MealPlanStatus,
|
||||
} from '@meshitrack/shared';
|
||||
|
||||
export type MealPlanResponse = z.infer<typeof MealPlanResponseSchema>;
|
||||
export type MealPlanListResponse = z.infer<typeof MealPlanListResponseSchema>;
|
||||
|
||||
export interface MealPlanQuery {
|
||||
cursor?: string;
|
||||
limit?: number;
|
||||
}
|
||||
|
||||
export interface RecipeSuggestion {
|
||||
recipeId: string;
|
||||
recipeName: string;
|
||||
totalScore: number;
|
||||
scores: {
|
||||
coverage: number;
|
||||
urgency: number;
|
||||
nutrition: number;
|
||||
variety: number;
|
||||
};
|
||||
reasoning: string[];
|
||||
}
|
||||
|
||||
export interface ShoppingGapReport {
|
||||
mealPlanId: string;
|
||||
missingItems: Array<{
|
||||
productId: string;
|
||||
productName: string;
|
||||
category: string;
|
||||
requiredQuantity: number;
|
||||
pantryQuantity: number;
|
||||
missingQuantity: number;
|
||||
unit: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
export async function listMealPlans(
|
||||
householdId: string,
|
||||
query?: MealPlanQuery
|
||||
): Promise<MealPlanListResponse> {
|
||||
const params = new URLSearchParams();
|
||||
if (query?.cursor) params.set('cursor', query.cursor);
|
||||
if (query?.limit) params.set('limit', String(query.limit));
|
||||
const qs = params.toString();
|
||||
return apiClient.get<MealPlanListResponse>(
|
||||
`/households/${householdId}/meal-plans${qs ? `?${qs}` : ''}`
|
||||
);
|
||||
}
|
||||
|
||||
export async function getMealPlanByWeek(
|
||||
householdId: string,
|
||||
weekStartDate: string
|
||||
): Promise<MealPlanResponse | { message: string }> {
|
||||
return apiClient.get<MealPlanResponse | { message: string }>(
|
||||
`/households/${householdId}/meal-plans/week/${weekStartDate}`
|
||||
);
|
||||
}
|
||||
|
||||
export async function getMealPlan(
|
||||
householdId: string,
|
||||
id: string
|
||||
): Promise<MealPlanResponse> {
|
||||
return apiClient.get<MealPlanResponse>(
|
||||
`/households/${householdId}/meal-plans/${id}`
|
||||
);
|
||||
}
|
||||
|
||||
export async function createMealPlan(
|
||||
householdId: string,
|
||||
data: CreateMealPlanInput
|
||||
): Promise<MealPlanResponse> {
|
||||
return apiClient.post<MealPlanResponse>(
|
||||
`/households/${householdId}/meal-plans`,
|
||||
data
|
||||
);
|
||||
}
|
||||
|
||||
export async function updateMealPlan(
|
||||
householdId: string,
|
||||
id: string,
|
||||
data: UpdateMealPlanInput
|
||||
): Promise<MealPlanResponse> {
|
||||
return apiClient.patch<MealPlanResponse>(
|
||||
`/households/${householdId}/meal-plans/${id}`,
|
||||
data
|
||||
);
|
||||
}
|
||||
|
||||
export async function updateMealPlanStatus(
|
||||
householdId: string,
|
||||
id: string,
|
||||
status: MealPlanStatus
|
||||
): Promise<MealPlanResponse> {
|
||||
return apiClient.patch<MealPlanResponse>(
|
||||
`/households/${householdId}/meal-plans/${id}/status`,
|
||||
{ status }
|
||||
);
|
||||
}
|
||||
|
||||
export async function deleteMealPlan(
|
||||
householdId: string,
|
||||
id: string
|
||||
): Promise<void> {
|
||||
return apiClient.delete(`/households/${householdId}/meal-plans/${id}`);
|
||||
}
|
||||
|
||||
export async function getSuggestions(
|
||||
householdId: string,
|
||||
limit = 5
|
||||
): Promise<RecipeSuggestion[]> {
|
||||
return apiClient.get<RecipeSuggestion[]>(
|
||||
`/households/${householdId}/meal-plans/suggestions?limit=${limit}`
|
||||
);
|
||||
}
|
||||
|
||||
export async function getShoppingGap(
|
||||
householdId: string,
|
||||
id: string
|
||||
): Promise<ShoppingGapReport> {
|
||||
return apiClient.get<ShoppingGapReport>(
|
||||
`/households/${householdId}/meal-plans/${id}/gap`
|
||||
);
|
||||
}
|
||||
45
packages/web/src/services/nutrition-targets.ts
Normal file
45
packages/web/src/services/nutrition-targets.ts
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
import { apiClient } from './api-client';
|
||||
import type { z } from 'zod/v4';
|
||||
import type {
|
||||
NutritionTargetResponseSchema,
|
||||
SetNutritionTargetInput,
|
||||
} from '@meshitrack/shared';
|
||||
|
||||
export type NutritionTargetResponse = z.infer<typeof NutritionTargetResponseSchema>;
|
||||
|
||||
export async function getActiveNutritionTarget(
|
||||
householdId: string
|
||||
): Promise<NutritionTargetResponse | { message: string }> {
|
||||
return apiClient.get<NutritionTargetResponse | { message: string }>(
|
||||
`/households/${householdId}/nutrition-targets`
|
||||
);
|
||||
}
|
||||
|
||||
export async function getNutritionTargetHistory(
|
||||
householdId: string
|
||||
): Promise<NutritionTargetResponse[]> {
|
||||
return apiClient.get<NutritionTargetResponse[]>(
|
||||
`/households/${householdId}/nutrition-targets/history`
|
||||
);
|
||||
}
|
||||
|
||||
export async function setNutritionTarget(
|
||||
householdId: string,
|
||||
data: SetNutritionTargetInput
|
||||
): Promise<NutritionTargetResponse> {
|
||||
return apiClient.post<NutritionTargetResponse>(
|
||||
`/households/${householdId}/nutrition-targets`,
|
||||
data
|
||||
);
|
||||
}
|
||||
|
||||
export async function calculateTargetPreset(
|
||||
householdId: string,
|
||||
calories: number,
|
||||
strategy: 'maintenance' | 'loss' | 'gain'
|
||||
): Promise<SetNutritionTargetInput> {
|
||||
return apiClient.post<SetNutritionTargetInput>(
|
||||
`/households/${householdId}/nutrition-targets/presets`,
|
||||
{ calories, strategy }
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue