refactor: Projektstruktur in app/ und backend/ aufgeteilt

This commit is contained in:
2026-03-24 15:06:44 +01:00
parent 5f5d5995c5
commit 2673ef658d
449 changed files with 28551 additions and 167 deletions

View File

@@ -1,19 +0,0 @@
{
"permissions": {
"allow": [
"Bash(./mvnw clean compile -q)",
"mcp__ide__getDiagnostics",
"Bash(find:*)",
"Bash(./mvnw:*)",
"Bash(rm:*)",
"Bash(lsof:*)",
"Bash(xargs kill:*)",
"Bash(cat:*)",
"Bash(mongosh:*)",
"Bash(mongo:*)",
"Bash(kill:*)"
],
"deny": [],
"ask": []
}
}

32
.gitignore vendored
View File

@@ -1,4 +1,3 @@
/target/
.idea/
.vscode/
.settings
@@ -8,14 +7,27 @@
*.iml
.DS_Store
# The following files are generated/updated by vaadin-maven-plugin
node_modules/
src/main/frontend/generated/
vite.generated.ts
# Backend (Spring Boot / Vaadin)
/backend/target/
/backend/node_modules/
/backend/src/main/frontend/generated/
/backend/vite.generated.ts
/backend/logs/
/backend/.env
# Log files
logs/
# Flutter app
/app/.dart_tool/
/app/build/
/app/coverage/
/app/.flutter-plugins
/app/.flutter-plugins-dependencies
/app/android/.gradle/
/app/android/.kotlin/
/app/android/local.properties
/app/ios/Pods/
/app/ios/.symlinks/
/app/macos/Pods/
# Root build artifacts
/target/
*.log
# Environment variables
.env

View File

@@ -1,22 +0,0 @@
# Repository Guidelines
## Project Structure & Module Organization
Backend Java sits in `src/main/java/de/assecutor/votianlt`; domain models stay in `model`, persistence logic in `repository`, services in `service`, MQTT integration under `mqtt`, and access control in `security`. Vaadin views and UI helpers live in `pages/view` and `pages/base`. TypeScript, styles, and theming live in `src/main/frontend` (leave `generated/` untouched). Shared configs and templates live in `src/main/resources`, while Vaadin bundle descriptors reside in `src/main/bundles`. Maven output lands in `target/`.
## Build, Test, and Development Commands
Use `./mvnw` for the Spring Boot dev server with frontend hot reload. Build production bits with `./mvnw -Pproduction package`. Run `./mvnw test` for unit checks and `./mvnw -Pintegration-test verify` when integration coverage is needed. After dependency changes, refresh Vaadin assets with `./mvnw vaadin:prepare-frontend`. Apply formatting via `./mvnw spotless:apply`.
## Coding Style & Naming Conventions
Spotless enforces Java 21 formatting using `eclipse-formatter.xml`; keep imports ordered and rely on Lombok already present. Classes remain PascalCase, Spring stereotypes end with `Service`, `Repository`, or `Config`, and Vaadin views retain the `*View` naming within `pages/view`. Frontend code follows the repos Prettier rules (`.prettierrc.json`); keep TypeScript modules co-located with their views, prefer camelCase for variables, and avoid checking in generated `.class` files.
## Testing Guidelines
Create tests under `src/test/java` mirroring the production package path. Name unit classes `*Test` and integration suites `*IT` so the failsafe profile picks them up. Lean on Spring Boots testing annotations for wiring, Mockito for isolates, and add Testcontainers when MongoDB or MQTT brokers are involved. Run `./mvnw test` before any push; trigger `./mvnw -Pintegration-test verify` for messaging, persistence, or security changes.
## Commit & Pull Request Guidelines
History currently uses brief German titles; shift to imperative, scoped summaries such as `feat: add PDF mailer` or `fix: guard MQTT reconnects`. Keep unrelated updates out of the same commit and exclude artifacts like `node_modules/` or `target/`. Pull requests should explain the motivation, link issues, note config or data-seed impacts, and attach screenshots or screencasts when Vaadin views change. List manual verification steps and flag any migrations or bundle adjustments for reviewers.
## Security & Configuration Tips
External service credentials for MongoDB, SMTP, and MQTT belong in environment variables or a developer-specific `application-local.properties` kept out of version control. Document default ports and topics when touching `MqttConfig` so ops can replicate environments. For two-factor flows, keep shared secrets in secure storage and avoid logging codes during development.
# Misc
Never start the application; leave that to the user.

103
CLAUDE.md
View File

@@ -1,103 +0,0 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Development Commands
- **Start development server**: `./mvnw` (runs Spring Boot with Vaadin dev mode)
- **Build for production**: `./mvnw -Pproduction package`
- **Clean build**: `./mvnw clean compile`
- **Format code**: `./mvnw spotless:apply` (applies Eclipse formatter for Java, Prettier for TypeScript)
- **Check formatting**: `./mvnw spotless:check`
## Architecture Overview
This is a **Vaadin Spring Boot** application for job/task management with real-time mobile app communication via a pluggable messaging transport layer. The system manages logistics jobs with tasks that mobile app users complete.
### Core Architecture Layers
**Frontend**: Vaadin Flow views (server-side rendered)
- `src/main/java/de/assecutor/votianlt/pages/view/` - Main UI views
- `src/main/java/de/assecutor/votianlt/pages/base/ui/` - Shared UI components
**Backend Services**:
- `src/main/java/de/assecutor/votianlt/service/` - Business logic
- `src/main/java/de/assecutor/votianlt/controller/` - Message handling (routes inbound messages to processors)
- `src/main/java/de/assecutor/votianlt/repository/` - MongoDB data access
**Messaging Layer** (`src/main/java/de/assecutor/votianlt/messaging/`):
- `plugin/` - Transport plugin interface and implementations (MQTT, extensible for WebSocket, gRPC)
- `delivery/` - Reliable message delivery with acknowledgment tracking, retries, and expiry
- `model/` - Message envelopes, delivery status, pending deliveries
- `config/` - Messaging configuration and wiring
**Models**:
- `src/main/java/de/assecutor/votianlt/model/` - Domain entities
- Task hierarchy: `BaseTask` with subtypes (`PhotoTask`, `BarcodeTask`, `SignatureTask`, etc.)
### Key Architectural Patterns
**Job-Task Relationship**: Jobs contain multiple ordered tasks. Tasks have completion states and can store completion data (photos, barcodes, signatures).
**User Hierarchy**:
- `User` - Web interface users (job managers)
- `AppUser` - Mobile app users (task executors)
- `AppUser.owner` field links to `User` for notifications
**Messaging Plugin Architecture**:
- `MessagingPlugin` interface abstracts transport protocols (currently MQTT via HiveMQ)
- `MessageDeliveryService` provides guaranteed delivery with acknowledgment tracking
- `AcknowledgmentHandler` processes ACKs and updates delivery status
- Plugins are responsible for topic/channel structure; delivery layer uses `clientId` and `messageType`
**Client Connection Monitoring**:
- `ClientConnectionService` tracks connected mobile clients via ping/pong mechanism
- Server sends ping to `/client/{clientId}/ping`, client responds on `/server/{clientId}/pong`
**History Tracking**: `JobHistoryService` logs all job/task changes with detailed audit trail displayed in `JobHistoryView`.
**Email Notifications**: `EmailService` sends notifications for job creation, task completion, and job completion using Spring Mail with SMTP.
## Data Storage
**MongoDB Collections**:
- `jobs` - Main job entities with status tracking
- `tasks` - Polymorphic task storage (discriminated by `taskType`)
- `job_history` - Audit trail for all job changes
- `pending_deliveries` - Message delivery tracking for retries
- `photos`, `barcodes`, `signatures` - Task completion data
- `users` - Web interface users
- `app_user` - Mobile app users
- `cargo_item` - Job cargo/delivery items
## Configuration
**Database**: MongoDB (configurable via `spring.data.mongodb.uri`)
**Messaging**: Plugin-based, currently MQTT via HiveMQ (`app.messaging.plugin.*` properties)
**Email**: SMTP via Spring Boot mail auto-configuration
## Development Environment
**Java 21** with **Spring Boot 3.4.3** and **Vaadin 24.7.0**
**Security**: Spring Security with role-based access (`USER` role required)
**Formatting**: Spotless Maven plugin with Eclipse formatter (Java) and Prettier (TypeScript)
**Profiles**: `production` profile for optimized builds, `integration-test` profile for failsafe plugin
## Key Integration Points
When adding new task types:
1. Extend `BaseTask` and add to `@JsonSubTypes`
2. Add completion logic in `MessageController.handleTaskCompleted()`
3. Update `JobHistoryView` for task-specific previews if needed
When modifying job status flow:
1. Update `JobStatus` enum
2. Modify `EmailService.updateJobStatusToCompleted()` logic
3. Consider email notification templates
When adding new messaging transports:
1. Implement `MessagingPlugin` interface
2. Register in `PluginMessagingConfig`
3. Add configuration properties under `app.messaging.plugin.<type>.*`
Message routing follows pattern: `MessageController` receives messages via `MessageDeliveryService`, extracts `taskType`/`messageType` from payload, routes to appropriate processor method.

View File

@@ -1,6 +1,40 @@
docker buildx build --platform linux/amd64 -t appcreationgmbh/votianlt:0.8.0 --push .
# VotianLT Monorepo
docker buildx build --platform linux/amd64 -t registry.assecutor.de/votianlt:0.9.10 --push .
## Struktur
adsg
G8m0T3vz
- `backend/`: Spring Boot / Vaadin Backend
- `app/`: Flutter App
- `.vscode/`: gemeinsame Workspace-Launches für Backend und Flutter
## Backend
```bash
cd backend
./mvnw
```
Wichtige Befehle:
```bash
cd backend && ./mvnw test
cd backend && ./mvnw -Pproduction package
cd backend && ./mvnw spotless:apply
```
## Flutter App
```bash
cd app
flutter pub get
flutter run
```
## Release Image
Das Release-Script liegt im Repo-Root und baut/pusht das Backend-Image:
```bash
docker login registry.assecutor.org
./docker_push.sh
./docker_push.sh 0.9.13
```

2
app/.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
.dart_tool/
build/

429
app/AGENTS.md Normal file
View File

@@ -0,0 +1,429 @@
# Votian LT App - Agent Guidelines
## Project Overview
**Votian LT** is a Flutter-based mobile application for logistics and transport management. The app enables drivers to manage transport jobs, complete tasks (photos, signatures, barcodes), communicate via real-time chat, and navigate to pickup/delivery locations.
### Key Features
- **Job Management**: View and manage assigned transport jobs with cargo items
- **Task System**: Complete various task types (confirmation, photo capture, signature, barcode scanning, todo lists, comments)
- **Real-time Chat**: Job-specific and general chat via WebSocket
- **Offline Support**: Full offline functionality with local ObjectBox database
- **Navigation**: Integration with external maps and embedded WebView routing
- **Push Notifications**: Local notifications for new jobs and messages
- **Implementation**: When making changes, be careful not to damage existing functionalities.
---
## Technology Stack
| Layer | Technology |
|-------|------------|
| Framework | Flutter 3.7+ with Dart |
| State Management | Singleton pattern + Custom DartMQ pub/sub |
| Local Database | ObjectBox (NoSQL) |
| Real-time Communication | WebSocket (STOMP-style protocol) |
| Backend Integration | WebSocket to `ws://localhost:8082/ws/messaging` |
| UI Design | Material Design 3 |
### Key Dependencies (pubspec.yaml)
- `web_socket_channel: ^3.0.0` - WebSocket client
- `objectbox: ^4.0.3` + `objectbox_flutter_libs` - Local database
- `camera: ^0.10.5+9` - Photo capture
- `mobile_scanner: ^5.0.0` - Barcode scanning
- `signature: ^5.5.0` - Signature capture canvas
- `flutter_local_notifications: ^18.0.0` - Local notifications
- `url_launcher: ^6.3.0` - External navigation
- `webview_flutter: ^4.8.0` - Embedded maps
- `image: ^4.2.0` - Image processing
- `package_info_plus: ^8.0.0` - App version info
---
## Project Structure
```
lib/
├── main.dart # App entry point, MaterialApp setup
├── app_state.dart # Global app state singleton (jobs, login)
├── navigation_observer.dart # Route observer for analytics
├── routing_view.dart # Embedded navigation WebView
├── models/ # Domain models (JSON serializable)
│ ├── job.dart # Transport job with cargo items and tasks
│ ├── cargo_item.dart # Cargo/load items
│ ├── task.dart # Abstract task base class
│ ├── tasks/ # Concrete task implementations
│ │ ├── confirmation_task.dart
│ │ ├── photo_task.dart
│ │ ├── signature_task.dart
│ │ ├── barcode_task.dart
│ │ ├── todolist_task.dart
│ │ ├── comment_task.dart
│ │ └── generic_task.dart
│ ├── chat.dart # Chat conversation
│ ├── chat_message.dart # Individual message
│ ├── message_envelope.dart # WebSocket message wrapper
│ ├── acknowledgment_message.dart
│ └── queued_message.dart # Offline message queue
├── entities/ # ObjectBox database entities
│ ├── job_entity.dart
│ ├── task_status_entity.dart
│ ├── user_data_entity.dart
│ ├── photo_entity.dart
│ ├── queued_message_entity.dart
│ └── chat_message_entity.dart
├── services/ # Business logic services (singletons)
│ ├── websocket_service.dart # WebSocket connection & messaging
│ ├── database_service.dart # ObjectBox database operations
│ ├── chat_service.dart # Chat management
│ ├── dart_mq.dart # In-app pub/sub message bus
│ ├── notification_service.dart # Local notifications
│ ├── message_handler.dart # Incoming message processing
│ ├── ack_tracker.dart # Message acknowledgment tracking
│ └── developer.dart # Developer utilities/logging
├── views/ # Main UI screens
│ ├── login_view.dart # Authentication screen
│ ├── jobs_view.dart # Job list screen
│ ├── cargo_items_view.dart # Cargo details for a job
│ ├── chats_view.dart # Chat list screen
│ ├── chat_details_view.dart # Individual chat conversation
│ └── task_view.dart # Task completion screen
├── tasks/ # Task-specific UI screens
│ ├── photo_capture_screen.dart
│ ├── signature_capture_screen.dart
│ └── barcode_capture_screen.dart
└── widgets/ # Reusable UI components
├── chat_photo_dialog.dart
└── offline_banner.dart
test/ # Unit and widget tests
├── models/
│ ├── job_parsing_test.dart
│ ├── message_envelope_test.dart
│ └── acknowledgment_message_test.dart
└── services/
├── ack_tracker_test.dart
├── message_handler_test.dart
└── mqtt_integration_test.dart
android/, ios/, macos/, linux/, windows/ # Platform-specific code
```
---
## Build, Test, and Development Commands
### Setup
```bash
# Install dependencies
flutter pub get
# Generate ObjectBox code (after entity changes)
flutter pub run build_runner build
```
### Development
```bash
# Run static analysis
flutter analyze
# Format code (CI expects formatted code)
dart format lib/ test/
# Run the app
flutter run
# Run on specific device
flutter run -d <device_id>
```
### Testing
```bash
# Run all tests
flutter test
# Run with coverage
flutter test --coverage
# Run specific test file
flutter test test/models/job_parsing_test.dart
```
---
## Architecture Patterns
### Singleton Services
All major services use the singleton pattern for app-wide state:
```dart
// Accessing services
final appState = AppState();
final chatService = ChatService();
final wsService = WebSocketService();
final dbService = DatabaseService();
```
### DartMQ Pub/Sub
Custom lightweight message bus for decoupled communication:
```dart
// Subscribe to topics
final sub = DartMQ().subscribe<Map<String, dynamic>>(
MQTopics.authResponse,
(data) => handleAuth(data),
);
// Publish messages
DartMQ().publish<bool>(MQTopics.connectionStatus, true);
// Cleanup
sub.cancel();
```
**Common Topics** (`lib/services/dart_mq.dart`):
- `connection/status` - WebSocket connection state (bool)
- `auth/response` - Authentication responses (Map)
- `jobs/response` - Job list updates (List)
- `jobsUpdated` - Job data changed notification (void)
- `job/deleted` - Job deletion event (Map)
- `job/created` - New job created (Map)
- `chat/incoming` - New chat message (ChatMessage)
### Task System
Tasks are polymorphic based on `taskType` field:
| Task Type | Description |
|-----------|-------------|
| `CONFIRMATION` | Button tap confirmation |
| `PHOTO` | Capture photos (min/max count) |
| `SIGNATURE` | Capture signature as SVG |
| `BARCODE` | Scan barcodes/QR codes |
| `TODOLIST` | Checklist of items |
| `COMMENT` | Text input field |
| `GENERIC` | Fallback type |
---
## Coding Style Guidelines
### Dart/Flutter Conventions
- **Indentation**: 2 spaces
- **Trailing Commas**: Use trailing commas to encourage proper auto-formatting
- **Naming**:
- Classes: `UpperCamelCase`
- Methods/variables: `lowerCamelCase`
- Private members: `_leadingUnderscore`
- Constants: `camelCase` or `UPPER_SNAKE_CASE` for static const
### Code Style Examples
```dart
// Good: trailing commas for multi-line
final job = Job(
id: '123',
jobNumber: 'JOB-001',
status: 'ASSIGNED',
// ...
);
// Good: private helper methods
String _formatAddress(String street, String city) {
return '$street, $city';
}
// Good: type annotations for public APIs
Future<List<Job>> loadJobs() async {
// ...
}
```
### Imports
- Order: Dart SDK → Flutter → Third-party → Project (alphabetical within groups)
- Use `package:votianlt_app/` prefix for project imports
---
## Testing Guidelines
### Test Structure
```dart
import 'package:flutter_test/flutter_test.dart';
import 'package:votianlt_app/models/job.dart';
void main() {
group('Job Parsing', () {
test('parses basic fields correctly', () {
// Arrange
final json = {'job': {'id': '123', ...}};
// Act
final job = Job.fromJson(json);
// Assert
expect(job.id, '123');
});
});
}
```
### Testing Patterns
- Mirror the `lib/` directory structure in `test/`
- Name tests after the unit: `job_parsing_test.dart`
- Use `group()` for related assertions
- Test both success and edge cases
- Test round-trip serialization (`fromJson``toJson``fromJson`)
### Mocking
Use `mocktail` for mocking dependencies in service tests.
---
## Database (ObjectBox)
### Entity Definition Example
```dart
@Entity()
class JobEntity {
@Id()
int id = 0;
@Unique()
String jobId;
String jobData; // JSON-encoded
@Property(type: PropertyType.date)
DateTime createdAt;
@Property(type: PropertyType.date)
DateTime updatedAt;
JobEntity({...});
}
```
### Regenerating Code
After modifying entities in `lib/entities/`:
```bash
flutter pub run build_runner build
```
This generates `lib/objectbox.g.dart`.
---
## WebSocket Protocol
### Connection
- URL: `ws://localhost:8082/ws/messaging` (desktop)
- Android Emulator: `ws://10.0.2.2:8082/ws/messaging`
### Message Format
```json
{
"topic": "/client/auth",
"payload": { ... }
}
```
### Client → Server Topics
- `/server/login` - Authentication
- `/server/jobs/assigned` - Request job list
- `/server/message` - Send chat message
- `/server/task_completed` - Mark task complete
### Server → Client Topics
- `/client/{userId}/auth` - Auth response
- `/client/{userId}/jobs` - Job list
- `/client/{userId}/message` - Incoming chat
- `/client/{userId}/job_deleted` - Job deleted
- `/client/{userId}/job_created` - New job
---
## Security Considerations
### Credentials
- Email/password stored in ObjectBox (encrypted at rest by OS)
- Credentials cleared on logout
- Auto-login with saved credentials
### WebSocket
- Reconnection with 15-second interval
- Message buffering when offline
- Unique App ID per installation for client identification
### Secrets
- Do not commit API keys or credentials
- Server endpoint configurable in `WebSocketService._buildWebSocketUrl()`
---
## Platform-Specific Notes
### Android
- Minimum SDK: Defined in `android/app/build.gradle`
- Permissions in `AndroidManifest.xml`:
- `INTERNET` - WebSocket communication
- `CAMERA` - Photo/barcode capture
- `WRITE_EXTERNAL_STORAGE` - Photo storage
- `POST_NOTIFICATIONS` - Push notifications
- `VIBRATE` - Notification vibration
### iOS
- Camera and photo permissions in `ios/Runner/Info.plist`
- Notification permissions configured
---
## Common Development Tasks
### Adding a New Task Type
1. Create model in `lib/models/tasks/new_task_type.dart`
2. Add to `Task.fromJson()` factory in `lib/models/task.dart`
3. Add UI screen in `lib/tasks/` if needed
4. Update `task_view.dart` to handle the new type
5. Add tests in `test/models/`
### Adding a New Database Entity
1. Create entity class in `lib/entities/`
2. Run `flutter pub run build_runner build`
3. Add CRUD operations in `DatabaseService`
### Modifying WebSocket Messages
1. Update message handler in `WebSocketService._handleMessage()`
2. Add topic constant to `MQTopics` if needed
3. Update `MessageHandler` for processing logic
---
## Localization
The app uses German for UI text:
- Job status: "Erstellt", "Zugewiesen", "In Bearbeitung", "Abgeschlossen"
- Notifications: "Neue Jobs", "Neue Nachricht"
- Chat: "Allgemeine Nachrichten"
---
## Debugging
### Logging
Use the developer log utility:
```dart
import 'package:votianlt_app/services/developer.dart' as developer;
developer.log('Debug message', name: 'ComponentName');
```
### Common Issues
1. **WebSocket not connecting**: Check server is running on port 8082
2. **Database errors**: Run `flutter clean` and `flutter pub get`
3. **ObjectBox issues**: Regenerate with `build_runner`
4. **Camera not working**: Check platform permissions

71
app/CLAUDE.md Normal file
View File

@@ -0,0 +1,71 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Build & Development Commands
```bash
flutter pub get # Install dependencies
flutter analyze # Run static analysis (run after every task)
dart format <paths> # Format code
flutter test # Run tests
flutter run -d <device> # Run app on device
dart run build_runner build # Generate ObjectBox code after entity changes
```
**Important:** Run `flutter analyze` after every task and fix any reported issues before committing.
## Architecture Overview
This is a Flutter app for job/task management with MQTT-based backend communication. The app is written in German for end users.
### Core Components
**AppState** (`lib/app_state.dart`)
- Singleton managing global state: `appUserId`, in-memory jobs list
- Handles persistence via DatabaseService with coalesced writes
- Emits `jobsUpdated` events via both StreamController and DartMQ
**DartMQ** (`lib/services/dart_mq.dart`)
- Lightweight in-app pub/sub message bus for decoupled communication
- Key topics defined in `MQTopics`: `connectionStatus`, `authResponse`, `jobsResponse`, `taskEvents`, `jobsUpdated`, `chatIncoming`, `chatOutgoing`
- UI and services subscribe/publish without direct dependencies
**MqttService** (`lib/services/mqtt_service.dart`, aliased as `StompService`)
- MQTT client connecting to Mosquitto broker at `mqtt-2.assecutor.de:42099`
- Handles authentication, job loading, chat messages, task completion
- Message envelope pattern with ACK/retry system for reliable delivery
- Offline message queuing via DatabaseService
- Publishes all server events through DartMQ topics
**DatabaseService** (`lib/services/database_service.dart`)
- ObjectBox-based local persistence
- Stores jobs, task status, user data, chat messages, queued MQTT messages
- Entities in `lib/entities/` require `dart run build_runner build` after changes
### Data Flow
1. `LoginView` initiates MQTT connection, sends credentials to `/server/login`
2. Server responds on `/client/{appId}/auth` → MqttService publishes `MQTopics.authResponse`
3. On success, `AppState` stores `appUserId`, `JobsView` requests jobs via `/server/{userId}/jobs/assigned`
4. Jobs arrive on `/client/{userId}/jobs` → published to `MQTopics.jobsResponse` → persisted → UI refresh via `MQTopics.jobsUpdated`
5. Task updates flow through `/client/{userId}/notifications``MQTopics.taskEvents`
### Models
- **Job** (`lib/models/job.dart`): Contains pickup/delivery addresses, cargo items, and tasks
- **Task** (`lib/models/task.dart`): Abstract base with subtypes: `ConfirmationTask`, `PhotoTask`, `TodoListTask`, `SignatureTask`, `BarcodeTask`, `CommentTask`, `GenericTask`
- **ChatMessage** (`lib/models/chat_message.dart`): Chat with direction, content type, job linking
### Views
- `LoginView``JobsView``CargoItemsView` → Task screens
- `ChatsView``ChatDetailsView`
- Task capture screens in `lib/tasks/`: photo, signature, barcode
## Key Patterns
- All services are singletons (factory constructors returning `_instance`)
- MQTT messages wrapped in `MessageEnvelope` for reliable delivery with ACK
- UI subscribes to DartMQ topics rather than holding service references
- Jobs are normalized before persistence to ensure consistent data

16
app/README.md Normal file
View File

@@ -0,0 +1,16 @@
# votianlt_app
votian LT
## Getting Started
This project is a starting point for a Flutter application.
A few resources to get you started if this is your first Flutter project:
- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab)
- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook)
For help getting started with Flutter development, view the
[online documentation](https://docs.flutter.dev/), which offers tutorials,
samples, guidance on mobile development, and a full API reference.

28
app/analysis_options.yaml Normal file
View File

@@ -0,0 +1,28 @@
# This file configures the analyzer, which statically analyzes Dart code to
# check for errors, warnings, and lints.
#
# The issues identified by the analyzer are surfaced in the UI of Dart-enabled
# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
# invoked from the command line by running `flutter analyze`.
# The following line activates a set of recommended lints for Flutter apps,
# packages, and plugins designed to encourage good coding practices.
include: package:flutter_lints/flutter.yaml
linter:
# The lint rules applied to this project can be customized in the
# section below to disable rules from the `package:flutter_lints/flutter.yaml`
# included above or to enable additional rules. A list of all available lints
# and their documentation is published at https://dart.dev/lints.
#
# Instead of disabling a lint rule for the entire project in the
# section below, it can also be suppressed for a single line of code
# or a specific dart file by using the `// ignore: name_of_lint` and
# `// ignore_for_file: name_of_lint` syntax on the line or in the file
# producing the lint.
rules:
# avoid_print: false # Uncomment to disable the `avoid_print` rule
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
# Additional information about this file can be found at
# https://dart.dev/guides/language/analysis-options

14
app/android/.gitignore vendored Normal file
View File

@@ -0,0 +1,14 @@
gradle-wrapper.jar
/.gradle
/captures/
/gradlew
/gradlew.bat
/local.properties
GeneratedPluginRegistrant.java
.cxx/
# Remember to never publicly share your keystore.
# See https://flutter.dev/to/reference-keystore
key.properties
**/*.keystore
**/*.jks

View File

@@ -0,0 +1,49 @@
plugins {
id("com.android.application")
id("kotlin-android")
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
id("dev.flutter.flutter-gradle-plugin")
}
android {
namespace = "de.assecutor.votianlt_app"
compileSdk = 36
ndkVersion = "27.0.12077973"
compileOptions {
isCoreLibraryDesugaringEnabled = true
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
kotlinOptions {
jvmTarget = JavaVersion.VERSION_11.toString()
}
defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId = "de.assecutor.votianlt_app"
// You can update the following values to match your application needs.
// For more information, see: https://flutter.dev/to/review-gradle-config.
minSdk = flutter.minSdkVersion
targetSdk = flutter.targetSdkVersion
versionCode = flutter.versionCode
versionName = flutter.versionName
}
buildTypes {
release {
// TODO: Add your own signing config for the release build.
// Signing with the debug keys for now, so `flutter run --release` works.
signingConfig = signingConfigs.getByName("debug")
}
}
}
dependencies {
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.1.4")
}
flutter {
source = "../.."
}

View File

@@ -0,0 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- The INTERNET permission is required for development. Specifically,
the Flutter tool needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>

View File

@@ -0,0 +1,55 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.CAMERA"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
<uses-permission android:name="android.permission.VIBRATE"/>
<!-- GPS Location permissions -->
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
<application
android:label="votianlt_app"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher"
android:networkSecurityConfig="@xml/network_security_config">
<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTop"
android:taskAffinity=""
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize">
<!-- Specifies an Android theme to apply to this Activity as soon as
the Android process has started. This theme is visible to the user
while the Flutter UI initializes. After that, this theme continues
to determine the Window background behind the Flutter UI. -->
<meta-data
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme"
/>
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
<meta-data
android:name="flutterEmbedding"
android:value="2" />
</application>
<!-- Required to query activities that can process text, see:
https://developer.android.com/training/package-visibility and
https://developer.android.com/reference/android/content/Intent#ACTION_PROCESS_TEXT.
In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. -->
<queries>
<intent>
<action android:name="android.intent.action.PROCESS_TEXT"/>
<data android:mimeType="text/plain"/>
</intent>
</queries>
</manifest>

View File

@@ -0,0 +1,5 @@
package de.assecutor.votianlt_app
import io.flutter.embedding.android.FlutterActivity
class MainActivity : FlutterActivity()

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="?android:colorBackground" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
</layer-list>

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@android:color/white" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
</layer-list>

Binary file not shown.

After

Width:  |  Height:  |  Size: 544 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 442 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 721 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on -->
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>

View File

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->
<style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>

View File

@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<domain-config cleartextTrafficPermitted="true">
<!-- Allow cleartext traffic for local network addresses -->
<domain includeSubdomains="true">192.168.180.10</domain>
<domain includeSubdomains="true">192.168.0.0/16</domain>
<domain includeSubdomains="true">10.0.0.0/8</domain>
<domain includeSubdomains="true">10.0.2.2</domain>
<domain includeSubdomains="true">172.16.0.0/12</domain>
<domain includeSubdomains="true">localhost</domain>
<domain includeSubdomains="true">127.0.0.1</domain>
</domain-config>
</network-security-config>

View File

@@ -0,0 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- The INTERNET permission is required for development. Specifically,
the Flutter tool needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>

View File

@@ -0,0 +1,21 @@
allprojects {
repositories {
google()
mavenCentral()
}
}
val newBuildDir: Directory = rootProject.layout.buildDirectory.dir("../../build").get()
rootProject.layout.buildDirectory.value(newBuildDir)
subprojects {
val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name)
project.layout.buildDirectory.value(newSubprojectBuildDir)
}
subprojects {
project.evaluationDependsOn(":app")
}
tasks.register<Delete>("clean") {
delete(rootProject.layout.buildDirectory)
}

View File

@@ -0,0 +1,3 @@
org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError
android.useAndroidX=true
android.enableJetifier=true

View File

@@ -0,0 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-all.zip

View File

@@ -0,0 +1,25 @@
pluginManagement {
val flutterSdkPath = run {
val properties = java.util.Properties()
file("local.properties").inputStream().use { properties.load(it) }
val flutterSdkPath = properties.getProperty("flutter.sdk")
require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" }
flutterSdkPath
}
includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
}
plugins {
id("dev.flutter.flutter-plugin-loader") version "1.0.0"
id("com.android.application") version "8.7.0" apply false
id("org.jetbrains.kotlin.android") version "2.1.0" apply false
}
include(":app")

View File

@@ -0,0 +1,3 @@
description: This file stores settings for Dart & Flutter DevTools.
documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states
extensions:

34
app/ios/.gitignore vendored Normal file
View File

@@ -0,0 +1,34 @@
**/dgph
*.mode1v3
*.mode2v3
*.moved-aside
*.pbxuser
*.perspectivev3
**/*sync/
.sconsign.dblite
.tags*
**/.vagrant/
**/DerivedData/
Icon?
**/Pods/
**/.symlinks/
profile
xcuserdata
**/.generated/
Flutter/App.framework
Flutter/Flutter.framework
Flutter/Flutter.podspec
Flutter/Generated.xcconfig
Flutter/ephemeral/
Flutter/app.flx
Flutter/app.zip
Flutter/flutter_assets/
Flutter/flutter_export_environment.sh
ServiceDefinitions.json
Runner/GeneratedPluginRegistrant.*
# Exceptions to above rules.
!default.mode1v3
!default.mode2v3
!default.pbxuser
!default.perspectivev3

View File

@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleExecutable</key>
<string>App</string>
<key>CFBundleIdentifier</key>
<string>io.flutter.flutter.app</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>App</string>
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>1.0</string>
<key>MinimumOSVersion</key>
<string>12.0</string>
</dict>
</plist>

View File

@@ -0,0 +1,2 @@
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
#include "Generated.xcconfig"

View File

@@ -0,0 +1,2 @@
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
#include "Generated.xcconfig"

43
app/ios/Podfile Normal file
View File

@@ -0,0 +1,43 @@
# Uncomment this line to define a global platform for your project
# platform :ios, '12.0'
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
project 'Runner', {
'Debug' => :debug,
'Profile' => :release,
'Release' => :release,
}
def flutter_root
generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__)
unless File.exist?(generated_xcode_build_settings_path)
raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first"
end
File.foreach(generated_xcode_build_settings_path) do |line|
matches = line.match(/FLUTTER_ROOT\=(.*)/)
return matches[1].strip if matches
end
raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get"
end
require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root)
flutter_ios_podfile_setup
target 'Runner' do
use_frameworks!
flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__))
target 'RunnerTests' do
inherit! :search_paths
end
end
post_install do |installer|
installer.pods_project.targets.each do |target|
flutter_additional_ios_build_settings(target)
end
end

198
app/ios/Podfile.lock Normal file
View File

@@ -0,0 +1,198 @@
PODS:
- camera_avfoundation (0.0.1):
- Flutter
- DKImagePickerController/Core (4.3.9):
- DKImagePickerController/ImageDataManager
- DKImagePickerController/Resource
- DKImagePickerController/ImageDataManager (4.3.9)
- DKImagePickerController/PhotoGallery (4.3.9):
- DKImagePickerController/Core
- DKPhotoGallery
- DKImagePickerController/Resource (4.3.9)
- DKPhotoGallery (0.0.19):
- DKPhotoGallery/Core (= 0.0.19)
- DKPhotoGallery/Model (= 0.0.19)
- DKPhotoGallery/Preview (= 0.0.19)
- DKPhotoGallery/Resource (= 0.0.19)
- SDWebImage
- SwiftyGif
- DKPhotoGallery/Core (0.0.19):
- DKPhotoGallery/Model
- DKPhotoGallery/Preview
- SDWebImage
- SwiftyGif
- DKPhotoGallery/Model (0.0.19):
- SDWebImage
- SwiftyGif
- DKPhotoGallery/Preview (0.0.19):
- DKPhotoGallery/Model
- DKPhotoGallery/Resource
- SDWebImage
- SwiftyGif
- DKPhotoGallery/Resource (0.0.19):
- SDWebImage
- SwiftyGif
- file_picker (0.0.1):
- DKImagePickerController/PhotoGallery
- Flutter
- file_selector_ios (0.0.1):
- Flutter
- Flutter (1.0.0)
- GoogleDataTransport (9.4.1):
- GoogleUtilities/Environment (~> 7.7)
- nanopb (< 2.30911.0, >= 2.30908.0)
- PromisesObjC (< 3.0, >= 1.2)
- GoogleMLKit/BarcodeScanning (6.0.0):
- GoogleMLKit/MLKitCore
- MLKitBarcodeScanning (~> 5.0.0)
- GoogleMLKit/MLKitCore (6.0.0):
- MLKitCommon (~> 11.0.0)
- GoogleToolboxForMac/Defines (4.2.1)
- GoogleToolboxForMac/Logger (4.2.1):
- GoogleToolboxForMac/Defines (= 4.2.1)
- "GoogleToolboxForMac/NSData+zlib (4.2.1)":
- GoogleToolboxForMac/Defines (= 4.2.1)
- GoogleUtilities/Environment (7.13.3):
- GoogleUtilities/Privacy
- PromisesObjC (< 3.0, >= 1.2)
- GoogleUtilities/Logger (7.13.3):
- GoogleUtilities/Environment
- GoogleUtilities/Privacy
- GoogleUtilities/Privacy (7.13.3)
- GoogleUtilities/UserDefaults (7.13.3):
- GoogleUtilities/Logger
- GoogleUtilities/Privacy
- GoogleUtilitiesComponents (1.1.0):
- GoogleUtilities/Logger
- GTMSessionFetcher/Core (3.5.0)
- MLImage (1.0.0-beta5)
- MLKitBarcodeScanning (5.0.0):
- MLKitCommon (~> 11.0)
- MLKitVision (~> 7.0)
- MLKitCommon (11.0.0):
- GoogleDataTransport (< 10.0, >= 9.4.1)
- GoogleToolboxForMac/Logger (< 5.0, >= 4.2.1)
- "GoogleToolboxForMac/NSData+zlib (< 5.0, >= 4.2.1)"
- GoogleUtilities/UserDefaults (< 8.0, >= 7.13.0)
- GoogleUtilitiesComponents (~> 1.0)
- GTMSessionFetcher/Core (< 4.0, >= 3.3.2)
- MLKitVision (7.0.0):
- GoogleToolboxForMac/Logger (< 5.0, >= 4.2.1)
- "GoogleToolboxForMac/NSData+zlib (< 5.0, >= 4.2.1)"
- GTMSessionFetcher/Core (< 4.0, >= 3.3.2)
- MLImage (= 1.0.0-beta5)
- MLKitCommon (~> 11.0)
- mobile_scanner (5.2.3):
- Flutter
- GoogleMLKit/BarcodeScanning (~> 6.0.0)
- nanopb (2.30910.0):
- nanopb/decode (= 2.30910.0)
- nanopb/encode (= 2.30910.0)
- nanopb/decode (2.30910.0)
- nanopb/encode (2.30910.0)
- ObjectBox (4.4.1)
- objectbox_flutter_libs (0.0.1):
- Flutter
- ObjectBox (= 4.4.1)
- package_info_plus (0.4.5):
- Flutter
- path_provider_foundation (0.0.1):
- Flutter
- FlutterMacOS
- PromisesObjC (2.4.0)
- SDWebImage (5.21.5):
- SDWebImage/Core (= 5.21.5)
- SDWebImage/Core (5.21.5)
- SwiftyGif (5.4.5)
- url_launcher_ios (0.0.1):
- Flutter
- webview_flutter_wkwebview (0.0.1):
- Flutter
- FlutterMacOS
DEPENDENCIES:
- camera_avfoundation (from `.symlinks/plugins/camera_avfoundation/ios`)
- file_picker (from `.symlinks/plugins/file_picker/ios`)
- file_selector_ios (from `.symlinks/plugins/file_selector_ios/ios`)
- Flutter (from `Flutter`)
- mobile_scanner (from `.symlinks/plugins/mobile_scanner/ios`)
- objectbox_flutter_libs (from `.symlinks/plugins/objectbox_flutter_libs/ios`)
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
- webview_flutter_wkwebview (from `.symlinks/plugins/webview_flutter_wkwebview/darwin`)
SPEC REPOS:
trunk:
- DKImagePickerController
- DKPhotoGallery
- GoogleDataTransport
- GoogleMLKit
- GoogleToolboxForMac
- GoogleUtilities
- GoogleUtilitiesComponents
- GTMSessionFetcher
- MLImage
- MLKitBarcodeScanning
- MLKitCommon
- MLKitVision
- nanopb
- ObjectBox
- PromisesObjC
- SDWebImage
- SwiftyGif
EXTERNAL SOURCES:
camera_avfoundation:
:path: ".symlinks/plugins/camera_avfoundation/ios"
file_picker:
:path: ".symlinks/plugins/file_picker/ios"
file_selector_ios:
:path: ".symlinks/plugins/file_selector_ios/ios"
Flutter:
:path: Flutter
mobile_scanner:
:path: ".symlinks/plugins/mobile_scanner/ios"
objectbox_flutter_libs:
:path: ".symlinks/plugins/objectbox_flutter_libs/ios"
package_info_plus:
:path: ".symlinks/plugins/package_info_plus/ios"
path_provider_foundation:
:path: ".symlinks/plugins/path_provider_foundation/darwin"
url_launcher_ios:
:path: ".symlinks/plugins/url_launcher_ios/ios"
webview_flutter_wkwebview:
:path: ".symlinks/plugins/webview_flutter_wkwebview/darwin"
SPEC CHECKSUMS:
camera_avfoundation: be3be85408cd4126f250386828e9b1dfa40ab436
DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c
DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60
file_picker: a0560bc09d61de87f12d246fc47d2119e6ef37be
file_selector_ios: f92e583d43608aebc2e4a18daac30b8902845502
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
GoogleDataTransport: 6c09b596d841063d76d4288cc2d2f42cc36e1e2a
GoogleMLKit: 97ac7af399057e99182ee8edfa8249e3226a4065
GoogleToolboxForMac: d1a2cbf009c453f4d6ded37c105e2f67a32206d8
GoogleUtilities: ea963c370a38a8069cc5f7ba4ca849a60b6d7d15
GoogleUtilitiesComponents: 679b2c881db3b615a2777504623df6122dd20afe
GTMSessionFetcher: 5aea5ba6bd522a239e236100971f10cb71b96ab6
MLImage: 1824212150da33ef225fbd3dc49f184cf611046c
MLKitBarcodeScanning: 10ca0845a6d15f2f6e911f682a1998b68b973e8b
MLKitCommon: afec63980417d29ffbb4790529a1b0a2291699e1
MLKitVision: e858c5f125ecc288e4a31127928301eaba9ae0c1
mobile_scanner: 92e8812bf22a8f84131e2a7f9d0f44dad1a4742b
nanopb: 438bc412db1928dac798aa6fd75726007be04262
ObjectBox: 7da4aceb5013d041bfafdbc6d744a26918b09757
objectbox_flutter_libs: 09b1dec1b4cd27bf1a5f9bae7ccaa7e43588bf31
package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499
path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
SDWebImage: e9c98383c7572d713c1a0d7dd2783b10599b9838
SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4
url_launcher_ios: 694010445543906933d732453a59da0a173ae33d
webview_flutter_wkwebview: 1821ceac936eba6f7984d89a9f3bcb4dea99ebb2
PODFILE CHECKSUM: 4305caec6b40dde0ae97be1573c53de1882a07e5
COCOAPODS: 1.16.2

View File

@@ -0,0 +1,746 @@
// !$*UTF8*$!
{
archiveVersion = 1;
classes = {
};
objectVersion = 54;
objects = {
/* Begin PBXBuildFile section */
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
2510A7C53652D94A0BD1AC0E /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B1894F93A282ED106CA1628A /* Pods_RunnerTests.framework */; };
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; };
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
7750814138033D7B5089EA79 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6A466C5081D4AFB2E83EE48C /* Pods_Runner.framework */; };
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
331C8085294A63A400263BE5 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 97C146E61CF9000F007C117D /* Project object */;
proxyType = 1;
remoteGlobalIDString = 97C146ED1CF9000F007C117D;
remoteInfo = Runner;
};
/* End PBXContainerItemProxy section */
/* Begin PBXCopyFilesBuildPhase section */
9705A1C41CF9048500538489 /* Embed Frameworks */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = "";
dstSubfolderSpec = 10;
files = (
);
name = "Embed Frameworks";
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; };
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; };
331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
3A0BB33DF424113620931648 /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = "<group>"; };
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
48563AD38CD52F6EB5981C45 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<group>"; };
6352F6ED7E5B9DC92334C090 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = "<group>"; };
6A466C5081D4AFB2E83EE48C /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; };
9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; };
97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; };
97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
97E626E31BDD11C098C2470E /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = "<group>"; };
A042B5F29AFF592248DD7D50 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = "<group>"; };
AB23306D1B7EEAFBF39838CE /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = "<group>"; };
B1894F93A282ED106CA1628A /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
353CB3693FF9EBD22F03DEB9 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
2510A7C53652D94A0BD1AC0E /* Pods_RunnerTests.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
97C146EB1CF9000F007C117D /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
7750814138033D7B5089EA79 /* Pods_Runner.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
2AC4A5BBFC8A63F231B5394E /* Pods */ = {
isa = PBXGroup;
children = (
A042B5F29AFF592248DD7D50 /* Pods-Runner.debug.xcconfig */,
6352F6ED7E5B9DC92334C090 /* Pods-Runner.release.xcconfig */,
48563AD38CD52F6EB5981C45 /* Pods-Runner.profile.xcconfig */,
AB23306D1B7EEAFBF39838CE /* Pods-RunnerTests.debug.xcconfig */,
97E626E31BDD11C098C2470E /* Pods-RunnerTests.release.xcconfig */,
3A0BB33DF424113620931648 /* Pods-RunnerTests.profile.xcconfig */,
);
name = Pods;
path = Pods;
sourceTree = "<group>";
};
331C8082294A63A400263BE5 /* RunnerTests */ = {
isa = PBXGroup;
children = (
331C807B294A618700263BE5 /* RunnerTests.swift */,
);
path = RunnerTests;
sourceTree = "<group>";
};
511A3FF81F794991B7581FB8 /* Frameworks */ = {
isa = PBXGroup;
children = (
6A466C5081D4AFB2E83EE48C /* Pods_Runner.framework */,
B1894F93A282ED106CA1628A /* Pods_RunnerTests.framework */,
);
name = Frameworks;
sourceTree = "<group>";
};
9740EEB11CF90186004384FC /* Flutter */ = {
isa = PBXGroup;
children = (
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,
9740EEB21CF90195004384FC /* Debug.xcconfig */,
7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
9740EEB31CF90195004384FC /* Generated.xcconfig */,
);
name = Flutter;
sourceTree = "<group>";
};
97C146E51CF9000F007C117D = {
isa = PBXGroup;
children = (
9740EEB11CF90186004384FC /* Flutter */,
97C146F01CF9000F007C117D /* Runner */,
97C146EF1CF9000F007C117D /* Products */,
331C8082294A63A400263BE5 /* RunnerTests */,
2AC4A5BBFC8A63F231B5394E /* Pods */,
511A3FF81F794991B7581FB8 /* Frameworks */,
);
sourceTree = "<group>";
};
97C146EF1CF9000F007C117D /* Products */ = {
isa = PBXGroup;
children = (
97C146EE1CF9000F007C117D /* Runner.app */,
331C8081294A63A400263BE5 /* RunnerTests.xctest */,
);
name = Products;
sourceTree = "<group>";
};
97C146F01CF9000F007C117D /* Runner */ = {
isa = PBXGroup;
children = (
97C146FA1CF9000F007C117D /* Main.storyboard */,
97C146FD1CF9000F007C117D /* Assets.xcassets */,
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */,
97C147021CF9000F007C117D /* Info.plist */,
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */,
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */,
74858FAE1ED2DC5600515810 /* AppDelegate.swift */,
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */,
);
path = Runner;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
331C8080294A63A400263BE5 /* RunnerTests */ = {
isa = PBXNativeTarget;
buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */;
buildPhases = (
34F233ACE4BA3F5B820F795F /* [CP] Check Pods Manifest.lock */,
331C807D294A63A400263BE5 /* Sources */,
331C807F294A63A400263BE5 /* Resources */,
353CB3693FF9EBD22F03DEB9 /* Frameworks */,
);
buildRules = (
);
dependencies = (
331C8086294A63A400263BE5 /* PBXTargetDependency */,
);
name = RunnerTests;
productName = RunnerTests;
productReference = 331C8081294A63A400263BE5 /* RunnerTests.xctest */;
productType = "com.apple.product-type.bundle.unit-test";
};
97C146ED1CF9000F007C117D /* Runner */ = {
isa = PBXNativeTarget;
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
buildPhases = (
B808696A3E30F0EC4B8DF6F5 /* [CP] Check Pods Manifest.lock */,
9740EEB61CF901F6004384FC /* Run Script */,
97C146EA1CF9000F007C117D /* Sources */,
97C146EB1CF9000F007C117D /* Frameworks */,
97C146EC1CF9000F007C117D /* Resources */,
9705A1C41CF9048500538489 /* Embed Frameworks */,
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
70582023162C4E4C0B4CC063 /* [CP] Embed Pods Frameworks */,
DE505B8273E1DC801F9E27AD /* [CP] Copy Pods Resources */,
);
buildRules = (
);
dependencies = (
);
name = Runner;
productName = Runner;
productReference = 97C146EE1CF9000F007C117D /* Runner.app */;
productType = "com.apple.product-type.application";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
97C146E61CF9000F007C117D /* Project object */ = {
isa = PBXProject;
attributes = {
BuildIndependentTargetsInParallel = YES;
LastUpgradeCheck = 1510;
ORGANIZATIONNAME = "";
TargetAttributes = {
331C8080294A63A400263BE5 = {
CreatedOnToolsVersion = 14.0;
TestTargetID = 97C146ED1CF9000F007C117D;
};
97C146ED1CF9000F007C117D = {
CreatedOnToolsVersion = 7.3.1;
LastSwiftMigration = 1100;
};
};
};
buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */;
compatibilityVersion = "Xcode 9.3";
developmentRegion = en;
hasScannedForEncodings = 0;
knownRegions = (
en,
Base,
);
mainGroup = 97C146E51CF9000F007C117D;
productRefGroup = 97C146EF1CF9000F007C117D /* Products */;
projectDirPath = "";
projectRoot = "";
targets = (
97C146ED1CF9000F007C117D /* Runner */,
331C8080294A63A400263BE5 /* RunnerTests */,
);
};
/* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */
331C807F294A63A400263BE5 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
97C146EC1CF9000F007C117D /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */,
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */,
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */,
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */
34F233ACE4BA3F5B820F795F /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
"${PODS_ROOT}/Manifest.lock",
);
name = "[CP] Check Pods Manifest.lock";
outputFileListPaths = (
);
outputPaths = (
"$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
showEnvVarsInLog = 0;
};
3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
"${TARGET_BUILD_DIR}/${INFOPLIST_PATH}",
);
name = "Thin Binary";
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
};
70582023162C4E4C0B4CC063 /* [CP] Embed Pods Frameworks */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist",
);
name = "[CP] Embed Pods Frameworks";
outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
showEnvVarsInLog = 0;
};
9740EEB61CF901F6004384FC /* Run Script */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
);
name = "Run Script";
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
};
B808696A3E30F0EC4B8DF6F5 /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
"${PODS_ROOT}/Manifest.lock",
);
name = "[CP] Check Pods Manifest.lock";
outputFileListPaths = (
);
outputPaths = (
"$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
showEnvVarsInLog = 0;
};
DE505B8273E1DC801F9E27AD /* [CP] Copy Pods Resources */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist",
);
name = "[CP] Copy Pods Resources";
outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n";
showEnvVarsInLog = 0;
};
/* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
331C807D294A63A400263BE5 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
97C146EA1CF9000F007C117D /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */,
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin PBXTargetDependency section */
331C8086294A63A400263BE5 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = 97C146ED1CF9000F007C117D /* Runner */;
targetProxy = 331C8085294A63A400263BE5 /* PBXContainerItemProxy */;
};
/* End PBXTargetDependency section */
/* Begin PBXVariantGroup section */
97C146FA1CF9000F007C117D /* Main.storyboard */ = {
isa = PBXVariantGroup;
children = (
97C146FB1CF9000F007C117D /* Base */,
);
name = Main.storyboard;
sourceTree = "<group>";
};
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = {
isa = PBXVariantGroup;
children = (
97C147001CF9000F007C117D /* Base */,
);
name = LaunchScreen.storyboard;
sourceTree = "<group>";
};
/* End PBXVariantGroup section */
/* Begin XCBuildConfiguration section */
249021D3217E4FDB00AE95B9 /* Profile */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
TARGETED_DEVICE_FAMILY = "1,2";
VALIDATE_PRODUCT = YES;
};
name = Profile;
};
249021D4217E4FDB00AE95B9 /* Profile */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = de.assecutor.votianltApp;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
};
name = Profile;
};
331C8088294A63A400263BE5 /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = AB23306D1B7EEAFBF39838CE /* Pods-RunnerTests.debug.xcconfig */;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = de.assecutor.votianltApp.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
};
name = Debug;
};
331C8089294A63A400263BE5 /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 97E626E31BDD11C098C2470E /* Pods-RunnerTests.release.xcconfig */;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = de.assecutor.votianltApp.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
};
name = Release;
};
331C808A294A63A400263BE5 /* Profile */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 3A0BB33DF424113620931648 /* Pods-RunnerTests.profile.xcconfig */;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = de.assecutor.votianltApp.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
};
name = Profile;
};
97C147031CF9000F007C117D /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = dwarf;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_DYNAMIC_NO_PIC = NO;
GCC_NO_COMMON_BLOCKS = YES;
GCC_OPTIMIZATION_LEVEL = 0;
GCC_PREPROCESSOR_DEFINITIONS = (
"DEBUG=1",
"$(inherited)",
);
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
};
97C147041CF9000F007C117D /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_OPTIMIZATION_LEVEL = "-O";
TARGETED_DEVICE_FAMILY = "1,2";
VALIDATE_PRODUCT = YES;
};
name = Release;
};
97C147061CF9000F007C117D /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = de.assecutor.votianltApp;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
};
name = Debug;
};
97C147071CF9000F007C117D /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = de.assecutor.votianltApp;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
};
name = Release;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = {
isa = XCConfigurationList;
buildConfigurations = (
331C8088294A63A400263BE5 /* Debug */,
331C8089294A63A400263BE5 /* Release */,
331C808A294A63A400263BE5 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = {
isa = XCConfigurationList;
buildConfigurations = (
97C147031CF9000F007C117D /* Debug */,
97C147041CF9000F007C117D /* Release */,
249021D3217E4FDB00AE95B9 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = {
isa = XCConfigurationList;
buildConfigurations = (
97C147061CF9000F007C117D /* Debug */,
97C147071CF9000F007C117D /* Release */,
249021D4217E4FDB00AE95B9 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
};
rootObject = 97C146E61CF9000F007C117D /* Project object */;
}

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "self:">
</FileRef>
</Workspace>

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>PreviewsEnabled</key>
<false/>
</dict>
</plist>

View File

@@ -0,0 +1,99 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1510"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</MacroExpansion>
<Testables>
<TestableReference
skipped = "NO"
parallelizable = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "331C8080294A63A400263BE5"
BuildableName = "RunnerTests.xctest"
BlueprintName = "RunnerTests"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</TestableReference>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
enableGPUValidationMode = "1"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</LaunchAction>
<ProfileAction
buildConfiguration = "Profile"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "group:Runner.xcodeproj">
</FileRef>
<FileRef
location = "group:Pods/Pods.xcodeproj">
</FileRef>
</Workspace>

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>PreviewsEnabled</key>
<false/>
</dict>
</plist>

View File

@@ -0,0 +1,13 @@
import Flutter
import UIKit
@main
@objc class AppDelegate: FlutterAppDelegate {
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
GeneratedPluginRegistrant.register(with: self)
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
}

View File

@@ -0,0 +1,122 @@
{
"images" : [
{
"size" : "20x20",
"idiom" : "iphone",
"filename" : "Icon-App-20x20@2x.png",
"scale" : "2x"
},
{
"size" : "20x20",
"idiom" : "iphone",
"filename" : "Icon-App-20x20@3x.png",
"scale" : "3x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-App-29x29@1x.png",
"scale" : "1x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-App-29x29@2x.png",
"scale" : "2x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-App-29x29@3x.png",
"scale" : "3x"
},
{
"size" : "40x40",
"idiom" : "iphone",
"filename" : "Icon-App-40x40@2x.png",
"scale" : "2x"
},
{
"size" : "40x40",
"idiom" : "iphone",
"filename" : "Icon-App-40x40@3x.png",
"scale" : "3x"
},
{
"size" : "60x60",
"idiom" : "iphone",
"filename" : "Icon-App-60x60@2x.png",
"scale" : "2x"
},
{
"size" : "60x60",
"idiom" : "iphone",
"filename" : "Icon-App-60x60@3x.png",
"scale" : "3x"
},
{
"size" : "20x20",
"idiom" : "ipad",
"filename" : "Icon-App-20x20@1x.png",
"scale" : "1x"
},
{
"size" : "20x20",
"idiom" : "ipad",
"filename" : "Icon-App-20x20@2x.png",
"scale" : "2x"
},
{
"size" : "29x29",
"idiom" : "ipad",
"filename" : "Icon-App-29x29@1x.png",
"scale" : "1x"
},
{
"size" : "29x29",
"idiom" : "ipad",
"filename" : "Icon-App-29x29@2x.png",
"scale" : "2x"
},
{
"size" : "40x40",
"idiom" : "ipad",
"filename" : "Icon-App-40x40@1x.png",
"scale" : "1x"
},
{
"size" : "40x40",
"idiom" : "ipad",
"filename" : "Icon-App-40x40@2x.png",
"scale" : "2x"
},
{
"size" : "76x76",
"idiom" : "ipad",
"filename" : "Icon-App-76x76@1x.png",
"scale" : "1x"
},
{
"size" : "76x76",
"idiom" : "ipad",
"filename" : "Icon-App-76x76@2x.png",
"scale" : "2x"
},
{
"size" : "83.5x83.5",
"idiom" : "ipad",
"filename" : "Icon-App-83.5x83.5@2x.png",
"scale" : "2x"
},
{
"size" : "1024x1024",
"idiom" : "ios-marketing",
"filename" : "Icon-App-1024x1024@1x.png",
"scale" : "1x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 295 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 406 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 450 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 282 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 462 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 704 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 406 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 586 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 862 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 862 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 762 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -0,0 +1,23 @@
{
"images" : [
{
"idiom" : "universal",
"filename" : "LaunchImage.png",
"scale" : "1x"
},
{
"idiom" : "universal",
"filename" : "LaunchImage@2x.png",
"scale" : "2x"
},
{
"idiom" : "universal",
"filename" : "LaunchImage@3x.png",
"scale" : "3x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

View File

@@ -0,0 +1,5 @@
# Launch Screen Assets
You can customize the launch screen with your own desired assets by replacing the image files in this directory.
You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images.

View File

@@ -0,0 +1,37 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="12121" systemVersion="16G29" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="12089"/>
</dependencies>
<scenes>
<!--View Controller-->
<scene sceneID="EHf-IW-A2E">
<objects>
<viewController id="01J-lp-oVM" sceneMemberID="viewController">
<layoutGuides>
<viewControllerLayoutGuide type="top" id="Ydg-fD-yQy"/>
<viewControllerLayoutGuide type="bottom" id="xbc-2k-c8Z"/>
</layoutGuides>
<view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<imageView opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" image="LaunchImage" translatesAutoresizingMaskIntoConstraints="NO" id="YRO-k0-Ey4">
</imageView>
</subviews>
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerX" secondItem="Ze5-6b-2t3" secondAttribute="centerX" id="1a2-6s-vTC"/>
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerY" secondItem="Ze5-6b-2t3" secondAttribute="centerY" id="4X2-HB-R7a"/>
</constraints>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="53" y="375"/>
</scene>
</scenes>
<resources>
<image name="LaunchImage" width="168" height="185"/>
</resources>
</document>

View File

@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="10117" systemVersion="15F34" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" initialViewController="BYZ-38-t0r">
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="10085"/>
</dependencies>
<scenes>
<!--Flutter View Controller-->
<scene sceneID="tne-QT-ifu">
<objects>
<viewController id="BYZ-38-t0r" customClass="FlutterViewController" sceneMemberID="viewController">
<layoutGuides>
<viewControllerLayoutGuide type="top" id="y3c-jy-aDJ"/>
<viewControllerLayoutGuide type="bottom" id="wfy-db-euE"/>
</layoutGuides>
<view key="view" contentMode="scaleToFill" id="8bC-Xf-vdC">
<rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="calibratedWhite"/>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
</objects>
</scene>
</scenes>
</document>

68
app/ios/Runner/Info.plist Normal file
View File

@@ -0,0 +1,68 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
<string>Votianlt App</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>votianlt_app</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>${FLUTTER_BUILD_NAME:1.0.0}</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>${FLUTTER_BUILD_NUMBER:1}</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIMainStoryboardFile</key>
<string>Main</string>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
<!-- Network configuration for STOMP WebSocket connections -->
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<true/>
<key>NSAllowsLocalNetworking</key>
<true/>
</dict>
<key>NSLocalNetworkUsageDescription</key>
<string>This app needs to connect to local network services for STOMP messaging.</string>
<key>NSCameraUsageDescription</key>
<string>This app needs access to camera to take photos for task completion.</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>This app needs access to photo library to save and manage task photos.</string>
<!-- GPS Location permissions -->
<key>NSLocationWhenInUseUsageDescription</key>
<string>This app needs access to your location to track delivery routes.</string>
<key>NSLocationAlwaysUsageDescription</key>
<string>This app needs access to your location to track delivery routes even when in the background.</string>
</dict>
</plist>

View File

@@ -0,0 +1 @@
#import "GeneratedPluginRegistrant.h"

View File

@@ -0,0 +1,12 @@
import Flutter
import UIKit
import XCTest
class RunnerTests: XCTestCase {
func testExample() {
// If you add code to the Runner application, consider adding tests here.
// See https://developer.apple.com/documentation/xctest for more information about using XCTest.
}
}

173
app/lib/app_state.dart Normal file
View File

@@ -0,0 +1,173 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'models/job.dart';
import 'services/database_service.dart';
import 'services/dart_mq.dart';
import 'l10n/app_localizations.dart';
/// Global notifier for language changes
final ValueNotifier<Locale> localeNotifier = ValueNotifier<Locale>(const Locale('de'));
class AppState {
static final AppState _instance = AppState._internal();
factory AppState() => _instance;
AppState._internal();
String? _loggedInEmail;
List<Job> _assignedJobs = [];
final DatabaseService _databaseService = DatabaseService();
// Language settings
String _languageCode = 'de';
String get languageCode => _languageCode;
/// Get current locale
Locale get currentLocale => Locale(_languageCode);
/// Set language and update the global notifier
Future<void> setLanguage(String languageCode) async {
if (supportedLanguageCodes.contains(languageCode)) {
_languageCode = languageCode;
await _databaseService.saveLanguagePreference(languageCode);
localeNotifier.value = Locale(languageCode);
}
}
/// Load language preference from database
Future<void> loadLanguagePreference() async {
final savedLanguage = await _databaseService.loadLanguagePreference();
if (savedLanguage != null && supportedLanguageCodes.contains(savedLanguage)) {
_languageCode = savedLanguage;
localeNotifier.value = Locale(savedLanguage);
}
}
// Serialize persistence to avoid overlapping DB save/load cycles
bool _isPersistingJobs = false;
List<Job>? _pendingJobs; // holds the latest jobs to persist if calls overlap
// Jobs update notification (emitted once after DB was updated)
final StreamController<void> _jobsUpdatedController = StreamController<void>.broadcast();
Stream<void> get jobsUpdated => _jobsUpdatedController.stream;
/// The logged-in user's email (used as local identifier for chats)
String? get loggedInEmail => _loggedInEmail;
List<Job> get assignedJobs => List.unmodifiable(_assignedJobs);
void setLoggedInEmail(String email) {
_loggedInEmail = email;
}
Future<void> clearLogin() async {
_loggedInEmail = null;
_assignedJobs.clear();
// Clear database
await _databaseService.clearAllData();
// Notify listeners/UI that jobs were cleared
_jobsUpdatedController.add(null);
DartMQ().publish<void>(MQTopics.jobsUpdated, null);
}
bool get isLoggedIn => _loggedInEmail != null;
Future<void> setAssignedJobs(List<Job> jobs) async {
// Coalesce overlapping calls: if a persist is already running, remember only the latest
if (_isPersistingJobs) {
_pendingJobs = jobs;
return;
}
_isPersistingJobs = true;
try {
// Start with the initial batch to persist
var toPersist = jobs;
while (true) {
// Normalize first
final normalized = toPersist.map((j) => j.normalized()).toList();
// Persist normalized list to DB only (no UI notifications here)
await _databaseService.saveJobs(normalized);
// If another request came in during persistence, handle only the latest once
if (_pendingJobs != null) {
toPersist = _pendingJobs!;
_pendingJobs = null;
continue;
}
break;
}
// After DB is updated with the latest data, notify listeners once to refresh UI
_jobsUpdatedController.add(null);
// Also publish via dart_mq for app-wide decoupled messaging
DartMQ().publish<void>(MQTopics.jobsUpdated, null);
} finally {
_isPersistingJobs = false;
}
}
void addJob(Job job) {
if (!_assignedJobs.contains(job)) {
_assignedJobs.add(job);
}
}
void removeJob(String jobId) {
_assignedJobs.removeWhere((job) => job.id == jobId);
// Update database
_databaseService.saveJobs(_assignedJobs);
}
/// Delete a job by ID (called when server sends job_deleted event)
Future<void> deleteJob(String jobId) async {
_assignedJobs.removeWhere((job) => job.id == jobId);
// Delete from database
await _databaseService.deleteJob(jobId);
// Notify listeners
_jobsUpdatedController.add(null);
DartMQ().publish<void>(MQTopics.jobsUpdated, null);
}
/// Add a new job (called when server sends job_created event)
Future<void> addNewJob(Job job) async {
// Check if job already exists
if (_assignedJobs.any((j) => j.id == job.id)) {
return;
}
// Add to memory
_assignedJobs.insert(0, job);
// Persist to database
await _databaseService.saveOrUpdateJob(job);
// Notify listeners
_jobsUpdatedController.add(null);
DartMQ().publish<void>(MQTopics.jobsUpdated, null);
}
/// Load login state from saved credentials on app start
Future<void> loadLoginFromDatabase() async {
final credentials = await _databaseService.loadCredentials();
if (credentials != null) {
_loggedInEmail = credentials.email;
}
}
void updateJob(Job updatedJob) {
final index = _assignedJobs.indexWhere((job) => job.id == updatedJob.id);
if (index != -1) {
_assignedJobs[index] = updatedJob;
}
}
/// Refresh in-memory jobs from the database without emitting other notifications
Future<void> refreshJobsFromDatabase() async {
final jobs = await _databaseService.loadJobs();
_assignedJobs = jobs;
}
/// Persistently upsert a single job and refresh in-memory list
Future<void> upsertJob(Job job) async {
await _databaseService.saveOrUpdateJob(job);
final persisted = await _databaseService.loadJobs();
_assignedJobs = persisted.isNotEmpty ? persisted : _assignedJobs;
}
}

View File

@@ -0,0 +1,437 @@
import 'package:flutter/material.dart';
import 'l10n/app_localizations.dart';
import 'models/delivery_station.dart';
import 'models/job.dart';
import 'services/database_service.dart';
import 'task_view.dart';
import 'widgets/offline_banner.dart';
@visibleForTesting
Color? deliveryStationCardBackgroundColor(
DeliveryStation station,
Map<String, bool> taskStatuses,
) {
if (station.tasks.isEmpty) {
return null;
}
final isCompleted = station.tasks.every(
(task) => taskStatuses[task.id] ?? task.completed,
);
return isCompleted ? Colors.green[50] : null;
}
class CargoItemsView extends StatefulWidget {
final Job job;
const CargoItemsView({super.key, required this.job});
@override
State<CargoItemsView> createState() => _CargoItemsViewState();
}
class _CargoItemsViewState extends State<CargoItemsView> {
final DatabaseService _databaseService = DatabaseService();
Map<String, bool> _taskStatuses = const {};
@override
void initState() {
super.initState();
_loadLocalTaskStatuses();
}
Future<void> _loadLocalTaskStatuses() async {
final map = await _databaseService.loadAllTaskStatuses();
if (!mounted) return;
setState(() {
_taskStatuses = map;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.job.jobNumber),
backgroundColor: Colors.deepPurple[100],
leading: IconButton(
icon: const Icon(Icons.arrow_back),
onPressed: () {
Navigator.of(context).pop();
},
),
actions: [
IconButton(
icon: const Icon(Icons.chat),
onPressed: () {
Navigator.of(context).pushNamed('/chats');
},
tooltip: AppLocalizations.of(context).openChat,
),
],
),
body: Column(
children: [
OfflineBanner(),
// Main content area
Expanded(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Job summary card
Card(
margin: const EdgeInsets.only(bottom: 16),
elevation: 2,
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
widget.job.jobNumber.isNotEmpty
? widget.job.jobNumber
: widget.job.title,
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
if (widget.job.customerSelection.isNotEmpty) ...[
const SizedBox(height: 4),
Text(
widget.job.customerSelection,
style: TextStyle(
fontSize: 14,
color: Colors.grey[700],
fontWeight: FontWeight.w500,
),
),
],
const SizedBox(height: 8),
Row(
children: [
Icon(
Icons.arrow_upward,
size: 16,
color: Colors.green[600],
),
const SizedBox(width: 4),
Text(
widget.job.pickupCity,
style: TextStyle(
fontSize: 12,
color: Colors.grey[700],
),
),
const SizedBox(width: 8),
Icon(
Icons.arrow_forward,
size: 16,
color: Colors.grey[600],
),
const SizedBox(width: 8),
Icon(
Icons.arrow_downward,
size: 16,
color: Colors.blue[600],
),
const SizedBox(width: 4),
Text(
widget.job.deliveryCitiesDisplay.isNotEmpty
? widget.job.deliveryCitiesDisplay
: widget.job.deliveryCity,
style: TextStyle(
fontSize: 12,
color: Colors.grey[700],
),
),
],
),
],
),
),
),
// Delivery stations section header
Row(
children: [
Icon(
Icons.local_shipping_outlined,
size: 24,
color: Colors.deepPurple[600],
),
const SizedBox(width: 8),
Text(
'Lieferstationen (${_deliveryStations.length})',
style: const TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
],
),
const SizedBox(height: 16),
Expanded(child: _buildDeliveryStationsList()),
],
),
),
),
],
),
);
}
List<DeliveryStation> get _deliveryStations {
if (widget.job.deliveryStations.isNotEmpty) {
return widget.job.deliveryStations;
}
return [
DeliveryStation(
stationOrder: 0,
company: widget.job.deliveryCompany,
salutation: widget.job.deliverySalutation,
firstName: widget.job.deliveryFirstName,
lastName: widget.job.deliveryLastName,
phone: widget.job.deliveryPhone,
street: widget.job.deliveryStreet,
houseNumber: widget.job.deliveryHouseNumber,
addressAddition: widget.job.deliveryAddressAddition,
zip: widget.job.deliveryZip,
city: widget.job.deliveryCity,
deliveryDate: widget.job.deliveryDate,
deliveryTime: widget.job.deliveryTime,
tasks: widget.job.tasks,
),
];
}
Widget _buildDeliveryStationsList() {
if (_deliveryStations.isEmpty) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.local_shipping_outlined,
size: 64,
color: Colors.grey[400],
),
const SizedBox(height: 16),
Text(
'Keine Lieferstationen',
style: TextStyle(fontSize: 16, color: Colors.grey[600]),
),
const SizedBox(height: 8),
Text(
'Dieser Job enthält aktuell keine Lieferstationen.',
style: TextStyle(fontSize: 14, color: Colors.grey[500]),
textAlign: TextAlign.center,
),
],
),
);
}
return ListView.builder(
itemCount: _deliveryStations.length,
itemBuilder: (context, index) {
final station = _deliveryStations[index];
return _buildDeliveryStationCard(station);
},
);
}
Widget _buildDeliveryStationCard(DeliveryStation station) {
final backgroundColor = deliveryStationCardBackgroundColor(
station,
_taskStatuses,
);
final title =
station.displayName.isNotEmpty ? station.displayName : station.company;
final subtitle =
station.company.isNotEmpty && station.company != title
? station.company
: null;
final addressLines =
<String>[
[
station.street,
station.houseNumber,
].where((part) => part.trim().isNotEmpty).join(' '),
if (station.addressAddition.trim().isNotEmpty)
station.addressAddition,
[
station.zip,
station.city,
].where((part) => part.trim().isNotEmpty).join(' '),
].where((line) => line.trim().isNotEmpty).toList();
return Card(
color: backgroundColor,
margin: const EdgeInsets.symmetric(horizontal: 0, vertical: 8),
elevation: 2,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
side: BorderSide(color: Colors.grey[300]!, width: 1),
),
clipBehavior: Clip.antiAlias,
child: InkWell(
onTap: () async {
await Navigator.of(context).push(
MaterialPageRoute(
builder:
(context) => TaskView(
job: widget.job,
stationOrder: station.stationOrder,
stationTitle:
station.displayName.isNotEmpty
? station.displayName
: 'Station ${station.stationOrder + 1}',
),
),
);
await _loadLocalTaskStatuses();
},
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
padding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 4,
),
decoration: BoxDecoration(
color: Colors.deepPurple[100],
borderRadius: BorderRadius.circular(12),
),
child: Text(
'Station ${station.stationOrder + 1}',
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
color: Colors.deepPurple[700],
),
),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title.isNotEmpty ? title : 'Unbenannte Station',
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
if (subtitle != null) ...[
const SizedBox(height: 2),
Text(
subtitle,
style: TextStyle(
fontSize: 13,
color: Colors.grey[700],
),
),
],
],
),
),
],
),
const SizedBox(height: 12),
_buildDetailItem(
Icons.location_on_outlined,
AppLocalizations.of(context).location,
addressLines.join('\n'),
Colors.blue,
),
if (station.phone.trim().isNotEmpty) ...[
const SizedBox(height: 12),
_buildDetailItem(
Icons.phone_outlined,
'Telefon',
station.phone,
Colors.green,
),
],
if (station.deliveryDate.trim().isNotEmpty ||
station.deliveryTime.trim().isNotEmpty) ...[
const SizedBox(height: 12),
_buildDetailItem(
Icons.schedule,
AppLocalizations.of(context).delivery,
[
station.deliveryDate,
station.deliveryTime,
].where((part) => part.trim().isNotEmpty).join(' '),
Colors.orange,
),
],
const SizedBox(height: 12),
_buildDetailItem(
Icons.task_alt,
AppLocalizations.of(context).tasks,
'${station.tasks.length}',
Colors.deepPurple,
),
],
),
),
),
);
}
Widget _buildDetailItem(
IconData icon,
String label,
String value,
Color color,
) {
return Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: color.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(8),
border: Border.all(color: color.withValues(alpha: 0.3)),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(icon, size: 16, color: color.withValues(alpha: 0.8)),
const SizedBox(width: 4),
Text(
label,
style: TextStyle(
fontSize: 12,
color: Colors.grey[700],
fontWeight: FontWeight.w500,
),
),
],
),
const SizedBox(height: 4),
Text(
value,
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: Colors.grey[800],
),
),
],
),
);
}
}

View File

@@ -0,0 +1,692 @@
import 'dart:async';
import 'dart:convert';
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:image/image.dart' as img;
import 'l10n/app_localizations.dart';
import 'app_state.dart';
import 'models/chat.dart';
import 'models/chat_message.dart';
import 'services/chat_service.dart';
import 'services/websocket_service.dart';
import 'services/notification_service.dart';
import 'widgets/chat_photo_dialog.dart';
import 'widgets/offline_banner.dart';
class ChatDetailsView extends StatefulWidget {
final Chat chat;
const ChatDetailsView({super.key, required this.chat});
@override
State<ChatDetailsView> createState() => _ChatDetailsViewState();
}
class _PreparedImage {
const _PreparedImage({required this.base64DataUri, required this.bytes});
final String base64DataUri;
final Uint8List bytes;
}
class _ChatDetailsViewState extends State<ChatDetailsView> {
final TextEditingController _messageController = TextEditingController();
final ScrollController _scrollController = ScrollController();
late List<ChatMessage> _messages;
final WebSocketService _webSocketService = WebSocketService();
StreamSubscription<List<Chat>>? _chatsStreamSubscription;
String? _currentUserId;
late final String _conversationKey;
final ChatService _chatService = ChatService();
final Map<String, Uint8List> _imageCache = <String, Uint8List>{};
late Chat _activeChat;
static const int _maxDisplayMessages = 30;
@override
void initState() {
super.initState();
_activeChat = widget.chat;
_conversationKey = _activeChat.id;
NotificationService().activeConversationKey = _conversationKey;
_messages = _lastMessages(_activeChat.messages);
_currentUserId = AppState().loggedInEmail;
_chatsStreamSubscription = _chatService.chatsStream.listen(
_handleChatsUpdate,
);
_chatService.initialize().then((_) async {
_syncActiveChatFromService(replaceMessages: _messages.isEmpty);
final history = await _chatService.loadMessagesForChat(_conversationKey);
if (!mounted) return;
if (history.isNotEmpty) {
setState(() {
_imageCache.clear();
_messages = _lastMessages(history);
});
_scrollToBottom(immediate: true);
}
_syncActiveChatFromService();
await _chatService.markConversationRead(_conversationKey);
});
// Scroll to bottom after initial build is complete
_scrollToBottom(immediate: true);
}
@override
void dispose() {
if (NotificationService().activeConversationKey == _conversationKey) {
NotificationService().activeConversationKey = null;
}
_chatsStreamSubscription?.cancel();
_messageController.dispose();
_scrollController.dispose();
super.dispose();
}
void _scrollToBottom({bool immediate = false}) {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (!_scrollController.hasClients) {
return;
}
final target = _scrollController.position.maxScrollExtent;
if (immediate) {
_scrollController.jumpTo(target);
} else {
_scrollController.animateTo(
target,
duration: const Duration(milliseconds: 250),
curve: Curves.easeOut,
);
}
});
}
void _handleChatsUpdate(List<Chat> chats) {
if (!mounted) {
return;
}
final updated = _findChatById(chats);
if (updated == null) {
return;
}
final shouldReplace = _shouldReplaceMessages(updated);
setState(() {
_activeChat = updated;
if (shouldReplace) {
_imageCache.clear();
_messages = _lastMessages(updated.messages);
}
});
if (shouldReplace) {
_scrollToBottom();
unawaited(_chatService.markConversationRead(_conversationKey));
}
}
bool _shouldReplaceMessages(Chat chat) {
if (chat.messages.length != _messages.length) {
return true;
}
if (_messages.isEmpty) {
return false;
}
final currentLast = _messages.last;
final updatedLast = chat.messages.last;
return currentLast.id != updatedLast.id ||
currentLast.content != updatedLast.content ||
currentLast.contentType != updatedLast.contentType;
}
List<ChatMessage> _lastMessages(List<ChatMessage> messages) {
final sorted = List<ChatMessage>.from(messages)
..sort((a, b) => a.createdAt.compareTo(b.createdAt));
if (sorted.length > _maxDisplayMessages) {
return sorted.sublist(sorted.length - _maxDisplayMessages);
}
return sorted;
}
Chat? _findChatById(List<Chat> chats) {
for (final chat in chats) {
if (chat.id == _conversationKey) {
return chat;
}
}
return null;
}
void _syncActiveChatFromService({bool replaceMessages = false}) {
final updated = _findChatById(_chatService.currentChats);
if (updated == null || !mounted) {
return;
}
final shouldReplace = replaceMessages || _shouldReplaceMessages(updated);
setState(() {
_activeChat = updated;
if (shouldReplace) {
_imageCache.clear();
_messages = _lastMessages(updated.messages);
}
});
if (shouldReplace) {
_scrollToBottom();
unawaited(_chatService.markConversationRead(_conversationKey));
}
}
Future<void> _sendMessage() async {
final text = _messageController.text.trim();
if (text.isEmpty) {
return;
}
final sender = _currentUserId;
final receiver = _activeChat.receiver;
if (sender == null || sender.isEmpty) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(AppLocalizations.of(context).noSenderMessage),
),
);
}
return;
}
if (receiver == null || receiver.isEmpty) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(AppLocalizations.of(context).noRecipientMessage),
),
);
}
return;
}
final result = await _webSocketService.sendChatMessage(
sender: sender,
receiver: receiver,
content: text,
jobId: _activeChat.jobId,
jobNumber: _activeChat.jobNumber,
);
if (result == null) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(AppLocalizations.of(context).messageSendError),
),
);
}
return;
}
await _chatService.saveOutgoingMessage(result);
_syncActiveChatFromService();
_messageController.clear();
_scrollToBottom();
}
@override
Widget build(BuildContext context) {
final isJobChat = _activeChat.type == ChatType.jobSpecific;
return Scaffold(
appBar: AppBar(
title: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(_activeChat.title, style: const TextStyle(fontSize: 16)),
if (isJobChat && _activeChat.jobNumber != null)
Text(
'Job-Nr: ${_activeChat.jobNumber}',
style: TextStyle(
fontSize: 12,
color: Colors.grey[600],
fontWeight: FontWeight.normal,
),
),
],
),
backgroundColor: Colors.deepPurple[100],
actions: [
IconButton(
icon: Icon(isJobChat ? Icons.work : Icons.support_agent),
onPressed: () {
// Show chat info
_showChatInfo();
},
tooltip: AppLocalizations.of(context).chatInfo,
),
],
),
body: Column(
children: [
const OfflineBanner(),
// Messages list
Expanded(
child: Container(
decoration: BoxDecoration(color: Colors.grey[50]),
child: ListView.builder(
controller: _scrollController,
padding: const EdgeInsets.fromLTRB(8, 8, 8, 96),
itemCount: _messages.length,
itemBuilder: (context, index) {
final message = _messages[index];
return _buildMessageBubble(message);
},
),
),
),
// Message input
_buildMessageInput(),
],
),
);
}
Widget _buildMessageBubble(ChatMessage message) {
final isOwn = message.isOwn;
final isImage = message.contentType == ChatContentType.image;
return Padding(
padding: const EdgeInsets.symmetric(vertical: 2),
child: Row(
mainAxisAlignment:
isOwn ? MainAxisAlignment.end : MainAxisAlignment.start,
children: [
if (!isOwn) const SizedBox(width: 8),
Flexible(
child: Container(
constraints: BoxConstraints(
maxWidth: MediaQuery.of(context).size.width * 0.7,
),
margin: EdgeInsets.only(
left: isOwn ? 40 : 0,
right: isOwn ? 0 : 40,
),
padding: EdgeInsets.symmetric(
horizontal: isImage ? 6 : 12,
vertical: isImage ? 6 : 8,
),
decoration: BoxDecoration(
color: isOwn ? Colors.deepPurple[100] : Colors.white,
borderRadius: BorderRadius.only(
topLeft: const Radius.circular(12),
topRight: const Radius.circular(12),
bottomLeft: Radius.circular(isOwn ? 12 : 4),
bottomRight: Radius.circular(isOwn ? 4 : 12),
),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.1),
blurRadius: 2,
offset: const Offset(0, 1),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildMessageContent(message, isImage: isImage),
const SizedBox(height: 4),
Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.end,
children: [
Text(
_formatMessageTime(message.createdAt),
style: TextStyle(fontSize: 11, color: Colors.grey[600]),
),
if (isOwn) ...[
const SizedBox(width: 4),
Icon(
message.pendingSync
? Icons.schedule
: (message.read ? Icons.done_all : Icons.done),
size: 14,
color:
message.pendingSync
? Colors.orange[700]
: (message.read
? Colors.deepPurple[400]
: Colors.grey[600]),
),
],
],
),
],
),
),
),
if (isOwn) const SizedBox(width: 8),
],
),
);
}
Widget _buildMessageContent(ChatMessage message, {required bool isImage}) {
if (!isImage) {
return Text(
message.content,
style: TextStyle(fontSize: 15, color: Colors.grey[800]),
);
}
final imageBytes = _imageCache[message.id] ?? _decodeImageBytes(message);
if (imageBytes == null) {
return const Text(
'Bild konnte nicht geladen werden.',
style: TextStyle(fontSize: 15),
);
}
return GestureDetector(
onTap: () => _showImagePreview(imageBytes),
child: ClipRRect(
borderRadius: BorderRadius.circular(12),
child: Container(
color: Colors.black.withValues(alpha: 0.05),
constraints: const BoxConstraints(maxWidth: 260, minWidth: 140),
child: AspectRatio(
aspectRatio: 4 / 3,
child: Image.memory(imageBytes, fit: BoxFit.cover),
),
),
),
);
}
Uint8List? _decodeImageBytes(ChatMessage message) {
final rawContent = message.content.trim();
if (rawContent.isEmpty) {
return null;
}
final base64Payload =
rawContent.startsWith('data:')
? rawContent.substring(rawContent.indexOf(',') + 1)
: rawContent;
final normalized = base64Payload.replaceAll(RegExp(r'\s'), '');
try {
final bytes = base64Decode(normalized);
_imageCache[message.id] = bytes;
return bytes;
} catch (_) {
return null;
}
}
Future<void> _showImagePreview(Uint8List imageBytes) async {
if (!mounted) return;
await showDialog<void>(
context: context,
builder: (context) {
return Dialog(
insetPadding: const EdgeInsets.all(16),
backgroundColor: Colors.black,
child: InteractiveViewer(
child: Image.memory(imageBytes, fit: BoxFit.contain),
),
);
},
);
}
Widget _buildMessageInput() {
return Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.white,
border: Border(top: BorderSide(color: Colors.grey[300]!)),
),
child: SafeArea(
child: Row(
children: [
GestureDetector(
onTap: _handleAttachmentTap,
child: Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.grey[200],
borderRadius: BorderRadius.circular(20),
),
child: const Icon(
Icons.attach_file,
color: Colors.black87,
size: 20,
),
),
),
const SizedBox(width: 8),
Expanded(
child: Container(
decoration: BoxDecoration(
color: Colors.grey[100],
borderRadius: BorderRadius.circular(20),
),
child: TextField(
controller: _messageController,
decoration: InputDecoration(
hintText: AppLocalizations.of(context).typeMessage,
contentPadding: EdgeInsets.symmetric(
horizontal: 16,
vertical: 8,
),
border: InputBorder.none,
),
maxLines: null,
keyboardType: TextInputType.multiline,
textInputAction: TextInputAction.send,
onSubmitted: (_) => _sendMessage(),
),
),
),
const SizedBox(width: 8),
GestureDetector(
onTap: () {
_sendMessage();
},
child: Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.deepPurple,
borderRadius: BorderRadius.circular(20),
),
child: const Icon(Icons.send, color: Colors.white, size: 20),
),
),
],
),
),
);
}
Future<void> _handleAttachmentTap() async {
if (!mounted) return;
final Uint8List? photoBytes = await showDialog<Uint8List>(
context: context,
builder: (context) => const ChatPhotoDialog(),
);
if (photoBytes == null || photoBytes.isEmpty) {
return;
}
await _sendImageMessage(photoBytes);
}
Future<void> _sendImageMessage(Uint8List imageBytes) async {
final sender = _currentUserId;
final receiver = _activeChat.receiver;
if (sender == null || sender.isEmpty) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(AppLocalizations.of(context).noSenderMessage),
),
);
}
return;
}
if (receiver == null || receiver.isEmpty) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(AppLocalizations.of(context).noRecipientMessage),
),
);
}
return;
}
final prepared = await _prepareImagePayload(imageBytes);
if (prepared == null) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(AppLocalizations.of(context).photoProcessError),
),
);
}
return;
}
final result = await _webSocketService.sendChatMessage(
sender: sender,
receiver: receiver,
content: prepared.base64DataUri,
contentType: ChatContentType.image,
jobId: _activeChat.jobId,
jobNumber: _activeChat.jobNumber,
);
if (result == null) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(AppLocalizations.of(context).imageSendError)),
);
}
return;
}
await _chatService.saveOutgoingMessage(result);
_syncActiveChatFromService();
if (prepared.bytes.isNotEmpty) {
_imageCache[result.id] = prepared.bytes;
}
}
Future<_PreparedImage?> _prepareImagePayload(Uint8List originalBytes) async {
try {
final decoded = img.decodeImage(originalBytes);
if (decoded == null) {
return null;
}
final baked = img.bakeOrientation(decoded);
const maxDimension = 1280;
img.Image processed = baked;
if (baked.width > maxDimension || baked.height > maxDimension) {
final scale =
baked.width > baked.height
? maxDimension / baked.width
: maxDimension / baked.height;
final targetWidth = (baked.width * scale).round();
final targetHeight = (baked.height * scale).round();
processed = img.copyResize(
baked,
width: targetWidth,
height: targetHeight,
interpolation: img.Interpolation.average,
);
}
final encodedBytes = Uint8List.fromList(
img.encodeJpg(processed, quality: 85),
);
final base64Payload = base64Encode(encodedBytes);
final dataUri = 'data:image/jpeg;base64,$base64Payload';
return _PreparedImage(base64DataUri: dataUri, bytes: encodedBytes);
} catch (_) {
return null;
}
}
String _formatMessageTime(DateTime dateTime) {
final now = DateTime.now();
final today = DateTime(now.year, now.month, now.day);
final messageDate = DateTime(dateTime.year, dateTime.month, dateTime.day);
if (messageDate == today) {
// Today - show only time
return '${dateTime.hour.toString().padLeft(2, '0')}:${dateTime.minute.toString().padLeft(2, '0')}';
} else if (messageDate == today.subtract(const Duration(days: 1))) {
// Yesterday
return 'Gestern ${dateTime.hour.toString().padLeft(2, '0')}:${dateTime.minute.toString().padLeft(2, '0')}';
} else {
// Older - show date and time
return '${dateTime.day.toString().padLeft(2, '0')}.${dateTime.month.toString().padLeft(2, '0')} ${dateTime.hour.toString().padLeft(2, '0')}:${dateTime.minute.toString().padLeft(2, '0')}';
}
}
void _showChatInfo() {
final isJobChat = _activeChat.type == ChatType.jobSpecific;
showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: Text(_activeChat.title),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('${AppLocalizations.of(context).status}: ${isJobChat ? AppLocalizations.of(context).chatTypeJob : AppLocalizations.of(context).chatTypeGeneral}'),
const SizedBox(height: 8),
if (isJobChat && _activeChat.jobNumber != null) ...[
Text('${AppLocalizations.of(context).jobNumber}: ${_activeChat.jobNumber}'),
const SizedBox(height: 8),
],
Text('${AppLocalizations.of(context).messages}: ${_messages.length}'),
const SizedBox(height: 8),
Text(
'Erstellt: ${_formatMessageTime(_messages.isNotEmpty ? _messages.first.createdAt : DateTime.now())}',
),
],
),
actions: [
TextButton(
onPressed: () {
Navigator.of(context).pop();
},
child: Text(AppLocalizations.of(context).close),
),
],
);
},
);
}
}

186
app/lib/chats_view.dart Normal file
View File

@@ -0,0 +1,186 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'l10n/app_localizations.dart';
import 'models/chat.dart';
import 'services/chat_service.dart';
import 'widgets/offline_banner.dart';
class ChatsView extends StatefulWidget {
const ChatsView({super.key});
@override
State<ChatsView> createState() => _ChatsViewState();
}
class _ChatsViewState extends State<ChatsView> {
final ChatService _chatService = ChatService();
List<Chat> _chats = const [];
StreamSubscription<List<Chat>>? _chatSubscription;
bool _isInitializing = true;
@override
void initState() {
super.initState();
_initializeChats();
}
Future<void> _initializeChats() async {
await _chatService.initialize();
if (!mounted) return;
setState(() {
_chats = _chatService.currentChats;
_isInitializing = false;
});
_chatSubscription = _chatService.chatsStream.listen((chats) {
if (!mounted) return;
setState(() {
_chats = chats;
});
});
}
@override
void dispose() {
_chatSubscription?.cancel();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(AppLocalizations.of(context).chats),
backgroundColor: Colors.deepPurple[100],
),
body: Column(
children: [
const OfflineBanner(),
Expanded(child: _buildBody()),
],
),
);
}
Widget _buildBody() {
if (_isInitializing) {
return const Center(child: CircularProgressIndicator());
}
if (_chats.isEmpty) {
return const Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.chat_outlined, size: 64, color: Colors.grey),
SizedBox(height: 16),
Text(
'Keine Chats verfügbar',
style: TextStyle(fontSize: 16, color: Colors.grey),
),
],
),
);
}
return ListView.builder(
itemCount: _chats.length,
itemBuilder: (context, index) {
final chat = _chats[index];
return _buildChatTile(chat);
},
);
}
Widget _buildChatTile(Chat chat) {
final isJobChat = chat.type == ChatType.jobSpecific;
final hasMessages = chat.messages.isNotEmpty;
final previewText =
hasMessages ? chat.lastMessagePreview : 'Noch keine Nachrichten';
final timeLabel = hasMessages ? _formatTime(chat.lastMessageTime) : '--';
final jobId = chat.jobId?.trim();
final jobNumber = chat.jobNumber?.trim();
final showJobId = isJobChat && jobId != null && jobId.isNotEmpty;
return Card(
margin: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
child: ListTile(
leading: CircleAvatar(
backgroundColor: isJobChat ? Colors.blue[100] : Colors.green[100],
child: Icon(
isJobChat ? Icons.work : Icons.support_agent,
color: isJobChat ? Colors.blue[700] : Colors.green[700],
),
),
title: Text(() {
if (isJobChat) {
if (jobNumber != null && jobNumber.isNotEmpty) {
return 'Job $jobNumber';
}
if (showJobId) {
return 'Job $jobId';
}
}
return chat.type == ChatType.general
? 'Allgemeine Nachrichten'
: chat.title;
}(), style: const TextStyle(fontWeight: FontWeight.w600, fontSize: 16)),
subtitle: Text(
previewText,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(fontSize: 14, color: Colors.grey[700]),
),
trailing: Column(
crossAxisAlignment: CrossAxisAlignment.end,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
timeLabel,
style: TextStyle(fontSize: 12, color: Colors.grey[500]),
),
const SizedBox(height: 4),
Container(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
decoration: BoxDecoration(
color: isJobChat ? Colors.blue[50] : Colors.green[50],
borderRadius: BorderRadius.circular(10),
border: Border.all(
color: isJobChat ? Colors.blue[200]! : Colors.green[200]!,
),
),
child: Text(
isJobChat ? 'JOB' : 'ALLG',
style: TextStyle(
fontSize: 10,
fontWeight: FontWeight.w600,
color: isJobChat ? Colors.blue[700] : Colors.green[700],
),
),
),
],
),
onTap: () {
Navigator.of(context).pushNamed('/chat_details', arguments: chat);
},
),
);
}
String _formatTime(DateTime dateTime) {
final now = DateTime.now();
final difference = now.difference(dateTime);
if (difference.inDays > 0) {
return '${difference.inDays}T';
} else if (difference.inHours > 0) {
return '${difference.inHours}h';
} else if (difference.inMinutes > 0) {
return '${difference.inMinutes}m';
} else {
return 'jetzt';
}
}
}

View File

@@ -0,0 +1,32 @@
enum TranslationBackend { lmStudio, moonshot }
class TranslationConfig {
TranslationConfig._();
/// Das aktive Übersetzungs-Backend.
/// Hier umschalten zwischen LM Studio (lokal) und Moonshot AI (Cloud).
static const TranslationBackend activeBackend = TranslationBackend.moonshot;
// ---------------------------------------------------------------------------
// LM Studio (lokales Modell)
// ---------------------------------------------------------------------------
/// Basis-URL des LM Studio REST-Servers (lokales Netzwerk)
static const String lmStudioBaseUrl = 'http://lmstudio.appcreation.de';
/// Modellname LM Studio ignoriert diesen Wert normalerweise
static const String lmStudioModel = 'local-model';
// ---------------------------------------------------------------------------
// Moonshot AI (Kimi Cloud API)
// ---------------------------------------------------------------------------
/// Basis-URL der Moonshot AI API
static const String moonshotBaseUrl = 'https://api.moonshot.ai/v1';
/// API-Key für die Moonshot AI Authentifizierung
static const String moonshotApiKey = 'sk-EfHJfwCsxiZbOoBJ21OLWb9RUJQXSXAFIFGKnOedKke5JYZp';
/// Moonshot-Modell: moonshot-v1-8k (kurze Texte), moonshot-v1-32k, moonshot-v1-128k
static const String moonshotModel = 'moonshot-v1-8k';
}

View File

@@ -0,0 +1,48 @@
import 'package:objectbox/objectbox.dart';
@Entity()
class ChatMessageEntity {
@Id()
int id = 0;
@Unique()
String messageId;
@Index()
String conversationKey;
String content;
String contentType; // 'TEXT' or 'IMAGE'
@Property(type: PropertyType.date)
@Index()
DateTime createdAt;
String origin; // 'INCOMING' or 'OUTGOING'
String messageType; // 'NORMAL', 'JOB_ASSIGNMENT', etc.
String? jobId;
String? jobNumber;
bool read;
bool pendingSync;
ChatMessageEntity({
required this.messageId,
required this.conversationKey,
required this.content,
this.contentType = 'TEXT',
required this.createdAt,
required this.origin,
required this.messageType,
this.jobId,
this.jobNumber,
this.read = false,
this.pendingSync = false,
});
}

View File

@@ -0,0 +1,26 @@
import 'package:objectbox/objectbox.dart';
@Entity()
class JobEntity {
@Id()
int id = 0;
@Unique()
String jobId; // The original job ID from the Job model
String jobData; // JSON-encoded job data
@Property(type: PropertyType.date)
DateTime createdAt;
@Property(type: PropertyType.date)
DateTime updatedAt;
JobEntity({
required this.jobId,
required this.jobData,
required this.createdAt,
required this.updatedAt,
});
}

View File

@@ -0,0 +1,23 @@
import 'package:objectbox/objectbox.dart';
@Entity()
class PhotoEntity {
@Id()
int id = 0;
String taskId;
int photoIndex;
String data; // Base64-encoded photo data
@Property(type: PropertyType.date)
DateTime createdAt;
PhotoEntity({
required this.taskId,
required this.photoIndex,
required this.data,
required this.createdAt,
});
}

View File

@@ -0,0 +1,27 @@
import 'package:objectbox/objectbox.dart';
@Entity()
class QueuedMessageEntity {
@Id()
int id = 0;
@Unique()
String messageId;
String topic;
String payload; // JSON-encoded payload
@Property(type: PropertyType.date)
DateTime createdAt;
int retryCount;
QueuedMessageEntity({
required this.messageId,
required this.topic,
required this.payload,
required this.createdAt,
this.retryCount = 0,
});
}

View File

@@ -0,0 +1,30 @@
import 'package:objectbox/objectbox.dart';
@Entity()
class TaskStatusEntity {
@Id()
int id = 0;
@Unique()
String taskId;
bool completed;
@Property(type: PropertyType.date)
DateTime? completedAt;
@Property(type: PropertyType.date)
DateTime createdAt;
@Property(type: PropertyType.date)
DateTime updatedAt;
TaskStatusEntity({
required this.taskId,
required this.completed,
this.completedAt,
required this.createdAt,
required this.updatedAt,
});
}

View File

@@ -0,0 +1,26 @@
import 'package:objectbox/objectbox.dart';
@Entity()
class UserDataEntity {
@Id()
int id = 0;
@Unique()
String key;
String value;
@Property(type: PropertyType.date)
DateTime createdAt;
@Property(type: PropertyType.date)
DateTime updatedAt;
UserDataEntity({
required this.key,
required this.value,
required this.createdAt,
required this.updatedAt,
});
}

View File

@@ -0,0 +1,23 @@
import 'package:flutter/material.dart';
import 'navigation_observer.dart';
mixin RouteAwareState<T extends StatefulWidget> on State<T> implements RouteAware {
@override
void didPopNext() {
// When returning to this route, subclasses can override to refresh state.
}
void subscribeRouteAware() {
WidgetsBinding.instance.addPostFrameCallback((_) {
final route = ModalRoute.of(context);
if (route != null) {
routeObserver.subscribe(this, route);
}
});
}
void unsubscribeRouteAware() {
routeObserver.unsubscribe(this);
}
}

1866
app/lib/jobs_view.dart Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,267 @@
import 'package:flutter/material.dart';
import 'app_localizations_de.dart';
import 'app_localizations_en.dart';
import 'app_localizations_es.dart';
import 'app_localizations_fr.dart';
import 'app_localizations_pl.dart';
import 'app_localizations_ru.dart';
import 'app_localizations_tr.dart';
import 'app_localizations_et.dart';
import 'app_localizations_lv.dart';
import 'app_localizations_lt.dart';
/// Supported language codes
const List<String> supportedLanguageCodes = ['de', 'en', 'es', 'fr', 'pl', 'ru', 'tr', 'et', 'lv', 'lt'];
/// AppLocalizations provides localized strings for the app
abstract class AppLocalizations {
static AppLocalizations of(BuildContext context) {
return Localizations.of<AppLocalizations>(context, AppLocalizations) ?? AppLocalizationsDe();
}
static const LocalizationsDelegate<AppLocalizations> delegate = _AppLocalizationsDelegate();
/// Language name
String get languageName;
/// Flag emoji
String get flagEmoji;
// ==================== GENERAL ====================
String get appTitle;
String get ok;
String get cancel;
String get save;
String get delete;
String get close;
String get confirm;
String get error;
String get success;
String get loading;
String get refresh;
String get version;
String get unknown;
// ==================== NAVIGATION ====================
String get jobs;
String get availableJobs;
String get chats;
String get settings;
String get logout;
String get logoutConfirm;
String get logoutConfirmMessage;
String get openChat;
String get chatInfo;
String get routePlan;
// ==================== LOGIN ====================
String get welcomeBack;
String get loginSubtitle;
String get email;
String get password;
String get login;
String get loggingIn;
String get forgotPassword;
String get forgotPasswordMessage;
String get loginSuccess;
String get loginFailed;
String get connectionFailed;
String get connectionTimeout;
String get connecting;
String get connectionError;
String get loginError;
// ==================== JOBS ====================
String get noJobsAssigned;
String get noJobsMessage;
String get pullToRefresh;
String get newLabel;
String get tasksToComplete;
String get pickup;
String get delivery;
String get created;
String get status;
String get priority;
String get dueDate;
String get location;
String get description;
String get cargo;
String get quantity;
String get weight;
String get dimensions;
String get jobDeleted;
String get jobDeleteError;
String get jobCompleted;
String get from;
String get to;
String get jobsUpdated;
String get connectionRestored;
String get connectionLost;
String get offline;
String get deleteJob;
String get jobRemoved;
String get newJobReceived;
// ==================== TASKS ====================
String get tasks;
String get noTasks;
String get noTasksMessage;
String get taskOrder;
String get confirmationRequired;
String get confirmationDescription;
String get checklist;
String get checklistDescription;
String get completeTask;
String get completeTaskConfirm;
String get completeTaskNote;
String get taskCompleted;
String get comment;
String get commentRequired;
String get enterComment;
String get commentDescription;
String get finish;
String get signature;
String get signatureCapture;
String get signatureRequired;
String get clear;
String get signatureError;
String get signatureInstruction;
String get photoCapture;
String get requiredPhotos;
String get photosTaken;
String get photos;
String get takePhoto;
String get selectFromLibrary;
String get retakePhoto;
String get photoRequired;
String get minPhotos;
String get maxPhotos;
String get photoError;
String get deletePhoto;
String get deletePhotoConfirm;
String get barcode;
String get barcodeScan;
String get scanBarcode;
String get barcodeRequired;
String get minBarcodes;
String get maxBarcodes;
String get scanned;
String get scannedBarcodes;
String get barcodesRequired;
String get enterBarcode;
String get barcodeEnterDescription;
String barcodeNumberRequired(int number);
String barcodeNumberOptional(int number);
String get barcodeError;
String get cameraError;
String get cameraNotReady;
String get cameraNotAvailable;
String get cameraNotSupportedMessage;
String get cameraNotSupportedOnPlatform;
String get maxPhotosReached;
String get cameraReadyNoPreview;
String get cameraLoading;
String get cameraInitializing;
String get cameraLoadingMessage;
String get addPhotos;
String get addPhotosInstruction;
String get photoOf;
// ==================== CHAT ====================
String get typeMessage;
String get send;
String get noSender;
String get noSenderMessage;
String get noRecipient;
String get noRecipientMessage;
String get messageSendError;
String get photoSendError;
String get photoProcessError;
String get imageSendError;
String get chatTypeJob;
String get chatTypeGeneral;
String get jobNumber;
String get messages;
String get selectPhoto;
String get unreadMessages;
// ==================== CARGO ====================
String get cargoDetails;
String get itemName;
String get itemNumber;
String get item;
String get weightUnit;
String get dimensionUnit;
String get noCargoItems;
String get noCargoItemsMessage;
String get article;
// ==================== TASK TYPES ====================
String get takePhotos;
String get photosCount;
String get checklistPoints;
String get signatureRequiredText;
String get scanBarcodes;
String get barcodeCount;
String get commentOptional;
String get genericTask;
String get complete;
String get abort;
String get optional;
String get skipTask;
// ==================== SETTINGS ====================
String get language;
String get languageChanged;
String get appInfo;
// ==================== STATUS ====================
String get statusCreated;
String get statusAssigned;
String get statusInProgress;
String get statusCompleted;
String get priorityLow;
String get priorityMedium;
String get priorityHigh;
String get priorityUrgent;
}
class _AppLocalizationsDelegate extends LocalizationsDelegate<AppLocalizations> {
const _AppLocalizationsDelegate();
@override
bool isSupported(Locale locale) {
return supportedLanguageCodes.contains(locale.languageCode);
}
@override
Future<AppLocalizations> load(Locale locale) async {
switch (locale.languageCode) {
case 'de':
return AppLocalizationsDe();
case 'en':
return AppLocalizationsEn();
case 'es':
return AppLocalizationsEs();
case 'fr':
return AppLocalizationsFr();
case 'pl':
return AppLocalizationsPl();
case 'ru':
return AppLocalizationsRu();
case 'tr':
return AppLocalizationsTr();
case 'et':
return AppLocalizationsEt();
case 'lv':
return AppLocalizationsLv();
case 'lt':
return AppLocalizationsLt();
default:
return AppLocalizationsDe();
}
}
@override
bool shouldReload(LocalizationsDelegate<AppLocalizations> old) => false;
}

View File

@@ -0,0 +1,551 @@
import 'app_localizations.dart';
class AppLocalizationsDe extends AppLocalizations {
@override
String get languageName => 'Deutsch';
@override
String get flagEmoji => '🇩🇪';
// ==================== GENERAL ====================
@override
String get appTitle => 'VotianLT App';
@override
String get ok => 'OK';
@override
String get cancel => 'Abbrechen';
@override
String get save => 'Speichern';
@override
String get delete => 'Löschen';
@override
String get close => 'Schließen';
@override
String get confirm => 'Bestätigen';
@override
String get error => 'Fehler';
@override
String get success => 'Erfolg';
@override
String get loading => 'Laden...';
@override
String get refresh => 'Aktualisieren';
@override
String get version => 'Version';
@override
String get unknown => 'Unbekannt';
// ==================== NAVIGATION ====================
@override
String get jobs => 'Jobs';
@override
String get availableJobs => 'Verfügbare Jobs';
@override
String get chats => 'Chats';
@override
String get settings => 'Einstellungen';
@override
String get logout => 'Abmelden';
@override
String get logoutConfirm => 'Abmelden';
@override
String get logoutConfirmMessage => 'Möchten Sie sich wirklich abmelden?';
@override
String get openChat => 'Chat öffnen';
@override
String get chatInfo => 'Chat-Info';
@override
String get routePlan => 'Route planen';
// ==================== LOGIN ====================
@override
String get welcomeBack => 'Willkommen zurück';
@override
String get loginSubtitle => 'Melden Sie sich in Ihrem Konto an';
@override
String get email => 'E-Mail';
@override
String get password => 'Passwort';
@override
String get login => 'Anmelden';
@override
String get loggingIn => 'Verbinden…';
@override
String get forgotPassword => 'Passwort vergessen?';
@override
String get forgotPasswordMessage => 'Passwort vergessen Funktion noch nicht implementiert';
@override
String get loginSuccess => 'Erfolgreich abgemeldet';
@override
String get loginFailed => 'Anmeldung fehlgeschlagen';
@override
String get connectionFailed => 'Verbindung zum Server fehlgeschlagen (Timeout).';
@override
String get connectionTimeout => 'Verbindung zum Server fehlgeschlagen (Timeout).';
@override
String get connecting => 'Verbindung zum Server wird hergestellt...';
@override
String get connectionError => 'Verbindungsfehler';
@override
String get loginError => 'Fehler bei der Anmeldung';
// ==================== JOBS ====================
@override
String get noJobsAssigned => 'Keine Jobs zugewiesen';
@override
String get noJobsMessage => 'Ihre zugewiesenen Jobs werden hier angezeigt.';
@override
String get pullToRefresh => 'Nach unten ziehen zum Aktualisieren';
@override
String get newLabel => 'NEU';
@override
String get tasksToComplete => 'Zu erledigende Aufgaben';
@override
String get pickup => 'Abholung';
@override
String get delivery => 'Zustellung';
@override
String get created => 'Erstellt';
@override
String get status => 'Status';
@override
String get priority => 'Priorität';
@override
String get dueDate => 'Fälligkeitsdatum';
@override
String get location => 'Ort';
@override
String get description => 'Beschreibung';
@override
String get cargo => 'Fracht';
@override
String get quantity => 'Anzahl';
@override
String get weight => 'Gewicht';
@override
String get dimensions => 'Abmessungen';
@override
String get jobDeleted => 'Job gelöscht';
@override
String get jobDeleteError => 'Fehler beim Löschen des Jobs';
@override
String get jobCompleted => 'Job abgeschlossen';
@override
String get from => 'Von';
@override
String get to => 'nach';
@override
String get jobsUpdated => 'Jobs aktualisiert';
@override
String get connectionRestored => 'Verbindung wiederhergestellt. Lade Jobs...';
@override
String get connectionLost => 'Verbindung verloren. Offline.';
@override
String get offline => 'Offline';
@override
String get deleteJob => 'Job löschen';
@override
String get jobRemoved => 'wurde entfernt';
@override
String get newJobReceived => 'Neuer Job erhalten';
// ==================== TASKS ====================
@override
String get tasks => 'Aufgaben';
@override
String get noTasks => 'Keine Aufgaben';
@override
String get noTasksMessage => 'Für diesen Job sind keine Aufgaben definiert.';
@override
String get taskOrder => 'Reihenfolge';
@override
String get confirmationRequired => 'Bestätigung erforderlich';
@override
String get confirmationDescription => 'Klicken Sie auf den Button um die Aufgabe zu erledigen.';
@override
String get checklist => 'Checkliste';
@override
String get checklistDescription => 'Bitte alle Punkte abhaken:';
@override
String get completeTask => 'Aufgabe abschließen';
@override
String get completeTaskConfirm => 'Möchten Sie diese Aufgabe als erledigt markieren?';
@override
String get completeTaskNote => 'Notiz (optional)';
@override
String get taskCompleted => 'Aufgabe erledigt';
@override
String get comment => 'Kommentar';
@override
String get commentRequired => 'Kommentar (erforderlich)';
@override
String get enterComment => 'Kommentar eingeben';
@override
String get commentDescription => 'Bitte geben Sie einen Kommentar ein:';
@override
String get finish => 'Fertig';
@override
String get signature => 'Unterschrift';
@override
String get signatureCapture => 'Unterschrift erfassen';
@override
String get signatureRequired => 'Bitte eine Unterschrift erfassen.';
@override
String get clear => 'Leeren';
@override
String get signatureError => 'Fehler beim Speichern der Unterschrift';
@override
String get signatureInstruction => 'Bitte unterschreiben Sie im Feld unten (Maus oder Finger).';
@override
String get photoCapture => 'Fotos aufnehmen';
@override
String get requiredPhotos => 'Benötigte Fotos';
@override
String get photosTaken => 'Aufgenommen';
@override
String get photos => 'Fotos';
@override
String get takePhoto => 'Foto aufnehmen';
@override
String get selectFromLibrary => 'Aus Bibliothek wählen';
@override
String get retakePhoto => 'Neu aufnehmen';
@override
String get photoRequired => 'Foto erforderlich';
@override
String get minPhotos => 'Mindestens';
@override
String get maxPhotos => 'Maximal';
@override
String get photoError => 'Fehler beim Aufnehmen des Fotos';
@override
String get deletePhoto => 'Foto löschen';
@override
String get deletePhotoConfirm => 'Möchten Sie dieses Foto wirklich löschen?';
@override
String get barcode => 'Barcode';
@override
String get barcodeScan => 'Barcode scannen';
@override
String get scanBarcode => 'Barcode scannen';
@override
String get barcodeRequired => 'Barcode erforderlich';
@override
String get minBarcodes => 'Mindestens';
@override
String get maxBarcodes => 'Maximal';
@override
String get scanned => 'Gescannt';
@override
String get scannedBarcodes => 'Gescannte Barcodes';
@override
String get barcodesRequired => 'Barcodes erforderlich';
@override
String get enterBarcode => 'Barcode eingeben';
@override
String get barcodeEnterDescription => 'Bitte geben Sie die Barcodes ein:';
@override
String barcodeNumberRequired(int number) => 'Barcode $number (erforderlich)';
@override
String barcodeNumberOptional(int number) => 'Barcode $number (optional)';
@override
String get barcodeError => 'Fehler beim Scannen des Barcodes';
@override
String get cameraError => 'Fehler beim Initialisieren der Kamera';
@override
String get cameraNotReady => 'Kamera ist nicht bereit oder nicht verfügbar';
@override
String get cameraNotAvailable => 'Kamera nicht verfügbar';
@override
String get cameraNotSupportedMessage => 'Auf dieser Plattform wird die Kamera nicht unterstützt.';
@override
String get cameraNotSupportedOnPlatform => 'Nicht unterstützt auf dieser Plattform';
@override
String get maxPhotosReached => 'Maximum erreicht';
@override
String get cameraReadyNoPreview => 'Kamera bereit (ohne Vorschau)';
@override
String get cameraLoading => 'Kamera lädt...';
@override
String get cameraInitializing => 'Kamera wird initialisiert...';
@override
String get cameraLoadingMessage => 'Bitte warten Sie, während die Kamera geladen wird';
@override
String get addPhotos => 'Fotos hinzufügen';
@override
String get addPhotosInstruction => 'Verwenden Sie den Button „Foto auswählen", um Bilder von Ihrer Kamera oder Festplatte hinzuzufügen.';
@override
String get photoOf => 'von';
// ==================== CHAT ====================
@override
String get typeMessage => 'Nachricht eingeben...';
@override
String get send => 'Senden';
@override
String get noSender => 'Kein Absender verfügbar';
@override
String get noSenderMessage => 'Kein Absender verfügbar. Bitte erneut anmelden.';
@override
String get noRecipient => 'Kein Empfänger konfiguriert';
@override
String get noRecipientMessage => 'Kein Empfänger für diesen Chat konfiguriert.';
@override
String get messageSendError => 'Nachricht konnte nicht gesendet werden.';
@override
String get photoSendError => 'Foto konnte nicht gesendet werden.';
@override
String get photoProcessError => 'Foto konnte nicht verarbeitet werden.';
@override
String get imageSendError => 'Bild konnte nicht gesendet werden.';
@override
String get chatTypeJob => 'Job-spezifisch';
@override
String get chatTypeGeneral => 'Allgemein';
@override
String get jobNumber => 'Job-Nummer';
@override
String get messages => 'Nachrichten';
@override
String get selectPhoto => 'Foto auswählen';
@override
String get unreadMessages => 'Ungelesene Nachrichten';
// ==================== SETTINGS ====================
@override
String get language => 'Sprache';
@override
String get languageChanged => 'Sprache geändert zu';
@override
String get appInfo => 'APP-INFO';
// ==================== CARGO ====================
@override
String get cargoDetails => 'Frachtdetails';
@override
String get itemName => 'Bezeichnung';
@override
String get itemNumber => 'Positions-Nr.';
@override
String get item => 'Position';
@override
String get weightUnit => 'kg';
@override
String get dimensionUnit => 'cm';
@override
String get noCargoItems => 'Keine Frachtgüter';
@override
String get noCargoItemsMessage => 'Für diesen Job sind keine Frachtgüter definiert.';
@override
String get article => 'Artikel';
// ==================== TASK TYPES ====================
@override
String get takePhotos => 'Fotos aufnehmen';
@override
String get photosCount => 'Fotos';
@override
String get checklistPoints => 'Punkte';
@override
String get signatureRequiredText => 'Unterschrift erforderlich';
@override
String get scanBarcodes => 'Barcode scannen';
@override
String get barcodeCount => 'Codes';
@override
String get commentOptional => 'Kommentar';
@override
String get genericTask => 'Allgemeine Aufgabe';
@override
String get complete => 'Abschließen';
@override
String get abort => 'Abbrechen';
@override
String get optional => 'Optional';
@override
String get skipTask => 'Überspringen';
// ==================== STATUS ====================
@override
String get statusCreated => 'Erstellt';
@override
String get statusAssigned => 'Zugewiesen';
@override
String get statusInProgress => 'In Bearbeitung';
@override
String get statusCompleted => 'Abgeschlossen';
@override
String get priorityLow => 'Niedrig';
@override
String get priorityMedium => 'Mittel';
@override
String get priorityHigh => 'Hoch';
@override
String get priorityUrgent => 'Dringend';
}

View File

@@ -0,0 +1,551 @@
import 'app_localizations.dart';
class AppLocalizationsEn extends AppLocalizations {
@override
String get languageName => 'English';
@override
String get flagEmoji => '🇬🇧';
// ==================== GENERAL ====================
@override
String get appTitle => 'VotianLT App';
@override
String get ok => 'OK';
@override
String get cancel => 'Cancel';
@override
String get save => 'Save';
@override
String get delete => 'Delete';
@override
String get close => 'Close';
@override
String get confirm => 'Confirm';
@override
String get error => 'Error';
@override
String get success => 'Success';
@override
String get loading => 'Loading...';
@override
String get refresh => 'Refresh';
@override
String get version => 'Version';
@override
String get unknown => 'Unknown';
// ==================== NAVIGATION ====================
@override
String get jobs => 'Jobs';
@override
String get availableJobs => 'Available Jobs';
@override
String get chats => 'Chats';
@override
String get settings => 'Settings';
@override
String get logout => 'Logout';
@override
String get logoutConfirm => 'Logout';
@override
String get logoutConfirmMessage => 'Do you really want to logout?';
@override
String get openChat => 'Open Chat';
@override
String get chatInfo => 'Chat Info';
@override
String get routePlan => 'Plan Route';
// ==================== LOGIN ====================
@override
String get welcomeBack => 'Welcome Back';
@override
String get loginSubtitle => 'Sign in to your account';
@override
String get email => 'Email';
@override
String get password => 'Password';
@override
String get login => 'Login';
@override
String get loggingIn => 'Connecting...';
@override
String get forgotPassword => 'Forgot Password?';
@override
String get forgotPasswordMessage => 'Forgot password feature not yet implemented';
@override
String get loginSuccess => 'Successfully logged out';
@override
String get loginFailed => 'Login failed';
@override
String get connectionFailed => 'Connection to server failed (Timeout).';
@override
String get connectionTimeout => 'Connection to server failed (Timeout).';
@override
String get connecting => 'Connecting to server...';
@override
String get connectionError => 'Connection error';
@override
String get loginError => 'Error during login';
// ==================== JOBS ====================
@override
String get noJobsAssigned => 'No Jobs Assigned';
@override
String get noJobsMessage => 'Your assigned jobs will be displayed here.';
@override
String get pullToRefresh => 'Pull down to refresh';
@override
String get newLabel => 'NEW';
@override
String get tasksToComplete => 'Tasks to Complete';
@override
String get pickup => 'Pickup';
@override
String get delivery => 'Delivery';
@override
String get created => 'Created';
@override
String get status => 'Status';
@override
String get priority => 'Priority';
@override
String get dueDate => 'Due Date';
@override
String get location => 'Location';
@override
String get description => 'Description';
@override
String get cargo => 'Cargo';
@override
String get quantity => 'Quantity';
@override
String get weight => 'Weight';
@override
String get dimensions => 'Dimensions';
@override
String get jobDeleted => 'Job deleted';
@override
String get jobDeleteError => 'Error deleting job';
@override
String get jobCompleted => 'Job completed';
@override
String get from => 'From';
@override
String get to => 'to';
@override
String get jobsUpdated => 'Jobs updated';
@override
String get connectionRestored => 'Connection restored. Loading jobs...';
@override
String get connectionLost => 'Connection lost. Offline.';
@override
String get offline => 'Offline';
@override
String get deleteJob => 'Delete Job';
@override
String get jobRemoved => 'was removed';
@override
String get newJobReceived => 'New job received';
// ==================== TASKS ====================
@override
String get tasks => 'Tasks';
@override
String get noTasks => 'No Tasks';
@override
String get noTasksMessage => 'No tasks defined for this job.';
@override
String get taskOrder => 'Order';
@override
String get confirmationRequired => 'Confirmation Required';
@override
String get confirmationDescription => 'Click the button to complete the task.';
@override
String get checklist => 'Checklist';
@override
String get checklistDescription => 'Please check all items:';
@override
String get completeTask => 'Complete Task';
@override
String get completeTaskConfirm => 'Do you want to mark this task as completed?';
@override
String get completeTaskNote => 'Note (optional)';
@override
String get taskCompleted => 'Task completed';
@override
String get comment => 'Comment';
@override
String get commentRequired => 'Comment (required)';
@override
String get enterComment => 'Enter Comment';
@override
String get commentDescription => 'Please enter a comment:';
@override
String get finish => 'Finish';
@override
String get signature => 'Signature';
@override
String get signatureCapture => 'Capture Signature';
@override
String get signatureRequired => 'Please capture a signature.';
@override
String get clear => 'Clear';
@override
String get signatureError => 'Error saving signature';
@override
String get signatureInstruction => 'Please sign in the field below (mouse or finger).';
@override
String get photoCapture => 'Take Photos';
@override
String get requiredPhotos => 'Required Photos';
@override
String get photosTaken => 'Taken';
@override
String get photos => 'Photos';
@override
String get takePhoto => 'Take Photo';
@override
String get selectFromLibrary => 'Select from Library';
@override
String get retakePhoto => 'Retake';
@override
String get photoRequired => 'Photo required';
@override
String get minPhotos => 'At least';
@override
String get maxPhotos => 'Maximum';
@override
String get photoError => 'Error taking photo';
@override
String get deletePhoto => 'Delete Photo';
@override
String get deletePhotoConfirm => 'Do you really want to delete this photo?';
@override
String get barcode => 'Barcode';
@override
String get barcodeScan => 'Scan Barcode';
@override
String get scanBarcode => 'Scan Barcode';
@override
String get barcodeRequired => 'Barcode required';
@override
String get minBarcodes => 'At least';
@override
String get maxBarcodes => 'Maximum';
@override
String get scanned => 'Scanned';
@override
String get scannedBarcodes => 'Scanned Barcodes';
@override
String get barcodesRequired => 'Barcodes Required';
@override
String get enterBarcode => 'Enter Barcode';
@override
String get barcodeEnterDescription => 'Please enter the barcodes:';
@override
String barcodeNumberRequired(int number) => 'Barcode $number (required)';
@override
String barcodeNumberOptional(int number) => 'Barcode $number (optional)';
@override
String get barcodeError => 'Error scanning barcode';
@override
String get cameraError => 'Error initializing camera';
@override
String get cameraNotReady => 'Camera is not ready or not available';
@override
String get cameraNotAvailable => 'Camera not available';
@override
String get cameraNotSupportedMessage => 'The camera is not supported on this platform.';
@override
String get cameraNotSupportedOnPlatform => 'Not supported on this platform';
@override
String get maxPhotosReached => 'Maximum reached';
@override
String get cameraReadyNoPreview => 'Camera ready (no preview)';
@override
String get cameraLoading => 'Camera loading...';
@override
String get cameraInitializing => 'Initializing camera...';
@override
String get cameraLoadingMessage => 'Please wait while the camera is loading';
@override
String get addPhotos => 'Add photos';
@override
String get addPhotosInstruction => 'Use the "Select photo" button to add images from your camera or hard drive.';
@override
String get photoOf => 'of';
// ==================== CHAT ====================
@override
String get typeMessage => 'Type a message...';
@override
String get send => 'Send';
@override
String get noSender => 'No sender available';
@override
String get noSenderMessage => 'No sender available. Please login again.';
@override
String get noRecipient => 'No recipient configured';
@override
String get noRecipientMessage => 'No recipient configured for this chat.';
@override
String get messageSendError => 'Message could not be sent.';
@override
String get photoSendError => 'Photo could not be sent.';
@override
String get photoProcessError => 'Photo could not be processed.';
@override
String get imageSendError => 'Image could not be sent.';
@override
String get chatTypeJob => 'Job-specific';
@override
String get chatTypeGeneral => 'General';
@override
String get jobNumber => 'Job Number';
@override
String get messages => 'Messages';
@override
String get selectPhoto => 'Select Photo';
@override
String get unreadMessages => 'Unread Messages';
// ==================== CARGO ====================
@override
String get cargoDetails => 'Cargo Details';
@override
String get itemName => 'Description';
@override
String get itemNumber => 'Item Number';
@override
String get item => 'Item';
@override
String get weightUnit => 'kg';
@override
String get dimensionUnit => 'cm';
@override
String get noCargoItems => 'No Cargo Items';
@override
String get noCargoItemsMessage => 'No cargo items defined for this job.';
@override
String get article => 'Article';
// ==================== TASK TYPES ====================
@override
String get takePhotos => 'Take Photos';
@override
String get photosCount => 'Photos';
@override
String get checklistPoints => 'Points';
@override
String get signatureRequiredText => 'Signature Required';
@override
String get scanBarcodes => 'Scan Barcodes';
@override
String get barcodeCount => 'Codes';
@override
String get commentOptional => 'Comment';
@override
String get genericTask => 'Generic Task';
@override
String get complete => 'Complete';
@override
String get abort => 'Cancel';
@override
String get optional => 'Optional';
@override
String get skipTask => 'Skip';
// ==================== SETTINGS ====================
@override
String get language => 'Language';
@override
String get languageChanged => 'Language changed to';
@override
String get appInfo => 'APP INFO';
// ==================== STATUS ====================
@override
String get statusCreated => 'Created';
@override
String get statusAssigned => 'Assigned';
@override
String get statusInProgress => 'In Progress';
@override
String get statusCompleted => 'Completed';
@override
String get priorityLow => 'Low';
@override
String get priorityMedium => 'Medium';
@override
String get priorityHigh => 'High';
@override
String get priorityUrgent => 'Urgent';
}

View File

@@ -0,0 +1,385 @@
import 'app_localizations.dart';
class AppLocalizationsEs extends AppLocalizations {
@override
String get languageName => 'Español';
@override
String get flagEmoji => '🇪🇸';
// ==================== GENERAL ====================
@override
String get appTitle => 'VotianLT App';
@override
String get ok => 'OK';
@override
String get cancel => 'Cancelar';
@override
String get save => 'Guardar';
@override
String get delete => 'Eliminar';
@override
String get close => 'Cerrar';
@override
String get confirm => 'Confirmar';
@override
String get error => 'Error';
@override
String get success => 'Éxito';
@override
String get loading => 'Cargando...';
@override
String get refresh => 'Actualizar';
@override
String get version => 'Versión';
@override
String get unknown => 'Desconocido';
// ==================== NAVIGATION ====================
@override
String get jobs => 'Trabajos';
@override
String get availableJobs => 'Trabajos Disponibles';
@override
String get chats => 'Chats';
@override
String get settings => 'Ajustes';
@override
String get logout => 'Cerrar sesión';
@override
String get logoutConfirm => 'Cerrar sesión';
@override
String get logoutConfirmMessage => '¿Realmente desea cerrar sesión?';
@override
String get openChat => 'Abrir chat';
@override
String get chatInfo => 'Info del chat';
@override
String get routePlan => 'Planificar ruta';
// ==================== LOGIN ====================
@override
String get welcomeBack => 'Bienvenido de nuevo';
@override
String get loginSubtitle => 'Inicie sesión en su cuenta';
@override
String get email => 'Correo electrónico';
@override
String get password => 'Contraseña';
@override
String get login => 'Iniciar sesión';
@override
String get loggingIn => 'Conectando...';
@override
String get forgotPassword => '¿Olvidó su contraseña?';
@override
String get forgotPasswordMessage => 'Función de contraseña olvidada aún no implementada';
@override
String get loginSuccess => 'Sesión cerrada correctamente';
@override
String get loginFailed => 'Error al iniciar sesión';
@override
String get connectionFailed => 'Error de conexión al servidor (Tiempo agotado).';
@override
String get connectionTimeout => 'Error de conexión al servidor (Tiempo agotado).';
@override
String get connecting => 'Conectando al servidor...';
@override
String get connectionError => 'Error de conexión';
@override
String get loginError => 'Error durante el inicio de sesión';
// ==================== JOBS ====================
@override
String get noJobsAssigned => 'No hay trabajos asignados';
@override
String get noJobsMessage => 'Sus trabajos asignados se mostrarán aquí.';
@override
String get pullToRefresh => 'Deslice hacia abajo para actualizar';
@override
String get newLabel => 'NUEVO';
@override
String get tasksToComplete => 'Tareas por completar';
@override
String get pickup => 'Recogida';
@override
String get delivery => 'Entrega';
@override
String get created => 'Creado';
@override
String get status => 'Estado';
@override
String get priority => 'Prioridad';
@override
String get dueDate => 'Fecha de vencimiento';
@override
String get location => 'Ubicación';
@override
String get description => 'Descripción';
@override
String get cargo => 'Carga';
@override
String get quantity => 'Cantidad';
@override
String get weight => 'Peso';
@override
String get dimensions => 'Dimensiones';
@override
String get jobDeleted => 'Trabajo eliminado';
@override
String get jobDeleteError => 'Error al eliminar el trabajo';
@override
String get jobCompleted => 'Trabajo completado';
@override
String get from => 'De';
@override
String get to => 'a';
@override
String get jobsUpdated => 'Trabajos actualizados';
@override
String get connectionRestored => 'Conexión restaurada. Cargando trabajos...';
@override
String get connectionLost => 'Conexión perdida. Sin conexión.';
@override
String get offline => 'Sin conexión';
@override
String get deleteJob => 'Eliminar trabajo';
@override
String get jobRemoved => 'fue eliminado';
@override
String get newJobReceived => 'Nuevo trabajo recibido';
// ==================== TASKS ====================
@override
String get tasks => 'Tareas';
@override
String get noTasks => 'Sin tareas';
@override
String get noTasksMessage => 'No hay tareas definidas para este trabajo.';
@override
String get taskOrder => 'Orden';
@override
String get confirmationRequired => 'Confirmación requerida';
@override
String get confirmationDescription => 'Haga clic en el botón para completar la tarea.';
@override
String get checklist => 'Lista de verificación';
@override
String get checklistDescription => 'Por favor marque todos los elementos:';
@override
String get completeTask => 'Completar tarea';
@override
String get completeTaskConfirm => '¿Desea marcar esta tarea como completada?';
@override
String get completeTaskNote => 'Nota (opcional)';
@override
String get taskCompleted => 'Tarea completada';
@override
String get comment => 'Comentario';
@override
String get commentRequired => 'Comentario (requerido)';
@override
String get enterComment => 'Ingrese comentario';
@override
String get commentDescription => 'Por favor ingrese un comentario:';
@override
String get finish => 'Finalizar';
@override
String get signature => 'Firma';
@override
String get signatureCapture => 'Capturar firma';
@override
String get signatureRequired => 'Por favor capture una firma.';
@override
String get clear => 'Limpiar';
@override
String get signatureError => 'Error al guardar la firma';
@override
String get signatureInstruction => 'Por favor, firme en el campo de abajo (ratón o dedo).';
@override
String get photoCapture => 'Tomar fotos';
@override
String get requiredPhotos => 'Fotos requeridas';
@override
String get photosTaken => 'Tomadas';
@override
String get photos => 'Fotos';
@override
String get takePhoto => 'Tomar foto';
@override
String get selectFromLibrary => 'Seleccionar de la biblioteca';
@override
String get retakePhoto => 'Volver a tomar';
@override
String get photoRequired => 'Foto requerida';
@override
String get minPhotos => 'Al menos';
@override
String get maxPhotos => 'Máximo';
@override
String get photoError => 'Error al tomar la foto';
@override
String get deletePhoto => 'Eliminar foto';
@override
String get deletePhotoConfirm => '¿Realmente desea eliminar esta foto?';
@override
String get barcode => 'Código de barras';
@override
String get barcodeScan => 'Escanear código de barras';
@override
String get scanBarcode => 'Escanear código de barras';
@override
String get barcodeRequired => 'Código de barras requerido';
@override
String get minBarcodes => 'Al menos';
@override
String get maxBarcodes => 'Máximo';
@override
String get scanned => 'Escaneado';
@override
String get scannedBarcodes => 'Códigos de barras escaneados';
@override
String get barcodesRequired => 'Códigos de barras requeridos';
@override
String get enterBarcode => 'Ingresar código de barras';
@override
String get barcodeEnterDescription => 'Por favor ingrese los códigos de barras:';
@override
String barcodeNumberRequired(int number) => 'Código de barras $number (requerido)';
@override
String barcodeNumberOptional(int number) => 'Código de barras $number (opcional)';
@override
String get barcodeError => 'Error al escanear el código de barras';
@override
String get cameraError => 'Error al inicializar la cámara';
@override
String get cameraNotReady => 'La cámara no está lista o no disponible';
@override
String get cameraNotAvailable => 'Cámara no disponible';
@override
String get cameraNotSupportedMessage => 'La cámara no es compatible con esta plataforma.';
@override
String get cameraNotSupportedOnPlatform => 'No soportado en esta plataforma';
@override
String get maxPhotosReached => 'Máximo alcanzado';
@override
String get cameraReadyNoPreview => 'Cámara lista (sin vista previa)';
@override
String get cameraLoading => 'Cargando cámara...';
@override
String get cameraInitializing => 'Inicializando cámara...';
@override
String get cameraLoadingMessage => 'Por favor espere mientras se carga la cámara';
@override
String get addPhotos => 'Añadir fotos';
@override
String get addPhotosInstruction => 'Use el botón "Seleccionar foto" para añadir imágenes de su cámara o disco duro.';
@override
String get photoOf => 'de';
// ==================== CHAT ====================
@override
String get typeMessage => 'Escriba un mensaje...';
@override
String get send => 'Enviar';
@override
String get noSender => 'No hay remitente disponible';
@override
String get noSenderMessage => 'No hay remitente disponible. Por favor inicie sesión de nuevo.';
@override
String get noRecipient => 'No hay destinatario configurado';
@override
String get noRecipientMessage => 'No hay destinatario configurado para este chat.';
@override
String get messageSendError => 'El mensaje no pudo ser enviado.';
@override
String get photoSendError => 'La foto no pudo ser enviada.';
@override
String get photoProcessError => 'La foto no pudo ser procesada.';
@override
String get imageSendError => 'La imagen no pudo ser enviada.';
@override
String get chatTypeJob => 'Específico del trabajo';
@override
String get chatTypeGeneral => 'General';
@override
String get jobNumber => 'Número de trabajo';
@override
String get messages => 'Mensajes';
@override
String get selectPhoto => 'Seleccionar foto';
@override
String get unreadMessages => 'Mensajes no leídos';
// ==================== CARGO ====================
@override
String get cargoDetails => 'Detalles de carga';
@override
String get itemName => 'Descripción';
@override
String get itemNumber => 'Nº de posición';
@override
String get item => 'Posición';
@override
String get weightUnit => 'kg';
@override
String get dimensionUnit => 'cm';
@override
String get noCargoItems => 'Sin artículos de carga';
@override
String get noCargoItemsMessage => 'No hay artículos de carga definidos para este trabajo.';
@override
String get article => 'Artículo';
// ==================== TASK TYPES ====================
@override
String get takePhotos => 'Tomar fotos';
@override
String get photosCount => 'Fotos';
@override
String get checklistPoints => 'Puntos';
@override
String get signatureRequiredText => 'Firma requerida';
@override
String get scanBarcodes => 'Escanear códigos';
@override
String get barcodeCount => 'Códigos';
@override
String get commentOptional => 'Comentario';
@override
String get genericTask => 'Tarea genérica';
@override
String get complete => 'Completar';
@override
String get abort => 'Cancelar';
@override
String get optional => 'Opcional';
@override
String get skipTask => 'Omitir';
// ==================== SETTINGS ====================
@override
String get language => 'Idioma';
@override
String get languageChanged => 'Idioma cambiado a';
@override
String get appInfo => 'INFO DE LA APP';
// ==================== STATUS ====================
@override
String get statusCreated => 'Creado';
@override
String get statusAssigned => 'Asignado';
@override
String get statusInProgress => 'En progreso';
@override
String get statusCompleted => 'Completado';
@override
String get priorityLow => 'Baja';
@override
String get priorityMedium => 'Media';
@override
String get priorityHigh => 'Alta';
@override
String get priorityUrgent => 'Urgente';
}

View File

@@ -0,0 +1,385 @@
import 'app_localizations.dart';
class AppLocalizationsEt extends AppLocalizations {
@override
String get languageName => 'Eesti';
@override
String get flagEmoji => '🇪🇪';
// ==================== GENERAL ====================
@override
String get appTitle => 'VotianLT App';
@override
String get ok => 'OK';
@override
String get cancel => 'Tühista';
@override
String get save => 'Salvesta';
@override
String get delete => 'Kustuta';
@override
String get close => 'Sulge';
@override
String get confirm => 'Kinnita';
@override
String get error => 'Viga';
@override
String get success => 'Edu';
@override
String get loading => 'Laadimine...';
@override
String get refresh => 'Värskenda';
@override
String get version => 'Versioon';
@override
String get unknown => 'Tundmatu';
// ==================== NAVIGATION ====================
@override
String get jobs => 'Tööd';
@override
String get availableJobs => 'Saadaolevad tööd';
@override
String get chats => 'Vestlused';
@override
String get settings => 'Seaded';
@override
String get logout => 'Logi välja';
@override
String get logoutConfirm => 'Logi välja';
@override
String get logoutConfirmMessage => 'Kas soovite tõesti välja logida?';
@override
String get openChat => 'Ava vestlus';
@override
String get chatInfo => 'Vestluse info';
@override
String get routePlan => 'Kavanda marsruut';
// ==================== LOGIN ====================
@override
String get welcomeBack => 'Tere tulemast tagasi';
@override
String get loginSubtitle => 'Logige oma kontosse sisse';
@override
String get email => 'E-post';
@override
String get password => 'Parool';
@override
String get login => 'Logi sisse';
@override
String get loggingIn => 'Ühendamine...';
@override
String get forgotPassword => 'Unustasid parooli?';
@override
String get forgotPasswordMessage => 'Unustatud parooli funktsioon pole veel rakendatud';
@override
String get loginSuccess => 'Edukalt välja logitud';
@override
String get loginFailed => 'Sisselogimine ebaõnnestus';
@override
String get connectionFailed => 'Serveriga ühenduse loomine ebaõnnestus (Aegunud).';
@override
String get connectionTimeout => 'Serveriga ühenduse loomine ebaõnnestus (Aegunud).';
@override
String get connecting => 'Serveriga ühendamine...';
@override
String get connectionError => 'Ühenduse viga';
@override
String get loginError => 'Viga sisselogimisel';
// ==================== JOBS ====================
@override
String get noJobsAssigned => 'Ülesandeid pole määratud';
@override
String get noJobsMessage => 'Teie määratud tööd kuvatakse siin.';
@override
String get pullToRefresh => 'Värskendamiseks tõmmake alla';
@override
String get newLabel => 'UUS';
@override
String get tasksToComplete => 'Täitmiseks ülesanded';
@override
String get pickup => 'Pealevõtt';
@override
String get delivery => 'Kohaletoimetamine';
@override
String get created => 'Loodud';
@override
String get status => 'Olek';
@override
String get priority => 'Prioriteet';
@override
String get dueDate => 'Tähtaeg';
@override
String get location => 'Asukoht';
@override
String get description => 'Kirjeldus';
@override
String get cargo => 'Kaup';
@override
String get quantity => 'Kogus';
@override
String get weight => 'Kaal';
@override
String get dimensions => 'Mõõtmed';
@override
String get jobDeleted => 'Töö kustutatud';
@override
String get jobDeleteError => 'Viga töö kustutamisel';
@override
String get jobCompleted => 'Töö lõpetatud';
@override
String get from => 'Kust';
@override
String get to => 'kus';
@override
String get jobsUpdated => 'Tööd värskendatud';
@override
String get connectionRestored => 'Ühendus taastatud. Tööde laadimine...';
@override
String get connectionLost => 'Ühendus kaotatud. Võrguühenduseta.';
@override
String get offline => 'Võrguühenduseta';
@override
String get deleteJob => 'Kustuta töö';
@override
String get jobRemoved => 'eemaldati';
@override
String get newJobReceived => 'Uus töö saadud';
// ==================== TASKS ====================
@override
String get tasks => 'Ülesanded';
@override
String get noTasks => 'Ülesandeid pole';
@override
String get noTasksMessage => 'Selle töö jaoks pole ülesandeid määratud.';
@override
String get taskOrder => 'Järjekord';
@override
String get confirmationRequired => 'Vajalik kinnitus';
@override
String get confirmationDescription => 'Ülesande lõpuleviimiseks klõpsake nuppu.';
@override
String get checklist => 'Kontrollnimekiri';
@override
String get checklistDescription => 'Palun märkige kõik punktid:';
@override
String get completeTask => 'Lõpeta ülesanne';
@override
String get completeTaskConfirm => 'Kas soovite selle ülesande lõpetatuks märgistada?';
@override
String get completeTaskNote => 'Märkus (valikuline)';
@override
String get taskCompleted => 'Ülesanne lõpetatud';
@override
String get comment => 'Kommentaar';
@override
String get commentRequired => 'Kommentaar (nõutav)';
@override
String get enterComment => 'Sisesta kommentaar';
@override
String get commentDescription => 'Palun sisestage kommentaar:';
@override
String get finish => 'Lõpeta';
@override
String get signature => 'Allkiri';
@override
String get signatureCapture => 'Salvesta allkiri';
@override
String get signatureRequired => 'Palun salvestage allkiri.';
@override
String get clear => 'Tühjenda';
@override
String get signatureError => 'Viga allkirja salvestamisel';
@override
String get signatureInstruction => 'Palun allkirjastage allolevas väljas (hiir või sõrm).';
@override
String get photoCapture => 'Tee pilte';
@override
String get requiredPhotos => 'Vajalikud fotod';
@override
String get photosTaken => 'Tehtud';
@override
String get photos => 'Fotod';
@override
String get takePhoto => 'Tee foto';
@override
String get selectFromLibrary => 'Vali galeriist';
@override
String get retakePhoto => 'Pildista uuesti';
@override
String get photoRequired => 'Foto nõutav';
@override
String get minPhotos => 'Vähemalt';
@override
String get maxPhotos => 'Maksimum';
@override
String get photoError => 'Viga foto tegemisel';
@override
String get deletePhoto => 'Kustuta foto';
@override
String get deletePhotoConfirm => 'Kas soovite tõesti selle foto kustutada?';
@override
String get barcode => 'Vöötkood';
@override
String get barcodeScan => 'Skaneeri vöötkood';
@override
String get scanBarcode => 'Skaneeri vöötkood';
@override
String get barcodeRequired => 'Vöötkood nõutav';
@override
String get minBarcodes => 'Vähemalt';
@override
String get maxBarcodes => 'Maksimum';
@override
String get scanned => 'Skaneeritud';
@override
String get scannedBarcodes => 'Skaneeritud vöötkoodid';
@override
String get barcodesRequired => 'Vöötkoodid nõutavad';
@override
String get enterBarcode => 'Sisesta vöötkood';
@override
String get barcodeEnterDescription => 'Palun sisestage vöötkoodid:';
@override
String barcodeNumberRequired(int number) => 'Vöötkood $number (nõutav)';
@override
String barcodeNumberOptional(int number) => 'Vöötkood $number (valikuline)';
@override
String get barcodeError => 'Viga vöötkoodi skaneerimisel';
@override
String get cameraError => 'Viga kaamera käivitamisel';
@override
String get cameraNotReady => 'Kaamera pole valmis või pole saadaval';
@override
String get cameraNotAvailable => 'Kaamera pole saadaval';
@override
String get cameraNotSupportedMessage => 'Kaamerat ei toetata sellel platvormil.';
@override
String get cameraNotSupportedOnPlatform => 'Sellel platvormil ei toetata';
@override
String get maxPhotosReached => 'Maksimaalne arv saavutatud';
@override
String get cameraReadyNoPreview => 'Kaamera valmis (eelvaade puudub)';
@override
String get cameraLoading => 'Kaamera laadib...';
@override
String get cameraInitializing => 'Kaamera initsialiseerimine...';
@override
String get cameraLoadingMessage => 'Palun oodake, kuni kaamera laadib';
@override
String get addPhotos => 'Lisa fotod';
@override
String get addPhotosInstruction => 'Kasutage nuppu "Vali foto", et lisada pilte kaamerast või kõvakettalt.';
@override
String get photoOf => '/';
// ==================== CHAT ====================
@override
String get typeMessage => 'Sisesta sõnum...';
@override
String get send => 'Saada';
@override
String get noSender => 'Saatja pole saadaval';
@override
String get noSenderMessage => 'Saatja pole saadaval. Palun logige uuesti sisse.';
@override
String get noRecipient => 'Vastuvõtjat pole konfigureeritud';
@override
String get noRecipientMessage => 'Selle vestluse jaoks pole vastuvõtjat konfigureeritud.';
@override
String get messageSendError => 'Sõnumi saatmine ebaõnnestus.';
@override
String get photoSendError => 'Foto saatmine ebaõnnestus.';
@override
String get photoProcessError => 'Foto töötlemine ebaõnnestus.';
@override
String get imageSendError => 'Pildi saatmine ebaõnnestus.';
@override
String get chatTypeJob => 'Töö-spetsiifiline';
@override
String get chatTypeGeneral => 'Üldine';
@override
String get jobNumber => 'Töö number';
@override
String get messages => 'Sõnumid';
@override
String get selectPhoto => 'Vali foto';
@override
String get unreadMessages => 'Lugemata sõnumid';
// ==================== CARGO ====================
@override
String get cargoDetails => 'Kauba detailid';
@override
String get itemName => 'Kirjeldus';
@override
String get itemNumber => 'Positsiooni nr';
@override
String get item => 'Positsioon';
@override
String get weightUnit => 'kg';
@override
String get dimensionUnit => 'cm';
@override
String get noCargoItems => 'Kaubaosi puuduvad';
@override
String get noCargoItemsMessage => 'Selle töö jaoks pole kaubaosi määratud.';
@override
String get article => 'Artikkel';
// ==================== TASK TYPES ====================
@override
String get takePhotos => 'Tee pilte';
@override
String get photosCount => 'Fotod';
@override
String get checklistPoints => 'Punktid';
@override
String get signatureRequiredText => 'Allkiri nõutav';
@override
String get scanBarcodes => 'Skaneeri vöötkoode';
@override
String get barcodeCount => 'Koodid';
@override
String get commentOptional => 'Kommentaar';
@override
String get genericTask => 'Üldine ülesanne';
@override
String get complete => 'Lõpeta';
@override
String get abort => 'Tühista';
@override
String get optional => 'Valikuline';
@override
String get skipTask => 'Vahele jätta';
// ==================== SETTINGS ====================
@override
String get language => 'Keel';
@override
String get languageChanged => 'Keel muudetud:';
@override
String get appInfo => 'RAKENDUSE INFO';
// ==================== STATUS ====================
@override
String get statusCreated => 'Loodud';
@override
String get statusAssigned => 'Määratud';
@override
String get statusInProgress => 'Töös';
@override
String get statusCompleted => 'Lõpetatud';
@override
String get priorityLow => 'Madal';
@override
String get priorityMedium => 'Keskmine';
@override
String get priorityHigh => 'Kõrge';
@override
String get priorityUrgent => 'Kiire';
}

View File

@@ -0,0 +1,385 @@
import 'app_localizations.dart';
class AppLocalizationsFr extends AppLocalizations {
@override
String get languageName => 'Français';
@override
String get flagEmoji => '🇫🇷';
// ==================== GENERAL ====================
@override
String get appTitle => 'VotianLT App';
@override
String get ok => 'OK';
@override
String get cancel => 'Annuler';
@override
String get save => 'Enregistrer';
@override
String get delete => 'Supprimer';
@override
String get close => 'Fermer';
@override
String get confirm => 'Confirmer';
@override
String get error => 'Erreur';
@override
String get success => 'Succès';
@override
String get loading => 'Chargement...';
@override
String get refresh => 'Actualiser';
@override
String get version => 'Version';
@override
String get unknown => 'Inconnu';
// ==================== NAVIGATION ====================
@override
String get jobs => 'Emplois';
@override
String get availableJobs => 'Emplois Disponibles';
@override
String get chats => 'Discussions';
@override
String get settings => 'Paramètres';
@override
String get logout => 'Déconnexion';
@override
String get logoutConfirm => 'Déconnexion';
@override
String get logoutConfirmMessage => 'Voulez-vous vraiment vous déconnecter?';
@override
String get openChat => 'Ouvrir la discussion';
@override
String get chatInfo => 'Info discussion';
@override
String get routePlan => 'Planifier l\'itinéraire';
// ==================== LOGIN ====================
@override
String get welcomeBack => 'Bon retour';
@override
String get loginSubtitle => 'Connectez-vous à votre compte';
@override
String get email => 'E-mail';
@override
String get password => 'Mot de passe';
@override
String get login => 'Connexion';
@override
String get loggingIn => 'Connexion...';
@override
String get forgotPassword => 'Mot de passe oublié?';
@override
String get forgotPasswordMessage => 'Fonction mot de passe oublié pas encore implémentée';
@override
String get loginSuccess => 'Déconnexion réussie';
@override
String get loginFailed => 'Échec de la connexion';
@override
String get connectionFailed => 'Échec de la connexion au serveur (Délai dépassé).';
@override
String get connectionTimeout => 'Échec de la connexion au serveur (Délai dépassé).';
@override
String get connecting => 'Connexion au serveur...';
@override
String get connectionError => 'Erreur de connexion';
@override
String get loginError => 'Erreur lors de la connexion';
// ==================== JOBS ====================
@override
String get noJobsAssigned => 'Aucun emploi assigné';
@override
String get noJobsMessage => 'Vos emplois assignés seront affichés ici.';
@override
String get pullToRefresh => 'Tirez vers le bas pour actualiser';
@override
String get newLabel => 'NOUVEAU';
@override
String get tasksToComplete => 'Tâches à accomplir';
@override
String get pickup => 'Ramassage';
@override
String get delivery => 'Livraison';
@override
String get created => 'Créé';
@override
String get status => 'Statut';
@override
String get priority => 'Priorité';
@override
String get dueDate => 'Date d\'échéance';
@override
String get location => 'Lieu';
@override
String get description => 'Description';
@override
String get cargo => 'Cargaison';
@override
String get quantity => 'Quantité';
@override
String get weight => 'Poids';
@override
String get dimensions => 'Dimensions';
@override
String get jobDeleted => 'Emploi supprimé';
@override
String get jobDeleteError => 'Erreur lors de la suppression de l\'emploi';
@override
String get jobCompleted => 'Emploi terminé';
@override
String get from => 'De';
@override
String get to => 'à';
@override
String get jobsUpdated => 'Emplois actualisés';
@override
String get connectionRestored => 'Connexion restaurée. Chargement des emplois...';
@override
String get connectionLost => 'Connexion perdue. Hors ligne.';
@override
String get offline => 'Hors ligne';
@override
String get deleteJob => 'Supprimer l\'emploi';
@override
String get jobRemoved => 'a été supprimé';
@override
String get newJobReceived => 'Nouvel emploi reçu';
// ==================== TASKS ====================
@override
String get tasks => 'Tâches';
@override
String get noTasks => 'Aucune tâche';
@override
String get noTasksMessage => 'Aucune tâche définie pour cet emploi.';
@override
String get taskOrder => 'Ordre';
@override
String get confirmationRequired => 'Confirmation requise';
@override
String get confirmationDescription => 'Cliquez sur le bouton pour terminer la tâche.';
@override
String get checklist => 'Liste de contrôle';
@override
String get checklistDescription => 'Veuillez cocher tous les éléments:';
@override
String get completeTask => 'Terminer la tâche';
@override
String get completeTaskConfirm => 'Voulez-vous marquer cette tâche comme terminée?';
@override
String get completeTaskNote => 'Note (optionnelle)';
@override
String get taskCompleted => 'Tâche terminée';
@override
String get comment => 'Commentaire';
@override
String get commentRequired => 'Commentaire (requis)';
@override
String get enterComment => 'Saisir un commentaire';
@override
String get commentDescription => 'Veuillez saisir un commentaire:';
@override
String get finish => 'Terminer';
@override
String get signature => 'Signature';
@override
String get signatureCapture => 'Capturer la signature';
@override
String get signatureRequired => 'Veuillez capturer une signature.';
@override
String get clear => 'Effacer';
@override
String get signatureError => 'Erreur lors de l\'enregistrement de la signature';
@override
String get signatureInstruction => 'Veuillez signer dans le champ ci-dessous (souris ou doigt).';
@override
String get photoCapture => 'Prendre des photos';
@override
String get requiredPhotos => 'Photos requises';
@override
String get photosTaken => 'Prises';
@override
String get photos => 'Photos';
@override
String get takePhoto => 'Prendre une photo';
@override
String get selectFromLibrary => 'Sélectionner depuis la bibliothèque';
@override
String get retakePhoto => 'Reprendre';
@override
String get photoRequired => 'Photo requise';
@override
String get minPhotos => 'Au moins';
@override
String get maxPhotos => 'Maximum';
@override
String get photoError => 'Erreur lors de la prise de photo';
@override
String get deletePhoto => 'Supprimer la photo';
@override
String get deletePhotoConfirm => 'Voulez-vous vraiment supprimer cette photo?';
@override
String get barcode => 'Code-barres';
@override
String get barcodeScan => 'Scanner le code-barres';
@override
String get scanBarcode => 'Scanner le code-barres';
@override
String get barcodeRequired => 'Code-barres requis';
@override
String get minBarcodes => 'Au moins';
@override
String get maxBarcodes => 'Maximum';
@override
String get scanned => 'Scanné';
@override
String get scannedBarcodes => 'Codes-barres scannés';
@override
String get barcodesRequired => 'Codes-barres requis';
@override
String get enterBarcode => 'Entrer le code-barres';
@override
String get barcodeEnterDescription => 'Veuillez entrer les codes-barres:';
@override
String barcodeNumberRequired(int number) => 'Code-barres $number (requis)';
@override
String barcodeNumberOptional(int number) => 'Code-barres $number (optionnel)';
@override
String get barcodeError => 'Erreur lors du scan du code-barres';
@override
String get cameraError => 'Erreur lors de l\'initialisation de la caméra';
@override
String get cameraNotReady => 'La caméra n\'est pas prête ou non disponible';
@override
String get cameraNotAvailable => 'Caméra non disponible';
@override
String get cameraNotSupportedMessage => 'La caméra n\'est pas prise en charge sur cette plateforme.';
@override
String get cameraNotSupportedOnPlatform => 'Non supporté sur cette plateforme';
@override
String get maxPhotosReached => 'Maximum atteint';
@override
String get cameraReadyNoPreview => 'Caméra prête (sans aperçu)';
@override
String get cameraLoading => 'Chargement de la caméra...';
@override
String get cameraInitializing => 'Initialisation de la caméra...';
@override
String get cameraLoadingMessage => 'Veuillez patienter pendant le chargement de la caméra';
@override
String get addPhotos => 'Ajouter des photos';
@override
String get addPhotosInstruction => 'Utilisez le bouton "Sélectionner une photo" pour ajouter des images depuis votre appareil photo ou disque dur.';
@override
String get photoOf => 'sur';
// ==================== CHAT ====================
@override
String get typeMessage => 'Tapez un message...';
@override
String get send => 'Envoyer';
@override
String get noSender => 'Aucun expéditeur disponible';
@override
String get noSenderMessage => 'Aucun expéditeur disponible. Veuillez vous reconnecter.';
@override
String get noRecipient => 'Aucun destinataire configuré';
@override
String get noRecipientMessage => 'Aucun destinataire configuré pour cette discussion.';
@override
String get messageSendError => 'Le message n\'a pas pu être envoyé.';
@override
String get photoSendError => 'La photo n\'a pas pu être envoyée.';
@override
String get photoProcessError => 'La photo n\'a pas pu être traitée.';
@override
String get imageSendError => 'L\'image n\'a pas pu être envoyée.';
@override
String get chatTypeJob => 'Spécifique à l\'emploi';
@override
String get chatTypeGeneral => 'Général';
@override
String get jobNumber => 'Numéro d\'emploi';
@override
String get messages => 'Messages';
@override
String get selectPhoto => 'Sélectionner une photo';
@override
String get unreadMessages => 'Messages non lus';
// ==================== CARGO ====================
@override
String get cargoDetails => 'Détails de cargaison';
@override
String get itemName => 'Description';
@override
String get itemNumber => 'N° de position';
@override
String get item => 'Position';
@override
String get weightUnit => 'kg';
@override
String get dimensionUnit => 'cm';
@override
String get noCargoItems => 'Aucun article de cargaison';
@override
String get noCargoItemsMessage => 'Aucun article de cargaison défini pour cet emploi.';
@override
String get article => 'Article';
// ==================== TASK TYPES ====================
@override
String get takePhotos => 'Prendre des photos';
@override
String get photosCount => 'Photos';
@override
String get checklistPoints => 'Points';
@override
String get signatureRequiredText => 'Signature requise';
@override
String get scanBarcodes => 'Scanner les codes-barres';
@override
String get barcodeCount => 'Codes';
@override
String get commentOptional => 'Commentaire';
@override
String get genericTask => 'Tâche générique';
@override
String get complete => 'Terminer';
@override
String get abort => 'Annuler';
@override
String get optional => 'Facultatif';
@override
String get skipTask => 'Ignorer';
// ==================== SETTINGS ====================
@override
String get language => 'Langue';
@override
String get languageChanged => 'Langue changée en';
@override
String get appInfo => 'INFO APP';
// ==================== STATUS ====================
@override
String get statusCreated => 'Créé';
@override
String get statusAssigned => 'Assigné';
@override
String get statusInProgress => 'En cours';
@override
String get statusCompleted => 'Terminé';
@override
String get priorityLow => 'Basse';
@override
String get priorityMedium => 'Moyenne';
@override
String get priorityHigh => 'Haute';
@override
String get priorityUrgent => 'Urgente';
}

View File

@@ -0,0 +1,385 @@
import 'app_localizations.dart';
class AppLocalizationsLt extends AppLocalizations {
@override
String get languageName => 'Lietuvių';
@override
String get flagEmoji => '🇱🇹';
// ==================== GENERAL ====================
@override
String get appTitle => 'VotianLT App';
@override
String get ok => 'Gerai';
@override
String get cancel => 'Atšaukti';
@override
String get save => 'Išsaugoti';
@override
String get delete => 'Ištrinti';
@override
String get close => 'Uždaryti';
@override
String get confirm => 'Patvirtinti';
@override
String get error => 'Klaida';
@override
String get success => 'Sėkmė';
@override
String get loading => 'Kraunama...';
@override
String get refresh => 'Atnaujinti';
@override
String get version => 'Versija';
@override
String get unknown => 'Nežinoma';
// ==================== NAVIGATION ====================
@override
String get jobs => 'Darbai';
@override
String get availableJobs => 'Galimi darbai';
@override
String get chats => 'Pokalbiai';
@override
String get settings => 'Nustatymai';
@override
String get logout => 'Atsijungti';
@override
String get logoutConfirm => 'Atsijungti';
@override
String get logoutConfirmMessage => 'Ar tikrai norite atsijungti?';
@override
String get openChat => 'Atidaryti pokalbį';
@override
String get chatInfo => 'Pokalbio info';
@override
String get routePlan => 'Planuoti maršrutą';
// ==================== LOGIN ====================
@override
String get welcomeBack => 'Sveiki sugrįžę';
@override
String get loginSubtitle => 'Prisijunkite prie savo paskyros';
@override
String get email => 'El. paštas';
@override
String get password => 'Slaptažodis';
@override
String get login => 'Prisijungti';
@override
String get loggingIn => 'Jungiamasi...';
@override
String get forgotPassword => 'Pamiršote slaptažodį?';
@override
String get forgotPasswordMessage => 'Pamiršto slaptažodžio funkcija dar neįdiegta';
@override
String get loginSuccess => 'Sėkmingai atsijungta';
@override
String get loginFailed => 'Prisijungimas nepavyko';
@override
String get connectionFailed => 'Nepavyko prisijungti prie serverio (Laikas baigėsi).';
@override
String get connectionTimeout => 'Nepavyko prisijungti prie serverio (Laikas baigėsi).';
@override
String get connecting => 'Jungiamasi prie serverio...';
@override
String get connectionError => 'Ryšio klaida';
@override
String get loginError => 'Klaida prisijungiant';
// ==================== JOBS ====================
@override
String get noJobsAssigned => 'Nėra priskirtų darbų';
@override
String get noJobsMessage => 'Jūsų priskirti darbai bus rodomi čia.';
@override
String get pullToRefresh => 'Patraukite žemyn, kad atnaujintumėte';
@override
String get newLabel => 'NAUJAS';
@override
String get tasksToComplete => 'Užduotys, kurias reikia atlikti';
@override
String get pickup => 'Paėmimas';
@override
String get delivery => 'Pristatymas';
@override
String get created => 'Sukurta';
@override
String get status => 'Būsena';
@override
String get priority => 'Prioritetas';
@override
String get dueDate => 'Terminas';
@override
String get location => 'Vieta';
@override
String get description => 'Aprašymas';
@override
String get cargo => 'Krovinys';
@override
String get quantity => 'Kiekis';
@override
String get weight => 'Svoris';
@override
String get dimensions => 'Matmenys';
@override
String get jobDeleted => 'Darbas ištrintas';
@override
String get jobDeleteError => 'Klaida ištrinant darbą';
@override
String get jobCompleted => 'Darbas baigtas';
@override
String get from => '';
@override
String get to => 'į';
@override
String get jobsUpdated => 'Darbai atnaujinti';
@override
String get connectionRestored => 'Ryšys atkurtas. Kraunami darbai...';
@override
String get connectionLost => 'Ryšys prarastas. Neprisijungta.';
@override
String get offline => 'Neprisijungta';
@override
String get deleteJob => 'Ištrinti darbą';
@override
String get jobRemoved => 'buvo pašalintas';
@override
String get newJobReceived => 'Gautas naujas darbas';
// ==================== TASKS ====================
@override
String get tasks => 'Užduotys';
@override
String get noTasks => 'Nėra užduočių';
@override
String get noTasksMessage => 'Šiam darbui nėra apibrėžtų užduočių.';
@override
String get taskOrder => 'Eilės tvarka';
@override
String get confirmationRequired => 'Reikalingas patvirtinimas';
@override
String get confirmationDescription => 'Spustelėkite mygtuką, kad atliktumėte užduotį.';
@override
String get checklist => 'Patikros sąrašas';
@override
String get checklistDescription => 'Prašome pažymėti visus punktus:';
@override
String get completeTask => 'Baigti užduotį';
@override
String get completeTaskConfirm => 'Ar norite pažymėti šią užduotį kaip baigtą?';
@override
String get completeTaskNote => 'Pastaba (neprivaloma)';
@override
String get taskCompleted => 'Užduotis baigta';
@override
String get comment => 'Komentaras';
@override
String get commentRequired => 'Komentaras (būtinas)';
@override
String get enterComment => 'Įveskite komentarą';
@override
String get commentDescription => 'Prašome įvesti komentarą:';
@override
String get finish => 'Baigti';
@override
String get signature => 'Parašas';
@override
String get signatureCapture => 'Įrašyti parašą';
@override
String get signatureRequired => 'Prašome įrašyti parašą.';
@override
String get clear => 'Išvalyti';
@override
String get signatureError => 'Klaida išsaugant parašą';
@override
String get signatureInstruction => 'Prašome pasirašyti laukelyje žemiau (pele arba pirštu).';
@override
String get photoCapture => 'Daryti nuotraukas';
@override
String get requiredPhotos => 'Reikalingos nuotraukos';
@override
String get photosTaken => 'Padaryta';
@override
String get photos => 'Nuotraukos';
@override
String get takePhoto => 'Daryti nuotrauką';
@override
String get selectFromLibrary => 'Pasirinkti iš bibliotekos';
@override
String get retakePhoto => 'Perdaryti';
@override
String get photoRequired => 'Reikalinga nuotrauka';
@override
String get minPhotos => 'Mažiausiai';
@override
String get maxPhotos => 'Daugiausia';
@override
String get photoError => 'Klaida darant nuotrauką';
@override
String get deletePhoto => 'Ištrinti nuotrauką';
@override
String get deletePhotoConfirm => 'Ar tikrai norite ištrinti šią nuotrauką?';
@override
String get barcode => 'Brūkšninis kodas';
@override
String get barcodeScan => 'Skaityti brūkšninį kodą';
@override
String get scanBarcode => 'Skaityti brūkšninį kodą';
@override
String get barcodeRequired => 'Reikalingas brūkšninis kodas';
@override
String get minBarcodes => 'Mažiausiai';
@override
String get maxBarcodes => 'Daugiausia';
@override
String get scanned => 'Nuskaityta';
@override
String get scannedBarcodes => 'Nuskaityti brūkšniniai kodai';
@override
String get barcodesRequired => 'Reikalingi brūkšniniai kodai';
@override
String get enterBarcode => 'Įveskite brūkšninį kodą';
@override
String get barcodeEnterDescription => 'Prašome įvesti brūkšninius kodus:';
@override
String barcodeNumberRequired(int number) => 'Brūkšninis kodas $number (būtinas)';
@override
String barcodeNumberOptional(int number) => 'Brūkšninis kodas $number (neprivalomas)';
@override
String get barcodeError => 'Klaida skaitant brūkšninį kodą';
@override
String get cameraError => 'Klaida inicializuojant kamerą';
@override
String get cameraNotReady => 'Kamera nėra pasiruošusi arba nepasiekiama';
@override
String get cameraNotAvailable => 'Kamera nepasiekiama';
@override
String get cameraNotSupportedMessage => 'Šioje platformoje kamera nepalaikoma.';
@override
String get cameraNotSupportedOnPlatform => 'Nepalaikoma šioje platformoje';
@override
String get maxPhotosReached => 'Pasiektas maksimumas';
@override
String get cameraReadyNoPreview => 'Kamera paruošta (be peržiūros)';
@override
String get cameraLoading => 'Kamera kraunama...';
@override
String get cameraInitializing => 'Kamera inicializuojama...';
@override
String get cameraLoadingMessage => 'Palaukite, kol kamera įkraunama';
@override
String get addPhotos => 'Pridėti nuotraukas';
@override
String get addPhotosInstruction => 'Naudokite mygtuką "Pasirinkti nuotrauką", norėdami pridėti vaizdų iš fotoaparato ar standžiojo disko.';
@override
String get photoOf => '';
// ==================== CHAT ====================
@override
String get typeMessage => 'Įveskite žinutę...';
@override
String get send => 'Siųsti';
@override
String get noSender => 'Siuntėjas nepasiekiamas';
@override
String get noSenderMessage => 'Siuntėjas nepasiekiamas. Prašome prisijungti dar kartą.';
@override
String get noRecipient => 'Gavėjas nesukonfigūruotas';
@override
String get noRecipientMessage => 'Šiam pokalbiui nesukonfigūruotas gavėjas.';
@override
String get messageSendError => 'Žinutės išsiųsti nepavyko.';
@override
String get photoSendError => 'Nuotraukos išsiųsti nepavyko.';
@override
String get photoProcessError => 'Nuotraukos apdoroti nepavyko.';
@override
String get imageSendError => 'Vaizdo išsiųsti nepavyko.';
@override
String get chatTypeJob => 'Specifinis darbui';
@override
String get chatTypeGeneral => 'Bendras';
@override
String get jobNumber => 'Darbo numeris';
@override
String get messages => 'Žinutės';
@override
String get selectPhoto => 'Pasirinkti nuotrauką';
@override
String get unreadMessages => 'Neskaitytos žinutės';
// ==================== CARGO ====================
@override
String get cargoDetails => 'Krovinio detalės';
@override
String get itemName => 'Aprašymas';
@override
String get itemNumber => 'Pozicijos Nr.';
@override
String get item => 'Pozicija';
@override
String get weightUnit => 'kg';
@override
String get dimensionUnit => 'cm';
@override
String get noCargoItems => 'Nėra krovinių pozicijų';
@override
String get noCargoItemsMessage => 'Šiam darbui nėra apibrėžtų krovinių pozicijų.';
@override
String get article => 'Pozicija';
// ==================== TASK TYPES ====================
@override
String get takePhotos => 'Daryti nuotraukas';
@override
String get photosCount => 'Nuotraukos';
@override
String get checklistPoints => 'Taškai';
@override
String get signatureRequiredText => 'Parašas būtinas';
@override
String get scanBarcodes => 'Skaityti brūkšninius kodus';
@override
String get barcodeCount => 'Kodai';
@override
String get commentOptional => 'Komentaras';
@override
String get genericTask => 'Bendra užduotis';
@override
String get complete => 'Baigti';
@override
String get abort => 'Atšaukti';
@override
String get optional => 'Neprivaloma';
@override
String get skipTask => 'Praleisti';
// ==================== SETTINGS ====================
@override
String get language => 'Kalba';
@override
String get languageChanged => 'Kalba pakeista į';
@override
String get appInfo => 'PROGRAMĖLĖS INFO';
// ==================== STATUS ====================
@override
String get statusCreated => 'Sukurta';
@override
String get statusAssigned => 'Priskirta';
@override
String get statusInProgress => 'Vykdoma';
@override
String get statusCompleted => 'Baigta';
@override
String get priorityLow => 'Žemas';
@override
String get priorityMedium => 'Vidutinis';
@override
String get priorityHigh => 'Aukštas';
@override
String get priorityUrgent => 'Skubus';
}

View File

@@ -0,0 +1,385 @@
import 'app_localizations.dart';
class AppLocalizationsLv extends AppLocalizations {
@override
String get languageName => 'Latviešu';
@override
String get flagEmoji => '🇱🇻';
// ==================== GENERAL ====================
@override
String get appTitle => 'VotianLT App';
@override
String get ok => 'Labi';
@override
String get cancel => 'Atcelt';
@override
String get save => 'Saglabāt';
@override
String get delete => 'Dzēst';
@override
String get close => 'Aizvērt';
@override
String get confirm => 'Apstiprināt';
@override
String get error => 'Kļūda';
@override
String get success => 'Veiksmīgi';
@override
String get loading => 'Ielādē...';
@override
String get refresh => 'Atsvaidzināt';
@override
String get version => 'Versija';
@override
String get unknown => 'Nezināms';
// ==================== NAVIGATION ====================
@override
String get jobs => 'Darbi';
@override
String get availableJobs => 'Pieejamie darbi';
@override
String get chats => 'Tērzēšanas';
@override
String get settings => 'Iestatījumi';
@override
String get logout => 'Iziet';
@override
String get logoutConfirm => 'Iziet';
@override
String get logoutConfirmMessage => 'Vai tiešām vēlaties iziet?';
@override
String get openChat => 'Atvērt tērzēšanu';
@override
String get chatInfo => 'Tērzēšanas info';
@override
String get routePlan => 'Plānot maršrutu';
// ==================== LOGIN ====================
@override
String get welcomeBack => 'Laipni lūgti atpakaļ';
@override
String get loginSubtitle => 'Pierakstieties savā kontā';
@override
String get email => 'E-pasts';
@override
String get password => 'Parole';
@override
String get login => 'Pierakstīties';
@override
String get loggingIn => 'Savienojas...';
@override
String get forgotPassword => 'Aizmirsāt paroli?';
@override
String get forgotPasswordMessage => 'Aizmirstās paroles funkcija vēl nav ieviesta';
@override
String get loginSuccess => 'Veiksmīgi izrakstījās';
@override
String get loginFailed => 'Pierakstīšanās neizdevās';
@override
String get connectionFailed => 'Savienojuma kļūda ar serveri (Noildze).';
@override
String get connectionTimeout => 'Savienojuma kļūda ar serveri (Noildze).';
@override
String get connecting => 'Savienojas ar serveri...';
@override
String get connectionError => 'Savienojuma kļūda';
@override
String get loginError => 'Kļūda pierakstīšanās laikā';
// ==================== JOBS ====================
@override
String get noJobsAssigned => 'Nav piešķirtu darbu';
@override
String get noJobsMessage => 'Jūsu piešķirtie darbi tiks parādīti šeit.';
@override
String get pullToRefresh => 'Velciet uz leju, lai atsvaidzinātu';
@override
String get newLabel => 'JAUNS';
@override
String get tasksToComplete => 'Uzdevumi, kas jāveic';
@override
String get pickup => 'Saņemšana';
@override
String get delivery => 'Piegāde';
@override
String get created => 'Izveidots';
@override
String get status => 'Statuss';
@override
String get priority => 'Prioritāte';
@override
String get dueDate => 'Izpildes termiņš';
@override
String get location => 'Atrašanās vieta';
@override
String get description => 'Apraksts';
@override
String get cargo => 'Krava';
@override
String get quantity => 'Daudzums';
@override
String get weight => 'Svars';
@override
String get dimensions => 'Izmēri';
@override
String get jobDeleted => 'Darbs izdzēsts';
@override
String get jobDeleteError => 'Kļūda dzēšot darbu';
@override
String get jobCompleted => 'Darbs pabeigts';
@override
String get from => 'No';
@override
String get to => 'uz';
@override
String get jobsUpdated => 'Darbi atsvaidzināti';
@override
String get connectionRestored => 'Savienojums atjaunots. Ielādē darbus...';
@override
String get connectionLost => 'Savienojums pazaudēts. Bezsaistē.';
@override
String get offline => 'Bezsaistē';
@override
String get deleteJob => 'Dzēst darbu';
@override
String get jobRemoved => 'tika noņemts';
@override
String get newJobReceived => 'Saņemts jauns darbs';
// ==================== TASKS ====================
@override
String get tasks => 'Uzdevumi';
@override
String get noTasks => 'Nav uzdevumu';
@override
String get noTasksMessage => 'Šim darbam nav definētu uzdevumu.';
@override
String get taskOrder => 'Secība';
@override
String get confirmationRequired => 'Nepieciešams apstiprinājums';
@override
String get confirmationDescription => 'Noklikšķiniet uz pogas, lai pabeigtu uzdevumu.';
@override
String get checklist => 'Pārbaudes saraksts';
@override
String get checklistDescription => 'Lūdzu, atzīmējiet visus punktus:';
@override
String get completeTask => 'Pabeigt uzdevumu';
@override
String get completeTaskConfirm => 'Vai vēlaties atzīmēt šo uzdevumu kā pabeigtu?';
@override
String get completeTaskNote => 'Piezīme (neobligāta)';
@override
String get taskCompleted => 'Uzdevums pabeigts';
@override
String get comment => 'Komentārs';
@override
String get commentRequired => 'Komentārs (obligāts)';
@override
String get enterComment => 'Ievadiet komentāru';
@override
String get commentDescription => 'Lūdzu, ievadiet komentāru:';
@override
String get finish => 'Pabeigt';
@override
String get signature => 'Paraksts';
@override
String get signatureCapture => 'Uzņemt parakstu';
@override
String get signatureRequired => 'Lūdzu, uzņemiet parakstu.';
@override
String get clear => 'Notīrīt';
@override
String get signatureError => 'Kļūda saglabājot parakstu';
@override
String get signatureInstruction => 'Lūdzu parakstieties zemāk esošajā laukā (pele vai pirksts).';
@override
String get photoCapture => 'Uzņemt fotogrāfijas';
@override
String get requiredPhotos => 'Nepieciešamās fotogrāfijas';
@override
String get photosTaken => 'Uzņemtas';
@override
String get photos => 'Fotogrāfijas';
@override
String get takePhoto => 'Uzņemt fotogrāfiju';
@override
String get selectFromLibrary => 'Izvēlēties no bibliotēkas';
@override
String get retakePhoto => 'Uzņemt vēlreiz';
@override
String get photoRequired => 'Nepieciešama fotogrāfija';
@override
String get minPhotos => 'Vismaz';
@override
String get maxPhotos => 'Maksimums';
@override
String get photoError => 'Kļūda uzņemot fotogrāfiju';
@override
String get deletePhoto => 'Dzēst fotogrāfiju';
@override
String get deletePhotoConfirm => 'Vai tiešām vēlaties dzēst šo fotogrāfiju?';
@override
String get barcode => 'Svītrkods';
@override
String get barcodeScan => 'Skenēt svītrkodu';
@override
String get scanBarcode => 'Skenēt svītrkodu';
@override
String get barcodeRequired => 'Nepieciešams svītrkods';
@override
String get minBarcodes => 'Vismaz';
@override
String get maxBarcodes => 'Maksimums';
@override
String get scanned => 'Skenēts';
@override
String get scannedBarcodes => 'Skenēti svītrkodi';
@override
String get barcodesRequired => 'Nepieciešami svītrkodi';
@override
String get enterBarcode => 'Ievadiet svītrkodu';
@override
String get barcodeEnterDescription => 'Lūdzu, ievadiet svītrkodus:';
@override
String barcodeNumberRequired(int number) => 'Svītrkods $number (obligāts)';
@override
String barcodeNumberOptional(int number) => 'Svītrkods $number (neobligāts)';
@override
String get barcodeError => 'Kļūda skenējot svītrkodu';
@override
String get cameraError => 'Kļūda inicializējot kameru';
@override
String get cameraNotReady => 'Kamera nav gatava vai nav pieejama';
@override
String get cameraNotAvailable => 'Kamera nav pieejama';
@override
String get cameraNotSupportedMessage => 'Šajā platformā kamera netiek atbalstīta.';
@override
String get cameraNotSupportedOnPlatform => 'Šajā platformā netiek atbalstīts';
@override
String get maxPhotosReached => 'Maksimums sasniegts';
@override
String get cameraReadyNoPreview => 'Kamera gatava (bez priekšskatījuma)';
@override
String get cameraLoading => 'Kamera ielādē...';
@override
String get cameraInitializing => 'Kamera tiek inicializēta...';
@override
String get cameraLoadingMessage => 'Lūdzu, uzgaidiet, kamēr kamera tiek ielādēta';
@override
String get addPhotos => 'Pievienot fotogrāfijas';
@override
String get addPhotosInstruction => 'Izmantojiet pogu "Izvēlēties fotogrāfiju", lai pievienotu attēlus no kameras vai cietā diska.';
@override
String get photoOf => 'no';
// ==================== CHAT ====================
@override
String get typeMessage => 'Ierakstiet ziņojumu...';
@override
String get send => 'Sūtīt';
@override
String get noSender => 'Sūtītājs nav pieejams';
@override
String get noSenderMessage => 'Sūtītājs nav pieejams. Lūdzu, piesakieties vēlreiz.';
@override
String get noRecipient => 'Saņēmējs nav konfigurēts';
@override
String get noRecipientMessage => 'Šai tērzēšanai nav konfigurēts saņēmējs.';
@override
String get messageSendError => 'Ziņojumu neizdevās nosūtīt.';
@override
String get photoSendError => 'Fotogrāfiju neizdevās nosūtīt.';
@override
String get photoProcessError => 'Fotogrāfiju neizdevās apstrādāt.';
@override
String get imageSendError => 'Attēlu neizdevās nosūtīt.';
@override
String get chatTypeJob => 'Darba specifisks';
@override
String get chatTypeGeneral => 'Vispārējs';
@override
String get jobNumber => 'Darba numurs';
@override
String get messages => 'Ziņojumi';
@override
String get selectPhoto => 'Izvēlēties fotogrāfiju';
@override
String get unreadMessages => 'Nelasīti ziņojumi';
// ==================== CARGO ====================
@override
String get cargoDetails => 'Kravas detaļas';
@override
String get itemName => 'Apraksts';
@override
String get itemNumber => 'Pozīcijas Nr.';
@override
String get item => 'Pozīcija';
@override
String get weightUnit => 'kg';
@override
String get dimensionUnit => 'cm';
@override
String get noCargoItems => 'Nav kravas pozīciju';
@override
String get noCargoItemsMessage => 'Šim darbam nav definētu kravas pozīciju.';
@override
String get article => 'Pozīcija';
// ==================== TASK TYPES ====================
@override
String get takePhotos => 'Uzņemt fotogrāfijas';
@override
String get photosCount => 'Fotogrāfijas';
@override
String get checklistPoints => 'Punkti';
@override
String get signatureRequiredText => 'Paraksts nepieciešams';
@override
String get scanBarcodes => 'Skenēt svītrkodus';
@override
String get barcodeCount => 'Kodi';
@override
String get commentOptional => 'Komentārs';
@override
String get genericTask => 'Vispārējs uzdevums';
@override
String get complete => 'Pabeigt';
@override
String get abort => 'Atcelt';
@override
String get optional => 'Neobligāts';
@override
String get skipTask => 'Izlaist';
// ==================== SETTINGS ====================
@override
String get language => 'Valoda';
@override
String get languageChanged => 'Valoda mainīta uz';
@override
String get appInfo => 'LIETOTNES INFO';
// ==================== STATUS ====================
@override
String get statusCreated => 'Izveidots';
@override
String get statusAssigned => 'Piešķirts';
@override
String get statusInProgress => 'Procesā';
@override
String get statusCompleted => 'Pabeigts';
@override
String get priorityLow => 'Zema';
@override
String get priorityMedium => 'Vidēja';
@override
String get priorityHigh => 'Augsta';
@override
String get priorityUrgent => 'Steidzama';
}

View File

@@ -0,0 +1,385 @@
import 'app_localizations.dart';
class AppLocalizationsPl extends AppLocalizations {
@override
String get languageName => 'Polski';
@override
String get flagEmoji => '🇵🇱';
// ==================== GENERAL ====================
@override
String get appTitle => 'VotianLT App';
@override
String get ok => 'OK';
@override
String get cancel => 'Anuluj';
@override
String get save => 'Zapisz';
@override
String get delete => 'Usuń';
@override
String get close => 'Zamknij';
@override
String get confirm => 'Potwierdź';
@override
String get error => 'Błąd';
@override
String get success => 'Sukces';
@override
String get loading => 'Ładowanie...';
@override
String get refresh => 'Odśwież';
@override
String get version => 'Wersja';
@override
String get unknown => 'Nieznany';
// ==================== NAVIGATION ====================
@override
String get jobs => 'Zadania';
@override
String get availableJobs => 'Dostępne Zadania';
@override
String get chats => 'Czaty';
@override
String get settings => 'Ustawienia';
@override
String get logout => 'Wyloguj';
@override
String get logoutConfirm => 'Wyloguj';
@override
String get logoutConfirmMessage => 'Czy na pewno chcesz się wylogować?';
@override
String get openChat => 'Otwórz czat';
@override
String get chatInfo => 'Info o czacie';
@override
String get routePlan => 'Planuj trasę';
// ==================== LOGIN ====================
@override
String get welcomeBack => 'Witaj ponownie';
@override
String get loginSubtitle => 'Zaloguj się do swojego konta';
@override
String get email => 'E-mail';
@override
String get password => 'Hasło';
@override
String get login => 'Zaloguj';
@override
String get loggingIn => 'Łączenie...';
@override
String get forgotPassword => 'Zapomniałeś hasła?';
@override
String get forgotPasswordMessage => 'Funkcja zapomnianego hasła jeszcze nie zaimplementowana';
@override
String get loginSuccess => 'Pomyślnie wylogowano';
@override
String get loginFailed => 'Logowanie nie powiodło się';
@override
String get connectionFailed => 'Błąd połączenia z serwerem (Upłynął czas).';
@override
String get connectionTimeout => 'Błąd połączenia z serwerem (Upłynął czas).';
@override
String get connecting => 'Łączenie z serwerem...';
@override
String get connectionError => 'Błąd połączenia';
@override
String get loginError => 'Błąd podczas logowania';
// ==================== JOBS ====================
@override
String get noJobsAssigned => 'Brak przypisanych zadań';
@override
String get noJobsMessage => 'Twoje przypisane zadania będą wyświetlane tutaj.';
@override
String get pullToRefresh => 'Przeciągnij w dół, aby odświeżyć';
@override
String get newLabel => 'NOWE';
@override
String get tasksToComplete => 'Zadania do wykonania';
@override
String get pickup => 'Odbiór';
@override
String get delivery => 'Dostawa';
@override
String get created => 'Utworzono';
@override
String get status => 'Status';
@override
String get priority => 'Priorytet';
@override
String get dueDate => 'Termin';
@override
String get location => 'Lokalizacja';
@override
String get description => 'Opis';
@override
String get cargo => 'Ładunek';
@override
String get quantity => 'Ilość';
@override
String get weight => 'Waga';
@override
String get dimensions => 'Wymiary';
@override
String get jobDeleted => 'Zadanie usunięte';
@override
String get jobDeleteError => 'Błąd podczas usuwania zadania';
@override
String get jobCompleted => 'Zadanie ukończone';
@override
String get from => 'Z';
@override
String get to => 'do';
@override
String get jobsUpdated => 'Zadania zaktualizowane';
@override
String get connectionRestored => 'Połączenie przywrócone. Ładowanie zadań...';
@override
String get connectionLost => 'Utracono połączenie. Offline.';
@override
String get offline => 'Offline';
@override
String get deleteJob => 'Usuń zadanie';
@override
String get jobRemoved => 'zostało usunięte';
@override
String get newJobReceived => 'Otrzymano nowe zadanie';
// ==================== TASKS ====================
@override
String get tasks => 'Zadania';
@override
String get noTasks => 'Brak zadań';
@override
String get noTasksMessage => 'Brak zdefiniowanych zadań dla tego zadania.';
@override
String get taskOrder => 'Kolejność';
@override
String get confirmationRequired => 'Wymagane potwierdzenie';
@override
String get confirmationDescription => 'Kliknij przycisk, aby ukończyć zadanie.';
@override
String get checklist => 'Lista kontrolna';
@override
String get checklistDescription => 'Proszę zaznaczyć wszystkie punkty:';
@override
String get completeTask => 'Ukończ zadanie';
@override
String get completeTaskConfirm => 'Czy chcesz oznaczyć to zadanie jako ukończone?';
@override
String get completeTaskNote => 'Notatka (opcjonalnie)';
@override
String get taskCompleted => 'Zadanie ukończone';
@override
String get comment => 'Komentarz';
@override
String get commentRequired => 'Komentarz (wymagany)';
@override
String get enterComment => 'Wprowadź komentarz';
@override
String get commentDescription => 'Proszę wprowadzić komentarz:';
@override
String get finish => 'Zakończ';
@override
String get signature => 'Podpis';
@override
String get signatureCapture => 'Przechwyć podpis';
@override
String get signatureRequired => 'Proszę przechwycić podpis.';
@override
String get clear => 'Wyczyść';
@override
String get signatureError => 'Błąd podczas zapisywania podpisu';
@override
String get signatureInstruction => 'Proszę podpisać się w polu poniżej (mysz lub palec).';
@override
String get photoCapture => 'Zrób zdjęcia';
@override
String get requiredPhotos => 'Wymagane zdjęcia';
@override
String get photosTaken => 'Wykonane';
@override
String get photos => 'Zdjęcia';
@override
String get takePhoto => 'Zrób zdjęcie';
@override
String get selectFromLibrary => 'Wybierz z biblioteki';
@override
String get retakePhoto => 'Ponów';
@override
String get photoRequired => 'Zdjęcie wymagane';
@override
String get minPhotos => 'Co najmniej';
@override
String get maxPhotos => 'Maksimum';
@override
String get photoError => 'Błąd podczas robienia zdjęcia';
@override
String get deletePhoto => 'Usuń zdjęcie';
@override
String get deletePhotoConfirm => 'Czy na pewno chcesz usunąć to zdjęcie?';
@override
String get barcode => 'Kod kreskowy';
@override
String get barcodeScan => 'Skanuj kod kreskowy';
@override
String get scanBarcode => 'Skanuj kod kreskowy';
@override
String get barcodeRequired => 'Kod kreskowy wymagany';
@override
String get minBarcodes => 'Co najmniej';
@override
String get maxBarcodes => 'Maksimum';
@override
String get scanned => 'Zeskanowano';
@override
String get scannedBarcodes => 'Zeskanowane kody kreskowe';
@override
String get barcodesRequired => 'Wymagane kody kreskowe';
@override
String get enterBarcode => 'Wprowadź kod kreskowy';
@override
String get barcodeEnterDescription => 'Proszę wprowadzić kody kreskowe:';
@override
String barcodeNumberRequired(int number) => 'Kod kreskowy $number (wymagany)';
@override
String barcodeNumberOptional(int number) => 'Kod kreskowy $number (opcjonalny)';
@override
String get barcodeError => 'Błąd podczas skanowania kodu kreskowego';
@override
String get cameraError => 'Błąd podczas inicjalizacji kamery';
@override
String get cameraNotReady => 'Kamera nie jest gotowa lub niedostępna';
@override
String get cameraNotAvailable => 'Kamera niedostępna';
@override
String get cameraNotSupportedMessage => 'Kamera nie jest obsługiwana na tej platformie.';
@override
String get cameraNotSupportedOnPlatform => 'Nieobsługiwane na tej platformie';
@override
String get maxPhotosReached => 'Maksimum osiągnięte';
@override
String get cameraReadyNoPreview => 'Kamera gotowa (bez podglądu)';
@override
String get cameraLoading => 'Kamera ładuje się...';
@override
String get cameraInitializing => 'Inicjalizacja kamery...';
@override
String get cameraLoadingMessage => 'Proszę czekać, trwa ładowanie kamery';
@override
String get addPhotos => 'Dodaj zdjęcia';
@override
String get addPhotosInstruction => 'Użyj przycisku "Wybierz zdjęcie", aby dodać obrazy z kamery lub dysku twardego.';
@override
String get photoOf => 'z';
// ==================== CHAT ====================
@override
String get typeMessage => 'Wpisz wiadomość...';
@override
String get send => 'Wyślij';
@override
String get noSender => 'Brak dostępnego nadawcy';
@override
String get noSenderMessage => 'Brak dostępnego nadawcy. Proszę zalogować się ponownie.';
@override
String get noRecipient => 'Brak skonfigurowanego odbiorcy';
@override
String get noRecipientMessage => 'Brak skonfigurowanego odbiorcy dla tego czatu.';
@override
String get messageSendError => 'Wiadomość nie mogła zostać wysłana.';
@override
String get photoSendError => 'Zdjęcie nie mogło zostać wysłane.';
@override
String get photoProcessError => 'Zdjęcie nie mogło zostać przetworzone.';
@override
String get imageSendError => 'Obraz nie mógł zostać wysłany.';
@override
String get chatTypeJob => 'Specyficzne dla zadania';
@override
String get chatTypeGeneral => 'Ogólny';
@override
String get jobNumber => 'Numer zadania';
@override
String get messages => 'Wiadomości';
@override
String get selectPhoto => 'Wybierz zdjęcie';
@override
String get unreadMessages => 'Nieprzeczytane wiadomości';
// ==================== CARGO ====================
@override
String get cargoDetails => 'Szczegóły ładunku';
@override
String get itemName => 'Opis';
@override
String get itemNumber => 'Nr pozycji';
@override
String get item => 'Pozycja';
@override
String get weightUnit => 'kg';
@override
String get dimensionUnit => 'cm';
@override
String get noCargoItems => 'Brak pozycji ładunku';
@override
String get noCargoItemsMessage => 'Brak pozycji ładunku zdefiniowanych dla tego zadania.';
@override
String get article => 'Pozycja';
// ==================== TASK TYPES ====================
@override
String get takePhotos => 'Zrób zdjęcia';
@override
String get photosCount => 'Zdjęcia';
@override
String get checklistPoints => 'Punkty';
@override
String get signatureRequiredText => 'Wymagany podpis';
@override
String get scanBarcodes => 'Skanuj kody kreskowe';
@override
String get barcodeCount => 'Kody';
@override
String get commentOptional => 'Komentarz';
@override
String get genericTask => 'Zadanie ogólne';
@override
String get complete => 'Zakończ';
@override
String get abort => 'Anuluj';
@override
String get optional => 'Opcjonalny';
@override
String get skipTask => 'Pomiń';
// ==================== SETTINGS ====================
@override
String get language => 'Język';
@override
String get languageChanged => 'Język zmieniony na';
@override
String get appInfo => 'INFO O APLIKACJI';
// ==================== STATUS ====================
@override
String get statusCreated => 'Utworzono';
@override
String get statusAssigned => 'Przypisano';
@override
String get statusInProgress => 'W trakcie';
@override
String get statusCompleted => 'Ukończono';
@override
String get priorityLow => 'Niski';
@override
String get priorityMedium => 'Średni';
@override
String get priorityHigh => 'Wysoki';
@override
String get priorityUrgent => 'Pilny';
}

View File

@@ -0,0 +1,385 @@
import 'app_localizations.dart';
class AppLocalizationsRu extends AppLocalizations {
@override
String get languageName => 'Русский';
@override
String get flagEmoji => '🇷🇺';
// ==================== GENERAL ====================
@override
String get appTitle => 'VotianLT App';
@override
String get ok => 'OK';
@override
String get cancel => 'Отмена';
@override
String get save => 'Сохранить';
@override
String get delete => 'Удалить';
@override
String get close => 'Закрыть';
@override
String get confirm => 'Подтвердить';
@override
String get error => 'Ошибка';
@override
String get success => 'Успех';
@override
String get loading => 'Загрузка...';
@override
String get refresh => 'Обновить';
@override
String get version => 'Версия';
@override
String get unknown => 'Неизвестно';
// ==================== NAVIGATION ====================
@override
String get jobs => 'Задания';
@override
String get availableJobs => 'Доступные задания';
@override
String get chats => 'Чаты';
@override
String get settings => 'Настройки';
@override
String get logout => 'Выход';
@override
String get logoutConfirm => 'Выход';
@override
String get logoutConfirmMessage => 'Вы действительно хотите выйти?';
@override
String get openChat => 'Открыть чат';
@override
String get chatInfo => 'Информация о чате';
@override
String get routePlan => 'Планировать маршрут';
// ==================== LOGIN ====================
@override
String get welcomeBack => 'С возвращением';
@override
String get loginSubtitle => 'Войдите в свою учетную запись';
@override
String get email => 'Эл. почта';
@override
String get password => 'Пароль';
@override
String get login => 'Войти';
@override
String get loggingIn => 'Подключение...';
@override
String get forgotPassword => 'Забыли пароль?';
@override
String get forgotPasswordMessage => 'Функция восстановления пароля еще не реализована';
@override
String get loginSuccess => 'Успешный выход из системы';
@override
String get loginFailed => 'Ошибка входа';
@override
String get connectionFailed => 'Ошибка подключения к серверу (Таймаут).';
@override
String get connectionTimeout => 'Ошибка подключения к серверу (Таймаут).';
@override
String get connecting => 'Подключение к серверу...';
@override
String get connectionError => 'Ошибка подключения';
@override
String get loginError => 'Ошибка при входе';
// ==================== JOBS ====================
@override
String get noJobsAssigned => 'Нет назначенных заданий';
@override
String get noJobsMessage => 'Ваши назначенные задания будут отображаться здесь.';
@override
String get pullToRefresh => 'Потяните вниз, чтобы обновить';
@override
String get newLabel => 'НОВОЕ';
@override
String get tasksToComplete => 'Задачи для выполнения';
@override
String get pickup => 'Забор';
@override
String get delivery => 'Доставка';
@override
String get created => 'Создано';
@override
String get status => 'Статус';
@override
String get priority => 'Приоритет';
@override
String get dueDate => 'Срок выполнения';
@override
String get location => 'Местоположение';
@override
String get description => 'Описание';
@override
String get cargo => 'Груз';
@override
String get quantity => 'Количество';
@override
String get weight => 'Вес';
@override
String get dimensions => 'Размеры';
@override
String get jobDeleted => 'Задание удалено';
@override
String get jobDeleteError => 'Ошибка при удалении задания';
@override
String get jobCompleted => 'Задание завершено';
@override
String get from => 'Из';
@override
String get to => 'в';
@override
String get jobsUpdated => 'Задания обновлены';
@override
String get connectionRestored => 'Соединение восстановлено. Загрузка заданий...';
@override
String get connectionLost => 'Соединение потеряно. Офлайн.';
@override
String get offline => 'Офлайн';
@override
String get deleteJob => 'Удалить задание';
@override
String get jobRemoved => 'было удалено';
@override
String get newJobReceived => 'Получено новое задание';
// ==================== TASKS ====================
@override
String get tasks => 'Задачи';
@override
String get noTasks => 'Нет задач';
@override
String get noTasksMessage => 'Для этого задания не определены задачи.';
@override
String get taskOrder => 'Порядок';
@override
String get confirmationRequired => 'Требуется подтверждение';
@override
String get confirmationDescription => 'Нажмите кнопку, чтобы выполнить задачу.';
@override
String get checklist => 'Контрольный список';
@override
String get checklistDescription => 'Пожалуйста, отметьте все пункты:';
@override
String get completeTask => 'Завершить задачу';
@override
String get completeTaskConfirm => 'Хотите отметить эту задачу как выполненную?';
@override
String get completeTaskNote => 'Примечание (необязательно)';
@override
String get taskCompleted => 'Задача выполнена';
@override
String get comment => 'Комментарий';
@override
String get commentRequired => 'Комментарий (обязательно)';
@override
String get enterComment => 'Введите комментарий';
@override
String get commentDescription => 'Пожалуйста, введите комментарий:';
@override
String get finish => 'Готово';
@override
String get signature => 'Подпись';
@override
String get signatureCapture => 'Захватить подпись';
@override
String get signatureRequired => 'Пожалуйста, сделайте подпись.';
@override
String get clear => 'Очистить';
@override
String get signatureError => 'Ошибка при сохранении подписи';
@override
String get signatureInstruction => 'Пожалуйста, подпишитесь в поле ниже (мышь или палец).';
@override
String get photoCapture => 'Сделать фото';
@override
String get requiredPhotos => 'Необходимые фото';
@override
String get photosTaken => 'Сделано';
@override
String get photos => 'Фото';
@override
String get takePhoto => 'Сделать фото';
@override
String get selectFromLibrary => 'Выбрать из библиотеки';
@override
String get retakePhoto => 'Переснять';
@override
String get photoRequired => 'Требуется фото';
@override
String get minPhotos => 'Минимум';
@override
String get maxPhotos => 'Максимум';
@override
String get photoError => 'Ошибка при съемке фото';
@override
String get deletePhoto => 'Удалить фото';
@override
String get deletePhotoConfirm => 'Вы действительно хотите удалить это фото?';
@override
String get barcode => 'Штрих-код';
@override
String get barcodeScan => 'Сканировать штрих-код';
@override
String get scanBarcode => 'Сканировать штрих-код';
@override
String get barcodeRequired => 'Требуется штрих-код';
@override
String get minBarcodes => 'Минимум';
@override
String get maxBarcodes => 'Максимум';
@override
String get scanned => 'Отсканировано';
@override
String get scannedBarcodes => 'Отсканированные штрих-коды';
@override
String get barcodesRequired => 'Требуются штрих-коды';
@override
String get enterBarcode => 'Введите штрих-код';
@override
String get barcodeEnterDescription => 'Пожалуйста, введите штрих-коды:';
@override
String barcodeNumberRequired(int number) => 'Штрих-код $number (обязательно)';
@override
String barcodeNumberOptional(int number) => 'Штрих-код $number (необязательно)';
@override
String get barcodeError => 'Ошибка при сканировании штрих-кода';
@override
String get cameraError => 'Ошибка инициализации камеры';
@override
String get cameraNotReady => 'Камера не готова или недоступна';
@override
String get cameraNotAvailable => 'Камера недоступна';
@override
String get cameraNotSupportedMessage => 'Камера не поддерживается на этой платформе.';
@override
String get cameraNotSupportedOnPlatform => 'Не поддерживается на этой платформе';
@override
String get maxPhotosReached => 'Максимум достигнут';
@override
String get cameraReadyNoPreview => 'Камера готова (без предпросмотра)';
@override
String get cameraLoading => 'Камера загружается...';
@override
String get cameraInitializing => 'Инициализация камеры...';
@override
String get cameraLoadingMessage => 'Пожалуйста, подождите, пока загружается камера';
@override
String get addPhotos => 'Добавить фото';
@override
String get addPhotosInstruction => 'Используйте кнопку "Выбрать фото", чтобы добавить изображения с камеры или жёсткого диска.';
@override
String get photoOf => 'из';
// ==================== CHAT ====================
@override
String get typeMessage => 'Введите сообщение...';
@override
String get send => 'Отправить';
@override
String get noSender => 'Отправитель недоступен';
@override
String get noSenderMessage => 'Отправитель недоступен. Пожалуйста, войдите снова.';
@override
String get noRecipient => 'Получатель не настроен';
@override
String get noRecipientMessage => 'Получатель не настроен для этого чата.';
@override
String get messageSendError => 'Сообщение не удалось отправить.';
@override
String get photoSendError => 'Фото не удалось отправить.';
@override
String get photoProcessError => 'Фото не удалось обработать.';
@override
String get imageSendError => 'Изображение не удалось отправить.';
@override
String get chatTypeJob => 'Специфичный для задания';
@override
String get chatTypeGeneral => 'Общий';
@override
String get jobNumber => 'Номер задания';
@override
String get messages => 'Сообщения';
@override
String get selectPhoto => 'Выбрать фото';
@override
String get unreadMessages => 'Непрочитанные сообщения';
// ==================== CARGO ====================
@override
String get cargoDetails => 'Детали груза';
@override
String get itemName => 'Описание';
@override
String get itemNumber => 'Номер позиции';
@override
String get item => 'Позиция';
@override
String get weightUnit => 'кг';
@override
String get dimensionUnit => 'см';
@override
String get noCargoItems => 'Нет позиций груза';
@override
String get noCargoItemsMessage => 'Для этого задания не определены позиции груза.';
@override
String get article => 'Позиция';
// ==================== TASK TYPES ====================
@override
String get takePhotos => 'Сделать фото';
@override
String get photosCount => 'Фото';
@override
String get checklistPoints => 'Пункты';
@override
String get signatureRequiredText => 'Требуется подпись';
@override
String get scanBarcodes => 'Сканировать штрих-коды';
@override
String get barcodeCount => 'Коды';
@override
String get commentOptional => 'Комментарий';
@override
String get genericTask => 'Общая задача';
@override
String get complete => 'Завершить';
@override
String get abort => 'Отмена';
@override
String get optional => 'Необязательно';
@override
String get skipTask => 'Пропустить';
// ==================== SETTINGS ====================
@override
String get language => 'Язык';
@override
String get languageChanged => 'Язык изменен на';
@override
String get appInfo => 'ИНФОРМАЦИЯ О ПРИЛОЖЕНИИ';
// ==================== STATUS ====================
@override
String get statusCreated => 'Создано';
@override
String get statusAssigned => 'Назначено';
@override
String get statusInProgress => 'В процессе';
@override
String get statusCompleted => 'Завершено';
@override
String get priorityLow => 'Низкий';
@override
String get priorityMedium => 'Средний';
@override
String get priorityHigh => 'Высокий';
@override
String get priorityUrgent => 'Срочный';
}

View File

@@ -0,0 +1,385 @@
import 'app_localizations.dart';
class AppLocalizationsTr extends AppLocalizations {
@override
String get languageName => 'Türkçe';
@override
String get flagEmoji => '🇹🇷';
// ==================== GENERAL ====================
@override
String get appTitle => 'VotianLT App';
@override
String get ok => 'Tamam';
@override
String get cancel => 'İptal';
@override
String get save => 'Kaydet';
@override
String get delete => 'Sil';
@override
String get close => 'Kapat';
@override
String get confirm => 'Onayla';
@override
String get error => 'Hata';
@override
String get success => 'Başarılı';
@override
String get loading => 'Yükleniyor...';
@override
String get refresh => 'Yenile';
@override
String get version => 'Versiyon';
@override
String get unknown => 'Bilinmiyor';
// ==================== NAVIGATION ====================
@override
String get jobs => 'İşler';
@override
String get availableJobs => 'Mevcut İşler';
@override
String get chats => 'Sohbetler';
@override
String get settings => 'Ayarlar';
@override
String get logout => 'Çıkış';
@override
String get logoutConfirm => 'Çıkış';
@override
String get logoutConfirmMessage => 'Gerçekten çıkış yapmak istiyor musunuz?';
@override
String get openChat => 'Sohbeti aç';
@override
String get chatInfo => 'Sohbet bilgisi';
@override
String get routePlan => 'Rota planla';
// ==================== LOGIN ====================
@override
String get welcomeBack => 'Tekrar hoş geldiniz';
@override
String get loginSubtitle => 'Hesabınıza giriş yapın';
@override
String get email => 'E-posta';
@override
String get password => 'Şifre';
@override
String get login => 'Giriş';
@override
String get loggingIn => 'Bağlanıyor...';
@override
String get forgotPassword => 'Şifrenizi mi unuttunuz?';
@override
String get forgotPasswordMessage => 'Şifremi unuttum özelliği henüz uygulanmadı';
@override
String get loginSuccess => 'Başarıyla çıkış yapıldı';
@override
String get loginFailed => 'Giriş başarısız';
@override
String get connectionFailed => 'Sunucu bağlantısı başarısız (Zaman aşımı).';
@override
String get connectionTimeout => 'Sunucu bağlantısı başarısız (Zaman aşımı).';
@override
String get connecting => 'Sunucuya bağlanılıyor...';
@override
String get connectionError => 'Bağlantı hatası';
@override
String get loginError => 'Giriş sırasında hata';
// ==================== JOBS ====================
@override
String get noJobsAssigned => 'Atanmış iş yok';
@override
String get noJobsMessage => 'Atanmış işleriniz burada görüntülenecek.';
@override
String get pullToRefresh => 'Yenilemek için aşağı çekin';
@override
String get newLabel => 'YENİ';
@override
String get tasksToComplete => 'Tamamlanacak görevler';
@override
String get pickup => 'Alım';
@override
String get delivery => 'Teslimat';
@override
String get created => 'Oluşturuldu';
@override
String get status => 'Durum';
@override
String get priority => 'Öncelik';
@override
String get dueDate => 'Bitiş tarihi';
@override
String get location => 'Konum';
@override
String get description => 'ıklama';
@override
String get cargo => 'Yük';
@override
String get quantity => 'Miktar';
@override
String get weight => 'ırlık';
@override
String get dimensions => 'Boyutlar';
@override
String get jobDeleted => 'İş silindi';
@override
String get jobDeleteError => 'İş silinirken hata oluştu';
@override
String get jobCompleted => 'İş tamamlandı';
@override
String get from => 'Kimden';
@override
String get to => 'den';
@override
String get jobsUpdated => 'İşler güncellendi';
@override
String get connectionRestored => 'Bağlantı geri yüklendi. İşler yükleniyor...';
@override
String get connectionLost => 'Bağlantı kesildi. Çevrimdışı.';
@override
String get offline => 'Çevrimdışı';
@override
String get deleteJob => 'İşi sil';
@override
String get jobRemoved => 'kaldırıldı';
@override
String get newJobReceived => 'Yeni iş alındı';
// ==================== TASKS ====================
@override
String get tasks => 'Görevler';
@override
String get noTasks => 'Görev yok';
@override
String get noTasksMessage => 'Bu iş için tanımlanmış görev yok.';
@override
String get taskOrder => 'Sıra';
@override
String get confirmationRequired => 'Onay gerekli';
@override
String get confirmationDescription => 'Görevi tamamlamak için butona tıklayın.';
@override
String get checklist => 'Kontrol listesi';
@override
String get checklistDescription => 'Lütfen tüm maddeleri işaretleyin:';
@override
String get completeTask => 'Görevi tamamla';
@override
String get completeTaskConfirm => 'Bu görevi tamamlandı olarak işaretlemek istiyor musunuz?';
@override
String get completeTaskNote => 'Not (isteğe bağlı)';
@override
String get taskCompleted => 'Görev tamamlandı';
@override
String get comment => 'Yorum';
@override
String get commentRequired => 'Yorum (gerekli)';
@override
String get enterComment => 'Yorum gir';
@override
String get commentDescription => 'Lütfen bir yorum girin:';
@override
String get finish => 'Bitir';
@override
String get signature => 'İmza';
@override
String get signatureCapture => 'İmza yakalama';
@override
String get signatureRequired => 'Lütfen bir imza yakalayın.';
@override
String get clear => 'Temizle';
@override
String get signatureError => 'İmza kaydedilirken hata oluştu';
@override
String get signatureInstruction => 'Lütfen aşağıdaki alana imzanızı atın (fare veya parmak).';
@override
String get photoCapture => 'Fotoğraf çek';
@override
String get requiredPhotos => 'Gerekli fotoğraflar';
@override
String get photosTaken => 'Çekilen';
@override
String get photos => 'Fotoğraflar';
@override
String get takePhoto => 'Fotoğraf çek';
@override
String get selectFromLibrary => 'Kütüphaneden seç';
@override
String get retakePhoto => 'Tekrar çek';
@override
String get photoRequired => 'Fotoğraf gerekli';
@override
String get minPhotos => 'En az';
@override
String get maxPhotos => 'En fazla';
@override
String get photoError => 'Fotoğraf çekilirken hata oluştu';
@override
String get deletePhoto => 'Fotoğrafı sil';
@override
String get deletePhotoConfirm => 'Bu fotoğrafı gerçekten silmek istiyor musunuz?';
@override
String get barcode => 'Barkod';
@override
String get barcodeScan => 'Barkod tara';
@override
String get scanBarcode => 'Barkod tara';
@override
String get barcodeRequired => 'Barkod gerekli';
@override
String get minBarcodes => 'En az';
@override
String get maxBarcodes => 'En fazla';
@override
String get scanned => 'Tarandı';
@override
String get scannedBarcodes => 'Taranan barkodlar';
@override
String get barcodesRequired => 'Barkodlar gerekli';
@override
String get enterBarcode => 'Barkod gir';
@override
String get barcodeEnterDescription => 'Lütfen barkodları girin:';
@override
String barcodeNumberRequired(int number) => 'Barkod $number (gerekli)';
@override
String barcodeNumberOptional(int number) => 'Barkod $number (isteğe bağlı)';
@override
String get barcodeError => 'Barkod taranırken hata oluştu';
@override
String get cameraError => 'Kamera başlatılırken hata oluştu';
@override
String get cameraNotReady => 'Kamera hazır değil veya kullanılamıyor';
@override
String get cameraNotAvailable => 'Kamera kullanılamıyor';
@override
String get cameraNotSupportedMessage => 'Bu platformda kamera desteklenmiyor.';
@override
String get cameraNotSupportedOnPlatform => 'Bu platformda desteklenmiyor';
@override
String get maxPhotosReached => 'Maksimum ulaşıldı';
@override
String get cameraReadyNoPreview => 'Kamera hazır (önizleme yok)';
@override
String get cameraLoading => 'Kamera yükleniyor...';
@override
String get cameraInitializing => 'Kamera başlatılıyor...';
@override
String get cameraLoadingMessage => 'Kamera yüklenirken lütfen bekleyin';
@override
String get addPhotos => 'Fotoğraf ekle';
@override
String get addPhotosInstruction => 'Kamera veya sabit diskten görüntü eklemek için "Fotoğraf seç" düğmesini kullanın.';
@override
String get photoOf => '/';
// ==================== CHAT ====================
@override
String get typeMessage => 'Mesaj yazın...';
@override
String get send => 'Gönder';
@override
String get noSender => 'Gönderen mevcut değil';
@override
String get noSenderMessage => 'Gönderen mevcut değil. Lütfen tekrar giriş yapın.';
@override
String get noRecipient => 'Alıcı yapılandırılmamış';
@override
String get noRecipientMessage => 'Bu sohbet için alıcı yapılandırılmamış.';
@override
String get messageSendError => 'Mesaj gönderilemedi.';
@override
String get photoSendError => 'Fotoğraf gönderilemedi.';
@override
String get photoProcessError => 'Fotoğraf işlenemedi.';
@override
String get imageSendError => 'Görüntü gönderilemedi.';
@override
String get chatTypeJob => 'İşe özel';
@override
String get chatTypeGeneral => 'Genel';
@override
String get jobNumber => 'İş numarası';
@override
String get messages => 'Mesajlar';
@override
String get selectPhoto => 'Fotoğraf seç';
@override
String get unreadMessages => 'Okunmamış mesajlar';
// ==================== CARGO ====================
@override
String get cargoDetails => 'Yük Detayları';
@override
String get itemName => 'ıklama';
@override
String get itemNumber => 'Pozisyon No';
@override
String get item => 'Pozisyon';
@override
String get weightUnit => 'kg';
@override
String get dimensionUnit => 'cm';
@override
String get noCargoItems => 'Yük kalemi yok';
@override
String get noCargoItemsMessage => 'Bu iş için tanımlanmış yük kalemi yok.';
@override
String get article => 'Kalem';
// ==================== TASK TYPES ====================
@override
String get takePhotos => 'Fotoğraf çek';
@override
String get photosCount => 'Fotoğraflar';
@override
String get checklistPoints => 'Noktalar';
@override
String get signatureRequiredText => 'İmza gerekli';
@override
String get scanBarcodes => 'Barkodları tara';
@override
String get barcodeCount => 'Kodlar';
@override
String get commentOptional => 'Yorum';
@override
String get genericTask => 'Genel görev';
@override
String get complete => 'Tamamla';
@override
String get abort => 'İptal';
@override
String get optional => 'İsteğe bağlı';
@override
String get skipTask => 'Atla';
// ==================== SETTINGS ====================
@override
String get language => 'Dil';
@override
String get languageChanged => 'Dil değiştirildi:';
@override
String get appInfo => 'UYGULAMA BİLGİSİ';
// ==================== STATUS ====================
@override
String get statusCreated => 'Oluşturuldu';
@override
String get statusAssigned => 'Atandı';
@override
String get statusInProgress => 'Devam ediyor';
@override
String get statusCompleted => 'Tamamlandı';
@override
String get priorityLow => 'Düşük';
@override
String get priorityMedium => 'Orta';
@override
String get priorityHigh => 'Yüksek';
@override
String get priorityUrgent => 'Acil';
}

383
app/lib/login_view.dart Normal file
View File

@@ -0,0 +1,383 @@
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:votianlt_app/services/developer.dart' as developer;
import 'package:flutter/material.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'services/websocket_service.dart';
import 'services/dart_mq.dart';
import 'services/database_service.dart';
import 'app_state.dart';
import 'l10n/app_localizations.dart';
class LoginView extends StatefulWidget {
const LoginView({super.key, this.suppressConnectionSnack = false});
// If true, suppress connection-related SnackBars until the user attempts login
final bool suppressConnectionSnack;
@override
State<LoginView> createState() => _LoginViewState();
}
class _LoginViewState extends State<LoginView> {
final _formKey = GlobalKey<FormState>();
final _emailController = TextEditingController();
final _passwordController = TextEditingController();
bool _isPasswordVisible = false;
bool _isLoggingIn = false;
final StompService _stompService = StompService();
final AppState _appState = AppState();
// DartMQ subscriptions for proper cleanup
DartMQSubscription? _connectionStatusSubscription;
DartMQSubscription? _authResponseSubscription;
bool _logoutNoticeShown = false;
bool _hasNavigatedToJobs = false;
String _appVersion = '';
@override
void initState() {
super.initState();
// Pre-populate with test data
if (kDebugMode) {
_emailController.text = 'mail@svencarstensen.de';
_passwordController.text = 'test123';
}
_loadAppVersion();
_initializeStompService();
// If we came here due to logout, show only a success message and suppress other connection snacks
if (widget.suppressConnectionSnack && !_logoutNoticeShown) {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (!mounted) return;
_logoutNoticeShown = true;
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(AppLocalizations.of(context).loginSuccess), backgroundColor: Colors.green, duration: const Duration(seconds: 1)));
});
}
}
@override
void dispose() {
// Cancel all stream subscriptions to prevent setState() after dispose
_connectionStatusSubscription?.cancel();
_authResponseSubscription?.cancel();
_emailController.dispose();
_passwordController.dispose();
// Don't dispose the singleton StompService as it may be used elsewhere
// _stompService.dispose();
super.dispose();
}
Future<void> _loadAppVersion() async {
try {
final PackageInfo packageInfo = await PackageInfo.fromPlatform();
setState(() {
_appVersion = packageInfo.version;
});
} catch (e) {
developer.log('Error loading app version: $e', name: 'LoginView');
}
}
void _initializeStompService() {
// Listen to connection status changes via dart_mq
// Note: Don't reset _isLoggingIn here - the login flow in _handleLogin
// manages button state through its own error/success handling.
_connectionStatusSubscription = DartMQ().subscribe<bool>(MQTopics.connectionStatus, (isConnected) {
if (mounted) {
setState(() {});
}
});
// Listen to authentication responses via dart_mq
_authResponseSubscription = DartMQ().subscribe<Map<String, dynamic>>(MQTopics.authResponse, (response) {
final responseTime = DateTime.now();
developer.log('=== AUTHENTICATION RESPONSE RECEIVED ===', name: 'LoginView');
developer.log('Timestamp: ${responseTime.toIso8601String()}', name: 'LoginView');
developer.log('Response data: $response', name: 'LoginView');
if (mounted) {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (!mounted) return;
setState(() {
_isLoggingIn = false;
});
if (response['success'] == true) {
// Prevent duplicate navigation from multiple auth responses
if (_hasNavigatedToJobs) {
developer.log('Already navigated to jobs view - ignoring duplicate auth response', name: 'LoginView');
return;
}
_hasNavigatedToJobs = true;
final message = response['message'] ?? 'Anmeldung erfolgreich';
final email = _emailController.text.trim();
final password = _passwordController.text;
developer.log('=== LOGIN SUCCESS ===', name: 'LoginView');
developer.log('Email: $email', name: 'LoginView');
developer.log('Message: $message', name: 'LoginView');
// Store email as login identifier
_appState.setLoggedInEmail(email);
// Save credentials for auto-login on app restart
DatabaseService().saveCredentials(email, password);
// Navigate directly to jobs view - jobs will be loaded there
developer.log('Navigating to jobs view - jobs will be loaded there...', name: 'LoginView');
Navigator.of(context).pushReplacementNamed('/jobs');
} else {
final errorMessage = response['message'] ?? 'Unbekannter Fehler';
final errorCode = response['code'] ?? 'No code';
developer.log('=== LOGIN FAILURE ===', name: 'LoginView');
developer.log('Error message: $errorMessage', name: 'LoginView');
developer.log('Error code: $errorCode', name: 'LoginView');
developer.log('Full error response: $response', name: 'LoginView');
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('${AppLocalizations.of(context).loginFailed}: $errorMessage'), backgroundColor: Colors.red, duration: const Duration(seconds: 1)));
}
});
} else {
developer.log('Widget not mounted - skipping UI updates for auth response', name: 'LoginView');
}
developer.log('Authentication response processing completed', name: 'LoginView');
});
}
Future<void> _handleLogin() async {
final loginStartTime = DateTime.now();
final sessionId = loginStartTime.millisecondsSinceEpoch.toString();
developer.log('=== LOGIN ATTEMPT STARTED ===', name: 'LoginView');
developer.log('Session ID: $sessionId', name: 'LoginView');
developer.log('Timestamp: ${loginStartTime.toIso8601String()}', name: 'LoginView');
if (!_formKey.currentState!.validate()) {
developer.log('Login validation failed - form is invalid', name: 'LoginView');
return;
}
if (_isLoggingIn) {
developer.log('Login already in progress - ignoring duplicate request', name: 'LoginView');
return;
}
String email = _emailController.text.trim();
developer.log('Login attempt for email: $email', name: 'LoginView');
developer.log('Password length: ${_passwordController.text.length} characters', name: 'LoginView');
// Capture ScaffoldMessenger and localizations before any async operations
final scaffoldMessenger = ScaffoldMessenger.of(context);
final localizations = AppLocalizations.of(context);
if (!_stompService.isConnected) {
developer.log('Not connected to STOMP server - establishing connection first', name: 'LoginView');
developer.log('STOMP service connection state: ${_stompService.isConnected}', name: 'LoginView');
// Always attempt connection to fixed STOMP endpoint (no discovery gating)
// Show connecting message
if (!widget.suppressConnectionSnack) {
scaffoldMessenger.showSnackBar(SnackBar(content: Text(localizations.connecting), backgroundColor: Colors.blue, duration: const Duration(seconds: 1)));
}
// Set loading state
setState(() {
_isLoggingIn = true;
});
try {
// Start connection to STOMP server
await _stompService.connect();
// Check if already connected after connect returns
if (!_stompService.isConnected) {
// Wait for connection to be established with a timeout
try {
final completer = Completer<bool>();
final subscription = DartMQ().subscribe<bool>(MQTopics.connectionStatus, (isConnected) {
if (isConnected && !completer.isCompleted) {
completer.complete(true);
}
});
await completer.future.timeout(const Duration(seconds: 12));
subscription.cancel();
developer.log('STOMP connection established - proceeding with login', name: 'LoginView');
} on TimeoutException {
developer.log('STOMP connection timed out', name: 'LoginView');
}
} else {
developer.log('STOMP already connected after connect - proceeding with login', name: 'LoginView');
}
// Check if connection was successful
if (!_stompService.isConnected) {
setState(() {
_isLoggingIn = false;
});
scaffoldMessenger.showSnackBar(SnackBar(content: Text(localizations.connectionTimeout), backgroundColor: Colors.red, duration: const Duration(seconds: 2)));
return;
}
} catch (e, stackTrace) {
setState(() {
_isLoggingIn = false;
});
developer.log('Error connecting to STOMP server: $e', name: 'LoginView');
developer.log('Stack trace: $stackTrace', name: 'LoginView');
scaffoldMessenger.showSnackBar(SnackBar(content: Text('${localizations.connectionError}: $e'), backgroundColor: Colors.red, duration: const Duration(seconds: 1)));
return;
}
}
developer.log('Pre-login checks passed - initiating login request', name: 'LoginView');
developer.log('Connection status: connected=${_stompService.isConnected}', name: 'LoginView');
setState(() {
_isLoggingIn = true;
});
String password = _passwordController.text;
developer.log('Sending login request via STOMP service...', name: 'LoginView');
try {
// Send login request via STOMP
await _stompService.login(email, password);
final requestSentTime = DateTime.now();
final requestDuration = requestSentTime.difference(loginStartTime).inMilliseconds;
developer.log('Login request sent successfully after ${requestDuration}ms', name: 'LoginView');
} catch (e, stackTrace) {
final errorTime = DateTime.now();
final errorDuration = errorTime.difference(loginStartTime).inMilliseconds;
developer.log('LOGIN ERROR: Exception during login request after ${errorDuration}ms', name: 'LoginView');
developer.log('Error: $e', name: 'LoginView');
developer.log('Stack trace: $stackTrace', name: 'LoginView');
setState(() {
_isLoggingIn = false;
});
scaffoldMessenger.showSnackBar(SnackBar(content: Text('${localizations.loginError}: $e'), backgroundColor: Colors.red, duration: const Duration(seconds: 1)));
}
// The auth response will be handled by the stream listener
// _isLoggingIn will be set to false in the listener
developer.log('Login request phase completed - waiting for auth response', name: 'LoginView');
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.grey[50],
body: Column(
children: [
Expanded(
child: SafeArea(
child: Center(
child: SingleChildScrollView(
padding: const EdgeInsets.all(24.0),
child: Form(
key: _formKey,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// Logo oder App-Name
Icon(Icons.account_circle, size: 100, color: Colors.deepPurple),
const SizedBox(height: 32),
Text(AppLocalizations.of(context).welcomeBack, style: Theme.of(context).textTheme.headlineMedium?.copyWith(fontWeight: FontWeight.bold, color: Colors.grey[800]), textAlign: TextAlign.center),
const SizedBox(height: 8),
Text(AppLocalizations.of(context).loginSubtitle, style: Theme.of(context).textTheme.bodyLarge?.copyWith(color: Colors.grey[600]), textAlign: TextAlign.center),
const SizedBox(height: 32),
// E-Mail-Feld
TextFormField(
controller: _emailController,
keyboardType: TextInputType.emailAddress,
decoration: InputDecoration(labelText: 'E-Mail-Adresse', hintText: 'Geben Sie Ihre E-Mail-Adresse ein', prefixIcon: const Icon(Icons.email_outlined), border: OutlineInputBorder(borderRadius: BorderRadius.circular(12)), filled: true, fillColor: Colors.white),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Bitte geben Sie Ihre E-Mail-Adresse ein';
}
if (!RegExp(r'^[A-Za-z0-9_.+-]+@[A-Za-z0-9-]+\.[A-Za-z0-9.-]+$').hasMatch(value)) {
return 'Bitte geben Sie eine gültige E-Mail-Adresse ein';
}
return null;
},
),
const SizedBox(height: 16),
// Passwort-Feld
TextFormField(
controller: _passwordController,
obscureText: !_isPasswordVisible,
decoration: InputDecoration(
labelText: 'Passwort',
hintText: 'Geben Sie Ihr Passwort ein',
prefixIcon: const Icon(Icons.lock_outlined),
suffixIcon: IconButton(
icon: Icon(_isPasswordVisible ? Icons.visibility : Icons.visibility_off),
onPressed: () {
setState(() {
_isPasswordVisible = !_isPasswordVisible;
});
},
),
border: OutlineInputBorder(borderRadius: BorderRadius.circular(12)),
filled: true,
fillColor: Colors.white,
),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Bitte geben Sie Ihr Passwort ein';
}
if (value.length < 6) {
return 'Das Passwort muss mindestens 6 Zeichen lang sein';
}
return null;
},
),
const SizedBox(height: 24),
// Passwort vergessen Link
Align(
alignment: Alignment.centerRight,
child: TextButton(
onPressed: () {
// Hier würde die "Passwort vergessen" Funktionalität implementiert werden
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(AppLocalizations.of(context).forgotPasswordMessage), duration: const Duration(seconds: 1)));
},
child: Text(AppLocalizations.of(context).forgotPassword, style: const TextStyle(color: Colors.deepPurple, fontWeight: FontWeight.w500)),
),
),
const SizedBox(height: 24),
// Verbindungsstatus
// Anmelden Button
ElevatedButton(onPressed: _isLoggingIn ? null : _handleLogin, style: ElevatedButton.styleFrom(backgroundColor: Colors.deepPurple, foregroundColor: Colors.white, padding: const EdgeInsets.symmetric(vertical: 16), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), elevation: 2), child: _isLoggingIn ? Row(mainAxisAlignment: MainAxisAlignment.center, children: const [SizedBox(width: 18, height: 18, child: CircularProgressIndicator(strokeWidth: 2.5, valueColor: AlwaysStoppedAnimation<Color>(Colors.white))), SizedBox(width: 12), Text('Verbinden…', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600))]) : const Text('Anmelden', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600))),
const SizedBox(height: 24),
],
),
),
),
),
),
),
// Version number at the bottom
if (_appVersion.isNotEmpty) Padding(padding: const EdgeInsets.only(bottom: 16.0), child: Text('Version $_appVersion', style: TextStyle(fontSize: 12, color: Colors.grey[500]), textAlign: TextAlign.center)),
],
),
);
}
}

122
app/lib/main.dart Normal file
View File

@@ -0,0 +1,122 @@
import 'package:flutter/material.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'login_view.dart';
import 'jobs_view.dart';
import 'cargo_items_view.dart';
import 'chats_view.dart';
import 'chat_details_view.dart';
import 'settings_view.dart';
import 'models/job.dart';
import 'models/chat.dart';
import 'services/database_service.dart';
import 'services/chat_service.dart';
import 'app_state.dart';
import 'navigation_observer.dart';
import 'services/notification_service.dart';
import 'l10n/app_localizations.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
// Initialize SQLite database
await DatabaseService().initialize();
// Load data from database
await AppState().loadLoginFromDatabase();
// Load language preference
await AppState().loadLanguagePreference();
// Load jobs from database to trigger message type logging at startup
await AppState().refreshJobsFromDatabase();
// Prepare chat service before WebSocket events start flowing
await ChatService().initialize();
// Initialize notification service for local notifications with sound
await NotificationService().initialize();
// Note: WebSocket connection is initiated from the view that needs it:
// - If userId exists: JobsView initiates connection on startup
// - If no userId: LoginView initiates connection when login button is clicked
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
// Check if user is already logged in
final appState = AppState();
final initialRoute = appState.isLoggedIn ? '/jobs' : '/login';
return ValueListenableBuilder<Locale>(
valueListenable: localeNotifier,
builder: (context, locale, child) {
return MaterialApp(
title: 'VotianLT App',
debugShowCheckedModeBanner: false,
theme: ThemeData(colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple), useMaterial3: true),
// Localization configuration
locale: locale,
localizationsDelegates: const [AppLocalizations.delegate, GlobalMaterialLocalizations.delegate, GlobalWidgetsLocalizations.delegate, GlobalCupertinoLocalizations.delegate],
supportedLocales: supportedLanguageCodes.map((code) => Locale(code)).toList(),
navigatorObservers: [routeObserver],
initialRoute: initialRoute,
onGenerateRoute: (settings) {
switch (settings.name) {
case '/login':
final arg = settings.arguments;
final suppress = (arg is bool) ? arg : false;
return MaterialPageRoute(builder: (_) => LoginView(suppressConnectionSnack: suppress));
case '/jobs':
return MaterialPageRoute(builder: (_) => const JobsView());
case '/cargo_items':
final job = settings.arguments as Job;
return MaterialPageRoute(builder: (_) => CargoItemsView(job: job));
case '/chats':
return MaterialPageRoute(builder: (_) => const ChatsView());
case '/chat_details':
final chat = settings.arguments as Chat;
return MaterialPageRoute(builder: (_) => ChatDetailsView(chat: chat));
case '/settings':
return MaterialPageRoute(builder: (_) => const SettingsView());
default:
return MaterialPageRoute(builder: (_) => const LoginView(suppressConnectionSnack: false));
}
},
);
},
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key, required this.title});
final String title;
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
int _counter = 0;
void _incrementCounter() {
setState(() {
_counter++;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(backgroundColor: Theme.of(context).colorScheme.inversePrimary, title: Text(widget.title)),
body: Center(child: Column(mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[const Text('You have pushed the button this many times:'), Text('$_counter', style: Theme.of(context).textTheme.headlineMedium)])),
floatingActionButton: FloatingActionButton(onPressed: _incrementCounter, tooltip: 'Increment', child: const Icon(Icons.add)), // This trailing comma makes auto-formatting nicer for build methods.
);
}
}

View File

@@ -0,0 +1,85 @@
/// Acknowledgment message sent by client to confirm message receipt
class AcknowledgmentMessage {
/// ID of the message being acknowledged
final String messageId;
/// Status of the acknowledgment
final AcknowledgmentStatus status;
/// Timestamp when the acknowledgment was created
final DateTime timestamp;
/// Optional error message if status is FAILED
final String? errorMessage;
AcknowledgmentMessage({
required this.messageId,
required this.status,
required this.timestamp,
this.errorMessage,
});
/// Create AcknowledgmentMessage from JSON
factory AcknowledgmentMessage.fromJson(Map<String, dynamic> json) {
return AcknowledgmentMessage(
messageId: json['messageId'] as String,
status: AcknowledgmentStatus.fromString(json['status'] as String),
timestamp: DateTime.parse(json['timestamp'] as String),
errorMessage: json['errorMessage'] as String?,
);
}
/// Convert AcknowledgmentMessage to JSON
Map<String, dynamic> toJson() {
return {
'messageId': messageId,
'status': status.toString(),
'timestamp': timestamp.toIso8601String(),
if (errorMessage != null) 'errorMessage': errorMessage,
};
}
@override
String toString() {
return 'AcknowledgmentMessage(messageId: $messageId, status: $status)';
}
}
/// Status of an acknowledgment
enum AcknowledgmentStatus {
/// Message was received by the client
received,
/// Message was processed successfully
processed,
/// Message processing failed
failed;
/// Convert string to AcknowledgmentStatus
static AcknowledgmentStatus fromString(String value) {
switch (value.toUpperCase()) {
case 'RECEIVED':
return AcknowledgmentStatus.received;
case 'PROCESSED':
return AcknowledgmentStatus.processed;
case 'FAILED':
return AcknowledgmentStatus.failed;
default:
return AcknowledgmentStatus.received;
}
}
@override
String toString() {
switch (this) {
case AcknowledgmentStatus.received:
return 'RECEIVED';
case AcknowledgmentStatus.processed:
return 'PROCESSED';
case AcknowledgmentStatus.failed:
return 'FAILED';
}
}
}

View File

@@ -0,0 +1,99 @@
class CargoItem {
final String id; // Will store the timestamp as string
final String jobId; // Will store the timestamp as string
final String description;
final int quantity;
final double weightKg;
final double lengthCm;
final double widthCm;
final double heightCm;
CargoItem({
required this.id,
required this.jobId,
required this.description,
required this.quantity,
required this.weightKg,
required this.lengthCm,
required this.widthCm,
required this.heightCm,
});
static String _readString(dynamic value, {String fallback = ''}) {
if (value is String) {
return value;
}
if (value is num || value is bool) {
return value.toString();
}
return fallback;
}
factory CargoItem.fromJson(Map<String, dynamic> json) {
// Parse the complex id object - can be either a Map or a simple string
String idValue = '';
if (json['id'] is Map) {
final idMap = json['id'] as Map<String, dynamic>;
idValue = idMap['timestamp']?.toString() ?? '';
} else {
idValue = json['id']?.toString() ?? '';
}
// Parse the complex jobId object - can be either a Map or a simple string
String jobIdValue = '';
if (json['jobId'] is Map) {
final jobIdMap = json['jobId'] as Map<String, dynamic>;
jobIdValue = jobIdMap['timestamp']?.toString() ?? '';
} else {
jobIdValue = json['jobId']?.toString() ?? '';
}
return CargoItem(
id: idValue,
jobId: jobIdValue,
description: _readString(json['description']),
quantity: json['quantity'] is num ? json['quantity'].toInt() : 0,
weightKg: json['weightKg'] is num ? json['weightKg'].toDouble() : 0.0,
lengthCm: json['lengthMm'] is num ? json['lengthMm'].toDouble() : 0.0,
widthCm: json['widthMm'] is num ? json['widthMm'].toDouble() : 0.0,
heightCm: json['heightMm'] is num ? json['heightMm'].toDouble() : 0.0,
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'jobId': jobId,
'description': description,
'quantity': quantity,
'weightKg': weightKg,
'lengthMm': lengthCm,
'widthMm': widthCm,
'heightMm': heightCm,
};
}
@override
String toString() {
return 'CargoItem(id: $id, description: $description, quantity: $quantity)';
}
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
return other is CargoItem && other.id == id;
}
@override
int get hashCode => id.hashCode;
/// Get formatted dimensions string for display
String get formattedDimensions {
return '${lengthCm.toInt()} × ${widthCm.toInt()} × ${heightCm.toInt()} cm';
}
/// Get formatted weight string for display
String get formattedWeight {
return '${weightKg.toStringAsFixed(1)} kg';
}
}

Some files were not shown because too many files have changed in this diff Show More