Front end

This commit is contained in:
rhanarmt 2026-04-09 22:09:30 +07:00
commit a33f0f1cdd
165 changed files with 13861 additions and 0 deletions

45
.gitignore vendored Normal file
View File

@ -0,0 +1,45 @@
# Miscellaneous
*.class
*.log
*.pyc
*.swp
.DS_Store
.atom/
.build/
.buildlog/
.history
.svn/
.swiftpm/
migrate_working_dir/
# IntelliJ related
*.iml
*.ipr
*.iws
.idea/
# The .vscode folder contains launch configuration and tasks you configure in
# VS Code which you may wish to be included in version control, so this line
# is commented out by default.
#.vscode/
# Flutter/Dart/Pub related
**/doc/api/
**/ios/Flutter/.last_build_id
.dart_tool/
.flutter-plugins
.flutter-plugins-dependencies
.pub-cache/
.pub/
/build/
# Symbolication related
app.*.symbols
# Obfuscation related
app.*.map.json
# Android Studio will place build artifacts here
/android/app/debug
/android/app/profile
/android/app/release

45
.metadata Normal file
View File

@ -0,0 +1,45 @@
# This file tracks properties of this Flutter project.
# Used by Flutter tool to assess capabilities and perform upgrades etc.
#
# This file should be version controlled and should not be manually edited.
version:
revision: "35c388afb57ef061d06a39b537336c87e0e3d1b1"
channel: "stable"
project_type: app
# Tracks metadata for the flutter migrate command
migration:
platforms:
- platform: root
create_revision: 35c388afb57ef061d06a39b537336c87e0e3d1b1
base_revision: 35c388afb57ef061d06a39b537336c87e0e3d1b1
- platform: android
create_revision: 35c388afb57ef061d06a39b537336c87e0e3d1b1
base_revision: 35c388afb57ef061d06a39b537336c87e0e3d1b1
- platform: ios
create_revision: 35c388afb57ef061d06a39b537336c87e0e3d1b1
base_revision: 35c388afb57ef061d06a39b537336c87e0e3d1b1
- platform: linux
create_revision: 35c388afb57ef061d06a39b537336c87e0e3d1b1
base_revision: 35c388afb57ef061d06a39b537336c87e0e3d1b1
- platform: macos
create_revision: 35c388afb57ef061d06a39b537336c87e0e3d1b1
base_revision: 35c388afb57ef061d06a39b537336c87e0e3d1b1
- platform: web
create_revision: 35c388afb57ef061d06a39b537336c87e0e3d1b1
base_revision: 35c388afb57ef061d06a39b537336c87e0e3d1b1
- platform: windows
create_revision: 35c388afb57ef061d06a39b537336c87e0e3d1b1
base_revision: 35c388afb57ef061d06a39b537336c87e0e3d1b1
# User provided section
# List of Local paths (relative to this file) that should be
# ignored by the migrate tool.
#
# Files that are not part of the templates will be ignored by default.
unmanaged_files:
- 'lib/main.dart'
- 'ios/Runner.xcodeproj/project.pbxproj'

210
DEPLOYMENT_GUIDE.md Normal file
View File

@ -0,0 +1,210 @@
# DEPLOYMENT GUIDE - API & FLUTTER APP
## 📱 DEPLOYMENT FLUTTER APP
### Step 1: Build APK (Android)
```bash
cd c:\fluuter.u\permintaandanprediksi_stok_bahan_kue\finalproject
flutter clean
flutter pub get
flutter build apk --release
```
Output APK: `build\app\outputs\flutter-apk\app-release.apk`
### Step 2: Build AppBundle (Google Play)
```bash
flutter build appbundle --release
```
Output: `build\app\outputs\bundle\release\app-release.aab`
### Step 3: Install APK to Device
```bash
flutter install
# OR
adb install build\app\outputs\flutter-apk\app-release.apk
```
---
## 🚀 DEPLOYMENT PYTHON API
### Option A: Heroku (Cloud - Recommended)
#### 1. Install Heroku CLI
https://devcenter.heroku.com/articles/heroku-cli
#### 2. Create Procfile
File: `ml_model/Procfile`
```
web: gunicorn app:app
```
#### 3. Update requirements.txt
```bash
cd ml_model
pip freeze > requirements.txt
# Add gunicorn:
echo "gunicorn==20.1.0" >> requirements.txt
```
#### 4. Deploy to Heroku
```bash
heroku login
heroku create prediksi-stok-api
git push heroku main
```
#### 5. Access API
```
https://prediksi-stok-api.herokuapp.com/health
```
#### 6. Update Flutter API URL
`lib/services/ml_service.dart`:
```dart
static const String baseUrl = 'https://prediksi-stok-api.herokuapp.com';
```
---
### Option B: Local Server (Development)
#### 1. Keep API Running
```bash
cd ml_model
python app.py
```
#### 2. Access from Phone
Use IP address instead of localhost:
```
http://192.168.1.75:5000
```
Ganti `192.168.1.75` dengan IP lokal Anda.
#### 3. Update Flutter API URL
`lib/services/ml_service.dart`:
```dart
static const String baseUrl = 'http://192.168.1.75:5000';
```
---
### Option C: AWS EC2 (Advanced)
1. Launch EC2 instance (Ubuntu)
2. Install Python & dependencies
3. Deploy Flask with Gunicorn + Nginx
4. Get Elastic IP
5. Update Flutter API URL
---
## ✅ CHECKLIST SEBELUM DEPLOY
### Backend
- [ ] API running locally? (`python app.py`)
- [ ] All endpoints working? (Test 1-4)
- [ ] Models loaded correctly?
- [ ] CORS enabled?
- [ ] Requirements.txt updated?
### Frontend
- [ ] ML Service created? (`ml_service.dart`)
- [ ] Prediction Page created? (`prediction_page.dart`)
- [ ] main.dart updated? (imports PredictionPage)
- [ ] pubspec.yaml has `http` package?
- [ ] API URL configured?
- [ ] App builds without errors?
```bash
flutter clean
flutter pub get
flutter build apk --release
```
---
## 🔍 TESTING DEPLOYED APP
### 1. Test API Health
```bash
curl https://prediksi-stok-api.herokuapp.com/health
# Expected: status: "healthy"
```
### 2. Test App on Device
- Install APK
- Open app
- Fill form
- Click PREDIKSI
- Check result
---
## 🆘 COMMON DEPLOYMENT ISSUES
### Issue 1: "API tidak tersedia"
**Solution:**
- API not running? Start it: `python app.py`
- Wrong IP address? Check: `ipconfig`
- Firewall blocking port 5000?
### Issue 2: "Connection refused"
**Solution:**
- Use IP instead of localhost
- Both API & app on same WiFi?
- Firewall allows port 5000?
### Issue 3: "Endpoint tidak ditemukan"
**Solution:**
- API URL correct?
- Endpoint spelling correct? (`/prediksi` not `/prediksi/`)
- Headers correct? (`Content-Type: application/json`)
---
## 📊 DEPLOYMENT SUMMARY
| Step | Status | Notes |
| --------------- | ------ | ------------------------- |
| **API Running** | ✅ | `python app.py` |
| **Flutter UI** | ✅ | `PredictionPage` created |
| **ML Service** | ✅ | `ml_service.dart` created |
| **API URL** | ⚙️ | Configure for production |
| **Build APK** | ⚙️ | `flutter build apk` |
| **Deploy API** | ⚙️ | Choose Heroku/AWS/Local |
| **Publish App** | ⚙️ | Google Play Store |
---
**Status:** Ready for Production Deployment ✅

0
DESIGN_SYSTEM.md Normal file
View File

345
INTEGRATION_GUIDE.md Normal file
View File

@ -0,0 +1,345 @@
# 🔌 INTEGRASI API FLASK KE FLUTTER - SELESAI ✅
## ✅ Yang Sudah Dibuat
### 1. **pubspec.yaml** - Updated dengan dependencies
```yaml
dependencies:
http: ^1.1.0 # HTTP Client untuk API
intl: ^0.19.0 # Date formatting
```
### 2. **lib/services/ml_service.dart** - API Service
- `healthCheck()` - Verifikasi API running
- `getMetadata()` - Ambil daftar produk & kategori
- `prediksiStok()` - Single prediction
- `batchPrediksi()` - Multiple predictions
### 3. **lib/models/prediction_model.dart** - Data Models
- `PredictionRequest` - Request model
- `PredictionResult` - Response model
- `PredictionHistory` - History model
### 4. **lib/pages/prediction_page.dart** - UI Complete
- Form input lengkap (tanggal, produk, kategori, harga)
- Loading indicator saat API call
- Display hasil prediksi
- Riwayat prediksi dengan clear history
- Error handling lengkap
### 5. **lib/main.dart** - Updated Entry Point
- Hubung ke PredictionPage
- Theme configuration
---
## 🚀 CARA MENGGUNAKAN
### Step 1: Install Dependencies
```bash
cd finalproject
flutter pub get
```
### Step 2: Update API URL (PENTING!)
Edit `lib/services/ml_service.dart`:
```dart
static const String baseUrl = 'http://YOUR_IP:5000';
```
Ganti `YOUR_IP` dengan:
- **Localhost:** `http://localhost:5000` (jika testing di emulator/simulator PC sama)
- **Local Network:** `http://192.168.1.X:5000` (ganti X dengan IP dari `ipconfig`)
- **Production:** URL cloud API (Heroku, AWS, etc.)
### Step 3: Start Flask API
```bash
cd ml_model
python app.py
```
Output:
```
* Running on http://0.0.0.0:5000
```
### Step 4: Run Flutter App
```bash
cd finalproject
flutter run
```
Atau di Android Studio:
- Press `F5` atau click Run button
### Step 5: Test Aplikasi
1. App akan loading metadata (daftar produk/kategori)
2. Akan muncul pesan: **"✅ API Connected!"** jika API terbuka
3. Isi form: Tanggal, Produk, Kategori, Harga
4. Click **PREDIKSI** button
5. Lihat hasil di bawah
---
## 📋 STRUKTUR FILE YANG DIBUAT
```
lib/
├── main.dart ✅ UPDATED - Entry point
├── services/
│ └── ml_service.dart ✅ NEW - API Service
├── models/
│ └── prediction_model.dart ✅ NEW - Data Models
└── pages/
└── prediction_page.dart ✅ NEW - Prediction UI
pubspec.yaml ✅ UPDATED - Dependencies
```
---
## 🔗 API ENDPOINTS YANG DIGUNAKAN
| Method | Endpoint | Purpose |
| ------ | ----------------- | ------------------------------ |
| GET | `/health` | Cek API running |
| GET | `/metadata` | Ambil daftar produk & kategori |
| POST | `/prediksi` | Single prediction |
| POST | `/batch-prediksi` | Batch predictions |
---
## 📝 CONTOH FLOW
```
User Input:
├─ Tanggal: 2025-04-15
├─ Produk: Gula Pasir 1kg
├─ Kategori: Gula
└─ Harga: 12500
Flutter App (PredictionPage):
├─ Validasi input
├─ Call MLService.prediksiStok()
└─ Display results
Python API (app.py):
├─ Terima request
├─ Load model & encoders
├─ Preprocess data
├─ Run prediction
└─ Return JSON response
Response ke Flutter:
{
"status": "success",
"prediksi": {
"jumlah_unit": 5,
"nilai_raw": 5.29,
"estimasi_total_harga": 62500
},
"model_info": {
"akurasi_r2": -0.0035,
"error_mae": 2.51
}
}
Flutter Display:
✅ Hasil Prediksi
├─ Estimasi Jumlah: 5 unit
├─ Nilai Prediksi: 5.29 unit
├─ Estimasi Total Harga: Rp 62500
├─ Model Accuracy (R²): -0.0035
└─ Error (MAE): 2.51 unit
```
---
## ⚠️ TROUBLESHOOTING
### Error: "Connection refused"
```
❌ Problem: API tidak running atau URL salah
✅ Solution:
1. Pastikan Flask API running: python app.py
2. Cek URL di MLService: http://localhost:5000
3. Cek firewall allow port 5000
```
### Error: "Failed to connect to 192.168.x.x"
```
❌ Problem: Phone tidak bisa reach API di lokal network
✅ Solution:
1. Pastikan phone & PC di network yang sama (WiFi)
2. Gunakan IP dari ipconfig bukan localhost
3. Disable VPN di phone
4. Test: curl http://192.168.x.x:5000/health
```
### Error: "No response from server"
```
❌ Problem: API response timeout (>30 detik)
✅ Solution:
1. Check API logs untuk error
2. Cek request ke API:
curl -X POST http://localhost:5000/prediksi \
-H "Content-Type: application/json" \
-d '{"tanggal":"2025-04-15","produk":"Gula Pasir 1kg","kategori":"Gula","harga":12500}'
3. Increase timeout di MLService (default 30s)
```
### App muncul pesan: "❌ API tidak terbuka!"
```
❌ Problem: App tidak bisa connect ke API
✅ Solution:
1. Buka Terminal/CMD
2. Go to ml_model folder
3. Run: python app.py
4. Tunggu sampai melihat: "Running on http://..."
5. Back ke Flutter app, swipe down atau restart app
```
---
## 🎯 FITUR-FITUR
✅ **Form Input Lengkap**
- Date picker untuk tanggal
- Dropdown untuk produk (8 pilihan)
- Dropdown untuk kategori (8 pilihan)
- Input harga satuan
✅ **Form Validation**
- Cek semua field wajib diisi
- Cek harga positif
✅ **Loading State**
- Loading indicator saat fetch metadata
- Loading indicator saat submit prediksi
- Disable button saat loading
✅ **Hasil Display**
- Jumlah unit prediksi
- Nilai raw prediksi
- Estimasi total harga
- Model accuracy & error
✅ **History Tracking**
- Simpan riwayat prediksi
- Display dengan timestamp
- Tombol clear history
✅ **Error Handling**
- API connection error
- Invalid input error
- Server error
- Network timeout
✅ **API Health Check**
- Verifikasi API running saat app start
- Notification jika API tidak terbuka
- Auto retry metadata loading
---
## 📱 TESTING CHECKLIST
- [ ] API running: `python app.py` di ml_model folder
- [ ] Dependencies installed: `flutter pub get`
- [ ] Update URL di MLService (jika tidak localhost)
- [ ] Run app: `flutter run`
- [ ] Lihat "✅ API Connected!" notification
- [ ] Isi form dengan data valid
- [ ] Click PREDIKSI button
- [ ] Lihat hasil prediksi
- [ ] Add to history
- [ ] Clear history
- [ ] Test error cases (invalid product, empty field)
---
## 🔄 DEPLOYMENT SELANJUTNYA
### Untuk Local Testing:
✅ API di localhost atau local IP
✅ Flutter app di emulator/simulator/device
### Untuk Production:
⬜ Deploy Flask API ke cloud (Heroku/AWS/Railway)
⬜ Update baseUrl di MLService ke production URL
⬜ Build APK/AAB: `flutter build apk --release`
⬜ Upload ke Google Play Store
---
## 📞 QUICK REFERENCE
### File Penting:
- Backend API: `ml_model/app.py` (Flask)
- Frontend Service: `lib/services/ml_service.dart` (Flutter)
- Frontend UI: `lib/pages/prediction_page.dart` (Flutter)
### Commands:
```bash
# Start API
cd ml_model && python app.py
# Test API
curl http://localhost:5000/health
# Run Flutter
flutter run
# Build APK
flutter build apk --release
```
### API URL untuk berbagai skenario:
- Emulator/Simulator lokal: `http://localhost:5000`
- Device lokal: `http://192.168.1.X:5000` (ganti X)
- Production: `https://your-api.herokuapp.com` atau IP server
---
**Status:** ✅ INTEGRASI SELESAI & SIAP RUN
**Next Step:** `flutter run` di terminal & test aplikasi!
Generated: 2026-04-01

421
MYSQL_INTEGRATION.md Normal file
View File

@ -0,0 +1,421 @@
# MySQL Integration Summary - Prediksi Stok Bahan Kue
## ✅ What's Done
### Backend (Flask) - NEW FILES & UPDATES
#### New Files Created:
1. **`database_setup.py`** - Script untuk initialize MySQL database
- Buat database `prediksi_stok_db`
- Buat 3 tables: `products`, `transactions`, `predictions`
- Insert 8 default products
2. **`MYSQL_SETUP.md`** - Comprehensive setup guide
- MySQL installation instructions
- Database setup steps (automatic & manual)
- API endpoint documentation
- Troubleshooting guide
#### Updated Files:
1. **`requirements.txt`** - Added MySQL dependencies
```
flask-sqlalchemy==3.0.0
PyMySQL==1.1.0
```
2. **`app.py`** - Added 6 new endpoints
- `GET /products` - Ambil daftar produk
- `GET /products/<id>` - Ambil 1 produk by ID
- `POST /transactions` - Simpan transaksi
- `GET /transactions` - Ambil history transaksi
- `POST /predictions` - Simpan prediction results
- Updated `/info` endpoint
### Frontend (Flutter) - UPDATES
#### Updated Files:
1. **`lib/services/ml_service.dart`** - Added 5 new methods
- `getProducts()` - Get products from API
- `getProduct(id)` - Get single product
- `saveTransaction()` - Save transaction to MySQL
- `getTransactions()` - Get transaction history
- `savePrediction()` - Save prediction to MySQL
2. **`lib/screens/transaction_screen.dart`** - Integrated with API
- `_submitTransaction()` now calls API
- Added loading state during save
- Show success/error messages
- Data di-save ke MySQL, bukan cuma local
## 📋 Architecture
```
┌─────────────────┐
│ FLUTTER APP │
│ (Mobile) │
└────────┬────────┘
┌────────▼────────┐
│ Flask API │
│ (Python) │
│ on localhost: │
│ 5000 │
└────────┬────────┘
┌────────▼────────┐
│ MySQL Database │
│ localhost:3306 │
└─────────────────┘
Data Flow:
User Input → Flutter → API → MySQL Database
```
## 🚀 Quick Start (3 Steps)
### Step 1: Install MySQL & Setup Database
```bash
# Navigate to ml_model folder
cd c:\fluuter.u\permintaandanprediksi_stok_bahan_kue\finalproject\ml_model
# Install Python dependencies
pip install -r requirements.txt
# Setup MySQL database
python database_setup.py
```
**Expected Output:**
```
✅ Database 'prediksi_stok_db' created successfully
✅ Products table created successfully
✅ Transactions table created successfully
✅ Predictions table created successfully
✅ Inserted 8 default products
✅ Database setup completed successfully!
```
### Step 2: Start Flask API
```bash
# From ml_model folder
python app.py
```
**Expected Output:**
```
Starting Prediksi Stok API
Model: RandomForest
Accuracy (R²): 0.9964
Endpoints: /health, /metadata, /info, /prediksi, /batch-prediksi, /products, /transactions, /predictions
Access API at: http://localhost:5000
```
### Step 3: Run Flutter App
```bash
# From finalproject folder
flutter run
```
## 🔌 API Endpoints
### Products
```
GET /products
Response: {
"status": "success",
"total": 8,
"products": [
{"id": 1, "name": "Tepung Terigu 1kg", "category": "Tepung", "price": 15000, "stock": 45, ...},
...
]
}
GET /products/1
Response: {
"status": "success",
"product": {"id": 1, "name": "Tepung Terigu 1kg", ...}
}
```
### Transactions
```
POST /transactions
Body: {
"product_name": "Tepung Terigu 1kg",
"category": "Tepung",
"quantity": 5,
"unit_price": 15000,
"total_price": 75000,
"transaction_date": "2024-04-05"
}
Response: {
"status": "success",
"message": "Transaction saved successfully",
"transaction_id": 1
}
GET /transactions?limit=100&offset=0
Response: {
"status": "success",
"total": 10,
"transactions": [...]
}
GET /transactions?product_name=Tepung
Response: {...} // Filtered by product name
```
### Predictions
```
POST /predictions
Body: {
"product_name": "Tepung Terigu 1kg",
"category": "Tepung",
"unit_price": 15000,
"prediction_date": "2024-04-05",
"predicted_quantity": 45,
"raw_value": 44.8,
"estimated_total_price": 672000,
"accuracy_r2": 0.9964,
"error_mae": 2.51
}
Response: {
"status": "success",
"message": "Prediction saved successfully",
"prediction_id": 1
}
```
## 📊 Database Schema
### Products Table
```sql
CREATE TABLE products (
id INT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(255) NOT NULL,
category VARCHAR(100) NOT NULL,
price INT NOT NULL,
stock INT NOT NULL DEFAULT 0,
status VARCHAR(50) NOT NULL DEFAULT 'tersedia',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
```
### Transactions Table
```sql
CREATE TABLE transactions (
id INT PRIMARY KEY AUTO_INCREMENT,
product_name VARCHAR(255) NOT NULL,
category VARCHAR(100) NOT NULL,
quantity INT NOT NULL,
unit_price INT NOT NULL,
total_price INT NOT NULL,
transaction_date DATE NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
```
### Predictions Table
```sql
CREATE TABLE predictions (
id INT PRIMARY KEY AUTO_INCREMENT,
product_name VARCHAR(255) NOT NULL,
category VARCHAR(100) NOT NULL,
unit_price INT NOT NULL,
prediction_date DATE NOT NULL,
predicted_quantity INT,
raw_value DOUBLE,
estimated_total_price INT,
accuracy_r2 DOUBLE,
error_mae DOUBLE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
```
## 🔄 Flutter Integration Examples
### Example 1: Get All Products
```dart
List<Map<String, dynamic>> products = await MLService.getProducts();
```
### Example 2: Save Transaction
```dart
bool success = await MLService.saveTransaction(
productName: 'Tepung Terigu 1kg',
category: 'Tepung',
quantity: 5,
unitPrice: 15000,
totalPrice: 75000,
transactionDate: '2024-04-05',
);
```
### Example 3: Get Transaction History
```dart
List<Map<String, dynamic>> transactions = await MLService.getTransactions(
limit: 50,
offset: 0,
productName: null,
);
```
### Example 4: Save Prediction
```dart
bool success = await MLService.savePrediction(
productName: 'Tepung Terigu 1kg',
category: 'Tepung',
unitPrice: 15000,
predictionDate: '2024-04-05',
predictedQuantity: 45,
rawValue: 44.8,
estimatedTotalPrice: 672000,
accuracyR2: 0.9964,
errorMae: 2.51,
);
```
## 📝 Files Modified/Created
### Backend
```
ml_model/
├── database_setup.py ✨ NEW
├── MYSQL_SETUP.md ✨ NEW
├── app.py ✏️ UPDATED (added 6 endpoints)
└── requirements.txt ✏️ UPDATED (added MySQL libs)
```
### Frontend
```
lib/
├── services/
│ └── ml_service.dart ✏️ UPDATED (added 5 methods)
└── screens/
└── transaction_screen.dart ✏️ UPDATED (integrated with API)
```
## ✅ Verification Checklist
Before running the app:
- [ ] MySQL server installed & running
- [ ] Database setup completed (`python database_setup.py`)
- [ ] Flask API running (`python app.py`) on port 5000
- [ ] Can access `http://localhost:5000/health` in browser
- [ ] Can access `http://localhost:5000/products` in browser
- [ ] Flutter app can connect to API
## 🐛 Common Issues & Solutions
### Issue: "Can't connect to MySQL server"
**Solution**: Check MySQL is running
```bash
# Windows: Check Services
# macOS: brew services list
# Linux: sudo systemctl status mysql
```
### Issue: "Database 'prediksi_stok_db' doesn't exist"
**Solution**: Run the database setup script
```bash
python database_setup.py
```
### Issue: "Access denied for user 'root'@'localhost'"
**Solution**: Update database credentials in `app.py` and `database_setup.py`
```python
DB_PASSWORD = 'your_mysql_password'
```
### Issue: "Port 5000 already in use"
**Solution**: Change port in `app.py`
```python
app.run(port=5001) # Use different port
```
## 🔐 Security Notes
For production use:
1. Never use `root` user without password
2. Create dedicated database user with restricted privileges
3. Use environment variables for credentials
4. Enable SSL/TLS for connections
5. Implement API authentication (JWT, OAuth)
6. Add input validation & SQL injection protection
Example:
```python
# Use environment variables
import os
DB_PASSWORD = os.getenv('DB_PASSWORD', 'default_password')
```
## 📚 Next Steps
1. ✅ Test all endpoints with Postman
2. ✅ Test Transaction Screen with real data
3. ✅ Implement Prediction Screen API integration
4. ✅ Add local caching for offline support
5. ✅ Setup authentication & user management
6. ✅ Implement data sync & backup
7. ✅ Optimize query performance with indexes
8. ✅ Add comprehensive error handling
## 📞 Support
Lihat dokumentasi lengkap di: `ml_model/MYSQL_SETUP.md`
Untuk questions atau issues, check logs:
```bash
# Flask API logs
python app.py # Check console output
# MySQL logs
# Windows: MySQL Workbench → Administration → Server Logs
# macOS/Linux: /var/log/mysql/error.log
```
---
**Integration Status: ✅ COMPLETE!**
Data flow sekarang:
- Flutter → API → MySQL Database
- Semua transactions & predictions tersimpan di database
- Bisa diakses dari mana saja
- Persistent data untuk analysis

276
QUICK_START.md Normal file
View File

@ -0,0 +1,276 @@
# Quick Command Reference - MySQL Integration
## 🚀 Setup & Run (Copy-Paste Ready)
### 1⃣ Install Python Dependencies
```bash
cd c:\fluuter.u\permintaandanprediksi_stok_bahan_kue\finalproject\ml_model
pip install -r requirements.txt
```
### 2⃣ Setup MySQL Database
```bash
python database_setup.py
```
**Expected Output:**
```
============================================================
SETUP DATABASE MYSQL - PREDIKSI STOK BAHAN KUE
============================================================
Database Configuration:
Host: localhost
User: root
Database: prediksi_stok_db
✅ Database 'prediksi_stok_db' created successfully
✅ Products table created successfully
✅ Transactions table created successfully
✅ Predictions table created successfully
✅ Inserted 8 default products
✅ Database setup completed successfully!
✅ Database connected! Found 8 products
```
### 3⃣ Start Flask API
```bash
python app.py
```
**Expected Output:**
```
================================================================================
Starting Prediksi Stok API
================================================================================
Model: RandomForest
Accuracy (R²): 0.9964
Features: 10
Endpoints: /health, /metadata, /info, /prediksi, /batch-prediksi, /products, /transactions, /predictions
Access API at: http://localhost:5000
================================================================================
* Running on http://0.0.0.0:5000
```
### 4⃣ Run Flutter App (In new terminal)
```bash
cd c:\fluuter.u\permintaandanprediksi_stok_bahan_kue\finalproject
flutter run
```
---
## ✅ Test Endpoints (Postman / cURL)
### Test 1: Health Check
```bash
curl http://localhost:5000/health
```
### Test 2: Get All Products
```bash
curl http://localhost:5000/products
```
### Test 3: Save Transaction
```bash
curl -X POST http://localhost:5000/transactions \
-H "Content-Type: application/json" \
-d '{
"product_name": "Tepung Terigu 1kg",
"category": "Tepung",
"quantity": 5,
"unit_price": 15000,
"total_price": 75000,
"transaction_date": "2024-04-05"
}'
```
### Test 4: Get Transactions
```bash
curl http://localhost:5000/transactions
```
### Test 5: Save Prediction
```bash
curl -X POST http://localhost:5000/predictions \
-H "Content-Type: application/json" \
-d '{
"product_name": "Tepung Terigu 1kg",
"category": "Tepung",
"unit_price": 15000,
"prediction_date": "2024-04-05",
"predicted_quantity": 45,
"raw_value": 44.8,
"estimated_total_price": 672000,
"accuracy_r2": 0.9964,
"error_mae": 2.51
}'
```
---
## 🔧 Troubleshooting Commands
### Check MySQL is running
```bash
# Windows cmd
tasklist | find "MySQL"
# macOS
brew services list | grep mysql
# Linux
sudo systemctl status mysql
```
### Check if port 5000 is available
```bash
# Windows
netstat -ano | findstr :5000
# macOS/Linux
lsof -i :5000
```
### View MySQL data
```bash
# Login to MySQL
mysql -u root -p prediksi_stok_db
# View all products
SELECT * FROM products;
# View all transactions
SELECT * FROM transactions;
# View all predictions
SELECT * FROM predictions;
# Count transactions
SELECT COUNT(*) as total_transactions FROM transactions;
# Exit
exit
```
### Stop/Restart Flask API
Press `Ctrl + C` in terminal running Flask
### Reset Database
```bash
# Delete and recreate
python database_setup.py
# Or manually in MySQL
DROP DATABASE prediksi_stok_db;
# Then run database_setup.py
```
---
## 📱 Flutter Integration
### In Transaction Screen - Automatic Integration
When user presses "Simpan Transaksi":
1. ✅ Form validation
2. ✅ Calls `MLService.saveTransaction()`
3. ✅ Saves to MySQL database
4. ✅ Shows success message
5. ✅ Updates local UI
### To use in other screens:
```dart
// Import
import 'package:finalproject/services/ml_service.dart';
// Get products
List<Map<String, dynamic>> products = await MLService.getProducts();
// Save transaction
bool success = await MLService.saveTransaction(
productName: 'Tepung Terigu 1kg',
category: 'Tepung',
quantity: 5,
unitPrice: 15000,
totalPrice: 75000,
transactionDate: '2024-04-05',
);
// Get transactions
List<Map<String, dynamic>> transactions = await MLService.getTransactions();
// Save prediction
bool success = await MLService.savePrediction(
productName: 'Tepung Terigu 1kg',
category: 'Tepung',
unitPrice: 15000,
predictionDate: '2024-04-05',
predictedQuantity: 45,
rawValue: 44.8,
estimatedTotalPrice: 672000,
accuracyR2: 0.9964,
errorMae: 2.51,
);
```
---
## 📊 Database Files Modified
**Backend:**
- ✨ `ml_model/database_setup.py` - NEW (Database initialization)
- ✨ `ml_model/MYSQL_SETUP.md` - NEW (Setup guide)
- ✏️ `ml_model/app.py` - UPDATED (6 new endpoints)
- ✏️ `ml_model/requirements.txt` - UPDATED (MySQL dependencies)
**Frontend:**
- ✏️ `lib/services/ml_service.dart` - UPDATED (5 new methods)
- ✏️ `lib/screens/transaction_screen.dart` - UPDATED (API integration)
---
## 🎯 What's Working Now
✅ Flutter ↔ Flask API ↔ MySQL
✅ Products saved in database
✅ Transactions saved to MySQL
✅ Predictions saved to MySQL
✅ Real-time data persistence
✅ Transaction history retrieval
---
## 📚 Documentation
- **Full Setup Guide**: `ml_model/MYSQL_SETUP.md`
- **Integration Details**: `MYSQL_INTEGRATION.md`
- **API Reference**: `ml_model/app.py` (comments)
- **DB Schema**: `MYSQL_INTEGRATION.md` (Database Schema section)
---
**Everything is ready!** 🎉
Just run the 4 steps above and your app will be connected to MySQL!

16
README.md Normal file
View File

@ -0,0 +1,16 @@
# finalproject
A new Flutter project.
## 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.

0
README_START_HERE.md Normal file
View File

28
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
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,44 @@
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 = "com.example.finalproject"
compileSdk = flutter.compileSdkVersion
ndkVersion = flutter.ndkVersion
compileOptions {
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 = "com.example.finalproject"
// 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")
}
}
}
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,45 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application
android:label="finalproject"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher">
<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 com.example.finalproject
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,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>

21
android/build.gradle.kts Normal file
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 "1.8.22" apply false
}
include(":app")

34
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 @@
#include "Generated.xcconfig"

View File

@ -0,0 +1 @@
#include "Generated.xcconfig"

View File

@ -0,0 +1,616 @@
// !$*UTF8*$!
{
archiveVersion = 1;
classes = {
};
objectVersion = 54;
objects = {
/* Begin PBXBuildFile section */
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
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 */; };
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; };
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
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>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
97C146EB1CF9000F007C117D /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
331C8082294A63A400263BE5 /* RunnerTests */ = {
isa = PBXGroup;
children = (
331C807B294A618700263BE5 /* RunnerTests.swift */,
);
path = RunnerTests;
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 */,
);
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 = (
331C807D294A63A400263BE5 /* Sources */,
331C807F294A63A400263BE5 /* Resources */,
);
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 = (
9740EEB61CF901F6004384FC /* Run Script */,
97C146EA1CF9000F007C117D /* Sources */,
97C146EB1CF9000F007C117D /* Frameworks */,
97C146EC1CF9000F007C117D /* Resources */,
9705A1C41CF9048500538489 /* Embed Frameworks */,
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
);
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 */
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";
};
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";
};
/* 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 = com.example.finalproject;
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;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.example.finalproject.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;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.example.finalproject.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;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.example.finalproject.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 = com.example.finalproject;
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 = com.example.finalproject;
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,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "group:Runner.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>

49
ios/Runner/Info.plist Normal file
View File

@ -0,0 +1,49 @@
<?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>Finalproject</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>finalproject</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>$(FLUTTER_BUILD_NAME)</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>$(FLUTTER_BUILD_NUMBER)</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/>
</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.
}
}

38
lib/main.dart Normal file
View File

@ -0,0 +1,38 @@
import 'package:flutter/material.dart';
import 'package:finalproject/theme/app_theme.dart';
import 'package:finalproject/screens/splash_screen.dart';
import 'package:finalproject/screens/login_screen.dart';
import 'package:finalproject/screens/dashboard_screen.dart';
import 'package:finalproject/screens/prediction_screen.dart';
import 'package:finalproject/screens/transaction_screen.dart';
import 'package:finalproject/screens/product_list_screen.dart';
import 'package:finalproject/screens/report_screen.dart';
import 'package:finalproject/screens/settings_screen.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Prediksi Stok Bahan Kue',
theme: AppTheme.lightTheme(),
debugShowCheckedModeBanner: false,
initialRoute: '/login',
routes: {
'/splash': (context) => const SplashScreen(),
'/login': (context) => const LoginScreen(),
'/dashboard': (context) => const DashboardScreen(),
'/prediction': (context) => const PredictionScreen(),
'/transaction': (context) => const TransactionScreen(),
'/products': (context) => const ProductListScreen(),
'/reports': (context) => const ReportScreen(),
'/settings': (context) => const SettingsScreen(),
},
);
}
}

View File

@ -0,0 +1,72 @@
class PredictionRequest {
final String tanggal;
final String produk;
final String kategori;
final int harga;
PredictionRequest({
required this.tanggal,
required this.produk,
required this.kategori,
required this.harga,
});
Map<String, dynamic> toJson() => {
'tanggal': tanggal,
'produk': produk,
'kategori': kategori,
'harga': harga,
};
}
class PredictionResult {
final String status;
final int? jumlahUnit;
final double? nilaiRaw;
final int? estimasiTotalHarga;
final double? akurasiR2;
final double? errorMae;
final String? message;
PredictionResult({
required this.status,
this.jumlahUnit,
this.nilaiRaw,
this.estimasiTotalHarga,
this.akurasiR2,
this.errorMae,
this.message,
});
factory PredictionResult.fromJson(Map<String, dynamic> json) {
return PredictionResult(
status: json['status'] ?? 'error',
jumlahUnit: json['prediksi']?['jumlah_unit'],
nilaiRaw: (json['prediksi']?['nilai_raw'] ?? 0).toDouble(),
estimasiTotalHarga: json['prediksi']?['estimasi_total_harga'],
akurasiR2: (json['model_info']?['akurasi_r2'] ?? 0).toDouble(),
errorMae: (json['model_info']?['error_mae'] ?? 0).toDouble(),
message: json['message'],
);
}
bool get isSuccess => status == 'success';
}
class PredictionHistory {
final String tanggal;
final String produk;
final String kategori;
final int harga;
final int prediksi;
final DateTime timestamp;
PredictionHistory({
required this.tanggal,
required this.produk,
required this.kategori,
required this.harga,
required this.prediksi,
required this.timestamp,
});
}

View File

@ -0,0 +1,60 @@
class Product {
final int id;
final String name;
final String category;
final int price;
final int stock;
final String status; // 'tersedia', 'rendah', 'kritis'
Product({
required this.id,
required this.name,
required this.category,
required this.price,
required this.stock,
this.status = 'tersedia',
});
factory Product.fromJson(Map<String, dynamic> json) {
return Product(
id: json['id'] as int,
name: json['name'] as String,
category: json['category'] as String,
price: json['price'] as int,
stock: json['stock'] as int,
status: json['status'] as String? ?? 'tersedia',
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'name': name,
'category': category,
'price': price,
'stock': stock,
'status': status,
};
}
Product copyWith({
int? id,
String? name,
String? category,
int? price,
int? stock,
String? status,
}) {
return Product(
id: id ?? this.id,
name: name ?? this.name,
category: category ?? this.category,
price: price ?? this.price,
stock: stock ?? this.stock,
status: status ?? this.status,
);
}
@override
String toString() => 'Product(id: $id, name: $name, stock: $stock)';
}

View File

@ -0,0 +1,67 @@
class Transaction {
final int id;
final String productName;
final String category;
final int quantity;
final int unitPrice;
final int totalPrice;
final DateTime date;
Transaction({
required this.id,
required this.productName,
required this.category,
required this.quantity,
required this.unitPrice,
required this.totalPrice,
required this.date,
});
factory Transaction.fromJson(Map<String, dynamic> json) {
return Transaction(
id: json['id'] as int,
productName: json['product_name'] as String,
category: json['category'] as String,
quantity: json['quantity'] as int,
unitPrice: json['unit_price'] as int,
totalPrice: json['total_price'] as int,
date: DateTime.parse(json['date'] as String),
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'product_name': productName,
'category': category,
'quantity': quantity,
'unit_price': unitPrice,
'total_price': totalPrice,
'date': date.toIso8601String(),
};
}
Transaction copyWith({
int? id,
String? productName,
String? category,
int? quantity,
int? unitPrice,
int? totalPrice,
DateTime? date,
}) {
return Transaction(
id: id ?? this.id,
productName: productName ?? this.productName,
category: category ?? this.category,
quantity: quantity ?? this.quantity,
unitPrice: unitPrice ?? this.unitPrice,
totalPrice: totalPrice ?? this.totalPrice,
date: date ?? this.date,
);
}
@override
String toString() =>
'Transaction(id: $id, product: $productName, qty: $quantity, date: $date)';
}

View File

@ -0,0 +1,135 @@
import 'package:flutter/material.dart';
import '../services/ml_service.dart';
class PredictionPage extends StatefulWidget {
const PredictionPage({Key? key}) : super(key: key);
@override
State<PredictionPage> createState() => _PredictionPageState();
}
class _PredictionPageState extends State<PredictionPage> {
bool _isLoading = false;
String? _errorMessage;
Map<String, dynamic>? _predictionResult;
final _tahunController = TextEditingController(text: '2024');
final _bulanController = TextEditingController(text: '4');
final _hariController = TextEditingController(text: '4');
final _hariDalamMingguController = TextEditingController(text: '3');
@override
void initState() {
super.initState();
_checkAPIHealth();
}
Future<void> _checkAPIHealth() async {
final isHealthy = await MLService.healthCheck();
if (!isHealthy) {
setState(() {
_errorMessage = 'API tidak tersedia. Pastikan server Python sudah berjalan.';
});
}
}
Future<void> _predict() async {
setState(() {
_isLoading = true;
_errorMessage = null;
_predictionResult = null;
});
try {
final result = await MLService.prediksiStok(
tahun: int.parse(_tahunController.text),
bulan: int.parse(_bulanController.text),
hari: int.parse(_hariController.text),
hariDalamMinggu: int.parse(_hariDalamMingguController.text),
hariMinggu: int.parse(_hariDalamMingguController.text),
hargaSatuanUpdate: 50000,
totalHargaUpdate: 250000,
produkEncoded: 2,
namaProdukEncoded: 2,
kategoriProdukEncoded: 1,
);
setState(() {
if (result['status'] == 'success') {
_predictionResult = result;
_errorMessage = null;
} else {
_errorMessage = result['message'] ?? 'Prediksi gagal';
_predictionResult = null;
}
});
} catch (e) {
setState(() {
_errorMessage = 'Error: $e';
_predictionResult = null;
});
} finally {
setState(() => _isLoading = false);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Prediksi Permintaan Stok')),
body: SingleChildScrollView(
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
if (_errorMessage != null)
Container(
padding: const EdgeInsets.all(12.0),
margin: const EdgeInsets.only(bottom: 16.0),
decoration: BoxDecoration(
color: Colors.red[100],
border: Border.all(color: Colors.red),
borderRadius: BorderRadius.circular(8.0),
),
child: Text(_errorMessage!, style: const TextStyle(color: Colors.red)),
),
TextField(controller: _tahunController, decoration: const InputDecoration(labelText: 'Tahun')),
TextField(controller: _bulanController, decoration: const InputDecoration(labelText: 'Bulan')),
TextField(controller: _hariController, decoration: const InputDecoration(labelText: 'Hari')),
TextField(controller: _hariDalamMingguController, decoration: const InputDecoration(labelText: 'Hari Minggu')),
const SizedBox(height: 20.0),
ElevatedButton(onPressed: _isLoading ? null : _predict, style: ElevatedButton.styleFrom(minimumSize: const Size.fromHeight(50)), child: _isLoading ? const CircularProgressIndicator() : const Text('PREDIKSI')),
const SizedBox(height: 20.0),
if (_predictionResult != null)
Card(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('HASIL PREDIKSI', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
const SizedBox(height: 12.0),
Text('Jumlah Unit: ${_predictionResult!['prediksi']['jumlah_unit']}', style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: Colors.green)),
Text('Nilai Raw: ${_predictionResult!['prediksi']['nilai_raw']}'),
const Divider(),
Text('R² Score: ${_predictionResult!['model_accuracy']['r2_score']}'),
Text('MAE: ${_predictionResult!['model_accuracy']['mae']}'),
Text('RMSE: ${_predictionResult!['model_accuracy']['rmse']}'),
],
),
),
),
],
),
),
);
}
@override
void dispose() {
_tahunController.dispose();
_bulanController.dispose();
_hariController.dispose();
_hariDalamMingguController.dispose();
super.dispose();
}
}

181
lib/screens/README.md Normal file
View File

@ -0,0 +1,181 @@
# Flutter Screens Documentation
## Completed Screens (Phase 5)
### 1. Splash Screen (`splash_screen.dart`)
- **Purpose**: Initial loading screen with API health check
- **Features**:
- Animated logo (scale transition with elasticOut curve)
- Fade animation for app title
- Rotating loading indicator
- API connectivity status indicator
- **Usage**: Set as initial route in main.dart
### 2. Login Screen (`login_screen.dart`)
- **Purpose**: User authentication
- **Features**:
- Username & password input fields
- Password visibility toggle
- Form validation
- "Forgot password" & "Sign up" links
- Loading state during login
- **TODO**: Implement actual authentication logic
### 3. Dashboard Screen (`dashboard_screen.dart`)
- **Purpose**: Main hub showing key metrics and quick actions
- **Features**:
- Welcome greeting
- 4 stat cards (sales, revenue, low stock, critical stock)
- Quick action buttons (4 main actions)
- Recent transactions list
- Bottom navigation bar
- **Navigation**: Central to app flow, navigates to other screens
### 4. Transaction Screen (`transaction_screen.dart`) ⭐ CRITICAL
- **Purpose**: Input sales data in real-time from shop
- **Features**:
- Product dropdown selection (auto-fills category)
- Quantity & price input with auto-calculated total
- Date picker for transaction date
- Form validation
- Transaction history display (in-memory for now)
- **Importance**: This is the PRIMARY data input for ML prediction model
- **Integration**: Saves transactions to local database (future enhancement)
### 5. Product List Screen (`product_list_screen.dart`)
- **Purpose**: View and manage product inventory
- **Features**:
- Product search functionality
- Filter by status (semua/tersedia/rendah/kritis)
- Summary cards (total, available, need stock)
- Product cards with:
- Name, category, status badge
- Price and stock quantity
- Edit button (placeholder)
- **Status Colors**:
- Tersedia (Available) = Green
- Rendah (Low) = Yellow
- Kritis (Critical) = Red
### 6. Prediction Screen (`prediction_screen.dart`)
- **Purpose**: ML model prediction for stock demand
- **Features**:
- Input form: Product, Category (auto-fill), Price, Date
- API integration with MLService
- Results display showing:
- Predicted quantity
- Raw value, estimated total price
- Model accuracy (R²) and error (MAE)
- Recommendation card based on prediction
- Error handling and loading state
- **Integration**: Calls `/prediksi` endpoint from Flask API
### 7. Report Screen (`report_screen.dart`)
- **Purpose**: Sales analytics and business intelligence
- **Features**:
- Period selector (harian/mingguan/bulanan/tahunan)
- 4 KPI cards with trend indicators
- Sales trend chart (placeholder for chart library)
- Top 4 selling products with progress bars
- Category breakdown with percentage distribution
- PDF export button
- **TODO**: Integrate with actual data and chart library (e.g., fl_chart)
## Design System
### Colors (`lib/theme/colors.dart`)
- **Primary**: Brown (#8B7355)
- **Secondary**: Blue, Green, Orange, Red
- **Status**: Success (Green), Warning (Yellow), Error (Red)
- **Neutrals**: Grey scale with light cream background
### Typography (`lib/theme/text_styles.dart`)
- Display, Headline, Title, Body, Label styles
- Multiple size variants (Large, Medium, Small)
- Proper font weights and letter spacing
### Theme (`lib/theme/app_theme.dart`)
- Material 3 design system
- Consistent theming for all components
- Custom input decoration, buttons, cards, navigation
## Navigation Flow
```
Splash Screen
Login Screen
Dashboard Screen (home)
├→ Transaction Screen (primary input)
├→ Product List Screen
├→ Prediction Screen
└→ Report Screen
```
## Data Models
- **Product** (`lib/models/product_model.dart`): Inventory items
- **Transaction** (`lib/models/transaction_model.dart`): Sales records
- **PredictionRequest/Result** (`lib/models/prediction_model.dart`): ML API data
## Next Steps
1. **Navigation Setup**:
- Create app routing/navigation
- Set Splash as initial route
- Connect bottom navigation
2. **State Management**:
- Implement Provider pattern for state
- Create TransactionProvider, PredictionProvider, etc.
3. **Local Storage**:
- Add Hive or SQLite for persistence
- Save transaction history
- Cache product list
4. **API Integration**:
- Complete MLService integration in Prediction Screen
- Handle transactions upload to backend
- Sync data between app and server
5. **Polish & Testing**:
- Add unit tests
- Test API connectivity
- Performance optimization
- Error boundary handling
## File Structure
```
lib/
├── screens/
│ ├── splash_screen.dart
│ ├── login_screen.dart
│ ├── dashboard_screen.dart
│ ├── transaction_screen.dart
│ ├── product_list_screen.dart
│ ├── prediction_screen.dart
│ └── report_screen.dart
├── models/
│ ├── product_model.dart
│ ├── transaction_model.dart
│ └── prediction_model.dart
├── theme/
│ ├── colors.dart
│ ├── text_styles.dart
│ └── app_theme.dart
└── services/
└── ml_service.dart
```

View File

@ -0,0 +1,540 @@
import 'package:flutter/material.dart';
import 'package:finalproject/theme/colors.dart';
import 'package:finalproject/theme/text_styles.dart';
class DashboardScreen extends StatefulWidget {
const DashboardScreen({Key? key}) : super(key: key);
@override
State<DashboardScreen> createState() => _DashboardScreenState();
}
class _DashboardScreenState extends State<DashboardScreen> {
int _selectedIndex = 0;
// Sample data for low stock items
final List<Map<String, dynamic>> lowStockItems = [
{
'name': 'Tepung Terigu',
'stock': '5 kg',
'status': 'Kritis',
'statusColor': AppColors.statusError,
},
{
'name': 'Gula Pasir',
'stock': '8 kg',
'status': 'Rendah',
'statusColor': AppColors.statusWarning,
},
{
'name': 'Mentega',
'stock': '3 kg',
'status': 'Kritis',
'statusColor': AppColors.statusError,
},
];
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: AppColors.bgLight,
appBar: PreferredSize(
preferredSize: const Size.fromHeight(240),
child: Container(
decoration: BoxDecoration(
color: AppColors.primaryBrown,
borderRadius: const BorderRadius.only(
bottomLeft: Radius.circular(24),
bottomRight: Radius.circular(24),
),
),
child: SafeArea(
child: Column(
children: [
// Top section: Menu + Greeting + Notification
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Top row: Menu + Notification
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.3),
borderRadius: BorderRadius.circular(10),
),
child: IconButton(
icon: const Icon(Icons.menu,
color: Colors.white, size: 20),
onPressed: () {},
padding: EdgeInsets.zero,
),
),
Stack(
children: [
Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.3),
borderRadius: BorderRadius.circular(10),
),
child: IconButton(
icon: const Icon(Icons.notifications_none,
color: Colors.white, size: 20),
onPressed: () {},
padding: EdgeInsets.zero,
),
),
Positioned(
right: 4,
top: 4,
child: Container(
width: 22,
height: 22,
decoration: BoxDecoration(
color: AppColors.statusError,
borderRadius: BorderRadius.circular(11),
border:
Border.all(
color: AppColors.primaryBrown,
width: 2,
),
),
child: Center(
child: Text(
'3',
style: const TextStyle(
color: Colors.white,
fontSize: 12,
fontWeight: FontWeight.bold,
),
),
),
),
),
],
),
],
),
// Greeting + Title
const SizedBox(height: 6),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: const [
Text(
'Selamat Datang',
style: TextStyle(
color: Colors.white70,
fontSize: 13,
fontWeight: FontWeight.w400,
),
),
SizedBox(height: 2),
Text(
'Admin Sulastri',
style: TextStyle(
color: Colors.white,
fontSize: 22,
fontWeight: FontWeight.w700,
letterSpacing: 0.5,
),
),
],
),
],
),
),
// Stats Cards section (overlap ke bawah)
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: Row(
children: [
Expanded(
child: _buildStatCard(
title: 'Total Penjualan',
value: 'Rp 67 Jt',
change: '+12.5%',
icon: Icons.trending_up,
iconBgColor: AppColors.statusSuccess,
),
),
const SizedBox(width: 12),
Expanded(
child: _buildStatCard(
title: 'Produk',
value: '24',
change: 'Aktif',
icon: Icons.shopping_bag,
iconBgColor: AppColors.secondaryBlue,
),
),
],
),
),
],
),
),
),
),
body: SingleChildScrollView(
child: Column(
children: [
Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 8),
// Chart Section
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: AppColors.bgWhite,
borderRadius: BorderRadius.circular(16),
boxShadow: [AppColors.shadowLight],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Grafik Penjualan',
style: AppTextStyles.headlineSmall.copyWith(
color: AppColors.textPrimary,
),
),
const SizedBox(height: 2),
Text(
'6 Bulan Terakhir',
style: AppTextStyles.bodySmall.copyWith(
color: AppColors.textTertiary,
),
),
],
),
Icon(Icons.trending_up,
color: AppColors.statusSuccess, size: 20),
],
),
const SizedBox(height: 20),
// Area chart placeholder dengan axes
SizedBox(
height: 200,
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Y-axis labels
...['80000000', '60000000', '40000000', '20000000',
'0']
.map((label) {
return Text(
label,
style: AppTextStyles.bodySmall.copyWith(
color: AppColors.grey300,
),
);
}).toList(),
],
),
),
const SizedBox(height: 12),
// X-axis labels
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children:
['Jan', 'Feb', 'Mar', 'Apr', 'Mei', 'Jun']
.map((month) {
return Text(
month,
style: AppTextStyles.labelSmall.copyWith(
color: AppColors.textTertiary,
),
);
}).toList(),
),
],
),
),
const SizedBox(height: 24),
// Low Stock Section
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: AppColors.bgWhite,
borderRadius: BorderRadius.circular(16),
boxShadow: [AppColors.shadowLight],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(Icons.warning_amber_rounded,
color: AppColors.statusWarning, size: 22),
const SizedBox(width: 10),
Text(
'Stok Menipis',
style: AppTextStyles.headlineSmall.copyWith(
color: AppColors.textPrimary,
),
),
],
),
const SizedBox(height: 16),
...lowStockItems.map((item) {
return Padding(
padding: const EdgeInsets.only(bottom: 16),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Column(
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
Text(
item['name'],
style: AppTextStyles.labelLarge.copyWith(
color: AppColors.textPrimary,
),
),
const SizedBox(height: 4),
Text(
'Stok: ${item['stock']}',
style: AppTextStyles.bodySmall.copyWith(
color: AppColors.textTertiary,
),
),
],
),
),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 12, vertical: 6),
decoration: BoxDecoration(
color: item['statusColor'],
borderRadius: BorderRadius.circular(6),
),
child: Text(
item['status'],
style: const TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
color: Colors.white,
),
),
),
],
),
);
}).toList(),
],
),
),
const SizedBox(height: 24),
// Action Buttons - BIGGER & BOLDER
Row(
children: [
Expanded(
child: ElevatedButton.icon(
onPressed: () {
Navigator.of(context).pushNamed('/prediction');
},
icon: const Icon(Icons.trending_up, size: 20),
label: const Text(
'Lihat Prediksi',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
),
),
style: ElevatedButton.styleFrom(
backgroundColor: AppColors.secondaryBlue,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 16),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(14),
),
elevation: 2,
),
),
),
const SizedBox(width: 12),
Expanded(
child: ElevatedButton.icon(
onPressed: () {},
icon: const Icon(Icons.check_circle, size: 20),
label: const Text(
'Rekomendasi Stok',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
),
),
style: ElevatedButton.styleFrom(
backgroundColor: AppColors.statusSuccess,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 16),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(14),
),
elevation: 2,
),
),
),
],
),
const SizedBox(height: 24),
],
),
),
],
),
),
bottomNavigationBar: BottomNavigationBar(
currentIndex: _selectedIndex,
onTap: (index) {
setState(() => _selectedIndex = index);
switch (index) {
case 0:
break;
case 1:
Navigator.of(context).pushNamed('/products');
break;
case 2:
Navigator.of(context).pushNamed('/transaction');
break;
case 3:
Navigator.of(context).pushNamed('/prediction');
break;
case 4:
Navigator.of(context).pushNamed('/reports');
break;
case 5:
Navigator.of(context).pushNamed('/settings');
break;
}
},
type: BottomNavigationBarType.fixed,
backgroundColor: Colors.white,
items: const [
BottomNavigationBarItem(
icon: Icon(Icons.home_outlined),
label: 'Dashboard',
),
BottomNavigationBarItem(
icon: Icon(Icons.shopping_bag_outlined),
label: 'Produk',
),
BottomNavigationBarItem(
icon: Icon(Icons.shopping_cart_outlined),
label: 'Transaksi',
),
BottomNavigationBarItem(
icon: Icon(Icons.trending_up_outlined),
label: 'Prediksi',
),
BottomNavigationBarItem(
icon: Icon(Icons.description_outlined),
label: 'Laporan',
),
BottomNavigationBarItem(
icon: Icon(Icons.settings_outlined),
label: 'Pengaturan',
),
],
),
);
}
Widget _buildStatCard({
required String title,
required String value,
required String change,
required IconData icon,
required Color iconBgColor,
}) {
return Container(
padding: const EdgeInsets.all(14),
decoration: BoxDecoration(
color: AppColors.bgWhite,
borderRadius: BorderRadius.circular(14),
boxShadow: [AppColors.shadowLight],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: AppTextStyles.bodySmall.copyWith(
color: AppColors.textTertiary,
fontWeight: FontWeight.w500,
),
),
const SizedBox(height: 6),
Text(
value,
style: AppTextStyles.titleLarge.copyWith(
color: AppColors.textPrimary,
fontWeight: FontWeight.w700,
),
),
const SizedBox(height: 6),
Text(
change,
style: AppTextStyles.bodySmall.copyWith(
color:
change.contains('+')
? AppColors.statusSuccess
: AppColors.textTertiary,
fontWeight: FontWeight.w600,
),
),
],
),
),
Container(
width: 44,
height: 44,
decoration: BoxDecoration(
color: iconBgColor.withOpacity(0.15),
borderRadius: BorderRadius.circular(10),
),
child: Center(
child: Icon(
icon,
color: iconBgColor,
size: 22,
),
),
),
],
),
],
),
);
}
}

View File

@ -0,0 +1,325 @@
import 'package:flutter/material.dart';
import 'package:finalproject/theme/colors.dart';
import 'package:finalproject/theme/text_styles.dart';
class LoginScreen extends StatefulWidget {
const LoginScreen({Key? key}) : super(key: key);
@override
State<LoginScreen> createState() => _LoginScreenState();
}
class _LoginScreenState extends State<LoginScreen> {
late TextEditingController _usernameController;
late TextEditingController _passwordController;
bool _isPasswordVisible = false;
bool _isLoading = false;
@override
void initState() {
super.initState();
_usernameController = TextEditingController(text: 'admin@sulastri.com');
_passwordController = TextEditingController(text: 'password');
}
@override
void dispose() {
_usernameController.dispose();
_passwordController.dispose();
super.dispose();
}
Future<void> _handleLogin() async {
// Validation
if (_usernameController.text.isEmpty || _passwordController.text.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: const Text('Email dan password tidak boleh kosong'),
backgroundColor: AppColors.statusError,
),
);
return;
}
setState(() => _isLoading = true);
// Simulate login process
await Future.delayed(const Duration(seconds: 1));
setState(() => _isLoading = false);
if (mounted) {
Navigator.of(context).pushReplacementNamed('/dashboard');
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: AppColors.bgLight,
body: SafeArea(
child: SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 20.0),
child: Center(
child: Column(
children: [
const SizedBox(height: 40),
// Card Container
Container(
width: double.infinity,
decoration: BoxDecoration(
color: AppColors.bgWhite,
borderRadius: BorderRadius.circular(16),
boxShadow: [AppColors.shadowLight],
),
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 24.0,
vertical: 32.0,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
// Logo Icon
Container(
width: 72,
height: 72,
decoration: BoxDecoration(
color: AppColors.primaryBrown,
borderRadius: BorderRadius.circular(16),
),
child: const Icon(
Icons.home_rounded,
color: Colors.white,
size: 40,
),
),
const SizedBox(height: 24),
// Greeting Title
Text(
'Selamat Datang',
style: AppTextStyles.displaySmall.copyWith(
color: AppColors.primaryBrown,
fontWeight: FontWeight.w700,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 8),
// Subtitle
Text(
'Masuk ke akun Anda',
style: AppTextStyles.bodyMedium.copyWith(
color: AppColors.textSecondary,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 32),
// Email/Username Label
Align(
alignment: Alignment.centerLeft,
child: Text(
'Email atau Username',
style: AppTextStyles.labelLarge.copyWith(
color: AppColors.textPrimary,
),
),
),
const SizedBox(height: 8),
// Email/Username Field
TextField(
controller: _usernameController,
enabled: !_isLoading,
decoration: InputDecoration(
hintText: 'Masukkan email atau username',
hintStyle:
AppTextStyles.bodyMedium.copyWith(
color: AppColors.grey300,
),
prefixIcon: Icon(
Icons.email_outlined,
color: AppColors.textSecondary,
),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
borderSide:
BorderSide(color: AppColors.grey300),
),
filled: true,
fillColor: AppColors.bgLight,
contentPadding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 12,
),
),
),
const SizedBox(height: 24),
// Password Label
Align(
alignment: Alignment.centerLeft,
child: Text(
'Password',
style: AppTextStyles.labelLarge.copyWith(
color: AppColors.textPrimary,
),
),
),
const SizedBox(height: 8),
// Password Field
TextField(
controller: _passwordController,
enabled: !_isLoading,
obscureText: !_isPasswordVisible,
decoration: InputDecoration(
hintText: 'Masukkan password',
hintStyle:
AppTextStyles.bodyMedium.copyWith(
color: AppColors.grey300,
),
prefixIcon: Icon(
Icons.lock_outline,
color: AppColors.textSecondary,
),
suffixIcon: IconButton(
icon: Icon(
_isPasswordVisible
? Icons.visibility
: Icons.visibility_off,
color: AppColors.textSecondary,
),
onPressed: _isLoading
? null
: () {
setState(() {
_isPasswordVisible =
!_isPasswordVisible;
});
},
),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
borderSide:
BorderSide(color: AppColors.grey300),
),
filled: true,
fillColor: AppColors.bgLight,
contentPadding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 12,
),
),
),
const SizedBox(height: 16),
// Forgot Password Link
Align(
alignment: Alignment.centerRight,
child: TextButton(
onPressed: _isLoading ? null : () {},
style: TextButton.styleFrom(
padding: EdgeInsets.zero,
),
child: Text(
'Lupa Password?',
style: AppTextStyles.labelMedium.copyWith(
color: AppColors.primaryBrown,
fontWeight: FontWeight.w600,
),
),
),
),
const SizedBox(height: 32),
// Login Button
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: _isLoading ? null : _handleLogin,
style: ElevatedButton.styleFrom(
backgroundColor: AppColors.primaryBrown,
foregroundColor: Colors.white,
padding:
const EdgeInsets.symmetric(vertical: 14),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
),
child: _isLoading
? const SizedBox(
height: 20,
width: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor:
AlwaysStoppedAnimation<Color>(
Colors.white,
),
),
)
: Text(
'Masuk',
style:
AppTextStyles.labelLarge.copyWith(
color: Colors.white,
fontWeight: FontWeight.w700,
),
),
),
),
const SizedBox(height: 24),
// Demo Info Box
Container(
width: double.infinity,
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: AppColors.bgLight,
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: AppColors.grey300,
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Demo: Klik "Masuk" untuk melanjutkan',
style: AppTextStyles.bodySmall.copyWith(
color: AppColors.statusError,
fontWeight: FontWeight.w600,
),
),
],
),
),
],
),
),
),
const SizedBox(height: 48),
// Footer Copyright
Text(
'© 2025 Toko Bahan Kue Sulastri',
style: AppTextStyles.bodySmall.copyWith(
color: AppColors.textTertiary,
),
),
const SizedBox(height: 16),
],
),
),
),
),
),
);
}
}

View File

@ -0,0 +1,878 @@
import 'package:flutter/material.dart';
import 'package:finalproject/theme/colors.dart';
import 'package:finalproject/theme/text_styles.dart';
class PredictionScreen extends StatefulWidget {
const PredictionScreen({Key? key}) : super(key: key);
@override
State<PredictionScreen> createState() => _PredictionScreenState();
}
class _PredictionScreenState extends State<PredictionScreen> {
String? _selectedRecipe;
int _productionQuantity = 0;
bool _isCalculated = false;
// Produk yang bisa dibuat
final List<String> recipes = [
'Donat',
'Roti Putih',
'Kue Brownies',
'Kue Tart',
];
// Resep untuk setiap produk (ingredient: gram/butir per unit)
final Map<String, Map<String, int>> recipeDetails = {
'Donat': {
'Tepung Terigu 1kg': 500,
'Telur 1kg': 2,
'Gula Pasir 1kg': 100,
'Mentega 500gr': 50,
'Baking Powder': 5,
},
'Roti Putih': {
'Tepung Terigu 1kg': 800,
'Telur 1kg': 3,
'Gula Pasir 1kg': 80,
'Mentega 500gr': 80,
'Susu Bubuk': 50,
'Baking Powder': 8,
},
'Kue Brownies': {
'Tepung Terigu 1kg': 300,
'Cokelat Bubuk 250gr': 100,
'Telur 1kg': 4,
'Gula Pasir 1kg': 200,
'Mentega 500gr': 150,
'Baking Powder': 5,
},
'Kue Tart': {
'Tepung Terigu 1kg': 400,
'Telur 1kg': 5,
'Gula Pasir 1kg': 150,
'Mentega 500gr': 200,
'Keju Parut 250gr': 100,
'Susu Bubuk': 80,
},
};
// Stok saat ini (sama dengan di product list)
final Map<String, int> currentStock = {
'Tepung Terigu 1kg': 45000, // gram
'Telur 1kg': 12, // butir
'Gula Pasir 1kg': 28000, // gram
'Susu Bubuk': 8000, // gram
'Cokelat Bubuk 250gr': 22000, // gram
'Mentega 500gr': 15000, // gram
'Keju Parut 250gr': 3000, // gram
'Baking Powder': 60000, // gram
};
// Satuan untuk setiap ingredient
final Map<String, String> ingredientUnits = {
'Tepung Terigu 1kg': 'gr',
'Telur 1kg': 'butir',
'Gula Pasir 1kg': 'gr',
'Susu Bubuk': 'gr',
'Cokelat Bubuk 250gr': 'gr',
'Mentega 500gr': 'gr',
'Keju Parut 250gr': 'gr',
'Baking Powder': 'gr',
};
Map<String, int> get requiredIngredients {
if (_selectedRecipe == null || _productionQuantity == 0) {
return {};
}
final recipe = recipeDetails[_selectedRecipe]!;
return recipe.map((ingredient, amount) =>
MapEntry(ingredient, amount * _productionQuantity));
}
Map<String, int> get insufficientStock {
final required = requiredIngredients;
final insufficient = <String, int>{};
required.forEach((ingredient, neededAmount) {
final available = currentStock[ingredient] ?? 0;
if (available < neededAmount) {
insufficient[ingredient] = neededAmount - available;
}
});
return insufficient;
}
bool get isStockSufficient => insufficientStock.isEmpty;
Color _getStatusColor(String ingredient) {
final required = requiredIngredients[ingredient] ?? 0;
final available = currentStock[ingredient] ?? 0;
return available >= required ? Color(0xFF10B981) : Color(0xFFDC2626);
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Color(0xFFF5F5F5),
// Brown Header
appBar: PreferredSize(
preferredSize: const Size.fromHeight(200),
child: Container(
decoration: BoxDecoration(
color: Color(0xFFA89080),
borderRadius: const BorderRadius.only(
bottomLeft: Radius.circular(24),
bottomRight: Radius.circular(24),
),
),
child: SafeArea(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Back button + Title
Row(
children: [
GestureDetector(
onTap: () => Navigator.pop(context),
child: Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.3),
borderRadius: BorderRadius.circular(10),
),
child: const Icon(Icons.arrow_back,
color: Colors.white, size: 20),
),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: const [
Text(
'Prediksi Kebutuhan Bahan',
style: TextStyle(
color: Colors.white,
fontSize: 18,
fontWeight: FontWeight.w700,
),
),
SizedBox(height: 2),
Text(
'Kalkulasi bahan berdasarkan rencana produksi',
style: TextStyle(
color: Colors.white70,
fontSize: 12,
fontWeight: FontWeight.w400,
),
),
],
),
),
],
),
const SizedBox(height: 12),
// Info Box
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.15),
borderRadius: BorderRadius.circular(10),
border: Border.all(
color: Colors.white.withOpacity(0.2),
width: 1,
),
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
width: 32,
height: 32,
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.2),
borderRadius: BorderRadius.circular(6),
),
child: const Icon(Icons.calculate,
color: Colors.white, size: 18),
),
const SizedBox(width: 10),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: const [
Text(
'Sistem Kalkulasi Otomatis',
style: TextStyle(
color: Colors.white,
fontSize: 12,
fontWeight: FontWeight.w600,
),
),
SizedBox(height: 4),
Text(
'Pilih produk dan jumlah untuk lihat kebutuhan bahan',
style: TextStyle(
color: Colors.white70,
fontSize: 11,
fontWeight: FontWeight.w400,
height: 1.3,
),
),
],
),
),
],
),
),
],
),
),
),
),
),
body: SingleChildScrollView(
child: Column(
children: [
Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Input Form Section
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 10,
offset: const Offset(0, 2),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Rencana Produksi',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: Color(0xFF1F2937),
),
),
const SizedBox(height: 16),
// Product Selection
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Pilih Produk',
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.w600,
color: Color(0xFF1F2937),
),
),
const SizedBox(height: 8),
Container(
decoration: BoxDecoration(
border: Border.all(
color: Color(0xFFE5E7EB),
width: 1,
),
borderRadius: BorderRadius.circular(10),
),
child: DropdownButtonHideUnderline(
child: DropdownButton<String>(
value: _selectedRecipe,
hint: const Padding(
padding: EdgeInsets.symmetric(horizontal: 12),
child: Text(
'-- Pilih Produk --',
style: TextStyle(
color: Color(0xFF9CA3AF),
fontSize: 14,
),
),
),
isExpanded: true,
icon: const Padding(
padding: EdgeInsets.only(right: 12),
child: Icon(Icons.expand_more,
color: Color(0xFF9CA3AF)),
),
items: recipes
.map((recipe) =>
DropdownMenuItem(
value: recipe,
child: Padding(
padding:
const EdgeInsets.symmetric(
horizontal: 12),
child: Text(recipe),
),
))
.toList(),
onChanged: (value) {
setState(() {
_selectedRecipe = value;
_isCalculated = false;
});
},
),
),
),
],
),
const SizedBox(height: 16),
// Quantity Input
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Jumlah Produksi',
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.w600,
color: Color(0xFF1F2937),
),
),
const SizedBox(height: 8),
TextField(
keyboardType: TextInputType.number,
onChanged: (value) {
setState(() {
_productionQuantity =
int.tryParse(value) ?? 0;
_isCalculated = false;
});
},
decoration: InputDecoration(
hintText: '0',
hintStyle:
const TextStyle(color: Color(0xFF9CA3AF)),
suffixText: 'pcs',
suffixStyle: const TextStyle(
color: Color(0xFF9CA3AF),
fontSize: 12,
),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
borderSide: const BorderSide(
color: Color(0xFFE5E7EB),
),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
borderSide: const BorderSide(
color: Color(0xFFE5E7EB),
),
),
contentPadding:
const EdgeInsets.symmetric(
horizontal: 12, vertical: 12),
),
),
],
),
const SizedBox(height: 20),
// Calculate Button
Row(
children: [
Expanded(
child: ElevatedButton.icon(
onPressed: (_selectedRecipe != null &&
_productionQuantity > 0)
? () {
setState(() => _isCalculated = true);
}
: null,
icon: const Icon(Icons.calculate, size: 18),
label: const Text(
'Hitung Kebutuhan',
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.w600,
),
),
style: ElevatedButton.styleFrom(
backgroundColor: Color(0xFFA89080),
foregroundColor: Colors.white,
padding:
const EdgeInsets.symmetric(vertical: 12),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
),
),
),
const SizedBox(width: 10),
OutlinedButton(
onPressed: () {
setState(() {
_selectedRecipe = null;
_productionQuantity = 0;
_isCalculated = false;
});
},
style: OutlinedButton.styleFrom(
side: const BorderSide(
color: Color(0xFFE5E7EB),
width: 1,
),
padding:
const EdgeInsets.symmetric(horizontal: 20),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
),
child: const Text(
'Reset',
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.w600,
color: Color(0xFF1F2937),
),
),
),
],
),
],
),
),
const SizedBox(height: 24),
// Results Section
if (_isCalculated) ...[
// Status Card
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: isStockSufficient
? Color(0xFF10B981).withOpacity(0.1)
: Color(0xFFDC2626).withOpacity(0.1),
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: isStockSufficient
? Color(0xFF10B981).withOpacity(0.3)
: Color(0xFFDC2626).withOpacity(0.3),
width: 1,
),
),
child: Row(
children: [
Container(
width: 48,
height: 48,
decoration: BoxDecoration(
color: isStockSufficient
? Color(0xFF10B981).withOpacity(0.2)
: Color(0xFFDC2626).withOpacity(0.2),
borderRadius: BorderRadius.circular(12),
),
child: Icon(
isStockSufficient
? Icons.check_circle
: Icons.warning_amber,
color: isStockSufficient
? Color(0xFF10B981)
: Color(0xFFDC2626),
size: 24,
),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
isStockSufficient
? 'Stok Cukup'
: 'Stok Belum Cukup',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w700,
color: isStockSufficient
? Color(0xFF10B981)
: Color(0xFFDC2626),
),
),
const SizedBox(height: 2),
Text(
isStockSufficient
? 'Semua bahan tersedia untuk produksi'
: 'Ada bahan yang perlu ditambah',
style: const TextStyle(
fontSize: 12,
color: Color(0xFF6B7280),
),
),
],
),
),
],
),
),
const SizedBox(height: 16),
// Ingredients Table
Text(
'Kebutuhan Bahan',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w700,
color: Color(0xFF1F2937),
),
),
const SizedBox(height: 12),
Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 10,
offset: const Offset(0, 2),
),
],
),
child: Column(
children: requiredIngredients.entries
.toList()
.asMap()
.entries
.map((entry) {
final isLast =
entry.key == requiredIngredients.length - 1;
final ingredient = entry.value.key;
final neededAmount = entry.value.value;
final availableAmount =
currentStock[ingredient] ?? 0;
final unit = ingredientUnits[ingredient] ?? 'gr';
final isSufficient =
availableAmount >= neededAmount;
return Column(
children: [
Padding(
padding: const EdgeInsets.all(14),
child: Column(
children: [
Row(
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
Expanded(
child: Column(
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
Text(
ingredient
.replaceAll(
RegExp(r' \d+(kg|gr)'),
'')
.trim(),
style: const TextStyle(
fontSize: 13,
fontWeight: FontWeight.w600,
color: Color(0xFF1F2937),
),
),
const SizedBox(height: 6),
Row(
children: [
Expanded(
child: Column(
crossAxisAlignment:
CrossAxisAlignment
.start,
children: [
Text(
'Kebutuhan',
style: TextStyle(
fontSize: 11,
color: Color(
0xFF9CA3AF),
),
),
Text(
'$neededAmount $unit',
style:
const TextStyle(
fontSize: 12,
fontWeight:
FontWeight.w700,
color: Color(
0xFF1F2937),
),
),
],
),
),
Expanded(
child: Column(
crossAxisAlignment:
CrossAxisAlignment
.start,
children: [
Text(
'Stok Saat Ini',
style: TextStyle(
fontSize: 11,
color: Color(
0xFF9CA3AF),
),
),
Text(
'$availableAmount $unit',
style:
const TextStyle(
fontSize: 12,
fontWeight:
FontWeight.w700,
color: Color(
0xFF1F2937),
),
),
],
),
),
],
),
],
),
),
const SizedBox(width: 10),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 10,
vertical: 6,
),
decoration: BoxDecoration(
color: _getStatusColor(ingredient)
.withOpacity(0.15),
borderRadius:
BorderRadius.circular(8),
border: Border.all(
color: _getStatusColor(ingredient)
.withOpacity(0.3),
width: 1,
),
),
child: Text(
isSufficient ? 'Aman' : 'Kurang',
style: TextStyle(
fontSize: 11,
fontWeight: FontWeight.w700,
color: _getStatusColor(
ingredient),
),
),
),
],
),
],
),
),
if (!isLast)
Container(
height: 1,
color: Color(0xFFF3F4F6),
),
],
);
}).toList(),
),
),
const SizedBox(height: 16),
// Recommendation Card
if (!isStockSufficient) ...[
Text(
'Rekomendasi Penambahan Stok',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w700,
color: Color(0xFF1F2937),
),
),
const SizedBox(height: 12),
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Color(0xFFDC2626).withOpacity(0.05),
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: Color(0xFFDC2626).withOpacity(0.2),
width: 1,
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: insufficientStock.entries
.map((entry) {
final ingredient = entry.key;
final deficitAmount = entry.value;
final unit =
ingredientUnits[ingredient] ?? 'gr';
return Padding(
padding:
const EdgeInsets.symmetric(vertical: 8),
child: Row(
mainAxisAlignment:
MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Text(
ingredient
.replaceAll(
RegExp(r' \d+(kg|gr)'), '')
.trim(),
style: const TextStyle(
fontSize: 13,
fontWeight: FontWeight.w600,
color: Color(0xFF1F2937),
),
),
),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 6,
),
decoration: BoxDecoration(
color: Color(0xFFDC2626)
.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
),
child: Text(
'+$deficitAmount $unit',
style: const TextStyle(
fontSize: 12,
fontWeight: FontWeight.w700,
color: Color(0xFFDC2626),
),
),
),
],
),
);
}).toList(),
),
),
],
] else
Container(
width: double.infinity,
padding: const EdgeInsets.all(32),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 10,
offset: const Offset(0, 2),
),
],
),
child: Column(
children: [
Container(
width: 60,
height: 60,
decoration: BoxDecoration(
color: Color(0xFF9CA3AF).withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
),
child: const Icon(Icons.calculate,
color: Color(0xFF9CA3AF), size: 32),
),
const SizedBox(height: 16),
const Text(
'Belum Ada Kalkulasi',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: Color(0xFF1F2937),
),
),
const SizedBox(height: 8),
const Text(
'Pilih produk dan masukkan jumlah produksi untuk melihat kebutuhan bahan',
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 13,
color: Color(0xFF9CA3AF),
height: 1.5,
),
),
],
),
),
],
),
),
],
),
),
bottomNavigationBar: BottomNavigationBar(
currentIndex: 3,
type: BottomNavigationBarType.fixed,
onTap: (index) {
switch (index) {
case 0:
Navigator.popUntil(context, (route) => route.isFirst);
break;
case 1:
Navigator.pushNamed(context, '/products');
break;
case 2:
Navigator.pushNamed(context, '/transaction');
break;
case 4:
Navigator.pushNamed(context, '/reports');
break;
}
},
items: const [
BottomNavigationBarItem(
icon: Icon(Icons.home_outlined),
label: 'Dashboard',
),
BottomNavigationBarItem(
icon: Icon(Icons.shopping_bag_outlined),
label: 'Produk',
),
BottomNavigationBarItem(
icon: Icon(Icons.shopping_cart_outlined),
label: 'Transaksi',
),
BottomNavigationBarItem(
icon: Icon(Icons.trending_up_outlined),
label: 'Prediksi',
),
BottomNavigationBarItem(
icon: Icon(Icons.description_outlined),
label: 'Laporan',
),
],
),
);
}
}

View File

@ -0,0 +1,634 @@
import 'package:flutter/material.dart';
import 'package:finalproject/models/product_model.dart';
import 'package:finalproject/theme/colors.dart';
import 'package:finalproject/theme/text_styles.dart';
class ProductListScreen extends StatefulWidget {
const ProductListScreen({Key? key}) : super(key: key);
@override
State<ProductListScreen> createState() => _ProductListScreenState();
}
class _ProductListScreenState extends State<ProductListScreen> {
String _selectedFilter = 'semua';
String _searchQuery = '';
final List<Product> products = [
Product(
id: 1,
name: 'Tepung Terigu 1kg',
category: 'Tepung',
price: 15000,
stock: 45,
status: 'tersedia',
),
Product(
id: 2,
name: 'Telur 1kg',
category: 'Telur',
price: 35000,
stock: 12,
status: 'rendah',
),
Product(
id: 3,
name: 'Gula Pasir 1kg',
category: 'Gula',
price: 20000,
stock: 28,
status: 'tersedia',
),
Product(
id: 4,
name: 'Susu Bubuk',
category: 'Susu',
price: 45000,
stock: 8,
status: 'kritis',
),
Product(
id: 5,
name: 'Cokelat Bubuk 250gr',
category: 'Cokelat',
price: 35000,
stock: 22,
status: 'tersedia',
),
Product(
id: 6,
name: 'Mentega 500gr',
category: 'Mentega',
price: 50000,
stock: 15,
status: 'tersedia',
),
Product(
id: 7,
name: 'Keju Parut 250gr',
category: 'Keju',
price: 40000,
stock: 3,
status: 'rendah',
),
Product(
id: 8,
name: 'Baking Powder',
category: 'Bahan Tambahan',
price: 12000,
stock: 60,
status: 'tersedia',
),
];
List<Product> get filteredProducts {
List<Product> result = products;
// Filter by status
if (_selectedFilter != 'semua') {
result = result.where((p) => p.status == _selectedFilter).toList();
}
// Filter by search
if (_searchQuery.isNotEmpty) {
result = result
.where((p) =>
p.name.toLowerCase().contains(_searchQuery.toLowerCase()) ||
p.category.toLowerCase().contains(_searchQuery.toLowerCase()))
.toList();
}
return result;
}
Color _getStatusColor(String status) {
switch (status) {
case 'tersedia':
return const Color(0xFF10B981);
case 'rendah':
return const Color(0xFFFB923C);
case 'kritis':
return const Color(0xFFDC2626);
default:
return const Color(0xFF9CA3AF);
}
}
IconData _getCategoryIcon(String category) {
switch (category) {
case 'Tepung':
return Icons.grain;
case 'Telur':
return Icons.circle;
case 'Gula':
return Icons.blur_circular;
case 'Susu':
return Icons.local_drink;
case 'Cokelat':
return Icons.square_rounded;
case 'Mentega':
return Icons.spa;
case 'Keju':
return Icons.lunch_dining;
case 'Bahan Tambahan':
return Icons.miscellaneous_services;
default:
return Icons.shopping_bag;
}
}
String _formatPrice(int price) {
return 'Rp ${(price ~/ 1000).toString()}K';
}
int _getMaxStock() {
return products.fold<int>(0, (max, p) => p.stock > max ? p.stock : max);
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: AppColors.bgLight,
// Brown Header with Search
appBar: PreferredSize(
preferredSize: const Size.fromHeight(160),
child: Container(
decoration: BoxDecoration(
color: AppColors.primaryBrown,
borderRadius: const BorderRadius.only(
bottomLeft: Radius.circular(24),
bottomRight: Radius.circular(24),
),
),
child: SafeArea(
child: Column(
children: [
// Top: Back button & Title
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
child: Row(
children: [
GestureDetector(
onTap: () => Navigator.pop(context),
child: Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.3),
borderRadius: BorderRadius.circular(10),
),
child: const Icon(
Icons.arrow_back,
color: Colors.white,
size: 20,
),
),
),
const SizedBox(width: 12),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: const [
Text(
'Data Produk',
style: TextStyle(
color: Colors.white,
fontSize: 18,
fontWeight: FontWeight.w700,
),
),
SizedBox(height: 2),
Text(
'8 Produk',
style: TextStyle(
color: Colors.white70,
fontSize: 12,
fontWeight: FontWeight.w400,
),
),
],
),
],
),
),
// Bottom: Search Bar (dalam brown area)
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: Container(
decoration: BoxDecoration(
border: Border.all(
color: AppColors.bgWhite,
width: 1,
),
borderRadius: BorderRadius.circular(12),
color: Colors.white,
),
child: TextField(
onChanged: (value) {
setState(() => _searchQuery = value);
},
decoration: InputDecoration(
hintText: 'Cari produk...',
hintStyle: AppTextStyles.bodySmall.copyWith(
color: AppColors.textSecondary,
),
prefixIcon: Icon(
Icons.search,
color: AppColors.textSecondary,
size: 20,
),
border: InputBorder.none,
contentPadding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 10,
),
),
),
),
),
],
),
),
),
),
body: SingleChildScrollView(
child: Column(
children: [
// Filter Chips Section (moved to body)
Container(
color: AppColors.bgWhite,
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
children: [
'semua',
'tersedia',
'rendah',
'kritis'
]
.map((filter) => Padding(
padding: const EdgeInsets.only(right: 8),
child: FilterChip(
label: Text(
_capitalize(filter),
style: AppTextStyles.labelSmall.copyWith(
color: _selectedFilter == filter
? Colors.white
: AppColors.textSecondary,
fontWeight: FontWeight.w600,
),
),
backgroundColor: _selectedFilter == filter
? AppColors.primaryBrown
: AppColors.bgLight,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
side: BorderSide(
color: _selectedFilter == filter
? AppColors.primaryBrown
: AppColors.grey200,
width: 1,
),
),
onSelected: (selected) {
setState(() => _selectedFilter = filter);
},
),
))
.toList(),
),
),
),
// Products List
Padding(
padding: const EdgeInsets.all(16),
child: filteredProducts.isNotEmpty
? Column(
children: filteredProducts
.map((product) => _buildProductCard(product))
.toList()
.expand((card) =>
[card, const SizedBox(height: 12)])
.toList(),
)
: Center(
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 60),
child: Column(
children: [
Container(
width: 80,
height: 80,
decoration: BoxDecoration(
color: AppColors.grey200,
borderRadius: BorderRadius.circular(20),
),
child: Icon(
Icons.shopping_bag_outlined,
size: 40,
color: AppColors.textSecondary,
),
),
const SizedBox(height: 16),
Text(
'Produk Tidak Ditemukan',
style: AppTextStyles.headlineSmall.copyWith(
color: AppColors.textPrimary,
),
),
const SizedBox(height: 8),
Text(
'Coba ubah filter atau cari dengan kata kunci lain',
style: AppTextStyles.bodySmall.copyWith(
color: AppColors.textSecondary,
),
textAlign: TextAlign.center,
),
],
),
),
),
),
],
),
),
bottomNavigationBar: BottomNavigationBar(
currentIndex: 1,
type: BottomNavigationBarType.fixed,
onTap: (index) {
switch (index) {
case 0:
Navigator.popUntil(context, (route) => route.isFirst);
break;
case 2:
Navigator.pushNamed(context, '/transaction');
break;
case 3:
Navigator.pushNamed(context, '/prediction');
break;
case 4:
Navigator.pushNamed(context, '/reports');
break;
}
},
items: const [
BottomNavigationBarItem(
icon: Icon(Icons.home_outlined),
label: 'Dashboard',
),
BottomNavigationBarItem(
icon: Icon(Icons.shopping_bag_outlined),
label: 'Produk',
),
BottomNavigationBarItem(
icon: Icon(Icons.shopping_cart_outlined),
label: 'Transaksi',
),
BottomNavigationBarItem(
icon: Icon(Icons.trending_up_outlined),
label: 'Prediksi',
),
BottomNavigationBarItem(
icon: Icon(Icons.description_outlined),
label: 'Laporan',
),
],
),
);
}
Widget _buildProductCard(Product product) {
final maxStock = _getMaxStock();
final stockPercentage = (product.stock / maxStock * 100).toInt();
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: AppColors.bgWhite,
borderRadius: BorderRadius.circular(16),
boxShadow: [AppColors.shadowLight],
),
child: Column(
children: [
// Top Row: Icon & Info
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Product Icon (Category-based)
Container(
width: 56,
height: 56,
decoration: BoxDecoration(
color: _getStatusColor(product.status).withOpacity(0.12),
borderRadius: BorderRadius.circular(14),
),
child: Icon(
_getCategoryIcon(product.category),
color: _getStatusColor(product.status),
size: 28,
),
),
const SizedBox(width: 14),
// Product Info (Middle)
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
product.name,
style: AppTextStyles.labelLarge.copyWith(
color: AppColors.textPrimary,
fontWeight: FontWeight.w700,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 6),
Row(
children: [
Container(
padding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 3,
),
decoration: BoxDecoration(
color: AppColors.primaryBrown.withOpacity(0.1),
borderRadius: BorderRadius.circular(6),
),
child: Text(
product.category,
style: AppTextStyles.labelSmall.copyWith(
color: AppColors.primaryBrown,
fontWeight: FontWeight.w600,
),
),
),
const SizedBox(width: 8),
Text(
_formatPrice(product.price),
style: AppTextStyles.labelSmall.copyWith(
color: AppColors.textSecondary,
fontWeight: FontWeight.w600,
),
),
],
),
],
),
),
// Status Badge (Right)
Container(
padding: const EdgeInsets.symmetric(
horizontal: 10,
vertical: 6,
),
decoration: BoxDecoration(
color: _getStatusColor(product.status).withOpacity(0.15),
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: _getStatusColor(product.status).withOpacity(0.3),
width: 1,
),
),
child: Text(
_getStatusLabel(product.status),
style: AppTextStyles.labelSmall.copyWith(
color: _getStatusColor(product.status),
fontWeight: FontWeight.w700,
),
),
),
],
),
const SizedBox(height: 12),
// Divider
Container(
height: 1,
color: AppColors.grey200,
),
const SizedBox(height: 12),
// Bottom Row: Stock Info
Row(
children: [
// Stock Display
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Stok Tersedia',
style: AppTextStyles.labelSmall.copyWith(
color: AppColors.textSecondary,
fontWeight: FontWeight.w500,
),
),
const SizedBox(height: 4),
Text(
'${product.stock} kg',
style: AppTextStyles.labelLarge.copyWith(
color: AppColors.textPrimary,
fontWeight: FontWeight.w700,
),
),
],
),
const SizedBox(width: 16),
// Stock Progress Bar
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Kapasitas',
style: AppTextStyles.labelSmall.copyWith(
color: AppColors.textSecondary,
fontWeight: FontWeight.w500,
),
),
Text(
'$stockPercentage%',
style: AppTextStyles.labelSmall.copyWith(
color: AppColors.textPrimary,
fontWeight: FontWeight.w700,
),
),
],
),
const SizedBox(height: 6),
ClipRRect(
borderRadius: BorderRadius.circular(6),
child: LinearProgressIndicator(
value: product.stock / maxStock,
minHeight: 6,
backgroundColor: AppColors.grey200,
valueColor: AlwaysStoppedAnimation<Color>(
_getStatusColor(product.status),
),
),
),
],
),
),
const SizedBox(width: 12),
// Action Button
GestureDetector(
onTap: () {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('${product.name} - Detail'),
duration: const Duration(seconds: 1),
),
);
},
child: Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: AppColors.primaryBrown.withOpacity(0.1),
borderRadius: BorderRadius.circular(10),
),
child: Icon(
Icons.arrow_forward_ios,
color: AppColors.primaryBrown,
size: 16,
),
),
),
],
),
],
),
);
}
String _getStatusLabel(String status) {
switch (status) {
case 'tersedia':
return '✅ Tersedia';
case 'rendah':
return '⚠️ Rendah';
case 'kritis':
return '🔴 Kritis';
default:
return 'Unknown';
}
}
String _capitalize(String text) {
return "${text[0].toUpperCase()}${text.substring(1)}";
}
}

View File

@ -0,0 +1,468 @@
import 'package:flutter/material.dart';
class ReportScreen extends StatefulWidget {
const ReportScreen({Key? key}) : super(key: key);
@override
State<ReportScreen> createState() => _ReportScreenState();
}
class _ReportScreenState extends State<ReportScreen> {
String _selectedPeriod = 'bulanan';
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Color(0xFFF5F5F5),
appBar: AppBar(
backgroundColor: Color(0xFFA89080),
elevation: 0,
leading: IconButton(
icon: const Icon(Icons.arrow_back, color: Colors.white),
onPressed: () => Navigator.pop(context),
),
title: const Text(
'Laporan & Analitik',
style: TextStyle(
color: Colors.white,
fontSize: 18,
fontWeight: FontWeight.w700,
),
),
centerTitle: false,
),
body: SingleChildScrollView(
child: Column(
children: [
// Period Filter
Container(
color: Colors.white,
padding: const EdgeInsets.all(16),
child: Row(
children: [
Text(
'Period:',
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
color: Color(0xFF1F2937),
),
),
const SizedBox(width: 8),
Container(
decoration: BoxDecoration(
border: Border.all(
color: Color(0xFFE5E7EB),
width: 1,
),
borderRadius: BorderRadius.circular(8),
),
padding: const EdgeInsets.symmetric(horizontal: 12),
child: DropdownButtonHideUnderline(
child: DropdownButton<String>(
value: _selectedPeriod,
items: [
'harian',
'mingguan',
'bulanan',
'tahunan'
]
.map((period) => DropdownMenuItem(
value: period,
child: Text(period.capitalize()),
))
.toList(),
onChanged: (value) {
if (value != null) {
setState(() => _selectedPeriod = value);
}
},
),
),
),
],
),
),
Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Chart Section
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 10,
offset: const Offset(0, 2),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Grafik Penjualan',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: Color(0xFF1F2937),
),
),
const SizedBox(height: 2),
Text(
'6 ${_selectedPeriod}',
style: const TextStyle(
fontSize: 12,
color: Color(0xFF9CA3AF),
),
),
],
),
Icon(
Icons.trending_up,
color: Color(0xFF10B981),
size: 20,
),
],
),
const SizedBox(height: 20),
// Chart Placeholder
Container(
height: 200,
decoration: BoxDecoration(
color: Color(0xFFF9FAFB),
borderRadius: BorderRadius.circular(8),
),
child: Center(
child: Text(
'[Area Chart: Sales Data by ${_selectedPeriod.capitalize()}]',
style: TextStyle(
color: Color(0xFFD1D5DB),
fontSize: 12,
),
),
),
),
const SizedBox(height: 12),
Row(
mainAxisAlignment:
MainAxisAlignment.spaceBetween,
children: ['Jan', 'Feb', 'Mar', 'Apr', 'Mei', 'Jun']
.map((month) => Text(
month,
style: const TextStyle(
fontSize: 11,
color: Color(0xFF9CA3AF),
),
))
.toList(),
),
],
),
),
const SizedBox(height: 16),
// Statistics Section
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 10,
offset: const Offset(0, 2),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Statistik Penjualan',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: Color(0xFF1F2937),
),
),
const SizedBox(height: 16),
GridView.count(
crossAxisCount: 2,
shrinkWrap: true,
physics: NeverScrollableScrollPhysics(),
mainAxisSpacing: 12,
crossAxisSpacing: 12,
children: [
_buildStatItem(
icon: Icons.shopping_bag,
label: 'Total Transaksi',
value: '150',
color: Color(0xFF2563EB),
),
_buildStatItem(
icon: Icons.attach_money,
label: 'Total Penjualan',
value: 'Rp 2.5M',
color: Color(0xFF10B981),
),
_buildStatItem(
icon: Icons.trending_up,
label: 'Rata-rata',
value: 'Rp 16K',
color: Color(0xFFFB923C),
),
_buildStatItem(
icon: Icons.inventory_2,
label: 'Produk Terjual',
value: '8',
color: Color(0xFFA89080),
),
],
),
],
),
),
const SizedBox(height: 16),
// Top Products Section
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 10,
offset: const Offset(0, 2),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Top 5 Produk Terlaris',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: Color(0xFF1F2937),
),
),
const SizedBox(height: 16),
...[
('Tepung Terigu', 25),
('Gula Pasir', 18),
('Telur', 15),
('Mentega', 12),
('Cokelat Bubuk', 10),
]
.asMap()
.entries
.map((entry) =>
_buildTopProductItem(
rank: entry.key + 1,
name: entry.value.$1,
quantity: entry.value.$2,
))
.toList()
.expand((item) => [item, SizedBox(height: 12)])
.toList(),
],
),
),
const SizedBox(height: 24),
],
),
),
],
),
),
bottomNavigationBar: BottomNavigationBar(
currentIndex: 4,
type: BottomNavigationBarType.fixed,
onTap: (index) {
switch (index) {
case 0:
Navigator.popUntil(context, (route) => route.isFirst);
break;
case 1:
Navigator.pushNamed(context, '/products');
break;
case 2:
Navigator.pushNamed(context, '/transaction');
break;
case 3:
Navigator.pushNamed(context, '/prediction');
break;
}
},
items: const [
BottomNavigationBarItem(
icon: Icon(Icons.home_outlined),
label: 'Dashboard',
),
BottomNavigationBarItem(
icon: Icon(Icons.shopping_bag_outlined),
label: 'Produk',
),
BottomNavigationBarItem(
icon: Icon(Icons.shopping_cart_outlined),
label: 'Transaksi',
),
BottomNavigationBarItem(
icon: Icon(Icons.trending_up_outlined),
label: 'Prediksi',
),
BottomNavigationBarItem(
icon: Icon(Icons.description_outlined),
label: 'Laporan',
),
],
),
);
}
Widget _buildStatItem({
required IconData icon,
required String label,
required String value,
required Color color,
}) {
return Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: color.withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: color.withOpacity(0.2),
width: 1,
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Container(
width: 36,
height: 36,
decoration: BoxDecoration(
color: color.withOpacity(0.15),
borderRadius: BorderRadius.circular(8),
),
child: Icon(icon, color: color, size: 18),
),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
value,
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w700,
color: color,
),
),
SizedBox(height: 2),
Text(
label,
style: const TextStyle(
fontSize: 11,
color: Color(0xFF9CA3AF),
fontWeight: FontWeight.w500,
),
),
],
),
],
),
);
}
Widget _buildTopProductItem({
required int rank,
required String name,
required int quantity,
}) {
return Row(
children: [
Container(
width: 32,
height: 32,
decoration: BoxDecoration(
color: Color(0xFFA89080),
borderRadius: BorderRadius.circular(8),
),
child: Center(
child: Text(
rank.toString(),
style: const TextStyle(
color: Colors.white,
fontSize: 12,
fontWeight: FontWeight.w700,
),
),
),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
name,
style: const TextStyle(
fontSize: 13,
fontWeight: FontWeight.w600,
color: Color(0xFF1F2937),
),
),
Text(
'$quantity unit',
style: const TextStyle(
fontSize: 11,
color: Color(0xFF9CA3AF),
),
),
],
),
),
Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
decoration: BoxDecoration(
color: Color(0xFFA89080).withOpacity(0.1),
borderRadius: BorderRadius.circular(6),
),
child: Text(
'${((quantity / 25) * 100).toStringAsFixed(0)}%',
style: TextStyle(
fontSize: 11,
fontWeight: FontWeight.w600,
color: Color(0xFFA89080),
),
),
),
],
);
}
}
extension StringExtension on String {
String capitalize() {
return "${this[0].toUpperCase()}${substring(1)}";
}
}

View File

@ -0,0 +1,412 @@
import 'package:flutter/material.dart';
import 'package:finalproject/theme/colors.dart';
import 'package:finalproject/theme/text_styles.dart';
class SettingsScreen extends StatefulWidget {
const SettingsScreen({Key? key}) : super(key: key);
@override
State<SettingsScreen> createState() => _SettingsScreenState();
}
class _SettingsScreenState extends State<SettingsScreen> {
int _selectedIndex = 5; // Settings tab
void _showLogoutDialog() {
showDialog(
context: context,
builder:
(context) => AlertDialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
title: Text(
'Logout',
style: AppTextStyles.headlineSmall.copyWith(
color: AppColors.textPrimary,
),
),
content: Text(
'Apakah Anda yakin ingin keluar dari aplikasi?',
style: AppTextStyles.bodyMedium.copyWith(
color: AppColors.textSecondary,
),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: Text(
'Batal',
style: AppTextStyles.labelLarge.copyWith(
color: AppColors.textSecondary,
),
),
),
ElevatedButton(
onPressed: () {
Navigator.pop(context);
Navigator.of(
context,
).pushNamedAndRemoveUntil('/login', (route) => false);
},
style: ElevatedButton.styleFrom(
backgroundColor: AppColors.statusError,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
child: Text(
'Logout',
style: AppTextStyles.labelLarge.copyWith(color: Colors.white),
),
),
],
),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: AppColors.bgLight,
appBar: AppBar(
backgroundColor: AppColors.primaryBrown,
elevation: 0,
title: Text(
'Pengaturan',
style: AppTextStyles.headlineLarge.copyWith(color: Colors.white),
),
),
body: SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Account Section
Text(
'Akun',
style: AppTextStyles.headlineSmall.copyWith(
color: AppColors.textPrimary,
),
),
const SizedBox(height: 16),
// User Profile Card
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: AppColors.bgWhite,
borderRadius: BorderRadius.circular(16),
boxShadow: [AppColors.shadowLight],
),
child: Row(
children: [
Container(
width: 60,
height: 60,
decoration: BoxDecoration(
color: AppColors.primaryBrown,
borderRadius: BorderRadius.circular(12),
),
child: const Icon(
Icons.person,
color: Colors.white,
size: 32,
),
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Admin Sulastri',
style: AppTextStyles.labelLarge.copyWith(
color: AppColors.textPrimary,
fontWeight: FontWeight.w700,
),
),
const SizedBox(height: 4),
Text(
'admin@sulastri.com',
style: AppTextStyles.bodySmall.copyWith(
color: AppColors.textSecondary,
),
),
],
),
),
],
),
),
const SizedBox(height: 32),
// General Settings Section
Text(
'Umum',
style: AppTextStyles.headlineSmall.copyWith(
color: AppColors.textPrimary,
),
),
const SizedBox(height: 16),
// Settings Item: Language
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: AppColors.bgWhite,
borderRadius: BorderRadius.circular(12),
boxShadow: [AppColors.shadowLight],
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
Icon(
Icons.language,
color: AppColors.primaryBrown,
size: 24,
),
const SizedBox(width: 12),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Bahasa',
style: AppTextStyles.labelLarge.copyWith(
color: AppColors.textPrimary,
),
),
Text(
'Bahasa Indonesia',
style: AppTextStyles.bodySmall.copyWith(
color: AppColors.textSecondary,
),
),
],
),
],
),
Icon(Icons.chevron_right, color: AppColors.textSecondary),
],
),
),
const SizedBox(height: 12),
// Settings Item: Notifikasi
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: AppColors.bgWhite,
borderRadius: BorderRadius.circular(12),
boxShadow: [AppColors.shadowLight],
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
Icon(
Icons.notifications_outlined,
color: AppColors.primaryBrown,
size: 24,
),
const SizedBox(width: 12),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Notifikasi',
style: AppTextStyles.labelLarge.copyWith(
color: AppColors.textPrimary,
),
),
Text(
'Aktifkan notifikasi penting',
style: AppTextStyles.bodySmall.copyWith(
color: AppColors.textSecondary,
),
),
],
),
],
),
Switch(
value: true,
onChanged: (value) {},
activeColor: AppColors.primaryBrown,
),
],
),
),
const SizedBox(height: 32),
// Security Section
Text(
'Keamanan',
style: AppTextStyles.headlineSmall.copyWith(
color: AppColors.textPrimary,
),
),
const SizedBox(height: 16),
// Settings Item: Change Password
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: AppColors.bgWhite,
borderRadius: BorderRadius.circular(12),
boxShadow: [AppColors.shadowLight],
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
Icon(
Icons.lock_outlined,
color: AppColors.primaryBrown,
size: 24,
),
const SizedBox(width: 12),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Ubah Password',
style: AppTextStyles.labelLarge.copyWith(
color: AppColors.textPrimary,
),
),
Text(
'Perbarui password akun Anda',
style: AppTextStyles.bodySmall.copyWith(
color: AppColors.textSecondary,
),
),
],
),
],
),
Icon(Icons.chevron_right, color: AppColors.textSecondary),
],
),
),
const SizedBox(height: 32),
// Logout Button
SizedBox(
width: double.infinity,
child: ElevatedButton.icon(
onPressed: _showLogoutDialog,
icon: const Icon(Icons.logout),
label: const Text('Logout'),
style: ElevatedButton.styleFrom(
backgroundColor: AppColors.statusError,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 14),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
),
),
),
const SizedBox(height: 32),
// App Info
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: AppColors.bgLight,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: AppColors.grey300),
),
child: Column(
children: [
Text(
'Prediksi Stok Bahan Kue',
style: AppTextStyles.labelLarge.copyWith(
color: AppColors.textPrimary,
fontWeight: FontWeight.w700,
),
),
const SizedBox(height: 8),
Text(
'Version 1.0.0',
style: AppTextStyles.bodySmall.copyWith(
color: AppColors.textSecondary,
),
),
const SizedBox(height: 12),
Text(
'© 2025 Toko Bahan Kue Sulastri',
style: AppTextStyles.bodySmall.copyWith(
color: AppColors.textTertiary,
),
textAlign: TextAlign.center,
),
],
),
),
],
),
),
),
bottomNavigationBar: BottomNavigationBar(
currentIndex: _selectedIndex,
onTap: (index) {
setState(() => _selectedIndex = index);
switch (index) {
case 0:
Navigator.of(context).pushNamed('/dashboard');
break;
case 1:
Navigator.of(context).pushNamed('/products');
break;
case 2:
Navigator.of(context).pushNamed('/transaction');
break;
case 3:
Navigator.of(context).pushNamed('/prediction');
break;
case 4:
Navigator.of(context).pushNamed('/reports');
break;
case 5:
break;
}
},
type: BottomNavigationBarType.fixed,
backgroundColor: Colors.white,
items: const [
BottomNavigationBarItem(
icon: Icon(Icons.home_outlined),
label: 'Dashboard',
),
BottomNavigationBarItem(
icon: Icon(Icons.shopping_bag_outlined),
label: 'Produk',
),
BottomNavigationBarItem(
icon: Icon(Icons.shopping_cart_outlined),
label: 'Transaksi',
),
BottomNavigationBarItem(
icon: Icon(Icons.trending_up_outlined),
label: 'Prediksi',
),
BottomNavigationBarItem(
icon: Icon(Icons.description_outlined),
label: 'Laporan',
),
BottomNavigationBarItem(
icon: Icon(Icons.settings_outlined),
label: 'Pengaturan',
),
],
),
);
}
}

View File

@ -0,0 +1,317 @@
import 'package:flutter/material.dart';
import 'package:finalproject/theme/colors.dart';
class SplashScreen extends StatefulWidget {
const SplashScreen({Key? key}) : super(key: key);
@override
State<SplashScreen> createState() => _SplashScreenState();
}
class _SplashScreenState extends State<SplashScreen>
with TickerProviderStateMixin {
late AnimationController _fadeController;
late AnimationController _scaleController;
late AnimationController _rotateController;
String _statusMessage = 'Mempersiapkan aplikasi...';
@override
void initState() {
super.initState();
_initializeAnimations();
_checkApiHealth();
}
void _initializeAnimations() {
// Fade animation untuk teks
_fadeController = AnimationController(
duration: const Duration(milliseconds: 1500),
vsync: this,
);
// Scale animation untuk logo
_scaleController = AnimationController(
duration: const Duration(milliseconds: 1000),
vsync: this,
);
// Rotate animation untuk loading circle
_rotateController = AnimationController(
duration: const Duration(milliseconds: 2000),
vsync: this,
);
_scaleController.forward();
_fadeController.forward();
_rotateController.repeat();
}
Future<void> _checkApiHealth() async {
await Future.delayed(const Duration(seconds: 2));
try {
setState(() => _statusMessage = 'Menghubungkan ke server...');
// Simulate API check (uncomment MLService when needed)
// final isHealthy = await MLService.healthCheck();
// For now, always proceed to dashboard
setState(() {
_statusMessage = 'Siap!';
});
// Tunggu sebentar sebelum navigate
await Future.delayed(const Duration(seconds: 1));
if (mounted) {
// Navigate ke Dashboard
Navigator.of(context).pushReplacementNamed('/dashboard');
}
} catch (e) {
setState(() => _statusMessage = 'Error: ${e.toString()}');
await Future.delayed(const Duration(seconds: 2));
if (mounted) {
// Navigate ke Dashboard even on error
Navigator.of(context).pushReplacementNamed('/dashboard');
}
}
}
@override
void dispose() {
_fadeController.dispose();
_scaleController.dispose();
_rotateController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Container(
width: double.infinity,
height: double.infinity,
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
AppColors.creamGradient.colors[0],
AppColors.creamGradient.colors[1],
],
),
),
child: Stack(
children: [
// Background pattern (subtle cake/bakery theme)
Positioned.fill(
child: Opacity(
opacity: 0.08,
child: Image.network(
'https://images.unsplash.com/photo-1578985545062-69928b1d9587?w=800',
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) {
return Container(color: Colors.transparent);
},
),
),
),
// Main content
SafeArea(
child: Column(
children: [
// Top spacing
const Spacer(flex: 2),
// Center content
Expanded(
flex: 4,
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// Logo with scale animation
ScaleTransition(
scale: Tween<double>(begin: 0.5, end: 1.0).animate(
CurvedAnimation(
parent: _scaleController,
curve: Curves.elasticOut,
),
),
child: Container(
width: 110,
height: 110,
decoration: BoxDecoration(
color: AppColors.primaryBrown,
borderRadius: BorderRadius.circular(28),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.15),
blurRadius: 25,
offset: const Offset(0, 10),
),
],
),
child: Center(
child: Text(
'🍰',
style: TextStyle(fontSize: 60),
),
),
),
),
const SizedBox(height: 40),
// App name with fade-in
FadeTransition(
opacity: Tween<double>(begin: 0.0, end: 1.0)
.animate(
CurvedAnimation(
parent: _fadeController,
curve: Curves.easeInOut,
),
),
child: Column(
children: [
// Main title
Text(
'Sulastri',
style: TextStyle(
fontSize: 36,
fontWeight: FontWeight.w700,
color: AppColors.primaryBrown,
letterSpacing: 0.5,
),
),
const SizedBox(height: 6),
// Subtitle
Text(
'Toko Bahan Kue',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
color: AppColors.primaryBrownDark,
),
),
const SizedBox(height: 16),
// Tagline with icon
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.star,
size: 14,
color: AppColors.primaryBrownLight,
),
const SizedBox(width: 6),
Text(
'Smart Inventory Management',
style: TextStyle(
fontSize: 13,
color: AppColors.textTertiary,
fontWeight: FontWeight.w400,
),
),
],
),
],
),
),
],
),
),
),
// Bottom section with loading & version
Expanded(
flex: 2,
child: Column(
mainAxisAlignment: MainAxisAlignment.end,
children: [
// Loading dots animation
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ScaleTransition(
scale: Tween<double>(begin: 0.4, end: 1.0)
.animate(
CurvedAnimation(
parent: _rotateController,
curve: Curves.easeInOut,
),
),
child: Container(
width: 8,
height: 8,
decoration: BoxDecoration(
color: AppColors.primaryBrown,
borderRadius: BorderRadius.circular(4),
),
),
),
const SizedBox(width: 6),
ScaleTransition(
scale: Tween<double>(begin: 0.4, end: 1.0)
.animate(
CurvedAnimation(
parent: _rotateController,
curve: Curves.easeInOut,
),
),
child: Container(
width: 8,
height: 8,
decoration: BoxDecoration(
color: AppColors.primaryBrown,
borderRadius: BorderRadius.circular(4),
),
),
),
const SizedBox(width: 6),
ScaleTransition(
scale: Tween<double>(begin: 0.4, end: 1.0)
.animate(
CurvedAnimation(
parent: _rotateController,
curve: Curves.easeInOut,
),
),
child: Container(
width: 8,
height: 8,
decoration: BoxDecoration(
color: AppColors.primaryBrown,
borderRadius: BorderRadius.circular(4),
),
),
),
],
),
const SizedBox(height: 40),
// Version text
Text(
'Version 1.0.0',
style: TextStyle(
fontSize: 12,
color: AppColors.textTertiary,
fontWeight: FontWeight.w400,
),
),
const SizedBox(height: 24),
],
),
),
],
),
),
],
),
),
);
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,341 @@
import 'package:http/http.dart' as http;
import 'dart:convert';
class MLService {
// API URL - Change based on environment
static const String baseUrl = 'http://127.0.0.1:5000';
// For remote access: 'http://192.168.1.75:5000'
static const int timeoutSeconds = 30;
/// Health Check - Test if API is running
static Future<bool> healthCheck() async {
try {
final response = await http
.get(Uri.parse('$baseUrl/health'))
.timeout(Duration(seconds: 5));
return response.statusCode == 200;
} catch (e) {
print('Health check error: $e');
return false;
}
}
/// Get Metadata - Retrieve model information
static Future<Map<String, dynamic>> getMetadata() async {
try {
final response = await http
.get(Uri.parse('$baseUrl/metadata'))
.timeout(Duration(seconds: timeoutSeconds));
if (response.statusCode == 200) {
return jsonDecode(response.body);
} else {
return {'status': 'error', 'message': 'Failed to get metadata'};
}
} catch (e) {
return {'status': 'error', 'message': 'Connection error: $e'};
}
}
/// Simplified Prediction - For UI use
static Future<Map<String, dynamic>> simplePrediksi({
required String productName,
required String category,
required int unitPrice,
DateTime? predictionDate,
}) async {
try {
final date = predictionDate ?? DateTime.now();
// Map product names to encoded values (adjust based on your encoding)
final productMap = {
'Tepung Terigu 1kg': 1,
'Telur 1kg': 2,
'Gula Pasir 1kg': 3,
'Susu Bubuk': 4,
'Cokelat Bubuk 250gr': 5,
'Mentega 500gr': 6,
'Keju Parut 250gr': 7,
'Baking Powder': 8,
};
final categoryMap = {
'Tepung': 1,
'Telur': 2,
'Gula': 3,
'Susu': 4,
'Cokelat': 5,
'Mentega': 6,
'Keju': 7,
'Bahan Tambahan': 8,
};
final produkEncoded = productMap[productName] ?? 1;
final kategoriEncoded = categoryMap[category] ?? 1;
return await prediksiStok(
tahun: date.year,
bulan: date.month,
hari: date.day,
hariDalamMinggu: date.weekday,
hariMinggu: date.weekday,
hargaSatuanUpdate: unitPrice,
totalHargaUpdate: unitPrice, // Simplified
produkEncoded: produkEncoded,
namaProdukEncoded: produkEncoded,
kategoriProdukEncoded: kategoriEncoded,
);
} catch (e) {
return {'status': 'error', 'message': 'Connection error: $e'};
}
}
/// Single Prediction - Predict stock demand
static Future<Map<String, dynamic>> prediksiStok({
required int tahun,
required int bulan,
required int hari,
required int hariDalamMinggu,
required int hariMinggu,
required int hargaSatuanUpdate,
required int totalHargaUpdate,
required int produkEncoded,
required int namaProdukEncoded,
required int kategoriProdukEncoded,
}) async {
try {
final data = {
'tahun': tahun,
'bulan': bulan,
'hari': hari,
'hari_dalam_minggu': hariDalamMinggu,
'hari_minggu': hariMinggu,
'harga_satuan_update': hargaSatuanUpdate,
'total_harga_update': totalHargaUpdate,
'produk_encoded': produkEncoded,
'nama_produk_encoded': namaProdukEncoded,
'kategori_produk_encoded': kategoriProdukEncoded,
};
final response = await http
.post(
Uri.parse('$baseUrl/prediksi'),
headers: {'Content-Type': 'application/json'},
body: jsonEncode(data),
)
.timeout(Duration(seconds: timeoutSeconds));
if (response.statusCode == 200) {
return jsonDecode(response.body);
} else {
return {
'status': 'error',
'message': 'Server error: ${response.statusCode}',
};
}
} catch (e) {
return {'status': 'error', 'message': 'Connection error: $e'};
}
}
/// Batch Prediction - Predict multiple items
static Future<Map<String, dynamic>> batchPrediksi(
List<Map<String, dynamic>> items,
) async {
try {
final data = {'items': items};
final response = await http
.post(
Uri.parse('$baseUrl/batch-prediksi'),
headers: {'Content-Type': 'application/json'},
body: jsonEncode(data),
)
.timeout(Duration(seconds: timeoutSeconds));
if (response.statusCode == 200) {
return jsonDecode(response.body);
} else {
return {
'status': 'error',
'message': 'Server error: ${response.statusCode}'
};
}
} catch (e) {
return {'status': 'error', 'message': 'Connection error: $e'};
}
}
/// Get API Info
static Future<Map<String, dynamic>> getInfo() async {
try {
final response = await http
.get(Uri.parse('$baseUrl/info'))
.timeout(Duration(seconds: timeoutSeconds));
if (response.statusCode == 200) {
return jsonDecode(response.body);
} else {
return {'status': 'error', 'message': 'Failed to get info'};
}
} catch (e) {
return {'status': 'error', 'message': 'Connection error: $e'};
}
}
// ========================================================================
// DATABASE ENDPOINTS - PRODUCTS & TRANSACTIONS
// ========================================================================
/// Get all products from database
static Future<List<Map<String, dynamic>>> getProducts() async {
try {
final response = await http
.get(Uri.parse('$baseUrl/products'))
.timeout(Duration(seconds: timeoutSeconds));
if (response.statusCode == 200) {
final data = jsonDecode(response.body);
if (data['status'] == 'success') {
return List<Map<String, dynamic>>.from(data['products']);
}
}
return [];
} catch (e) {
print('Get products error: $e');
return [];
}
}
/// Get specific product by ID
static Future<Map<String, dynamic>?> getProduct(int productId) async {
try {
final response = await http
.get(Uri.parse('$baseUrl/products/$productId'))
.timeout(Duration(seconds: timeoutSeconds));
if (response.statusCode == 200) {
final data = jsonDecode(response.body);
if (data['status'] == 'success') {
return data['product'];
}
}
return null;
} catch (e) {
print('Get product error: $e');
return null;
}
}
/// Save transaction to database
static Future<bool> saveTransaction({
required String productName,
required String category,
required int quantity,
required int unitPrice,
required int totalPrice,
required String transactionDate, // Format: 'YYYY-MM-DD'
}) async {
try {
final data = {
'product_name': productName,
'category': category,
'quantity': quantity,
'unit_price': unitPrice,
'total_price': totalPrice,
'transaction_date': transactionDate,
};
final response = await http
.post(
Uri.parse('$baseUrl/transactions'),
headers: {'Content-Type': 'application/json'},
body: jsonEncode(data),
)
.timeout(Duration(seconds: timeoutSeconds));
if (response.statusCode == 201) {
final result = jsonDecode(response.body);
return result['status'] == 'success';
}
return false;
} catch (e) {
print('Save transaction error: $e');
return false;
}
}
/// Get transaction history from database
static Future<List<Map<String, dynamic>>> getTransactions({
int limit = 100,
int offset = 0,
String? productName,
}) async {
try {
var url = '$baseUrl/transactions?limit=$limit&offset=$offset';
if (productName != null && productName.isNotEmpty) {
url += '&product_name=$productName';
}
final response = await http
.get(Uri.parse(url))
.timeout(Duration(seconds: timeoutSeconds));
if (response.statusCode == 200) {
final data = jsonDecode(response.body);
if (data['status'] == 'success') {
return List<Map<String, dynamic>>.from(data['transactions']);
}
}
return [];
} catch (e) {
print('Get transactions error: $e');
return [];
}
}
/// Save prediction result to database
static Future<bool> savePrediction({
required String productName,
required String category,
required int unitPrice,
required String predictionDate, // Format: 'YYYY-MM-DD'
required int predictedQuantity,
double? rawValue,
int? estimatedTotalPrice,
double? accuracyR2,
double? errorMae,
}) async {
try {
final data = {
'product_name': productName,
'category': category,
'unit_price': unitPrice,
'prediction_date': predictionDate,
'predicted_quantity': predictedQuantity,
if (rawValue != null) 'raw_value': rawValue,
if (estimatedTotalPrice != null) 'estimated_total_price': estimatedTotalPrice,
if (accuracyR2 != null) 'accuracy_r2': accuracyR2,
if (errorMae != null) 'error_mae': errorMae,
};
final response = await http
.post(
Uri.parse('$baseUrl/predictions'),
headers: {'Content-Type': 'application/json'},
body: jsonEncode(data),
)
.timeout(Duration(seconds: timeoutSeconds));
if (response.statusCode == 201) {
final result = jsonDecode(response.body);
return result['status'] == 'success';
}
return false;
} catch (e) {
print('Save prediction error: $e');
return false;
}
}
}

199
lib/theme/app_theme.dart Normal file
View File

@ -0,0 +1,199 @@
import 'package:flutter/material.dart';
import 'colors.dart';
import 'text_styles.dart';
class AppTheme {
static ThemeData lightTheme() {
return ThemeData(
useMaterial3: true,
brightness: Brightness.light,
// Primary Colors
primaryColor: AppColors.primaryBrown,
scaffoldBackgroundColor: AppColors.bgLight,
// Color Scheme
colorScheme: ColorScheme.light(
primary: AppColors.primaryBrown,
secondary: AppColors.secondaryBlue,
tertiary: AppColors.secondaryGreen,
error: AppColors.secondaryRed,
onPrimary: AppColors.white,
onSecondary: AppColors.white,
onTertiary: AppColors.white,
onError: AppColors.white,
surface: AppColors.bgWhite,
onSurface: AppColors.textPrimary,
),
// AppBar Theme
appBarTheme: AppBarTheme(
elevation: 0,
backgroundColor: AppColors.primaryBrown,
foregroundColor: AppColors.white,
centerTitle: false,
titleTextStyle: AppTextStyles.appBarTitle,
iconTheme: const IconThemeData(color: AppColors.white),
),
// Text Theme
textTheme: TextTheme(
displayLarge: AppTextStyles.displayLarge,
displayMedium: AppTextStyles.displayMedium,
displaySmall: AppTextStyles.displaySmall,
headlineLarge: AppTextStyles.headlineLarge,
headlineMedium: AppTextStyles.headlineMedium,
headlineSmall: AppTextStyles.headlineSmall,
titleLarge: AppTextStyles.titleLarge,
titleMedium: AppTextStyles.titleMedium,
titleSmall: AppTextStyles.titleSmall,
bodyLarge: AppTextStyles.bodyLarge,
bodyMedium: AppTextStyles.bodyMedium,
bodySmall: AppTextStyles.bodySmall,
labelLarge: AppTextStyles.labelLarge,
labelMedium: AppTextStyles.labelMedium,
labelSmall: AppTextStyles.labelSmall,
),
// Button Theme
elevatedButtonTheme: ElevatedButtonThemeData(
style: ElevatedButton.styleFrom(
backgroundColor: AppColors.primaryBrown,
foregroundColor: AppColors.white,
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
textStyle: AppTextStyles.buttonText,
elevation: 0,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
),
),
outlinedButtonTheme: OutlinedButtonThemeData(
style: OutlinedButton.styleFrom(
foregroundColor: AppColors.primaryBrown,
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
textStyle: AppTextStyles.buttonText,
side: const BorderSide(color: AppColors.primaryBrown, width: 1.5),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
),
),
textButtonTheme: TextButtonThemeData(
style: TextButton.styleFrom(
foregroundColor: AppColors.primaryBrown,
textStyle: AppTextStyles.labelLarge,
),
),
// Input Decoration Theme
inputDecorationTheme: InputDecorationTheme(
filled: true,
fillColor: AppColors.bgWhite,
contentPadding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 12,
),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: const BorderSide(color: AppColors.grey300, width: 1),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: const BorderSide(color: AppColors.grey300, width: 1),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: const BorderSide(color: AppColors.primaryBrown, width: 2),
),
errorBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: const BorderSide(color: AppColors.secondaryRed, width: 1),
),
focusedErrorBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: const BorderSide(color: AppColors.secondaryRed, width: 2),
),
hintStyle: AppTextStyles.hintText,
labelStyle: AppTextStyles.labelMedium,
errorStyle: AppTextStyles.errorText,
),
// Card Theme
cardTheme: CardTheme(
elevation: 0,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
color: AppColors.bgWhite,
margin: EdgeInsets.zero,
),
// Dialog Theme
dialogTheme: DialogTheme(
elevation: 4,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
backgroundColor: AppColors.bgWhite,
),
// Divider Theme
dividerTheme: const DividerThemeData(
color: AppColors.grey200,
thickness: 1,
space: 1,
),
// Chip Theme
chipTheme: ChipThemeData(
backgroundColor: AppColors.bgWhite,
selectedColor: AppColors.primaryBrown,
disabledColor: AppColors.grey200,
labelStyle: AppTextStyles.labelMedium,
secondaryLabelStyle: AppTextStyles.labelMedium.copyWith(
color: AppColors.white,
),
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
showCheckmark: true,
),
// Progress Indicator Theme
progressIndicatorTheme: const ProgressIndicatorThemeData(
color: AppColors.primaryBrown,
circularTrackColor: AppColors.grey200,
),
// Floating Action Button Theme
floatingActionButtonTheme: FloatingActionButtonThemeData(
backgroundColor: AppColors.secondaryBlue,
foregroundColor: AppColors.white,
elevation: 4,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
),
// Bottom App Bar Theme
bottomAppBarTheme: const BottomAppBarTheme(
elevation: 8,
color: AppColors.bgWhite,
),
// Bottom Navigation Bar Theme
bottomNavigationBarTheme: BottomNavigationBarThemeData(
backgroundColor: AppColors.bgWhite,
selectedItemColor: AppColors.primaryBrown,
unselectedItemColor: AppColors.grey400,
type: BottomNavigationBarType.fixed,
elevation: 8,
selectedLabelStyle: AppTextStyles.labelSmall,
unselectedLabelStyle: AppTextStyles.labelSmall,
),
// Snack Bar Theme
snackBarTheme: SnackBarThemeData(
backgroundColor: AppColors.textPrimary,
contentTextStyle: AppTextStyles.bodyMedium.copyWith(
color: AppColors.white,
),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
behavior: SnackBarBehavior.floating,
elevation: 4,
),
);
}
}

83
lib/theme/colors.dart Normal file
View File

@ -0,0 +1,83 @@
import 'package:flutter/material.dart';
class AppColors {
// Primary Colors (from design system)
static const Color primaryBrown = Color(0xFF8B6E58);
static const Color primaryBrownDark = Color(0xFF6B5040);
static const Color primaryBrownLight = Color(0xFFC4A882);
// Secondary Colors
static const Color secondaryBlue = Color(0xFF2196F3);
static const Color secondaryGreen = Color(0xFF4CAF50);
static const Color secondaryOrange = Color(0xFFFF9800);
static const Color secondaryRed = Color(0xFFF44336);
// Status Colors
static const Color statusSuccess = Color(0xFF4CAF50); // Tersedia (green)
static const Color statusWarning = Color(0xFFFF9800); // Rendah (orange)
static const Color statusError = Color(0xFFF44336); // Kritis (red)
// Neutral Colors
static const Color white = Color(0xFFFFFFFF);
static const Color black = Color(0xFF000000);
static const Color grey100 = Color(0xFFF5EFE8); // Primary BG
static const Color grey200 = Color(0xFFE8DDD5); // Border
static const Color grey300 = Color(0xFFD1D5DB);
static const Color grey400 = Color(0xFF9E8070); // Text Soft
static const Color grey500 = Color(0xFF9E8070);
static const Color grey600 = Color(0xFF6B5040); // Text Mid
static const Color grey700 = Color(0xFF6B5040);
static const Color grey800 = Color(0xFF2C1810); // Text Dark
static const Color grey900 = Color(0xFF2C1810);
// Background
static const Color bgLight = Color(0xFFF5EFE8);
static const Color bgWhite = Color(0xFFFFFFFF);
static const Color bgGrey = Color(0xFFF5EFE8);
// Text
static const Color textPrimary = Color(0xFF2C1810);
static const Color textSecondary = Color(0xFF6B5040);
static const Color textTertiary = Color(0xFF9E8070);
static const Color textLight = Color(0xFFF5EFE8);
// Gradient (using primary brown)
static const LinearGradient primaryGradient = LinearGradient(
colors: [primaryBrown, primaryBrownDark],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
);
// Cream gradient for splash screen
static const LinearGradient creamGradient = LinearGradient(
colors: [Color(0xFFF5EFE8), Color(0xFFE8DDD5)],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
);
// Shadow (with aliases for compatibility)
static const BoxShadow lightShadow = BoxShadow(
color: Color(0x19000000),
blurRadius: 12,
offset: Offset(0, 2),
);
static const BoxShadow shadowLight = lightShadow;
static const BoxShadow mediumShadow = BoxShadow(
color: Color(0x25000000),
blurRadius: 16,
offset: Offset(0, 4),
);
static const BoxShadow shadowMedium = mediumShadow;
static const BoxShadow heavyShadow = BoxShadow(
color: Color(0x35000000),
blurRadius: 24,
offset: Offset(0, 8),
);
// Aliases for status colors
static const Color successGreen = statusSuccess;
static const Color warningYellow = statusWarning;
static const Color errorRed = statusError;
}

134
lib/theme/text_styles.dart Normal file
View File

@ -0,0 +1,134 @@
import 'package:flutter/material.dart';
class AppTextStyles {
// Display
static const TextStyle displayLarge = TextStyle(
fontSize: 32,
fontWeight: FontWeight.bold,
letterSpacing: -0.5,
color: Color(0xFF1F2937),
);
static const TextStyle displayMedium = TextStyle(
fontSize: 28,
fontWeight: FontWeight.bold,
letterSpacing: -0.25,
color: Color(0xFF1F2937),
);
static const TextStyle displaySmall = TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
color: Color(0xFF1F2937),
);
// Headline
static const TextStyle headlineLarge = TextStyle(
fontSize: 20,
fontWeight: FontWeight.w700,
color: Color(0xFF1F2937),
);
static const TextStyle headlineMedium = TextStyle(
fontSize: 18,
fontWeight: FontWeight.w700,
color: Color(0xFF1F2937),
);
static const TextStyle headlineSmall = TextStyle(
fontSize: 16,
fontWeight: FontWeight.w700,
color: Color(0xFF1F2937),
);
// Title
static const TextStyle titleLarge = TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
letterSpacing: 0.15,
color: Color(0xFF1F2937),
);
static const TextStyle titleMedium = TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
letterSpacing: 0.1,
color: Color(0xFF1F2937),
);
static const TextStyle titleSmall = TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
letterSpacing: 0.1,
color: Color(0xFF1F2937),
);
// Body
static const TextStyle bodyLarge = TextStyle(
fontSize: 16,
fontWeight: FontWeight.w400,
letterSpacing: 0.5,
color: Color(0xFF374151),
);
static const TextStyle bodyMedium = TextStyle(
fontSize: 14,
fontWeight: FontWeight.w400,
letterSpacing: 0.25,
color: Color(0xFF374151),
);
static const TextStyle bodySmall = TextStyle(
fontSize: 12,
fontWeight: FontWeight.w400,
letterSpacing: 0.4,
color: Color(0xFF6B7280),
);
// Label
static const TextStyle labelLarge = TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
letterSpacing: 0.1,
color: Color(0xFF1F2937),
);
static const TextStyle labelMedium = TextStyle(
fontSize: 12,
fontWeight: FontWeight.w500,
letterSpacing: 0.5,
color: Color(0xFF374151),
);
static const TextStyle labelSmall = TextStyle(
fontSize: 11,
fontWeight: FontWeight.w500,
letterSpacing: 0.5,
color: Color(0xFF6B7280),
);
// Custom Styles
static const TextStyle appBarTitle = TextStyle(
fontSize: 20,
fontWeight: FontWeight.w700,
color: Color(0xFFFFFFFF),
);
static const TextStyle buttonText = TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
letterSpacing: 0.1,
);
static const TextStyle errorText = TextStyle(
fontSize: 12,
fontWeight: FontWeight.w400,
color: Color(0xFFEF4444),
);
static const TextStyle hintText = TextStyle(
fontSize: 14,
fontWeight: FontWeight.w400,
color: Color(0xFF9CA3AF),
);
}

1
linux/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
flutter/ephemeral

128
linux/CMakeLists.txt Normal file
View File

@ -0,0 +1,128 @@
# Project-level configuration.
cmake_minimum_required(VERSION 3.13)
project(runner LANGUAGES CXX)
# The name of the executable created for the application. Change this to change
# the on-disk name of your application.
set(BINARY_NAME "finalproject")
# The unique GTK application identifier for this application. See:
# https://wiki.gnome.org/HowDoI/ChooseApplicationID
set(APPLICATION_ID "com.example.finalproject")
# Explicitly opt in to modern CMake behaviors to avoid warnings with recent
# versions of CMake.
cmake_policy(SET CMP0063 NEW)
# Load bundled libraries from the lib/ directory relative to the binary.
set(CMAKE_INSTALL_RPATH "$ORIGIN/lib")
# Root filesystem for cross-building.
if(FLUTTER_TARGET_PLATFORM_SYSROOT)
set(CMAKE_SYSROOT ${FLUTTER_TARGET_PLATFORM_SYSROOT})
set(CMAKE_FIND_ROOT_PATH ${CMAKE_SYSROOT})
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
endif()
# Define build configuration options.
if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
set(CMAKE_BUILD_TYPE "Debug" CACHE
STRING "Flutter build mode" FORCE)
set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS
"Debug" "Profile" "Release")
endif()
# Compilation settings that should be applied to most targets.
#
# Be cautious about adding new options here, as plugins use this function by
# default. In most cases, you should add new options to specific targets instead
# of modifying this function.
function(APPLY_STANDARD_SETTINGS TARGET)
target_compile_features(${TARGET} PUBLIC cxx_std_14)
target_compile_options(${TARGET} PRIVATE -Wall -Werror)
target_compile_options(${TARGET} PRIVATE "$<$<NOT:$<CONFIG:Debug>>:-O3>")
target_compile_definitions(${TARGET} PRIVATE "$<$<NOT:$<CONFIG:Debug>>:NDEBUG>")
endfunction()
# Flutter library and tool build rules.
set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter")
add_subdirectory(${FLUTTER_MANAGED_DIR})
# System-level dependencies.
find_package(PkgConfig REQUIRED)
pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0)
# Application build; see runner/CMakeLists.txt.
add_subdirectory("runner")
# Run the Flutter tool portions of the build. This must not be removed.
add_dependencies(${BINARY_NAME} flutter_assemble)
# Only the install-generated bundle's copy of the executable will launch
# correctly, since the resources must in the right relative locations. To avoid
# people trying to run the unbundled copy, put it in a subdirectory instead of
# the default top-level location.
set_target_properties(${BINARY_NAME}
PROPERTIES
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/intermediates_do_not_run"
)
# Generated plugin build rules, which manage building the plugins and adding
# them to the application.
include(flutter/generated_plugins.cmake)
# === Installation ===
# By default, "installing" just makes a relocatable bundle in the build
# directory.
set(BUILD_BUNDLE_DIR "${PROJECT_BINARY_DIR}/bundle")
if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT)
set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE)
endif()
# Start with a clean build bundle directory every time.
install(CODE "
file(REMOVE_RECURSE \"${BUILD_BUNDLE_DIR}/\")
" COMPONENT Runtime)
set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data")
set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}/lib")
install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}"
COMPONENT Runtime)
install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}"
COMPONENT Runtime)
install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
COMPONENT Runtime)
foreach(bundled_library ${PLUGIN_BUNDLED_LIBRARIES})
install(FILES "${bundled_library}"
DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
COMPONENT Runtime)
endforeach(bundled_library)
# Copy the native assets provided by the build.dart from all packages.
set(NATIVE_ASSETS_DIR "${PROJECT_BUILD_DIR}native_assets/linux/")
install(DIRECTORY "${NATIVE_ASSETS_DIR}"
DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
COMPONENT Runtime)
# Fully re-copy the assets directory on each build to avoid having stale files
# from a previous install.
set(FLUTTER_ASSET_DIR_NAME "flutter_assets")
install(CODE "
file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\")
" COMPONENT Runtime)
install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}"
DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime)
# Install the AOT library on non-Debug builds only.
if(NOT CMAKE_BUILD_TYPE MATCHES "Debug")
install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
COMPONENT Runtime)
endif()

View File

@ -0,0 +1,88 @@
# This file controls Flutter-level build steps. It should not be edited.
cmake_minimum_required(VERSION 3.10)
set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral")
# Configuration provided via flutter tool.
include(${EPHEMERAL_DIR}/generated_config.cmake)
# TODO: Move the rest of this into files in ephemeral. See
# https://github.com/flutter/flutter/issues/57146.
# Serves the same purpose as list(TRANSFORM ... PREPEND ...),
# which isn't available in 3.10.
function(list_prepend LIST_NAME PREFIX)
set(NEW_LIST "")
foreach(element ${${LIST_NAME}})
list(APPEND NEW_LIST "${PREFIX}${element}")
endforeach(element)
set(${LIST_NAME} "${NEW_LIST}" PARENT_SCOPE)
endfunction()
# === Flutter Library ===
# System-level dependencies.
find_package(PkgConfig REQUIRED)
pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0)
pkg_check_modules(GLIB REQUIRED IMPORTED_TARGET glib-2.0)
pkg_check_modules(GIO REQUIRED IMPORTED_TARGET gio-2.0)
set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/libflutter_linux_gtk.so")
# Published to parent scope for install step.
set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE)
set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE)
set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE)
set(AOT_LIBRARY "${PROJECT_DIR}/build/lib/libapp.so" PARENT_SCOPE)
list(APPEND FLUTTER_LIBRARY_HEADERS
"fl_basic_message_channel.h"
"fl_binary_codec.h"
"fl_binary_messenger.h"
"fl_dart_project.h"
"fl_engine.h"
"fl_json_message_codec.h"
"fl_json_method_codec.h"
"fl_message_codec.h"
"fl_method_call.h"
"fl_method_channel.h"
"fl_method_codec.h"
"fl_method_response.h"
"fl_plugin_registrar.h"
"fl_plugin_registry.h"
"fl_standard_message_codec.h"
"fl_standard_method_codec.h"
"fl_string_codec.h"
"fl_value.h"
"fl_view.h"
"flutter_linux.h"
)
list_prepend(FLUTTER_LIBRARY_HEADERS "${EPHEMERAL_DIR}/flutter_linux/")
add_library(flutter INTERFACE)
target_include_directories(flutter INTERFACE
"${EPHEMERAL_DIR}"
)
target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}")
target_link_libraries(flutter INTERFACE
PkgConfig::GTK
PkgConfig::GLIB
PkgConfig::GIO
)
add_dependencies(flutter flutter_assemble)
# === Flutter tool backend ===
# _phony_ is a non-existent file to force this command to run every time,
# since currently there's no way to get a full input/output list from the
# flutter tool.
add_custom_command(
OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS}
${CMAKE_CURRENT_BINARY_DIR}/_phony_
COMMAND ${CMAKE_COMMAND} -E env
${FLUTTER_TOOL_ENVIRONMENT}
"${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.sh"
${FLUTTER_TARGET_PLATFORM} ${CMAKE_BUILD_TYPE}
VERBATIM
)
add_custom_target(flutter_assemble DEPENDS
"${FLUTTER_LIBRARY}"
${FLUTTER_LIBRARY_HEADERS}
)

View File

@ -0,0 +1,11 @@
//
// Generated file. Do not edit.
//
// clang-format off
#include "generated_plugin_registrant.h"
void fl_register_plugins(FlPluginRegistry* registry) {
}

View File

@ -0,0 +1,15 @@
//
// Generated file. Do not edit.
//
// clang-format off
#ifndef GENERATED_PLUGIN_REGISTRANT_
#define GENERATED_PLUGIN_REGISTRANT_
#include <flutter_linux/flutter_linux.h>
// Registers Flutter plugins.
void fl_register_plugins(FlPluginRegistry* registry);
#endif // GENERATED_PLUGIN_REGISTRANT_

View File

@ -0,0 +1,23 @@
#
# Generated file, do not edit.
#
list(APPEND FLUTTER_PLUGIN_LIST
)
list(APPEND FLUTTER_FFI_PLUGIN_LIST
)
set(PLUGIN_BUNDLED_LIBRARIES)
foreach(plugin ${FLUTTER_PLUGIN_LIST})
add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/linux plugins/${plugin})
target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin)
list(APPEND PLUGIN_BUNDLED_LIBRARIES $<TARGET_FILE:${plugin}_plugin>)
list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries})
endforeach(plugin)
foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST})
add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/linux plugins/${ffi_plugin})
list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries})
endforeach(ffi_plugin)

View File

@ -0,0 +1,26 @@
cmake_minimum_required(VERSION 3.13)
project(runner LANGUAGES CXX)
# Define the application target. To change its name, change BINARY_NAME in the
# top-level CMakeLists.txt, not the value here, or `flutter run` will no longer
# work.
#
# Any new source files that you add to the application should be added here.
add_executable(${BINARY_NAME}
"main.cc"
"my_application.cc"
"${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc"
)
# Apply the standard set of build settings. This can be removed for applications
# that need different build settings.
apply_standard_settings(${BINARY_NAME})
# Add preprocessor definitions for the application ID.
add_definitions(-DAPPLICATION_ID="${APPLICATION_ID}")
# Add dependency libraries. Add any application-specific dependencies here.
target_link_libraries(${BINARY_NAME} PRIVATE flutter)
target_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::GTK)
target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}")

6
linux/runner/main.cc Normal file
View File

@ -0,0 +1,6 @@
#include "my_application.h"
int main(int argc, char** argv) {
g_autoptr(MyApplication) app = my_application_new();
return g_application_run(G_APPLICATION(app), argc, argv);
}

View File

@ -0,0 +1,130 @@
#include "my_application.h"
#include <flutter_linux/flutter_linux.h>
#ifdef GDK_WINDOWING_X11
#include <gdk/gdkx.h>
#endif
#include "flutter/generated_plugin_registrant.h"
struct _MyApplication {
GtkApplication parent_instance;
char** dart_entrypoint_arguments;
};
G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION)
// Implements GApplication::activate.
static void my_application_activate(GApplication* application) {
MyApplication* self = MY_APPLICATION(application);
GtkWindow* window =
GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application)));
// Use a header bar when running in GNOME as this is the common style used
// by applications and is the setup most users will be using (e.g. Ubuntu
// desktop).
// If running on X and not using GNOME then just use a traditional title bar
// in case the window manager does more exotic layout, e.g. tiling.
// If running on Wayland assume the header bar will work (may need changing
// if future cases occur).
gboolean use_header_bar = TRUE;
#ifdef GDK_WINDOWING_X11
GdkScreen* screen = gtk_window_get_screen(window);
if (GDK_IS_X11_SCREEN(screen)) {
const gchar* wm_name = gdk_x11_screen_get_window_manager_name(screen);
if (g_strcmp0(wm_name, "GNOME Shell") != 0) {
use_header_bar = FALSE;
}
}
#endif
if (use_header_bar) {
GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new());
gtk_widget_show(GTK_WIDGET(header_bar));
gtk_header_bar_set_title(header_bar, "finalproject");
gtk_header_bar_set_show_close_button(header_bar, TRUE);
gtk_window_set_titlebar(window, GTK_WIDGET(header_bar));
} else {
gtk_window_set_title(window, "finalproject");
}
gtk_window_set_default_size(window, 1280, 720);
gtk_widget_show(GTK_WIDGET(window));
g_autoptr(FlDartProject) project = fl_dart_project_new();
fl_dart_project_set_dart_entrypoint_arguments(project, self->dart_entrypoint_arguments);
FlView* view = fl_view_new(project);
gtk_widget_show(GTK_WIDGET(view));
gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view));
fl_register_plugins(FL_PLUGIN_REGISTRY(view));
gtk_widget_grab_focus(GTK_WIDGET(view));
}
// Implements GApplication::local_command_line.
static gboolean my_application_local_command_line(GApplication* application, gchar*** arguments, int* exit_status) {
MyApplication* self = MY_APPLICATION(application);
// Strip out the first argument as it is the binary name.
self->dart_entrypoint_arguments = g_strdupv(*arguments + 1);
g_autoptr(GError) error = nullptr;
if (!g_application_register(application, nullptr, &error)) {
g_warning("Failed to register: %s", error->message);
*exit_status = 1;
return TRUE;
}
g_application_activate(application);
*exit_status = 0;
return TRUE;
}
// Implements GApplication::startup.
static void my_application_startup(GApplication* application) {
//MyApplication* self = MY_APPLICATION(object);
// Perform any actions required at application startup.
G_APPLICATION_CLASS(my_application_parent_class)->startup(application);
}
// Implements GApplication::shutdown.
static void my_application_shutdown(GApplication* application) {
//MyApplication* self = MY_APPLICATION(object);
// Perform any actions required at application shutdown.
G_APPLICATION_CLASS(my_application_parent_class)->shutdown(application);
}
// Implements GObject::dispose.
static void my_application_dispose(GObject* object) {
MyApplication* self = MY_APPLICATION(object);
g_clear_pointer(&self->dart_entrypoint_arguments, g_strfreev);
G_OBJECT_CLASS(my_application_parent_class)->dispose(object);
}
static void my_application_class_init(MyApplicationClass* klass) {
G_APPLICATION_CLASS(klass)->activate = my_application_activate;
G_APPLICATION_CLASS(klass)->local_command_line = my_application_local_command_line;
G_APPLICATION_CLASS(klass)->startup = my_application_startup;
G_APPLICATION_CLASS(klass)->shutdown = my_application_shutdown;
G_OBJECT_CLASS(klass)->dispose = my_application_dispose;
}
static void my_application_init(MyApplication* self) {}
MyApplication* my_application_new() {
// Set the program name to the application ID, which helps various systems
// like GTK and desktop environments map this running application to its
// corresponding .desktop file. This ensures better integration by allowing
// the application to be recognized beyond its binary name.
g_set_prgname(APPLICATION_ID);
return MY_APPLICATION(g_object_new(my_application_get_type(),
"application-id", APPLICATION_ID,
"flags", G_APPLICATION_NON_UNIQUE,
nullptr));
}

View File

@ -0,0 +1,18 @@
#ifndef FLUTTER_MY_APPLICATION_H_
#define FLUTTER_MY_APPLICATION_H_
#include <gtk/gtk.h>
G_DECLARE_FINAL_TYPE(MyApplication, my_application, MY, APPLICATION,
GtkApplication)
/**
* my_application_new:
*
* Creates a new Flutter-based application.
*
* Returns: a new #MyApplication.
*/
MyApplication* my_application_new();
#endif // FLUTTER_MY_APPLICATION_H_

7
macos/.gitignore vendored Normal file
View File

@ -0,0 +1,7 @@
# Flutter-related
**/Flutter/ephemeral/
**/Pods/
# Xcode-related
**/dgph
**/xcuserdata/

View File

@ -0,0 +1 @@
#include "ephemeral/Flutter-Generated.xcconfig"

View File

@ -0,0 +1 @@
#include "ephemeral/Flutter-Generated.xcconfig"

View File

@ -0,0 +1,10 @@
//
// Generated file. Do not edit.
//
import FlutterMacOS
import Foundation
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
}

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