profileShare

rasmusjy / splitapp-frontend-vue

Read-only snapshot

No repository description.

main default branch 96 files Expires Sep 13, 2026, 9:06 AM
CreateView.vue 7,308 bytes
1 <script setup lang="ts">
2 import { ref, computed, onMounted } from 'vue'
3 import { useRouter } from 'vue-router'
4 import { useI18n } from 'vue-i18n'
5 import TripService from '@/services/TripService'
6 import CurrencyService from '@/services/CurrencyService'
7 import type { ICurrency } from '@/types/ICurrency'
8
9 const router = useRouter()
10 const { t } = useI18n()
11
12 const name = ref('')
13 const description = ref('')
14 const destination = ref('')
15 const startDate = ref('')
16 const endDate = ref('')
17 const defaultCurrencyId = ref('')
18 const currencies = ref<ICurrency[]>([])
19 const errors = ref<string[]>([])
20 const isSaving = ref(false)
21
22 // Inline validation: end date must be on or after start date.
23 // The ISO yyyy-MM-dd format from <input type="date"> compares lexicographically.
24 const dateError = computed<string | null>(() => {
25 if (!startDate.value || !endDate.value) return null
26 return endDate.value < startDate.value
27 ? t('trips.create.dateError')
28 : null
29 })
30
31 const tripNights = computed<number | null>(() => {
32 if (!startDate.value || !endDate.value || dateError.value) return null
33 const ms = new Date(endDate.value).getTime() - new Date(startDate.value).getTime()
34 return Math.max(0, Math.round(ms / 86_400_000))
35 })
36
37 onMounted(async () => {
38 const result = await CurrencyService.getAll()
39 if (result.data) {
40 currencies.value = result.data
41 const eur = result.data.find((c) => c.code === 'EUR')
42 if (eur) defaultCurrencyId.value = eur.id
43 }
44 })
45
46 async function handleSubmit() {
47 if (dateError.value) {
48 errors.value = [dateError.value]
49 return
50 }
51 errors.value = []
52 isSaving.value = true
53
54 const result = await TripService.create({
55 name: name.value,
56 description: description.value || null,
57 destination: destination.value || null,
58 startDate: startDate.value ? new Date(startDate.value).toISOString() : null,
59 endDate: endDate.value ? new Date(endDate.value).toISOString() : null,
60 defaultCurrencyId: defaultCurrencyId.value,
61 })
62
63 if (result.errors) {
64 errors.value = result.errors
65 } else {
66 router.push({ name: 'TripsIndex' })
67 }
68
69 isSaving.value = false
70 }
71 </script>
72
73 <template>
74 <div class="row justify-content-center">
75 <div class="col-lg-8 col-xl-6">
76 <!-- Gradient hero -->
77 <div class="sa-gradient-header">
78 <h2 class="mb-1"><i class="bi bi-suitcase-lg me-2"></i>{{ t('trips.create.title') }}</h2>
79 <p class="mb-0 text-muted" style="font-size: 0.9rem">
80 {{ t('trips.create.subtitle') }}
81 </p>
82 </div>
83
84 <div v-if="errors.length" class="alert alert-danger">
85 <div v-for="error in errors" :key="error">{{ error }}</div>
86 </div>
87
88 <!-- Form card -->
89 <div class="sa-card">
90 <div class="sa-card-body">
91 <form @submit.prevent="handleSubmit">
92 <div class="mb-3">
93 <label for="name" class="form-label">
94 <i class="bi bi-pencil-square me-1"></i> {{ t('trips.create.name') }} *
95 </label>
96 <input
97 id="name"
98 v-model="name"
99 type="text"
100 class="form-control"
101 :placeholder="t('trips.create.namePlaceholder')"
102 required
103 />
104 </div>
105
106 <div class="mb-3">
107 <label for="destination" class="form-label">
108 <i class="bi bi-geo-alt me-1"></i> {{ t('trips.create.destination') }}
109 </label>
110 <input
111 id="destination"
112 v-model="destination"
113 type="text"
114 class="form-control"
115 :placeholder="t('trips.create.destinationPlaceholder')"
116 />
117 </div>
118
119 <div class="mb-3">
120 <label for="description" class="form-label">
121 <i class="bi bi-card-text me-1"></i> {{ t('trips.create.description') }}
122 </label>
123 <textarea
124 id="description"
125 v-model="description"
126 class="form-control"
127 rows="3"
128 :placeholder="t('trips.create.descriptionPlaceholder')"
129 ></textarea>
130 </div>
131
132 <div class="row g-3 mb-1">
133 <div class="col-6">
134 <label for="startDate" class="form-label">
135 <i class="bi bi-calendar-event me-1"></i> {{ t('trips.create.start') }}
136 </label>
137 <input
138 id="startDate"
139 v-model="startDate"
140 type="date"
141 class="form-control"
142 />
143 </div>
144 <div class="col-6">
145 <label for="endDate" class="form-label">
146 <i class="bi bi-calendar-check me-1"></i> {{ t('trips.create.end') }}
147 </label>
148 <input
149 id="endDate"
150 v-model="endDate"
151 type="date"
152 class="form-control"
153 :class="{ 'is-invalid': dateError }"
154 :min="startDate || undefined"
155 />
156 <div v-if="dateError" class="invalid-feedback">{{ dateError }}</div>
157 </div>
158 </div>
159 <div class="mb-3" style="min-height: 1.25rem">
160 <small v-if="tripNights !== null" class="text-muted">
161 <i class="bi bi-moon-stars me-1"></i>
162 {{ t('trips.create.nightCount', { n: tripNights }, tripNights) }}
163 </small>
164 </div>
165
166 <div class="mb-4">
167 <label for="currency" class="form-label">
168 <i class="bi bi-currency-exchange me-1"></i> {{ t('trips.create.defaultCurrency') }}
169 </label>
170 <select
171 id="currency"
172 v-model="defaultCurrencyId"
173 class="form-select"
174 required
175 >
176 <option value="" disabled>{{ t('trips.create.selectCurrency') }}</option>
177 <option v-for="c in currencies" :key="c.id" :value="c.id">
178 {{ c.code }} — {{ c.name }} ({{ c.symbol }})
179 </option>
180 </select>
181 </div>
182
183 <div class="d-flex justify-content-end gap-2">
184 <button
185 type="button"
186 class="sa-btn sa-btn-ghost sa-btn-pill"
187 @click="router.push({ name: 'TripsIndex' })"
188 >
189 {{ t('common.cancel') }}
190 </button>
191 <button
192 type="submit"
193 class="sa-btn sa-btn-primary sa-btn-pill"
194 :disabled="isSaving || !!dateError"
195 >
196 <span v-if="isSaving">
197 <span
198 class="spinner-border spinner-border-sm me-1"
199 role="status"
200 aria-hidden="true"
201 ></span>
202 {{ t('trips.create.creating') }}
203 </span>
204 <span v-else>
205 <i class="bi bi-check-lg me-1"></i> {{ t('trips.create.createTrip') }}
206 </span>
207 </button>
208 </div>
209 </form>
210 </div>
211 </div>
212 </div>
213 </div>
214 </template>
215