| @@ -6,8 +6,35 @@ | ||
| 6 | 6 | use FluentForm\App\Modules\Payments\PaymentHelper; |
| 7 | 7 | use FluentForm\App\Services\Manager\FormManagerService; |
| 8 | 8 | use FluentForm\Framework\Support\Arr; |
| 9 | 9 | |
| 10 | +/** | |
| 11 | + * Column properties resolved at runtime via the ORM's magic __get; declared | |
| 12 | + * here so static analysis can verify attribute access against the real schema | |
| 13 | + * (database/Migrations/Submissions.php). | |
| 14 | + * | |
| 15 | + * @property int $id | |
| 16 | + * @property int|null $form_id | |
| 17 | + * @property int|null $serial_number | |
| 18 | + * @property string|null $response | |
| 19 | + * @property string|null $source_url | |
| 20 | + * @property int|null $user_id | |
| 21 | + * @property string|null $status | |
| 22 | + * @property int $is_favourite | |
| 23 | + * @property string|null $browser | |
| 24 | + * @property string|null $device | |
| 25 | + * @property string|null $ip | |
| 26 | + * @property string|null $city | |
| 27 | + * @property string|null $country | |
| 28 | + * @property string|null $payment_status | |
| 29 | + * @property string|null $payment_method | |
| 30 | + * @property string|null $payment_type | |
| 31 | + * @property string|null $currency | |
| 32 | + * @property float|null $payment_total | |
| 33 | + * @property float|null $total_paid | |
| 34 | + * @property string|null $created_at | |
| 35 | + * @property string|null $updated_at | |
| 36 | + */ | |
| 10 | 37 | class Submission extends Model |
| 11 | 38 | { |
| 12 | 39 | /** |
| 13 | 40 | * The table associated with the model. |
| @@ -95,8 +122,22 @@ | ||
| 95 | 122 | { |
| 96 | 123 | return $this->hasMany(OrderItem::class, 'submission_id', 'id'); |
| 97 | 124 | } |
| 98 | 125 | |
| 126 | + /** | |
| 127 | + * Returns the column the entries listing should be sorted by. | |
| 128 | + * Defaults to created_at so imported entries respect their original | |
| 129 | + * submission date. Site owners can switch back to legacy id-based | |
| 130 | + * ordering via the fluentform/entries_default_sort_column filter. | |
| 131 | + */ | |
| 132 | + public static function getSortColumn() | |
| 133 | + { | |
| 134 | + $column = apply_filters('fluentform/entries_default_sort_column', 'created_at'); | |
| 135 | + $allowed = ['id', 'created_at']; | |
| 136 | + | |
| 137 | + return in_array($column, $allowed, true) ? $column : 'created_at'; | |
| 138 | + } | |
| 139 | + | |
| 99 | 140 | public function customQuery($attributes = [], $searchExtender = null) |
| 100 | 141 | { |
| 101 | 142 | $entryType = Arr::get($attributes, 'entry_type'); |
| 102 | 143 | $dateRange = Arr::get($attributes, 'date_range'); |
| @@ -121,9 +162,18 @@ | ||
| 121 | 162 | if ($paymentStatuses && is_array($paymentStatuses)) { |
| 122 | 163 | $wheres[] = ['payment_status', $paymentStatuses]; |
| 123 | 164 | } |
| 124 | 165 | |
| 125 | - $query = $this->orderBy('fluentform_submissions.id', $sortBy) | |
| 166 | + // Sort by submission date so imported entries (which get new auto-increment ids | |
| 167 | + // but carry their original created_at) interleave correctly with native ones. | |
| 168 | + // Tie-break on id to keep pagination stable for rows sharing a timestamp. | |
| 169 | + // Filter lets site owners revert to legacy id-based ordering if needed. | |
| 170 | + $sortColumn = self::getSortColumn(); | |
| 171 | + $query = $this->orderBy('fluentform_submissions.' . $sortColumn, $sortBy); | |
| 172 | + if ('id' !== $sortColumn) { | |
| 173 | + $query = $query->orderBy('fluentform_submissions.id', $sortBy); | |
| 174 | + } | |
| 175 | + $query = $query | |
| 126 | 176 | ->when($formId, function ($q) use ($formId) { |
| 127 | 177 | return $q->where('fluentform_submissions.form_id', $formId); |
| 128 | 178 | }) |
| 129 | 179 | ->when($isFavourite, function ($q) { |
| @@ -229,10 +279,31 @@ | ||
| 229 | 279 | $columns = Arr::get($attributes, 'columns', 'id'); |
| 230 | 280 | |
| 231 | 281 | $query = $this->customQuery($attributes); |
| 232 | 282 | |
| 233 | - $submission = $query->select($columns)->where('id', $operator, $entryId)->first(); | |
| 283 | + // Adjacency must match customQuery's sort order. Compare on the same | |
| 284 | + // (sortColumn, id) row-tuple the listing orders by, so Next/Prev walk | |
| 285 | + // in display order regardless of which sort column the filter selects. | |
| 286 | + // When sortColumn is id the tuple degenerates to a simple id compare. | |
| 287 | + $sortColumn = self::getSortColumn(); | |
| 288 | + // Re-assert whitelist; $sortColumn is interpolated into whereRaw below. | |
| 289 | + if (!in_array($sortColumn, ['id', 'created_at'], true)) { | |
| 290 | + $sortColumn = 'created_at'; | |
| 291 | + } | |
| 292 | + $current = static::select(['id', $sortColumn])->find($entryId); | |
| 293 | + if (!$current) { | |
| 294 | + return apply_filters('fluentform/next_submission', null, $entryId, $attributes); | |
| 295 | + } | |
| 234 | 296 | |
| 297 | + global $wpdb; | |
| 298 | + $table = $wpdb->prefix . 'fluentform_submissions'; | |
| 299 | + $submission = $query->select($columns) | |
| 300 | + ->whereRaw( | |
| 301 | + "({$table}.{$sortColumn}, {$table}.id) {$operator} (?, ?)", | |
| 302 | + [$current->{$sortColumn}, $entryId] | |
| 303 | + ) | |
| 304 | + ->first(); | |
| 305 | + | |
| 235 | 306 | return apply_filters('fluentform/next_submission', $submission, $entryId, $attributes); |
| 236 | 307 | } |
| 237 | 308 | |
| 238 | 309 | public function countByGroup($formId) |
| @@ -273,10 +344,25 @@ | ||
| 273 | 344 | { |
| 274 | 345 | $this->where('id', $id)->update($data); |
| 275 | 346 | } |
| 276 | 347 | |
| 277 | - public static function remove($submissionIds) | |
| 348 | + public static function remove($submissionIds, $formId = null) | |
| 278 | 349 | { |
| 350 | + // Fail-closed scope guard: $formId scopes every delete to its owning form; | |
| 351 | + // a missing scope throws rather than ever deleting unscoped. | |
| 352 | + if (empty($formId)) { | |
| 353 | + throw new \InvalidArgumentException('Submission::remove() requires a form id to scope the deletion.'); | |
| 354 | + } | |
| 355 | + | |
| 356 | + $submissionIds = static::where('form_id', $formId) | |
| 357 | + ->whereIn('id', (array) $submissionIds) | |
| 358 | + ->pluck('id') | |
| 359 | + ->all(); | |
| 360 | + | |
| 361 | + if (!$submissionIds) { | |
| 362 | + return; | |
| 363 | + } | |
| 364 | + | |
| 279 | 365 | static::whereIn('id', $submissionIds)->delete(); |
| 280 | 366 | |
| 281 | 367 | SubmissionMeta::whereIn('response_id', $submissionIds)->delete(); |
| 282 | 368 | |
| @@ -303,9 +389,12 @@ | ||
| 303 | 389 | } |
| 304 | 390 | } |
| 305 | 391 | |
| 306 | 392 | public function allSubmissions($attributes = []) { |
| 307 | - $searchExtender = function ($q, $escaped) { | |
| 393 | + // Match by form title only in the cross-form view; once a form is | |
| 394 | + // selected the search must hit submission data only, so the listing | |
| 395 | + // stays identical to the (form-scoped) export and single-form list. | |
| 396 | + $searchExtender = Arr::get($attributes, 'form_id') ? null : function ($q, $escaped) { | |
| 308 | 397 | $q->orWhereHas('form', function ($q) use ($escaped) { |
| 309 | 398 | $q->where('title', 'LIKE', "%{$escaped}%"); |
| 310 | 399 | }); |
| 311 | 400 | }; |