@@ -231,6 +233,7 @@ import { useUserStore } from '@/scripts/admin/stores/user'
import abilities from '@/scripts/admin/stub/abilities'
import UFOIcon from '@/scripts/components/icons/empty/UFOIcon.vue'
+import DuplicateExpenseModal from '@/scripts/admin/components/modal-components/DuplicateExpenseModal.vue'
import ExpenseDropdown from '@/scripts/admin/components/dropdowns/ExpenseIndexDropdown.vue'
const companyStore = useCompanyStore()
diff --git a/resources/scripts/components/base/BaseModal.vue b/resources/scripts/components/base/BaseModal.vue
index 2d0c85d2..3c2ade36 100644
--- a/resources/scripts/components/base/BaseModal.vue
+++ b/resources/scripts/components/base/BaseModal.vue
@@ -6,6 +6,7 @@
static
class="fixed inset-0 z-20 overflow-y-auto"
:open="show"
+ :initial-focus="initialFocus"
@close="$emit('close')"
>
group(function () {
Route::post('/expenses/delete', [ExpensesController::class, 'delete']);
+ Route::post('/expenses/{expense}/duplicate', DuplicateExpenseController::class);
+
Route::apiResource('expenses', ExpensesController::class);
Route::apiResource('categories', ExpenseCategoriesController::class);
diff --git a/tests/Feature/Admin/ExpenseTest.php b/tests/Feature/Admin/ExpenseTest.php
index 4267a153..e4c10528 100644
--- a/tests/Feature/Admin/ExpenseTest.php
+++ b/tests/Feature/Admin/ExpenseTest.php
@@ -1,6 +1,8 @@
assertOk();
});
+test('duplicate expense', function () {
+ $expense = Expense::factory()->create([
+ 'expense_date' => '2019-02-05',
+ 'notes' => 'Monthly rent',
+ ]);
+
+ $response = postJson("api/v1/expenses/{$expense->id}/duplicate", [
+ 'expense_date' => '2019-02-05',
+ ]);
+
+ $response->assertStatus(201);
+
+ $newId = $response->json('data.id');
+
+ expect($newId)->not->toBe($expense->id);
+
+ $this->assertDatabaseHas('expenses', [
+ 'id' => $newId,
+ 'expense_date' => '2019-02-05',
+ 'notes' => 'Monthly rent (copy)',
+ 'expense_category_id' => $expense->expense_category_id,
+ 'amount' => $expense->amount,
+ ]);
+});
+
+test('duplicate expense with empty note uses copy as note', function () {
+ $expense = Expense::factory()->create([
+ 'expense_date' => '2019-02-05',
+ 'notes' => null,
+ ]);
+
+ postJson("api/v1/expenses/{$expense->id}/duplicate", [
+ 'expense_date' => '2019-02-05',
+ ])
+ ->assertStatus(201)
+ ->assertJsonPath('data.notes', '(copy)');
+});
+
+test('duplicate expense uses submitted expense date', function () {
+ $expense = Expense::factory()->create([
+ 'expense_date' => '2019-02-05',
+ ]);
+
+ $response = postJson("api/v1/expenses/{$expense->id}/duplicate", [
+ 'expense_date' => '2024-03-10',
+ ]);
+
+ $response->assertStatus(201);
+
+ $this->assertDatabaseHas('expenses', [
+ 'id' => $response->json('data.id'),
+ 'expense_date' => '2024-03-10',
+ ]);
+});
+
+test('duplicate expense requires expense date', function () {
+ $expense = Expense::factory()->create([
+ 'expense_date' => '2019-02-05',
+ ]);
+
+ postJson("api/v1/expenses/{$expense->id}/duplicate", [])
+ ->assertUnprocessable()
+ ->assertJsonValidationErrors(['expense_date']);
+});
+
+test('duplicate validates using a form request', function () {
+ $this->assertActionUsesFormRequest(
+ DuplicateExpenseController::class,
+ '__invoke',
+ DuplicateExpenseRequest::class
+ );
+});
+
test('delete multiple expenses', function () {
$expenses = Expense::factory()->count(3)->create([
'expense_date' => '2019-02-05',