Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion backend/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

39 changes: 24 additions & 15 deletions backend/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -14,23 +14,32 @@ RUN apt-get update && apt-get install -y \
sqlite3 \
&& rm -rf /var/lib/apt/lists/*

# Copy dependencies manifests
COPY Cargo.toml Cargo.lock ./
COPY migration/Cargo.toml ./migration/Cargo.toml
COPY entity/Cargo.toml ./entity/Cargo.toml

# Create a dummy lib.rs to cache dependencies
RUN mkdir -p src && echo "fn main() {}" > src/main.rs
RUN mkdir -p migration/src && touch migration/src/lib.rs
RUN mkdir -p entity/src && touch entity/src/lib.rs
# Copy workspace Cargo files
COPY Cargo.toml ./
COPY migration/Cargo.toml ./migration/
COPY entity/Cargo.toml ./entity/

# Create dummy source files for dependency caching
RUN mkdir -p src && \
echo "fn main() {}" > src/main.rs && \
mkdir -p migration/src && \
echo "pub use sea_orm_migration::prelude::*; pub struct Migrator; impl MigratorTrait for Migrator { fn migrations() -> Vec<Box<dyn MigrationTrait>> { vec![] } }" > migration/src/lib.rs && \
mkdir -p entity/src && \
echo "pub mod entities; pub mod prelude; pub use entities::*;" > entity/src/lib.rs && \
mkdir -p entity/src/entities && \
echo "pub mod prelude { pub use super::*; }" > entity/src/entities/mod.rs

# Build dependencies (this will be cached)
RUN cargo build --release || true

# Copy all source code
COPY migration/src ./migration/src
COPY entity/src ./entity/src
COPY src ./src

# Build the actual application
RUN cargo build --release

# Copy the rest of the source code
COPY . .

# Rebuild with real source
RUN touch src/main.rs && cargo build --release

###################
# Final Runtime Stage
###################
Expand Down
29 changes: 29 additions & 0 deletions backend/Dockerfile.dev
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# Development Dockerfile for FinanceVault Backend
FROM rust:latest

WORKDIR /usr/src/app

# Install system dependencies
RUN apt-get update && apt-get install -y \
pkg-config \
libssl-dev \
sqlite3 \
&& rm -rf /var/lib/apt/lists/*

# Install cargo-watch for hot reload
RUN cargo install cargo-watch

# Create data directory for SQLite
RUN mkdir -p /data

# Expose backend port
EXPOSE 8000

# Set environment variables
ENV DATABASE_URL=sqlite:/data/finance.db
ENV RUST_LOG=debug
ENV CARGO_HOME=/usr/local/cargo

# The source code will be mounted as a volume
# Command will be overridden in docker-compose.dev.yml
CMD ["cargo", "watch", "-x", "run"]
5 changes: 3 additions & 2 deletions docker-compose.dev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ services:
- .env
build:
context: ./backend
dockerfile: Dockerfile
dockerfile: Dockerfile.dev
ports:
- "8000:8000"
volumes:
Expand All @@ -34,7 +34,8 @@ services:
- DATABASE_URL=sqlite:/data/finance.db
- CARGO_HOME=/usr/local/cargo
- JWT_SECRET=${JWT_SECRET}
command: sh -c "cargo install cargo-watch && JWT_SECRET=${JWT_SECRET} cargo watch -x run"
- RUST_LOG=debug
command: cargo watch -x run

volumes:
cargo_registry:
Expand Down
6 changes: 3 additions & 3 deletions frontend/src/app.css
Original file line number Diff line number Diff line change
Expand Up @@ -58,10 +58,10 @@
--border: oklch(1 0 0 / 10%);
--input: oklch(1 0 0 / 15%);
--ring: oklch(0.541 0.281 293.009);
--chart-1: oklch(0.488 0.243 264.376);
--chart-2: oklch(0.696 0.17 162.48);
--chart-1: oklch(59.651% 0.28069 318.49);
--chart-2: oklch(43.953% 0.21478 294.435);
--chart-3: oklch(0.769 0.188 70.08);
--chart-4: oklch(0.627 0.265 303.9);
--chart-4: oklch(57.951% 0.2596 315.282);
--chart-5: oklch(0.645 0.246 16.439);
--sidebar: oklch(0.21 0.006 285.885);
--sidebar-foreground: oklch(0.985 0 0);
Expand Down
82 changes: 82 additions & 0 deletions frontend/src/lib/components/budget/budget-overview-card.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
<script lang="ts">
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "$lib/components/ui/card";
import { Progress } from "$lib/components/ui/progress";
import { TrendingUp, TrendingDown, AlertCircle } from "@lucide/svelte";
import type { BudgetOverview } from "$lib/types";

interface Props {
overview: BudgetOverview | null;
}

let { overview }: Props = $props();

function getStatusColor(percentage: number): string {
if (percentage >= 100) return "text-red-600";
if (percentage >= 80) return "text-yellow-600";
return "text-green-600";
}

function getProgressColor(percentage: number): string {
if (percentage >= 100) return "bg-red-600";
if (percentage >= 80) return "bg-yellow-600";
return "bg-green-600";
}
</script>

{#if overview}
<Card class="w-full">
<CardHeader>
<CardTitle class="text-2xl">Budget-Übersicht</CardTitle>
<CardDescription>
{new Date(overview.budget.month + "-01").toLocaleDateString("de-DE", {
month: "long",
year: "numeric",
})}
</CardDescription>
</CardHeader>
<CardContent class="space-y-6">
<!-- Category Breakdown -->
<div class="space-y-4">
<h3 class="text-lg font-semibold">Kategorien</h3>
{#each overview.categories as category}
<div class="space-y-2">
<div class="flex justify-between text-sm">
<span class="font-medium">{category.category}</span>
<span class={getStatusColor(category.percentage_used)}>
€{category.spent.toFixed(2)} / €{category.allocated.toFixed(2)}
</span>
</div>
<Progress
value={category.percentage_used}
class={getProgressColor(category.percentage_used)}
/>
<div class="flex justify-between text-xs text-muted-foreground">
<span>
{#if category.remaining < 0}
Überschritten um €{Math.abs(category.remaining).toFixed(2)}
{:else}
Verbleibend: €{category.remaining.toFixed(2)}
{/if}
</span>
<span>{category.percentage_used.toFixed(1)}%</span>
</div>
</div>
{/each}
</div>
</CardContent>
</Card>
{:else}
<Card class="w-full">
<CardContent class="py-12">
<p class="text-center text-muted-foreground">
Für diesen Monat ist noch kein Budget festgelegt
</p>
</CardContent>
</Card>
{/if}
184 changes: 184 additions & 0 deletions frontend/src/lib/components/budget/budget-radial-chart.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
<script lang="ts">
import * as Chart from "$lib/components/ui/chart/index.js";
import * as Card from "$lib/components/ui/card/index.js";
import { PieChart, Text } from "layerchart";
import { Progress } from "$lib/components/ui/progress";
import { TrendingUp, TrendingDown, AlertCircle } from "@lucide/svelte";
import type { BudgetOverview } from "$lib/types";

interface Props {
overview: BudgetOverview | null;
}

let { overview }: Props = $props();

// Nur zwei Farben: Ausgegeben und Verfügbar
const spentColor = "hsl(221.2, 83.2%, 53.3%)"; // Blau für ausgegeben
const availableColor = "hsl(240, 5.9%, 90%)"; // Grau für verfügbar

let chartData = $derived.by(() => {
if (!overview) return [];

const spent = overview.total_spent;
const remaining = Math.max(0, overview.remaining); // Nur positive Werte

return [
{
label: "Ausgegeben",
amount: spent,
color: spentColor,
},
{
label: "Verfügbar",
amount: remaining,
color: availableColor,
},
];
});

let chartConfig = $derived({
ausgegeben: {
label: "Ausgegeben",
color: spentColor,
},
verfügbar: {
label: "Verfügbar",
color: availableColor,
},
});

let totalSpent = $derived(overview?.total_spent ?? 0);
let totalBudget = $derived(overview?.budget.total_budget ?? 0);
let percentageUsed = $derived(overview?.percentage_used ?? 0);
let remaining = $derived(overview?.remaining ?? 0);

function getStatusColor(percentage: number): string {
if (percentage >= 100) return "text-red-600";
if (percentage >= 80) return "text-yellow-600";
return "text-green-600";
}

function getProgressColor(percentage: number): string {
if (percentage >= 100) return "bg-red-600";
if (percentage >= 80) return "bg-yellow-600";
return "bg-green-600";
}
</script>

{#if overview}
<Card.Root>
<Card.Header class="items-center pb-0">
<Card.Title class="text-2xl">Budget-Verteilung</Card.Title>
<Card.Description>
{new Date(overview.budget.month + "-01").toLocaleDateString("de-DE", {
month: "long",
year: "numeric",
})}
</Card.Description>
</Card.Header>
<Card.Content class="pb-0">
<!-- Radial Chart -->
<Chart.Container
config={chartConfig}
class="mx-auto aspect-square w-full max-h-[500px]"
>
<PieChart
data={chartData}
key="label"
value="amount"
c="color"
innerRadius={120}
padding={50}
range={[-90, 90]}
props={{ pie: { sort: null } }}
cornerRadius={8}
>
{#snippet aboveMarks()}
<Text
value={`€${totalSpent.toFixed(0)}`}
textAnchor="middle"
verticalAnchor="middle"
class="fill-foreground text-5xl! font-bold"
dy={-25}
/>
<Text
value="ausgegeben"
textAnchor="middle"
verticalAnchor="middle"
class="fill-muted-foreground! text-muted-foreground text-lg!"
dy={5}
/>
<Text
value={`von €${totalBudget.toFixed(0)}`}
textAnchor="middle"
verticalAnchor="middle"
class="fill-muted-foreground! text-muted-foreground text-base!"
dy={30}
/>
{/snippet}
{#snippet tooltip()}
<Chart.Tooltip hideLabel />
{/snippet}
</PieChart>
</Chart.Container>
<!-- Total Budget Block -->
<div class="p-6 border rounded-lg space-y-4 bg-muted/30">
<div class="flex justify-between items-start">
<div>
<p class="text-sm text-muted-foreground">Gesamtbudget</p>
<p class="text-3xl font-bold">
€{overview.budget.total_budget.toFixed(2)}
</p>
</div>
<div class="text-right">
<p class="text-sm text-muted-foreground">Ausgegeben</p>
<p
class="text-2xl font-semibold {getStatusColor(
overview.percentage_used
)}"
>
€{overview.total_spent.toFixed(2)}
</p>
</div>
</div>

<Progress
value={overview.percentage_used}
class={getProgressColor(overview.percentage_used)}
/>

<div class="flex justify-between items-center text-sm">
<div class="flex items-center gap-2">
{#if overview.remaining < 0}
<TrendingDown class="h-4 w-4 text-red-600" />
<span class="text-red-600 font-medium">
Überschritten um €{Math.abs(overview.remaining).toFixed(2)}
</span>
{:else if overview.percentage_used >= 80}
<AlertCircle class="h-4 w-4 text-yellow-600" />
<span class="text-yellow-600 font-medium">
Verbleibend: €{overview.remaining.toFixed(2)}
</span>
{:else}
<TrendingUp class="h-4 w-4 text-green-600" />
<span class="text-green-600 font-medium">
Verbleibend: €{overview.remaining.toFixed(2)}
</span>
{/if}
</div>
<span class="text-muted-foreground"
>{overview.percentage_used.toFixed(1)}% verwendet</span
>
</div>
</div>
</Card.Content>
</Card.Root>
{:else}
<Card.Root>
<Card.Content class="py-12 flex items-center justify-center">
<p class="text-center text-muted-foreground">
Keine Budget-Daten verfügbar
</p>
</Card.Content>
</Card.Root>
{/if}
Loading