fix printer
This commit is contained in:
parent
a0610459bf
commit
c7ed45a374
219
README.md
219
README.md
@ -1,16 +1,215 @@
|
|||||||
# apskel_pos_flutter_v2
|
# Apskel POS Flutter
|
||||||
|
|
||||||
A new Flutter project.
|
Aplikasi Point of Sale (POS) berbasis Flutter untuk manajemen kasir restoran. Mendukung manajemen order, produk, meja, pelanggan, pembayaran, printer bluetooth/network, analitik, dan push notification via FCM.
|
||||||
|
|
||||||
## Getting Started
|
---
|
||||||
|
|
||||||
This project is a starting point for a Flutter application.
|
## Fitur Utama
|
||||||
|
|
||||||
A few resources to get you started if this is your first Flutter project:
|
- **Autentikasi** — Login dengan email & password, logout, session management
|
||||||
|
- **Order Management** — Buat, kelola, dan proses order
|
||||||
|
- **Produk & Kategori** — Manajemen menu dan kategori produk
|
||||||
|
- **Meja** — Manajemen meja restoran
|
||||||
|
- **Pelanggan** — Data pelanggan dan riwayat transaksi
|
||||||
|
- **Checkout & Pembayaran** — Proses checkout dengan berbagai metode pembayaran
|
||||||
|
- **Split Bill** — Pembagian tagihan
|
||||||
|
- **Void & Refund** — Pembatalan dan pengembalian transaksi
|
||||||
|
- **Printer** — Cetak struk via Bluetooth dan Network printer
|
||||||
|
- **Analitik** — Dashboard, laporan penjualan, produk, kategori, payment method, profit/loss, inventory
|
||||||
|
- **Sinkronisasi** — Sinkronisasi data offline/online
|
||||||
|
- **Push Notification (FCM)** — Notifikasi real-time via Firebase Cloud Messaging
|
||||||
|
|
||||||
- [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
|
## Tech Stack
|
||||||
[online documentation](https://docs.flutter.dev/), which offers tutorials,
|
|
||||||
samples, guidance on mobile development, and a full API reference.
|
| Kategori | Library |
|
||||||
|
|---|---|
|
||||||
|
| State Management | `flutter_bloc` + `bloc` |
|
||||||
|
| Dependency Injection | `get_it` + `injectable` |
|
||||||
|
| Navigation | `auto_route` |
|
||||||
|
| HTTP Client | `dio` |
|
||||||
|
| Local Database | `sqflite` |
|
||||||
|
| Local Storage | `shared_preferences` |
|
||||||
|
| Firebase | `firebase_core`, `firebase_crashlytics`, `firebase_messaging` |
|
||||||
|
| Push Notification | `flutter_local_notifications` |
|
||||||
|
| Device Info | `device_info_plus`, `package_info_plus` |
|
||||||
|
| Code Generation | `freezed`, `json_serializable` |
|
||||||
|
| Printer | `print_bluetooth_thermal`, `flutter_esc_pos_network` |
|
||||||
|
| Chart | `fl_chart` |
|
||||||
|
| Connectivity | `connectivity_plus` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Arsitektur
|
||||||
|
|
||||||
|
Project menggunakan **Clean Architecture** dengan 4 layer:
|
||||||
|
|
||||||
|
```
|
||||||
|
lib/
|
||||||
|
├── application/ # BLoC — state management per fitur
|
||||||
|
├── domain/ # Entity, repository interface, failure
|
||||||
|
├── infrastructure/ # Implementasi repository, DTO, datasource
|
||||||
|
├── presentation/ # UI — pages, components, router
|
||||||
|
└── common/ # Shared utilities, DI modules, service, theme
|
||||||
|
```
|
||||||
|
|
||||||
|
### Struktur per Fitur
|
||||||
|
|
||||||
|
```
|
||||||
|
feature/
|
||||||
|
├── application/
|
||||||
|
│ └── feature_bloc.dart # BLoC
|
||||||
|
├── domain/
|
||||||
|
│ ├── entities/ # Domain model (freezed)
|
||||||
|
│ ├── repositories/ # Interface repository
|
||||||
|
│ └── failures/ # Failure types
|
||||||
|
└── infrastructure/
|
||||||
|
├── datasources/
|
||||||
|
│ ├── remote_data_provider.dart
|
||||||
|
│ └── local_data_provider.dart
|
||||||
|
├── dtos/ # Data Transfer Object (json_serializable)
|
||||||
|
└── repositories/ # Implementasi repository
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Setup & Menjalankan
|
||||||
|
|
||||||
|
### Prasyarat
|
||||||
|
|
||||||
|
- Flutter SDK `^3.8.1`
|
||||||
|
- Dart SDK `^3.8.1`
|
||||||
|
- Android SDK / Xcode (untuk iOS)
|
||||||
|
- Firebase project yang sudah dikonfigurasi
|
||||||
|
|
||||||
|
### Instalasi
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Clone repository
|
||||||
|
git clone <repo-url>
|
||||||
|
cd apskel-pos-flutter-v2
|
||||||
|
|
||||||
|
# Install dependencies
|
||||||
|
flutter pub get
|
||||||
|
|
||||||
|
# Generate kode (freezed, injectable, auto_route, json_serializable)
|
||||||
|
dart run build_runner build --delete-conflicting-outputs
|
||||||
|
```
|
||||||
|
|
||||||
|
### Menjalankan App
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Development
|
||||||
|
flutter run
|
||||||
|
|
||||||
|
# Release
|
||||||
|
flutter run --release
|
||||||
|
```
|
||||||
|
|
||||||
|
App otomatis menggunakan environment `dev` saat debug dan `prod` saat release.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Environment
|
||||||
|
|
||||||
|
Konfigurasi environment ada di `lib/env.dart`:
|
||||||
|
|
||||||
|
| Environment | Base URL | Database |
|
||||||
|
|---|---|---|
|
||||||
|
| `dev` | `https://api-pos.apskel.id` | `apskel_pos_dev.db` |
|
||||||
|
| `prod` | `https://api-pos.apskel.id` | `apskel_pos_dev.db` |
|
||||||
|
|
||||||
|
Environment dipilih otomatis di `main.dart`:
|
||||||
|
|
||||||
|
```dart
|
||||||
|
await configureDependencies(
|
||||||
|
kReleaseMode ? Environment.prod : Environment.dev,
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Dependency Injection
|
||||||
|
|
||||||
|
DI menggunakan `get_it` + `injectable`. Semua service, repository, dan BLoC didaftarkan via annotation.
|
||||||
|
|
||||||
|
### Modul DI
|
||||||
|
|
||||||
|
| File | Isi |
|
||||||
|
|---|---|
|
||||||
|
| `di_firebase.dart` | `FirebaseMessaging`, `FlutterLocalNotificationsPlugin`, `DeviceInfoPlugin`, `PackageInfo` |
|
||||||
|
| `di_dio.dart` | `Dio` HTTP client |
|
||||||
|
| `di_shared_preferences.dart` | `SharedPreferences` |
|
||||||
|
| `di_database.dart` | `DatabaseHelper` (SQLite) |
|
||||||
|
| `di_auto_route.dart` | `AppRouter` |
|
||||||
|
| `di_connectivity.dart` | `Connectivity` |
|
||||||
|
|
||||||
|
Setelah menambah atau mengubah class yang menggunakan annotation injectable, jalankan:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
dart run build_runner build --delete-conflicting-outputs
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Firebase & FCM
|
||||||
|
|
||||||
|
### Setup
|
||||||
|
|
||||||
|
Firebase sudah dikonfigurasi di `android/app/google-services.json`. Inisialisasi dilakukan di `main.dart` sebelum app berjalan.
|
||||||
|
|
||||||
|
### FCM Service
|
||||||
|
|
||||||
|
`FcmService` (`lib/common/service/fcm_service.dart`) menangani:
|
||||||
|
|
||||||
|
- Request permission notifikasi (Android 13+ / iOS)
|
||||||
|
- Ambil dan log FCM token
|
||||||
|
- Foreground notification via `flutter_local_notifications`
|
||||||
|
- Background & terminated message handler
|
||||||
|
- Subscribe/unsubscribe topic
|
||||||
|
|
||||||
|
FCM token dikirim ke server saat login bersama device info.
|
||||||
|
|
||||||
|
### Login Payload
|
||||||
|
|
||||||
|
Saat login, app mengirim data berikut ke API:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"email": "user@example.com",
|
||||||
|
"password": "secret",
|
||||||
|
"fcm_token": "dXj3k9...",
|
||||||
|
"device_id": "abc123",
|
||||||
|
"device_name": "Samsung Galaxy Tab S8",
|
||||||
|
"device_type": "tablet",
|
||||||
|
"platform": "android",
|
||||||
|
"app_version": "1.0.4+9",
|
||||||
|
"os_version": "Android 13 (SDK 33)"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Nilai valid: `device_type` → `mobile | tablet | desktop`, `platform` → `android | ios | web`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Printer
|
||||||
|
|
||||||
|
Mendukung dua jenis printer:
|
||||||
|
|
||||||
|
- **Bluetooth** — via `print_bluetooth_thermal`
|
||||||
|
- **Network (LAN)** — via `flutter_esc_pos_network`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Orientasi Layar
|
||||||
|
|
||||||
|
App dikunci ke mode **landscape** (kiri & kanan) karena didesain untuk tablet POS.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Catatan Development
|
||||||
|
|
||||||
|
- Jangan edit `injection.config.dart` dan file `*.freezed.dart` / `*.g.dart` secara manual — file tersebut di-generate otomatis
|
||||||
|
- Gunakan `log()` dari `dart:developer` untuk logging, bukan `print()`
|
||||||
|
- `print()` dinonaktifkan di release mode
|
||||||
|
|||||||
@ -117,6 +117,13 @@ class PrinterFormBloc extends Bloc<PrinterFormEvent, PrinterFormState> {
|
|||||||
|
|
||||||
final currentOutlet = await _outletRepository.currentOutlet();
|
final currentOutlet = await _outletRepository.currentOutlet();
|
||||||
|
|
||||||
|
// Ambil paper size dari DB, fallback ke state.paper jika belum tersimpan
|
||||||
|
final savedPrinter = await _printerRepository.getPrinterByCode(e.code);
|
||||||
|
final paper = savedPrinter.fold(
|
||||||
|
(_) => int.tryParse(state.paper) ?? 58,
|
||||||
|
(p) => int.tryParse(p.paper) ?? 58,
|
||||||
|
);
|
||||||
|
|
||||||
List<int> printValue = [];
|
List<int> printValue = [];
|
||||||
|
|
||||||
if (e.code == "receipt") {
|
if (e.code == "receipt") {
|
||||||
@ -124,29 +131,34 @@ class PrinterFormBloc extends Bloc<PrinterFormEvent, PrinterFormState> {
|
|||||||
order: Order.mockOrder(),
|
order: Order.mockOrder(),
|
||||||
outlet: currentOutlet,
|
outlet: currentOutlet,
|
||||||
cashierName: 'Kasir Test',
|
cashierName: 'Kasir Test',
|
||||||
|
paper: paper,
|
||||||
);
|
);
|
||||||
} else if (e.code == "checker") {
|
} else if (e.code == "checker") {
|
||||||
printValue = await PrintUi().printChecker(
|
printValue = await PrintUi().printChecker(
|
||||||
order: Order.mockOrder(),
|
order: Order.mockOrder(),
|
||||||
outlet: currentOutlet,
|
outlet: currentOutlet,
|
||||||
cashierName: 'Kasir Test',
|
cashierName: 'Kasir Test',
|
||||||
|
paper: paper,
|
||||||
);
|
);
|
||||||
} else if (e.code == 'kitchen') {
|
} else if (e.code == 'kitchen') {
|
||||||
printValue = await PrintUi().printKitchen(
|
printValue = await PrintUi().printKitchen(
|
||||||
order: Order.mockOrder(),
|
order: Order.mockOrder(),
|
||||||
outlet: currentOutlet,
|
outlet: currentOutlet,
|
||||||
cashierName: 'Kasir Test',
|
cashierName: 'Kasir Test',
|
||||||
|
paper: paper,
|
||||||
);
|
);
|
||||||
} else if (e.code == 'bar') {
|
} else if (e.code == 'bar') {
|
||||||
printValue = await PrintUi().printBar(
|
printValue = await PrintUi().printBar(
|
||||||
order: Order.mockOrder(),
|
order: Order.mockOrder(),
|
||||||
outlet: currentOutlet,
|
outlet: currentOutlet,
|
||||||
cashierName: 'Kasir Test',
|
cashierName: 'Kasir Test',
|
||||||
|
paper: paper,
|
||||||
);
|
);
|
||||||
} else if (e.code == 'ticket') {
|
} else if (e.code == 'ticket') {
|
||||||
printValue = await PrintUi().printTicket(
|
printValue = await PrintUi().printTicket(
|
||||||
order: Order.mockOrder(),
|
order: Order.mockOrder(),
|
||||||
outlet: currentOutlet,
|
outlet: currentOutlet,
|
||||||
|
paper: paper,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -154,7 +166,7 @@ class PrinterFormBloc extends Bloc<PrinterFormEvent, PrinterFormState> {
|
|||||||
code: e.code,
|
code: e.code,
|
||||||
name: state.name,
|
name: state.name,
|
||||||
address: e.macAccdress,
|
address: e.macAccdress,
|
||||||
paper: state.paper,
|
paper: paper.toString(),
|
||||||
type: state.type,
|
type: state.type,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@ -10,7 +10,7 @@ abstract class Env {
|
|||||||
@dev
|
@dev
|
||||||
class DevEnv implements Env {
|
class DevEnv implements Env {
|
||||||
@override
|
@override
|
||||||
String get baseUrl => 'http://192.168.1.13:4000';
|
String get baseUrl => 'https://api-pos.apskel.id';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get dbName => "apskel_pos_dev.db"; // example value
|
String get dbName => "apskel_pos_dev.db"; // example value
|
||||||
|
|||||||
@ -15,8 +15,19 @@ class PrinterDto with _$PrinterDto {
|
|||||||
@JsonKey(name: 'updated_at') required DateTime updatedAt,
|
@JsonKey(name: 'updated_at') required DateTime updatedAt,
|
||||||
}) = _PrinterDto;
|
}) = _PrinterDto;
|
||||||
|
|
||||||
factory PrinterDto.fromJson(Map<String, dynamic> json) =>
|
factory PrinterDto.fromJson(Map<String, dynamic> json) => _$PrinterDtoFromJson(
|
||||||
_$PrinterDtoFromJson(json);
|
// Normalize paper field: SQLite bisa return int atau String
|
||||||
|
{
|
||||||
|
...json,
|
||||||
|
'paper': json['paper']?.toString() ?? '58',
|
||||||
|
'created_at': json['created_at'] is DateTime
|
||||||
|
? (json['created_at'] as DateTime).toIso8601String()
|
||||||
|
: json['created_at'],
|
||||||
|
'updated_at': json['updated_at'] is DateTime
|
||||||
|
? (json['updated_at'] as DateTime).toIso8601String()
|
||||||
|
: json['updated_at'],
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
// Optional mapper to domain
|
// Optional mapper to domain
|
||||||
Printer toDomain() => Printer(
|
Printer toDomain() => Printer(
|
||||||
|
|||||||
@ -674,6 +674,7 @@ class PrinterRepository implements IPrinterRepository {
|
|||||||
if (printResult.isLeft()) return printResult.map((_) => unit);
|
if (printResult.isLeft()) return printResult.map((_) => unit);
|
||||||
|
|
||||||
log('Finished printed receipt', name: _logName);
|
log('Finished printed receipt', name: _logName);
|
||||||
|
log('Printer Size: ', name: printer.paper);
|
||||||
|
|
||||||
return right(unit);
|
return right(unit);
|
||||||
} catch (e, stackTrace) {
|
} catch (e, stackTrace) {
|
||||||
|
|||||||
@ -21,7 +21,7 @@ class PrintUi {
|
|||||||
);
|
);
|
||||||
final builder = ReceiptComponentBuilder(
|
final builder = ReceiptComponentBuilder(
|
||||||
generator: generator,
|
generator: generator,
|
||||||
paperSize: 58,
|
paperSize: paper,
|
||||||
);
|
);
|
||||||
|
|
||||||
bytes += generator.reset();
|
bytes += generator.reset();
|
||||||
@ -85,7 +85,7 @@ class PrintUi {
|
|||||||
);
|
);
|
||||||
final builder = ReceiptComponentBuilder(
|
final builder = ReceiptComponentBuilder(
|
||||||
generator: generator,
|
generator: generator,
|
||||||
paperSize: 58,
|
paperSize: paper,
|
||||||
);
|
);
|
||||||
|
|
||||||
bytes += generator.reset();
|
bytes += generator.reset();
|
||||||
@ -147,7 +147,7 @@ class PrintUi {
|
|||||||
);
|
);
|
||||||
final builder = ReceiptComponentBuilder(
|
final builder = ReceiptComponentBuilder(
|
||||||
generator: generator,
|
generator: generator,
|
||||||
paperSize: 58,
|
paperSize: paper,
|
||||||
);
|
);
|
||||||
|
|
||||||
for (final item in order.orderItems) {
|
for (final item in order.orderItems) {
|
||||||
@ -198,7 +198,7 @@ class PrintUi {
|
|||||||
);
|
);
|
||||||
final builder = ReceiptComponentBuilder(
|
final builder = ReceiptComponentBuilder(
|
||||||
generator: generator,
|
generator: generator,
|
||||||
paperSize: 58,
|
paperSize: paper,
|
||||||
);
|
);
|
||||||
|
|
||||||
for (final item in order.orderItems) {
|
for (final item in order.orderItems) {
|
||||||
@ -248,7 +248,7 @@ class PrintUi {
|
|||||||
);
|
);
|
||||||
final builder = ReceiptComponentBuilder(
|
final builder = ReceiptComponentBuilder(
|
||||||
generator: generator,
|
generator: generator,
|
||||||
paperSize: 58,
|
paperSize: paper,
|
||||||
);
|
);
|
||||||
|
|
||||||
bytes += generator.reset();
|
bytes += generator.reset();
|
||||||
@ -289,7 +289,7 @@ class PrintUi {
|
|||||||
);
|
);
|
||||||
final builder = ReceiptComponentBuilder(
|
final builder = ReceiptComponentBuilder(
|
||||||
generator: generator,
|
generator: generator,
|
||||||
paperSize: 58,
|
paperSize: paper,
|
||||||
);
|
);
|
||||||
|
|
||||||
bytes += generator.reset();
|
bytes += generator.reset();
|
||||||
@ -355,7 +355,7 @@ class PrintUi {
|
|||||||
);
|
);
|
||||||
final builder = ReceiptComponentBuilder(
|
final builder = ReceiptComponentBuilder(
|
||||||
generator: generator,
|
generator: generator,
|
||||||
paperSize: 58,
|
paperSize: paper,
|
||||||
);
|
);
|
||||||
|
|
||||||
bytes += generator.reset();
|
bytes += generator.reset();
|
||||||
@ -420,7 +420,7 @@ class PrintUi {
|
|||||||
);
|
);
|
||||||
final builder = ReceiptComponentBuilder(
|
final builder = ReceiptComponentBuilder(
|
||||||
generator: generator,
|
generator: generator,
|
||||||
paperSize: 58,
|
paperSize: paper,
|
||||||
);
|
);
|
||||||
|
|
||||||
bytes += generator.reset();
|
bytes += generator.reset();
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import 'package:esc_pos_utils_plus/esc_pos_utils_plus.dart';
|
import 'package:esc_pos_utils_plus/esc_pos_utils_plus.dart';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
|
|
||||||
/// Reusable component builder untuk thermal receipt printer
|
/// Reusable component builder untuk thermal receipt printer
|
||||||
@ -265,7 +266,11 @@ class ReceiptComponentBuilder {
|
|||||||
height: PosTextSize.size1,
|
height: PosTextSize.size1,
|
||||||
width: PosTextSize.size1,
|
width: PosTextSize.size1,
|
||||||
);
|
);
|
||||||
|
if (kDebugMode) {
|
||||||
|
bytes += textCenter("$paperSize MM", );
|
||||||
|
}
|
||||||
bytes += feed(paperSize == 80 ? 3 : 1);
|
bytes += feed(paperSize == 80 ? 3 : 1);
|
||||||
|
|
||||||
bytes += generator.cut();
|
bytes += generator.cut();
|
||||||
|
|
||||||
return bytes;
|
return bytes;
|
||||||
|
|||||||
@ -3,7 +3,7 @@ description: "A new Flutter project."
|
|||||||
|
|
||||||
publish_to: "none"
|
publish_to: "none"
|
||||||
|
|
||||||
version: 1.0.4+9
|
version: 1.0.5+10
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ^3.8.1
|
sdk: ^3.8.1
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user