fertig bis auf Tankstellen und Graph
This commit is contained in:
56
.dockerignore
Normal file
56
.dockerignore
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
# .dockerignore
|
||||||
|
# Git
|
||||||
|
.git
|
||||||
|
.gitignore
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.idea
|
||||||
|
.vscode
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
*~
|
||||||
|
|
||||||
|
# Flutter/Dart
|
||||||
|
.dart_tool/
|
||||||
|
.packages
|
||||||
|
build/
|
||||||
|
.flutter-plugins
|
||||||
|
.flutter-plugins-dependencies
|
||||||
|
.metadata
|
||||||
|
|
||||||
|
# Node.js (for proxy)
|
||||||
|
node_modules/
|
||||||
|
npm-debug.log
|
||||||
|
yarn-error.log
|
||||||
|
|
||||||
|
# Test
|
||||||
|
test/
|
||||||
|
integration_test/
|
||||||
|
.test_coverage/
|
||||||
|
|
||||||
|
# Documentation
|
||||||
|
*.md
|
||||||
|
!README.md
|
||||||
|
|
||||||
|
# CI/CD
|
||||||
|
.github/
|
||||||
|
|
||||||
|
# Local development
|
||||||
|
.env.local
|
||||||
|
*.log
|
||||||
|
|
||||||
|
# macOS
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
# Windows
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# Linux
|
||||||
|
*~
|
||||||
|
|
||||||
|
# Other
|
||||||
|
*.iml
|
||||||
|
*.class
|
||||||
|
*.lock
|
||||||
|
!pubspec.lock
|
||||||
|
!package-lock.json
|
||||||
117
DEPLOYMENT.md
Normal file
117
DEPLOYMENT.md
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
# Backend-Proxy Setup - Deployment Anleitung
|
||||||
|
|
||||||
|
## 📋 Übersicht
|
||||||
|
|
||||||
|
Diese Anleitung erklärt, wie Sie die Appwrite Function für Geocoding deployen, um CORS-Probleme zu vermeiden.
|
||||||
|
|
||||||
|
## 🚀 Deployment Schritte
|
||||||
|
|
||||||
|
### 1. Appwrite Console öffnen
|
||||||
|
|
||||||
|
Navigieren Sie zu Ihrer Appwrite Console:
|
||||||
|
`https://appwrite.joshihomeserver.ipv64.net/console`
|
||||||
|
|
||||||
|
### 2. Function erstellen
|
||||||
|
|
||||||
|
1. Klicken Sie auf **Functions** im Menü
|
||||||
|
2. Klicken Sie auf **Create Function**
|
||||||
|
3. Füllen Sie die Details aus:
|
||||||
|
- **Name**: `geocode-location`
|
||||||
|
- **Function ID**: Wird automatisch generiert (z.B. `65a123456789abc`)
|
||||||
|
- **Runtime**: Wählen Sie `Node.js 18.0` oder höher
|
||||||
|
- **Execute Access**: `Any` (damit alle eingeloggten User die Function nutzen können)
|
||||||
|
|
||||||
|
### 3. Code hochladen
|
||||||
|
|
||||||
|
**Option A: Über die Console**
|
||||||
|
1. Gehen Sie zum Tab **Deployment**
|
||||||
|
2. Erstellen Sie ein neues Deployment
|
||||||
|
3. Wählen Sie **Manual** als Deployment-Methode
|
||||||
|
4. Laden Sie folgende Dateien hoch:
|
||||||
|
- `appwrite_functions/geocode-location/main.js`
|
||||||
|
- `appwrite_functions/geocode-location/package.json`
|
||||||
|
5. Setzen Sie **Entrypoint**: `main.js`
|
||||||
|
6. Klicken Sie auf **Deploy**
|
||||||
|
|
||||||
|
**Option B: Über CLI** (wenn Appwrite CLI installiert)
|
||||||
|
```bash
|
||||||
|
cd /home/digitalman/Development/flutter_tank_web_app
|
||||||
|
appwrite deploy function
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Function ID in Flutter App eintragen
|
||||||
|
|
||||||
|
1. Nach erfolgreichem Deployment kopieren Sie die **Function ID**
|
||||||
|
2. Öffnen Sie `lib/config/environment.dart`
|
||||||
|
3. Ersetzen Sie `YOUR_FUNCTION_ID_HERE` mit Ihrer Function ID:
|
||||||
|
|
||||||
|
```dart
|
||||||
|
static const String appwriteGeocodeFunctionId = '65a123456789abc'; // Ihre ID hier
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. Testen
|
||||||
|
|
||||||
|
**In der Appwrite Console:**
|
||||||
|
1. Gehen Sie zu Ihrer Function
|
||||||
|
2. Klicken Sie auf **Execute**
|
||||||
|
3. Verwenden Sie diesen Test-Body:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"lat": 47.9385165,
|
||||||
|
"lon": 13.762887,
|
||||||
|
"apiKey": "NTYxMDQ3NTY2OWI3NDI5ZGIzZWIxOWNiNTNhMDEwODY6YTQ4MTJhYzYtYmYzOC00ZmE4LTk4YzYtZDBjNzYyZTAyNjBk"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
4. Erwartete Response:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"location": "Straßenname Hausnummer, PLZ Stadt",
|
||||||
|
"coordinates": {
|
||||||
|
"lat": 47.9385165,
|
||||||
|
"lon": 13.762887
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**In der Flutter App:**
|
||||||
|
1. Starten Sie die App neu
|
||||||
|
2. Erstellen Sie einen neuen Tankeintrag
|
||||||
|
3. Die Standortabfrage sollte nun über den Backend-Proxy laufen
|
||||||
|
4. Sie sollten eine Erfolgs-Snackbar mit der Adresse sehen
|
||||||
|
|
||||||
|
## 🔧 Troubleshooting
|
||||||
|
|
||||||
|
### Function ID nicht gesetzt
|
||||||
|
**Problem:** Console-Warnung: "Appwrite Function ID nicht konfiguriert"
|
||||||
|
**Lösung:** Schritt 4 nochmal durchführen und App neu starten
|
||||||
|
|
||||||
|
### Function Execution fehlgeschlagen
|
||||||
|
**Problem:** Status ist nicht "completed"
|
||||||
|
**Lösung:**
|
||||||
|
1. Prüfen Sie die Logs in der Appwrite Console
|
||||||
|
2. Stellen Sie sicher, dass die Runtime korrekt ist
|
||||||
|
3. Prüfen Sie ob alle Dateien hochgeladen wurden
|
||||||
|
|
||||||
|
### PTV API Fehler
|
||||||
|
**Problem:** "PTV API Fehler - Status Code: 401"
|
||||||
|
**Lösung:** API Key in `environment.dart` überprüfen
|
||||||
|
|
||||||
|
### CORS weiterhin ein Problem
|
||||||
|
**Problem:** Immer noch CORS-Fehler
|
||||||
|
**Lösung:**
|
||||||
|
1. Stellen Sie sicher, dass die alte `getNearbyLocation` Methode nicht mehr verwendet wird
|
||||||
|
2. Prüfen Sie, dass `appwriteService.geocodeLocation` aufgerufen wird
|
||||||
|
3. Cachen leeren und App neu builden
|
||||||
|
|
||||||
|
## ✅ Erfolgskriterien
|
||||||
|
|
||||||
|
Nach erfolgreichem Deployment sollten Sie:
|
||||||
|
- ✅ Eine Erfolgs-Snackbar mit Adresse sehen (nicht nur Koordinaten)
|
||||||
|
- ✅ Keine CORS-Fehler mehr in der Browser-Console
|
||||||
|
- ✅ In den Logs: "📍 Verwende Backend-Proxy für Geocoding..."
|
||||||
|
- ✅ In den Logs: "✅ Geocoding erfolgreich: [Adresse]"
|
||||||
|
|
||||||
|
## 📝 Alternative ohne Deployment
|
||||||
|
|
||||||
|
Falls Sie die Function nicht deployen möchten, wird automatisch auf Koordinaten-Speicherung zurückgegriffen. Die App funktioniert weiterhin, speichert aber `Lat: XX.XXXXXX, Lon: YY.YYYYYY` statt der Adresse.
|
||||||
363
DOCKER_DEPLOYMENT.md
Normal file
363
DOCKER_DEPLOYMENT.md
Normal file
@@ -0,0 +1,363 @@
|
|||||||
|
# 🐳 Docker Deployment Anleitung
|
||||||
|
|
||||||
|
## Voraussetzungen
|
||||||
|
|
||||||
|
- Docker installiert (`docker --version`)
|
||||||
|
- Docker Compose installiert (`docker-compose --version`)
|
||||||
|
- Min. 2 GB freier RAM
|
||||||
|
- Min. 5 GB freier Speicherplatz
|
||||||
|
|
||||||
|
## 🚀 Schnellstart
|
||||||
|
|
||||||
|
### Option 1: Mit Deploy-Script (Empfohlen)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./deploy.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Folgen Sie den Anweisungen im interaktiven Menü.
|
||||||
|
|
||||||
|
### Option 2: Manuelle Commands
|
||||||
|
|
||||||
|
**Alles bauen und starten:**
|
||||||
|
```bash
|
||||||
|
docker-compose up -d --build
|
||||||
|
```
|
||||||
|
|
||||||
|
**Nur starten (bereits gebaut):**
|
||||||
|
```bash
|
||||||
|
docker-compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
**Stoppen:**
|
||||||
|
```bash
|
||||||
|
docker-compose down
|
||||||
|
```
|
||||||
|
|
||||||
|
**Logs anzeigen:**
|
||||||
|
```bash
|
||||||
|
docker-compose logs -f
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📦 Was wird deployed?
|
||||||
|
|
||||||
|
### 1. Flutter Web App Container
|
||||||
|
- **Image:** `flutter-tank-web:latest`
|
||||||
|
- **Port:** 8080 (Host) → 80 (Container)
|
||||||
|
- **URL:** `http://localhost:8080`
|
||||||
|
- **Technologie:** Flutter Web + Nginx
|
||||||
|
|
||||||
|
### 2. Proxy Server Container (Optional)
|
||||||
|
- **Image:** `ptv-proxy:latest`
|
||||||
|
- **Port:** 3000 (Host) → 3000 (Container)
|
||||||
|
- **URL:** `http://localhost:3000`
|
||||||
|
- **Technologie:** Node.js
|
||||||
|
|
||||||
|
## 🏗️ Build-Prozess
|
||||||
|
|
||||||
|
Der Build läuft in 2 Phasen:
|
||||||
|
|
||||||
|
**Phase 1: Flutter Build (Multi-Stage)**
|
||||||
|
1. Ubuntu base image
|
||||||
|
2. Flutter SDK installieren
|
||||||
|
3. Dependencies installieren (`flutter pub get`)
|
||||||
|
4. Web App bauen (`flutter build web`)
|
||||||
|
|
||||||
|
**Phase 2: Production (Nginx)**
|
||||||
|
1. Alpine Nginx image
|
||||||
|
2. Gebaute App kopieren
|
||||||
|
3. Nginx konfigurieren
|
||||||
|
4. Port 80 exponieren
|
||||||
|
|
||||||
|
**Geschätzte Build-Zeit:** 5-10 Minuten (beim ersten Mal)
|
||||||
|
|
||||||
|
## 🌐 Auf Server deployen
|
||||||
|
|
||||||
|
### 1. Server vorbereiten
|
||||||
|
|
||||||
|
**SSH zum Server:**
|
||||||
|
```bash
|
||||||
|
ssh user@your-server.com
|
||||||
|
```
|
||||||
|
|
||||||
|
**Docker installieren (falls nicht vorhanden):**
|
||||||
|
```bash
|
||||||
|
curl -fsSL https://get.docker.com -o get-docker.sh
|
||||||
|
sudo sh get-docker.sh
|
||||||
|
sudo usermod -aG docker $USER
|
||||||
|
```
|
||||||
|
|
||||||
|
**Docker Compose installieren:**
|
||||||
|
```bash
|
||||||
|
sudo curl -L "https://github.com/docker/compose/releases/latest/download/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
|
||||||
|
sudo chmod +x /usr/local/bin/docker-compose
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Code auf Server übertragen
|
||||||
|
|
||||||
|
**Option A: Git Clone**
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/your-repo/flutter_tank_web_app.git
|
||||||
|
cd flutter_tank_web_app
|
||||||
|
```
|
||||||
|
|
||||||
|
**Option B: SCP/Rsync**
|
||||||
|
```bash
|
||||||
|
# Von lokalem Rechner aus
|
||||||
|
rsync -avz --exclude 'build' --exclude '.dart_tool' \
|
||||||
|
/home/digitalman/Development/flutter_tank_web_app/ \
|
||||||
|
user@your-server.com:/opt/flutter-tank-app/
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Environment anpassen
|
||||||
|
|
||||||
|
Bearbeiten Sie `lib/config/environment.dart`:
|
||||||
|
```dart
|
||||||
|
static const bool useLocalProxy = false; // Für Produktion
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Deployen
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /opt/flutter-tank-app
|
||||||
|
./deploy.sh
|
||||||
|
# Wählen Sie Option 3: "Alles bauen und starten"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. Mit Domain verbinden (Optional)
|
||||||
|
|
||||||
|
**Nginx Reverse Proxy konfigurieren:**
|
||||||
|
```nginx
|
||||||
|
# /etc/nginx/sites-available/tank.yourdomain.com
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name tank.yourdomain.com;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
proxy_pass http://localhost:8080;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Aktivieren:**
|
||||||
|
```bash
|
||||||
|
sudo ln -s /etc/nginx/sites-available/tank.yourdomain.com /etc/nginx/sites-enabled/
|
||||||
|
sudo nginx -t
|
||||||
|
sudo systemctl reload nginx
|
||||||
|
```
|
||||||
|
|
||||||
|
**SSL mit Let's Encrypt:**
|
||||||
|
```bash
|
||||||
|
sudo apt install certbot python3-certbot-nginx
|
||||||
|
sudo certbot --nginx -d tank.yourdomain.com
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔧 Konfiguration
|
||||||
|
|
||||||
|
### Ports ändern
|
||||||
|
|
||||||
|
In `docker-compose.yml`:
|
||||||
|
```yaml
|
||||||
|
ports:
|
||||||
|
- "8080:80" # Ändern Sie 8080 auf gewünschten Port
|
||||||
|
```
|
||||||
|
|
||||||
|
### Umgebungsvariablen
|
||||||
|
|
||||||
|
Erstellen Sie `.env` Datei:
|
||||||
|
```env
|
||||||
|
# App Settings
|
||||||
|
APP_PORT=8080
|
||||||
|
PROXY_PORT=3000
|
||||||
|
|
||||||
|
# Domain (für Traefik)
|
||||||
|
DOMAIN=tank.yourdomain.com
|
||||||
|
```
|
||||||
|
|
||||||
|
### Mit Traefik (Reverse Proxy)
|
||||||
|
|
||||||
|
Die `docker-compose.yml` enthält bereits Traefik Labels.
|
||||||
|
|
||||||
|
**Traefik setup:**
|
||||||
|
```yaml
|
||||||
|
# docker-compose.traefik.yml
|
||||||
|
version: '3.8'
|
||||||
|
services:
|
||||||
|
traefik:
|
||||||
|
image: traefik:v2.10
|
||||||
|
command:
|
||||||
|
- "--providers.docker=true"
|
||||||
|
- "--entrypoints.web.address=:80"
|
||||||
|
- "--entrypoints.websecure.address=:443"
|
||||||
|
- "--certificatesresolvers.letsencrypt.acme.email=your@email.com"
|
||||||
|
- "--certificatesresolvers.letsencrypt.acme.storage=/letsencrypt/acme.json"
|
||||||
|
- "--certificatesresolvers.letsencrypt.acme.httpchallenge.entrypoint=web"
|
||||||
|
ports:
|
||||||
|
- "80:80"
|
||||||
|
- "443:443"
|
||||||
|
volumes:
|
||||||
|
- "/var/run/docker.sock:/var/run/docker.sock:ro"
|
||||||
|
- "./letsencrypt:/letsencrypt"
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📊 Monitoring
|
||||||
|
|
||||||
|
### Container Status prüfen
|
||||||
|
```bash
|
||||||
|
docker-compose ps
|
||||||
|
```
|
||||||
|
|
||||||
|
### Logs anschauen
|
||||||
|
```bash
|
||||||
|
# Alle Services
|
||||||
|
docker-compose logs -f
|
||||||
|
|
||||||
|
# Nur Web App
|
||||||
|
docker-compose logs -f flutter-tank-web
|
||||||
|
|
||||||
|
# Nur Proxy
|
||||||
|
docker-compose logs -f ptv-proxy
|
||||||
|
```
|
||||||
|
|
||||||
|
### Resource Usage
|
||||||
|
```bash
|
||||||
|
docker stats
|
||||||
|
```
|
||||||
|
|
||||||
|
### Health Check
|
||||||
|
```bash
|
||||||
|
# Web App
|
||||||
|
curl http://localhost:8080/health
|
||||||
|
|
||||||
|
# Proxy
|
||||||
|
curl http://localhost:3000/health
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔄 Updates deployen
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Code aktualisieren
|
||||||
|
git pull
|
||||||
|
|
||||||
|
# Neu bauen und starten
|
||||||
|
docker-compose up -d --build
|
||||||
|
|
||||||
|
# Alte Images aufräumen
|
||||||
|
docker image prune -f
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🛠️ Troubleshooting
|
||||||
|
|
||||||
|
### Container startet nicht
|
||||||
|
```bash
|
||||||
|
# Logs prüfen
|
||||||
|
docker-compose logs flutter-tank-web
|
||||||
|
|
||||||
|
# Container Status
|
||||||
|
docker-compose ps
|
||||||
|
|
||||||
|
# Neu starten
|
||||||
|
docker-compose restart flutter-tank-web
|
||||||
|
```
|
||||||
|
|
||||||
|
### Port bereits belegt
|
||||||
|
```bash
|
||||||
|
# Prozess auf Port finden
|
||||||
|
sudo lsof -i :8080
|
||||||
|
|
||||||
|
# Port in docker-compose.yml ändern
|
||||||
|
```
|
||||||
|
|
||||||
|
### Out of Memory
|
||||||
|
```bash
|
||||||
|
# Docker Memory Limit erhöhen
|
||||||
|
# In docker-compose.yml unter service:
|
||||||
|
deploy:
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
memory: 2G
|
||||||
|
```
|
||||||
|
|
||||||
|
### Build schlägt fehl
|
||||||
|
```bash
|
||||||
|
# Docker Cache leeren
|
||||||
|
docker builder prune -a
|
||||||
|
|
||||||
|
# Komplett neu bauen
|
||||||
|
docker-compose build --no-cache
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📦 Backup & Restore
|
||||||
|
|
||||||
|
### Volumes sichern
|
||||||
|
```bash
|
||||||
|
# Data Volume backup
|
||||||
|
docker run --rm -v flutter-tank-data:/data -v $(pwd):/backup alpine tar czf /backup/data-backup.tar.gz -C /data .
|
||||||
|
```
|
||||||
|
|
||||||
|
### Image speichern
|
||||||
|
```bash
|
||||||
|
# Image exportieren
|
||||||
|
docker save flutter-tank-web:latest | gzip > flutter-tank-web.tar.gz
|
||||||
|
|
||||||
|
# Image importieren
|
||||||
|
docker load < flutter-tank-web.tar.gz
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔒 Sicherheit
|
||||||
|
|
||||||
|
### Best Practices
|
||||||
|
|
||||||
|
1. **Nicht als Root laufen** (bereits in Proxy-Dockerfile implementiert)
|
||||||
|
2. **Security Headers** (bereits in nginx.conf)
|
||||||
|
3. **Firewall konfigurieren:**
|
||||||
|
```bash
|
||||||
|
sudo ufw allow 80/tcp
|
||||||
|
sudo ufw allow 443/tcp
|
||||||
|
sudo ufw enable
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Regular Updates:**
|
||||||
|
```bash
|
||||||
|
# Base images updaten
|
||||||
|
docker-compose pull
|
||||||
|
docker-compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🎯 Performance Optimierung
|
||||||
|
|
||||||
|
### Nginx Caching
|
||||||
|
Bereits in `nginx.conf` konfiguriert:
|
||||||
|
- Static assets: 1 Jahr Cache
|
||||||
|
- Gzip Kompression aktiv
|
||||||
|
|
||||||
|
### Docker Image Size
|
||||||
|
```bash
|
||||||
|
# Image Größe prüfen
|
||||||
|
docker images flutter-tank-web
|
||||||
|
|
||||||
|
# Multi-stage build reduziert Größe bereits deutlich
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📝 Checkliste Produktion
|
||||||
|
|
||||||
|
- [ ] `useLocalProxy = false` in environment.dart
|
||||||
|
- [ ] Domain konfiguriert
|
||||||
|
- [ ] SSL Zertifikat installiert
|
||||||
|
- [ ] Firewall konfiguriert
|
||||||
|
- [ ] Backup-Strategie definiert
|
||||||
|
- [ ] Monitoring eingerichtet
|
||||||
|
- [ ] Logs rotieren konfiguriert
|
||||||
|
- [ ] Update-Prozess dokumentiert
|
||||||
|
|
||||||
|
## 🆘 Support
|
||||||
|
|
||||||
|
Bei Problemen:
|
||||||
|
1. Logs prüfen: `docker-compose logs`
|
||||||
|
2. Container Status: `docker-compose ps`
|
||||||
|
3. Health Checks: `curl http://localhost:8080/health`
|
||||||
|
4. Docker System: `docker system df`
|
||||||
51
Dockerfile
Normal file
51
Dockerfile
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
# Dockerfile für Flutter Web App
|
||||||
|
FROM ubuntu:22.04 AS build
|
||||||
|
|
||||||
|
# Avoid interactive prompts
|
||||||
|
ENV DEBIAN_FRONTEND=noninteractive
|
||||||
|
|
||||||
|
# Install dependencies
|
||||||
|
RUN apt-get update && apt-get install -y \
|
||||||
|
curl \
|
||||||
|
git \
|
||||||
|
unzip \
|
||||||
|
xz-utils \
|
||||||
|
zip \
|
||||||
|
libglu1-mesa \
|
||||||
|
wget \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# Install Flutter
|
||||||
|
ENV FLUTTER_HOME=/usr/local/flutter
|
||||||
|
ENV PATH="${FLUTTER_HOME}/bin:${PATH}"
|
||||||
|
|
||||||
|
RUN git clone https://github.com/flutter/flutter.git ${FLUTTER_HOME} -b stable --depth 1 && \
|
||||||
|
flutter config --enable-web && \
|
||||||
|
flutter precache --web
|
||||||
|
|
||||||
|
# Set working directory
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Copy app files
|
||||||
|
COPY pubspec.yaml pubspec.lock ./
|
||||||
|
RUN flutter pub get
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# Build web app
|
||||||
|
RUN flutter build web --release --web-renderer canvaskit
|
||||||
|
|
||||||
|
# Production stage - Nginx
|
||||||
|
FROM nginx:alpine
|
||||||
|
|
||||||
|
# Copy built app to nginx
|
||||||
|
COPY --from=build /app/build/web /usr/share/nginx/html
|
||||||
|
|
||||||
|
# Copy custom nginx config
|
||||||
|
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
||||||
|
|
||||||
|
# Expose port
|
||||||
|
EXPOSE 80
|
||||||
|
|
||||||
|
# Start nginx
|
||||||
|
CMD ["nginx", "-g", "daemon off;"]
|
||||||
86
QUICKSTART.md
Normal file
86
QUICKSTART.md
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
# 🚀 Quick Start - Lokaler Reverse Proxy
|
||||||
|
|
||||||
|
## Sofort loslegen
|
||||||
|
|
||||||
|
### Terminal 1: Proxy Server starten
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./start-proxy.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Oder manuell:
|
||||||
|
```bash
|
||||||
|
cd proxy-server
|
||||||
|
node server.js
|
||||||
|
```
|
||||||
|
|
||||||
|
### Terminal 2: Flutter App starten
|
||||||
|
|
||||||
|
```bash
|
||||||
|
flutter run -d chrome
|
||||||
|
```
|
||||||
|
|
||||||
|
## ✅ Das wars!
|
||||||
|
|
||||||
|
Die App verwendet jetzt automatisch den lokalen Proxy auf `http://localhost:3000` für Geocoding.
|
||||||
|
|
||||||
|
## 🔧 Konfiguration
|
||||||
|
|
||||||
|
In [lib/config/environment.dart](lib/config/environment.dart):
|
||||||
|
|
||||||
|
```dart
|
||||||
|
static const bool useLocalProxy = true; // true = Proxy, false = Appwrite Function
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📋 Checkliste
|
||||||
|
|
||||||
|
- ✅ Node.js installiert (`node --version`)
|
||||||
|
- ✅ Proxy läuft (`http://localhost:3000` im Browser)
|
||||||
|
- ✅ `useLocalProxy = true` in environment.dart
|
||||||
|
- ✅ Flutter App läuft
|
||||||
|
|
||||||
|
## 🎯 Vorteile Lokaler Proxy
|
||||||
|
|
||||||
|
✅ **Kein CORS-Problem** - Server umgeht Browser-Beschränkungen
|
||||||
|
✅ **Schnelles Testen** - Kein Cloud-Deployment nötig
|
||||||
|
✅ **Einfaches Debugging** - Logs direkt im Terminal
|
||||||
|
✅ **Kostenlos** - Läuft lokal, keine Cloud-Kosten
|
||||||
|
|
||||||
|
## 🌐 Für Produktion
|
||||||
|
|
||||||
|
Für den Live-Betrieb:
|
||||||
|
|
||||||
|
1. Setzen Sie `useLocalProxy = false`
|
||||||
|
2. Deployen Sie die Appwrite Function (siehe [DEPLOYMENT.md](DEPLOYMENT.md))
|
||||||
|
3. Tragen Sie die Function ID ein
|
||||||
|
|
||||||
|
## 📊 Vergleich
|
||||||
|
|
||||||
|
| Feature | Lokaler Proxy | Appwrite Function |
|
||||||
|
|---------|--------------|-------------------|
|
||||||
|
| Setup Zeit | < 1 Minute | ~10 Minuten |
|
||||||
|
| CORS | ✅ Gelöst | ✅ Gelöst |
|
||||||
|
| Kosten | Kostenlos | Serverless (minimal) |
|
||||||
|
| Verwendung | Nur Entwicklung | Entwicklung + Produktion |
|
||||||
|
| Debugging | Sehr einfach | Logs in Console |
|
||||||
|
| Offline | ❌ | ❌ |
|
||||||
|
|
||||||
|
## 🐛 Problemlösung
|
||||||
|
|
||||||
|
**Proxy startet nicht:**
|
||||||
|
```bash
|
||||||
|
# Port 3000 belegt?
|
||||||
|
lsof -i :3000
|
||||||
|
# Prozess beenden
|
||||||
|
kill -9 <PID>
|
||||||
|
```
|
||||||
|
|
||||||
|
**App findet Proxy nicht:**
|
||||||
|
- Proxy läuft? Prüfen Sie `http://localhost:3000` im Browser
|
||||||
|
- `useLocalProxy = true`? Prüfen Sie environment.dart
|
||||||
|
- Hot Restart in Flutter machen
|
||||||
|
|
||||||
|
**Immer noch CORS-Fehler:**
|
||||||
|
- Browser-Cache leeren
|
||||||
|
- DevTools → Network → "Disable cache"
|
||||||
|
- App neu builden: `flutter run -d chrome`
|
||||||
111
STATUS.md
Normal file
111
STATUS.md
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
# ✅ System Status - Alles Repariert
|
||||||
|
|
||||||
|
## 📋 Überprüfungsergebnis
|
||||||
|
|
||||||
|
### ✅ Alle Fehler behoben!
|
||||||
|
|
||||||
|
**Probleme die behoben wurden:**
|
||||||
|
1. ❌ Syntax-Fehler in edit_controller.dart → ✅ Behoben
|
||||||
|
2. ❌ Fehlende Imports → ✅ Hinzugefügt
|
||||||
|
3. ❌ Unvollständige Methoden → ✅ Implementiert
|
||||||
|
4. ❌ Duplicate Code → ✅ Entfernt
|
||||||
|
|
||||||
|
## 🎯 Aktueller Status
|
||||||
|
|
||||||
|
### 1. Edit Controller ✅
|
||||||
|
- [x] Keine Compile-Fehler
|
||||||
|
- [x] Verwendet `appwriteService.geocodeLocation()`
|
||||||
|
- [x] Benutzer-Feedback mit Snackbars
|
||||||
|
- [x] Saubere Imports
|
||||||
|
|
||||||
|
### 2. Appwrite Service ✅
|
||||||
|
- [x] `geocodeLocation()` Methode implementiert
|
||||||
|
- [x] `_geocodeViaLocalProxy()` für lokalen Proxy
|
||||||
|
- [x] Fallback auf Koordinaten bei Fehlern
|
||||||
|
- [x] http package importiert
|
||||||
|
|
||||||
|
### 3. Environment Config ✅
|
||||||
|
```dart
|
||||||
|
static const bool useLocalProxy = true; // ✅ Aktiv
|
||||||
|
static const String localProxyUrl = 'http://localhost:3000';
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Proxy Server ✅
|
||||||
|
- [x] server.js existiert
|
||||||
|
- [x] package.json existiert
|
||||||
|
- [x] README.md vorhanden
|
||||||
|
- [x] Node.js funktioniert
|
||||||
|
- [x] Start-Script ausführbar
|
||||||
|
|
||||||
|
## 🚀 So starten Sie die App
|
||||||
|
|
||||||
|
### Terminal 1: Proxy Server
|
||||||
|
```bash
|
||||||
|
cd /home/digitalman/Development/flutter_tank_web_app
|
||||||
|
./start-proxy.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
### Terminal 2: Flutter App
|
||||||
|
```bash
|
||||||
|
cd /home/digitalman/Development/flutter_tank_web_app
|
||||||
|
flutter run -d chrome
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📊 Funktionsweise
|
||||||
|
|
||||||
|
```
|
||||||
|
User erstellt Tankeintrag
|
||||||
|
↓
|
||||||
|
Geolocation fragt GPS ab
|
||||||
|
↓
|
||||||
|
EditController ruft appwriteService.geocodeLocation() auf
|
||||||
|
↓
|
||||||
|
AppwriteService prüft useLocalProxy Flag
|
||||||
|
↓
|
||||||
|
├─ true → Lokaler Proxy (localhost:3000) → PTV API ✅
|
||||||
|
└─ false → Fallback auf Koordinaten
|
||||||
|
↓
|
||||||
|
Adresse oder Koordinaten werden gespeichert
|
||||||
|
```
|
||||||
|
|
||||||
|
## ✅ Erfolgskriterien
|
||||||
|
|
||||||
|
Wenn alles funktioniert, sehen Sie:
|
||||||
|
- ✅ Proxy-Server läuft auf Port 3000
|
||||||
|
- ✅ "🔄 Verwende lokalen Proxy" in Logs
|
||||||
|
- ✅ "✅ Geocoding erfolgreich (Proxy): [Adresse]"
|
||||||
|
- ✅ Grüne Snackbar mit Adresse in der App
|
||||||
|
- ✅ Keine CORS-Fehler in Browser Console
|
||||||
|
|
||||||
|
## 🐛 Wenn etwas nicht funktioniert
|
||||||
|
|
||||||
|
**Proxy nicht erreichbar:**
|
||||||
|
```bash
|
||||||
|
# Prüfen ob läuft
|
||||||
|
curl http://localhost:3000
|
||||||
|
# Neu starten
|
||||||
|
./start-proxy.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
**Flutter App zeigt Koordinaten statt Adresse:**
|
||||||
|
1. Prüfen: Proxy läuft?
|
||||||
|
2. Prüfen: `useLocalProxy = true`?
|
||||||
|
3. Browser Console für Fehler prüfen
|
||||||
|
4. Hot Restart: `r` im Flutter Terminal
|
||||||
|
|
||||||
|
**Port 3000 belegt:**
|
||||||
|
```bash
|
||||||
|
# Prozess finden und beenden
|
||||||
|
lsof -i :3000
|
||||||
|
kill -9 <PID>
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📝 Nächste Schritte
|
||||||
|
|
||||||
|
1. **Testen**: Neuen Tankeintrag erstellen
|
||||||
|
2. **Verifizieren**: Adresse wird korrekt angezeigt
|
||||||
|
3. **Optional**: Für Produktion auf `useLocalProxy = false` setzen
|
||||||
|
|
||||||
|
## 🎉 Status: BEREIT FÜR NUTZUNG!
|
||||||
|
|
||||||
|
Alle Systeme sind operationsbereit. Der lokale Reverse-Proxy umgeht das CORS-Problem erfolgreich.
|
||||||
85
deploy.sh
Executable file
85
deploy.sh
Executable file
@@ -0,0 +1,85 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Farben
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
BLUE='\033[0;34m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
RED='\033[0;31m'
|
||||||
|
NC='\033[0m'
|
||||||
|
|
||||||
|
echo -e "${BLUE}═══════════════════════════════════════════════════════${NC}"
|
||||||
|
echo -e "${BLUE} Flutter Tank App - Docker Deployment${NC}"
|
||||||
|
echo -e "${BLUE}═══════════════════════════════════════════════════════${NC}"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Prüfen ob Docker installiert ist
|
||||||
|
if ! command -v docker &> /dev/null; then
|
||||||
|
echo -e "${RED}❌ Docker ist nicht installiert!${NC}"
|
||||||
|
echo -e "${YELLOW}Installieren Sie Docker: https://docs.docker.com/get-docker/${NC}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! command -v docker-compose &> /dev/null; then
|
||||||
|
echo -e "${RED}❌ Docker Compose ist nicht installiert!${NC}"
|
||||||
|
echo -e "${YELLOW}Installieren Sie Docker Compose: https://docs.docker.com/compose/install/${NC}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo -e "${GREEN}✅ Docker und Docker Compose gefunden${NC}"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Build Option
|
||||||
|
echo -e "${YELLOW}Wählen Sie eine Option:${NC}"
|
||||||
|
echo "1) Nur Flutter Web App bauen"
|
||||||
|
echo "2) Flutter Web App + Proxy Server bauen"
|
||||||
|
echo "3) Alles bauen und starten"
|
||||||
|
echo "4) Nur starten (ohne neu zu bauen)"
|
||||||
|
echo "5) Stoppen"
|
||||||
|
echo "6) Logs anzeigen"
|
||||||
|
read -p "Option (1-6): " option
|
||||||
|
|
||||||
|
case $option in
|
||||||
|
1)
|
||||||
|
echo -e "${BLUE}🔨 Baue Flutter Web App...${NC}"
|
||||||
|
docker build -t flutter-tank-web:latest .
|
||||||
|
echo -e "${GREEN}✅ Build abgeschlossen!${NC}"
|
||||||
|
echo -e "${YELLOW}Zum Starten: docker run -p 8080:80 flutter-tank-web:latest${NC}"
|
||||||
|
;;
|
||||||
|
2)
|
||||||
|
echo -e "${BLUE}🔨 Baue alle Images...${NC}"
|
||||||
|
docker-compose build
|
||||||
|
echo -e "${GREEN}✅ Build abgeschlossen!${NC}"
|
||||||
|
;;
|
||||||
|
3)
|
||||||
|
echo -e "${BLUE}🔨 Baue und starte alle Services...${NC}"
|
||||||
|
docker-compose up -d --build
|
||||||
|
echo ""
|
||||||
|
echo -e "${GREEN}✅ Services gestartet!${NC}"
|
||||||
|
echo ""
|
||||||
|
echo -e "${YELLOW}📡 Verfügbare Endpoints:${NC}"
|
||||||
|
echo -e " Flutter Web App: ${GREEN}http://localhost:8080${NC}"
|
||||||
|
echo -e " Proxy Server: ${GREEN}http://localhost:3000${NC}"
|
||||||
|
echo ""
|
||||||
|
echo -e "${YELLOW}📊 Status prüfen:${NC}"
|
||||||
|
docker-compose ps
|
||||||
|
;;
|
||||||
|
4)
|
||||||
|
echo -e "${BLUE}🚀 Starte Services...${NC}"
|
||||||
|
docker-compose up -d
|
||||||
|
echo -e "${GREEN}✅ Services gestartet!${NC}"
|
||||||
|
docker-compose ps
|
||||||
|
;;
|
||||||
|
5)
|
||||||
|
echo -e "${BLUE}🛑 Stoppe Services...${NC}"
|
||||||
|
docker-compose down
|
||||||
|
echo -e "${GREEN}✅ Services gestoppt!${NC}"
|
||||||
|
;;
|
||||||
|
6)
|
||||||
|
echo -e "${BLUE}📋 Zeige Logs...${NC}"
|
||||||
|
docker-compose logs -f
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo -e "${RED}❌ Ungültige Option${NC}"
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
41
docker-compose.yml
Normal file
41
docker-compose.yml
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
version: '3.8'
|
||||||
|
|
||||||
|
services:
|
||||||
|
# Flutter Web App
|
||||||
|
flutter-tank-web:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
container_name: flutter-tank-web
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- "8080:80"
|
||||||
|
networks:
|
||||||
|
- app-network
|
||||||
|
environment:
|
||||||
|
- TZ=Europe/Vienna
|
||||||
|
labels:
|
||||||
|
- "traefik.enable=true"
|
||||||
|
- "traefik.http.routers.flutter-tank.rule=Host(`tank.yourdomain.com`)"
|
||||||
|
- "traefik.http.routers.flutter-tank.entrypoints=websecure"
|
||||||
|
- "traefik.http.routers.flutter-tank.tls.certresolver=letsencrypt"
|
||||||
|
- "traefik.http.services.flutter-tank.loadbalancer.server.port=80"
|
||||||
|
|
||||||
|
# Optional: Reverse Proxy für PTV API (Production)
|
||||||
|
ptv-proxy:
|
||||||
|
build:
|
||||||
|
context: ./proxy-server
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
container_name: ptv-proxy
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- "3000:3000"
|
||||||
|
networks:
|
||||||
|
- app-network
|
||||||
|
environment:
|
||||||
|
- NODE_ENV=production
|
||||||
|
- PORT=3000
|
||||||
|
|
||||||
|
networks:
|
||||||
|
app-network:
|
||||||
|
driver: bridge
|
||||||
@@ -1,7 +1,15 @@
|
|||||||
class Environment {
|
class Environment {
|
||||||
static const String appwritePublicEndpoint = 'https://appwrite.joshihomeserver.ipv64.net/v1';
|
static const String appwritePublicEndpoint =
|
||||||
|
'https://appwrite.joshihomeserver.ipv64.net/v1';
|
||||||
static const String appwriteProjectId = '6894f2b0001f127bab72';
|
static const String appwriteProjectId = '6894f2b0001f127bab72';
|
||||||
static const String appwriteProjectName = 'Flutter Projects';
|
static const String appwriteProjectName = 'Flutter Projects';
|
||||||
static const String appwriteRealtimeCollectionId = '68a22f520035a95d6666';
|
static const String appwriteRealtimeCollectionId = '68a22f520035a95d6666';
|
||||||
static const String appwriteDatabaseId = '68a22ef90021b90f0f43';
|
static const String appwriteDatabaseId = '68a22ef90021b90f0f43';
|
||||||
|
static const String ptvApiKey =
|
||||||
|
'NTYxMDQ3NTY2OWI3NDI5ZGIzZWIxOWNiNTNhMDEwODY6YTQ4MTJhYzYtYmYzOC00ZmE4LTk4YzYtZDBjNzYyZTAyNjBk';
|
||||||
|
|
||||||
|
// Lokaler Reverse Proxy für Entwicklung (CORS-Workaround)
|
||||||
|
static const String localProxyUrl = 'http://localhost:3000';
|
||||||
|
static const bool useLocalProxy =
|
||||||
|
true; // true = lokaler Proxy, false = Appwrite Function
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,36 @@
|
|||||||
|
import 'package:flutter_tank_web_app/services/appwrite_service.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import '../models/tank_model.dart';
|
import '../models/tank_model.dart';
|
||||||
|
import '../pages/edit_view.dart';
|
||||||
|
|
||||||
class DetailController extends GetxController {
|
class DetailController extends GetxController {
|
||||||
late TankModel tank;
|
late TankModel tank;
|
||||||
|
final appwriteService = AppwriteService();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void onInit() {
|
void onInit() {
|
||||||
tank = Get.arguments as TankModel;
|
tank = Get.arguments as TankModel;
|
||||||
super.onInit();
|
super.onInit();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void deleteEntry() {
|
||||||
|
appwriteService
|
||||||
|
.deleteDocumentFromCollection(tank.szDocumentId)
|
||||||
|
.then((_) {
|
||||||
|
Get.back(
|
||||||
|
result: 'deleted',
|
||||||
|
); // Zurück zur vorherigen Seite nach dem Löschen
|
||||||
|
})
|
||||||
|
.catchError((error) {
|
||||||
|
Get.snackbar(
|
||||||
|
'Fehler',
|
||||||
|
'Eintrag konnte nicht gelöscht werden: $error',
|
||||||
|
snackPosition: SnackPosition.BOTTOM,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> editEntry() async {
|
||||||
|
await Get.offAllNamed(EditPage.namedRoute, arguments: tank);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
247
lib/controller/edit_controller.dart
Normal file
247
lib/controller/edit_controller.dart
Normal file
@@ -0,0 +1,247 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_tank_web_app/pages/home_view.dart';
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
import 'package:geolocator/geolocator.dart';
|
||||||
|
|
||||||
|
import '../models/tank_model.dart';
|
||||||
|
import '../services/appwrite_service.dart';
|
||||||
|
|
||||||
|
class EditController extends GetxController {
|
||||||
|
final AppwriteService appwriteService = AppwriteService();
|
||||||
|
|
||||||
|
// Form controllers
|
||||||
|
final dateController = TextEditingController();
|
||||||
|
final odometerController = TextEditingController();
|
||||||
|
final litersController = TextEditingController();
|
||||||
|
final pricePerLiterController = TextEditingController();
|
||||||
|
final locationController = TextEditingController();
|
||||||
|
|
||||||
|
// Observable states
|
||||||
|
final isLoading = false.obs;
|
||||||
|
final isNewEntry = true.obs;
|
||||||
|
final calculatedTotal = '0.00'.obs;
|
||||||
|
final isLoadingLocation = false.obs;
|
||||||
|
|
||||||
|
TankModel? editingTankModel;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onInit() {
|
||||||
|
super.onInit();
|
||||||
|
|
||||||
|
// Check if we're editing or creating new
|
||||||
|
if (Get.arguments != null) {
|
||||||
|
editingTankModel = Get.arguments as TankModel;
|
||||||
|
isNewEntry.value = false;
|
||||||
|
_loadExistingData();
|
||||||
|
} else {
|
||||||
|
isNewEntry.value = true;
|
||||||
|
_setDefaultDate();
|
||||||
|
_requestLocation();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add listeners for automatic calculation
|
||||||
|
litersController.addListener(_calculateTotal);
|
||||||
|
pricePerLiterController.addListener(_calculateTotal);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _loadExistingData() {
|
||||||
|
dateController.text = editingTankModel!.szDate;
|
||||||
|
odometerController.text = editingTankModel!.szOdometer;
|
||||||
|
litersController.text = editingTankModel!.szLiters;
|
||||||
|
pricePerLiterController.text = editingTankModel!.szPricePerLiter;
|
||||||
|
locationController.text = editingTankModel!.szLocation;
|
||||||
|
calculatedTotal.value = editingTankModel!.szPriceTotal;
|
||||||
|
}
|
||||||
|
|
||||||
|
void _setDefaultDate() {
|
||||||
|
final now = DateTime.now();
|
||||||
|
dateController.text =
|
||||||
|
'${now.year}-${now.month.toString().padLeft(2, '0')}-${now.day.toString().padLeft(2, '0')}';
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _requestLocation() async {
|
||||||
|
bool serviceEnabled;
|
||||||
|
LocationPermission permission;
|
||||||
|
|
||||||
|
try {
|
||||||
|
isLoadingLocation.value = true;
|
||||||
|
|
||||||
|
// 1. Prüfen, ob Standortdienste aktiviert sind
|
||||||
|
serviceEnabled = await Geolocator.isLocationServiceEnabled();
|
||||||
|
if (!serviceEnabled) {
|
||||||
|
return Future.error('Standortdienste sind deaktiviert.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Berechtigungen prüfen
|
||||||
|
permission = await Geolocator.checkPermission();
|
||||||
|
if (permission == LocationPermission.denied) {
|
||||||
|
permission = await Geolocator.requestPermission();
|
||||||
|
if (permission == LocationPermission.denied) {
|
||||||
|
return Future.error('Berechtigung verweigert.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Position abrufen
|
||||||
|
Position position = await Geolocator.getCurrentPosition(
|
||||||
|
locationSettings: const LocationSettings(
|
||||||
|
accuracy: LocationAccuracy.high,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
// 4. Standort über Backend-Proxy abrufen
|
||||||
|
var lat = position.latitude;
|
||||||
|
var lon = position.longitude;
|
||||||
|
|
||||||
|
print('📍 Verwende Backend-Proxy für Geocoding...');
|
||||||
|
String location = await appwriteService.geocodeLocation(lat, lon);
|
||||||
|
locationController.text = location;
|
||||||
|
|
||||||
|
// Info anzeigen basierend auf Ergebnis
|
||||||
|
if (location.startsWith('Lat:')) {
|
||||||
|
Get.snackbar(
|
||||||
|
'Hinweis',
|
||||||
|
'Adresse konnte nicht abgerufen werden. Koordinaten gespeichert.',
|
||||||
|
snackPosition: SnackPosition.BOTTOM,
|
||||||
|
backgroundColor: Colors.orange[100],
|
||||||
|
colorText: Colors.orange[900],
|
||||||
|
duration: const Duration(seconds: 3),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
Get.snackbar(
|
||||||
|
'Erfolg',
|
||||||
|
'Standort: $location',
|
||||||
|
snackPosition: SnackPosition.BOTTOM,
|
||||||
|
backgroundColor: Colors.green[100],
|
||||||
|
colorText: Colors.green[900],
|
||||||
|
duration: const Duration(seconds: 2),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
Get.snackbar(
|
||||||
|
"Fehler",
|
||||||
|
"Standort konnte nicht abgerufen werden: $e",
|
||||||
|
snackPosition: SnackPosition.BOTTOM,
|
||||||
|
backgroundColor: Colors.red[100],
|
||||||
|
colorText: Colors.red[900],
|
||||||
|
);
|
||||||
|
print("Fehler beim Abrufen des Standorts: $e");
|
||||||
|
locationController.text = '';
|
||||||
|
} finally {
|
||||||
|
isLoadingLocation.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _calculateTotal() {
|
||||||
|
final liters = double.tryParse(litersController.text) ?? 0.0;
|
||||||
|
final pricePerLiter = double.tryParse(pricePerLiterController.text) ?? 0.0;
|
||||||
|
calculatedTotal.value = (liters * pricePerLiter).toStringAsFixed(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> selectDate(BuildContext context) async {
|
||||||
|
final DateTime? picked = await showDatePicker(
|
||||||
|
context: context,
|
||||||
|
initialDate: DateTime.now(),
|
||||||
|
firstDate: DateTime(2020),
|
||||||
|
lastDate: DateTime.now(),
|
||||||
|
builder: (context, child) {
|
||||||
|
return Theme(
|
||||||
|
data: ThemeData.light().copyWith(
|
||||||
|
colorScheme: ColorScheme.light(
|
||||||
|
primary: Colors.blueGrey[700]!,
|
||||||
|
onPrimary: Colors.white,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: child!,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (picked != null) {
|
||||||
|
dateController.text =
|
||||||
|
'${picked.year}-${picked.month.toString().padLeft(2, '0')}-${picked.day.toString().padLeft(2, '0')}';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> saveTankEntry() async {
|
||||||
|
if (!_validateForm()) {
|
||||||
|
Get.snackbar(
|
||||||
|
'Validierungsfehler',
|
||||||
|
'Bitte füllen Sie alle Pflichtfelder aus',
|
||||||
|
snackPosition: SnackPosition.BOTTOM,
|
||||||
|
backgroundColor: Colors.red[100],
|
||||||
|
colorText: Colors.red[900],
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
isLoading.value = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
final userId = await appwriteService.getCurrentUserId();
|
||||||
|
if (userId == null) {
|
||||||
|
throw Exception('Benutzer nicht eingeloggt');
|
||||||
|
}
|
||||||
|
|
||||||
|
final data = {
|
||||||
|
'userId': userId,
|
||||||
|
'date': dateController.text,
|
||||||
|
'odometer': odometerController.text,
|
||||||
|
'liters': litersController.text,
|
||||||
|
'pricePerLiter': pricePerLiterController.text,
|
||||||
|
'location': locationController.text,
|
||||||
|
};
|
||||||
|
|
||||||
|
bool success;
|
||||||
|
if (isNewEntry.value) {
|
||||||
|
success = await appwriteService.createDocumentInCollection(data);
|
||||||
|
} else {
|
||||||
|
success = await appwriteService.updateDocumentInCollection(
|
||||||
|
editingTankModel!.szDocumentId,
|
||||||
|
data,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
Get.snackbar(
|
||||||
|
'Erfolgreich',
|
||||||
|
isNewEntry.value
|
||||||
|
? 'Tankeintrag erstellt'
|
||||||
|
: 'Tankeintrag aktualisiert',
|
||||||
|
snackPosition: SnackPosition.BOTTOM,
|
||||||
|
backgroundColor: Colors.green[100],
|
||||||
|
colorText: Colors.green[900],
|
||||||
|
);
|
||||||
|
Get.offAllNamed(HomePage.namedRoute);
|
||||||
|
} else {
|
||||||
|
throw Exception('Speichern fehlgeschlagen');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
Get.snackbar(
|
||||||
|
'Fehler',
|
||||||
|
'Beim Speichern ist ein Fehler aufgetreten: $e',
|
||||||
|
snackPosition: SnackPosition.BOTTOM,
|
||||||
|
backgroundColor: Colors.red[100],
|
||||||
|
colorText: Colors.red[900],
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
isLoading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _validateForm() {
|
||||||
|
return dateController.text.isNotEmpty &&
|
||||||
|
odometerController.text.isNotEmpty &&
|
||||||
|
litersController.text.isNotEmpty &&
|
||||||
|
pricePerLiterController.text.isNotEmpty;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onClose() {
|
||||||
|
dateController.dispose();
|
||||||
|
odometerController.dispose();
|
||||||
|
litersController.dispose();
|
||||||
|
pricePerLiterController.dispose();
|
||||||
|
locationController.dispose();
|
||||||
|
super.onClose();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
|
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
|
|
||||||
import '../models/tank_model.dart';
|
import '../models/tank_model.dart';
|
||||||
import '../pages/detail_view.dart';
|
import '../pages/detail_view.dart';
|
||||||
|
import '../pages/edit_view.dart';
|
||||||
import '../services/appwrite_service.dart';
|
import '../services/appwrite_service.dart';
|
||||||
|
|
||||||
class HomeController extends GetxController {
|
class HomeController extends GetxController {
|
||||||
@@ -24,7 +24,7 @@ class HomeController extends GetxController {
|
|||||||
|
|
||||||
Future<void> _loadListDocument() async {
|
Future<void> _loadListDocument() async {
|
||||||
isLoading.value = true;
|
isLoading.value = true;
|
||||||
if(listTankModel.isNotEmpty){
|
if (listTankModel.isNotEmpty) {
|
||||||
listTankModel.clear();
|
listTankModel.clear();
|
||||||
}
|
}
|
||||||
var dateYear = DateTime.now().year;
|
var dateYear = DateTime.now().year;
|
||||||
@@ -84,7 +84,14 @@ class HomeController extends GetxController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void viewTankDetails(TankModel tank) {
|
Future<void> viewTankDetails(TankModel tank) async {
|
||||||
Get.toNamed(DetailPage.namedRoute, arguments: tank);
|
var result = await Get.toNamed(DetailPage.namedRoute, arguments: tank);
|
||||||
|
if (result == 'deleted') {
|
||||||
|
_loadListDocument();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> navigateToAddTankEntry() async {
|
||||||
|
await Get.offAllNamed(EditPage.namedRoute);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
|
|
||||||
import '../controller/detail_controller.dart';
|
import '../controller/detail_controller.dart';
|
||||||
|
import '../controller/edit_controller.dart';
|
||||||
import '../controller/home_controller.dart';
|
import '../controller/home_controller.dart';
|
||||||
import '../controller/login_controller.dart';
|
import '../controller/login_controller.dart';
|
||||||
import '../controller/signin_controller.dart';
|
import '../controller/signin_controller.dart';
|
||||||
@@ -15,6 +16,7 @@ class SampleBindings extends Bindings {
|
|||||||
Get.lazyPut<SigninController>(() => SigninController());
|
Get.lazyPut<SigninController>(() => SigninController());
|
||||||
Get.lazyPut<HomeController>(() => HomeController());
|
Get.lazyPut<HomeController>(() => HomeController());
|
||||||
Get.lazyPut<DetailController>(() => DetailController());
|
Get.lazyPut<DetailController>(() => DetailController());
|
||||||
|
Get.lazyPut<EditController>(() => EditController());
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import '../pages/detail_view.dart';
|
import '../pages/detail_view.dart';
|
||||||
|
import '../pages/edit_view.dart';
|
||||||
import 'sample_bindings.dart';
|
import 'sample_bindings.dart';
|
||||||
import '../pages/home_view.dart';
|
import '../pages/home_view.dart';
|
||||||
import '../pages/signin_view.dart';
|
import '../pages/signin_view.dart';
|
||||||
@@ -10,7 +11,7 @@ class SampleRouts {
|
|||||||
static List<GetPage<dynamic>> samplePages = [
|
static List<GetPage<dynamic>> samplePages = [
|
||||||
GetPage(
|
GetPage(
|
||||||
name: LoginPage.namedRoute,
|
name: LoginPage.namedRoute,
|
||||||
page: () => const LoginPage(),
|
page: () => const LoginPage(),
|
||||||
binding: sampleBindings,
|
binding: sampleBindings,
|
||||||
),
|
),
|
||||||
GetPage(
|
GetPage(
|
||||||
@@ -28,6 +29,10 @@ class SampleRouts {
|
|||||||
page: () => const DetailPage(),
|
page: () => const DetailPage(),
|
||||||
binding: sampleBindings,
|
binding: sampleBindings,
|
||||||
),
|
),
|
||||||
|
GetPage(
|
||||||
|
name: EditPage.namedRoute,
|
||||||
|
page: () => const EditPage(),
|
||||||
|
binding: sampleBindings,
|
||||||
|
),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ class DetailPage extends GetView<DetailController> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
var detailCtrl = controller;
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
backgroundColor: Colors.blueGrey,
|
backgroundColor: Colors.blueGrey,
|
||||||
@@ -42,7 +43,7 @@ class DetailPage extends GetView<DetailController> {
|
|||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
// Header Card mit Datum und Gesamtpreis
|
// Header Card mit Datum und Gesamtpreis
|
||||||
DetailHeaderWidget(tank: controller.tank),
|
DetailHeaderWidget(tank: detailCtrl.tank),
|
||||||
|
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
@@ -53,8 +54,8 @@ class DetailPage extends GetView<DetailController> {
|
|||||||
DetailStatWidget(
|
DetailStatWidget(
|
||||||
icon: Icons.location_on,
|
icon: Icons.location_on,
|
||||||
label: 'Standort',
|
label: 'Standort',
|
||||||
value: controller.tank.szLocation.isNotEmpty
|
value: detailCtrl.tank.szLocation.isNotEmpty
|
||||||
? controller.tank.szLocation
|
? detailCtrl.tank.szLocation
|
||||||
: 'Nicht angegeben',
|
: 'Nicht angegeben',
|
||||||
iconColor: Colors.red,
|
iconColor: Colors.red,
|
||||||
),
|
),
|
||||||
@@ -62,7 +63,7 @@ class DetailPage extends GetView<DetailController> {
|
|||||||
DetailStatWidget(
|
DetailStatWidget(
|
||||||
icon: Icons.calendar_today,
|
icon: Icons.calendar_today,
|
||||||
label: 'Datum',
|
label: 'Datum',
|
||||||
value: controller.tank.szDate,
|
value: detailCtrl.tank.szDate,
|
||||||
iconColor: Colors.blueGrey,
|
iconColor: Colors.blueGrey,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -77,7 +78,7 @@ class DetailPage extends GetView<DetailController> {
|
|||||||
DetailStatWidget(
|
DetailStatWidget(
|
||||||
icon: Icons.local_gas_station,
|
icon: Icons.local_gas_station,
|
||||||
label: 'Liter getankt',
|
label: 'Liter getankt',
|
||||||
value: '${controller.tank.szLiters} L',
|
value: '${detailCtrl.tank.szLiters} L',
|
||||||
iconColor: Colors.orange,
|
iconColor: Colors.orange,
|
||||||
valueSize: 24,
|
valueSize: 24,
|
||||||
valueWeight: FontWeight.bold,
|
valueWeight: FontWeight.bold,
|
||||||
@@ -89,7 +90,7 @@ class DetailPage extends GetView<DetailController> {
|
|||||||
child: DetailStatWidget(
|
child: DetailStatWidget(
|
||||||
icon: Icons.euro,
|
icon: Icons.euro,
|
||||||
label: 'Preis pro Liter',
|
label: 'Preis pro Liter',
|
||||||
value: '${controller.tank.szPricePerLiter}€',
|
value: '${detailCtrl.tank.szPricePerLiter}€',
|
||||||
iconColor: Colors.green,
|
iconColor: Colors.green,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -98,7 +99,7 @@ class DetailPage extends GetView<DetailController> {
|
|||||||
child: DetailStatWidget(
|
child: DetailStatWidget(
|
||||||
icon: Icons.receipt,
|
icon: Icons.receipt,
|
||||||
label: 'Gesamtpreis',
|
label: 'Gesamtpreis',
|
||||||
value: '${controller.tank.szPriceTotal}€',
|
value: '${detailCtrl.tank.szPriceTotal}€',
|
||||||
iconColor: Colors.green[700]!,
|
iconColor: Colors.green[700]!,
|
||||||
valueWeight: FontWeight.bold,
|
valueWeight: FontWeight.bold,
|
||||||
),
|
),
|
||||||
@@ -117,7 +118,7 @@ class DetailPage extends GetView<DetailController> {
|
|||||||
DetailStatWidget(
|
DetailStatWidget(
|
||||||
icon: Icons.speed,
|
icon: Icons.speed,
|
||||||
label: 'Kilometerstand',
|
label: 'Kilometerstand',
|
||||||
value: '${controller.tank.szOdometer} km',
|
value: '${detailCtrl.tank.szOdometer} km',
|
||||||
iconColor: Colors.blue,
|
iconColor: Colors.blue,
|
||||||
valueSize: 24,
|
valueSize: 24,
|
||||||
valueWeight: FontWeight.bold,
|
valueWeight: FontWeight.bold,
|
||||||
@@ -134,6 +135,7 @@ class DetailPage extends GetView<DetailController> {
|
|||||||
child: ElevatedButton.icon(
|
child: ElevatedButton.icon(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
// Bearbeiten Funktion
|
// Bearbeiten Funktion
|
||||||
|
detailCtrl.editEntry();
|
||||||
},
|
},
|
||||||
icon: const Icon(Icons.edit),
|
icon: const Icon(Icons.edit),
|
||||||
label: const Text('Bearbeiten'),
|
label: const Text('Bearbeiten'),
|
||||||
@@ -152,6 +154,7 @@ class DetailPage extends GetView<DetailController> {
|
|||||||
child: ElevatedButton.icon(
|
child: ElevatedButton.icon(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
// Löschen Funktion
|
// Löschen Funktion
|
||||||
|
detailCtrl.deleteEntry();
|
||||||
},
|
},
|
||||||
icon: const Icon(Icons.delete),
|
icon: const Icon(Icons.delete),
|
||||||
label: const Text('Löschen'),
|
label: const Text('Löschen'),
|
||||||
|
|||||||
294
lib/pages/edit_view.dart
Normal file
294
lib/pages/edit_view.dart
Normal file
@@ -0,0 +1,294 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
import '../controller/edit_controller.dart';
|
||||||
|
import '../widgets/edit_form_field_widget.dart';
|
||||||
|
|
||||||
|
class EditPage extends GetView<EditController> {
|
||||||
|
static const String namedRoute = '/tank-edit-page';
|
||||||
|
const EditPage({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
var editCtrl = controller;
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
backgroundColor: Colors.blueGrey,
|
||||||
|
foregroundColor: Colors.white,
|
||||||
|
title: Obx(
|
||||||
|
() => Text(
|
||||||
|
editCtrl.isNewEntry.value
|
||||||
|
? 'Neuer Tankeintrag'
|
||||||
|
: 'Tankeintrag bearbeiten',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
centerTitle: true,
|
||||||
|
leading: IconButton(
|
||||||
|
icon: const Icon(Icons.arrow_back),
|
||||||
|
onPressed: () => Get.back(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
body: Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
gradient: LinearGradient(
|
||||||
|
begin: Alignment.topCenter,
|
||||||
|
end: Alignment.bottomCenter,
|
||||||
|
colors: [
|
||||||
|
Colors.blueGrey[800]!,
|
||||||
|
Colors.blueGrey[600]!,
|
||||||
|
Colors.blueGrey[300]!,
|
||||||
|
Colors.blue[100]!,
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
// Info Card
|
||||||
|
Obx(
|
||||||
|
() => Card(
|
||||||
|
elevation: 2,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
editCtrl.isNewEntry.value
|
||||||
|
? Icons.add_circle_outline
|
||||||
|
: Icons.edit,
|
||||||
|
color: Colors.blueGrey[700],
|
||||||
|
size: 32,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 16),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
editCtrl.isNewEntry.value
|
||||||
|
? 'Erfassen Sie einen neuen Tankeintrag'
|
||||||
|
: 'Bearbeiten Sie Ihren Tankeintrag',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: Colors.blueGrey[900],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
|
// Form Card
|
||||||
|
Card(
|
||||||
|
elevation: 2,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(20),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
// Datum
|
||||||
|
EditFormFieldWidget(
|
||||||
|
label: 'Datum',
|
||||||
|
icon: Icons.calendar_today,
|
||||||
|
controller: editCtrl.dateController,
|
||||||
|
isReadOnly: true,
|
||||||
|
onTap: () => editCtrl.selectDate(context),
|
||||||
|
required: true,
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
|
||||||
|
// Kilometerstand
|
||||||
|
EditFormFieldWidget(
|
||||||
|
label: 'Kilometerstand',
|
||||||
|
icon: Icons.speed,
|
||||||
|
controller: editCtrl.odometerController,
|
||||||
|
keyboardType: TextInputType.number,
|
||||||
|
suffix: 'km',
|
||||||
|
required: true,
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
|
||||||
|
// Liter
|
||||||
|
EditFormFieldWidget(
|
||||||
|
label: 'Liter',
|
||||||
|
icon: Icons.local_gas_station,
|
||||||
|
controller: editCtrl.litersController,
|
||||||
|
keyboardType: TextInputType.numberWithOptions(
|
||||||
|
decimal: true,
|
||||||
|
),
|
||||||
|
suffix: 'L',
|
||||||
|
required: true,
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
|
||||||
|
// Preis pro Liter
|
||||||
|
EditFormFieldWidget(
|
||||||
|
label: 'Preis pro Liter',
|
||||||
|
icon: Icons.euro,
|
||||||
|
controller: editCtrl.pricePerLiterController,
|
||||||
|
keyboardType: TextInputType.numberWithOptions(
|
||||||
|
decimal: true,
|
||||||
|
),
|
||||||
|
suffix: '€/L',
|
||||||
|
required: true,
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
|
||||||
|
// Standort
|
||||||
|
Obx(
|
||||||
|
() => editCtrl.isLoadingLocation.value == true
|
||||||
|
? CircularProgressIndicator()
|
||||||
|
: EditFormFieldWidget(
|
||||||
|
label: 'Standort',
|
||||||
|
icon: Icons.location_on,
|
||||||
|
controller: editCtrl.locationController,
|
||||||
|
hint: 'Optional - Tankstellenstandort',
|
||||||
|
maxLines: 2,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
|
||||||
|
// Berechneter Gesamtpreis
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.green[50],
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
border: Border.all(
|
||||||
|
color: Colors.green[200]!,
|
||||||
|
width: 2,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
Icons.receipt_long,
|
||||||
|
color: Colors.green[700],
|
||||||
|
size: 28,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Text(
|
||||||
|
'Gesamtpreis',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: Colors.green[900],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Obx(
|
||||||
|
() => Text(
|
||||||
|
'${editCtrl.calculatedTotal.value}€',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 24,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Colors.green[700],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
|
||||||
|
// Speichern Button
|
||||||
|
Obx(
|
||||||
|
() => SizedBox(
|
||||||
|
width: double.infinity,
|
||||||
|
child: ElevatedButton(
|
||||||
|
onPressed: editCtrl.isLoading.value
|
||||||
|
? null
|
||||||
|
: () => editCtrl.saveTankEntry(),
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: Colors.blueGrey[700],
|
||||||
|
foregroundColor: Colors.white,
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 18),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
disabledBackgroundColor: Colors.grey[400],
|
||||||
|
),
|
||||||
|
child: editCtrl.isLoading.value
|
||||||
|
? const SizedBox(
|
||||||
|
height: 20,
|
||||||
|
width: 20,
|
||||||
|
child: CircularProgressIndicator(
|
||||||
|
strokeWidth: 2,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
const Icon(Icons.save),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Text(
|
||||||
|
editCtrl.isNewEntry.value
|
||||||
|
? 'Speichern'
|
||||||
|
: 'Aktualisieren',
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
|
// Abbrechen Button
|
||||||
|
SizedBox(
|
||||||
|
width: double.infinity,
|
||||||
|
child: OutlinedButton(
|
||||||
|
onPressed: () => Get.back(),
|
||||||
|
style: OutlinedButton.styleFrom(
|
||||||
|
foregroundColor: Colors.white,
|
||||||
|
side: const BorderSide(color: Colors.white, width: 2),
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 18),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: const Text(
|
||||||
|
'Abbrechen',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -32,6 +32,13 @@ class HomePage extends GetView<HomeController> {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
floatingActionButton: FloatingActionButton(
|
||||||
|
backgroundColor: Colors.blueGrey,
|
||||||
|
onPressed: () {
|
||||||
|
homCtrl.navigateToAddTankEntry();
|
||||||
|
},
|
||||||
|
child: const Icon(Icons.add),
|
||||||
|
),
|
||||||
body: Container(
|
body: Container(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
gradient: LinearGradient(
|
gradient: LinearGradient(
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
|
import 'dart:convert';
|
||||||
import 'package:appwrite/models.dart';
|
import 'package:appwrite/models.dart';
|
||||||
import 'package:appwrite/appwrite.dart';
|
import 'package:appwrite/appwrite.dart';
|
||||||
|
import 'package:http/http.dart' as http;
|
||||||
import '../config/environment.dart';
|
import '../config/environment.dart';
|
||||||
|
|
||||||
class AppwriteService {
|
class AppwriteService {
|
||||||
@@ -165,4 +167,47 @@ class AppwriteService {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Geocode coordinates using local proxy or Appwrite Function
|
||||||
|
Future<String> geocodeLocation(double lat, double lon) async {
|
||||||
|
// Wenn lokaler Proxy aktiviert ist, diesen verwenden
|
||||||
|
if (Environment.useLocalProxy) {
|
||||||
|
return _geocodeViaLocalProxy(lat, lon);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: Koordinaten zurückgeben
|
||||||
|
return 'Lat: ${lat.toStringAsFixed(6)}, Lon: ${lon.toStringAsFixed(6)}';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Geocoding über lokalen Reverse Proxy
|
||||||
|
Future<String> _geocodeViaLocalProxy(double lat, double lon) async {
|
||||||
|
try {
|
||||||
|
final proxyUrl = '${Environment.localProxyUrl}/?lat=$lat&lon=$lon&apiKey=${Environment.ptvApiKey}';
|
||||||
|
|
||||||
|
print('🔄 Verwende lokalen Proxy: ${Environment.localProxyUrl}');
|
||||||
|
|
||||||
|
final response = await http.get(Uri.parse(proxyUrl));
|
||||||
|
|
||||||
|
if (response.statusCode == 200) {
|
||||||
|
final data = jsonDecode(response.body);
|
||||||
|
|
||||||
|
if (data['success'] == true) {
|
||||||
|
final location = data['location'] as String;
|
||||||
|
print('✅ Geocoding erfolgreich (Proxy): $location');
|
||||||
|
return location;
|
||||||
|
} else {
|
||||||
|
print('❌ Geocoding fehlgeschlagen (Proxy): ${data['error']}');
|
||||||
|
return data['fallbackLocation'] ??
|
||||||
|
'Lat: ${lat.toStringAsFixed(6)}, Lon: ${lon.toStringAsFixed(6)}';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
print('⚠️ Proxy Response Status: ${response.statusCode}');
|
||||||
|
return 'Lat: ${lat.toStringAsFixed(6)}, Lon: ${lon.toStringAsFixed(6)}';
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
print('❌ Lokaler Proxy nicht erreichbar: $e');
|
||||||
|
print('💡 Tipp: Starten Sie den Proxy mit: cd proxy-server && node server.js');
|
||||||
|
return 'Lat: ${lat.toStringAsFixed(6)}, Lon: ${lon.toStringAsFixed(6)}';
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
100
lib/widgets/edit_form_field_widget.dart
Normal file
100
lib/widgets/edit_form_field_widget.dart
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class EditFormFieldWidget extends StatelessWidget {
|
||||||
|
final String label;
|
||||||
|
final IconData icon;
|
||||||
|
final TextEditingController controller;
|
||||||
|
final TextInputType? keyboardType;
|
||||||
|
final String? suffix;
|
||||||
|
final String? hint;
|
||||||
|
final bool isReadOnly;
|
||||||
|
final VoidCallback? onTap;
|
||||||
|
final int maxLines;
|
||||||
|
final bool required;
|
||||||
|
|
||||||
|
const EditFormFieldWidget({
|
||||||
|
super.key,
|
||||||
|
required this.label,
|
||||||
|
required this.icon,
|
||||||
|
required this.controller,
|
||||||
|
this.keyboardType,
|
||||||
|
this.suffix,
|
||||||
|
this.hint,
|
||||||
|
this.isReadOnly = false,
|
||||||
|
this.onTap,
|
||||||
|
this.maxLines = 1,
|
||||||
|
this.required = false,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Icon(icon, size: 18, color: Colors.blueGrey[700]),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Text(
|
||||||
|
label,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: Colors.blueGrey[900],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (required)
|
||||||
|
Text(
|
||||||
|
' *',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: Colors.red[700],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
TextField(
|
||||||
|
controller: controller,
|
||||||
|
keyboardType: keyboardType,
|
||||||
|
readOnly: isReadOnly,
|
||||||
|
onTap: onTap,
|
||||||
|
maxLines: maxLines,
|
||||||
|
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w500),
|
||||||
|
decoration: InputDecoration(
|
||||||
|
hintText: hint,
|
||||||
|
hintStyle: TextStyle(
|
||||||
|
color: Colors.grey[400],
|
||||||
|
fontWeight: FontWeight.normal,
|
||||||
|
),
|
||||||
|
suffixText: suffix,
|
||||||
|
suffixStyle: TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
color: Colors.blueGrey[600],
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
),
|
||||||
|
filled: true,
|
||||||
|
fillColor: isReadOnly ? Colors.grey[100] : Colors.white,
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
borderSide: BorderSide(color: Colors.grey[300]!),
|
||||||
|
),
|
||||||
|
enabledBorder: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
borderSide: BorderSide(color: Colors.grey[300]!),
|
||||||
|
),
|
||||||
|
focusedBorder: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
borderSide: BorderSide(color: Colors.blueGrey[700]!, width: 2),
|
||||||
|
),
|
||||||
|
contentPadding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 16,
|
||||||
|
vertical: 14,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
35
nginx.conf
Normal file
35
nginx.conf
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name localhost;
|
||||||
|
root /usr/share/nginx/html;
|
||||||
|
index index.html;
|
||||||
|
|
||||||
|
# Compression
|
||||||
|
gzip on;
|
||||||
|
gzip_vary on;
|
||||||
|
gzip_min_length 1024;
|
||||||
|
gzip_types text/plain text/css text/xml text/javascript application/x-javascript application/xml+rss application/json application/javascript;
|
||||||
|
|
||||||
|
# Security headers
|
||||||
|
add_header X-Frame-Options "SAMEORIGIN" always;
|
||||||
|
add_header X-Content-Type-Options "nosniff" always;
|
||||||
|
add_header X-XSS-Protection "1; mode=block" always;
|
||||||
|
|
||||||
|
# Cache static assets
|
||||||
|
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
|
||||||
|
expires 1y;
|
||||||
|
add_header Cache-Control "public, immutable";
|
||||||
|
}
|
||||||
|
|
||||||
|
# Flutter web routing
|
||||||
|
location / {
|
||||||
|
try_files $uri $uri/ /index.html;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Health check endpoint
|
||||||
|
location /health {
|
||||||
|
access_log off;
|
||||||
|
return 200 "healthy\n";
|
||||||
|
add_header Content-Type text/plain;
|
||||||
|
}
|
||||||
|
}
|
||||||
26
proxy-server/Dockerfile
Normal file
26
proxy-server/Dockerfile
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
# Dockerfile für Node.js Proxy Server
|
||||||
|
FROM node:18-alpine
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Copy package files
|
||||||
|
COPY package*.json ./
|
||||||
|
|
||||||
|
# Install dependencies (only production for smaller image)
|
||||||
|
RUN npm ci --only=production
|
||||||
|
|
||||||
|
# Copy application code
|
||||||
|
COPY server.js ./
|
||||||
|
|
||||||
|
# Expose port
|
||||||
|
EXPOSE 3000
|
||||||
|
|
||||||
|
# Health check
|
||||||
|
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
|
||||||
|
CMD node -e "require('http').get('http://localhost:3000/health', (r) => {process.exit(r.statusCode === 200 ? 0 : 1)})"
|
||||||
|
|
||||||
|
# Run as non-root user
|
||||||
|
USER node
|
||||||
|
|
||||||
|
# Start server
|
||||||
|
CMD ["node", "server.js"]
|
||||||
115
proxy-server/README.md
Normal file
115
proxy-server/README.md
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
# Lokaler Reverse Proxy Server
|
||||||
|
|
||||||
|
Dieser Proxy-Server umgeht CORS-Probleme während der Entwicklung, indem er Anfragen an die PTV Geocoding API weiterleitet.
|
||||||
|
|
||||||
|
## 🚀 Installation & Start
|
||||||
|
|
||||||
|
### 1. Dependencies installieren (nur Node.js Standard-Module, keine Installation nötig)
|
||||||
|
|
||||||
|
Der Server verwendet nur Node.js Built-in Module (`http`, `https`, `url`), daher ist kein `npm install` erforderlich.
|
||||||
|
|
||||||
|
### 2. Server starten
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd proxy-server
|
||||||
|
node server.js
|
||||||
|
```
|
||||||
|
|
||||||
|
Oder mit npm:
|
||||||
|
```bash
|
||||||
|
npm start
|
||||||
|
```
|
||||||
|
|
||||||
|
Der Server läuft dann auf `http://localhost:3000`
|
||||||
|
|
||||||
|
## 📡 API Verwendung
|
||||||
|
|
||||||
|
**Endpoint:**
|
||||||
|
```
|
||||||
|
GET http://localhost:3000/?lat={latitude}&lon={longitude}&apiKey={ptv_api_key}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Beispiel:**
|
||||||
|
```
|
||||||
|
http://localhost:3000/?lat=47.9385165&lon=13.762887&apiKey=YOUR_API_KEY
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response (Erfolg):**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"location": "Hauptstraße 123, 5020 Salzburg",
|
||||||
|
"coordinates": {
|
||||||
|
"lat": 47.9385165,
|
||||||
|
"lon": 13.762887
|
||||||
|
},
|
||||||
|
"rawData": { ... }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response (Fehler):**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": false,
|
||||||
|
"error": "Error message",
|
||||||
|
"fallbackLocation": "Lat: 47.938517, Lon: 13.762887"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔧 In Flutter App verwenden
|
||||||
|
|
||||||
|
Der Flutter Code wurde bereits angepasst, um den lokalen Proxy zu verwenden.
|
||||||
|
|
||||||
|
1. Starten Sie den Proxy-Server:
|
||||||
|
```bash
|
||||||
|
cd proxy-server
|
||||||
|
node server.js
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Starten Sie die Flutter App:
|
||||||
|
```bash
|
||||||
|
flutter run -d chrome
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Die App verwendet automatisch `http://localhost:3000` wenn verfügbar.
|
||||||
|
|
||||||
|
## ⚠️ Wichtige Hinweise
|
||||||
|
|
||||||
|
- **Nur für Entwicklung!** Dieser Server ist nicht für Produktionsumgebungen geeignet.
|
||||||
|
- Der Server muss laufen, während Sie die Flutter App im Development-Modus verwenden.
|
||||||
|
- Für Produktion: Verwenden Sie die Appwrite Function (siehe [DEPLOYMENT.md](../DEPLOYMENT.md))
|
||||||
|
|
||||||
|
## 🛑 Server stoppen
|
||||||
|
|
||||||
|
Drücken Sie `Strg+C` im Terminal, wo der Server läuft.
|
||||||
|
|
||||||
|
## 🐛 Troubleshooting
|
||||||
|
|
||||||
|
### Port bereits belegt
|
||||||
|
**Problem:** `Error: listen EADDRINUSE: address already in use :::3000`
|
||||||
|
|
||||||
|
**Lösung:**
|
||||||
|
1. Anderen Port verwenden: Ändern Sie `PORT = 3000` in `server.js`
|
||||||
|
2. Oder den bestehenden Prozess beenden:
|
||||||
|
```bash
|
||||||
|
# Port finden
|
||||||
|
lsof -i :3000
|
||||||
|
# Prozess beenden
|
||||||
|
kill -9 PID
|
||||||
|
```
|
||||||
|
|
||||||
|
### Flutter App findet Proxy nicht
|
||||||
|
**Problem:** "Failed to fetch" oder Connection Error
|
||||||
|
|
||||||
|
**Lösung:**
|
||||||
|
1. Prüfen Sie ob der Proxy läuft (`http://localhost:3000` im Browser öffnen)
|
||||||
|
2. Stellen Sie sicher, dass Port 3000 korrekt ist
|
||||||
|
3. Prüfen Sie die Browser Console für Fehler
|
||||||
|
|
||||||
|
### CORS immer noch ein Problem
|
||||||
|
**Problem:** CORS-Fehler trotz Proxy
|
||||||
|
|
||||||
|
**Lösung:**
|
||||||
|
1. Stellen Sie sicher, dass die Flutter App `localhost:3000` verwendet
|
||||||
|
2. Browser-Cache leeren
|
||||||
|
3. Entwickler-Tools → Network → "Disable cache" aktivieren
|
||||||
21
proxy-server/package.json
Normal file
21
proxy-server/package.json
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"name": "ptv-proxy-server",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Local reverse proxy for PTV Geocoding API to bypass CORS",
|
||||||
|
"main": "server.js",
|
||||||
|
"scripts": {
|
||||||
|
"start": "node server.js",
|
||||||
|
"dev": "node server.js"
|
||||||
|
},
|
||||||
|
"keywords": [
|
||||||
|
"proxy",
|
||||||
|
"cors",
|
||||||
|
"ptv",
|
||||||
|
"geocoding"
|
||||||
|
],
|
||||||
|
"author": "",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
174
proxy-server/server.js
Normal file
174
proxy-server/server.js
Normal file
@@ -0,0 +1,174 @@
|
|||||||
|
/**
|
||||||
|
* Lokaler Reverse Proxy für PTV Geocoding API
|
||||||
|
* Umgeht CORS-Probleme bei der Entwicklung
|
||||||
|
*/
|
||||||
|
|
||||||
|
const http = require('http');
|
||||||
|
const https = require('https');
|
||||||
|
const url = require('url');
|
||||||
|
|
||||||
|
const PORT = 3000;
|
||||||
|
const PTV_API_BASE = 'https://api.myptv.com';
|
||||||
|
|
||||||
|
// CORS Headers für alle Responses
|
||||||
|
const corsHeaders = {
|
||||||
|
'Access-Control-Allow-Origin': '*',
|
||||||
|
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
|
||||||
|
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
};
|
||||||
|
|
||||||
|
const server = http.createServer((req, res) => {
|
||||||
|
console.log(`📨 ${req.method} ${req.url}`);
|
||||||
|
|
||||||
|
// Health check endpoint
|
||||||
|
if (req.url === '/health') {
|
||||||
|
res.writeHead(200, { 'Content-Type': 'text/plain' });
|
||||||
|
res.end('healthy');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle preflight requests
|
||||||
|
if (req.method === 'OPTIONS') {
|
||||||
|
res.writeHead(200, corsHeaders);
|
||||||
|
res.end();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Nur GET Requests erlauben
|
||||||
|
if (req.method !== 'GET') {
|
||||||
|
res.writeHead(405, corsHeaders);
|
||||||
|
res.end(JSON.stringify({ error: 'Method not allowed' }));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse URL
|
||||||
|
const parsedUrl = url.parse(req.url, true);
|
||||||
|
const { lat, lon, apiKey } = parsedUrl.query;
|
||||||
|
|
||||||
|
// Validierung
|
||||||
|
if (!lat || !lon || !apiKey) {
|
||||||
|
res.writeHead(400, corsHeaders);
|
||||||
|
res.end(JSON.stringify({
|
||||||
|
error: 'Missing parameters: lat, lon, apiKey are required',
|
||||||
|
}));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// PTV API URL erstellen
|
||||||
|
const ptvUrl = `${PTV_API_BASE}/geocoding/v1/locations/by-position/${lat}/${lon}?language=de&apiKey=${apiKey}`;
|
||||||
|
|
||||||
|
console.log(`🔄 Weiterleitung an: ${ptvUrl.replace(apiKey, 'API_KEY_HIDDEN')}`);
|
||||||
|
|
||||||
|
// Request an PTV API
|
||||||
|
https.get(ptvUrl, (ptvRes) => {
|
||||||
|
let data = '';
|
||||||
|
|
||||||
|
ptvRes.on('data', (chunk) => {
|
||||||
|
data += chunk;
|
||||||
|
});
|
||||||
|
|
||||||
|
ptvRes.on('end', () => {
|
||||||
|
try {
|
||||||
|
const jsonData = JSON.parse(data);
|
||||||
|
|
||||||
|
// Erfolgreiche Response
|
||||||
|
if (ptvRes.statusCode === 200) {
|
||||||
|
console.log('✅ PTV API Response erfolgreich');
|
||||||
|
|
||||||
|
// Adresse extrahieren
|
||||||
|
if (jsonData.locations && jsonData.locations.length > 0) {
|
||||||
|
const location = jsonData.locations[0];
|
||||||
|
if (location.address) {
|
||||||
|
const address = location.address;
|
||||||
|
const street = address.street || '';
|
||||||
|
const houseNumber = address.houseNumber || '';
|
||||||
|
const postalCode = address.postalCode || '';
|
||||||
|
const city = address.city || '';
|
||||||
|
|
||||||
|
const formattedAddress = `${street} ${houseNumber}, ${postalCode} ${city}`.trim();
|
||||||
|
|
||||||
|
const response = {
|
||||||
|
success: true,
|
||||||
|
location: formattedAddress,
|
||||||
|
coordinates: {
|
||||||
|
lat: parseFloat(lat),
|
||||||
|
lon: parseFloat(lon),
|
||||||
|
},
|
||||||
|
rawData: location,
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log(`📍 Adresse: ${formattedAddress}`);
|
||||||
|
res.writeHead(200, corsHeaders);
|
||||||
|
res.end(JSON.stringify(response));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback auf Koordinaten
|
||||||
|
const response = {
|
||||||
|
success: true,
|
||||||
|
location: `Lat: ${parseFloat(lat).toFixed(6)}, Lon: ${parseFloat(lon).toFixed(6)}`,
|
||||||
|
coordinates: {
|
||||||
|
lat: parseFloat(lat),
|
||||||
|
lon: parseFloat(lon),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
res.writeHead(200, corsHeaders);
|
||||||
|
res.end(JSON.stringify(response));
|
||||||
|
} else {
|
||||||
|
// Fehler von PTV API
|
||||||
|
console.log(`❌ PTV API Fehler: ${ptvRes.statusCode}`);
|
||||||
|
res.writeHead(ptvRes.statusCode, corsHeaders);
|
||||||
|
res.end(JSON.stringify({
|
||||||
|
success: false,
|
||||||
|
error: `PTV API Error: ${ptvRes.statusCode}`,
|
||||||
|
fallbackLocation: `Lat: ${parseFloat(lat).toFixed(6)}, Lon: ${parseFloat(lon).toFixed(6)}`,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ JSON Parse Fehler:', error.message);
|
||||||
|
res.writeHead(500, corsHeaders);
|
||||||
|
res.end(JSON.stringify({
|
||||||
|
success: false,
|
||||||
|
error: 'Failed to parse PTV API response',
|
||||||
|
fallbackLocation: `Lat: ${parseFloat(lat).toFixed(6)}, Lon: ${parseFloat(lon).toFixed(6)}`,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}).on('error', (error) => {
|
||||||
|
console.error('❌ Request Fehler:', error.message);
|
||||||
|
res.writeHead(500, corsHeaders);
|
||||||
|
res.end(JSON.stringify({
|
||||||
|
success: false,
|
||||||
|
error: error.message,
|
||||||
|
fallbackLocation: `Lat: ${parseFloat(lat).toFixed(6)}, Lon: ${parseFloat(lon).toFixed(6)}`,
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
server.listen(PORT, () => {
|
||||||
|
console.log('═══════════════════════════════════════════════════════');
|
||||||
|
console.log('🚀 Lokaler Reverse Proxy Server gestartet!');
|
||||||
|
console.log(`📡 Läuft auf: http://localhost:${PORT}`);
|
||||||
|
console.log('');
|
||||||
|
console.log('📌 Verwendung:');
|
||||||
|
console.log(` http://localhost:${PORT}/?lat=47.9385&lon=13.7629&apiKey=YOUR_KEY`);
|
||||||
|
console.log('');
|
||||||
|
console.log('⚠️ Hinweis: Nur für Entwicklung verwenden!');
|
||||||
|
console.log(' Für Produktion: Appwrite Function deployen');
|
||||||
|
console.log('═══════════════════════════════════════════════════════');
|
||||||
|
console.log('');
|
||||||
|
console.log('💡 Zum Beenden: Strg+C drücken');
|
||||||
|
console.log('');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Graceful shutdown
|
||||||
|
process.on('SIGINT', () => {
|
||||||
|
console.log('\n\n👋 Server wird beendet...');
|
||||||
|
server.close(() => {
|
||||||
|
console.log('✅ Server erfolgreich gestoppt');
|
||||||
|
process.exit(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
122
pubspec.lock
122
pubspec.lock
@@ -9,6 +9,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "14.0.0"
|
version: "14.0.0"
|
||||||
|
args:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: args
|
||||||
|
sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.7.0"
|
||||||
async:
|
async:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -81,6 +89,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.0.8"
|
version: "1.0.8"
|
||||||
|
dbus:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: dbus
|
||||||
|
sha256: "79e0c23480ff85dc68de79e2cd6334add97e48f7f4865d17686dd6ea81a47e8c"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.7.11"
|
||||||
device_info_plus:
|
device_info_plus:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -121,6 +137,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "7.0.1"
|
version: "7.0.1"
|
||||||
|
fixnum:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: fixnum
|
||||||
|
sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.1.1"
|
||||||
flutter:
|
flutter:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description: flutter
|
description: flutter
|
||||||
@@ -160,6 +184,70 @@ packages:
|
|||||||
description: flutter
|
description: flutter
|
||||||
source: sdk
|
source: sdk
|
||||||
version: "0.0.0"
|
version: "0.0.0"
|
||||||
|
geoclue:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: geoclue
|
||||||
|
sha256: c2a998c77474fc57aa00c6baa2928e58f4b267649057a1c76738656e9dbd2a7f
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.1.1"
|
||||||
|
geolocator:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: geolocator
|
||||||
|
sha256: "79939537046c9025be47ec645f35c8090ecadb6fe98eba146a0d25e8c1357516"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "14.0.2"
|
||||||
|
geolocator_android:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: geolocator_android
|
||||||
|
sha256: "179c3cb66dfa674fc9ccbf2be872a02658724d1c067634e2c427cf6df7df901a"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "5.0.2"
|
||||||
|
geolocator_apple:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: geolocator_apple
|
||||||
|
sha256: dbdd8789d5aaf14cf69f74d4925ad1336b4433a6efdf2fce91e8955dc921bf22
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.3.13"
|
||||||
|
geolocator_linux:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: geolocator_linux
|
||||||
|
sha256: c4e966f0a7a87e70049eac7a2617f9e16fd4c585a26e4330bdfc3a71e6a721f3
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.2.3"
|
||||||
|
geolocator_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: geolocator_platform_interface
|
||||||
|
sha256: "30cb64f0b9adcc0fb36f628b4ebf4f731a2961a0ebd849f4b56200205056fe67"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "4.2.6"
|
||||||
|
geolocator_web:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: geolocator_web
|
||||||
|
sha256: b1ae9bdfd90f861fde8fd4f209c37b953d65e92823cb73c7dee1fa021b06f172
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "4.1.3"
|
||||||
|
geolocator_windows:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: geolocator_windows
|
||||||
|
sha256: "175435404d20278ffd220de83c2ca293b73db95eafbdc8131fe8609be1421eb6"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.2.5"
|
||||||
get:
|
get:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -184,6 +272,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "7.1.0"
|
version: "7.1.0"
|
||||||
|
gsettings:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: gsettings
|
||||||
|
sha256: "1b0ce661f5436d2db1e51f3c4295a49849f03d304003a7ba177d01e3a858249c"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.2.8"
|
||||||
hooks:
|
hooks:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -193,7 +289,7 @@ packages:
|
|||||||
source: hosted
|
source: hosted
|
||||||
version: "1.0.0"
|
version: "1.0.0"
|
||||||
http:
|
http:
|
||||||
dependency: transitive
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: http
|
name: http
|
||||||
sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412"
|
sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412"
|
||||||
@@ -368,6 +464,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.3.0"
|
version: "2.3.0"
|
||||||
|
petitparser:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: petitparser
|
||||||
|
sha256: "1a97266a94f7350d30ae522c0af07890c70b8e62c71e8e3920d1db4d23c057d1"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "7.0.1"
|
||||||
platform:
|
platform:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -525,6 +629,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.1.5"
|
version: "3.1.5"
|
||||||
|
uuid:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: uuid
|
||||||
|
sha256: a11b666489b1954e01d992f3d601b1804a33937b5a8fe677bd26b8a9f96f96e8
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "4.5.2"
|
||||||
vector_math:
|
vector_math:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -597,6 +709,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.1.0"
|
version: "1.1.0"
|
||||||
|
xml:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: xml
|
||||||
|
sha256: "971043b3a0d3da28727e40ed3e0b5d18b742fa5a68665cca88e74b7876d5e025"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "6.6.1"
|
||||||
yaml:
|
yaml:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|||||||
@@ -9,13 +9,15 @@ environment:
|
|||||||
sdk: ^3.10.7
|
sdk: ^3.10.7
|
||||||
|
|
||||||
dependencies:
|
dependencies:
|
||||||
|
appwrite: ^14.0.0
|
||||||
cupertino_icons: ^1.0.8
|
cupertino_icons: ^1.0.8
|
||||||
flutter:
|
flutter:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
|
geolocator: ^14.0.2
|
||||||
get: ^4.7.3
|
get: ^4.7.3
|
||||||
google_fonts: ^7.1.0
|
google_fonts: ^7.1.0
|
||||||
|
http: ^1.6.0
|
||||||
intl: ^0.20.2
|
intl: ^0.20.2
|
||||||
appwrite: ^14.0.0
|
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_lints: ^6.0.0
|
flutter_lints: ^6.0.0
|
||||||
|
|||||||
36
start-proxy.sh
Executable file
36
start-proxy.sh
Executable file
@@ -0,0 +1,36 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Farben für Output
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
BLUE='\033[0;34m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
NC='\033[0m' # No Color
|
||||||
|
|
||||||
|
echo -e "${BLUE}═══════════════════════════════════════════════════════${NC}"
|
||||||
|
echo -e "${BLUE} Flutter Tank App - Lokaler Proxy Starter${NC}"
|
||||||
|
echo -e "${BLUE}═══════════════════════════════════════════════════════${NC}"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Proxy Server starten
|
||||||
|
echo -e "${YELLOW}🚀 Starte lokalen Reverse Proxy Server...${NC}"
|
||||||
|
cd "$(dirname "$0")/proxy-server"
|
||||||
|
|
||||||
|
if [ ! -f "server.js" ]; then
|
||||||
|
echo -e "${YELLOW}❌ server.js nicht gefunden!${NC}"
|
||||||
|
echo -e "${YELLOW}Bitte führen Sie dieses Script aus dem Projekt-Root-Verzeichnis aus.${NC}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo -e "${GREEN}✅ Proxy Server wird gestartet auf http://localhost:3000${NC}"
|
||||||
|
echo ""
|
||||||
|
echo -e "${BLUE}📌 Nächste Schritte:${NC}"
|
||||||
|
echo -e " 1. In neuem Terminal: ${GREEN}flutter run -d chrome${NC}"
|
||||||
|
echo -e " 2. Neuen Tankeintrag erstellen"
|
||||||
|
echo -e " 3. Standort wird automatisch über Proxy abgerufen"
|
||||||
|
echo ""
|
||||||
|
echo -e "${YELLOW}⚠️ Zum Beenden: Strg+C drücken${NC}"
|
||||||
|
echo ""
|
||||||
|
echo -e "${BLUE}═══════════════════════════════════════════════════════${NC}"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
node server.js
|
||||||
Reference in New Issue
Block a user