diff --git a/backend/AGENTS.md b/backend/AGENTS.md new file mode 100644 index 0000000..b2165ae --- /dev/null +++ b/backend/AGENTS.md @@ -0,0 +1,584 @@ +# Backend Architecture & Design Patterns Guide + +## Overview + +Bank App Backend jest zbudowany na Laravel 13 z uwzględnieniem architektury opartej na warstwach i designowych wzorcach. Dokument ten opisuje główne wzorce i strukturę kodu, którą powinni trzymać się agenci AI (oraz programiści) podczas implementacji nowych funkcjonalności. + +--- + +## 1. Struktura Katalogów + +``` +app/ +├── Console/ # Komendy CLI +├── Enums/ # Enumeracje do wartości stałych +├── Exceptions/ # Custom wyjątki +├── Http/ +│ ├── Controllers/ # Kontrolery REST API +│ ├── Resources/ # Data Transfer Objects (JSON) +│ ├── Requests/ # Form Requests (walidacja) +│ └── Concerns/ # Traits dla kontrolerów +├── Models/ # Modele Eloquent ORM +├── Notifications/ # Klasy notyfikacji +├── Providers/ # Service Providers +├── Rules/ # Custom validation rules +├── Services/ # Logika biznesowa +└── View/ # View models / Response builders +``` + +--- + +## 2. Wzorce Architektoniczne + +### 2.1 Service Layer Pattern + +Całą logikę biznesową umieszczamy w **Service klasach**, kontrolery powinny być lean (cienkie). + +**Lokalizacja:** `app/Services/{Domain}/{Feature}/` + +**Przykład - CardService:** + +```php +generateCardNumber($network); + + // Tworzenie rekordu + return Card::create([ + 'account_id' => $accountId, + 'card_number' => $cardNumber, + 'status' => 'active', + // ... + ]); + } + + public function blockCard(Card $card): Card + { + $card->update(['status' => 'blocked']); + return $card->fresh(); + } +} +``` + +**Reguły:** +- ✅ Jedna funkcjonalność = jedna metoda w serwisie +- ✅ Serwisy są injektowane do kontrolerów poprzez DI +- ✅ Serwisy mogą używać innych serwisów +- ✅ Logika biznesowa nigdy w kontrolerze! + +--- + +### 2.2 Enums Pattern + +Dla stałych wartości używamy **PHP 8.1+ enums** zamiast stringów/intów. + +**Lokalizacja:** `app/Enums/{Domain}/` + +**Przykład - CardNetwork:** + +```php + ['4'], + self::MASTERCARD => ['51', '52', '53', '54', '55'], + }; + } + + public function label(): string + { + return match ($this) { + self::VISA => 'Visa', + self::MASTERCARD => 'Mastercard', + }; + } +} +``` + +**Reguły:** +- ✅ Enum dla każdego zdefinowanego zestawu wartości +- ✅ Metody w enum'ie do konwersji, labelów, logiki +- ✅ Bezpieczne typowanie - kompilacja IDE i IDE help + +--- + +### 2.3 Controller Pattern + +Kontrolery są **cienkie** - delegują pracę do serwisów. + +**Lokalizacja:** `app/Http/Controllers/{Domain}/{Feature}/` + +**Przykład - CardController:** + +```php +id(); + + $cards = Card::whereHas('account', function ($q) use ($userId) { + $q->where('user_id', $userId); + })->paginate($this->perPage(), page: $this->currentPage()); + + return CardResource::collection($cards); + } + + #[AuthorizeToken(['cards-manage'], anyScope: true)] + public function store(Account $account): JsonResponse + { + $this->authorize($account); // Sprawdzenie dostępu + + $card = $this->cardService->createCard($account->id); + + return new CardResource($card) + ->response() + ->setStatusCode(201); + } +} +``` + +**Reguły:** +- ✅ Dependency Injection do konstruktora +- ✅ Użyj Traits do wspólnych funkcjonalności +- ✅ Operacje CRUD -> createCard, updateCard, deleteCard w serwisie +- ✅ Autoryzacja w kontrolerze (`$this->authorize()`) +- ✅ Zwrot Resourceów, nie modeli surowych + +--- + +### 2.4 Model & Eloquent Pattern + +Modele reprezentują tabele w bazie danych z logiką relacji. + +**Lokalizacja:** `app/Models/{Domain}/` + +**Przykład - Card Model:** + +```php + 'integer', + 'exp_year' => 'integer', + 'cvv' => 'integer', + ]; + } + + public function account(): BelongsTo + { + return $this->belongsTo(Account::class); + } + + public function transactions(): HasMany + { + return $this->hasMany(Transaction::class, 'from_card_id'); + } +} +``` + +**Reguły:** +- ✅ Krótkie, czytelne nazwy metod relacji +- ✅ Relacje w modelu, nie w kontrolerze +- ✅ Attribute Casting do automatycznej konwersji +- ✅ Fillable lub Guarded do bezpieczeństwa +- ✅ UUID zamiast auto-increment IDs + +--- + +### 2.5 Resource Pattern (DTO) + +Resources transformują modele w JSON. + +**Lokalizacja:** `app/Http/Resources/{Domain}/` + +**Przykład - CardResource:** + +```php + $this->id, + /* @example "**** **** **** 1234" */ + 'card_number' => $this->when( + $this->card_number, + $this->maskCardNumber($this->card_number), + ), + /* @example "1234" */ + 'card_last_four' => $this->when( + $this->card_number, + substr($this->card_number, -4), + ), + 'network' => $this->network, + 'type' => $this->type, + 'status' => $this->status, + 'exp_month' => $this->exp_month, + 'exp_year' => $this->exp_year, + 'created_at' => $this->created_at?->toISOString(), + 'updated_at' => $this->updated_at?->toISOString(), + ]; + } +} +``` + +**Reguły:** +- ✅ Nigdy nie ekspozuj całego modelu surowo +- ✅ Maskuj dane wrażliwe (np. numery kart, CVV) +- ✅ Zwróć `$this->when()` dla pól warunkowych +- ✅ Zawsze ISO 8601 dla dat +- ✅ Dokumentuj każde pole za pomocą `@example` + +--- + +### 2.6 Authorization & Authentication + +**Passport OAuth2:** Używamy Laravel Passport do OAuth2 authorization. + +```php +#[AuthorizeToken(['cards-view'], anyScope: true)] +public function index(Account $account) +{ + // Middleware sprawdzi token i scopes + if ($account->user_id !== auth()->id()) { + throw new AccessDeniedHttpException('Access denied'); + } +} +``` + +**Reguły:** +- ✅ Użyj `#[AuthorizeToken]` atrybutu do sprawdzenia scopes +- ✅ Zawsze sprawdzaj ownership w metodzie +- ✅ Zwróć `AccessDeniedHttpException` dla błędów dostępu + +--- + +### 2.7 Traits Pattern + +Wspólna funkcjonalność w Traits, aby uniknąć duplikacji. + +**Przykład - WithPagination:** + +```php +get('per_page', 20); + } + + protected function currentPage(): int + { + return (int) request()->get('page', 1); + } +} +``` + +**Reguły:** +- ✅ Traits dla krzyżowych problemów (pagination, timestamps, etc.) +- ✅ Umieszczaj w `app/Http/Concerns/` lub `app/Services/Concerns/` + +--- + +### 2.8 API Documentation + +Używamy **Dedoc Scramble** z atrybutami PHP do dokumentacji API. + +```php +/** + * Zarządzanie kartami płatniczymi. + * + * @tags Karty + * @description API dla zarządzania kartami + */ +class CardController extends Controller +{ + /** + * Wyświetla listę wszystkich kart użytkownika. + */ + #[AuthorizeToken(['cards-view'], anyScope: true)] + #[QueryParameter('per_page', description: 'Ilość na stronę', type: 'int', default: 20)] + #[QueryParameter('page', description: 'Numer strony', type: 'int', default: 1)] + #[PathParameter('account', description: 'ID konta', type: 'string', format: 'uuid')] + public function index(Account $account) { } +} +``` + +**Reguły:** +- ✅ Dokumentuj każdy endpoint w zasobie +- ✅ Dodaj `@tags` dla grupowania +- ✅ Dokumentuj parametry z `#[QueryParameter]`, `#[PathParameter]` +- ✅ Dokumentuj w phpdoc parametry, response, exceptiony + +--- + +## 3. API Routing Pattern + +**Lokalizacja:** `routes/api.php` + +```php +Route::middleware(['auth:api', 'adult'])->group(function () { + // Cards + Route::get('cards', [CardController::class, 'allCards']); + Route::get('cards/{card}', [CardController::class, 'show']); + Route::apiResource('accounts.cards', CardController::class)->only(['index', 'store']); + + // Block/Unblock are custom actions + Route::patch('cards/{card}/block', [CardController::class, 'block']); + Route::patch('cards/{card}/unblock', [CardController::class, 'unblock']); + Route::delete('cards/{card}', [CardController::class, 'destroy']); +}); +``` + +**Reguły:** +- ✅ Używaj `apiResource()` do RESTful CRUD +- ✅ Custom akcje jak `.../block` jako dodatkowe routes +- ✅ Middleware na grupach, nie indywidualne +- ✅ Hierarchia: `/accounts/{id}/cards` dla zagnieżdżonych zasobów + +--- + +## 4. Guidelines dla Nowych Features + +### Dodawanie nowej funkcjonalności do domeny: + +1. **Model** (`app/Models/{Domain}/NewFeature.php`) + - Relacje do innych modeli + - Fillable/Guarded + - Casts + +2. **Service** (`app/Services/{Domain}/NewFeatureService.php`) + - Biznesowa logika + - Metody dla CRUD operacji + +3. **Controller** (`app/Http/Controllers/{Domain}/NewFeatureController.php`) + - Lean - delegacja do serwisu + - Authorization checks + - Resource responses + +4. **Resource** (`app/Http/Resources/{Domain}/NewFeatureResource.php`) + - Transformacja do JSON + - Dokumentacja pól + +5. **Routes** (routes/api.php) + - RESTful CRUD paths + - Custom action routes + - Middleware/Authorization + +6. **Testy** (tests/Feature/) + - Test każdy endpoint + - Test authorization + - Test data masking + +7. **Migracje** (database/migrations/) + - Tabel, foreign keys, indexes + - Seeders do demo danych + +--- + +## 5. Database Patterns + +### UUID Primary Keys + +```php +use Illuminate\Database\Eloquent\Concerns\HasUuids; + +class Card extends Model +{ + use HasUuids; + // ... +} +``` + +### Polymorphic Relations (Future) + +```php +// Dla features z wiele typami +class Notification extends Model +{ + public function notifiable() + { + return $this->morphTo(); + } +} +``` + +--- + +## 6. Security Patterns + +### Password Hashing +```php +use Illuminate\Support\Facades\Hash; + +$user->password = Hash::make('password'); +``` + +### Data Masking +```php +// W Resource +'card_number' => $this->maskCardNumber($this->card_number), +``` + +### CORS Configuration +```php +// config/cors.php - już skonfigurowany +``` + +--- + +## 7. Common Mistakes to Avoid + +❌ Logika biznesowa w kontrolerach +✅ Umieść w Service klasach + +❌ Zwracanie surowych modeli w API +✅ Zawsze użyj Resources + +❌ String enumeracje `'visa'` zamiast enums +✅ Użyj PHP Enums + +❌ Duplicated validation logic +✅ Form Requests + Custom Rules + +❌ N+1 queries +✅ Eager load relacje `with()` + +❌ Brak error handling +✅ Custom Exceptions, proper HTTP codes + +--- + +## 8. Dependencies & Frameworks + +- **Laravel 13** - Full-stack framework +- **Laravel Passport** - OAuth2 authentication +- **Dedoc Scramble** - API documentation +- **Laravel Tinker** - REPL +- **PHPUnit** - Testing +- **PHP 8.4+** - Language features (enums, attributes, etc.) + +--- + +## 9. Testing Patterns + +```php +namespace Tests\Feature; + +class CardControllerTest extends TestCase +{ + #[Test] + public function it_returns_all_cards_for_authenticated_user() + { + $user = User::factory()->create(); + $account = Account::factory()->for($user)->create(); + $cards = Card::factory(3)->for($account)->create(); + + $response = $this->actingAs($user) + ->getJson('/api/cards'); + + $response->assertOk() + ->assertJsonCount(3, 'data'); + } +} +``` + +--- + +## 10. Useful Commands + +```bash +# Generate new service +php artisan make:service Services/Account/Card/CardService + +# Generate resource +php artisan make:resource Account/Card/CardResource + +# Generate model with migration +php artisan make:model Models/Account/Card/Card -m + +# Generate controller +php artisan make:controller Account/Card/CardController --api + +# Run migrations +php artisan migrate + +# Run tests +php artisan test + +# API docs +php artisan scramble:generate +``` + +--- + +## AI Agent Instructions + +Kiedy dodajesz nowe funkcjonalności: + +1. **ZAWSZE** rozpocznij od Service klasach +2. **NIGDY** nie umieszczaj logiki w kontrolerach +3. **ZAWSZE** używaj Resources do API responses +4. **ZAWSZE** dokumentuj poprzez atrybuty PHP +5. **ZAWSZE** sprawdzaj authorization +6. **ZAWSZE** maskuj dane wrażliwe +7. **HINTED TYPES** - używaj strict type hints +8. Read-only properties gdzie to możliwe +9. Dependency Injection do konstruktora +10. Keep methods small and focused + +Powodzenia! 🚀 + diff --git a/backend/app/Http/Controllers/Account/Card/CardController.php b/backend/app/Http/Controllers/Account/Card/CardController.php index bb195ce..1a32923 100644 --- a/backend/app/Http/Controllers/Account/Card/CardController.php +++ b/backend/app/Http/Controllers/Account/Card/CardController.php @@ -30,6 +30,26 @@ public function __construct( ) { } + /** + * Wyświetla listę wszystkich kart płatniczych użytkownika. + */ + #[AuthorizeToken(['cards-view'], anyScope: true)] + #[QueryParameter('per_page', description: 'Ilość elementów na stronę.', type: 'int', default: 20, example: 30)] + #[QueryParameter('page', description: 'Numer obecnej strony.', type: 'int', default: 1, example: 2)] + public function allCards() + { + $userId = auth()->id(); + + $cards = Card::whereHas('account', function ($query) use ($userId) { + $query->where('user_id', $userId); + })->paginate( + perPage: $this->perPage(), + page: $this->currentPage(), + ); + + return CardResource::collection($cards); + } + /** * Wyświetla listę kart płatniczych przypisanych do konta. */ diff --git a/backend/bootstrap/cache/.gitignore b/backend/bootstrap/cache/.gitignore old mode 100644 new mode 100755 diff --git a/backend/routes/api.php b/backend/routes/api.php index 99d9373..7bc80a1 100644 --- a/backend/routes/api.php +++ b/backend/routes/api.php @@ -48,6 +48,7 @@ Route::get('accounts/balance', [AccountController::class, 'balance']); Route::apiResource('accounts', AccountController::class); Route::apiResource('accounts.cards', CardController::class)->only(['index', 'store']); + Route::get('cards', [CardController::class, 'allCards']); Route::get('cards/{card}', [CardController::class, 'show']); Route::patch('cards/{card}/block', [CardController::class, 'block']); Route::patch('cards/{card}/unblock', [CardController::class, 'unblock']); diff --git a/backend/storage/api-docs/api-docs.json b/backend/storage/api-docs/api-docs.json old mode 100644 new mode 100755 diff --git a/backend/storage/app/.gitignore b/backend/storage/app/.gitignore old mode 100644 new mode 100755 diff --git a/backend/storage/app/private/.gitignore b/backend/storage/app/private/.gitignore old mode 100644 new mode 100755 diff --git a/backend/storage/app/public/.gitignore b/backend/storage/app/public/.gitignore old mode 100644 new mode 100755 diff --git a/backend/storage/framework/.gitignore b/backend/storage/framework/.gitignore old mode 100644 new mode 100755 diff --git a/backend/storage/framework/cache/.gitignore b/backend/storage/framework/cache/.gitignore old mode 100644 new mode 100755 diff --git a/backend/storage/framework/cache/data/.gitignore b/backend/storage/framework/cache/data/.gitignore old mode 100644 new mode 100755 diff --git a/backend/storage/framework/sessions/.gitignore b/backend/storage/framework/sessions/.gitignore old mode 100644 new mode 100755 diff --git a/backend/storage/framework/testing/.gitignore b/backend/storage/framework/testing/.gitignore old mode 100644 new mode 100755 diff --git a/backend/storage/framework/views/.gitignore b/backend/storage/framework/views/.gitignore old mode 100644 new mode 100755 diff --git a/backend/storage/logs/.gitignore b/backend/storage/logs/.gitignore old mode 100644 new mode 100755 diff --git a/frontend/AGENTS.md b/frontend/AGENTS.md new file mode 100644 index 0000000..9a58a5b --- /dev/null +++ b/frontend/AGENTS.md @@ -0,0 +1,821 @@ +# Frontend Architecture & Design Patterns Guide + +## Overview + +Bank App Frontend jest zbudowany na React 18+ z TypeScript, Material-UI i React Router v6. Dokument ten opisuje główne wzorce i strukturę kodu, którą powinni trzymać się agenci AI (oraz programiści) podczas implementacji nowych funkcjonalności. + +--- + +## 1. Struktura Katalogów + +``` +src/ +├── assets/ # Statyczne zasoby (obrazy, ikony) +├── components/ # Komponenty wielokrotnego użytku +│ ├── Layout/ # Layout komponenty (Header, Sidebar) +│ ├── Fields/ # Form fields +│ ├── Auth/ # Auth-specifyczne komponenty +│ ├── Notification/ # Notification komponenty +│ └── Assets/ # Asset komponenty (Loading, etc.) +├── context/ # React Context Providers +├── hooks/ # Custom React Hooks +├── pages/ # Page komponenty (screen level) +│ ├── Account/ +│ ├── User/ +│ ├── Forms/ +│ └── Guardian/ +├── services/ # API communication layer +├── types/ # TypeScript typ definicje +├── utils/ # Utility funkcje +├── App.tsx # Main App component +├── main.tsx # Entry point +└── index.css # Global styles +``` + +--- + +## 2. Wzorce Architektoniczne + +### 2.1 React Context + Hooks Pattern + +Używamy **React Context API** dla global state management (auth, toast, loading). + +**Lokalizacja:** `src/context/` + +**Przykład - AuthContext:** + +```typescript +import { createContext, useContext, useState, useEffect } from 'react'; +import type { AuthContextType, User } from '../types/types'; + +const AuthContext = createContext(null); + +export const useAuth = (): AuthContextType => { + const context = useContext(AuthContext); + if (!context) { + throw new Error('useAuth must be used within AuthProvider'); + } + return context; +}; + +export const AuthProvider = ({ children }: { children: React.ReactNode }) => { + const [user, setUser] = useState(null); + const [loading, setLoading] = useState(true); + + useEffect(() => { + loadUser(); + }, []); + + const handleLogin = async (email: string, password: string): Promise => { + const userData = await loginWithPassword(email, password); + setUser(userData); + return userData; + }; + + const value: AuthContextType = { + user, + setUser, + loading, + handleLogin, + handleLogout, + isAuthenticated: !!user, + }; + + return {children}; +}; +``` + +**Reguły:** +- ✅ Jeden Context = jeden aspekt state (auth, toast, loading) +- ✅ Custom Hook `useAuth()` zawsze sprawdza null +- ✅ Provider na top level (App.tsx) +- ✅ Nie mieszaj logiki - jeden context = jedna odpowiedzialność + +--- + +### 2.2 API Service Layer + +Services komunikują się z backend API. + +**Lokalizacja:** `src/services/` + +**Przykład - accountService.ts:** + +```typescript +import { fetchWithAuth } from "./authService"; +import { Account, Card, PaginationResponse, ApiError } from "../types/types"; + +export const getAllCards = async ( + page: number = 1, + perPage: number = 20 +): Promise> => { + const response = await fetchWithAuth( + `/cards?page=${page}&per_page=${perPage}`, + { method: 'GET' } + ); + + if (!response.ok) { + const error: ApiError = await response.json(); + throw new Error(error.message || 'Błąd pobierania kart'); + } + + return await response.json(); +}; + +export const blockCard = async (cardId: string): Promise => { + const response = await fetchWithAuth(`/cards/${cardId}/block`, { + method: 'PATCH', + }); + + if (!response.ok) { + throw new Error('Nie udało się zablokować karty'); + } +}; +``` + +**Reguły:** +- ✅ Funkcje asyncowe do każdej operacji +- ✅ Zawsze sprawdzaj `response.ok` +- ✅ Throw Error z descriptive message +- ✅ Zwracaj typed responses +- ✅ Nie zmieniaj stanu w services +- ✅ Używaj `fetchWithAuth()` dla auth headers + +--- + +### 2.3 Custom Hooks Pattern + +Custom Hooks enkapsulują logikę reutilizacyjną. + +**Lokalizacja:** `src/hooks/` + +**Przykład - useAuth Hook:** + +```typescript +import { useContext } from 'react'; +import { AuthContext } from '../context/AuthContext'; + +export const useAuth = () => { + const context = useContext(AuthContext); + if (!context) { + throw new Error('useAuth must be used within AuthProvider'); + } + return context; +}; +``` + +**Reguły:** +- ✅ Hook names zaczynaj z `use` prefix +- ✅ Custom hooks enkapsulują complex useEffect logic +- ✅ Zwracaj objects z logią i state +- ✅ Rzucaj error jeśli hook used bez providera + +--- + +### 2.4 Page Component Pattern + +Page komponenty to top-level komponenty dla każdego screeningu. + +**Lokalizacja:** `src/pages/{Feature}/` + +**Przykład - CardPage.tsx:** + +```typescript +export const CardPage = () => { + const [cards, setCards] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const { showSuccess, showError } = useToast(); + + const loadCards = async (pageNumber: number = 1) => { + setLoading(true); + setError(null); + try { + const response = await getAllCards(pageNumber); + setCards(response.data); + } catch (err: any) { + setError(err.message); + showError(err.message); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + loadCards(); + }, []); + + return ( + + {loading && } + {error && {error}} + {/* Render content */} + + ); +}; +``` + +**Reguły:** +- ✅ Pages zarządzają własnym local state +- ✅ Pages fetchują dane w useEffect +- ✅ Obierz context hooks dla global state +- ✅ Deleguj complex logikę do custom hooks +- ✅ Render error/loading states + +--- + +### 2.5 Material-UI Component Pattern + +Używamy **Material-UI (MUI)** do UI komponentów. + +```typescript +import { + Box, + Card, + CardContent, + Button, + TextField, + Dialog, + Table, + TableHead, + TableBody, + TableRow, + TableCell, + Alert, +} from '@mui/material'; +import { Add as AddIcon } from '@mui/icons-material'; + +export const MyComponent = () => { + const [open, setOpen] = useState(false); + + return ( + + + + + + + + + setOpen(false)}> + {/* Dialog content */} + + + ); +}; +``` + +**Reguły:** +- ✅ Używaj Box zamiast div dla layout +- ✅ sx prop dla inline styles +- ✅ Icons from `@mui/icons-material` +- ✅ Zmieniaj warianty (variant="contained", variant="outlined") +- ✅ Używaj Material-UI components zamiast HTML + +--- + +### 2.6 Type Definitions + +TypeScript types dla całej aplikacji. + +**Lokalizacja:** `src/types/types.ts` + +```typescript +// User related +export interface User { + id?: number; + name?: string; + email?: string; + email_verified_at?: string; + created_at?: string; +} + +// Card related +export interface Card { + id: string; + card_number: string; + card_last_four: string; + exp_month: number; + exp_year: number; + network: string; + type: string; + status: string; + created_at: string; + updated_at: string; +} + +// Pagination +export interface PaginationMeta { + current_page: number; + last_page: number; + total: number; + from: number | null; + to: number | null; +} + +export interface PaginationResponse { + data: T[]; + meta: PaginationMeta; + links: PaginationLinks; +} + +// API Errors +export interface ApiError { + status?: string; + message?: string; + errors?: Record; +} +``` + +**Reguły:** +- ✅ Centralizuj wszystkie typy w `types.ts` +- ✅ Exportuj interfaces, nie types +- ✅ Czytaj structury API response dokładnie +- ✅ Optional pola z `?` +- ✅ Dokumentuj complex types + +--- + +### 2.7 Form Pattern + +Formularze w Reactcie ze state managementem. + +```typescript +const [formData, setFormData] = useState({ + email: '', + password: '', + rememberMe: false, +}); + +const [errors, setErrors] = useState>({}); + +const handleFormChange = (e: React.ChangeEvent) => { + const { name, value, type, checked } = e.target; + setFormData(prev => ({ + ...prev, + [name]: type === 'checkbox' ? checked : value, + })); +}; + +const handleSubmit = async () => { + setErrors({}); + try { + await login(formData.email, formData.password); + } catch (err: any) { + setErrors(err.errors || { general: err.message }); + } +}; + +return ( + + + + +); +``` + +**Reguły:** +- ✅ Każdy input ma name attribute +- ✅ Centralizuj form state w jednym object +- ✅ Error state as Record +- ✅ Disable submit button kiedy submitting +- ✅ Show helperText na error fields + +--- + +### 2.8 Error Handling & Toast Notifications + +Używamy Toast Context do notifications. + +```typescript +const { showSuccess, showError, showWarning } = useToast(); + +const handleDeleteCard = async () => { + try { + await deleteCard(cardId); + showSuccess('Karta została usunięta!'); + await loadCards(); + } catch (err: any) { + const errorMsg = err.message || 'Błąd usuwania karty'; + showError(errorMsg); + } +}; +``` + +**Reguły:** +- ✅ Toast context dla notifications +- ✅ showSuccess/showError metody +- ✅ Zawsze catch error i show toast +- ✅ Error message do użytkownika +- ✅ Loading state podczas async operacji + +--- + +### 2.9 Routing & Navigation + +React Router v6 dla nawigacji. + +**Lokalizacja:** `App.tsx` + +```typescript + + + {/* Public routes */} + }> + } /> + } /> + + + {/* Protected routes */} + }> + }> + } /> + } /> + } /> + } /> + + + + } /> + + +``` + +**Reguły:** +- ✅ Route guards (ProtectedRoute, GuestOnlyRoute) +- ✅ Nested routes dla shared layout +- ✅ Named params z `:` +- ✅ 404 fallback do default page +- ✅ useNavigate() do programmatic navigation + +--- + +### 2.10 Data Formatting Utilities + +Utility funkcje do formatowania danych. + +**Lokalizacja:** `src/utils/` + +```typescript +// formatDate.ts +export const formatDate = (dateString: string): string => { + return new Date(dateString).toLocaleDateString('pl-PL'); +}; + +export const formatDateShort = (dateString: string): string => { + return new Date(dateString).toLocaleDateString('pl-PL', { + year: '2-digit', + month: '2-digit', + day: '2-digit', + }); +}; + +// formatAccount.ts +export const formatAccountNumber = (accountNumber: string): string => { + return accountNumber.replace(/(\d{2})/g, '$1 ').trim(); +}; +``` + +**Reguły:** +- ✅ Pure functions bez side effects +- ✅ Eksportuj helpers do reuse +- ✅ Consistent formatting (locale, format) +- ✅ Zapamiętaj formatowanie dla UI display + +--- + +## 3. Responsive Design Pattern + +Material-UI Grid systemu do responsywności. + +```typescript +import { Grid } from '@mui/material'; + + + + {/* Full width na mobile, 50% na tablet, 33% na desktop */} + + +``` + +**Breakpoints:** +- `xs` - 0px (mobile) +- `sm` - 600px (tablet) +- `md` - 960px (laptop) +- `lg` - 1280px (desktop) +- `xl` - 1920px (wide desktop) + +--- + +## 4. Loading & Error States + +Zawsze renderuj loading i error states. + +```typescript +if (loading) { + return ( + + {[1, 2, 3].map((i) => ( + + + + ))} + + ); +} + +if (error || !data) { + return ( + + {error || 'Nie znaleziono danych'} + + ); +} + +return ( + {/* Render data */} +); +``` + +**Reguły:** +- ✅ Loading state z Skeleton +- ✅ Error state z Alert +- ✅ Empty state message +- ✅ Try/catch gdzie async + +--- + +## 5. State Management Rules + +### Local State (useState) +- UI state (modal open, form data, focused field) +- Loading/error flags +- Temporary data + +### Global State (Context) +- User auth info +- Toast notifications +- Global loading indicator +- Theme preferences (future) + +### Backend State (Services) +- Data fetched from API +- Pagination info +- Never store in Context unless really global + +```typescript +// ❌ Wrong +const [allCards, setAllCards] = useState([]); // Too much global +const [isGlobalLoading, setIsGlobalLoading] = useState(false); // Each page loads its own + +// ✅ Right +const [cards, setCards] = useState([]); // Local to page +const [loading, setLoading] = useState(true); // Local to page +const { showSuccess } = useToast(); // Global toast +const { user } = useAuth(); // Global auth +``` + +--- + +## 6. Component Composition + +Mniejsze, reusable komponenty zamiast mega-komponentów. + +```typescript +// ❌ Bad - All in one +export const CardListPage = () => { + // 500 lines of code +}; + +// ✅ Good - Decomposed +export const CardListPage = () => { + return ( + + + + + + ); +}; + +const CardListHeader = () => { + return {/* ... */}; +}; + +const CardListTable = ({ cards }: { cards: Card[] }) => { + return {/* ... */}
; +}; +``` + +**Reguły:** +- ✅ Komponenty do 200 linii +- ✅ Reusable komponenty w `components/` +- ✅ Page-specific w `pages/` +- ✅ Props typing + +--- + +## 7. Testing Patterns (Future) + +```typescript +import { render, screen, waitFor } from '@testing-library/react'; +import { CardPage } from './CardPage'; + +describe('CardPage', () => { + it('should display cards list', async () => { + render(); + + await waitFor(() => { + expect(screen.getByText('Moje karty')).toBeInTheDocument(); + }); + }); +}); +``` + +--- + +## 8. Common Mistakes to Avoid + +❌ SetState w loop +✅ Initialize state z array, map w render + +❌ Render bez key prop +✅ Zawsze key na list items + +❌ API call w render +✅ Umieść w useEffect + +❌ useEffect bez dependencies +✅ Spróbuj zawsze specify dependencies + +❌ Inline funkcje w onClick +✅ Define funkcje poza render lub useCallback + +❌ State w Context dla wszystkiego +✅ Rozdziel local vs global + +❌ prop drilling 10 levels deep +✅ Użyj Context dla deeply nested data + +❌ Mixing UI logic i business logic +✅ Umieść busines logic w services + +--- + +## 9. Dependencies & Frameworks + +- **React 18+** - UI library +- **React Router v6** - Routing +- **TypeScript** - Type safety +- **Material-UI v5+** - Component library +- **MUI Icons** - Icon library +- **Fetch API** - HTTP (no axios) +- **Vite** - Build tool +- **Biome** - Linter/Formatter + +--- + +## 10. Project Structure Best Practices + +``` +src/ +├── assets/ +│ ├── icons/ +│ └── images/ +├── components/ +│ ├── Common/ # Shared always +│ ├── Layout/ # Layout wrappers +│ ├── Fields/ # Form fields +│ └── UI/ # UI components +├── context/ # React Context +│ ├── AuthContext.tsx +│ ├── ToastContext.tsx +│ └── LoadingContext.tsx +├── hooks/ # Custom hooks +│ └── useAuth.tsx +├── pages/ +│ ├── Account/ +│ │ ├── AccountPage.tsx +│ │ ├── Card/ +│ │ │ └── CardPage.tsx +│ │ └── AccoutViewPage.tsx +│ ├── User/ +│ ├── Dashboard/ +│ └── ... +├── services/ # API Services +│ ├── accountService.ts +│ ├── authService.ts +│ └── ... +├── types/ # Type definitions +│ └── types.ts +├── utils/ # Utility functions +│ ├── formatDate.ts +│ └── formatAccount.ts +├── App.tsx +├── App.css +├── main.tsx +└── index.css +``` + +--- + +## 11. Useful Commands + +```bash +# Start dev server +npm run dev + +# Build for production +npm run build + +# Run linter +npm run lint + +# Format code +npm run format +``` + +--- + +## 12. AI Agent Instructions + +Kiedy dodajesz nowe features: + +1. **ZAWSZE** start z page komponentem +2. **NIGDY** nie mieszaj API logic z UI +3. **ZAWSZE** używaj Services dla API calls +4. **ZAWSZE** handle loading/error states +5. **ZAWSZE** type everything with TypeScript +6. **ZAWSZE** use React Context dla global state +7. **ZAWSZE** show toast notification na success/error +8. Keep components small and focused +9. Extract reusable components early +10. Test responsiveness on mobile + +## Sekcja Komponentów Material-UI - Quick Reference + +```typescript +// Box - Flexbox container + + +// Card - Elevated container + + + {/* Content */} + + + +// Button - Actions + + + +// TextField - Input + + +// Table - Data grid + + + + {items.map(...)} +
+
+ +// Dialog - Modal + + Title + Content + + + +// Alert - Messages +Error message + +// Chip - Tags + + +// Pagination + + +// Grid - Layout + + + +``` + +Powodzenia! 🚀 + diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 7ffdd44..03c7b57 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -15,6 +15,7 @@ import { GuestOnlyRoute } from "./components/Layout/GuestOnlyRoute"; import { DashboardLayout } from "./components/Layout/DashboardLayout"; import { AccountPage } from "./pages/Account/AccountPage"; import { AccountViewPage } from "./pages/Account/AccoutViewPage"; +import { CardPage } from "./pages/Account/Card/CardPage"; import { GuardianApprovalPage } from "./pages/Guardian/GuardianApprovalPage"; import { NotificationBell } from "./components/Notification/NotificationBell"; import { NotificationsPage } from "./pages/Notification/NotificationsPage"; @@ -302,6 +303,7 @@ function App(): JSX.Element { } /> } /> } /> + } /> } /> } /> diff --git a/frontend/src/context/AuthContext.tsx b/frontend/src/context/AuthContext.tsx index 39345b9..263b4b2 100644 --- a/frontend/src/context/AuthContext.tsx +++ b/frontend/src/context/AuthContext.tsx @@ -21,10 +21,6 @@ export const AuthProvider = ({ children }: { children: React.ReactNode }) => { const [user, setUser] = useState(null); const [loading, setLoading] = useState(true); - useEffect(() => { - loadUser(); - }, []); - const loadUser = async (): Promise => { try { const userData = await checkAuth(); @@ -36,6 +32,10 @@ export const AuthProvider = ({ children }: { children: React.ReactNode }) => { } }; + useEffect(() => { + loadUser(); + }, []); + const handleLogin = async (email: string, password: string): Promise => { const userData = await loginWithPassword(email, password); setUser(userData); diff --git a/frontend/src/pages/Account/AccoutViewPage.tsx b/frontend/src/pages/Account/AccoutViewPage.tsx index 95a331e..9c5c791 100644 --- a/frontend/src/pages/Account/AccoutViewPage.tsx +++ b/frontend/src/pages/Account/AccoutViewPage.tsx @@ -99,6 +99,8 @@ export const AccountViewPage = () => { try { await blockCard(cardId); await loadData(); + + showSuccess('Karta została zablokowana!'); } catch (err: any) { setError(err.message); } @@ -108,6 +110,8 @@ export const AccountViewPage = () => { try { await unblockCard(cardId); await loadData(); + + showSuccess('Karta została odblokowana!'); } catch (err: any) { setError(err.message); } @@ -119,6 +123,8 @@ export const AccountViewPage = () => { await deleteCard(deleteCardId); setDeleteCardId(null); await loadData(); + + showSuccess('Karta została usunięta!'); } catch (err: any) { setError(err.message); } diff --git a/frontend/src/pages/Account/Card/CardPage.tsx b/frontend/src/pages/Account/Card/CardPage.tsx index e69de29..abce87e 100644 --- a/frontend/src/pages/Account/Card/CardPage.tsx +++ b/frontend/src/pages/Account/Card/CardPage.tsx @@ -0,0 +1,423 @@ +import { useEffect, useState } from 'react'; +import { + Box, + Typography, + Card, + CardContent, + Grid, + Chip, + Skeleton, + Alert, + Pagination, + Stack, + Button, + Dialog, + DialogTitle, + DialogContent, + DialogActions, + TableContainer, + Table, + TableHead, + TableRow, + TableCell, + TableBody, + Paper, + IconButton, + Divider, +} from '@mui/material'; +import { + CreditCard as CardIcon, + Block as BlockIcon, + LockOpen as UnblockIcon, + Delete as DeleteIcon, + Visibility as ViewIcon, +} from '@mui/icons-material'; +import { getAllCards, blockCard, unblockCard, deleteCard } from '../../../services/accountService'; +import type { Card as CardType, PaginationMeta } from '../../../types/types'; +import { useToast } from '../../../context/ToastContext'; +import { formatDate } from '../../../utils/formatDate'; + +export const CardPage = () => { + const [cards, setCards] = useState([]); + const [meta, setMeta] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [page, setPage] = useState(1); + const { showSuccess, showError } = useToast(); + + // Delete confirmation + const [deleteCardId, setDeleteCardId] = useState(null); + + // Card details + const [selectedCard, setSelectedCard] = useState(null); + const [openDetailsDialog, setOpenDetailsDialog] = useState(false); + + const loadCards = async (pageNumber: number = 1) => { + setLoading(true); + setError(null); + try { + const response = await getAllCards(pageNumber); + setCards(response.data); + setMeta(response.meta); + } catch (err: any) { + setError(err.message || 'Błąd pobierania kart'); + showError(err.message || 'Błąd pobierania kart'); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + loadCards(page); + }, [page]); + + const handlePageChange = (_: React.ChangeEvent, value: number) => { + setPage(value); + }; + + const handleBlockCard = async (cardId: string) => { + try { + await blockCard(cardId); + await loadCards(page); + showSuccess('Karta została zablokowana!'); + } catch (err: any) { + const errorMsg = err.message || 'Błąd blokowania karty'; + setError(errorMsg); + showError(errorMsg); + } + }; + + const handleUnblockCard = async (cardId: string) => { + try { + await unblockCard(cardId); + await loadCards(page); + showSuccess('Karta została odblokowana!'); + } catch (err: any) { + const errorMsg = err.message || 'Błąd odblokowywania karty'; + setError(errorMsg); + showError(errorMsg); + } + }; + + const handleDeleteCard = async () => { + if (!deleteCardId) return; + try { + await deleteCard(deleteCardId); + setDeleteCardId(null); + await loadCards(page); + showSuccess('Karta została usunięta!'); + } catch (err: any) { + const errorMsg = err.message || 'Błąd usuwania karty'; + setError(errorMsg); + showError(errorMsg); + } + }; + + const handleViewDetails = (card: CardType) => { + setSelectedCard(card); + setOpenDetailsDialog(true); + }; + + const getStatusColor = (status: string): 'success' | 'error' | 'warning' | 'default' => { + switch (status) { + case 'active': + return 'success'; + case 'blocked': + return 'error'; + case 'inactive': + return 'warning'; + case 'expired': + return 'default'; + default: + return 'default'; + } + }; + + const getStatusLabel = (status: string): string => { + switch (status) { + case 'active': + return 'Aktywna'; + case 'blocked': + return 'Zablokowana'; + case 'inactive': + return 'Nieaktywna'; + case 'expired': + return 'Wygasła'; + default: + return status; + } + }; + + const getCardTypeLabel = (type: string): string => { + switch (type) { + case 'debit': + return 'Debetowa'; + case 'credit': + return 'Kredytowa'; + case 'virtual': + return 'Wirtualna'; + default: + return type; + } + }; + + return ( + + + Moje karty płatnicze + + + {error && ( + setError(null)}> + {error} + + )} + + {loading ? ( + + {[1, 2, 3].map((i) => ( + + + + ))} + + ) : cards.length === 0 ? ( + + + + + Brak kart płatniczych + + + Utwórz swoją pierwszą kartę płatniczą na stronie kont + + + + ) : ( + <> + + + + + Karta + Network + Typ + Ważność + Status + Data utworzenia + Akcje + + + + {cards.map((card) => ( + + + + + + + •••• {card.card_last_four} + + + {card.card_number} + + + + + + + + + + {getCardTypeLabel(card.type)} + + + + + {String(card.exp_month).padStart(2, '0')}/{card.exp_year} + + + + + + + + {formatDate(card.created_at)} + + + + + handleViewDetails(card)} + title="Szczegóły" + > + + + {card.status === 'active' && ( + handleBlockCard(card.id)} + title="Zablokuj" + > + + + )} + {card.status === 'blocked' && ( + handleUnblockCard(card.id)} + title="Odblokuj" + > + + + )} + setDeleteCardId(card.id)} + title="Usuń" + > + + + + + + ))} + +
+
+ + {meta && meta.last_page > 1 && ( + + + + Wyświetlono {meta.from ?? 0}-{meta.to ?? 0} z {meta.total} kart + + + )} + + )} + + {/* Card Details Dialog */} + setOpenDetailsDialog(false)} maxWidth="sm" fullWidth> + Szczegóły karty + + {selectedCard && ( + + + + Numer karty + + + {selectedCard.card_number} + + + + + + + + Network + + {selectedCard.network.toUpperCase()} + + + + + Typ karty + + {getCardTypeLabel(selectedCard.type)} + + + + + + Ważność + + + {String(selectedCard.exp_month).padStart(2, '0')}/{selectedCard.exp_year} + + + + + CVV + + ••• (ukryte) + + + + + + + + Status + + + + + + + + + + Data utworzenia + + {formatDate(selectedCard.created_at)} + + + + Ostatnia zmiana + + {formatDate(selectedCard.updated_at)} + + + + )} + + + + + + + {/* Delete Confirmation Dialog */} + setDeleteCardId(null)}> + Usunąć kartę? + + + Ta operacja jest nieodwracalna. Karta zostanie trwale usunięta. + + + + + + + +
+ ); +}; + diff --git a/frontend/src/services/accountService.ts b/frontend/src/services/accountService.ts index 8b4ed3a..713fb0c 100644 --- a/frontend/src/services/accountService.ts +++ b/frontend/src/services/accountService.ts @@ -60,6 +60,19 @@ export const getDeleteAccount = async (id: string) => { }; // Card +export const getAllCards = async (page: number = 1, perPage: number = 20): Promise> => { + const response = await fetchWithAuth(`/cards?page=${page}&per_page=${perPage}`, { + method: 'GET', + }); + + if (!response.ok) { + const error: ApiError = await response.json(); + throw new Error(error.message || 'Błąd pobierania kart'); + } + + return await response.json(); +}; + export const getCards = async (accountId: string): Promise> => { const response = await fetchWithAuth(`/accounts/${accountId}/cards`, { method: 'GET', diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index 5a8dbb1..7994f9b 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -16,11 +16,11 @@ export default defineConfig({ changeOrigin: false, secure: false, }, - '/login': { - target: 'http://backend:80', - changeOrigin: false, - secure: false, - } + // '/login': { + // target: 'http://backend:80', + // changeOrigin: false, + // secure: false, + // } } } }) \ No newline at end of file