Compare commits
2 Commits
32d387c94b
...
868862d031
| Author | SHA1 | Date | |
|---|---|---|---|
| 868862d031 | |||
| ac32e05c31 |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1,2 +1,2 @@
|
|||||||
.DS_Store
|
.DS_Store
|
||||||
.idea/workspace.xml
|
.idea
|
||||||
6
.idea/misc.xml
generated
6
.idea/misc.xml
generated
@@ -1,6 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
|
||||||
<component name="ProjectRootManager" version="2" project-jdk-name="25" project-jdk-type="JavaSDK">
|
|
||||||
<output url="file://$PROJECT_DIR$/out" />
|
|
||||||
</component>
|
|
||||||
</project>
|
|
||||||
8
.idea/modules.xml
generated
8
.idea/modules.xml
generated
@@ -1,8 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
|
||||||
<component name="ProjectModuleManager">
|
|
||||||
<modules>
|
|
||||||
<module fileurl="file://$PROJECT_DIR$/.idea/recipe-mockup.iml" filepath="$PROJECT_DIR$/.idea/recipe-mockup.iml" />
|
|
||||||
</modules>
|
|
||||||
</component>
|
|
||||||
</project>
|
|
||||||
9
.idea/recipe-mockup.iml
generated
9
.idea/recipe-mockup.iml
generated
@@ -1,9 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<module type="JAVA_MODULE" version="4">
|
|
||||||
<component name="NewModuleRootManager" inherit-compiler-output="true">
|
|
||||||
<exclude-output />
|
|
||||||
<content url="file://$MODULE_DIR$" />
|
|
||||||
<orderEntry type="inheritedJdk" />
|
|
||||||
<orderEntry type="sourceFolder" forTests="false" />
|
|
||||||
</component>
|
|
||||||
</module>
|
|
||||||
6
.idea/vcs.xml
generated
6
.idea/vcs.xml
generated
@@ -1,6 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
|
||||||
<component name="VcsDirectoryMappings">
|
|
||||||
<mapping directory="" vcs="Git" />
|
|
||||||
</component>
|
|
||||||
</project>
|
|
||||||
188
.idea/workspace.xml
generated
188
.idea/workspace.xml
generated
@@ -1,188 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
|
||||||
<component name="AutoImportSettings">
|
|
||||||
<option name="autoReloadType" value="SELECTIVE" />
|
|
||||||
</component>
|
|
||||||
<component name="ChangeListManager">
|
|
||||||
<list default="true" id="ae0e4ce8-372f-4cfd-a04f-600640f32223" name="Changes" comment="Restore gitea action">
|
|
||||||
<change beforePath="$PROJECT_DIR$/.gitignore" beforeDir="false" afterPath="$PROJECT_DIR$/.gitignore" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/js/views/MealPlanner.js" beforeDir="false" afterPath="$PROJECT_DIR$/js/views/MealPlanner.js" afterDir="false" />
|
|
||||||
</list>
|
|
||||||
<option name="SHOW_DIALOG" value="false" />
|
|
||||||
<option name="HIGHLIGHT_CONFLICTS" value="true" />
|
|
||||||
<option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
|
|
||||||
<option name="LAST_RESOLUTION" value="IGNORE" />
|
|
||||||
</component>
|
|
||||||
<component name="ChangesViewManager">
|
|
||||||
<option name="groupingKeys">
|
|
||||||
<option value="directory" />
|
|
||||||
<option value="repository" />
|
|
||||||
</option>
|
|
||||||
</component>
|
|
||||||
<component name="EmbeddingIndexingInfo">
|
|
||||||
<option name="cachedIndexableFilesCount" value="35" />
|
|
||||||
<option name="fileBasedEmbeddingIndicesEnabled" value="true" />
|
|
||||||
</component>
|
|
||||||
<component name="Git.Settings">
|
|
||||||
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" />
|
|
||||||
</component>
|
|
||||||
<component name="KubernetesApiPersistence">{}</component>
|
|
||||||
<component name="KubernetesApiProvider">{
|
|
||||||
"isMigrated": true
|
|
||||||
}</component>
|
|
||||||
<component name="McpProjectServerCommands">
|
|
||||||
<commands />
|
|
||||||
<urls />
|
|
||||||
</component>
|
|
||||||
<component name="ProjectColorInfo">{
|
|
||||||
"associatedIndex": 8,
|
|
||||||
"fromUser": false
|
|
||||||
}</component>
|
|
||||||
<component name="ProjectId" id="3BqiA2Jye2iyFuy4jKirNGx94l7" />
|
|
||||||
<component name="ProjectViewState">
|
|
||||||
<option name="hideEmptyMiddlePackages" value="true" />
|
|
||||||
<option name="showLibraryContents" value="true" />
|
|
||||||
</component>
|
|
||||||
<component name="PropertiesComponent">{
|
|
||||||
"keyToString": {
|
|
||||||
"ModuleVcsDetector.initialDetectionPerformed": "true",
|
|
||||||
"RunOnceActivity.MCP Project settings loaded": "true",
|
|
||||||
"RunOnceActivity.ShowReadmeOnStart": "true",
|
|
||||||
"RunOnceActivity.TerminalTabsStorage.copyFrom.TerminalArrangementManager.252": "true",
|
|
||||||
"RunOnceActivity.cidr.known.project.marker": "true",
|
|
||||||
"RunOnceActivity.git.unshallow": "true",
|
|
||||||
"RunOnceActivity.readMode.enableVisualFormatting": "true",
|
|
||||||
"RunOnceActivity.typescript.service.memoryLimit.init": "true",
|
|
||||||
"cidr.known.project.marker": "true",
|
|
||||||
"codeWithMe.voiceChat.enabledByDefault": "false",
|
|
||||||
"com.intellij.ml.llm.matterhorn.ej.ui.settings.DefaultModelSelectionForGA.v1": "true",
|
|
||||||
"git-widget-placeholder": "master",
|
|
||||||
"junie.onboarding.icon.badge.shown": "true",
|
|
||||||
"kotlin-language-version-configured": "true",
|
|
||||||
"last_opened_file_path": "/Users/rwilk/dev/repo/recipe-mockup",
|
|
||||||
"node.js.detected.package.eslint": "true",
|
|
||||||
"node.js.detected.package.tslint": "true",
|
|
||||||
"node.js.selected.package.eslint": "(autodetect)",
|
|
||||||
"node.js.selected.package.tslint": "(autodetect)",
|
|
||||||
"nodejs_package_manager_path": "npm",
|
|
||||||
"settings.editor.selected.configurable": "consents",
|
|
||||||
"to.speed.mode.migration.done": "true",
|
|
||||||
"vue.rearranger.settings.migration": "true"
|
|
||||||
}
|
|
||||||
}</component>
|
|
||||||
<component name="RunManager">
|
|
||||||
<configuration default="true" type="AppleRunConfiguration" factoryName="Application" REDIRECT_INPUT="false" ELEVATE="false" USE_EXTERNAL_CONSOLE="false" EMULATE_TERMINAL="false" PASS_PARENT_ENVS_2="true" IS_LOCATION_SIMULATION_SUPPORTED="false" IS_LOCATION_SIMULATION_ALLOWED="true" APPLICATION_LANGUAGE="IDELaunchSchemeLanguageUseSystemLanguage" APPLICATION_REGION="" DEVELOPMENT_TEAM="${TEAM_ID}" MAKE_ACTIVE="TRUE" SHOULD_DEBUG_EXTENSIONS="false">
|
|
||||||
<embedded_app_extension_list />
|
|
||||||
<method v="2">
|
|
||||||
<option name="com.jetbrains.cidr.execution.CidrBuildBeforeRunTaskProvider$BuildBeforeRunTask" enabled="true" />
|
|
||||||
</method>
|
|
||||||
</configuration>
|
|
||||||
</component>
|
|
||||||
<component name="TaskManager">
|
|
||||||
<task active="true" id="Default" summary="Default task">
|
|
||||||
<changelist id="ae0e4ce8-372f-4cfd-a04f-600640f32223" name="Changes" comment="" />
|
|
||||||
<created>1775222853874</created>
|
|
||||||
<option name="number" value="Default" />
|
|
||||||
<option name="presentableId" value="Default" />
|
|
||||||
<updated>1775222853874</updated>
|
|
||||||
<workItem from="1775222854878" duration="7564000" />
|
|
||||||
</task>
|
|
||||||
<task id="LOCAL-00001" summary="Rework calendar">
|
|
||||||
<option name="closed" value="true" />
|
|
||||||
<created>1775314482369</created>
|
|
||||||
<option name="number" value="00001" />
|
|
||||||
<option name="presentableId" value="LOCAL-00001" />
|
|
||||||
<option name="project" value="LOCAL" />
|
|
||||||
<updated>1775314482369</updated>
|
|
||||||
</task>
|
|
||||||
<task id="LOCAL-00002" summary="Redesign meal plan editor">
|
|
||||||
<option name="closed" value="true" />
|
|
||||||
<created>1775332140771</created>
|
|
||||||
<option name="number" value="00002" />
|
|
||||||
<option name="presentableId" value="LOCAL-00002" />
|
|
||||||
<option name="project" value="LOCAL" />
|
|
||||||
<updated>1775332140771</updated>
|
|
||||||
</task>
|
|
||||||
<task id="LOCAL-00003" summary="Redesign recipe details">
|
|
||||||
<option name="closed" value="true" />
|
|
||||||
<created>1775334379181</created>
|
|
||||||
<option name="number" value="00003" />
|
|
||||||
<option name="presentableId" value="LOCAL-00003" />
|
|
||||||
<option name="project" value="LOCAL" />
|
|
||||||
<updated>1775334379182</updated>
|
|
||||||
</task>
|
|
||||||
<task id="LOCAL-00004" summary="Redesign meal planner">
|
|
||||||
<option name="closed" value="true" />
|
|
||||||
<created>1775337998927</created>
|
|
||||||
<option name="number" value="00004" />
|
|
||||||
<option name="presentableId" value="LOCAL-00004" />
|
|
||||||
<option name="project" value="LOCAL" />
|
|
||||||
<updated>1775337998927</updated>
|
|
||||||
</task>
|
|
||||||
<task id="LOCAL-00005" summary="Restore gitea action">
|
|
||||||
<option name="closed" value="true" />
|
|
||||||
<created>1775338197344</created>
|
|
||||||
<option name="number" value="00005" />
|
|
||||||
<option name="presentableId" value="LOCAL-00005" />
|
|
||||||
<option name="project" value="LOCAL" />
|
|
||||||
<updated>1775338197344</updated>
|
|
||||||
</task>
|
|
||||||
<option name="localTasksCounter" value="6" />
|
|
||||||
<servers />
|
|
||||||
</component>
|
|
||||||
<component name="TypeScriptGeneratedFilesManager">
|
|
||||||
<option name="version" value="3" />
|
|
||||||
</component>
|
|
||||||
<component name="Vcs.Log.Tabs.Properties">
|
|
||||||
<option name="RECENT_FILTERS">
|
|
||||||
<map>
|
|
||||||
<entry key="Branch">
|
|
||||||
<value>
|
|
||||||
<list>
|
|
||||||
<RecentGroup>
|
|
||||||
<option name="FILTER_VALUES">
|
|
||||||
<option value="HEAD" />
|
|
||||||
</option>
|
|
||||||
</RecentGroup>
|
|
||||||
</list>
|
|
||||||
</value>
|
|
||||||
</entry>
|
|
||||||
</map>
|
|
||||||
</option>
|
|
||||||
<option name="TAB_STATES">
|
|
||||||
<map>
|
|
||||||
<entry key="MAIN">
|
|
||||||
<value>
|
|
||||||
<State>
|
|
||||||
<option name="CUSTOM_BOOLEAN_PROPERTIES">
|
|
||||||
<map>
|
|
||||||
<entry key="Show.Git.Branches" value="false" />
|
|
||||||
</map>
|
|
||||||
</option>
|
|
||||||
<option name="FILTERS">
|
|
||||||
<map>
|
|
||||||
<entry key="branch">
|
|
||||||
<value>
|
|
||||||
<list>
|
|
||||||
<option value="HEAD" />
|
|
||||||
</list>
|
|
||||||
</value>
|
|
||||||
</entry>
|
|
||||||
</map>
|
|
||||||
</option>
|
|
||||||
</State>
|
|
||||||
</value>
|
|
||||||
</entry>
|
|
||||||
</map>
|
|
||||||
</option>
|
|
||||||
</component>
|
|
||||||
<component name="VcsManagerConfiguration">
|
|
||||||
<MESSAGE value="Rework calendar" />
|
|
||||||
<MESSAGE value="Redesign meal plan editor" />
|
|
||||||
<MESSAGE value="Redesign recipe details" />
|
|
||||||
<MESSAGE value="Redesign meal planner" />
|
|
||||||
<MESSAGE value="Restore gitea action" />
|
|
||||||
<option name="LAST_COMMIT_MESSAGE" value="Restore gitea action" />
|
|
||||||
</component>
|
|
||||||
</project>
|
|
||||||
@@ -20,6 +20,7 @@ export const CATEGORY_LABELS = {
|
|||||||
* @typedef {{ kcal: number, protein: number, fat: number, carbs: number }} NutritionPer100
|
* @typedef {{ kcal: number, protein: number, fat: number, carbs: number }} NutritionPer100
|
||||||
* @typedef {{ amount: number, label?: string }} PurchasePack
|
* @typedef {{ amount: number, label?: string }} PurchasePack
|
||||||
* @typedef {{ id: string, name: string, category: keyof typeof CATEGORY_LABELS, pantryUnit: 'g'|'ml'|'szt', purchasePack?: PurchasePack, nutritionPer100g?: NutritionPer100 }} IngredientDef
|
* @typedef {{ id: string, name: string, category: keyof typeof CATEGORY_LABELS, pantryUnit: 'g'|'ml'|'szt', purchasePack?: PurchasePack, nutritionPer100g?: NutritionPer100 }} IngredientDef
|
||||||
|
* @typedef {{ id: string, ingredientId: string, name: string, brand?: string, packSize: number, packLabel?: string, nutritionPer100g: NutritionPer100 }} ProductDef
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/** @type {Record<string, IngredientDef>} */
|
/** @type {Record<string, IngredientDef>} */
|
||||||
@@ -59,6 +60,14 @@ export const INGREDIENTS = {
|
|||||||
purchasePack: { amount: 250, label: 'opakowanie 250 g' },
|
purchasePack: { amount: 250, label: 'opakowanie 250 g' },
|
||||||
nutritionPer100g: { kcal: 174, protein: 11, fat: 13, carbs: 3 },
|
nutritionPer100g: { kcal: 174, protein: 11, fat: 13, carbs: 3 },
|
||||||
},
|
},
|
||||||
|
burrata: {
|
||||||
|
id: 'burrata',
|
||||||
|
name: 'Burrata',
|
||||||
|
category: 'nabial',
|
||||||
|
pantryUnit: 'g',
|
||||||
|
purchasePack: { amount: 150, label: 'opakowanie 150 g' },
|
||||||
|
nutritionPer100g: { kcal: 280, protein: 16, fat: 22, carbs: 2 },
|
||||||
|
},
|
||||||
serek_wiejski: {
|
serek_wiejski: {
|
||||||
id: 'serek_wiejski',
|
id: 'serek_wiejski',
|
||||||
name: 'Serek wiejski',
|
name: 'Serek wiejski',
|
||||||
@@ -339,7 +348,7 @@ export const RECIPES = {
|
|||||||
{ ingredientId: 'czosnek', amount: 6, unit: 'g' },
|
{ ingredientId: 'czosnek', amount: 6, unit: 'g' },
|
||||||
{ ingredientId: 'tymianek', amount: 1, unit: 'g' },
|
{ ingredientId: 'tymianek', amount: 1, unit: 'g' },
|
||||||
{ ingredientId: 'oliwa', amount: 5, unit: 'ml' },
|
{ ingredientId: 'oliwa', amount: 5, unit: 'ml' },
|
||||||
{ ingredientId: 'ricotta', amount: 75, unit: 'g' },
|
{ ingredientId: 'ricotta', amount: 75, unit: 'g', alternatives: ['burrata'] },
|
||||||
{ ingredientId: 'bazylia_swieza', amount: 3, unit: 'g' },
|
{ ingredientId: 'bazylia_swieza', amount: 3, unit: 'g' },
|
||||||
{ ingredientId: 'nasiona_slonecznika', amount: 15, unit: 'g' },
|
{ ingredientId: 'nasiona_slonecznika', amount: 15, unit: 'g' },
|
||||||
],
|
],
|
||||||
@@ -448,6 +457,119 @@ export const RECIPES = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/* ══════════════════════════════════════════════════════════════════════
|
||||||
|
* Konkretne produkty — warianty składników generycznych.
|
||||||
|
* Każdy produkt należy do jednego IngredientDef (przez ingredientId).
|
||||||
|
* ══════════════════════════════════════════════════════════════════════ */
|
||||||
|
|
||||||
|
/** @type {Record<string, ProductDef>} */
|
||||||
|
export const PRODUCTS = {
|
||||||
|
/* ── Nabiał ───────────────────────────────────────── */
|
||||||
|
almette_naturalny: {
|
||||||
|
id: 'almette_naturalny',
|
||||||
|
ingredientId: 'serek_smietankowy',
|
||||||
|
name: 'Almette naturalny',
|
||||||
|
brand: 'Almette',
|
||||||
|
packSize: 150,
|
||||||
|
packLabel: '150 g',
|
||||||
|
nutritionPer100g: { kcal: 234, protein: 5.5, fat: 22, carbs: 3.5 },
|
||||||
|
},
|
||||||
|
philadelphia_original: {
|
||||||
|
id: 'philadelphia_original',
|
||||||
|
ingredientId: 'serek_smietankowy',
|
||||||
|
name: 'Philadelphia Original',
|
||||||
|
brand: 'Philadelphia',
|
||||||
|
packSize: 125,
|
||||||
|
packLabel: '125 g',
|
||||||
|
nutritionPer100g: { kcal: 235, protein: 5.4, fat: 21.5, carbs: 4.1 },
|
||||||
|
},
|
||||||
|
mozzarella_galbani: {
|
||||||
|
id: 'mozzarella_galbani',
|
||||||
|
ingredientId: 'mozzarella',
|
||||||
|
name: 'Galbani Mozzarella',
|
||||||
|
brand: 'Galbani',
|
||||||
|
packSize: 125,
|
||||||
|
packLabel: '125 g',
|
||||||
|
nutritionPer100g: { kcal: 253, protein: 18.4, fat: 19.1, carbs: 1.6 },
|
||||||
|
},
|
||||||
|
ricotta_galbani: {
|
||||||
|
id: 'ricotta_galbani',
|
||||||
|
ingredientId: 'ricotta',
|
||||||
|
name: 'Galbani Ricotta',
|
||||||
|
brand: 'Galbani',
|
||||||
|
packSize: 250,
|
||||||
|
packLabel: '250 g',
|
||||||
|
nutritionPer100g: { kcal: 138, protein: 10, fat: 10, carbs: 3 },
|
||||||
|
},
|
||||||
|
serek_wiejski_piatnica: {
|
||||||
|
id: 'serek_wiejski_piatnica',
|
||||||
|
ingredientId: 'serek_wiejski',
|
||||||
|
name: 'Piątnica Serek wiejski',
|
||||||
|
brand: 'Piątnica',
|
||||||
|
packSize: 200,
|
||||||
|
packLabel: '200 g',
|
||||||
|
nutritionPer100g: { kcal: 97, protein: 11, fat: 5, carbs: 2 },
|
||||||
|
},
|
||||||
|
serek_wiejski_piatnica_wb: {
|
||||||
|
id: 'serek_wiejski_piatnica_wb',
|
||||||
|
ingredientId: 'serek_wiejski',
|
||||||
|
name: 'Piątnica Serek wiejski wysokobiałkowy',
|
||||||
|
brand: 'Piątnica',
|
||||||
|
packSize: 200,
|
||||||
|
packLabel: '200 g',
|
||||||
|
nutritionPer100g: { kcal: 93, protein: 14, fat: 3, carbs: 2.4 },
|
||||||
|
},
|
||||||
|
/* ── Mięso i ryby ─────────────────────────────────── */
|
||||||
|
burrata_milbona: {
|
||||||
|
id: 'burrata_milbona',
|
||||||
|
ingredientId: 'burrata',
|
||||||
|
name: 'Milbona Burrata',
|
||||||
|
brand: 'Milbona',
|
||||||
|
packSize: 125,
|
||||||
|
packLabel: '125 g',
|
||||||
|
nutritionPer100g: { kcal: 254, protein: 10, fat: 23, carbs: 1.8 },
|
||||||
|
},
|
||||||
|
burrata_gustobello: {
|
||||||
|
id: 'burrata_gustobello',
|
||||||
|
ingredientId: 'burrata',
|
||||||
|
name: 'GustoBello Burrata',
|
||||||
|
brand: 'GustoBello',
|
||||||
|
packSize: 100,
|
||||||
|
packLabel: '100 g',
|
||||||
|
nutritionPer100g: { kcal: 259, protein: 10, fat: 23, carbs: 2 },
|
||||||
|
},
|
||||||
|
/* ── Mięso i ryby ─────────────────────────────────── */
|
||||||
|
losos_wedzony_suempol: {
|
||||||
|
id: 'losos_wedzony_suempol',
|
||||||
|
ingredientId: 'losos_wedzony',
|
||||||
|
name: 'Suempol Łosoś atlantycki wędzony',
|
||||||
|
brand: 'Suempol',
|
||||||
|
packSize: 100,
|
||||||
|
packLabel: '100 g',
|
||||||
|
nutritionPer100g: { kcal: 160, protein: 21.5, fat: 8, carbs: 0.5 },
|
||||||
|
},
|
||||||
|
/* ── Inne ─────────────────────────────────────────── */
|
||||||
|
hummus_klasyczny_well_well: {
|
||||||
|
id: 'hummus_klasyczny_well_well',
|
||||||
|
ingredientId: 'hummus',
|
||||||
|
name: 'Well Well Hummus klasyczny',
|
||||||
|
brand: 'Well Well',
|
||||||
|
packSize: 200,
|
||||||
|
packLabel: '200 g',
|
||||||
|
nutritionPer100g: { kcal: 198, protein: 6.6, fat: 12, carbs: 16 },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
/** @param {string} ingredientId @returns {ProductDef[]} */
|
||||||
|
export function getProductsForIngredient(ingredientId) {
|
||||||
|
return Object.values(PRODUCTS).filter(p => p.ingredientId === ingredientId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param {string} ingredientId @returns {boolean} */
|
||||||
|
export function ingredientHasProducts(ingredientId) {
|
||||||
|
return Object.values(PRODUCTS).some(p => p.ingredientId === ingredientId);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Krok +/- w spiżarni: całe opakowanie albo domyślny krok (10 g/ml lub 1 szt.).
|
* Krok +/- w spiżarni: całe opakowanie albo domyślny krok (10 g/ml lub 1 szt.).
|
||||||
* @param {string} ingredientId
|
* @param {string} ingredientId
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { INGREDIENTS, CATEGORY_LABELS } from '../data/catalog.js?v=2';
|
import { INGREDIENTS, CATEGORY_LABELS, PRODUCTS, ingredientHasProducts } from '../data/catalog.js?v=6';
|
||||||
import { PANTRY_STORAGE_KEY, SHOPPING_STORAGE_KEY } from '../storageKeys.js';
|
import { PANTRY_STORAGE_KEY, PANTRY_STORAGE_KEY_V2, SHOPPING_STORAGE_KEY } from '../storageKeys.js';
|
||||||
|
|
||||||
export const KITCHEN_LIST_ID = 'kitchen';
|
export const KITCHEN_LIST_ID = 'kitchen';
|
||||||
export const MISC_LIST_ID = 'misc';
|
export const MISC_LIST_ID = 'misc';
|
||||||
@@ -11,7 +11,7 @@ function newId(prefix) {
|
|||||||
return `${prefix}${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
|
return `${prefix}${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @typedef {{ id: string, ingredientId: string, name: string, amount: number, unit: string, category: string, checked: boolean, sourceNote?: string }} KitchenShoppingItem */
|
/** @typedef {{ id: string, ingredientId: string, productId?: string, name: string, amount: number, unit: string, category: string, checked: boolean, sourceNote?: string }} KitchenShoppingItem */
|
||||||
/** @typedef {{ id: string, text: string, note?: string, checked: boolean }} FreeformShoppingItem */
|
/** @typedef {{ id: string, text: string, note?: string, checked: boolean }} FreeformShoppingItem */
|
||||||
/** @typedef {{ id: string, name: string, type: 'kitchen'|'freeform', items: (KitchenShoppingItem|FreeformShoppingItem)[] }} ShoppingListDef */
|
/** @typedef {{ id: string, name: string, type: 'kitchen'|'freeform', items: (KitchenShoppingItem|FreeformShoppingItem)[] }} ShoppingListDef */
|
||||||
|
|
||||||
@@ -39,7 +39,7 @@ function normalizeKitchenItem(x) {
|
|||||||
if (!o.ingredientId || !Number.isFinite(Number(o.amount))) return null;
|
if (!o.ingredientId || !Number.isFinite(Number(o.amount))) return null;
|
||||||
const id = String(o.id && String(o.id).length ? o.id : newId('s'));
|
const id = String(o.id && String(o.id).length ? o.id : newId('s'));
|
||||||
const ingId = String(o.ingredientId);
|
const ingId = String(o.ingredientId);
|
||||||
return {
|
const item = {
|
||||||
id,
|
id,
|
||||||
ingredientId: ingId,
|
ingredientId: ingId,
|
||||||
name: String(o.name || INGREDIENTS[ingId]?.name || ingId),
|
name: String(o.name || INGREDIENTS[ingId]?.name || ingId),
|
||||||
@@ -49,6 +49,10 @@ function normalizeKitchenItem(x) {
|
|||||||
checked: Boolean(o.checked),
|
checked: Boolean(o.checked),
|
||||||
sourceNote: o.sourceNote ? String(o.sourceNote) : undefined,
|
sourceNote: o.sourceNote ? String(o.sourceNote) : undefined,
|
||||||
};
|
};
|
||||||
|
if (o.productId && typeof o.productId === 'string' && PRODUCTS[o.productId]) {
|
||||||
|
item.productId = o.productId;
|
||||||
|
}
|
||||||
|
return item;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @param {unknown} x */
|
/** @param {unknown} x */
|
||||||
@@ -222,23 +226,28 @@ export function addOrMergeShoppingLines(lines, listId = KITCHEN_LIST_ID) {
|
|||||||
const def = INGREDIENTS[L.ingredientId];
|
const def = INGREDIENTS[L.ingredientId];
|
||||||
const name = L.name || def?.name || L.ingredientId;
|
const name = L.name || def?.name || L.ingredientId;
|
||||||
const category = L.category || def?.category || 'inne';
|
const category = L.category || def?.category || 'inne';
|
||||||
|
const productId = L.productId && PRODUCTS[L.productId] ? L.productId : undefined;
|
||||||
|
const productName = productId ? PRODUCTS[productId].name : null;
|
||||||
|
const displayName = productName || name;
|
||||||
const idx = open.findIndex(
|
const idx = open.findIndex(
|
||||||
(x) => x.ingredientId === L.ingredientId && x.unit === L.unit,
|
(x) => x.ingredientId === L.ingredientId && x.unit === L.unit && (x.productId || '') === (productId || ''),
|
||||||
);
|
);
|
||||||
if (idx >= 0) {
|
if (idx >= 0) {
|
||||||
open[idx].amount = Math.round((open[idx].amount + L.amount) * 100) / 100;
|
open[idx].amount = Math.round((open[idx].amount + L.amount) * 100) / 100;
|
||||||
if (L.sourceNote && !open[idx].sourceNote) open[idx].sourceNote = L.sourceNote;
|
if (L.sourceNote && !open[idx].sourceNote) open[idx].sourceNote = L.sourceNote;
|
||||||
} else {
|
} else {
|
||||||
open.push({
|
const item = {
|
||||||
id: newId('s'),
|
id: newId('s'),
|
||||||
ingredientId: L.ingredientId,
|
ingredientId: L.ingredientId,
|
||||||
name,
|
name: displayName,
|
||||||
amount: Math.round(L.amount * 100) / 100,
|
amount: Math.round(L.amount * 100) / 100,
|
||||||
unit: L.unit,
|
unit: L.unit,
|
||||||
category,
|
category,
|
||||||
checked: false,
|
checked: false,
|
||||||
sourceNote: L.sourceNote,
|
sourceNote: L.sourceNote,
|
||||||
});
|
};
|
||||||
|
if (productId) item.productId = productId;
|
||||||
|
open.push(item);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -338,7 +347,7 @@ export function computeShortfalls(needLines, pantry = loadPantry()) {
|
|||||||
short.push({ ...L, pantryQty: 0, shortfall: L.amount, unitMismatch: true });
|
short.push({ ...L, pantryQty: 0, shortfall: L.amount, unitMismatch: true });
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const have = Number(pantry[L.ingredientId]) || 0;
|
const have = getPantryTotal(L.ingredientId, pantry);
|
||||||
const miss = Math.max(0, Math.round((L.amount - have) * 100) / 100);
|
const miss = Math.max(0, Math.round((L.amount - have) * 100) / 100);
|
||||||
if (miss > 0) {
|
if (miss > 0) {
|
||||||
short.push({
|
short.push({
|
||||||
@@ -370,35 +379,154 @@ export function categoryLabel(cat) {
|
|||||||
return CATEGORY_LABELS[cat] || CATEGORY_LABELS.inne;
|
return CATEGORY_LABELS[cat] || CATEGORY_LABELS.inne;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @returns {Record<string, number>} ingredientId -> ilość w pantryUnit */
|
/* ══════════════════════════════════════════════════════════════════════
|
||||||
export function loadPantry() {
|
* Pantry v2 — hybrydowy format.
|
||||||
|
*
|
||||||
|
* Wartość dla składnika może być:
|
||||||
|
* number — składnik generyczny (bez zdefiniowanych produktów)
|
||||||
|
* { _total, items: [{productId, qty}], generic } — składnik z produktami
|
||||||
|
*
|
||||||
|
* _total = generic + sum(items[].qty) (cache, zawsze przeliczany przy zapisie)
|
||||||
|
* ══════════════════════════════════════════════════════════════════════ */
|
||||||
|
|
||||||
|
/** @typedef {{ productId: string, qty: number }} PantryProductItem */
|
||||||
|
/** @typedef {{ _total: number, items: PantryProductItem[], generic: number }} PantryProductEntry */
|
||||||
|
/** @typedef {Record<string, number | PantryProductEntry>} PantryV2 */
|
||||||
|
|
||||||
|
function recalcTotal(entry) {
|
||||||
|
const itemSum = entry.items.reduce((s, i) => s + i.qty, 0);
|
||||||
|
entry._total = Math.round((entry.generic + itemSum) * 1000) / 1000;
|
||||||
|
return entry;
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizePantryEntry(ingredientId, val) {
|
||||||
|
if (val && typeof val === 'object' && !Array.isArray(val)) {
|
||||||
|
const items = Array.isArray(val.items)
|
||||||
|
? val.items
|
||||||
|
.filter(i => i && typeof i.productId === 'string' && PRODUCTS[i.productId] && Number.isFinite(Number(i.qty)) && Number(i.qty) > 0)
|
||||||
|
.map(i => ({ productId: i.productId, qty: Math.round(Number(i.qty) * 1000) / 1000 }))
|
||||||
|
: [];
|
||||||
|
const generic = Math.max(0, Number(val.generic) || 0);
|
||||||
|
return recalcTotal({ items, generic, _total: 0 });
|
||||||
|
}
|
||||||
|
const n = Number(val);
|
||||||
|
if (!Number.isFinite(n) || n < 0) return null;
|
||||||
|
if (ingredientHasProducts(ingredientId)) {
|
||||||
|
return recalcTotal({ items: [], generic: n, _total: 0 });
|
||||||
|
}
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
|
||||||
|
function migrateV1toV2() {
|
||||||
try {
|
try {
|
||||||
const raw = localStorage.getItem(PANTRY_STORAGE_KEY);
|
const raw = localStorage.getItem(PANTRY_STORAGE_KEY);
|
||||||
if (!raw) return {};
|
if (!raw) return {};
|
||||||
const p = JSON.parse(raw);
|
const p = JSON.parse(raw);
|
||||||
if (typeof p !== 'object' || p === null) return {};
|
if (typeof p !== 'object' || p === null) return {};
|
||||||
const out = {};
|
const out = {};
|
||||||
Object.keys(p).forEach((k) => {
|
for (const [k, v] of Object.entries(p)) {
|
||||||
const n = Number(p[k]);
|
const norm = normalizePantryEntry(k, v);
|
||||||
if (!Number.isFinite(n) || n < 0) return;
|
if (norm !== null) out[k] = norm;
|
||||||
out[k] = n;
|
}
|
||||||
});
|
|
||||||
return out;
|
return out;
|
||||||
} catch {
|
} catch {
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @returns {PantryV2} */
|
||||||
|
export function loadPantry() {
|
||||||
|
try {
|
||||||
|
const rawV2 = localStorage.getItem(PANTRY_STORAGE_KEY_V2);
|
||||||
|
if (rawV2) {
|
||||||
|
const p = JSON.parse(rawV2);
|
||||||
|
if (typeof p !== 'object' || p === null) return {};
|
||||||
|
const out = {};
|
||||||
|
for (const [k, v] of Object.entries(p)) {
|
||||||
|
const norm = normalizePantryEntry(k, v);
|
||||||
|
if (norm !== null) out[k] = norm;
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
const migrated = migrateV1toV2();
|
||||||
|
if (Object.keys(migrated).length > 0) {
|
||||||
|
savePantry(migrated);
|
||||||
|
}
|
||||||
|
return migrated;
|
||||||
|
} catch {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param {PantryV2} pantry */
|
||||||
export function savePantry(pantry) {
|
export function savePantry(pantry) {
|
||||||
try {
|
try {
|
||||||
localStorage.setItem(PANTRY_STORAGE_KEY, JSON.stringify(pantry));
|
localStorage.setItem(PANTRY_STORAGE_KEY_V2, JSON.stringify(pantry));
|
||||||
} catch { /* ignore */ }
|
} catch { /* ignore */ }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Łączna ilość składnika (generyczna + produkty). */
|
||||||
|
export function getPantryTotal(ingredientId, pantry) {
|
||||||
|
const val = pantry[ingredientId];
|
||||||
|
if (val == null) return 0;
|
||||||
|
if (typeof val === 'number') return val;
|
||||||
|
return val._total || 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Lista produktów w spiżarni dla danego składnika. */
|
||||||
|
export function getPantryProducts(ingredientId, pantry) {
|
||||||
|
const val = pantry[ingredientId];
|
||||||
|
if (!val || typeof val === 'number') return [];
|
||||||
|
return val.items || [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Ilość generyczna (nieprzypisana do produktu). */
|
||||||
|
export function getPantryGeneric(ingredientId, pantry) {
|
||||||
|
const val = pantry[ingredientId];
|
||||||
|
if (typeof val === 'number') return val;
|
||||||
|
if (!val) return 0;
|
||||||
|
return val.generic || 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Ustaw ilość generyczną składnika (bez produktu). */
|
||||||
export function setPantryQty(ingredientId, qty) {
|
export function setPantryQty(ingredientId, qty) {
|
||||||
const pantry = loadPantry();
|
const pantry = loadPantry();
|
||||||
|
const val = pantry[ingredientId];
|
||||||
|
if (val && typeof val === 'object') {
|
||||||
|
val.generic = Math.max(0, Math.round(qty * 1000) / 1000);
|
||||||
|
recalcTotal(val);
|
||||||
|
if (val._total <= 0) delete pantry[ingredientId];
|
||||||
|
} else {
|
||||||
if (qty <= 0 || !Number.isFinite(qty)) delete pantry[ingredientId];
|
if (qty <= 0 || !Number.isFinite(qty)) delete pantry[ingredientId];
|
||||||
else pantry[ingredientId] = Math.round(qty * 1000) / 1000;
|
else if (ingredientHasProducts(ingredientId)) {
|
||||||
|
pantry[ingredientId] = recalcTotal({ items: [], generic: Math.round(qty * 1000) / 1000, _total: 0 });
|
||||||
|
} else {
|
||||||
|
pantry[ingredientId] = Math.round(qty * 1000) / 1000;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
savePantry(pantry);
|
||||||
|
return pantry;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Ustaw ilość konkretnego produktu w spiżarni. */
|
||||||
|
export function setPantryProductQty(ingredientId, productId, qty) {
|
||||||
|
const pantry = loadPantry();
|
||||||
|
let val = pantry[ingredientId];
|
||||||
|
if (!val || typeof val === 'number') {
|
||||||
|
const generic = typeof val === 'number' ? val : 0;
|
||||||
|
val = { items: [], generic, _total: 0 };
|
||||||
|
pantry[ingredientId] = val;
|
||||||
|
}
|
||||||
|
const idx = val.items.findIndex(i => i.productId === productId);
|
||||||
|
const q = Math.max(0, Math.round(qty * 1000) / 1000);
|
||||||
|
if (q > 0) {
|
||||||
|
if (idx >= 0) val.items[idx].qty = q;
|
||||||
|
else val.items.push({ productId, qty: q });
|
||||||
|
} else {
|
||||||
|
if (idx >= 0) val.items.splice(idx, 1);
|
||||||
|
}
|
||||||
|
recalcTotal(val);
|
||||||
|
if (val._total <= 0) delete pantry[ingredientId];
|
||||||
savePantry(pantry);
|
savePantry(pantry);
|
||||||
return pantry;
|
return pantry;
|
||||||
}
|
}
|
||||||
@@ -417,9 +545,30 @@ export function applyCheckedKitchenListToPantry() {
|
|||||||
const def = INGREDIENTS[it.ingredientId];
|
const def = INGREDIENTS[it.ingredientId];
|
||||||
if (!def) continue;
|
if (!def) continue;
|
||||||
if (normalizeUnitForPantry(it.unit, def.pantryUnit) === null) continue;
|
if (normalizeUnitForPantry(it.unit, def.pantryUnit) === null) continue;
|
||||||
const cur = Number(pantry[it.ingredientId]) || 0;
|
const val = pantry[it.ingredientId];
|
||||||
|
if (val && typeof val === 'object') {
|
||||||
|
if (it.productId && PRODUCTS[it.productId]) {
|
||||||
|
const idx = val.items.findIndex(i => i.productId === it.productId);
|
||||||
|
if (idx >= 0) val.items[idx].qty = Math.round((val.items[idx].qty + it.amount) * 1000) / 1000;
|
||||||
|
else val.items.push({ productId: it.productId, qty: Math.round(it.amount * 1000) / 1000 });
|
||||||
|
} else {
|
||||||
|
val.generic = Math.round(((val.generic || 0) + it.amount) * 1000) / 1000;
|
||||||
|
}
|
||||||
|
recalcTotal(val);
|
||||||
|
} else if (ingredientHasProducts(it.ingredientId)) {
|
||||||
|
const cur = typeof val === 'number' ? val : 0;
|
||||||
|
const entry = { items: [], generic: cur, _total: 0 };
|
||||||
|
if (it.productId && PRODUCTS[it.productId]) {
|
||||||
|
entry.items.push({ productId: it.productId, qty: Math.round(it.amount * 1000) / 1000 });
|
||||||
|
} else {
|
||||||
|
entry.generic = Math.round((entry.generic + it.amount) * 1000) / 1000;
|
||||||
|
}
|
||||||
|
pantry[it.ingredientId] = recalcTotal(entry);
|
||||||
|
} else {
|
||||||
|
const cur = typeof val === 'number' ? val : 0;
|
||||||
pantry[it.ingredientId] = Math.round((cur + it.amount) * 1000) / 1000;
|
pantry[it.ingredientId] = Math.round((cur + it.amount) * 1000) / 1000;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
kitchen.items = items.filter((i) => !i.checked);
|
kitchen.items = items.filter((i) => !i.checked);
|
||||||
savePantry(pantry);
|
savePantry(pantry);
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { INGREDIENTS, RECIPES } from '../data/catalog.js?v=2';
|
import { INGREDIENTS, RECIPES, PRODUCTS, getProductsForIngredient } from '../data/catalog.js?v=6';
|
||||||
import { MEAL_SLOTS } from '../planner/mealSlots.js';
|
import { MEAL_SLOTS } from '../planner/mealSlots.js';
|
||||||
import { addDays } from './dateUtils.js';
|
import { addDays } from './dateUtils.js';
|
||||||
import { getDayPlan } from './planStore.js';
|
import { getDayPlan } from './planStore.js?v=2';
|
||||||
|
import { getPantryTotal } from './pantryShopping.js?v=2';
|
||||||
|
|
||||||
export function dayHasAnyMeal(plans, d) {
|
export function dayHasAnyMeal(plans, d) {
|
||||||
const p = getDayPlan(plans, d);
|
const p = getDayPlan(plans, d);
|
||||||
@@ -17,20 +18,24 @@ function hasCustomizations(entry) {
|
|||||||
return (entry.excludedIngredients?.length > 0) ||
|
return (entry.excludedIngredients?.length > 0) ||
|
||||||
(entry.amountOverrides && Object.keys(entry.amountOverrides).length > 0) ||
|
(entry.amountOverrides && Object.keys(entry.amountOverrides).length > 0) ||
|
||||||
(entry.addedIngredients?.length > 0) ||
|
(entry.addedIngredients?.length > 0) ||
|
||||||
(entry.substitutions && Object.keys(entry.substitutions).length > 0);
|
(entry.substitutions && Object.keys(entry.substitutions).length > 0) ||
|
||||||
|
(entry.productSelections && Object.keys(entry.productSelections).length > 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
function nutritionForAmountRaw(ingredientId, amount, unit) {
|
function nutritionForAmountRaw(ingredientId, amount, unit, productId = null) {
|
||||||
const def = INGREDIENTS[ingredientId];
|
const def = INGREDIENTS[ingredientId];
|
||||||
if (!def?.nutritionPer100g) return null;
|
if (!def) return null;
|
||||||
|
const product = productId ? PRODUCTS[productId] : null;
|
||||||
|
const nutrition = product?.nutritionPer100g || def.nutritionPer100g;
|
||||||
|
if (!nutrition) return null;
|
||||||
let g = amount;
|
let g = amount;
|
||||||
if ((unit === 'szt.' || unit === 'szt') && def.weightPerPiece) g = amount * def.weightPerPiece;
|
if ((unit === 'szt.' || unit === 'szt') && def.weightPerPiece) g = amount * def.weightPerPiece;
|
||||||
const f = g / 100;
|
const f = g / 100;
|
||||||
return {
|
return {
|
||||||
kcal: def.nutritionPer100g.kcal * f,
|
kcal: nutrition.kcal * f,
|
||||||
protein: def.nutritionPer100g.protein * f,
|
protein: nutrition.protein * f,
|
||||||
fat: def.nutritionPer100g.fat * f,
|
fat: nutrition.fat * f,
|
||||||
carbs: def.nutritionPer100g.carbs * f,
|
carbs: nutrition.carbs * f,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -52,17 +57,20 @@ export function computeEntryNutrition(entry) {
|
|||||||
const excluded = new Set(entry.excludedIngredients || []);
|
const excluded = new Set(entry.excludedIngredients || []);
|
||||||
const overrides = entry.amountOverrides || {};
|
const overrides = entry.amountOverrides || {};
|
||||||
const subs = entry.substitutions || {};
|
const subs = entry.substitutions || {};
|
||||||
|
const ps = entry.productSelections || {};
|
||||||
let kcal = 0, protein = 0, fat = 0, carbs = 0;
|
let kcal = 0, protein = 0, fat = 0, carbs = 0;
|
||||||
|
|
||||||
for (const ing of r.ingredients) {
|
for (const ing of r.ingredients) {
|
||||||
if (excluded.has(ing.ingredientId)) continue;
|
if (excluded.has(ing.ingredientId)) continue;
|
||||||
const eid = subs[ing.ingredientId] || ing.ingredientId;
|
const eid = subs[ing.ingredientId] || ing.ingredientId;
|
||||||
const base = overrides[ing.ingredientId] ?? ing.amount;
|
const base = overrides[ing.ingredientId] ?? ing.amount;
|
||||||
const n = nutritionForAmountRaw(eid, base * s, ing.unit);
|
const productId = ps[eid] || null;
|
||||||
|
const n = nutritionForAmountRaw(eid, base * s, ing.unit, productId);
|
||||||
if (n) { kcal += n.kcal; protein += n.protein; fat += n.fat; carbs += n.carbs; }
|
if (n) { kcal += n.kcal; protein += n.protein; fat += n.fat; carbs += n.carbs; }
|
||||||
}
|
}
|
||||||
for (const a of (entry.addedIngredients || [])) {
|
for (const a of (entry.addedIngredients || [])) {
|
||||||
const n = nutritionForAmountRaw(a.ingredientId, a.amount * s, a.unit);
|
const productId = ps[a.ingredientId] || null;
|
||||||
|
const n = nutritionForAmountRaw(a.ingredientId, a.amount * s, a.unit, productId);
|
||||||
if (n) { kcal += n.kcal; protein += n.protein; fat += n.fat; carbs += n.carbs; }
|
if (n) { kcal += n.kcal; protein += n.protein; fat += n.fat; carbs += n.carbs; }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -231,7 +239,7 @@ export function countDayShortfalls(dayPlan, pantry) {
|
|||||||
const lines = mergeIngredientLines(flattenDayIngredientLines(dayPlan));
|
const lines = mergeIngredientLines(flattenDayIngredientLines(dayPlan));
|
||||||
let count = 0;
|
let count = 0;
|
||||||
for (const line of lines) {
|
for (const line of lines) {
|
||||||
if ((Number(pantry[line.ingredientId]) || 0) < line.amount) count++;
|
if (getPantryTotal(line.ingredientId, pantry) < line.amount) count++;
|
||||||
}
|
}
|
||||||
return count;
|
return count;
|
||||||
}
|
}
|
||||||
@@ -242,7 +250,11 @@ export function countDayShortfalls(dayPlan, pantry) {
|
|||||||
* ile jest w spiżarni (po odjęciu zużycia z poprzednich dni) i ile brakuje.
|
* ile jest w spiżarni (po odjęciu zużycia z poprzednich dni) i ile brakuje.
|
||||||
*/
|
*/
|
||||||
export function computeFullForecast(plans, pantry, startDate, lookAheadDays = 8) {
|
export function computeFullForecast(plans, pantry, startDate, lookAheadDays = 8) {
|
||||||
const running = { ...pantry };
|
// Flatten pantry to simple totals for forecast (running deduction)
|
||||||
|
const running = {};
|
||||||
|
for (const [k, v] of Object.entries(pantry)) {
|
||||||
|
running[k] = typeof v === 'number' ? v : (v && v._total) || 0;
|
||||||
|
}
|
||||||
const days = [];
|
const days = [];
|
||||||
|
|
||||||
for (let i = 0; i < lookAheadDays; i++) {
|
for (let i = 0; i < lookAheadDays; i++) {
|
||||||
@@ -276,3 +288,55 @@ export function computeFullForecast(plans, pantry, startDate, lookAheadDays = 8)
|
|||||||
|
|
||||||
return days;
|
return days;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const LAST_PRODUCTS_KEY = 'recipe-last-product-selections';
|
||||||
|
|
||||||
|
function loadLastProductSelections() {
|
||||||
|
try {
|
||||||
|
const raw = localStorage.getItem(LAST_PRODUCTS_KEY);
|
||||||
|
return raw ? JSON.parse(raw) : {};
|
||||||
|
} catch { return {}; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Save user's product choice so it becomes the default next time. */
|
||||||
|
export function saveLastProductSelection(ingredientId, productId) {
|
||||||
|
const prev = loadLastProductSelections();
|
||||||
|
prev[ingredientId] = productId;
|
||||||
|
try { localStorage.setItem(LAST_PRODUCTS_KEY, JSON.stringify(prev)); } catch {}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Auto-select products for a recipe.
|
||||||
|
* Priority: last user choice > pantry stock (highest qty) > first from catalog.
|
||||||
|
* Only selects for ingredients that have products defined.
|
||||||
|
*/
|
||||||
|
export function autoSelectProducts(recipe, pantry) {
|
||||||
|
const selections = {};
|
||||||
|
const lastUsed = loadLastProductSelections();
|
||||||
|
for (const ing of recipe.ingredients) {
|
||||||
|
const products = getProductsForIngredient(ing.ingredientId);
|
||||||
|
if (products.length === 0) continue;
|
||||||
|
|
||||||
|
// 1. Last user choice (if product still exists)
|
||||||
|
const lastPid = lastUsed[ing.ingredientId];
|
||||||
|
if (lastPid && products.some(p => p.id === lastPid)) {
|
||||||
|
selections[ing.ingredientId] = lastPid;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Pantry stock — pick product with most qty
|
||||||
|
const val = pantry[ing.ingredientId];
|
||||||
|
if (val && typeof val === 'object') {
|
||||||
|
const items = (val.items || []).filter(i => i.qty > 0);
|
||||||
|
if (items.length > 0) {
|
||||||
|
items.sort((a, b) => b.qty - a.qty);
|
||||||
|
selections[ing.ingredientId] = items[0].productId;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. First from catalog
|
||||||
|
selections[ing.ingredientId] = products[0].id;
|
||||||
|
}
|
||||||
|
return selections;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { INGREDIENTS, RECIPES } from '../data/catalog.js?v=2';
|
import { INGREDIENTS, RECIPES, PRODUCTS } from '../data/catalog.js?v=6';
|
||||||
import { MEAL_SLOTS } from '../planner/mealSlots.js';
|
import { MEAL_SLOTS } from '../planner/mealSlots.js';
|
||||||
import { PLANS_STORAGE_KEY } from '../storageKeys.js';
|
import { PLANS_STORAGE_KEY } from '../storageKeys.js';
|
||||||
import { startOfDay } from './dateUtils.js';
|
import { startOfDay } from './dateUtils.js';
|
||||||
@@ -40,6 +40,13 @@ function normalizeEntryExtras(x) {
|
|||||||
.map((a) => ({ ingredientId: a.ingredientId, amount: a.amount, unit: a.unit }));
|
.map((a) => ({ ingredientId: a.ingredientId, amount: a.amount, unit: a.unit }));
|
||||||
if (valid.length > 0) out.addedIngredients = valid;
|
if (valid.length > 0) out.addedIngredients = valid;
|
||||||
}
|
}
|
||||||
|
if (x.productSelections && typeof x.productSelections === 'object' && !Array.isArray(x.productSelections)) {
|
||||||
|
const ps = {};
|
||||||
|
for (const [k, v] of Object.entries(x.productSelections)) {
|
||||||
|
if (typeof v === 'string' && PRODUCTS[v]) ps[k] = v;
|
||||||
|
}
|
||||||
|
if (Object.keys(ps).length > 0) out.productSelections = ps;
|
||||||
|
}
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
export const PLANS_STORAGE_KEY = 'recipe-planner-plans-v1';
|
export const PLANS_STORAGE_KEY = 'recipe-planner-plans-v1';
|
||||||
export const PANTRY_STORAGE_KEY = 'recipe-pantry-v1';
|
export const PANTRY_STORAGE_KEY = 'recipe-pantry-v1';
|
||||||
|
export const PANTRY_STORAGE_KEY_V2 = 'recipe-pantry-v2';
|
||||||
export const SHOPPING_STORAGE_KEY = 'recipe-shopping-v1';
|
export const SHOPPING_STORAGE_KEY = 'recipe-shopping-v1';
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { INGREDIENTS, RECIPES } from '../data/catalog.js?v=2';
|
import { INGREDIENTS, RECIPES, PRODUCTS, getProductsForIngredient } from '../data/catalog.js?v=6';
|
||||||
import { MEAL_SLOTS } from '../planner/mealSlots.js';
|
import { MEAL_SLOTS } from '../planner/mealSlots.js';
|
||||||
import {
|
import {
|
||||||
addDays,
|
addDays,
|
||||||
@@ -13,8 +13,9 @@ import {
|
|||||||
loadPlans,
|
loadPlans,
|
||||||
newPlanEntryId,
|
newPlanEntryId,
|
||||||
savePlans,
|
savePlans,
|
||||||
} from '../services/planStore.js';
|
} from '../services/planStore.js?v=2';
|
||||||
import { dayHasAnyMeal } from '../services/planIngredients.js';
|
import { dayHasAnyMeal, autoSelectProducts, saveLastProductSelection } from '../services/planIngredients.js?v=3';
|
||||||
|
import { loadPantry } from '../services/pantryShopping.js?v=2';
|
||||||
import { showAppToast } from './toast.js';
|
import { showAppToast } from './toast.js';
|
||||||
import {
|
import {
|
||||||
bindCalendarDayClicks,
|
bindCalendarDayClicks,
|
||||||
@@ -113,21 +114,26 @@ export function setupMealPlanEditor() {
|
|||||||
altOpen: new Set(),
|
altOpen: new Set(),
|
||||||
addOpen: false,
|
addOpen: false,
|
||||||
addQuery: '',
|
addQuery: '',
|
||||||
|
productSelections: {},
|
||||||
};
|
};
|
||||||
|
|
||||||
/* ── helpers ───────────────────────────────────── */
|
/* ── helpers ───────────────────────────────────── */
|
||||||
|
|
||||||
function nutFor(ingredientId, amount, unit) {
|
function nutFor(ingredientId, amount, unit) {
|
||||||
const def = INGREDIENTS[ingredientId];
|
const def = INGREDIENTS[ingredientId];
|
||||||
if (!def?.nutritionPer100g) return null;
|
if (!def) return null;
|
||||||
|
const productId = S.productSelections[ingredientId] || null;
|
||||||
|
const product = productId ? PRODUCTS[productId] : null;
|
||||||
|
const nutrition = product?.nutritionPer100g || def.nutritionPer100g;
|
||||||
|
if (!nutrition) return null;
|
||||||
let g = amount;
|
let g = amount;
|
||||||
if ((unit === 'szt.' || unit === 'szt') && def.weightPerPiece) g = amount * def.weightPerPiece;
|
if ((unit === 'szt.' || unit === 'szt') && def.weightPerPiece) g = amount * def.weightPerPiece;
|
||||||
const f = g / 100;
|
const f = g / 100;
|
||||||
return {
|
return {
|
||||||
kcal: Math.round(def.nutritionPer100g.kcal * f),
|
kcal: Math.round(nutrition.kcal * f),
|
||||||
protein: Math.round(def.nutritionPer100g.protein * f * 10) / 10,
|
protein: Math.round(nutrition.protein * f * 10) / 10,
|
||||||
fat: Math.round(def.nutritionPer100g.fat * f * 10) / 10,
|
fat: Math.round(nutrition.fat * f * 10) / 10,
|
||||||
carbs: Math.round(def.nutritionPer100g.carbs * f * 10) / 10,
|
carbs: Math.round(nutrition.carbs * f * 10) / 10,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -291,8 +297,14 @@ export function setupMealPlanEditor() {
|
|||||||
const modDot = modified ? '<span class="w-1.5 h-1.5 rounded-full bg-amber-400 shrink-0"></span>' : '';
|
const modDot = modified ? '<span class="w-1.5 h-1.5 rounded-full bg-amber-400 shrink-0"></span>' : '';
|
||||||
|
|
||||||
html += `<div class="mpe-ing-row rounded-xl p-2.5" style="${rowStyle}" data-orig-id="${esc(id)}" data-type="recipe">`;
|
html += `<div class="mpe-ing-row rounded-xl p-2.5" style="${rowStyle}" data-orig-id="${esc(id)}" data-type="recipe">`;
|
||||||
|
const selectedProductId = S.productSelections[eid];
|
||||||
|
const selectedProduct = selectedProductId ? PRODUCTS[selectedProductId] : null;
|
||||||
|
const productBadge = selectedProduct
|
||||||
|
? `<div class="flex items-center gap-1 mt-0.5"><span class="text-[10px] text-emerald-400 truncate">${esc(selectedProduct.name)}</span><button type="button" class="mpe-change-product text-[9px] text-gray-500 hover:text-gray-300 transition-colors" data-eid="${esc(eid)}" data-orig-id="${esc(id)}">zmień</button></div>`
|
||||||
|
: '';
|
||||||
|
|
||||||
html += `<div class="flex items-center gap-2">`;
|
html += `<div class="flex items-center gap-2">`;
|
||||||
html += `<div class="flex-1 min-w-0"><span class="text-[12px] font-semibold text-gray-900 truncate block">${esc(eName)}</span></div>`;
|
html += `<div class="flex-1 min-w-0"><span class="text-[12px] font-semibold text-gray-900 truncate block">${esc(eName)}</span>${productBadge}</div>`;
|
||||||
html += `<div class="shrink-0 flex items-center gap-2">`;
|
html += `<div class="shrink-0 flex items-center gap-2">`;
|
||||||
html += shuffleBtn;
|
html += shuffleBtn;
|
||||||
html += `<button type="button" class="mpe-edit-amt shrink-0 flex items-center gap-1 px-2 py-1 rounded-lg hover:bg-gray-100 transition-colors" data-orig-id="${esc(id)}" data-type="recipe">`;
|
html += `<button type="button" class="mpe-edit-amt shrink-0 flex items-center gap-1 px-2 py-1 rounded-lg hover:bg-gray-100 transition-colors" data-orig-id="${esc(id)}" data-type="recipe">`;
|
||||||
@@ -446,6 +458,13 @@ export function setupMealPlanEditor() {
|
|||||||
S.addOpen = false;
|
S.addOpen = false;
|
||||||
S.addQuery = '';
|
S.addQuery = '';
|
||||||
|
|
||||||
|
// Auto-select products from pantry, then apply saved selections
|
||||||
|
const pantry = loadPantry();
|
||||||
|
S.productSelections = autoSelectProducts(recipe, pantry);
|
||||||
|
if (opts.entry?.productSelections) {
|
||||||
|
Object.assign(S.productSelections, opts.entry.productSelections);
|
||||||
|
}
|
||||||
|
|
||||||
if (opts.date && opts.slotId) {
|
if (opts.date && opts.slotId) {
|
||||||
S.date = startOfDay(new Date(opts.date));
|
S.date = startOfDay(new Date(opts.date));
|
||||||
S.calDate = new Date(S.date);
|
S.calDate = new Date(S.date);
|
||||||
@@ -496,6 +515,7 @@ export function setupMealPlanEditor() {
|
|||||||
if (Object.keys(ov).length > 0) entry.amountOverrides = ov;
|
if (Object.keys(ov).length > 0) entry.amountOverrides = ov;
|
||||||
}
|
}
|
||||||
if (S.added.length > 0) entry.addedIngredients = S.added.map((a) => ({ ingredientId: a.ingredientId, amount: a.amount, unit: a.unit }));
|
if (S.added.length > 0) entry.addedIngredients = S.added.map((a) => ({ ingredientId: a.ingredientId, amount: a.amount, unit: a.unit }));
|
||||||
|
if (Object.keys(S.productSelections).length > 0) entry.productSelections = { ...S.productSelections };
|
||||||
return entry;
|
return entry;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -650,12 +670,76 @@ export function setupMealPlanEditor() {
|
|||||||
if (altPick) {
|
if (altPick) {
|
||||||
const origId = altPick.dataset.origId;
|
const origId = altPick.dataset.origId;
|
||||||
const altId = altPick.dataset.altId;
|
const altId = altPick.dataset.altId;
|
||||||
|
const prevEid = S.subs[origId] || origId;
|
||||||
if (altId === origId) delete S.subs[origId]; else S.subs[origId] = altId;
|
if (altId === origId) delete S.subs[origId]; else S.subs[origId] = altId;
|
||||||
|
const newEid = S.subs[origId] || origId;
|
||||||
|
// Update product selection for the new effective ingredient
|
||||||
|
if (newEid !== prevEid) {
|
||||||
|
delete S.productSelections[prevEid];
|
||||||
|
const newProducts = getProductsForIngredient(newEid);
|
||||||
|
if (newProducts.length > 0) {
|
||||||
|
const pantry = loadPantry();
|
||||||
|
const auto = autoSelectProducts({ ingredients: [{ ingredientId: newEid }] }, pantry);
|
||||||
|
if (auto[newEid]) S.productSelections[newEid] = auto[newEid];
|
||||||
|
}
|
||||||
|
}
|
||||||
S.altOpen.delete(origId);
|
S.altOpen.delete(origId);
|
||||||
renderIngList(); renderNutrition();
|
renderIngList(); renderNutrition();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const changeProd = e.target.closest('.mpe-change-product');
|
||||||
|
if (changeProd) {
|
||||||
|
const eid = changeProd.dataset.eid;
|
||||||
|
const products = getProductsForIngredient(eid);
|
||||||
|
if (products.length === 0) return;
|
||||||
|
// Toggle product picker open/closed using a data attribute on the row
|
||||||
|
const row = changeProd.closest('.mpe-ing-row');
|
||||||
|
const existing = row?.querySelector('.mpe-product-picker');
|
||||||
|
if (existing) { existing.remove(); return; }
|
||||||
|
|
||||||
|
const currentPid = S.productSelections[eid] || null;
|
||||||
|
const origId = changeProd.dataset.origId || eid;
|
||||||
|
const recipe = RECIPES[S.recipeId];
|
||||||
|
const recipeIng = recipe?.ingredients.find(i => i.ingredientId === origId);
|
||||||
|
const ingUnit = recipeIng?.unit || 'g';
|
||||||
|
const ingBase = S.overrides[origId] ?? recipeIng?.amount ?? 0;
|
||||||
|
const ingDisp = ingBase * S.servings;
|
||||||
|
|
||||||
|
const checkmark = (sel) => `<span class="ml-auto self-center w-[18px] h-[18px] rounded-full shrink-0 flex items-center justify-center" style="border:1.5px solid #56534f; background:transparent;">${sel ? '<i class="fas fa-check" style="color:#9b978f; font-size:8px; line-height:1; display:block; transform:translateY(0.5px);"></i>' : ''}</span>`;
|
||||||
|
|
||||||
|
let pickerHtml = '<div class="mpe-product-picker mt-2 ml-1 space-y-1">';
|
||||||
|
for (const p of products) {
|
||||||
|
const isSel = currentPid === p.id;
|
||||||
|
const pNut = p.nutritionPer100g;
|
||||||
|
// Calculate nutrition for the actual ingredient amount using product values
|
||||||
|
const def = INGREDIENTS[eid];
|
||||||
|
let g = ingDisp;
|
||||||
|
if ((ingUnit === 'szt.' || ingUnit === 'szt') && def?.weightPerPiece) g = ingDisp * def.weightPerPiece;
|
||||||
|
const f = g / 100;
|
||||||
|
const n = pNut ? { kcal: Math.round(pNut.kcal * f), protein: Math.round(pNut.protein * f * 10) / 10, fat: Math.round(pNut.fat * f * 10) / 10, carbs: Math.round(pNut.carbs * f * 10) / 10 } : null;
|
||||||
|
const nLine = n ? `<div class="text-[10px] text-gray-400 mt-0.5 tabular-nums">${n.kcal} kcal · ${n.protein}g B · ${n.fat}g T · ${n.carbs}g W</div>` : '';
|
||||||
|
pickerHtml += `<button type="button" class="mpe-prod-pick w-full text-left p-2.5 rounded-lg transition-all" style="background:#2f2f2d !important; background-image:none !important; border:none !important; box-shadow:none !important;" data-eid="${esc(eid)}" data-prod-id="${esc(p.id)}">
|
||||||
|
<div class="flex items-center gap-3"><div class="min-w-0 flex-1"><div class="text-[11px] font-semibold text-gray-900">${esc(p.name)}</div>${nLine}</div>${checkmark(isSel)}</div>
|
||||||
|
</button>`;
|
||||||
|
}
|
||||||
|
pickerHtml += '</div>';
|
||||||
|
row?.insertAdjacentHTML('beforeend', pickerHtml);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const prodPick = e.target.closest('.mpe-prod-pick');
|
||||||
|
if (prodPick) {
|
||||||
|
const eid = prodPick.dataset.eid;
|
||||||
|
const prodId = prodPick.dataset.prodId;
|
||||||
|
if (prodId) {
|
||||||
|
S.productSelections[eid] = prodId;
|
||||||
|
saveLastProductSelection(eid, prodId);
|
||||||
|
}
|
||||||
|
renderIngList(); renderNutrition();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const editAmt = e.target.closest('.mpe-edit-amt');
|
const editAmt = e.target.closest('.mpe-edit-amt');
|
||||||
if (editAmt && !editAmt.disabled) {
|
if (editAmt && !editAmt.disabled) {
|
||||||
startAmountEdit(editAmt);
|
startAmountEdit(editAmt);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { RECIPES } from '../data/catalog.js?v=2';
|
import { RECIPES } from '../data/catalog.js?v=6';
|
||||||
import { MEAL_SLOTS } from '../planner/mealSlots.js';
|
import { MEAL_SLOTS } from '../planner/mealSlots.js';
|
||||||
import { applyFilters, getFilterState } from './RecipeList.js';
|
import { applyFilters, getFilterState } from './RecipeList.js';
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { INGREDIENTS, RECIPES } from '../data/catalog.js?v=2';
|
import { INGREDIENTS, RECIPES } from '../data/catalog.js?v=6';
|
||||||
import { MEAL_SLOTS } from '../planner/mealSlots.js';
|
import { MEAL_SLOTS } from '../planner/mealSlots.js';
|
||||||
import {
|
import {
|
||||||
addMonths,
|
addMonths,
|
||||||
@@ -15,15 +15,15 @@ import {
|
|||||||
countDayShortfalls,
|
countDayShortfalls,
|
||||||
dayHasAnyMeal,
|
dayHasAnyMeal,
|
||||||
sumDayNutrition,
|
sumDayNutrition,
|
||||||
} from '../services/planIngredients.js';
|
} from '../services/planIngredients.js?v=3';
|
||||||
import { addOrMergeShoppingLines, loadPantry } from '../services/pantryShopping.js';
|
import { addOrMergeShoppingLines, loadPantry } from '../services/pantryShopping.js?v=2';
|
||||||
import {
|
import {
|
||||||
dateKey,
|
dateKey,
|
||||||
getDayPlan,
|
getDayPlan,
|
||||||
loadPlans,
|
loadPlans,
|
||||||
newPlanEntryId,
|
newPlanEntryId,
|
||||||
savePlans,
|
savePlans,
|
||||||
} from '../services/planStore.js';
|
} from '../services/planStore.js?v=2';
|
||||||
import {
|
import {
|
||||||
CALENDAR_HANDLE_CLASS,
|
CALENDAR_HANDLE_CLASS,
|
||||||
CALENDAR_MONTHS_SHORT,
|
CALENDAR_MONTHS_SHORT,
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
import {
|
import {
|
||||||
INGREDIENTS,
|
INGREDIENTS,
|
||||||
CATEGORY_LABELS,
|
CATEGORY_LABELS,
|
||||||
|
PRODUCTS,
|
||||||
pantryQtyStep,
|
pantryQtyStep,
|
||||||
} from '../data/catalog.js?v=2';
|
getProductsForIngredient,
|
||||||
import { addIngredientToKitchenList, categoryLabel, loadPantry, setPantryQty } from '../services/pantryShopping.js';
|
ingredientHasProducts,
|
||||||
|
} from '../data/catalog.js?v=6';
|
||||||
|
import { addIngredientToKitchenList, categoryLabel, loadPantry, setPantryQty, setPantryProductQty, getPantryTotal, getPantryProducts, getPantryGeneric } from '../services/pantryShopping.js?v=2';
|
||||||
import { showAppToast } from '../ui/toast.js';
|
import { showAppToast } from '../ui/toast.js';
|
||||||
|
|
||||||
/* ── helpers ── */
|
/* ── helpers ── */
|
||||||
@@ -97,6 +100,8 @@ export function getPantryHTML() {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div id="pv2-product-breakdown" class="shrink-0"></div>
|
||||||
|
|
||||||
<div class="border-t border-gray-100 shrink-0"></div>
|
<div class="border-t border-gray-100 shrink-0"></div>
|
||||||
|
|
||||||
<div class="shrink-0 space-y-1">
|
<div class="shrink-0 space-y-1">
|
||||||
@@ -172,7 +177,7 @@ function getFilteredIds(searchRaw) {
|
|||||||
|
|
||||||
function chipHtml(id, pantry) {
|
function chipHtml(id, pantry) {
|
||||||
const def = INGREDIENTS[id];
|
const def = INGREDIENTS[id];
|
||||||
const qty = Number(pantry[id]) || 0;
|
const qty = getPantryTotal(id, pantry);
|
||||||
const u = unitLabel(def.pantryUnit);
|
const u = unitLabel(def.pantryUnit);
|
||||||
|
|
||||||
if (qty > 0) {
|
if (qty > 0) {
|
||||||
@@ -209,7 +214,7 @@ function renderBoard() {
|
|||||||
const pantry = loadPantry();
|
const pantry = loadPantry();
|
||||||
const allFiltered = getFilteredIds(q);
|
const allFiltered = getFilteredIds(q);
|
||||||
const visible = showOnlyStock
|
const visible = showOnlyStock
|
||||||
? allFiltered.filter(id => (Number(pantry[id]) || 0) > 0)
|
? allFiltered.filter(id => getPantryTotal(id, pantry) > 0)
|
||||||
: allFiltered;
|
: allFiltered;
|
||||||
|
|
||||||
if (visible.length === 0) {
|
if (visible.length === 0) {
|
||||||
@@ -271,7 +276,7 @@ function openEditSheet(ingredientId) {
|
|||||||
editingId = ingredientId;
|
editingId = ingredientId;
|
||||||
|
|
||||||
const pantry = loadPantry();
|
const pantry = loadPantry();
|
||||||
const qty = Number(pantry[ingredientId]) || 0;
|
const qty = getPantryTotal(ingredientId, pantry);
|
||||||
const u = unitLabel(def.pantryUnit);
|
const u = unitLabel(def.pantryUnit);
|
||||||
const step = pantryQtyStep(ingredientId);
|
const step = pantryQtyStep(ingredientId);
|
||||||
const pack = def.purchasePack;
|
const pack = def.purchasePack;
|
||||||
@@ -311,6 +316,16 @@ function openEditSheet(ingredientId) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Hide main +/- when products exist (total is sum of product rows)
|
||||||
|
const hasProds = ingredientHasProducts(ingredientId);
|
||||||
|
const mainMinus = document.getElementById('pv2-edit-minus');
|
||||||
|
const mainPlus = document.getElementById('pv2-edit-plus');
|
||||||
|
const mainQtyInput = document.getElementById('pv2-edit-qty');
|
||||||
|
if (mainMinus) mainMinus.classList.toggle('hidden', hasProds);
|
||||||
|
if (mainPlus) mainPlus.classList.toggle('hidden', hasProds);
|
||||||
|
if (mainQtyInput) mainQtyInput.readOnly = hasProds;
|
||||||
|
|
||||||
|
renderProductBreakdown(ingredientId, pantry);
|
||||||
renderNutritionInSheet(def);
|
renderNutritionInSheet(def);
|
||||||
|
|
||||||
const bg = document.getElementById('pv2-edit-bg');
|
const bg = document.getElementById('pv2-edit-bg');
|
||||||
@@ -331,6 +346,83 @@ function nutritionListRow(label, valueHtml) {
|
|||||||
</li>`;
|
</li>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function renderProductBreakdown(ingredientId, pantry) {
|
||||||
|
const wrap = document.getElementById('pv2-product-breakdown');
|
||||||
|
if (!wrap) return;
|
||||||
|
const products = getProductsForIngredient(ingredientId);
|
||||||
|
if (products.length === 0) { wrap.innerHTML = ''; return; }
|
||||||
|
|
||||||
|
const def = INGREDIENTS[ingredientId];
|
||||||
|
const u = unitLabel(def.pantryUnit);
|
||||||
|
const pantryProducts = getPantryProducts(ingredientId, pantry);
|
||||||
|
const generic = getPantryGeneric(ingredientId, pantry);
|
||||||
|
|
||||||
|
const productQty = (pid) => {
|
||||||
|
const item = pantryProducts.find(i => i.productId === pid);
|
||||||
|
return item ? item.qty : 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
const rows = products.map(p => {
|
||||||
|
const q = Math.round(productQty(p.id));
|
||||||
|
return `<div class="flex items-center gap-1.5 py-1" data-product-row="${esc(p.id)}">
|
||||||
|
<span class="flex-1 text-[12px] text-gray-700 truncate" title="${esc(p.name)}">${esc(p.name)}</span>
|
||||||
|
<button type="button" class="pv2-prod-minus w-7 h-7 rounded-lg bg-gray-100 text-gray-600 hover:bg-gray-200 flex items-center justify-center transition-colors active:scale-95 shrink-0" data-pid="${esc(p.id)}" data-step="${p.packSize}">
|
||||||
|
<i class="fas fa-minus text-[9px]"></i>
|
||||||
|
</button>
|
||||||
|
<span class="pv2-prod-qty w-12 text-center text-[13px] font-semibold tabular-nums text-gray-800">${q} ${esc(u)}</span>
|
||||||
|
<button type="button" class="pv2-prod-plus w-7 h-7 rounded-lg bg-gray-100 text-gray-600 hover:bg-gray-200 flex items-center justify-center transition-colors active:scale-95 shrink-0" data-pid="${esc(p.id)}" data-step="${p.packSize}">
|
||||||
|
<i class="fas fa-plus text-[9px]"></i>
|
||||||
|
</button>
|
||||||
|
</div>`;
|
||||||
|
}).join('');
|
||||||
|
|
||||||
|
const genericRow = `<div class="flex items-center gap-1.5 py-1" data-product-row="_generic">
|
||||||
|
<span class="flex-1 text-[12px] text-gray-500 italic truncate">Nieokreślony</span>
|
||||||
|
<button type="button" class="pv2-prod-minus w-7 h-7 rounded-lg bg-gray-100 text-gray-600 hover:bg-gray-200 flex items-center justify-center transition-colors active:scale-95 shrink-0" data-pid="_generic" data-step="${pantryQtyStep(ingredientId)}">
|
||||||
|
<i class="fas fa-minus text-[9px]"></i>
|
||||||
|
</button>
|
||||||
|
<span class="pv2-prod-qty w-12 text-center text-[13px] font-semibold tabular-nums text-gray-800">${Math.round(generic)} ${esc(u)}</span>
|
||||||
|
<button type="button" class="pv2-prod-plus w-7 h-7 rounded-lg bg-gray-100 text-gray-600 hover:bg-gray-200 flex items-center justify-center transition-colors active:scale-95 shrink-0" data-pid="_generic" data-step="${pantryQtyStep(ingredientId)}">
|
||||||
|
<i class="fas fa-plus text-[9px]"></i>
|
||||||
|
</button>
|
||||||
|
</div>`;
|
||||||
|
|
||||||
|
wrap.innerHTML = `
|
||||||
|
<div class="space-y-0.5 px-0.5">
|
||||||
|
<p class="text-[9px] font-semibold uppercase tracking-wide text-gray-400 mb-1">Produkty</p>
|
||||||
|
${rows}
|
||||||
|
${genericRow}
|
||||||
|
</div>`;
|
||||||
|
|
||||||
|
// Bind product +/- buttons
|
||||||
|
wrap.querySelectorAll('.pv2-prod-plus, .pv2-prod-minus').forEach(btn => {
|
||||||
|
btn.addEventListener('click', () => {
|
||||||
|
if (!editingId) return;
|
||||||
|
const pid = btn.dataset.pid;
|
||||||
|
const step = Number(btn.dataset.step) || 1;
|
||||||
|
const isPlus = btn.classList.contains('pv2-prod-plus');
|
||||||
|
const pantry = loadPantry();
|
||||||
|
|
||||||
|
if (pid === '_generic') {
|
||||||
|
const cur = getPantryGeneric(editingId, pantry);
|
||||||
|
const next = Math.max(0, cur + (isPlus ? step : -step));
|
||||||
|
setPantryQty(editingId, next);
|
||||||
|
} else {
|
||||||
|
const items = getPantryProducts(editingId, pantry);
|
||||||
|
const cur = items.find(i => i.productId === pid)?.qty || 0;
|
||||||
|
const next = Math.max(0, cur + (isPlus ? step : -step));
|
||||||
|
setPantryProductQty(editingId, pid, next);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Re-render breakdown and total
|
||||||
|
const freshPantry = loadPantry();
|
||||||
|
renderProductBreakdown(editingId, freshPantry);
|
||||||
|
const totalQty = getPantryTotal(editingId, freshPantry);
|
||||||
|
setEditQty(totalQty);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function renderNutritionInSheet(def) {
|
function renderNutritionInSheet(def) {
|
||||||
const wrap = document.getElementById('pv2-edit-nutrition');
|
const wrap = document.getElementById('pv2-edit-nutrition');
|
||||||
if (!wrap) return;
|
if (!wrap) return;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { RECIPES, INGREDIENTS } from '../data/catalog.js?v=2';
|
import { RECIPES, INGREDIENTS } from '../data/catalog.js?v=6';
|
||||||
import { MEAL_SLOTS } from '../planner/mealSlots.js';
|
import { MEAL_SLOTS } from '../planner/mealSlots.js';
|
||||||
|
|
||||||
function escapeHtml(s) {
|
function escapeHtml(s) {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { RECIPES } from '../data/catalog.js?v=2';
|
import { RECIPES } from '../data/catalog.js?v=6';
|
||||||
import { MEAL_SLOTS } from '../planner/mealSlots.js';
|
import { MEAL_SLOTS } from '../planner/mealSlots.js';
|
||||||
|
|
||||||
function escapeHtml(s) {
|
function escapeHtml(s) {
|
||||||
|
|||||||
Reference in New Issue
Block a user