chore: release v0.25.3
This commit is contained in:
@@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
## [0.25.3] - 2026-04-26
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Delete actions in all seven modules (tasks, notes, budget, calendar, contacts, meals, recipes) and shopping list deletion no longer show a confirmation dialog; instead the item is removed immediately and a toast with an Undo button gives a 5-second window to reverse the action before the API call is made
|
||||||
|
|
||||||
## [0.25.2] - 2026-04-26
|
## [0.25.2] - 2026-04-26
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "oikos",
|
"name": "oikos",
|
||||||
"version": "0.25.2",
|
"version": "0.25.3",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "oikos",
|
"name": "oikos",
|
||||||
"version": "0.25.2",
|
"version": "0.25.3",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"bcrypt": "^6.0.0",
|
"bcrypt": "^6.0.0",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "oikos",
|
"name": "oikos",
|
||||||
"version": "0.25.2",
|
"version": "0.25.3",
|
||||||
"description": "Self-hosted family planner - calendar, tasks, shopping, meal planning, budget and more. Private, open-source, no subscription.",
|
"description": "Self-hosted family planner - calendar, tasks, shopping, meal planning, budget and more. Private, open-source, no subscription.",
|
||||||
"main": "server/index.js",
|
"main": "server/index.js",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
@@ -258,7 +258,8 @@
|
|||||||
"savedRecipeLabel": "Saved recipe",
|
"savedRecipeLabel": "Saved recipe",
|
||||||
"savedRecipePlaceholder": "Select recipe",
|
"savedRecipePlaceholder": "Select recipe",
|
||||||
"saveAsRecipe": "Save as recipe",
|
"saveAsRecipe": "Save as recipe",
|
||||||
"recipeScaleLabel": "Scale ingredients"
|
"recipeScaleLabel": "Scale ingredients",
|
||||||
|
"deletedToast": "Meal deleted"
|
||||||
},
|
},
|
||||||
"calendar": {
|
"calendar": {
|
||||||
"title": "التقويم",
|
"title": "التقويم",
|
||||||
|
|||||||
@@ -264,7 +264,8 @@
|
|||||||
"savedRecipeLabel": "Gespeichertes Rezept",
|
"savedRecipeLabel": "Gespeichertes Rezept",
|
||||||
"savedRecipePlaceholder": "Rezept auswählen",
|
"savedRecipePlaceholder": "Rezept auswählen",
|
||||||
"saveAsRecipe": "Als Rezept speichern",
|
"saveAsRecipe": "Als Rezept speichern",
|
||||||
"recipeScaleLabel": "Zutaten skalieren"
|
"recipeScaleLabel": "Zutaten skalieren",
|
||||||
|
"deletedToast": "Mahlzeit gelöscht"
|
||||||
},
|
},
|
||||||
"calendar": {
|
"calendar": {
|
||||||
"title": "Kalender",
|
"title": "Kalender",
|
||||||
|
|||||||
@@ -258,7 +258,8 @@
|
|||||||
"savedRecipeLabel": "Saved recipe",
|
"savedRecipeLabel": "Saved recipe",
|
||||||
"savedRecipePlaceholder": "Select recipe",
|
"savedRecipePlaceholder": "Select recipe",
|
||||||
"saveAsRecipe": "Save as recipe",
|
"saveAsRecipe": "Save as recipe",
|
||||||
"recipeScaleLabel": "Scale ingredients"
|
"recipeScaleLabel": "Scale ingredients",
|
||||||
|
"deletedToast": "Meal deleted"
|
||||||
},
|
},
|
||||||
"calendar": {
|
"calendar": {
|
||||||
"title": "Ημερολόγιο",
|
"title": "Ημερολόγιο",
|
||||||
|
|||||||
@@ -258,7 +258,8 @@
|
|||||||
"savedRecipeLabel": "Saved recipe",
|
"savedRecipeLabel": "Saved recipe",
|
||||||
"savedRecipePlaceholder": "Select recipe",
|
"savedRecipePlaceholder": "Select recipe",
|
||||||
"saveAsRecipe": "Save as recipe",
|
"saveAsRecipe": "Save as recipe",
|
||||||
"recipeScaleLabel": "Scale ingredients"
|
"recipeScaleLabel": "Scale ingredients",
|
||||||
|
"deletedToast": "Meal deleted"
|
||||||
},
|
},
|
||||||
"calendar": {
|
"calendar": {
|
||||||
"title": "Calendar",
|
"title": "Calendar",
|
||||||
|
|||||||
@@ -258,7 +258,8 @@
|
|||||||
"savedRecipeLabel": "Saved recipe",
|
"savedRecipeLabel": "Saved recipe",
|
||||||
"savedRecipePlaceholder": "Select recipe",
|
"savedRecipePlaceholder": "Select recipe",
|
||||||
"saveAsRecipe": "Save as recipe",
|
"saveAsRecipe": "Save as recipe",
|
||||||
"recipeScaleLabel": "Scale ingredients"
|
"recipeScaleLabel": "Scale ingredients",
|
||||||
|
"deletedToast": "Meal deleted"
|
||||||
},
|
},
|
||||||
"calendar": {
|
"calendar": {
|
||||||
"title": "Calendario",
|
"title": "Calendario",
|
||||||
|
|||||||
@@ -258,7 +258,8 @@
|
|||||||
"savedRecipeLabel": "Saved recipe",
|
"savedRecipeLabel": "Saved recipe",
|
||||||
"savedRecipePlaceholder": "Select recipe",
|
"savedRecipePlaceholder": "Select recipe",
|
||||||
"saveAsRecipe": "Save as recipe",
|
"saveAsRecipe": "Save as recipe",
|
||||||
"recipeScaleLabel": "Scale ingredients"
|
"recipeScaleLabel": "Scale ingredients",
|
||||||
|
"deletedToast": "Meal deleted"
|
||||||
},
|
},
|
||||||
"calendar": {
|
"calendar": {
|
||||||
"title": "Calendrier",
|
"title": "Calendrier",
|
||||||
|
|||||||
@@ -258,7 +258,8 @@
|
|||||||
"savedRecipeLabel": "Saved recipe",
|
"savedRecipeLabel": "Saved recipe",
|
||||||
"savedRecipePlaceholder": "Select recipe",
|
"savedRecipePlaceholder": "Select recipe",
|
||||||
"saveAsRecipe": "Save as recipe",
|
"saveAsRecipe": "Save as recipe",
|
||||||
"recipeScaleLabel": "Scale ingredients"
|
"recipeScaleLabel": "Scale ingredients",
|
||||||
|
"deletedToast": "Meal deleted"
|
||||||
},
|
},
|
||||||
"calendar": {
|
"calendar": {
|
||||||
"title": "कैलेंडर",
|
"title": "कैलेंडर",
|
||||||
|
|||||||
@@ -258,7 +258,8 @@
|
|||||||
"savedRecipeLabel": "Saved recipe",
|
"savedRecipeLabel": "Saved recipe",
|
||||||
"savedRecipePlaceholder": "Select recipe",
|
"savedRecipePlaceholder": "Select recipe",
|
||||||
"saveAsRecipe": "Save as recipe",
|
"saveAsRecipe": "Save as recipe",
|
||||||
"recipeScaleLabel": "Scale ingredients"
|
"recipeScaleLabel": "Scale ingredients",
|
||||||
|
"deletedToast": "Meal deleted"
|
||||||
},
|
},
|
||||||
"calendar": {
|
"calendar": {
|
||||||
"title": "Calendario",
|
"title": "Calendario",
|
||||||
|
|||||||
@@ -258,7 +258,8 @@
|
|||||||
"savedRecipeLabel": "Saved recipe",
|
"savedRecipeLabel": "Saved recipe",
|
||||||
"savedRecipePlaceholder": "Select recipe",
|
"savedRecipePlaceholder": "Select recipe",
|
||||||
"saveAsRecipe": "Save as recipe",
|
"saveAsRecipe": "Save as recipe",
|
||||||
"recipeScaleLabel": "Scale ingredients"
|
"recipeScaleLabel": "Scale ingredients",
|
||||||
|
"deletedToast": "Meal deleted"
|
||||||
},
|
},
|
||||||
"calendar": {
|
"calendar": {
|
||||||
"title": "カレンダー",
|
"title": "カレンダー",
|
||||||
|
|||||||
@@ -258,7 +258,8 @@
|
|||||||
"savedRecipeLabel": "Saved recipe",
|
"savedRecipeLabel": "Saved recipe",
|
||||||
"savedRecipePlaceholder": "Select recipe",
|
"savedRecipePlaceholder": "Select recipe",
|
||||||
"saveAsRecipe": "Save as recipe",
|
"saveAsRecipe": "Save as recipe",
|
||||||
"recipeScaleLabel": "Scale ingredients"
|
"recipeScaleLabel": "Scale ingredients",
|
||||||
|
"deletedToast": "Meal deleted"
|
||||||
},
|
},
|
||||||
"calendar": {
|
"calendar": {
|
||||||
"title": "Calendário",
|
"title": "Calendário",
|
||||||
|
|||||||
@@ -258,7 +258,8 @@
|
|||||||
"savedRecipeLabel": "Saved recipe",
|
"savedRecipeLabel": "Saved recipe",
|
||||||
"savedRecipePlaceholder": "Select recipe",
|
"savedRecipePlaceholder": "Select recipe",
|
||||||
"saveAsRecipe": "Save as recipe",
|
"saveAsRecipe": "Save as recipe",
|
||||||
"recipeScaleLabel": "Scale ingredients"
|
"recipeScaleLabel": "Scale ingredients",
|
||||||
|
"deletedToast": "Meal deleted"
|
||||||
},
|
},
|
||||||
"calendar": {
|
"calendar": {
|
||||||
"title": "Календарь",
|
"title": "Календарь",
|
||||||
|
|||||||
@@ -258,7 +258,8 @@
|
|||||||
"savedRecipeLabel": "Sparat recept",
|
"savedRecipeLabel": "Sparat recept",
|
||||||
"savedRecipePlaceholder": "Välj recept",
|
"savedRecipePlaceholder": "Välj recept",
|
||||||
"saveAsRecipe": "Spara som recept",
|
"saveAsRecipe": "Spara som recept",
|
||||||
"recipeScaleLabel": "Skala ingredienser"
|
"recipeScaleLabel": "Skala ingredienser",
|
||||||
|
"deletedToast": "Meal deleted"
|
||||||
},
|
},
|
||||||
"calendar": {
|
"calendar": {
|
||||||
"title": "Kalender",
|
"title": "Kalender",
|
||||||
|
|||||||
@@ -258,7 +258,8 @@
|
|||||||
"savedRecipeLabel": "Saved recipe",
|
"savedRecipeLabel": "Saved recipe",
|
||||||
"savedRecipePlaceholder": "Select recipe",
|
"savedRecipePlaceholder": "Select recipe",
|
||||||
"saveAsRecipe": "Save as recipe",
|
"saveAsRecipe": "Save as recipe",
|
||||||
"recipeScaleLabel": "Scale ingredients"
|
"recipeScaleLabel": "Scale ingredients",
|
||||||
|
"deletedToast": "Meal deleted"
|
||||||
},
|
},
|
||||||
"calendar": {
|
"calendar": {
|
||||||
"title": "Takvim",
|
"title": "Takvim",
|
||||||
|
|||||||
@@ -258,7 +258,8 @@
|
|||||||
"savedRecipeLabel": "Saved recipe",
|
"savedRecipeLabel": "Saved recipe",
|
||||||
"savedRecipePlaceholder": "Select recipe",
|
"savedRecipePlaceholder": "Select recipe",
|
||||||
"saveAsRecipe": "Save as recipe",
|
"saveAsRecipe": "Save as recipe",
|
||||||
"recipeScaleLabel": "Scale ingredients"
|
"recipeScaleLabel": "Scale ingredients",
|
||||||
|
"deletedToast": "Meal deleted"
|
||||||
},
|
},
|
||||||
"calendar": {
|
"calendar": {
|
||||||
"title": "Календар",
|
"title": "Календар",
|
||||||
|
|||||||
@@ -258,7 +258,8 @@
|
|||||||
"savedRecipeLabel": "Saved recipe",
|
"savedRecipeLabel": "Saved recipe",
|
||||||
"savedRecipePlaceholder": "Select recipe",
|
"savedRecipePlaceholder": "Select recipe",
|
||||||
"saveAsRecipe": "Save as recipe",
|
"saveAsRecipe": "Save as recipe",
|
||||||
"recipeScaleLabel": "Scale ingredients"
|
"recipeScaleLabel": "Scale ingredients",
|
||||||
|
"deletedToast": "Meal deleted"
|
||||||
},
|
},
|
||||||
"calendar": {
|
"calendar": {
|
||||||
"title": "日历",
|
"title": "日历",
|
||||||
|
|||||||
+23
-6
@@ -6,7 +6,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { api } from '/api.js';
|
import { api } from '/api.js';
|
||||||
import { openModal as openSharedModal, closeModal, confirmModal } from '/components/modal.js';
|
import { openModal as openSharedModal, closeModal } from '/components/modal.js';
|
||||||
import { stagger, vibrate } from '/utils/ux.js';
|
import { stagger, vibrate } from '/utils/ux.js';
|
||||||
import { t, formatDate, getLocale } from '/i18n.js';
|
import { t, formatDate, getLocale } from '/i18n.js';
|
||||||
import { esc } from '/utils/html.js';
|
import { esc } from '/utils/html.js';
|
||||||
@@ -660,18 +660,35 @@ function openBudgetModal({ mode, entry = null }) {
|
|||||||
// --------------------------------------------------------
|
// --------------------------------------------------------
|
||||||
|
|
||||||
async function deleteEntry(id) {
|
async function deleteEntry(id) {
|
||||||
if (!await confirmModal(t('budget.deleteConfirm'), { danger: true, confirmLabel: t('common.delete') })) return;
|
const entry = state.entries.find((e) => e.id === id);
|
||||||
|
state.entries = state.entries.filter((e) => e.id !== id);
|
||||||
|
renderBody();
|
||||||
|
vibrate([30, 50, 30]);
|
||||||
|
|
||||||
|
let undone = false;
|
||||||
|
window.oikos?.showToast(t('budget.deletedToast'), 'default', 5000, () => {
|
||||||
|
undone = true;
|
||||||
|
if (entry) {
|
||||||
|
state.entries = [...state.entries, entry].sort((a, b) => new Date(b.date) - new Date(a.date));
|
||||||
|
renderBody();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
setTimeout(async () => {
|
||||||
|
if (undone) return;
|
||||||
try {
|
try {
|
||||||
await api.delete(`/budget/${id}`);
|
await api.delete(`/budget/${id}`);
|
||||||
state.entries = state.entries.filter((e) => e.id !== id);
|
|
||||||
const sumRes = await api.get(`/budget/summary?month=${state.month}`);
|
const sumRes = await api.get(`/budget/summary?month=${state.month}`);
|
||||||
state.summary = sumRes.data;
|
state.summary = sumRes.data;
|
||||||
renderBody();
|
renderBody();
|
||||||
vibrate([30, 50, 30]);
|
|
||||||
window.oikos?.showToast(t('budget.deletedToast'), 'success');
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
window.oikos?.showToast(err.data?.error ?? t('common.unknownError'), 'error');
|
if (entry) {
|
||||||
|
state.entries = [...state.entries, entry].sort((a, b) => new Date(b.date) - new Date(a.date));
|
||||||
|
renderBody();
|
||||||
}
|
}
|
||||||
|
window.oikos?.showToast(err.data?.error ?? t('common.unknownError'), 'danger');
|
||||||
|
}
|
||||||
|
}, 5000);
|
||||||
}
|
}
|
||||||
|
|
||||||
// --------------------------------------------------------
|
// --------------------------------------------------------
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
|
|
||||||
import { api } from '/api.js';
|
import { api } from '/api.js';
|
||||||
import { renderRRuleFields, bindRRuleEvents, getRRuleValues } from '/rrule-ui.js';
|
import { renderRRuleFields, bindRRuleEvents, getRRuleValues } from '/rrule-ui.js';
|
||||||
import { openModal as openSharedModal, closeModal, confirmModal } from '/components/modal.js';
|
import { openModal as openSharedModal, closeModal } from '/components/modal.js';
|
||||||
import { stagger } from '/utils/ux.js';
|
import { stagger } from '/utils/ux.js';
|
||||||
import { t, formatTime } from '/i18n.js';
|
import { t, formatTime } from '/i18n.js';
|
||||||
import { esc, fmtLocation } from '/utils/html.js';
|
import { esc, fmtLocation } from '/utils/html.js';
|
||||||
@@ -764,7 +764,6 @@ function showEventPopup(ev, anchor) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
popup.querySelector('#popup-delete').addEventListener('click', async () => {
|
popup.querySelector('#popup-delete').addEventListener('click', async () => {
|
||||||
if (!await confirmModal(t('calendar.deleteConfirm', { title: ev.title }), { danger: true, confirmLabel: t('common.delete') })) return;
|
|
||||||
popup.remove();
|
popup.remove();
|
||||||
await deleteEvent(ev.id);
|
await deleteEvent(ev.id);
|
||||||
});
|
});
|
||||||
@@ -868,7 +867,6 @@ function openEventModal({ mode, event = null, date = null, reminder = null }) {
|
|||||||
panel.querySelector('#modal-cancel').addEventListener('click', closeModal);
|
panel.querySelector('#modal-cancel').addEventListener('click', closeModal);
|
||||||
|
|
||||||
panel.querySelector('#modal-delete')?.addEventListener('click', async () => {
|
panel.querySelector('#modal-delete')?.addEventListener('click', async () => {
|
||||||
if (!await confirmModal(t('calendar.deleteConfirm', { title: event.title }), { danger: true, confirmLabel: t('common.delete') })) return;
|
|
||||||
closeModal();
|
closeModal();
|
||||||
await deleteEvent(event.id);
|
await deleteEvent(event.id);
|
||||||
});
|
});
|
||||||
@@ -1073,15 +1071,32 @@ async function saveEvent(overlay, mode, eventId, existingReminder = null) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function deleteEvent(id) {
|
async function deleteEvent(id) {
|
||||||
|
const event = state.events.find((e) => e.id === id);
|
||||||
|
state.events = state.events.filter((e) => e.id !== id);
|
||||||
|
renderView();
|
||||||
|
|
||||||
|
let undone = false;
|
||||||
|
window.oikos?.showToast(t('calendar.deletedToast'), 'default', 5000, () => {
|
||||||
|
undone = true;
|
||||||
|
if (event) {
|
||||||
|
state.events = [...state.events, event];
|
||||||
|
renderView();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
setTimeout(async () => {
|
||||||
|
if (undone) return;
|
||||||
try {
|
try {
|
||||||
await api.delete(`/calendar/${id}`);
|
await api.delete(`/calendar/${id}`);
|
||||||
api.delete(`/reminders?entity_type=event&entity_id=${id}`).catch(() => {});
|
api.delete(`/reminders?entity_type=event&entity_id=${id}`).catch(() => {});
|
||||||
refreshReminders();
|
refreshReminders();
|
||||||
state.events = state.events.filter((e) => e.id !== id);
|
|
||||||
renderView();
|
|
||||||
window.oikos?.showToast(t('calendar.deletedToast'), 'success');
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
window.oikos?.showToast(err.data?.error ?? t('calendar.deleteError'), 'error');
|
if (event) {
|
||||||
|
state.events = [...state.events, event];
|
||||||
|
renderView();
|
||||||
}
|
}
|
||||||
|
window.oikos?.showToast(err.data?.error ?? t('calendar.deleteError'), 'danger');
|
||||||
|
}
|
||||||
|
}, 5000);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { api } from '/api.js';
|
import { api } from '/api.js';
|
||||||
import { openModal as openSharedModal, closeModal, confirmModal } from '/components/modal.js';
|
import { openModal as openSharedModal, closeModal } from '/components/modal.js';
|
||||||
import { stagger, vibrate } from '/utils/ux.js';
|
import { stagger, vibrate } from '/utils/ux.js';
|
||||||
import { t } from '/i18n.js';
|
import { t } from '/i18n.js';
|
||||||
import { esc } from '/utils/html.js';
|
import { esc } from '/utils/html.js';
|
||||||
@@ -350,16 +350,32 @@ function openContactModal({ mode, contact = null }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function deleteContact(id) {
|
async function deleteContact(id) {
|
||||||
if (!await confirmModal(t('contacts.deleteConfirm'), { danger: true, confirmLabel: t('common.delete') })) return;
|
const contact = state.contacts.find((c) => c.id === id);
|
||||||
try {
|
|
||||||
await api.delete(`/contacts/${id}`);
|
|
||||||
state.contacts = state.contacts.filter((c) => c.id !== id);
|
state.contacts = state.contacts.filter((c) => c.id !== id);
|
||||||
renderList();
|
renderList();
|
||||||
vibrate([30, 50, 30]);
|
vibrate([30, 50, 30]);
|
||||||
window.oikos?.showToast(t('contacts.deletedToast'), 'success');
|
|
||||||
} catch (err) {
|
let undone = false;
|
||||||
window.oikos?.showToast(err.data?.error ?? t('common.unknownError'), 'error');
|
window.oikos?.showToast(t('contacts.deletedToast'), 'default', 5000, () => {
|
||||||
|
undone = true;
|
||||||
|
if (contact) {
|
||||||
|
state.contacts = [...state.contacts, contact].sort((a, b) => a.name.localeCompare(b.name));
|
||||||
|
renderList();
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
setTimeout(async () => {
|
||||||
|
if (undone) return;
|
||||||
|
try {
|
||||||
|
await api.delete(`/contacts/${id}`);
|
||||||
|
} catch (err) {
|
||||||
|
if (contact) {
|
||||||
|
state.contacts = [...state.contacts, contact].sort((a, b) => a.name.localeCompare(b.name));
|
||||||
|
renderList();
|
||||||
|
}
|
||||||
|
window.oikos?.showToast(err.data?.error ?? t('common.unknownError'), 'danger');
|
||||||
|
}
|
||||||
|
}, 5000);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+16
-4
@@ -5,7 +5,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { api } from '/api.js';
|
import { api } from '/api.js';
|
||||||
import { openModal as openSharedModal, closeModal as closeSharedModal, selectModal, confirmModal } from '/components/modal.js';
|
import { openModal as openSharedModal, closeModal as closeSharedModal, selectModal } from '/components/modal.js';
|
||||||
import { stagger } from '/utils/ux.js';
|
import { stagger } from '/utils/ux.js';
|
||||||
import { t, formatDate } from '/i18n.js';
|
import { t, formatDate } from '/i18n.js';
|
||||||
import { esc } from '/utils/html.js';
|
import { esc } from '/utils/html.js';
|
||||||
@@ -920,15 +920,27 @@ function collectModalIngredients(overlay) {
|
|||||||
// --------------------------------------------------------
|
// --------------------------------------------------------
|
||||||
|
|
||||||
async function deleteMeal(mealId) {
|
async function deleteMeal(mealId) {
|
||||||
if (!await confirmModal(t('meals.deleteMeal') + '?', { danger: true, confirmLabel: t('common.delete') })) return;
|
const meal = state.meals.find((m) => m.id === mealId);
|
||||||
|
const itemEl = _container.querySelector(`.meal-slot--has-meal[data-meal-id="${mealId}"]`);
|
||||||
|
if (itemEl) itemEl.style.display = 'none';
|
||||||
|
|
||||||
|
let undone = false;
|
||||||
|
window.oikos?.showToast(t('meals.deletedToast'), 'default', 5000, () => {
|
||||||
|
undone = true;
|
||||||
|
if (itemEl) itemEl.style.display = '';
|
||||||
|
});
|
||||||
|
|
||||||
|
setTimeout(async () => {
|
||||||
|
if (undone) return;
|
||||||
try {
|
try {
|
||||||
await api.delete(`/meals/${mealId}`);
|
await api.delete(`/meals/${mealId}`);
|
||||||
state.meals = state.meals.filter((m) => m.id !== mealId);
|
state.meals = state.meals.filter((m) => m.id !== mealId);
|
||||||
renderWeekGrid();
|
renderWeekGrid();
|
||||||
window.oikos?.showToast(t('meals.deleteMeal'), 'success');
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
window.oikos?.showToast(err.data?.error ?? t('common.errorGeneric'), 'error');
|
if (itemEl) itemEl.style.display = '';
|
||||||
|
window.oikos?.showToast(err.data?.error ?? t('common.unknownError'), 'danger');
|
||||||
}
|
}
|
||||||
|
}, 5000);
|
||||||
}
|
}
|
||||||
|
|
||||||
// --------------------------------------------------------
|
// --------------------------------------------------------
|
||||||
|
|||||||
+24
-7
@@ -5,7 +5,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { api } from '/api.js';
|
import { api } from '/api.js';
|
||||||
import { openModal as openSharedModal, closeModal, btnError, confirmModal } from '/components/modal.js';
|
import { openModal as openSharedModal, closeModal, btnError } from '/components/modal.js';
|
||||||
import { stagger, vibrate } from '/utils/ux.js';
|
import { stagger, vibrate } from '/utils/ux.js';
|
||||||
import { t } from '/i18n.js';
|
import { t } from '/i18n.js';
|
||||||
import { esc } from '/utils/html.js';
|
import { esc } from '/utils/html.js';
|
||||||
@@ -476,16 +476,33 @@ async function togglePin(id) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function deleteNote(id) {
|
async function deleteNote(id) {
|
||||||
if (!await confirmModal(t('notes.deleteConfirm'), { danger: true, confirmLabel: t('common.delete') })) return;
|
closeModal();
|
||||||
try {
|
const note = state.notes.find((n) => n.id === id);
|
||||||
await api.delete(`/notes/${id}`);
|
|
||||||
state.notes = state.notes.filter((n) => n.id !== id);
|
state.notes = state.notes.filter((n) => n.id !== id);
|
||||||
renderGrid();
|
renderGrid();
|
||||||
vibrate([30, 50, 30]);
|
vibrate([30, 50, 30]);
|
||||||
window.oikos?.showToast(t('notes.deletedToast'), 'success');
|
|
||||||
} catch (err) {
|
let undone = false;
|
||||||
window.oikos?.showToast(err.data?.error ?? t('common.unknownError'), 'error');
|
window.oikos?.showToast(t('notes.deletedToast'), 'default', 5000, () => {
|
||||||
|
undone = true;
|
||||||
|
if (note) {
|
||||||
|
state.notes = [...state.notes, note].sort((a, b) => b.pinned - a.pinned);
|
||||||
|
renderGrid();
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
setTimeout(async () => {
|
||||||
|
if (undone) return;
|
||||||
|
try {
|
||||||
|
await api.delete(`/notes/${id}`);
|
||||||
|
} catch (err) {
|
||||||
|
if (note) {
|
||||||
|
state.notes = [...state.notes, note].sort((a, b) => b.pinned - a.pinned);
|
||||||
|
renderGrid();
|
||||||
|
}
|
||||||
|
window.oikos?.showToast(err.data?.error ?? t('common.unknownError'), 'danger');
|
||||||
|
}
|
||||||
|
}, 5000);
|
||||||
}
|
}
|
||||||
|
|
||||||
// --------------------------------------------------------
|
// --------------------------------------------------------
|
||||||
|
|||||||
+14
-8
@@ -5,7 +5,7 @@
|
|||||||
|
|
||||||
import { api } from '/api.js';
|
import { api } from '/api.js';
|
||||||
import { t } from '/i18n.js';
|
import { t } from '/i18n.js';
|
||||||
import { openModal as openSharedModal, closeModal as closeSharedModal, confirmModal } from '/components/modal.js';
|
import { openModal as openSharedModal, closeModal as closeSharedModal } from '/components/modal.js';
|
||||||
import { DEFAULT_CATEGORY_NAME, categoryLabel } from '/utils/shopping-categories.js';
|
import { DEFAULT_CATEGORY_NAME, categoryLabel } from '/utils/shopping-categories.js';
|
||||||
|
|
||||||
let _container = null;
|
let _container = null;
|
||||||
@@ -134,6 +134,7 @@ function renderRecipeList() {
|
|||||||
for (const recipe of state.recipes) {
|
for (const recipe of state.recipes) {
|
||||||
const card = document.createElement('article');
|
const card = document.createElement('article');
|
||||||
card.className = 'recipe-card';
|
card.className = 'recipe-card';
|
||||||
|
card.dataset.id = String(recipe.id);
|
||||||
|
|
||||||
const h = document.createElement('h2');
|
const h = document.createElement('h2');
|
||||||
h.className = 'recipe-card__title';
|
h.className = 'recipe-card__title';
|
||||||
@@ -370,21 +371,26 @@ async function saveRecipe(panel, mode, recipe) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function removeRecipe(recipe) {
|
async function removeRecipe(recipe) {
|
||||||
const ok = await confirmModal(t('recipes.deleteConfirm', { title: recipe.title }), {
|
const itemEl = _container.querySelector(`.recipe-card[data-id="${recipe.id}"]`);
|
||||||
danger: true,
|
if (itemEl) itemEl.style.display = 'none';
|
||||||
confirmLabel: t('common.delete'),
|
|
||||||
|
let undone = false;
|
||||||
|
window.oikos?.showToast(t('recipes.deleted'), 'default', 5000, () => {
|
||||||
|
undone = true;
|
||||||
|
if (itemEl) itemEl.style.display = '';
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!ok) return;
|
setTimeout(async () => {
|
||||||
|
if (undone) return;
|
||||||
try {
|
try {
|
||||||
await api.delete(`/recipes/${recipe.id}`);
|
await api.delete(`/recipes/${recipe.id}`);
|
||||||
state.recipes = state.recipes.filter((r) => r.id !== recipe.id);
|
state.recipes = state.recipes.filter((r) => r.id !== recipe.id);
|
||||||
renderRecipeList();
|
renderRecipeList();
|
||||||
window.oikos?.showToast(t('recipes.deleted'), 'success');
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
window.oikos?.showToast(err.data?.error ?? t('common.errorGeneric'), 'error');
|
if (itemEl) itemEl.style.display = '';
|
||||||
|
window.oikos?.showToast(err.data?.error ?? t('common.unknownError'), 'danger');
|
||||||
}
|
}
|
||||||
|
}, 5000);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function duplicateRecipe(recipe) {
|
async function duplicateRecipe(recipe) {
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { api } from '/api.js';
|
|||||||
import { stagger, vibrate } from '/utils/ux.js';
|
import { stagger, vibrate } from '/utils/ux.js';
|
||||||
import { t } from '/i18n.js';
|
import { t } from '/i18n.js';
|
||||||
import { esc } from '/utils/html.js';
|
import { esc } from '/utils/html.js';
|
||||||
import { promptModal, confirmModal } from '/components/modal.js';
|
import { promptModal } from '/components/modal.js';
|
||||||
import { DEFAULT_CATEGORY_NAME, categoryLabel } from '/utils/shopping-categories.js';
|
import { DEFAULT_CATEGORY_NAME, categoryLabel } from '/utils/shopping-categories.js';
|
||||||
|
|
||||||
// --------------------------------------------------------
|
// --------------------------------------------------------
|
||||||
@@ -781,10 +781,18 @@ function wireListContentEvents(container) {
|
|||||||
|
|
||||||
// ---- Liste löschen ----
|
// ---- Liste löschen ----
|
||||||
if (action === 'delete-list') {
|
if (action === 'delete-list') {
|
||||||
if (!await confirmModal(t('shopping.deleteListConfirm', { name: state.activeList?.name }), { danger: true, confirmLabel: t('common.delete') })) return;
|
const deletedListId = state.activeListId;
|
||||||
|
|
||||||
|
let undone = false;
|
||||||
|
window.oikos.showToast(t('shopping.deletedListToast'), 'default', 5000, () => {
|
||||||
|
undone = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
setTimeout(async () => {
|
||||||
|
if (undone) return;
|
||||||
try {
|
try {
|
||||||
await api.delete(`/shopping/${state.activeListId}`);
|
await api.delete(`/shopping/${deletedListId}`);
|
||||||
state.lists = state.lists.filter((l) => l.id !== state.activeListId);
|
state.lists = state.lists.filter((l) => l.id !== deletedListId);
|
||||||
state.activeListId = state.lists[0]?.id ?? null;
|
state.activeListId = state.lists[0]?.id ?? null;
|
||||||
if (state.activeListId) {
|
if (state.activeListId) {
|
||||||
await switchList(state.activeListId, container);
|
await switchList(state.activeListId, container);
|
||||||
@@ -794,10 +802,10 @@ function wireListContentEvents(container) {
|
|||||||
renderTabs(container);
|
renderTabs(container);
|
||||||
renderListContent(container);
|
renderListContent(container);
|
||||||
}
|
}
|
||||||
window.oikos.showToast(t('shopping.deletedListToast'));
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
window.oikos.showToast(err.message, 'danger');
|
window.oikos.showToast(err.message ?? t('common.unknownError'), 'danger');
|
||||||
}
|
}
|
||||||
|
}, 5000);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
+16
-5
@@ -6,7 +6,7 @@
|
|||||||
|
|
||||||
import { api } from '/api.js';
|
import { api } from '/api.js';
|
||||||
import { renderRRuleFields, bindRRuleEvents, getRRuleValues } from '/rrule-ui.js';
|
import { renderRRuleFields, bindRRuleEvents, getRRuleValues } from '/rrule-ui.js';
|
||||||
import { openModal as openSharedModal, closeModal, wireBlurValidation, validateAll, btnSuccess, btnError, promptModal, confirmModal } from '/components/modal.js';
|
import { openModal as openSharedModal, closeModal, wireBlurValidation, validateAll, btnSuccess, btnError, promptModal } from '/components/modal.js';
|
||||||
import { stagger, vibrate } from '/utils/ux.js';
|
import { stagger, vibrate } from '/utils/ux.js';
|
||||||
import { t, formatDate, formatTime } from '/i18n.js';
|
import { t, formatDate, formatTime } from '/i18n.js';
|
||||||
import { esc } from '/utils/html.js';
|
import { esc } from '/utils/html.js';
|
||||||
@@ -582,18 +582,29 @@ async function handleFormSubmit(e, container) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function handleDeleteTask(id, container) {
|
async function handleDeleteTask(id, container) {
|
||||||
if (!await confirmModal(t('tasks.deleteConfirm'), { danger: true, confirmLabel: t('common.delete') })) return;
|
closeModal();
|
||||||
|
const itemEl = container.querySelector(`[data-task-id="${id}"]`);
|
||||||
|
if (itemEl) itemEl.style.display = 'none';
|
||||||
|
|
||||||
|
let undone = false;
|
||||||
|
window.oikos.showToast(t('tasks.deletedToast'), 'default', 5000, () => {
|
||||||
|
undone = true;
|
||||||
|
if (itemEl) itemEl.style.display = '';
|
||||||
|
});
|
||||||
|
|
||||||
|
setTimeout(async () => {
|
||||||
|
if (undone) return;
|
||||||
try {
|
try {
|
||||||
await api.delete(`/tasks/${id}`);
|
await api.delete(`/tasks/${id}`);
|
||||||
// Erinnerungen für diese Aufgabe ebenfalls entfernen
|
// Erinnerungen für diese Aufgabe ebenfalls entfernen
|
||||||
api.delete(`/reminders?entity_type=task&entity_id=${id}`).catch(() => {});
|
api.delete(`/reminders?entity_type=task&entity_id=${id}`).catch(() => {});
|
||||||
refreshReminders();
|
refreshReminders();
|
||||||
closeModal();
|
|
||||||
window.oikos.showToast(t('tasks.deletedToast'), 'default');
|
|
||||||
await loadTasks(container);
|
await loadTasks(container);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
window.oikos.showToast(err.message, 'danger');
|
if (itemEl) itemEl.style.display = '';
|
||||||
|
window.oikos.showToast(err.message ?? t('common.unknownError'), 'danger');
|
||||||
}
|
}
|
||||||
|
}, 5000);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleAddSubtask(parentId, container) {
|
async function handleAddSubtask(parentId, container) {
|
||||||
|
|||||||
Reference in New Issue
Block a user