Przejdź do głównej zawartości

Budżet domowy (rejestr wydatków)

Stworzysz Budżet domowy - aplikację do rejestrowania wydatków i przychodów z podziałem na kategorie. System pozwala śledzić finanse, generować podsumowania i tworzyć raporty miesięczne.

Czego się nauczysz?

  • Walidacji kwot i formatów liczbowych
  • Pracy z kategoriami i typami transakcji
  • Agregowania danych (sumy, średnie)
  • Generowania raportów i statystyk
  • Filtrowania po dacie i kategorii

W prawdziwej pracy...

Aplikacje finansowe są podstawą zarządzania w każdej firmie i gospodarstwie domowym - od rejestrów wydatków, przez systemy księgowe, po aplikacje bankowe. Umiejętność projektowania systemów z walidacją kwot, kategoriami i raportowaniem finansowym jest fundamentem dla każdego programisty aplikacji biznesowych.

  1. Formularz dodawania wpisu Użytkownik podaje typ (wydatek/przychód), kwotę, kategorię, opis i datę.

  2. Walidacja danych System sprawdza poprawność wprowadzonych danych - czy kwota jest dodatnia, czy kategoria jest z dozwolonej listy.

  3. Lista transakcji Wszystkie wpisy są wyświetlane z informacją o typie, kwocie i kategorii.

  4. Podsumowania System pokazuje sumy wydatków i przychodów oraz bilans.

Przykładowa struktura pliku JSON:

{
"transactions": [
{
"id": 1,
"type": "expense",
"amount": 150.50,
"category": "jedzenie",
"description": "Zakupy spożywcze w Biedronce",
"date": "2026-02-10",
"created_at": "2026-02-10 18:30:00"
},
{
"id": 2,
"type": "income",
"amount": 5000.00,
"category": "wynagrodzenie",
"description": "Pensja za luty",
"date": "2026-02-01",
"created_at": "2026-02-01 10:00:00"
},
{
"id": 3,
"type": "expense",
"amount": 89.99,
"category": "rozrywka",
"description": "Netflix + Spotify",
"date": "2026-02-05",
"created_at": "2026-02-05 12:15:00"
},
{
"id": 4,
"type": "expense",
"amount": 1200.00,
"category": "mieszkanie",
"description": "Czynsz za luty",
"date": "2026-02-02",
"created_at": "2026-02-02 09:00:00"
}
]
}
  • Folderbudzet-domowy/
    • index.php (lista transakcji + podsumowanie)
    • dodaj.php (formularz dodawania)
    • kategorie.php (podsumowanie per kategoria)
    • raport.php (raport miesięczny - wariant C)
    • Folderincludes/
      • config.php
      • functions.php
      • validation.php
      • auth.php (wariant C)
    • Folderdata/
      • transactions.json
      • categories.json
      • users.json (wariant C)
    • Foldercss/
      • style.css
    • Folderjs/
      • validation.js
      • charts.js (opcjonalnie)

Wymagane funkcje:

  • Formularz: typ (select), kwota, kategoria (select), opis, data
  • Walidacja PHP (kwota > 0, kategoria z listy)
  • Min. 1 walidacja JavaScript
  • Zapis transakcji do pliku JSON
  • Lista wszystkich transakcji
  • Komunikaty błędów i sukcesu

Przykładowy scenariusz:

Użytkownik wybiera “Wydatek”, wpisuje kwotę 150.50 zł, kategorię “Jedzenie”, opis “Zakupy” i datę. Po zapisie widzi transakcję na liście z czerwoną ikonką wydatku.

Ocena: 3.0

Walidacja transakcji:

$type = $_POST['type'] ?? '';
$amount = $_POST['amount'] ?? '';
$category = $_POST['category'] ?? '';
$description = trim($_POST['description'] ?? '');
$date = $_POST['date'] ?? '';
$validTypes = ['expense', 'income'];
$validCategories = [
'expense' => ['jedzenie', 'mieszkanie', 'transport', 'rozrywka', 'zdrowie', 'inne'],
'income' => ['wynagrodzenie', 'premia', 'freelance', 'inne'],
];
if (!in_array($type, $validTypes)) {
$errors[] = "Wybierz typ transakcji";
}
if (!is_numeric($amount) || $amount <= 0) {
$errors[] = "Kwota musi być liczbą większą od zera";
}
if ($amount > 1000000) {
$errors[] = "Kwota nie może przekraczać 1 000 000 zł";
}
if (!in_array($category, $validCategories[$type] ?? [])) {
$errors[] = "Wybierz poprawną kategorię";
}
if (empty($date)) {
$errors[] = "Data jest wymagana";
}

Kategorie z ikonami i kolorami:

function getExpenseCategories(): array {
return [
'jedzenie' => ['label' => 'Jedzenie', 'icon' => '🍕', 'color' => '#e74c3c'],
'mieszkanie' => ['label' => 'Mieszkanie', 'icon' => '🏠', 'color' => '#3498db'],
'transport' => ['label' => 'Transport', 'icon' => '🚗', 'color' => '#9b59b6'],
'rozrywka' => ['label' => 'Rozrywka', 'icon' => '🎮', 'color' => '#f39c12'],
'zdrowie' => ['label' => 'Zdrowie', 'icon' => '💊', 'color' => '#27ae60'],
'inne' => ['label' => 'Inne', 'icon' => '📦', 'color' => '#95a5a6'],
];
}
function getIncomeCategories(): array {
return [
'wynagrodzenie' => ['label' => 'Wynagrodzenie', 'icon' => '💼', 'color' => '#27ae60'],
'premia' => ['label' => 'Premia', 'icon' => '🎁', 'color' => '#2ecc71'],
'freelance' => ['label' => 'Freelance', 'icon' => '💻', 'color' => '#1abc9c'],
'inne' => ['label' => 'Inne', 'icon' => '💰', 'color' => '#16a085'],
];
}

Dodawanie transakcji:

function addTransaction(array &$transactions, array $data): int {
$newId = empty($transactions) ? 1 : max(array_column($transactions, 'id')) + 1;
$newTransaction = [
'id' => $newId,
'type' => $data['type'],
'amount' => (float) $data['amount'],
'category' => $data['category'],
'description' => $data['description'],
'date' => $data['date'],
'created_at' => date('Y-m-d H:i:s'),
];
$transactions[] = $newTransaction;
return $newId;
}

Obliczanie podsumowań:

function calculateSummary(array $transactions): array {
$income = 0;
$expense = 0;
foreach ($transactions as $t) {
if ($t['type'] === 'income') {
$income += $t['amount'];
} else {
$expense += $t['amount'];
}
}
return [
'income' => $income,
'expense' => $expense,
'balance' => $income - $expense,
];
}
function calculateByCategory(array $transactions, string $type = 'expense'): array {
$filtered = array_filter($transactions, fn($t) => $t['type'] === $type);
$byCategory = [];
foreach ($filtered as $t) {
$category = $t['category'];
if (!isset($byCategory[$category])) {
$byCategory[$category] = 0;
}
$byCategory[$category] += $t['amount'];
}
arsort($byCategory);
return $byCategory;
}

Filtrowanie transakcji:

function filterByType(array $transactions, string $type): array {
if (empty($type) || $type === 'all') {
return $transactions;
}
return array_filter($transactions, fn($t) => $t['type'] === $type);
}
function filterByMonth(array $transactions, string $yearMonth): array {
// $yearMonth format: "2026-02"
return array_filter($transactions, function($t) use ($yearMonth) {
return strpos($t['date'], $yearMonth) === 0;
});
}
function filterByDateRange(array $transactions, string $from, string $to): array {
return array_filter($transactions, function($t) use ($from, $to) {
return $t['date'] >= $from && $t['date'] <= $to;
});
}
function sortByDate(array $transactions, string $order = 'desc'): array {
usort($transactions, function($a, $b) use ($order) {
$result = strcmp($a['date'], $b['date']);
return $order === 'desc' ? -$result : $result;
});
return $transactions;
}

Formatowanie kwot:

function formatAmount(float $amount, string $type = 'expense'): string {
$formatted = number_format($amount, 2, ',', ' ') . '';
if ($type === 'income') {
return '+' . $formatted;
} elseif ($type === 'expense') {
return '-' . $formatted;
}
return $formatted;
}
function formatBalance(float $balance): string {
$formatted = number_format(abs($balance), 2, ',', ' ') . '';
if ($balance > 0) {
return '<span class="positive">+' . $formatted . '</span>';
} elseif ($balance < 0) {
return '<span class="negative">-' . $formatted . '</span>';
}
return '<span class="neutral">' . $formatted . '</span>';
}

Generowanie raportu miesięcznego:

function generateMonthlyReport(array $transactions, string $yearMonth): string {
$filtered = filterByMonth($transactions, $yearMonth);
$summary = calculateSummary($filtered);
$byCategory = calculateByCategory($filtered, 'expense');
$monthName = date('F Y', strtotime($yearMonth . '-01'));
$html = '<!DOCTYPE html><html><head><meta charset="UTF-8">';
$html .= '<title>Raport budżetowy - ' . $monthName . '</title>';
$html .= '<style>
body { font-family: Arial, sans-serif; max-width: 800px; margin: 0 auto; padding: 20px; }
.summary { display: flex; gap: 20px; margin-bottom: 30px; }
.summary-box { flex: 1; padding: 20px; border-radius: 10px; text-align: center; }
.income { background: #d4edda; }
.expense { background: #f8d7da; }
.balance { background: #cce5ff; }
.amount { font-size: 2em; font-weight: bold; }
table { width: 100%; border-collapse: collapse; margin-top: 20px; }
th, td { padding: 10px; border: 1px solid #ddd; text-align: left; }
th { background: #333; color: white; }
</style></head><body>';
$html .= '<h1>Raport budżetowy: ' . htmlspecialchars($monthName) . '</h1>';
$html .= '<div class="summary">';
$html .= '<div class="summary-box income"><div>Przychody</div><div class="amount">' . formatAmount($summary['income'], 'income') . '</div></div>';
$html .= '<div class="summary-box expense"><div>Wydatki</div><div class="amount">' . formatAmount($summary['expense'], 'expense') . '</div></div>';
$html .= '<div class="summary-box balance"><div>Bilans</div><div class="amount">' . formatAmount($summary['balance']) . '</div></div>';
$html .= '</div>';
$html .= '<h2>Wydatki per kategoria</h2>';
$html .= '<table><tr><th>Kategoria</th><th>Kwota</th><th>Udział</th></tr>';
$totalExpense = $summary['expense'] ?: 1;
foreach ($byCategory as $category => $amount) {
$percent = round(($amount / $totalExpense) * 100, 1);
$html .= '<tr>';
$html .= '<td>' . htmlspecialchars($category) . '</td>';
$html .= '<td>' . formatAmount($amount) . '</td>';
$html .= '<td>' . $percent . '%</td>';
$html .= '</tr>';
}
$html .= '</table></body></html>';
return $html;
}

Wykorzystaj lekcje!

Cotygodniowe spotkania podczas lekcji to idealny moment, by:

  • Pokazać postępy - nawet małe kroki się liczą
  • Wyjaśnić wątpliwości - pytaj, nie zgaduj
  • Skonsultować rozwiązania - feedback pomoże Ci się rozwijać

Pracuj iteracyjnie - lepiej mieć działający wariant A niż niedokończony C!