| @@ -33,8 +33,17 @@ | ||
| 33 | 33 | * @throws \InvalidArgumentException If day exists and $allowExisting is false |
| 34 | 34 | */ |
| 35 | 35 | public function getOrCreateDay(int $tripId, int $dayNumber, ?string $dayTitle = null, ?string $dayDescription = null, bool $allowExisting = true): int |
| 36 | 36 | { |
| 37 | + // The method body uses $wpdb and $tableDays directly for the day | |
| 38 | + // update/insert below. The cached-query closures declare their own copies, | |
| 39 | + // but the body never did — so $wpdb was null ("Call to a member function | |
| 40 | + // update() on null") and $tableDays was empty ("Incorrect table name '')". | |
| 41 | + // This broke saving an activity onto an existing, titled day and silently | |
| 42 | + // mis-created new days. Declare both here. | |
| 43 | + global $wpdb; | |
| 44 | + $tableDays = TripItineraryDaysTable::getTableName(); | |
| 45 | + | |
| 37 | 46 | // Use QueryCache for caching day existence checks |
| 38 | 47 | $cacheKey = Cache::KEY_DAY_EXISTS . "_{$tripId}_day_{$dayNumber}"; |
| 39 | 48 | |
| 40 | 49 | $existingDay = $this->cacheQueryResult($cacheKey, function() use ($tripId, $dayNumber) { |
| @@ -883,8 +892,16 @@ | ||
| 883 | 892 | |
| 884 | 893 | return $result !== false; |
| 885 | 894 | } |
| 886 | 895 | |
| 896 | + // Explicit 'day' mode must never fall through to delete an activity. | |
| 897 | + // Day ids and activity ids come from different tables and can collide | |
| 898 | + // numerically; if the day no longer exists (e.g. already removed in | |
| 899 | + // another tab), bail rather than silently deleting a same-id activity. | |
| 900 | + if ($mode === 'day') { | |
| 901 | + return false; | |
| 902 | + } | |
| 903 | + | |
| 887 | 904 | // Check if this is an activity entry ID (from entries table) |
| 888 | 905 | $activityEntry = $wpdb->get_row( |
| 889 | 906 | $wpdb->prepare("SELECT * FROM `{$tableEntries}` WHERE id = %d", $id) |
| 890 | 907 | ); |
| @@ -903,100 +920,58 @@ | ||
| 903 | 920 | return false; |
| 904 | 921 | } |
| 905 | 922 | |
| 906 | 923 | /** |
| 907 | - * Bulk delete entries | |
| 908 | - * @param array $ids Array of entry IDs to delete | |
| 924 | + * Bulk delete itinerary days and/or activity entries. | |
| 925 | + * | |
| 926 | + * Days and activities live in two different tables with independent | |
| 927 | + * auto-increment id spaces, so a single flat list of ids is ambiguous | |
| 928 | + * (the same number can be a valid day id *and* a valid activity id). The | |
| 929 | + * caller therefore tells us which is which: `$dayIds` are day-table ids | |
| 930 | + * (deleting one removes the day row and all of its activities) and `$ids` | |
| 931 | + * are activity-entry ids. Each id is routed through the single-item | |
| 932 | + * {@see self::delete()} with an explicit mode so the correct table is | |
| 933 | + * always used. | |
| 934 | + * | |
| 935 | + * @param array $ids Activity entry ids (day_entry table) | |
| 936 | + * @param array $dayIds Day ids (days table); the day and its activities are removed | |
| 909 | 937 | * @return array ['deleted' => count, 'failed' => count] |
| 910 | 938 | */ |
| 911 | - public function bulkDelete(array $ids): array | |
| 939 | + public function bulkDelete(array $ids, array $dayIds = []): array | |
| 912 | 940 | { |
| 913 | - global $wpdb; | |
| 914 | - $tableEntries = $this->getTableName(); | |
| 915 | - | |
| 916 | - $tableDays = TripItineraryDaysTable::getTableName(); | |
| 917 | - | |
| 918 | - if (empty($ids)) { | |
| 919 | - return ['deleted' => 0, 'failed' => 0]; | |
| 920 | - } | |
| 921 | - | |
| 922 | - // Sanitize IDs | |
| 923 | - $ids = array_map('intval', $ids); | |
| 924 | - $ids = array_filter($ids, function($id) { | |
| 925 | - return $id > 0; | |
| 926 | - }); | |
| 927 | - | |
| 928 | - if (empty($ids)) { | |
| 929 | - return ['deleted' => 0, 'failed' => 0]; | |
| 930 | - } | |
| 931 | - | |
| 932 | 941 | $deleted = 0; |
| 933 | 942 | $failed = 0; |
| 934 | - $processedDayIds = []; // Track days we've already processed | |
| 935 | 943 | |
| 936 | - foreach ($ids as $id) { | |
| 944 | + // Delete whole days first — this also removes every activity that | |
| 945 | + // belongs to the day, so any of those activity ids that also appear in | |
| 946 | + // $ids become harmless no-ops below. | |
| 947 | + $dayIds = array_unique(array_filter(array_map('intval', $dayIds), static function ($id) { | |
| 948 | + return $id > 0; | |
| 949 | + })); | |
| 950 | + foreach ($dayIds as $dayId) { | |
| 937 | 951 | try { |
| 938 | - // Get the entry to check if it's a day entry | |
| 939 | - $entry = $wpdb->get_row( | |
| 940 | - $wpdb->prepare("SELECT day_id, item_type_id, item_id FROM `{$tableEntries}` WHERE id = %d", $id) | |
| 941 | - ); | |
| 942 | - | |
| 943 | - if (!$entry) { | |
| 952 | + if ($this->delete($dayId, 'day')) { | |
| 953 | + $deleted++; | |
| 954 | + } else { | |
| 944 | 955 | $failed++; |
| 945 | - continue; | |
| 946 | 956 | } |
| 957 | + } catch (\Throwable $e) { | |
| 958 | + $failed++; | |
| 959 | + } | |
| 960 | + } | |
| 947 | 961 | |
| 948 | - $dayId = (int) $entry->day_id; | |
| 949 | - $isDayEntry = ($entry->item_type_id === null || $entry->item_type_id === 0) && | |
| 950 | - ($entry->item_id === null || $entry->item_id === 0); | |
| 951 | - | |
| 952 | - // If this is a day entry, delete all entries for this day | |
| 953 | - if ($isDayEntry) { | |
| 954 | - // Skip if we've already processed this day | |
| 955 | - if (in_array($dayId, $processedDayIds)) { | |
| 956 | - continue; | |
| 957 | - } | |
| 958 | - | |
| 959 | - $processedDayIds[] = $dayId; | |
| 960 | - | |
| 961 | - // Get all entry IDs for this day | |
| 962 | - $dayEntryIds = $wpdb->get_col( | |
| 963 | - $wpdb->prepare("SELECT id FROM `{$tableEntries}` WHERE day_id = %d", $dayId) | |
| 964 | - ); | |
| 965 | - | |
| 966 | - if (!empty($dayEntryIds)) { | |
| 967 | - // Delete images for all entries | |
| 968 | - $placeholders = implode(',', array_fill(0, count($dayEntryIds), '%d')); | |
| 969 | - $wpdb->query( | |
| 970 | - $wpdb->prepare( | |
| 971 | - "DELETE FROM `{$tableImages}` WHERE entry_id IN ($placeholders)", | |
| 972 | - ...$dayEntryIds | |
| 973 | - ) | |
| 974 | - ); | |
| 975 | - | |
| 976 | - // Delete all entries for this day | |
| 977 | - $wpdb->delete($tableEntries, ['day_id' => $dayId], ['%d']); | |
| 978 | - | |
| 979 | - // Delete the day itself | |
| 980 | - $wpdb->delete($tableDays, ['id' => $dayId], ['%d']); | |
| 981 | - } | |
| 982 | - | |
| 962 | + // Delete standalone activity entries. | |
| 963 | + $ids = array_unique(array_filter(array_map('intval', $ids), static function ($id) { | |
| 964 | + return $id > 0; | |
| 965 | + })); | |
| 966 | + foreach ($ids as $id) { | |
| 967 | + try { | |
| 968 | + if ($this->delete($id, 'activity')) { | |
| 983 | 969 | $deleted++; |
| 984 | 970 | } else { |
| 985 | - // For activity entries, just delete the entry and its images | |
| 986 | - // Delete related images | |
| 987 | - $wpdb->delete($tableImages, ['entry_id' => $id], ['%d']); | |
| 988 | - | |
| 989 | - // Delete entry | |
| 990 | - $result = $wpdb->delete($tableEntries, ['id' => $id], ['%d']); | |
| 991 | - | |
| 992 | - if ($result !== false) { | |
| 993 | - $deleted++; | |
| 994 | - } else { | |
| 995 | - $failed++; | |
| 996 | - } | |
| 971 | + $failed++; | |
| 997 | 972 | } |
| 998 | - } catch (\Exception $e) { | |
| 973 | + } catch (\Throwable $e) { | |
| 999 | 974 | $failed++; |
| 1000 | 975 | } |
| 1001 | 976 | } |
| 1002 | 977 | |