feat: Complete Expo SDK 54 migration with React 19.1.0
- Migrated to Expo SDK 54.0.24 with proper React 19.1.0 support - Fixed all dependency compatibility issues (react-native-reanimated, worklets, etc.) - Implemented complete Dashboard with contract management - Added AddContractScreen with full form functionality - Added ContractDetailsScreen with edit/delete capabilities - Implemented ProfileScreen placeholder - Fixed navigation with proper Bottom Tabs - Added DatePickerInput and IconPickerModal components - Updated all TypeScript types for contracts - Removed deprecated react-native-screens enableScreens call - Successfully bundled and tested on iOS via Expo Go
This commit is contained in:
parent
4d48142752
commit
acd29e1a11
|
|
@ -7,7 +7,7 @@ SUPABASE_ANON_KEY=your-anon-key-here
|
||||||
# Gitea (NICHT IN GIT COMMITEN!)
|
# Gitea (NICHT IN GIT COMMITEN!)
|
||||||
GITEA_URL=http://192.168.1.142:3000
|
GITEA_URL=http://192.168.1.142:3000
|
||||||
GITEA_TOKEN=ec01d92db7f02dec1089cbb00076d9cbd533fd3f
|
GITEA_TOKEN=ec01d92db7f02dec1089cbb00076d9cbd533fd3f
|
||||||
GITEA_USER=Firstly
|
GITEA_ORG=Fristy-app
|
||||||
|
|
||||||
# App Configuration
|
# App Configuration
|
||||||
NODE_ENV=development
|
NODE_ENV=development
|
||||||
|
|
|
||||||
46
App.tsx
46
App.tsx
|
|
@ -1,44 +1,12 @@
|
||||||
import React, { useEffect } from 'react';
|
import React from 'react';
|
||||||
import { SafeAreaProvider } from 'react-native-safe-area-context';
|
import { RootNavigation } from './src/navigation/RootNavigation';
|
||||||
import { RootNavigation } from './src/navigation';
|
import { StatusBar } from 'expo-status-bar';
|
||||||
import { authService } from './src/modules/auth/services/authService';
|
|
||||||
import { useAuthStore } from './src/store';
|
|
||||||
|
|
||||||
function App(): React.JSX.Element {
|
|
||||||
const { setLoading } = useAuthStore();
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
// Session wiederherstellen beim App-Start
|
|
||||||
const initAuth = async () => {
|
|
||||||
setLoading(true);
|
|
||||||
await authService.restoreSession();
|
|
||||||
setLoading(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
initAuth();
|
|
||||||
|
|
||||||
// Auth State Listener
|
|
||||||
const { data: subscription } = authService.onAuthStateChange((session) => {
|
|
||||||
if (session) {
|
|
||||||
// Session aktiv
|
|
||||||
console.log('User authenticated');
|
|
||||||
} else {
|
|
||||||
// Keine Session
|
|
||||||
console.log('User logged out');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Cleanup
|
|
||||||
return () => {
|
|
||||||
subscription?.subscription?.unsubscribe();
|
|
||||||
};
|
|
||||||
}, [setLoading]);
|
|
||||||
|
|
||||||
|
export default function App() {
|
||||||
return (
|
return (
|
||||||
<SafeAreaProvider>
|
<>
|
||||||
<RootNavigation />
|
<RootNavigation />
|
||||||
</SafeAreaProvider>
|
<StatusBar style="auto" />
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default App;
|
|
||||||
|
|
|
||||||
|
|
@ -36,5 +36,5 @@ und dieses Projekt folgt [Semantic Versioning](https://semver.org/lang/de/).
|
||||||
- Supabase 2.38.4
|
- Supabase 2.38.4
|
||||||
- React Navigation 6.x
|
- React Navigation 6.x
|
||||||
|
|
||||||
[Unreleased]: http://192.168.1.142:3000/Firstly/fristy/compare/v0.0.1...HEAD
|
[Unreleased]: http://192.168.1.142:3000/Fristy-app/fristy/compare/v0.0.1...HEAD
|
||||||
[0.0.1]: http://192.168.1.142:3000/Firstly/fristy/releases/tag/v0.0.1
|
[0.0.1]: http://192.168.1.142:3000/Fristy-app/fristy/releases/tag/v0.0.1
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
## Server Details
|
## Server Details
|
||||||
- **URL**: http://192.168.1.142:3000
|
- **URL**: http://192.168.1.142:3000
|
||||||
- **Organization/User**: Firstly
|
- **Organization**: Fristy-app
|
||||||
- **Token**: Gespeichert in `.env` (nicht in Git!)
|
- **Token**: Gespeichert in `.env` (nicht in Git!)
|
||||||
|
|
||||||
## Repository Setup
|
## Repository Setup
|
||||||
|
|
@ -11,7 +11,7 @@
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 1. Remote hinzufügen
|
# 1. Remote hinzufügen
|
||||||
git remote add origin http://192.168.1.142:3000/Firstly/fristy.git
|
git remote add origin http://192.168.1.142:3000/Fristy-app/fristy.git
|
||||||
|
|
||||||
# 2. Initial Commit (falls noch nicht gemacht)
|
# 2. Initial Commit (falls noch nicht gemacht)
|
||||||
git add .
|
git add .
|
||||||
|
|
@ -30,7 +30,7 @@ git push -u origin develop
|
||||||
**Option 1: HTTPS mit Token**
|
**Option 1: HTTPS mit Token**
|
||||||
```bash
|
```bash
|
||||||
# Token als Passwort verwenden
|
# Token als Passwort verwenden
|
||||||
git remote set-url origin http://<username>:<token>@192.168.1.142:3000/Firstly/fristy.git
|
git remote set-url origin http://<username>:<token>@192.168.1.142:3000/Fristy-app/fristy.git
|
||||||
```
|
```
|
||||||
|
|
||||||
**Option 2: Git Credential Helper**
|
**Option 2: Git Credential Helper**
|
||||||
|
|
@ -49,7 +49,7 @@ ssh-keygen -t ed25519 -C "your-email@example.com"
|
||||||
# Unter: http://192.168.1.142:3000/user/settings/keys
|
# Unter: http://192.168.1.142:3000/user/settings/keys
|
||||||
|
|
||||||
# Remote auf SSH umstellen
|
# Remote auf SSH umstellen
|
||||||
git remote set-url origin git@192.168.1.142:Firstly/fristy.git
|
git remote set-url origin git@192.168.1.142:Fristy-app/fristy.git
|
||||||
```
|
```
|
||||||
|
|
||||||
## Gitea-spezifische Features
|
## Gitea-spezifische Features
|
||||||
|
|
@ -146,7 +146,9 @@ brew install tea # macOS
|
||||||
tea login add
|
tea login add
|
||||||
|
|
||||||
# Repository clonen
|
# Repository clonen
|
||||||
tea clone Firstly/fristy
|
```bash
|
||||||
|
# Repository clonen
|
||||||
|
tea clone Fristy-app/fristy
|
||||||
|
|
||||||
# Issues erstellen
|
# Issues erstellen
|
||||||
tea issues create
|
tea issues create
|
||||||
|
|
@ -185,7 +187,7 @@ git config --global http.sslVerify false
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Token in URL
|
# Token in URL
|
||||||
git clone http://token@192.168.1.142:3000/Firstly/fristy.git
|
git clone http://token@192.168.1.142:3000/Fristy-app/fristy.git
|
||||||
|
|
||||||
# Oder in .netrc speichern
|
# Oder in .netrc speichern
|
||||||
# ~/.netrc
|
# ~/.netrc
|
||||||
|
|
@ -220,11 +222,11 @@ git push origin main
|
||||||
|
|
||||||
## Repository-Links
|
## Repository-Links
|
||||||
|
|
||||||
- **Repository**: http://192.168.1.142:3000/Firstly/fristy
|
- **Repository**: http://192.168.1.142:3000/Fristy-app/fristy
|
||||||
- **Issues**: http://192.168.1.142:3000/Firstly/fristy/issues
|
- **Issues**: http://192.168.1.142:3000/Fristy-app/fristy/issues
|
||||||
- **Pull Requests**: http://192.168.1.142:3000/Firstly/fristy/pulls
|
- **Pull Requests**: http://192.168.1.142:3000/Fristy-app/fristy/pulls
|
||||||
- **Releases**: http://192.168.1.142:3000/Firstly/fristy/releases
|
- **Releases**: http://192.168.1.142:3000/Fristy-app/fristy/releases
|
||||||
- **Settings**: http://192.168.1.142:3000/Firstly/fristy/settings
|
- **Settings**: http://192.168.1.142:3000/Fristy-app/fristy/settings
|
||||||
|
|
||||||
## Quick Commands
|
## Quick Commands
|
||||||
|
|
||||||
|
|
@ -233,7 +235,7 @@ git push origin main
|
||||||
git init
|
git init
|
||||||
git add .
|
git add .
|
||||||
git commit -m "chore: initial project setup"
|
git commit -m "chore: initial project setup"
|
||||||
git remote add origin http://192.168.1.142:3000/Firstly/fristy.git
|
git remote add origin http://192.168.1.142:3000/Fristy-app/fristy.git
|
||||||
git push -u origin main
|
git push -u origin main
|
||||||
|
|
||||||
# Develop Branch
|
# Develop Branch
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,174 @@
|
||||||
|
# React Native Test Guide
|
||||||
|
|
||||||
|
## 📱 Erste Schritte zum App-Testen
|
||||||
|
|
||||||
|
### Option 1: Expo (Schnellste Methode - EMPFOHLEN für erste Tests)
|
||||||
|
|
||||||
|
Expo ist einfacher und schneller für erste Tests ohne native Builds.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Expo installieren
|
||||||
|
npm install --global expo-cli
|
||||||
|
|
||||||
|
# 2. Expo projekt initialisieren (in separatem Ordner)
|
||||||
|
npx create-expo-app fristy-test
|
||||||
|
cd fristy-test
|
||||||
|
|
||||||
|
# 3. Unsere Source-Files kopieren
|
||||||
|
# (Nur src/ Ordner kopieren)
|
||||||
|
|
||||||
|
# 4. Expo starten
|
||||||
|
npx expo start
|
||||||
|
|
||||||
|
# 5. Expo Go App auf Handy installieren
|
||||||
|
# iOS: App Store
|
||||||
|
# Android: Play Store
|
||||||
|
|
||||||
|
# 6. QR-Code scannen und App testen
|
||||||
|
```
|
||||||
|
|
||||||
|
### Option 2: React Native CLI (Für Production)
|
||||||
|
|
||||||
|
Für vollständige iOS/Android Builds.
|
||||||
|
|
||||||
|
#### Voraussetzungen:
|
||||||
|
|
||||||
|
**macOS (für iOS):**
|
||||||
|
```bash
|
||||||
|
# Xcode installieren (App Store)
|
||||||
|
# Command Line Tools
|
||||||
|
xcode-select --install
|
||||||
|
|
||||||
|
# Homebrew
|
||||||
|
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
|
||||||
|
|
||||||
|
# Node, Watchman, CocoaPods
|
||||||
|
brew install node
|
||||||
|
brew install watchman
|
||||||
|
sudo gem install cocoapods
|
||||||
|
```
|
||||||
|
|
||||||
|
**Android:**
|
||||||
|
```bash
|
||||||
|
# Android Studio herunterladen
|
||||||
|
# https://developer.android.com/studio
|
||||||
|
|
||||||
|
# Android SDK installieren
|
||||||
|
# Android Studio > Preferences > Appearance & Behavior > System Settings > Android SDK
|
||||||
|
# - Android 13 (Tiramisu) SDK installieren
|
||||||
|
# - Android SDK Build-Tools
|
||||||
|
# - Android SDK Platform-Tools
|
||||||
|
|
||||||
|
# Umgebungsvariablen setzen
|
||||||
|
export ANDROID_HOME=$HOME/Library/Android/sdk
|
||||||
|
export PATH=$PATH:$ANDROID_HOME/emulator
|
||||||
|
export PATH=$PATH:$ANDROID_HOME/platform-tools
|
||||||
|
```
|
||||||
|
|
||||||
|
#### React Native Projekt initialisieren:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# React Native CLI global installieren
|
||||||
|
npm install -g react-native-cli
|
||||||
|
|
||||||
|
# Neues Projekt mit TypeScript
|
||||||
|
npx react-native@latest init FristyApp --template react-native-template-typescript
|
||||||
|
|
||||||
|
# In Projekt-Ordner
|
||||||
|
cd FristyApp
|
||||||
|
|
||||||
|
# Unsere Source-Files kopieren
|
||||||
|
# src/ Ordner von unserem Projekt kopieren
|
||||||
|
```
|
||||||
|
|
||||||
|
### Option 3: Expo mit unserem bestehenden Code (BESTE OPTION)
|
||||||
|
|
||||||
|
Wir konvertieren unser Projekt zu Expo für schnelles Testen:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Expo Dependencies hinzufügen
|
||||||
|
npm install expo expo-status-bar
|
||||||
|
|
||||||
|
# 2. Metro Config für Expo
|
||||||
|
# (Wird automatisch erstellt)
|
||||||
|
|
||||||
|
# 3. App starten
|
||||||
|
npx expo start
|
||||||
|
|
||||||
|
# 4. Im Terminal:
|
||||||
|
# - i für iOS Simulator
|
||||||
|
# - a für Android Emulator
|
||||||
|
# - w für Web Browser
|
||||||
|
# - QR-Code für Expo Go App
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🚀 Schnellstart: Expo-Setup (EMPFOHLEN)
|
||||||
|
|
||||||
|
Ich empfehle Expo für die ersten Tests, da es:
|
||||||
|
- ✅ Keine native Builds benötigt
|
||||||
|
- ✅ Sofort auf echtem Gerät testbar
|
||||||
|
- ✅ Hot Reload funktioniert perfekt
|
||||||
|
- ✅ Später zu React Native CLI migrierbar
|
||||||
|
|
||||||
|
### Schritt-für-Schritt:
|
||||||
|
|
||||||
|
1. **Expo Dependencies installieren:**
|
||||||
|
```bash
|
||||||
|
cd /Users/justinursan/Documents/Projekt/fristy/app-v0.0.1
|
||||||
|
npm install expo expo-status-bar --legacy-peer-deps
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Metro Bundler starten:**
|
||||||
|
```bash
|
||||||
|
npx expo start
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Expo Go App installieren:**
|
||||||
|
- iOS: https://apps.apple.com/app/expo-go/id982107779
|
||||||
|
- Android: https://play.google.com/store/apps/details?id=host.exp.exponent
|
||||||
|
|
||||||
|
4. **QR-Code scannen und testen!**
|
||||||
|
|
||||||
|
## 🔧 Aktuelle Einschränkungen
|
||||||
|
|
||||||
|
Unser aktuelles Setup hat noch keine nativen Projektdateien (ios/, android/).
|
||||||
|
|
||||||
|
**Du hast 2 Optionen:**
|
||||||
|
|
||||||
|
### Option A: Expo-basiert (schnell & einfach)
|
||||||
|
- Installiere Expo Dependencies
|
||||||
|
- Teste sofort mit Expo Go
|
||||||
|
- Später React Native CLI hinzufügen
|
||||||
|
|
||||||
|
### Option B: Vollständiges React Native Setup
|
||||||
|
- Native Projekte generieren
|
||||||
|
- Xcode/Android Studio konfigurieren
|
||||||
|
- Volle Kontrolle über native Code
|
||||||
|
|
||||||
|
## 📝 Empfohlene Reihenfolge
|
||||||
|
|
||||||
|
1. **Jetzt: Expo für schnelles Prototyping**
|
||||||
|
```bash
|
||||||
|
npm install expo expo-status-bar --legacy-peer-deps
|
||||||
|
npx expo start
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Später: Migration zu React Native CLI**
|
||||||
|
```bash
|
||||||
|
npx create-react-native-app --template
|
||||||
|
# Native Ordner generieren
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Production: Vollständige Builds**
|
||||||
|
- iOS: Xcode
|
||||||
|
- Android: Android Studio
|
||||||
|
|
||||||
|
## 🎯 Was ich jetzt machen soll?
|
||||||
|
|
||||||
|
Sag mir einfach:
|
||||||
|
|
||||||
|
**A)** "Expo Setup" - Ich richte Expo ein für schnelles Testen
|
||||||
|
**B)** "React Native CLI" - Ich erstelle vollständiges RN-Projekt
|
||||||
|
**C)** "Erst mal nur Code sehen" - Ich zeige dir die Struktur
|
||||||
|
|
||||||
|
Welche Option möchtest du? 😊
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
{
|
||||||
|
"expo": {
|
||||||
|
"name": "Fristy",
|
||||||
|
"slug": "fristy",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"orientation": "portrait",
|
||||||
|
"icon": "./assets/icon.png",
|
||||||
|
"userInterfaceStyle": "light",
|
||||||
|
"splash": {
|
||||||
|
"image": "./assets/splash.png",
|
||||||
|
"resizeMode": "contain",
|
||||||
|
"backgroundColor": "#6366F1"
|
||||||
|
},
|
||||||
|
"assetBundlePatterns": [
|
||||||
|
"**/*"
|
||||||
|
],
|
||||||
|
"ios": {
|
||||||
|
"supportsTablet": true,
|
||||||
|
"bundleIdentifier": "com.fristyapp.fristy"
|
||||||
|
},
|
||||||
|
"android": {
|
||||||
|
"adaptiveIcon": {
|
||||||
|
"foregroundImage": "./assets/adaptive-icon.png",
|
||||||
|
"backgroundColor": "#6366F1"
|
||||||
|
},
|
||||||
|
"package": "com.fristyapp.fristy"
|
||||||
|
},
|
||||||
|
"web": {
|
||||||
|
"favicon": "./assets/favicon.png"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,21 +1,6 @@
|
||||||
module.exports = {
|
module.exports = function(api) {
|
||||||
presets: ['module:@react-native/babel-preset'],
|
api.cache(true);
|
||||||
plugins: [
|
return {
|
||||||
[
|
presets: ['babel-preset-expo'],
|
||||||
'module-resolver',
|
};
|
||||||
{
|
|
||||||
root: ['./src'],
|
|
||||||
extensions: ['.ios.js', '.android.js', '.js', '.ts', '.tsx', '.json'],
|
|
||||||
alias: {
|
|
||||||
'@modules': './src/modules',
|
|
||||||
'@shared': './src/shared',
|
|
||||||
'@navigation': './src/navigation',
|
|
||||||
'@store': './src/store',
|
|
||||||
'@config': './src/config',
|
|
||||||
'@assets': './assets',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
'react-native-reanimated/plugin',
|
|
||||||
],
|
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
const { getDefaultConfig } = require('expo/metro-config');
|
||||||
|
|
||||||
|
const config = getDefaultConfig(__dirname);
|
||||||
|
|
||||||
|
module.exports = config;
|
||||||
File diff suppressed because it is too large
Load Diff
62
package.json
62
package.json
|
|
@ -34,47 +34,59 @@
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@react-native-community/datetimepicker": "8.4.4",
|
||||||
|
"@react-native-community/netinfo": "^11.4.1",
|
||||||
|
"@react-navigation/bottom-tabs": "^6.5.11",
|
||||||
"@react-navigation/native": "^6.1.9",
|
"@react-navigation/native": "^6.1.9",
|
||||||
"@react-navigation/native-stack": "^6.9.17",
|
"@react-navigation/native-stack": "^6.9.17",
|
||||||
"@react-navigation/bottom-tabs": "^6.5.11",
|
|
||||||
"@supabase/supabase-js": "^2.38.4",
|
"@supabase/supabase-js": "^2.38.4",
|
||||||
"react": "18.2.0",
|
|
||||||
"react-native": "0.73.0",
|
|
||||||
"react-native-safe-area-context": "^4.8.0",
|
|
||||||
"react-native-screens": "^3.29.0",
|
|
||||||
"react-native-vector-icons": "^10.0.3",
|
|
||||||
"react-native-gesture-handler": "^2.14.0",
|
|
||||||
"react-native-reanimated": "^3.6.1",
|
|
||||||
"zustand": "^4.4.7",
|
|
||||||
"date-fns": "^3.0.6",
|
"date-fns": "^3.0.6",
|
||||||
|
"expo": "^54.0.24",
|
||||||
|
"expo-constants": "^18.0.10",
|
||||||
|
"expo-status-bar": "^3.0.8",
|
||||||
|
"react": "19.1.0",
|
||||||
|
"react-dom": "19.1.0",
|
||||||
|
"react-native": "0.81.5",
|
||||||
|
"react-native-biometrics": "^3.0.1",
|
||||||
"react-native-document-picker": "^9.1.1",
|
"react-native-document-picker": "^9.1.1",
|
||||||
"react-native-push-notification": "^8.1.1",
|
|
||||||
"react-native-encrypted-storage": "^4.0.3",
|
"react-native-encrypted-storage": "^4.0.3",
|
||||||
"react-native-biometrics": "^3.0.1"
|
"react-native-gesture-handler": "~2.28.0",
|
||||||
|
"react-native-push-notification": "^8.1.1",
|
||||||
|
"react-native-reanimated": "~4.1.1",
|
||||||
|
"react-native-safe-area-context": "~5.6.0",
|
||||||
|
"react-native-screens": "~4.16.0",
|
||||||
|
"react-native-vector-icons": "^10.0.3",
|
||||||
|
"react-native-web": "^0.21.2",
|
||||||
|
"react-native-worklets": "^0.6.1",
|
||||||
|
"react-native-worklets-core": "^1.6.2",
|
||||||
|
"zustand": "^4.4.7"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.23.5",
|
"@babel/core": "^7.23.5",
|
||||||
"@babel/preset-env": "^7.23.5",
|
"@babel/preset-env": "^7.23.5",
|
||||||
"@babel/runtime": "^7.23.5",
|
"@babel/runtime": "^7.28.4",
|
||||||
|
"@commitlint/cli": "^18.4.3",
|
||||||
|
"@commitlint/config-conventional": "^18.4.3",
|
||||||
"@react-native/babel-preset": "^0.73.18",
|
"@react-native/babel-preset": "^0.73.18",
|
||||||
"@react-native/eslint-config": "^0.73.1",
|
"@react-native/eslint-config": "^0.73.1",
|
||||||
"@react-native/metro-config": "^0.73.2",
|
"@react-native/metro-config": "^0.73.2",
|
||||||
"@react-native/typescript-config": "^0.73.1",
|
"@react-native/typescript-config": "^0.73.1",
|
||||||
"@types/react": "^18.2.45",
|
|
||||||
"@types/react-test-renderer": "^18.0.7",
|
|
||||||
"typescript": "^5.3.3",
|
|
||||||
"jest": "^29.7.0",
|
|
||||||
"@testing-library/react-native": "^12.4.2",
|
|
||||||
"prettier": "^3.1.1",
|
|
||||||
"eslint": "^8.56.0",
|
|
||||||
"@commitlint/cli": "^18.4.3",
|
|
||||||
"@commitlint/config-conventional": "^18.4.3",
|
|
||||||
"husky": "^8.0.3",
|
|
||||||
"lint-staged": "^15.2.0",
|
|
||||||
"standard-version": "^9.5.0",
|
|
||||||
"@semantic-release/changelog": "^6.0.3",
|
"@semantic-release/changelog": "^6.0.3",
|
||||||
"@semantic-release/git": "^10.0.1",
|
"@semantic-release/git": "^10.0.1",
|
||||||
"semantic-release": "^22.0.12"
|
"@testing-library/react-native": "^12.4.2",
|
||||||
|
"@types/react": "~19.1.10",
|
||||||
|
"@types/react-test-renderer": "^18.0.7",
|
||||||
|
"babel-plugin-module-resolver": "^5.0.2",
|
||||||
|
"babel-preset-expo": "^54.0.7",
|
||||||
|
"eslint": "^8.56.0",
|
||||||
|
"husky": "^8.0.3",
|
||||||
|
"jest": "^29.7.0",
|
||||||
|
"lint-staged": "^15.2.0",
|
||||||
|
"prettier": "^3.1.1",
|
||||||
|
"react-test-renderer": "18.2.0",
|
||||||
|
"semantic-release": "^22.0.12",
|
||||||
|
"standard-version": "^9.5.0",
|
||||||
|
"typescript": "^5.3.3"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18",
|
"node": ">=18",
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,34 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Fristy Repository in Gitea erstellen
|
||||||
|
# Verwendung: ./scripts/create-gitea-repo.sh
|
||||||
|
|
||||||
|
GITEA_URL="http://192.168.1.142:3000"
|
||||||
|
GITEA_TOKEN="ec01d92db7f02dec1089cbb00076d9cbd533fd3f"
|
||||||
|
REPO_NAME="fristy"
|
||||||
|
REPO_DESCRIPTION="Modulare Vertrags-Management App für iOS und Android"
|
||||||
|
|
||||||
|
echo "📦 Erstelle Repository in Gitea..."
|
||||||
|
|
||||||
|
# Repository erstellen
|
||||||
|
curl -X POST "${GITEA_URL}/api/v1/user/repos" \
|
||||||
|
-H "Authorization: token ${GITEA_TOKEN}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"name": "'"${REPO_NAME}"'",
|
||||||
|
"description": "'"${REPO_DESCRIPTION}"'",
|
||||||
|
"private": false,
|
||||||
|
"auto_init": false,
|
||||||
|
"default_branch": "main",
|
||||||
|
"gitignores": "",
|
||||||
|
"license": "",
|
||||||
|
"readme": "Default"
|
||||||
|
}'
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "✅ Repository erstellt!"
|
||||||
|
echo "🔗 URL: ${GITEA_URL}/Firstly/${REPO_NAME}"
|
||||||
|
echo ""
|
||||||
|
echo "Nächste Schritte:"
|
||||||
|
echo "1. git remote set-url origin ${GITEA_URL}/Firstly/${REPO_NAME}.git"
|
||||||
|
echo "2. git push -u origin main"
|
||||||
|
|
@ -0,0 +1,191 @@
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
import { View, Text, StyleSheet, ScrollView, TouchableOpacity, TextInput, Switch, Platform } from 'react-native';
|
||||||
|
import { Contract, ContractCategory } from '../../../shared/types';
|
||||||
|
import { DatePickerInput } from '../../../shared/components/DatePickerInput';
|
||||||
|
|
||||||
|
interface AddContractScreenProps {
|
||||||
|
onClose: () => void;
|
||||||
|
onSave: (contract: Omit<Contract, 'id'>) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AddContractScreen: React.FC<AddContractScreenProps> = ({ onClose, onSave }) => {
|
||||||
|
const [name, setName] = useState('');
|
||||||
|
const [provider, setProvider] = useState('');
|
||||||
|
const [category, setCategory] = useState<ContractCategory>('other');
|
||||||
|
const [amount, setAmount] = useState('0');
|
||||||
|
const [billingCycle, setBillingCycle] = useState<'monthly' | 'quarterly' | 'yearly'>('monthly');
|
||||||
|
const [startDate, setStartDate] = useState('');
|
||||||
|
const [endDate, setEndDate] = useState('');
|
||||||
|
const [autoRenewal, setAutoRenewal] = useState(true);
|
||||||
|
const [cancellationPeriod, setCancellationPeriod] = useState('');
|
||||||
|
const [notes, setNotes] = useState('');
|
||||||
|
|
||||||
|
const getCategoryIcon = (cat: string) => {
|
||||||
|
const icons: Record<string, string> = {
|
||||||
|
internet: '🌐',
|
||||||
|
mobile: '📱',
|
||||||
|
streaming: '📺',
|
||||||
|
utilities: '💡',
|
||||||
|
insurance: '🛡️',
|
||||||
|
other: '📄',
|
||||||
|
};
|
||||||
|
return icons[cat] || '📄';
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSave = () => {
|
||||||
|
if (!name.trim() || !provider.trim()) {
|
||||||
|
alert('Bitte fülle mindestens Name und Anbieter aus!');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const newContract: Omit<Contract, 'id'> = {
|
||||||
|
name: name.trim(),
|
||||||
|
provider: provider.trim(),
|
||||||
|
category,
|
||||||
|
amount: parseFloat(amount) || 0,
|
||||||
|
currency: 'EUR',
|
||||||
|
billingCycle,
|
||||||
|
startDate: startDate || new Date().toISOString().split('T')[0],
|
||||||
|
endDate: endDate || undefined,
|
||||||
|
autoRenewal,
|
||||||
|
cancellationPeriod: cancellationPeriod || undefined,
|
||||||
|
notes: notes || undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
onSave(newContract);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={styles.container}>
|
||||||
|
<View style={styles.header}>
|
||||||
|
<TouchableOpacity onPress={onClose} style={styles.closeButton}>
|
||||||
|
<Text style={styles.closeIcon}>✕</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
<Text style={styles.headerTitle}>Neuer Vertrag</Text>
|
||||||
|
<View style={styles.placeholder} />
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<ScrollView style={styles.content} contentContainerStyle={styles.scrollContent}>
|
||||||
|
<View style={styles.card}>
|
||||||
|
<Text style={styles.cardTitle}>Kategorie wählen</Text>
|
||||||
|
<View style={styles.categoryGrid}>
|
||||||
|
{(['internet', 'mobile', 'streaming', 'utilities', 'insurance', 'other'] as ContractCategory[]).map((cat) => (
|
||||||
|
<TouchableOpacity
|
||||||
|
key={cat}
|
||||||
|
style={[styles.categoryCard, category === cat && styles.categoryCardActive]}
|
||||||
|
onPress={() => setCategory(cat)}
|
||||||
|
>
|
||||||
|
<Text style={styles.categoryIcon}>{getCategoryIcon(cat)}</Text>
|
||||||
|
<Text style={styles.categoryLabel}>
|
||||||
|
{cat === 'internet' ? 'Internet' :
|
||||||
|
cat === 'mobile' ? 'Mobilfunk' :
|
||||||
|
cat === 'streaming' ? 'Streaming' :
|
||||||
|
cat === 'utilities' ? 'Versorgung' :
|
||||||
|
cat === 'insurance' ? 'Versicherung' : 'Sonstiges'}
|
||||||
|
</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View style={styles.card}>
|
||||||
|
<Text style={styles.cardTitle}>Grunddaten</Text>
|
||||||
|
<View style={styles.inputGroup}>
|
||||||
|
<Text style={styles.inputLabel}>Vertragsname *</Text>
|
||||||
|
<TextInput style={styles.textInput} value={name} onChangeText={setName} placeholder="z.B. Vodafone DSL" placeholderTextColor="#C7C7CC" />
|
||||||
|
</View>
|
||||||
|
<View style={styles.inputGroup}>
|
||||||
|
<Text style={styles.inputLabel}>Anbieter *</Text>
|
||||||
|
<TextInput style={styles.textInput} value={provider} onChangeText={setProvider} placeholder="z.B. Vodafone" placeholderTextColor="#C7C7CC" />
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View style={styles.card}>
|
||||||
|
<Text style={styles.cardTitle}>Kosten</Text>
|
||||||
|
<View style={styles.inputGroup}>
|
||||||
|
<Text style={styles.inputLabel}>Betrag (EUR)</Text>
|
||||||
|
<TextInput style={styles.textInput} value={amount} onChangeText={setAmount} placeholder="0.00" keyboardType="decimal-pad" placeholderTextColor="#C7C7CC" />
|
||||||
|
</View>
|
||||||
|
<View style={styles.inputGroup}>
|
||||||
|
<Text style={styles.inputLabel}>Zahlungsrhythmus</Text>
|
||||||
|
<View style={styles.cycleButtons}>
|
||||||
|
{[
|
||||||
|
{ value: 'monthly', label: 'Monatlich' },
|
||||||
|
{ value: 'quarterly', label: 'Vierteljährlich' },
|
||||||
|
{ value: 'yearly', label: 'Jährlich' }
|
||||||
|
].map((cycle) => (
|
||||||
|
<TouchableOpacity key={cycle.value} style={[styles.cycleButton, billingCycle === cycle.value && styles.cycleButtonActive]} onPress={() => setBillingCycle(cycle.value as any)}>
|
||||||
|
<Text style={[styles.cycleButtonText, billingCycle === cycle.value && styles.cycleButtonTextActive]}>{cycle.label}</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View style={styles.card}>
|
||||||
|
<Text style={styles.cardTitle}>Vertragslaufzeit</Text>
|
||||||
|
<View style={styles.inputGroup}>
|
||||||
|
<Text style={styles.inputLabel}>Startdatum</Text>
|
||||||
|
<DatePickerInput value={startDate} onChange={setStartDate} placeholder="Startdatum wählen" />
|
||||||
|
</View>
|
||||||
|
<View style={styles.inputGroup}>
|
||||||
|
<Text style={styles.inputLabel}>Enddatum (optional)</Text>
|
||||||
|
<DatePickerInput value={endDate} onChange={setEndDate} placeholder="Enddatum wählen (optional)" />
|
||||||
|
</View>
|
||||||
|
<View style={styles.switchRow}>
|
||||||
|
<Text style={styles.inputLabel}>Automatische Verlängerung</Text>
|
||||||
|
<Switch value={autoRenewal} onValueChange={setAutoRenewal} trackColor={{ false: '#D1D1D6', true: '#34C75980' }} thumbColor={autoRenewal ? '#34C759' : '#F4F3F4'} />
|
||||||
|
</View>
|
||||||
|
<View style={styles.inputGroup}>
|
||||||
|
<Text style={styles.inputLabel}>Kündigungsfrist (optional)</Text>
|
||||||
|
<TextInput style={styles.textInput} value={cancellationPeriod} onChangeText={setCancellationPeriod} placeholder="z.B. 3 Monate" placeholderTextColor="#C7C7CC" />
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View style={styles.card}>
|
||||||
|
<Text style={styles.cardTitle}>Notizen (optional)</Text>
|
||||||
|
<TextInput style={styles.notesInput} value={notes} onChangeText={setNotes} placeholder="Füge zusätzliche Informationen hinzu..." placeholderTextColor="#C7C7CC" multiline numberOfLines={4} />
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View style={styles.actionButtons}>
|
||||||
|
<TouchableOpacity style={styles.cancelButton} onPress={onClose}><Text style={styles.cancelButtonText}>Abbrechen</Text></TouchableOpacity>
|
||||||
|
<TouchableOpacity style={styles.saveButton} onPress={handleSave}><Text style={styles.saveButtonText}>Speichern</Text></TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
<View style={{ height: 100 }} />
|
||||||
|
</ScrollView>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
container: { flex: 1, backgroundColor: '#F2F2F7' },
|
||||||
|
header: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingTop: 60, paddingHorizontal: 20, paddingBottom: 16, backgroundColor: '#F2F2F7' },
|
||||||
|
closeButton: { width: 40, height: 40, borderRadius: 20, backgroundColor: 'rgba(255, 255, 255, 0.7)', alignItems: 'center', justifyContent: 'center' },
|
||||||
|
closeIcon: { fontSize: 24, color: '#007AFF', fontWeight: '300' as any },
|
||||||
|
headerTitle: { fontSize: 17, fontWeight: '600' as any, color: '#000000' },
|
||||||
|
placeholder: { width: 40 },
|
||||||
|
content: { flex: 1 },
|
||||||
|
scrollContent: { paddingHorizontal: 20 },
|
||||||
|
card: { backgroundColor: Platform.OS === 'ios' ? 'rgba(255, 255, 255, 0.7)' : '#FFFFFF', borderRadius: 20, padding: 20, marginBottom: 16, borderWidth: 1, borderColor: 'rgba(255, 255, 255, 0.5)', shadowColor: '#000', shadowOffset: { width: 0, height: 4 }, shadowOpacity: 0.1, shadowRadius: 12, elevation: 4 },
|
||||||
|
cardTitle: { fontSize: 20, fontWeight: 'bold', color: '#000000', marginBottom: 16 },
|
||||||
|
categoryGrid: { flexDirection: 'row', flexWrap: 'wrap', gap: 12 },
|
||||||
|
categoryCard: { width: '30%', aspectRatio: 1, backgroundColor: '#F2F2F7', borderRadius: 16, alignItems: 'center', justifyContent: 'center', padding: 12 },
|
||||||
|
categoryCardActive: { backgroundColor: '#007AFF20', borderWidth: 2, borderColor: '#007AFF' },
|
||||||
|
categoryIcon: { fontSize: 36, marginBottom: 8 },
|
||||||
|
categoryLabel: { fontSize: 12, color: '#000000', textAlign: 'center', fontWeight: '600' as any },
|
||||||
|
inputGroup: { marginBottom: 16 },
|
||||||
|
inputLabel: { fontSize: 15, color: '#000000', marginBottom: 8, fontWeight: '600' as any },
|
||||||
|
textInput: { backgroundColor: '#F2F2F7', borderRadius: 12, padding: 14, fontSize: 17, color: '#000000', borderWidth: 1, borderColor: 'transparent' },
|
||||||
|
cycleButtons: { gap: 8 },
|
||||||
|
cycleButton: { backgroundColor: '#F2F2F7', padding: 14, borderRadius: 12, alignItems: 'center' },
|
||||||
|
cycleButtonActive: { backgroundColor: '#007AFF' },
|
||||||
|
cycleButtonText: { fontSize: 17, color: '#000000', fontWeight: '600' as any },
|
||||||
|
cycleButtonTextActive: { color: '#FFFFFF' },
|
||||||
|
switchRow: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginBottom: 16 },
|
||||||
|
notesInput: { backgroundColor: '#F2F2F7', borderRadius: 12, padding: 14, fontSize: 15, color: '#000000', minHeight: 100, textAlignVertical: 'top' },
|
||||||
|
actionButtons: { flexDirection: 'row', gap: 12, marginTop: 8 },
|
||||||
|
cancelButton: { flex: 1, backgroundColor: '#F2F2F7', padding: 16, borderRadius: 12, alignItems: 'center' },
|
||||||
|
cancelButtonText: { fontSize: 17, fontWeight: '600' as any, color: '#000000' },
|
||||||
|
saveButton: { flex: 1, backgroundColor: '#007AFF', padding: 16, borderRadius: 12, alignItems: 'center' },
|
||||||
|
saveButtonText: { fontSize: 17, fontWeight: '600' as any, color: '#FFFFFF' },
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,127 @@
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
import { View, Text, StyleSheet, ScrollView, TouchableOpacity, TextInput, Switch, Platform } from 'react-native';
|
||||||
|
import { Contract } from '../../../shared/types';
|
||||||
|
import { DatePickerInput } from '../../../shared/components/DatePickerInput';
|
||||||
|
import { IconPickerModal } from '../../../shared/components/IconPickerModal';
|
||||||
|
|
||||||
|
interface ContractDetailsScreenProps {
|
||||||
|
contract: Contract;
|
||||||
|
onClose: () => void;
|
||||||
|
onSave: (contract: Contract) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ContractDetailsScreen: React.FC<ContractDetailsScreenProps> = ({ contract, onClose, onSave }) => {
|
||||||
|
const [isEditing, setIsEditing] = useState(false);
|
||||||
|
const [editedContract, setEditedContract] = useState<Contract>(contract);
|
||||||
|
const [showIconPicker, setShowIconPicker] = useState(false);
|
||||||
|
|
||||||
|
const getCategoryIcon = (category: string) => {
|
||||||
|
const icons: Record<string, string> = { internet: '🌐', mobile: '📱', streaming: '📺', utilities: '💡', insurance: '🛡️', other: '📄' };
|
||||||
|
return icons[category] || '📄';
|
||||||
|
};
|
||||||
|
|
||||||
|
const getDefaultColor = (category: string) => {
|
||||||
|
const colors: Record<string, string> = { internet: '#007AFF', mobile: '#34C759', streaming: '#E50914', utilities: '#FFCC00', insurance: '#0066B2', other: '#8E8E93' };
|
||||||
|
return colors[category] || '#8E8E93';
|
||||||
|
};
|
||||||
|
|
||||||
|
const displayIcon = editedContract.customIcon || getCategoryIcon(editedContract.category);
|
||||||
|
const displayColor = editedContract.iconColor || getDefaultColor(editedContract.category);
|
||||||
|
|
||||||
|
const handleSave = () => {
|
||||||
|
onSave(editedContract);
|
||||||
|
setIsEditing(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleIconSelect = (icon: string, color: string) => {
|
||||||
|
setEditedContract({ ...editedContract, customIcon: icon, iconColor: color });
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={styles.container}>
|
||||||
|
<View style={styles.header}>
|
||||||
|
<TouchableOpacity onPress={onClose} style={styles.closeButton}><Text style={styles.closeIcon}>✕</Text></TouchableOpacity>
|
||||||
|
<Text style={styles.headerTitle}>Vertragsdetails</Text>
|
||||||
|
<TouchableOpacity onPress={isEditing ? handleSave : () => setIsEditing(true)} style={styles.editButton}>
|
||||||
|
<Text style={styles.editButtonText}>{isEditing ? 'Fertig' : 'Bearbeiten'}</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<ScrollView style={styles.content} contentContainerStyle={styles.scrollContent}>
|
||||||
|
<View style={styles.iconSection}>
|
||||||
|
<TouchableOpacity style={[styles.iconContainer, { backgroundColor: displayColor + '20' }]} onPress={isEditing ? () => setShowIconPicker(true) : undefined} disabled={!isEditing}>
|
||||||
|
<Text style={styles.icon}>{displayIcon}</Text>
|
||||||
|
{isEditing && <View style={styles.editIconBadge}><Text style={styles.editIconBadgeText}>✎</Text></View>}
|
||||||
|
</TouchableOpacity>
|
||||||
|
{isEditing ? <TextInput style={styles.titleInput} value={editedContract.name} onChangeText={(text) => setEditedContract({ ...editedContract, name: text })} /> : <Text style={styles.title}>{contract.name}</Text>}
|
||||||
|
<Text style={styles.provider}>{contract.provider}</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View style={styles.card}>
|
||||||
|
<Text style={styles.cardTitle}>Kosten</Text>
|
||||||
|
<View style={styles.detailRow}><Text style={styles.detailLabel}>Betrag</Text>
|
||||||
|
{isEditing ? <TextInput style={styles.detailInput} value={editedContract.amount.toString()} onChangeText={(text) => setEditedContract({ ...editedContract, amount: parseFloat(text) || 0 })} keyboardType="decimal-pad" /> : <Text style={styles.detailValue}>€{contract.amount.toFixed(2)}</Text>}
|
||||||
|
</View>
|
||||||
|
<View style={styles.detailRow}><Text style={styles.detailLabel}>Zahlungsrhythmus</Text><Text style={styles.detailValue}>{contract.billingCycle === 'monthly' ? 'Monatlich' : contract.billingCycle === 'quarterly' ? 'Vierteljährlich' : 'Jährlich'}</Text></View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View style={styles.card}>
|
||||||
|
<Text style={styles.cardTitle}>Vertragslaufzeit</Text>
|
||||||
|
<View style={styles.detailRow}><Text style={styles.detailLabel}>Startdatum</Text>
|
||||||
|
{isEditing ? <DatePickerInput value={editedContract.startDate} onChange={(date) => setEditedContract({ ...editedContract, startDate: date })} /> : <Text style={styles.detailValue}>{new Date(contract.startDate).toLocaleDateString('de-DE')}</Text>}
|
||||||
|
</View>
|
||||||
|
{(contract.endDate || isEditing) && (
|
||||||
|
<View style={styles.detailRow}><Text style={styles.detailLabel}>Enddatum</Text>
|
||||||
|
{isEditing ? <DatePickerInput value={editedContract.endDate || ''} onChange={(date) => setEditedContract({ ...editedContract, endDate: date })} /> : <Text style={styles.detailValue}>{contract.endDate ? new Date(contract.endDate).toLocaleDateString('de-DE') : '-'}</Text>}
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
<View style={styles.detailRow}><Text style={styles.detailLabel}>Automatische Verlängerung</Text>
|
||||||
|
{isEditing ? <Switch value={editedContract.autoRenewal} onValueChange={(value) => setEditedContract({ ...editedContract, autoRenewal: value })} trackColor={{ false: '#D1D1D6', true: '#34C75980' }} thumbColor={editedContract.autoRenewal ? '#34C759' : '#F4F3F4'} /> : <Text style={styles.detailValue}>{contract.autoRenewal ? 'Ja' : 'Nein'}</Text>}
|
||||||
|
</View>
|
||||||
|
{(contract.cancellationPeriod || isEditing) && (
|
||||||
|
<View style={styles.detailRow}><Text style={styles.detailLabel}>Kündigungsfrist</Text>
|
||||||
|
{isEditing ? <TextInput style={styles.detailInput} value={editedContract.cancellationPeriod || ''} onChangeText={(text) => setEditedContract({ ...editedContract, cancellationPeriod: text })} placeholder="z.B. 3 Monate" /> : <Text style={styles.detailValue}>{contract.cancellationPeriod || '-'}</Text>}
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{(contract.notes || isEditing) && (
|
||||||
|
<View style={styles.card}><Text style={styles.cardTitle}>Notizen</Text>
|
||||||
|
{isEditing ? <TextInput style={styles.notesInput} value={editedContract.notes || ''} onChangeText={(text) => setEditedContract({ ...editedContract, notes: text })} placeholder="Füge Notizen hinzu..." multiline numberOfLines={4} /> : <Text style={styles.notesText}>{contract.notes || 'Keine Notizen'}</Text>}
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
<View style={{ height: 100 }} />
|
||||||
|
</ScrollView>
|
||||||
|
|
||||||
|
<IconPickerModal visible={showIconPicker} onClose={() => setShowIconPicker(false)} onSelect={handleIconSelect} currentIcon={displayIcon} currentColor={displayColor} />
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
container: { flex: 1, backgroundColor: '#F2F2F7' },
|
||||||
|
header: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingTop: 60, paddingHorizontal: 20, paddingBottom: 16, backgroundColor: '#F2F2F7' },
|
||||||
|
closeButton: { width: 40, height: 40, borderRadius: 20, backgroundColor: 'rgba(255, 255, 255, 0.7)', alignItems: 'center', justifyContent: 'center' },
|
||||||
|
closeIcon: { fontSize: 24, color: '#007AFF', fontWeight: '300' as any },
|
||||||
|
headerTitle: { fontSize: 17, fontWeight: '600' as any, color: '#000000' },
|
||||||
|
editButton: { paddingHorizontal: 16, paddingVertical: 8 },
|
||||||
|
editButtonText: { fontSize: 17, fontWeight: '600' as any, color: '#007AFF' },
|
||||||
|
content: { flex: 1 },
|
||||||
|
scrollContent: { paddingHorizontal: 20 },
|
||||||
|
iconSection: { alignItems: 'center', paddingVertical: 32 },
|
||||||
|
iconContainer: { width: 120, height: 120, borderRadius: 30, alignItems: 'center', justifyContent: 'center', marginBottom: 20, position: 'relative' },
|
||||||
|
icon: { fontSize: 64 },
|
||||||
|
editIconBadge: { position: 'absolute', bottom: 0, right: 0, backgroundColor: '#007AFF', width: 32, height: 32, borderRadius: 16, alignItems: 'center', justifyContent: 'center', borderWidth: 3, borderColor: '#F2F2F7' },
|
||||||
|
editIconBadgeText: { color: '#FFFFFF', fontSize: 16, fontWeight: 'bold' },
|
||||||
|
title: { fontSize: 28, fontWeight: 'bold', color: '#000000', marginBottom: 4, textAlign: 'center' },
|
||||||
|
titleInput: { fontSize: 28, fontWeight: 'bold', color: '#000000', marginBottom: 4, textAlign: 'center', backgroundColor: '#FFFFFF', paddingHorizontal: 16, paddingVertical: 8, borderRadius: 12, minWidth: 200 },
|
||||||
|
provider: { fontSize: 17, color: '#8E8E93', textAlign: 'center' },
|
||||||
|
card: { backgroundColor: Platform.OS === 'ios' ? 'rgba(255, 255, 255, 0.7)' : '#FFFFFF', borderRadius: 20, padding: 20, marginBottom: 16, borderWidth: 1, borderColor: 'rgba(255, 255, 255, 0.5)', shadowColor: '#000', shadowOffset: { width: 0, height: 4 }, shadowOpacity: 0.1, shadowRadius: 12, elevation: 4 },
|
||||||
|
cardTitle: { fontSize: 20, fontWeight: 'bold', color: '#000000', marginBottom: 16 },
|
||||||
|
detailRow: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', paddingVertical: 12, borderBottomWidth: 1, borderBottomColor: '#E5E5EA' },
|
||||||
|
detailLabel: { fontSize: 17, color: '#000000', fontWeight: '500' as any, flex: 1 },
|
||||||
|
detailValue: { fontSize: 17, color: '#8E8E93', textAlign: 'right', flex: 1 },
|
||||||
|
detailInput: { fontSize: 17, color: '#000000', textAlign: 'right', flex: 1, backgroundColor: '#F2F2F7', paddingHorizontal: 12, paddingVertical: 8, borderRadius: 8 },
|
||||||
|
notesInput: { backgroundColor: '#F2F2F7', borderRadius: 12, padding: 14, fontSize: 15, color: '#000000', minHeight: 100, textAlignVertical: 'top' },
|
||||||
|
notesText: { fontSize: 15, color: '#000000', lineHeight: 22 },
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,94 @@
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
import { View, Text, StyleSheet, ScrollView, TouchableOpacity, Modal, Platform } from 'react-native';
|
||||||
|
import { Contract } from '../../../shared/types';
|
||||||
|
import { ContractCard } from '../../../shared/components/ContractCard';
|
||||||
|
import { ContractDetailsScreen } from '../../contracts/screens/ContractDetailsScreen';
|
||||||
|
import { AddContractScreen } from '../../contracts/screens/AddContractScreen';
|
||||||
|
|
||||||
|
export const DashboardScreen = () => {
|
||||||
|
const [contracts, setContracts] = useState<Contract[]>([
|
||||||
|
{ id: '1', name: 'Vodafone DSL', provider: 'Vodafone', category: 'internet', amount: 39.99, currency: 'EUR', billingCycle: 'monthly', startDate: '2024-01-01', autoRenewal: true, customIcon: '🌐', iconColor: '#007AFF' },
|
||||||
|
{ id: '2', name: 'iPhone 15 Pro', provider: 'Apple', category: 'mobile', amount: 59.99, currency: 'EUR', billingCycle: 'monthly', startDate: '2024-03-15', endDate: '2026-03-14', autoRenewal: false, customIcon: '📱', iconColor: '#34C759' },
|
||||||
|
{ id: '3', name: 'Netflix Premium', provider: 'Netflix', category: 'streaming', amount: 17.99, currency: 'EUR', billingCycle: 'monthly', startDate: '2023-06-01', autoRenewal: true, customIcon: '🎬', iconColor: '#E50914' },
|
||||||
|
{ id: '4', name: 'Amazon Prime', provider: 'Amazon', category: 'streaming', amount: 8.99, currency: 'EUR', billingCycle: 'monthly', startDate: '2023-01-10', autoRenewal: true, customIcon: '📦', iconColor: '#FF9900' },
|
||||||
|
{ id: '5', name: 'Spotify Family', provider: 'Spotify', category: 'streaming', amount: 16.99, currency: 'EUR', billingCycle: 'monthly', startDate: '2023-08-20', autoRenewal: true, customIcon: '🎵', iconColor: '#1DB954' },
|
||||||
|
{ id: '6', name: 'Haftpflicht', provider: 'Allianz', category: 'insurance', amount: 89.99, currency: 'EUR', billingCycle: 'yearly', startDate: '2023-01-01', endDate: '2025-12-31', autoRenewal: true, cancellationPeriod: '3 Monate', customIcon: '🛡️', iconColor: '#0066B2' },
|
||||||
|
{ id: '7', name: 'Strom', provider: 'Vattenfall', category: 'utilities', amount: 85.00, currency: 'EUR', billingCycle: 'monthly', startDate: '2022-11-01', autoRenewal: true, customIcon: '⚡', iconColor: '#FFCC00' },
|
||||||
|
]);
|
||||||
|
|
||||||
|
const [selectedContract, setSelectedContract] = useState<Contract | null>(null);
|
||||||
|
const [showDetails, setShowDetails] = useState(false);
|
||||||
|
const [showAddContract, setShowAddContract] = useState(false);
|
||||||
|
|
||||||
|
const calculateTotalMonthly = () => {
|
||||||
|
return contracts.reduce((total, contract) => {
|
||||||
|
const monthlyAmount = contract.billingCycle === 'yearly' ? contract.amount / 12 : contract.billingCycle === 'quarterly' ? contract.amount / 3 : contract.amount;
|
||||||
|
return total + monthlyAmount;
|
||||||
|
}, 0);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleContractPress = (contract: Contract) => {
|
||||||
|
setSelectedContract(contract);
|
||||||
|
setShowDetails(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleUpdateContract = (updatedContract: Contract) => {
|
||||||
|
setContracts(contracts.map(c => c.id === updatedContract.id ? updatedContract : c));
|
||||||
|
setShowDetails(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAddContract = (newContract: Omit<Contract, 'id'>) => {
|
||||||
|
const contract: Contract = {
|
||||||
|
...newContract,
|
||||||
|
id: Date.now().toString(),
|
||||||
|
};
|
||||||
|
setContracts([...contracts, contract]);
|
||||||
|
setShowAddContract(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={styles.container}>
|
||||||
|
{/* Fixed Summary Card */}
|
||||||
|
<View style={styles.summaryCard}>
|
||||||
|
<View>
|
||||||
|
<Text style={styles.summaryLabel}>Monatliche Gesamtkosten</Text>
|
||||||
|
<Text style={styles.summaryAmount}>€{calculateTotalMonthly().toFixed(2)}</Text>
|
||||||
|
</View>
|
||||||
|
<TouchableOpacity style={styles.addButton} onPress={() => setShowAddContract(true)}>
|
||||||
|
<Text style={styles.addButtonText}>+</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Scrollable Contract List */}
|
||||||
|
<ScrollView style={styles.scrollView} contentContainerStyle={styles.scrollContent} showsVerticalScrollIndicator={false}>
|
||||||
|
<Text style={styles.sectionTitle}>Meine Verträge</Text>
|
||||||
|
{contracts.map((contract) => (
|
||||||
|
<ContractCard key={contract.id} contract={contract} onPress={() => handleContractPress(contract)} />
|
||||||
|
))}
|
||||||
|
<View style={{ height: 20 }} />
|
||||||
|
</ScrollView>
|
||||||
|
|
||||||
|
{/* Contract Details Modal */}
|
||||||
|
<Modal visible={showDetails} animationType="slide" presentationStyle="pageSheet" onRequestClose={() => setShowDetails(false)}>
|
||||||
|
{selectedContract && <ContractDetailsScreen contract={selectedContract} onClose={() => setShowDetails(false)} onSave={handleUpdateContract} />}
|
||||||
|
</Modal>
|
||||||
|
|
||||||
|
{/* Add Contract Modal */}
|
||||||
|
<Modal visible={showAddContract} animationType="slide" presentationStyle="pageSheet" onRequestClose={() => setShowAddContract(false)}>
|
||||||
|
<AddContractScreen onClose={() => setShowAddContract(false)} onSave={handleAddContract} />
|
||||||
|
</Modal>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
container: { flex: 1, backgroundColor: '#F2F2F7' },
|
||||||
|
summaryCard: { backgroundColor: Platform.OS === 'ios' ? 'rgba(255, 255, 255, 0.7)' : '#FFFFFF', marginHorizontal: 20, marginTop: 60, marginBottom: 16, padding: 20, borderRadius: 20, flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', borderWidth: 1, borderColor: 'rgba(255, 255, 255, 0.5)', shadowColor: '#000', shadowOffset: { width: 0, height: 4 }, shadowOpacity: 0.1, shadowRadius: 12, elevation: 4 },
|
||||||
|
summaryLabel: { fontSize: 15, color: '#8E8E93', marginBottom: 4 },
|
||||||
|
summaryAmount: { fontSize: 32, fontWeight: 'bold', color: '#000000' },
|
||||||
|
addButton: { width: 56, height: 56, borderRadius: 14, backgroundColor: '#007AFF', alignItems: 'center', justifyContent: 'center', shadowColor: '#007AFF', shadowOffset: { width: 0, height: 4 }, shadowOpacity: 0.3, shadowRadius: 8, elevation: 8 },
|
||||||
|
addButtonText: { fontSize: 32, color: '#FFFFFF', fontWeight: '300' as any, marginTop: -4 },
|
||||||
|
scrollView: { flex: 1 },
|
||||||
|
scrollContent: { paddingHorizontal: 20 },
|
||||||
|
sectionTitle: { fontSize: 22, fontWeight: 'bold', color: '#000000', marginBottom: 16 },
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,29 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { View, Text, StyleSheet } from 'react-native';
|
||||||
|
|
||||||
|
export const ProfileScreen = () => {
|
||||||
|
return (
|
||||||
|
<View style={styles.container}>
|
||||||
|
<Text style={styles.title}>Profil & Einstellungen</Text>
|
||||||
|
<Text style={styles.subtitle}>Coming soon...</Text>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
container: {
|
||||||
|
flex: 1,
|
||||||
|
backgroundColor: '#F2F2F7',
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
fontSize: 24,
|
||||||
|
fontWeight: 'bold',
|
||||||
|
marginBottom: 8,
|
||||||
|
},
|
||||||
|
subtitle: {
|
||||||
|
fontSize: 16,
|
||||||
|
color: '#8E8E93',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
@ -1,142 +1,44 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { NavigationContainer } from '@react-navigation/native';
|
import { NavigationContainer } from '@react-navigation/native';
|
||||||
import { createNativeStackNavigator } from '@react-navigation/native-stack';
|
|
||||||
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
|
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
|
||||||
import { useAuthStore } from '@store';
|
import { Text } from 'react-native';
|
||||||
|
|
||||||
// Auth Screens (werden später erstellt)
|
// Screens
|
||||||
// import { LoginScreen } from '@modules/auth/screens/LoginScreen';
|
import { DashboardScreen } from '../modules/dashboard/screens/DashboardScreen';
|
||||||
// import { RegisterScreen } from '@modules/auth/screens/RegisterScreen';
|
import { ProfileScreen } from '../modules/profile/screens/ProfileScreen';
|
||||||
|
|
||||||
// App Screens (werden später erstellt)
|
|
||||||
// import { DashboardScreen } from '@modules/dashboard/screens/DashboardScreen';
|
|
||||||
// import { ContractsScreen } from '@modules/contracts/screens/ContractsScreen';
|
|
||||||
// import { AddContractScreen } from '@modules/contracts/screens/AddContractScreen';
|
|
||||||
// import { NotificationsScreen } from '@modules/notifications/screens/NotificationsScreen';
|
|
||||||
// import { ProfileScreen } from '@modules/profile/screens/ProfileScreen';
|
|
||||||
|
|
||||||
const Stack = createNativeStackNavigator();
|
|
||||||
const Tab = createBottomTabNavigator();
|
const Tab = createBottomTabNavigator();
|
||||||
|
|
||||||
// Placeholder Komponenten für die Navigation
|
|
||||||
const PlaceholderScreen = ({ title }: { title: string }) => (
|
|
||||||
<div style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
|
|
||||||
<h1>{title}</h1>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
const LoginScreen = () => <PlaceholderScreen title="Login" />;
|
|
||||||
const RegisterScreen = () => <PlaceholderScreen title="Register" />;
|
|
||||||
const DashboardScreen = () => <PlaceholderScreen title="Dashboard" />;
|
|
||||||
const ContractsScreen = () => <PlaceholderScreen title="Contracts" />;
|
|
||||||
const AddContractScreen = () => <PlaceholderScreen title="Add Contract" />;
|
|
||||||
const NotificationsScreen = () => <PlaceholderScreen title="Notifications" />;
|
|
||||||
const ProfileScreen = () => <PlaceholderScreen title="Profile" />;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Auth Stack - für nicht eingeloggte Benutzer
|
* Root Navigation mit Bottom Tabs
|
||||||
*/
|
|
||||||
const AuthStack = () => {
|
|
||||||
return (
|
|
||||||
<Stack.Navigator
|
|
||||||
screenOptions={{
|
|
||||||
headerShown: false,
|
|
||||||
}}>
|
|
||||||
<Stack.Screen name="Login" component={LoginScreen} />
|
|
||||||
<Stack.Screen name="Register" component={RegisterScreen} />
|
|
||||||
</Stack.Navigator>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Tab Navigator - Hauptnavigation für eingeloggte Benutzer
|
|
||||||
*/
|
|
||||||
const AppTabs = () => {
|
|
||||||
return (
|
|
||||||
<Tab.Navigator
|
|
||||||
screenOptions={{
|
|
||||||
headerShown: true,
|
|
||||||
tabBarActiveTintColor: '#6366F1',
|
|
||||||
tabBarInactiveTintColor: '#9CA3AF',
|
|
||||||
}}>
|
|
||||||
<Tab.Screen
|
|
||||||
name="Dashboard"
|
|
||||||
component={DashboardScreen}
|
|
||||||
options={{
|
|
||||||
title: 'Übersicht',
|
|
||||||
// tabBarIcon: ({ color, size }) => (
|
|
||||||
// <Icon name="home" size={size} color={color} />
|
|
||||||
// ),
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Tab.Screen
|
|
||||||
name="Contracts"
|
|
||||||
component={ContractsScreen}
|
|
||||||
options={{
|
|
||||||
title: 'Verträge',
|
|
||||||
// tabBarIcon: ({ color, size }) => (
|
|
||||||
// <Icon name="file-text" size={size} color={color} />
|
|
||||||
// ),
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Tab.Screen
|
|
||||||
name="AddContract"
|
|
||||||
component={AddContractScreen}
|
|
||||||
options={{
|
|
||||||
title: 'Hinzufügen',
|
|
||||||
// tabBarIcon: ({ color, size }) => (
|
|
||||||
// <Icon name="plus-circle" size={size} color={color} />
|
|
||||||
// ),
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Tab.Screen
|
|
||||||
name="Notifications"
|
|
||||||
component={NotificationsScreen}
|
|
||||||
options={{
|
|
||||||
title: 'Erinnerungen',
|
|
||||||
// tabBarIcon: ({ color, size }) => (
|
|
||||||
// <Icon name="bell" size={size} color={color} />
|
|
||||||
// ),
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Tab.Screen
|
|
||||||
name="Profile"
|
|
||||||
component={ProfileScreen}
|
|
||||||
options={{
|
|
||||||
title: 'Profil',
|
|
||||||
// tabBarIcon: ({ color, size }) => (
|
|
||||||
// <Icon name="user" size={size} color={color} />
|
|
||||||
// ),
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Tab.Navigator>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* App Stack - für eingeloggte Benutzer
|
|
||||||
*/
|
|
||||||
const AppStack = () => {
|
|
||||||
return (
|
|
||||||
<Stack.Navigator>
|
|
||||||
<Stack.Screen
|
|
||||||
name="MainTabs"
|
|
||||||
component={AppTabs}
|
|
||||||
options={{ headerShown: false }}
|
|
||||||
/>
|
|
||||||
</Stack.Navigator>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Root Navigation
|
|
||||||
*/
|
*/
|
||||||
export const RootNavigation = () => {
|
export const RootNavigation = () => {
|
||||||
const { user } = useAuthStore();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<NavigationContainer>
|
<NavigationContainer>
|
||||||
{user ? <AppStack /> : <AuthStack />}
|
<Tab.Navigator
|
||||||
|
screenOptions={{
|
||||||
|
headerShown: false,
|
||||||
|
tabBarActiveTintColor: '#007AFF',
|
||||||
|
tabBarInactiveTintColor: '#8E8E93',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Tab.Screen
|
||||||
|
name="Contracts"
|
||||||
|
component={DashboardScreen}
|
||||||
|
options={{
|
||||||
|
tabBarLabel: 'Verträge',
|
||||||
|
tabBarIcon: ({ color }) => <Text style={{ fontSize: 28 }}>📄</Text>,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Tab.Screen
|
||||||
|
name="Profile"
|
||||||
|
component={ProfileScreen}
|
||||||
|
options={{
|
||||||
|
tabBarLabel: 'Profil',
|
||||||
|
tabBarIcon: ({ color }) => <Text style={{ fontSize: 28 }}>⚙️</Text>,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Tab.Navigator>
|
||||||
</NavigationContainer>
|
</NavigationContainer>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,112 +1,39 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import {
|
import { View, Text, TouchableOpacity, StyleSheet, Platform } from 'react-native';
|
||||||
View,
|
import { Contract } from '../types';
|
||||||
Text,
|
|
||||||
TouchableOpacity,
|
|
||||||
StyleSheet,
|
|
||||||
ViewStyle,
|
|
||||||
} from 'react-native';
|
|
||||||
import { Contract, ContractCategory } from '@shared/types';
|
|
||||||
import { COLORS, SPACING, FONT_SIZE, BORDER_RADIUS, SHADOWS } from '@shared/theme';
|
|
||||||
import { format } from 'date-fns';
|
|
||||||
import { de } from 'date-fns/locale';
|
|
||||||
|
|
||||||
interface ContractCardProps {
|
interface ContractCardProps {
|
||||||
contract: Contract;
|
contract: Contract;
|
||||||
onPress: () => void;
|
onPress: () => void;
|
||||||
style?: ViewStyle;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ContractCard: React.FC<ContractCardProps> = ({
|
export const ContractCard: React.FC<ContractCardProps> = ({ contract, onPress }) => {
|
||||||
contract,
|
const getCategoryIcon = (category: string) => {
|
||||||
onPress,
|
const icons: Record<string, string> = {
|
||||||
style,
|
internet: '🌐', mobile: '📱', streaming: '📺', utilities: '💡', insurance: '🛡️', other: '📄',
|
||||||
}) => {
|
};
|
||||||
const getCategoryColor = (category: ContractCategory): string => {
|
return icons[category] || '📄';
|
||||||
return COLORS.categories[category] || COLORS.textSecondary;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const formatCost = (cost: number, cycle: string): string => {
|
const getDefaultColor = (category: string) => {
|
||||||
const formatted = cost.toFixed(2);
|
const colors: Record<string, string> = {
|
||||||
switch (cycle) {
|
internet: '#007AFF', mobile: '#34C759', streaming: '#E50914', utilities: '#FFCC00', insurance: '#0066B2', other: '#8E8E93',
|
||||||
case 'monthly':
|
};
|
||||||
return `${formatted}€/Monat`;
|
return colors[category] || '#8E8E93';
|
||||||
case 'quarterly':
|
|
||||||
return `${formatted}€/Quartal`;
|
|
||||||
case 'yearly':
|
|
||||||
return `${formatted}€/Jahr`;
|
|
||||||
default:
|
|
||||||
return `${formatted}€`;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const getDaysUntilDeadline = (): number | null => {
|
const displayIcon = contract.customIcon || getCategoryIcon(contract.category);
|
||||||
if (!contract.cancellationDeadline) return null;
|
const displayColor = contract.iconColor || getDefaultColor(contract.category);
|
||||||
const today = new Date();
|
|
||||||
const deadline = new Date(contract.cancellationDeadline);
|
|
||||||
const diff = deadline.getTime() - today.getTime();
|
|
||||||
return Math.ceil(diff / (1000 * 60 * 60 * 24));
|
|
||||||
};
|
|
||||||
|
|
||||||
const daysLeft = getDaysUntilDeadline();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TouchableOpacity
|
<TouchableOpacity style={styles.card} onPress={onPress} activeOpacity={0.7}>
|
||||||
style={[styles.card, style]}
|
<View style={[styles.iconContainer, { backgroundColor: displayColor + '20' }]}>
|
||||||
onPress={onPress}
|
<Text style={styles.icon}>{displayIcon}</Text>
|
||||||
activeOpacity={0.7}>
|
</View>
|
||||||
<View
|
|
||||||
style={[
|
|
||||||
styles.categoryIndicator,
|
|
||||||
{ backgroundColor: getCategoryColor(contract.category) },
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<View style={styles.content}>
|
<View style={styles.content}>
|
||||||
<View style={styles.header}>
|
<Text style={styles.name} numberOfLines={1}>{contract.name}</Text>
|
||||||
<Text style={styles.name} numberOfLines={1}>
|
<Text style={styles.provider} numberOfLines={1}>{contract.provider}</Text>
|
||||||
{contract.name}
|
<Text style={styles.cost}>€{contract.amount.toFixed(2)}{contract.billingCycle === 'monthly' ? '/Monat' : contract.billingCycle === 'quarterly' ? '/Quartal' : '/Jahr'}</Text>
|
||||||
</Text>
|
|
||||||
<Text style={styles.cost}>
|
|
||||||
{formatCost(contract.cost, contract.billingCycle)}
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<Text style={styles.provider} numberOfLines={1}>
|
|
||||||
{contract.provider}
|
|
||||||
</Text>
|
|
||||||
|
|
||||||
{contract.cancellationDeadline && (
|
|
||||||
<View style={styles.deadline}>
|
|
||||||
<Text
|
|
||||||
style={[
|
|
||||||
styles.deadlineText,
|
|
||||||
daysLeft && daysLeft < 30 && styles.deadlineWarning,
|
|
||||||
daysLeft && daysLeft < 7 && styles.deadlineDanger,
|
|
||||||
]}>
|
|
||||||
Kündigung bis:{' '}
|
|
||||||
{format(new Date(contract.cancellationDeadline), 'dd.MM.yyyy', {
|
|
||||||
locale: de,
|
|
||||||
})}
|
|
||||||
</Text>
|
|
||||||
{daysLeft !== null && daysLeft > 0 && (
|
|
||||||
<Text
|
|
||||||
style={[
|
|
||||||
styles.daysLeft,
|
|
||||||
daysLeft < 30 && styles.daysLeftWarning,
|
|
||||||
daysLeft < 7 && styles.daysLeftDanger,
|
|
||||||
]}>
|
|
||||||
{daysLeft} Tage
|
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
</View>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{contract.status === 'cancelled' && (
|
|
||||||
<View style={styles.statusBadge}>
|
|
||||||
<Text style={styles.statusText}>Gekündigt</Text>
|
|
||||||
</View>
|
|
||||||
)}
|
|
||||||
</View>
|
</View>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
);
|
);
|
||||||
|
|
@ -114,82 +41,31 @@ export const ContractCard: React.FC<ContractCardProps> = ({
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
card: {
|
card: {
|
||||||
backgroundColor: COLORS.surface,
|
backgroundColor: Platform.OS === 'ios' ? 'rgba(255, 255, 255, 0.7)' : '#FFFFFF',
|
||||||
borderRadius: BORDER_RADIUS.lg,
|
borderRadius: 20,
|
||||||
marginBottom: SPACING.md,
|
padding: 16,
|
||||||
|
marginBottom: 12,
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
overflow: 'hidden',
|
|
||||||
...SHADOWS.md,
|
|
||||||
},
|
|
||||||
categoryIndicator: {
|
|
||||||
width: 6,
|
|
||||||
},
|
|
||||||
content: {
|
|
||||||
flex: 1,
|
|
||||||
padding: SPACING.md,
|
|
||||||
},
|
|
||||||
header: {
|
|
||||||
flexDirection: 'row',
|
|
||||||
justifyContent: 'space-between',
|
|
||||||
alignItems: 'flex-start',
|
|
||||||
marginBottom: SPACING.xs,
|
|
||||||
},
|
|
||||||
name: {
|
|
||||||
flex: 1,
|
|
||||||
fontSize: FONT_SIZE.lg,
|
|
||||||
fontWeight: '600',
|
|
||||||
color: COLORS.text,
|
|
||||||
marginRight: SPACING.sm,
|
|
||||||
},
|
|
||||||
cost: {
|
|
||||||
fontSize: FONT_SIZE.md,
|
|
||||||
fontWeight: '700',
|
|
||||||
color: COLORS.primary,
|
|
||||||
},
|
|
||||||
provider: {
|
|
||||||
fontSize: FONT_SIZE.sm,
|
|
||||||
color: COLORS.textSecondary,
|
|
||||||
marginBottom: SPACING.sm,
|
|
||||||
},
|
|
||||||
deadline: {
|
|
||||||
flexDirection: 'row',
|
|
||||||
justifyContent: 'space-between',
|
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: 'rgba(255, 255, 255, 0.5)',
|
||||||
|
shadowColor: '#000',
|
||||||
|
shadowOffset: { width: 0, height: 2 },
|
||||||
|
shadowOpacity: 0.1,
|
||||||
|
shadowRadius: 8,
|
||||||
|
elevation: 3,
|
||||||
},
|
},
|
||||||
deadlineText: {
|
iconContainer: {
|
||||||
fontSize: FONT_SIZE.sm,
|
width: 60,
|
||||||
color: COLORS.textSecondary,
|
height: 60,
|
||||||
},
|
borderRadius: 15,
|
||||||
deadlineWarning: {
|
alignItems: 'center',
|
||||||
color: COLORS.warning,
|
justifyContent: 'center',
|
||||||
},
|
marginRight: 16,
|
||||||
deadlineDanger: {
|
|
||||||
color: COLORS.danger,
|
|
||||||
fontWeight: '600',
|
|
||||||
},
|
|
||||||
daysLeft: {
|
|
||||||
fontSize: FONT_SIZE.xs,
|
|
||||||
color: COLORS.textLight,
|
|
||||||
fontWeight: '500',
|
|
||||||
},
|
|
||||||
daysLeftWarning: {
|
|
||||||
color: COLORS.warning,
|
|
||||||
},
|
|
||||||
daysLeftDanger: {
|
|
||||||
color: COLORS.danger,
|
|
||||||
fontWeight: '700',
|
|
||||||
},
|
|
||||||
statusBadge: {
|
|
||||||
marginTop: SPACING.sm,
|
|
||||||
alignSelf: 'flex-start',
|
|
||||||
backgroundColor: COLORS.danger,
|
|
||||||
paddingHorizontal: SPACING.sm,
|
|
||||||
paddingVertical: 4,
|
|
||||||
borderRadius: BORDER_RADIUS.sm,
|
|
||||||
},
|
|
||||||
statusText: {
|
|
||||||
fontSize: FONT_SIZE.xs,
|
|
||||||
color: '#FFF',
|
|
||||||
fontWeight: '600',
|
|
||||||
},
|
},
|
||||||
|
icon: { fontSize: 32 },
|
||||||
|
content: { flex: 1 },
|
||||||
|
name: { fontSize: 17, fontWeight: '600' as any, color: '#000000', marginBottom: 4 },
|
||||||
|
provider: { fontSize: 15, color: '#8E8E93', marginBottom: 4 },
|
||||||
|
cost: { fontSize: 17, fontWeight: 'bold', color: '#007AFF' },
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,136 @@
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
import { View, Text, TouchableOpacity, StyleSheet, Platform } from 'react-native';
|
||||||
|
import DateTimePicker, { DateTimePickerEvent } from '@react-native-community/datetimepicker';
|
||||||
|
|
||||||
|
interface DatePickerInputProps {
|
||||||
|
value: string; // ISO date string YYYY-MM-DD
|
||||||
|
onChange: (date: string) => void;
|
||||||
|
placeholder?: string;
|
||||||
|
style?: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DatePickerInput: React.FC<DatePickerInputProps> = ({
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
placeholder = 'Datum wählen',
|
||||||
|
style,
|
||||||
|
}) => {
|
||||||
|
const [showPicker, setShowPicker] = useState(false);
|
||||||
|
|
||||||
|
const parseDate = (dateString: string): Date => {
|
||||||
|
if (!dateString) return new Date();
|
||||||
|
return new Date(dateString + 'T12:00:00'); // Noon to avoid timezone issues
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatDisplayDate = (dateString: string): string => {
|
||||||
|
if (!dateString) return '';
|
||||||
|
const date = parseDate(dateString);
|
||||||
|
return date.toLocaleDateString('de-DE', {
|
||||||
|
day: '2-digit',
|
||||||
|
month: '2-digit',
|
||||||
|
year: 'numeric',
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDateChange = (event: DateTimePickerEvent, selectedDate?: Date) => {
|
||||||
|
if (Platform.OS === 'android') {
|
||||||
|
setShowPicker(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.type === 'set' && selectedDate) {
|
||||||
|
const isoDate = selectedDate.toISOString().split('T')[0];
|
||||||
|
onChange(isoDate);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleConfirm = () => {
|
||||||
|
setShowPicker(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={style}>
|
||||||
|
<TouchableOpacity
|
||||||
|
style={styles.input}
|
||||||
|
onPress={() => setShowPicker(true)}
|
||||||
|
>
|
||||||
|
<Text style={[styles.inputText, !value && styles.placeholder]}>
|
||||||
|
{value ? formatDisplayDate(value) : placeholder}
|
||||||
|
</Text>
|
||||||
|
<Text style={styles.icon}>📅</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
|
||||||
|
{showPicker && Platform.OS === 'ios' && (
|
||||||
|
<View style={styles.iosPickerContainer}>
|
||||||
|
<View style={styles.iosPickerHeader}>
|
||||||
|
<TouchableOpacity onPress={handleConfirm} style={styles.doneButton}>
|
||||||
|
<Text style={styles.doneButtonText}>Fertig</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
<DateTimePicker
|
||||||
|
value={parseDate(value)}
|
||||||
|
mode="date"
|
||||||
|
display="spinner"
|
||||||
|
onChange={handleDateChange}
|
||||||
|
locale="de-DE"
|
||||||
|
textColor="#000000"
|
||||||
|
style={styles.picker}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{showPicker && Platform.OS === 'android' && (
|
||||||
|
<DateTimePicker
|
||||||
|
value={parseDate(value)}
|
||||||
|
mode="date"
|
||||||
|
display="default"
|
||||||
|
onChange={handleDateChange}
|
||||||
|
locale="de-DE"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
input: {
|
||||||
|
backgroundColor: '#F2F2F7',
|
||||||
|
borderRadius: 12,
|
||||||
|
padding: 14,
|
||||||
|
flexDirection: 'row',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
alignItems: 'center',
|
||||||
|
},
|
||||||
|
inputText: {
|
||||||
|
fontSize: 17,
|
||||||
|
color: '#000000',
|
||||||
|
},
|
||||||
|
placeholder: {
|
||||||
|
color: '#C7C7CC',
|
||||||
|
},
|
||||||
|
icon: {
|
||||||
|
fontSize: 20,
|
||||||
|
},
|
||||||
|
iosPickerContainer: {
|
||||||
|
backgroundColor: '#FFFFFF',
|
||||||
|
borderRadius: 12,
|
||||||
|
marginTop: 8,
|
||||||
|
overflow: 'hidden',
|
||||||
|
},
|
||||||
|
iosPickerHeader: {
|
||||||
|
backgroundColor: '#F2F2F7',
|
||||||
|
padding: 12,
|
||||||
|
alignItems: 'flex-end',
|
||||||
|
},
|
||||||
|
doneButton: {
|
||||||
|
paddingHorizontal: 16,
|
||||||
|
paddingVertical: 8,
|
||||||
|
},
|
||||||
|
doneButtonText: {
|
||||||
|
color: '#007AFF',
|
||||||
|
fontSize: 17,
|
||||||
|
fontWeight: '600',
|
||||||
|
},
|
||||||
|
picker: {
|
||||||
|
backgroundColor: '#FFFFFF',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,237 @@
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
import { View, Text, Modal, TouchableOpacity, StyleSheet, ScrollView, Platform } from 'react-native';
|
||||||
|
|
||||||
|
interface IconPickerModalProps {
|
||||||
|
visible: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
onSelect: (icon: string, color: string) => void;
|
||||||
|
currentIcon?: string;
|
||||||
|
currentColor?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ICONS = [
|
||||||
|
'🌐', '📱', '📺', '💡', '🛡️', '📄', '🎬', '🎵', '🎮', '📦',
|
||||||
|
'💳', '🏠', '🚗', '✈️', '🏋️', '🍕', '☕', '📚', '💼', '🔒',
|
||||||
|
'📧', '💰', '🎓', '🏥', '🔧', '🎨', '📷', '🎯', '⚡', '🔥',
|
||||||
|
'💎', '🌟', '🎁', '🔔', '📊', '📈', '💻', '⌚', '🎪', '🎭',
|
||||||
|
'🏆', '🎸', '🎹', '🎤', '🎧', '📹', '🎞️', '📡',
|
||||||
|
];
|
||||||
|
|
||||||
|
const COLORS = [
|
||||||
|
'#007AFF', // iOS Blue
|
||||||
|
'#34C759', // iOS Green
|
||||||
|
'#FF3B30', // iOS Red
|
||||||
|
'#FF9500', // iOS Orange
|
||||||
|
'#FFCC00', // iOS Yellow
|
||||||
|
'#AF52DE', // iOS Purple
|
||||||
|
'#FF2D55', // iOS Pink
|
||||||
|
'#5856D6', // iOS Indigo
|
||||||
|
'#00C7BE', // iOS Teal
|
||||||
|
'#E50914', // Netflix Red
|
||||||
|
'#1DB954', // Spotify Green
|
||||||
|
'#0066B2', // Professional Blue
|
||||||
|
];
|
||||||
|
|
||||||
|
export const IconPickerModal: React.FC<IconPickerModalProps> = ({
|
||||||
|
visible,
|
||||||
|
onClose,
|
||||||
|
onSelect,
|
||||||
|
currentIcon = '📄',
|
||||||
|
currentColor = '#007AFF',
|
||||||
|
}) => {
|
||||||
|
const [selectedIcon, setSelectedIcon] = useState(currentIcon);
|
||||||
|
const [selectedColor, setSelectedColor] = useState(currentColor);
|
||||||
|
|
||||||
|
const handleSave = () => {
|
||||||
|
onSelect(selectedIcon, selectedColor);
|
||||||
|
onClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
visible={visible}
|
||||||
|
animationType="slide"
|
||||||
|
transparent={true}
|
||||||
|
onRequestClose={onClose}
|
||||||
|
>
|
||||||
|
<View style={styles.modalOverlay}>
|
||||||
|
<View style={styles.modalContent}>
|
||||||
|
{/* Header */}
|
||||||
|
<View style={styles.header}>
|
||||||
|
<TouchableOpacity onPress={onClose}>
|
||||||
|
<Text style={styles.cancelText}>Abbrechen</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
<Text style={styles.headerTitle}>Icon & Farbe wählen</Text>
|
||||||
|
<TouchableOpacity onPress={handleSave}>
|
||||||
|
<Text style={styles.saveText}>Fertig</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<ScrollView style={styles.scrollView} showsVerticalScrollIndicator={false}>
|
||||||
|
{/* Preview */}
|
||||||
|
<View style={styles.previewSection}>
|
||||||
|
<Text style={styles.sectionTitle}>Vorschau</Text>
|
||||||
|
<View style={[styles.previewIcon, { backgroundColor: selectedColor + '20' }]}>
|
||||||
|
<Text style={styles.previewEmoji}>{selectedIcon}</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Color Selection */}
|
||||||
|
<View style={styles.section}>
|
||||||
|
<Text style={styles.sectionTitle}>Farbe</Text>
|
||||||
|
<View style={styles.colorGrid}>
|
||||||
|
{COLORS.map((color) => (
|
||||||
|
<TouchableOpacity
|
||||||
|
key={color}
|
||||||
|
style={[
|
||||||
|
styles.colorOption,
|
||||||
|
{ backgroundColor: color },
|
||||||
|
selectedColor === color && styles.colorOptionSelected,
|
||||||
|
]}
|
||||||
|
onPress={() => setSelectedColor(color)}
|
||||||
|
>
|
||||||
|
{selectedColor === color && (
|
||||||
|
<Text style={styles.checkmark}>✓</Text>
|
||||||
|
)}
|
||||||
|
</TouchableOpacity>
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Icon Selection */}
|
||||||
|
<View style={styles.section}>
|
||||||
|
<Text style={styles.sectionTitle}>Icon</Text>
|
||||||
|
<View style={styles.iconGrid}>
|
||||||
|
{ICONS.map((icon) => (
|
||||||
|
<TouchableOpacity
|
||||||
|
key={icon}
|
||||||
|
style={[
|
||||||
|
styles.iconOption,
|
||||||
|
selectedIcon === icon && styles.iconOptionSelected,
|
||||||
|
]}
|
||||||
|
onPress={() => setSelectedIcon(icon)}
|
||||||
|
>
|
||||||
|
<Text style={styles.iconEmoji}>{icon}</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View style={{ height: 40 }} />
|
||||||
|
</ScrollView>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
modalOverlay: {
|
||||||
|
flex: 1,
|
||||||
|
backgroundColor: 'rgba(0, 0, 0, 0.5)',
|
||||||
|
justifyContent: 'flex-end',
|
||||||
|
},
|
||||||
|
modalContent: {
|
||||||
|
backgroundColor: '#F2F2F7',
|
||||||
|
borderTopLeftRadius: 20,
|
||||||
|
borderTopRightRadius: 20,
|
||||||
|
maxHeight: '80%',
|
||||||
|
},
|
||||||
|
header: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
alignItems: 'center',
|
||||||
|
padding: 20,
|
||||||
|
paddingTop: 24,
|
||||||
|
borderBottomWidth: 1,
|
||||||
|
borderBottomColor: '#E5E5EA',
|
||||||
|
},
|
||||||
|
headerTitle: {
|
||||||
|
fontSize: 17,
|
||||||
|
fontWeight: '600',
|
||||||
|
color: '#000000',
|
||||||
|
},
|
||||||
|
cancelText: {
|
||||||
|
fontSize: 17,
|
||||||
|
color: '#8E8E93',
|
||||||
|
},
|
||||||
|
saveText: {
|
||||||
|
fontSize: 17,
|
||||||
|
fontWeight: '600',
|
||||||
|
color: '#007AFF',
|
||||||
|
},
|
||||||
|
scrollView: {
|
||||||
|
flex: 1,
|
||||||
|
},
|
||||||
|
previewSection: {
|
||||||
|
alignItems: 'center',
|
||||||
|
padding: 24,
|
||||||
|
},
|
||||||
|
section: {
|
||||||
|
padding: 20,
|
||||||
|
},
|
||||||
|
sectionTitle: {
|
||||||
|
fontSize: 15,
|
||||||
|
fontWeight: '600',
|
||||||
|
color: '#000000',
|
||||||
|
marginBottom: 12,
|
||||||
|
},
|
||||||
|
previewIcon: {
|
||||||
|
width: 100,
|
||||||
|
height: 100,
|
||||||
|
borderRadius: 20,
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
marginTop: 12,
|
||||||
|
},
|
||||||
|
previewEmoji: {
|
||||||
|
fontSize: 56,
|
||||||
|
},
|
||||||
|
colorGrid: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
flexWrap: 'wrap',
|
||||||
|
gap: 12,
|
||||||
|
},
|
||||||
|
colorOption: {
|
||||||
|
width: 50,
|
||||||
|
height: 50,
|
||||||
|
borderRadius: 25,
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
},
|
||||||
|
colorOptionSelected: {
|
||||||
|
borderWidth: 3,
|
||||||
|
borderColor: '#FFFFFF',
|
||||||
|
shadowColor: '#000',
|
||||||
|
shadowOffset: { width: 0, height: 2 },
|
||||||
|
shadowOpacity: 0.3,
|
||||||
|
shadowRadius: 4,
|
||||||
|
elevation: 4,
|
||||||
|
},
|
||||||
|
checkmark: {
|
||||||
|
color: '#FFFFFF',
|
||||||
|
fontSize: 24,
|
||||||
|
fontWeight: 'bold',
|
||||||
|
},
|
||||||
|
iconGrid: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
flexWrap: 'wrap',
|
||||||
|
gap: 8,
|
||||||
|
},
|
||||||
|
iconOption: {
|
||||||
|
width: 60,
|
||||||
|
height: 60,
|
||||||
|
backgroundColor: '#FFFFFF',
|
||||||
|
borderRadius: 12,
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
},
|
||||||
|
iconOptionSelected: {
|
||||||
|
backgroundColor: '#007AFF20',
|
||||||
|
borderWidth: 2,
|
||||||
|
borderColor: '#007AFF',
|
||||||
|
},
|
||||||
|
iconEmoji: {
|
||||||
|
fontSize: 32,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
@ -1,14 +1,5 @@
|
||||||
// TypeScript Typen für die gesamte App
|
// TypeScript Typen für die gesamte App
|
||||||
export enum ContractCategory {
|
export type ContractCategory = 'mobile' | 'internet' | 'streaming' | 'insurance' | 'utilities' | 'other';
|
||||||
MOBILE = 'mobile',
|
|
||||||
INTERNET = 'internet',
|
|
||||||
STREAMING = 'streaming',
|
|
||||||
INSURANCE = 'insurance',
|
|
||||||
UTILITIES = 'utilities',
|
|
||||||
GYM = 'gym',
|
|
||||||
SUBSCRIPTIONS = 'subscriptions',
|
|
||||||
OTHER = 'other',
|
|
||||||
}
|
|
||||||
|
|
||||||
export type BillingCycle = 'monthly' | 'quarterly' | 'yearly';
|
export type BillingCycle = 'monthly' | 'quarterly' | 'yearly';
|
||||||
export type ContractStatus = 'active' | 'cancelled' | 'expired';
|
export type ContractStatus = 'active' | 'cancelled' | 'expired';
|
||||||
|
|
@ -32,24 +23,27 @@ export interface Document {
|
||||||
|
|
||||||
export interface Contract {
|
export interface Contract {
|
||||||
id: string;
|
id: string;
|
||||||
userId: string;
|
userId?: string;
|
||||||
name: string;
|
name: string;
|
||||||
provider: string;
|
provider: string;
|
||||||
category: ContractCategory;
|
category: ContractCategory;
|
||||||
startDate: Date;
|
startDate: string; // ISO date format YYYY-MM-DD
|
||||||
endDate?: Date;
|
endDate?: string; // ISO date format YYYY-MM-DD
|
||||||
cancellationDeadline?: Date;
|
cancellationDeadline?: Date;
|
||||||
noticePeriod: number; // in Monaten
|
cancellationPeriod?: string; // z.B. "3 Monate"
|
||||||
cost: number;
|
amount: number;
|
||||||
|
currency: string;
|
||||||
billingCycle: BillingCycle;
|
billingCycle: BillingCycle;
|
||||||
autoRenewal: boolean;
|
autoRenewal: boolean;
|
||||||
notes?: string;
|
notes?: string;
|
||||||
documents: Document[];
|
customIcon?: string; // Emoji für benutzerdefinierte Icons
|
||||||
reminderEnabled: boolean;
|
iconColor?: string; // Hex color code
|
||||||
reminderDays: number;
|
documents?: Document[];
|
||||||
status: ContractStatus;
|
reminderEnabled?: boolean;
|
||||||
createdAt: Date;
|
reminderDays?: number;
|
||||||
updatedAt: Date;
|
status?: ContractStatus;
|
||||||
|
createdAt?: Date;
|
||||||
|
updatedAt?: Date;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Notification {
|
export interface Notification {
|
||||||
|
|
|
||||||
|
|
@ -1,30 +1,10 @@
|
||||||
{
|
{
|
||||||
"extends": "@react-native/typescript-config/tsconfig.json",
|
"extends": "expo/tsconfig.base",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "esnext",
|
"strict": true
|
||||||
"module": "commonjs",
|
|
||||||
"lib": ["es2019"],
|
|
||||||
"allowJs": true,
|
|
||||||
"jsx": "react-native",
|
|
||||||
"noEmit": true,
|
|
||||||
"isolatedModules": true,
|
|
||||||
"strict": true,
|
|
||||||
"moduleResolution": "node",
|
|
||||||
"resolveJsonModule": true,
|
|
||||||
"allowSyntheticDefaultImports": true,
|
|
||||||
"esModuleInterop": true,
|
|
||||||
"skipLibCheck": true,
|
|
||||||
"forceConsistentCasingInFileNames": true,
|
|
||||||
"baseUrl": ".",
|
|
||||||
"paths": {
|
|
||||||
"@modules/*": ["src/modules/*"],
|
|
||||||
"@shared/*": ["src/shared/*"],
|
|
||||||
"@navigation/*": ["src/navigation/*"],
|
|
||||||
"@store/*": ["src/store/*"],
|
|
||||||
"@config/*": ["src/config/*"],
|
|
||||||
"@assets/*": ["assets/*"]
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"include": ["src/**/*", "App.tsx"],
|
"include": [
|
||||||
"exclude": ["node_modules", "babel.config.js", "metro.config.js", "jest.config.js"]
|
"**/*.ts",
|
||||||
|
"**/*.tsx"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue