profileShare

rasmusjy / splitapp-frontend-vue

Read-only snapshot

No repository description.

main default branch 96 files Expires Sep 13, 2026, 9:06 AM
README.md 16,565 bytes
1 # SplitApp — Vue 3 frontend
2 https://travel.rasmusj.com/
3
4 Frontend (separate client app) for the **SplitApp** travel planning and shared expense management system. Built with **Vue 3 + TypeScript + Vite**, talks to the ASP.NET Core backend over REST with JWT + refresh token authentication.
5
6 This repo satisfies the "Separate client app" requirements of the TalTech Personal Project assignment — see [Assignment compliance](#assignment-compliance) below.
7
8 ## Features
9
10 - **Authentication** — register, login, logout, JWT + refresh token flow with auto-refresh on 401
11 - **Trips** — create, view, edit, delete trips with destination, dates, currency (`views/trips`)
12 - **Members & invitations** — invite travel companions by token, accept/decline/revoke (`views/members`, `views/invitations`)
13 - **Expenses** — record shared costs with four split methods (see below), select any trip member as the payer (not just the logged-in user), and see totals summed in the trip's default currency even when individual expenses were entered in other currencies (`views/expenses`, `components/SplitMethodSelector.vue`)
14 - **Budget categories** — categorize expenses, track spent vs planned (`views/budget-categories`)
15 - **Wishlist** — trip activities and destinations with group voting and completion tracking (`views/wishlist`)
16 - **Polls** — group decision-making with single/multi vote and close-poll workflow (`views/polls`)
17 - **Settlements** — real-time balance tracking with suggested payments preview; organizer finalizes the trip (`Active → Finalizing`) to lock in the settlement plan, then participants complete the mark-paid / confirm-paid workflow — trip auto-advances to `Settled` once every recipient has confirmed. The **Mark Paid** button appears only for the payer of a row and **Confirm** only for the payee; other viewers see an "Awaiting Confirmation" / "Pending" badge. **Reopen** is available to the organizer while the plan is not yet `Completed` (`views/settlements`)
18 - **Multi-currency support** (`services/CurrencyService.ts`)
19
20 ### Expense split methods
21
22 The `SplitMethodSelector` component supports four validated split strategies, matching the backend's `ESplitMethod` enum:
23
24 | Method | Behavior |
25 | -------------- | ------------------------------------------------------------------ |
26 | `EqualAll` | Total divided equally among every trip member |
27 | `EqualSubset` | User picks a subset of members; total divided equally among picked |
28 | `ExactAmounts` | Per-member fixed amount input; must sum exactly to total |
29 | `Percentages` | Per-member percentage input; must sum to exactly 100% |
30
31 Each method is validated live and blocks the save button when invalid.
32
33 ## Tech stack
34
35 - **Vue 3.5** (Composition API) + **TypeScript 6**
36 - **Vite 8** (dev server + production build)
37 - **Vue Router 5** — nested routes with an auth navigation guard
38 - **Pinia 3** — auth store (JWT, refresh token, user name) with localStorage sync
39 - **Axios** — shared `httpClient.ts` with request/response interceptors
40 - **Bootstrap 5** + custom coral/teal CSS variable theme
41 - **Vitest 4** + `@vue/test-utils` + **MSW** — unit & integration tests
42 - **Playwright** — browser-based end-to-end tests
43 - **ESLint** (flat config) + **Oxlint** + **Prettier**
44
45 ## Project structure
46
47 ```
48 src/
49 ├── components/ # SplitMethodSelector, ToastContainer
50 ├── composables/ # useToast — global toast system
51 ├── directives/ # v-animate — IntersectionObserver scroll animations
52 ├── router/ # vue-router config + auth guard
53 ├── services/ # 9 axios-based API clients, one per resource
54 │ ├── httpClient.ts # shared axios instance with JWT interceptors
55 │ ├── AccountService.ts
56 │ ├── TripService.ts
57 │ ├── ExpenseService.ts
58 │ ├── BudgetCategoryService.ts
59 │ ├── CurrencyService.ts
60 │ ├── PollService.ts
61 │ ├── WishlistService.ts
62 │ ├── SettlementService.ts
63 │ └── InvitationService.ts
64 ├── stores/
65 │ └── auth.ts # jwt, refreshToken, userName, isAuthenticated
66 ├── types/ # TypeScript contracts mirroring backend DTOs
67 ├── utils/ # formatCurrency, parseJwt
68 ├── views/ # Pages (HomeView, LoginView, RegisterView, trips/, expenses/, ...)
69 ├── App.vue # Root layout (navbar + router-view + toasts)
70 └── main.ts
71 e2e/ # Playwright tests
72 src/__tests__/
73 ├── unit/ # Vitest unit tests
74 ├── integration/ # Vitest + MSW integration tests
75 └── vitest.setup.ts
76 ```
77
78 ## Requirements
79
80 - **Node.js** `^20.19.0 || >=22.12.0`
81 - **npm**
82 - A running instance of the SplitApp backend (`splitapp-backend-clean-onion`) on the URL configured in `.env`
83
84 ## Setup
85
86 1. Install dependencies:
87
88 ```sh
89 npm install
90 ```
91
92 2. Create a `.env` file at the project root:
93
94 ```env
95 VITE_API_BASE_URL=http://localhost:90/api/v1/
96 ```
97
98 Use `http://localhost:5086/api/v1/` if you run the backend locally via `dotnet run` instead of Docker.
99
100 3. Start the backend (separate repo):
101
102 ```sh
103 cd ../splitapp-backend-clean-onion
104 docker compose up -d
105 ```
106
107 4. Run the frontend:
108 ```sh
109 npm run dev
110 ```
111 → http://localhost:5173
112
113 ## Scripts
114
115 ```sh
116 npm run dev # Vite dev server (http://localhost:5173)
117 npm run build # Type-check + production build
118 npm run preview # Preview the production bundle
119 npm run test:unit # Vitest unit + integration tests (watch mode)
120 npm run test:unit -- --run # One-off CI-style run
121 npm run test:e2e # Playwright end-to-end tests (needs backend running)
122 npm run test:e2e:ui # Playwright interactive UI mode
123 npm run lint # Oxlint + ESLint with --fix
124 npm run type-check # vue-tsc strict type check
125 npm run format # Prettier
126 ```
127
128 ## Testing
129
130 The project has three testing layers — **39 tests in 7 files**, all green against the phase 3 modular monolith backend.
131
132 | Layer | Files | Tests | Backend? | Runtime |
133 |---|---|---|---|---|
134 | Unit | 4 | 31 | No (pure functions / isolated component) | ~0.4s |
135 | Integration | 1 | 4 | No (MSW mocks the network at the HTTP layer) | ~0.5s |
136 | E2E | 2 | 4 | **Yes** (real Chromium, real backend) | ~11s |
137
138 ### Unit (`npm run test:unit`)
139
140 Vitest + jsdom. Each file targets one tightly-scoped piece of the codebase:
141
142 | File | What it covers | Why it matters |
143 |---|---|---|
144 | `formatCurrency.spec.ts` | sign / symbol / decimals / thousands separators | every page renders amounts — a regression here visually breaks the whole app |
145 | `parseJwt.spec.ts` | base64url decoding (`+/` → `-_`), ASP.NET `nameidentifier` claim with `sub` fallback | wrong user-id parsing silently mis-attributes expenses to the wrong user |
146 | `auth-store.spec.ts` | Pinia store: `localStorage` hydration, `isAuthenticated` reactivity, `logout()`, watcher-based sync | broken sync drifts store and `localStorage` apart → user sees stale identity after reload |
147 | `SplitMethodSelector.spec.ts` | all four split methods, live validation, edit-mode `existingSplits` pre-load | most complex business logic on the front — invalid splits would let the user save a malformed expense |
148
149 ### Integration (`npm run test:unit`, in the same suite)
150
151 | File | What it covers |
152 |---|---|
153 | `token-refresh.spec.ts` | the **most security-sensitive glue** in the app — `httpClient`'s 401 → refresh → retry pipeline, end to end. MSW intercepts at the network layer so axios behaves exactly as in production. Three scenarios: (1) successful refresh + replay, (2) refresh fails → logout + redirect to `/login`, (3) no refresh token present → straight to logout. |
154
155 ### End-to-end (`npm run test:e2e`)
156
157 Playwright drives a real Chromium browser. The `webServer` config auto-starts `npm run dev` for you, so you only need the backend reachable.
158
159 | File | What it covers |
160 |---|---|
161 | `auth.spec.ts` | login as seed user `alice@taltech.ee`, logout, wrong-password stays on `/login`, protected routes redirect anonymous users to `/login` |
162 | `trip-crud.spec.ts` | **positive happy flow** — login → create a uniquely-named trip → see it in the list → open Edit → rename → verify the rename persists. Delete is intentionally skipped because backend `DeleteBehavior.Restrict` blocks deleting a trip that has any participant (even the auto-added Organizer) — that's a backend issue, not a frontend one. |
163
164 **How to run locally:**
165
166 E2E hits the backend via whatever URL is in `.env`:
167
168 ```sh
169 # Option A — against deployed phase 3 backend (no local docker needed)
170 # .env already points at https://travel.rasmusj.com/api/v1/
171 npm run test:e2e
172
173 # Option B — against a local backend
174 cd ../splitapp-backend-clean-onion && docker compose up -d # backend on :90
175 # Override .env locally with VITE_API_BASE_URL=http://localhost:90/api/v1/
176 cd ../splitapp-frontend-vue
177 npm run test:e2e # headless
178 npm run test:e2e:ui # interactive UI mode for debugging
179 ```
180
181 > **Note:** the `trip-crud` test creates a real trip in whichever backend you point at. Against prod, that's a real row in the deployed database (it does not clean up).
182
183 ## Docker
184
185 The project ships with a multi-stage `Dockerfile` (Node build → Nginx serve) and a `docker-compose.yml` with two profiles.
186
187 ### Production (Nginx)
188
189 ```sh
190 docker compose up --build
191 ```
192
193 → http://localhost:91
194
195 The backend URL is injected as a **build arg** (`VITE_API_BASE_URL`) so the same image can target different environments:
196
197 ```sh
198 # Override at build time for a real deploy
199 VITE_API_BASE_URL=https://api.mydomain.com/api/v1/ docker compose up --build
200 ```
201
202 Nginx is configured with an SPA fallback (`try_files $uri $uri/ /index.html`) so Vue Router's history mode works on direct URLs.
203
204 ### Port allocation
205
206 | Service | Host port | Notes |
207 | -------------------------- | ----------- | --------------------- |
208 | Frontend (prod, Nginx) | **91** | separate from backend |
209 | Backend API (Docker) | 90 | `rasmju-csweb-phase3` |
210 | Backend API (`dotnet run`) | 5086 / 7040 | http / https |
211 | PostgreSQL | 5432 | |
212
213 ## CI/CD
214
215 Configured via [.gitlab-ci.yml](.gitlab-ci.yml):
216
217 - **deploy** stage — on merges to `main`, runs `docker compose -p rasmju-js-a7 up --build --remove-orphans --detach` on a shared runner (deploys the Nginx production image)
218 - **Tests run locally** before pushing — there is intentionally no test stage in the pipeline. Run `npm run test:unit -- --run` and `npm run test:e2e` on your machine before merging.
219
220 The frontend is hosted on a separate URL from the backend and relies on the backend's permissive CORS policy (`AllowAnyOrigin` in `Program.cs`).
221
222 ## Architecture notes
223
224 ### Authentication flow
225
226 1. `LoginView` → `AccountService.loginAsync` → backend returns `{ jwt, refreshToken, firstName, lastName }`
227 2. Auth store saves them; `watch`ers mirror values to `localStorage`
228 3. Every outgoing request picks up `Authorization: Bearer <jwt>` from the request interceptor
229 4. On `401`, the response interceptor:
230 - calls `AccountService.refreshTokenAsync(jwt, refreshToken)`
231 - on success: updates the store, replays the original request transparently
232 - on failure: calls `logoutAsync`, clears the store, navigates to `/login`
233 5. `logout()` tears down store state and removes localStorage entries
234
235 ### Nested routing
236
237 `/trips/:tripId` acts as a parent route whose `DetailView` provides `isOrganizer`, `currentUserId`, `tripStatus`, and `tripCurrencySymbol` to child routes via Vue's `provide/inject`. Children use these to:
238
239 - Hide organizer-only actions (budget category CRUD, finalize/reopen trip)
240 - Hide creator-only actions (edit/delete own wishlist items and expenses); the edit/delete slot in the expense list reserves layout space even when hidden so amounts stay aligned in a single column
241 - Lock expense CRUD whenever trip status is not `Active` (i.e. `Finalizing`, `Settled`, or `Archived`)
242 - Show suggested payment previews while trip is `Active`, and the real settlement plan once trip enters `Finalizing` or `Settled`; the header badge distinguishes the two states (warning hourglass vs. lock icon)
243 - Hide payment action buttons from users who cannot act on a row — Mark Paid only for the payer, Confirm only for the payee (matching the BLL guards so the UI never surfaces a click that would silently 403)
244
245 ### Currency conversion
246
247 Expenses can be recorded in any currency (EUR, USD, GBP, SEK, NOK). To keep conversion rules in a single place, the frontend does **not** implement any currency math — it relies on the backend. The `ExpenseDto` returned by `GET /api/v1/expenses/trip/{tripId}` includes an `amountInTripCurrency` field that the backend pre-computes (`SplitApp.Modules.Expenses.Application.CurrencyConverter` in the modular monolith). List totals (`expenses/IndexView.vue`, trip dashboard) sum this field with `e.amount` as a fallback. Per-expense rows still display the original amount + currency symbol so the user sees what was actually entered.
248
249 ### Error handling
250
251 Services return an `IResultObject<T>` wrapper (`{ data } | { errors: string[] }`) instead of throwing. Views inspect `.errors` and show toast messages via the `useToast` composable.
252
253 ## Assignment compliance
254
255 This repository targets the **"Separate client app"** portion of the TalTech Personal Project Assignment 3.
256
257 | Requirement | Status | Where |
258 | ----------------------------------------------------------- | ---------------- | ------------------------------------------------------------------------ |
259 | Written in a chosen technology (react/angular/vue/blazor/…) | ✅ | Vue 3 + TypeScript |
260 | Uses own backend REST API | ✅ | `VITE_API_BASE_URL=http://localhost:90/api/v1/` |
261 | JWT + refresh token authentication | ✅ | `services/httpClient.ts`, `services/AccountService.ts`, `stores/auth.ts` |
262 | Login / logout | ✅ | `LoginView.vue`, `RegisterView.vue`, `App.vue` |
263 | CRUD on ≥3 entities | ✅ (5) | Trips, Expenses, BudgetCategories, Wishlist, Polls |
264 | CI/CD deploy — client hosted on a separate URL from backend | ✅ | `.gitlab-ci.yml`, separate Docker container on port 91 |
265 | CORS handling | ✅ | Backend `CorsAllowAll` policy; frontend on separate origin |
266 | Bonus: full unit + integration + e2e test coverage | ✅ | Vitest + MSW + Playwright — 39 tests, all green against phase 3 backend |
267
268 ### Seed users
269
270 The backend seeds five demo users on first startup (if `DataInitialization:SeedIdentity=true`). Password for all of them is `Kala.12345`, which is in the source on purpose: this is a demo and the data is invented.
271
272 The administrator is separate. It is seeded only when `SEED_ADMIN_PASSWORD` is set on the backend, and there is no default, so without that variable there is no admin account at all.
273
274 | Email | Role |
275 | -------------------- | ----- |
276 | `user@taltech.ee` | user |
277 | `alice@taltech.ee` | user |
278 | `bob@taltech.ee` | user |
279 | `charlie@taltech.ee` | user |
280 | `diana@taltech.ee` | user |
281
282 Four example trips are also seeded (Barcelona Weekend, London Business Trip, Summer Cabin Getaway, NYC Adventure), each with expenses, polls and wishlist items.
283
284 ## Related documentation
285
286 - [`YLEVAADE.md`](YLEVAADE.md) — comprehensive system overview in Estonian, covering both frontend and backend
287 - `splitapp-backend-clean-onion` — backend repository (ASP.NET Core 10 + PostgreSQL)
288
289 ## Recommended IDE
290
291 [VS Code](https://code.visualstudio.com/) + [Vue (Official)](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (disable Vetur).
292