diff --git a/CHANGELOG.md b/CHANGELOG.md index f12a628..97434b6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [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 ### Changed diff --git a/package-lock.json b/package-lock.json index 910da4e..df2cd7e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "oikos", - "version": "0.25.2", + "version": "0.25.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "oikos", - "version": "0.25.2", + "version": "0.25.3", "license": "MIT", "dependencies": { "bcrypt": "^6.0.0", diff --git a/package.json b/package.json index 5c06a9f..19eeda1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "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.", "main": "server/index.js", "type": "module", diff --git a/public/locales/ar.json b/public/locales/ar.json index b2144a0..f65615d 100644 --- a/public/locales/ar.json +++ b/public/locales/ar.json @@ -258,7 +258,8 @@ "savedRecipeLabel": "Saved recipe", "savedRecipePlaceholder": "Select recipe", "saveAsRecipe": "Save as recipe", - "recipeScaleLabel": "Scale ingredients" + "recipeScaleLabel": "Scale ingredients", + "deletedToast": "Meal deleted" }, "calendar": { "title": "التقويم", diff --git a/public/locales/de.json b/public/locales/de.json index 9bdea2c..294f40a 100644 --- a/public/locales/de.json +++ b/public/locales/de.json @@ -264,7 +264,8 @@ "savedRecipeLabel": "Gespeichertes Rezept", "savedRecipePlaceholder": "Rezept auswählen", "saveAsRecipe": "Als Rezept speichern", - "recipeScaleLabel": "Zutaten skalieren" + "recipeScaleLabel": "Zutaten skalieren", + "deletedToast": "Mahlzeit gelöscht" }, "calendar": { "title": "Kalender", diff --git a/public/locales/el.json b/public/locales/el.json index 2ed0b72..bb5c421 100644 --- a/public/locales/el.json +++ b/public/locales/el.json @@ -258,7 +258,8 @@ "savedRecipeLabel": "Saved recipe", "savedRecipePlaceholder": "Select recipe", "saveAsRecipe": "Save as recipe", - "recipeScaleLabel": "Scale ingredients" + "recipeScaleLabel": "Scale ingredients", + "deletedToast": "Meal deleted" }, "calendar": { "title": "Ημερολόγιο", diff --git a/public/locales/en.json b/public/locales/en.json index 76505d6..083a815 100644 --- a/public/locales/en.json +++ b/public/locales/en.json @@ -258,7 +258,8 @@ "savedRecipeLabel": "Saved recipe", "savedRecipePlaceholder": "Select recipe", "saveAsRecipe": "Save as recipe", - "recipeScaleLabel": "Scale ingredients" + "recipeScaleLabel": "Scale ingredients", + "deletedToast": "Meal deleted" }, "calendar": { "title": "Calendar", diff --git a/public/locales/es.json b/public/locales/es.json index e774628..4720f89 100644 --- a/public/locales/es.json +++ b/public/locales/es.json @@ -258,7 +258,8 @@ "savedRecipeLabel": "Saved recipe", "savedRecipePlaceholder": "Select recipe", "saveAsRecipe": "Save as recipe", - "recipeScaleLabel": "Scale ingredients" + "recipeScaleLabel": "Scale ingredients", + "deletedToast": "Meal deleted" }, "calendar": { "title": "Calendario", diff --git a/public/locales/fr.json b/public/locales/fr.json index d30fb93..3134b52 100644 --- a/public/locales/fr.json +++ b/public/locales/fr.json @@ -258,7 +258,8 @@ "savedRecipeLabel": "Saved recipe", "savedRecipePlaceholder": "Select recipe", "saveAsRecipe": "Save as recipe", - "recipeScaleLabel": "Scale ingredients" + "recipeScaleLabel": "Scale ingredients", + "deletedToast": "Meal deleted" }, "calendar": { "title": "Calendrier", diff --git a/public/locales/hi.json b/public/locales/hi.json index b42c0e8..0753297 100644 --- a/public/locales/hi.json +++ b/public/locales/hi.json @@ -258,7 +258,8 @@ "savedRecipeLabel": "Saved recipe", "savedRecipePlaceholder": "Select recipe", "saveAsRecipe": "Save as recipe", - "recipeScaleLabel": "Scale ingredients" + "recipeScaleLabel": "Scale ingredients", + "deletedToast": "Meal deleted" }, "calendar": { "title": "कैलेंडर", diff --git a/public/locales/it.json b/public/locales/it.json index c6c13db..a6cb3fd 100644 --- a/public/locales/it.json +++ b/public/locales/it.json @@ -258,7 +258,8 @@ "savedRecipeLabel": "Saved recipe", "savedRecipePlaceholder": "Select recipe", "saveAsRecipe": "Save as recipe", - "recipeScaleLabel": "Scale ingredients" + "recipeScaleLabel": "Scale ingredients", + "deletedToast": "Meal deleted" }, "calendar": { "title": "Calendario", diff --git a/public/locales/ja.json b/public/locales/ja.json index 31d97f9..953ecb1 100644 --- a/public/locales/ja.json +++ b/public/locales/ja.json @@ -258,7 +258,8 @@ "savedRecipeLabel": "Saved recipe", "savedRecipePlaceholder": "Select recipe", "saveAsRecipe": "Save as recipe", - "recipeScaleLabel": "Scale ingredients" + "recipeScaleLabel": "Scale ingredients", + "deletedToast": "Meal deleted" }, "calendar": { "title": "カレンダー", diff --git a/public/locales/pt.json b/public/locales/pt.json index ff5808c..62d88ff 100644 --- a/public/locales/pt.json +++ b/public/locales/pt.json @@ -258,7 +258,8 @@ "savedRecipeLabel": "Saved recipe", "savedRecipePlaceholder": "Select recipe", "saveAsRecipe": "Save as recipe", - "recipeScaleLabel": "Scale ingredients" + "recipeScaleLabel": "Scale ingredients", + "deletedToast": "Meal deleted" }, "calendar": { "title": "Calendário", diff --git a/public/locales/ru.json b/public/locales/ru.json index 9e8b56e..c78b3c0 100644 --- a/public/locales/ru.json +++ b/public/locales/ru.json @@ -258,7 +258,8 @@ "savedRecipeLabel": "Saved recipe", "savedRecipePlaceholder": "Select recipe", "saveAsRecipe": "Save as recipe", - "recipeScaleLabel": "Scale ingredients" + "recipeScaleLabel": "Scale ingredients", + "deletedToast": "Meal deleted" }, "calendar": { "title": "Календарь", diff --git a/public/locales/sv.json b/public/locales/sv.json index fdde958..c1082a4 100644 --- a/public/locales/sv.json +++ b/public/locales/sv.json @@ -258,7 +258,8 @@ "savedRecipeLabel": "Sparat recept", "savedRecipePlaceholder": "Välj recept", "saveAsRecipe": "Spara som recept", - "recipeScaleLabel": "Skala ingredienser" + "recipeScaleLabel": "Skala ingredienser", + "deletedToast": "Meal deleted" }, "calendar": { "title": "Kalender", diff --git a/public/locales/tr.json b/public/locales/tr.json index 63da054..e5ee568 100644 --- a/public/locales/tr.json +++ b/public/locales/tr.json @@ -258,7 +258,8 @@ "savedRecipeLabel": "Saved recipe", "savedRecipePlaceholder": "Select recipe", "saveAsRecipe": "Save as recipe", - "recipeScaleLabel": "Scale ingredients" + "recipeScaleLabel": "Scale ingredients", + "deletedToast": "Meal deleted" }, "calendar": { "title": "Takvim", diff --git a/public/locales/uk.json b/public/locales/uk.json index edc73b9..fe01e5e 100644 --- a/public/locales/uk.json +++ b/public/locales/uk.json @@ -258,7 +258,8 @@ "savedRecipeLabel": "Saved recipe", "savedRecipePlaceholder": "Select recipe", "saveAsRecipe": "Save as recipe", - "recipeScaleLabel": "Scale ingredients" + "recipeScaleLabel": "Scale ingredients", + "deletedToast": "Meal deleted" }, "calendar": { "title": "Календар", diff --git a/public/locales/zh.json b/public/locales/zh.json index cf18b5b..e603cca 100644 --- a/public/locales/zh.json +++ b/public/locales/zh.json @@ -258,7 +258,8 @@ "savedRecipeLabel": "Saved recipe", "savedRecipePlaceholder": "Select recipe", "saveAsRecipe": "Save as recipe", - "recipeScaleLabel": "Scale ingredients" + "recipeScaleLabel": "Scale ingredients", + "deletedToast": "Meal deleted" }, "calendar": { "title": "日历", diff --git a/public/pages/budget.js b/public/pages/budget.js index df34311..bdc69cf 100644 --- a/public/pages/budget.js +++ b/public/pages/budget.js @@ -6,7 +6,7 @@ */ 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 { t, formatDate, getLocale } from '/i18n.js'; import { esc } from '/utils/html.js'; @@ -660,18 +660,35 @@ function openBudgetModal({ mode, entry = null }) { // -------------------------------------------------------- async function deleteEntry(id) { - if (!await confirmModal(t('budget.deleteConfirm'), { danger: true, confirmLabel: t('common.delete') })) return; - try { - await api.delete(`/budget/${id}`); - state.entries = state.entries.filter((e) => e.id !== id); - const sumRes = await api.get(`/budget/summary?month=${state.month}`); - state.summary = sumRes.data; - renderBody(); - vibrate([30, 50, 30]); - window.oikos?.showToast(t('budget.deletedToast'), 'success'); - } catch (err) { - window.oikos?.showToast(err.data?.error ?? t('common.unknownError'), 'error'); - } + 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 { + await api.delete(`/budget/${id}`); + const sumRes = await api.get(`/budget/summary?month=${state.month}`); + state.summary = sumRes.data; + renderBody(); + } catch (err) { + 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); } // -------------------------------------------------------- diff --git a/public/pages/calendar.js b/public/pages/calendar.js index 0041f21..698c7ac 100644 --- a/public/pages/calendar.js +++ b/public/pages/calendar.js @@ -6,7 +6,7 @@ import { api } from '/api.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 { t, formatTime } from '/i18n.js'; import { esc, fmtLocation } from '/utils/html.js'; @@ -764,7 +764,6 @@ function showEventPopup(ev, anchor) { }); 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(); 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-delete')?.addEventListener('click', async () => { - if (!await confirmModal(t('calendar.deleteConfirm', { title: event.title }), { danger: true, confirmLabel: t('common.delete') })) return; closeModal(); await deleteEvent(event.id); }); @@ -1073,15 +1071,32 @@ async function saveEvent(overlay, mode, eventId, existingReminder = null) { } async function deleteEvent(id) { - try { - await api.delete(`/calendar/${id}`); - api.delete(`/reminders?entity_type=event&entity_id=${id}`).catch(() => {}); - refreshReminders(); - state.events = state.events.filter((e) => e.id !== id); - renderView(); - window.oikos?.showToast(t('calendar.deletedToast'), 'success'); - } catch (err) { - window.oikos?.showToast(err.data?.error ?? t('calendar.deleteError'), 'error'); - } + 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 { + await api.delete(`/calendar/${id}`); + api.delete(`/reminders?entity_type=event&entity_id=${id}`).catch(() => {}); + refreshReminders(); + } catch (err) { + if (event) { + state.events = [...state.events, event]; + renderView(); + } + window.oikos?.showToast(err.data?.error ?? t('calendar.deleteError'), 'danger'); + } + }, 5000); } diff --git a/public/pages/contacts.js b/public/pages/contacts.js index 4c78bde..e4551a3 100644 --- a/public/pages/contacts.js +++ b/public/pages/contacts.js @@ -5,7 +5,7 @@ */ 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 { t } from '/i18n.js'; import { esc } from '/utils/html.js'; @@ -350,16 +350,32 @@ function openContactModal({ mode, contact = null }) { } async function deleteContact(id) { - if (!await confirmModal(t('contacts.deleteConfirm'), { danger: true, confirmLabel: t('common.delete') })) return; - try { - await api.delete(`/contacts/${id}`); - state.contacts = state.contacts.filter((c) => c.id !== id); - renderList(); - vibrate([30, 50, 30]); - window.oikos?.showToast(t('contacts.deletedToast'), 'success'); - } catch (err) { - window.oikos?.showToast(err.data?.error ?? t('common.unknownError'), 'error'); - } + const contact = state.contacts.find((c) => c.id === id); + state.contacts = state.contacts.filter((c) => c.id !== id); + renderList(); + vibrate([30, 50, 30]); + + let undone = false; + 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); } diff --git a/public/pages/meals.js b/public/pages/meals.js index 6b0e50f..0dbea21 100644 --- a/public/pages/meals.js +++ b/public/pages/meals.js @@ -5,7 +5,7 @@ */ 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 { t, formatDate } from '/i18n.js'; import { esc } from '/utils/html.js'; @@ -920,15 +920,27 @@ function collectModalIngredients(overlay) { // -------------------------------------------------------- async function deleteMeal(mealId) { - if (!await confirmModal(t('meals.deleteMeal') + '?', { danger: true, confirmLabel: t('common.delete') })) return; - try { - await api.delete(`/meals/${mealId}`); - state.meals = state.meals.filter((m) => m.id !== mealId); - renderWeekGrid(); - window.oikos?.showToast(t('meals.deleteMeal'), 'success'); - } catch (err) { - window.oikos?.showToast(err.data?.error ?? t('common.errorGeneric'), 'error'); - } + 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 { + await api.delete(`/meals/${mealId}`); + state.meals = state.meals.filter((m) => m.id !== mealId); + renderWeekGrid(); + } catch (err) { + if (itemEl) itemEl.style.display = ''; + window.oikos?.showToast(err.data?.error ?? t('common.unknownError'), 'danger'); + } + }, 5000); } // -------------------------------------------------------- diff --git a/public/pages/notes.js b/public/pages/notes.js index 5fe3c37..f04dc89 100644 --- a/public/pages/notes.js +++ b/public/pages/notes.js @@ -5,7 +5,7 @@ */ 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 { t } from '/i18n.js'; import { esc } from '/utils/html.js'; @@ -476,16 +476,33 @@ async function togglePin(id) { } async function deleteNote(id) { - if (!await confirmModal(t('notes.deleteConfirm'), { danger: true, confirmLabel: t('common.delete') })) return; - try { - await api.delete(`/notes/${id}`); - state.notes = state.notes.filter((n) => n.id !== id); - renderGrid(); - vibrate([30, 50, 30]); - window.oikos?.showToast(t('notes.deletedToast'), 'success'); - } catch (err) { - window.oikos?.showToast(err.data?.error ?? t('common.unknownError'), 'error'); - } + closeModal(); + const note = state.notes.find((n) => n.id === id); + state.notes = state.notes.filter((n) => n.id !== id); + renderGrid(); + vibrate([30, 50, 30]); + + let undone = false; + 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); } // -------------------------------------------------------- diff --git a/public/pages/recipes.js b/public/pages/recipes.js index 48e85cd..8ead862 100644 --- a/public/pages/recipes.js +++ b/public/pages/recipes.js @@ -5,7 +5,7 @@ import { api } from '/api.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'; let _container = null; @@ -134,6 +134,7 @@ function renderRecipeList() { for (const recipe of state.recipes) { const card = document.createElement('article'); card.className = 'recipe-card'; + card.dataset.id = String(recipe.id); const h = document.createElement('h2'); h.className = 'recipe-card__title'; @@ -370,21 +371,26 @@ async function saveRecipe(panel, mode, recipe) { } async function removeRecipe(recipe) { - const ok = await confirmModal(t('recipes.deleteConfirm', { title: recipe.title }), { - danger: true, - confirmLabel: t('common.delete'), + const itemEl = _container.querySelector(`.recipe-card[data-id="${recipe.id}"]`); + if (itemEl) itemEl.style.display = 'none'; + + let undone = false; + window.oikos?.showToast(t('recipes.deleted'), 'default', 5000, () => { + undone = true; + if (itemEl) itemEl.style.display = ''; }); - if (!ok) return; - - try { - await api.delete(`/recipes/${recipe.id}`); - state.recipes = state.recipes.filter((r) => r.id !== recipe.id); - renderRecipeList(); - window.oikos?.showToast(t('recipes.deleted'), 'success'); - } catch (err) { - window.oikos?.showToast(err.data?.error ?? t('common.errorGeneric'), 'error'); - } + setTimeout(async () => { + if (undone) return; + try { + await api.delete(`/recipes/${recipe.id}`); + state.recipes = state.recipes.filter((r) => r.id !== recipe.id); + renderRecipeList(); + } catch (err) { + if (itemEl) itemEl.style.display = ''; + window.oikos?.showToast(err.data?.error ?? t('common.unknownError'), 'danger'); + } + }, 5000); } async function duplicateRecipe(recipe) { diff --git a/public/pages/shopping.js b/public/pages/shopping.js index 4c7379b..5ca1b7d 100644 --- a/public/pages/shopping.js +++ b/public/pages/shopping.js @@ -8,7 +8,7 @@ import { api } from '/api.js'; import { stagger, vibrate } from '/utils/ux.js'; import { t } from '/i18n.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'; // -------------------------------------------------------- @@ -781,23 +781,31 @@ function wireListContentEvents(container) { // ---- Liste löschen ---- if (action === 'delete-list') { - if (!await confirmModal(t('shopping.deleteListConfirm', { name: state.activeList?.name }), { danger: true, confirmLabel: t('common.delete') })) return; - try { - await api.delete(`/shopping/${state.activeListId}`); - state.lists = state.lists.filter((l) => l.id !== state.activeListId); - state.activeListId = state.lists[0]?.id ?? null; - if (state.activeListId) { - await switchList(state.activeListId, container); - } else { - state.items = []; - state.activeList = null; - renderTabs(container); - renderListContent(container); + const deletedListId = state.activeListId; + + let undone = false; + window.oikos.showToast(t('shopping.deletedListToast'), 'default', 5000, () => { + undone = true; + }); + + setTimeout(async () => { + if (undone) return; + try { + await api.delete(`/shopping/${deletedListId}`); + state.lists = state.lists.filter((l) => l.id !== deletedListId); + state.activeListId = state.lists[0]?.id ?? null; + if (state.activeListId) { + await switchList(state.activeListId, container); + } else { + state.items = []; + state.activeList = null; + renderTabs(container); + renderListContent(container); + } + } catch (err) { + window.oikos.showToast(err.message ?? t('common.unknownError'), 'danger'); } - window.oikos.showToast(t('shopping.deletedListToast')); - } catch (err) { - window.oikos.showToast(err.message, 'danger'); - } + }, 5000); } }); diff --git a/public/pages/tasks.js b/public/pages/tasks.js index 91ce4ec..660d121 100644 --- a/public/pages/tasks.js +++ b/public/pages/tasks.js @@ -6,7 +6,7 @@ import { api } from '/api.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 { t, formatDate, formatTime } from '/i18n.js'; import { esc } from '/utils/html.js'; @@ -582,18 +582,29 @@ async function handleFormSubmit(e, container) { } async function handleDeleteTask(id, container) { - if (!await confirmModal(t('tasks.deleteConfirm'), { danger: true, confirmLabel: t('common.delete') })) return; - try { - await api.delete(`/tasks/${id}`); - // Erinnerungen für diese Aufgabe ebenfalls entfernen - api.delete(`/reminders?entity_type=task&entity_id=${id}`).catch(() => {}); - refreshReminders(); - closeModal(); - window.oikos.showToast(t('tasks.deletedToast'), 'default'); - await loadTasks(container); - } catch (err) { - window.oikos.showToast(err.message, 'danger'); - } + 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 { + await api.delete(`/tasks/${id}`); + // Erinnerungen für diese Aufgabe ebenfalls entfernen + api.delete(`/reminders?entity_type=task&entity_id=${id}`).catch(() => {}); + refreshReminders(); + await loadTasks(container); + } catch (err) { + if (itemEl) itemEl.style.display = ''; + window.oikos.showToast(err.message ?? t('common.unknownError'), 'danger'); + } + }, 5000); } async function handleAddSubtask(parentId, container) {