--- title: "PWA z JavaScript" date: "2025-05-23" author: "Igor Barcik 131780" subtitle: "Projekt" papersize: a4 geometry: margin=2cm header-includes: - \usepackage{float} - \usepackage{fontspec} - \usepackage{unicode-math} - \usepackage{xcolor} - \usepackage{minted} - \usepackage{mdframed} - \floatplacement{figure}{H} - \setmonofont{FiraCode Nerd Font Mono} # Set the font for code blocks - \definecolor{light-gray}{gray}{0.95} # Define a custom color - \BeforeBeginEnvironment{Highlighting}{\begin{mdframed}[backgroundcolor=light-gray,linewidth=0pt]} # Wrap Highlighting environments in a mdframed environment 1/2 - \AfterEndEnvironment{Highlighting}{\end{mdframed}} # Wrap Highlighting environments in a mdframed environment 2/2 - \DefineVerbatimEnvironment{Highlighting}{Verbatim}{breaklines, commandchars=\\\{\}, breakanywhere=true, fontsize=\small, breakafter=d, breakaftersymbolpre=, breakaftersymbolpost=} # Define the Verbatim environment for code blocks --- \newpage # Budget Manager PWA - Raport projektu ## 1. Wprowadzenie Aplikacja "Budget Manager PWA" *([link do git](https://gitea.bigoscloud.com/biggy/budget-manager))* to progresywna aplikacja internetowa (PWA) służąca do zarządzania budżetem osobistym. Umożliwia śledzenie wydatków i przychodów, kategoryzowanie transakcji, generowanie raportów oraz ustawianie powiadomień związanych z budżetem. Aplikacja działa zarówno online, jak i offline, dzięki wykorzystaniu nowoczesnych technologii webowych. ### 1.1 Cel projektu Celem projektu było stworzenie aplikacji PWA, która: - Umożliwia śledzenie wydatków i przychodów użytkownika - Działa zarówno online, jak i offline - Przechowuje dane w Local Storage oraz MongoDB - Oferuje funkcje budżetowania, raportowania i analizy finansowej ### 1.2 Wykorzystane technologie Frontend: - Vue 3 z Composition API - TypeScript - Tailwind CSS - Vite (bundler) - Pinia (zarządzanie stanem) - Vue Router Backend: - Koa.js - TypeScript - MongoDB PWA: - Service Worker - Web App Manifest - IndexedDB (poprzez abstrakcję) ## 2. Architektura aplikacji ### 2.1 Struktura projektu Projekt jest podzielony na dwie główne części: #### Frontend ```bash frontend/ ├── public/ # Statyczne pliki i manifest PWA │ ├── manifest.json # Konfiguracja PWA │ ├── sw.js # Service Worker │ └── icons/ # Ikony dla PWA ├── src/ │ ├── components/ # Komponenty Vue │ ├── pages/ # Widoki aplikacji │ ├── stores/ # Pinia stores │ ├── lib/ # Funkcje pomocnicze │ │ └── pwa-utils.ts # Narzędzia związane z PWA │ └── main.ts # Główny plik aplikacji └── styles/ # Style CSS ``` #### Backend ```bash backend/ ├── src/ │ ├── app.ts # Główny plik aplikacji │ ├── controllers/ # Kontrolery REST API │ ├── middleware/ # Middleware (np. autoryzacja) │ └── services/ # Serwisy (np. baza danych) └── scripts/ └── generate-vapid-keys.js # Skrypt do generowania kluczy VAPID ``` ### 2.2 Przepływ danych 1. Dane użytkownika są przechowywane lokalnie w Local Storage dla dostępu offline 2. Przy połączeniu z internetem dane są synchronizowane z bazą MongoDB na serwerze 3. Service Worker zapisuje dane do cache, umożliwiając działanie aplikacji offline 4. Background Sync API jest używane do synchronizacji danych, gdy połączenie internetowe zostanie przywrócone ## 3. Implementacja PWA ### 3.1 Manifest aplikacji Manifest definiuje jak aplikacja ma się zachowywać po zainstalowaniu na urządzeniu użytkownika: ```json { "name": "Budget Manager PWA", "short_name": "Budget", "description": "Budget Manager application for tracking expenses and income", "start_url": "/", "display": "standalone", "background_color": "#ffffff", "theme_color": "#3b82f6", "icons": [ { "src": "/icons/icon-192px.jpg", "sizes": "192x192", "type": "image/png", "purpose": "any maskable" }, { "src": "/icons/icon-512px.jpg", "sizes": "512x512", "type": "image/png", "purpose": "any maskable" } ], "shortcuts": [ { "name": "Add Transaction", "short_name": "Add", "description": "Add a new transaction", "url": "/add", "icons": [{ "src": "/icons/add-icon-96x96.png", "sizes": "96x96" }] }, { "name": "View Reports", "short_name": "Reports", "description": "View budget reports", "url": "/reports", "icons": [{ "src": "/icons/report-icon-96x96.png", "sizes": "96x96" }] } ] } ``` ### 3.2 Service Worker Service Worker obsługuje: - Cache'owanie zasobów dla dostępu offline - Synchronizację w tle (background sync) Kluczowe fragmenty implementacji: ```javascript // Instalacja Service Worker'a self.addEventListener("install", (event) => { console.log("Service worker install event"); event.waitUntil( caches .open(CACHE_NAME) .then((cache) => { return cache.addAll(precacheResources); }) .then(() => { return self.skipWaiting(); }), ); }); // Strategia fetch: cache-first, następnie sieć self.addEventListener("fetch", (event) => { // Pomijanie żądań cross-origin i API if ( !event.request.url.startsWith(self.location.origin) || event.request.url.includes("/api/") ) { return; } event.respondWith( caches.match(event.request).then((cachedResponse) => { if (cachedResponse) { return cachedResponse; } return fetch(event.request) .then((response) => { // Cache'owanie odpowiedzi if ( response && response.status === 200 && response.type === "basic" ) { const responseToCache = response.clone(); caches.open(CACHE_NAME).then((cache) => { cache.put(event.request, responseToCache); }); } return response; }) .catch(() => { // Fallback w przypadku braku sieci if (event.request.mode === "navigate") { return caches.match("/"); } return null; }); }), ); }); // Obsługa background sync self.addEventListener("sync", (event) => { if (event.tag === "sync-transactions") { event.waitUntil(syncTransactions()); } else if (event.tag === "sync-categories") { event.waitUntil(syncCategories()); } }); ``` ### 3.3 Rejestracja Service Worker Rejestracja Service Worker'a w aplikacji: ```typescript export async function registerServiceWorker() { if ("serviceWorker" in navigator) { try { const registration = await navigator.serviceWorker.register("/sw.js", { scope: "/", }); if (registration.installing) { console.log("Service worker installing"); } else if (registration.waiting) { console.log("Service worker installed"); } else if (registration.active) { console.log("Service worker active"); } // Upewniamy się, że service worker jest gotowy przed próbą subskrypcji await navigator.serviceWorker.ready; return registration; } catch (error) { console.error("Registration failed with error:", error); return null; } } return null; } ``` ### 3.4 Powiadomienia Push Powiadomienia Push są kluczowym elementem aplikacji PWA, umożliwiającym informowanie użytkownika o ważnych zdarzeniach, nawet gdy aplikacja nie jest aktywna w przeglądarce. W Budget Manager PWA, powiadomienia push są wykorzystywane do informowania użytkowników o nowych transakcjach dodanych do ich budżetu. #### 3.4.1 Klucze VAPID Do bezpiecznego wysyłania powiadomień push wykorzystywany jest protokół VAPID (Voluntary Application Server Identification). Klucze VAPID (publiczny i prywatny) są generowane za pomocą skryptu i przechowywane jako zmienne środowiskowe na serwerze. Skrypt `backend/scripts/generate-vapid-keys.js` generuje te klucze: ```bash # backend/scripts/generate-vapid-keys.js const webpush = require('web-push'); const fs = require('fs'); const vapidKeys = webpush.generateVAPIDKeys(); console.log('Public Key: ', vapidKeys.publicKey); console.log('Private Key: ', vapidKeys.privateKey); // Zapisz klucze do pliku .env lub użyj bezpośrednio fs.appendFileSync('.env.example', `\nVAPID_PUBLIC_KEY=${vapidKeys.publicKey}`); fs.appendFileSync('.env.example', `VAPID_PRIVATE_KEY=${vapidKeys.privateKey}\n`); console.log('VAPID keys appended to .env.example'); ``` Klucz publiczny VAPID jest udostępniany frontendowi, aby mógł zasubskrybować usługę push. Klucze są ładowane na serwerze przy starcie aplikacji. #### 3.4.2 Implementacja Frontend **Subskrypcja powiadomień:** Logika subskrypcji znajduje się w `frontend/src/lib/pwa-utils.ts`. Użytkownik może włączyć lub wyłączyć powiadomienia na stronie ustawień (`frontend/src/pages/Settings.vue`). Proces obejmuje: 1. Sprawdzenie wsparcia dla Service Worker i Push API. 2. Poproszenie użytkownika o zgodę na otrzymywanie powiadomień. 3. Pobranie klucza publicznego VAPID z backendu (`/api/push/vapid-public-key`). 4. Subskrypcja usługi push za pomocą `registration.pushManager.subscribe()`. 5. Wysłanie obiektu subskrypcji na backend (`/api/push/subscribe`) w celu zapisania. Fragment kodu z `pwa-utils.ts` odpowiedzialny za subskrypcję: ```typescript // frontend/src/lib/pwa-utils.ts // ... (uproszczony przykład) async function subscribeToPushNotifications(registration: ServiceWorkerRegistration) { try { const vapidPublicKeyResponse = await fetch('/api/push/vapid-public-key', { headers: { 'Accept': 'text/plain' } }); if (!vapidPublicKeyResponse.ok) { throw new Error(`Failed to get VAPID public key: ${vapidPublicKeyResponse.statusText}`); } const vapidPublicKey = await vapidPublicKeyResponse.text(); const subscription = await registration.pushManager.subscribe({ userVisibleOnly: true, applicationServerKey: urlBase64ToUint8Array(vapidPublicKey), }); // Odczyt tokena JWT z localStorage const token = localStorage.getItem('authToken'); // Upewnij się, że klucz jest poprawny if (!token) { console.warn('No auth token found, cannot save subscription to backend.'); // Można rozważyć zapisanie subskrypcji lokalnie i wysłanie później return false; } await fetch('/api/push/subscribe', { method: 'POST', body: JSON.stringify(subscription), headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` }, }); console.log('User is subscribed.'); return true; } catch (error) { console.error('Failed to subscribe the user: ', error); return false; } } function urlBase64ToUint8Array(base64String: string): ArrayBuffer { const padding = '='.repeat((4 - base64String.length % 4) % 4); const base64 = (base64String + padding) .replace(/-/g, '+') .replace(/_/g, '/'); const rawData = window.atob(base64); const outputArray = new Uint8Array(rawData.length); for (let i = 0; i < rawData.length; ++i) { outputArray[i] = rawData.charCodeAt(i); } return outputArray.buffer; // Zwraca ArrayBuffer } ``` **Obsługa powiadomień w Service Worker (`frontend/public/sw.js`):** Service Worker nasłuchuje na zdarzenie `push` i `notificationclick`. - Po otrzymaniu zdarzenia `push`, Service Worker wyświetla powiadomienie systemowe z danymi otrzymanymi od serwera (np. tytuł, treść, ikonę). Wiadomość jest również przekazywana do aktywnych klientów aplikacji w celu wyświetlenia toastu. - Zdarzenie `notificationclick` obsługuje interakcję użytkownika z powiadomieniem, np. otwarcie aplikacji lub konkretnej strony. ```javascript // frontend/public/sw.js self.addEventListener('push', event => { let data = { title: 'Nowa wiadomość', body: 'Otrzymałeś nowe powiadomienie.', data: { url: '/' } }; if (event.data) { try { data = event.data.json(); } catch (e) { console.error('Push event data is not valid JSON:', event.data.text()); data.body = event.data.text(); // Użyj tekstu jako body jeśli JSON się nie sparsuje } } const title = data.title || 'Budget Manager'; const options = { body: data.body || 'Nowa aktywność na Twoim koncie.', icon: data.icon || '/icons/icon-192px.jpg', badge: '/icons/icon-192px.jpg', // Użyj tej samej ikony dla badge data: data.data || { url: '/' } }; event.waitUntil(self.registration.showNotification(title, options)); // Wysyłanie wiadomości do aktywnych klientów (zakładek aplikacji) self.clients.matchAll({ type: 'window', includeUncontrolled: true }).then(clients => { if (clients && clients.length) { clients.forEach(client => { client.postMessage({ type: 'PUSH_NOTIFICATION_RECEIVED', payload: data // Przekaż całe dane, w tym title, body, data.url }); }); } }); }); self.addEventListener('notificationclick', event => { event.notification.close(); const urlToOpen = event.notification.data && event.notification.data.url ? event.notification.data.url : '/'; event.waitUntil( clients.matchAll({ type: 'window', includeUncontrolled: true }).then(windowClients => { for (let i = 0; i < windowClients.length; i++) { const client = windowClients[i]; // Sprawdź, czy URL klienta (po usunięciu hasha) pasuje do urlToOpen const clientURL = new URL(client.url); const targetURL = new URL(urlToOpen, self.location.origin); // Upewnij się, że targetURL jest absolutny if (clientURL.origin + clientURL.pathname === targetURL.origin + targetURL.pathname && 'focus' in client) { return client.focus(); } } if (clients.openWindow) { return clients.openWindow(urlToOpen); } }) ); }); ``` #### 3.4.3 Implementacja Backend Backend (`backend/src/controllers/push.ts`) odpowiada za: 1. Udostępnianie publicznego klucza VAPID (`/api/push/vapid-public-key`). Ten endpoint jest publicznie dostępny. 2. Rejestrowanie subskrypcji użytkowników (`/api/push/subscribe`). Subskrypcje są przechowywane w bazie danych, powiązane z użytkownikiem. 3. Wysłanie powiadomień do zasubskrybowanych użytkowników. Na przykład, po dodaniu nowej transakcji (`backend/src/controllers/protected.ts`), serwer wysyła powiadomienie. Fragment kodu z `pushController.ts` (uproszczony): ```typescript // backend/src/controllers/push.ts import webpush from 'web-push'; import { Context } from 'koa'; import User from '../models'; // Załóżmy, że User model ma pole pushSubscriptions // Konfiguracja VAPID keys przy starcie aplikacji (np. w app.ts lub dedykowanym pliku konfiguracyjnym) // webpush.setVapidDetails( // 'mailto:your-email@example.com', // process.env.VAPID_PUBLIC_KEY!, // process.env.VAPID_PRIVATE_KEY! // ); export const getVapidPublicKey = async (ctx: Context) => { if (!process.env.VAPID_PUBLIC_KEY) { ctx.throw(500, 'VAPID public key not configured'); return; } ctx.body = process.env.VAPID_PUBLIC_KEY; ctx.set('Content-Type', 'text/plain'); }; export const subscribe = async (ctx: Context) => { const subscription = ctx.request.body as webpush.PushSubscription; const userId = ctx.state.user.id; // ID zalogowanego użytkownika try { // Zapisz subskrypcję w dokumencie użytkownika // Załóżmy, że model User ma pole pushSubscriptions: webpush.PushSubscription[] await User.findByIdAndUpdate(userId, { $addToSet: { pushSubscriptions: subscription } }); ctx.status = 201; ctx.body = { message: 'Subscribed successfully' }; } catch (error) { console.error('Error saving subscription:', error); ctx.throw(500, 'Failed to save subscription'); } }; // Funkcja do wysyłania powiadomień (może być w serwisie) export const sendPushNotification = async (userId: string, payload: object) => { try { const user = await User.findById(userId); if (!user || !user.pushSubscriptions || user.pushSubscriptions.length === 0) { console.log(`No push subscriptions found for user ${userId}`); return; } const notificationPayload = JSON.stringify(payload); for (const sub of user.pushSubscriptions) { try { await webpush.sendNotification(sub, notificationPayload); } catch (error: any) { console.error('Error sending notification to a subscription: ', error.body); // Jeśli subskrypcja jest nieaktualna (np. status 410 Gone), usuń ją if (error.statusCode === 410 || error.statusCode === 404) { await User.findByIdAndUpdate(userId, { $pull: { pushSubscriptions: sub } }); console.log('Removed outdated subscription.'); } } } } catch (error) { console.error(`Failed to send push notifications for user ${userId}:`, error); } }; ``` Integracja wysyłania powiadomień przy tworzeniu transakcji w `protectedController.ts`: ```typescript // backend/src/controllers/protected.ts // ... import { sendPushNotification } from './push'; // Zaimportuj funkcję export const addTransaction = async (ctx: Context) => { // ... logika dodawania transakcji ... // const newTransaction = ...; // Wynik dodania transakcji // const userId = ctx.state.user.id; // Po pomyślnym dodaniu transakcji: if (newTransaction && userId) { // Upewnij się, że newTransaction i userId istnieją const notificationPayload = { title: 'Nowa transakcja', body: `Dodano: ${newTransaction.description} (${newTransaction.amount} ${newTransaction.currency || 'PLN'})`, data: { url: `/transactions` } // Ogólny link do transakcji lub ID: /transactions/${newTransaction._id} }; // Wyślij asynchronicznie, aby nie blokować odpowiedzi sendPushNotification(userId, notificationPayload).catch(console.error); } // ... }; ``` Powiadomienia push znacząco zwiększają zaangażowanie użytkowników i użyteczność aplikacji Budget Manager PWA, informując ich na bieżąco o zmianach w ich budżecie. ## 4. Zarządzanie stanem aplikacji ### 4.1 Stores w Pinia Aplikacja używa Pinia do zarządzania stanem, co ułatwia komunikację między komponentami i zarządzanie danymi aplikacji. Przykładowy store dla transakcji: ```typescript export const useTransactionsStore = defineStore("transactions", () => { // Stan const transactions = ref([]); const loading = ref(false); const error = ref(""); // API URL const apiBaseUrl = "http://localhost:3000/api"; // Store autoryzacji const authStore = useAuthStore(); // Gettery const incomes = computed(() => transactions.value.filter((t) => t.type === "income"), ); const expenses = computed(() => transactions.value.filter((t) => t.type === "expense"), ); const totalIncome = computed(() => incomes.value.reduce((sum, t) => sum + t.amount, 0), ); const totalExpenses = computed(() => expenses.value.reduce((sum, t) => sum + t.amount, 0), ); const balance = computed(() => totalIncome.value - totalExpenses.value); // Akcje async function fetchTransactions() { if (!authStore.token) return; loading.value = true; error.value = ""; try { const response = await fetch(`${apiBaseUrl}/transactions`, { headers: { Authorization: `Bearer ${authStore.token}`, }, }); if (!response.ok) { throw new Error("Failed to fetch transactions"); } const data = await response.json(); transactions.value = data.data; // Zapisywanie do localStorage dla dostępu offline saveToLocalStorage(); } catch (err: any) { error.value = err.message; // Jeśli API call nie powiedzie się, próba załadowania z localStorage loadFromLocalStorage(); } finally { loading.value = false; } } // ... inne metody return { transactions, loading, error, incomes, expenses, totalIncome, totalExpenses, balance, fetchTransactions, // ... inne eksportowane metody }; }); ``` ### 4.2 Synchronizacja offline/online Aplikacja wykorzystuje strategię "offline-first", gdzie dane są najpierw zapisywane lokalnie, a następnie synchronizowane z serwerem gdy połączenie jest dostępne: ```typescript // Dodawanie transakcji async function addTransaction(transaction: Omit) { try { error.value = ""; // Generujemy tymczasowe ID dla transakcji const tempId = `temp_${Date.now()}`; const newTransaction = { ...transaction, _id: tempId, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), synced: false }; // Dodajemy do listy lokalnie transactions.value.push(newTransaction); saveToLocalStorage(); // Próbujemy zapisać na serwerze if (navigator.onLine && authStore.token) { const response = await fetch(`${apiBaseUrl}/transactions`, { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${authStore.token}`, }, body: JSON.stringify(transaction), }); if (response.ok) { // Aktualizujemy transakcję z danymi z serwera const data = await response.json(); const index = transactions.value.findIndex(t => t._id === tempId); if (index !== -1) { transactions.value[index] = { ...data.data, synced: true }; saveToLocalStorage(); } } else { // Oznaczamy transakcję do późniejszej synchronizacji const syncStore = useSyncStore(); syncStore.addPendingTransaction(newTransaction); throw new Error("Failed to save transaction to server"); } } else { // Jesteśmy offline, dodajemy do listy oczekujących na sync const syncStore = useSyncStore(); syncStore.addPendingTransaction(newTransaction); } } catch (err: any) { error.value = err.message; throw err; } } ``` ## 5. Interfejs użytkownika ### 5.1 Komponenty Aplikacja wykorzystuje komponenty Vue do zbudowania interfejsu użytkownika. Przykładowy komponent Dashboard: ![Komponent Dashboard](dashboard-preview.png) ```vue ``` ### 5.2 Responsywność Aplikacja wykorzystuje Tailwind CSS do tworzenia responsywnego interfejsu, który działa dobrze na urządzeniach mobilnych i desktopowych: ![Dashboard mobilny](responsive-dashboard-image.png) - Używanie systemu grid z breakpointami (md:, lg:) - Elastyczne komponenty dopasowujące się do różnych rozmiarów ekranów - Mobilne menu nawigacyjne dla małych ekranów ### 5.3 Dostępność offline Interfejs uwzględnia stan połączenia internetowego: ![Tryb offline](no-internet-image.png) - Wyświetlanie informacji o trybie offline - Możliwość dodawania transakcji offline - Automatyczna synchronizacja po ponownym połączeniu ## 6. Backend API ### 6.1 Struktura API Backend implementuje RESTful API do komunikacji z frontendem: - `POST /api/auth/login` - logowanie użytkownika - `POST /api/auth/register` - rejestracja nowego użytkownika - `GET /api/transactions` - pobieranie listy transakcji - `POST /api/transactions` - dodawanie nowej transakcji - `PUT /api/transactions/:id` - aktualizacja transakcji - `DELETE /api/transactions/:id` - usunięcie transakcji - `GET /api/categories` - pobieranie kategorii - `PUT /api/categories` - dodawanie nowej kategorii ### 6.2 Bezpieczeństwo API API zabezpieczone jest przez: - Tokeny JWT do autoryzacji - Middleware uwierzytelniające - Zabezpieczenie przed CSRF - CORS ```typescript // Middleware autoryzacji export const authenticate = async (ctx: Context, next: Next) => { // Pomijamy middleware dla ścieżek publicznych if (ctx.path.startsWith('/api/auth/login') || ctx.path.startsWith('/api/auth/register')) { return next(); } const token = ctx.headers.authorization?.split(' ')[1]; if (!token) { ctx.status = 401; ctx.body = { success: false, message: 'No token provided' }; return; } try { const decoded = jwt.verify(token, jwtConfig.secret); ctx.state.user = decoded; return next(); } catch (err) { ctx.throw(401, 'Invalid or expired token'); } }; ``` ## 7. Baza danych ### 7.1 Model danych Aplikacja wykorzystuje MongoDB do przechowywania danych, z następującymi kolekcjami: 1. `users` - informacje o użytkownikach 2. `transactions` - transakcje finansowe 3. `categories` - kategorie transakcji ### 7.2 Usługa bazodanowa Abstrakcja dostępu do bazy danych zapewniona jest przez klasę DatabaseService: ```typescript export default class DatabaseService { private static instance: DatabaseService; private client: MongoClient | null = null; private db: Db | null = null; private constructor() {} // Wzorzec Singleton public static getInstance(): DatabaseService { if (!DatabaseService.instance) { DatabaseService.instance = new DatabaseService(); } return DatabaseService.instance; } // Połączenie z bazą danych public async connect() { try { this.client = await MongoClient.connect(dbConfig.url); this.db = this.client.db(dbConfig.name); console.log('Connected to MongoDB'); } catch (error) { console.error('Error connecting to MongoDB:', error); throw error; } } // Pobranie kolekcji public getCollection(collectionName: string): Collection { if (!this.db) { throw new Error('Database not connected'); } return this.db.collection(collectionName); } // Zamknięcie połączenia public async disconnect() { if (this.client) { await this.client.close(); this.client = null; this.db = null; console.log('Disconnected from MongoDB'); } } } ``` ## 8. Testy i użycie ### 8.1 Testowanie offline Aplikacja została przetestowana w różnych scenariuszach: - Dodawanie transakcji w trybie offline - Automatyczna synchronizacja po przywróceniu połączenia - Działanie Service Worker'a i cache'owanie zasobów ### 8.2 Instalacja jako PWA Aplikacja może być zainstalowana na urządzeniach: - Poprawnie wyświetla prompt instalacyjny - Działa jako samodzielna aplikacja po zainstalowaniu - Wykorzystuje ikony i kolory zdefiniowane w manifeście ## 9. Wnioski i perspektywy rozwoju ### 9.1 Osiągnięte cele Aplikacja spełnia wszystkie założenia projektowe: - Funkcjonuje jako pełnoprawna PWA - Umożliwia zarządzanie budżetem osobistym - Działa offline z synchronizacją - Ma responsywny i przyjazny interfejs ### 9.2 Możliwe rozszerzenia Potencjalne kierunki rozwoju aplikacji: - Dodanie eksportu danych do CSV/PDF - Implementacja celów oszczędnościowych - Rozbudowa funkcji raportowania o wykresy - Dodanie powiadomień o zbliżających się regularnych płatnościach - Integracja z bankami poprzez API ## 10. Zrzuty ekranu ### 10.1 Ekran logowania ![Ekran logowania](login_screen.png) ### 10.2 Ekran transakcji ![Ekran transakcji](transaction_screen.png) ### 10.3 Ekran kategorii ![Ekran kategorii](category_screen.png) ### 10.4 Ekran raportów ![Ekran raportów](report_screenshot_1.png) ### 10.5 Ekran ustawień ![Ekran ustawień](settings_screen.png) ## 11. Bibliografia 1. Vue.js Documentation: [https://vuejs.org/guide/introduction.html](https://vuejs.org/guide/introduction.html) 2. Progressive Web Apps: [https://web.dev/progressive-web-apps/](https://web.dev/progressive-web-apps/) 3. MDN Web Docs - Service Worker API: [https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API](https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API) 4. Tailwind CSS Documentation: [https://tailwindcss.com/docs](https://tailwindcss.com/docs) 5. Koa.js Documentation: [https://koajs.com/](https://koajs.com/) 6. MongoDB Documentation: [https://docs.mongodb.com/](https://docs.mongodb.com/)