profileShare

rasmusjy / splitapp-frontend-vue

Read-only snapshot

No repository description.

main default branch 96 files Expires Sep 13, 2026, 9:06 AM
EditView.vue 7,865 bytes
1 <script setup lang="ts">
2 import { ref, computed, onMounted } from 'vue'
3 import { useRouter, useRoute } from 'vue-router'
4 import { useI18n } from 'vue-i18n'
5 import ExpenseService from '@/services/ExpenseService'
6 import BudgetCategoryService from '@/services/BudgetCategoryService'
7 import CurrencyService from '@/services/CurrencyService'
8 import TripService from '@/services/TripService'
9 import type { IBudgetCategory } from '@/types/IBudgetCategory'
10 import type { ICurrency } from '@/types/ICurrency'
11 import type { ITripParticipant } from '@/types/ITrip'
12 import type { IExpenseSplit, IExpenseSplitCreate } from '@/types/IExpense'
13 import { useToast } from '@/composables/useToast'
14 import SplitMethodSelector from '@/components/SplitMethodSelector.vue'
15
16 const router = useRouter()
17 const route = useRoute()
18 const toast = useToast()
19 const { t } = useI18n()
20
21 const tripId = route.params.tripId as string
22 const expenseId = route.params.id as string
23
24 const amount = ref<number>(0)
25 const description = ref('')
26 const expenseDate = ref('')
27 const splitMethod = ref('EqualAll')
28 const budgetCategoryId = ref('')
29 const currencyId = ref('')
30
31 const budgetCategories = ref<IBudgetCategory[]>([])
32 const currencies = ref<ICurrency[]>([])
33 const participants = ref<ITripParticipant[]>([])
34 const existingSplits = ref<IExpenseSplit[] | null>(null)
35 const splits = ref<IExpenseSplitCreate[]>([])
36 const splitsValid = ref(true)
37 const errors = ref<string[]>([])
38 const isSaving = ref(false)
39 const isLoading = ref(true)
40
41 const splitMethods = computed(() => [
42 { value: 'EqualAll', label: t('expenses.splitMethod.EqualAll'), icon: 'bi-people-fill' },
43 { value: 'EqualSubset', label: t('expenses.splitMethod.EqualSubset'), icon: 'bi-person-check-fill' },
44 { value: 'ExactAmounts', label: t('expenses.splitMethod.ExactAmounts'), icon: 'bi-hash' },
45 { value: 'Percentages', label: t('expenses.splitMethod.Percentages'), icon: 'bi-percent' },
46 ])
47
48 onMounted(async () => {
49 const [expResult, catResult, currResult, partResult] = await Promise.all([
50 ExpenseService.getById(expenseId),
51 BudgetCategoryService.getByTrip(tripId),
52 CurrencyService.getAll(),
53 TripService.getParticipants(tripId),
54 ])
55
56 if (catResult.data) budgetCategories.value = catResult.data
57 if (currResult.data) currencies.value = currResult.data
58 if (partResult.data) participants.value = partResult.data
59
60 if (expResult.data) {
61 amount.value = expResult.data.amount
62 description.value = expResult.data.description ?? ''
63 expenseDate.value = expResult.data.expenseDate.substring(0, 10)
64 splitMethod.value = expResult.data.splitMethod
65 budgetCategoryId.value = expResult.data.budgetCategoryId ?? ''
66 currencyId.value = expResult.data.currencyId ?? ''
67 existingSplits.value = expResult.data.splits ?? null
68 } else {
69 errors.value = expResult.errors ?? [t('expenses.edit.notFound')]
70 }
71
72 isLoading.value = false
73 })
74
75 async function handleSubmit() {
76 errors.value = []
77 isSaving.value = true
78
79 const result = await ExpenseService.update(expenseId, {
80 tripId,
81 amount: amount.value,
82 description: description.value || null,
83 expenseDate: new Date(expenseDate.value).toISOString(),
84 splitMethod: splitMethod.value,
85 budgetCategoryId: budgetCategoryId.value || null,
86 currencyId: currencyId.value || null,
87 splits: splits.value,
88 })
89
90 if (result.errors) {
91 errors.value = result.errors
92 } else {
93 toast.success(t('expenses.edit.updated'))
94 router.push({ name: 'ExpensesIndex', params: { tripId } })
95 }
96
97 isSaving.value = false
98 }
99
100 async function handleDelete() {
101 if (!confirm(t('expenses.edit.confirmDelete'))) return
102 const result = await ExpenseService.delete(expenseId)
103 if (result.errors) {
104 errors.value = result.errors
105 } else {
106 toast.success(t('expenses.edit.deleted'))
107 router.push({ name: 'ExpensesIndex', params: { tripId } })
108 }
109 }
110 </script>
111
112 <template>
113 <div class="row justify-content-center">
114 <div v-if="isLoading" class="text-center py-5">
115 <div class="spinner-border" style="color: var(--sa-primary)" role="status"></div>
116 </div>
117
118 <div v-else class="col-md-8 col-lg-6">
119 <h3 class="mb-4"><i class="bi bi-pencil me-2 sa-text-primary"></i>{{ t('expenses.edit.title') }}</h3>
120
121 <div v-if="errors.length" class="sa-card-static sa-card-accent sa-card-accent-danger mb-4">
122 <div class="sa-card-body">
123 <div v-for="err in errors" :key="err" style="color: var(--sa-danger); font-size: 0.9rem">{{ err }}</div>
124 </div>
125 </div>
126
127 <form @submit.prevent="handleSubmit">
128 <div class="mb-4 text-center">
129 <label for="amount" class="form-label">{{ t('expenses.create.amount') }}</label>
130 <input id="amount" v-model.number="amount" type="number" step="0.01" min="0.01" class="form-control sa-amount-input" required />
131 </div>
132
133 <div class="mb-3">
134 <label for="description" class="form-label">{{ t('expenses.create.description') }}</label>
135 <input id="description" v-model="description" type="text" class="form-control" />
136 </div>
137
138 <div class="mb-3">
139 <label for="expenseDate" class="form-label">{{ t('expenses.create.date') }}</label>
140 <input id="expenseDate" v-model="expenseDate" type="date" class="form-control" required />
141 </div>
142
143 <div class="mb-4">
144 <label class="form-label">{{ t('expenses.create.splitMethod') }}</label>
145 <div class="sa-split-methods">
146 <label v-for="method in splitMethods" :key="method.value" class="sa-split-option">
147 <input type="radio" :value="method.value" v-model="splitMethod" />
148 <div class="sa-split-option-label">
149 <div class="sa-split-option-icon"><i :class="['bi', method.icon]"></i></div>
150 <div class="sa-split-option-text">{{ method.label }}</div>
151 </div>
152 </label>
153 </div>
154
155 <SplitMethodSelector
156 v-if="participants.length > 0"
157 :participants="participants"
158 :total-amount="amount"
159 :split-method="splitMethod"
160 :existing-splits="existingSplits"
161 @update:splits="splits = $event"
162 @update:valid="splitsValid = $event"
163 />
164 </div>
165
166 <div class="mb-3">
167 <label for="budgetCategory" class="form-label">{{ t('expenses.create.budgetCategory') }}</label>
168 <select id="budgetCategory" v-model="budgetCategoryId" class="form-select">
169 <option value="">{{ t('common.none') }}</option>
170 <option v-for="cat in budgetCategories" :key="cat.id" :value="cat.id">{{ cat.name }}</option>
171 </select>
172 </div>
173
174 <div class="mb-4">
175 <label for="currency" class="form-label">{{ t('expenses.create.currency') }}</label>
176 <select id="currency" v-model="currencyId" class="form-select">
177 <option value="">{{ t('common.default') }}</option>
178 <option v-for="c in currencies" :key="c.id" :value="c.id">{{ c.code }} — {{ c.name }} ({{ c.symbol }})</option>
179 </select>
180 </div>
181
182 <div class="d-flex gap-2">
183 <button type="button" class="sa-btn sa-btn-danger sa-btn-sm" @click="handleDelete">
184 <i class="bi bi-trash3"></i> {{ t('common.delete') }}
185 </button>
186 <div class="flex-grow-1"></div>
187 <button type="button" class="sa-btn sa-btn-ghost" @click="router.push({ name: 'ExpensesIndex', params: { tripId } })">
188 {{ t('common.cancel') }}
189 </button>
190 <button type="submit" class="sa-btn sa-btn-primary" :class="{ 'sa-btn-loading': isSaving }" :disabled="isSaving || !splitsValid">
191 <i class="bi bi-check-lg"></i> {{ t('common.saveChanges') }}
192 </button>
193 </div>
194 </form>
195 </div>
196 </div>
197 </template>
198