Compare commits
No commits in common. "25b4fc97f018a3266716ae4972e16b47d9b2d26d" and "f146e65348d8684c004aa6f6e4081937f3191fe9" have entirely different histories.
25b4fc97f0
...
f146e65348
8 changed files with 0 additions and 413 deletions
|
|
@ -1,108 +0,0 @@
|
||||||
// Layout-mode spec for the planning header's leftovers basket — mocks
|
|
||||||
// `GET /planning` (this job has no live backend; apps/api's Mocha suite
|
|
||||||
// covers the real endpoints). Exercises the popover open/close, an
|
|
||||||
// outside click, and the "Jeter" button firing `PATCH .../discard-leftover`.
|
|
||||||
|
|
||||||
const authenticatedProfile = {
|
|
||||||
id: 1,
|
|
||||||
firstName: "Alice",
|
|
||||||
lastName: "Martin",
|
|
||||||
email: "alice@example.com",
|
|
||||||
tokenVersion: 0,
|
|
||||||
houseId: 1,
|
|
||||||
dietId: null,
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A week with one fresh item that cooked 12 for 10 requested (pool of 2)
|
|
||||||
* and one leftovers item serving 1 of that pool — so the basket should
|
|
||||||
* offer 1 line with 1 portion still available.
|
|
||||||
*/
|
|
||||||
const planningWithLeftovers = {
|
|
||||||
id: 1,
|
|
||||||
startDate: "2026-08-17T00:00:00.000Z",
|
|
||||||
finishDate: "2026-08-23T00:00:00.000Z",
|
|
||||||
items: [
|
|
||||||
{
|
|
||||||
id: 1,
|
|
||||||
weekDay: "lundi",
|
|
||||||
meal: "diner",
|
|
||||||
portions: 12,
|
|
||||||
portionsRequested: 10,
|
|
||||||
discardedPortions: 0,
|
|
||||||
sourceItem: null,
|
|
||||||
recipe: { id: 1, name: "Lasagnes" },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 2,
|
|
||||||
weekDay: "mardi",
|
|
||||||
meal: "dejeuner",
|
|
||||||
portions: 1,
|
|
||||||
portionsRequested: 1,
|
|
||||||
discardedPortions: 0,
|
|
||||||
sourceItem: { id: 1 },
|
|
||||||
recipe: { id: 1, name: "Lasagnes" },
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
describe("Leftovers basket", () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
cy.viewport(1600, 900);
|
|
||||||
cy.clock(new Date("2026-08-17T09:00:00Z"), ["Date"]);
|
|
||||||
cy.intercept("GET", "**/auth/me", { statusCode: 200, body: authenticatedProfile });
|
|
||||||
});
|
|
||||||
|
|
||||||
it("lists a week's available leftovers, opens and closes on an outside click", () => {
|
|
||||||
cy.intercept("GET", /\/planning\?/, { statusCode: 200, body: planningWithLeftovers });
|
|
||||||
cy.visit("/");
|
|
||||||
|
|
||||||
// Count badge = 1 fresh item with a remaining pool.
|
|
||||||
cy.get(".leftovers-basket__count").should("have.text", "1");
|
|
||||||
|
|
||||||
cy.get(".leftovers-basket__toggle").click();
|
|
||||||
cy.contains(".leftovers-basket__heading", "Restes disponibles").should("be.visible");
|
|
||||||
cy.contains(".leftovers-basket__line", "Lasagnes")
|
|
||||||
.should("contain.text", "Lundi")
|
|
||||||
.and("contain.text", "1 en restes");
|
|
||||||
|
|
||||||
// Clicking away from the popover closes it.
|
|
||||||
cy.get("h1").click();
|
|
||||||
cy.get(".leftovers-basket__popover").should("not.exist");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("shows an empty state when nothing is left over", () => {
|
|
||||||
cy.intercept("GET", /\/planning\?/, {
|
|
||||||
statusCode: 200,
|
|
||||||
body: {
|
|
||||||
...planningWithLeftovers,
|
|
||||||
items: [{ ...planningWithLeftovers.items[0], portions: 10, portionsRequested: 10 }],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
cy.visit("/");
|
|
||||||
|
|
||||||
cy.get(".leftovers-basket__count").should("not.exist");
|
|
||||||
cy.get(".leftovers-basket__toggle").click();
|
|
||||||
cy.contains(".leftovers-basket__empty", "Aucun reste cette semaine.").should("be.visible");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("throws away a leftover pool via PATCH and drops it from the list", () => {
|
|
||||||
cy.intercept("GET", /\/planning\?/, { statusCode: 200, body: planningWithLeftovers });
|
|
||||||
cy.intercept("PATCH", "**/planning/items/1/discard-leftover", {
|
|
||||||
statusCode: 200,
|
|
||||||
body: {
|
|
||||||
...planningWithLeftovers.items[0],
|
|
||||||
discardedPortions: 1,
|
|
||||||
},
|
|
||||||
}).as("discard");
|
|
||||||
|
|
||||||
cy.visit("/");
|
|
||||||
cy.get(".leftovers-basket__toggle").click();
|
|
||||||
cy.contains(".leftovers-basket__line", "Lasagnes").find(".leftovers-basket__discard").click();
|
|
||||||
|
|
||||||
cy.wait("@discard");
|
|
||||||
// Pool was 1, now fully discarded -> the line is gone, the popover is empty.
|
|
||||||
cy.contains(".leftovers-basket__empty", "Aucun reste cette semaine.").should("be.visible");
|
|
||||||
cy.get(".leftovers-basket__count").should("not.exist");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
@ -166,11 +166,6 @@ export class ApiClient {
|
||||||
return this._request(`/planning/items/${id}`, { method: "DELETE" });
|
return this._request(`/planning/items/${id}`, { method: "DELETE" });
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Throws away planning item `id`'s whole remaining leftover pool (`discardedPortions += available`) — idempotent, returns the updated item. Rejects with `PLANNING_ITEM_NOT_FOUND`. */
|
|
||||||
public discardLeftover(id: number): Promise<PlanningItemView> {
|
|
||||||
return this._request(`/planning/items/${id}/discard-leftover`, { method: "PATCH" });
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetches the current user's household's shopping list for the week
|
* Fetches the current user's household's shopping list for the week
|
||||||
* covering `date` (`YYYY-MM-DD`, e.g. from `date-tools`'s
|
* covering `date` (`YYYY-MM-DD`, e.g. from `date-tools`'s
|
||||||
|
|
|
||||||
|
|
@ -1,111 +0,0 @@
|
||||||
import type { PlanningItemView, PlanningView } from "@batch-cooking/shared";
|
|
||||||
import { useEffect, useRef, useState } from "react";
|
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
import { apiClient } from "../../api/client";
|
|
||||||
import { collectLeftoverLines } from "./leftovers-basket";
|
|
||||||
import "./leftovers-basket.scss";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The planning header's leftovers basket — an icon (with a count badge)
|
|
||||||
* that opens a popover listing this week's still-available leftover pools:
|
|
||||||
* one line per fresh planning item that cooked more than it needed, with
|
|
||||||
* where it came from, how many portions are left, and a "jeter" button.
|
|
||||||
*
|
|
||||||
* Read-only for now: dragging a leftover onto another slot arrives in a
|
|
||||||
* later iteration. Same conditional-mount + outside-click-to-close pattern
|
|
||||||
* as `WeekNavigator`'s `CalendarPopover`. The list is derived on every
|
|
||||||
* render from `planning` (see {@link collectLeftoverLines}) rather than
|
|
||||||
* held in its own state, so a discard elsewhere on the page is reflected
|
|
||||||
* here for free.
|
|
||||||
*/
|
|
||||||
export function LeftoversBasket({
|
|
||||||
planning,
|
|
||||||
onLeftoverDiscarded,
|
|
||||||
}: {
|
|
||||||
planning: PlanningView | null;
|
|
||||||
/** The updated item as returned by `PATCH .../discard-leftover` — the parent patches it into its planning state. */
|
|
||||||
onLeftoverDiscarded: (item: PlanningItemView) => void;
|
|
||||||
}) {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
|
||||||
const [discardingId, setDiscardingId] = useState<number | null>(null);
|
|
||||||
const popoverRef = useRef<HTMLDivElement>(null);
|
|
||||||
|
|
||||||
const lines = collectLeftoverLines(planning);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!isOpen) return;
|
|
||||||
function handleClickOutside(e: MouseEvent) {
|
|
||||||
if (popoverRef.current && !popoverRef.current.contains(e.target as Node)) {
|
|
||||||
setIsOpen(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
document.addEventListener("mousedown", handleClickOutside);
|
|
||||||
return () => document.removeEventListener("mousedown", handleClickOutside);
|
|
||||||
}, [isOpen]);
|
|
||||||
|
|
||||||
async function discard(item: PlanningItemView) {
|
|
||||||
setDiscardingId(item.id);
|
|
||||||
try {
|
|
||||||
const updated = await apiClient.discardLeftover(item.id);
|
|
||||||
onLeftoverDiscarded(updated);
|
|
||||||
} catch {
|
|
||||||
// Leave the line in place — the user can retry. The error surface
|
|
||||||
// here is intentionally minimal: nothing is lost by a failed discard.
|
|
||||||
} finally {
|
|
||||||
setDiscardingId(null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="leftovers-basket">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="leftovers-basket__toggle"
|
|
||||||
onClick={() => setIsOpen((open) => !open)}
|
|
||||||
aria-expanded={isOpen}
|
|
||||||
aria-label={t("planning.leftovers.tooltip")}
|
|
||||||
title={t("planning.leftovers.tooltip")}
|
|
||||||
>
|
|
||||||
<span aria-hidden="true">🧺</span>
|
|
||||||
{lines.length > 0 && <span className="leftovers-basket__count">{lines.length}</span>}
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{isOpen && (
|
|
||||||
<div className="leftovers-basket__popover" ref={popoverRef}>
|
|
||||||
<h2 className="leftovers-basket__heading">{t("planning.leftovers.title")}</h2>
|
|
||||||
{lines.length === 0 ? (
|
|
||||||
<p className="leftovers-basket__empty">{t("planning.leftovers.empty")}</p>
|
|
||||||
) : (
|
|
||||||
<ul className="leftovers-basket__list">
|
|
||||||
{lines.map(({ item, available }) => (
|
|
||||||
<li key={item.id} className="leftovers-basket__line">
|
|
||||||
<div className="leftovers-basket__meta">
|
|
||||||
<span className="leftovers-basket__recipe">{item.recipe.name}</span>
|
|
||||||
<span className="leftovers-basket__origin">
|
|
||||||
{t("planning.leftovers.origin", {
|
|
||||||
day: t(`common.days.${item.weekDay}`),
|
|
||||||
meal: t(`planning.meals.${item.meal}`),
|
|
||||||
})}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<span className="leftovers-basket__qty">
|
|
||||||
{t("planning.leftovers.available", { count: available })}
|
|
||||||
</span>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="leftovers-basket__discard"
|
|
||||||
onClick={() => void discard(item)}
|
|
||||||
disabled={discardingId === item.id}
|
|
||||||
>
|
|
||||||
{t("planning.leftovers.discard")}
|
|
||||||
</button>
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -1,116 +0,0 @@
|
||||||
// Planning header's leftovers basket — an icon button that toggles a
|
|
||||||
// popover of this week's still-available leftover pools. Same "absolutely
|
|
||||||
// positioned panel hanging off a relative wrapper" shape as
|
|
||||||
// `.calendar-popover` (week-navigator.scss).
|
|
||||||
.leftovers-basket {
|
|
||||||
position: relative;
|
|
||||||
|
|
||||||
&__toggle {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: var(--space-xs);
|
|
||||||
padding: var(--space-xs) var(--space-sm);
|
|
||||||
font-size: var(--font-size-base);
|
|
||||||
line-height: 1;
|
|
||||||
cursor: pointer;
|
|
||||||
border: 1px solid var(--color-border);
|
|
||||||
border-radius: var(--radius-base);
|
|
||||||
background: var(--color-surface);
|
|
||||||
color: var(--color-text);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Small pill showing how many source items still have leftovers.
|
|
||||||
&__count {
|
|
||||||
min-width: 1.1rem;
|
|
||||||
padding: 0 var(--space-xs);
|
|
||||||
font-size: var(--font-size-xs);
|
|
||||||
font-weight: 700;
|
|
||||||
text-align: center;
|
|
||||||
border-radius: 999px;
|
|
||||||
background: var(--color-primary);
|
|
||||||
color: var(--color-surface);
|
|
||||||
}
|
|
||||||
|
|
||||||
&__popover {
|
|
||||||
position: absolute;
|
|
||||||
top: calc(100% + var(--space-xs));
|
|
||||||
right: 0;
|
|
||||||
z-index: 10;
|
|
||||||
width: 20rem;
|
|
||||||
padding: var(--space-md);
|
|
||||||
background: var(--color-surface);
|
|
||||||
border: 1px solid var(--color-border);
|
|
||||||
border-radius: var(--radius-md);
|
|
||||||
box-shadow: var(--shadow-md);
|
|
||||||
}
|
|
||||||
|
|
||||||
&__heading {
|
|
||||||
margin: 0 0 var(--space-sm);
|
|
||||||
font-size: var(--font-size-sm);
|
|
||||||
font-weight: 700;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__empty {
|
|
||||||
margin: 0;
|
|
||||||
color: var(--color-text-muted);
|
|
||||||
font-size: var(--font-size-sm);
|
|
||||||
}
|
|
||||||
|
|
||||||
&__list {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: var(--space-sm);
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
list-style: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__line {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: var(--space-sm);
|
|
||||||
}
|
|
||||||
|
|
||||||
&__meta {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
flex: 1;
|
|
||||||
min-width: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__recipe {
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__origin {
|
|
||||||
color: var(--color-text-muted);
|
|
||||||
font-size: var(--font-size-xs);
|
|
||||||
}
|
|
||||||
|
|
||||||
&__qty {
|
|
||||||
flex-shrink: 0;
|
|
||||||
font-size: var(--font-size-sm);
|
|
||||||
color: var(--color-text-muted);
|
|
||||||
}
|
|
||||||
|
|
||||||
&__discard {
|
|
||||||
flex-shrink: 0;
|
|
||||||
padding: var(--space-xs) var(--space-sm);
|
|
||||||
font-size: var(--font-size-sm);
|
|
||||||
cursor: pointer;
|
|
||||||
border: 1px solid var(--color-border);
|
|
||||||
border-radius: var(--radius-base);
|
|
||||||
background: var(--color-surface);
|
|
||||||
color: var(--color-text-muted);
|
|
||||||
|
|
||||||
&:hover:not(:disabled) {
|
|
||||||
color: var(--color-error);
|
|
||||||
border-color: var(--color-error);
|
|
||||||
}
|
|
||||||
|
|
||||||
&:disabled {
|
|
||||||
opacity: 0.6;
|
|
||||||
cursor: default;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,39 +0,0 @@
|
||||||
import {
|
|
||||||
computeAvailableLeftover,
|
|
||||||
type PlanningItemView,
|
|
||||||
type PlanningView,
|
|
||||||
} from "@batch-cooking/shared";
|
|
||||||
|
|
||||||
/** One row of the leftovers basket — a fresh planning item that still has an unassigned, undiscarded portion pool. */
|
|
||||||
export interface LeftoverLine {
|
|
||||||
item: PlanningItemView;
|
|
||||||
/** Portions still available to serve elsewhere or throw away — always `> 0` for a line that made it into the list. */
|
|
||||||
available: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The leftovers-basket rows for a week's planning: every **fresh** item
|
|
||||||
* (`sourceItem === null`) whose leftover pool still has something in it,
|
|
||||||
* newest slots first (grid order). Pure — the `leftoverUses` each item
|
|
||||||
* needs for {@link computeAvailableLeftover} is reconstructed from the same
|
|
||||||
* flat `planning.items` array (the "leftovers" items that point back at it),
|
|
||||||
* so no extra API round-trip: the exact "one formula, two callers" split
|
|
||||||
* `computeAvailableLeftover`'s own doc comment describes.
|
|
||||||
*/
|
|
||||||
export function collectLeftoverLines(planning: PlanningView | null): LeftoverLine[] {
|
|
||||||
const items = planning?.items ?? [];
|
|
||||||
return items
|
|
||||||
.filter((item) => item.sourceItem === null)
|
|
||||||
.map((item) => ({
|
|
||||||
item,
|
|
||||||
available: computeAvailableLeftover({
|
|
||||||
portions: item.portions,
|
|
||||||
portionsRequested: item.portionsRequested,
|
|
||||||
discardedPortions: item.discardedPortions,
|
|
||||||
leftoverUses: items
|
|
||||||
.filter((other) => other.sourceItem?.id === item.id)
|
|
||||||
.map((other) => ({ portions: other.portions })),
|
|
||||||
}),
|
|
||||||
}))
|
|
||||||
.filter((line) => line.available > 0);
|
|
||||||
}
|
|
||||||
|
|
@ -125,14 +125,6 @@
|
||||||
"title": "Planning de la semaine",
|
"title": "Planning de la semaine",
|
||||||
"startCooking": "Commencer à cuisiner",
|
"startCooking": "Commencer à cuisiner",
|
||||||
"loading": "Chargement du planning…",
|
"loading": "Chargement du planning…",
|
||||||
"leftovers": {
|
|
||||||
"tooltip": "Restes de la semaine",
|
|
||||||
"title": "Restes disponibles",
|
|
||||||
"empty": "Aucun reste cette semaine.",
|
|
||||||
"origin": "{{day}} · {{meal}}",
|
|
||||||
"available": "{{count}} en restes",
|
|
||||||
"discard": "Jeter"
|
|
||||||
},
|
|
||||||
"meals": {
|
"meals": {
|
||||||
"petit-dejeuner": "Petit-déjeuner",
|
"petit-dejeuner": "Petit-déjeuner",
|
||||||
"collation": "Collation",
|
"collation": "Collation",
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,6 @@ import { useEffect, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { apiClient } from "../../api/client";
|
import { apiClient } from "../../api/client";
|
||||||
import { LeftoversBasket } from "../../features/planning/LeftoversBasket";
|
|
||||||
import { type PlanningSlot, RecipePickerDialog } from "../../features/planning/RecipePickerDialog";
|
import { type PlanningSlot, RecipePickerDialog } from "../../features/planning/RecipePickerDialog";
|
||||||
import { WeekNavigator } from "../../features/planning/WeekNavigator";
|
import { WeekNavigator } from "../../features/planning/WeekNavigator";
|
||||||
import "./planning-page.scss";
|
import "./planning-page.scss";
|
||||||
|
|
@ -94,11 +93,6 @@ export function PlanningPage() {
|
||||||
patchPlanningItems((items) => [...items, item]);
|
patchPlanningItems((items) => [...items, item]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** `PATCH .../discard-leftover` returns the updated item — swap it in by id so the basket recomputes. */
|
|
||||||
function handleLeftoverDiscarded(updated: PlanningItemView) {
|
|
||||||
patchPlanningItems((items) => items.map((i) => (i.id === updated.id ? updated : i)));
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Optimistic removal (same rollback-on-failure idea as `FavoriteStarButton`) — the API call already happened by the time this needs to roll back, `removePlanningItem` having already rejected. */
|
/** Optimistic removal (same rollback-on-failure idea as `FavoriteStarButton`) — the API call already happened by the time this needs to roll back, `removePlanningItem` having already rejected. */
|
||||||
async function handleRemove(item: PlanningItemView) {
|
async function handleRemove(item: PlanningItemView) {
|
||||||
patchPlanningItems((items) => items.filter((i) => i.id !== item.id));
|
patchPlanningItems((items) => items.filter((i) => i.id !== item.id));
|
||||||
|
|
@ -118,12 +112,6 @@ export function PlanningPage() {
|
||||||
<div className="planning-page__header">
|
<div className="planning-page__header">
|
||||||
<h1>{t("planning.title")}</h1>
|
<h1>{t("planning.title")}</h1>
|
||||||
<WeekNavigator weekStart={weekStart} onChangeWeek={setWeekStart} />
|
<WeekNavigator weekStart={weekStart} onChangeWeek={setWeekStart} />
|
||||||
{state.status === "loaded" && (
|
|
||||||
<LeftoversBasket
|
|
||||||
planning={state.planning}
|
|
||||||
onLeftoverDiscarded={handleLeftoverDiscarded}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="planning-page__cook-btn"
|
className="planning-page__cook-btn"
|
||||||
|
|
|
||||||
|
|
@ -38,7 +38,6 @@ apps/web/src/
|
||||||
│ ├── planning/
|
│ ├── planning/
|
||||||
│ │ ├── RecipePickerDialog.tsx # dialogue "ajouter au planning" — parcourir/prévisualiser/importer, voir plus bas
|
│ │ ├── RecipePickerDialog.tsx # dialogue "ajouter au planning" — parcourir/prévisualiser/importer, voir plus bas
|
||||||
│ │ ├── recipe-picker-dialog.scss
|
│ │ ├── recipe-picker-dialog.scss
|
||||||
│ │ ├── LeftoversBasket.tsx + leftovers-basket.ts + leftovers-basket.scss # icône + popover des restes de la semaine dans l'en-tête du planning (issue #34)
|
|
||||||
│ │ └── WeekNavigator.tsx + week-navigator.scss # arrows + calendrier de sélection de semaine — partagé par PlanningPage et ShoppingListPage, voir plus bas
|
│ │ └── WeekNavigator.tsx + week-navigator.scss # arrows + calendrier de sélection de semaine — partagé par PlanningPage et ShoppingListPage, voir plus bas
|
||||||
│ └── recipes/ # catalogue, import, édition — voir plus bas ; sous-dossiers par sous-domaine, pas de fichiers à plat
|
│ └── recipes/ # catalogue, import, édition — voir plus bas ; sous-dossiers par sous-domaine, pas de fichiers à plat
|
||||||
│ ├── RecipeTable.tsx / RecipeTabs.tsx / RecipeDetailPanel.tsx # racine : composants transverses au sous-domaine (utilisés par plusieurs des sous-dossiers ci-dessous)
|
│ ├── RecipeTable.tsx / RecipeTabs.tsx / RecipeDetailPanel.tsx # racine : composants transverses au sous-domaine (utilisés par plusieurs des sous-dossiers ci-dessous)
|
||||||
|
|
@ -391,19 +390,6 @@ une bordure appuyée après `collation`/`dejeuner`/`gouter`.
|
||||||
immédiatement, `DELETE /planning/items/:id`, restauré en cas d'échec).
|
immédiatement, `DELETE /planning/items/:id`, restauré en cas d'échec).
|
||||||
- Chaque recette planifiée s'affiche en pastille `"{nom} · ×{portions}"` avec
|
- Chaque recette planifiée s'affiche en pastille `"{nom} · ×{portions}"` avec
|
||||||
un bouton de retrait (✕).
|
un bouton de retrait (✕).
|
||||||
- **Panier de restes** — `LeftoversBasket` (`features/planning/`) dans
|
|
||||||
l'en-tête, à côté de `WeekNavigator` : une icône 🧺 avec un badge de
|
|
||||||
compte qui ouvre un popover (même patron montage conditionnel +
|
|
||||||
`mousedown` extérieur que `CalendarPopover`) listant les restes encore
|
|
||||||
disponibles de la semaine. Les lignes sont dérivées de `planning.items`
|
|
||||||
à chaque rendu par `collectLeftoverLines` (`leftovers-basket.ts`, pur) :
|
|
||||||
chaque item « frais » (`sourceItem === null`) dont
|
|
||||||
`computeAvailableLeftover` (`@batch-cooking/shared`, `leftoverUses`
|
|
||||||
reconstitué depuis le tableau plat) est `> 0`. Bouton « Jeter » par
|
|
||||||
ligne → `PATCH /planning/items/:id/discard-leftover`, la réponse est
|
|
||||||
patchée dans `planning.items` par id. Le glisser-déposer d'un reste sur
|
|
||||||
un autre créneau arrive dans une itération ultérieure. Libellés :
|
|
||||||
`planning.leftovers.*`.
|
|
||||||
|
|
||||||
### `RecipePickerDialog` — parcourir, prévisualiser, importer
|
### `RecipePickerDialog` — parcourir, prévisualiser, importer
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue