commit 481859052a8a9e87b4db416db4a64c0bb34ec420 Author: nextaliakbar Date: Thu Jun 11 07:18:56 2026 +0700 Upload Proyek Tugas Akhir Berbasis Mobile diff --git a/Backend/.editorconfig b/Backend/.editorconfig new file mode 100644 index 0000000..a186cd2 --- /dev/null +++ b/Backend/.editorconfig @@ -0,0 +1,18 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +indent_size = 4 +indent_style = space +insert_final_newline = true +trim_trailing_whitespace = true + +[*.md] +trim_trailing_whitespace = false + +[*.{yml,yaml}] +indent_size = 2 + +[compose.yaml] +indent_size = 4 diff --git a/Backend/.env.example b/Backend/.env.example new file mode 100644 index 0000000..c0660ea --- /dev/null +++ b/Backend/.env.example @@ -0,0 +1,65 @@ +APP_NAME=Laravel +APP_ENV=local +APP_KEY= +APP_DEBUG=true +APP_URL=http://localhost + +APP_LOCALE=en +APP_FALLBACK_LOCALE=en +APP_FAKER_LOCALE=en_US + +APP_MAINTENANCE_DRIVER=file +# APP_MAINTENANCE_STORE=database + +# PHP_CLI_SERVER_WORKERS=4 + +BCRYPT_ROUNDS=12 + +LOG_CHANNEL=stack +LOG_STACK=single +LOG_DEPRECATIONS_CHANNEL=null +LOG_LEVEL=debug + +DB_CONNECTION=sqlite +# DB_HOST=127.0.0.1 +# DB_PORT=3306 +# DB_DATABASE=laravel +# DB_USERNAME=root +# DB_PASSWORD= + +SESSION_DRIVER=database +SESSION_LIFETIME=120 +SESSION_ENCRYPT=false +SESSION_PATH=/ +SESSION_DOMAIN=null + +BROADCAST_CONNECTION=log +FILESYSTEM_DISK=local +QUEUE_CONNECTION=database + +CACHE_STORE=database +# CACHE_PREFIX= + +MEMCACHED_HOST=127.0.0.1 + +REDIS_CLIENT=phpredis +REDIS_HOST=127.0.0.1 +REDIS_PASSWORD=null +REDIS_PORT=6379 + +MAIL_MAILER=log +MAIL_SCHEME=null +MAIL_HOST=127.0.0.1 +MAIL_PORT=2525 +MAIL_USERNAME=null +MAIL_PASSWORD=null +MAIL_FROM_ADDRESS="hello@example.com" +MAIL_FROM_NAME="${APP_NAME}" + +AWS_ACCESS_KEY_ID= +AWS_SECRET_ACCESS_KEY= +AWS_DEFAULT_REGION=us-east-1 +AWS_BUCKET= +AWS_USE_PATH_STYLE_ENDPOINT=false + +VITE_APP_NAME="${APP_NAME}" diff --git a/Backend/.gitattributes b/Backend/.gitattributes new file mode 100644 index 0000000..fcb21d3 --- /dev/null +++ b/Backend/.gitattributes @@ -0,0 +1,11 @@ +* text=auto eol=lf + +*.blade.php diff=html +*.css diff=css +*.html diff=html +*.md diff=markdown +*.php diff=php + +/.github export-ignore +CHANGELOG.md export-ignore +.styleci.yml export-ignore diff --git a/Backend/.gitignore b/Backend/.gitignore new file mode 100644 index 0000000..b71b1ea --- /dev/null +++ b/Backend/.gitignore @@ -0,0 +1,24 @@ +*.log +.DS_Store +.env +.env.backup +.env.production +.phpactor.json +.phpunit.result.cache +/.fleet +/.idea +/.nova +/.phpunit.cache +/.vscode +/.zed +/auth.json +/node_modules +/public/build +/public/hot +/public/storage +/storage/*.key +/storage/pail +/vendor +Homestead.json +Homestead.yaml +Thumbs.db diff --git a/Backend/README.md b/Backend/README.md new file mode 100644 index 0000000..0165a77 --- /dev/null +++ b/Backend/README.md @@ -0,0 +1,59 @@ +

Laravel Logo

+ +

+Build Status +Total Downloads +Latest Stable Version +License +

+ +## About Laravel + +Laravel is a web application framework with expressive, elegant syntax. We believe development must be an enjoyable and creative experience to be truly fulfilling. Laravel takes the pain out of development by easing common tasks used in many web projects, such as: + +- [Simple, fast routing engine](https://laravel.com/docs/routing). +- [Powerful dependency injection container](https://laravel.com/docs/container). +- Multiple back-ends for [session](https://laravel.com/docs/session) and [cache](https://laravel.com/docs/cache) storage. +- Expressive, intuitive [database ORM](https://laravel.com/docs/eloquent). +- Database agnostic [schema migrations](https://laravel.com/docs/migrations). +- [Robust background job processing](https://laravel.com/docs/queues). +- [Real-time event broadcasting](https://laravel.com/docs/broadcasting). + +Laravel is accessible, powerful, and provides tools required for large, robust applications. + +## Learning Laravel + +Laravel has the most extensive and thorough [documentation](https://laravel.com/docs) and video tutorial library of all modern web application frameworks, making it a breeze to get started with the framework. You can also check out [Laravel Learn](https://laravel.com/learn), where you will be guided through building a modern Laravel application. + +If you don't feel like reading, [Laracasts](https://laracasts.com) can help. Laracasts contains thousands of video tutorials on a range of topics including Laravel, modern PHP, unit testing, and JavaScript. Boost your skills by digging into our comprehensive video library. + +## Laravel Sponsors + +We would like to extend our thanks to the following sponsors for funding Laravel development. If you are interested in becoming a sponsor, please visit the [Laravel Partners program](https://partners.laravel.com). + +### Premium Partners + +- **[Vehikl](https://vehikl.com)** +- **[Tighten Co.](https://tighten.co)** +- **[Kirschbaum Development Group](https://kirschbaumdevelopment.com)** +- **[64 Robots](https://64robots.com)** +- **[Curotec](https://www.curotec.com/services/technologies/laravel)** +- **[DevSquad](https://devsquad.com/hire-laravel-developers)** +- **[Redberry](https://redberry.international/laravel-development)** +- **[Active Logic](https://activelogic.com)** + +## Contributing + +Thank you for considering contributing to the Laravel framework! The contribution guide can be found in the [Laravel documentation](https://laravel.com/docs/contributions). + +## Code of Conduct + +In order to ensure that the Laravel community is welcoming to all, please review and abide by the [Code of Conduct](https://laravel.com/docs/contributions#code-of-conduct). + +## Security Vulnerabilities + +If you discover a security vulnerability within Laravel, please send an e-mail to Taylor Otwell via [taylor@laravel.com](mailto:taylor@laravel.com). All security vulnerabilities will be promptly addressed. + +## License + +The Laravel framework is open-sourced software licensed under the [MIT license](https://opensource.org/licenses/MIT). diff --git a/Backend/app/Events/CancelOrder.php b/Backend/app/Events/CancelOrder.php new file mode 100644 index 0000000..b03fe71 --- /dev/null +++ b/Backend/app/Events/CancelOrder.php @@ -0,0 +1,53 @@ +order = $order; + } + + /** + * Get the channels the event should broadcast on. + * + * @return array + */ + public function broadcastOn(): array + { + return [ + new PrivateChannel('tenant.' . $this->order->tenant_id), + ]; + } + + public function broadcastAs(): string + { + return 'order.canceled'; + } + + public function broadcastWith(): array + { + return [ + 'id' => $this->order->uuid, + 'outlet_id' => $this->order->outlet_id, + 'updated_at' => $this->order->updated_at->toDateTimeString() + ]; + } +} diff --git a/Backend/app/Events/CancellationRequest.php b/Backend/app/Events/CancellationRequest.php new file mode 100644 index 0000000..b70db82 --- /dev/null +++ b/Backend/app/Events/CancellationRequest.php @@ -0,0 +1,54 @@ +order = $order; + } + + /** + * Get the channels the event should broadcast on. + * + * @return array + */ + public function broadcastOn(): array + { + return [ + new PrivateChannel('tenant.' . $this->order->tenant_id), + ]; + } + + public function broadcastAs(): string + { + return 'order.cancellation_request'; + } + + public function broadcastWith(): array + { + return [ + 'id' => $this->order->uuid, + 'outlet_id' => $this->order->outlet_id, + 'updated_at' => $this->order->updated_at->toDateTimeString() + ]; + } +} diff --git a/Backend/app/Events/DataBatchUpdated.php b/Backend/app/Events/DataBatchUpdated.php new file mode 100644 index 0000000..53416dc --- /dev/null +++ b/Backend/app/Events/DataBatchUpdated.php @@ -0,0 +1,48 @@ +tenantId = $tenantId; + $this->changedTables = $changedTables; + } + + public function broadcastOn(): array + { + return [ + new Channel('tenant.' . $this->tenantId), + ]; + } + + public function broadcastAs(): string + { + return 'data.batch.updated'; + } + + public function broadcastWith(): array + { + return [ + 'tenant_id' => $this->tenantId, + 'changed_tables' => $this->changedTables, + 'updated_at' => now()->toDateTimeString(), + ]; + } +} diff --git a/Backend/app/Events/OrderPlaced.php b/Backend/app/Events/OrderPlaced.php new file mode 100644 index 0000000..a3b87f5 --- /dev/null +++ b/Backend/app/Events/OrderPlaced.php @@ -0,0 +1,59 @@ +order = $order; + } + + /** + * Menentukan Private Channel berdasarkan Tenant ID + */ + public function broadcastOn(): array + { + return [ + new PrivateChannel('tenant.' . $this->order->tenant_id), + ]; + } + + /** + * Nama Event yang akan didengarkan oleh Laravel Echo di Flutter + */ + public function broadcastAs(): string + { + return 'order.created'; + } + + /** + * Payload data yang dikirimkan ke WebSocket + * Pastikan field ini sesuai dengan kolom di tabel Order Anda + */ + public function broadcastWith() + { + + return [ + 'order_id' => $this->order->uuid, + 'outlet_id' => $this->order->outlet_id + ]; + } +} \ No newline at end of file diff --git a/Backend/app/Http/Controllers/Api/AuthController.php b/Backend/app/Http/Controllers/Api/AuthController.php new file mode 100644 index 0000000..7c1168f --- /dev/null +++ b/Backend/app/Http/Controllers/Api/AuthController.php @@ -0,0 +1,83 @@ +authService = $authService; + } + + public function login(Request $request) + { + $request->validate([ + 'identifier' => 'required|string', + 'password' => 'required|string', + 'business_code' => 'nullable|string', + ]); + + try { + $result = $this->authService->login($request->all(), $request->ip()); + + if (isset($result['error']) && $result['error'] === 'lockout') { + return $this->errorResponse( + $result['error'], + 429, + [ + 'seconds_remaining' => $result['seconds'], + 'locked_until' => $result['until'] + ] + ); + } + + if (isset($result['error'])) { + return $this->errorResponse($result['error'], 401); + } + + return $this->successResponse([ + 'access_token' => $result['token'], + 'user' => new UserResource($result['user']), + ], 'login success', 200); + } catch(\Exception $e) { + return $this->errorResponse('internal server error', 500, $e->getMessage()); + } + } + + public function register(Request $request) + { + $data = $request->validate([ + 'business_name' => 'required|string|max:255', + 'business_code' => 'required|string|unique:tenants,business_code', + 'name' => 'required|string|max:255', + 'phone_number' => 'required|string|unique:users,phone_number', + 'email' => 'nullable|email|unique:users,email', + 'password' => 'required|min:8|confirmed', + 'password_confirmation' => 'required|min:8' + ], [ + 'phone_number.unique' => 'phone number already exists', + 'email.unique' => 'email already exists', + ]); + + try { + $result = $this->authService->register($data); + + return $this->successResponse([ + 'business_code' => $result['business_code'], + 'owner_name' => $result['user']->name, + ], 'registration success', 201); + } catch (\Exception $e) { + return $this->errorResponse('internal server error', 500, $e->getMessage()); + } + } +} diff --git a/Backend/app/Http/Controllers/Api/CheckoutController.php b/Backend/app/Http/Controllers/Api/CheckoutController.php new file mode 100644 index 0000000..fda18be --- /dev/null +++ b/Backend/app/Http/Controllers/Api/CheckoutController.php @@ -0,0 +1,47 @@ +checkoutService = $checkoutService; + } + + public function store(Request $request) + { + try { + $data = json_decode($request->input('data'), true); + + if (!$data) { + throw new \Exception("Invalid order data format"); + } + + $order = $this->checkoutService->execute( + $data, + $request->file('payment_proof_file') + ); + + broadcast(new OrderPlaced($order))->toOthers(); + + return $this->successResponse([ + 'order_id' => $order->uuid, + 'order_number' => $order->order_number + ], 'Process checkout successfully'); + + } catch (\Exception $e) { + return $this->errorResponse('Failed to process checkout', 400, $e->getMessage()); + } + } +} diff --git a/Backend/app/Http/Controllers/Api/ConfigurationController.php b/Backend/app/Http/Controllers/Api/ConfigurationController.php new file mode 100644 index 0000000..723315a --- /dev/null +++ b/Backend/app/Http/Controllers/Api/ConfigurationController.php @@ -0,0 +1,30 @@ +configurationService = $configurationService; + } + + public function getConfigurationService() + { + try { + $data = $this->configurationService->getConfigurationService(); + return $this->successResponse($data); + } catch(\Exception $e) { + return $this->errorResponse('Failed to fetch configuration service', 400, $e->getMessage()); + } + } +} diff --git a/Backend/app/Http/Controllers/Api/CustomerController.php b/Backend/app/Http/Controllers/Api/CustomerController.php new file mode 100644 index 0000000..8a55c6c --- /dev/null +++ b/Backend/app/Http/Controllers/Api/CustomerController.php @@ -0,0 +1,122 @@ +customerService = $customerService; + } + + public function login(Request $request) + { + $request->validate([ + 'identifier' => 'required|string', + 'password' => 'required|string', + 'business_code' => 'nullable|string', + ]); + + try { + $result = $this->customerService->login($request->all(), $request->ip()); + + if (isset($result['error']) && $result['error'] === 'lockout') { + return $this->errorResponse( + $result['error'], + 429, + [ + 'seconds_remaining' => $result['seconds'], + 'locked_until' => $result['until'] + ] + ); + } + + if (isset($result['error'])) { + return $this->errorResponse($result['error'], 401); + } + + return $this->successResponse([ + 'access_token' => $result['token'], + 'customer' => new CustomerResource($result['customer']) + ], 'login success', 200); + } catch(\Exception $e) { + return $this->errorResponse('internal server error', 500, $e->getMessage()); + } + } + + public function register(Request $request) + { + try { + $this->customerService->register($request->all()); + return $this->successResponse(null, 'registration success', 201); + } catch (\Exception $e) { + return $this->errorResponse('internal server error', 500, $e->getMessage()); + } + } + + public function updatePassword(Request $request, $customerId) + { + try { + $result = $this->customerService->updatePassword($request->all(), $customerId); + + if(isset($result['error'])) { + return $this->errorResponse('internal server error', 500, $result['error']); + } + return $this->successResponse(null, 'update password success', 201); + } catch (\Exception $e) { + return $this->errorResponse('internal server error', 500, $e->getMessage()); + } + } + + public function updateCustomer(Request $request, $customerId) + { + try { + $data = $request->all(); + $customerInfo = $this->customerService->updateCustomer($data, $customerId); + return $this->successResponse($customerInfo, 'Update customer successfully'); + } catch(\Exception $e) { + return $this->errorResponse('Failed to update customer', errorDetails: $e->getMessage()); + } + } + + public function getCustomerAddress($customerId) + { + try { + $customerAddress = $this->customerService->getCustomerAddress($customerId); + return $this->successResponse($customerAddress); + } catch (\Exception $e) { + return $this->errorResponse('Failed to retrieve customer address', errorDetails: $e->getMessage()); + } + } + + public function saveCustomerAddress(Request $request, $customerId) + { + try { + $data = $request->all(); + $this->customerService->saveCustomerAddress($data, $customerId); + return $this->successResponse(null, 'Save customer address successfully'); + } catch(\Exception $e) { + return $this->errorResponse('Failed to save customer address', errorDetails: $e->getMessage()); + } + } + + public function deleteCustomerAddress($customerId, $id) + { + try { + $this->customerService->deleteCustomerAddress($customerId, $id); + return $this->successResponse(null, 'Delete customer address successfully'); + } catch(\Exception $e) { + return $this->errorResponse('Failed to delete customer address', errorDetails: $e->getMessage()); + } + } +} diff --git a/Backend/app/Http/Controllers/Api/HomeController.php b/Backend/app/Http/Controllers/Api/HomeController.php new file mode 100644 index 0000000..cd8763e --- /dev/null +++ b/Backend/app/Http/Controllers/Api/HomeController.php @@ -0,0 +1,54 @@ +homeService = $homeService; + } + + public function home($outletId) + { + try { + $data = $this->homeService->home($outletId); + + return $this->successResponse($data); + + } catch(\Exception $e) { + return $this->errorResponse(errorDetails: $e->getMessage()); + } + } + + public function getOutlets() + { + try { + $data = $this->homeService->getOutlets(); + + return $this->successResponse($data); + + } catch(\Exception $e) { + return $this->errorResponse(errorDetails: $e->getMessage()); + } + } + + public function getMainOutlet() + { + try { + $data = $this->homeService->getMainOutlet(); + return $this->successResponse($data); + } catch(\Exception $e) { + return $this->errorResponse(errorDetails: $e->getMessage()); + } + } +} diff --git a/Backend/app/Http/Controllers/Api/OrderController.php b/Backend/app/Http/Controllers/Api/OrderController.php new file mode 100644 index 0000000..1230f97 --- /dev/null +++ b/Backend/app/Http/Controllers/Api/OrderController.php @@ -0,0 +1,79 @@ +orderService = $orderService; + } + + public function getOrderSync($orderId) + { + try { + $data = $this->orderService->getOrderSync($orderId); + + return $this->successResponse($data); + } catch(\Exception $e) { + return $this->errorResponse(errorDetails: $e->getMessage()); + } + } + + public function getOrders($customerId) + { + try { + $data = $this->orderService->getOrders($customerId); + + return $this->successResponse($data); + } catch(\Exception $e) { + return $this->errorResponse(errorDetails: $e->getMessage()); + } + } + + public function getOrderById($customerId, $orderId) + { + try { + $data = $this->orderService->getOrderById($customerId, $orderId); + return $this->successResponse($data); + } catch(\Exception $e) { + return $this->errorResponse(errorDetails: $e->getMessage()); + } + } + + public function cancelOrder($customerId, $orderId) + { + try { + $order = $this->orderService->cancelOrder($customerId, $orderId); + + broadcast(new CancelOrder($order))->toOthers(); + + return $this->successResponse(null, 'Cancel order successfully'); + } catch(\Exception $e) { + return $this->errorResponse('Failed to cancel order', 400, $e->getMessage()); + } + } + + public function cancellationRequest($customerId, $orderId) + { + try { + $order = $this->orderService->cancellationRequest($customerId, $orderId); + + broadcast(new CancellationRequest($order))->toOthers(); + + return $this->successResponse(null, 'Cancellation order request successfully'); + } catch(\Exception $e) { + return $this->errorResponse('Failed to cancellation order request', 400, $e->getMessage()); + } + } +} diff --git a/Backend/app/Http/Controllers/Api/ProductController.php b/Backend/app/Http/Controllers/Api/ProductController.php new file mode 100644 index 0000000..4004fd3 --- /dev/null +++ b/Backend/app/Http/Controllers/Api/ProductController.php @@ -0,0 +1,66 @@ +productService = $productService; + } + + public function getProductById($id) + { + try { + $data = $this->productService->getProductById($id); + + return $this->successResponse($data); + } catch(\Exception $e) { + return $this->errorResponse(errorDetails:$e->getMessage()); + } + } + + public function getProductByName(Request $request, $currentOutletId) + { + $name = $request->query('name'); + + try { + $data = $this->productService->getProductByName($currentOutletId, $name); + + return $this->successResponse($data); + } catch(\Exception $e) { + return $this->errorResponse(errorDetails:$e->getMessage()); + } + } + + public function getProductByCategory($currentOutletId, $categoryId) + { + try { + $data = $this->productService->getProductByCategory($currentOutletId, $categoryId); + + return $this->successResponse($data); + } catch(\Exception $e) { + return $this->errorResponse(errorDetails:$e->getMessage()); + } + } + + public function getProductBestSellerByOutlet($currentOutletId) + { + try { + $data = $this->productService->getProductBestSellerByOutlet($currentOutletId); + + return $this->successResponse($data); + } catch(\Exception $e) { + return $this->errorResponse(errorDetails:$e->getMessage()); + } + } +} diff --git a/Backend/app/Http/Controllers/Api/ReviewController.php b/Backend/app/Http/Controllers/Api/ReviewController.php new file mode 100644 index 0000000..633f4ce --- /dev/null +++ b/Backend/app/Http/Controllers/Api/ReviewController.php @@ -0,0 +1,40 @@ +reviewService = $reviewService; + } + + public function getProductReviewItem($orderId) + { + try { + $data = $this->reviewService->getProductReviewItem($orderId); + return $this->successResponse($data, 'Fetching product review item successfully'); + } catch(\Exception $e) { + return $this->errorResponse('Failed to fetch product review item', 400, $e->getMessage()); + } + } + + public function reviewProduct(Request $request, $orderId) + { + try { + $this->reviewService->reviewProduct($request->all(), $orderId); + return $this->successResponse(null, 'Review product successfully'); + } catch(\Exception $e) { + return $this->errorResponse('Failed to review product', 400, $e->getMessage()); + } + } +} diff --git a/Backend/app/Http/Controllers/Api/SyncController.php b/Backend/app/Http/Controllers/Api/SyncController.php new file mode 100644 index 0000000..8fc7173 --- /dev/null +++ b/Backend/app/Http/Controllers/Api/SyncController.php @@ -0,0 +1,72 @@ +syncService = $syncService; + } + + public function getDeltaSync(Request $request) + { + $lastSync = $request->query('last_sync'); + try { + $currentTime = Carbon::now(); + + $data = $this->syncService->getDeltaSync($lastSync); + + $this->syncService->updateServerTime($currentTime); + return response()->json([ + 'status' => 'success', + 'server_time' => $currentTime->toDateTimeString(), + 'data' => $data + ], 200); + } catch(\Exception $e) { + return response()->json([ + 'status' => 'error', + 'message' => $e->getMessage() + ], 500); + } + } + + public function batchUpSync(Request $request) + { + $request->validate([ + 'batch_id' => 'required|string', + 'items' => 'required|array', + ]); + + $payload = $request->all(); + + $payload['items'] = array_map(function($item) { + return is_string($item) ? json_decode($item, true) : $item; + }, $payload['items']); + + try { + $report = $this->syncService->processBatchSync($payload); + + return response()->json([ + 'status' => 'success', + 'batch_id' => $request->batch_id, + 'report' => $report, + ], 200); + + } catch (\Exception $e) { + return response()->json([ + 'status' => 'error', + 'message' => $e->getMessage(), + ], 500); + } + } +} diff --git a/Backend/app/Http/Controllers/Api/UploadController.php b/Backend/app/Http/Controllers/Api/UploadController.php new file mode 100644 index 0000000..9431c37 --- /dev/null +++ b/Backend/app/Http/Controllers/Api/UploadController.php @@ -0,0 +1,39 @@ +uploadService = $uploadService; + } + + public function uploadImage(Request $request) + { + $request->validate([ + 'image' => 'required|image|max:5120', + 'type' => 'required|string' + ]); + + try { + $serverPath = $this->uploadService->uploadImage( + $request->file('image'), + $request->type + ); + + return $this->successResponse(['server_path' => $serverPath ],'upload image success', 200); + } catch (\Exception $e) { + return $this->errorResponse('upload image failed', 500, $e->getMessage()); + } + } +} diff --git a/Backend/app/Http/Controllers/Controller.php b/Backend/app/Http/Controllers/Controller.php new file mode 100644 index 0000000..8036a5e --- /dev/null +++ b/Backend/app/Http/Controllers/Controller.php @@ -0,0 +1,6 @@ +header('X-Business-Code'); + + if (!$businessCode) { + return response()->json(['error' => 'Missing Business Code Header'], 403); + } + + $tenant = Tenant::where('business_code', $businessCode)->first(); + + if (!$tenant) { + return response()->json(['error' => 'Business not found'], 404); + } + + Config::set('app.current_tenant_id', $tenant->uuid); + + Config::set('app.current_tenant_code', $businessCode); + + + return $next($request); + } +} diff --git a/Backend/app/Http/Resources/CustomerResource.php b/Backend/app/Http/Resources/CustomerResource.php new file mode 100644 index 0000000..e62ed01 --- /dev/null +++ b/Backend/app/Http/Resources/CustomerResource.php @@ -0,0 +1,24 @@ + + */ + public function toArray(Request $request): array + { + return [ + 'uuid' => $this->uuid, + 'name' => $this->name, + 'email' => $this->email, + 'phone_number' => $this->phone_number + ]; + } +} diff --git a/Backend/app/Http/Resources/Home/AllCategoriesResource.php b/Backend/app/Http/Resources/Home/AllCategoriesResource.php new file mode 100644 index 0000000..8453c86 --- /dev/null +++ b/Backend/app/Http/Resources/Home/AllCategoriesResource.php @@ -0,0 +1,23 @@ + + */ + public function toArray(Request $request): array + { + return [ + 'id' => $this->uuid, + 'server_image_url' => $this->server_image_url, + 'name' => $this->name, + ]; + } +} diff --git a/Backend/app/Http/Resources/Home/CampaignByOutletResource.php b/Backend/app/Http/Resources/Home/CampaignByOutletResource.php new file mode 100644 index 0000000..ba70101 --- /dev/null +++ b/Backend/app/Http/Resources/Home/CampaignByOutletResource.php @@ -0,0 +1,23 @@ + + */ + public function toArray(Request $request): array + { + return [ + 'campaign_type' => $this->action_type, + 'action_ref_id' => $this->action_reference_id, + 'server_banner_url' => $this->server_banner_url + ]; + } +} diff --git a/Backend/app/Http/Resources/Home/CurrentOutletResource.php b/Backend/app/Http/Resources/Home/CurrentOutletResource.php new file mode 100644 index 0000000..37be9ce --- /dev/null +++ b/Backend/app/Http/Resources/Home/CurrentOutletResource.php @@ -0,0 +1,26 @@ + + */ + public function toArray(Request $request): array + { + return [ + 'id' => $this->uuid, + 'name' => $this->name, + 'location' => $this->full_address, + 'latitude' => (float) $this->latitude, + 'longitude' => (float) $this->longitude, + 'is_active' => $this->is_active + ]; + } +} diff --git a/Backend/app/Http/Resources/Home/OtherOutletResource.php b/Backend/app/Http/Resources/Home/OtherOutletResource.php new file mode 100644 index 0000000..74709cb --- /dev/null +++ b/Backend/app/Http/Resources/Home/OtherOutletResource.php @@ -0,0 +1,28 @@ + + */ + public function toArray(Request $request): array + { + return [ + 'id' => $this->uuid, + 'server_banner_url' => $this->server_banner_url, + 'name' => $this->name, + 'phone_number' => $this->phone_number, + 'location' => $this->full_address, + 'latitude' => (float) $this->latitude, + 'longitude' => (float) $this->longitude, + 'is_active' => $this->is_active + ]; + } +} diff --git a/Backend/app/Http/Resources/Home/ProductByOutletResource.php b/Backend/app/Http/Resources/Home/ProductByOutletResource.php new file mode 100644 index 0000000..86eea66 --- /dev/null +++ b/Backend/app/Http/Resources/Home/ProductByOutletResource.php @@ -0,0 +1,28 @@ + + */ + public function toArray(Request $request): array + { + return [ + 'id' => $this->uuid, + 'server_image_url' => Arr::last($this->server_image_url), + 'name' => $this->name, + 'total_sold' => (int) $this->total_sold, + 'average_rating' => round($this->average_rating, 2), + 'selling_price' => optional($this->variants->first()?->inventory)->selling_price, + 'likes' => $this->likes_count ?? 0 + ]; + } +} diff --git a/Backend/app/Http/Resources/Home/ProductCategoryResource.php b/Backend/app/Http/Resources/Home/ProductCategoryResource.php new file mode 100644 index 0000000..0607564 --- /dev/null +++ b/Backend/app/Http/Resources/Home/ProductCategoryResource.php @@ -0,0 +1,22 @@ + + */ + public function toArray(Request $request): array + { + return [ + 'server_image_url' => $this->server_image_url, + '' + ]; + } +} diff --git a/Backend/app/Http/Resources/Order/OrderInfoResource.php b/Backend/app/Http/Resources/Order/OrderInfoResource.php new file mode 100644 index 0000000..900f41d --- /dev/null +++ b/Backend/app/Http/Resources/Order/OrderInfoResource.php @@ -0,0 +1,40 @@ + + */ + public function toArray(Request $request): array + { + return [ + 'id' => $this->uuid, + 'order_number' => $this->order_number, + 'created_at' => $this->created_at->toDateTimeString(), + 'order_status' => $this->order_status, + 'is_cancellation' => $this->is_cancellation, + 'cancellation_accepted' => $this->cancellation_accepted, + 'approval_process' => $this->approval_process, + 'delivery_type' => $this->delivery_type, + 'delivery_preference' => $this->delivery_preference, + 'outlet_name_snapshot' => $this->outlet_name_snapshot, + 'outlet_address_snapshot' => $this->outlet_address_snapshot, + 'customer_address_snapshot' => $this->customer_address_snapshot, + 'order_items' => OrderItemInfoResource::collection($this->whenLoaded('items')), + 'payment_method' => $this->payment_method, + 'payment_proof_url' => $this->payment_proof_url, + 'total_order' => (float) $this->total_order, + 'delivery_fee_type' => $this->delivery_fee_type, + 'delivery_fee' => (float) $this->delivery_fee, + 'total_delivery_fee' => (float) $this->total_delivery_fee, + 'total_amount' => (float) $this->total_amount + ]; + } +} diff --git a/Backend/app/Http/Resources/Order/OrderItemInfoResource.php b/Backend/app/Http/Resources/Order/OrderItemInfoResource.php new file mode 100644 index 0000000..77bc949 --- /dev/null +++ b/Backend/app/Http/Resources/Order/OrderItemInfoResource.php @@ -0,0 +1,26 @@ + + */ + public function toArray(Request $request): array + { + return [ + 'id' => $this->variant->product_id, + 'product_image_url_snapshot' => $this->product_image_url_snapshot, + 'product_name_snapshot' => $this->product_name_snapshot, + 'product_variant_name_snapshot' => $this->product_variant_name_snapshot, + 'selling_price_snapshot' => (float) $this->selling_price_snapshot, + 'quantity' => (int) $this->quantity + ]; + } +} diff --git a/Backend/app/Http/Resources/Order/OrderReportResource.php b/Backend/app/Http/Resources/Order/OrderReportResource.php new file mode 100644 index 0000000..71b8b6c --- /dev/null +++ b/Backend/app/Http/Resources/Order/OrderReportResource.php @@ -0,0 +1,31 @@ + + */ + public function toArray(Request $request): array + { + return [ + 'id' => $this->uuid, + 'product_images' => $this->whenLoaded('items') + ->pluck('product_image_url_snapshot') + ->values(), + 'total_items' => $this->total_items, + 'order_number' => $this->order_number, + 'total_amount' => $this->total_amount, + 'order_status' => $this->order_status, + 'delivery_type' => $this->delivery_type, + 'has_been_assessed' => $this->has_been_assessed, + 'created_at' => $this->created_at->toDateTimeString() + ]; + } +} diff --git a/Backend/app/Http/Resources/Order/OrderSyncResource.php b/Backend/app/Http/Resources/Order/OrderSyncResource.php new file mode 100644 index 0000000..d67c415 --- /dev/null +++ b/Backend/app/Http/Resources/Order/OrderSyncResource.php @@ -0,0 +1,50 @@ + + */ + public function toArray(Request $request): array + { + return [ + 'order' => [ + 'uuid' => $this->uuid, + 'order_number' => $this->order_number, + 'outlet_id' => $this->outlet_id, + 'outlet_name_snapshot' => $this->outlet_name_snapshot, + 'outlet_phone_number_snapshot' => $this->outlet_phone_number_snapshot, + 'outlet_address_snapshot' => $this->outlet_address_snapshot, + 'customer_id' => $this->customer_id, + 'customer_name_snapshot' => $this->customer_name_snapshot, + 'customer_email_snapshot' => $this->customer_email_snapshot, + 'customer_phone_number_snapshot' => $this->customer_phone_number_snapshot, + 'customer_address_snapshot' => $this->customer_address_snapshot, + 'source' => $this->source, + 'already_read' => $this->already_read, + 'order_status' => $this->order_status, + 'delivery_type' => $this->delivery_type, + 'delivery_preference' => $this->delivery_preference, + 'delivery_fee_type' => $this->delivery_fee_type, + 'delivery_fee' => (float) $this->delivery_fee, + 'total_delivery_fee' => (float) $this->total_delivery_fee, + 'total_order' => (float) $this->total_order, + 'total_amount' => (float) $this->total_amount, + 'notes' => $this->notes, + 'payment_proof_url' => $this->payment_proof_url, + 'payment_method' => $this->payment_method, + 'created_at' => $this->created_at->toDateTimeString(), + 'updated_at' => $this->updated_at->toDateTimeString(), + ], + 'items' => $this->whenLoaded('items'), + 'customer' => $this->whenLoaded('customer'), + ]; + } +} diff --git a/Backend/app/Http/Resources/Order/ProductImageResource.php b/Backend/app/Http/Resources/Order/ProductImageResource.php new file mode 100644 index 0000000..6b0c040 --- /dev/null +++ b/Backend/app/Http/Resources/Order/ProductImageResource.php @@ -0,0 +1,19 @@ + + */ + public function toArray(Request $request): array + { + return $this->product_image_url_snapshot; + } +} diff --git a/Backend/app/Http/Resources/OutletResource.php b/Backend/app/Http/Resources/OutletResource.php new file mode 100644 index 0000000..77c9a67 --- /dev/null +++ b/Backend/app/Http/Resources/OutletResource.php @@ -0,0 +1,28 @@ + $this->uuid, + 'name' => $this->name, + 'email' => $this->email, + 'phone_number' => $this->phone_number, + 'full_address' => $this->full_address, + 'latitude' => (float) $this->latitude, + 'longitude' => (float) $this->longitude, + 'server_photo_url' => $this->server_photo_url, + 'server_banner_url' => $this->server_banner_url, + 'is_main_outlet' => (bool) $this->is_main_outlet, + 'is_active' => (bool) $this->is_active, + 'created_at' => $this->created_at?->toIso8601String(), + 'updated_at' => $this->updated_at?->toIso8601String(), + ]; + } +} diff --git a/Backend/app/Http/Resources/Product/ProductInfoResource.php b/Backend/app/Http/Resources/Product/ProductInfoResource.php new file mode 100644 index 0000000..9ecb6d9 --- /dev/null +++ b/Backend/app/Http/Resources/Product/ProductInfoResource.php @@ -0,0 +1,27 @@ + + */ + public function toArray(Request $request): array + { + return [ + 'id' => $this->uuid, + 'server_image_url' => $this->server_image_url, + 'name' => $this->name, + 'has_variant' => $this->has_variant, + 'description' => $this->description, + 'unit' => $this->unit->name, + 'variants' => ProductVariantResource::collection($this->whenLoaded('variants')), + ]; + } +} diff --git a/Backend/app/Http/Resources/Product/ProductReviewResource.php b/Backend/app/Http/Resources/Product/ProductReviewResource.php new file mode 100644 index 0000000..905c302 --- /dev/null +++ b/Backend/app/Http/Resources/Product/ProductReviewResource.php @@ -0,0 +1,24 @@ + + */ + public function toArray(Request $request): array + { + return [ + 'id' => $this->uuid, + 'customer_name' => $this->customer->name, + 'rating_count' => $this->rating, + 'comment' => $this->comment + ]; + } +} diff --git a/Backend/app/Http/Resources/Product/ProductVariantResource.php b/Backend/app/Http/Resources/Product/ProductVariantResource.php new file mode 100644 index 0000000..2ac717a --- /dev/null +++ b/Backend/app/Http/Resources/Product/ProductVariantResource.php @@ -0,0 +1,29 @@ + + */ + public function toArray(Request $request): array + { + return [ + 'id' => $this->uuid, + 'server_image_url' => $this->server_image_url, + 'name' => $this->name, + 'is_product_variant' => $this->is_product_variant, + 'selling_price' => $this->inventory->selling_price, + 'stock_type' => $this->inventory->stock_type, + 'stock' => $this->inventory->stock, + 'total_sold' => $this->total_sold, + 'reviews' => ProductReviewResource::collection($this->whenLoaded('reviews')) + ]; + } +} diff --git a/Backend/app/Http/Resources/StaffResource.php b/Backend/app/Http/Resources/StaffResource.php new file mode 100644 index 0000000..92c7b5c --- /dev/null +++ b/Backend/app/Http/Resources/StaffResource.php @@ -0,0 +1,29 @@ + $this->uuid, + 'outlet_id' => $this->outlet?->uuid, + 'name' => $this->name, + 'email' => $this->email, + 'phone_number' => $this->phone_number, + 'role' => $this->role, + 'place_of_birth' => $this->place_of_birth, + 'date_of_birth' => $this->date_of_birth, + 'shift_name' => $this->shift_name, + 'shift_start_time' => $this->shift_start_time, + 'shift_end_time' => $this->shift_end_time, + 'is_active' => (bool) $this->is_active, + 'created_at' => $this->created_at?->toIso8601String(), + 'updated_at' => $this->updated_at?->toIso8601String(), + ]; + } +} diff --git a/Backend/app/Http/Resources/TenantResource.php b/Backend/app/Http/Resources/TenantResource.php new file mode 100644 index 0000000..9bcefa7 --- /dev/null +++ b/Backend/app/Http/Resources/TenantResource.php @@ -0,0 +1,41 @@ + $this->uuid, + 'business_code' => $this->business_code, + 'business_name' => $this->business_name, + 'business_email' => $this->business_email, + 'business_phone' => $this->business_phone, + 'business_type' => $this->business_type, + 'server_logo_url' => $this->server_logo_url, + 'subdomain' => $this->subdomain, + 'online_store_name' => $this->online_store_name, + 'online_open_time' => $this->online_open_time, + 'online_close_time' => $this->online_close_time, + 'is_close_service' => (bool) $this->is_close_service, + 'preparation_time_minutes' => (int) $this->preparation_time_minutes, + 'order_interval_minutes' => (int) $this->order_interval_minutes, + 'allow_cod' => (bool) $this->allow_cod, + 'max_delivery_radius_km' => (float) $this->max_delivery_radius_km, + 'delivery_fee_type' => $this->delivery_fee_type, + 'delivery_flat_fee' => (float) $this->delivery_flat_fee, + 'delivery_fee_per_km' => (float) $this->delivery_fee_per_km, + 'delivery_base_fee' => (float) $this->delivery_base_fee, + 'review_display_type' => $this->review_display_type, + 'review_display_policy' => $this->review_display_policy, + 'subscription_level' => $this->subscription_level, + 'limit_outlet' => (int) $this->limit_outlet, + 'subscription_expires_at' => $this->subscription_expires_at?->toIso8601String(), + 'created_at' => $this->created_at?->toIso8601String(), + 'updated_at' => $this->updated_at?->toIso8601String(), + ]; + } +} diff --git a/Backend/app/Http/Resources/UserResource.php b/Backend/app/Http/Resources/UserResource.php new file mode 100644 index 0000000..2336342 --- /dev/null +++ b/Backend/app/Http/Resources/UserResource.php @@ -0,0 +1,39 @@ + $this->uuid, + 'tenant' => new TenantResource($this->whenLoaded('tenant')? : $this->tenant), + 'outlet_id' => $this->outlet->uuid, + 'outlets' => $this->role === 'owner' + ? OutletResource::collection($this->tenant->outlets) + : new OutletResource($this->outlet), + 'staff_list' => $this->role === 'owner' + ? StaffResource::collection($this->tenant->users->where('uuid', '!=', $this->uuid)) + : [], + 'name' => $this->name, + 'email' => $this->email, + 'phone_number' => $this->phone_number, + 'role' => $this->role, + 'place_of_birth' => $this->place_of_birth, + 'date_of_birth' => $this->date_of_birth, + 'shift_name' => $this->shift_name, + 'shift_start_time' => $this->shift_start_time, + 'shift_end_time' => $this->shift_end_time, + 'is_active' => (bool) $this->is_active, + 'last_sync' => $this->last_sync?->toIso8601String(), + 'created_at' => $this->created_at?->toIso8601String(), + 'updated_at' => $this->updated_at?->toIso8601String(), + ]; + } +} diff --git a/Backend/app/Models/Campaign.php b/Backend/app/Models/Campaign.php new file mode 100644 index 0000000..baae587 --- /dev/null +++ b/Backend/app/Models/Campaign.php @@ -0,0 +1,54 @@ + 'boolean', + 'created_at' => 'datetime', + 'updated_at' => 'datetime', + ]; + + protected $dates = ['deleted_at']; + + protected function serializeDate(DateTimeInterface $date) + { + return $date->format('Y-m-d H:i:s'); + } + + public function outlet_campaigns() { + return $this->hasMany(OutletCampaign::class, 'campaign_id', 'uuid'); + } + + public function getServerBannerUrlAttribute($value) + { + if (!$value) return null; + + return asset('storage/' . $value); + } +} diff --git a/Backend/app/Models/Category.php b/Backend/app/Models/Category.php new file mode 100644 index 0000000..d9503e5 --- /dev/null +++ b/Backend/app/Models/Category.php @@ -0,0 +1,48 @@ + 'datetime', + 'updated_at' => 'datetime' + ]; + + protected function serializeDate(DateTimeInterface $date) + { + return $date->format('Y-m-d H:i:s'); + } + + public function products() + { + return $this->hasMany(Product::class, 'category_id', 'uuid'); + } + + public function getServerImageUrlAttribute($value) + { + if (!$value) return null; + + return asset('storage/' . $value); + } +} diff --git a/Backend/app/Models/Customer.php b/Backend/app/Models/Customer.php new file mode 100644 index 0000000..f7295cd --- /dev/null +++ b/Backend/app/Models/Customer.php @@ -0,0 +1,51 @@ + 'datetime', + 'updated_at' => 'datetime', + ]; + + protected $dates = ['deleted_at']; + + protected function serializeDate(DateTimeInterface $date) + { + return $date->format('Y-m-d H:i:s'); + } + + public function addresses(): HasMany + { + return $this->hasMany(CustomerAddress::class, 'customer_id', 'uuid'); + } + public function orders(): HasMany + { + return $this->hasMany(Order::class, 'customer_id', 'uuid'); + } +} diff --git a/Backend/app/Models/CustomerAddress.php b/Backend/app/Models/CustomerAddress.php new file mode 100644 index 0000000..3e35217 --- /dev/null +++ b/Backend/app/Models/CustomerAddress.php @@ -0,0 +1,47 @@ + 'datetime', + 'updated_at' => 'datetime', + ]; + + protected $dates = ['deleted_at']; + + protected function serializeDate(DateTimeInterface $date) + { + return $date->format('Y-m-d H:i:s'); + } + + + public function customer(): BelongsTo + { + return $this->belongsTo(Customer::class, 'customer_id', 'uuid'); + } +} diff --git a/Backend/app/Models/Order.php b/Backend/app/Models/Order.php new file mode 100644 index 0000000..f8ae054 --- /dev/null +++ b/Backend/app/Models/Order.php @@ -0,0 +1,97 @@ + 'array', + 'customer_address_snapshot' => 'array', + 'already_read' => 'boolean', + 'is_cancellation' => 'boolean', + 'cancellation_accepted' => 'boolean', + 'has_been_assessed' => 'boolean' + ]; + + protected function serializeDate(DateTimeInterface $date) + { + return $date->format('Y-m-d H:i:s'); + } + + public function items(): HasMany + { + return $this->hasMany(OrderItem::class, 'order_id', 'uuid'); + } + + public function customer(): BelongsTo + { + return $this->belongsTo(Customer::class, 'customer_id', 'uuid'); + } + + /** + * Relasi ke tabel Transactions sebagai polimorfik + */ + public function transaction(): MorphOne + { + return $this->morphOne(Transaction::class, 'reference', 'reference_type', 'reference_id', 'uuid'); + } + + public function getPaymentProofUrlAttribute($value) + { + if (!$value) return null; + + return asset('storage/' . $value); + } +} \ No newline at end of file diff --git a/Backend/app/Models/OrderItem.php b/Backend/app/Models/OrderItem.php new file mode 100644 index 0000000..e762edb --- /dev/null +++ b/Backend/app/Models/OrderItem.php @@ -0,0 +1,56 @@ + 'datetime', + 'updated_at' => 'datetime', + ]; + + protected function serializeDate(DateTimeInterface $date) + { + return $date->format('Y-m-d H:i:s'); + } + + public function order(): BelongsTo + { + return $this->belongsTo(Order::class, 'order_id', 'uuid'); + } + public function variant(): BelongsTo + { + return $this->belongsTo(ProductVariant::class, 'product_variant_id', 'uuid'); + } + + public function getProductImageUrlSnapshotAttribute($value) + { + if (!$value) return null; + + return asset('storage/' . $value); + } +} diff --git a/Backend/app/Models/Outlet.php b/Backend/app/Models/Outlet.php new file mode 100644 index 0000000..a256977 --- /dev/null +++ b/Backend/app/Models/Outlet.php @@ -0,0 +1,71 @@ + 'boolean', + 'is_active' => 'boolean', + 'created_at' => 'datetime', + 'updated_at' => 'datetime', + ]; + + protected $dates = ['deleted_at']; + + protected function serializeDate(DateTimeInterface $date) + { + return $date->format('Y-m-d H:i:s'); + } + + // Outlet ini milik Tenant mana + public function tenant(): BelongsTo + { + return $this->belongsTo(Tenant::class, 'tenant_id', 'uuid'); + } + + // Daftar User/Staf yang terdaftar di Outlet ini + public function users(): HasMany + { + return $this->hasMany(User::class, 'outlet_id', 'uuid'); + } + + public function getServerBannerUrlAttribute($value) + { + if (!$value) return null; + + return asset('storage/' . $value); + } +} diff --git a/Backend/app/Models/OutletCampaign.php b/Backend/app/Models/OutletCampaign.php new file mode 100644 index 0000000..1b0de3a --- /dev/null +++ b/Backend/app/Models/OutletCampaign.php @@ -0,0 +1,44 @@ + 'datetime', + 'updated_at' => 'datetime', + ]; + + protected $dates = ['deleted_at']; + + protected function serializeDate(DateTimeInterface $date) + { + return $date->format('Y-m-d H:i:s'); + } + + public function campaign() { + return $this->belongsTo(Campaign::class, 'campaign_id', 'uuid'); + } + + public function outlet() { + return $this->belongsTo(Outlet::class, 'outlet_id', 'uuid'); + } +} diff --git a/Backend/app/Models/OutletInventory.php b/Backend/app/Models/OutletInventory.php new file mode 100644 index 0000000..1f584ef --- /dev/null +++ b/Backend/app/Models/OutletInventory.php @@ -0,0 +1,55 @@ + 'boolean', + 'created_at' => 'datetime', + 'updated_at' => 'datetime', + ]; + + protected $dates = ['deleted_at']; + + protected function serializeDate(DateTimeInterface $date) + { + return $date->format('Y-m-d H:i:s'); + } + + public function item() + { + return $this->morphTo('item', 'item_type', 'item_id'); + } + + public function stockCards() + { + return $this->hasMany(StockCard::class, 'outlet_inventory_id'); + } +} diff --git a/Backend/app/Models/Product.php b/Backend/app/Models/Product.php new file mode 100644 index 0000000..9a7593b --- /dev/null +++ b/Backend/app/Models/Product.php @@ -0,0 +1,76 @@ + 'boolean', + 'server_image_url' => 'array', + 'created_at' => 'datetime', + 'updated_at' => 'datetime', + ]; + + protected $dates = ['deleted_at']; + + protected function serializeDate(DateTimeInterface $date) + { + return $date->format('Y-m-d H:i:s'); + } + + public function category() + { + return $this->belongsTo(Category::class, 'category_id'); + } + + public function unit() + { + return $this->belongsTo(Unit::class, 'unit_id'); + } + + public function variants() + { + return $this->hasMany(ProductVariant::class, 'product_id'); + } + + public function likes() + { + return $this->hasMany(ProductLike::class, 'product_id'); + } + + public function getServerImageUrlAttribute($value) + { + if (!$value) return null; + + $paths = is_array($value) ? $value : json_decode($value, true); + + return collect($paths)->map(function ($path) { + return asset('storage/' . $path); + })->toArray(); + } +} diff --git a/Backend/app/Models/ProductLike.php b/Backend/app/Models/ProductLike.php new file mode 100644 index 0000000..473a1f4 --- /dev/null +++ b/Backend/app/Models/ProductLike.php @@ -0,0 +1,44 @@ + 'boolean', + 'created_at' => 'datetime', + 'updated_at' => 'datetime', + ]; + + protected function serializeDate(DateTimeInterface $date) + { + return $date->format('Y-m-d H:i:s'); + } + + + public function customer() { + return $this->belongsTo(Customer::class, 'customer_id', 'uuid'); + } + + public function product() { + return $this->belongsTo(Product::class, 'product_id', 'uuid'); + } +} diff --git a/Backend/app/Models/ProductReview.php b/Backend/app/Models/ProductReview.php new file mode 100644 index 0000000..f0bffc0 --- /dev/null +++ b/Backend/app/Models/ProductReview.php @@ -0,0 +1,42 @@ + 'datetime', + 'updated_at' => 'datetime', + ]; + + protected function serializeDate(DateTimeInterface $date) + { + return $date->format('Y-m-d H:i:s'); + } + + public function customer() { + return $this->belongsTo(Customer::class, 'customer_id', 'uuid'); + } + + public function variant() { + return $this->belongsTo(ProductVariant::class, 'product_variant_id', 'uuid'); + } +} diff --git a/Backend/app/Models/ProductVariant.php b/Backend/app/Models/ProductVariant.php new file mode 100644 index 0000000..07932be --- /dev/null +++ b/Backend/app/Models/ProductVariant.php @@ -0,0 +1,70 @@ + 'boolean', + 'is_product_variant' => 'boolean', + 'created_at' => 'datetime', + 'updated_at' => 'datetime', + ]; + + protected $dates = ['deleted_at']; + + protected function serializeDate(DateTimeInterface $date) + { + return $date->format('Y-m-d H:i:s'); + } + + public function product() + { + return $this->belongsTo(Product::class, 'product_id'); + } + + public function inventory() + { + return $this->morphOne(OutletInventory::class, 'item'); + } + + public function reviews() + { + return $this->hasMany(ProductReview::class, 'product_variant_id', 'uuid'); + } + + public function getServerImageUrlAttribute($value) + { + if (!$value) return null; + + return asset('storage/' . $value); + } +} diff --git a/Backend/app/Models/Purchase.php b/Backend/app/Models/Purchase.php new file mode 100644 index 0000000..8c98546 --- /dev/null +++ b/Backend/app/Models/Purchase.php @@ -0,0 +1,64 @@ + 'datetime', + 'updated_at' => 'datetime', + ]; + + protected function serializeDate(DateTimeInterface $date) + { + return $date->format('Y-m-d H:i:s'); + } + + public function items() { + return $this->hasMany(PurchaseItem::class, 'purchase_id', 'uuid'); + } + + public function supplier() { + return $this->belongsTo(Supplier::class, 'supplier_id', 'uuid'); + } + + public function transaction(): MorphOne { + return $this->morphOne(Transaction::class, 'reference', 'reference_type', 'reference_id', 'uuid'); + } +} diff --git a/Backend/app/Models/PurchaseItem.php b/Backend/app/Models/PurchaseItem.php new file mode 100644 index 0000000..0c0640f --- /dev/null +++ b/Backend/app/Models/PurchaseItem.php @@ -0,0 +1,48 @@ + 'datetime', + 'updated_at' => 'datetime', + ]; + + protected function serializeDate(DateTimeInterface $date) + { + return $date->format('Y-m-d H:i:s'); + } + + public function purchase() { + return $this->belongsTo(Purchase::class, 'purchase_id', 'uuid'); + } + + // Relasi dinamis tergantung item_type (Product/RawMaterial) + public function item() { + return $this->morphTo(null, 'item_type', 'item_id', 'uuid'); + } +} diff --git a/Backend/app/Models/RawMaterial.php b/Backend/app/Models/RawMaterial.php new file mode 100644 index 0000000..dfd0d92 --- /dev/null +++ b/Backend/app/Models/RawMaterial.php @@ -0,0 +1,38 @@ +format('Y-m-d H:i:s'); + } + + protected $casts = [ + 'created_at' => 'datetime', + 'updated_at' => 'datetime', + ]; + + protected $dates = ['deleted_at']; +} diff --git a/Backend/app/Models/ReceiptItem.php b/Backend/app/Models/ReceiptItem.php new file mode 100644 index 0000000..1f6cb0b --- /dev/null +++ b/Backend/app/Models/ReceiptItem.php @@ -0,0 +1,40 @@ + 'datetime', + 'updated_at' => 'datetime', + ]; + + protected $dates = ['deleted_at']; + + protected function serializeDate(DateTimeInterface $date) + { + return $date->format('Y-m-d H:i:s'); + } + +} diff --git a/Backend/app/Models/StockCard.php b/Backend/app/Models/StockCard.php new file mode 100644 index 0000000..a4e368f --- /dev/null +++ b/Backend/app/Models/StockCard.php @@ -0,0 +1,48 @@ + 'boolean', + 'created_at' => 'datetime' + ]; + + protected function serializeDate(DateTimeInterface $date) + { + return $date->format('Y-m-d H:i:s'); + } +} diff --git a/Backend/app/Models/Supplier.php b/Backend/app/Models/Supplier.php new file mode 100644 index 0000000..28b5265 --- /dev/null +++ b/Backend/app/Models/Supplier.php @@ -0,0 +1,40 @@ + 'datetime', + 'updated_at' => 'datetime', + ]; + + protected $dates = ['deleted_at']; + + protected function serializeDate(DateTimeInterface $date) + { + return $date->format('Y-m-d H:i:s'); + } + + public function purchases() { + return $this->hasMany(Purchase::class, 'supplier_id', 'uuid'); + } +} diff --git a/Backend/app/Models/SyncDelete.php b/Backend/app/Models/SyncDelete.php new file mode 100644 index 0000000..58d9f89 --- /dev/null +++ b/Backend/app/Models/SyncDelete.php @@ -0,0 +1,27 @@ +format('Y-m-d H:i:s'); + } +} diff --git a/Backend/app/Models/Tenant.php b/Backend/app/Models/Tenant.php new file mode 100644 index 0000000..0b19983 --- /dev/null +++ b/Backend/app/Models/Tenant.php @@ -0,0 +1,80 @@ + 'boolean', + 'allow_cod' => 'boolean', + 'created_at' => 'datetime', + 'updated_at' => 'datetime', + ]; + + protected function serializeDate(DateTimeInterface $date) + { + return $date->format('Y-m-d H:i:s'); + } + + public function users(): HasMany + { + return $this->hasMany(User::class, 'tenant_id', 'uuid'); + } + + public function outlets(): HasMany + { + return $this->hasMany(Outlet::class, 'tenant_id', 'uuid'); + } + + public function payment_methods(): HasMany + { + return $this->hasMany(TenantPaymentMethod::class, 'tenant_id', 'uuid'); + } + + public function getServerLogoUrlAttribute($value) + { + if (!$value) return null; + + return asset('storage/' . $value); + } +} diff --git a/Backend/app/Models/TenantPaymentMethod.php b/Backend/app/Models/TenantPaymentMethod.php new file mode 100644 index 0000000..1f6806a --- /dev/null +++ b/Backend/app/Models/TenantPaymentMethod.php @@ -0,0 +1,37 @@ + 'datetime', + 'updated_at' => 'datetime', + ]; + + protected function serializeDate(DateTimeInterface $date) + { + return $date->format('Y-m-d H:i:s'); + } + + public function tenant() { + return $this->belongsTo(Tenant::class, 'tenant_id', 'uuid'); + } +} diff --git a/Backend/app/Models/Transaction.php b/Backend/app/Models/Transaction.php new file mode 100644 index 0000000..348605f --- /dev/null +++ b/Backend/app/Models/Transaction.php @@ -0,0 +1,64 @@ + 'datetime', + 'updated_at' => 'datetime', + ]; + + protected function serializeDate(DateTimeInterface $date) + { + return $date->format('Y-m-d H:i:s'); + } + + /** + * Relasi Polimorfik ke Orders atau Purchases + */ + public function reference(): MorphTo + { + return $this->morphTo('reference', 'reference_type', 'reference_id', 'uuid'); + } + + public function tenant(): BelongsTo + { + return $this->belongsTo(Tenant::class, 'tenant_id', 'uuid'); + } + public function outlet(): BelongsTo + { + return $this->belongsTo(Outlet::class, 'outlet_id', 'uuid'); + } + public function user(): BelongsTo + { + return $this->belongsTo(User::class, 'user_id', 'uuid'); + } +} diff --git a/Backend/app/Models/Unit.php b/Backend/app/Models/Unit.php new file mode 100644 index 0000000..4ab4825 --- /dev/null +++ b/Backend/app/Models/Unit.php @@ -0,0 +1,34 @@ + 'datetime', + 'updated_at' => 'datetime', + ]; + + protected function serializeDate(DateTimeInterface $date) + { + return $date->format('Y-m-d H:i:s'); + } +} diff --git a/Backend/app/Models/User.php b/Backend/app/Models/User.php new file mode 100644 index 0000000..983ff15 --- /dev/null +++ b/Backend/app/Models/User.php @@ -0,0 +1,71 @@ + 'boolean', + 'last_sync' => 'datetime', + 'created_at' => 'datetime', + 'updated_at' => 'datetime', + ]; + + protected $dates = ['deleted_at']; + + protected function serializeDate(DateTimeInterface $date) + { + return $date->format('Y-m-d H:i:s'); + } + + /** + * Relasi ke Tenant (Satu User memiliki satu Tenant) + */ + public function tenant(): BelongsTo + { + return $this->belongsTo(Tenant::class, 'tenant_id', 'uuid'); + } + + /** + * Relasi ke Outlet (Satu User terikat ke satu Outlet, opsional untuk Owner) + */ + public function outlet(): BelongsTo + { + return $this->belongsTo(Outlet::class, 'outlet_id', 'uuid'); + } +} diff --git a/Backend/app/Observers/SyncDeleteObserver.php b/Backend/app/Observers/SyncDeleteObserver.php new file mode 100644 index 0000000..4fa344f --- /dev/null +++ b/Backend/app/Observers/SyncDeleteObserver.php @@ -0,0 +1,17 @@ + $model->getTable(), + 'record_uuid' => $model->uuid, + 'created_at' => $model->deleted_at + ]); + } +} diff --git a/Backend/app/Providers/AppServiceProvider.php b/Backend/app/Providers/AppServiceProvider.php new file mode 100644 index 0000000..2cfb7cc --- /dev/null +++ b/Backend/app/Providers/AppServiceProvider.php @@ -0,0 +1,62 @@ + ProductVariant::class, + 'raw_material' => RawMaterial::class, + ]); + + $models = [ + Campaign::class, + Outlet::class, + OutletCampaign::class, + User::class, + Product::class, + ProductVariant::class, + RawMaterial::class, + ReceiptItem::class, + OutletInventory::class, + Customer::class, + CustomerAddress::class, + Supplier::class + ]; + + foreach ($models as $model) { + $model::observe(SyncDeleteObserver::class); + } + } +} diff --git a/Backend/app/Providers/TelescopeServiceProvider.php b/Backend/app/Providers/TelescopeServiceProvider.php new file mode 100644 index 0000000..4de2a6b --- /dev/null +++ b/Backend/app/Providers/TelescopeServiceProvider.php @@ -0,0 +1,65 @@ +hideSensitiveRequestDetails(); + + $isLocal = $this->app->environment('local'); + + Telescope::filter(function (IncomingEntry $entry) use ($isLocal) { + return $isLocal || + $entry->isReportableException() || + $entry->isFailedRequest() || + $entry->isFailedJob() || + $entry->isScheduledTask() || + $entry->hasMonitoredTag(); + }); + } + + /** + * Prevent sensitive request details from being logged by Telescope. + */ + protected function hideSensitiveRequestDetails(): void + { + if ($this->app->environment('local')) { + return; + } + + Telescope::hideRequestParameters(['_token']); + + Telescope::hideRequestHeaders([ + 'cookie', + 'x-csrf-token', + 'x-xsrf-token', + ]); + } + + /** + * Register the Telescope gate. + * + * This gate determines who can access Telescope in non-local environments. + */ + protected function gate(): void + { + Gate::define('viewTelescope', function (User $user) { + return in_array($user->email, [ + // + ]); + }); + } +} diff --git a/Backend/app/Services/AuthService.php b/Backend/app/Services/AuthService.php new file mode 100644 index 0000000..ef19c61 --- /dev/null +++ b/Backend/app/Services/AuthService.php @@ -0,0 +1,108 @@ + 'lockout', + 'seconds' => $seconds, + 'until' => now()->addSeconds($seconds)->toIso8601String(), + ]; + } + + $query = User::withoutGlobalScopes(); + + if ($businessCode) { + $tenant = Tenant::where('business_code', '=', $businessCode)->first(); + + if(!$tenant) { + return ['error' => 'invalid_business_code']; + } + + $query->where('tenant_id', '=', $tenant->uuid); + } + + $loginField = filter_var($identifier, FILTER_VALIDATE_EMAIL) ? 'email' : 'phone_number'; + + $user = $query->where($loginField, $identifier)->first(); + + $storedHash = $user->password; + + if (str_starts_with($storedHash, '$2a$')) { + $storedHash = str_replace('$2a$', '$2y$', $storedHash); + } + + if (!$user || !Hash::check($data['password'], $storedHash)) { + RateLimiter::hit($throttleKey, 3600); + return ['error' => 'invalid_credentials']; + } + + if ($user->role === 'owner') { + $user->load(['tenant.outlets', 'tenant.users']); + } else { + $user->load(['tenant', 'outlet']); + } + + RateLimiter::clear($throttleKey); + $user->tokens()->delete(); // logout dari device lain + + return [ + 'token' => $user->createToken('UserAuthToken')->plainTextToken, + 'user' => $user, + ]; + } + + public function register(array $data) + { + return DB::transaction(function () use ($data) { + $tenant = Tenant::create([ + 'uuid' => (string) Str::uuid7(), + 'business_name' => $data['business_name'], + 'business_code' => $data['business_code'], + 'subscription_level' => 'standart', + ]); + + $outlet = Outlet::create([ + 'uuid' => (string) Str::uuid7(), + 'tenant_id' => $tenant->uuid, + 'name' => $data['business_name'], + 'is_main_outlet' => true, + ]); + + $user = User::create([ + 'uuid' => (string) Str::uuid7(), + 'tenant_id' => $tenant->uuid, + 'outlet_id' => $outlet->uuid, + 'name' => $data['name'], + 'phone_number' => $data['phone_number'], + 'email' => $data['email'], + 'password' => Hash::make($data['password']), + 'role' => 'owner', + 'is_active' => true, + 'last_sync' => Carbon::now() + ]); + + return [ + 'business_code' => $tenant->business_code, + 'user' => $user, + ]; + }); + } +} \ No newline at end of file diff --git a/Backend/app/Services/CheckoutService.php b/Backend/app/Services/CheckoutService.php new file mode 100644 index 0000000..93d0d49 --- /dev/null +++ b/Backend/app/Services/CheckoutService.php @@ -0,0 +1,109 @@ +format('ymd'); // Format: 260406 + + $chars = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789'; + $randomPart = ''; + for ($i = 0; $i < 3; $i++) { + $randomPart .= $chars[rand(0, strlen($chars) - 1)]; + } + + $prefix = "INVC-$businessCode-$dateCode-"; + + $lastOrder = Order::where('order_number', 'like', $prefix . '%') + ->orderBy('order_number', 'desc') + ->first(); + + $nextSequence = 1; + + if ($lastOrder) { + $parts = explode('-', $lastOrder->order_number); + $lastPart = end($parts); + + if (strlen($lastPart) >= 5) { + $lastSeqStr = substr($lastPart, -5); + $nextSequence = (int)$lastSeqStr + 1; + } + } + + $finalSequence = $randomPart . str_pad($nextSequence, 5, '0', STR_PAD_LEFT); + + return $prefix . $finalSequence; + } + + public function execute(array $data, $file) + { + return DB::transaction(function () use ($data, $file) { + $tenantId = config('app.current_tenant_id'); + + $businessCode = config('app.current_tenant_code'); + + $paymentProof = null; + if ($file) { + $filename = (string) Carbon::now()->getPreciseTimestamp(3). '.' . $file->getClientOriginalExtension(); + $paymentProof = $file->storeAs("{$businessCode}/payment_proof_orders", $filename, 'public'); + } + + $order = Order::create([ + 'uuid' => Str::uuid7(), + 'tenant_id' => $tenantId, + 'order_number' => $this->generateOrderNumber(), + 'outlet_id' => $data['outlet_id'], + 'outlet_name_snapshot' => $data['outlet_name_snapshot'], + 'outlet_phone_number_snapshot' => $data['outlet_phone_number_snapshot'], + 'outlet_address_snapshot' => $data['outlet_address_snapshot'], + 'customer_id' => $data['customer_id'], + 'customer_name_snapshot' => $data['customer_name_snapshot'], + 'customer_email_snapshot' => $data['customer_email_snapshot'] ?? null, + 'customer_phone_number_snapshot' => $data['customer_phone_number_snapshot'], + 'customer_address_snapshot' => $data['customer_address_snapshot'] ?? null, + 'source' => 'online', + 'already_read' => false, + 'order_status' => $data['order_status'], + 'delivery_type' => $data['delivery_type'], + 'delivery_preference' => $data['delivery_preference'], + 'delivery_fee_type' => $data['delivery_fee_type'], + 'delivery_fee' => $data['delivery_fee'], + 'total_delivery_fee' => $data['total_delivery_fee'], + 'total_order' => $data['total_order'], + 'total_amount' => $data['total_amount'], + 'notes' => $data['notes'], + 'payment_proof_url' => $paymentProof ? "{$businessCode}/payment_proof_orders/{$filename}" : null, + 'payment_method' => $data['payment_method'], + ]); + + foreach ($data['items'] as $item) { + OrderItem::create([ + 'uuid' => Str::uuid7(), + 'tenant_id' => $tenantId, + 'order_id' => $order->uuid, + 'product_variant_id' => $item['product_variant_id'], + 'quantity' => $item['quantity'], + 'product_image_url_snapshot' => $item['product_image_url_snapshot'] ?? null, + 'product_name_snapshot' => $item['product_name_snapshot'], + 'product_variant_name_snapshot' => $item['product_variant_name_snapshot'] ?? null, + 'selling_price_snapshot' => $item['selling_price_snapshot'], + 'subtotal' => $item['quantity'] * $item['selling_price_snapshot'], + ]); + } + + return $order; + }); + } +} \ No newline at end of file diff --git a/Backend/app/Services/ConfigurationService.php b/Backend/app/Services/ConfigurationService.php new file mode 100644 index 0000000..d444e15 --- /dev/null +++ b/Backend/app/Services/ConfigurationService.php @@ -0,0 +1,23 @@ + function($q) { + $q->whereNotNull('source_name') + ->whereNotNull('account_number') + ->whereNotNull('receipent_name'); + }]) + ->where('uuid', $tenantId) + ->first(); + + return $tenant; + } +} \ No newline at end of file diff --git a/Backend/app/Services/CustomerService.php b/Backend/app/Services/CustomerService.php new file mode 100644 index 0000000..c509e34 --- /dev/null +++ b/Backend/app/Services/CustomerService.php @@ -0,0 +1,149 @@ + 'lockout', + 'seconds' => $seconds, + 'until' => now()->addSeconds($seconds)->toIso8601String(), + ]; + } + + + $loginField = filter_var($identifier, FILTER_VALIDATE_EMAIL) ? 'email' : 'phone_number'; + + $customer = Customer::where($loginField, $identifier)->first(); + + $storedHash = $customer->password; + + if (str_starts_with($storedHash, '$2a$')) { + $storedHash = str_replace('$2a$', '$2y$', $storedHash); + } + + if (!$customer || !Hash::check($data['password'], $storedHash)) { + RateLimiter::hit($throttleKey, 3600); + return ['error' => 'invalid_credentials']; + } + + RateLimiter::clear($throttleKey); + $customer->tokens()->delete(); + + return [ + 'token' => $customer->createToken('CustomerAuthToken')->plainTextToken, + 'customer' => $customer, + ]; + } + + public function register($data) + { + $tenantId = config('app.current_tenant_id'); + + $outletId = Outlet::where('tenant_id', $tenantId) + ->where('is_main_outlet', true) + ->first() + ->value('uuid'); + + Customer::create([ + 'uuid' => (string) Str::uuid7(), + 'tenant_id' => $tenantId, + 'outlet_id' => $outletId, + 'name' => $data['name'], + 'email' => $data['email'], + 'phone_number' => $data['phone_number'], + 'password' => Hash::make($data['password']), + 'source' => 'online' + ]); + } + + public function updateCustomer($data, $customerId) + { + $tenantId = config('app.current_tenant_id'); + + $customer = Customer::where('uuid', $customerId) + ->where('tenant_id', $tenantId) + ->first(); + + $customer->update([ + 'name' => $data['name'], + ]); + + return [ + 'name' => $customer->name, + 'email' => $customer->email, + 'phone_number' => $customer->phone_number, + ]; + } + + public function updatePassword($data, $customerId) + { + $tenantId = config('app.current_tenant_id'); + + $customer = Customer::where('uuid', $customerId) + ->where('tenant_id', $tenantId) + ->first(); + + if(!Hash::check($data['old_password'], $customer->password)) { + return ['error' => 'invalid old password']; + } + + $customer->update([ + 'password' => Hash::make($data['new_password']) + ]); + } + + public function getCustomerAddress($customerId) + { + $tenantId = config('app.current_tenant_id'); + + $customerAddress = CustomerAddress::select([ + 'uuid', + 'label', + 'full_address', + 'latitude', + 'longitude' + ]) + ->where('customer_id', $customerId) + ->where('tenant_id', $tenantId) + ->get(); + + return $customerAddress; + } + + public function saveCustomerAddress($data, $customerId) + { + $tenantId = config('app.current_tenant_id'); + + CustomerAddress::updateOrCreate([ + 'uuid' => $data['uuid'], + 'customer_id' => $customerId, + 'tenant_id' => $tenantId + ], [ + 'label' => $data['label'], + 'full_address' => $data['full_address'], + 'latitude' => $data['latitude'], + 'longitude' => $data['longitude'] + ]); + } + + public function deleteCustomerAddress(string $customerId, string $id) + { + CustomerAddress::where('uuid', $id) + ->where('customer_id', $customerId) + ->delete(); + } +} \ No newline at end of file diff --git a/Backend/app/Services/HomeService.php b/Backend/app/Services/HomeService.php new file mode 100644 index 0000000..39f42bf --- /dev/null +++ b/Backend/app/Services/HomeService.php @@ -0,0 +1,221 @@ +select( + 'online_open_time', + 'online_close_time', + 'is_close_service' + ) + ->where('uuid', $tenantId) + ->first(); + + $currentOutlet = Outlet::select([ + 'uuid', + 'name', + 'full_address', + 'latitude', + 'longitude', + 'is_active' + ]) + ->where('uuid', $outletId) + ->first(); + + if(!$currentOutlet) { + $currentOutlet = Outlet::select([ + 'uuid', + 'name', + 'full_address', + 'latitude', + 'longitude', + 'is_active' + ]) + ->where('is_main_outlet', true) + ->first(); + } + + $campaigns = Campaign::select([ + 'campaigns.uuid', + 'campaigns.action_type', + 'campaigns.action_reference_id', + 'campaigns.server_banner_url' + + ]) + ->with('outlet_campaigns') + ->whereHas('outlet_campaigns', function($q) use ($currentOutlet) { + $q->select('outlet_campaigns.uuid') + ->where('outlet_campaigns.outlet_id', $currentOutlet->uuid); + }) + ->where('campaigns.is_active', true) + ->where(function ($q) { + $q->where(function ($q2) { + $q2->where('campaigns.start_date', '<=', Carbon::now()->startOfDay()) + ->where('campaigns.end_date', '>=', Carbon::now()->startOfDay()); + })->orWhere(function ($q2) { + $q2->whereNull('campaigns.start_date') + ->whereNull('campaigns.end_date'); + }); + }) + ->get(); + + $categories = Category::select([ + 'uuid', + 'name', + 'server_image_url' + + ]) + ->get(); + + $otherOutlets = Outlet::select([ + 'uuid', + 'name', + 'phone_number', + 'full_address', + 'latitude', + 'longitude', + 'server_banner_url', + 'is_active' + ]) + ->whereNot('uuid', $currentOutlet->uuid) + ->get(); + + $productRecommendations = Product::select([ + 'products.uuid', + 'products.name', + 'products.server_image_url' + ]) + ->whereHas('variants', function($q) use ($currentOutlet) { + $q->select('product_variants.is_visible_online') + ->whereHas('inventory', function($q2) use ($currentOutlet) { + $q2->where('outlet_inventories.outlet_id', $currentOutlet->uuid); + }) + ->where('product_variants.is_visible_online', true); + }) + ->withSum(['variants as total_sold'], 'total_sold') + ->withSum(['variants as total_rating'], 'total_rating') + ->selectRaw(' + (SELECT SUM(average_rating * total_rating) + FROM product_variants + WHERE product_variants.product_id = products.uuid + ) / + NULLIF((SELECT SUM(total_rating) + FROM product_variants + WHERE product_variants.product_id = products.uuid + ), 0) as average_rating + ') + ->with([ + 'variants.inventory' => function ($q) use ($currentOutlet) { + $q->select('outlet_inventories.item_id', 'selling_price') + ->where('outlet_id', $currentOutlet->uuid); + } + ]) + ->orderByDesc('total_sold') + ->orderByDesc('average_rating') + ->withCount('likes') + ->take(5) + ->get(); + + $products = Product::select([ + 'products.uuid', + 'products.name', + 'products.server_image_url' + ]) + ->whereHas('variants', function($q) use ($currentOutlet) { + $q->select('product_variants.is_visible_online') + ->whereHas('inventory', function($q2) use ($currentOutlet) { + $q2->where('outlet_inventories.outlet_id', $currentOutlet->uuid); + }) + ->where('product_variants.is_visible_online', true); + }) + ->withSum(['variants as total_sold'], 'total_sold') + ->withSum(['variants as total_rating'], 'total_rating') + ->selectRaw(' + (SELECT SUM(average_rating * total_rating) + FROM product_variants + WHERE product_variants.product_id = products.uuid + ) / + NULLIF((SELECT SUM(total_rating) + FROM product_variants + WHERE product_variants.product_id = products.uuid + ), 0) as average_rating + ') + ->with([ + 'variants.inventory' => function ($q) use ($currentOutlet) { + $q->select('outlet_inventories.item_id', 'selling_price') + ->where('outlet_id', $currentOutlet->uuid); + } + ]) + ->orderByDesc('total_sold') + ->orderByDesc('average_rating') + ->orderByDesc('created_at') + ->withCount('likes') + ->get(); + + return [ + 'operational_service' => $tenant, + 'current_outlet' => new CurrentOutletResource($currentOutlet), + 'campaign_by_outlets' => CampaignByOutletResource::collection($campaigns), + 'all_categories' => AllCategoriesResource::collection($categories), + 'other_outlets' => OtherOutletResource::collection($otherOutlets), + 'product_recommendations' => ProductByOutletResource::collection($productRecommendations), + 'all_products' => ProductByOutletResource::collection($products) + ]; + } + + public function getOutlets() + { + $outlets = Outlet::select([ + 'uuid', + 'name', + 'phone_number', + 'full_address', + 'latitude', + 'longitude', + 'server_banner_url', + 'is_active' + ]) + ->whereNotNull('full_address') + ->whereNotNull('latitude') + ->whereNotNull('longitude') + ->get(); + + return OtherOutletResource::collection($outlets); + } + + public function getMainOutlet() + { + return Outlet::select([ + 'uuid', + 'name', + 'phone_number', + 'full_address', + 'latitude', + 'longitude', + ]) + ->where('is_main_outlet', true) + ->first(); + } +} \ No newline at end of file diff --git a/Backend/app/Services/OrderService.php b/Backend/app/Services/OrderService.php new file mode 100644 index 0000000..2ea3c8d --- /dev/null +++ b/Backend/app/Services/OrderService.php @@ -0,0 +1,152 @@ +where('uuid', $orderId) + ->first(); + + return new OrderSyncResource($order); + } + + public function getOrders($customerId) + { + $tenantId = config('app.current_tenant_id'); + + $orders = Order::select([ + 'orders.uuid', + 'orders.order_number', + 'orders.total_amount', + 'orders.order_status', + 'orders.delivery_type', + 'orders.has_been_assessed', + 'orders.created_at' + ]) + ->with(['items' => function ($q) { + $q->select( + 'order_items.order_id', + 'order_items.product_image_url_snapshot' + ) + ->take(2); + }]) + ->withCount(['items as total_items'], 'total_items') + ->orderByDesc('orders.created_at') + ->where('orders.tenant_id', $tenantId) + ->where('orders.customer_id', $customerId) + ->where('orders.source', 'online') + ->get(); + + return OrderReportResource::collection($orders); + } + + public function getOrderById($customerId, $orderId) + { + $tenantId = config('app.current_tenant_id'); + + $orders = Order::select([ + 'orders.uuid', + 'orders.order_number', + 'orders.created_at', + 'orders.order_status', + 'orders.is_cancellation', + 'orders.cancellation_accepted', + 'orders.approval_process', + 'orders.delivery_type', + 'orders.delivery_preference', + 'orders.outlet_name_snapshot', + 'orders.outlet_address_snapshot', + 'orders.customer_address_snapshot', + 'orders.payment_method', + 'orders.payment_proof_url', + 'orders.total_order', + 'orders.delivery_fee_type', + 'orders.delivery_fee', + 'orders.total_delivery_fee', + 'orders.total_amount' + ]) + ->with(['items' => function ($q1) { + $q1->select( + 'order_items.order_id' , + 'order_items.product_variant_id', + 'order_items.product_image_url_snapshot', + 'order_items.product_name_snapshot', + 'order_items.product_variant_name_snapshot', + 'order_items.selling_price_snapshot', + 'order_items.quantity' + ) + ->with(['variant' => function($q2) { + $q2->select( + 'product_variants.uuid', + 'product_variants.product_id as product_id' + ); + }]); + }]) + ->where('orders.uuid', $orderId) + ->where('orders.tenant_id', $tenantId) + ->where('orders.customer_id', $customerId) + ->limit(1) + ->first(); + + return new OrderInfoResource($orders); + } + + public function cancelOrder($customerId, $orderId) + { + $tenantId = config('app.current_tenant_id'); + + $order = Order::select([ + 'orders.uuid', + 'orders.outlet_id', + 'orders.updated_at' + ]) + ->where('uuid', $orderId) + ->where('tenant_id', $tenantId) + ->where('customer_id', $customerId) + ->first(); + + $order->update([ + 'already_read' => false, + 'order_status' => 'canceled', + 'canceled_by' => 'customer', + ]); + + return $order; + } + + public function cancellationRequest($customerId, $orderId) + { + $tenantId = config('app.current_tenant_id'); + + $order = Order::select([ + 'orders.uuid', + 'orders.outlet_id', + 'orders.updated_at' + ]) + ->where('uuid', $orderId) + ->where('tenant_id', $tenantId) + ->where('customer_id', $customerId) + ->first(); + + $order->update([ + 'already_read' => false, + 'is_cancellation' => true, + 'approval_process' => 'waiting', + ]); + + return $order; + } +} \ No newline at end of file diff --git a/Backend/app/Services/ProductService.php b/Backend/app/Services/ProductService.php new file mode 100644 index 0000000..4d46351 --- /dev/null +++ b/Backend/app/Services/ProductService.php @@ -0,0 +1,195 @@ +select('review_display_type', 'review_display_policy') + ->where('uuid', $tenantId) + ->first(); + + $product = Product::select([ + 'products.uuid', + 'products.server_image_url', + 'products.name', + 'products.has_variant', + 'products.description', + 'products.unit_id' + ]) + ->with(['unit' => function($q) { + $q->select('units.uuid', 'units.name'); + },'variants' => function($q) { + $q->select( + 'product_variants.uuid', + 'product_variants.product_id', + 'product_variants.server_image_url', + 'product_variants.total_sold', + 'product_variants.name', + 'product_variants.is_product_variant' + ) + ->where('product_variants.is_visible_online', true); + },'variants.inventory' => function($q) { + $q->select( + 'outlet_inventories.item_id', + 'outlet_inventories.selling_price', + 'outlet_inventories.stock_type', + 'outlet_inventories.stock' + ); + },'variants.reviews' => function($q) { + $q->select( + 'product_reviews.uuid', + 'product_reviews.customer_id', + 'product_reviews.product_variant_id', + 'product_reviews.rating', + 'product_reviews.comment' + ) + ->with(['customer' => function($q2) { + $q2->select('customers.uuid','customers.name'); + }]); + }]) + ->where('uuid', $uuid) + ->first(); + + return [ + 'review_display_type' => $tenant->review_display_type, + 'review_display_policy' => $tenant->review_display_policy, + 'product_info' => new ProductInfoResource($product) + ]; + } + + public function getProductByName($currentOutletId, $name) + { + $products = Product::select([ + 'products.uuid', + 'products.name', + 'products.server_image_url' + ]) + ->whereHas('variants', function($q) use ($currentOutletId) { + $q->select('product_variants.is_visible_online') + ->whereHas('inventory', function($q2) use ($currentOutletId) { + $q2->where('outlet_inventories.outlet_id', $currentOutletId); + }) + ->where('product_variants.is_visible_online', true); + }) + ->withSum(['variants as total_sold'], 'total_sold') + ->withAvg(['variants as average_rating'], 'average_rating') + ->with([ + 'variants.inventory' => function ($q) use ($currentOutletId) { + $q->select('outlet_inventories.item_id', 'selling_price') + ->where('outlet_id', $currentOutletId); + } + ]) + ->where('products.name', 'like', '%'.$name.'%') + ->orderByDesc('total_sold') + ->withSum(['variants as total_rating'], 'total_rating') + ->selectRaw(' + (SELECT SUM(average_rating * total_rating) + FROM product_variants + WHERE product_variants.product_id = products.uuid + ) / + NULLIF((SELECT SUM(total_rating) + FROM product_variants + WHERE product_variants.product_id = products.uuid + ), 0) as average_rating + ') + ->withCount('likes') + ->get(); + + return ProductByOutletResource::collection($products); + } + + public function getProductByCategory($currentOutletId, $cateogryId) + { + $products = Product::select([ + 'products.uuid', + 'products.name', + 'products.server_image_url' + ]) + ->whereHas('variants', function($q) use ($currentOutletId) { + $q->select('product_variants.is_visible_online') + ->whereHas('inventory', function($q2) use ($currentOutletId) { + $q2->where('outlet_inventories.outlet_id', $currentOutletId); + }) + ->where('product_variants.is_visible_online', true); + }) + ->withSum(['variants as total_sold'], 'total_sold') + ->withSum(['variants as total_rating'], 'total_rating') + ->selectRaw(' + (SELECT SUM(average_rating * total_rating) + FROM product_variants + WHERE product_variants.product_id = products.uuid + ) / + NULLIF((SELECT SUM(total_rating) + FROM product_variants + WHERE product_variants.product_id = products.uuid + ), 0) as average_rating + ') + ->with([ + 'variants.inventory' => function ($q) use ($currentOutletId) { + $q->select('outlet_inventories.item_id', 'selling_price') + ->where('outlet_id', $currentOutletId); + } + ]) + ->where('products.category_id', $cateogryId) + ->orderByDesc('total_sold') + ->orderByDesc('average_rating') + ->withCount('likes') + ->get(); + + return ProductByOutletResource::collection($products); + } + + public function getProductBestSellerByOutlet($currentOutletId) + { + $products = Product::select([ + 'products.uuid', + 'products.name', + 'products.server_image_url' + ]) + ->whereHas('variants', function($q) use ($currentOutletId) { + $q->select('product_variants.is_visible_online') + ->whereHas('inventory', function($q2) use ($currentOutletId) { + $q2->where('outlet_inventories.outlet_id', $currentOutletId); + }) + ->where('product_variants.is_visible_online', true); + }) + ->withSum(['variants as total_sold'], 'total_sold') + ->withSum(['variants as total_rating'], 'total_rating') + ->selectRaw(' + (SELECT SUM(average_rating * total_rating) + FROM product_variants + WHERE product_variants.product_id = products.uuid + ) / + NULLIF((SELECT SUM(total_rating) + FROM product_variants + WHERE product_variants.product_id = products.uuid + ), 0) as average_rating + ') + ->with([ + 'variants.inventory' => function ($q) use ($currentOutletId) { + $q->select('outlet_inventories.item_id', 'selling_price') + ->where('outlet_id', $currentOutletId); + } + ]) + ->orderByDesc('total_sold') + ->orderByDesc('average_rating') + ->withCount('likes') + ->take(5) + ->get(); + + return ProductByOutletResource::collection($products); + } + +} \ No newline at end of file diff --git a/Backend/app/Services/ReviewService.php b/Backend/app/Services/ReviewService.php new file mode 100644 index 0000000..7fd89cb --- /dev/null +++ b/Backend/app/Services/ReviewService.php @@ -0,0 +1,67 @@ +where('order_id', $orderId) + ->get(); + + return $products; + } + + public function reviewProduct($data, $orderId) + { + $tenantId = config('app.current_tenant_id'); + DB::transaction(function () use ($data, $orderId, $tenantId) { + foreach ($data as $item) { + ProductReview::create([ + 'uuid' => (string) Str::uuid7(), + 'tenant_id' => $tenantId, + 'customer_id' => $item['customer_id'], + 'product_variant_id' => $item['product_variant_id'], + 'rating' => $item['rating'], + 'comment' => $item['comment'] + ]); + + $productVariant = ProductVariant::lockForUpdate() + ->where('uuid', $item['product_variant_id']) + ->first(); + + if ($productVariant) { + $newTotalRatingCount = $productVariant->total_rating + 1; + + $sumRating = ProductReview::where('product_variant_id', $item['product_variant_id']) + ->sum('rating'); + + $newAverage = $sumRating / $newTotalRatingCount; + + $productVariant->update([ + 'total_rating' => $newTotalRatingCount, + 'average_rating' => $newAverage + ]); + } + } + + Order::where('uuid', $orderId)->update([ + 'has_been_assessed' => true + ]); + }); + } +} \ No newline at end of file diff --git a/Backend/app/Services/SyncService.php b/Backend/app/Services/SyncService.php new file mode 100644 index 0000000..5eb8aa8 --- /dev/null +++ b/Backend/app/Services/SyncService.php @@ -0,0 +1,352 @@ + Campaign::class, + 'categories' => Category::class, + 'customers' => Customer::class, + 'customer_address' => CustomerAddress::class, + 'orders' => Order::class, + 'order_items' => OrderItem::class, + 'outlets' => Outlet::class, + 'outlet_campaigns' => OutletCampaign::class, + 'outlet_inventories' => OutletInventory::class, + 'products' => Product::class, + 'product_likes' => ProductLike::class, + 'product_reviews' => ProductReview::class, + 'product_variants' => ProductVariant::class, + 'purchases' => Purchase::class, + 'purchase_items' => PurchaseItem::class, + 'raw_materials' => RawMaterial::class, + 'receipt_items' => ReceiptItem::class, + 'stock_cards' => StockCard::class, + 'suppliers' => Supplier::class, + 'sync_deletes' => SyncDelete::class, + 'tenants' => Tenant::class, + 'tenant_payment_methods'=> TenantPaymentMethod::class, + 'transactions' => Transaction::class, + 'units' => Unit::class, + 'users' => User::class, + ]; + + public function getDeltaSync($lastSync) + { + $tenantId = Auth::user()->tenant_id; + return [ + 'tenants' => $this->tableMap['tenants']::withoutGlobalScopes() + ->where('uuid', $tenantId) + ->where('updated_at', '>', $lastSync) + ->first(), + 'tenant_payment_methods' => $this->tableMap['tenant_payment_methods']::where('updated_at', '>', $lastSync)->get(), + 'campaigns' => $this->tableMap['campaigns']::withTrashed()->where('updated_at', '>', $lastSync)->get(), + 'outlets' => $this->tableMap['outlets']::withTrashed()->where('updated_at', '>', $lastSync)->get(), + 'outlet_campaigns' => $this->tableMap['outlet_campaigns']::withTrashed()->where('updated_at', '>', $lastSync)->get(), + 'users' => $this->tableMap['users']::withTrashed()->where('updated_at', '>', $lastSync)->get(), + 'units' => $this->tableMap['units']::where('updated_at', '>', $lastSync)->get(), + 'categories' => $this->tableMap['categories']::where('updated_at', '>', $lastSync)->get(), + 'products' => $this->tableMap['products']::withTrashed()->where('updated_at', '>', $lastSync)->get(), + 'product_variants' => $this->tableMap['product_variants']::withTrashed()->where('updated_at', '>', $lastSync)->get(), + 'raw_materials' => $this->tableMap['raw_materials']::withTrashed()->where('updated_at', '>', $lastSync)->get(), + 'receipt_items' => $this->tableMap['receipt_items']::withTrashed()->where('updated_at', '>', $lastSync)->get(), + 'outlet_inventories' => $this->tableMap['outlet_inventories']::withTrashed()->where('updated_at', '>', $lastSync)->get(), + 'customers' => $this->tableMap['customers']::withTrashed()->where('updated_at', '>', $lastSync)->get(), + 'customer_address' => $this->tableMap['customer_address']::withTrashed()->where('updated_at', '>', $lastSync)->get(), + 'product_likes' => $this->tableMap['product_likes']::where('updated_at', '>', $lastSync)->get(), + 'product_reviews' => $this->tableMap['product_reviews']::where('updated_at', '>', $lastSync)->get(), + 'orders' => $this->tableMap['orders']::where('updated_at', '>', $lastSync)->get(), + 'order_items' => $this->tableMap['order_items']::with(['variant' => function($q) { + $q->select('uuid','product_id')->withTrashed(); + }])->where('updated_at', '>', $lastSync)->get(), + 'suppliers' => $this->tableMap['suppliers']::withTrashed()->where('updated_at', '>', $lastSync)->get(), + 'purchases' => $this->tableMap['purchases']::where('updated_at', '>', $lastSync)->get(), + 'purchase_items' => $this->tableMap['purchase_items']::with(['item' => function (MorphTo $morphTo) { + $morphTo->constrain([ + ProductVariant::class => fn($q) => + $q->select('uuid', 'product_id')->withTrashed(), + + RawMaterial::class => fn($q) => + $q->select('uuid')->withTrashed(), + ]); + } + ])->where('updated_at', '>', $lastSync)->get(), + 'transactions' => $this->tableMap['transactions']::where('updated_at', '>', $lastSync)->get(), + 'stock_cards' => $this->tableMap['stock_cards']::where('created_at', '>', $lastSync)->get(), + 'sync_deletes' => $this->tableMap['sync_deletes']::where('created_at', '>', $lastSync)->get() + ]; + } + + public function updateServerTime(Carbon $currentTime) + { + Auth::user()->update(['last_sync' => $currentTime]); + } + + public function processBatchSync(array $payload) + { + $items = $payload['items']; + $results = ['success_count' => 0, 'ignored_count' => 0, 'errors' => []]; + $changedTables = []; + + foreach ($items as $item) { + try { + $tableName = $item['table']; + $uuid = $item['local_id']; + $action = $item['action']; + $data = $item['data']; + + $modelClass = $this->tableMap[$tableName] ?? null; + + if (!$modelClass) throw new \Exception("Table {$tableName} not mapped."); + + DB::transaction(function () use ($modelClass, $uuid, $data, $action, $tableName, &$results, &$changedTables) { + if ($action === 'bulk_update') { + if($tableName === 'product_variants') { + $visibleIds = $data['visible_ids'] ?? []; + + $modelClass::whereNotIn('uuid', $visibleIds) + ->update(['is_visible_online' => false, 'updated_at' => $data['updated_at']]); + + $modelClass::whereIn('uuid', $visibleIds) + ->update(['is_visible_online' => true, 'updated_at' => $data['updated_at']]); + + $results['success_count']++; + $changedTables[] = 'product_variants'; + return; + } + + if($tableName === 'outlet_campaigns') { + $ids = $data['ids'] ?? []; + + $modelClass::whereIn('uuid', $ids) + ->update(['deleted_at' => $data['deleted_at']]); + + $results['success_count']++; + $changedTables = 'outlet_campaigns'; + return; + } + } + + $usesSoftDeletes = $this->usesSoftDeletes($modelClass); + + if($tableName === 'tenants') { + $query = $modelClass::withoutGlobalScopes()->where('uuid', $uuid); + } else { + $query = $modelClass::where('uuid', $uuid); + } + + if (in_array($tableName, ['product_variants','outlet_inventories', 'stock_cards'])) { + $query->lockForUpdate(); + } + + if($usesSoftDeletes) { + $query->withTrashed(); + } + + $existing = $query->first(); + + $deltaTotalSold = 0; + if ($tableName === 'product_variants' && isset($data['qty_sold'])) { + $deltaTotalSold = (float) $data['qty_sold']; + unset($data['qty_sold']); + } + + $shouldUpdateMetadata = true; + if ($existing && isset($data['updated_at'])) { + if (Carbon::parse($data['updated_at'])->lte($existing->updated_at)) { + $shouldUpdateMetadata = false; + $results['ignored_count']++; + } + } + + if($shouldUpdateMetadata) { + if (strtoupper($action) === 'DELETE') { + $this->handleSoftDelete($modelClass, $uuid, $data); + } else { + if ($tableName === 'stock_cards') { + $this->handleDeltaStock($data); + $modelClass::create($data); + } else { + if($tableName === 'outlets' && isset($data['is_main_outlet']) && $data['is_main_outlet'] === true) { + Outlet::whereNot('uuid', $uuid)->update(['is_main_outlet' => false]); + } + + $modelClass::updateOrCreate(['uuid' => $uuid], $data); + + } + } + $results['success_count']++; + } + + if ($deltaTotalSold != 0) { + $targetRecord = $existing ?? $modelClass::where('uuid', $uuid)->first(); + + if ($targetRecord) { + $targetRecord->increment('total_sold', $deltaTotalSold); + } + } + + if(!in_array($tableName, $changedTables)) { + $changedTables[] = $tableName; + } + + }, 3); + } catch (\Exception $e) { + $results['errors'][] = [ + 'table_name' => $item['table'], + 'local_id' => $item['local_id'], + 'message' => $e->getMessage() + ]; + } + } + + if($results['success_count'] > 0 && !empty($changedTables)) { + $masterTables = [ + 'tenants', + 'tenant_payment_methods', + 'outlets', + 'campaigns', + 'categories', + 'products', + 'product_variants', + 'outlet_inventories', + 'orders' + ]; + + $relevantChanges = array_intersect($changedTables, $masterTables); + + $tenantId = Auth::user()->tenant_id; + broadcast(new DataBatchUpdated($tenantId, array_values($relevantChanges)))->toOthers(); + } + return $results; + } + + protected function tableHasColumn(string $table, string $column): bool + { + $cacheKey = "table_columns_{$table}"; + + if (isset(static::$columnCache[$table])) { + return in_array($column, static::$columnCache[$table]); + } + + $columns = Cache::remember($cacheKey, now()->addDay(), function () use ($table) { + return Schema::getColumnListing($table); + }); + + static::$columnCache[$table] = $columns; + + return in_array($column, $columns); + } + + protected function usesSoftDeletes($modelClass): bool + { + if (isset(static::$softDeleteCache[$modelClass])) { + return static::$softDeleteCache[$modelClass]; + } + + $uses = in_array( + 'Illuminate\Database\Eloquent\SoftDeletes', + class_uses_recursive($modelClass) + ); + + static::$softDeleteCache[$modelClass] = $uses; + return $uses; + } + + private function handleDeltaStock($data) + { + $inventory = OutletInventory::withTrashed() + ->lockForUpdate() + ->where('uuid', '=', $data['outlet_inventory_id']) + ->first(); + + if (!$inventory) { + throw new \Exception("inventory with id {$data['outlet_inventory_id']} is not found for stock synchronize"); + } + + if($inventory->stock_type === 'unlimited') return; + + if(isset($data['type']) && isset($data['stock_type']) && $data['stock_type'] === 'fixed') { + $type = $data['type']; + if (in_array($type, ['adjustment_in', 'adjustment_out'])) { + $inventory->stock = $data['current_stock']; + $inventory->updated_at = $data['created_at']; + $inventory->save(); + } else if(in_array($type, ['sale', 'transfer_out'])) { + $inventory->decrement('stock', $data['qty_change']); + } else if(in_array($type, ['production', 'purchase', 'transfer_in'])) { + $inventory->increment('stock', $data['qty_change']); + } + } + + } + + private function handleSoftDelete($modelClass, $uuid, $data) + { + $tableName = (new $modelClass())->getTable(); + $deletedAt = isset($data['deleted_at']) ? Carbon::parse($data['deleted_at']) : now(); + $query = $modelClass::withTrashed(); + + if (empty($data) || (!isset($data['uuid']) && count($data) === 1)) { + $query->where('uuid', $uuid); + } else { + foreach ($data as $key => $value) { + if (in_array($key, ['is_active', 'is_delete_by_owner', 'deleted_at'])) continue; + $query->where($key, $value); + } + } + + $records = $query->get(); + + if ($records->isNotEmpty()) { + foreach ($records as $record) { + if (isset($data['is_delete_by_owner']) && $this->tableHasColumn($tableName, 'is_delete_by_owner')) { + $record->is_delete_by_owner = $data['is_delete_by_owner']; + } + + if(isset($data['is_active']) && $this->tableHasColumn($tableName, 'is_active')) { + $record->is_active = $data['is_active']; + } + $record->deleted_at = $deletedAt; + $record->save(); + $record->delete(); + $record->updateQuietly(['deleted_at' => $deletedAt]); + } + } + } +} \ No newline at end of file diff --git a/Backend/app/Services/UploadService.php b/Backend/app/Services/UploadService.php new file mode 100644 index 0000000..0777d2b --- /dev/null +++ b/Backend/app/Services/UploadService.php @@ -0,0 +1,20 @@ +tenant->business_code; + + $fileName = pathinfo($file->getClientOriginalName(), PATHINFO_BASENAME); + + $targetDirectory = "{$tenantCode}/{$folderType}"; + + return $file->storeAs($targetDirectory, $fileName, 'public'); + } +} \ No newline at end of file diff --git a/Backend/app/Traits/ApiResponse.php b/Backend/app/Traits/ApiResponse.php new file mode 100644 index 0000000..1cc1530 --- /dev/null +++ b/Backend/app/Traits/ApiResponse.php @@ -0,0 +1,34 @@ +json([ + 'status' => 'success', + 'message' => $message, + 'data' => $data, + 'error' => null + ], $code); + } + + /** + * Respons error standar + */ + protected function errorResponse($message = "An error occurred", $code = 400, $errorDetails = null): JsonResponse + { + return response()->json([ + 'status' => 'error', + 'message' => $message, + 'data' => null, + 'error' => $errorDetails + ], $code); + } +} \ No newline at end of file diff --git a/Backend/app/Traits/Multitenantable.php b/Backend/app/Traits/Multitenantable.php new file mode 100644 index 0000000..7bdcb43 --- /dev/null +++ b/Backend/app/Traits/Multitenantable.php @@ -0,0 +1,31 @@ +tenant_id =Auth::user()->tenant_id; + } + } + }); + + // Global Scope: Otomatis filter setiap query (select, update, delete) + static::addGlobalScope('tenant_id', function (Builder $builder) { + $tenantId = Auth::check() ?Auth::user()->tenant_id : config('app.current_tenant_id'); + + if($tenantId && !(static::class === Tenant::class)) { + $builder->where($builder->getModel()->getTable() .'.tenant_id', $tenantId); + } + }); + } +} diff --git a/Backend/artisan b/Backend/artisan new file mode 100644 index 0000000..c35e31d --- /dev/null +++ b/Backend/artisan @@ -0,0 +1,18 @@ +#!/usr/bin/env php +handleCommand(new ArgvInput); + +exit($status); diff --git a/Backend/bootstrap/app.php b/Backend/bootstrap/app.php new file mode 100644 index 0000000..c0cb19f --- /dev/null +++ b/Backend/bootstrap/app.php @@ -0,0 +1,23 @@ +withRouting( + web: __DIR__.'/../routes/web.php', + api: __DIR__.'/../routes/api.php', + commands: __DIR__.'/../routes/console.php', + channels: __DIR__.'/../routes/channels.php', + health: '/up', + ) + ->withMiddleware(function (Middleware $middleware): void { + $middleware->alias([ + 'identify.tenant' => IdentifyTenant::class + ]); // + }) + ->withExceptions(function (Exceptions $exceptions): void { + // + })->create(); diff --git a/Backend/bootstrap/cache/.gitignore b/Backend/bootstrap/cache/.gitignore new file mode 100644 index 0000000..d6b7ef3 --- /dev/null +++ b/Backend/bootstrap/cache/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/Backend/bootstrap/providers.php b/Backend/bootstrap/providers.php new file mode 100644 index 0000000..d544739 --- /dev/null +++ b/Backend/bootstrap/providers.php @@ -0,0 +1,6 @@ +=5.0.0" + }, + "require-dev": { + "doctrine/dbal": "^4.0.0", + "nesbot/carbon": "^2.71.0 || ^3.0.0", + "phpunit/phpunit": "^10.3" + }, + "type": "library", + "autoload": { + "psr-4": { + "Carbon\\Doctrine\\": "src/Carbon/Doctrine/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "KyleKatarn", + "email": "kylekatarnls@gmail.com" + } + ], + "description": "Types to use Carbon in Doctrine", + "keywords": [ + "carbon", + "date", + "datetime", + "doctrine", + "time" + ], + "support": { + "issues": "https://github.com/CarbonPHP/carbon-doctrine-types/issues", + "source": "https://github.com/CarbonPHP/carbon-doctrine-types/tree/3.2.0" + }, + "funding": [ + { + "url": "https://github.com/kylekatarnls", + "type": "github" + }, + { + "url": "https://opencollective.com/Carbon", + "type": "open_collective" + }, + { + "url": "https://tidelift.com/funding/github/packagist/nesbot/carbon", + "type": "tidelift" + } + ], + "time": "2024-02-09T16:56:22+00:00" + }, + { + "name": "clue/redis-protocol", + "version": "v0.3.2", + "source": { + "type": "git", + "url": "https://github.com/clue/redis-protocol.git", + "reference": "6f565332f5531b7722d1e9c445314b91862f6d6c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/clue/redis-protocol/zipball/6f565332f5531b7722d1e9c445314b91862f6d6c", + "reference": "6f565332f5531b7722d1e9c445314b91862f6d6c", + "shasum": "" + }, + "require": { + "php": ">=5.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36" + }, + "type": "library", + "autoload": { + "psr-4": { + "Clue\\Redis\\Protocol\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian Lück", + "email": "christian@lueck.tv" + } + ], + "description": "A streaming Redis protocol (RESP) parser and serializer written in pure PHP.", + "homepage": "https://github.com/clue/redis-protocol", + "keywords": [ + "parser", + "protocol", + "redis", + "resp", + "serializer", + "streaming" + ], + "support": { + "issues": "https://github.com/clue/redis-protocol/issues", + "source": "https://github.com/clue/redis-protocol/tree/v0.3.2" + }, + "funding": [ + { + "url": "https://clue.engineering/support", + "type": "custom" + }, + { + "url": "https://github.com/clue", + "type": "github" + } + ], + "time": "2024-08-07T11:06:28+00:00" + }, + { + "name": "clue/redis-react", + "version": "v2.8.0", + "source": { + "type": "git", + "url": "https://github.com/clue/reactphp-redis.git", + "reference": "84569198dfd5564977d2ae6a32de4beb5a24bdca" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/clue/reactphp-redis/zipball/84569198dfd5564977d2ae6a32de4beb5a24bdca", + "reference": "84569198dfd5564977d2ae6a32de4beb5a24bdca", + "shasum": "" + }, + "require": { + "clue/redis-protocol": "^0.3.2", + "evenement/evenement": "^3.0 || ^2.0 || ^1.0", + "php": ">=5.3", + "react/event-loop": "^1.2", + "react/promise": "^3.2 || ^2.0 || ^1.1", + "react/promise-timer": "^1.11", + "react/socket": "^1.16" + }, + "require-dev": { + "clue/block-react": "^1.5", + "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36" + }, + "type": "library", + "autoload": { + "psr-4": { + "Clue\\React\\Redis\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian Lück", + "email": "christian@clue.engineering" + } + ], + "description": "Async Redis client implementation, built on top of ReactPHP.", + "homepage": "https://github.com/clue/reactphp-redis", + "keywords": [ + "async", + "client", + "database", + "reactphp", + "redis" + ], + "support": { + "issues": "https://github.com/clue/reactphp-redis/issues", + "source": "https://github.com/clue/reactphp-redis/tree/v2.8.0" + }, + "funding": [ + { + "url": "https://clue.engineering/support", + "type": "custom" + }, + { + "url": "https://github.com/clue", + "type": "github" + } + ], + "time": "2025-01-03T16:18:33+00:00" + }, + { + "name": "dflydev/dot-access-data", + "version": "v3.0.3", + "source": { + "type": "git", + "url": "https://github.com/dflydev/dflydev-dot-access-data.git", + "reference": "a23a2bf4f31d3518f3ecb38660c95715dfead60f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/dflydev/dflydev-dot-access-data/zipball/a23a2bf4f31d3518f3ecb38660c95715dfead60f", + "reference": "a23a2bf4f31d3518f3ecb38660c95715dfead60f", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^0.12.42", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.3", + "scrutinizer/ocular": "1.6.0", + "squizlabs/php_codesniffer": "^3.5", + "vimeo/psalm": "^4.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Dflydev\\DotAccessData\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Dragonfly Development Inc.", + "email": "info@dflydev.com", + "homepage": "http://dflydev.com" + }, + { + "name": "Beau Simensen", + "email": "beau@dflydev.com", + "homepage": "http://beausimensen.com" + }, + { + "name": "Carlos Frutos", + "email": "carlos@kiwing.it", + "homepage": "https://github.com/cfrutos" + }, + { + "name": "Colin O'Dell", + "email": "colinodell@gmail.com", + "homepage": "https://www.colinodell.com" + } + ], + "description": "Given a deep data structure, access data by dot notation.", + "homepage": "https://github.com/dflydev/dflydev-dot-access-data", + "keywords": [ + "access", + "data", + "dot", + "notation" + ], + "support": { + "issues": "https://github.com/dflydev/dflydev-dot-access-data/issues", + "source": "https://github.com/dflydev/dflydev-dot-access-data/tree/v3.0.3" + }, + "time": "2024-07-08T12:26:09+00:00" + }, + { + "name": "doctrine/inflector", + "version": "2.1.0", + "source": { + "type": "git", + "url": "https://github.com/doctrine/inflector.git", + "reference": "6d6c96277ea252fc1304627204c3d5e6e15faa3b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/inflector/zipball/6d6c96277ea252fc1304627204c3d5e6e15faa3b", + "reference": "6d6c96277ea252fc1304627204c3d5e6e15faa3b", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "require-dev": { + "doctrine/coding-standard": "^12.0 || ^13.0", + "phpstan/phpstan": "^1.12 || ^2.0", + "phpstan/phpstan-phpunit": "^1.4 || ^2.0", + "phpstan/phpstan-strict-rules": "^1.6 || ^2.0", + "phpunit/phpunit": "^8.5 || ^12.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Inflector\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + } + ], + "description": "PHP Doctrine Inflector is a small library that can perform string manipulations with regard to upper/lowercase and singular/plural forms of words.", + "homepage": "https://www.doctrine-project.org/projects/inflector.html", + "keywords": [ + "inflection", + "inflector", + "lowercase", + "manipulation", + "php", + "plural", + "singular", + "strings", + "uppercase", + "words" + ], + "support": { + "issues": "https://github.com/doctrine/inflector/issues", + "source": "https://github.com/doctrine/inflector/tree/2.1.0" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Finflector", + "type": "tidelift" + } + ], + "time": "2025-08-10T19:31:58+00:00" + }, + { + "name": "doctrine/lexer", + "version": "3.0.1", + "source": { + "type": "git", + "url": "https://github.com/doctrine/lexer.git", + "reference": "31ad66abc0fc9e1a1f2d9bc6a42668d2fbbcd6dd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/lexer/zipball/31ad66abc0fc9e1a1f2d9bc6a42668d2fbbcd6dd", + "reference": "31ad66abc0fc9e1a1f2d9bc6a42668d2fbbcd6dd", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "require-dev": { + "doctrine/coding-standard": "^12", + "phpstan/phpstan": "^1.10", + "phpunit/phpunit": "^10.5", + "psalm/plugin-phpunit": "^0.18.3", + "vimeo/psalm": "^5.21" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Common\\Lexer\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + } + ], + "description": "PHP Doctrine Lexer parser library that can be used in Top-Down, Recursive Descent Parsers.", + "homepage": "https://www.doctrine-project.org/projects/lexer.html", + "keywords": [ + "annotations", + "docblock", + "lexer", + "parser", + "php" + ], + "support": { + "issues": "https://github.com/doctrine/lexer/issues", + "source": "https://github.com/doctrine/lexer/tree/3.0.1" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Flexer", + "type": "tidelift" + } + ], + "time": "2024-02-05T11:56:58+00:00" + }, + { + "name": "dragonmantank/cron-expression", + "version": "v3.6.0", + "source": { + "type": "git", + "url": "https://github.com/dragonmantank/cron-expression.git", + "reference": "d61a8a9604ec1f8c3d150d09db6ce98b32675013" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/dragonmantank/cron-expression/zipball/d61a8a9604ec1f8c3d150d09db6ce98b32675013", + "reference": "d61a8a9604ec1f8c3d150d09db6ce98b32675013", + "shasum": "" + }, + "require": { + "php": "^8.2|^8.3|^8.4|^8.5" + }, + "replace": { + "mtdowling/cron-expression": "^1.0" + }, + "require-dev": { + "phpstan/extension-installer": "^1.4.3", + "phpstan/phpstan": "^1.12.32|^2.1.31", + "phpunit/phpunit": "^8.5.48|^9.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Cron\\": "src/Cron/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Chris Tankersley", + "email": "chris@ctankersley.com", + "homepage": "https://github.com/dragonmantank" + } + ], + "description": "CRON for PHP: Calculate the next or previous run date and determine if a CRON expression is due", + "keywords": [ + "cron", + "schedule" + ], + "support": { + "issues": "https://github.com/dragonmantank/cron-expression/issues", + "source": "https://github.com/dragonmantank/cron-expression/tree/v3.6.0" + }, + "funding": [ + { + "url": "https://github.com/dragonmantank", + "type": "github" + } + ], + "time": "2025-10-31T18:51:33+00:00" + }, + { + "name": "egulias/email-validator", + "version": "4.0.4", + "source": { + "type": "git", + "url": "https://github.com/egulias/EmailValidator.git", + "reference": "d42c8731f0624ad6bdc8d3e5e9a4524f68801cfa" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/egulias/EmailValidator/zipball/d42c8731f0624ad6bdc8d3e5e9a4524f68801cfa", + "reference": "d42c8731f0624ad6bdc8d3e5e9a4524f68801cfa", + "shasum": "" + }, + "require": { + "doctrine/lexer": "^2.0 || ^3.0", + "php": ">=8.1", + "symfony/polyfill-intl-idn": "^1.26" + }, + "require-dev": { + "phpunit/phpunit": "^10.2", + "vimeo/psalm": "^5.12" + }, + "suggest": { + "ext-intl": "PHP Internationalization Libraries are required to use the SpoofChecking validation" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Egulias\\EmailValidator\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Eduardo Gulias Davis" + } + ], + "description": "A library for validating emails against several RFCs", + "homepage": "https://github.com/egulias/EmailValidator", + "keywords": [ + "email", + "emailvalidation", + "emailvalidator", + "validation", + "validator" + ], + "support": { + "issues": "https://github.com/egulias/EmailValidator/issues", + "source": "https://github.com/egulias/EmailValidator/tree/4.0.4" + }, + "funding": [ + { + "url": "https://github.com/egulias", + "type": "github" + } + ], + "time": "2025-03-06T22:45:56+00:00" + }, + { + "name": "evenement/evenement", + "version": "v3.0.2", + "source": { + "type": "git", + "url": "https://github.com/igorw/evenement.git", + "reference": "0a16b0d71ab13284339abb99d9d2bd813640efbc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/igorw/evenement/zipball/0a16b0d71ab13284339abb99d9d2bd813640efbc", + "reference": "0a16b0d71ab13284339abb99d9d2bd813640efbc", + "shasum": "" + }, + "require": { + "php": ">=7.0" + }, + "require-dev": { + "phpunit/phpunit": "^9 || ^6" + }, + "type": "library", + "autoload": { + "psr-4": { + "Evenement\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Igor Wiedler", + "email": "igor@wiedler.ch" + } + ], + "description": "Événement is a very simple event dispatching library for PHP", + "keywords": [ + "event-dispatcher", + "event-emitter" + ], + "support": { + "issues": "https://github.com/igorw/evenement/issues", + "source": "https://github.com/igorw/evenement/tree/v3.0.2" + }, + "time": "2023-08-08T05:53:35+00:00" + }, + { + "name": "fruitcake/php-cors", + "version": "v1.4.0", + "source": { + "type": "git", + "url": "https://github.com/fruitcake/php-cors.git", + "reference": "38aaa6c3fd4c157ffe2a4d10aa8b9b16ba8de379" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/fruitcake/php-cors/zipball/38aaa6c3fd4c157ffe2a4d10aa8b9b16ba8de379", + "reference": "38aaa6c3fd4c157ffe2a4d10aa8b9b16ba8de379", + "shasum": "" + }, + "require": { + "php": "^8.1", + "symfony/http-foundation": "^5.4|^6.4|^7.3|^8" + }, + "require-dev": { + "phpstan/phpstan": "^2", + "phpunit/phpunit": "^9", + "squizlabs/php_codesniffer": "^4" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.3-dev" + } + }, + "autoload": { + "psr-4": { + "Fruitcake\\Cors\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fruitcake", + "homepage": "https://fruitcake.nl" + }, + { + "name": "Barryvdh", + "email": "barryvdh@gmail.com" + } + ], + "description": "Cross-origin resource sharing library for the Symfony HttpFoundation", + "homepage": "https://github.com/fruitcake/php-cors", + "keywords": [ + "cors", + "laravel", + "symfony" + ], + "support": { + "issues": "https://github.com/fruitcake/php-cors/issues", + "source": "https://github.com/fruitcake/php-cors/tree/v1.4.0" + }, + "funding": [ + { + "url": "https://fruitcake.nl", + "type": "custom" + }, + { + "url": "https://github.com/barryvdh", + "type": "github" + } + ], + "time": "2025-12-03T09:33:47+00:00" + }, + { + "name": "graham-campbell/result-type", + "version": "v1.1.4", + "source": { + "type": "git", + "url": "https://github.com/GrahamCampbell/Result-Type.git", + "reference": "e01f4a821471308ba86aa202fed6698b6b695e3b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/GrahamCampbell/Result-Type/zipball/e01f4a821471308ba86aa202fed6698b6b695e3b", + "reference": "e01f4a821471308ba86aa202fed6698b6b695e3b", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0", + "phpoption/phpoption": "^1.9.5" + }, + "require-dev": { + "phpunit/phpunit": "^8.5.41 || ^9.6.22 || ^10.5.45 || ^11.5.7" + }, + "type": "library", + "autoload": { + "psr-4": { + "GrahamCampbell\\ResultType\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + } + ], + "description": "An Implementation Of The Result Type", + "keywords": [ + "Graham Campbell", + "GrahamCampbell", + "Result Type", + "Result-Type", + "result" + ], + "support": { + "issues": "https://github.com/GrahamCampbell/Result-Type/issues", + "source": "https://github.com/GrahamCampbell/Result-Type/tree/v1.1.4" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/graham-campbell/result-type", + "type": "tidelift" + } + ], + "time": "2025-12-27T19:43:20+00:00" + }, + { + "name": "guzzlehttp/guzzle", + "version": "7.10.0", + "source": { + "type": "git", + "url": "https://github.com/guzzle/guzzle.git", + "reference": "b51ac707cfa420b7bfd4e4d5e510ba8008e822b4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/guzzle/zipball/b51ac707cfa420b7bfd4e4d5e510ba8008e822b4", + "reference": "b51ac707cfa420b7bfd4e4d5e510ba8008e822b4", + "shasum": "" + }, + "require": { + "ext-json": "*", + "guzzlehttp/promises": "^2.3", + "guzzlehttp/psr7": "^2.8", + "php": "^7.2.5 || ^8.0", + "psr/http-client": "^1.0", + "symfony/deprecation-contracts": "^2.2 || ^3.0" + }, + "provide": { + "psr/http-client-implementation": "1.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "ext-curl": "*", + "guzzle/client-integration-tests": "3.0.2", + "php-http/message-factory": "^1.1", + "phpunit/phpunit": "^8.5.39 || ^9.6.20", + "psr/log": "^1.1 || ^2.0 || ^3.0" + }, + "suggest": { + "ext-curl": "Required for CURL handler support", + "ext-intl": "Required for Internationalized Domain Name (IDN) support", + "psr/log": "Required for using the Log middleware" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } + }, + "autoload": { + "files": [ + "src/functions_include.php" + ], + "psr-4": { + "GuzzleHttp\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "Jeremy Lindblom", + "email": "jeremeamia@gmail.com", + "homepage": "https://github.com/jeremeamia" + }, + { + "name": "George Mponos", + "email": "gmponos@gmail.com", + "homepage": "https://github.com/gmponos" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://github.com/sagikazarmark" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" + } + ], + "description": "Guzzle is a PHP HTTP client library", + "keywords": [ + "client", + "curl", + "framework", + "http", + "http client", + "psr-18", + "psr-7", + "rest", + "web service" + ], + "support": { + "issues": "https://github.com/guzzle/guzzle/issues", + "source": "https://github.com/guzzle/guzzle/tree/7.10.0" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/guzzle", + "type": "tidelift" + } + ], + "time": "2025-08-23T22:36:01+00:00" + }, + { + "name": "guzzlehttp/promises", + "version": "2.3.0", + "source": { + "type": "git", + "url": "https://github.com/guzzle/promises.git", + "reference": "481557b130ef3790cf82b713667b43030dc9c957" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/promises/zipball/481557b130ef3790cf82b713667b43030dc9c957", + "reference": "481557b130ef3790cf82b713667b43030dc9c957", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "phpunit/phpunit": "^8.5.44 || ^9.6.25" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\Promise\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" + } + ], + "description": "Guzzle promises library", + "keywords": [ + "promise" + ], + "support": { + "issues": "https://github.com/guzzle/promises/issues", + "source": "https://github.com/guzzle/promises/tree/2.3.0" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/promises", + "type": "tidelift" + } + ], + "time": "2025-08-22T14:34:08+00:00" + }, + { + "name": "guzzlehttp/psr7", + "version": "2.9.0", + "source": { + "type": "git", + "url": "https://github.com/guzzle/psr7.git", + "reference": "7d0ed42f28e42d61352a7a79de682e5e67fec884" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/7d0ed42f28e42d61352a7a79de682e5e67fec884", + "reference": "7d0ed42f28e42d61352a7a79de682e5e67fec884", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0", + "psr/http-factory": "^1.0", + "psr/http-message": "^1.1 || ^2.0", + "ralouphie/getallheaders": "^3.0" + }, + "provide": { + "psr/http-factory-implementation": "1.0", + "psr/http-message-implementation": "1.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "http-interop/http-factory-tests": "0.9.0", + "jshttp/mime-db": "1.54.0.1", + "phpunit/phpunit": "^8.5.44 || ^9.6.25" + }, + "suggest": { + "laminas/laminas-httphandlerrunner": "Emit PSR-7 responses" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\Psr7\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "George Mponos", + "email": "gmponos@gmail.com", + "homepage": "https://github.com/gmponos" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://github.com/sagikazarmark" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://sagikazarmark.hu" + } + ], + "description": "PSR-7 message implementation that also provides common utility methods", + "keywords": [ + "http", + "message", + "psr-7", + "request", + "response", + "stream", + "uri", + "url" + ], + "support": { + "issues": "https://github.com/guzzle/psr7/issues", + "source": "https://github.com/guzzle/psr7/tree/2.9.0" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/psr7", + "type": "tidelift" + } + ], + "time": "2026-03-10T16:41:02+00:00" + }, + { + "name": "guzzlehttp/uri-template", + "version": "v1.0.5", + "source": { + "type": "git", + "url": "https://github.com/guzzle/uri-template.git", + "reference": "4f4bbd4e7172148801e76e3decc1e559bdee34e1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/uri-template/zipball/4f4bbd4e7172148801e76e3decc1e559bdee34e1", + "reference": "4f4bbd4e7172148801e76e3decc1e559bdee34e1", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0", + "symfony/polyfill-php80": "^1.24" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "phpunit/phpunit": "^8.5.44 || ^9.6.25", + "uri-template/tests": "1.0.0" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\UriTemplate\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "George Mponos", + "email": "gmponos@gmail.com", + "homepage": "https://github.com/gmponos" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + } + ], + "description": "A polyfill class for uri_template of PHP", + "keywords": [ + "guzzlehttp", + "uri-template" + ], + "support": { + "issues": "https://github.com/guzzle/uri-template/issues", + "source": "https://github.com/guzzle/uri-template/tree/v1.0.5" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/uri-template", + "type": "tidelift" + } + ], + "time": "2025-08-22T14:27:06+00:00" + }, + { + "name": "laravel/framework", + "version": "v12.53.0", + "source": { + "type": "git", + "url": "https://github.com/laravel/framework.git", + "reference": "f57f035c0d34503d9ff30be76159bb35a003cd1f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/framework/zipball/f57f035c0d34503d9ff30be76159bb35a003cd1f", + "reference": "f57f035c0d34503d9ff30be76159bb35a003cd1f", + "shasum": "" + }, + "require": { + "brick/math": "^0.11|^0.12|^0.13|^0.14", + "composer-runtime-api": "^2.2", + "doctrine/inflector": "^2.0.5", + "dragonmantank/cron-expression": "^3.4", + "egulias/email-validator": "^3.2.1|^4.0", + "ext-ctype": "*", + "ext-filter": "*", + "ext-hash": "*", + "ext-mbstring": "*", + "ext-openssl": "*", + "ext-session": "*", + "ext-tokenizer": "*", + "fruitcake/php-cors": "^1.3", + "guzzlehttp/guzzle": "^7.8.2", + "guzzlehttp/uri-template": "^1.0", + "laravel/prompts": "^0.3.0", + "laravel/serializable-closure": "^1.3|^2.0", + "league/commonmark": "^2.7", + "league/flysystem": "^3.25.1", + "league/flysystem-local": "^3.25.1", + "league/uri": "^7.5.1", + "monolog/monolog": "^3.0", + "nesbot/carbon": "^3.8.4", + "nunomaduro/termwind": "^2.0", + "php": "^8.2", + "psr/container": "^1.1.1|^2.0.1", + "psr/log": "^1.0|^2.0|^3.0", + "psr/simple-cache": "^1.0|^2.0|^3.0", + "ramsey/uuid": "^4.7", + "symfony/console": "^7.2.0", + "symfony/error-handler": "^7.2.0", + "symfony/finder": "^7.2.0", + "symfony/http-foundation": "^7.2.0", + "symfony/http-kernel": "^7.2.0", + "symfony/mailer": "^7.2.0", + "symfony/mime": "^7.2.0", + "symfony/polyfill-php83": "^1.33", + "symfony/polyfill-php84": "^1.33", + "symfony/polyfill-php85": "^1.33", + "symfony/process": "^7.2.0", + "symfony/routing": "^7.2.0", + "symfony/uid": "^7.2.0", + "symfony/var-dumper": "^7.2.0", + "tijsverkoyen/css-to-inline-styles": "^2.2.5", + "vlucas/phpdotenv": "^5.6.1", + "voku/portable-ascii": "^2.0.2" + }, + "conflict": { + "tightenco/collect": "<5.5.33" + }, + "provide": { + "psr/container-implementation": "1.1|2.0", + "psr/log-implementation": "1.0|2.0|3.0", + "psr/simple-cache-implementation": "1.0|2.0|3.0" + }, + "replace": { + "illuminate/auth": "self.version", + "illuminate/broadcasting": "self.version", + "illuminate/bus": "self.version", + "illuminate/cache": "self.version", + "illuminate/collections": "self.version", + "illuminate/concurrency": "self.version", + "illuminate/conditionable": "self.version", + "illuminate/config": "self.version", + "illuminate/console": "self.version", + "illuminate/container": "self.version", + "illuminate/contracts": "self.version", + "illuminate/cookie": "self.version", + "illuminate/database": "self.version", + "illuminate/encryption": "self.version", + "illuminate/events": "self.version", + "illuminate/filesystem": "self.version", + "illuminate/hashing": "self.version", + "illuminate/http": "self.version", + "illuminate/json-schema": "self.version", + "illuminate/log": "self.version", + "illuminate/macroable": "self.version", + "illuminate/mail": "self.version", + "illuminate/notifications": "self.version", + "illuminate/pagination": "self.version", + "illuminate/pipeline": "self.version", + "illuminate/process": "self.version", + "illuminate/queue": "self.version", + "illuminate/redis": "self.version", + "illuminate/reflection": "self.version", + "illuminate/routing": "self.version", + "illuminate/session": "self.version", + "illuminate/support": "self.version", + "illuminate/testing": "self.version", + "illuminate/translation": "self.version", + "illuminate/validation": "self.version", + "illuminate/view": "self.version", + "spatie/once": "*" + }, + "require-dev": { + "ably/ably-php": "^1.0", + "aws/aws-sdk-php": "^3.322.9", + "ext-gmp": "*", + "fakerphp/faker": "^1.24", + "guzzlehttp/promises": "^2.0.3", + "guzzlehttp/psr7": "^2.4", + "laravel/pint": "^1.18", + "league/flysystem-aws-s3-v3": "^3.25.1", + "league/flysystem-ftp": "^3.25.1", + "league/flysystem-path-prefixing": "^3.25.1", + "league/flysystem-read-only": "^3.25.1", + "league/flysystem-sftp-v3": "^3.25.1", + "mockery/mockery": "^1.6.10", + "opis/json-schema": "^2.4.1", + "orchestra/testbench-core": "^10.9.0", + "pda/pheanstalk": "^5.0.6|^7.0.0", + "php-http/discovery": "^1.15", + "phpstan/phpstan": "^2.0", + "phpunit/phpunit": "^10.5.35|^11.5.3|^12.0.1", + "predis/predis": "^2.3|^3.0", + "resend/resend-php": "^0.10.0|^1.0", + "symfony/cache": "^7.2.0", + "symfony/http-client": "^7.2.0", + "symfony/psr-http-message-bridge": "^7.2.0", + "symfony/translation": "^7.2.0" + }, + "suggest": { + "ably/ably-php": "Required to use the Ably broadcast driver (^1.0).", + "aws/aws-sdk-php": "Required to use the SQS queue driver, DynamoDb failed job storage, and SES mail driver (^3.322.9).", + "brianium/paratest": "Required to run tests in parallel (^7.0|^8.0).", + "ext-apcu": "Required to use the APC cache driver.", + "ext-fileinfo": "Required to use the Filesystem class.", + "ext-ftp": "Required to use the Flysystem FTP driver.", + "ext-gd": "Required to use Illuminate\\Http\\Testing\\FileFactory::image().", + "ext-memcached": "Required to use the memcache cache driver.", + "ext-pcntl": "Required to use all features of the queue worker and console signal trapping.", + "ext-pdo": "Required to use all database features.", + "ext-posix": "Required to use all features of the queue worker.", + "ext-redis": "Required to use the Redis cache and queue drivers (^4.0|^5.0|^6.0).", + "fakerphp/faker": "Required to generate fake data using the fake() helper (^1.23).", + "filp/whoops": "Required for friendly error pages in development (^2.14.3).", + "laravel/tinker": "Required to use the tinker console command (^2.0).", + "league/flysystem-aws-s3-v3": "Required to use the Flysystem S3 driver (^3.25.1).", + "league/flysystem-ftp": "Required to use the Flysystem FTP driver (^3.25.1).", + "league/flysystem-path-prefixing": "Required to use the scoped driver (^3.25.1).", + "league/flysystem-read-only": "Required to use read-only disks (^3.25.1)", + "league/flysystem-sftp-v3": "Required to use the Flysystem SFTP driver (^3.25.1).", + "mockery/mockery": "Required to use mocking (^1.6).", + "pda/pheanstalk": "Required to use the beanstalk queue driver (^5.0).", + "php-http/discovery": "Required to use PSR-7 bridging features (^1.15).", + "phpunit/phpunit": "Required to use assertions and run tests (^10.5.35|^11.5.3|^12.0.1).", + "predis/predis": "Required to use the predis connector (^2.3|^3.0).", + "psr/http-message": "Required to allow Storage::put to accept a StreamInterface (^1.0).", + "pusher/pusher-php-server": "Required to use the Pusher broadcast driver (^6.0|^7.0).", + "resend/resend-php": "Required to enable support for the Resend mail transport (^0.10.0|^1.0).", + "symfony/cache": "Required to PSR-6 cache bridge (^7.2).", + "symfony/filesystem": "Required to enable support for relative symbolic links (^7.2).", + "symfony/http-client": "Required to enable support for the Symfony API mail transports (^7.2).", + "symfony/mailgun-mailer": "Required to enable support for the Mailgun mail transport (^7.2).", + "symfony/postmark-mailer": "Required to enable support for the Postmark mail transport (^7.2).", + "symfony/psr-http-message-bridge": "Required to use PSR-7 bridging features (^7.2)." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "12.x-dev" + } + }, + "autoload": { + "files": [ + "src/Illuminate/Collections/functions.php", + "src/Illuminate/Collections/helpers.php", + "src/Illuminate/Events/functions.php", + "src/Illuminate/Filesystem/functions.php", + "src/Illuminate/Foundation/helpers.php", + "src/Illuminate/Log/functions.php", + "src/Illuminate/Reflection/helpers.php", + "src/Illuminate/Support/functions.php", + "src/Illuminate/Support/helpers.php" + ], + "psr-4": { + "Illuminate\\": "src/Illuminate/", + "Illuminate\\Support\\": [ + "src/Illuminate/Macroable/", + "src/Illuminate/Collections/", + "src/Illuminate/Conditionable/", + "src/Illuminate/Reflection/" + ] + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "The Laravel Framework.", + "homepage": "https://laravel.com", + "keywords": [ + "framework", + "laravel" + ], + "support": { + "issues": "https://github.com/laravel/framework/issues", + "source": "https://github.com/laravel/framework" + }, + "time": "2026-02-24T14:35:15+00:00" + }, + { + "name": "laravel/prompts", + "version": "v0.3.13", + "source": { + "type": "git", + "url": "https://github.com/laravel/prompts.git", + "reference": "ed8c466571b37e977532fb2fd3c272c784d7050d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/prompts/zipball/ed8c466571b37e977532fb2fd3c272c784d7050d", + "reference": "ed8c466571b37e977532fb2fd3c272c784d7050d", + "shasum": "" + }, + "require": { + "composer-runtime-api": "^2.2", + "ext-mbstring": "*", + "php": "^8.1", + "symfony/console": "^6.2|^7.0|^8.0" + }, + "conflict": { + "illuminate/console": ">=10.17.0 <10.25.0", + "laravel/framework": ">=10.17.0 <10.25.0" + }, + "require-dev": { + "illuminate/collections": "^10.0|^11.0|^12.0|^13.0", + "mockery/mockery": "^1.5", + "pestphp/pest": "^2.3|^3.4|^4.0", + "phpstan/phpstan": "^1.12.28", + "phpstan/phpstan-mockery": "^1.1.3" + }, + "suggest": { + "ext-pcntl": "Required for the spinner to be animated." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "0.3.x-dev" + } + }, + "autoload": { + "files": [ + "src/helpers.php" + ], + "psr-4": { + "Laravel\\Prompts\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Add beautiful and user-friendly forms to your command-line applications.", + "support": { + "issues": "https://github.com/laravel/prompts/issues", + "source": "https://github.com/laravel/prompts/tree/v0.3.13" + }, + "time": "2026-02-06T12:17:10+00:00" + }, + { + "name": "laravel/reverb", + "version": "v1.10.0", + "source": { + "type": "git", + "url": "https://github.com/laravel/reverb.git", + "reference": "a9c2b24ba455d0b2c22bb2851c15ba1adcb75240" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/reverb/zipball/a9c2b24ba455d0b2c22bb2851c15ba1adcb75240", + "reference": "a9c2b24ba455d0b2c22bb2851c15ba1adcb75240", + "shasum": "" + }, + "require": { + "clue/redis-react": "^2.6", + "guzzlehttp/psr7": "^2.6", + "illuminate/console": "^10.47|^11.0|^12.0|^13.0", + "illuminate/contracts": "^10.47|^11.0|^12.0|^13.0", + "illuminate/http": "^10.47|^11.0|^12.0|^13.0", + "illuminate/support": "^10.47|^11.0|^12.0|^13.0", + "laravel/prompts": "^0.1.15|^0.2.0|^0.3.0", + "php": "^8.2", + "pusher/pusher-php-server": "^7.2", + "ratchet/rfc6455": "^0.4", + "react/promise-timer": "^1.10", + "react/socket": "^1.14", + "symfony/console": "^6.0|^7.0|^8.0", + "symfony/http-foundation": "^6.3|^7.0|^8.0" + }, + "require-dev": { + "orchestra/testbench": "^8.36|^9.15|^10.8|^11.0", + "pestphp/pest": "^2.0|^3.0|^4.0", + "phpstan/phpstan": "^1.10", + "ratchet/pawl": "^0.4.1", + "react/async": "^4.2", + "react/http": "^1.9" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Laravel\\Reverb\\ApplicationManagerServiceProvider", + "Laravel\\Reverb\\ReverbServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "Laravel\\Reverb\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + }, + { + "name": "Joe Dixon", + "email": "joe@laravel.com" + } + ], + "description": "Laravel Reverb provides a real-time WebSocket communication backend for Laravel applications.", + "keywords": [ + "WebSockets", + "laravel", + "real-time", + "websocket" + ], + "support": { + "issues": "https://github.com/laravel/reverb/issues", + "source": "https://github.com/laravel/reverb/tree/v1.10.0" + }, + "time": "2026-03-29T14:51:57+00:00" + }, + { + "name": "laravel/sanctum", + "version": "v4.3.1", + "source": { + "type": "git", + "url": "https://github.com/laravel/sanctum.git", + "reference": "e3b85d6e36ad00e5db2d1dcc27c81ffdf15cbf76" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/sanctum/zipball/e3b85d6e36ad00e5db2d1dcc27c81ffdf15cbf76", + "reference": "e3b85d6e36ad00e5db2d1dcc27c81ffdf15cbf76", + "shasum": "" + }, + "require": { + "ext-json": "*", + "illuminate/console": "^11.0|^12.0|^13.0", + "illuminate/contracts": "^11.0|^12.0|^13.0", + "illuminate/database": "^11.0|^12.0|^13.0", + "illuminate/support": "^11.0|^12.0|^13.0", + "php": "^8.2", + "symfony/console": "^7.0|^8.0" + }, + "require-dev": { + "mockery/mockery": "^1.6", + "orchestra/testbench": "^9.15|^10.8|^11.0", + "phpstan/phpstan": "^1.10" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Laravel\\Sanctum\\SanctumServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "Laravel\\Sanctum\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "Laravel Sanctum provides a featherweight authentication system for SPAs and simple APIs.", + "keywords": [ + "auth", + "laravel", + "sanctum" + ], + "support": { + "issues": "https://github.com/laravel/sanctum/issues", + "source": "https://github.com/laravel/sanctum" + }, + "time": "2026-02-07T17:19:31+00:00" + }, + { + "name": "laravel/sentinel", + "version": "v1.1.0", + "source": { + "type": "git", + "url": "https://github.com/laravel/sentinel.git", + "reference": "972d9885d9d14312a118e9565c4e6ecc5e751ea1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/sentinel/zipball/972d9885d9d14312a118e9565c4e6ecc5e751ea1", + "reference": "972d9885d9d14312a118e9565c4e6ecc5e751ea1", + "shasum": "" + }, + "require": { + "ext-json": "*", + "illuminate/container": "^8.37|^9.0|^10.0|^11.0|^12.0|^13.0", + "php": "^8.0" + }, + "require-dev": { + "laravel/pint": "^1.27", + "orchestra/testbench": "^6.47.1|^7.56|^8.37|^9.16|^10.9|^11.0", + "phpstan/phpstan": "^2.1.33" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Laravel\\Sentinel\\SentinelServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "Laravel\\Sentinel\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + }, + { + "name": "Mior Muhammad Zaki", + "email": "mior@laravel.com" + } + ], + "support": { + "source": "https://github.com/laravel/sentinel/tree/v1.1.0" + }, + "time": "2026-03-24T14:03:38+00:00" + }, + { + "name": "laravel/serializable-closure", + "version": "v2.0.10", + "source": { + "type": "git", + "url": "https://github.com/laravel/serializable-closure.git", + "reference": "870fc81d2f879903dfc5b60bf8a0f94a1609e669" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/serializable-closure/zipball/870fc81d2f879903dfc5b60bf8a0f94a1609e669", + "reference": "870fc81d2f879903dfc5b60bf8a0f94a1609e669", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "require-dev": { + "illuminate/support": "^10.0|^11.0|^12.0|^13.0", + "nesbot/carbon": "^2.67|^3.0", + "pestphp/pest": "^2.36|^3.0|^4.0", + "phpstan/phpstan": "^2.0", + "symfony/var-dumper": "^6.2.0|^7.0.0|^8.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.x-dev" + } + }, + "autoload": { + "psr-4": { + "Laravel\\SerializableClosure\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + }, + { + "name": "Nuno Maduro", + "email": "nuno@laravel.com" + } + ], + "description": "Laravel Serializable Closure provides an easy and secure way to serialize closures in PHP.", + "keywords": [ + "closure", + "laravel", + "serializable" + ], + "support": { + "issues": "https://github.com/laravel/serializable-closure/issues", + "source": "https://github.com/laravel/serializable-closure" + }, + "time": "2026-02-20T19:59:49+00:00" + }, + { + "name": "laravel/telescope", + "version": "v5.19.0", + "source": { + "type": "git", + "url": "https://github.com/laravel/telescope.git", + "reference": "5e95df170d14e03dd74c4b744969cf01f67a050b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/telescope/zipball/5e95df170d14e03dd74c4b744969cf01f67a050b", + "reference": "5e95df170d14e03dd74c4b744969cf01f67a050b", + "shasum": "" + }, + "require": { + "ext-json": "*", + "laravel/framework": "^8.37|^9.0|^10.0|^11.0|^12.0|^13.0", + "laravel/sentinel": "^1.0", + "php": "^8.0", + "symfony/console": "^5.3|^6.0|^7.0|^8.0", + "symfony/var-dumper": "^5.0|^6.0|^7.0|^8.0" + }, + "require-dev": { + "ext-gd": "*", + "guzzlehttp/guzzle": "^6.0|^7.0", + "laravel/octane": "^1.4|^2.0", + "orchestra/testbench": "^6.47.1|^7.55|^8.36|^9.15|^10.8|^11.0", + "phpstan/phpstan": "^1.10" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Laravel\\Telescope\\TelescopeServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "Laravel\\Telescope\\": "src/", + "Laravel\\Telescope\\Database\\Factories\\": "database/factories/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + }, + { + "name": "Mohamed Said", + "email": "mohamed@laravel.com" + } + ], + "description": "An elegant debug assistant for the Laravel framework.", + "keywords": [ + "debugging", + "laravel", + "monitoring" + ], + "support": { + "issues": "https://github.com/laravel/telescope/issues", + "source": "https://github.com/laravel/telescope/tree/v5.19.0" + }, + "time": "2026-03-24T18:37:14+00:00" + }, + { + "name": "laravel/tinker", + "version": "v2.11.1", + "source": { + "type": "git", + "url": "https://github.com/laravel/tinker.git", + "reference": "c9f80cc835649b5c1842898fb043f8cc098dd741" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/tinker/zipball/c9f80cc835649b5c1842898fb043f8cc098dd741", + "reference": "c9f80cc835649b5c1842898fb043f8cc098dd741", + "shasum": "" + }, + "require": { + "illuminate/console": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0|^12.0", + "illuminate/contracts": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0|^12.0", + "illuminate/support": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0|^12.0", + "php": "^7.2.5|^8.0", + "psy/psysh": "^0.11.1|^0.12.0", + "symfony/var-dumper": "^4.3.4|^5.0|^6.0|^7.0|^8.0" + }, + "require-dev": { + "mockery/mockery": "~1.3.3|^1.4.2", + "phpstan/phpstan": "^1.10", + "phpunit/phpunit": "^8.5.8|^9.3.3|^10.0" + }, + "suggest": { + "illuminate/database": "The Illuminate Database package (^6.0|^7.0|^8.0|^9.0|^10.0|^11.0|^12.0)." + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Laravel\\Tinker\\TinkerServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "Laravel\\Tinker\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "Powerful REPL for the Laravel framework.", + "keywords": [ + "REPL", + "Tinker", + "laravel", + "psysh" + ], + "support": { + "issues": "https://github.com/laravel/tinker/issues", + "source": "https://github.com/laravel/tinker/tree/v2.11.1" + }, + "time": "2026-02-06T14:12:35+00:00" + }, + { + "name": "league/commonmark", + "version": "2.8.0", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/commonmark.git", + "reference": "4efa10c1e56488e658d10adf7b7b7dcd19940bfb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/commonmark/zipball/4efa10c1e56488e658d10adf7b7b7dcd19940bfb", + "reference": "4efa10c1e56488e658d10adf7b7b7dcd19940bfb", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "league/config": "^1.1.1", + "php": "^7.4 || ^8.0", + "psr/event-dispatcher": "^1.0", + "symfony/deprecation-contracts": "^2.1 || ^3.0", + "symfony/polyfill-php80": "^1.16" + }, + "require-dev": { + "cebe/markdown": "^1.0", + "commonmark/cmark": "0.31.1", + "commonmark/commonmark.js": "0.31.1", + "composer/package-versions-deprecated": "^1.8", + "embed/embed": "^4.4", + "erusev/parsedown": "^1.0", + "ext-json": "*", + "github/gfm": "0.29.0", + "michelf/php-markdown": "^1.4 || ^2.0", + "nyholm/psr7": "^1.5", + "phpstan/phpstan": "^1.8.2", + "phpunit/phpunit": "^9.5.21 || ^10.5.9 || ^11.0.0", + "scrutinizer/ocular": "^1.8.1", + "symfony/finder": "^5.3 | ^6.0 | ^7.0", + "symfony/process": "^5.4 | ^6.0 | ^7.0", + "symfony/yaml": "^2.3 | ^3.0 | ^4.0 | ^5.0 | ^6.0 | ^7.0", + "unleashedtech/php-coding-standard": "^3.1.1", + "vimeo/psalm": "^4.24.0 || ^5.0.0 || ^6.0.0" + }, + "suggest": { + "symfony/yaml": "v2.3+ required if using the Front Matter extension" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "2.9-dev" + } + }, + "autoload": { + "psr-4": { + "League\\CommonMark\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Colin O'Dell", + "email": "colinodell@gmail.com", + "homepage": "https://www.colinodell.com", + "role": "Lead Developer" + } + ], + "description": "Highly-extensible PHP Markdown parser which fully supports the CommonMark spec and GitHub-Flavored Markdown (GFM)", + "homepage": "https://commonmark.thephpleague.com", + "keywords": [ + "commonmark", + "flavored", + "gfm", + "github", + "github-flavored", + "markdown", + "md", + "parser" + ], + "support": { + "docs": "https://commonmark.thephpleague.com/", + "forum": "https://github.com/thephpleague/commonmark/discussions", + "issues": "https://github.com/thephpleague/commonmark/issues", + "rss": "https://github.com/thephpleague/commonmark/releases.atom", + "source": "https://github.com/thephpleague/commonmark" + }, + "funding": [ + { + "url": "https://www.colinodell.com/sponsor", + "type": "custom" + }, + { + "url": "https://www.paypal.me/colinpodell/10.00", + "type": "custom" + }, + { + "url": "https://github.com/colinodell", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/league/commonmark", + "type": "tidelift" + } + ], + "time": "2025-11-26T21:48:24+00:00" + }, + { + "name": "league/config", + "version": "v1.2.0", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/config.git", + "reference": "754b3604fb2984c71f4af4a9cbe7b57f346ec1f3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/config/zipball/754b3604fb2984c71f4af4a9cbe7b57f346ec1f3", + "reference": "754b3604fb2984c71f4af4a9cbe7b57f346ec1f3", + "shasum": "" + }, + "require": { + "dflydev/dot-access-data": "^3.0.1", + "nette/schema": "^1.2", + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^1.8.2", + "phpunit/phpunit": "^9.5.5", + "scrutinizer/ocular": "^1.8.1", + "unleashedtech/php-coding-standard": "^3.1", + "vimeo/psalm": "^4.7.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.2-dev" + } + }, + "autoload": { + "psr-4": { + "League\\Config\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Colin O'Dell", + "email": "colinodell@gmail.com", + "homepage": "https://www.colinodell.com", + "role": "Lead Developer" + } + ], + "description": "Define configuration arrays with strict schemas and access values with dot notation", + "homepage": "https://config.thephpleague.com", + "keywords": [ + "array", + "config", + "configuration", + "dot", + "dot-access", + "nested", + "schema" + ], + "support": { + "docs": "https://config.thephpleague.com/", + "issues": "https://github.com/thephpleague/config/issues", + "rss": "https://github.com/thephpleague/config/releases.atom", + "source": "https://github.com/thephpleague/config" + }, + "funding": [ + { + "url": "https://www.colinodell.com/sponsor", + "type": "custom" + }, + { + "url": "https://www.paypal.me/colinpodell/10.00", + "type": "custom" + }, + { + "url": "https://github.com/colinodell", + "type": "github" + } + ], + "time": "2022-12-11T20:36:23+00:00" + }, + { + "name": "league/flysystem", + "version": "3.32.0", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/flysystem.git", + "reference": "254b1595b16b22dbddaaef9ed6ca9fdac4956725" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/flysystem/zipball/254b1595b16b22dbddaaef9ed6ca9fdac4956725", + "reference": "254b1595b16b22dbddaaef9ed6ca9fdac4956725", + "shasum": "" + }, + "require": { + "league/flysystem-local": "^3.0.0", + "league/mime-type-detection": "^1.0.0", + "php": "^8.0.2" + }, + "conflict": { + "async-aws/core": "<1.19.0", + "async-aws/s3": "<1.14.0", + "aws/aws-sdk-php": "3.209.31 || 3.210.0", + "guzzlehttp/guzzle": "<7.0", + "guzzlehttp/ringphp": "<1.1.1", + "phpseclib/phpseclib": "3.0.15", + "symfony/http-client": "<5.2" + }, + "require-dev": { + "async-aws/s3": "^1.5 || ^2.0", + "async-aws/simple-s3": "^1.1 || ^2.0", + "aws/aws-sdk-php": "^3.295.10", + "composer/semver": "^3.0", + "ext-fileinfo": "*", + "ext-ftp": "*", + "ext-mongodb": "^1.3|^2", + "ext-zip": "*", + "friendsofphp/php-cs-fixer": "^3.5", + "google/cloud-storage": "^1.23", + "guzzlehttp/psr7": "^2.6", + "microsoft/azure-storage-blob": "^1.1", + "mongodb/mongodb": "^1.2|^2", + "phpseclib/phpseclib": "^3.0.36", + "phpstan/phpstan": "^1.10", + "phpunit/phpunit": "^9.5.11|^10.0", + "sabre/dav": "^4.6.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "League\\Flysystem\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Frank de Jonge", + "email": "info@frankdejonge.nl" + } + ], + "description": "File storage abstraction for PHP", + "keywords": [ + "WebDAV", + "aws", + "cloud", + "file", + "files", + "filesystem", + "filesystems", + "ftp", + "s3", + "sftp", + "storage" + ], + "support": { + "issues": "https://github.com/thephpleague/flysystem/issues", + "source": "https://github.com/thephpleague/flysystem/tree/3.32.0" + }, + "time": "2026-02-25T17:01:41+00:00" + }, + { + "name": "league/flysystem-local", + "version": "3.31.0", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/flysystem-local.git", + "reference": "2f669db18a4c20c755c2bb7d3a7b0b2340488079" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/flysystem-local/zipball/2f669db18a4c20c755c2bb7d3a7b0b2340488079", + "reference": "2f669db18a4c20c755c2bb7d3a7b0b2340488079", + "shasum": "" + }, + "require": { + "ext-fileinfo": "*", + "league/flysystem": "^3.0.0", + "league/mime-type-detection": "^1.0.0", + "php": "^8.0.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "League\\Flysystem\\Local\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Frank de Jonge", + "email": "info@frankdejonge.nl" + } + ], + "description": "Local filesystem adapter for Flysystem.", + "keywords": [ + "Flysystem", + "file", + "files", + "filesystem", + "local" + ], + "support": { + "source": "https://github.com/thephpleague/flysystem-local/tree/3.31.0" + }, + "time": "2026-01-23T15:30:45+00:00" + }, + { + "name": "league/mime-type-detection", + "version": "1.16.0", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/mime-type-detection.git", + "reference": "2d6702ff215bf922936ccc1ad31007edc76451b9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/mime-type-detection/zipball/2d6702ff215bf922936ccc1ad31007edc76451b9", + "reference": "2d6702ff215bf922936ccc1ad31007edc76451b9", + "shasum": "" + }, + "require": { + "ext-fileinfo": "*", + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^3.2", + "phpstan/phpstan": "^0.12.68", + "phpunit/phpunit": "^8.5.8 || ^9.3 || ^10.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "League\\MimeTypeDetection\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Frank de Jonge", + "email": "info@frankdejonge.nl" + } + ], + "description": "Mime-type detection for Flysystem", + "support": { + "issues": "https://github.com/thephpleague/mime-type-detection/issues", + "source": "https://github.com/thephpleague/mime-type-detection/tree/1.16.0" + }, + "funding": [ + { + "url": "https://github.com/frankdejonge", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/league/flysystem", + "type": "tidelift" + } + ], + "time": "2024-09-21T08:32:55+00:00" + }, + { + "name": "league/uri", + "version": "7.8.0", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/uri.git", + "reference": "4436c6ec8d458e4244448b069cc572d088230b76" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/uri/zipball/4436c6ec8d458e4244448b069cc572d088230b76", + "reference": "4436c6ec8d458e4244448b069cc572d088230b76", + "shasum": "" + }, + "require": { + "league/uri-interfaces": "^7.8", + "php": "^8.1", + "psr/http-factory": "^1" + }, + "conflict": { + "league/uri-schemes": "^1.0" + }, + "suggest": { + "ext-bcmath": "to improve IPV4 host parsing", + "ext-dom": "to convert the URI into an HTML anchor tag", + "ext-fileinfo": "to create Data URI from file contennts", + "ext-gmp": "to improve IPV4 host parsing", + "ext-intl": "to handle IDN host with the best performance", + "ext-uri": "to use the PHP native URI class", + "jeremykendall/php-domain-parser": "to further parse the URI host and resolve its Public Suffix and Top Level Domain", + "league/uri-components": "to provide additional tools to manipulate URI objects components", + "league/uri-polyfill": "to backport the PHP URI extension for older versions of PHP", + "php-64bit": "to improve IPV4 host parsing", + "rowbot/url": "to handle URLs using the WHATWG URL Living Standard specification", + "symfony/polyfill-intl-idn": "to handle IDN host via the Symfony polyfill if ext-intl is not present" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "7.x-dev" + } + }, + "autoload": { + "psr-4": { + "League\\Uri\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ignace Nyamagana Butera", + "email": "nyamsprod@gmail.com", + "homepage": "https://nyamsprod.com" + } + ], + "description": "URI manipulation library", + "homepage": "https://uri.thephpleague.com", + "keywords": [ + "URN", + "data-uri", + "file-uri", + "ftp", + "hostname", + "http", + "https", + "middleware", + "parse_str", + "parse_url", + "psr-7", + "query-string", + "querystring", + "rfc2141", + "rfc3986", + "rfc3987", + "rfc6570", + "rfc8141", + "uri", + "uri-template", + "url", + "ws" + ], + "support": { + "docs": "https://uri.thephpleague.com", + "forum": "https://thephpleague.slack.com", + "issues": "https://github.com/thephpleague/uri-src/issues", + "source": "https://github.com/thephpleague/uri/tree/7.8.0" + }, + "funding": [ + { + "url": "https://github.com/sponsors/nyamsprod", + "type": "github" + } + ], + "time": "2026-01-14T17:24:56+00:00" + }, + { + "name": "league/uri-interfaces", + "version": "7.8.1", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/uri-interfaces.git", + "reference": "85d5c77c5d6d3af6c54db4a78246364908f3c928" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/uri-interfaces/zipball/85d5c77c5d6d3af6c54db4a78246364908f3c928", + "reference": "85d5c77c5d6d3af6c54db4a78246364908f3c928", + "shasum": "" + }, + "require": { + "ext-filter": "*", + "php": "^8.1", + "psr/http-message": "^1.1 || ^2.0" + }, + "suggest": { + "ext-bcmath": "to improve IPV4 host parsing", + "ext-gmp": "to improve IPV4 host parsing", + "ext-intl": "to handle IDN host with the best performance", + "php-64bit": "to improve IPV4 host parsing", + "rowbot/url": "to handle URLs using the WHATWG URL Living Standard specification", + "symfony/polyfill-intl-idn": "to handle IDN host via the Symfony polyfill if ext-intl is not present" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "7.x-dev" + } + }, + "autoload": { + "psr-4": { + "League\\Uri\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ignace Nyamagana Butera", + "email": "nyamsprod@gmail.com", + "homepage": "https://nyamsprod.com" + } + ], + "description": "Common tools for parsing and resolving RFC3987/RFC3986 URI", + "homepage": "https://uri.thephpleague.com", + "keywords": [ + "data-uri", + "file-uri", + "ftp", + "hostname", + "http", + "https", + "parse_str", + "parse_url", + "psr-7", + "query-string", + "querystring", + "rfc3986", + "rfc3987", + "rfc6570", + "uri", + "url", + "ws" + ], + "support": { + "docs": "https://uri.thephpleague.com", + "forum": "https://thephpleague.slack.com", + "issues": "https://github.com/thephpleague/uri-src/issues", + "source": "https://github.com/thephpleague/uri-interfaces/tree/7.8.1" + }, + "funding": [ + { + "url": "https://github.com/sponsors/nyamsprod", + "type": "github" + } + ], + "time": "2026-03-08T20:05:35+00:00" + }, + { + "name": "monolog/monolog", + "version": "3.10.0", + "source": { + "type": "git", + "url": "https://github.com/Seldaek/monolog.git", + "reference": "b321dd6749f0bf7189444158a3ce785cc16d69b0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Seldaek/monolog/zipball/b321dd6749f0bf7189444158a3ce785cc16d69b0", + "reference": "b321dd6749f0bf7189444158a3ce785cc16d69b0", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/log": "^2.0 || ^3.0" + }, + "provide": { + "psr/log-implementation": "3.0.0" + }, + "require-dev": { + "aws/aws-sdk-php": "^3.0", + "doctrine/couchdb": "~1.0@dev", + "elasticsearch/elasticsearch": "^7 || ^8", + "ext-json": "*", + "graylog2/gelf-php": "^1.4.2 || ^2.0", + "guzzlehttp/guzzle": "^7.4.5", + "guzzlehttp/psr7": "^2.2", + "mongodb/mongodb": "^1.8 || ^2.0", + "php-amqplib/php-amqplib": "~2.4 || ^3", + "php-console/php-console": "^3.1.8", + "phpstan/phpstan": "^2", + "phpstan/phpstan-deprecation-rules": "^2", + "phpstan/phpstan-strict-rules": "^2", + "phpunit/phpunit": "^10.5.17 || ^11.0.7", + "predis/predis": "^1.1 || ^2", + "rollbar/rollbar": "^4.0", + "ruflin/elastica": "^7 || ^8", + "symfony/mailer": "^5.4 || ^6", + "symfony/mime": "^5.4 || ^6" + }, + "suggest": { + "aws/aws-sdk-php": "Allow sending log messages to AWS services like DynamoDB", + "doctrine/couchdb": "Allow sending log messages to a CouchDB server", + "elasticsearch/elasticsearch": "Allow sending log messages to an Elasticsearch server via official client", + "ext-amqp": "Allow sending log messages to an AMQP server (1.0+ required)", + "ext-curl": "Required to send log messages using the IFTTTHandler, the LogglyHandler, the SendGridHandler, the SlackWebhookHandler or the TelegramBotHandler", + "ext-mbstring": "Allow to work properly with unicode symbols", + "ext-mongodb": "Allow sending log messages to a MongoDB server (via driver)", + "ext-openssl": "Required to send log messages using SSL", + "ext-sockets": "Allow sending log messages to a Syslog server (via UDP driver)", + "graylog2/gelf-php": "Allow sending log messages to a GrayLog2 server", + "mongodb/mongodb": "Allow sending log messages to a MongoDB server (via library)", + "php-amqplib/php-amqplib": "Allow sending log messages to an AMQP server using php-amqplib", + "rollbar/rollbar": "Allow sending log messages to Rollbar", + "ruflin/elastica": "Allow sending log messages to an Elastic Search server" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Monolog\\": "src/Monolog" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "https://seld.be" + } + ], + "description": "Sends your logs to files, sockets, inboxes, databases and various web services", + "homepage": "https://github.com/Seldaek/monolog", + "keywords": [ + "log", + "logging", + "psr-3" + ], + "support": { + "issues": "https://github.com/Seldaek/monolog/issues", + "source": "https://github.com/Seldaek/monolog/tree/3.10.0" + }, + "funding": [ + { + "url": "https://github.com/Seldaek", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/monolog/monolog", + "type": "tidelift" + } + ], + "time": "2026-01-02T08:56:05+00:00" + }, + { + "name": "nesbot/carbon", + "version": "3.11.1", + "source": { + "type": "git", + "url": "https://github.com/CarbonPHP/carbon.git", + "reference": "f438fcc98f92babee98381d399c65336f3a3827f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/CarbonPHP/carbon/zipball/f438fcc98f92babee98381d399c65336f3a3827f", + "reference": "f438fcc98f92babee98381d399c65336f3a3827f", + "shasum": "" + }, + "require": { + "carbonphp/carbon-doctrine-types": "<100.0", + "ext-json": "*", + "php": "^8.1", + "psr/clock": "^1.0", + "symfony/clock": "^6.3.12 || ^7.0 || ^8.0", + "symfony/polyfill-mbstring": "^1.0", + "symfony/translation": "^4.4.18 || ^5.2.1 || ^6.0 || ^7.0 || ^8.0" + }, + "provide": { + "psr/clock-implementation": "1.0" + }, + "require-dev": { + "doctrine/dbal": "^3.6.3 || ^4.0", + "doctrine/orm": "^2.15.2 || ^3.0", + "friendsofphp/php-cs-fixer": "^v3.87.1", + "kylekatarnls/multi-tester": "^2.5.3", + "phpmd/phpmd": "^2.15.0", + "phpstan/extension-installer": "^1.4.3", + "phpstan/phpstan": "^2.1.22", + "phpunit/phpunit": "^10.5.53", + "squizlabs/php_codesniffer": "^3.13.4 || ^4.0.0" + }, + "bin": [ + "bin/carbon" + ], + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Carbon\\Laravel\\ServiceProvider" + ] + }, + "phpstan": { + "includes": [ + "extension.neon" + ] + }, + "branch-alias": { + "dev-2.x": "2.x-dev", + "dev-master": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Carbon\\": "src/Carbon/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Brian Nesbitt", + "email": "brian@nesbot.com", + "homepage": "https://markido.com" + }, + { + "name": "kylekatarnls", + "homepage": "https://github.com/kylekatarnls" + } + ], + "description": "An API extension for DateTime that supports 281 different languages.", + "homepage": "https://carbonphp.github.io/carbon/", + "keywords": [ + "date", + "datetime", + "time" + ], + "support": { + "docs": "https://carbonphp.github.io/carbon/guide/getting-started/introduction.html", + "issues": "https://github.com/CarbonPHP/carbon/issues", + "source": "https://github.com/CarbonPHP/carbon" + }, + "funding": [ + { + "url": "https://github.com/sponsors/kylekatarnls", + "type": "github" + }, + { + "url": "https://opencollective.com/Carbon#sponsor", + "type": "opencollective" + }, + { + "url": "https://tidelift.com/subscription/pkg/packagist-nesbot-carbon?utm_source=packagist-nesbot-carbon&utm_medium=referral&utm_campaign=readme", + "type": "tidelift" + } + ], + "time": "2026-01-29T09:26:29+00:00" + }, + { + "name": "nette/schema", + "version": "v1.3.5", + "source": { + "type": "git", + "url": "https://github.com/nette/schema.git", + "reference": "f0ab1a3cda782dbc5da270d28545236aa80c4002" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nette/schema/zipball/f0ab1a3cda782dbc5da270d28545236aa80c4002", + "reference": "f0ab1a3cda782dbc5da270d28545236aa80c4002", + "shasum": "" + }, + "require": { + "nette/utils": "^4.0", + "php": "8.1 - 8.5" + }, + "require-dev": { + "nette/phpstan-rules": "^1.0", + "nette/tester": "^2.6", + "phpstan/extension-installer": "^1.4@stable", + "phpstan/phpstan": "^2.1.39@stable", + "tracy/tracy": "^2.8" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.3-dev" + } + }, + "autoload": { + "psr-4": { + "Nette\\": "src" + }, + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause", + "GPL-2.0-only", + "GPL-3.0-only" + ], + "authors": [ + { + "name": "David Grudl", + "homepage": "https://davidgrudl.com" + }, + { + "name": "Nette Community", + "homepage": "https://nette.org/contributors" + } + ], + "description": "📐 Nette Schema: validating data structures against a given Schema.", + "homepage": "https://nette.org", + "keywords": [ + "config", + "nette" + ], + "support": { + "issues": "https://github.com/nette/schema/issues", + "source": "https://github.com/nette/schema/tree/v1.3.5" + }, + "time": "2026-02-23T03:47:12+00:00" + }, + { + "name": "nette/utils", + "version": "v4.1.3", + "source": { + "type": "git", + "url": "https://github.com/nette/utils.git", + "reference": "bb3ea637e3d131d72acc033cfc2746ee893349fe" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nette/utils/zipball/bb3ea637e3d131d72acc033cfc2746ee893349fe", + "reference": "bb3ea637e3d131d72acc033cfc2746ee893349fe", + "shasum": "" + }, + "require": { + "php": "8.2 - 8.5" + }, + "conflict": { + "nette/finder": "<3", + "nette/schema": "<1.2.2" + }, + "require-dev": { + "jetbrains/phpstorm-attributes": "^1.2", + "nette/phpstan-rules": "^1.0", + "nette/tester": "^2.5", + "phpstan/extension-installer": "^1.4@stable", + "phpstan/phpstan": "^2.1@stable", + "tracy/tracy": "^2.9" + }, + "suggest": { + "ext-gd": "to use Image", + "ext-iconv": "to use Strings::webalize(), toAscii(), chr() and reverse()", + "ext-intl": "to use Strings::webalize(), toAscii(), normalize() and compare()", + "ext-json": "to use Nette\\Utils\\Json", + "ext-mbstring": "to use Strings::lower() etc...", + "ext-tokenizer": "to use Nette\\Utils\\Reflection::getUseStatements()" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.1-dev" + } + }, + "autoload": { + "psr-4": { + "Nette\\": "src" + }, + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause", + "GPL-2.0-only", + "GPL-3.0-only" + ], + "authors": [ + { + "name": "David Grudl", + "homepage": "https://davidgrudl.com" + }, + { + "name": "Nette Community", + "homepage": "https://nette.org/contributors" + } + ], + "description": "🛠 Nette Utils: lightweight utilities for string & array manipulation, image handling, safe JSON encoding/decoding, validation, slug or strong password generating etc.", + "homepage": "https://nette.org", + "keywords": [ + "array", + "core", + "datetime", + "images", + "json", + "nette", + "paginator", + "password", + "slugify", + "string", + "unicode", + "utf-8", + "utility", + "validation" + ], + "support": { + "issues": "https://github.com/nette/utils/issues", + "source": "https://github.com/nette/utils/tree/v4.1.3" + }, + "time": "2026-02-13T03:05:33+00:00" + }, + { + "name": "nikic/php-parser", + "version": "v5.7.0", + "source": { + "type": "git", + "url": "https://github.com/nikic/PHP-Parser.git", + "reference": "dca41cd15c2ac9d055ad70dbfd011130757d1f82" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/dca41cd15c2ac9d055ad70dbfd011130757d1f82", + "reference": "dca41cd15c2ac9d055ad70dbfd011130757d1f82", + "shasum": "" + }, + "require": { + "ext-ctype": "*", + "ext-json": "*", + "ext-tokenizer": "*", + "php": ">=7.4" + }, + "require-dev": { + "ircmaxell/php-yacc": "^0.0.7", + "phpunit/phpunit": "^9.0" + }, + "bin": [ + "bin/php-parse" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.x-dev" + } + }, + "autoload": { + "psr-4": { + "PhpParser\\": "lib/PhpParser" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Nikita Popov" + } + ], + "description": "A PHP parser written in PHP", + "keywords": [ + "parser", + "php" + ], + "support": { + "issues": "https://github.com/nikic/PHP-Parser/issues", + "source": "https://github.com/nikic/PHP-Parser/tree/v5.7.0" + }, + "time": "2025-12-06T11:56:16+00:00" + }, + { + "name": "nunomaduro/termwind", + "version": "v2.4.0", + "source": { + "type": "git", + "url": "https://github.com/nunomaduro/termwind.git", + "reference": "712a31b768f5daea284c2169a7d227031001b9a8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nunomaduro/termwind/zipball/712a31b768f5daea284c2169a7d227031001b9a8", + "reference": "712a31b768f5daea284c2169a7d227031001b9a8", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "php": "^8.2", + "symfony/console": "^7.4.4 || ^8.0.4" + }, + "require-dev": { + "illuminate/console": "^11.47.0", + "laravel/pint": "^1.27.1", + "mockery/mockery": "^1.6.12", + "pestphp/pest": "^2.36.0 || ^3.8.4 || ^4.3.2", + "phpstan/phpstan": "^1.12.32", + "phpstan/phpstan-strict-rules": "^1.6.2", + "symfony/var-dumper": "^7.3.5 || ^8.0.4", + "thecodingmachine/phpstan-strict-rules": "^1.0.0" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Termwind\\Laravel\\TermwindServiceProvider" + ] + }, + "branch-alias": { + "dev-2.x": "2.x-dev" + } + }, + "autoload": { + "files": [ + "src/Functions.php" + ], + "psr-4": { + "Termwind\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nuno Maduro", + "email": "enunomaduro@gmail.com" + } + ], + "description": "It's like Tailwind CSS, but for the console.", + "keywords": [ + "cli", + "console", + "css", + "package", + "php", + "style" + ], + "support": { + "issues": "https://github.com/nunomaduro/termwind/issues", + "source": "https://github.com/nunomaduro/termwind/tree/v2.4.0" + }, + "funding": [ + { + "url": "https://www.paypal.com/paypalme/enunomaduro", + "type": "custom" + }, + { + "url": "https://github.com/nunomaduro", + "type": "github" + }, + { + "url": "https://github.com/xiCO2k", + "type": "github" + } + ], + "time": "2026-02-16T23:10:27+00:00" + }, + { + "name": "paragonie/sodium_compat", + "version": "v2.5.0", + "source": { + "type": "git", + "url": "https://github.com/paragonie/sodium_compat.git", + "reference": "4714da6efdc782c06690bc72ce34fae7941c2d9f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/paragonie/sodium_compat/zipball/4714da6efdc782c06690bc72ce34fae7941c2d9f", + "reference": "4714da6efdc782c06690bc72ce34fae7941c2d9f", + "shasum": "" + }, + "require": { + "php": "^8.1", + "php-64bit": "*" + }, + "require-dev": { + "infection/infection": "^0", + "nikic/php-fuzzer": "^0", + "phpunit/phpunit": "^7|^8|^9|^10|^11", + "vimeo/psalm": "^4|^5|^6" + }, + "suggest": { + "ext-sodium": "Better performance, password hashing (Argon2i), secure memory management (memzero), and better security." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "files": [ + "autoload.php" + ], + "psr-4": { + "ParagonIE\\Sodium\\": "namespaced/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "ISC" + ], + "authors": [ + { + "name": "Paragon Initiative Enterprises", + "email": "security@paragonie.com" + }, + { + "name": "Frank Denis", + "email": "jedisct1@pureftpd.org" + } + ], + "description": "Pure PHP implementation of libsodium; uses the PHP extension if it exists", + "keywords": [ + "Authentication", + "BLAKE2b", + "ChaCha20", + "ChaCha20-Poly1305", + "Chapoly", + "Curve25519", + "Ed25519", + "EdDSA", + "Edwards-curve Digital Signature Algorithm", + "Elliptic Curve Diffie-Hellman", + "Poly1305", + "Pure-PHP cryptography", + "RFC 7748", + "RFC 8032", + "Salpoly", + "Salsa20", + "X25519", + "XChaCha20-Poly1305", + "XSalsa20-Poly1305", + "Xchacha20", + "Xsalsa20", + "aead", + "cryptography", + "ecdh", + "elliptic curve", + "elliptic curve cryptography", + "encryption", + "libsodium", + "php", + "public-key cryptography", + "secret-key cryptography", + "side-channel resistant" + ], + "support": { + "issues": "https://github.com/paragonie/sodium_compat/issues", + "source": "https://github.com/paragonie/sodium_compat/tree/v2.5.0" + }, + "time": "2025-12-30T16:12:18+00:00" + }, + { + "name": "phpoption/phpoption", + "version": "1.9.5", + "source": { + "type": "git", + "url": "https://github.com/schmittjoh/php-option.git", + "reference": "75365b91986c2405cf5e1e012c5595cd487a98be" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/schmittjoh/php-option/zipball/75365b91986c2405cf5e1e012c5595cd487a98be", + "reference": "75365b91986c2405cf5e1e012c5595cd487a98be", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "phpunit/phpunit": "^8.5.44 || ^9.6.25 || ^10.5.53 || ^11.5.34" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + }, + "branch-alias": { + "dev-master": "1.9-dev" + } + }, + "autoload": { + "psr-4": { + "PhpOption\\": "src/PhpOption/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "Johannes M. Schmitt", + "email": "schmittjoh@gmail.com", + "homepage": "https://github.com/schmittjoh" + }, + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + } + ], + "description": "Option Type for PHP", + "keywords": [ + "language", + "option", + "php", + "type" + ], + "support": { + "issues": "https://github.com/schmittjoh/php-option/issues", + "source": "https://github.com/schmittjoh/php-option/tree/1.9.5" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpoption/phpoption", + "type": "tidelift" + } + ], + "time": "2025-12-27T19:41:33+00:00" + }, + { + "name": "psr/clock", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/clock.git", + "reference": "e41a24703d4560fd0acb709162f73b8adfc3aa0d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/clock/zipball/e41a24703d4560fd0acb709162f73b8adfc3aa0d", + "reference": "e41a24703d4560fd0acb709162f73b8adfc3aa0d", + "shasum": "" + }, + "require": { + "php": "^7.0 || ^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Psr\\Clock\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for reading the clock.", + "homepage": "https://github.com/php-fig/clock", + "keywords": [ + "clock", + "now", + "psr", + "psr-20", + "time" + ], + "support": { + "issues": "https://github.com/php-fig/clock/issues", + "source": "https://github.com/php-fig/clock/tree/1.0.0" + }, + "time": "2022-11-25T14:36:26+00:00" + }, + { + "name": "psr/container", + "version": "2.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/container.git", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/container/zipball/c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "shasum": "" + }, + "require": { + "php": ">=7.4.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Container\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common Container Interface (PHP FIG PSR-11)", + "homepage": "https://github.com/php-fig/container", + "keywords": [ + "PSR-11", + "container", + "container-interface", + "container-interop", + "psr" + ], + "support": { + "issues": "https://github.com/php-fig/container/issues", + "source": "https://github.com/php-fig/container/tree/2.0.2" + }, + "time": "2021-11-05T16:47:00+00:00" + }, + { + "name": "psr/event-dispatcher", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/event-dispatcher.git", + "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/event-dispatcher/zipball/dbefd12671e8a14ec7f180cab83036ed26714bb0", + "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0", + "shasum": "" + }, + "require": { + "php": ">=7.2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\EventDispatcher\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Standard interfaces for event handling.", + "keywords": [ + "events", + "psr", + "psr-14" + ], + "support": { + "issues": "https://github.com/php-fig/event-dispatcher/issues", + "source": "https://github.com/php-fig/event-dispatcher/tree/1.0.0" + }, + "time": "2019-01-08T18:20:26+00:00" + }, + { + "name": "psr/http-client", + "version": "1.0.3", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-client.git", + "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-client/zipball/bb5906edc1c324c9a05aa0873d40117941e5fa90", + "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90", + "shasum": "" + }, + "require": { + "php": "^7.0 || ^8.0", + "psr/http-message": "^1.0 || ^2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Client\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP clients", + "homepage": "https://github.com/php-fig/http-client", + "keywords": [ + "http", + "http-client", + "psr", + "psr-18" + ], + "support": { + "source": "https://github.com/php-fig/http-client" + }, + "time": "2023-09-23T14:17:50+00:00" + }, + { + "name": "psr/http-factory", + "version": "1.1.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-factory.git", + "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-factory/zipball/2b4765fddfe3b508ac62f829e852b1501d3f6e8a", + "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a", + "shasum": "" + }, + "require": { + "php": ">=7.1", + "psr/http-message": "^1.0 || ^2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "PSR-17: Common interfaces for PSR-7 HTTP message factories", + "keywords": [ + "factory", + "http", + "message", + "psr", + "psr-17", + "psr-7", + "request", + "response" + ], + "support": { + "source": "https://github.com/php-fig/http-factory" + }, + "time": "2024-04-15T12:06:14+00:00" + }, + { + "name": "psr/http-message", + "version": "2.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-message.git", + "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-message/zipball/402d35bcb92c70c026d1a6a9883f06b2ead23d71", + "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP messages", + "homepage": "https://github.com/php-fig/http-message", + "keywords": [ + "http", + "http-message", + "psr", + "psr-7", + "request", + "response" + ], + "support": { + "source": "https://github.com/php-fig/http-message/tree/2.0" + }, + "time": "2023-04-04T09:54:51+00:00" + }, + { + "name": "psr/log", + "version": "3.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/log.git", + "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/log/zipball/f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", + "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", + "shasum": "" + }, + "require": { + "php": ">=8.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Log\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for logging libraries", + "homepage": "https://github.com/php-fig/log", + "keywords": [ + "log", + "psr", + "psr-3" + ], + "support": { + "source": "https://github.com/php-fig/log/tree/3.0.2" + }, + "time": "2024-09-11T13:17:53+00:00" + }, + { + "name": "psr/simple-cache", + "version": "3.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/simple-cache.git", + "reference": "764e0b3939f5ca87cb904f570ef9be2d78a07865" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/simple-cache/zipball/764e0b3939f5ca87cb904f570ef9be2d78a07865", + "reference": "764e0b3939f5ca87cb904f570ef9be2d78a07865", + "shasum": "" + }, + "require": { + "php": ">=8.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\SimpleCache\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interfaces for simple caching", + "keywords": [ + "cache", + "caching", + "psr", + "psr-16", + "simple-cache" + ], + "support": { + "source": "https://github.com/php-fig/simple-cache/tree/3.0.0" + }, + "time": "2021-10-29T13:26:27+00:00" + }, + { + "name": "psy/psysh", + "version": "v0.12.20", + "source": { + "type": "git", + "url": "https://github.com/bobthecow/psysh.git", + "reference": "19678eb6b952a03b8a1d96ecee9edba518bb0373" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/bobthecow/psysh/zipball/19678eb6b952a03b8a1d96ecee9edba518bb0373", + "reference": "19678eb6b952a03b8a1d96ecee9edba518bb0373", + "shasum": "" + }, + "require": { + "ext-json": "*", + "ext-tokenizer": "*", + "nikic/php-parser": "^5.0 || ^4.0", + "php": "^8.0 || ^7.4", + "symfony/console": "^8.0 || ^7.0 || ^6.0 || ^5.0 || ^4.0 || ^3.4", + "symfony/var-dumper": "^8.0 || ^7.0 || ^6.0 || ^5.0 || ^4.0 || ^3.4" + }, + "conflict": { + "symfony/console": "4.4.37 || 5.3.14 || 5.3.15 || 5.4.3 || 5.4.4 || 6.0.3 || 6.0.4" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.2", + "composer/class-map-generator": "^1.6" + }, + "suggest": { + "composer/class-map-generator": "Improved tab completion performance with better class discovery.", + "ext-pcntl": "Enabling the PCNTL extension makes PsySH a lot happier :)", + "ext-posix": "If you have PCNTL, you'll want the POSIX extension as well." + }, + "bin": [ + "bin/psysh" + ], + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": false, + "forward-command": false + }, + "branch-alias": { + "dev-main": "0.12.x-dev" + } + }, + "autoload": { + "files": [ + "src/functions.php" + ], + "psr-4": { + "Psy\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Justin Hileman", + "email": "justin@justinhileman.info" + } + ], + "description": "An interactive shell for modern PHP.", + "homepage": "https://psysh.org", + "keywords": [ + "REPL", + "console", + "interactive", + "shell" + ], + "support": { + "issues": "https://github.com/bobthecow/psysh/issues", + "source": "https://github.com/bobthecow/psysh/tree/v0.12.20" + }, + "time": "2026-02-11T15:05:28+00:00" + }, + { + "name": "pusher/pusher-php-server", + "version": "7.2.7", + "source": { + "type": "git", + "url": "https://github.com/pusher/pusher-http-php.git", + "reference": "148b0b5100d000ed57195acdf548a2b1b38ee3f7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/pusher/pusher-http-php/zipball/148b0b5100d000ed57195acdf548a2b1b38ee3f7", + "reference": "148b0b5100d000ed57195acdf548a2b1b38ee3f7", + "shasum": "" + }, + "require": { + "ext-curl": "*", + "ext-json": "*", + "guzzlehttp/guzzle": "^7.2", + "paragonie/sodium_compat": "^1.6|^2.0", + "php": "^7.3|^8.0", + "psr/log": "^1.0|^2.0|^3.0" + }, + "require-dev": { + "overtrue/phplint": "^2.3", + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.0-dev" + } + }, + "autoload": { + "psr-4": { + "Pusher\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Library for interacting with the Pusher REST API", + "keywords": [ + "events", + "messaging", + "php-pusher-server", + "publish", + "push", + "pusher", + "real time", + "real-time", + "realtime", + "rest", + "trigger" + ], + "support": { + "issues": "https://github.com/pusher/pusher-http-php/issues", + "source": "https://github.com/pusher/pusher-http-php/tree/7.2.7" + }, + "time": "2025-01-06T10:56:20+00:00" + }, + { + "name": "ralouphie/getallheaders", + "version": "3.0.3", + "source": { + "type": "git", + "url": "https://github.com/ralouphie/getallheaders.git", + "reference": "120b605dfeb996808c31b6477290a714d356e822" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ralouphie/getallheaders/zipball/120b605dfeb996808c31b6477290a714d356e822", + "reference": "120b605dfeb996808c31b6477290a714d356e822", + "shasum": "" + }, + "require": { + "php": ">=5.6" + }, + "require-dev": { + "php-coveralls/php-coveralls": "^2.1", + "phpunit/phpunit": "^5 || ^6.5" + }, + "type": "library", + "autoload": { + "files": [ + "src/getallheaders.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ralph Khattar", + "email": "ralph.khattar@gmail.com" + } + ], + "description": "A polyfill for getallheaders.", + "support": { + "issues": "https://github.com/ralouphie/getallheaders/issues", + "source": "https://github.com/ralouphie/getallheaders/tree/develop" + }, + "time": "2019-03-08T08:55:37+00:00" + }, + { + "name": "ramsey/collection", + "version": "2.1.1", + "source": { + "type": "git", + "url": "https://github.com/ramsey/collection.git", + "reference": "344572933ad0181accbf4ba763e85a0306a8c5e2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ramsey/collection/zipball/344572933ad0181accbf4ba763e85a0306a8c5e2", + "reference": "344572933ad0181accbf4ba763e85a0306a8c5e2", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "require-dev": { + "captainhook/plugin-composer": "^5.3", + "ergebnis/composer-normalize": "^2.45", + "fakerphp/faker": "^1.24", + "hamcrest/hamcrest-php": "^2.0", + "jangregor/phpstan-prophecy": "^2.1", + "mockery/mockery": "^1.6", + "php-parallel-lint/php-console-highlighter": "^1.0", + "php-parallel-lint/php-parallel-lint": "^1.4", + "phpspec/prophecy-phpunit": "^2.3", + "phpstan/extension-installer": "^1.4", + "phpstan/phpstan": "^2.1", + "phpstan/phpstan-mockery": "^2.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpunit/phpunit": "^10.5", + "ramsey/coding-standard": "^2.3", + "ramsey/conventional-commits": "^1.6", + "roave/security-advisories": "dev-latest" + }, + "type": "library", + "extra": { + "captainhook": { + "force-install": true + }, + "ramsey/conventional-commits": { + "configFile": "conventional-commits.json" + } + }, + "autoload": { + "psr-4": { + "Ramsey\\Collection\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ben Ramsey", + "email": "ben@benramsey.com", + "homepage": "https://benramsey.com" + } + ], + "description": "A PHP library for representing and manipulating collections.", + "keywords": [ + "array", + "collection", + "hash", + "map", + "queue", + "set" + ], + "support": { + "issues": "https://github.com/ramsey/collection/issues", + "source": "https://github.com/ramsey/collection/tree/2.1.1" + }, + "time": "2025-03-22T05:38:12+00:00" + }, + { + "name": "ramsey/uuid", + "version": "4.9.2", + "source": { + "type": "git", + "url": "https://github.com/ramsey/uuid.git", + "reference": "8429c78ca35a09f27565311b98101e2826affde0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ramsey/uuid/zipball/8429c78ca35a09f27565311b98101e2826affde0", + "reference": "8429c78ca35a09f27565311b98101e2826affde0", + "shasum": "" + }, + "require": { + "brick/math": "^0.8.16 || ^0.9 || ^0.10 || ^0.11 || ^0.12 || ^0.13 || ^0.14", + "php": "^8.0", + "ramsey/collection": "^1.2 || ^2.0" + }, + "replace": { + "rhumsaa/uuid": "self.version" + }, + "require-dev": { + "captainhook/captainhook": "^5.25", + "captainhook/plugin-composer": "^5.3", + "dealerdirect/phpcodesniffer-composer-installer": "^1.0", + "ergebnis/composer-normalize": "^2.47", + "mockery/mockery": "^1.6", + "paragonie/random-lib": "^2", + "php-mock/php-mock": "^2.6", + "php-mock/php-mock-mockery": "^1.5", + "php-parallel-lint/php-parallel-lint": "^1.4.0", + "phpbench/phpbench": "^1.2.14", + "phpstan/extension-installer": "^1.4", + "phpstan/phpstan": "^2.1", + "phpstan/phpstan-mockery": "^2.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpunit/phpunit": "^9.6", + "slevomat/coding-standard": "^8.18", + "squizlabs/php_codesniffer": "^3.13" + }, + "suggest": { + "ext-bcmath": "Enables faster math with arbitrary-precision integers using BCMath.", + "ext-gmp": "Enables faster math with arbitrary-precision integers using GMP.", + "ext-uuid": "Enables the use of PeclUuidTimeGenerator and PeclUuidRandomGenerator.", + "paragonie/random-lib": "Provides RandomLib for use with the RandomLibAdapter", + "ramsey/uuid-doctrine": "Allows the use of Ramsey\\Uuid\\Uuid as Doctrine field type." + }, + "type": "library", + "extra": { + "captainhook": { + "force-install": true + } + }, + "autoload": { + "files": [ + "src/functions.php" + ], + "psr-4": { + "Ramsey\\Uuid\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "A PHP library for generating and working with universally unique identifiers (UUIDs).", + "keywords": [ + "guid", + "identifier", + "uuid" + ], + "support": { + "issues": "https://github.com/ramsey/uuid/issues", + "source": "https://github.com/ramsey/uuid/tree/4.9.2" + }, + "time": "2025-12-14T04:43:48+00:00" + }, + { + "name": "ratchet/rfc6455", + "version": "v0.4.0", + "source": { + "type": "git", + "url": "https://github.com/ratchetphp/RFC6455.git", + "reference": "859d95f85dda0912c6d5b936d036d044e3af47ef" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ratchetphp/RFC6455/zipball/859d95f85dda0912c6d5b936d036d044e3af47ef", + "reference": "859d95f85dda0912c6d5b936d036d044e3af47ef", + "shasum": "" + }, + "require": { + "php": ">=7.4", + "psr/http-factory-implementation": "^1.0", + "symfony/polyfill-php80": "^1.15" + }, + "require-dev": { + "guzzlehttp/psr7": "^2.7", + "phpunit/phpunit": "^9.5", + "react/socket": "^1.3" + }, + "type": "library", + "autoload": { + "psr-4": { + "Ratchet\\RFC6455\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "role": "Developer" + }, + { + "name": "Matt Bonneau", + "role": "Developer" + } + ], + "description": "RFC6455 WebSocket protocol handler", + "homepage": "http://socketo.me", + "keywords": [ + "WebSockets", + "rfc6455", + "websocket" + ], + "support": { + "chat": "https://gitter.im/reactphp/reactphp", + "issues": "https://github.com/ratchetphp/RFC6455/issues", + "source": "https://github.com/ratchetphp/RFC6455/tree/v0.4.0" + }, + "time": "2025-02-24T01:18:22+00:00" + }, + { + "name": "react/cache", + "version": "v1.2.0", + "source": { + "type": "git", + "url": "https://github.com/reactphp/cache.git", + "reference": "d47c472b64aa5608225f47965a484b75c7817d5b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/cache/zipball/d47c472b64aa5608225f47965a484b75c7817d5b", + "reference": "d47c472b64aa5608225f47965a484b75c7817d5b", + "shasum": "" + }, + "require": { + "php": ">=5.3.0", + "react/promise": "^3.0 || ^2.0 || ^1.1" + }, + "require-dev": { + "phpunit/phpunit": "^9.5 || ^5.7 || ^4.8.35" + }, + "type": "library", + "autoload": { + "psr-4": { + "React\\Cache\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian Lück", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" + } + ], + "description": "Async, Promise-based cache interface for ReactPHP", + "keywords": [ + "cache", + "caching", + "promise", + "reactphp" + ], + "support": { + "issues": "https://github.com/reactphp/cache/issues", + "source": "https://github.com/reactphp/cache/tree/v1.2.0" + }, + "funding": [ + { + "url": "https://opencollective.com/reactphp", + "type": "open_collective" + } + ], + "time": "2022-11-30T15:59:55+00:00" + }, + { + "name": "react/dns", + "version": "v1.14.0", + "source": { + "type": "git", + "url": "https://github.com/reactphp/dns.git", + "reference": "7562c05391f42701c1fccf189c8225fece1cd7c3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/dns/zipball/7562c05391f42701c1fccf189c8225fece1cd7c3", + "reference": "7562c05391f42701c1fccf189c8225fece1cd7c3", + "shasum": "" + }, + "require": { + "php": ">=5.3.0", + "react/cache": "^1.0 || ^0.6 || ^0.5", + "react/event-loop": "^1.2", + "react/promise": "^3.2 || ^2.7 || ^1.2.1" + }, + "require-dev": { + "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36", + "react/async": "^4.3 || ^3 || ^2", + "react/promise-timer": "^1.11" + }, + "type": "library", + "autoload": { + "psr-4": { + "React\\Dns\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian Lück", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" + } + ], + "description": "Async DNS resolver for ReactPHP", + "keywords": [ + "async", + "dns", + "dns-resolver", + "reactphp" + ], + "support": { + "issues": "https://github.com/reactphp/dns/issues", + "source": "https://github.com/reactphp/dns/tree/v1.14.0" + }, + "funding": [ + { + "url": "https://opencollective.com/reactphp", + "type": "open_collective" + } + ], + "time": "2025-11-18T19:34:28+00:00" + }, + { + "name": "react/event-loop", + "version": "v1.6.0", + "source": { + "type": "git", + "url": "https://github.com/reactphp/event-loop.git", + "reference": "ba276bda6083df7e0050fd9b33f66ad7a4ac747a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/event-loop/zipball/ba276bda6083df7e0050fd9b33f66ad7a4ac747a", + "reference": "ba276bda6083df7e0050fd9b33f66ad7a4ac747a", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36" + }, + "suggest": { + "ext-pcntl": "For signal handling support when using the StreamSelectLoop" + }, + "type": "library", + "autoload": { + "psr-4": { + "React\\EventLoop\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian Lück", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" + } + ], + "description": "ReactPHP's core reactor event loop that libraries can use for evented I/O.", + "keywords": [ + "asynchronous", + "event-loop" + ], + "support": { + "issues": "https://github.com/reactphp/event-loop/issues", + "source": "https://github.com/reactphp/event-loop/tree/v1.6.0" + }, + "funding": [ + { + "url": "https://opencollective.com/reactphp", + "type": "open_collective" + } + ], + "time": "2025-11-17T20:46:25+00:00" + }, + { + "name": "react/promise", + "version": "v3.3.0", + "source": { + "type": "git", + "url": "https://github.com/reactphp/promise.git", + "reference": "23444f53a813a3296c1368bb104793ce8d88f04a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/promise/zipball/23444f53a813a3296c1368bb104793ce8d88f04a", + "reference": "23444f53a813a3296c1368bb104793ce8d88f04a", + "shasum": "" + }, + "require": { + "php": ">=7.1.0" + }, + "require-dev": { + "phpstan/phpstan": "1.12.28 || 1.4.10", + "phpunit/phpunit": "^9.6 || ^7.5" + }, + "type": "library", + "autoload": { + "files": [ + "src/functions_include.php" + ], + "psr-4": { + "React\\Promise\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Christian Lück", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" + } + ], + "description": "A lightweight implementation of CommonJS Promises/A for PHP", + "keywords": [ + "promise", + "promises" + ], + "support": { + "issues": "https://github.com/reactphp/promise/issues", + "source": "https://github.com/reactphp/promise/tree/v3.3.0" + }, + "funding": [ + { + "url": "https://opencollective.com/reactphp", + "type": "open_collective" + } + ], + "time": "2025-08-19T18:57:03+00:00" + }, + { + "name": "react/promise-timer", + "version": "v1.11.0", + "source": { + "type": "git", + "url": "https://github.com/reactphp/promise-timer.git", + "reference": "4f70306ed66b8b44768941ca7f142092600fafc1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/promise-timer/zipball/4f70306ed66b8b44768941ca7f142092600fafc1", + "reference": "4f70306ed66b8b44768941ca7f142092600fafc1", + "shasum": "" + }, + "require": { + "php": ">=5.3", + "react/event-loop": "^1.2", + "react/promise": "^3.2 || ^2.7.0 || ^1.2.1" + }, + "require-dev": { + "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36" + }, + "type": "library", + "autoload": { + "files": [ + "src/functions_include.php" + ], + "psr-4": { + "React\\Promise\\Timer\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian Lück", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" + } + ], + "description": "A trivial implementation of timeouts for Promises, built on top of ReactPHP.", + "homepage": "https://github.com/reactphp/promise-timer", + "keywords": [ + "async", + "event-loop", + "promise", + "reactphp", + "timeout", + "timer" + ], + "support": { + "issues": "https://github.com/reactphp/promise-timer/issues", + "source": "https://github.com/reactphp/promise-timer/tree/v1.11.0" + }, + "funding": [ + { + "url": "https://opencollective.com/reactphp", + "type": "open_collective" + } + ], + "time": "2024-06-04T14:27:45+00:00" + }, + { + "name": "react/socket", + "version": "v1.17.0", + "source": { + "type": "git", + "url": "https://github.com/reactphp/socket.git", + "reference": "ef5b17b81f6f60504c539313f94f2d826c5faa08" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/socket/zipball/ef5b17b81f6f60504c539313f94f2d826c5faa08", + "reference": "ef5b17b81f6f60504c539313f94f2d826c5faa08", + "shasum": "" + }, + "require": { + "evenement/evenement": "^3.0 || ^2.0 || ^1.0", + "php": ">=5.3.0", + "react/dns": "^1.13", + "react/event-loop": "^1.2", + "react/promise": "^3.2 || ^2.6 || ^1.2.1", + "react/stream": "^1.4" + }, + "require-dev": { + "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36", + "react/async": "^4.3 || ^3.3 || ^2", + "react/promise-stream": "^1.4", + "react/promise-timer": "^1.11" + }, + "type": "library", + "autoload": { + "psr-4": { + "React\\Socket\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian Lück", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" + } + ], + "description": "Async, streaming plaintext TCP/IP and secure TLS socket server and client connections for ReactPHP", + "keywords": [ + "Connection", + "Socket", + "async", + "reactphp", + "stream" + ], + "support": { + "issues": "https://github.com/reactphp/socket/issues", + "source": "https://github.com/reactphp/socket/tree/v1.17.0" + }, + "funding": [ + { + "url": "https://opencollective.com/reactphp", + "type": "open_collective" + } + ], + "time": "2025-11-19T20:47:34+00:00" + }, + { + "name": "react/stream", + "version": "v1.4.0", + "source": { + "type": "git", + "url": "https://github.com/reactphp/stream.git", + "reference": "1e5b0acb8fe55143b5b426817155190eb6f5b18d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/stream/zipball/1e5b0acb8fe55143b5b426817155190eb6f5b18d", + "reference": "1e5b0acb8fe55143b5b426817155190eb6f5b18d", + "shasum": "" + }, + "require": { + "evenement/evenement": "^3.0 || ^2.0 || ^1.0", + "php": ">=5.3.8", + "react/event-loop": "^1.2" + }, + "require-dev": { + "clue/stream-filter": "~1.2", + "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36" + }, + "type": "library", + "autoload": { + "psr-4": { + "React\\Stream\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian Lück", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" + } + ], + "description": "Event-driven readable and writable streams for non-blocking I/O in ReactPHP", + "keywords": [ + "event-driven", + "io", + "non-blocking", + "pipe", + "reactphp", + "readable", + "stream", + "writable" + ], + "support": { + "issues": "https://github.com/reactphp/stream/issues", + "source": "https://github.com/reactphp/stream/tree/v1.4.0" + }, + "funding": [ + { + "url": "https://opencollective.com/reactphp", + "type": "open_collective" + } + ], + "time": "2024-06-11T12:45:25+00:00" + }, + { + "name": "symfony/clock", + "version": "v7.4.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/clock.git", + "reference": "9169f24776edde469914c1e7a1442a50f7a4e110" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/clock/zipball/9169f24776edde469914c1e7a1442a50f7a4e110", + "reference": "9169f24776edde469914c1e7a1442a50f7a4e110", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "psr/clock": "^1.0", + "symfony/polyfill-php83": "^1.28" + }, + "provide": { + "psr/clock-implementation": "1.0" + }, + "type": "library", + "autoload": { + "files": [ + "Resources/now.php" + ], + "psr-4": { + "Symfony\\Component\\Clock\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Decouples applications from the system clock", + "homepage": "https://symfony.com", + "keywords": [ + "clock", + "psr20", + "time" + ], + "support": { + "source": "https://github.com/symfony/clock/tree/v7.4.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-11-12T15:39:26+00:00" + }, + { + "name": "symfony/console", + "version": "v7.4.6", + "source": { + "type": "git", + "url": "https://github.com/symfony/console.git", + "reference": "6d643a93b47398599124022eb24d97c153c12f27" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/console/zipball/6d643a93b47398599124022eb24d97c153c12f27", + "reference": "6d643a93b47398599124022eb24d97c153c12f27", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-mbstring": "~1.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/string": "^7.2|^8.0" + }, + "conflict": { + "symfony/dependency-injection": "<6.4", + "symfony/dotenv": "<6.4", + "symfony/event-dispatcher": "<6.4", + "symfony/lock": "<6.4", + "symfony/process": "<6.4" + }, + "provide": { + "psr/log-implementation": "1.0|2.0|3.0" + }, + "require-dev": { + "psr/log": "^1|^2|^3", + "symfony/config": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/event-dispatcher": "^6.4|^7.0|^8.0", + "symfony/http-foundation": "^6.4|^7.0|^8.0", + "symfony/http-kernel": "^6.4|^7.0|^8.0", + "symfony/lock": "^6.4|^7.0|^8.0", + "symfony/messenger": "^6.4|^7.0|^8.0", + "symfony/process": "^6.4|^7.0|^8.0", + "symfony/stopwatch": "^6.4|^7.0|^8.0", + "symfony/var-dumper": "^6.4|^7.0|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Console\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Eases the creation of beautiful and testable command line interfaces", + "homepage": "https://symfony.com", + "keywords": [ + "cli", + "command-line", + "console", + "terminal" + ], + "support": { + "source": "https://github.com/symfony/console/tree/v7.4.6" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-02-25T17:02:47+00:00" + }, + { + "name": "symfony/css-selector", + "version": "v7.4.6", + "source": { + "type": "git", + "url": "https://github.com/symfony/css-selector.git", + "reference": "2e7c52c647b406e2107dd867db424a4dbac91864" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/css-selector/zipball/2e7c52c647b406e2107dd867db424a4dbac91864", + "reference": "2e7c52c647b406e2107dd867db424a4dbac91864", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\CssSelector\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Jean-François Simon", + "email": "jeanfrancois.simon@sensiolabs.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Converts CSS selectors to XPath expressions", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/css-selector/tree/v7.4.6" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-02-17T07:53:42+00:00" + }, + { + "name": "symfony/deprecation-contracts", + "version": "v3.6.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/deprecation-contracts.git", + "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/63afe740e99a13ba87ec199bb07bbdee937a5b62", + "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.6-dev" + } + }, + "autoload": { + "files": [ + "function.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "A generic function and convention to trigger deprecation notices", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/deprecation-contracts/tree/v3.6.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-25T14:21:43+00:00" + }, + { + "name": "symfony/error-handler", + "version": "v7.4.4", + "source": { + "type": "git", + "url": "https://github.com/symfony/error-handler.git", + "reference": "8da531f364ddfee53e36092a7eebbbd0b775f6b8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/error-handler/zipball/8da531f364ddfee53e36092a7eebbbd0b775f6b8", + "reference": "8da531f364ddfee53e36092a7eebbbd0b775f6b8", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "psr/log": "^1|^2|^3", + "symfony/polyfill-php85": "^1.32", + "symfony/var-dumper": "^6.4|^7.0|^8.0" + }, + "conflict": { + "symfony/deprecation-contracts": "<2.5", + "symfony/http-kernel": "<6.4" + }, + "require-dev": { + "symfony/console": "^6.4|^7.0|^8.0", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/http-kernel": "^6.4|^7.0|^8.0", + "symfony/serializer": "^6.4|^7.0|^8.0", + "symfony/webpack-encore-bundle": "^1.0|^2.0" + }, + "bin": [ + "Resources/bin/patch-type-declarations" + ], + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\ErrorHandler\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides tools to manage errors and ease debugging PHP code", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/error-handler/tree/v7.4.4" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-01-20T16:42:42+00:00" + }, + { + "name": "symfony/event-dispatcher", + "version": "v7.4.4", + "source": { + "type": "git", + "url": "https://github.com/symfony/event-dispatcher.git", + "reference": "dc2c0eba1af673e736bb851d747d266108aea746" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/dc2c0eba1af673e736bb851d747d266108aea746", + "reference": "dc2c0eba1af673e736bb851d747d266108aea746", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/event-dispatcher-contracts": "^2.5|^3" + }, + "conflict": { + "symfony/dependency-injection": "<6.4", + "symfony/service-contracts": "<2.5" + }, + "provide": { + "psr/event-dispatcher-implementation": "1.0", + "symfony/event-dispatcher-implementation": "2.0|3.0" + }, + "require-dev": { + "psr/log": "^1|^2|^3", + "symfony/config": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/error-handler": "^6.4|^7.0|^8.0", + "symfony/expression-language": "^6.4|^7.0|^8.0", + "symfony/framework-bundle": "^6.4|^7.0|^8.0", + "symfony/http-foundation": "^6.4|^7.0|^8.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/stopwatch": "^6.4|^7.0|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\EventDispatcher\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/event-dispatcher/tree/v7.4.4" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-01-05T11:45:34+00:00" + }, + { + "name": "symfony/event-dispatcher-contracts", + "version": "v3.6.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/event-dispatcher-contracts.git", + "reference": "59eb412e93815df44f05f342958efa9f46b1e586" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/59eb412e93815df44f05f342958efa9f46b1e586", + "reference": "59eb412e93815df44f05f342958efa9f46b1e586", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/event-dispatcher": "^1" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.6-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\EventDispatcher\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to dispatching event", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v3.6.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-25T14:21:43+00:00" + }, + { + "name": "symfony/finder", + "version": "v7.4.6", + "source": { + "type": "git", + "url": "https://github.com/symfony/finder.git", + "reference": "8655bf1076b7a3a346cb11413ffdabff50c7ffcf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/finder/zipball/8655bf1076b7a3a346cb11413ffdabff50c7ffcf", + "reference": "8655bf1076b7a3a346cb11413ffdabff50c7ffcf", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "symfony/filesystem": "^6.4|^7.0|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Finder\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Finds files and directories via an intuitive fluent interface", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/finder/tree/v7.4.6" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-01-29T09:40:50+00:00" + }, + { + "name": "symfony/http-foundation", + "version": "v7.4.6", + "source": { + "type": "git", + "url": "https://github.com/symfony/http-foundation.git", + "reference": "fd97d5e926e988a363cef56fbbf88c5c528e9065" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/fd97d5e926e988a363cef56fbbf88c5c528e9065", + "reference": "fd97d5e926e988a363cef56fbbf88c5c528e9065", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-mbstring": "^1.1" + }, + "conflict": { + "doctrine/dbal": "<3.6", + "symfony/cache": "<6.4.12|>=7.0,<7.1.5" + }, + "require-dev": { + "doctrine/dbal": "^3.6|^4", + "predis/predis": "^1.1|^2.0", + "symfony/cache": "^6.4.12|^7.1.5|^8.0", + "symfony/clock": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/expression-language": "^6.4|^7.0|^8.0", + "symfony/http-kernel": "^6.4|^7.0|^8.0", + "symfony/mime": "^6.4|^7.0|^8.0", + "symfony/rate-limiter": "^6.4|^7.0|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\HttpFoundation\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Defines an object-oriented layer for the HTTP specification", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/http-foundation/tree/v7.4.6" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-02-21T16:25:55+00:00" + }, + { + "name": "symfony/http-kernel", + "version": "v7.4.6", + "source": { + "type": "git", + "url": "https://github.com/symfony/http-kernel.git", + "reference": "002ac0cf4cd972a7fd0912dcd513a95e8a81ce83" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/http-kernel/zipball/002ac0cf4cd972a7fd0912dcd513a95e8a81ce83", + "reference": "002ac0cf4cd972a7fd0912dcd513a95e8a81ce83", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "psr/log": "^1|^2|^3", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/error-handler": "^6.4|^7.0|^8.0", + "symfony/event-dispatcher": "^7.3|^8.0", + "symfony/http-foundation": "^7.4|^8.0", + "symfony/polyfill-ctype": "^1.8" + }, + "conflict": { + "symfony/browser-kit": "<6.4", + "symfony/cache": "<6.4", + "symfony/config": "<6.4", + "symfony/console": "<6.4", + "symfony/dependency-injection": "<6.4", + "symfony/doctrine-bridge": "<6.4", + "symfony/flex": "<2.10", + "symfony/form": "<6.4", + "symfony/http-client": "<6.4", + "symfony/http-client-contracts": "<2.5", + "symfony/mailer": "<6.4", + "symfony/messenger": "<6.4", + "symfony/translation": "<6.4", + "symfony/translation-contracts": "<2.5", + "symfony/twig-bridge": "<6.4", + "symfony/validator": "<6.4", + "symfony/var-dumper": "<6.4", + "twig/twig": "<3.12" + }, + "provide": { + "psr/log-implementation": "1.0|2.0|3.0" + }, + "require-dev": { + "psr/cache": "^1.0|^2.0|^3.0", + "symfony/browser-kit": "^6.4|^7.0|^8.0", + "symfony/clock": "^6.4|^7.0|^8.0", + "symfony/config": "^6.4|^7.0|^8.0", + "symfony/console": "^6.4|^7.0|^8.0", + "symfony/css-selector": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4.1|^7.0.1|^8.0", + "symfony/dom-crawler": "^6.4|^7.0|^8.0", + "symfony/expression-language": "^6.4|^7.0|^8.0", + "symfony/finder": "^6.4|^7.0|^8.0", + "symfony/http-client-contracts": "^2.5|^3", + "symfony/process": "^6.4|^7.0|^8.0", + "symfony/property-access": "^7.1|^8.0", + "symfony/routing": "^6.4|^7.0|^8.0", + "symfony/serializer": "^7.1|^8.0", + "symfony/stopwatch": "^6.4|^7.0|^8.0", + "symfony/translation": "^6.4|^7.0|^8.0", + "symfony/translation-contracts": "^2.5|^3", + "symfony/uid": "^6.4|^7.0|^8.0", + "symfony/validator": "^6.4|^7.0|^8.0", + "symfony/var-dumper": "^6.4|^7.0|^8.0", + "symfony/var-exporter": "^6.4|^7.0|^8.0", + "twig/twig": "^3.12" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\HttpKernel\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides a structured process for converting a Request into a Response", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/http-kernel/tree/v7.4.6" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-02-26T08:30:57+00:00" + }, + { + "name": "symfony/mailer", + "version": "v7.4.6", + "source": { + "type": "git", + "url": "https://github.com/symfony/mailer.git", + "reference": "b02726f39a20bc65e30364f5c750c4ddbf1f58e9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/mailer/zipball/b02726f39a20bc65e30364f5c750c4ddbf1f58e9", + "reference": "b02726f39a20bc65e30364f5c750c4ddbf1f58e9", + "shasum": "" + }, + "require": { + "egulias/email-validator": "^2.1.10|^3|^4", + "php": ">=8.2", + "psr/event-dispatcher": "^1", + "psr/log": "^1|^2|^3", + "symfony/event-dispatcher": "^6.4|^7.0|^8.0", + "symfony/mime": "^7.2|^8.0", + "symfony/service-contracts": "^2.5|^3" + }, + "conflict": { + "symfony/http-client-contracts": "<2.5", + "symfony/http-kernel": "<6.4", + "symfony/messenger": "<6.4", + "symfony/mime": "<6.4", + "symfony/twig-bridge": "<6.4" + }, + "require-dev": { + "symfony/console": "^6.4|^7.0|^8.0", + "symfony/http-client": "^6.4|^7.0|^8.0", + "symfony/messenger": "^6.4|^7.0|^8.0", + "symfony/twig-bridge": "^6.4|^7.0|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Mailer\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Helps sending emails", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/mailer/tree/v7.4.6" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-02-25T16:50:00+00:00" + }, + { + "name": "symfony/mime", + "version": "v7.4.6", + "source": { + "type": "git", + "url": "https://github.com/symfony/mime.git", + "reference": "9fc881d95feae4c6c48678cb6372bd8a7ba04f5f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/mime/zipball/9fc881d95feae4c6c48678cb6372bd8a7ba04f5f", + "reference": "9fc881d95feae4c6c48678cb6372bd8a7ba04f5f", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-intl-idn": "^1.10", + "symfony/polyfill-mbstring": "^1.0" + }, + "conflict": { + "egulias/email-validator": "~3.0.0", + "phpdocumentor/reflection-docblock": "<5.2|>=7", + "phpdocumentor/type-resolver": "<1.5.1", + "symfony/mailer": "<6.4", + "symfony/serializer": "<6.4.3|>7.0,<7.0.3" + }, + "require-dev": { + "egulias/email-validator": "^2.1.10|^3.1|^4", + "league/html-to-markdown": "^5.0", + "phpdocumentor/reflection-docblock": "^5.2|^6.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/process": "^6.4|^7.0|^8.0", + "symfony/property-access": "^6.4|^7.0|^8.0", + "symfony/property-info": "^6.4|^7.0|^8.0", + "symfony/serializer": "^6.4.3|^7.0.3|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Mime\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Allows manipulating MIME messages", + "homepage": "https://symfony.com", + "keywords": [ + "mime", + "mime-type" + ], + "support": { + "source": "https://github.com/symfony/mime/tree/v7.4.6" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-02-05T15:57:06+00:00" + }, + { + "name": "symfony/polyfill-ctype", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-ctype.git", + "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/a3cc8b044a6ea513310cbd48ef7333b384945638", + "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "provide": { + "ext-ctype": "*" + }, + "suggest": { + "ext-ctype": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Ctype\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Gert de Pagter", + "email": "BackEndTea@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for ctype functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "ctype", + "polyfill", + "portable" + ], + "support": { + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-09T11:45:10+00:00" + }, + { + "name": "symfony/polyfill-intl-grapheme", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-grapheme.git", + "reference": "380872130d3a5dd3ace2f4010d95125fde5d5c70" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/380872130d3a5dd3ace2f4010d95125fde5d5c70", + "reference": "380872130d3a5dd3ace2f4010d95125fde5d5c70", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Grapheme\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's grapheme_* functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "grapheme", + "intl", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-06-27T09:58:17+00:00" + }, + { + "name": "symfony/polyfill-intl-idn", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-idn.git", + "reference": "9614ac4d8061dc257ecc64cba1b140873dce8ad3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/9614ac4d8061dc257ecc64cba1b140873dce8ad3", + "reference": "9614ac4d8061dc257ecc64cba1b140873dce8ad3", + "shasum": "" + }, + "require": { + "php": ">=7.2", + "symfony/polyfill-intl-normalizer": "^1.10" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Idn\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Laurent Bassin", + "email": "laurent@bassin.info" + }, + { + "name": "Trevor Rowbotham", + "email": "trevor.rowbotham@pm.me" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's idn_to_ascii and idn_to_utf8 functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "idn", + "intl", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-10T14:38:51+00:00" + }, + { + "name": "symfony/polyfill-intl-normalizer", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-normalizer.git", + "reference": "3833d7255cc303546435cb650316bff708a1c75c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/3833d7255cc303546435cb650316bff708a1c75c", + "reference": "3833d7255cc303546435cb650316bff708a1c75c", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Normalizer\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's Normalizer class and related functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "intl", + "normalizer", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-09T11:45:10+00:00" + }, + { + "name": "symfony/polyfill-mbstring", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-mbstring.git", + "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/6d857f4d76bd4b343eac26d6b539585d2bc56493", + "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493", + "shasum": "" + }, + "require": { + "ext-iconv": "*", + "php": ">=7.2" + }, + "provide": { + "ext-mbstring": "*" + }, + "suggest": { + "ext-mbstring": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Mbstring\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for the Mbstring extension", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "mbstring", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-12-23T08:48:59+00:00" + }, + { + "name": "symfony/polyfill-php80", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php80.git", + "reference": "0cc9dd0f17f61d8131e7df6b84bd344899fe2608" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/0cc9dd0f17f61d8131e7df6b84bd344899fe2608", + "reference": "0cc9dd0f17f61d8131e7df6b84bd344899fe2608", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php80\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ion Bazan", + "email": "ion.bazan@gmail.com" + }, + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php80/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-01-02T08:10:11+00:00" + }, + { + "name": "symfony/polyfill-php83", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php83.git", + "reference": "17f6f9a6b1735c0f163024d959f700cfbc5155e5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php83/zipball/17f6f9a6b1735c0f163024d959f700cfbc5155e5", + "reference": "17f6f9a6b1735c0f163024d959f700cfbc5155e5", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php83\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.3+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php83/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-07-08T02:45:35+00:00" + }, + { + "name": "symfony/polyfill-php84", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php84.git", + "reference": "d8ced4d875142b6a7426000426b8abc631d6b191" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php84/zipball/d8ced4d875142b6a7426000426b8abc631d6b191", + "reference": "d8ced4d875142b6a7426000426b8abc631d6b191", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php84\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.4+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php84/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-06-24T13:30:11+00:00" + }, + { + "name": "symfony/polyfill-php85", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php85.git", + "reference": "d4e5fcd4ab3d998ab16c0db48e6cbb9a01993f91" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php85/zipball/d4e5fcd4ab3d998ab16c0db48e6cbb9a01993f91", + "reference": "d4e5fcd4ab3d998ab16c0db48e6cbb9a01993f91", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php85\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.5+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php85/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-06-23T16:12:55+00:00" + }, + { + "name": "symfony/polyfill-uuid", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-uuid.git", + "reference": "21533be36c24be3f4b1669c4725c7d1d2bab4ae2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-uuid/zipball/21533be36c24be3f4b1669c4725c7d1d2bab4ae2", + "reference": "21533be36c24be3f4b1669c4725c7d1d2bab4ae2", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "provide": { + "ext-uuid": "*" + }, + "suggest": { + "ext-uuid": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Uuid\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Grégoire Pineau", + "email": "lyrixx@lyrixx.info" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for uuid functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "uuid" + ], + "support": { + "source": "https://github.com/symfony/polyfill-uuid/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-09T11:45:10+00:00" + }, + { + "name": "symfony/process", + "version": "v7.4.5", + "source": { + "type": "git", + "url": "https://github.com/symfony/process.git", + "reference": "608476f4604102976d687c483ac63a79ba18cc97" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/process/zipball/608476f4604102976d687c483ac63a79ba18cc97", + "reference": "608476f4604102976d687c483ac63a79ba18cc97", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Process\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Executes commands in sub-processes", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/process/tree/v7.4.5" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-01-26T15:07:59+00:00" + }, + { + "name": "symfony/routing", + "version": "v7.4.6", + "source": { + "type": "git", + "url": "https://github.com/symfony/routing.git", + "reference": "238d749c56b804b31a9bf3e26519d93b65a60938" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/routing/zipball/238d749c56b804b31a9bf3e26519d93b65a60938", + "reference": "238d749c56b804b31a9bf3e26519d93b65a60938", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3" + }, + "conflict": { + "symfony/config": "<6.4", + "symfony/dependency-injection": "<6.4", + "symfony/yaml": "<6.4" + }, + "require-dev": { + "psr/log": "^1|^2|^3", + "symfony/config": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/expression-language": "^6.4|^7.0|^8.0", + "symfony/http-foundation": "^6.4|^7.0|^8.0", + "symfony/yaml": "^6.4|^7.0|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Routing\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Maps an HTTP request to a set of configuration variables", + "homepage": "https://symfony.com", + "keywords": [ + "router", + "routing", + "uri", + "url" + ], + "support": { + "source": "https://github.com/symfony/routing/tree/v7.4.6" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-02-25T16:50:00+00:00" + }, + { + "name": "symfony/service-contracts", + "version": "v3.6.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/service-contracts.git", + "reference": "45112560a3ba2d715666a509a0bc9521d10b6c43" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/45112560a3ba2d715666a509a0bc9521d10b6c43", + "reference": "45112560a3ba2d715666a509a0bc9521d10b6c43", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/container": "^1.1|^2.0", + "symfony/deprecation-contracts": "^2.5|^3" + }, + "conflict": { + "ext-psr": "<1.1|>=2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.6-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\Service\\": "" + }, + "exclude-from-classmap": [ + "/Test/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to writing services", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/service-contracts/tree/v3.6.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-07-15T11:30:57+00:00" + }, + { + "name": "symfony/string", + "version": "v7.4.6", + "source": { + "type": "git", + "url": "https://github.com/symfony/string.git", + "reference": "9f209231affa85aa930a5e46e6eb03381424b30b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/string/zipball/9f209231affa85aa930a5e46e6eb03381424b30b", + "reference": "9f209231affa85aa930a5e46e6eb03381424b30b", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3.0", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-intl-grapheme": "~1.33", + "symfony/polyfill-intl-normalizer": "~1.0", + "symfony/polyfill-mbstring": "~1.0" + }, + "conflict": { + "symfony/translation-contracts": "<2.5" + }, + "require-dev": { + "symfony/emoji": "^7.1|^8.0", + "symfony/http-client": "^6.4|^7.0|^8.0", + "symfony/intl": "^6.4|^7.0|^8.0", + "symfony/translation-contracts": "^2.5|^3.0", + "symfony/var-exporter": "^6.4|^7.0|^8.0" + }, + "type": "library", + "autoload": { + "files": [ + "Resources/functions.php" + ], + "psr-4": { + "Symfony\\Component\\String\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides an object-oriented API to strings and deals with bytes, UTF-8 code points and grapheme clusters in a unified way", + "homepage": "https://symfony.com", + "keywords": [ + "grapheme", + "i18n", + "string", + "unicode", + "utf-8", + "utf8" + ], + "support": { + "source": "https://github.com/symfony/string/tree/v7.4.6" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-02-09T09:33:46+00:00" + }, + { + "name": "symfony/translation", + "version": "v7.4.6", + "source": { + "type": "git", + "url": "https://github.com/symfony/translation.git", + "reference": "1888cf064399868af3784b9e043240f1d89d25ce" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/translation/zipball/1888cf064399868af3784b9e043240f1d89d25ce", + "reference": "1888cf064399868af3784b9e043240f1d89d25ce", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-mbstring": "~1.0", + "symfony/translation-contracts": "^2.5.3|^3.3" + }, + "conflict": { + "nikic/php-parser": "<5.0", + "symfony/config": "<6.4", + "symfony/console": "<6.4", + "symfony/dependency-injection": "<6.4", + "symfony/http-client-contracts": "<2.5", + "symfony/http-kernel": "<6.4", + "symfony/service-contracts": "<2.5", + "symfony/twig-bundle": "<6.4", + "symfony/yaml": "<6.4" + }, + "provide": { + "symfony/translation-implementation": "2.3|3.0" + }, + "require-dev": { + "nikic/php-parser": "^5.0", + "psr/log": "^1|^2|^3", + "symfony/config": "^6.4|^7.0|^8.0", + "symfony/console": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/finder": "^6.4|^7.0|^8.0", + "symfony/http-client-contracts": "^2.5|^3.0", + "symfony/http-kernel": "^6.4|^7.0|^8.0", + "symfony/intl": "^6.4|^7.0|^8.0", + "symfony/polyfill-intl-icu": "^1.21", + "symfony/routing": "^6.4|^7.0|^8.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/yaml": "^6.4|^7.0|^8.0" + }, + "type": "library", + "autoload": { + "files": [ + "Resources/functions.php" + ], + "psr-4": { + "Symfony\\Component\\Translation\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides tools to internationalize your application", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/translation/tree/v7.4.6" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-02-17T07:53:42+00:00" + }, + { + "name": "symfony/translation-contracts", + "version": "v3.6.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/translation-contracts.git", + "reference": "65a8bc82080447fae78373aa10f8d13b38338977" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/translation-contracts/zipball/65a8bc82080447fae78373aa10f8d13b38338977", + "reference": "65a8bc82080447fae78373aa10f8d13b38338977", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.6-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\Translation\\": "" + }, + "exclude-from-classmap": [ + "/Test/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to translation", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/translation-contracts/tree/v3.6.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-07-15T13:41:35+00:00" + }, + { + "name": "symfony/uid", + "version": "v7.4.4", + "source": { + "type": "git", + "url": "https://github.com/symfony/uid.git", + "reference": "7719ce8aba76be93dfe249192f1fbfa52c588e36" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/uid/zipball/7719ce8aba76be93dfe249192f1fbfa52c588e36", + "reference": "7719ce8aba76be93dfe249192f1fbfa52c588e36", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/polyfill-uuid": "^1.15" + }, + "require-dev": { + "symfony/console": "^6.4|^7.0|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Uid\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Grégoire Pineau", + "email": "lyrixx@lyrixx.info" + }, + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides an object-oriented API to generate and represent UIDs", + "homepage": "https://symfony.com", + "keywords": [ + "UID", + "ulid", + "uuid" + ], + "support": { + "source": "https://github.com/symfony/uid/tree/v7.4.4" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-01-03T23:30:35+00:00" + }, + { + "name": "symfony/var-dumper", + "version": "v7.4.6", + "source": { + "type": "git", + "url": "https://github.com/symfony/var-dumper.git", + "reference": "045321c440ac18347b136c63d2e9bf28a2dc0291" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/var-dumper/zipball/045321c440ac18347b136c63d2e9bf28a2dc0291", + "reference": "045321c440ac18347b136c63d2e9bf28a2dc0291", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-mbstring": "~1.0" + }, + "conflict": { + "symfony/console": "<6.4" + }, + "require-dev": { + "symfony/console": "^6.4|^7.0|^8.0", + "symfony/http-kernel": "^6.4|^7.0|^8.0", + "symfony/process": "^6.4|^7.0|^8.0", + "symfony/uid": "^6.4|^7.0|^8.0", + "twig/twig": "^3.12" + }, + "bin": [ + "Resources/bin/var-dump-server" + ], + "type": "library", + "autoload": { + "files": [ + "Resources/functions/dump.php" + ], + "psr-4": { + "Symfony\\Component\\VarDumper\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides mechanisms for walking through any arbitrary PHP variable", + "homepage": "https://symfony.com", + "keywords": [ + "debug", + "dump" + ], + "support": { + "source": "https://github.com/symfony/var-dumper/tree/v7.4.6" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-02-15T10:53:20+00:00" + }, + { + "name": "tijsverkoyen/css-to-inline-styles", + "version": "v2.4.0", + "source": { + "type": "git", + "url": "https://github.com/tijsverkoyen/CssToInlineStyles.git", + "reference": "f0292ccf0ec75843d65027214426b6b163b48b41" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/tijsverkoyen/CssToInlineStyles/zipball/f0292ccf0ec75843d65027214426b6b163b48b41", + "reference": "f0292ccf0ec75843d65027214426b6b163b48b41", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "php": "^7.4 || ^8.0", + "symfony/css-selector": "^5.4 || ^6.0 || ^7.0 || ^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^2.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpunit/phpunit": "^8.5.21 || ^9.5.10" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.x-dev" + } + }, + "autoload": { + "psr-4": { + "TijsVerkoyen\\CssToInlineStyles\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Tijs Verkoyen", + "email": "css_to_inline_styles@verkoyen.eu", + "role": "Developer" + } + ], + "description": "CssToInlineStyles is a class that enables you to convert HTML-pages/files into HTML-pages/files with inline styles. This is very useful when you're sending emails.", + "homepage": "https://github.com/tijsverkoyen/CssToInlineStyles", + "support": { + "issues": "https://github.com/tijsverkoyen/CssToInlineStyles/issues", + "source": "https://github.com/tijsverkoyen/CssToInlineStyles/tree/v2.4.0" + }, + "time": "2025-12-02T11:56:42+00:00" + }, + { + "name": "vlucas/phpdotenv", + "version": "v5.6.3", + "source": { + "type": "git", + "url": "https://github.com/vlucas/phpdotenv.git", + "reference": "955e7815d677a3eaa7075231212f2110983adecc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/vlucas/phpdotenv/zipball/955e7815d677a3eaa7075231212f2110983adecc", + "reference": "955e7815d677a3eaa7075231212f2110983adecc", + "shasum": "" + }, + "require": { + "ext-pcre": "*", + "graham-campbell/result-type": "^1.1.4", + "php": "^7.2.5 || ^8.0", + "phpoption/phpoption": "^1.9.5", + "symfony/polyfill-ctype": "^1.26", + "symfony/polyfill-mbstring": "^1.26", + "symfony/polyfill-php80": "^1.26" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "ext-filter": "*", + "phpunit/phpunit": "^8.5.34 || ^9.6.13 || ^10.4.2" + }, + "suggest": { + "ext-filter": "Required to use the boolean validator." + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + }, + "branch-alias": { + "dev-master": "5.6-dev" + } + }, + "autoload": { + "psr-4": { + "Dotenv\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Vance Lucas", + "email": "vance@vancelucas.com", + "homepage": "https://github.com/vlucas" + } + ], + "description": "Loads environment variables from `.env` to `getenv()`, `$_ENV` and `$_SERVER` automagically.", + "keywords": [ + "dotenv", + "env", + "environment" + ], + "support": { + "issues": "https://github.com/vlucas/phpdotenv/issues", + "source": "https://github.com/vlucas/phpdotenv/tree/v5.6.3" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/vlucas/phpdotenv", + "type": "tidelift" + } + ], + "time": "2025-12-27T19:49:13+00:00" + }, + { + "name": "voku/portable-ascii", + "version": "2.0.3", + "source": { + "type": "git", + "url": "https://github.com/voku/portable-ascii.git", + "reference": "b1d923f88091c6bf09699efcd7c8a1b1bfd7351d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/voku/portable-ascii/zipball/b1d923f88091c6bf09699efcd7c8a1b1bfd7351d", + "reference": "b1d923f88091c6bf09699efcd7c8a1b1bfd7351d", + "shasum": "" + }, + "require": { + "php": ">=7.0.0" + }, + "require-dev": { + "phpunit/phpunit": "~6.0 || ~7.0 || ~9.0" + }, + "suggest": { + "ext-intl": "Use Intl for transliterator_transliterate() support" + }, + "type": "library", + "autoload": { + "psr-4": { + "voku\\": "src/voku/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Lars Moelleken", + "homepage": "https://www.moelleken.org/" + } + ], + "description": "Portable ASCII library - performance optimized (ascii) string functions for php.", + "homepage": "https://github.com/voku/portable-ascii", + "keywords": [ + "ascii", + "clean", + "php" + ], + "support": { + "issues": "https://github.com/voku/portable-ascii/issues", + "source": "https://github.com/voku/portable-ascii/tree/2.0.3" + }, + "funding": [ + { + "url": "https://www.paypal.me/moelleken", + "type": "custom" + }, + { + "url": "https://github.com/voku", + "type": "github" + }, + { + "url": "https://opencollective.com/portable-ascii", + "type": "open_collective" + }, + { + "url": "https://www.patreon.com/voku", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/voku/portable-ascii", + "type": "tidelift" + } + ], + "time": "2024-11-21T01:49:47+00:00" + } + ], + "packages-dev": [ + { + "name": "fakerphp/faker", + "version": "v1.24.1", + "source": { + "type": "git", + "url": "https://github.com/FakerPHP/Faker.git", + "reference": "e0ee18eb1e6dc3cda3ce9fd97e5a0689a88a64b5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/FakerPHP/Faker/zipball/e0ee18eb1e6dc3cda3ce9fd97e5a0689a88a64b5", + "reference": "e0ee18eb1e6dc3cda3ce9fd97e5a0689a88a64b5", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0", + "psr/container": "^1.0 || ^2.0", + "symfony/deprecation-contracts": "^2.2 || ^3.0" + }, + "conflict": { + "fzaninotto/faker": "*" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.4.1", + "doctrine/persistence": "^1.3 || ^2.0", + "ext-intl": "*", + "phpunit/phpunit": "^9.5.26", + "symfony/phpunit-bridge": "^5.4.16" + }, + "suggest": { + "doctrine/orm": "Required to use Faker\\ORM\\Doctrine", + "ext-curl": "Required by Faker\\Provider\\Image to download images.", + "ext-dom": "Required by Faker\\Provider\\HtmlLorem for generating random HTML.", + "ext-iconv": "Required by Faker\\Provider\\ru_RU\\Text::realText() for generating real Russian text.", + "ext-mbstring": "Required for multibyte Unicode string functionality." + }, + "type": "library", + "autoload": { + "psr-4": { + "Faker\\": "src/Faker/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "François Zaninotto" + } + ], + "description": "Faker is a PHP library that generates fake data for you.", + "keywords": [ + "data", + "faker", + "fixtures" + ], + "support": { + "issues": "https://github.com/FakerPHP/Faker/issues", + "source": "https://github.com/FakerPHP/Faker/tree/v1.24.1" + }, + "time": "2024-11-21T13:46:39+00:00" + }, + { + "name": "filp/whoops", + "version": "2.18.4", + "source": { + "type": "git", + "url": "https://github.com/filp/whoops.git", + "reference": "d2102955e48b9fd9ab24280a7ad12ed552752c4d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/filp/whoops/zipball/d2102955e48b9fd9ab24280a7ad12ed552752c4d", + "reference": "d2102955e48b9fd9ab24280a7ad12ed552752c4d", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0", + "psr/log": "^1.0.1 || ^2.0 || ^3.0" + }, + "require-dev": { + "mockery/mockery": "^1.0", + "phpunit/phpunit": "^7.5.20 || ^8.5.8 || ^9.3.3", + "symfony/var-dumper": "^4.0 || ^5.0" + }, + "suggest": { + "symfony/var-dumper": "Pretty print complex values better with var-dumper available", + "whoops/soap": "Formats errors as SOAP responses" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.7-dev" + } + }, + "autoload": { + "psr-4": { + "Whoops\\": "src/Whoops/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Filipe Dobreira", + "homepage": "https://github.com/filp", + "role": "Developer" + } + ], + "description": "php error handling for cool kids", + "homepage": "https://filp.github.io/whoops/", + "keywords": [ + "error", + "exception", + "handling", + "library", + "throwable", + "whoops" + ], + "support": { + "issues": "https://github.com/filp/whoops/issues", + "source": "https://github.com/filp/whoops/tree/2.18.4" + }, + "funding": [ + { + "url": "https://github.com/denis-sokolov", + "type": "github" + } + ], + "time": "2025-08-08T12:00:00+00:00" + }, + { + "name": "hamcrest/hamcrest-php", + "version": "v2.1.1", + "source": { + "type": "git", + "url": "https://github.com/hamcrest/hamcrest-php.git", + "reference": "f8b1c0173b22fa6ec77a81fe63e5b01eba7e6487" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/hamcrest/hamcrest-php/zipball/f8b1c0173b22fa6ec77a81fe63e5b01eba7e6487", + "reference": "f8b1c0173b22fa6ec77a81fe63e5b01eba7e6487", + "shasum": "" + }, + "require": { + "php": "^7.4|^8.0" + }, + "replace": { + "cordoval/hamcrest-php": "*", + "davedevelopment/hamcrest-php": "*", + "kodova/hamcrest-php": "*" + }, + "require-dev": { + "phpunit/php-file-iterator": "^1.4 || ^2.0 || ^3.0", + "phpunit/phpunit": "^4.8.36 || ^5.7 || ^6.5 || ^7.0 || ^8.0 || ^9.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.1-dev" + } + }, + "autoload": { + "classmap": [ + "hamcrest" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "description": "This is the PHP port of Hamcrest Matchers", + "keywords": [ + "test" + ], + "support": { + "issues": "https://github.com/hamcrest/hamcrest-php/issues", + "source": "https://github.com/hamcrest/hamcrest-php/tree/v2.1.1" + }, + "time": "2025-04-30T06:54:44+00:00" + }, + { + "name": "laravel/pail", + "version": "v1.2.6", + "source": { + "type": "git", + "url": "https://github.com/laravel/pail.git", + "reference": "aa71a01c309e7f66bc2ec4fb1a59291b82eb4abf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/pail/zipball/aa71a01c309e7f66bc2ec4fb1a59291b82eb4abf", + "reference": "aa71a01c309e7f66bc2ec4fb1a59291b82eb4abf", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "illuminate/console": "^10.24|^11.0|^12.0|^13.0", + "illuminate/contracts": "^10.24|^11.0|^12.0|^13.0", + "illuminate/log": "^10.24|^11.0|^12.0|^13.0", + "illuminate/process": "^10.24|^11.0|^12.0|^13.0", + "illuminate/support": "^10.24|^11.0|^12.0|^13.0", + "nunomaduro/termwind": "^1.15|^2.0", + "php": "^8.2", + "symfony/console": "^6.0|^7.0|^8.0" + }, + "require-dev": { + "laravel/framework": "^10.24|^11.0|^12.0|^13.0", + "laravel/pint": "^1.13", + "orchestra/testbench-core": "^8.13|^9.17|^10.8|^11.0", + "pestphp/pest": "^2.20|^3.0|^4.0", + "pestphp/pest-plugin-type-coverage": "^2.3|^3.0|^4.0", + "phpstan/phpstan": "^1.12.27", + "symfony/var-dumper": "^6.3|^7.0|^8.0", + "symfony/yaml": "^6.3|^7.0|^8.0" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Laravel\\Pail\\PailServiceProvider" + ] + }, + "branch-alias": { + "dev-main": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Laravel\\Pail\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + }, + { + "name": "Nuno Maduro", + "email": "enunomaduro@gmail.com" + } + ], + "description": "Easily delve into your Laravel application's log files directly from the command line.", + "homepage": "https://github.com/laravel/pail", + "keywords": [ + "dev", + "laravel", + "logs", + "php", + "tail" + ], + "support": { + "issues": "https://github.com/laravel/pail/issues", + "source": "https://github.com/laravel/pail" + }, + "time": "2026-02-09T13:44:54+00:00" + }, + { + "name": "laravel/pint", + "version": "v1.27.1", + "source": { + "type": "git", + "url": "https://github.com/laravel/pint.git", + "reference": "54cca2de13790570c7b6f0f94f37896bee4abcb5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/pint/zipball/54cca2de13790570c7b6f0f94f37896bee4abcb5", + "reference": "54cca2de13790570c7b6f0f94f37896bee4abcb5", + "shasum": "" + }, + "require": { + "ext-json": "*", + "ext-mbstring": "*", + "ext-tokenizer": "*", + "ext-xml": "*", + "php": "^8.2.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^3.93.1", + "illuminate/view": "^12.51.0", + "larastan/larastan": "^3.9.2", + "laravel-zero/framework": "^12.0.5", + "mockery/mockery": "^1.6.12", + "nunomaduro/termwind": "^2.3.3", + "pestphp/pest": "^3.8.5" + }, + "bin": [ + "builds/pint" + ], + "type": "project", + "autoload": { + "psr-4": { + "App\\": "app/", + "Database\\Seeders\\": "database/seeders/", + "Database\\Factories\\": "database/factories/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nuno Maduro", + "email": "enunomaduro@gmail.com" + } + ], + "description": "An opinionated code formatter for PHP.", + "homepage": "https://laravel.com", + "keywords": [ + "dev", + "format", + "formatter", + "lint", + "linter", + "php" + ], + "support": { + "issues": "https://github.com/laravel/pint/issues", + "source": "https://github.com/laravel/pint" + }, + "time": "2026-02-10T20:00:20+00:00" + }, + { + "name": "laravel/sail", + "version": "v1.53.0", + "source": { + "type": "git", + "url": "https://github.com/laravel/sail.git", + "reference": "e340eaa2bea9b99192570c48ed837155dbf24fbb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/sail/zipball/e340eaa2bea9b99192570c48ed837155dbf24fbb", + "reference": "e340eaa2bea9b99192570c48ed837155dbf24fbb", + "shasum": "" + }, + "require": { + "illuminate/console": "^9.52.16|^10.0|^11.0|^12.0|^13.0", + "illuminate/contracts": "^9.52.16|^10.0|^11.0|^12.0|^13.0", + "illuminate/support": "^9.52.16|^10.0|^11.0|^12.0|^13.0", + "php": "^8.0", + "symfony/console": "^6.0|^7.0|^8.0", + "symfony/yaml": "^6.0|^7.0|^8.0" + }, + "require-dev": { + "orchestra/testbench": "^7.0|^8.0|^9.0|^10.0|^11.0", + "phpstan/phpstan": "^2.0" + }, + "bin": [ + "bin/sail" + ], + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Laravel\\Sail\\SailServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "Laravel\\Sail\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "Docker files for running a basic Laravel application.", + "keywords": [ + "docker", + "laravel" + ], + "support": { + "issues": "https://github.com/laravel/sail/issues", + "source": "https://github.com/laravel/sail" + }, + "time": "2026-02-06T12:16:02+00:00" + }, + { + "name": "mockery/mockery", + "version": "1.6.12", + "source": { + "type": "git", + "url": "https://github.com/mockery/mockery.git", + "reference": "1f4efdd7d3beafe9807b08156dfcb176d18f1699" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/mockery/mockery/zipball/1f4efdd7d3beafe9807b08156dfcb176d18f1699", + "reference": "1f4efdd7d3beafe9807b08156dfcb176d18f1699", + "shasum": "" + }, + "require": { + "hamcrest/hamcrest-php": "^2.0.1", + "lib-pcre": ">=7.0", + "php": ">=7.3" + }, + "conflict": { + "phpunit/phpunit": "<8.0" + }, + "require-dev": { + "phpunit/phpunit": "^8.5 || ^9.6.17", + "symplify/easy-coding-standard": "^12.1.14" + }, + "type": "library", + "autoload": { + "files": [ + "library/helpers.php", + "library/Mockery.php" + ], + "psr-4": { + "Mockery\\": "library/Mockery" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Pádraic Brady", + "email": "padraic.brady@gmail.com", + "homepage": "https://github.com/padraic", + "role": "Author" + }, + { + "name": "Dave Marshall", + "email": "dave.marshall@atstsolutions.co.uk", + "homepage": "https://davedevelopment.co.uk", + "role": "Developer" + }, + { + "name": "Nathanael Esayeas", + "email": "nathanael.esayeas@protonmail.com", + "homepage": "https://github.com/ghostwriter", + "role": "Lead Developer" + } + ], + "description": "Mockery is a simple yet flexible PHP mock object framework", + "homepage": "https://github.com/mockery/mockery", + "keywords": [ + "BDD", + "TDD", + "library", + "mock", + "mock objects", + "mockery", + "stub", + "test", + "test double", + "testing" + ], + "support": { + "docs": "https://docs.mockery.io/", + "issues": "https://github.com/mockery/mockery/issues", + "rss": "https://github.com/mockery/mockery/releases.atom", + "security": "https://github.com/mockery/mockery/security/advisories", + "source": "https://github.com/mockery/mockery" + }, + "time": "2024-05-16T03:13:13+00:00" + }, + { + "name": "myclabs/deep-copy", + "version": "1.13.4", + "source": { + "type": "git", + "url": "https://github.com/myclabs/DeepCopy.git", + "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/07d290f0c47959fd5eed98c95ee5602db07e0b6a", + "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "conflict": { + "doctrine/collections": "<1.6.8", + "doctrine/common": "<2.13.3 || >=3 <3.2.2" + }, + "require-dev": { + "doctrine/collections": "^1.6.8", + "doctrine/common": "^2.13.3 || ^3.2.2", + "phpspec/prophecy": "^1.10", + "phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13" + }, + "type": "library", + "autoload": { + "files": [ + "src/DeepCopy/deep_copy.php" + ], + "psr-4": { + "DeepCopy\\": "src/DeepCopy/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Create deep copies (clones) of your objects", + "keywords": [ + "clone", + "copy", + "duplicate", + "object", + "object graph" + ], + "support": { + "issues": "https://github.com/myclabs/DeepCopy/issues", + "source": "https://github.com/myclabs/DeepCopy/tree/1.13.4" + }, + "funding": [ + { + "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy", + "type": "tidelift" + } + ], + "time": "2025-08-01T08:46:24+00:00" + }, + { + "name": "nunomaduro/collision", + "version": "v8.9.1", + "source": { + "type": "git", + "url": "https://github.com/nunomaduro/collision.git", + "reference": "a1ed3fa530fd60bc515f9303e8520fcb7d4bd935" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nunomaduro/collision/zipball/a1ed3fa530fd60bc515f9303e8520fcb7d4bd935", + "reference": "a1ed3fa530fd60bc515f9303e8520fcb7d4bd935", + "shasum": "" + }, + "require": { + "filp/whoops": "^2.18.4", + "nunomaduro/termwind": "^2.4.0", + "php": "^8.2.0", + "symfony/console": "^7.4.4 || ^8.0.4" + }, + "conflict": { + "laravel/framework": "<11.48.0 || >=14.0.0", + "phpunit/phpunit": "<11.5.50 || >=14.0.0" + }, + "require-dev": { + "brianium/paratest": "^7.8.5", + "larastan/larastan": "^3.9.2", + "laravel/framework": "^11.48.0 || ^12.52.0", + "laravel/pint": "^1.27.1", + "orchestra/testbench-core": "^9.12.0 || ^10.9.0", + "pestphp/pest": "^3.8.5 || ^4.4.1 || ^5.0.0", + "sebastian/environment": "^7.2.1 || ^8.0.3 || ^9.0.0" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "NunoMaduro\\Collision\\Adapters\\Laravel\\CollisionServiceProvider" + ] + }, + "branch-alias": { + "dev-8.x": "8.x-dev" + } + }, + "autoload": { + "files": [ + "./src/Adapters/Phpunit/Autoload.php" + ], + "psr-4": { + "NunoMaduro\\Collision\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nuno Maduro", + "email": "enunomaduro@gmail.com" + } + ], + "description": "Cli error handling for console/command-line PHP applications.", + "keywords": [ + "artisan", + "cli", + "command-line", + "console", + "dev", + "error", + "handling", + "laravel", + "laravel-zero", + "php", + "symfony" + ], + "support": { + "issues": "https://github.com/nunomaduro/collision/issues", + "source": "https://github.com/nunomaduro/collision" + }, + "funding": [ + { + "url": "https://www.paypal.com/paypalme/enunomaduro", + "type": "custom" + }, + { + "url": "https://github.com/nunomaduro", + "type": "github" + }, + { + "url": "https://www.patreon.com/nunomaduro", + "type": "patreon" + } + ], + "time": "2026-02-17T17:33:08+00:00" + }, + { + "name": "phar-io/manifest", + "version": "2.0.4", + "source": { + "type": "git", + "url": "https://github.com/phar-io/manifest.git", + "reference": "54750ef60c58e43759730615a392c31c80e23176" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/manifest/zipball/54750ef60c58e43759730615a392c31c80e23176", + "reference": "54750ef60c58e43759730615a392c31c80e23176", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "ext-phar": "*", + "ext-xmlwriter": "*", + "phar-io/version": "^3.0.1", + "php": "^7.2 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)", + "support": { + "issues": "https://github.com/phar-io/manifest/issues", + "source": "https://github.com/phar-io/manifest/tree/2.0.4" + }, + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2024-03-03T12:33:53+00:00" + }, + { + "name": "phar-io/version", + "version": "3.2.1", + "source": { + "type": "git", + "url": "https://github.com/phar-io/version.git", + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/version/zipball/4f7fd7836c6f332bb2933569e566a0d6c4cbed74", + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Library for handling version information and constraints", + "support": { + "issues": "https://github.com/phar-io/version/issues", + "source": "https://github.com/phar-io/version/tree/3.2.1" + }, + "time": "2022-02-21T01:04:05+00:00" + }, + { + "name": "phpunit/php-code-coverage", + "version": "11.0.12", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-code-coverage.git", + "reference": "2c1ed04922802c15e1de5d7447b4856de949cf56" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/2c1ed04922802c15e1de5d7447b4856de949cf56", + "reference": "2c1ed04922802c15e1de5d7447b4856de949cf56", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "ext-xmlwriter": "*", + "nikic/php-parser": "^5.7.0", + "php": ">=8.2", + "phpunit/php-file-iterator": "^5.1.0", + "phpunit/php-text-template": "^4.0.1", + "sebastian/code-unit-reverse-lookup": "^4.0.1", + "sebastian/complexity": "^4.0.1", + "sebastian/environment": "^7.2.1", + "sebastian/lines-of-code": "^3.0.1", + "sebastian/version": "^5.0.2", + "theseer/tokenizer": "^1.3.1" + }, + "require-dev": { + "phpunit/phpunit": "^11.5.46" + }, + "suggest": { + "ext-pcov": "PHP extension that provides line coverage", + "ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "11.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.", + "homepage": "https://github.com/sebastianbergmann/php-code-coverage", + "keywords": [ + "coverage", + "testing", + "xunit" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", + "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy", + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/11.0.12" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/php-code-coverage", + "type": "tidelift" + } + ], + "time": "2025-12-24T07:01:01+00:00" + }, + { + "name": "phpunit/php-file-iterator", + "version": "5.1.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-file-iterator.git", + "reference": "2f3a64888c814fc235386b7387dd5b5ed92ad903" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/2f3a64888c814fc235386b7387dd5b5ed92ad903", + "reference": "2f3a64888c814fc235386b7387dd5b5ed92ad903", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "FilterIterator implementation that filters files based on a list of suffixes.", + "homepage": "https://github.com/sebastianbergmann/php-file-iterator/", + "keywords": [ + "filesystem", + "iterator" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues", + "security": "https://github.com/sebastianbergmann/php-file-iterator/security/policy", + "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/5.1.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/php-file-iterator", + "type": "tidelift" + } + ], + "time": "2026-02-02T13:52:54+00:00" + }, + { + "name": "phpunit/php-invoker", + "version": "5.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-invoker.git", + "reference": "c1ca3814734c07492b3d4c5f794f4b0995333da2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/c1ca3814734c07492b3d4c5f794f4b0995333da2", + "reference": "c1ca3814734c07492b3d4c5f794f4b0995333da2", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "ext-pcntl": "*", + "phpunit/phpunit": "^11.0" + }, + "suggest": { + "ext-pcntl": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Invoke callables with a timeout", + "homepage": "https://github.com/sebastianbergmann/php-invoker/", + "keywords": [ + "process" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-invoker/issues", + "security": "https://github.com/sebastianbergmann/php-invoker/security/policy", + "source": "https://github.com/sebastianbergmann/php-invoker/tree/5.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T05:07:44+00:00" + }, + { + "name": "phpunit/php-text-template", + "version": "4.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-text-template.git", + "reference": "3e0404dc6b300e6bf56415467ebcb3fe4f33e964" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/3e0404dc6b300e6bf56415467ebcb3fe4f33e964", + "reference": "3e0404dc6b300e6bf56415467ebcb3fe4f33e964", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Simple template engine.", + "homepage": "https://github.com/sebastianbergmann/php-text-template/", + "keywords": [ + "template" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-text-template/issues", + "security": "https://github.com/sebastianbergmann/php-text-template/security/policy", + "source": "https://github.com/sebastianbergmann/php-text-template/tree/4.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T05:08:43+00:00" + }, + { + "name": "phpunit/php-timer", + "version": "7.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-timer.git", + "reference": "3b415def83fbcb41f991d9ebf16ae4ad8b7837b3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/3b415def83fbcb41f991d9ebf16ae4ad8b7837b3", + "reference": "3b415def83fbcb41f991d9ebf16ae4ad8b7837b3", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "7.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Utility class for timing", + "homepage": "https://github.com/sebastianbergmann/php-timer/", + "keywords": [ + "timer" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-timer/issues", + "security": "https://github.com/sebastianbergmann/php-timer/security/policy", + "source": "https://github.com/sebastianbergmann/php-timer/tree/7.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T05:09:35+00:00" + }, + { + "name": "phpunit/phpunit", + "version": "11.5.55", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/phpunit.git", + "reference": "adc7262fccc12de2b30f12a8aa0b33775d814f00" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/adc7262fccc12de2b30f12a8aa0b33775d814f00", + "reference": "adc7262fccc12de2b30f12a8aa0b33775d814f00", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-json": "*", + "ext-libxml": "*", + "ext-mbstring": "*", + "ext-xml": "*", + "ext-xmlwriter": "*", + "myclabs/deep-copy": "^1.13.4", + "phar-io/manifest": "^2.0.4", + "phar-io/version": "^3.2.1", + "php": ">=8.2", + "phpunit/php-code-coverage": "^11.0.12", + "phpunit/php-file-iterator": "^5.1.1", + "phpunit/php-invoker": "^5.0.1", + "phpunit/php-text-template": "^4.0.1", + "phpunit/php-timer": "^7.0.1", + "sebastian/cli-parser": "^3.0.2", + "sebastian/code-unit": "^3.0.3", + "sebastian/comparator": "^6.3.3", + "sebastian/diff": "^6.0.2", + "sebastian/environment": "^7.2.1", + "sebastian/exporter": "^6.3.2", + "sebastian/global-state": "^7.0.2", + "sebastian/object-enumerator": "^6.0.1", + "sebastian/recursion-context": "^6.0.3", + "sebastian/type": "^5.1.3", + "sebastian/version": "^5.0.2", + "staabm/side-effects-detector": "^1.0.5" + }, + "suggest": { + "ext-soap": "To be able to generate mocks based on WSDL files" + }, + "bin": [ + "phpunit" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "11.5-dev" + } + }, + "autoload": { + "files": [ + "src/Framework/Assert/Functions.php" + ], + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "The PHP Unit Testing framework.", + "homepage": "https://phpunit.de/", + "keywords": [ + "phpunit", + "testing", + "xunit" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/phpunit/issues", + "security": "https://github.com/sebastianbergmann/phpunit/security/policy", + "source": "https://github.com/sebastianbergmann/phpunit/tree/11.5.55" + }, + "funding": [ + { + "url": "https://phpunit.de/sponsors.html", + "type": "custom" + }, + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/phpunit", + "type": "tidelift" + } + ], + "time": "2026-02-18T12:37:06+00:00" + }, + { + "name": "sebastian/cli-parser", + "version": "3.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/cli-parser.git", + "reference": "15c5dd40dc4f38794d383bb95465193f5e0ae180" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/15c5dd40dc4f38794d383bb95465193f5e0ae180", + "reference": "15c5dd40dc4f38794d383bb95465193f5e0ae180", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for parsing CLI options", + "homepage": "https://github.com/sebastianbergmann/cli-parser", + "support": { + "issues": "https://github.com/sebastianbergmann/cli-parser/issues", + "security": "https://github.com/sebastianbergmann/cli-parser/security/policy", + "source": "https://github.com/sebastianbergmann/cli-parser/tree/3.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T04:41:36+00:00" + }, + { + "name": "sebastian/code-unit", + "version": "3.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/code-unit.git", + "reference": "54391c61e4af8078e5b276ab082b6d3c54c9ad64" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit/zipball/54391c61e4af8078e5b276ab082b6d3c54c9ad64", + "reference": "54391c61e4af8078e5b276ab082b6d3c54c9ad64", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Collection of value objects that represent the PHP code units", + "homepage": "https://github.com/sebastianbergmann/code-unit", + "support": { + "issues": "https://github.com/sebastianbergmann/code-unit/issues", + "security": "https://github.com/sebastianbergmann/code-unit/security/policy", + "source": "https://github.com/sebastianbergmann/code-unit/tree/3.0.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-03-19T07:56:08+00:00" + }, + { + "name": "sebastian/code-unit-reverse-lookup", + "version": "4.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git", + "reference": "183a9b2632194febd219bb9246eee421dad8d45e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/183a9b2632194febd219bb9246eee421dad8d45e", + "reference": "183a9b2632194febd219bb9246eee421dad8d45e", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Looks up which function or method a line of code belongs to", + "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/", + "support": { + "issues": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/issues", + "security": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/security/policy", + "source": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/tree/4.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T04:45:54+00:00" + }, + { + "name": "sebastian/comparator", + "version": "6.3.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/comparator.git", + "reference": "2c95e1e86cb8dd41beb8d502057d1081ccc8eca9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/2c95e1e86cb8dd41beb8d502057d1081ccc8eca9", + "reference": "2c95e1e86cb8dd41beb8d502057d1081ccc8eca9", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-mbstring": "*", + "php": ">=8.2", + "sebastian/diff": "^6.0", + "sebastian/exporter": "^6.0" + }, + "require-dev": { + "phpunit/phpunit": "^11.4" + }, + "suggest": { + "ext-bcmath": "For comparing BcMath\\Number objects" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.3-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@2bepublished.at" + } + ], + "description": "Provides the functionality to compare PHP values for equality", + "homepage": "https://github.com/sebastianbergmann/comparator", + "keywords": [ + "comparator", + "compare", + "equality" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/comparator/issues", + "security": "https://github.com/sebastianbergmann/comparator/security/policy", + "source": "https://github.com/sebastianbergmann/comparator/tree/6.3.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/comparator", + "type": "tidelift" + } + ], + "time": "2026-01-24T09:26:40+00:00" + }, + { + "name": "sebastian/complexity", + "version": "4.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/complexity.git", + "reference": "ee41d384ab1906c68852636b6de493846e13e5a0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/ee41d384ab1906c68852636b6de493846e13e5a0", + "reference": "ee41d384ab1906c68852636b6de493846e13e5a0", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^5.0", + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for calculating the complexity of PHP code units", + "homepage": "https://github.com/sebastianbergmann/complexity", + "support": { + "issues": "https://github.com/sebastianbergmann/complexity/issues", + "security": "https://github.com/sebastianbergmann/complexity/security/policy", + "source": "https://github.com/sebastianbergmann/complexity/tree/4.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T04:49:50+00:00" + }, + { + "name": "sebastian/diff", + "version": "6.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/diff.git", + "reference": "b4ccd857127db5d41a5b676f24b51371d76d8544" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/b4ccd857127db5d41a5b676f24b51371d76d8544", + "reference": "b4ccd857127db5d41a5b676f24b51371d76d8544", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.0", + "symfony/process": "^4.2 || ^5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Kore Nordmann", + "email": "mail@kore-nordmann.de" + } + ], + "description": "Diff implementation", + "homepage": "https://github.com/sebastianbergmann/diff", + "keywords": [ + "diff", + "udiff", + "unidiff", + "unified diff" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/diff/issues", + "security": "https://github.com/sebastianbergmann/diff/security/policy", + "source": "https://github.com/sebastianbergmann/diff/tree/6.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T04:53:05+00:00" + }, + { + "name": "sebastian/environment", + "version": "7.2.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/environment.git", + "reference": "a5c75038693ad2e8d4b6c15ba2403532647830c4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/a5c75038693ad2e8d4b6c15ba2403532647830c4", + "reference": "a5c75038693ad2e8d4b6c15ba2403532647830c4", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.3" + }, + "suggest": { + "ext-posix": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "7.2-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides functionality to handle HHVM/PHP environments", + "homepage": "https://github.com/sebastianbergmann/environment", + "keywords": [ + "Xdebug", + "environment", + "hhvm" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/environment/issues", + "security": "https://github.com/sebastianbergmann/environment/security/policy", + "source": "https://github.com/sebastianbergmann/environment/tree/7.2.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/environment", + "type": "tidelift" + } + ], + "time": "2025-05-21T11:55:47+00:00" + }, + { + "name": "sebastian/exporter", + "version": "6.3.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/exporter.git", + "reference": "70a298763b40b213ec087c51c739efcaa90bcd74" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/70a298763b40b213ec087c51c739efcaa90bcd74", + "reference": "70a298763b40b213ec087c51c739efcaa90bcd74", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "php": ">=8.2", + "sebastian/recursion-context": "^6.0" + }, + "require-dev": { + "phpunit/phpunit": "^11.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.3-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" + } + ], + "description": "Provides the functionality to export PHP variables for visualization", + "homepage": "https://www.github.com/sebastianbergmann/exporter", + "keywords": [ + "export", + "exporter" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/exporter/issues", + "security": "https://github.com/sebastianbergmann/exporter/security/policy", + "source": "https://github.com/sebastianbergmann/exporter/tree/6.3.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/exporter", + "type": "tidelift" + } + ], + "time": "2025-09-24T06:12:51+00:00" + }, + { + "name": "sebastian/global-state", + "version": "7.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/global-state.git", + "reference": "3be331570a721f9a4b5917f4209773de17f747d7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/3be331570a721f9a4b5917f4209773de17f747d7", + "reference": "3be331570a721f9a4b5917f4209773de17f747d7", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "sebastian/object-reflector": "^4.0", + "sebastian/recursion-context": "^6.0" + }, + "require-dev": { + "ext-dom": "*", + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "7.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Snapshotting of global state", + "homepage": "https://www.github.com/sebastianbergmann/global-state", + "keywords": [ + "global state" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/global-state/issues", + "security": "https://github.com/sebastianbergmann/global-state/security/policy", + "source": "https://github.com/sebastianbergmann/global-state/tree/7.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T04:57:36+00:00" + }, + { + "name": "sebastian/lines-of-code", + "version": "3.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/lines-of-code.git", + "reference": "d36ad0d782e5756913e42ad87cb2890f4ffe467a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/d36ad0d782e5756913e42ad87cb2890f4ffe467a", + "reference": "d36ad0d782e5756913e42ad87cb2890f4ffe467a", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^5.0", + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for counting the lines of code in PHP source code", + "homepage": "https://github.com/sebastianbergmann/lines-of-code", + "support": { + "issues": "https://github.com/sebastianbergmann/lines-of-code/issues", + "security": "https://github.com/sebastianbergmann/lines-of-code/security/policy", + "source": "https://github.com/sebastianbergmann/lines-of-code/tree/3.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T04:58:38+00:00" + }, + { + "name": "sebastian/object-enumerator", + "version": "6.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-enumerator.git", + "reference": "f5b498e631a74204185071eb41f33f38d64608aa" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/f5b498e631a74204185071eb41f33f38d64608aa", + "reference": "f5b498e631a74204185071eb41f33f38d64608aa", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "sebastian/object-reflector": "^4.0", + "sebastian/recursion-context": "^6.0" + }, + "require-dev": { + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Traverses array structures and object graphs to enumerate all referenced objects", + "homepage": "https://github.com/sebastianbergmann/object-enumerator/", + "support": { + "issues": "https://github.com/sebastianbergmann/object-enumerator/issues", + "security": "https://github.com/sebastianbergmann/object-enumerator/security/policy", + "source": "https://github.com/sebastianbergmann/object-enumerator/tree/6.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T05:00:13+00:00" + }, + { + "name": "sebastian/object-reflector", + "version": "4.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-reflector.git", + "reference": "6e1a43b411b2ad34146dee7524cb13a068bb35f9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/6e1a43b411b2ad34146dee7524cb13a068bb35f9", + "reference": "6e1a43b411b2ad34146dee7524cb13a068bb35f9", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Allows reflection of object attributes, including inherited and non-public ones", + "homepage": "https://github.com/sebastianbergmann/object-reflector/", + "support": { + "issues": "https://github.com/sebastianbergmann/object-reflector/issues", + "security": "https://github.com/sebastianbergmann/object-reflector/security/policy", + "source": "https://github.com/sebastianbergmann/object-reflector/tree/4.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T05:01:32+00:00" + }, + { + "name": "sebastian/recursion-context", + "version": "6.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/recursion-context.git", + "reference": "f6458abbf32a6c8174f8f26261475dc133b3d9dc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/f6458abbf32a6c8174f8f26261475dc133b3d9dc", + "reference": "f6458abbf32a6c8174f8f26261475dc133b3d9dc", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + } + ], + "description": "Provides functionality to recursively process PHP variables", + "homepage": "https://github.com/sebastianbergmann/recursion-context", + "support": { + "issues": "https://github.com/sebastianbergmann/recursion-context/issues", + "security": "https://github.com/sebastianbergmann/recursion-context/security/policy", + "source": "https://github.com/sebastianbergmann/recursion-context/tree/6.0.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/recursion-context", + "type": "tidelift" + } + ], + "time": "2025-08-13T04:42:22+00:00" + }, + { + "name": "sebastian/type", + "version": "5.1.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/type.git", + "reference": "f77d2d4e78738c98d9a68d2596fe5e8fa380f449" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/f77d2d4e78738c98d9a68d2596fe5e8fa380f449", + "reference": "f77d2d4e78738c98d9a68d2596fe5e8fa380f449", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Collection of value objects that represent the types of the PHP type system", + "homepage": "https://github.com/sebastianbergmann/type", + "support": { + "issues": "https://github.com/sebastianbergmann/type/issues", + "security": "https://github.com/sebastianbergmann/type/security/policy", + "source": "https://github.com/sebastianbergmann/type/tree/5.1.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/type", + "type": "tidelift" + } + ], + "time": "2025-08-09T06:55:48+00:00" + }, + { + "name": "sebastian/version", + "version": "5.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/version.git", + "reference": "c687e3387b99f5b03b6caa64c74b63e2936ff874" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/c687e3387b99f5b03b6caa64c74b63e2936ff874", + "reference": "c687e3387b99f5b03b6caa64c74b63e2936ff874", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that helps with managing the version number of Git-hosted PHP projects", + "homepage": "https://github.com/sebastianbergmann/version", + "support": { + "issues": "https://github.com/sebastianbergmann/version/issues", + "security": "https://github.com/sebastianbergmann/version/security/policy", + "source": "https://github.com/sebastianbergmann/version/tree/5.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-10-09T05:16:32+00:00" + }, + { + "name": "staabm/side-effects-detector", + "version": "1.0.5", + "source": { + "type": "git", + "url": "https://github.com/staabm/side-effects-detector.git", + "reference": "d8334211a140ce329c13726d4a715adbddd0a163" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/staabm/side-effects-detector/zipball/d8334211a140ce329c13726d4a715adbddd0a163", + "reference": "d8334211a140ce329c13726d4a715adbddd0a163", + "shasum": "" + }, + "require": { + "ext-tokenizer": "*", + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "phpstan/extension-installer": "^1.4.3", + "phpstan/phpstan": "^1.12.6", + "phpunit/phpunit": "^9.6.21", + "symfony/var-dumper": "^5.4.43", + "tomasvotruba/type-coverage": "1.0.0", + "tomasvotruba/unused-public": "1.0.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "lib/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "A static analysis tool to detect side effects in PHP code", + "keywords": [ + "static analysis" + ], + "support": { + "issues": "https://github.com/staabm/side-effects-detector/issues", + "source": "https://github.com/staabm/side-effects-detector/tree/1.0.5" + }, + "funding": [ + { + "url": "https://github.com/staabm", + "type": "github" + } + ], + "time": "2024-10-20T05:08:20+00:00" + }, + { + "name": "symfony/yaml", + "version": "v7.4.6", + "source": { + "type": "git", + "url": "https://github.com/symfony/yaml.git", + "reference": "58751048de17bae71c5aa0d13cb19d79bca26391" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/yaml/zipball/58751048de17bae71c5aa0d13cb19d79bca26391", + "reference": "58751048de17bae71c5aa0d13cb19d79bca26391", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-ctype": "^1.8" + }, + "conflict": { + "symfony/console": "<6.4" + }, + "require-dev": { + "symfony/console": "^6.4|^7.0|^8.0" + }, + "bin": [ + "Resources/bin/yaml-lint" + ], + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Yaml\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Loads and dumps YAML files", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/yaml/tree/v7.4.6" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-02-09T09:33:46+00:00" + }, + { + "name": "theseer/tokenizer", + "version": "1.3.1", + "source": { + "type": "git", + "url": "https://github.com/theseer/tokenizer.git", + "reference": "b7489ce515e168639d17feec34b8847c326b0b3c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/theseer/tokenizer/zipball/b7489ce515e168639d17feec34b8847c326b0b3c", + "reference": "b7489ce515e168639d17feec34b8847c326b0b3c", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-tokenizer": "*", + "ext-xmlwriter": "*", + "php": "^7.2 || ^8.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + } + ], + "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", + "support": { + "issues": "https://github.com/theseer/tokenizer/issues", + "source": "https://github.com/theseer/tokenizer/tree/1.3.1" + }, + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2025-11-17T20:03:58+00:00" + } + ], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": {}, + "prefer-stable": true, + "prefer-lowest": false, + "platform": { + "php": "^8.2" + }, + "platform-dev": {}, + "plugin-api-version": "2.6.0" +} diff --git a/Backend/config/app.php b/Backend/config/app.php new file mode 100644 index 0000000..c74a68d --- /dev/null +++ b/Backend/config/app.php @@ -0,0 +1,126 @@ + env('APP_NAME', 'Laravel'), + + /* + |-------------------------------------------------------------------------- + | Application Environment + |-------------------------------------------------------------------------- + | + | This value determines the "environment" your application is currently + | running in. This may determine how you prefer to configure various + | services the application utilizes. Set this in your ".env" file. + | + */ + + 'env' => env('APP_ENV', 'production'), + + /* + |-------------------------------------------------------------------------- + | Application Debug Mode + |-------------------------------------------------------------------------- + | + | When your application is in debug mode, detailed error messages with + | stack traces will be shown on every error that occurs within your + | application. If disabled, a simple generic error page is shown. + | + */ + + 'debug' => (bool) env('APP_DEBUG', false), + + /* + |-------------------------------------------------------------------------- + | Application URL + |-------------------------------------------------------------------------- + | + | This URL is used by the console to properly generate URLs when using + | the Artisan command line tool. You should set this to the root of + | the application so that it's available within Artisan commands. + | + */ + + 'url' => env('APP_URL', 'http://localhost'), + + /* + |-------------------------------------------------------------------------- + | Application Timezone + |-------------------------------------------------------------------------- + | + | Here you may specify the default timezone for your application, which + | will be used by the PHP date and date-time functions. The timezone + | is set to "UTC" by default as it is suitable for most use cases. + | + */ + + 'timezone' => 'Asia/Jakarta', + + /* + |-------------------------------------------------------------------------- + | Application Locale Configuration + |-------------------------------------------------------------------------- + | + | The application locale determines the default locale that will be used + | by Laravel's translation / localization methods. This option can be + | set to any locale for which you plan to have translation strings. + | + */ + + 'locale' => env('APP_LOCALE', 'en'), + + 'fallback_locale' => env('APP_FALLBACK_LOCALE', 'en'), + + 'faker_locale' => env('APP_FAKER_LOCALE', 'en_US'), + + /* + |-------------------------------------------------------------------------- + | Encryption Key + |-------------------------------------------------------------------------- + | + | This key is utilized by Laravel's encryption services and should be set + | to a random, 32 character string to ensure that all encrypted values + | are secure. You should do this prior to deploying the application. + | + */ + + 'cipher' => 'AES-256-CBC', + + 'key' => env('APP_KEY'), + + 'previous_keys' => [ + ...array_filter( + explode(',', (string) env('APP_PREVIOUS_KEYS', '')) + ), + ], + + /* + |-------------------------------------------------------------------------- + | Maintenance Mode Driver + |-------------------------------------------------------------------------- + | + | These configuration options determine the driver used to determine and + | manage Laravel's "maintenance mode" status. The "cache" driver will + | allow maintenance mode to be controlled across multiple machines. + | + | Supported drivers: "file", "cache" + | + */ + + 'maintenance' => [ + 'driver' => env('APP_MAINTENANCE_DRIVER', 'file'), + 'store' => env('APP_MAINTENANCE_STORE', 'database'), + ], + +]; diff --git a/Backend/config/auth.php b/Backend/config/auth.php new file mode 100644 index 0000000..4aea92e --- /dev/null +++ b/Backend/config/auth.php @@ -0,0 +1,118 @@ + [ + 'guard' => env('AUTH_GUARD', 'web'), + 'passwords' => env('AUTH_PASSWORD_BROKER', 'users'), + ], + + /* + |-------------------------------------------------------------------------- + | Authentication Guards + |-------------------------------------------------------------------------- + | + | Next, you may define every authentication guard for your application. + | Of course, a great default configuration has been defined for you + | which utilizes session storage plus the Eloquent user provider. + | + | All authentication guards have a user provider, which defines how the + | users are actually retrieved out of your database or other storage + | system used by the application. Typically, Eloquent is utilized. + | + | Supported: "session" + | + */ + + 'guards' => [ + 'web' => [ + 'driver' => 'session', + 'provider' => 'users', + ], + 'customer' => [ + 'driver' => 'sanctum', + 'provider' => 'customers' + ] + ], + + /* + |-------------------------------------------------------------------------- + | User Providers + |-------------------------------------------------------------------------- + | + | All authentication guards have a user provider, which defines how the + | users are actually retrieved out of your database or other storage + | system used by the application. Typically, Eloquent is utilized. + | + | If you have multiple user tables or models you may configure multiple + | providers to represent the model / table. These providers may then + | be assigned to any extra authentication guards you have defined. + | + | Supported: "database", "eloquent" + | + */ + + 'providers' => [ + 'users' => [ + 'driver' => 'eloquent', + 'model' => env('AUTH_MODEL', App\Models\User::class), + ], + 'customers' => [ + 'driver' => 'eloquent', + 'model' => App\Models\Customer::class + ] + ], + + /* + |-------------------------------------------------------------------------- + | Resetting Passwords + |-------------------------------------------------------------------------- + | + | These configuration options specify the behavior of Laravel's password + | reset functionality, including the table utilized for token storage + | and the user provider that is invoked to actually retrieve users. + | + | The expiry time is the number of minutes that each reset token will be + | considered valid. This security feature keeps tokens short-lived so + | they have less time to be guessed. You may change this as needed. + | + | The throttle setting is the number of seconds a user must wait before + | generating more password reset tokens. This prevents the user from + | quickly generating a very large amount of password reset tokens. + | + */ + + 'passwords' => [ + 'users' => [ + 'provider' => 'users', + 'table' => env('AUTH_PASSWORD_RESET_TOKEN_TABLE', 'password_reset_tokens'), + 'expire' => 60, + 'throttle' => 60, + ], + ], + + /* + |-------------------------------------------------------------------------- + | Password Confirmation Timeout + |-------------------------------------------------------------------------- + | + | Here you may define the number of seconds before a password confirmation + | window expires and users are asked to re-enter their password via the + | confirmation screen. By default, the timeout lasts for three hours. + | + */ + + 'password_timeout' => env('AUTH_PASSWORD_TIMEOUT', 10800), + +]; diff --git a/Backend/config/broadcasting.php b/Backend/config/broadcasting.php new file mode 100644 index 0000000..ebc3fb9 --- /dev/null +++ b/Backend/config/broadcasting.php @@ -0,0 +1,82 @@ + env('BROADCAST_CONNECTION', 'null'), + + /* + |-------------------------------------------------------------------------- + | Broadcast Connections + |-------------------------------------------------------------------------- + | + | Here you may define all of the broadcast connections that will be used + | to broadcast events to other systems or over WebSockets. Samples of + | each available type of connection are provided inside this array. + | + */ + + 'connections' => [ + + 'reverb' => [ + 'driver' => 'reverb', + 'key' => env('REVERB_APP_KEY'), + 'secret' => env('REVERB_APP_SECRET'), + 'app_id' => env('REVERB_APP_ID'), + 'options' => [ + 'host' => env('REVERB_HOST'), + 'port' => env('REVERB_PORT', 443), + 'scheme' => env('REVERB_SCHEME', 'https'), + 'useTLS' => env('REVERB_SCHEME', 'https') === 'https', + ], + 'client_options' => [ + // Guzzle client options: https://docs.guzzlephp.org/en/stable/request-options.html + ], + ], + + 'pusher' => [ + 'driver' => 'pusher', + 'key' => env('PUSHER_APP_KEY'), + 'secret' => env('PUSHER_APP_SECRET'), + 'app_id' => env('PUSHER_APP_ID'), + 'options' => [ + 'cluster' => env('PUSHER_APP_CLUSTER'), + 'host' => env('PUSHER_HOST') ?: 'api-'.env('PUSHER_APP_CLUSTER', 'mt1').'.pusher.com', + 'port' => env('PUSHER_PORT', 443), + 'scheme' => env('PUSHER_SCHEME', 'https'), + 'encrypted' => true, + 'useTLS' => env('PUSHER_SCHEME', 'https') === 'https', + ], + 'client_options' => [ + // Guzzle client options: https://docs.guzzlephp.org/en/stable/request-options.html + ], + ], + + 'ably' => [ + 'driver' => 'ably', + 'key' => env('ABLY_KEY'), + ], + + 'log' => [ + 'driver' => 'log', + ], + + 'null' => [ + 'driver' => 'null', + ], + + ], + +]; diff --git a/Backend/config/cache.php b/Backend/config/cache.php new file mode 100644 index 0000000..b32aead --- /dev/null +++ b/Backend/config/cache.php @@ -0,0 +1,117 @@ + env('CACHE_STORE', 'database'), + + /* + |-------------------------------------------------------------------------- + | Cache Stores + |-------------------------------------------------------------------------- + | + | Here you may define all of the cache "stores" for your application as + | well as their drivers. You may even define multiple stores for the + | same cache driver to group types of items stored in your caches. + | + | Supported drivers: "array", "database", "file", "memcached", + | "redis", "dynamodb", "octane", + | "failover", "null" + | + */ + + 'stores' => [ + + 'array' => [ + 'driver' => 'array', + 'serialize' => false, + ], + + 'database' => [ + 'driver' => 'database', + 'connection' => env('DB_CACHE_CONNECTION'), + 'table' => env('DB_CACHE_TABLE', 'cache'), + 'lock_connection' => env('DB_CACHE_LOCK_CONNECTION'), + 'lock_table' => env('DB_CACHE_LOCK_TABLE'), + ], + + 'file' => [ + 'driver' => 'file', + 'path' => storage_path('framework/cache/data'), + 'lock_path' => storage_path('framework/cache/data'), + ], + + 'memcached' => [ + 'driver' => 'memcached', + 'persistent_id' => env('MEMCACHED_PERSISTENT_ID'), + 'sasl' => [ + env('MEMCACHED_USERNAME'), + env('MEMCACHED_PASSWORD'), + ], + 'options' => [ + // Memcached::OPT_CONNECT_TIMEOUT => 2000, + ], + 'servers' => [ + [ + 'host' => env('MEMCACHED_HOST', '127.0.0.1'), + 'port' => env('MEMCACHED_PORT', 11211), + 'weight' => 100, + ], + ], + ], + + 'redis' => [ + 'driver' => 'redis', + 'connection' => env('REDIS_CACHE_CONNECTION', 'cache'), + 'lock_connection' => env('REDIS_CACHE_LOCK_CONNECTION', 'default'), + ], + + 'dynamodb' => [ + 'driver' => 'dynamodb', + 'key' => env('AWS_ACCESS_KEY_ID'), + 'secret' => env('AWS_SECRET_ACCESS_KEY'), + 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'), + 'table' => env('DYNAMODB_CACHE_TABLE', 'cache'), + 'endpoint' => env('DYNAMODB_ENDPOINT'), + ], + + 'octane' => [ + 'driver' => 'octane', + ], + + 'failover' => [ + 'driver' => 'failover', + 'stores' => [ + 'database', + 'array', + ], + ], + + ], + + /* + |-------------------------------------------------------------------------- + | Cache Key Prefix + |-------------------------------------------------------------------------- + | + | When utilizing the APC, database, memcached, Redis, and DynamoDB cache + | stores, there might be other applications using the same cache. For + | that reason, you may prefix every cache key to avoid collisions. + | + */ + + 'prefix' => env('CACHE_PREFIX', Str::slug((string) env('APP_NAME', 'laravel')).'-cache-'), + +]; diff --git a/Backend/config/cors.php b/Backend/config/cors.php new file mode 100644 index 0000000..f2d44aa --- /dev/null +++ b/Backend/config/cors.php @@ -0,0 +1,34 @@ + ['api/*', 'sanctum/csrf-cookie', 'storage/*'], + + 'allowed_methods' => ['*'], + + 'allowed_origins' => ['*'], + + 'allowed_origins_patterns' => [], + + 'allowed_headers' => ['*'], + + 'exposed_headers' => [], + + 'max_age' => 0, + + 'supports_credentials' => false, + +]; diff --git a/Backend/config/database.php b/Backend/config/database.php new file mode 100644 index 0000000..97d39b6 --- /dev/null +++ b/Backend/config/database.php @@ -0,0 +1,192 @@ + env('DB_CONNECTION', 'sqlite'), + + /* + |-------------------------------------------------------------------------- + | Database Connections + |-------------------------------------------------------------------------- + | + | Below are all of the database connections defined for your application. + | An example configuration is provided for each database system which + | is supported by Laravel. You're free to add / remove connections. + | + */ + + 'connections' => [ + + 'sqlite' => [ + 'driver' => 'sqlite', + 'url' => env('DB_URL'), + 'database' => env('DB_DATABASE', database_path('database.sqlite')), + 'prefix' => '', + 'foreign_key_constraints' => env('DB_FOREIGN_KEYS', true), + 'busy_timeout' => null, + 'journal_mode' => null, + 'synchronous' => null, + 'transaction_mode' => 'DEFERRED', + ], + + 'mysql' => [ + 'driver' => 'mysql', + 'url' => env('DB_URL'), + 'host' => env('DB_HOST', '127.0.0.1'), + 'port' => env('DB_PORT', '3306'), + 'database' => env('DB_DATABASE', 'laravel'), + 'username' => env('DB_USERNAME', 'root'), + 'password' => env('DB_PASSWORD', ''), + 'options' => extension_loaded('pdo_mysql') ? array_filter([ + PDO::ATTR_EMULATE_PREPARES => true, + PDO::ATTR_TIMEOUT => 5, + ]) : [], + 'after_contents' => [ + "SET NAMES 'utf8mb4'", + "SET SESSION innodb_lock_wait_timeout = 5", + "SET SESSION wait_timeout = 30", + ], + 'unix_socket' => env('DB_SOCKET', ''), + 'charset' => env('DB_CHARSET', 'utf8mb4'), + 'collation' => env('DB_COLLATION', 'utf8mb4_unicode_ci'), + 'prefix' => '', + 'prefix_indexes' => true, + 'strict' => true, + 'engine' => null, + 'options' => extension_loaded('pdo_mysql') ? array_filter([ + (PHP_VERSION_ID >= 80500 ? \Pdo\Mysql::ATTR_SSL_CA : \PDO::MYSQL_ATTR_SSL_CA) => env('MYSQL_ATTR_SSL_CA'), + ]) : [], + ], + + 'mariadb' => [ + 'driver' => 'mariadb', + 'url' => env('DB_URL'), + 'host' => env('DB_HOST', '127.0.0.1'), + 'port' => env('DB_PORT', '3306'), + 'database' => env('DB_DATABASE', 'laravel'), + 'username' => env('DB_USERNAME', 'root'), + 'password' => env('DB_PASSWORD', ''), + 'unix_socket' => env('DB_SOCKET', ''), + 'charset' => env('DB_CHARSET', 'utf8mb4'), + 'collation' => env('DB_COLLATION', 'utf8mb4_unicode_ci'), + 'prefix' => '', + 'prefix_indexes' => true, + 'strict' => true, + 'engine' => null, + 'options' => extension_loaded('pdo_mysql') ? array_filter([ + (PHP_VERSION_ID >= 80500 ? \Pdo\Mysql::ATTR_SSL_CA : \PDO::MYSQL_ATTR_SSL_CA) => env('MYSQL_ATTR_SSL_CA'), + ]) : [], + ], + + 'pgsql' => [ + 'driver' => 'pgsql', + 'url' => env('DB_URL'), + 'host' => env('DB_HOST', '127.0.0.1'), + 'port' => env('DB_PORT', '5432'), + 'database' => env('DB_DATABASE', 'laravel'), + 'username' => env('DB_USERNAME', 'root'), + 'password' => env('DB_PASSWORD', ''), + 'charset' => env('DB_CHARSET', 'utf8'), + 'prefix' => '', + 'prefix_indexes' => true, + 'search_path' => 'public', + 'sslmode' => env('DB_SSLMODE', 'prefer'), + ], + + 'sqlsrv' => [ + 'driver' => 'sqlsrv', + 'url' => env('DB_URL'), + 'host' => env('DB_HOST', 'localhost'), + 'port' => env('DB_PORT', '1433'), + 'database' => env('DB_DATABASE', 'laravel'), + 'username' => env('DB_USERNAME', 'root'), + 'password' => env('DB_PASSWORD', ''), + 'charset' => env('DB_CHARSET', 'utf8'), + 'prefix' => '', + 'prefix_indexes' => true, + // 'encrypt' => env('DB_ENCRYPT', 'yes'), + // 'trust_server_certificate' => env('DB_TRUST_SERVER_CERTIFICATE', 'false'), + ], + + ], + + /* + |-------------------------------------------------------------------------- + | Migration Repository Table + |-------------------------------------------------------------------------- + | + | This table keeps track of all the migrations that have already run for + | your application. Using this information, we can determine which of + | the migrations on disk haven't actually been run on the database. + | + */ + + 'migrations' => [ + 'table' => 'migrations', + 'update_date_on_publish' => true, + ], + + /* + |-------------------------------------------------------------------------- + | Redis Databases + |-------------------------------------------------------------------------- + | + | Redis is an open source, fast, and advanced key-value store that also + | provides a richer body of commands than a typical key-value system + | such as Memcached. You may define your connection settings here. + | + */ + + 'redis' => [ + + 'client' => env('REDIS_CLIENT', 'phpredis'), + + 'options' => [ + 'cluster' => env('REDIS_CLUSTER', 'redis'), + 'prefix' => env('REDIS_PREFIX', Str::slug((string) env('APP_NAME', 'laravel')).'-database-'), + 'persistent' => env('REDIS_PERSISTENT', false), + ], + + 'default' => [ + 'url' => env('REDIS_URL'), + 'host' => env('REDIS_HOST', '127.0.0.1'), + 'username' => env('REDIS_USERNAME'), + 'password' => env('REDIS_PASSWORD'), + 'port' => env('REDIS_PORT', '6379'), + 'database' => env('REDIS_DB', '0'), + 'max_retries' => env('REDIS_MAX_RETRIES', 3), + 'backoff_algorithm' => env('REDIS_BACKOFF_ALGORITHM', 'decorrelated_jitter'), + 'backoff_base' => env('REDIS_BACKOFF_BASE', 100), + 'backoff_cap' => env('REDIS_BACKOFF_CAP', 1000), + ], + + 'cache' => [ + 'url' => env('REDIS_URL'), + 'host' => env('REDIS_HOST', '127.0.0.1'), + 'username' => env('REDIS_USERNAME'), + 'password' => env('REDIS_PASSWORD'), + 'port' => env('REDIS_PORT', '6379'), + 'database' => env('REDIS_CACHE_DB', '1'), + 'max_retries' => env('REDIS_MAX_RETRIES', 3), + 'backoff_algorithm' => env('REDIS_BACKOFF_ALGORITHM', 'decorrelated_jitter'), + 'backoff_base' => env('REDIS_BACKOFF_BASE', 100), + 'backoff_cap' => env('REDIS_BACKOFF_CAP', 1000), + ], + + ], + +]; diff --git a/Backend/config/filesystems.php b/Backend/config/filesystems.php new file mode 100644 index 0000000..37d8fca --- /dev/null +++ b/Backend/config/filesystems.php @@ -0,0 +1,80 @@ + env('FILESYSTEM_DISK', 'local'), + + /* + |-------------------------------------------------------------------------- + | Filesystem Disks + |-------------------------------------------------------------------------- + | + | Below you may configure as many filesystem disks as necessary, and you + | may even configure multiple disks for the same driver. Examples for + | most supported storage drivers are configured here for reference. + | + | Supported drivers: "local", "ftp", "sftp", "s3" + | + */ + + 'disks' => [ + + 'local' => [ + 'driver' => 'local', + 'root' => storage_path('app/private'), + 'serve' => true, + 'throw' => false, + 'report' => false, + ], + + 'public' => [ + 'driver' => 'local', + 'root' => storage_path('app/public'), + 'url' => rtrim(env('APP_URL', 'http://localhost'), '/').'/storage', + 'visibility' => 'public', + 'throw' => false, + 'report' => false, + ], + + 's3' => [ + 'driver' => 's3', + 'key' => env('AWS_ACCESS_KEY_ID'), + 'secret' => env('AWS_SECRET_ACCESS_KEY'), + 'region' => env('AWS_DEFAULT_REGION'), + 'bucket' => env('AWS_BUCKET'), + 'url' => env('AWS_URL'), + 'endpoint' => env('AWS_ENDPOINT'), + 'use_path_style_endpoint' => env('AWS_USE_PATH_STYLE_ENDPOINT', false), + 'throw' => false, + 'report' => false, + ], + + ], + + /* + |-------------------------------------------------------------------------- + | Symbolic Links + |-------------------------------------------------------------------------- + | + | Here you may configure the symbolic links that will be created when the + | `storage:link` Artisan command is executed. The array keys should be + | the locations of the links and the values should be their targets. + | + */ + + 'links' => [ + public_path('storage') => storage_path('app/public'), + ], + +]; diff --git a/Backend/config/logging.php b/Backend/config/logging.php new file mode 100644 index 0000000..9e998a4 --- /dev/null +++ b/Backend/config/logging.php @@ -0,0 +1,132 @@ + env('LOG_CHANNEL', 'stack'), + + /* + |-------------------------------------------------------------------------- + | Deprecations Log Channel + |-------------------------------------------------------------------------- + | + | This option controls the log channel that should be used to log warnings + | regarding deprecated PHP and library features. This allows you to get + | your application ready for upcoming major versions of dependencies. + | + */ + + 'deprecations' => [ + 'channel' => env('LOG_DEPRECATIONS_CHANNEL', 'null'), + 'trace' => env('LOG_DEPRECATIONS_TRACE', false), + ], + + /* + |-------------------------------------------------------------------------- + | Log Channels + |-------------------------------------------------------------------------- + | + | Here you may configure the log channels for your application. Laravel + | utilizes the Monolog PHP logging library, which includes a variety + | of powerful log handlers and formatters that you're free to use. + | + | Available drivers: "single", "daily", "slack", "syslog", + | "errorlog", "monolog", "custom", "stack" + | + */ + + 'channels' => [ + + 'stack' => [ + 'driver' => 'stack', + 'channels' => explode(',', (string) env('LOG_STACK', 'single')), + 'ignore_exceptions' => false, + ], + + 'single' => [ + 'driver' => 'single', + 'path' => storage_path('logs/laravel.log'), + 'level' => env('LOG_LEVEL', 'debug'), + 'replace_placeholders' => true, + ], + + 'daily' => [ + 'driver' => 'daily', + 'path' => storage_path('logs/laravel.log'), + 'level' => env('LOG_LEVEL', 'debug'), + 'days' => env('LOG_DAILY_DAYS', 14), + 'replace_placeholders' => true, + ], + + 'slack' => [ + 'driver' => 'slack', + 'url' => env('LOG_SLACK_WEBHOOK_URL'), + 'username' => env('LOG_SLACK_USERNAME', 'Laravel Log'), + 'emoji' => env('LOG_SLACK_EMOJI', ':boom:'), + 'level' => env('LOG_LEVEL', 'critical'), + 'replace_placeholders' => true, + ], + + 'papertrail' => [ + 'driver' => 'monolog', + 'level' => env('LOG_LEVEL', 'debug'), + 'handler' => env('LOG_PAPERTRAIL_HANDLER', SyslogUdpHandler::class), + 'handler_with' => [ + 'host' => env('PAPERTRAIL_URL'), + 'port' => env('PAPERTRAIL_PORT'), + 'connectionString' => 'tls://'.env('PAPERTRAIL_URL').':'.env('PAPERTRAIL_PORT'), + ], + 'processors' => [PsrLogMessageProcessor::class], + ], + + 'stderr' => [ + 'driver' => 'monolog', + 'level' => env('LOG_LEVEL', 'debug'), + 'handler' => StreamHandler::class, + 'handler_with' => [ + 'stream' => 'php://stderr', + ], + 'formatter' => env('LOG_STDERR_FORMATTER'), + 'processors' => [PsrLogMessageProcessor::class], + ], + + 'syslog' => [ + 'driver' => 'syslog', + 'level' => env('LOG_LEVEL', 'debug'), + 'facility' => env('LOG_SYSLOG_FACILITY', LOG_USER), + 'replace_placeholders' => true, + ], + + 'errorlog' => [ + 'driver' => 'errorlog', + 'level' => env('LOG_LEVEL', 'debug'), + 'replace_placeholders' => true, + ], + + 'null' => [ + 'driver' => 'monolog', + 'handler' => NullHandler::class, + ], + + 'emergency' => [ + 'path' => storage_path('logs/laravel.log'), + ], + + ], + +]; diff --git a/Backend/config/mail.php b/Backend/config/mail.php new file mode 100644 index 0000000..522b284 --- /dev/null +++ b/Backend/config/mail.php @@ -0,0 +1,118 @@ + env('MAIL_MAILER', 'log'), + + /* + |-------------------------------------------------------------------------- + | Mailer Configurations + |-------------------------------------------------------------------------- + | + | Here you may configure all of the mailers used by your application plus + | their respective settings. Several examples have been configured for + | you and you are free to add your own as your application requires. + | + | Laravel supports a variety of mail "transport" drivers that can be used + | when delivering an email. You may specify which one you're using for + | your mailers below. You may also add additional mailers if needed. + | + | Supported: "smtp", "sendmail", "mailgun", "ses", "ses-v2", + | "postmark", "resend", "log", "array", + | "failover", "roundrobin" + | + */ + + 'mailers' => [ + + 'smtp' => [ + 'transport' => 'smtp', + 'scheme' => env('MAIL_SCHEME'), + 'url' => env('MAIL_URL'), + 'host' => env('MAIL_HOST', '127.0.0.1'), + 'port' => env('MAIL_PORT', 2525), + 'username' => env('MAIL_USERNAME'), + 'password' => env('MAIL_PASSWORD'), + 'timeout' => null, + 'local_domain' => env('MAIL_EHLO_DOMAIN', parse_url((string) env('APP_URL', 'http://localhost'), PHP_URL_HOST)), + ], + + 'ses' => [ + 'transport' => 'ses', + ], + + 'postmark' => [ + 'transport' => 'postmark', + // 'message_stream_id' => env('POSTMARK_MESSAGE_STREAM_ID'), + // 'client' => [ + // 'timeout' => 5, + // ], + ], + + 'resend' => [ + 'transport' => 'resend', + ], + + 'sendmail' => [ + 'transport' => 'sendmail', + 'path' => env('MAIL_SENDMAIL_PATH', '/usr/sbin/sendmail -bs -i'), + ], + + 'log' => [ + 'transport' => 'log', + 'channel' => env('MAIL_LOG_CHANNEL'), + ], + + 'array' => [ + 'transport' => 'array', + ], + + 'failover' => [ + 'transport' => 'failover', + 'mailers' => [ + 'smtp', + 'log', + ], + 'retry_after' => 60, + ], + + 'roundrobin' => [ + 'transport' => 'roundrobin', + 'mailers' => [ + 'ses', + 'postmark', + ], + 'retry_after' => 60, + ], + + ], + + /* + |-------------------------------------------------------------------------- + | Global "From" Address + |-------------------------------------------------------------------------- + | + | You may wish for all emails sent by your application to be sent from + | the same address. Here you may specify a name and address that is + | used globally for all emails that are sent by your application. + | + */ + + 'from' => [ + 'address' => env('MAIL_FROM_ADDRESS', 'hello@example.com'), + 'name' => env('MAIL_FROM_NAME', 'Example'), + ], + +]; diff --git a/Backend/config/queue.php b/Backend/config/queue.php new file mode 100644 index 0000000..79c2c0a --- /dev/null +++ b/Backend/config/queue.php @@ -0,0 +1,129 @@ + env('QUEUE_CONNECTION', 'database'), + + /* + |-------------------------------------------------------------------------- + | Queue Connections + |-------------------------------------------------------------------------- + | + | Here you may configure the connection options for every queue backend + | used by your application. An example configuration is provided for + | each backend supported by Laravel. You're also free to add more. + | + | Drivers: "sync", "database", "beanstalkd", "sqs", "redis", + | "deferred", "background", "failover", "null" + | + */ + + 'connections' => [ + + 'sync' => [ + 'driver' => 'sync', + ], + + 'database' => [ + 'driver' => 'database', + 'connection' => env('DB_QUEUE_CONNECTION'), + 'table' => env('DB_QUEUE_TABLE', 'jobs'), + 'queue' => env('DB_QUEUE', 'default'), + 'retry_after' => (int) env('DB_QUEUE_RETRY_AFTER', 90), + 'after_commit' => false, + ], + + 'beanstalkd' => [ + 'driver' => 'beanstalkd', + 'host' => env('BEANSTALKD_QUEUE_HOST', 'localhost'), + 'queue' => env('BEANSTALKD_QUEUE', 'default'), + 'retry_after' => (int) env('BEANSTALKD_QUEUE_RETRY_AFTER', 90), + 'block_for' => 0, + 'after_commit' => false, + ], + + 'sqs' => [ + 'driver' => 'sqs', + 'key' => env('AWS_ACCESS_KEY_ID'), + 'secret' => env('AWS_SECRET_ACCESS_KEY'), + 'prefix' => env('SQS_PREFIX', 'https://sqs.us-east-1.amazonaws.com/your-account-id'), + 'queue' => env('SQS_QUEUE', 'default'), + 'suffix' => env('SQS_SUFFIX'), + 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'), + 'after_commit' => false, + ], + + 'redis' => [ + 'driver' => 'redis', + 'connection' => env('REDIS_QUEUE_CONNECTION', 'default'), + 'queue' => env('REDIS_QUEUE', 'default'), + 'retry_after' => (int) env('REDIS_QUEUE_RETRY_AFTER', 90), + 'block_for' => null, + 'after_commit' => false, + ], + + 'deferred' => [ + 'driver' => 'deferred', + ], + + 'background' => [ + 'driver' => 'background', + ], + + 'failover' => [ + 'driver' => 'failover', + 'connections' => [ + 'database', + 'deferred', + ], + ], + + ], + + /* + |-------------------------------------------------------------------------- + | Job Batching + |-------------------------------------------------------------------------- + | + | The following options configure the database and table that store job + | batching information. These options can be updated to any database + | connection and table which has been defined by your application. + | + */ + + 'batching' => [ + 'database' => env('DB_CONNECTION', 'sqlite'), + 'table' => 'job_batches', + ], + + /* + |-------------------------------------------------------------------------- + | Failed Queue Jobs + |-------------------------------------------------------------------------- + | + | These options configure the behavior of failed queue job logging so you + | can control how and where failed jobs are stored. Laravel ships with + | support for storing failed jobs in a simple file or in a database. + | + | Supported drivers: "database-uuids", "dynamodb", "file", "null" + | + */ + + 'failed' => [ + 'driver' => env('QUEUE_FAILED_DRIVER', 'database-uuids'), + 'database' => env('DB_CONNECTION', 'sqlite'), + 'table' => 'failed_jobs', + ], + +]; diff --git a/Backend/config/reverb.php b/Backend/config/reverb.php new file mode 100644 index 0000000..91f3880 --- /dev/null +++ b/Backend/config/reverb.php @@ -0,0 +1,102 @@ + env('REVERB_SERVER', 'reverb'), + + /* + |-------------------------------------------------------------------------- + | Reverb Servers + |-------------------------------------------------------------------------- + | + | Here you may define details for each of the supported Reverb servers. + | Each server has its own configuration options that are defined in + | the array below. You should ensure all the options are present. + | + */ + + 'servers' => [ + + 'reverb' => [ + 'host' => env('REVERB_SERVER_HOST', '0.0.0.0'), + 'port' => env('REVERB_SERVER_PORT', 8080), + 'path' => env('REVERB_SERVER_PATH', ''), + 'hostname' => env('REVERB_HOST'), + 'options' => [ + 'tls' => [], + ], + 'max_request_size' => env('REVERB_MAX_REQUEST_SIZE', 10_000), + 'scaling' => [ + 'enabled' => env('REVERB_SCALING_ENABLED', false), + 'channel' => env('REVERB_SCALING_CHANNEL', 'reverb'), + 'server' => [ + 'url' => env('REDIS_URL'), + 'host' => env('REDIS_HOST', '127.0.0.1'), + 'port' => env('REDIS_PORT', '6379'), + 'username' => env('REDIS_USERNAME'), + 'password' => env('REDIS_PASSWORD'), + 'database' => env('REDIS_DB', '0'), + 'timeout' => env('REDIS_TIMEOUT', 60), + ], + ], + 'pulse_ingest_interval' => env('REVERB_PULSE_INGEST_INTERVAL', 15), + 'telescope_ingest_interval' => env('REVERB_TELESCOPE_INGEST_INTERVAL', 15), + ], + + ], + + /* + |-------------------------------------------------------------------------- + | Reverb Applications + |-------------------------------------------------------------------------- + | + | Here you may define how Reverb applications are managed. If you choose + | to use the "config" provider, you may define an array of apps which + | your server will support, including their connection credentials. + | + */ + + 'apps' => [ + + 'provider' => 'config', + + 'apps' => [ + [ + 'key' => env('REVERB_APP_KEY'), + 'secret' => env('REVERB_APP_SECRET'), + 'app_id' => env('REVERB_APP_ID'), + 'options' => [ + 'host' => env('REVERB_HOST'), + 'port' => env('REVERB_PORT', 443), + 'scheme' => env('REVERB_SCHEME', 'https'), + 'useTLS' => env('REVERB_SCHEME', 'https') === 'https', + ], + 'allowed_origins' => ['*'], + 'ping_interval' => env('REVERB_APP_PING_INTERVAL', 60), + 'activity_timeout' => env('REVERB_APP_ACTIVITY_TIMEOUT', 30), + 'max_connections' => env('REVERB_APP_MAX_CONNECTIONS'), + 'max_message_size' => env('REVERB_APP_MAX_MESSAGE_SIZE', 10_000), + 'accept_client_events_from' => env('REVERB_APP_ACCEPT_CLIENT_EVENTS_FROM', 'members'), + 'rate_limiting' => [ + 'enabled' => env('REVERB_APP_RATE_LIMITING_ENABLED', false), + 'max_attempts' => env('REVERB_APP_RATE_LIMIT_MAX_ATTEMPTS', 60), + 'decay_seconds' => env('REVERB_APP_RATE_LIMIT_DECAY_SECONDS', 60), + 'terminate_on_limit' => env('REVERB_APP_RATE_LIMIT_TERMINATE', false), + ], + ], + ], + + ], + +]; diff --git a/Backend/config/sanctum.php b/Backend/config/sanctum.php new file mode 100644 index 0000000..44527d6 --- /dev/null +++ b/Backend/config/sanctum.php @@ -0,0 +1,84 @@ + explode(',', env('SANCTUM_STATEFUL_DOMAINS', sprintf( + '%s%s', + 'localhost,localhost:3000,127.0.0.1,127.0.0.1:8000,::1', + Sanctum::currentApplicationUrlWithPort(), + // Sanctum::currentRequestHost(), + ))), + + /* + |-------------------------------------------------------------------------- + | Sanctum Guards + |-------------------------------------------------------------------------- + | + | This array contains the authentication guards that will be checked when + | Sanctum is trying to authenticate a request. If none of these guards + | are able to authenticate the request, Sanctum will use the bearer + | token that's present on an incoming request for authentication. + | + */ + + 'guard' => ['web'], + + /* + |-------------------------------------------------------------------------- + | Expiration Minutes + |-------------------------------------------------------------------------- + | + | This value controls the number of minutes until an issued token will be + | considered expired. This will override any values set in the token's + | "expires_at" attribute, but first-party sessions are not affected. + | + */ + + 'expiration' => null, + + /* + |-------------------------------------------------------------------------- + | Token Prefix + |-------------------------------------------------------------------------- + | + | Sanctum can prefix new tokens in order to take advantage of numerous + | security scanning initiatives maintained by open source platforms + | that notify developers if they commit tokens into repositories. + | + | See: https://docs.github.com/en/code-security/secret-scanning/about-secret-scanning + | + */ + + 'token_prefix' => env('SANCTUM_TOKEN_PREFIX', ''), + + /* + |-------------------------------------------------------------------------- + | Sanctum Middleware + |-------------------------------------------------------------------------- + | + | When authenticating your first-party SPA with Sanctum you may need to + | customize some of the middleware Sanctum uses while processing the + | request. You may change the middleware listed below as required. + | + */ + + 'middleware' => [ + 'authenticate_session' => Laravel\Sanctum\Http\Middleware\AuthenticateSession::class, + 'encrypt_cookies' => Illuminate\Cookie\Middleware\EncryptCookies::class, + 'validate_csrf_token' => Illuminate\Foundation\Http\Middleware\ValidateCsrfToken::class, + ], + +]; diff --git a/Backend/config/services.php b/Backend/config/services.php new file mode 100644 index 0000000..6a90eb8 --- /dev/null +++ b/Backend/config/services.php @@ -0,0 +1,38 @@ + [ + 'key' => env('POSTMARK_API_KEY'), + ], + + 'resend' => [ + 'key' => env('RESEND_API_KEY'), + ], + + 'ses' => [ + 'key' => env('AWS_ACCESS_KEY_ID'), + 'secret' => env('AWS_SECRET_ACCESS_KEY'), + 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'), + ], + + 'slack' => [ + 'notifications' => [ + 'bot_user_oauth_token' => env('SLACK_BOT_USER_OAUTH_TOKEN'), + 'channel' => env('SLACK_BOT_USER_DEFAULT_CHANNEL'), + ], + ], + +]; diff --git a/Backend/config/session.php b/Backend/config/session.php new file mode 100644 index 0000000..5b541b7 --- /dev/null +++ b/Backend/config/session.php @@ -0,0 +1,217 @@ + env('SESSION_DRIVER', 'database'), + + /* + |-------------------------------------------------------------------------- + | Session Lifetime + |-------------------------------------------------------------------------- + | + | Here you may specify the number of minutes that you wish the session + | to be allowed to remain idle before it expires. If you want them + | to expire immediately when the browser is closed then you may + | indicate that via the expire_on_close configuration option. + | + */ + + 'lifetime' => (int) env('SESSION_LIFETIME', 120), + + 'expire_on_close' => env('SESSION_EXPIRE_ON_CLOSE', false), + + /* + |-------------------------------------------------------------------------- + | Session Encryption + |-------------------------------------------------------------------------- + | + | This option allows you to easily specify that all of your session data + | should be encrypted before it's stored. All encryption is performed + | automatically by Laravel and you may use the session like normal. + | + */ + + 'encrypt' => env('SESSION_ENCRYPT', false), + + /* + |-------------------------------------------------------------------------- + | Session File Location + |-------------------------------------------------------------------------- + | + | When utilizing the "file" session driver, the session files are placed + | on disk. The default storage location is defined here; however, you + | are free to provide another location where they should be stored. + | + */ + + 'files' => storage_path('framework/sessions'), + + /* + |-------------------------------------------------------------------------- + | Session Database Connection + |-------------------------------------------------------------------------- + | + | When using the "database" or "redis" session drivers, you may specify a + | connection that should be used to manage these sessions. This should + | correspond to a connection in your database configuration options. + | + */ + + 'connection' => env('SESSION_CONNECTION'), + + /* + |-------------------------------------------------------------------------- + | Session Database Table + |-------------------------------------------------------------------------- + | + | When using the "database" session driver, you may specify the table to + | be used to store sessions. Of course, a sensible default is defined + | for you; however, you're welcome to change this to another table. + | + */ + + 'table' => env('SESSION_TABLE', 'sessions'), + + /* + |-------------------------------------------------------------------------- + | Session Cache Store + |-------------------------------------------------------------------------- + | + | When using one of the framework's cache driven session backends, you may + | define the cache store which should be used to store the session data + | between requests. This must match one of your defined cache stores. + | + | Affects: "dynamodb", "memcached", "redis" + | + */ + + 'store' => env('SESSION_STORE'), + + /* + |-------------------------------------------------------------------------- + | Session Sweeping Lottery + |-------------------------------------------------------------------------- + | + | Some session drivers must manually sweep their storage location to get + | rid of old sessions from storage. Here are the chances that it will + | happen on a given request. By default, the odds are 2 out of 100. + | + */ + + 'lottery' => [2, 100], + + /* + |-------------------------------------------------------------------------- + | Session Cookie Name + |-------------------------------------------------------------------------- + | + | Here you may change the name of the session cookie that is created by + | the framework. Typically, you should not need to change this value + | since doing so does not grant a meaningful security improvement. + | + */ + + 'cookie' => env( + 'SESSION_COOKIE', + Str::slug((string) env('APP_NAME', 'laravel')).'-session' + ), + + /* + |-------------------------------------------------------------------------- + | Session Cookie Path + |-------------------------------------------------------------------------- + | + | The session cookie path determines the path for which the cookie will + | be regarded as available. Typically, this will be the root path of + | your application, but you're free to change this when necessary. + | + */ + + 'path' => env('SESSION_PATH', '/'), + + /* + |-------------------------------------------------------------------------- + | Session Cookie Domain + |-------------------------------------------------------------------------- + | + | This value determines the domain and subdomains the session cookie is + | available to. By default, the cookie will be available to the root + | domain without subdomains. Typically, this shouldn't be changed. + | + */ + + 'domain' => env('SESSION_DOMAIN'), + + /* + |-------------------------------------------------------------------------- + | HTTPS Only Cookies + |-------------------------------------------------------------------------- + | + | By setting this option to true, session cookies will only be sent back + | to the server if the browser has a HTTPS connection. This will keep + | the cookie from being sent to you when it can't be done securely. + | + */ + + 'secure' => env('SESSION_SECURE_COOKIE'), + + /* + |-------------------------------------------------------------------------- + | HTTP Access Only + |-------------------------------------------------------------------------- + | + | Setting this value to true will prevent JavaScript from accessing the + | value of the cookie and the cookie will only be accessible through + | the HTTP protocol. It's unlikely you should disable this option. + | + */ + + 'http_only' => env('SESSION_HTTP_ONLY', true), + + /* + |-------------------------------------------------------------------------- + | Same-Site Cookies + |-------------------------------------------------------------------------- + | + | This option determines how your cookies behave when cross-site requests + | take place, and can be used to mitigate CSRF attacks. By default, we + | will set this value to "lax" to permit secure cross-site requests. + | + | See: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#samesitesamesite-value + | + | Supported: "lax", "strict", "none", null + | + */ + + 'same_site' => env('SESSION_SAME_SITE', 'lax'), + + /* + |-------------------------------------------------------------------------- + | Partitioned Cookies + |-------------------------------------------------------------------------- + | + | Setting this value to true will tie the cookie to the top-level site for + | a cross-site context. Partitioned cookies are accepted by the browser + | when flagged "secure" and the Same-Site attribute is set to "none". + | + */ + + 'partitioned' => env('SESSION_PARTITIONED_COOKIE', false), + +]; diff --git a/Backend/config/telescope.php b/Backend/config/telescope.php new file mode 100644 index 0000000..6250e78 --- /dev/null +++ b/Backend/config/telescope.php @@ -0,0 +1,212 @@ + env('TELESCOPE_ENABLED', true), + + /* + |-------------------------------------------------------------------------- + | Telescope Domain + |-------------------------------------------------------------------------- + | + | This is the subdomain where Telescope will be accessible from. If the + | setting is null, Telescope will reside under the same domain as the + | application. Otherwise, this value will be used as the subdomain. + | + */ + + 'domain' => env('TELESCOPE_DOMAIN'), + + /* + |-------------------------------------------------------------------------- + | Telescope Path + |-------------------------------------------------------------------------- + | + | This is the URI path where Telescope will be accessible from. Feel free + | to change this path to anything you like. Note that the URI will not + | affect the paths of its internal API that aren't exposed to users. + | + */ + + 'path' => env('TELESCOPE_PATH', 'telescope'), + + /* + |-------------------------------------------------------------------------- + | Telescope Storage Driver + |-------------------------------------------------------------------------- + | + | This configuration options determines the storage driver that will + | be used to store Telescope's data. In addition, you may set any + | custom options as needed by the particular driver you choose. + | + */ + + 'driver' => env('TELESCOPE_DRIVER', 'database'), + + 'storage' => [ + 'database' => [ + 'connection' => env('DB_CONNECTION', 'mysql'), + 'chunk' => 1000, + ], + ], + + /* + |-------------------------------------------------------------------------- + | Telescope Queue + |-------------------------------------------------------------------------- + | + | This configuration options determines the queue connection and queue + | which will be used to process ProcessPendingUpdate jobs. This can + | be changed if you would prefer to use a non-default connection. + | + */ + + 'queue' => [ + 'connection' => env('TELESCOPE_QUEUE_CONNECTION'), + 'queue' => env('TELESCOPE_QUEUE'), + 'delay' => env('TELESCOPE_QUEUE_DELAY', 10), + ], + + /* + |-------------------------------------------------------------------------- + | Telescope Route Middleware + |-------------------------------------------------------------------------- + | + | These middleware will be assigned to every Telescope route, giving you + | the chance to add your own middleware to this list or change any of + | the existing middleware. Or, you can simply stick with this list. + | + */ + + 'middleware' => [ + 'web', + Authorize::class, + ], + + /* + |-------------------------------------------------------------------------- + | Allowed / Ignored Paths & Commands + |-------------------------------------------------------------------------- + | + | The following array lists the URI paths and Artisan commands that will + | not be watched by Telescope. In addition to this list, some Laravel + | commands, like migrations and queue commands, are always ignored. + | + */ + + 'only_paths' => [ + // 'api/*' + ], + + 'ignore_paths' => [ + 'livewire*', + 'nova-api*', + 'pulse*', + '_boost*', + '.well-known*', + ], + + 'ignore_commands' => [ + // + ], + + /* + |-------------------------------------------------------------------------- + | Telescope Watchers + |-------------------------------------------------------------------------- + | + | The following array lists the "watchers" that will be registered with + | Telescope. The watchers gather the application's profile data when + | a request or task is executed. Feel free to customize this list. + | + */ + + 'watchers' => [ + Watchers\BatchWatcher::class => env('TELESCOPE_BATCH_WATCHER', true), + + Watchers\CacheWatcher::class => [ + 'enabled' => env('TELESCOPE_CACHE_WATCHER', true), + 'hidden' => [], + 'ignore' => [], + ], + + Watchers\ClientRequestWatcher::class => [ + 'enabled' => env('TELESCOPE_CLIENT_REQUEST_WATCHER', true), + 'ignore_hosts' => [], + ], + + Watchers\CommandWatcher::class => [ + 'enabled' => env('TELESCOPE_COMMAND_WATCHER', true), + 'ignore' => [], + ], + + Watchers\DumpWatcher::class => [ + 'enabled' => env('TELESCOPE_DUMP_WATCHER', true), + 'always' => env('TELESCOPE_DUMP_WATCHER_ALWAYS', false), + ], + + Watchers\EventWatcher::class => [ + 'enabled' => env('TELESCOPE_EVENT_WATCHER', true), + 'ignore' => [], + ], + + Watchers\ExceptionWatcher::class => env('TELESCOPE_EXCEPTION_WATCHER', true), + + Watchers\GateWatcher::class => [ + 'enabled' => env('TELESCOPE_GATE_WATCHER', true), + 'ignore_abilities' => [], + 'ignore_packages' => true, + 'ignore_paths' => [], + ], + + Watchers\JobWatcher::class => env('TELESCOPE_JOB_WATCHER', true), + + Watchers\LogWatcher::class => [ + 'enabled' => env('TELESCOPE_LOG_WATCHER', true), + 'level' => 'error', + ], + + Watchers\MailWatcher::class => env('TELESCOPE_MAIL_WATCHER', true), + + Watchers\ModelWatcher::class => [ + 'enabled' => env('TELESCOPE_MODEL_WATCHER', true), + 'events' => ['eloquent.*'], + 'hydrations' => true, + ], + + Watchers\NotificationWatcher::class => env('TELESCOPE_NOTIFICATION_WATCHER', true), + + Watchers\QueryWatcher::class => [ + 'enabled' => env('TELESCOPE_QUERY_WATCHER', true), + 'ignore_packages' => true, + 'ignore_paths' => [], + 'slow' => 100, + ], + + Watchers\RedisWatcher::class => env('TELESCOPE_REDIS_WATCHER', true), + + Watchers\RequestWatcher::class => [ + 'enabled' => env('TELESCOPE_REQUEST_WATCHER', true), + 'size_limit' => env('TELESCOPE_RESPONSE_SIZE_LIMIT', 64), + 'ignore_http_methods' => [], + 'ignore_status_codes' => [], + ], + + Watchers\ScheduleWatcher::class => env('TELESCOPE_SCHEDULE_WATCHER', true), + Watchers\ViewWatcher::class => env('TELESCOPE_VIEW_WATCHER', true), + ], +]; diff --git a/Backend/database/.gitignore b/Backend/database/.gitignore new file mode 100644 index 0000000..9b19b93 --- /dev/null +++ b/Backend/database/.gitignore @@ -0,0 +1 @@ +*.sqlite* diff --git a/Backend/database/migrations/0001_01_01_000001_create_cache_table.php b/Backend/database/migrations/0001_01_01_000001_create_cache_table.php new file mode 100644 index 0000000..ed758bd --- /dev/null +++ b/Backend/database/migrations/0001_01_01_000001_create_cache_table.php @@ -0,0 +1,35 @@ +string('key')->primary(); + $table->mediumText('value'); + $table->integer('expiration')->index(); + }); + + Schema::create('cache_locks', function (Blueprint $table) { + $table->string('key')->primary(); + $table->string('owner'); + $table->integer('expiration')->index(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('cache'); + Schema::dropIfExists('cache_locks'); + } +}; diff --git a/Backend/database/migrations/0001_01_01_000002_create_jobs_table.php b/Backend/database/migrations/0001_01_01_000002_create_jobs_table.php new file mode 100644 index 0000000..425e705 --- /dev/null +++ b/Backend/database/migrations/0001_01_01_000002_create_jobs_table.php @@ -0,0 +1,57 @@ +id(); + $table->string('queue')->index(); + $table->longText('payload'); + $table->unsignedTinyInteger('attempts'); + $table->unsignedInteger('reserved_at')->nullable(); + $table->unsignedInteger('available_at'); + $table->unsignedInteger('created_at'); + }); + + Schema::create('job_batches', function (Blueprint $table) { + $table->string('id')->primary(); + $table->string('name'); + $table->integer('total_jobs'); + $table->integer('pending_jobs'); + $table->integer('failed_jobs'); + $table->longText('failed_job_ids'); + $table->mediumText('options')->nullable(); + $table->integer('cancelled_at')->nullable(); + $table->integer('created_at'); + $table->integer('finished_at')->nullable(); + }); + + Schema::create('failed_jobs', function (Blueprint $table) { + $table->id(); + $table->string('uuid')->unique(); + $table->text('connection'); + $table->text('queue'); + $table->longText('payload'); + $table->longText('exception'); + $table->timestamp('failed_at')->useCurrent(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('jobs'); + Schema::dropIfExists('job_batches'); + Schema::dropIfExists('failed_jobs'); + } +}; diff --git a/Backend/database/migrations/2026_03_20_231816_create_personal_access_tokens_table.php b/Backend/database/migrations/2026_03_20_231816_create_personal_access_tokens_table.php new file mode 100644 index 0000000..62a379f --- /dev/null +++ b/Backend/database/migrations/2026_03_20_231816_create_personal_access_tokens_table.php @@ -0,0 +1,33 @@ +id(); + $table->uuidMorphs('tokenable'); + $table->text('name'); + $table->string('token', 64)->unique(); + $table->text('abilities')->nullable(); + $table->timestamp('last_used_at')->nullable(); + $table->timestamp('expires_at')->nullable()->index(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('personal_access_tokens'); + } +}; diff --git a/Backend/database/migrations/2026_03_25_011056_create_database_schema.php b/Backend/database/migrations/2026_03_25_011056_create_database_schema.php new file mode 100644 index 0000000..29e4c54 --- /dev/null +++ b/Backend/database/migrations/2026_03_25_011056_create_database_schema.php @@ -0,0 +1,445 @@ +uuid('uuid')->primary(); + $table->string('business_code')->unique(); + $table->string('business_name'); + $table->string('business_email')->nullable(); + $table->string('business_phone')->nullable(); + $table->string('business_type')->nullable(); + $table->string('server_logo_url')->nullable(); + $table->string('subdomain')->nullable()->unique(); + $table->string('online_store_name')->nullable(); + $table->string('online_open_time')->nullable(); + $table->string('online_close_time')->nullable(); + $table->boolean('is_close_service')->default(false); + $table->integer('preparation_time_minutes')->default(0); + $table->integer('order_interval_minutes')->default(0); + $table->boolean('allow_cod')->default(false); + $table->decimal('max_delivery_radius_km', 8, 2)->default(0.0); + $table->string('delivery_fee_type')->default('fixed'); + $table->decimal('delivery_flat_fee', 12, 2)->default(0.0); + $table->decimal('delivery_fee_per_km', 12, 2)->default(0.0); + $table->decimal('delivery_base_fee', 12, 2)->default(0.0); + $table->string('review_display_type')->default('rating_only'); + $table->string('review_display_policy')->default('four_and_five_star_only'); + $table->string('subscription_level')->default('standart'); + $table->integer('limit_outlet')->default(5); + $table->timestamp('subscription_expires_at')->nullable(); + $table->timestamps(); + }); + + // Tenant Payment Methods + Schema::create('tenant_payment_methods', function (Blueprint $table) { + $table->uuid('uuid')->primary(); + $table->foreignUuid('tenant_id')->references('uuid')->on('tenants'); + $table->string('source_name')->nullable(); + $table->string('account_number')->nullable(); + $table->string('receipent_name')->nullable(); + $table->timestamps(); + }); + + // Campaigns + Schema::create('campaigns', function (Blueprint $table) { + $table->uuid('uuid')->primary(); + $table->foreignUuid('tenant_id')->references('uuid')->on('tenants'); + $table->string('title'); + $table->string('server_banner_url')->nullable(); + $table->dateTime('start_date')->nullable(); + $table->dateTime('end_date')->nullable(); + $table->string('action_type')->nullable(); + $table->string('action_reference_id')->nullable(); + $table->boolean('is_active')->default(true); + $table->timestamps(); + $table->softDeletes(); + }); + + // Outlets + Schema::create('outlets', function (Blueprint $table) { + $table->uuid('uuid')->primary(); + $table->foreignUuid('tenant_id')->references('uuid')->on('tenants'); + $table->string('name'); + $table->string('email')->nullable(); + $table->string('phone_number')->nullable(); + $table->text('full_address')->nullable(); + $table->decimal('latitude', 10, 8)->nullable(); + $table->decimal('longitude', 11, 8)->nullable(); + $table->string('server_photo_url')->nullable(); + $table->string('server_banner_url')->nullable(); + $table->boolean('is_main_outlet')->default(false); + $table->boolean('is_active')->default(true); + $table->timestamps(); + $table->softDeletes(); + }); + + // Outlet Campaigns + Schema::create('outlet_campaigns', function (Blueprint $table) { + $table->uuid('uuid')->primary(); + $table->foreignUuid('tenant_id')->references('uuid')->on('tenants'); + $table->foreignUuid('campaign_id')->references('uuid')->on('campaigns'); + $table->foreignUuid('outlet_id')->nullable()->references('uuid')->on('outlets'); + $table->timestamps(); + $table->softDeletes(); + }); + + // Users + Schema::create('users', function (Blueprint $table) { + $table->uuid('uuid')->primary(); + $table->foreignUuid('tenant_id')->references('uuid')->on('tenants'); + $table->foreignUuid('outlet_id')->nullable()->references('uuid')->on('outlets'); + $table->string('name'); + $table->string('email')->nullable(); + $table->string('phone_number')->nullable(); + $table->string('password'); + $table->string('role'); + $table->string('place_of_birth')->nullable(); + $table->string('date_of_birth')->nullable(); + $table->string('shift_name')->nullable(); + $table->string('shift_start_time')->nullable(); + $table->string('shift_end_time')->nullable(); + $table->boolean('is_active')->default(true); + $table->timestamp('last_sync')->nullable(); + $table->timestamps(); + $table->softDeletes(); + }); + + // Units + Schema::create('units', function (Blueprint $table) { + $table->uuid('uuid')->primary(); + $table->foreignUuid('tenant_id')->references('uuid')->on('tenants'); + $table->string('name'); + $table->timestamps(); + }); + + // Categories + Schema::create('categories', function (Blueprint $table) { + $table->uuid('uuid')->primary(); + $table->foreignUuid('tenant_id')->references('uuid')->on('tenants'); + $table->string('name'); + $table->string('server_image_url')->nullable(); + $table->timestamps(); + }); + + // Products + Schema::create('products', function (Blueprint $table) { + $table->uuid('uuid')->primary(); + $table->foreignUuid('tenant_id')->references('uuid')->on('tenants'); + $table->foreignUuid('category_id')->references('uuid')->on('categories'); + $table->foreignUuid('unit_id')->references('uuid')->on('units'); + $table->string('name'); + $table->json('server_image_url')->nullable(); + $table->boolean('has_variant')->default(false); + $table->string('description', 3000)->nullable(); + $table->timestamps(); + $table->softDeletes(); + }); + + // Product Variants + Schema::create('product_variants', function (Blueprint $table) { + $table->uuid('uuid')->primary(); + $table->foreignUuid('tenant_id')->references('uuid')->on('tenants'); + $table->foreignUuid('product_id')->references('uuid')->on('products'); + $table->string('name'); + $table->string('server_image_url')->nullable(); + $table->boolean('is_visible_online')->default(false); + $table->integer('total_rating')->default(0); + $table->double('average_rating')->default(0.0); + $table->double('total_sold')->default(0.0); + $table->boolean('is_product_variant')->default(false); + $table->timestamps(); + $table->softDeletes(); + }); + + // Raw Materials + Schema::create('raw_materials', function (Blueprint $table) { + $table->uuid('uuid')->primary(); + $table->foreignUuid('tenant_id')->references('uuid')->on('tenants'); + $table->foreignUuid('unit_id')->references('uuid')->on('units'); + $table->string('name'); + $table->timestamps(); + $table->softDeletes(); + }); + + // Receipt Items + Schema::create('receipt_items', function(Blueprint $table) { + $table->uuid('uuid')->primary(); + $table->foreignUuid('tenant_id')->references('uuid')->on('tenants'); + $table->foreignUuid('product_variant_id')->nullable()->references('uuid')->on('product_variants'); + $table->foreignUuid('raw_material_id')->nullable()->references('uuid')->on('raw_materials'); + $table->double('quantity')->default(0.0); + $table->timestamps(); + $table->softDeletes(); + }); + + // Outlet Inventories + Schema::create('outlet_inventories', function (Blueprint $table) { + $table->uuid('uuid')->primary(); + $table->foreignUuid('tenant_id')->references('uuid')->on('tenants'); + $table->foreignUuid('outlet_id')->references('uuid')->on('outlets'); + $table->string('item_type'); + $table->uuid('item_id'); + $table->string('stock_type')->default('fixed'); + $table->double('stock')->default(0.0); + $table->double('selling_price')->default(0.0); + $table->double('cost_price')->default(0.0); + $table->double('purchase_price')->default(0.0); + $table->boolean('is_delete_by_owner')->nullable(); + $table->timestamps(); + $table->softDeletes(); + }); + + // Customers + Schema::create('customers', function (Blueprint $table) { + $table->uuid('uuid')->primary(); + $table->foreignUuid('tenant_id')->references('uuid')->on('tenants'); + $table->foreignUuid('outlet_id')->references('uuid')->on('outlets'); + $table->string('name'); + $table->string('email')->nullable(); + $table->string('phone_number')->nullable(); + $table->string('password')->nullable(); + $table->string('source'); + $table->timestamps(); + $table->softDeletes(); + }); + + // Customer Addresses + Schema::create('customer_addresses', function (Blueprint $table) { + $table->uuid('uuid')->primary(); + $table->foreignUuid('tenant_id')->references('uuid')->on('tenants'); + $table->foreignUuid('customer_id')->references('uuid')->on('customers'); + $table->string('label')->nullable(); + $table->string('full_address')->nullable(); + $table->decimal('latitude', 10, 8)->nullable(); + $table->decimal('longitude', 11, 8)->nullable(); + $table->string('notes')->nullable(); + $table->timestamps(); + $table->softDeletes(); + }); + + // Product Likes + Schema::create('product_likes', function (Blueprint $table) { + $table->uuid('uuid')->primary(); + $table->foreignUuid('tenant_id')->references('uuid')->on('tenants'); + $table->uuid('customer_id')->nullable()->references('uuid')->on('customers'); + $table->uuid('product_id')->references('uuid')->on('products'); + $table->boolean('is_like')->default(false); + $table->timestamps(); + }); + + // Product Reviews + Schema::create('product_reviews', function (Blueprint $table) { + $table->uuid('uuid')->primary(); + $table->foreignUuid('tenant_id')->references('uuid')->on('tenants'); + $table->foreignUuid('customer_id')->nullable()->references('uuid')->on('customers'); + $table->foreignUuid('product_variant_id')->references('uuid')->on('product_variants'); + $table->integer('rating')->default(0); + $table->text('comment')->nullable(); + $table->timestamps(); + }); + + // Orders + Schema::create('orders', function (Blueprint $table) { + $table->uuid('uuid')->primary(); + $table->foreignUuid('tenant_id')->references('uuid')->on('tenants'); + $table->string('order_number')->unique(); + $table->foreignUuid('outlet_id')->references('uuid')->on('outlets'); + $table->string('outlet_name_snapshot'); + $table->string('outlet_phone_number_snapshot')->nullable(); + $table->json('outlet_address_snapshot')->nullable(); + $table->foreignUuid('customer_id')->nullable()->references('uuid')->on('customers'); + $table->string('customer_name_snapshot')->nullable(); + $table->string('customer_email_snapshot')->nullable(); + $table->string('customer_phone_number_snapshot')->nullable(); + $table->json('customer_address_snapshot')->nullable(); + $table->foreignUuid('user_id')->nullable()->references('uuid')->on('users'); + $table->string('admin_name_snapshot')->nullable(); + $table->string('source'); + $table->boolean('already_read')->nullable(); + $table->string('order_status'); + $table->boolean('is_cancellation')->nullable(); + $table->boolean('cancellation_accepted')->nullable(); + $table->string('approval_process')->nullable(); + $table->string('canceled_by')->nullable(); + $table->string('delivery_type')->nullable(); + $table->text('delivery_preference')->nullable(); + $table->string('delivery_fee_type')->nullable(); + $table->double('delivery_fee')->default(0.0); + $table->double('total_delivery_fee')->default(0.0); + $table->double('total_order')->default(0.0); + $table->double('discount')->default(0.0); + $table->double('tax')->default(0.0); + $table->double('total_amount')->default(0.0); + $table->double('amount_paid')->default(0.0); + $table->double('change_amount')->default(0.0); + $table->text('notes')->nullable(); + $table->string('payment_proof_url')->nullable(); + $table->string('payment_method')->nullable(); + $table->boolean('has_been_assessed')->nullable(); + $table->timestamps(); + }); + + // Order Items + Schema::create('order_items', function (Blueprint $table) { + $table->uuid('uuid')->primary(); + $table->foreignUuid('tenant_id')->references('uuid')->on('tenants'); + $table->foreignUuid('order_id')->references('uuid')->on('orders'); + $table->foreignUuid('product_variant_id')->references('uuid')->on('product_variants'); + $table->double('quantity')->default(0.0); + $table->string('product_image_url_snapshot')->nullable(); + $table->string('product_name_snapshot'); + $table->string('product_variant_name_snapshot')->nullable(); + $table->double('selling_price_snapshot')->default(0.0); + $table->double('subtotal')->default(0.0); + $table->timestamps(); + }); + + // Suppliers + Schema::create('suppliers', function (Blueprint $table) { + $table->uuid('uuid')->primary(); + $table->foreignUuid('tenant_id')->references('uuid')->on('tenants'); + $table->string('name'); + $table->string('email')->nullable(); + $table->string('phone_number')->nullable(); + $table->string('address')->nullable(); + $table->timestamps(); + $table->softDeletes(); + }); + + // Purchase + Schema::create('purchases', function (Blueprint $table) { + $table->uuid('uuid')->primary(); + $table->foreignUuid('tenant_id')->references('uuid')->on('tenants'); + $table->string('purchase_number')->unique(); + $table->foreignUuid('outlet_id')->references('uuid')->on('outlets'); + $table->string('outlet_name_snapshot'); + $table->foreignUuid('supplier_id')->nullable()->references('uuid')->on('suppliers'); + $table->string('supplier_name_snapshot')->nullable(); + $table->string('supplier_email_snapshot')->nullable(); + $table->string('supplier_phone_number_snapshot')->nullable(); + $table->foreignUuid('user_id')->references('uuid')->on('users'); + $table->string('admin_name_snapshot'); + $table->string('purchase_status'); + $table->double('total_purchase')->default(0); + $table->double('discount')->default(0); + $table->double('tax')->default(0); + $table->string('payment_status'); + $table->double('total_amount')->default(0); + $table->double('amount_paid')->default(0); + $table->double('change_amount')->default(0); + $table->double('under_payment_amount')->default(0); + $table->double('debt_amount')->default(0); + $table->string('payment_proof_url')->nullable(); + $table->text('notes')->nullable(); + $table->timestamps(); + }); + + // Purchase Item + Schema::create('purchase_items', function (Blueprint $table) { + $table->uuid('uuid')->primary(); + $table->foreignUuid('tenant_id')->references('uuid')->on('tenants'); + $table->foreignUuid('purchase_id')->references('uuid')->on('purchases'); + $table->string('item_type'); + $table->uuid('item_id'); + $table->double('quantity')->default(0); + $table->string('item_image_url_snapshot')->nullable(); + $table->string('item_name_snapshot'); + $table->string('item_variant_name_snapshot')->nullable(); + $table->double('purchase_price_snapshot')->default(0); + $table->double('subtotal')->default(0); + $table->timestamps(); + }); + + // Transactions + Schema::create('transactions', function(Blueprint $table) { + $table->uuid('uuid')->primary(); + $table->foreignUuid('tenant_id')->references('uuid')->on('tenants'); + $table->foreignUuid('outlet_id')->references('uuid')->on('outlets'); + $table->foreignUuid('user_id')->references('uuid')->on('users'); + $table->string('reference_type'); + $table->string('reference_id'); + $table->string('reference_number'); + $table->string('stakeholder')->nullable(); + $table->string('transaction_type'); + $table->string('category'); + $table->double('amount')->default(0.0); + $table->string('notes')->nullable(); + $table->timestamps(); + }); + + // Stock Cards + Schema::create('stock_cards', function (Blueprint $table) { + $table->uuid('uuid')->primary(); + $table->foreignUuid('tenant_id')->references('uuid')->on('tenants'); + $table->foreignUuid('outlet_inventory_id')->references('uuid')->on('outlet_inventories'); + $table->foreignUuid('user_id')->references('uuid')->on('users'); + $table->string('type'); + $table->string('reference_id')->nullable(); + $table->uuid('receive_from_id')->nullable(); + $table->string('receive_from_name')->nullable(); + $table->string('document_number')->nullable(); + $table->double('qty_change')->default(0.0); + $table->double('current_stock')->default(0.0); + $table->string('stock_type')->default('fixed'); + $table->boolean('mode_change')->default(false); + $table->string('mode_before_change')->nullable(); + $table->string('mode_after_change')->nullable(); + $table->double('cost_price')->default(0.0); + $table->text('notes')->nullable(); + $table->timestamp('created_at')->useCurrent(); + }); + + Schema::create('sync_deletes', function (Blueprint $table) { + $table->id(); + $table->string('tenant_id')->index(); + $table->string('table_name'); + $table->string('record_uuid'); + $table->timestamp('created_at')->useCurrent(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('tenants'); + Schema::dropIfExists('tenant_payment_methods'); + Schema::dropIfExists('campaigns'); + Schema::dropIfExists('outlets'); + Schema::dropIfExists('outlet_campaigns'); + Schema::dropIfExists('users'); + Schema::dropIfExists('units'); + Schema::dropIfExists('categories'); + Schema::dropIfExists('products'); + Schema::dropIfExists('product_variants'); + Schema::dropIfExists('raw_materials'); + Schema::dropIfExists('receipt_items'); + Schema::dropIfExists('outlet_inventories'); + Schema::dropIfExists('customers'); + Schema::dropIfExists('customer_addresses'); + Schema::dropIfExists('product_likes'); + Schema::dropIfExists('product_reviews'); + Schema::dropIfExists('orders'); + Schema::dropIfExists('order_items'); + Schema::dropIfExists('suppliers'); + Schema::dropIfExists('purchases'); + Schema::dropIfExists('purchase_items'); + Schema::dropIfExists('transactions'); + Schema::dropIfExists('stock_cards'); + Schema::dropIfExists('sync_deletes'); + Schema::dropIfExists('password_reset_tokens'); + Schema::dropIfExists('sessions'); + } +}; diff --git a/Backend/database/migrations/2026_04_02_212728_create_telescope_entries_table.php b/Backend/database/migrations/2026_04_02_212728_create_telescope_entries_table.php new file mode 100644 index 0000000..031b6f4 --- /dev/null +++ b/Backend/database/migrations/2026_04_02_212728_create_telescope_entries_table.php @@ -0,0 +1,70 @@ +getConnection()); + + $schema->create('telescope_entries', function (Blueprint $table) { + $table->bigIncrements('sequence'); + $table->uuid('uuid'); + $table->uuid('batch_id'); + $table->string('family_hash')->nullable(); + $table->boolean('should_display_on_index')->default(true); + $table->string('type', 20); + $table->longText('content'); + $table->dateTime('created_at')->nullable(); + + $table->unique('uuid'); + $table->index('batch_id'); + $table->index('family_hash'); + $table->index('created_at'); + $table->index(['type', 'should_display_on_index']); + }); + + $schema->create('telescope_entries_tags', function (Blueprint $table) { + $table->uuid('entry_uuid'); + $table->string('tag'); + + $table->primary(['entry_uuid', 'tag']); + $table->index('tag'); + + $table->foreign('entry_uuid') + ->references('uuid') + ->on('telescope_entries') + ->cascadeOnDelete(); + }); + + $schema->create('telescope_monitoring', function (Blueprint $table) { + $table->string('tag')->primary(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + $schema = Schema::connection($this->getConnection()); + + $schema->dropIfExists('telescope_entries_tags'); + $schema->dropIfExists('telescope_entries'); + $schema->dropIfExists('telescope_monitoring'); + } +}; diff --git a/Backend/database/seeders/DatabaseSeeder.php b/Backend/database/seeders/DatabaseSeeder.php new file mode 100644 index 0000000..43a9982 --- /dev/null +++ b/Backend/database/seeders/DatabaseSeeder.php @@ -0,0 +1,75 @@ + (string) Str::uuid(), + 'business_code' => 'QUEEN01', + 'business_name' => 'QueenFruits Indonesia', + 'business_type' => 'F&B - Fruit Store', + 'subscription_level' => 'standart', + ]); + + // 2. Buat Beberapa Outlet (Untuk testing Owner yang menerima Array) + $outletUtama = Outlet::create([ + 'uuid' => (string) Str::uuid(), + 'tenant_id' => $tenant->id, + 'name' => 'QueenFruits - Pusat Kaliwates', + 'phone_number' => '08123456789', + 'is_main_outlet' => true, + ]); + + $outletCabang = Outlet::create([ + 'uuid' => (string) Str::uuid(), + 'tenant_id' => $tenant->id, + 'name' => 'QueenFruits - Cabang Sumbersari', + 'phone_number' => '08123456788', + 'is_main_outlet' => false, + ]); + + // 3. Buat User Owner (Menerima semua outlet dalam bentuk Array) + User::create([ + 'uuid' => (string) Str::uuid(), + 'tenant_id' => $tenant->id, + 'outlet_id' => $outletUtama->id, + 'name' => 'Ali Akbar Rafsanjani', + 'email' => 'owner@niogu.com', + 'password' => Hash::make('password123'), + 'role' => 'owner', + 'is_active' => true, + ]); + + // 4. Buat User Staff (Menerima hanya satu outlet terkait dalam bentuk Object) + User::create([ + 'uuid' => (string) Str::uuid(), + 'tenant_id' => $tenant->id, + 'outlet_id' => $outletCabang->id, // Terikat ke Cabang Sumbersari + 'name' => 'Budi Kasir', + 'phone_number' => '08123456789', + 'password' => Hash::make('password123'), + 'role' => 'admin', + 'shift_name' => 'Shift 1', + 'shift_start_time' => '08.00', + 'shift_end_time' => '14.00', + 'is_active' => true, + ]); + } +} diff --git a/Backend/package.json b/Backend/package.json new file mode 100644 index 0000000..7686b29 --- /dev/null +++ b/Backend/package.json @@ -0,0 +1,17 @@ +{ + "$schema": "https://www.schemastore.org/package.json", + "private": true, + "type": "module", + "scripts": { + "build": "vite build", + "dev": "vite" + }, + "devDependencies": { + "@tailwindcss/vite": "^4.0.0", + "axios": "^1.11.0", + "concurrently": "^9.0.1", + "laravel-vite-plugin": "^2.0.0", + "tailwindcss": "^4.0.0", + "vite": "^7.0.7" + } +} diff --git a/Backend/phpunit.xml b/Backend/phpunit.xml new file mode 100644 index 0000000..d703241 --- /dev/null +++ b/Backend/phpunit.xml @@ -0,0 +1,35 @@ + + + + + tests/Unit + + + tests/Feature + + + + + app + + + + + + + + + + + + + + + + + + diff --git a/Backend/public/.htaccess b/Backend/public/.htaccess new file mode 100644 index 0000000..3039e36 --- /dev/null +++ b/Backend/public/.htaccess @@ -0,0 +1,29 @@ + + + Options -MultiViews -Indexes + + + + Header set Access-Control-Allow-Origin "*" + + + RewriteEngine On + + # Handle Authorization Header + RewriteCond %{HTTP:Authorization} . + RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}] + + # Handle X-XSRF-Token Header + RewriteCond %{HTTP:x-xsrf-token} . + RewriteRule .* - [E=HTTP_X_XSRF_TOKEN:%{HTTP:X-XSRF-Token}] + + # Redirect Trailing Slashes If Not A Folder... + RewriteCond %{REQUEST_FILENAME} !-d + RewriteCond %{REQUEST_URI} (.+)/$ + RewriteRule ^ %1 [L,R=301] + + # Send Requests To Front Controller... + RewriteCond %{REQUEST_FILENAME} !-d + RewriteCond %{REQUEST_FILENAME} !-f + RewriteRule ^ index.php [L] + diff --git a/Backend/public/favicon.ico b/Backend/public/favicon.ico new file mode 100644 index 0000000..e69de29 diff --git a/Backend/public/index.php b/Backend/public/index.php new file mode 100644 index 0000000..ee8f07e --- /dev/null +++ b/Backend/public/index.php @@ -0,0 +1,20 @@ +handleRequest(Request::capture()); diff --git a/Backend/public/robots.txt b/Backend/public/robots.txt new file mode 100644 index 0000000..eb05362 --- /dev/null +++ b/Backend/public/robots.txt @@ -0,0 +1,2 @@ +User-agent: * +Disallow: diff --git a/Backend/resources/css/app.css b/Backend/resources/css/app.css new file mode 100644 index 0000000..3e6abea --- /dev/null +++ b/Backend/resources/css/app.css @@ -0,0 +1,11 @@ +@import 'tailwindcss'; + +@source '../../vendor/laravel/framework/src/Illuminate/Pagination/resources/views/*.blade.php'; +@source '../../storage/framework/views/*.php'; +@source '../**/*.blade.php'; +@source '../**/*.js'; + +@theme { + --font-sans: 'Instrument Sans', ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', + 'Segoe UI Symbol', 'Noto Color Emoji'; +} diff --git a/Backend/resources/js/app.js b/Backend/resources/js/app.js new file mode 100644 index 0000000..e59d6a0 --- /dev/null +++ b/Backend/resources/js/app.js @@ -0,0 +1 @@ +import './bootstrap'; diff --git a/Backend/resources/js/bootstrap.js b/Backend/resources/js/bootstrap.js new file mode 100644 index 0000000..deb2e10 --- /dev/null +++ b/Backend/resources/js/bootstrap.js @@ -0,0 +1,12 @@ +import axios from 'axios'; +window.axios = axios; + +window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest'; + +/** + * Echo exposes an expressive API for subscribing to channels and listening + * for events that are broadcast by Laravel. Echo and event broadcasting + * allow your team to quickly build robust real-time web applications. + */ + +import './echo'; diff --git a/Backend/resources/js/echo.js b/Backend/resources/js/echo.js new file mode 100644 index 0000000..9349afa --- /dev/null +++ b/Backend/resources/js/echo.js @@ -0,0 +1,14 @@ +import Echo from 'laravel-echo'; + +import Pusher from 'pusher-js'; +window.Pusher = Pusher; + +window.Echo = new Echo({ + broadcaster: 'reverb', + key: import.meta.env.VITE_REVERB_APP_KEY, + wsHost: import.meta.env.VITE_REVERB_HOST, + wsPort: import.meta.env.VITE_REVERB_PORT ?? 80, + wssPort: import.meta.env.VITE_REVERB_PORT ?? 443, + forceTLS: (import.meta.env.VITE_REVERB_SCHEME ?? 'https') === 'https', + enabledTransports: ['ws', 'wss'], +}); diff --git a/Backend/resources/views/welcome.blade.php b/Backend/resources/views/welcome.blade.php new file mode 100644 index 0000000..b7355d7 --- /dev/null +++ b/Backend/resources/views/welcome.blade.php @@ -0,0 +1,277 @@ + + + + + + + {{ config('app.name', 'Laravel') }} + + + + + + + @if (file_exists(public_path('build/manifest.json')) || file_exists(public_path('hot'))) + @vite(['resources/css/app.css', 'resources/js/app.js']) + @else + + @endif + + +
+ @if (Route::has('login')) + + @endif +
+
+
+
+

Let's get started

+

Laravel has an incredibly rich ecosystem.
We suggest starting with the following.

+ + +
+
+ {{-- Laravel Logo --}} + + + + + + + + + + + {{-- Light Mode 12 SVG --}} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {{-- Dark Mode 12 SVG --}} + +
+
+
+
+ + @if (Route::has('login')) + + @endif + + diff --git a/Backend/routes/api.php b/Backend/routes/api.php new file mode 100644 index 0000000..22c4779 --- /dev/null +++ b/Backend/routes/api.php @@ -0,0 +1,83 @@ + ['auth:sanctum']]); + +// --- Public / Auth Routes --- +Route::post('/auth/login', [AuthController::class, 'login']); +Route::post('/auth/register', [AuthController::class, 'register']); + +// --- Protected Routes (Perlu Login) --- +Route::middleware('auth:sanctum')->group(function () { + // Media Upload + Route::post('/uploads/images', [UploadController::class, 'uploadImage']); + + // Core Synchronization + Route::prefix('sync')->group(function () { + Route::post('/up', [SyncController::class, 'batchUpSync']); + Route::get('/down', [SyncController::class, 'getDeltaSync']); + }); + + // Order Synchronization + Route::get('orders/sync/{orderId}', [OrderController::class, 'getOrderSync']); +}); + +// --- Tenant Specific Routes (Ecommerce / Client Side) --- +Route::middleware('identify.tenant')->group(function() { + + Route::prefix('tenant')->group(function() { + Route::get('/', [ConfigurationController::class, 'getConfigurationService']); + }); + + // Home & Storefront + Route::get('/home/{outletId}', [HomeController::class, 'home']); + Route::get('/outlets', [HomeController::class, 'getOutlets']); + Route::get('/outlets/main', [HomeController::class, 'getMainOutlet']); + + // Products + Route::prefix('products')->group(function () { + Route::get('/{id}', [ProductController::class, 'getProductById']); + Route::get('/search/{outletId}', [ProductController::class, 'getProductByName']); // Ganti dari search-products + Route::get('/categories/{outletId}/{categoryId}', [ProductController::class, 'getProductByCategory']); + Route::get('/best-sellers/{outletId}', [ProductController::class, 'getProductBestSellerByOutlet']); + }); + + // Orders & Checkout + Route::post('/checkout', [CheckoutController::class, 'store']); + + Route::prefix('orders')->group(function () { + Route::get('/customer/{customerId}', [OrderController::class, 'getOrders']); + Route::get('/customer/{customerId}/{orderId}', [OrderController::class, 'getOrderById']); + Route::patch('/{customerId}/{orderId}/cancel', [OrderController::class, 'cancelOrder']); + Route::patch('/{customerId}/{orderId}/cancellation-request', [OrderController::class, 'cancellationRequest']); + }); + + Route::prefix('review')->group(function() { + Route::get('/{orderId}', [ReviewController::class, 'getProductReviewItem']); + Route::post('/{orderId}', [ReviewController::class, 'reviewProduct']); + }); + + Route::prefix('customer')->group(function() { + Route::post('/auth/login', [CustomerController::class, 'login']); + Route::post('/auth/register', [CustomerController::class, 'register']); + Route::patch('/{customerId}', [CustomerController::class, 'updateCustomer']); + Route::patch('/{customerId}/password', [CustomerController::class, 'updatePassword']); + Route::post('/{customerId}', [CustomerController::class, 'saveCustomerAddress']); + Route::get('/{customerId}/addresses', [CustomerController::class, 'getCustomerAddress']); + Route::delete('/{customerId}/addresses/{id}', [CustomerController::class, 'deleteCustomerAddress']); + }); +}); diff --git a/Backend/routes/channels.php b/Backend/routes/channels.php new file mode 100644 index 0000000..00d9846 --- /dev/null +++ b/Backend/routes/channels.php @@ -0,0 +1,11 @@ +tenant_id === (string) $tenantId; +}); diff --git a/Backend/routes/console.php b/Backend/routes/console.php new file mode 100644 index 0000000..3c9adf1 --- /dev/null +++ b/Backend/routes/console.php @@ -0,0 +1,8 @@ +comment(Inspiring::quote()); +})->purpose('Display an inspiring quote'); diff --git a/Backend/routes/web.php b/Backend/routes/web.php new file mode 100644 index 0000000..69201d7 --- /dev/null +++ b/Backend/routes/web.php @@ -0,0 +1,9 @@ +get('/'); + + $response->assertStatus(200); + } +} diff --git a/Backend/tests/TestCase.php b/Backend/tests/TestCase.php new file mode 100644 index 0000000..fe1ffc2 --- /dev/null +++ b/Backend/tests/TestCase.php @@ -0,0 +1,10 @@ +assertTrue(true); + } +} diff --git a/Backend/vite.config.js b/Backend/vite.config.js new file mode 100644 index 0000000..f35b4e7 --- /dev/null +++ b/Backend/vite.config.js @@ -0,0 +1,18 @@ +import { defineConfig } from 'vite'; +import laravel from 'laravel-vite-plugin'; +import tailwindcss from '@tailwindcss/vite'; + +export default defineConfig({ + plugins: [ + laravel({ + input: ['resources/css/app.css', 'resources/js/app.js'], + refresh: true, + }), + tailwindcss(), + ], + server: { + watch: { + ignored: ['**/storage/framework/views/**'], + }, + }, +}); diff --git a/Mobile Commerce/.gitignore b/Mobile Commerce/.gitignore new file mode 100644 index 0000000..3820a95 --- /dev/null +++ b/Mobile Commerce/.gitignore @@ -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-dependencies +.pub-cache/ +.pub/ +/build/ +/coverage/ + +# 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 diff --git a/Mobile Commerce/.metadata b/Mobile Commerce/.metadata new file mode 100644 index 0000000..792284a --- /dev/null +++ b/Mobile Commerce/.metadata @@ -0,0 +1,30 @@ +# 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: "19074d12f7eaf6a8180cd4036a430c1d76de904e" + channel: "stable" + +project_type: app + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: 19074d12f7eaf6a8180cd4036a430c1d76de904e + base_revision: 19074d12f7eaf6a8180cd4036a430c1d76de904e + - platform: android + create_revision: 19074d12f7eaf6a8180cd4036a430c1d76de904e + base_revision: 19074d12f7eaf6a8180cd4036a430c1d76de904e + + # 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' diff --git a/Mobile Commerce/README.md b/Mobile Commerce/README.md new file mode 100644 index 0000000..e69de29 diff --git a/Mobile Commerce/analysis_options.yaml b/Mobile Commerce/analysis_options.yaml new file mode 100644 index 0000000..6d17993 --- /dev/null +++ b/Mobile Commerce/analysis_options.yaml @@ -0,0 +1,24 @@ +# 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`. + +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 diff --git a/Mobile Commerce/android/.gitignore b/Mobile Commerce/android/.gitignore new file mode 100644 index 0000000..be3943c --- /dev/null +++ b/Mobile Commerce/android/.gitignore @@ -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 diff --git a/Mobile Commerce/android/app/build.gradle.kts b/Mobile Commerce/android/app/build.gradle.kts new file mode 100644 index 0000000..e26d495 --- /dev/null +++ b/Mobile Commerce/android/app/build.gradle.kts @@ -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.niogu_ecommerce_v1" + compileSdk = flutter.compileSdkVersion + ndkVersion = flutter.ndkVersion + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + + kotlinOptions { + jvmTarget = JavaVersion.VERSION_17.toString() + } + + defaultConfig { + // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). + applicationId = "com.example.niogu_ecommerce_v1" + // 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 = "../.." +} diff --git a/Mobile Commerce/android/app/src/debug/AndroidManifest.xml b/Mobile Commerce/android/app/src/debug/AndroidManifest.xml new file mode 100644 index 0000000..399f698 --- /dev/null +++ b/Mobile Commerce/android/app/src/debug/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/Mobile Commerce/android/app/src/main/AndroidManifest.xml b/Mobile Commerce/android/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..5052eaa --- /dev/null +++ b/Mobile Commerce/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Mobile Commerce/android/app/src/main/kotlin/com/example/niogu_ecommerce_v1/MainActivity.kt b/Mobile Commerce/android/app/src/main/kotlin/com/example/niogu_ecommerce_v1/MainActivity.kt new file mode 100644 index 0000000..40e6084 --- /dev/null +++ b/Mobile Commerce/android/app/src/main/kotlin/com/example/niogu_ecommerce_v1/MainActivity.kt @@ -0,0 +1,5 @@ +package com.example.niogu_ecommerce_v1 + +import io.flutter.embedding.android.FlutterActivity + +class MainActivity : FlutterActivity() diff --git a/Mobile Commerce/android/app/src/main/res/drawable-v21/launch_background.xml b/Mobile Commerce/android/app/src/main/res/drawable-v21/launch_background.xml new file mode 100644 index 0000000..f74085f --- /dev/null +++ b/Mobile Commerce/android/app/src/main/res/drawable-v21/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/Mobile Commerce/android/app/src/main/res/drawable/launch_background.xml b/Mobile Commerce/android/app/src/main/res/drawable/launch_background.xml new file mode 100644 index 0000000..304732f --- /dev/null +++ b/Mobile Commerce/android/app/src/main/res/drawable/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/Mobile Commerce/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/Mobile Commerce/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000..db77bb4 Binary files /dev/null and b/Mobile Commerce/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/Mobile Commerce/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/Mobile Commerce/android/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000..17987b7 Binary files /dev/null and b/Mobile Commerce/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/Mobile Commerce/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/Mobile Commerce/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000..09d4391 Binary files /dev/null and b/Mobile Commerce/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/Mobile Commerce/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/Mobile Commerce/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000..d5f1c8d Binary files /dev/null and b/Mobile Commerce/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/Mobile Commerce/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/Mobile Commerce/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000..4d6372e Binary files /dev/null and b/Mobile Commerce/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/Mobile Commerce/android/app/src/main/res/values-night/styles.xml b/Mobile Commerce/android/app/src/main/res/values-night/styles.xml new file mode 100644 index 0000000..06952be --- /dev/null +++ b/Mobile Commerce/android/app/src/main/res/values-night/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/Mobile Commerce/android/app/src/main/res/values/styles.xml b/Mobile Commerce/android/app/src/main/res/values/styles.xml new file mode 100644 index 0000000..cb1ef88 --- /dev/null +++ b/Mobile Commerce/android/app/src/main/res/values/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/Mobile Commerce/android/app/src/profile/AndroidManifest.xml b/Mobile Commerce/android/app/src/profile/AndroidManifest.xml new file mode 100644 index 0000000..399f698 --- /dev/null +++ b/Mobile Commerce/android/app/src/profile/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/Mobile Commerce/android/build.gradle.kts b/Mobile Commerce/android/build.gradle.kts new file mode 100644 index 0000000..dbee657 --- /dev/null +++ b/Mobile Commerce/android/build.gradle.kts @@ -0,0 +1,24 @@ +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("clean") { + delete(rootProject.layout.buildDirectory) +} diff --git a/Mobile Commerce/android/gradle.properties b/Mobile Commerce/android/gradle.properties new file mode 100644 index 0000000..fbee1d8 --- /dev/null +++ b/Mobile Commerce/android/gradle.properties @@ -0,0 +1,2 @@ +org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError +android.useAndroidX=true diff --git a/Mobile Commerce/android/gradle/wrapper/gradle-wrapper.properties b/Mobile Commerce/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..e4ef43f --- /dev/null +++ b/Mobile Commerce/android/gradle/wrapper/gradle-wrapper.properties @@ -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.14-all.zip diff --git a/Mobile Commerce/android/settings.gradle.kts b/Mobile Commerce/android/settings.gradle.kts new file mode 100644 index 0000000..ca7fe06 --- /dev/null +++ b/Mobile Commerce/android/settings.gradle.kts @@ -0,0 +1,26 @@ +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.11.1" apply false + id("org.jetbrains.kotlin.android") version "2.2.20" apply false +} + +include(":app") diff --git a/Mobile Commerce/assets/fonts/Poppins-Bold.ttf b/Mobile Commerce/assets/fonts/Poppins-Bold.ttf new file mode 100644 index 0000000..00559ee Binary files /dev/null and b/Mobile Commerce/assets/fonts/Poppins-Bold.ttf differ diff --git a/Mobile Commerce/assets/fonts/Poppins-Regular.ttf b/Mobile Commerce/assets/fonts/Poppins-Regular.ttf new file mode 100644 index 0000000..9f0c71b Binary files /dev/null and b/Mobile Commerce/assets/fonts/Poppins-Regular.ttf differ diff --git a/Mobile Commerce/assets/gifs/canceled_order.gif b/Mobile Commerce/assets/gifs/canceled_order.gif new file mode 100644 index 0000000..5779a63 Binary files /dev/null and b/Mobile Commerce/assets/gifs/canceled_order.gif differ diff --git a/Mobile Commerce/assets/gifs/completed_order.gif b/Mobile Commerce/assets/gifs/completed_order.gif new file mode 100644 index 0000000..9a56437 Binary files /dev/null and b/Mobile Commerce/assets/gifs/completed_order.gif differ diff --git a/Mobile Commerce/assets/gifs/failed_order.gif b/Mobile Commerce/assets/gifs/failed_order.gif new file mode 100644 index 0000000..5038da0 Binary files /dev/null and b/Mobile Commerce/assets/gifs/failed_order.gif differ diff --git a/Mobile Commerce/assets/gifs/in_progress_order.gif b/Mobile Commerce/assets/gifs/in_progress_order.gif new file mode 100644 index 0000000..9ca966e Binary files /dev/null and b/Mobile Commerce/assets/gifs/in_progress_order.gif differ diff --git a/Mobile Commerce/assets/gifs/pending_order.gif b/Mobile Commerce/assets/gifs/pending_order.gif new file mode 100644 index 0000000..45f00f0 Binary files /dev/null and b/Mobile Commerce/assets/gifs/pending_order.gif differ diff --git a/Mobile Commerce/assets/images/Niogu_Logo_Update_1.0.png b/Mobile Commerce/assets/images/Niogu_Logo_Update_1.0.png new file mode 100644 index 0000000..1b0d512 Binary files /dev/null and b/Mobile Commerce/assets/images/Niogu_Logo_Update_1.0.png differ diff --git a/Mobile Commerce/assets/images/banner_mock_1.png b/Mobile Commerce/assets/images/banner_mock_1.png new file mode 100644 index 0000000..2fbef6b Binary files /dev/null and b/Mobile Commerce/assets/images/banner_mock_1.png differ diff --git a/Mobile Commerce/assets/images/banner_mock_2.png b/Mobile Commerce/assets/images/banner_mock_2.png new file mode 100644 index 0000000..8c6190a Binary files /dev/null and b/Mobile Commerce/assets/images/banner_mock_2.png differ diff --git a/Mobile Commerce/assets/images/bca.png b/Mobile Commerce/assets/images/bca.png new file mode 100644 index 0000000..09a6318 Binary files /dev/null and b/Mobile Commerce/assets/images/bca.png differ diff --git a/Mobile Commerce/assets/images/bni.png b/Mobile Commerce/assets/images/bni.png new file mode 100644 index 0000000..cd7bcea Binary files /dev/null and b/Mobile Commerce/assets/images/bni.png differ diff --git a/Mobile Commerce/assets/images/bri.png b/Mobile Commerce/assets/images/bri.png new file mode 100644 index 0000000..cf524b0 Binary files /dev/null and b/Mobile Commerce/assets/images/bri.png differ diff --git a/Mobile Commerce/assets/images/dana.png b/Mobile Commerce/assets/images/dana.png new file mode 100644 index 0000000..16845d8 Binary files /dev/null and b/Mobile Commerce/assets/images/dana.png differ diff --git a/Mobile Commerce/assets/images/gopay.png b/Mobile Commerce/assets/images/gopay.png new file mode 100644 index 0000000..ff52ea5 Binary files /dev/null and b/Mobile Commerce/assets/images/gopay.png differ diff --git a/Mobile Commerce/assets/images/mandiri.png b/Mobile Commerce/assets/images/mandiri.png new file mode 100644 index 0000000..a333a95 Binary files /dev/null and b/Mobile Commerce/assets/images/mandiri.png differ diff --git a/Mobile Commerce/assets/images/outlet_mock.jpg b/Mobile Commerce/assets/images/outlet_mock.jpg new file mode 100644 index 0000000..2526028 Binary files /dev/null and b/Mobile Commerce/assets/images/outlet_mock.jpg differ diff --git a/Mobile Commerce/assets/images/queen_fruits_logo.png b/Mobile Commerce/assets/images/queen_fruits_logo.png new file mode 100644 index 0000000..d81fb0a Binary files /dev/null and b/Mobile Commerce/assets/images/queen_fruits_logo.png differ diff --git a/Mobile Commerce/assets/images/shopeepay.png b/Mobile Commerce/assets/images/shopeepay.png new file mode 100644 index 0000000..50aedf4 Binary files /dev/null and b/Mobile Commerce/assets/images/shopeepay.png differ diff --git a/Mobile Commerce/assets/images/whatsapp.png b/Mobile Commerce/assets/images/whatsapp.png new file mode 100644 index 0000000..84d2f63 Binary files /dev/null and b/Mobile Commerce/assets/images/whatsapp.png differ diff --git a/Mobile Commerce/ios/.gitignore b/Mobile Commerce/ios/.gitignore new file mode 100644 index 0000000..7a7f987 --- /dev/null +++ b/Mobile Commerce/ios/.gitignore @@ -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 diff --git a/Mobile Commerce/ios/Flutter/AppFrameworkInfo.plist b/Mobile Commerce/ios/Flutter/AppFrameworkInfo.plist new file mode 100644 index 0000000..1dc6cf7 --- /dev/null +++ b/Mobile Commerce/ios/Flutter/AppFrameworkInfo.plist @@ -0,0 +1,26 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + App + CFBundleIdentifier + io.flutter.flutter.app + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + App + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1.0 + MinimumOSVersion + 13.0 + + diff --git a/Mobile Commerce/ios/Flutter/Debug.xcconfig b/Mobile Commerce/ios/Flutter/Debug.xcconfig new file mode 100644 index 0000000..592ceee --- /dev/null +++ b/Mobile Commerce/ios/Flutter/Debug.xcconfig @@ -0,0 +1 @@ +#include "Generated.xcconfig" diff --git a/Mobile Commerce/ios/Flutter/Release.xcconfig b/Mobile Commerce/ios/Flutter/Release.xcconfig new file mode 100644 index 0000000..592ceee --- /dev/null +++ b/Mobile Commerce/ios/Flutter/Release.xcconfig @@ -0,0 +1 @@ +#include "Generated.xcconfig" diff --git a/Mobile Commerce/ios/Runner.xcodeproj/project.pbxproj b/Mobile Commerce/ios/Runner.xcodeproj/project.pbxproj new file mode 100644 index 0000000..935a236 --- /dev/null +++ b/Mobile Commerce/ios/Runner.xcodeproj/project.pbxproj @@ -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 = ""; }; + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; + 331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; + 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 = ""; }; + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; + 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; + 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 = ""; }; + 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; +/* 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 = ""; + }; + 9740EEB11CF90186004384FC /* Flutter */ = { + isa = PBXGroup; + children = ( + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 9740EEB31CF90195004384FC /* Generated.xcconfig */, + ); + name = Flutter; + sourceTree = ""; + }; + 97C146E51CF9000F007C117D = { + isa = PBXGroup; + children = ( + 9740EEB11CF90186004384FC /* Flutter */, + 97C146F01CF9000F007C117D /* Runner */, + 97C146EF1CF9000F007C117D /* Products */, + 331C8082294A63A400263BE5 /* RunnerTests */, + ); + sourceTree = ""; + }; + 97C146EF1CF9000F007C117D /* Products */ = { + isa = PBXGroup; + children = ( + 97C146EE1CF9000F007C117D /* Runner.app */, + 331C8081294A63A400263BE5 /* RunnerTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 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 = ""; + }; +/* 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 = ""; + }; + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C147001CF9000F007C117D /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* 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 = 13.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.nioguEcommerceV1; + 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.nioguEcommerceV1.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.nioguEcommerceV1.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.nioguEcommerceV1.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 = 13.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 = 13.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.nioguEcommerceV1; + 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.nioguEcommerceV1; + 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 */; +} diff --git a/Mobile Commerce/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/Mobile Commerce/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..919434a --- /dev/null +++ b/Mobile Commerce/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/Mobile Commerce/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/Mobile Commerce/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/Mobile Commerce/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/Mobile Commerce/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/Mobile Commerce/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..f9b0d7c --- /dev/null +++ b/Mobile Commerce/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/Mobile Commerce/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/Mobile Commerce/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 0000000..e3773d4 --- /dev/null +++ b/Mobile Commerce/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,101 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Mobile Commerce/ios/Runner.xcworkspace/contents.xcworkspacedata b/Mobile Commerce/ios/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..1d526a1 --- /dev/null +++ b/Mobile Commerce/ios/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/Mobile Commerce/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/Mobile Commerce/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/Mobile Commerce/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/Mobile Commerce/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/Mobile Commerce/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..f9b0d7c --- /dev/null +++ b/Mobile Commerce/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/Mobile Commerce/ios/Runner/AppDelegate.swift b/Mobile Commerce/ios/Runner/AppDelegate.swift new file mode 100644 index 0000000..6266644 --- /dev/null +++ b/Mobile Commerce/ios/Runner/AppDelegate.swift @@ -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) + } +} diff --git a/Mobile Commerce/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/Mobile Commerce/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..d36b1fa --- /dev/null +++ b/Mobile Commerce/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -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" + } +} diff --git a/Mobile Commerce/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/Mobile Commerce/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png new file mode 100644 index 0000000..dc9ada4 Binary files /dev/null and b/Mobile Commerce/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png differ diff --git a/Mobile Commerce/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/Mobile Commerce/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png new file mode 100644 index 0000000..7353c41 Binary files /dev/null and b/Mobile Commerce/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png differ diff --git a/Mobile Commerce/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/Mobile Commerce/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png new file mode 100644 index 0000000..797d452 Binary files /dev/null and b/Mobile Commerce/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png differ diff --git a/Mobile Commerce/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/Mobile Commerce/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png new file mode 100644 index 0000000..6ed2d93 Binary files /dev/null and b/Mobile Commerce/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png differ diff --git a/Mobile Commerce/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/Mobile Commerce/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png new file mode 100644 index 0000000..4cd7b00 Binary files /dev/null and b/Mobile Commerce/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png differ diff --git a/Mobile Commerce/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/Mobile Commerce/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png new file mode 100644 index 0000000..fe73094 Binary files /dev/null and b/Mobile Commerce/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png differ diff --git a/Mobile Commerce/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/Mobile Commerce/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png new file mode 100644 index 0000000..321773c Binary files /dev/null and b/Mobile Commerce/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png differ diff --git a/Mobile Commerce/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/Mobile Commerce/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png new file mode 100644 index 0000000..797d452 Binary files /dev/null and b/Mobile Commerce/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png differ diff --git a/Mobile Commerce/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/Mobile Commerce/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png new file mode 100644 index 0000000..502f463 Binary files /dev/null and b/Mobile Commerce/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png differ diff --git a/Mobile Commerce/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/Mobile Commerce/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png new file mode 100644 index 0000000..0ec3034 Binary files /dev/null and b/Mobile Commerce/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png differ diff --git a/Mobile Commerce/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/Mobile Commerce/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png new file mode 100644 index 0000000..0ec3034 Binary files /dev/null and b/Mobile Commerce/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png differ diff --git a/Mobile Commerce/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/Mobile Commerce/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png new file mode 100644 index 0000000..e9f5fea Binary files /dev/null and b/Mobile Commerce/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png differ diff --git a/Mobile Commerce/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/Mobile Commerce/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png new file mode 100644 index 0000000..84ac32a Binary files /dev/null and b/Mobile Commerce/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png differ diff --git a/Mobile Commerce/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/Mobile Commerce/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png new file mode 100644 index 0000000..8953cba Binary files /dev/null and b/Mobile Commerce/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png differ diff --git a/Mobile Commerce/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/Mobile Commerce/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png new file mode 100644 index 0000000..0467bf1 Binary files /dev/null and b/Mobile Commerce/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png differ diff --git a/Mobile Commerce/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/Mobile Commerce/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json new file mode 100644 index 0000000..0bedcf2 --- /dev/null +++ b/Mobile Commerce/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json @@ -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" + } +} diff --git a/Mobile Commerce/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/Mobile Commerce/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/Mobile Commerce/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png differ diff --git a/Mobile Commerce/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/Mobile Commerce/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/Mobile Commerce/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png differ diff --git a/Mobile Commerce/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/Mobile Commerce/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/Mobile Commerce/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png differ diff --git a/Mobile Commerce/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/Mobile Commerce/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md new file mode 100644 index 0000000..89c2725 --- /dev/null +++ b/Mobile Commerce/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md @@ -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. \ No newline at end of file diff --git a/Mobile Commerce/ios/Runner/Base.lproj/LaunchScreen.storyboard b/Mobile Commerce/ios/Runner/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 0000000..f2e259c --- /dev/null +++ b/Mobile Commerce/ios/Runner/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Mobile Commerce/ios/Runner/Base.lproj/Main.storyboard b/Mobile Commerce/ios/Runner/Base.lproj/Main.storyboard new file mode 100644 index 0000000..f3c2851 --- /dev/null +++ b/Mobile Commerce/ios/Runner/Base.lproj/Main.storyboard @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Mobile Commerce/ios/Runner/Info.plist b/Mobile Commerce/ios/Runner/Info.plist new file mode 100644 index 0000000..f458537 --- /dev/null +++ b/Mobile Commerce/ios/Runner/Info.plist @@ -0,0 +1,49 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + Niogu Ecommerce V1 + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + niogu_ecommerce_v1 + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleSignature + ???? + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSRequiresIPhoneOS + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + CADisableMinimumFrameDurationOnPhone + + UIApplicationSupportsIndirectInputEvents + + + diff --git a/Mobile Commerce/ios/Runner/Runner-Bridging-Header.h b/Mobile Commerce/ios/Runner/Runner-Bridging-Header.h new file mode 100644 index 0000000..308a2a5 --- /dev/null +++ b/Mobile Commerce/ios/Runner/Runner-Bridging-Header.h @@ -0,0 +1 @@ +#import "GeneratedPluginRegistrant.h" diff --git a/Mobile Commerce/ios/RunnerTests/RunnerTests.swift b/Mobile Commerce/ios/RunnerTests/RunnerTests.swift new file mode 100644 index 0000000..86a7c3b --- /dev/null +++ b/Mobile Commerce/ios/RunnerTests/RunnerTests.swift @@ -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. + } + +} diff --git a/Mobile Commerce/lib/core/.gitignore b/Mobile Commerce/lib/core/.gitignore new file mode 100644 index 0000000..3573899 --- /dev/null +++ b/Mobile Commerce/lib/core/.gitignore @@ -0,0 +1 @@ +/templates \ No newline at end of file diff --git a/Mobile Commerce/lib/core/components/bottom_bar_app.dart b/Mobile Commerce/lib/core/components/bottom_bar_app.dart new file mode 100644 index 0000000..d0230cb --- /dev/null +++ b/Mobile Commerce/lib/core/components/bottom_bar_app.dart @@ -0,0 +1,111 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:niogu_ecommerce_v1/core/constant/app_color.dart'; +import 'package:niogu_ecommerce_v1/core/constant/app_font_size.dart'; +import 'package:niogu_ecommerce_v1/features/cart/presentation/providers/cart_provider.dart'; +import 'package:sizer/sizer.dart'; + +class BottomBarApp extends ConsumerWidget { + final int currentIndex; + final void Function(int) onTap; + + const BottomBarApp({ + super.key, + required this.currentIndex, + required this.onTap, + }); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final bool isTablet = 100.w >= 600; + + final double height = isTablet ? 14.h : 12.h; + + final cartItems = ref.watch(cartItemProvider); + + return Container( + height: height, + decoration: BoxDecoration( + color: Colors.white, + border: BoxBorder.fromLTRB( + top: BorderSide(color: Colors.grey.shade300), + ), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + _buildNavItem(Icons.home_filled, "Beranda", 0), + + _buildNavItem(Icons.favorite_outline, "Favorit", 1), + + _buildNavItem( + Icons.shopping_cart_checkout_outlined, + "Keranjang", + 2, + showBadge: true, + badgeCount: cartItems.length, + ), + + _buildNavItem(Icons.shopping_bag_outlined, "Pesanan", 3), + + _buildNavItem(Icons.person_outline_outlined, "Akun", 4), + ], + ), + ); + } + + Widget _buildNavItem( + IconData icon, + String label, + int index, { + bool showBadge = false, + int badgeCount = 0, + }) { + final bool isActive = currentIndex == index; + + return InkWell( + onTap: () => onTap(index), + borderRadius: BorderRadius.circular(2.5.w), + child: AnimatedContainer( + duration: const Duration(milliseconds: 200), + padding: EdgeInsets.symmetric(horizontal: 3.w, vertical: 1.h), + decoration: BoxDecoration( + color: isActive + ? AppColor.primaryColor.withOpacity(0.08) + : Colors.transparent, + borderRadius: BorderRadius.circular(2.5.w), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Badge( + isLabelVisible: showBadge && badgeCount > 0, + label: Text( + badgeCount.toString(), + style: TextStyle( + fontSize: (AppFontSize.small - 1.25).sp, + color: Colors.white, + ), + ), + backgroundColor: AppColor.primaryColor, + child: Icon( + icon, + color: isActive ? AppColor.primaryColor : Colors.grey, + size: 6.w, + ), + ), + SizedBox(height: 0.5.h), + Text( + label, + style: TextStyle( + fontSize: AppFontSize.small.sp, + fontWeight: isActive ? FontWeight.bold : FontWeight.normal, + color: isActive ? AppColor.primaryColor : Colors.grey, + ), + ), + ], + ), + ), + ); + } +} diff --git a/Mobile Commerce/lib/core/constant/.gitignore b/Mobile Commerce/lib/core/constant/.gitignore new file mode 100644 index 0000000..18db11d --- /dev/null +++ b/Mobile Commerce/lib/core/constant/.gitignore @@ -0,0 +1 @@ +app_temp.dart \ No newline at end of file diff --git a/Mobile Commerce/lib/core/constant/app_asset.dart b/Mobile Commerce/lib/core/constant/app_asset.dart new file mode 100644 index 0000000..09bb1ef --- /dev/null +++ b/Mobile Commerce/lib/core/constant/app_asset.dart @@ -0,0 +1,19 @@ +class AppAsset { + static const String LOGO = 'assets/images/queen_fruits_logo.png'; + static const String BANNER_MOCK_1 = 'assets/images/banner_mock_1.png'; + static const String BANNER_MOCK_2 = 'assets/images/banner_mock_2.png'; + static const String WHATSAPP = 'assets/images/whatsapp.png'; + static const String OUTLET_MOCK = 'assets/images/outlet_mock.jpg'; + static const String SHOPEEPAY_LOGO = 'assets/images/shopeepay.png'; + static const String GOPAY_LOGO = 'assets/images/gopay.png'; + static const String DANA_LOGO = 'assets/images/dana.png'; + static const String BRI_LOGO = 'assets/images/bri.png'; + static const String MANDIRI_LOGO = 'assets/images/mandiri.png'; + static const String BNI_LOGO = 'assets/images/bni.png'; + static const String BCA_LOGO = 'assets/images/bca.png'; + static const String PENDING_ORDER = 'assets/gifs/pending_order.gif'; + static const String IN_PROGRESS_ORDER = 'assets/gifs/in_progress_order.gif'; + static const String COMPLETED_ORDER = 'assets/gifs/completed_order.gif'; + static const String CANCELED_ORDER = 'assets/gifs/canceled_order.gif'; + static const String FAILED_ORDER = 'assets/gifs/failed_order.gif'; +} diff --git a/Mobile Commerce/lib/core/constant/app_color.dart b/Mobile Commerce/lib/core/constant/app_color.dart new file mode 100644 index 0000000..057228c --- /dev/null +++ b/Mobile Commerce/lib/core/constant/app_color.dart @@ -0,0 +1,5 @@ +import 'dart:ui'; + +class AppColor { + static const Color primaryColor = Color.fromARGB(255, 245, 84, 35); +} \ No newline at end of file diff --git a/Mobile Commerce/lib/core/constant/app_config.dart b/Mobile Commerce/lib/core/constant/app_config.dart new file mode 100644 index 0000000..2cefc80 --- /dev/null +++ b/Mobile Commerce/lib/core/constant/app_config.dart @@ -0,0 +1,19 @@ +import 'package:flutter/foundation.dart' show kIsWeb; + +class AppConfig { + static const String _localIp = kIsWeb ? "localhost" : "192.168.137.1"; + + static const String _port = "8000"; + + static const String _baseUrlDev = "http://$_localIp:$_port/api"; + + static const String _baseUrlProd = "https://api.niogu.com/api"; + + static const bool _isDevelopment = true; + + static String get BASE_URL => _isDevelopment ? _baseUrlDev : _baseUrlProd; + + static String get REVERB_APP_ID => '819785'; + + static String get REVERB_APP_KEY => 's4bqmwrabw0sc3gcplmm'; +} diff --git a/Mobile Commerce/lib/core/constant/app_font_size.dart b/Mobile Commerce/lib/core/constant/app_font_size.dart new file mode 100644 index 0000000..e6e1a69 --- /dev/null +++ b/Mobile Commerce/lib/core/constant/app_font_size.dart @@ -0,0 +1,6 @@ +class AppFontSize { + static const double extraLarge = 40.0; + static const double large = 18.0; + static const double medium = 16.0; + static const double small = 14.0; +} diff --git a/Mobile Commerce/lib/core/constant/app_key.dart b/Mobile Commerce/lib/core/constant/app_key.dart new file mode 100644 index 0000000..3d3a667 --- /dev/null +++ b/Mobile Commerce/lib/core/constant/app_key.dart @@ -0,0 +1,16 @@ +class AppKey { + static const String AUTH_TOKEN = 'auth_token'; + static const String TENANT_CODE = 'tenant_code'; + static const String OUTLET_ID = 'outlet_id'; + static const String OUTLET_NAME = 'outlet_name'; + static const String OUTLET_PHONE = 'outlet_phone'; + static const String OUTLET_LOCATION = 'outlet_location'; + static const String OUTLET_LATITUDE = 'outlet_latitude'; + static const String OUTLET_LONGITUDE = 'outlet_longitude'; + static const String CUSTOMER_LOGIN = 'customer_login'; + static const String CUSTOMER_ID = 'customer_id'; + static const String CUSTOMER_NAME = 'customer_name'; + static const String CUSTOMER_EMAIL = 'customer_email'; + static const String CUSTOMER_PHONE = 'customer_phone'; + static const String CART_ITEM_BY_OUTLET = 'cart_item_by_outlet'; +} \ No newline at end of file diff --git a/Mobile Commerce/lib/core/constant/app_url.dart b/Mobile Commerce/lib/core/constant/app_url.dart new file mode 100644 index 0000000..8c92efa --- /dev/null +++ b/Mobile Commerce/lib/core/constant/app_url.dart @@ -0,0 +1,12 @@ +class AppUrl { + static const String LOGIN = '/customer/auth/login'; + static const String REGISTER = '/customer/auth/register'; + static const String TENANT = '/tenant'; + static const String HOME = '/home'; + static const String OUTLETS = '/outlets'; + static const String PRODUCTS = '/products'; + static const String CHECKOUT = '/checkout'; + static const String ORDERS = '/orders'; + static const String REVIEW = '/review'; + static const String CUSTOMER = '/customer'; +} diff --git a/Mobile Commerce/lib/core/enums/action_type.dart b/Mobile Commerce/lib/core/enums/action_type.dart new file mode 100644 index 0000000..b0165cf --- /dev/null +++ b/Mobile Commerce/lib/core/enums/action_type.dart @@ -0,0 +1 @@ +enum ActionType { checkout, cancel, requestCancel } diff --git a/Mobile Commerce/lib/core/enums/approval_process.dart b/Mobile Commerce/lib/core/enums/approval_process.dart new file mode 100644 index 0000000..5cc942e --- /dev/null +++ b/Mobile Commerce/lib/core/enums/approval_process.dart @@ -0,0 +1,8 @@ +enum ApprovalProcess { + waiting('waiting'), + done('done'); + + final String process; + + const ApprovalProcess(this.process); +} \ No newline at end of file diff --git a/Mobile Commerce/lib/core/enums/campaign_type.dart b/Mobile Commerce/lib/core/enums/campaign_type.dart new file mode 100644 index 0000000..d77b03b --- /dev/null +++ b/Mobile Commerce/lib/core/enums/campaign_type.dart @@ -0,0 +1,8 @@ +enum CampaignType { + product('product'), + category('category'); + + final String type; + + const CampaignType(this.type); +} \ No newline at end of file diff --git a/Mobile Commerce/lib/core/enums/delivery_fee_type.dart b/Mobile Commerce/lib/core/enums/delivery_fee_type.dart new file mode 100644 index 0000000..cda873e --- /dev/null +++ b/Mobile Commerce/lib/core/enums/delivery_fee_type.dart @@ -0,0 +1,8 @@ +enum DeliveryFeeType { + fixed('fixed'), + per_km('per_km'); + + final String type; + + const DeliveryFeeType(this.type); +} \ No newline at end of file diff --git a/Mobile Commerce/lib/core/enums/delivery_type.dart b/Mobile Commerce/lib/core/enums/delivery_type.dart new file mode 100644 index 0000000..da3ac24 --- /dev/null +++ b/Mobile Commerce/lib/core/enums/delivery_type.dart @@ -0,0 +1,8 @@ +enum DeliveryType { + pick_up('pick_up'), + delivery('delivery'); + + final String type; + + const DeliveryType(this.type); +} \ No newline at end of file diff --git a/Mobile Commerce/lib/core/enums/order_status.dart b/Mobile Commerce/lib/core/enums/order_status.dart new file mode 100644 index 0000000..8649db5 --- /dev/null +++ b/Mobile Commerce/lib/core/enums/order_status.dart @@ -0,0 +1,12 @@ +enum OrderStatus { + pending_payment("pending_payment"), + pending_confirmation("pending_confirmation"), + confirmed("confirmed"), + in_progress("in_progress"), + completed("completed"), + canceled("canceled"); + + final String status; + + const OrderStatus(this.status); +} \ No newline at end of file diff --git a/Mobile Commerce/lib/core/enums/review_display_policy.dart b/Mobile Commerce/lib/core/enums/review_display_policy.dart new file mode 100644 index 0000000..79d60f9 --- /dev/null +++ b/Mobile Commerce/lib/core/enums/review_display_policy.dart @@ -0,0 +1,8 @@ +enum ReviewDisplayPolicy { + all_rating('all_rating'), + four_and_five_star_only('four_and_five_star_only'); + + final String policy; + + const ReviewDisplayPolicy(this.policy); +} diff --git a/Mobile Commerce/lib/core/enums/review_display_type.dart b/Mobile Commerce/lib/core/enums/review_display_type.dart new file mode 100644 index 0000000..ec33a3b --- /dev/null +++ b/Mobile Commerce/lib/core/enums/review_display_type.dart @@ -0,0 +1,8 @@ +enum ReviewDisplayType { + rating_only('rating_only'), + rating_and_review('rating_and_review'); + + final String type; + + const ReviewDisplayType(this.type); +} \ No newline at end of file diff --git a/Mobile Commerce/lib/core/enums/stock_type.dart b/Mobile Commerce/lib/core/enums/stock_type.dart new file mode 100644 index 0000000..3670338 --- /dev/null +++ b/Mobile Commerce/lib/core/enums/stock_type.dart @@ -0,0 +1,7 @@ +enum StockType { + fixed("fixed"), + unlimited("unlimited"); + + final String type; + const StockType(this.type); +} diff --git a/Mobile Commerce/lib/core/errors/exceptions.dart b/Mobile Commerce/lib/core/errors/exceptions.dart new file mode 100644 index 0000000..a5ac659 --- /dev/null +++ b/Mobile Commerce/lib/core/errors/exceptions.dart @@ -0,0 +1,7 @@ +class ServerException implements Exception { + final String message; + ServerException(this.message); + + @override + String toString() => message; +} diff --git a/Mobile Commerce/lib/core/models/api_response_model.dart b/Mobile Commerce/lib/core/models/api_response_model.dart new file mode 100644 index 0000000..f679fb1 --- /dev/null +++ b/Mobile Commerce/lib/core/models/api_response_model.dart @@ -0,0 +1,32 @@ +class ApiResponseModel { + final String status; + final String message; + final T? data; + final Map? error; + + ApiResponseModel({ + required this.status, + required this.message, + this.data, + this.error, + }); + + factory ApiResponseModel.fromJson( + Map json, + T Function(Object? json) fromJsonT, + ) { + return ApiResponseModel( + status: json['status'], + message: json['message'], + data: json['data'] != null ? fromJsonT(json['data']) : null, + error: json['error'], + ); + } + + Map toJson() => { + 'status': status, + 'message': message, + 'data': data, + 'error': error + }; +} diff --git a/Mobile Commerce/lib/core/models/tenant_model.dart b/Mobile Commerce/lib/core/models/tenant_model.dart new file mode 100644 index 0000000..84bfd25 --- /dev/null +++ b/Mobile Commerce/lib/core/models/tenant_model.dart @@ -0,0 +1,125 @@ +import 'package:json_annotation/json_annotation.dart'; +import 'package:niogu_ecommerce_v1/core/models/tenant_payment_method_model.dart'; +import 'package:niogu_ecommerce_v1/core/utils/converter.dart'; + +part 'tenant_model.g.dart'; + +@JsonSerializable() +class TenantModel { + final String uuid; + + @JsonKey(name: 'business_code') + final String businessCode; + + @JsonKey(name: 'business_name') + final String businessName; + + @JsonKey(name: 'business_email') + final String? businessEmail; + + @JsonKey(name: 'business_phone') + final String? businessPhone; + + @JsonKey(name: 'business_type') + final String? businessType; + + @JsonKey(name: 'server_logo_url') + final String? serverLogoUrl; + + final String? subdomain; + + @JsonKey(name: 'online_store_name') + final String? onlineStoreName; + + @JsonKey(name: 'online_open_time') + final String? onlineOpenTime; + + @JsonKey(name: 'online_close_time') + final String? onlineCloseTime; + + @JsonKey(name: 'is_close_service') + final bool? isCloseService; + + @JsonKey(name: 'preparation_time_minutes') + final int? preparationTimeMinutes; + + @JsonKey(name: 'order_interval_minutes') + final int? orderIntervalMinutes; + + @JsonKey(name: 'allow_cod') + final bool? allowCod; + + @JsonKey(name: 'max_delivery_radius_km', fromJson: toDouble) + final double? maxDeliveryRadiusKm; + + @JsonKey(name: 'delivery_fee_type') + final String? deliveryFeeType; + + @JsonKey(name: 'delivery_flat_fee', fromJson: toDouble) + final double? deliveryFlatFee; + + @JsonKey(name: 'delivery_fee_per_km', fromJson: toDouble) + final double? deliveryFeePerKm; + + @JsonKey(name: 'delivery_base_fee', fromJson: toDouble) + final double? deliveryBaseFee; + + @JsonKey(name: 'review_display_type') + final String? reviewDisplayType; + + @JsonKey(name: 'review_display_policy') + final String? reviewDisplayPolicy; + + @JsonKey(name: 'subscription_level') + final String? subscriptionLevel; + + @JsonKey(name: 'limit_outlet') + final int? limitOutlet; + + @JsonKey(name: 'subscription_expires_at') + final String? subscriptionExpiresAt; + + @JsonKey(name: 'created_at') + final String createdAt; + + @JsonKey(name: 'updated_at') + final String updatedAt; + + @JsonKey(name: 'payment_methods') + final List paymentMethods; + + const TenantModel({ + required this.uuid, + required this.businessCode, + required this.businessName, + required this.businessEmail, + required this.businessPhone, + required this.businessType, + required this.serverLogoUrl, + required this.subdomain, + required this.onlineStoreName, + required this.onlineOpenTime, + required this.onlineCloseTime, + required this.isCloseService, + required this.preparationTimeMinutes, + required this.orderIntervalMinutes, + required this.allowCod, + required this.maxDeliveryRadiusKm, + required this.deliveryFeeType, + required this.deliveryFlatFee, + required this.deliveryFeePerKm, + required this.deliveryBaseFee, + required this.reviewDisplayType, + required this.reviewDisplayPolicy, + required this.subscriptionLevel, + required this.limitOutlet, + required this.subscriptionExpiresAt, + required this.createdAt, + required this.updatedAt, + required this.paymentMethods + }); + + factory TenantModel.fromJson(Map json) => + _$TenantModelFromJson(json); + Map toJson() => _$TenantModelToJson(this); +} diff --git a/Mobile Commerce/lib/core/models/tenant_model.g.dart b/Mobile Commerce/lib/core/models/tenant_model.g.dart new file mode 100644 index 0000000..1998743 --- /dev/null +++ b/Mobile Commerce/lib/core/models/tenant_model.g.dart @@ -0,0 +1,72 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'tenant_model.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +TenantModel _$TenantModelFromJson(Map json) => TenantModel( + uuid: json['uuid'] as String, + businessCode: json['business_code'] as String, + businessName: json['business_name'] as String, + businessEmail: json['business_email'] as String?, + businessPhone: json['business_phone'] as String?, + businessType: json['business_type'] as String?, + serverLogoUrl: json['server_logo_url'] as String?, + subdomain: json['subdomain'] as String?, + onlineStoreName: json['online_store_name'] as String?, + onlineOpenTime: json['online_open_time'] as String?, + onlineCloseTime: json['online_close_time'] as String?, + isCloseService: json['is_close_service'] as bool?, + preparationTimeMinutes: (json['preparation_time_minutes'] as num?)?.toInt(), + orderIntervalMinutes: (json['order_interval_minutes'] as num?)?.toInt(), + allowCod: json['allow_cod'] as bool?, + maxDeliveryRadiusKm: toDouble(json['max_delivery_radius_km']), + deliveryFeeType: json['delivery_fee_type'] as String?, + deliveryFlatFee: toDouble(json['delivery_flat_fee']), + deliveryFeePerKm: toDouble(json['delivery_fee_per_km']), + deliveryBaseFee: toDouble(json['delivery_base_fee']), + reviewDisplayType: json['review_display_type'] as String?, + reviewDisplayPolicy: json['review_display_policy'] as String?, + subscriptionLevel: json['subscription_level'] as String?, + limitOutlet: (json['limit_outlet'] as num?)?.toInt(), + subscriptionExpiresAt: json['subscription_expires_at'] as String?, + createdAt: json['created_at'] as String, + updatedAt: json['updated_at'] as String, + paymentMethods: (json['payment_methods'] as List) + .map((e) => TenantPaymentMethodModel.fromJson(e as Map)) + .toList(), +); + +Map _$TenantModelToJson(TenantModel instance) => + { + 'uuid': instance.uuid, + 'business_code': instance.businessCode, + 'business_name': instance.businessName, + 'business_email': instance.businessEmail, + 'business_phone': instance.businessPhone, + 'business_type': instance.businessType, + 'server_logo_url': instance.serverLogoUrl, + 'subdomain': instance.subdomain, + 'online_store_name': instance.onlineStoreName, + 'online_open_time': instance.onlineOpenTime, + 'online_close_time': instance.onlineCloseTime, + 'is_close_service': instance.isCloseService, + 'preparation_time_minutes': instance.preparationTimeMinutes, + 'order_interval_minutes': instance.orderIntervalMinutes, + 'allow_cod': instance.allowCod, + 'max_delivery_radius_km': instance.maxDeliveryRadiusKm, + 'delivery_fee_type': instance.deliveryFeeType, + 'delivery_flat_fee': instance.deliveryFlatFee, + 'delivery_fee_per_km': instance.deliveryFeePerKm, + 'delivery_base_fee': instance.deliveryBaseFee, + 'review_display_type': instance.reviewDisplayType, + 'review_display_policy': instance.reviewDisplayPolicy, + 'subscription_level': instance.subscriptionLevel, + 'limit_outlet': instance.limitOutlet, + 'subscription_expires_at': instance.subscriptionExpiresAt, + 'created_at': instance.createdAt, + 'updated_at': instance.updatedAt, + 'payment_methods': instance.paymentMethods, + }; diff --git a/Mobile Commerce/lib/core/models/tenant_payment_method_model.dart b/Mobile Commerce/lib/core/models/tenant_payment_method_model.dart new file mode 100644 index 0000000..073c641 --- /dev/null +++ b/Mobile Commerce/lib/core/models/tenant_payment_method_model.dart @@ -0,0 +1,36 @@ +import 'package:json_annotation/json_annotation.dart'; + +part 'tenant_payment_method_model.g.dart'; + +@JsonSerializable() +class TenantPaymentMethodModel { + final String uuid; + + @JsonKey(name: 'source_name') + final String sourceName; + + @JsonKey(name: 'account_number') + final String accountNumber; + + @JsonKey(name: 'receipent_name') + final String receipentName; + + @JsonKey(name: 'created_at') + final String createdAt; + + @JsonKey(name: 'updated_at') + final String updatedAt; + + const TenantPaymentMethodModel({ + required this.uuid, + required this.sourceName, + required this.accountNumber, + required this.receipentName, + required this.createdAt, + required this.updatedAt, + }); + + factory TenantPaymentMethodModel.fromJson(Map json) => + _$TenantPaymentMethodModelFromJson(json); + Map toJson() => _$TenantPaymentMethodModelToJson(this); +} diff --git a/Mobile Commerce/lib/core/models/tenant_payment_method_model.g.dart b/Mobile Commerce/lib/core/models/tenant_payment_method_model.g.dart new file mode 100644 index 0000000..b45eaa5 --- /dev/null +++ b/Mobile Commerce/lib/core/models/tenant_payment_method_model.g.dart @@ -0,0 +1,29 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'tenant_payment_method_model.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +TenantPaymentMethodModel _$TenantPaymentMethodModelFromJson( + Map json, +) => TenantPaymentMethodModel( + uuid: json['uuid'] as String, + sourceName: json['source_name'] as String, + accountNumber: json['account_number'] as String, + receipentName: json['receipent_name'] as String, + createdAt: json['created_at'] as String, + updatedAt: json['updated_at'] as String, +); + +Map _$TenantPaymentMethodModelToJson( + TenantPaymentMethodModel instance, +) => { + 'uuid': instance.uuid, + 'source_name': instance.sourceName, + 'account_number': instance.accountNumber, + 'receipent_name': instance.receipentName, + 'created_at': instance.createdAt, + 'updated_at': instance.updatedAt, +}; diff --git a/Mobile Commerce/lib/core/network/dio_client.dart b/Mobile Commerce/lib/core/network/dio_client.dart new file mode 100644 index 0000000..7eeaece --- /dev/null +++ b/Mobile Commerce/lib/core/network/dio_client.dart @@ -0,0 +1,52 @@ +import 'package:dio/dio.dart'; +import 'package:flutter/foundation.dart'; +import 'package:niogu_ecommerce_v1/core/constant/app_config.dart'; +import 'package:niogu_ecommerce_v1/core/constant/app_temp.dart'; +import 'package:niogu_ecommerce_v1/core/system/system_secure.dart'; +import 'package:niogu_ecommerce_v1/core/utils/log_message.dart'; + +class DioClient { + late final Dio _dio; + + DioClient() { + _dio = Dio( + BaseOptions( + baseUrl: AppConfig.BASE_URL, + connectTimeout: const Duration(seconds: 15), + receiveTimeout: const Duration(seconds: 15), + responseType: ResponseType.json, + headers: { + 'Accept': 'application/json', + 'Content-Type': 'application/json', + 'X-Business-Code': AppTemp.BUSINESS_CODE, + }, + ), + ); + + _dio.interceptors.add( + LogInterceptor(requestBody: kDebugMode, responseBody: kDebugMode), + ); + + _dio.interceptors.add( + InterceptorsWrapper( + onRequest: (options, handler) async { + final token = await SystemSecure.getAccessToken(); + + if (token != null) { + options.headers['Authorization'] = 'Bearer $token'; + } + + return handler.next(options); + }, + onError: (DioException e, handler) { + if (e.response?.statusCode == 401) { + LogMessage.log.e("Sesi berakhir, silakan login ulang."); + } + return handler.next(e); + }, + ), + ); + } + + Dio get instance => _dio; +} diff --git a/Mobile Commerce/lib/core/providers/app_provider.dart b/Mobile Commerce/lib/core/providers/app_provider.dart new file mode 100644 index 0000000..69693f4 --- /dev/null +++ b/Mobile Commerce/lib/core/providers/app_provider.dart @@ -0,0 +1,29 @@ +import 'package:dio/dio.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:latlong2/latlong.dart'; +import 'package:niogu_ecommerce_v1/core/network/dio_client.dart'; +import 'package:niogu_ecommerce_v1/core/service/echo_service.dart'; + +final dioProvider = Provider((ref) => DioClient().instance); + +final echoServiceProvider = Provider((ref) => EchoService()); + +final currentOutletIdProvider = StateProvider((ref) => null); + +final currentOutletNameProvider = StateProvider((ref) => null); + +final currentOutletPhoneProvider = StateProvider((ref) => null); + +final currentOutletLocationProvider = StateProvider((ref) => null); + +final currentOutletCoordinateProvider = StateProvider((ref) => null); + +final currentCustomerIdProvider = StateProvider((ref) => null); + +final currentCustomerNameProvider = StateProvider((ref) => null); + +final currentCustomerEmailProvider = StateProvider((ref) => null); + +final currentCustomerPhoneProvider = StateProvider((ref) => null); + +final currentStatusLoginProvider = StateProvider((ref) => false); diff --git a/Mobile Commerce/lib/core/router/app_route.dart b/Mobile Commerce/lib/core/router/app_route.dart new file mode 100644 index 0000000..176e592 --- /dev/null +++ b/Mobile Commerce/lib/core/router/app_route.dart @@ -0,0 +1,421 @@ +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'package:latlong2/latlong.dart'; +import 'package:niogu_ecommerce_v1/core/enums/action_type.dart'; +import 'package:niogu_ecommerce_v1/core/widgets/custom_action_screen.dart'; +import 'package:niogu_ecommerce_v1/features/account/presentation/screens/account_screen.dart'; +import 'package:niogu_ecommerce_v1/features/account/presentation/screens/change_password_screen.dart'; +import 'package:niogu_ecommerce_v1/features/account/presentation/screens/map_address_screen.dart'; +import 'package:niogu_ecommerce_v1/features/account/presentation/screens/address_screen.dart'; +import 'package:niogu_ecommerce_v1/features/account/presentation/screens/save_address_screen.dart'; +import 'package:niogu_ecommerce_v1/features/auth/presentation/screens/login_screen.dart'; +import 'package:niogu_ecommerce_v1/features/auth/presentation/screens/register_screen.dart'; +import 'package:niogu_ecommerce_v1/features/cart/presentation/screens/cart_screen.dart'; +import 'package:niogu_ecommerce_v1/features/checkout/presentation/screens/checkout_screen.dart'; +import 'package:niogu_ecommerce_v1/features/checkout/presentation/screens/shipping_address_screen.dart'; +import 'package:niogu_ecommerce_v1/features/favorite/presentation/screens/favorite_screen.dart'; +import 'package:niogu_ecommerce_v1/features/home/domain/entities/home.dart'; +import 'package:niogu_ecommerce_v1/features/home/presentation/screens/home_screen.dart'; +import 'package:niogu_ecommerce_v1/features/home/presentation/screens/outlet_map_screen.dart'; +import 'package:niogu_ecommerce_v1/features/home/presentation/screens/product_category_screen.dart'; +import 'package:niogu_ecommerce_v1/features/home/presentation/screens/search_screen.dart'; +import 'package:niogu_ecommerce_v1/features/order/domain/entities/order.dart'; +import 'package:niogu_ecommerce_v1/features/order/presentation/screens/order_detail_screen.dart'; +import 'package:niogu_ecommerce_v1/features/order/presentation/screens/order_screen.dart'; +import 'package:niogu_ecommerce_v1/features/order/presentation/screens/rating_screen.dart'; +import 'package:niogu_ecommerce_v1/features/product/domain/entities/product.dart'; +import 'package:niogu_ecommerce_v1/features/product/presentation/screens/product_detail_screen.dart'; +import 'package:niogu_ecommerce_v1/features/splash/presentation/screens/splash_screen.dart'; +import 'package:niogu_ecommerce_v1/main_wrapper.dart'; + +final _rootNavigatorKey = GlobalKey(); + +class AppRoute { + static final String splashScreen = 'splash'; + static final String loginScreen = 'login'; + static final String registerScreen = 'register'; + static final String homeScreen = 'home'; + static final String favoriteScreen = 'favorite'; + static final String cartScreen = 'cart'; + static final String orderScreen = 'order'; + static final String accountScreen = 'account'; + static final String searchScreen = 'search'; + static final String outletMapScreen = 'outletMap'; + static final String productCategoryScreen = 'productCategory'; + static final String productDetailScreen = 'productDetail'; + static final String checkoutScreen = 'checkout'; + static final String shippingAddressScreen = 'shippingAddress'; + static final String ratingScreen = 'rating'; + static final String orderDetailScreen = 'orderDetail'; + static final String customActionScreen = 'customAction'; + static final String changePasswordScreen = 'changePassword'; + static final String addressScreen = 'addresses'; + static final String mapAddressScreen = 'mapAddressScreen'; + static final String saveAddressScreen = 'saveAddressScreen'; + + static final router = GoRouter( + navigatorKey: _rootNavigatorKey, + initialLocation: '/splash', + routes: [ + GoRoute( + name: splashScreen, + path: '/splash', + builder: (context, state) => const SplashScreen(), + ), + GoRoute( + name: loginScreen, + path: '/login', + builder: (context, state) => const LoginScreen(), + ), + GoRoute( + name: registerScreen, + path: '/register', + builder: (context, state) => const RegisterScreen(), + ), + StatefulShellRoute.indexedStack( + builder: (context, state, navigationShell) { + return MainWrapper(navigationShell: navigationShell); + }, + branches: [ + StatefulShellBranch( + routes: [ + GoRoute( + name: homeScreen, + path: '/home', + builder: (context, state) => const HomeScreen(), + ), + ], + ), + StatefulShellBranch( + routes: [ + GoRoute( + name: favoriteScreen, + path: '/favorites', + builder: (context, state) => const FavoriteScreen(), + ), + ], + ), + StatefulShellBranch( + routes: [ + GoRoute( + name: cartScreen, + path: '/carts', + builder: (context, state) => const CartScreen(), + ), + ], + ), + StatefulShellBranch( + routes: [ + GoRoute( + name: orderScreen, + path: '/orders', + builder: (context, state) => const OrderScreen(), + ), + ], + ), + StatefulShellBranch( + routes: [ + GoRoute( + name: accountScreen, + path: '/account', + builder: (context, state) => const AccountScreen(), + ), + ], + ), + ], + ), + GoRoute( + parentNavigatorKey: _rootNavigatorKey, + name: productDetailScreen, + path: '/product-info', + pageBuilder: (context, state) { + final productDetail = state.extra as ProductDetail; + return CustomTransitionPage( + key: state.pageKey, + child: ProductDetailScreen(productDetail: productDetail), + + transitionsBuilder: + (context, animation, secondaryAnimation, child) => + FadeTransition( + opacity: CurveTween( + curve: Curves.easeIn, + ).animate(animation), + child: child, + ), + ); + }, + ), + GoRoute( + parentNavigatorKey: _rootNavigatorKey, + name: searchScreen, + path: '/search', + pageBuilder: (context, state) { + final categories = state.extra as List; + return CustomTransitionPage( + key: state.pageKey, + child: SearchScreen(categories: categories), + + transitionsBuilder: + (context, animation, secondaryAnimation, child) => + FadeTransition( + opacity: CurveTween( + curve: Curves.easeIn, + ).animate(animation), + child: child, + ), + ); + }, + ), + GoRoute( + parentNavigatorKey: _rootNavigatorKey, + name: outletMapScreen, + path: '/outlet-map', + pageBuilder: (context, state) { + final data = state.extra as Map; + + final userLocation = data['user_location'] as LatLng; + + final outlets = data['outlets'] as List; + + return CustomTransitionPage( + key: state.pageKey, + child: OutletMapScreen( + userLocation: userLocation, + outlets: outlets, + ), + + transitionsBuilder: + (context, animation, secondaryAnimation, child) => + FadeTransition( + opacity: CurveTween( + curve: Curves.easeIn, + ).animate(animation), + child: child, + ), + ); + }, + ), + GoRoute( + parentNavigatorKey: _rootNavigatorKey, + name: productCategoryScreen, + path: '/product-categories', + pageBuilder: (context, state) { + final category = state.extra as CategoryItem; + + return CustomTransitionPage( + key: state.pageKey, + child: ProductCategoryScreen(category: category), + + transitionsBuilder: + (context, animation, secondaryAnimation, child) => + FadeTransition( + opacity: CurveTween( + curve: Curves.easeIn, + ).animate(animation), + child: child, + ), + ); + }, + ), + GoRoute( + parentNavigatorKey: _rootNavigatorKey, + name: checkoutScreen, + path: '/checkout', + pageBuilder: (context, state) { + return CustomTransitionPage( + key: state.pageKey, + child: const CheckoutScreen(), + + transitionsBuilder: + (context, animation, secondaryAnimation, child) => + FadeTransition( + opacity: CurveTween( + curve: Curves.easeIn, + ).animate(animation), + child: child, + ), + ); + }, + ), + GoRoute( + parentNavigatorKey: _rootNavigatorKey, + name: shippingAddressScreen, + path: '/shipping-address', + pageBuilder: (context, state) { + return CustomTransitionPage( + key: state.pageKey, + child: const ShippingAddressScreen(), + + transitionsBuilder: + (context, animation, secondaryAnimation, child) => + FadeTransition( + opacity: CurveTween( + curve: Curves.easeIn, + ).animate(animation), + child: child, + ), + ); + }, + ), + GoRoute( + parentNavigatorKey: _rootNavigatorKey, + name: ratingScreen, + path: '/rating/:id', + pageBuilder: (context, state) { + final orderId = state.pathParameters['id']; + final products = state.extra as List; + + return CustomTransitionPage( + key: state.pageKey, + child: RatingScreen(orderId: orderId!, products: products), + + transitionsBuilder: + (context, animation, secondaryAnimation, child) => + FadeTransition( + opacity: CurveTween( + curve: Curves.easeIn, + ).animate(animation), + child: child, + ), + ); + }, + ), + GoRoute( + parentNavigatorKey: _rootNavigatorKey, + name: orderDetailScreen, + path: '/order-detail', + pageBuilder: (context, state) { + final orderInfo = state.extra as OrderInfo; + + return CustomTransitionPage( + key: state.pageKey, + child: OrderDetailScreen(orderInfo: orderInfo), + + transitionsBuilder: + (context, animation, secondaryAnimation, child) => + FadeTransition( + opacity: CurveTween( + curve: Curves.easeIn, + ).animate(animation), + child: child, + ), + ); + }, + ), + GoRoute( + parentNavigatorKey: _rootNavigatorKey, + name: customActionScreen, + path: '/custom-action', + pageBuilder: (context, state) { + final data = state.extra as Map; + + final orderId = data['order_id'] as String?; + + final orderNumber = data['order_number'] as String; + + final type = data['type'] as ActionType; + + return CustomTransitionPage( + key: state.pageKey, + child: CustomActionScreen( + orderId: orderId ?? '', + orderNumber: orderNumber, + type: type, + ), + + transitionsBuilder: + (context, animation, secondaryAnimation, child) => + FadeTransition( + opacity: CurveTween( + curve: Curves.easeIn, + ).animate(animation), + child: child, + ), + ); + }, + ), + GoRoute( + parentNavigatorKey: _rootNavigatorKey, + name: changePasswordScreen, + path: '/change-password', + pageBuilder: (context, state) { + return CustomTransitionPage( + key: state.pageKey, + child: const ChangePasswordScreen(), + + transitionsBuilder: + (context, animation, secondaryAnimation, child) => + FadeTransition( + opacity: CurveTween( + curve: Curves.easeIn, + ).animate(animation), + child: child, + ), + ); + }, + ), + GoRoute( + parentNavigatorKey: _rootNavigatorKey, + name: addressScreen, + path: '/addresses', + pageBuilder: (context, state) { + return CustomTransitionPage( + key: state.pageKey, + child: const AddressScreen(), + + transitionsBuilder: + (context, animation, secondaryAnimation, child) => + FadeTransition( + opacity: CurveTween( + curve: Curves.easeIn, + ).animate(animation), + child: child, + ), + ); + }, + ), + GoRoute( + parentNavigatorKey: _rootNavigatorKey, + name: mapAddressScreen, + path: '/map-address', + pageBuilder: (context, state) { + return CustomTransitionPage( + key: state.pageKey, + child: const MapAddressScreen(), + + transitionsBuilder: + (context, animation, secondaryAnimation, child) => + FadeTransition( + opacity: CurveTween( + curve: Curves.easeIn, + ).animate(animation), + child: child, + ), + ); + }, + ), + GoRoute( + parentNavigatorKey: _rootNavigatorKey, + name: saveAddressScreen, + path: '/save-address', + pageBuilder: (context, state) { + final label = state.extra as String?; + + return CustomTransitionPage( + key: state.pageKey, + child: SaveAddressScreen(label: label), + + transitionsBuilder: + (context, animation, secondaryAnimation, child) => + FadeTransition( + opacity: CurveTween( + curve: Curves.easeIn, + ).animate(animation), + child: child, + ), + ); + }, + ), + ], + ); +} diff --git a/Mobile Commerce/lib/core/service/echo_service.dart b/Mobile Commerce/lib/core/service/echo_service.dart new file mode 100644 index 0000000..572619d --- /dev/null +++ b/Mobile Commerce/lib/core/service/echo_service.dart @@ -0,0 +1,37 @@ +import 'package:laravel_echo_null/laravel_echo_null.dart'; +import 'package:niogu_ecommerce_v1/core/constant/app_config.dart'; +import 'package:niogu_ecommerce_v1/core/constant/app_temp.dart'; +import 'package:pusher_client_socket/pusher_client_socket.dart' as PUSHER; + +class EchoService { + Echo? _echo; + + Future init({ + required Function(String event, Map data) listener, + }) async { + if (_echo != null) return; + + final tenantId = AppTemp.TENANT_ID; + + _echo = Echo.pusher( + AppConfig.REVERB_APP_KEY, + authEndPoint: AppConfig.BASE_URL, + authHeaders: () async => {}, + host: '192.168.137.1', + wsPort: 8080, + wssPort: 8080, + encrypted: false, + enableLogging: true, + autoConnect: true, + ); + + _echo!.channel('tenant.$tenantId').listen('.data.batch.updated', (data) { + listener('data.batch.updated', data); + }); + } + + void disconnect() { + _echo?.disconnect(); + _echo = null; + } +} diff --git a/Mobile Commerce/lib/core/system/system_secure.dart b/Mobile Commerce/lib/core/system/system_secure.dart new file mode 100644 index 0000000..c695d9b --- /dev/null +++ b/Mobile Commerce/lib/core/system/system_secure.dart @@ -0,0 +1,18 @@ +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; +import 'package:niogu_ecommerce_v1/core/constant/app_key.dart'; + +class SystemSecure { + static final _storage = const FlutterSecureStorage(); + + static Future saveAccessToken(String accessToken) async { + await _storage.write(key: AppKey.AUTH_TOKEN, value: accessToken); + } + + static Future getAccessToken() async { + return await _storage.read(key: AppKey.AUTH_TOKEN); + } + + static Future deleteAccessToken() async { + await _storage.delete(key: AppKey.AUTH_TOKEN); + } +} diff --git a/Mobile Commerce/lib/core/system/system_setting.dart b/Mobile Commerce/lib/core/system/system_setting.dart new file mode 100644 index 0000000..a474e04 --- /dev/null +++ b/Mobile Commerce/lib/core/system/system_setting.dart @@ -0,0 +1,161 @@ +import 'dart:convert'; + +import 'package:latlong2/latlong.dart'; +import 'package:niogu_ecommerce_v1/core/constant/app_key.dart'; +import 'package:niogu_ecommerce_v1/features/cart/domain/entities/cart.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +class SystemSetting { + static Future getTenantCode() async { + final prefs = await SharedPreferences.getInstance(); + + return await prefs.getString(AppKey.TENANT_CODE); + } + + static Future getCurrentOutletId() async { + final prefs = await SharedPreferences.getInstance(); + + return await prefs.getString(AppKey.OUTLET_ID); + } + + static Future getCurrentOutletName() async { + final prefs = await SharedPreferences.getInstance(); + + return await prefs.getString(AppKey.OUTLET_NAME); + } + + static Future getCurrentOutletPhone() async { + final prefs = await SharedPreferences.getInstance(); + + return await prefs.getString(AppKey.OUTLET_PHONE); + } + + static Future getCurrentOutletLocation() async { + final prefs = await SharedPreferences.getInstance(); + + return await prefs.getString(AppKey.OUTLET_LOCATION); + } + + static Future getCurrentOutletCoordinate() async { + final prefs = await SharedPreferences.getInstance(); + + final latitude = await prefs.getDouble(AppKey.OUTLET_LATITUDE); + + final longitude = await prefs.getDouble(AppKey.OUTLET_LONGITUDE); + + if (latitude == null || longitude == null) return null; + + return LatLng(latitude, longitude); + } + + static Future switchOutlet({ + required String outletId, + required String outletName, + String? outletPhone, + String? outletLocation, + LatLng? outletCoordinate, + }) async { + final prefs = await SharedPreferences.getInstance(); + + prefs.setString(AppKey.OUTLET_ID, outletId); + + prefs.setString(AppKey.OUTLET_NAME, outletName); + + if (outletPhone != null) { + prefs.setString(AppKey.OUTLET_PHONE, outletPhone); + } + + if (outletLocation != null && outletCoordinate != null) { + prefs.setString(AppKey.OUTLET_LOCATION, outletLocation); + prefs.setDouble(AppKey.OUTLET_LATITUDE, outletCoordinate.latitude); + prefs.setDouble(AppKey.OUTLET_LONGITUDE, outletCoordinate.longitude); + } + } + + static Future isLoggedIn() async { + final prefs = await SharedPreferences.getInstance(); + + return await prefs.getBool(AppKey.CUSTOMER_LOGIN) ?? false; + } + + static Future setIsLoggedIn() async { + final prefs = await SharedPreferences.getInstance(); + + await prefs.setBool(AppKey.CUSTOMER_LOGIN, true); + } + + static Future getCurrentCustomerId() async { + final prefs = await SharedPreferences.getInstance(); + + return prefs.getString(AppKey.CUSTOMER_ID); + } + + static Future getCurrentCustomerName() async { + final prefs = await SharedPreferences.getInstance(); + + return prefs.getString(AppKey.CUSTOMER_NAME); + } + + static Future getCurrentCustomerEmail() async { + final prefs = await SharedPreferences.getInstance(); + + return prefs.getString(AppKey.CUSTOMER_EMAIL); + } + + static Future getCurrentCustomerPhone() async { + final prefs = await SharedPreferences.getInstance(); + + return prefs.getString(AppKey.CUSTOMER_PHONE); + } + + static Future setCustomerInfo({ + String? id, + String? name, + String? email, + String? phoneNumber, + }) async { + final prefs = await SharedPreferences.getInstance(); + + if (id != null) { + prefs.setString(AppKey.CUSTOMER_ID, id); + } + + if (name != null) { + prefs.setString(AppKey.CUSTOMER_NAME, name); + } + + if (email != null) { + prefs.setString(AppKey.CUSTOMER_EMAIL, email); + } + + if (phoneNumber != null) { + prefs.setString(AppKey.CUSTOMER_PHONE, phoneNumber); + } + } + + static Future> getCartItemByOutlet() async { + final prefs = await SharedPreferences.getInstance(); + + final jsonList = prefs.getStringList(AppKey.CART_ITEM_BY_OUTLET); + + if (jsonList == null) return []; + + return jsonList.map((json) => CartItem.fromJson(jsonDecode(json))).toList(); + } + + static Future saveCartItemByOutlet(List cartItems) async { + final prefs = await SharedPreferences.getInstance(); + + final data = cartItems + .map((cartItem) => jsonEncode(cartItem.toJson())) + .toList(); + + prefs.setStringList(AppKey.CART_ITEM_BY_OUTLET, data); + } + + static Future clear() async { + final prefs = await SharedPreferences.getInstance(); + + await prefs.clear(); + } +} diff --git a/Mobile Commerce/lib/core/utils/converter.dart b/Mobile Commerce/lib/core/utils/converter.dart new file mode 100644 index 0000000..91b9f0f --- /dev/null +++ b/Mobile Commerce/lib/core/utils/converter.dart @@ -0,0 +1,5 @@ +double toDouble(dynamic value) { + if (value == null) return 0.0; + if (value is num) return value.toDouble(); + return double.tryParse(value.toString()) ?? 0.0; +} diff --git a/Mobile Commerce/lib/core/utils/currency_format.dart b/Mobile Commerce/lib/core/utils/currency_format.dart new file mode 100644 index 0000000..d804779 --- /dev/null +++ b/Mobile Commerce/lib/core/utils/currency_format.dart @@ -0,0 +1,12 @@ +import 'package:intl/intl.dart'; + +class CurrencyFormat { + static String formatToIdr(double amount, int decimalDigits) { + final NumberFormat currencyFormatter = NumberFormat.currency( + locale: 'id_ID', + symbol: 'Rp', + decimalDigits: decimalDigits, + ); + return currencyFormatter.format(amount); + } +} diff --git a/Mobile Commerce/lib/core/utils/extenstion_format.dart b/Mobile Commerce/lib/core/utils/extenstion_format.dart new file mode 100644 index 0000000..8b0a300 --- /dev/null +++ b/Mobile Commerce/lib/core/utils/extenstion_format.dart @@ -0,0 +1,66 @@ +extension DoubleFormatter on double { + String toStringWithoutTrailingZero() { + if (this % 1 == 0) { + return toStringAsFixed(0); + } else { + return toString(); + } + } + + String toRatingFormat({int maxDecimal = 2}) { + final rounded = double.parse(toStringAsFixed(maxDecimal)); + String result = rounded.toString(); + if (result.contains('.')) { + result = result.replaceFirst(RegExp(r'\.?0+$'), ''); + } + + return result; + } + + String get toCompact { + if (this < 1000) { + return toInt().toString(); + } else { + double result = this / 1000; + + String s = result.toStringAsFixed(2); + + s = s.replaceAll(RegExp(r"([.]*0+)(?!.*\d)"), ""); + + s = s.replaceAll('.', ','); + + return "${s}k"; + } + } +} + +extension StringFormatter on String { + String toUniqueNumberCleaner() { + return this.replaceFirst(RegExp(r'-[A-Z0-9]{8}-'), '-'); + } + + String capitalize() { + return "${this[0].toUpperCase()}${substring(1)}"; + } + + String toRelativeImagePath() { + final uri = Uri.parse(this); + return uri.path.replaceFirst('/storage/', ''); + } + + String normalizePhoneNumber() { + var phone = this.replaceAll(RegExp(r'[^0-9+]'), ''); + + if (phone.startsWith('+62')) { + phone = '62${phone.substring(3)}'; + } else if (phone.startsWith('08')) { + phone = '62${phone.substring(1)}'; + } + + if (!phone.startsWith('62')) { + throw Exception('Nomor bukan format Indonesia'); + } + + return phone; + } +} diff --git a/Mobile Commerce/lib/core/utils/image_service.dart b/Mobile Commerce/lib/core/utils/image_service.dart new file mode 100644 index 0000000..5c6b5c5 --- /dev/null +++ b/Mobile Commerce/lib/core/utils/image_service.dart @@ -0,0 +1,43 @@ +import 'dart:io'; +import 'package:niogu_ecommerce_v1/core/utils/log_message.dart'; +import 'package:path_provider/path_provider.dart'; +import 'package:path/path.dart' as p; + +class ImageService { + static Future saveImageToLocalDirectory( + File sourceFile, + String dir, + ) async { + try { + final directory = await getApplicationDocumentsDirectory(); + + final String path = directory.path; + final Directory imageDir = Directory('$path/$dir'); + if (!await imageDir.exists()) { + await imageDir.create(recursive: true); + } + + final String fileName = '${DateTime.now().millisecondsSinceEpoch}.jpg'; + + final String newPath = p.join(imageDir.path, fileName); + + final File newImage = await sourceFile.copy(newPath); + + return newImage.path; + } catch (e, st) { + LogMessage.log.e(e.toString(), error: e, stackTrace: st); + return null; + } + } + + static Future deleteLocalImage(String path) async { + try { + final file = File(path); + if (await file.exists()) { + await file.delete(); + } + } catch (e, st) { + LogMessage.log.e(e.toString(), error: e, stackTrace: st); + } + } +} diff --git a/Mobile Commerce/lib/core/utils/log_message.dart b/Mobile Commerce/lib/core/utils/log_message.dart new file mode 100644 index 0000000..c9c1b00 --- /dev/null +++ b/Mobile Commerce/lib/core/utils/log_message.dart @@ -0,0 +1,15 @@ +import 'package:logger/logger.dart'; + +class LogMessage { + + static Logger _getLog() { + return Logger( + filter: DevelopmentFilter(), + level: Level.all, + output: ConsoleOutput(), + printer: PrettyPrinter(methodCount: 10), + ); + } + + static Logger get log => _getLog(); +} \ No newline at end of file diff --git a/Mobile Commerce/lib/core/utils/time_zone.dart b/Mobile Commerce/lib/core/utils/time_zone.dart new file mode 100644 index 0000000..b44ef3a --- /dev/null +++ b/Mobile Commerce/lib/core/utils/time_zone.dart @@ -0,0 +1,16 @@ +class TimeZone { + static String getCurrentTimeZone() { + final int offset = DateTime.now().timeZoneOffset.inHours; + + switch (offset) { + case 7: + return 'WIB'; + case 8: + return 'WITA'; + case 9: + return 'WIT'; + default: + return DateTime.now().timeZoneName; + } + } +} \ No newline at end of file diff --git a/Mobile Commerce/lib/core/widgets/custom_action_screen.dart b/Mobile Commerce/lib/core/widgets/custom_action_screen.dart new file mode 100644 index 0000000..a9c4775 --- /dev/null +++ b/Mobile Commerce/lib/core/widgets/custom_action_screen.dart @@ -0,0 +1,178 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; +import 'package:niogu_ecommerce_v1/core/constant/app_font_size.dart'; +import 'package:niogu_ecommerce_v1/core/enums/action_type.dart'; +import 'package:niogu_ecommerce_v1/core/router/app_route.dart'; +import 'package:niogu_ecommerce_v1/core/utils/extenstion_format.dart'; +import 'package:niogu_ecommerce_v1/core/widgets/custom_snackbar.dart'; +import 'package:niogu_ecommerce_v1/features/order/presentation/providers/order_provider.dart'; +import 'package:sizer/sizer.dart'; +import 'package:niogu_ecommerce_v1/core/constant/app_color.dart'; + +class CustomActionScreen extends ConsumerWidget { + final String orderId; + final String orderNumber; + final ActionType type; + + const CustomActionScreen({ + super.key, + required this.orderId, + required this.orderNumber, + required this.type, + }); + + @override + Widget build(BuildContext context, WidgetRef ref) { + String title; + String description; + IconData icon; + Color themeColor; + + switch (type) { + case ActionType.checkout: + title = "Pesanan Berhasil!"; + description = + "Terima kasih! Pesanan anda sedang kami siapkan dan akan segera dikirim."; + icon = Icons.check_circle_outline; + themeColor = AppColor.primaryColor; + break; + case ActionType.cancel: + title = "Pesanan Dibatalkan"; + description = "Pesanan anda telah berhasil dibatalkan"; + icon = Icons.cancel_outlined; + themeColor = Colors.red; + break; + case ActionType.requestCancel: + title = "Pengajuan Terkirim"; + description = + "Permintaan pembatalan anda telah diterima dan sedang menunggu persetujuan admin."; + icon = Icons.access_time_outlined; + themeColor = Colors.orange; + break; + } + + return SafeArea( + top: false, + bottom: true, + right: false, + left: false, + child: Scaffold( + backgroundColor: Colors.white, + body: Container( + width: double.infinity, + padding: EdgeInsets.symmetric(horizontal: 8.w), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Container( + padding: EdgeInsets.all(6.w), + decoration: BoxDecoration( + color: themeColor.withOpacity(0.1), + shape: BoxShape.circle, + ), + child: Icon(icon, size: 25.w, color: themeColor), + ), + SizedBox(height: 5.h), + + Text( + title, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: AppFontSize.medium.sp, + fontWeight: FontWeight.bold, + color: Colors.black87, + ), + ), + SizedBox(height: 2.h), + + Container( + padding: EdgeInsets.symmetric(horizontal: 4.w, vertical: 1.h), + decoration: BoxDecoration( + color: Colors.grey.shade100, + borderRadius: BorderRadius.circular(2.5.w), + ), + child: Text( + "Nomor Pesanan : ${orderNumber.toUniqueNumberCleaner()}", + style: TextStyle( + fontSize: AppFontSize.small.sp, + fontWeight: FontWeight.w600, + color: Colors.grey.shade700, + ), + ), + ), + SizedBox(height: 3.h), + + Text( + description, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: AppFontSize.small.sp, + color: Colors.grey.shade600, + height: 1.5, + ), + ), + SizedBox(height: 8.h), + + SizedBox( + width: double.infinity, + child: ElevatedButton( + onPressed: () { + context.goNamed(AppRoute.homeScreen); + }, + style: ElevatedButton.styleFrom( + backgroundColor: AppColor.primaryColor, + padding: EdgeInsets.symmetric(vertical: 2.h), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(2.5.w), + ), + elevation: 0, + ), + child: Text( + "Kembali ke Beranda", + style: TextStyle( + fontSize: AppFontSize.medium.sp, + fontWeight: FontWeight.bold, + color: Colors.white, + ), + ), + ), + ), + SizedBox(height: 2.h), + + if (type == ActionType.checkout) + TextButton( + onPressed: () async { + final orderInfo = await ref + .read(orderRepositoryProvider) + .fetchOrderById(orderId); + + if (orderInfo == null) { + CustomSnackbar.showError( + context, + "Ups, terjadi kesalahan", + ); + return; + } + + context.goNamed( + AppRoute.orderDetailScreen, + extra: orderInfo, + ); + }, + child: Text( + "Lihat Detail Pesanan", + style: TextStyle( + fontSize: AppFontSize.medium.sp, + color: AppColor.primaryColor, + fontWeight: FontWeight.w600, + ), + ), + ), + ], + ), + ), + ), + ); + } +} diff --git a/Mobile Commerce/lib/core/widgets/custom_empty_screen.dart b/Mobile Commerce/lib/core/widgets/custom_empty_screen.dart new file mode 100644 index 0000000..ac56d50 --- /dev/null +++ b/Mobile Commerce/lib/core/widgets/custom_empty_screen.dart @@ -0,0 +1,52 @@ +import 'package:flutter/material.dart'; +import 'package:niogu_ecommerce_v1/core/constant/app_font_size.dart'; +import 'package:sizer/sizer.dart'; + +class CustomEmptyScreen extends StatelessWidget { + final IconData icon; + final String title; + final String? subtitle; + final double? height; + const CustomEmptyScreen({ + super.key, + required this.icon, + required this.title, + this.subtitle, + this.height, + }); + + @override + Widget build(BuildContext context) { + return Container( + width: double.infinity, + height: height ?? 20.h, + margin: EdgeInsets.symmetric(horizontal: 4.w, vertical: 2.h), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(2.5.w), + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(icon, color: Colors.grey.shade300, size: 40.sp), + SizedBox(height: 1.h), + Text( + title, + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: AppFontSize.small.sp, + color: Colors.grey.shade700, + ), + ), + if (subtitle != null) + Text( + subtitle!, + style: TextStyle( + fontSize: (AppFontSize.small - 1.25).sp, + color: Colors.grey.shade700, + ), + ), + ], + ), + ); + } +} diff --git a/Mobile Commerce/lib/core/widgets/custom_snackbar.dart b/Mobile Commerce/lib/core/widgets/custom_snackbar.dart new file mode 100644 index 0000000..b7ed75b --- /dev/null +++ b/Mobile Commerce/lib/core/widgets/custom_snackbar.dart @@ -0,0 +1,74 @@ +import 'package:flutter/material.dart'; +import 'package:niogu_ecommerce_v1/core/constant/app_font_size.dart'; +import 'package:sizer/sizer.dart'; + +class CustomSnackbar { + static const Color _successColor = Color(0xFF4CAF50); + static const Color _errorColor = Color(0xFFE53935); + static const Color _warningColor = Color(0xFFFFC107); + + static void showSuccess(BuildContext context, String message) { + _show(context, Icons.check_circle_rounded, _successColor, message); + } + + static void showError(BuildContext context, String message) { + _show(context, Icons.error_rounded, _errorColor, message); + } + + static void showWarning(BuildContext context, String message) { + _show(context, Icons.warning_amber_rounded, _warningColor, message); + } + + static void _show( + BuildContext context, + IconData icon, + Color iconColor, + String message, + ) { + final bool isTablet = 100.w >= 600; + final messenger = ScaffoldMessenger.of(context); + + messenger.removeCurrentSnackBar(); + + messenger.showSnackBar( + SnackBar( + content: Row( + children: [ + Container( + decoration: BoxDecoration( + color: icon == Icons.warning_amber_rounded + ? Colors.transparent + : Colors.white, + shape: BoxShape.circle, + ), + child: Icon(icon, color: iconColor, size: 6.w), + ), + SizedBox(width: 3.w), + Expanded( + child: Text( + message, + style: TextStyle( + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + fontWeight: FontWeight.w600, + color: Colors.white, + ), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + backgroundColor: Colors.black, + behavior: SnackBarBehavior.floating, + elevation: 4, + margin: EdgeInsets.all(4.w), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(2.5.w), + ), + duration: const Duration(seconds: 3), + ), + ); + } +} diff --git a/Mobile Commerce/lib/core/widgets/custom_text_form_field.dart b/Mobile Commerce/lib/core/widgets/custom_text_form_field.dart new file mode 100644 index 0000000..3e91e63 --- /dev/null +++ b/Mobile Commerce/lib/core/widgets/custom_text_form_field.dart @@ -0,0 +1,201 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:niogu_ecommerce_v1/core/constant/app_color.dart'; +import 'package:niogu_ecommerce_v1/core/constant/app_font_size.dart'; +import 'package:niogu_ecommerce_v1/core/widgets/custom_snackbar.dart'; +import 'package:sizer/sizer.dart'; + +class CustomTextFormField extends StatelessWidget { + final String label; + final Color? labelColor; + final String? hint; + final String? addInformation; + final TextEditingController controller; + final TextInputType keyboardType; + final bool autoFocus; + final String? errorText; + final String? Function(String?)? validator; + final Function(String)? onChanged; + final bool isObscure; + final bool readOnly; + final List? inputFormatters; + final String? onTapReadOnlyMessage; + final Color? enabledBorderColor; + final Color? focusedBorderColor; + final IconData? prefixIcon; + final IconData? suffixIcon; + final VoidCallback? suffixPressed; + final String? suffixText; + + CustomTextFormField({ + super.key, + required this.label, + this.labelColor, + this.hint, + this.addInformation, + required this.controller, + this.keyboardType = TextInputType.text, + this.autoFocus = false, + this.errorText, + this.validator, + this.onChanged, + this.isObscure = false, + this.readOnly = false, + this.inputFormatters, + this.onTapReadOnlyMessage, + this.enabledBorderColor, + this.focusedBorderColor, + this.prefixIcon, + this.suffixIcon, + this.suffixPressed, + this.suffixText, + }); + + @override + Widget build(BuildContext context) { + final bool isTablet = 100.w >= 600; + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + label, + style: TextStyle( + fontSize: isTablet ? AppFontSize.medium.sp : AppFontSize.small.sp, + fontWeight: FontWeight.bold, + color: labelColor ?? Colors.black87, + ), + ), + if (addInformation != null) ...[ + SizedBox(height: 1.h), + Padding( + padding: EdgeInsets.only(bottom: 1.5.h), + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon( + Icons.info_outline, + size: 3.5.w, + color: AppColor.primaryColor, + ), + SizedBox(width: 0.75.w), + Text( + addInformation!, + style: TextStyle( + color: AppColor.primaryColor, + fontSize: isTablet + ? (AppFontSize.medium - 1.25).sp + : (AppFontSize.small - 1.25).sp, + ), + ), + ], + ), + ], + ), + ), + ], + SizedBox(height: 1.h), + TextFormField( + controller: controller, + keyboardType: keyboardType, + autofocus: autoFocus, + obscureText: isObscure, + onChanged: onChanged, + validator: validator, + readOnly: readOnly, + onTap: !readOnly + ? null + : () { + if (onTapReadOnlyMessage != null && + onTapReadOnlyMessage!.isNotEmpty) { + CustomSnackbar.showWarning(context, onTapReadOnlyMessage!); + } + }, + autovalidateMode: AutovalidateMode.onUserInteraction, + style: TextStyle( + fontSize: isTablet ? AppFontSize.medium.sp : AppFontSize.small.sp, + fontWeight: FontWeight.w500, + ), + inputFormatters: inputFormatters, + decoration: InputDecoration( + filled: readOnly, + fillColor: readOnly ? Colors.grey.shade100 : Colors.white, + hintText: hint, + hintStyle: TextStyle( + color: Colors.grey.shade500, + fontSize: isTablet ? AppFontSize.medium.sp : AppFontSize.small.sp, + ), + contentPadding: EdgeInsets.symmetric( + horizontal: 4.w, + vertical: 1.8.h, + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(2.5.w), + borderSide: BorderSide( + color: enabledBorderColor ?? Colors.grey.shade300, + ), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(2.5.w), + borderSide: BorderSide( + color: readOnly + ? Colors.grey.shade300 + : (focusedBorderColor ?? AppColor.primaryColor), + width: 1, + ), + ), + errorText: errorText, + errorStyle: TextStyle( + color: Colors.red, + fontSize: isTablet ? AppFontSize.medium.sp : AppFontSize.small.sp, + ), + errorBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(2.5.w), + borderSide: const BorderSide(color: Colors.redAccent), + ), + focusedErrorBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(2.5.w), + borderSide: const BorderSide(color: Colors.redAccent, width: 1.5), + ), + prefixIcon: prefixIcon == null + ? null + : Padding( + padding: EdgeInsets.symmetric( + horizontal: isTablet ? 5.w : 2.5.w, + ), + child: Icon( + prefixIcon, + size: 5.w, + color: Colors.grey.shade500, + ), + ), + suffixIcon: suffixIcon == null + ? null + : Padding( + padding: EdgeInsets.symmetric( + horizontal: isTablet ? 5.w : 2.5.w, + ), + child: IconButton( + onPressed: suffixPressed, + icon: Icon( + suffixIcon, + size: 5.w, + color: Colors.grey.shade500, + ), + ), + ), + suffixText: suffixText, + suffixStyle: TextStyle( + fontSize: isTablet + ? (AppFontSize.medium - 1.25).sp + : (AppFontSize.small - 1.25).sp, + color: Colors.grey, + ), + ), + ), + ], + ); + } +} diff --git a/Mobile Commerce/lib/core/widgets/triangle_painter.dart b/Mobile Commerce/lib/core/widgets/triangle_painter.dart new file mode 100644 index 0000000..8a322d1 --- /dev/null +++ b/Mobile Commerce/lib/core/widgets/triangle_painter.dart @@ -0,0 +1,20 @@ +import 'package:flutter/material.dart'; + +class TrianglePainter extends CustomPainter { + final Color color; + TrianglePainter(this.color); + + @override + void paint(Canvas canvas, Size size) { + final paint = Paint()..color = color; + final path = Path() + ..moveTo(0, 0) + ..lineTo(size.width, 0) + ..lineTo(size.width / 2, size.height) + ..close(); + canvas.drawPath(path, paint); + } + + @override + bool shouldRepaint(CustomPainter oldDelegate) => false; +} \ No newline at end of file diff --git a/Mobile Commerce/lib/features/account/data/datasource/account_remote_datasource_impl.dart b/Mobile Commerce/lib/features/account/data/datasource/account_remote_datasource_impl.dart new file mode 100644 index 0000000..9d9f1f1 --- /dev/null +++ b/Mobile Commerce/lib/features/account/data/datasource/account_remote_datasource_impl.dart @@ -0,0 +1,104 @@ +import 'package:dio/dio.dart'; +import 'package:niogu_ecommerce_v1/core/constant/app_url.dart'; +import 'package:niogu_ecommerce_v1/core/models/api_response_model.dart'; +import 'package:niogu_ecommerce_v1/core/system/system_setting.dart'; +import 'package:niogu_ecommerce_v1/features/account/data/models/account_model.dart'; + +abstract class IAccountRemoteDatasource { + Future> updateCustomer( + CustomerInfoRequest request, + ); + + Future> updatePassword({ + required String oldPassword, + required String newPassword, + }); + + Future>> fetchCustomerAddresses(); + + Future> saveCustomerAddress( + CustomerAddressModel request, + ); + + Future> deleteCustomerAddress(String id); +} + +class AccountRemoteDatasourceImpl implements IAccountRemoteDatasource { + final Dio _dio; + + AccountRemoteDatasourceImpl(this._dio); + + @override + Future> updateCustomer( + CustomerInfoRequest request, + ) async { + final customerId = await SystemSetting.getCurrentCustomerId(); + + final response = await _dio.patch( + "${AppUrl.CUSTOMER}/$customerId", + data: request.toJson(), + options: Options(contentType: Headers.jsonContentType), + ); + + return ApiResponseModel.fromJson( + response.data, + (json) => CustomerInfoResponse.fromJson(json as Map), + ); + } + + @override + Future> updatePassword({ + required String oldPassword, + required String newPassword, + }) async { + final customerId = await SystemSetting.getCurrentCustomerId(); + + final response = await _dio.patch( + "${AppUrl.CUSTOMER}/$customerId/password", + data: {'old_password': oldPassword, 'new_password': newPassword}, + ); + + return ApiResponseModel.fromJson(response.data, (_) => null); + } + + @override + Future>> + fetchCustomerAddresses() async { + final customerId = await SystemSetting.getCurrentCustomerId(); + + final response = await _dio.get("${AppUrl.CUSTOMER}/$customerId/addresses"); + + return ApiResponseModel.fromJson( + response.data, + (json) => (json as List) + .map((e) => CustomerAddressModel.fromJson(e as Map)) + .toList(), + ); + } + + @override + Future> saveCustomerAddress( + CustomerAddressModel request, + ) async { + final customerId = await SystemSetting.getCurrentCustomerId(); + + final response = await _dio.post( + "${AppUrl.CUSTOMER}/$customerId", + data: request.toJson(), + options: Options(contentType: Headers.jsonContentType), + ); + + return ApiResponseModel.fromJson(response.data, (_) => null); + } + + @override + Future> deleteCustomerAddress(String id) async { + final customerId = await SystemSetting.getCurrentCustomerId(); + + final response = await _dio.delete( + "${AppUrl.CUSTOMER}/$customerId/addresses/$id", + ); + + return ApiResponseModel.fromJson(response.data, (_) => null); + } +} diff --git a/Mobile Commerce/lib/features/account/data/models/account_model.dart b/Mobile Commerce/lib/features/account/data/models/account_model.dart new file mode 100644 index 0000000..a7e4868 --- /dev/null +++ b/Mobile Commerce/lib/features/account/data/models/account_model.dart @@ -0,0 +1,79 @@ +import 'package:json_annotation/json_annotation.dart'; +import 'package:niogu_ecommerce_v1/core/utils/converter.dart'; +import 'package:niogu_ecommerce_v1/features/account/domain/entities/account.dart'; + +part 'account_model.g.dart'; + +@JsonSerializable() +class CustomerInfoResponse { + final String name; + final String? email; + @JsonKey(name: 'phone_number') + final String phoneNumber; + + const CustomerInfoResponse({ + required this.name, + this.email, + required this.phoneNumber, + }); + + factory CustomerInfoResponse.fromJson(Map json) => + _$CustomerInfoResponseFromJson(json); + + Map toJson() => _$CustomerInfoResponseToJson(this); + + CustomerInfo toCustomerInfo() { + return CustomerInfo(name: name, email: email, phoneNumber: phoneNumber); + } +} + +@JsonSerializable() +class CustomerInfoRequest { + final String name; + + const CustomerInfoRequest({required this.name}); + + factory CustomerInfoRequest.fromJson(Map json) => + _$CustomerInfoRequestFromJson(json); + + Map toJson() => _$CustomerInfoRequestToJson(this); +} + +@JsonSerializable() +class CustomerAddressModel { + final String uuid; + + final String label; + + @JsonKey(name: 'full_address') + final String fullAddress; + + @JsonKey(fromJson: toDouble) + final double latitude; + + @JsonKey(fromJson: toDouble) + final double longitude; + + const CustomerAddressModel({ + required this.uuid, + required this.label, + required this.fullAddress, + required this.latitude, + required this.longitude, + }); + + factory CustomerAddressModel.fromJson(Map json) => + _$CustomerAddressModelFromJson(json); + + Map toJson() => _$CustomerAddressModelToJson(this); + + CustomerAddress toCustomerAddress() { + return CustomerAddress( + uuid: uuid, + label: label, + fullAddress: fullAddress, + latitude: latitude, + longitude: longitude, + ); + } +} diff --git a/Mobile Commerce/lib/features/account/data/models/account_model.g.dart b/Mobile Commerce/lib/features/account/data/models/account_model.g.dart new file mode 100644 index 0000000..c8690a0 --- /dev/null +++ b/Mobile Commerce/lib/features/account/data/models/account_model.g.dart @@ -0,0 +1,50 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'account_model.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +CustomerInfoResponse _$CustomerInfoResponseFromJson( + Map json, +) => CustomerInfoResponse( + name: json['name'] as String, + email: json['email'] as String?, + phoneNumber: json['phone_number'] as String, +); + +Map _$CustomerInfoResponseToJson( + CustomerInfoResponse instance, +) => { + 'name': instance.name, + 'email': instance.email, + 'phone_number': instance.phoneNumber, +}; + +CustomerInfoRequest _$CustomerInfoRequestFromJson(Map json) => + CustomerInfoRequest(name: json['name'] as String); + +Map _$CustomerInfoRequestToJson( + CustomerInfoRequest instance, +) => {'name': instance.name}; + +CustomerAddressModel _$CustomerAddressModelFromJson( + Map json, +) => CustomerAddressModel( + uuid: json['uuid'] as String, + label: json['label'] as String, + fullAddress: json['full_address'] as String, + latitude: toDouble(json['latitude']), + longitude: toDouble(json['longitude']), +); + +Map _$CustomerAddressModelToJson( + CustomerAddressModel instance, +) => { + 'uuid': instance.uuid, + 'label': instance.label, + 'full_address': instance.fullAddress, + 'latitude': instance.latitude, + 'longitude': instance.longitude, +}; diff --git a/Mobile Commerce/lib/features/account/data/repositories/account_repository_impl.dart b/Mobile Commerce/lib/features/account/data/repositories/account_repository_impl.dart new file mode 100644 index 0000000..4090756 --- /dev/null +++ b/Mobile Commerce/lib/features/account/data/repositories/account_repository_impl.dart @@ -0,0 +1,81 @@ +import 'package:dio/dio.dart'; +import 'package:niogu_ecommerce_v1/core/errors/exceptions.dart'; +import 'package:niogu_ecommerce_v1/core/utils/log_message.dart'; +import 'package:niogu_ecommerce_v1/features/account/data/datasource/account_remote_datasource_impl.dart'; +import 'package:niogu_ecommerce_v1/features/account/data/models/account_model.dart'; +import 'package:niogu_ecommerce_v1/features/account/domain/entities/account.dart'; +import 'package:niogu_ecommerce_v1/features/account/domain/repositories/i_account_repository.dart'; + +class AccountRepositoryImpl implements IAccountRepository { + final IAccountRemoteDatasource _datasource; + + AccountRepositoryImpl(this._datasource); + + @override + Future updateCustomer(CustomerInfoRequest request) async { + try { + final response = await _datasource.updateCustomer(request); + + final data = response.data; + + if (response.status != 'success' || data == null) return null; + + return data.toCustomerInfo(); + } on DioException catch (e, st) { + LogMessage.log.e(e.toString(), error: e, stackTrace: st); + throw ServerException("Terjadi kesalahan koneksi"); + } + } + + @override + Future updatePassword({ + required String oldPassword, + required newPassword, + }) async { + try { + await _datasource.updatePassword( + oldPassword: oldPassword, + newPassword: newPassword, + ); + } on DioException catch (e, st) { + LogMessage.log.e(e.toString(), error: e, stackTrace: st); + throw ServerException("Password lama salah"); + } + } + + @override + Future> fetchCustomerAddresses() async { + try { + final response = await _datasource.fetchCustomerAddresses(); + + final data = response.data; + + if (response.status != 'success' || data == null) return []; + + return data.map((address) => address.toCustomerAddress()).toList(); + } on DioException catch (e, st) { + LogMessage.log.e(e.toString(), error: e, stackTrace: st); + throw ServerException("Terjadi kesalahan koneksi"); + } + } + + @override + Future saveCustomerAddress(CustomerAddress address) async { + try { + await _datasource.saveCustomerAddress(address.toCustomerAddressModel()); + } on DioException catch (e, st) { + LogMessage.log.e(e.toString(), error: e, stackTrace: st); + throw ServerException("Terjadi kesalahan koneksi"); + } + } + + @override + Future deleteCustomerAddress(String id) async { + try { + await _datasource.deleteCustomerAddress(id); + } on DioException catch (e, st) { + LogMessage.log.e(e.toString(), error: e, stackTrace: st); + throw ServerException("Terjadi kesalahan koneksi"); + } + } +} diff --git a/Mobile Commerce/lib/features/account/domain/entities/account.dart b/Mobile Commerce/lib/features/account/domain/entities/account.dart new file mode 100644 index 0000000..85e7258 --- /dev/null +++ b/Mobile Commerce/lib/features/account/domain/entities/account.dart @@ -0,0 +1,70 @@ +import 'package:niogu_ecommerce_v1/features/account/data/models/account_model.dart'; +import 'package:uuid/uuid.dart'; + +class CustomerInfo { + final String name; + final String? email; + final String phoneNumber; + + const CustomerInfo({ + required this.name, + required this.email, + required this.phoneNumber, + }); +} + +class CustomerAddress { + final String uuid; + + final String label; + + final String fullAddress; + + final double latitude; + + final double longitude; + + const CustomerAddress({ + required this.uuid, + required this.label, + required this.fullAddress, + required this.latitude, + required this.longitude, + }); + + CustomerAddressModel toCustomerAddressModel() { + return CustomerAddressModel( + uuid: uuid, + label: label, + fullAddress: fullAddress, + latitude: latitude, + longitude: longitude, + ); + } +} + +class SelectedAddress { + final String id; + final String? label; + final String fullAddress; + final double latitude; + final double longitude; + + SelectedAddress({ + String? id, + this.label, + required this.fullAddress, + required this.latitude, + required this.longitude, + }) : id = id ?? const Uuid().v7().toString(); + + SelectedAddress copyWith({required String label}) { + return SelectedAddress( + id: id, + label: label, + fullAddress: fullAddress, + latitude: latitude, + longitude: longitude, + ); + } +} diff --git a/Mobile Commerce/lib/features/account/domain/repositories/i_account_repository.dart b/Mobile Commerce/lib/features/account/domain/repositories/i_account_repository.dart new file mode 100644 index 0000000..2a109a3 --- /dev/null +++ b/Mobile Commerce/lib/features/account/domain/repositories/i_account_repository.dart @@ -0,0 +1,17 @@ +import 'package:niogu_ecommerce_v1/features/account/data/models/account_model.dart'; +import 'package:niogu_ecommerce_v1/features/account/domain/entities/account.dart'; + +abstract class IAccountRepository { + Future updateCustomer(CustomerInfoRequest request); + + Future updatePassword({ + required String oldPassword, + required newPassword, + }); + + Future> fetchCustomerAddresses(); + + Future saveCustomerAddress(CustomerAddress address); + + Future deleteCustomerAddress(String id); +} diff --git a/Mobile Commerce/lib/features/account/presentation/controllers/account_controller.dart b/Mobile Commerce/lib/features/account/presentation/controllers/account_controller.dart new file mode 100644 index 0000000..e11bf94 --- /dev/null +++ b/Mobile Commerce/lib/features/account/presentation/controllers/account_controller.dart @@ -0,0 +1,99 @@ +import 'dart:async'; + +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:niogu_ecommerce_v1/core/system/system_setting.dart'; +import 'package:niogu_ecommerce_v1/features/account/data/models/account_model.dart'; +import 'package:niogu_ecommerce_v1/features/account/domain/entities/account.dart'; +import 'package:niogu_ecommerce_v1/features/account/domain/repositories/i_account_repository.dart'; +import 'package:niogu_ecommerce_v1/features/account/presentation/providers/account_provider.dart'; + +class CustomerController extends AutoDisposeAsyncNotifier { + late final IAccountRepository _accountRepository; + + @override + FutureOr build() { + _accountRepository = ref.read(accountRepositoryProvider); + } + + Future updateCustomer(String name) async { + state = const AsyncValue.loading(); + + final result = await AsyncValue.guard(() async { + final data = await _accountRepository.updateCustomer( + CustomerInfoRequest(name: name), + ); + + if (data == null) return; + + await SystemSetting.setCustomerInfo( + name: data.name, + email: data.email, + phoneNumber: data.phoneNumber, + ); + }); + + state = result; + + if (result is AsyncError) throw result.error!; + } + + Future updatePassword({ + required String oldPassword, + required String newPassword, + }) async { + state = const AsyncValue.loading(); + + final result = await AsyncValue.guard(() async { + await _accountRepository.updatePassword( + oldPassword: oldPassword, + newPassword: newPassword, + ); + }); + + state = result; + + if (result is AsyncError) throw result.error!; + } + + Future saveCustomerAddress(CustomerAddress address) async { + state = const AsyncValue.loading(); + + final result = await AsyncValue.guard(() async { + await _accountRepository.saveCustomerAddress(address); + }); + + state = result; + + if (result is AsyncError) throw result.error!; + } + + Future deleteCustomerAddress(String id) async { + state = const AsyncValue.loading(); + + final result = await AsyncValue.guard(() async { + await _accountRepository.deleteCustomerAddress(id); + }); + + state = result; + + if (result is AsyncError) throw result.error!; + } +} + +class CustomerAddressController + extends AutoDisposeAsyncNotifier> { + @override + FutureOr> build() => _fetchCustomerAddresses(); + + Future> _fetchCustomerAddresses() async { + final accountRepository = ref.read(accountRepositoryProvider); + + return await accountRepository.fetchCustomerAddresses(); + } + + Future refresh() async { + ref.invalidateSelf(); + + await future; + } +} diff --git a/Mobile Commerce/lib/features/account/presentation/providers/account_provider.dart b/Mobile Commerce/lib/features/account/presentation/providers/account_provider.dart new file mode 100644 index 0000000..748ae60 --- /dev/null +++ b/Mobile Commerce/lib/features/account/presentation/providers/account_provider.dart @@ -0,0 +1,35 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:niogu_ecommerce_v1/core/providers/app_provider.dart'; +import 'package:niogu_ecommerce_v1/features/account/data/datasource/account_remote_datasource_impl.dart'; +import 'package:niogu_ecommerce_v1/features/account/data/repositories/account_repository_impl.dart'; +import 'package:niogu_ecommerce_v1/features/account/domain/entities/account.dart'; +import 'package:niogu_ecommerce_v1/features/account/domain/repositories/i_account_repository.dart'; +import 'package:niogu_ecommerce_v1/features/account/presentation/controllers/account_controller.dart'; + +final accountRemoteDatasourceProvider = + Provider.autoDispose((ref) { + final dio = ref.watch(dioProvider); + + return AccountRemoteDatasourceImpl(dio); + }); + +final accountRepositoryProvider = Provider.autoDispose(( + ref, +) { + final accountRemoteDatasource = ref.watch(accountRemoteDatasourceProvider); + + return AccountRepositoryImpl(accountRemoteDatasource); +}); + +final customerControllerProvider = + AsyncNotifierProvider.autoDispose( + CustomerController.new, + ); + +final customerAddressControllerProvider = + AsyncNotifierProvider.autoDispose< + CustomerAddressController, + List + >(CustomerAddressController.new); + +final selectedAddressProvider = StateProvider((ref) => null); diff --git a/Mobile Commerce/lib/features/account/presentation/screens/account_logout_screen.dart b/Mobile Commerce/lib/features/account/presentation/screens/account_logout_screen.dart new file mode 100644 index 0000000..c45b003 --- /dev/null +++ b/Mobile Commerce/lib/features/account/presentation/screens/account_logout_screen.dart @@ -0,0 +1,133 @@ +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'package:niogu_ecommerce_v1/core/constant/app_color.dart'; +import 'package:niogu_ecommerce_v1/core/constant/app_font_size.dart'; +import 'package:niogu_ecommerce_v1/core/router/app_route.dart'; +import 'package:sizer/sizer.dart'; + +class AccountLogoutScreen extends StatelessWidget { + const AccountLogoutScreen({super.key}); + + @override + Widget build(BuildContext context) { + return SafeArea( + top: false, + bottom: true, + right: false, + left: false, + child: Scaffold( + backgroundColor: Colors.white, + body: Padding( + padding: EdgeInsets.symmetric(horizontal: 10.w), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Container( + width: 50.w, + height: 50.w, + decoration: BoxDecoration( + color: AppColor.primaryColor.withOpacity(0.05), + shape: BoxShape.circle, + ), + child: Stack( + alignment: Alignment.center, + children: [ + Icon( + Icons.account_circle_outlined, + size: 30.w, + color: AppColor.primaryColor.withOpacity(0.2), + ), + Positioned( + bottom: 12.w, + right: 12.w, + child: CircleAvatar( + radius: 5.w, + backgroundColor: AppColor.primaryColor, + child: Icon( + Icons.login_rounded, + color: Colors.white, + size: 5.w, + ), + ), + ), + ], + ), + ), + + SizedBox(height: 5.h), + + Text( + "Yuk, Masuk ke Akunmu!", + textAlign: TextAlign.center, + style: TextStyle( + fontSize: AppFontSize.medium.sp, + fontWeight: FontWeight.bold, + color: Colors.black87, + ), + ), + SizedBox(height: 1.5.h), + Text( + "Masuk untuk melihat riwayat pesanan, mengelola alamat, dan menikmati kemudahan belanja", + textAlign: TextAlign.center, + style: TextStyle( + fontSize: (AppFontSize.small - 1).sp, + color: Colors.grey.shade600, + height: 1.5, + ), + ), + + SizedBox(height: 6.h), + + SizedBox( + width: double.infinity, + child: ElevatedButton( + onPressed: () => context.pushNamed(AppRoute.loginScreen), + style: ElevatedButton.styleFrom( + backgroundColor: AppColor.primaryColor, + padding: EdgeInsets.symmetric(vertical: 2.h), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(3.w), + ), + elevation: 0, + ), + child: Text( + "Masuk Akun", + style: TextStyle( + fontSize: AppFontSize.medium.sp, + fontWeight: FontWeight.bold, + color: Colors.white, + ), + ), + ), + ), + + SizedBox(height: 2.h), + + SizedBox( + width: double.infinity, + child: OutlinedButton( + onPressed: () => context.pushNamed(AppRoute.registerScreen), + style: OutlinedButton.styleFrom( + padding: EdgeInsets.symmetric(vertical: 2.h), + side: const BorderSide(color: AppColor.primaryColor), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(3.w), + ), + ), + child: Text( + "Daftar Sekarang", + style: TextStyle( + fontSize: AppFontSize.medium.sp, + fontWeight: FontWeight.bold, + color: AppColor.primaryColor, + ), + ), + ), + ), + ], + ), + ), + ), + ); + } +} diff --git a/Mobile Commerce/lib/features/account/presentation/screens/account_screen.dart b/Mobile Commerce/lib/features/account/presentation/screens/account_screen.dart new file mode 100644 index 0000000..2b26ece --- /dev/null +++ b/Mobile Commerce/lib/features/account/presentation/screens/account_screen.dart @@ -0,0 +1,341 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; +import 'package:niogu_ecommerce_v1/core/constant/app_font_size.dart'; +import 'package:niogu_ecommerce_v1/core/errors/exceptions.dart'; +import 'package:niogu_ecommerce_v1/core/providers/app_provider.dart'; +import 'package:niogu_ecommerce_v1/core/router/app_route.dart'; +import 'package:niogu_ecommerce_v1/core/system/system_secure.dart'; +import 'package:niogu_ecommerce_v1/core/system/system_setting.dart'; +import 'package:niogu_ecommerce_v1/core/utils/log_message.dart'; +import 'package:niogu_ecommerce_v1/core/widgets/custom_snackbar.dart'; +import 'package:niogu_ecommerce_v1/core/widgets/custom_text_form_field.dart'; +import 'package:niogu_ecommerce_v1/features/account/presentation/providers/account_provider.dart'; +import 'package:niogu_ecommerce_v1/features/account/presentation/screens/account_logout_screen.dart'; +import 'package:sizer/sizer.dart'; +import 'package:niogu_ecommerce_v1/core/constant/app_color.dart'; + +class AccountScreen extends ConsumerStatefulWidget { + const AccountScreen({super.key}); + + @override + ConsumerState createState() => _AccountScreenState(); +} + +class _AccountScreenState extends ConsumerState { + final _nameController = TextEditingController(); + + final _emailController = TextEditingController(); + + final _phoneNumberController = TextEditingController(); + + @override + void initState() { + // TODO: implement initState + super.initState(); + + WidgetsBinding.instance.addPostFrameCallback((_) { + _fetchCustomerInfo(); + }); + } + + @override + void dispose() { + // TODO: implement dispose + _nameController.dispose(); + _emailController.dispose(); + _phoneNumberController.dispose(); + super.dispose(); + } + + Future _setCustomerInfo() async { + final customerId = await SystemSetting.getCurrentCustomerId(); + + final customerName = await SystemSetting.getCurrentCustomerName(); + + final customerEmail = await SystemSetting.getCurrentCustomerEmail(); + + final customerPhone = await SystemSetting.getCurrentCustomerPhone(); + + ref.read(currentCustomerIdProvider.notifier).state = customerId; + + ref.read(currentCustomerNameProvider.notifier).state = customerName; + + ref.read(currentCustomerEmailProvider.notifier).state = customerEmail; + + ref.read(currentCustomerPhoneProvider.notifier).state = customerPhone; + + ref.read(currentStatusLoginProvider.notifier).state = true; + } + + Future _fetchCustomerInfo() async { + final name = ref.read(currentCustomerNameProvider); + + final email = ref.read(currentCustomerEmailProvider); + + final phone = ref.read(currentCustomerPhoneProvider); + + _nameController.text = name ?? ''; + + _emailController.text = email ?? ''; + + _phoneNumberController.text = phone ?? ''; + } + + Future _updateCustomer() async { + try { + final name = _nameController.text.trim(); + + await ref.read(customerControllerProvider.notifier).updateCustomer(name); + + if (!mounted) return; + + await _setCustomerInfo(); + + await _fetchCustomerInfo(); + + CustomSnackbar.showSuccess(context, "Informasi akun berhasil diperbarui"); + } on ServerException catch (e, st) { + LogMessage.log.e(e.toString(), error: e, stackTrace: st); + CustomSnackbar.showError(context, "Terjadi kesalahan"); + } + } + + Future _logout() async { + await SystemSecure.deleteAccessToken(); + + await SystemSetting.clear(); + + ref.read(currentCustomerIdProvider.notifier).state = null; + + ref.read(currentCustomerNameProvider.notifier).state = null; + + ref.read(currentCustomerEmailProvider.notifier).state = null; + + ref.read(currentCustomerPhoneProvider.notifier).state = null; + + ref.read(currentStatusLoginProvider.notifier).state = false; + + CustomSnackbar.showSuccess(context, "Berhasil keluar"); + } + + @override + Widget build(BuildContext context) { + return LayoutBuilder( + builder: (context, constraints) { + final isLoggedIn = ref.watch(currentStatusLoginProvider); + + if (!isLoggedIn) return const AccountLogoutScreen(); + + final updateState = ref.watch(customerControllerProvider); + + return SafeArea( + top: false, + bottom: true, + right: false, + left: false, + child: Scaffold( + backgroundColor: const Color(0xFFF8F9FA), + appBar: AppBar( + backgroundColor: Colors.white, + elevation: 0, + centerTitle: true, + title: Text( + "Informasi Akun", + style: TextStyle( + color: Colors.black, + fontSize: AppFontSize.medium.sp, + fontWeight: FontWeight.bold, + ), + ), + ), + body: RefreshIndicator( + onRefresh: _fetchCustomerInfo, + color: AppColor.primaryColor, + backgroundColor: Colors.white, + child: SingleChildScrollView( + physics: const AlwaysScrollableScrollPhysics(), + padding: EdgeInsets.all(5.w), + child: Column( + children: [ + Center( + child: CircleAvatar( + radius: 12.w, + backgroundColor: Colors.grey.shade200, + child: Icon( + Icons.person, + size: 15.w, + color: Colors.grey.shade400, + ), + ), + ), + SizedBox(height: 4.h), + + CustomTextFormField( + label: "Nama", + hint: "John Doe", + controller: _nameController, + prefixIcon: Icons.person_outline, + ), + + SizedBox(height: 2.h), + + CustomTextFormField( + label: "Email", + hint: ".@gmail.com", + controller: _emailController, + keyboardType: TextInputType.emailAddress, + prefixIcon: Icons.email_outlined, + readOnly: true, + ), + + SizedBox(height: 2.h), + + CustomTextFormField( + label: "No. Whatsapp", + hint: "081234567890", + controller: _phoneNumberController, + prefixIcon: Icons.phone_android, + readOnly: true, + ), + + SizedBox(height: 2.h), + + SizedBox( + width: double.infinity, + child: ElevatedButton( + onPressed: updateState.isLoading + ? null + : _updateCustomer, + style: ElevatedButton.styleFrom( + backgroundColor: AppColor.primaryColor, + padding: EdgeInsets.symmetric(vertical: 1.8.h), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(2.5.w), + ), + elevation: 0, + disabledBackgroundColor: Colors.grey.shade300, + ), + child: Text( + "Simpan Perubahan", + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: AppFontSize.medium.sp, + color: Colors.white, + ), + ), + ), + ), + + SizedBox(height: 4.h), + + _buildAccountMenu( + icon: Icons.lock_outline, + title: "Ubah Password", + subtitle: "Perbarui password untuk keamanan", + backgroundColor: Colors.blue.withOpacity(0.1), + iconColor: Colors.blue, + titleColor: Colors.black87, + onTap: () { + context.pushNamed(AppRoute.changePasswordScreen); + }, + ), + + SizedBox(height: 1.5.h), + + _buildAccountMenu( + icon: Icons.location_on_outlined, + title: "Alamat Saya", + subtitle: "Kelola alamat pengiriman", + backgroundColor: AppColor.primaryColor.withOpacity(0.1), + iconColor: AppColor.primaryColor, + titleColor: Colors.black87, + onTap: () => context.pushNamed(AppRoute.addressScreen), + ), + + SizedBox(height: 1.5.h), + + _buildAccountMenu( + icon: Icons.logout, + title: "Keluar", + subtitle: "Akhiri sesi akun anda", + backgroundColor: Colors.red.shade50, + iconColor: Colors.red, + titleColor: Colors.red, + onTap: _logout, + ), + + SizedBox(height: 15.h), + ], + ), + ), + ), + ), + ); + }, + ); + } + + Widget _buildAccountMenu({ + required IconData icon, + required String title, + required String subtitle, + required Color backgroundColor, + required Color iconColor, + required Color titleColor, + required VoidCallback onTap, + }) { + return InkWell( + onTap: onTap, + borderRadius: BorderRadius.circular(2.5.w), + child: Container( + padding: EdgeInsets.all(4.w), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(2.5.w), + border: Border.all(color: Colors.grey.shade100), + ), + child: Row( + children: [ + Container( + padding: EdgeInsets.all(2.w), + decoration: BoxDecoration( + color: backgroundColor, + borderRadius: BorderRadius.circular(2.w), + ), + child: Icon(icon, color: iconColor, size: 5.w), + ), + SizedBox(width: 4.w), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: AppFontSize.small.sp, + color: titleColor, + ), + ), + Text( + subtitle, + style: TextStyle( + fontSize: (AppFontSize.small - 1.25).sp, + color: Colors.grey.shade500, + ), + ), + ], + ), + ), + Icon( + Icons.arrow_forward_ios, + size: 3.5.w, + color: Colors.grey.shade400, + ), + ], + ), + ), + ); + } +} diff --git a/Mobile Commerce/lib/features/account/presentation/screens/address_screen.dart b/Mobile Commerce/lib/features/account/presentation/screens/address_screen.dart new file mode 100644 index 0000000..f6310c5 --- /dev/null +++ b/Mobile Commerce/lib/features/account/presentation/screens/address_screen.dart @@ -0,0 +1,339 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; +import 'package:niogu_ecommerce_v1/core/constant/app_font_size.dart'; +import 'package:niogu_ecommerce_v1/core/router/app_route.dart'; +import 'package:niogu_ecommerce_v1/core/widgets/custom_empty_screen.dart'; +import 'package:niogu_ecommerce_v1/features/account/domain/entities/account.dart'; +import 'package:niogu_ecommerce_v1/features/account/presentation/providers/account_provider.dart'; +import 'package:shimmer/shimmer.dart'; +import 'package:sizer/sizer.dart'; +import 'package:niogu_ecommerce_v1/core/constant/app_color.dart'; + +class AddressScreen extends ConsumerWidget { + const AddressScreen({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + return LayoutBuilder( + builder: (context, constraints) { + final customerAddressState = ref.watch( + customerAddressControllerProvider, + ); + + return SafeArea( + top: false, + bottom: true, + right: false, + left: false, + child: Scaffold( + backgroundColor: const Color(0xFFF8F9FA), + appBar: AppBar( + backgroundColor: Colors.white, + elevation: 0.5, + leading: IconButton( + icon: Icon( + Icons.arrow_back, + size: 7.w, + color: AppColor.primaryColor, + ), + onPressed: () => context.pop(), + ), + title: Text( + "Alamat Saya", + style: TextStyle( + color: Colors.black, + fontSize: AppFontSize.medium.sp, + fontWeight: FontWeight.bold, + ), + ), + ), + body: Column( + children: [ + Expanded( + child: RefreshIndicator( + onRefresh: () async { + await ref + .read(customerAddressControllerProvider.notifier) + .refresh(); + }, + color: AppColor.primaryColor, + backgroundColor: Colors.white, + child: SingleChildScrollView( + physics: const AlwaysScrollableScrollPhysics(), + padding: EdgeInsets.symmetric(vertical: 2.h), + child: Column( + children: [ + Container( + margin: EdgeInsets.symmetric(horizontal: 4.w), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(2.5.w), + border: Border.all(color: Colors.grey.shade200), + ), + child: customerAddressState.when( + data: (addresses) { + if (addresses.isEmpty) { + return CustomEmptyScreen( + icon: Icons.search_off_outlined, + title: "Belum Ada Alamat", + subtitle: "Tambahkan alamat pertamamu", + height: 40.h, + ); + } + return ListView.separated( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: addresses.length, + separatorBuilder: (context, index) => Divider( + height: 0, + thickness: 1, + color: Colors.grey.shade100, + indent: 4.w, + endIndent: 4.w, + ), + itemBuilder: (context, index) { + final address = addresses[index]; + return _buildAddressItem( + context, + ref, + address, + ); + }, + ); + }, + error: (error, stackTrace) => CustomEmptyScreen( + icon: Icons.cloud_off_outlined, + title: "Terjadi Kesalahan Koneksi", + subtitle: "Tarik ke bawah untuk mencoba lagi", + height: 40.h, + ), + loading: () => _buildAddressLoading(), + ), + ), + + if (!customerAddressState.isLoading) + _buildAddAddressButton(context), + ], + ), + ), + ), + ), + ], + ), + ), + ); + }, + ); + } + + Widget _buildAddressItem( + BuildContext context, + WidgetRef ref, + CustomerAddress address, + ) { + return InkWell( + onTap: () {}, + child: Padding( + padding: EdgeInsets.all(4.w), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + Text( + address.label, + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: AppFontSize.small.sp, + color: Colors.black87, + ), + ), + /** + if (item['is_utama']) ...[ + SizedBox(width: 2.w), + Container( + padding: EdgeInsets.symmetric( + horizontal: 2.w, + vertical: 0.4.h, + ), + decoration: BoxDecoration( + color: AppColor.primaryColor.withOpacity(0.1), + borderRadius: BorderRadius.circular(1.w), + border: Border.all( + color: AppColor.primaryColor.withOpacity(0.5), + ), + ), + child: Text( + "Utama", + style: TextStyle( + color: AppColor.primaryColor, + fontSize: AppFontSize.small.sp, + fontWeight: FontWeight.bold, + ), + ), + ), + ], + */ + ], + ), + TextButton( + onPressed: () { + ref + .read(selectedAddressProvider.notifier) + .state = SelectedAddress( + id: address.uuid, + fullAddress: address.fullAddress, + latitude: address.latitude, + longitude: address.longitude, + ); + context.pushNamed( + AppRoute.saveAddressScreen, + extra: address.label, + ); + }, + child: Text( + "Ubah", + style: TextStyle( + color: AppColor.primaryColor, + fontSize: AppFontSize.small.sp, + fontWeight: FontWeight.w600, + ), + ), + ), + ], + ), + /** + SizedBox(height: 1.5.h), + Text( + "${item['penerima']} | ${item['telepon']}", + style: TextStyle( + fontSize: AppFontSize.small.sp, + color: Colors.black87, + fontWeight: FontWeight.w500, + ), + ), + */ + Text( + address.fullAddress, + style: TextStyle( + fontSize: AppFontSize.small.sp, + color: Colors.grey.shade600, + height: 1.4, + ), + maxLines: 3, + overflow: TextOverflow.ellipsis, + ), + ], + ), + ), + ); + } + + Widget _buildAddAddressButton(BuildContext context) { + return Container( + padding: EdgeInsets.all(4.w), + child: SafeArea( + child: SizedBox( + width: double.infinity, + child: OutlinedButton.icon( + onPressed: () => context.pushNamed(AppRoute.mapAddressScreen), + icon: Icon( + Icons.add_circle_outline, + size: 5.w, + color: AppColor.primaryColor, + ), + label: Text( + "Tambah Alamat Baru", + style: TextStyle( + color: AppColor.primaryColor, + fontWeight: FontWeight.bold, + fontSize: AppFontSize.medium.sp, + ), + ), + style: OutlinedButton.styleFrom( + padding: EdgeInsets.symmetric(vertical: 1.8.h), + side: const BorderSide(color: AppColor.primaryColor), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(2.5.w), + ), + ), + ), + ), + ), + ); + } + + Widget _buildAddressLoading() { + return Shimmer.fromColors( + baseColor: Colors.grey.shade300, + highlightColor: Colors.grey.shade100, + child: ListView.separated( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: 5, + separatorBuilder: (context, index) => Divider( + height: 0, + thickness: 1, + color: Colors.grey.shade100, + indent: 4.w, + endIndent: 4.w, + ), + itemBuilder: (context, index) { + return Padding( + padding: EdgeInsets.all(4.w), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Skeleton Header (Label & Tombol Ubah) + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Container( + width: 20.w, + height: 2.h, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(1.w), + ), + ), + Container( + width: 12.w, + height: 2.h, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(1.w), + ), + ), + ], + ), + SizedBox(height: 2.h), + // Skeleton Baris Alamat 1 + Container( + width: double.infinity, + height: 1.5.h, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(1.w), + ), + ), + SizedBox(height: 1.h), + // Skeleton Baris Alamat 2 + Container( + width: 70.w, + height: 1.5.h, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(1.w), + ), + ), + ], + ), + ); + }, + ), + ); + } +} diff --git a/Mobile Commerce/lib/features/account/presentation/screens/change_password_screen.dart b/Mobile Commerce/lib/features/account/presentation/screens/change_password_screen.dart new file mode 100644 index 0000000..65ec507 --- /dev/null +++ b/Mobile Commerce/lib/features/account/presentation/screens/change_password_screen.dart @@ -0,0 +1,265 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; +import 'package:niogu_ecommerce_v1/core/constant/app_color.dart'; +import 'package:niogu_ecommerce_v1/core/constant/app_font_size.dart'; +import 'package:niogu_ecommerce_v1/core/errors/exceptions.dart'; +import 'package:niogu_ecommerce_v1/core/utils/log_message.dart'; +import 'package:niogu_ecommerce_v1/core/widgets/custom_snackbar.dart'; +import 'package:niogu_ecommerce_v1/core/widgets/custom_text_form_field.dart'; +import 'package:niogu_ecommerce_v1/features/account/presentation/providers/account_provider.dart'; +import 'package:sizer/sizer.dart'; + +class ChangePasswordScreen extends ConsumerStatefulWidget { + const ChangePasswordScreen({super.key}); + + @override + ConsumerState createState() => + _ChangePasswordScreenState(); +} + +class _ChangePasswordScreenState extends ConsumerState { + final _formKey = GlobalKey(); + + final _oldPasswordController = TextEditingController(); + + final _newPasswordController = TextEditingController(); + + final _confirmPasswordController = TextEditingController(); + + bool _obsecureOldPassword = true; + + bool _obsecureNewPassword = true; + + bool _obsecureConfirmPassword = true; + + @override + void dispose() { + _oldPasswordController.dispose(); + _newPasswordController.dispose(); + _confirmPasswordController.dispose(); + super.dispose(); + } + + String? _validation({ + String? value, + required String message, + bool isNewPassword = false, + bool isPasswordConfirm = false, + }) { + if (value == null || value.isEmpty) { + return message; + } + + if (isNewPassword) { + if (value.length < 8) { + return "Password minimal harus 8 karakter"; + } + + final passwordRegExp = RegExp( + r'^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&#])[A-Za-z\d@$!%*?&#]{8,}$', + ); + + if (!passwordRegExp.hasMatch(value)) { + return "Gunakan huruf besar, kecil, angka, dan simbol"; + } + } + + if (isPasswordConfirm && value != _newPasswordController.text.trim()) { + return "Konfirmasi password tidak sesuai"; + } + + return null; + } + + Future _updatePassword() async { + if (!_formKey.currentState!.validate()) return; + + try { + await ref + .read(customerControllerProvider.notifier) + .updatePassword( + oldPassword: _oldPasswordController.text.trim(), + newPassword: _newPasswordController.text.trim(), + ); + + if (!mounted) return; + + CustomSnackbar.showSuccess(context, "Password berhasil diubah"); + context.pop(); + } on ServerException catch (e, st) { + LogMessage.log.e(e.toString(), error: e, stackTrace: st); + CustomSnackbar.showError(context, e.message); + } + } + + @override + Widget build(BuildContext context) { + return LayoutBuilder( + builder: (context, constraints) { + final customerState = ref.watch(customerControllerProvider); + return SafeArea( + top: false, + bottom: true, + right: false, + left: false, + child: Scaffold( + backgroundColor: Colors.white, + appBar: AppBar( + backgroundColor: Colors.white, + elevation: 0.5, + leading: IconButton( + icon: Icon( + Icons.arrow_back, + color: AppColor.primaryColor, + size: 7.w, + ), + onPressed: () => context.pop(), + ), + title: Text( + "Ubah Password", + style: TextStyle( + color: Colors.black, + fontSize: AppFontSize.medium.sp, + fontWeight: FontWeight.bold, + ), + ), + centerTitle: true, + ), + body: SingleChildScrollView( + padding: EdgeInsets.all(5.w), + child: Form( + key: _formKey, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildInfoSection(), + SizedBox(height: 4.h), + + CustomTextFormField( + label: "Password Lama", + hint: "Masukkan password lama", + controller: _oldPasswordController, + isObscure: _obsecureOldPassword, + prefixIcon: Icons.lock_outline, + suffixIcon: _obsecureOldPassword + ? Icons.visibility + : Icons.visibility_off, + suffixPressed: () { + setState(() { + _obsecureOldPassword = !_obsecureOldPassword; + }); + }, + validator: (value) => _validation( + value: value, + message: "Password lama belum diisi", + ), + ), + + SizedBox(height: 3.h), + + CustomTextFormField( + label: "Password Baru", + hint: "Masukkan password baru", + controller: _newPasswordController, + isObscure: _obsecureNewPassword, + prefixIcon: Icons.lock_reset_outlined, + suffixIcon: _obsecureOldPassword + ? Icons.visibility + : Icons.visibility_off, + suffixPressed: () { + setState(() { + _obsecureNewPassword = !_obsecureNewPassword; + }); + }, + validator: (value) => _validation( + value: value, + message: "Password baru belum diisi", + isNewPassword: true, + ), + ), + + SizedBox(height: 3.h), + + CustomTextFormField( + label: "Konfirmasi Password Baru", + hint: "Konfirmasi password baru", + controller: _confirmPasswordController, + isObscure: _obsecureConfirmPassword, + prefixIcon: Icons.check_circle_outline, + suffixIcon: _obsecureOldPassword + ? Icons.visibility + : Icons.visibility_off, + suffixPressed: () { + setState(() { + _obsecureConfirmPassword = !_obsecureConfirmPassword; + }); + }, + validator: (value) => _validation( + value: value, + message: "Konfirmasi password belum diisi", + isPasswordConfirm: true, + ), + ), + + SizedBox(height: 6.h), + + SizedBox( + width: double.infinity, + child: ElevatedButton( + onPressed: customerState.isLoading + ? null + : _updatePassword, + style: ElevatedButton.styleFrom( + backgroundColor: AppColor.primaryColor, + padding: EdgeInsets.symmetric(vertical: 2.h), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(3.w), + ), + elevation: 0, + ), + child: Text( + "Simpan Perubahan", + style: TextStyle( + fontSize: AppFontSize.medium.sp, + fontWeight: FontWeight.bold, + color: Colors.white, + ), + ), + ), + ), + ], + ), + ), + ), + ), + ); + }, + ); + } + + Widget _buildInfoSection() { + return Container( + padding: EdgeInsets.all(4.w), + decoration: BoxDecoration( + color: AppColor.primaryColor.withOpacity(0.1), + borderRadius: BorderRadius.circular(2.w), + ), + child: Row( + children: [ + Icon(Icons.info_outline, color: AppColor.primaryColor, size: 5.w), + SizedBox(width: 3.w), + Expanded( + child: Text( + "Gunakan minimal 8 karakter dengan kombinasi huruf dan angka untuk password yang kuat.", + style: TextStyle( + fontSize: (AppFontSize.small - 1.25).sp, + color: AppColor.primaryColor, + ), + ), + ), + ], + ), + ); + } +} diff --git a/Mobile Commerce/lib/features/account/presentation/screens/map_address_screen.dart b/Mobile Commerce/lib/features/account/presentation/screens/map_address_screen.dart new file mode 100644 index 0000000..fe34894 --- /dev/null +++ b/Mobile Commerce/lib/features/account/presentation/screens/map_address_screen.dart @@ -0,0 +1,383 @@ +import 'dart:async'; +import 'dart:convert'; +import 'package:flutter/material.dart'; +import 'package:flutter_map/flutter_map.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:geocoding/geocoding.dart'; +import 'package:geolocator/geolocator.dart'; +import 'package:go_router/go_router.dart'; +import 'package:latlong2/latlong.dart'; +import 'package:http/http.dart' as http; +import 'package:niogu_ecommerce_v1/core/constant/app_color.dart'; +import 'package:niogu_ecommerce_v1/core/constant/app_font_size.dart'; +import 'package:niogu_ecommerce_v1/core/router/app_route.dart'; +import 'package:niogu_ecommerce_v1/core/utils/log_message.dart'; +import 'package:niogu_ecommerce_v1/core/widgets/triangle_painter.dart'; +import 'package:niogu_ecommerce_v1/features/account/domain/entities/account.dart'; +import 'package:niogu_ecommerce_v1/features/account/presentation/providers/account_provider.dart'; +import 'package:sizer/sizer.dart'; + +class MapAddressScreen extends ConsumerStatefulWidget { + const MapAddressScreen({super.key}); + + @override + ConsumerState createState() => _MapAddressScreenState(); +} + +class _MapAddressScreenState extends ConsumerState { + final MapController _mapController = MapController(); + + LatLng _selectedLocation = const LatLng(-6.2000, 106.8166); + + String _fullAddress = ""; + + bool _isSearching = false; + + bool _isLoadingMap = false; + + List _suggestions = []; + + Timer? _debounce; + + @override + void initState() { + // TODO: implement initState + super.initState(); + + WidgetsBinding.instance.addPostFrameCallback((_) { + _fetchSelectedAddress(); + }); + } + + void _fetchSelectedAddress() async { + final selectedAddress = ref.read(selectedAddressProvider); + + if (selectedAddress != null) { + await _updateLocation( + LatLng(selectedAddress.latitude, selectedAddress.longitude), + ); + } + } + + void _onSearchChanged(String query) { + if (_debounce?.isActive ?? false) _debounce?.cancel(); + + if (query.isEmpty) { + setState(() => _suggestions = []); + return; + } + + _debounce = Timer(const Duration(milliseconds: 800), () async { + setState(() => _isSearching = true); + + try { + final url = Uri.parse( + 'https://nominatim.openstreetmap.org/search?q=$query&format=json&limit=5&countrycodes=id', + ); + + final response = await http.get( + url, + headers: { + 'User-Agent': 'NioguEcommerceApp/1.0 (niaganusantara@gmail.com)', + 'Accept-Language': 'id', + }, + ); + + if (response.statusCode == 200) { + setState(() { + _suggestions = json.decode(response.body); + _isSearching = false; + }); + } + } catch (e, st) { + LogMessage.log.e(e.toString(), error: e, stackTrace: st); + setState(() => _isSearching = false); + } + }); + } + + Future _getCurrentPosition() async { + setState(() => _isLoadingMap = true); + + LocationPermission permission = await Geolocator.checkPermission(); + if (permission == LocationPermission.denied) { + permission = await Geolocator.requestPermission(); + } + + Position position = await Geolocator.getCurrentPosition(); + final newLatLng = LatLng(position.latitude, position.longitude); + + await _updateLocation(newLatLng); + } + + Future _updateLocation(LatLng point) async { + setState(() { + _selectedLocation = point; + _isLoadingMap = true; + }); + + _mapController.move(point, 16.0); + + try { + List placemarks = await placemarkFromCoordinates( + point.latitude, + point.longitude, + ); + if (placemarks.isNotEmpty) { + final place = placemarks[0]; + setState(() { + _fullAddress = + "${place.street}, ${place.subLocality}, ${place.locality}, ${place.subAdministrativeArea}"; + _isLoadingMap = false; + _suggestions = []; + }); + } + } catch (e) { + setState(() => _isLoadingMap = false); + } + } + + void _selectedAddress() { + ref.read(selectedAddressProvider.notifier).state = SelectedAddress( + fullAddress: _fullAddress, + latitude: _selectedLocation.latitude, + longitude: _selectedLocation.longitude, + ); + } + + @override + Widget build(BuildContext context) { + return LayoutBuilder( + builder: (context, constraints) { + return SafeArea( + top: false, + bottom: true, + right: false, + left: false, + child: Scaffold( + body: Stack( + children: [ + FlutterMap( + mapController: _mapController, + options: MapOptions( + initialCenter: _selectedLocation, + initialZoom: 16.0, + onTap: (_, point) => _updateLocation(point), + ), + children: [ + TileLayer( + urlTemplate: + 'https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png', + subdomains: const ['a', 'b', 'c', 'd'], + ), + MarkerLayer( + markers: [ + Marker( + point: _selectedLocation, + width: 80.w, + height: 15.h, + alignment: Alignment.topCenter, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + if (_fullAddress.isNotEmpty) ...[ + _buildAddressBubble(), + CustomPaint( + size: Size(5.w, 2.5.w), + painter: TrianglePainter(Colors.white), + ), + Icon( + Icons.location_on, + color: AppColor.primaryColor, + size: 10.w, + ), + ], + ], + ), + ), + ], + ), + ], + ), + + Positioned( + top: 6.h, + left: 5.w, + right: 5.w, + child: Column( + children: [ + _buildSearchBar(), + if (_suggestions.isNotEmpty || _isSearching) + _buildSuggestionList(), + ], + ), + ), + + Positioned( + bottom: 4.h, + left: 5.w, + right: 5.w, + child: Column( + children: [ + Align( + alignment: Alignment.centerRight, + child: FloatingActionButton( + mini: true, + backgroundColor: Colors.white, + onPressed: _getCurrentPosition, + child: Icon( + Icons.my_location, + color: AppColor.primaryColor, + ), + ), + ), + SizedBox(height: 2.h), + ElevatedButton( + onPressed: _fullAddress.isEmpty + ? null + : () { + _selectedAddress(); + context.pushReplacementNamed( + AppRoute.saveAddressScreen, + ); + }, + style: ElevatedButton.styleFrom( + minimumSize: Size(double.infinity, 6.h), + backgroundColor: AppColor.primaryColor, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(2.5.w), + ), + ), + child: Text( + "Pilih Lokasi Ini", + style: TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + fontSize: AppFontSize.medium.sp, + ), + ), + ), + ], + ), + ), + + if (_isLoadingMap) + Center( + child: CircularProgressIndicator( + color: AppColor.primaryColor, + ), + ), + ], + ), + ), + ); + }, + ); + } + + Widget _buildSearchBar() { + return Row( + children: [ + GestureDetector( + onTap: () => context.pop(), + child: Padding( + padding: EdgeInsets.all(2.w), + child: CircleAvatar( + maxRadius: 5.w, + minRadius: 5.w, + backgroundColor: Colors.white.withOpacity(0.9), + child: Center( + child: Icon(Icons.arrow_back, color: Colors.black, size: 6.w), + ), + ), + ), + ), + Expanded( + child: Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(2.5.w), + boxShadow: [ + BoxShadow( + color: Colors.black12, + blurRadius: 10, + offset: const Offset(0, 5), + ), + ], + ), + child: TextField( + onChanged: _onSearchChanged, + style: TextStyle(fontSize: AppFontSize.small.sp), + decoration: InputDecoration( + hintText: "Cari Lokasi...", + hintStyle: TextStyle(fontSize: AppFontSize.small.sp), + prefixIcon: Icon(Icons.search, size: 5.w), + suffixIcon: _isSearching + ? Transform.scale( + scale: 0.5, + child: CircularProgressIndicator( + color: AppColor.primaryColor, + ), + ) + : null, + border: InputBorder.none, + contentPadding: EdgeInsets.symmetric(vertical: 2.h), + ), + ), + ), + ), + ], + ); + } + + Widget _buildSuggestionList() { + return Container( + margin: EdgeInsets.only(top: 1.h), + constraints: BoxConstraints(maxHeight: 30.h), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(2.5.w), + ), + child: ListView.builder( + shrinkWrap: true, + padding: EdgeInsets.zero, + itemCount: _suggestions.length, + itemBuilder: (context, index) { + final item = _suggestions[index]; + return ListTile( + leading: Icon(Icons.location_on_outlined, size: 5.w), + title: Text( + item['display_name'], + style: TextStyle(fontSize: (AppFontSize.small - 1.25).sp), + ), + onTap: () { + final lat = double.parse(item['lat']); + final lon = double.parse(item['lon']); + _updateLocation(LatLng(lat, lon)); + }, + ); + }, + ), + ); + } + + Widget _buildAddressBubble() { + return Container( + padding: EdgeInsets.all(2.w), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(2.w), + boxShadow: const [BoxShadow(color: Colors.black12, blurRadius: 4)], + ), + child: Text( + _fullAddress, + maxLines: 2, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: (AppFontSize.small - 1.25).sp, + fontWeight: FontWeight.w500, + ), + ), + ); + } +} diff --git a/Mobile Commerce/lib/features/account/presentation/screens/save_address_screen.dart b/Mobile Commerce/lib/features/account/presentation/screens/save_address_screen.dart new file mode 100644 index 0000000..b3840d5 --- /dev/null +++ b/Mobile Commerce/lib/features/account/presentation/screens/save_address_screen.dart @@ -0,0 +1,439 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; +import 'package:niogu_ecommerce_v1/core/constant/app_color.dart'; +import 'package:niogu_ecommerce_v1/core/constant/app_font_size.dart'; +import 'package:niogu_ecommerce_v1/core/errors/exceptions.dart'; +import 'package:niogu_ecommerce_v1/core/router/app_route.dart'; +import 'package:niogu_ecommerce_v1/core/utils/log_message.dart'; +import 'package:niogu_ecommerce_v1/core/widgets/custom_snackbar.dart'; +import 'package:niogu_ecommerce_v1/features/account/domain/entities/account.dart'; +import 'package:niogu_ecommerce_v1/features/account/presentation/providers/account_provider.dart'; +import 'package:sizer/sizer.dart'; + +class SaveAddressScreen extends ConsumerStatefulWidget { + final String? label; + const SaveAddressScreen({super.key, this.label}); + + @override + ConsumerState createState() => _SaveAddressScreenState(); +} + +class _SaveAddressScreenState extends ConsumerState { + late String _selectedLabel; + + @override + void initState() { + super.initState(); + + _selectedLabel = widget.label ?? 'Rumah'; + } + + @override + void dispose() { + super.dispose(); + } + + Future _deleteCustomerAddress() async { + try { + final selectedAddress = ref.read(selectedAddressProvider); + + final id = selectedAddress?.id; + + if (id == null) return; + + await ref + .read(customerControllerProvider.notifier) + .deleteCustomerAddress(id); + + if (!context.mounted) return; + + CustomSnackbar.showSuccess(context, "Alamat berhasil dihapus"); + + await ref.read(customerAddressControllerProvider.notifier).refresh(); + context.pop(); + context.pop(); + } on ServerException catch (e, st) { + LogMessage.log.e(e.toString(), error: e, stackTrace: st); + CustomSnackbar.showError(context, "Terjadi kesalahan koneksi"); + } + } + + void _showDeleteConfirmation() { + final bool isTablet = 100.w >= 600; + final customerControllerState = ref.watch(customerControllerProvider); + showDialog( + context: context, + builder: (BuildContext context) { + return Dialog( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(4.5.w), + ), + elevation: 0, + backgroundColor: Colors.transparent, + child: Container( + padding: EdgeInsets.all(6.w), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(4.5.w), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + padding: EdgeInsets.all(4.w), + decoration: BoxDecoration( + color: Colors.red[50], + shape: BoxShape.circle, + ), + child: Icon( + Icons.warning_amber_rounded, + color: Colors.red, + size: 10.w, + ), + ), + SizedBox(height: 2.h), + + Text( + "Hapus Alamat?", + style: TextStyle( + fontSize: AppFontSize.medium.sp, + fontWeight: FontWeight.bold, + color: Colors.black87, + ), + ), + SizedBox(height: 1.h), + Text( + "Tindakan ini tidak dapat dibatalkan.", + textAlign: TextAlign.center, + style: TextStyle( + fontSize: isTablet + ? (AppFontSize.medium - 1.25).sp + : (AppFontSize.small - 1.25).sp, + color: Colors.grey[600], + height: 1.5, + ), + ), + SizedBox(height: 3.h), + + Row( + children: [ + Expanded( + child: ElevatedButton( + onPressed: () => Navigator.pop(context), + style: ElevatedButton.styleFrom( + backgroundColor: Colors.grey.shade300, + padding: EdgeInsets.symmetric(vertical: 1.5.h), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(2.5.w), + ), + elevation: 0, + ), + child: Text( + "Batal", + style: TextStyle( + color: Colors.grey[800], + fontWeight: FontWeight.bold, + fontSize: AppFontSize.medium.sp, + ), + ), + ), + ), + SizedBox(width: 3.w), + Expanded( + child: ElevatedButton( + onPressed: customerControllerState.isLoading + ? null + : _deleteCustomerAddress, + style: ElevatedButton.styleFrom( + padding: EdgeInsets.symmetric(vertical: 1.5.h), + backgroundColor: Colors.red, + elevation: 0, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(2.5.w), + ), + disabledBackgroundColor: Colors.grey.shade300, + ), + child: Text( + "Ya, Hapus", + style: TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + fontSize: AppFontSize.medium.sp, + ), + ), + ), + ), + ], + ), + ], + ), + ), + ); + }, + ); + } + + Future _saveCustomerAddress() async { + final selectedAddress = ref.read(selectedAddressProvider); + + if (selectedAddress == null) { + CustomSnackbar.showError(context, "Tentukan lokasi alamat"); + return; + } + + final address = CustomerAddress( + uuid: selectedAddress.id, + label: _selectedLabel, + fullAddress: selectedAddress.fullAddress, + latitude: selectedAddress.latitude, + longitude: selectedAddress.longitude, + ); + try { + await ref + .read(customerControllerProvider.notifier) + .saveCustomerAddress(address); + + if (!context.mounted) return; + + CustomSnackbar.showSuccess(context, "Alamat berhasil disimpan"); + + await ref.read(customerAddressControllerProvider.notifier).refresh(); + + ref.read(selectedAddressProvider.notifier).state = selectedAddress + .copyWith(label: _selectedLabel); + + context.pop(); + } on ServerException catch (e, st) { + LogMessage.log.e(e.toString(), error: e, stackTrace: st); + CustomSnackbar.showError(context, "Terjadi kesalahan koneksi"); + } + } + + @override + Widget build(BuildContext context) { + return LayoutBuilder( + builder: (context, constraints) { + final selectedAddressState = ref.watch(selectedAddressProvider); + + final customerState = ref.watch(customerControllerProvider); + return SafeArea( + top: false, + bottom: true, + right: false, + left: false, + child: Scaffold( + backgroundColor: Colors.white, + appBar: AppBar( + backgroundColor: Colors.white, + elevation: 0, + centerTitle: true, + leading: IconButton( + icon: Icon( + Icons.arrow_back, + size: 7.w, + color: AppColor.primaryColor, + ), + onPressed: () => context.pop(), + ), + title: Text( + "Simpan Alamat", + style: TextStyle( + color: Colors.black, + fontSize: AppFontSize.medium.sp, + fontWeight: FontWeight.bold, + ), + ), + ), + body: Padding( + padding: EdgeInsets.all(5.w), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Tandai Sebagai :", + style: TextStyle( + fontSize: AppFontSize.small.sp, + fontWeight: FontWeight.bold, + color: Colors.grey.shade700, + ), + ), + SizedBox(height: 2.h), + Row( + children: [ + _buildLabelOption("Rumah", Icons.home_outlined), + SizedBox(width: 4.w), + _buildLabelOption("Kantor", Icons.work_outline), + ], + ), + SizedBox(height: 4.h), + + Text( + "Alamat Lengkap", + style: TextStyle( + fontSize: AppFontSize.small.sp, + fontWeight: FontWeight.bold, + color: Colors.grey.shade700, + ), + ), + SizedBox(height: 1.5.h), + + Material( + color: Colors.transparent, + type: MaterialType.transparency, + child: InkWell( + onTap: () { + context.pushNamed(AppRoute.mapAddressScreen); + }, + child: Container( + width: double.infinity, + padding: EdgeInsets.symmetric( + horizontal: 4.w, + vertical: 2.h, + ), + decoration: BoxDecoration( + border: Border.all(color: Colors.grey.shade300), + borderRadius: BorderRadius.circular(2.05.w), + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Icon( + Icons.map_outlined, + color: Colors.black, + size: 5.w, + ), + + SizedBox(width: 2.5.w), + + Expanded( + child: Text( + selectedAddressState!.fullAddress, + style: TextStyle( + fontWeight: FontWeight.normal, + fontSize: AppFontSize.small.sp, + height: 1.4, + ), + ), + ), + ], + ), + ), + ), + ), + + SizedBox(height: 3.h), + + /** + TextField( + controller: _addressController, + maxLines: 4, + decoration: InputDecoration( + filled: true, + fillColor: Colors.grey.shade50, + hintText: "Masukkan detail alamat...", + contentPadding: EdgeInsets.all(4.w), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(2.5.w), + borderSide: BorderSide(color: Colors.grey.shade200), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(2.5.w), + borderSide: BorderSide(color: AppColor.primaryColor), + ), + ), + style: TextStyle(fontSize: (AppFontSize.small - 1).sp), + ), + const Spacer(), + */ + SizedBox( + width: double.infinity, + child: ElevatedButton( + onPressed: customerState.isLoading + ? null + : _saveCustomerAddress, + style: ElevatedButton.styleFrom( + backgroundColor: AppColor.primaryColor, + padding: EdgeInsets.symmetric(vertical: 1.8.h), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(2.5.w), + ), + elevation: 0, + disabledBackgroundColor: Colors.grey.shade300, + ), + child: Text( + "Simpan Alamat", + style: TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + fontSize: AppFontSize.medium.sp, + ), + ), + ), + ), + if (widget.label != null) + Center( + child: TextButton.icon( + onPressed: _showDeleteConfirmation, + style: TextButton.styleFrom( + foregroundColor: Colors.red[700], + padding: EdgeInsets.symmetric( + vertical: 1.5.h, + horizontal: 4.w, + ), + ), + icon: Icon(Icons.delete_outline_rounded, size: 5.w), + label: Text( + "Hapus Alamat Ini", + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: (AppFontSize.small - 1.25).sp, + ), + ), + ), + ), + ], + ), + ), + ), + ); + }, + ); + } + + Widget _buildLabelOption(String label, IconData icon) { + bool isSelected = _selectedLabel == label; + + return GestureDetector( + onTap: () => setState(() => _selectedLabel = label), + child: Container( + padding: EdgeInsets.symmetric(horizontal: 5.w, vertical: 1.2.h), + decoration: BoxDecoration( + color: isSelected ? AppColor.primaryColor : Colors.white, + borderRadius: BorderRadius.circular(2.w), + border: Border.all( + color: isSelected ? AppColor.primaryColor : Colors.grey.shade300, + ), + ), + child: Row( + children: [ + Icon( + icon, + color: isSelected ? Colors.white : Colors.grey, + size: 5.w, + ), + SizedBox(width: 2.w), + Text( + label, + style: TextStyle( + color: isSelected ? Colors.white : Colors.grey, + fontWeight: isSelected ? FontWeight.bold : FontWeight.normal, + fontSize: (AppFontSize.small - 1).sp, + ), + ), + ], + ), + ), + ); + } +} diff --git a/Mobile Commerce/lib/features/account/presentation/widgets/login_auth_modal.dart b/Mobile Commerce/lib/features/account/presentation/widgets/login_auth_modal.dart new file mode 100644 index 0000000..471dbfe --- /dev/null +++ b/Mobile Commerce/lib/features/account/presentation/widgets/login_auth_modal.dart @@ -0,0 +1,133 @@ +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'package:niogu_ecommerce_v1/core/constant/app_color.dart'; +import 'package:niogu_ecommerce_v1/core/constant/app_font_size.dart'; +import 'package:niogu_ecommerce_v1/core/router/app_route.dart'; +import 'package:sizer/sizer.dart'; + +class LoginAuthModal extends StatelessWidget { + const LoginAuthModal({super.key}); + + @override + Widget build(BuildContext context) { + return SafeArea( + top: false, + bottom: true, + right: false, + left: false, + child: Container( + width: double.infinity, + constraints: BoxConstraints(maxWidth: double.infinity), + padding: EdgeInsets.symmetric(horizontal: 7.w, vertical: 3.h), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.only( + topLeft: Radius.circular(5.w), + topRight: Radius.circular(5.w), + ), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + width: 10.w, + height: 4, + decoration: BoxDecoration( + color: Colors.grey.shade300, + borderRadius: BorderRadius.circular(2.w), + ), + ), + SizedBox(height: 3.h), + + Container( + padding: EdgeInsets.all(5.w), + decoration: BoxDecoration( + color: AppColor.primaryColor.withOpacity(0.1), + shape: BoxShape.circle, + ), + child: Icon( + Icons.lock_person_outlined, + size: 12.w, + color: AppColor.primaryColor, + ), + ), + SizedBox(height: 2.5.h), + + Text( + "Belum Masuk Akun", + style: TextStyle( + fontSize: AppFontSize.medium.sp, + fontWeight: FontWeight.bold, + color: Colors.black87, + ), + ), + SizedBox(height: 1.h), + Text( + "Silakan masuk ke akunmu terlebih dahulu untuk melanjutkan proses checkout pesanan.", + textAlign: TextAlign.center, + style: TextStyle( + fontSize: AppFontSize.small.sp, + color: Colors.grey.shade600, + height: 1.4, + ), + ), + SizedBox(height: 4.h), + + SizedBox( + width: double.infinity, + child: ElevatedButton( + onPressed: () { + context.pop(); + context.pushNamed(AppRoute.loginScreen); + }, + style: ElevatedButton.styleFrom( + backgroundColor: AppColor.primaryColor, + padding: EdgeInsets.symmetric(vertical: 1.8.h), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(2.5.w), + ), + elevation: 0, + ), + child: Text( + "Masuk Akun", + style: TextStyle( + fontSize: AppFontSize.small.sp, + fontWeight: FontWeight.bold, + color: Colors.white, + ), + ), + ), + ), + SizedBox(height: 1.5.h), + + SizedBox( + width: double.infinity, + child: OutlinedButton( + onPressed: () { + context.pop(); + context.pushNamed(AppRoute.registerScreen); + }, + style: OutlinedButton.styleFrom( + side: const BorderSide(color: AppColor.primaryColor), + padding: EdgeInsets.symmetric(vertical: 1.8.h), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(2.5.w), + ), + ), + child: Text( + "Daftar Sekarang", + style: TextStyle( + fontSize: AppFontSize.small.sp, + fontWeight: FontWeight.bold, + color: AppColor.primaryColor, + ), + ), + ), + ), + SizedBox(height: 1.h), + ], + ), + ), + ); + } +} diff --git a/Mobile Commerce/lib/features/auth/data/datasource/auth_remote_datasource_impl.dart b/Mobile Commerce/lib/features/auth/data/datasource/auth_remote_datasource_impl.dart new file mode 100644 index 0000000..52ea8d1 --- /dev/null +++ b/Mobile Commerce/lib/features/auth/data/datasource/auth_remote_datasource_impl.dart @@ -0,0 +1,45 @@ +import 'package:dio/dio.dart'; +import 'package:niogu_ecommerce_v1/core/constant/app_url.dart'; +import 'package:niogu_ecommerce_v1/core/models/api_response_model.dart'; +import 'package:niogu_ecommerce_v1/features/auth/data/models/auth_model.dart'; + +abstract class IAuthRemoteDatasource { + Future> login({ + required String identifier, + required String password, + }); + + Future> register(RegisterRequest request); +} + +class AuthRemoteDatasourceImpl implements IAuthRemoteDatasource { + final Dio _dio; + + AuthRemoteDatasourceImpl(this._dio); + + @override + Future> login({ + required String identifier, + required String password, + }) async { + final response = await _dio.post( + "${AppUrl.LOGIN}", + data: {"identifier": identifier, "password": password}, + ); + + return ApiResponseModel.fromJson( + response.data, + (json) => LoginResponse.fromJson(json as Map), + ); + } + + @override + Future> register(RegisterRequest request) async { + final response = await _dio.post( + "${AppUrl.REGISTER}", + data: request.toJson(), + ); + + return ApiResponseModel.fromJson(response.data, (_) => null); + } +} diff --git a/Mobile Commerce/lib/features/auth/data/models/auth_model.dart b/Mobile Commerce/lib/features/auth/data/models/auth_model.dart new file mode 100644 index 0000000..85559d2 --- /dev/null +++ b/Mobile Commerce/lib/features/auth/data/models/auth_model.dart @@ -0,0 +1,67 @@ +import 'package:json_annotation/json_annotation.dart'; + +part 'auth_model.g.dart'; + +@JsonSerializable() +class LoginResponse { + @JsonKey(name: 'access_token') + final String accessToken; + + @JsonKey(name: 'customer') + final CustomerResponse customer; + + const LoginResponse({required this.accessToken, required this.customer}); + + factory LoginResponse.fromJson(Map json) => + _$LoginResponseFromJson(json); + + Map toJson() => _$LoginResponseToJson(this); +} + +@JsonSerializable() +class CustomerResponse { + final String uuid; + + final String name; + + final String? email; + + @JsonKey(name: 'phone_number') + final String phoneNumber; + + const CustomerResponse({ + required this.uuid, + required this.name, + this.email, + required this.phoneNumber, + }); + + factory CustomerResponse.fromJson(Map json) => + _$CustomerResponseFromJson(json); + + Map toJson() => _$CustomerResponseToJson(this); +} + +@JsonSerializable() +class RegisterRequest { + final String name; + + final String? email; + + @JsonKey(name: 'phone_number') + final String phoneNumber; + + final String password; + + const RegisterRequest({ + required this.name, + this.email, + required this.phoneNumber, + required this.password, + }); + + factory RegisterRequest.fromJson(Map json) => + _$RegisterRequestFromJson(json); + + Map toJson() => _$RegisterRequestToJson(this); +} diff --git a/Mobile Commerce/lib/features/auth/data/models/auth_model.g.dart b/Mobile Commerce/lib/features/auth/data/models/auth_model.g.dart new file mode 100644 index 0000000..f6ea354 --- /dev/null +++ b/Mobile Commerce/lib/features/auth/data/models/auth_model.g.dart @@ -0,0 +1,53 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'auth_model.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +LoginResponse _$LoginResponseFromJson(Map json) => + LoginResponse( + accessToken: json['access_token'] as String, + customer: CustomerResponse.fromJson( + json['customer'] as Map, + ), + ); + +Map _$LoginResponseToJson(LoginResponse instance) => + { + 'access_token': instance.accessToken, + 'customer': instance.customer, + }; + +CustomerResponse _$CustomerResponseFromJson(Map json) => + CustomerResponse( + uuid: json['uuid'] as String, + name: json['name'] as String, + email: json['email'] as String?, + phoneNumber: json['phone_number'] as String, + ); + +Map _$CustomerResponseToJson(CustomerResponse instance) => + { + 'uuid': instance.uuid, + 'name': instance.name, + 'email': instance.email, + 'phone_number': instance.phoneNumber, + }; + +RegisterRequest _$RegisterRequestFromJson(Map json) => + RegisterRequest( + name: json['name'] as String, + email: json['email'] as String?, + phoneNumber: json['phone_number'] as String, + password: json['password'] as String, + ); + +Map _$RegisterRequestToJson(RegisterRequest instance) => + { + 'name': instance.name, + 'email': instance.email, + 'phone_number': instance.phoneNumber, + 'password': instance.password, + }; diff --git a/Mobile Commerce/lib/features/auth/data/repositories/auth_repository_impl.dart b/Mobile Commerce/lib/features/auth/data/repositories/auth_repository_impl.dart new file mode 100644 index 0000000..744428a --- /dev/null +++ b/Mobile Commerce/lib/features/auth/data/repositories/auth_repository_impl.dart @@ -0,0 +1,44 @@ +import 'package:dio/dio.dart'; +import 'package:niogu_ecommerce_v1/core/errors/exceptions.dart'; +import 'package:niogu_ecommerce_v1/core/utils/log_message.dart'; +import 'package:niogu_ecommerce_v1/features/auth/data/datasource/auth_remote_datasource_impl.dart'; +import 'package:niogu_ecommerce_v1/features/auth/data/models/auth_model.dart'; +import 'package:niogu_ecommerce_v1/features/auth/domain/repositories/i_auth_repository.dart'; + +class AuthRepositoryImpl implements IAuthRepository { + final IAuthRemoteDatasource _datasource; + + const AuthRepositoryImpl(this._datasource); + + @override + Future login({ + required String identifier, + required String password, + }) async { + try { + final response = await _datasource.login( + identifier: identifier, + password: password, + ); + + final data = response.data; + + if (response.status != 'success' || data == null) return null; + + return data; + } on DioException catch (e, st) { + LogMessage.log.i(e.toString(), error: e, stackTrace: st); + throw ServerException("Akun tidak ditemukan"); + } + } + + @override + Future register(RegisterRequest request) async { + try { + await _datasource.register(request); + } on DioException catch (e, st) { + LogMessage.log.i(e.toString(), error: e, stackTrace: st); + throw ServerException("Akun tidak ditemukan"); + } + } +} diff --git a/Mobile Commerce/lib/features/auth/domain/entities/auth.dart b/Mobile Commerce/lib/features/auth/domain/entities/auth.dart new file mode 100644 index 0000000..e69de29 diff --git a/Mobile Commerce/lib/features/auth/domain/repositories/i_auth_repository.dart b/Mobile Commerce/lib/features/auth/domain/repositories/i_auth_repository.dart new file mode 100644 index 0000000..7c85a10 --- /dev/null +++ b/Mobile Commerce/lib/features/auth/domain/repositories/i_auth_repository.dart @@ -0,0 +1,10 @@ +import 'package:niogu_ecommerce_v1/features/auth/data/models/auth_model.dart'; + +abstract class IAuthRepository { + Future login({ + required String identifier, + required String password + }); + + Future register(RegisterRequest request); +} \ No newline at end of file diff --git a/Mobile Commerce/lib/features/auth/presentation/controllers/auth_controller.dart b/Mobile Commerce/lib/features/auth/presentation/controllers/auth_controller.dart new file mode 100644 index 0000000..bceb7bf --- /dev/null +++ b/Mobile Commerce/lib/features/auth/presentation/controllers/auth_controller.dart @@ -0,0 +1,63 @@ +import 'dart:async'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:niogu_ecommerce_v1/core/system/system_secure.dart'; +import 'package:niogu_ecommerce_v1/core/system/system_setting.dart'; +import 'package:niogu_ecommerce_v1/features/auth/data/models/auth_model.dart'; +import 'package:niogu_ecommerce_v1/features/auth/domain/repositories/i_auth_repository.dart'; +import 'package:niogu_ecommerce_v1/features/auth/presentation/providers/auth_provider.dart'; + +class AuthController extends AutoDisposeAsyncNotifier { + late final IAuthRepository _authRepository; + + @override + FutureOr build() { + _authRepository = ref.read(authRepositoryProvider); + } + + Future login({ + required String identifier, + required String password, + }) async { + state = const AsyncValue.loading(); + + final result = await AsyncValue.guard(() async { + final data = await _authRepository.login( + identifier: identifier, + password: password, + ); + + if (data == null) return; + + final accessToken = data.accessToken; + + final customer = data.customer; + + await SystemSecure.saveAccessToken(accessToken); + + await SystemSetting.setCustomerInfo( + id: customer.uuid, + name: customer.name, + email: customer.email, + phoneNumber: customer.phoneNumber, + ); + + await SystemSetting.setIsLoggedIn(); + }); + + state = result; + + if (result is AsyncError) throw result.error!; + } + + Future register(RegisterRequest request) async { + state = const AsyncValue.loading(); + + final result = await AsyncValue.guard(() async { + await _authRepository.register(request); + }); + + state = result; + + if (result is AsyncError) throw result.error!; + } +} diff --git a/Mobile Commerce/lib/features/auth/presentation/providers/auth_provider.dart b/Mobile Commerce/lib/features/auth/presentation/providers/auth_provider.dart new file mode 100644 index 0000000..68d9471 --- /dev/null +++ b/Mobile Commerce/lib/features/auth/presentation/providers/auth_provider.dart @@ -0,0 +1,22 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:niogu_ecommerce_v1/core/providers/app_provider.dart'; +import 'package:niogu_ecommerce_v1/features/auth/data/datasource/auth_remote_datasource_impl.dart'; +import 'package:niogu_ecommerce_v1/features/auth/data/repositories/auth_repository_impl.dart'; +import 'package:niogu_ecommerce_v1/features/auth/domain/repositories/i_auth_repository.dart'; +import 'package:niogu_ecommerce_v1/features/auth/presentation/controllers/auth_controller.dart'; + +final authRemoteDatasourceProvider = + Provider.autoDispose((ref) { + final dio = ref.watch(dioProvider); + + return AuthRemoteDatasourceImpl(dio); + }); + +final authRepositoryProvider = Provider.autoDispose((ref) { + final datasource = ref.watch(authRemoteDatasourceProvider); + + return AuthRepositoryImpl(datasource); +}); + +final authControllerProvider = + AsyncNotifierProvider.autoDispose(AuthController.new); diff --git a/Mobile Commerce/lib/features/auth/presentation/screens/login_screen.dart b/Mobile Commerce/lib/features/auth/presentation/screens/login_screen.dart new file mode 100644 index 0000000..7d1885d --- /dev/null +++ b/Mobile Commerce/lib/features/auth/presentation/screens/login_screen.dart @@ -0,0 +1,275 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; +import 'package:niogu_ecommerce_v1/core/constant/app_asset.dart'; +import 'package:niogu_ecommerce_v1/core/constant/app_color.dart'; +import 'package:niogu_ecommerce_v1/core/constant/app_font_size.dart'; +import 'package:niogu_ecommerce_v1/core/errors/exceptions.dart'; +import 'package:niogu_ecommerce_v1/core/providers/app_provider.dart'; +import 'package:niogu_ecommerce_v1/core/router/app_route.dart'; +import 'package:niogu_ecommerce_v1/core/system/system_setting.dart'; +import 'package:niogu_ecommerce_v1/core/utils/log_message.dart'; +import 'package:niogu_ecommerce_v1/core/widgets/custom_snackbar.dart'; +import 'package:niogu_ecommerce_v1/core/widgets/custom_text_form_field.dart'; +import 'package:niogu_ecommerce_v1/features/auth/presentation/providers/auth_provider.dart'; +import 'package:sizer/sizer.dart'; + +class LoginScreen extends ConsumerStatefulWidget { + const LoginScreen({super.key}); + + @override + ConsumerState createState() => _LoginScreenState(); +} + +class _LoginScreenState extends ConsumerState { + final _formKey = GlobalKey(); + + final _identifierController = TextEditingController(); + + final _passwordController = TextEditingController(); + + bool _obsecurePassword = true; + + @override + void initState() { + // TODO: implement initState + super.initState(); + } + + @override + void dispose() { + _identifierController.dispose(); + _passwordController.dispose(); + super.dispose(); + } + + Future _setCustomerInfo() async { + final customerId = await SystemSetting.getCurrentCustomerId(); + + final customerName = await SystemSetting.getCurrentCustomerName(); + + final customerEmail = await SystemSetting.getCurrentCustomerEmail(); + + final customerPhone = await SystemSetting.getCurrentCustomerPhone(); + + ref.read(currentCustomerIdProvider.notifier).state = customerId; + + ref.read(currentCustomerNameProvider.notifier).state = customerName; + + ref.read(currentCustomerEmailProvider.notifier).state = customerEmail; + + ref.read(currentCustomerPhoneProvider.notifier).state = customerPhone; + + ref.read(currentStatusLoginProvider.notifier).state = true; + } + + String? _validation({String? value, required String message}) { + if (value == null || value.isEmpty) { + return message; + } + + return null; + } + + Future _login() async { + if (!_formKey.currentState!.validate()) return; + + try { + final identifier = _identifierController.text.trim(); + + final password = _passwordController.text.trim(); + + await ref + .read(authControllerProvider.notifier) + .login(identifier: identifier, password: password); + + if (!mounted) return; + + await _setCustomerInfo(); + CustomSnackbar.showSuccess(context, "Berhasil login"); + context.goNamed(AppRoute.homeScreen); + } on ServerException catch (e) { + CustomSnackbar.showError(context, e.message); + } catch (e, st) { + LogMessage.log.e(e.toString(), error: e, stackTrace: st); + CustomSnackbar.showError(context, "Ups, terjadi kesalahan"); + } + } + + @override + Widget build(BuildContext context) { + return LayoutBuilder( + builder: (context, constraints) { + final authControllerState = ref.watch(authControllerProvider); + + final isLoading = authControllerState.isLoading; + + return SafeArea( + top: false, + bottom: true, + right: false, + left: false, + child: Scaffold( + backgroundColor: Colors.white, + body: SingleChildScrollView( + padding: EdgeInsets.symmetric(horizontal: 7.w), + child: Form( + key: _formKey, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox(height: 8.h), + + Center( + child: Container( + height: 15.h, + width: 15.h, + decoration: BoxDecoration( + image: DecorationImage( + image: AssetImage(AppAsset.LOGO), + fit: BoxFit.cover, + ), + ), + ), + ), + + SizedBox(height: 5.h), + + Text( + "Selamat Datang", + style: TextStyle( + fontSize: AppFontSize.medium.sp, + fontWeight: FontWeight.bold, + color: Colors.black87, + ), + ), + SizedBox(height: 1.h), + Text( + "Silakan masuk ke akun Anda untuk mulai belanja.", + style: TextStyle( + fontSize: AppFontSize.small.sp, + color: Colors.grey.shade600, + ), + ), + + SizedBox(height: 5.h), + + CustomTextFormField( + label: "Email atau No. Whatsapp", + hint: "@gmail.com", + controller: _identifierController, + prefixIcon: Icons.person_outline_rounded, + validator: (value) => _validation( + value: value, + message: "Kolom ini belum diisi", + ), + readOnly: isLoading, + ), + SizedBox(height: 3.h), + + CustomTextFormField( + label: "Password", + hint: "••••••••", + controller: _passwordController, + isObscure: _obsecurePassword, + prefixIcon: Icons.lock_outline_rounded, + suffixIcon: _obsecurePassword + ? Icons.visibility_off_outlined + : Icons.visibility_outlined, + suffixPressed: () { + setState(() { + _obsecurePassword = !_obsecurePassword; + }); + }, + validator: (value) => _validation( + value: value, + message: "Password belum diisi", + ), + readOnly: isLoading, + ), + + SizedBox(height: 2.h), + + Align( + alignment: Alignment.centerRight, + child: Text( + "Lupa Kata Sandi?", + style: TextStyle( + color: AppColor.primaryColor, + fontWeight: FontWeight.bold, + fontSize: (AppFontSize.small - 1).sp, + ), + ), + ), + + SizedBox(height: 5.h), + + SizedBox( + width: double.infinity, + child: ElevatedButton( + onPressed: isLoading ? null : _login, + style: ElevatedButton.styleFrom( + backgroundColor: AppColor.primaryColor, + padding: EdgeInsets.symmetric(vertical: 2.h), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(2.5.w), + ), + elevation: 0, + disabledBackgroundColor: Colors.grey.shade300, + ), + child: Text( + "Masuk", + style: TextStyle( + fontSize: AppFontSize.medium.sp, + fontWeight: FontWeight.bold, + color: Colors.white, + ), + ), + ), + ), + + SizedBox(height: 4.h), + + Center( + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + "Belum punya akun? ", + style: TextStyle( + fontSize: (AppFontSize.small - 1.25).sp, + ), + ), + SizedBox(width: 0.5.w), + TextButton( + onPressed: () { + context.pushNamed(AppRoute.registerScreen); + }, + style: TextButton.styleFrom( + padding: EdgeInsets.zero, + minimumSize: Size.zero, + tapTargetSize: MaterialTapTargetSize.shrinkWrap, + overlayColor: Colors.transparent, + ), + child: Text( + "Daftar Sekarang", + style: TextStyle( + color: AppColor.primaryColor, + fontWeight: FontWeight.bold, + fontSize: (AppFontSize.small - 1.25).sp, + ), + ), + ), + ], + ), + ), + ], + ), + ), + ), + ), + ); + }, + ); + } +} diff --git a/Mobile Commerce/lib/features/auth/presentation/screens/register_screen.dart b/Mobile Commerce/lib/features/auth/presentation/screens/register_screen.dart new file mode 100644 index 0000000..35107cf --- /dev/null +++ b/Mobile Commerce/lib/features/auth/presentation/screens/register_screen.dart @@ -0,0 +1,373 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; +import 'package:niogu_ecommerce_v1/core/constant/app_asset.dart'; +import 'package:niogu_ecommerce_v1/core/constant/app_color.dart'; +import 'package:niogu_ecommerce_v1/core/constant/app_font_size.dart'; +import 'package:niogu_ecommerce_v1/core/errors/exceptions.dart'; +import 'package:niogu_ecommerce_v1/core/providers/app_provider.dart'; +import 'package:niogu_ecommerce_v1/core/router/app_route.dart'; +import 'package:niogu_ecommerce_v1/core/system/system_setting.dart'; +import 'package:niogu_ecommerce_v1/core/utils/log_message.dart'; +import 'package:niogu_ecommerce_v1/core/widgets/custom_snackbar.dart'; +import 'package:niogu_ecommerce_v1/core/widgets/custom_text_form_field.dart'; +import 'package:niogu_ecommerce_v1/features/auth/data/models/auth_model.dart'; +import 'package:niogu_ecommerce_v1/features/auth/presentation/providers/auth_provider.dart'; +import 'package:sizer/sizer.dart'; + +class RegisterScreen extends ConsumerStatefulWidget { + const RegisterScreen({super.key}); + + @override + ConsumerState createState() => _RegisterScreenState(); +} + +class _RegisterScreenState extends ConsumerState { + final _formKey = GlobalKey(); + + final _nameController = TextEditingController(); + + final _emailController = TextEditingController(); + + final _phoneController = TextEditingController(); + + final _passwordController = TextEditingController(); + + final _confirmPasswordController = TextEditingController(); + + bool _obsecurePassword = true; + + bool _obsecureConfirmPassword = true; + + @override + void initState() { + // TODO: implement initState + super.initState(); + } + + @override + void dispose() { + _nameController.dispose(); + _emailController.dispose(); + _phoneController.dispose(); + _passwordController.dispose(); + _confirmPasswordController.dispose(); + super.dispose(); + } + + Future _setCustomerInfo() async { + final customerId = await SystemSetting.getCurrentCustomerId(); + + final customerName = await SystemSetting.getCurrentCustomerName(); + + final customerEmail = await SystemSetting.getCurrentCustomerEmail(); + + final customerPhone = await SystemSetting.getCurrentCustomerPhone(); + + ref.read(currentCustomerIdProvider.notifier).state = customerId; + + ref.read(currentCustomerNameProvider.notifier).state = customerName; + + ref.read(currentCustomerEmailProvider.notifier).state = customerEmail; + + ref.read(currentCustomerPhoneProvider.notifier).state = customerPhone; + + ref.read(currentStatusLoginProvider.notifier).state = true; + } + + String? _validation({ + String? value, + String? message, + bool isEmail = false, + bool isPassword = false, + bool isPasswordConfirm = false, + }) { + if (value == null || value.isEmpty) { + return message; + } + + final _emailRegex = RegExp( + r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$', + ); + + if (isEmail && !_emailRegex.hasMatch(value)) { + return "Email tidak valid"; + } + + if (isPassword) { + if (value.length < 8) { + return "Password minimal harus 8 karakter"; + } + + final passwordRegExp = RegExp( + r'^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&#])[A-Za-z\d@$!%*?&#]{8,}$', + ); + + if (!passwordRegExp.hasMatch(value)) { + return "Gunakan huruf besar, kecil, angka, dan simbol"; + } + } + + if (isPasswordConfirm && value != _passwordController.text.trim()) { + return "Konfirmasi password tidak sesuai"; + } + + return null; + } + + Future _register() async { + if (!_formKey.currentState!.validate()) return; + + try { + await ref + .read(authControllerProvider.notifier) + .register( + RegisterRequest( + name: _nameController.text.trim(), + phoneNumber: _phoneController.text.trim(), + password: _passwordController.text.trim(), + ), + ); + + await ref + .read(authControllerProvider.notifier) + .login( + identifier: _phoneController.text.trim(), + password: _passwordController.text.trim(), + ); + + if (!mounted) return; + + await _setCustomerInfo(); + + CustomSnackbar.showSuccess(context, "Berhasil login"); + context.goNamed(AppRoute.homeScreen); + } on ServerException catch (e) { + if (!mounted) ; + + CustomSnackbar.showError(context, e.message); + } catch (e, st) { + LogMessage.log.e(e.toString(), error: e, stackTrace: st); + CustomSnackbar.showError(context, "Ups, terjadi kesalahan"); + } + } + + @override + Widget build(BuildContext context) { + return LayoutBuilder( + builder: (context, constraints) { + final authControllerState = ref.watch(authControllerProvider); + + final isLoading = authControllerState.isLoading; + + return SafeArea( + top: false, + bottom: true, + right: false, + left: false, + child: Scaffold( + backgroundColor: Colors.white, + body: SingleChildScrollView( + padding: EdgeInsets.symmetric(horizontal: 7.w), + child: Form( + key: _formKey, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox(height: 8.h), + + Center( + child: Image.asset( + AppAsset.LOGO, + height: 15.h, + width: 15.h, + fit: BoxFit.cover, + errorBuilder: (context, error, stackTrace) => + CircleAvatar( + radius: 10.w, + backgroundColor: AppColor.primaryColor + .withOpacity(0.1), + child: Icon( + Icons.person_add_outlined, + size: 10.w, + color: AppColor.primaryColor, + ), + ), + ), + ), + + SizedBox(height: 3.h), + Text( + "Buat Akun Baru", + style: TextStyle( + fontSize: AppFontSize.medium.sp, + fontWeight: FontWeight.bold, + ), + ), + SizedBox(height: 1.h), + Text( + "Lengkapi data di bawah ini untuk mendaftar.", + style: TextStyle( + fontSize: AppFontSize.small.sp, + color: Colors.grey.shade600, + ), + ), + + SizedBox(height: 4.h), + + CustomTextFormField( + label: "Nama Lengkap", + hint: "Masukkan nama lengkap", + controller: _nameController, + prefixIcon: Icons.person_outline, + validator: (value) => _validation( + value: value, + message: "Nama lengkap belum diisi", + ), + readOnly: isLoading, + ), + + SizedBox(height: 2.5.h), + + CustomTextFormField( + label: "Email", + hint: "@gmail.com", + controller: _emailController, + keyboardType: TextInputType.emailAddress, + prefixIcon: Icons.email_outlined, + validator: (value) => + _validation(value: value, isEmail: true), + readOnly: isLoading, + ), + + SizedBox(height: 2.5.h), + + CustomTextFormField( + label: "No. Whatsapp", + hint: "08123456xxx", + controller: _phoneController, + keyboardType: TextInputType.phone, + prefixIcon: Icons.person_outline_rounded, + validator: (value) => _validation( + value: value, + message: "No. Whatsapp belum diisi", + ), + readOnly: isLoading, + ), + + SizedBox(height: 2.5.h), + + CustomTextFormField( + label: "Password", + hint: "••••••••", + controller: _passwordController, + isObscure: _obsecurePassword, + prefixIcon: Icons.lock_outline, + suffixIcon: _obsecurePassword + ? Icons.visibility + : Icons.visibility_off, + suffixPressed: () { + setState(() { + _obsecurePassword = !_obsecurePassword; + }); + }, + validator: (value) => _validation( + value: value, + message: "Password belum diisi", + isPassword: true, + ), + readOnly: isLoading, + ), + + SizedBox(height: 2.5.h), + + CustomTextFormField( + label: "Konfirmasi Password", + hint: "••••••••", + controller: _confirmPasswordController, + isObscure: _obsecureConfirmPassword, + prefixIcon: Icons.lock_outline, + suffixIcon: _obsecureConfirmPassword + ? Icons.visibility + : Icons.visibility_off, + suffixPressed: () { + setState(() { + _obsecureConfirmPassword = !_obsecureConfirmPassword; + }); + }, + validator: (value) => _validation( + value: value, + message: "Konfirmasi password belum diisi", + isPasswordConfirm: true, + ), + readOnly: isLoading, + ), + + SizedBox(height: 5.h), + + SizedBox( + width: double.infinity, + child: ElevatedButton( + onPressed: isLoading ? null : _register, + style: ElevatedButton.styleFrom( + backgroundColor: AppColor.primaryColor, + padding: EdgeInsets.symmetric(vertical: 2.h), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(2.5.w), + ), + elevation: 0, + ), + child: Text( + "Daftar Sekarang", + style: TextStyle( + fontSize: AppFontSize.small.sp, + fontWeight: FontWeight.bold, + color: Colors.white, + ), + ), + ), + ), + + SizedBox(height: 3.h), + Center( + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + "Sudah punya akun? ", + style: TextStyle( + fontSize: (AppFontSize.small - 1.25).sp, + ), + ), + SizedBox(width: 0.5.w), + TextButton( + onPressed: () => context.pop(), + style: TextButton.styleFrom( + padding: EdgeInsets.zero, + minimumSize: Size.zero, + tapTargetSize: MaterialTapTargetSize.shrinkWrap, + overlayColor: Colors.transparent, + ), + child: Text( + "Masuk di sini", + style: TextStyle( + color: AppColor.primaryColor, + fontWeight: FontWeight.bold, + fontSize: (AppFontSize.small - 1.25).sp, + ), + ), + ), + ], + ), + ), + SizedBox(height: 5.h), + ], + ), + ), + ), + ), + ); + }, + ); + } +} diff --git a/Mobile Commerce/lib/features/cart/data/datasource/cart_remote_datasource_impl.dart b/Mobile Commerce/lib/features/cart/data/datasource/cart_remote_datasource_impl.dart new file mode 100644 index 0000000..5749e36 --- /dev/null +++ b/Mobile Commerce/lib/features/cart/data/datasource/cart_remote_datasource_impl.dart @@ -0,0 +1,33 @@ +import 'package:dio/dio.dart'; +import 'package:niogu_ecommerce_v1/core/constant/app_url.dart'; +import 'package:niogu_ecommerce_v1/core/models/api_response_model.dart'; +import 'package:niogu_ecommerce_v1/core/system/system_setting.dart'; +import 'package:niogu_ecommerce_v1/features/home/data/models/home_model.dart'; + +abstract class ICartRemoteDatasource { + Future>> fetchProductBestSellers(); +} + +class CartRemoteDatasourceImpl implements ICartRemoteDatasource { + final Dio _dio; + + CartRemoteDatasourceImpl(this._dio); + + @override + Future>> + fetchProductBestSellers() async { + final currentOutletId = await SystemSetting.getCurrentOutletId(); + + final response = await _dio.get("${AppUrl.PRODUCTS}/best-sellers/$currentOutletId"); + + return ApiResponseModel>.fromJson( + response.data, + (json) => (json as List) + .map( + (item) => + ProductItemResponse.fromJson(item as Map), + ) + .toList(), + ); + } +} diff --git a/Mobile Commerce/lib/features/cart/data/repositories/cart_repository_impl.dart b/Mobile Commerce/lib/features/cart/data/repositories/cart_repository_impl.dart new file mode 100644 index 0000000..4dedb96 --- /dev/null +++ b/Mobile Commerce/lib/features/cart/data/repositories/cart_repository_impl.dart @@ -0,0 +1,29 @@ +import 'package:dio/dio.dart'; +import 'package:niogu_ecommerce_v1/core/errors/exceptions.dart'; +import 'package:niogu_ecommerce_v1/core/utils/log_message.dart'; +import 'package:niogu_ecommerce_v1/features/cart/data/datasource/cart_remote_datasource_impl.dart'; +import 'package:niogu_ecommerce_v1/features/cart/domain/repositories/i_cart_repository.dart'; +import 'package:niogu_ecommerce_v1/features/home/domain/entities/home.dart'; + +class CartRepositoryImpl implements ICartRepository { + final ICartRemoteDatasource _datasource; + + CartRepositoryImpl(this._datasource); + + @override + Future> fetchProductBestSellers() async { + try { + final response = await _datasource.fetchProductBestSellers(); + + final data = response.data; + + if (response.status != 'success' || data == null) return []; + + return data.map((product) => product.toProductItem()).toList(); + } on DioException catch (e, st) { + LogMessage.log.e(e.toString(), error: e, stackTrace: st); + + throw ServerException("Terjadi kesalahan koneksi"); + } + } +} diff --git a/Mobile Commerce/lib/features/cart/domain/entities/cart.dart b/Mobile Commerce/lib/features/cart/domain/entities/cart.dart new file mode 100644 index 0000000..723ec6e --- /dev/null +++ b/Mobile Commerce/lib/features/cart/domain/entities/cart.dart @@ -0,0 +1,71 @@ +class CartItem { + final String id; + final String outletId; + final String? image; + final String name; + final String variantName; + final bool isProductVariant; + final double sellingPrice; + final String stockType; + final double stock; + final String unit; + final int quantity; + + const CartItem({ + required this.id, + required this.outletId, + this.image, + required this.name, + required this.variantName, + required this.isProductVariant, + required this.sellingPrice, + required this.stockType, + required this.stock, + required this.unit, + required this.quantity, + }); + + CartItem copyWith({required int quantity}) { + return CartItem( + id: id, + outletId: outletId, + image: image, + name: name, + variantName: variantName, + isProductVariant: isProductVariant, + sellingPrice: sellingPrice, + stockType: stockType, + stock: stock, + unit: unit, + quantity: quantity, + ); + } + + factory CartItem.fromJson(Map json) => CartItem( + id: json['id'] as String, + outletId: json['outlet_id'] as String, + image: json['image'] as String?, + name: json['name'] as String, + variantName: json['variant_name'] as String, + isProductVariant: json['is_product_variant'] as bool, + sellingPrice: json['selling_price'] as double, + stockType: json['stock_type'] as String, + stock: json['stock'] as double, + unit: json['unit'] as String, + quantity: json['quantity'] as int, + ); + + Map toJson() => { + 'id': id, + 'outlet_id': outletId, + 'image': image, + 'name': name, + 'variant_name': variantName, + 'is_product_variant': isProductVariant, + 'selling_price': sellingPrice, + 'stock_type': stockType, + 'stock': stock, + 'unit': unit, + 'quantity': quantity, + }; +} diff --git a/Mobile Commerce/lib/features/cart/domain/repositories/i_cart_repository.dart b/Mobile Commerce/lib/features/cart/domain/repositories/i_cart_repository.dart new file mode 100644 index 0000000..90ac6c8 --- /dev/null +++ b/Mobile Commerce/lib/features/cart/domain/repositories/i_cart_repository.dart @@ -0,0 +1,5 @@ +import 'package:niogu_ecommerce_v1/features/home/domain/entities/home.dart'; + +abstract class ICartRepository { + Future> fetchProductBestSellers(); +} \ No newline at end of file diff --git a/Mobile Commerce/lib/features/cart/presentation/controllers/cart_controller.dart b/Mobile Commerce/lib/features/cart/presentation/controllers/cart_controller.dart new file mode 100644 index 0000000..c17231c --- /dev/null +++ b/Mobile Commerce/lib/features/cart/presentation/controllers/cart_controller.dart @@ -0,0 +1,71 @@ +import 'dart:async'; + +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:niogu_ecommerce_v1/features/cart/domain/entities/cart.dart'; +import 'package:niogu_ecommerce_v1/features/cart/presentation/providers/cart_provider.dart'; +import 'package:niogu_ecommerce_v1/features/home/domain/entities/home.dart'; + +class CartItemsController extends StateNotifier> { + CartItemsController() : super({}); + + void hydrate(List cartItems) { + state = { + ...state, + for (final cartItem in cartItems) + "${cartItem.outletId}-${cartItem.id}": cartItem, + }; + } + + void increment(CartItem item, {int? quantity}) { + final key = "${item.outletId}-${item.id}"; + final cartItem = state[key]; + + if (cartItem == null) { + state = {...state, key: item}; + } else { + state = { + ...state, + key: item.copyWith(quantity: cartItem.quantity + (quantity ?? 1)), + }; + } + } + + void decrement(String outletId, String id) { + final key = "$outletId-$id"; + + final cartItem = state[key]; + + if (cartItem == null) return; + + if (cartItem.quantity <= 1) { + state = {...state}..remove(key); + } else { + state = { + ...state, + key: cartItem.copyWith(quantity: cartItem.quantity - 1), + }; + } + } + + void clear() { + state = {}; + } +} + +class ProductBestSellerController + extends AutoDisposeAsyncNotifier> { + @override + FutureOr> build() => _fetchProductBestSellers(); + + Future> _fetchProductBestSellers() async { + final cartRepository = ref.read(cartRepositoryProvider); + + return await cartRepository.fetchProductBestSellers(); + } + + Future refresh() async { + ref.invalidateSelf(); + + await future; + } +} diff --git a/Mobile Commerce/lib/features/cart/presentation/providers/cart_provider.dart b/Mobile Commerce/lib/features/cart/presentation/providers/cart_provider.dart new file mode 100644 index 0000000..e95ae29 --- /dev/null +++ b/Mobile Commerce/lib/features/cart/presentation/providers/cart_provider.dart @@ -0,0 +1,43 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:niogu_ecommerce_v1/core/providers/app_provider.dart'; +import 'package:niogu_ecommerce_v1/features/cart/data/datasource/cart_remote_datasource_impl.dart'; +import 'package:niogu_ecommerce_v1/features/cart/data/repositories/cart_repository_impl.dart'; +import 'package:niogu_ecommerce_v1/features/cart/domain/entities/cart.dart'; +import 'package:niogu_ecommerce_v1/features/cart/domain/repositories/i_cart_repository.dart'; +import 'package:niogu_ecommerce_v1/features/cart/presentation/controllers/cart_controller.dart'; +import 'package:niogu_ecommerce_v1/features/home/domain/entities/home.dart'; + +final cartRemoteDatasourceProvider = + Provider.autoDispose((ref) { + final dio = ref.watch(dioProvider); + + return CartRemoteDatasourceImpl(dio); + }); + +final cartRepositoryProvider = Provider.autoDispose((ref) { + final cartRemoteDatasource = ref.watch(cartRemoteDatasourceProvider); + + return CartRepositoryImpl(cartRemoteDatasource); +}); + +final productBestSellerControllerProvider = + AsyncNotifierProvider.autoDispose< + ProductBestSellerController, + List + >(ProductBestSellerController.new); + +final cartItemControllerProvider = + StateNotifierProvider>( + (ref) => CartItemsController(), + ); + +final cartItemProvider = Provider>((ref) { + final currentOutletId = ref.watch(currentOutletIdProvider); + + final cartItemController = ref.watch(cartItemControllerProvider); + + return cartItemController.values + .map((cartItem) => cartItem) + .where((item) => item.outletId == currentOutletId) + .toList(); +}); diff --git a/Mobile Commerce/lib/features/cart/presentation/screens/cart_screen.dart b/Mobile Commerce/lib/features/cart/presentation/screens/cart_screen.dart new file mode 100644 index 0000000..e9fdfdc --- /dev/null +++ b/Mobile Commerce/lib/features/cart/presentation/screens/cart_screen.dart @@ -0,0 +1,934 @@ +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; +import 'package:niogu_ecommerce_v1/core/constant/app_font_size.dart'; +import 'package:niogu_ecommerce_v1/core/enums/stock_type.dart'; +import 'package:niogu_ecommerce_v1/core/errors/exceptions.dart'; +import 'package:niogu_ecommerce_v1/core/providers/app_provider.dart'; +import 'package:niogu_ecommerce_v1/core/router/app_route.dart'; +import 'package:niogu_ecommerce_v1/core/system/system_setting.dart'; +import 'package:niogu_ecommerce_v1/core/utils/currency_format.dart'; +import 'package:niogu_ecommerce_v1/core/utils/extenstion_format.dart'; +import 'package:niogu_ecommerce_v1/core/utils/log_message.dart'; +import 'package:niogu_ecommerce_v1/core/widgets/custom_empty_screen.dart'; +import 'package:niogu_ecommerce_v1/core/widgets/custom_snackbar.dart'; +import 'package:niogu_ecommerce_v1/features/account/presentation/widgets/login_auth_modal.dart'; +import 'package:niogu_ecommerce_v1/features/cart/domain/entities/cart.dart'; +import 'package:niogu_ecommerce_v1/features/cart/presentation/providers/cart_provider.dart'; +import 'package:niogu_ecommerce_v1/features/favorite/domain/entities/favorite.dart'; +import 'package:niogu_ecommerce_v1/features/favorite/presentation/providers/favorite_provider.dart'; +import 'package:niogu_ecommerce_v1/features/home/domain/entities/home.dart'; +import 'package:niogu_ecommerce_v1/features/home/presentation/providers/home_provider.dart'; +import 'package:niogu_ecommerce_v1/features/product/presentation/providers/product_provider.dart'; +import 'package:shimmer/shimmer.dart'; +import 'package:sizer/sizer.dart'; +import 'package:niogu_ecommerce_v1/core/constant/app_color.dart'; + +class CartScreen extends ConsumerStatefulWidget { + const CartScreen({super.key}); + + @override + ConsumerState createState() => _CartScreenState(); +} + +class _CartScreenState extends ConsumerState { + @override + void initState() { + // TODO: implement initState + super.initState(); + } + + @override + void dispose() { + // TODO: implement dispose + super.dispose(); + } + + double _calculateTotalOrder(List cartItems) { + return cartItems.fold(0.0, (sum, cartItem) { + return sum + (cartItem.sellingPrice * cartItem.quantity); + }); + } + + Future _processCheckout() async { + final isLoggedIn = ref.read(currentStatusLoginProvider); + + if (!isLoggedIn) { + showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: Colors.transparent, + constraints: BoxConstraints(maxWidth: double.infinity), + builder: (context) => const LoginAuthModal(), + ); + return; + } + + final cartItems = ref.read(cartItemProvider); + + await SystemSetting.saveCartItemByOutlet(cartItems); + + context.pushNamed(AppRoute.checkoutScreen); + } + + @override + Widget build(BuildContext context) { + return LayoutBuilder( + builder: (context, constraints) { + final productState = ref.watch(productBestSellerControllerProvider); + + final currentOutletName = ref.watch(currentOutletNameProvider); + + final cartItemState = ref.watch(cartItemProvider); + + final currentOutletId = ref.watch(currentOutletIdProvider); + + final favoriteState = ref.watch(favoriteControllerProvider); + + return SafeArea( + top: false, + bottom: true, + right: false, + left: false, + child: Scaffold( + backgroundColor: Colors.white, + resizeToAvoidBottomInset: false, + appBar: AppBar( + backgroundColor: Colors.white, + elevation: 0, + centerTitle: true, + leading: IconButton( + icon: Icon( + Icons.arrow_back, + size: 7.w, + color: AppColor.primaryColor, + ), + onPressed: () => context.goNamed(AppRoute.homeScreen), + ), + title: Text( + "Keranjang Belanja", + style: TextStyle( + color: Colors.black, + fontSize: AppFontSize.medium.sp, + fontWeight: FontWeight.bold, + ), + ), + ), + body: productState.when( + data: (products) { + if (cartItemState.isEmpty) { + return _buildEmptyCart(); + } + return RefreshIndicator( + onRefresh: () async { + await ref + .read(productBestSellerControllerProvider.notifier) + .refresh(); + }, + color: AppColor.primaryColor, + backgroundColor: Colors.white, + child: SingleChildScrollView( + physics: const AlwaysScrollableScrollPhysics(), + child: Column( + children: [ + _buildOutletInfoSection(currentOutletName!), + + SizedBox(height: 2.h), + + _buildProductListSection(cartItemState), + + SizedBox(height: 3.h), + + _buildAddOtherProductSection( + products: products, + currentOutletId: currentOutletId!, + favorites: favoriteState, + ), + + SizedBox(height: 5.h), + ], + ), + ), + ); + }, + error: (error, stackTrace) => CustomEmptyScreen( + icon: Icons.cloud_off_outlined, + title: "Terjadi Kesalahan Koneksi", + subtitle: "Tarik ke bawah untuk mencoba lagi", + height: 40.h, + ), + loading: () => _buildCartLoading(), + ), + bottomNavigationBar: cartItemState.isEmpty + ? null + : _buildBottomCartAction(cartItemState), + ), + ); + }, + ); + } + + Widget _buildOutletInfoSection(String outletName) { + return Container( + width: double.infinity, + margin: EdgeInsets.all(4.w), + padding: EdgeInsets.all(4.w), + decoration: BoxDecoration( + color: const Color(0xFFFFF9E5), + borderRadius: BorderRadius.circular(2.5.w), + border: Border.all(color: const Color(0xFFFFEBB0)), + ), + child: Row( + children: [ + Container( + padding: EdgeInsets.all(2.5.w), + decoration: const BoxDecoration( + color: Colors.white, + shape: BoxShape.circle, + ), + child: Icon( + Icons.storefront, + color: AppColor.primaryColor, + size: 5.w, + ), + ), + SizedBox(width: 4.w), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Pengiriman dari :", + style: TextStyle( + fontSize: (AppFontSize.small - 1.25).sp, + color: Colors.grey.shade700, + ), + ), + Text( + outletName, + style: TextStyle( + fontSize: (AppFontSize.small - 1.25).sp, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + ), + /** + Text( + "Ubah", + style: TextStyle( + color: AppColor.primaryColor, + fontWeight: FontWeight.bold, + fontSize: (AppFontSize.small - 1.25).sp, + ), + ), + */ + ], + ), + ); + } + + Widget _buildProductListSection(List cartItems) { + return Container( + margin: EdgeInsets.symmetric(horizontal: 4.w), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(2.5.w), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.1), + blurRadius: 10, + offset: const Offset(0, 5), + ), + ], + ), + child: Column( + children: List.generate(cartItems.length, (index) { + final cartItem = cartItems[index]; + return Column( + children: [ + Padding( + padding: EdgeInsets.all(4.w), + child: Row( + children: [ + CachedNetworkImage( + imageUrl: cartItem.image ?? 'error', + imageBuilder: (context, imageProvider) { + return Container( + width: 18.w, + height: 18.w, + decoration: BoxDecoration( + color: Colors.grey.shade100, + borderRadius: BorderRadius.circular(2.w), + image: DecorationImage( + image: imageProvider, + fit: BoxFit.cover, + ), + ), + ); + }, + placeholder: (context, url) { + return Shimmer.fromColors( + baseColor: Colors.grey.shade300, + highlightColor: Colors.grey.shade100, + child: Container( + height: 18.w, + width: 18.w, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(2.w), + ), + ), + ); + }, + errorWidget: (context, url, error) { + return Container( + width: 18.w, + height: 18.w, + decoration: BoxDecoration( + color: Colors.grey.shade100, + borderRadius: BorderRadius.circular(2.w), + ), + child: Icon(Icons.image, color: Colors.grey.shade300), + ); + }, + ), + SizedBox(width: 3.w), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + cartItem.name, + maxLines: 2, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: (AppFontSize.small - 1.25).sp, + ), + ), + if (cartItem.isProductVariant) ...[ + SizedBox(height: 0.5.h), + Text( + cartItem.variantName, + style: TextStyle( + color: Colors.grey.shade700, + fontSize: (AppFontSize.small - 1.25).sp, + ), + ), + ], + if (cartItem.stockType == StockType.fixed.type) ...[ + SizedBox(height: 0.5.h), + Text( + "Stok: ${cartItem.stock.toStringWithoutTrailingZero()} ${cartItem.unit}", + style: TextStyle( + color: Colors.grey.shade700, + fontSize: (AppFontSize.small - 1.25).sp, + ), + ), + ], + SizedBox(height: 1.h), + Text( + CurrencyFormat.formatToIdr( + cartItem.sellingPrice, + 0, + ), + style: TextStyle( + fontWeight: FontWeight.bold, + color: AppColor.primaryColor, + fontSize: (AppFontSize.small - 1.25).sp, + ), + ), + ], + ), + ), + Row( + children: [ + _buildCounterBtn( + Icons.remove, + isMinus: true, + onTap: () async { + ref + .read(cartItemControllerProvider.notifier) + .decrement(cartItem.outletId, cartItem.id); + + final cartItems = ref.read(cartItemProvider); + + await SystemSetting.saveCartItemByOutlet(cartItems); + }, + ), + Padding( + padding: EdgeInsets.symmetric(horizontal: 3.5.w), + child: Text( + cartItem.quantity.toString(), + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: AppFontSize.small.sp, + ), + ), + ), + _buildCounterBtn( + Icons.add, + onTap: () async { + if (cartItem.stockType == StockType.fixed.type && + cartItem.quantity >= cartItem.stock) { + CustomSnackbar.showError( + context, + "Kuantitas mencapai batas maksimum", + ); + return; + } + ref + .read(cartItemControllerProvider.notifier) + .increment(cartItem); + + final cartItems = ref.read(cartItemProvider); + + await SystemSetting.saveCartItemByOutlet(cartItems); + }, + ), + ], + ), + ], + ), + ), + if (index != 2) + Divider( + height: 1, + thickness: 1, + color: Colors.grey.shade100, + indent: 4.w, + endIndent: 4.w, + ), + ], + ); + }), + ), + ); + } + + Widget _buildCounterBtn( + IconData icon, { + bool isMinus = false, + required VoidCallback onTap, + }) { + return GestureDetector( + onTap: onTap, + child: Container( + padding: EdgeInsets.all(1.w), + decoration: BoxDecoration( + shape: BoxShape.circle, + border: Border.all(color: Colors.grey.shade300), + color: isMinus ? Colors.white : AppColor.primaryColor, + ), + child: Icon( + icon, + size: 4.w, + color: isMinus ? Colors.grey : Colors.white, + ), + ), + ); + } + + Widget _buildAddOtherProductSection({ + required List products, + required String currentOutletId, + required Map favorites, + }) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: EdgeInsets.symmetric(horizontal: 4.w), + child: Text( + "Produk Terlaris", + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: AppFontSize.small.sp, + ), + ), + ), + SizedBox(height: 1.5.h), + SizedBox( + height: 25.h, + child: ListView.builder( + scrollDirection: Axis.horizontal, + padding: EdgeInsets.only(left: 4.w), + itemCount: products.length, + itemBuilder: (context, index) { + final product = products[index]; + + return _buildProductCard( + product, + currentOutletId, + favorites.containsKey("$currentOutletId-${product.id}"), + ); + }, + ), + ), + ], + ); + } + + Widget _buildProductCard( + ProductItem product, + String currentOutletId, + bool isFavorite, + ) { + return GestureDetector( + onTap: () async { + try { + final productDetail = await ref + .read(productRepositoryProvider) + .fetchProductById(product.id); + + if (productDetail == null) { + CustomSnackbar.showError(context, "Produk tidak ditemukan"); + ref.read(homeControllerProvider.notifier).refresh(); + return; + } + + context.pushNamed(AppRoute.productDetailScreen, extra: productDetail); + } on ServerException catch (e, st) { + LogMessage.log.i(e.toString(), error: e, stackTrace: st); + } + }, + child: Container( + width: 45.w, + margin: EdgeInsets.only(right: 3.w), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(2.5.w), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + flex: 3, + child: CachedNetworkImage( + imageUrl: product.image ?? 'error', + imageBuilder: (context, imageProvider) { + return Container( + width: double.infinity, + decoration: BoxDecoration( + color: Colors.grey.shade100, + borderRadius: BorderRadius.vertical( + top: Radius.circular(2.5.w), + ), + image: DecorationImage( + image: imageProvider, + fit: BoxFit.cover, + ), + ), + child: Stack( + children: [ + Positioned( + top: 1.w, + right: 1.w, + child: GestureDetector( + onTap: () { + final currentOutletId = ref.read( + currentOutletIdProvider, + ); + ref + .read(favoriteControllerProvider.notifier) + .toggle(product, currentOutletId!); + }, + child: CircleAvatar( + radius: 14, + backgroundColor: Colors.white.withOpacity(0.9), + child: Icon( + isFavorite + ? Icons.favorite + : Icons.favorite_outline, + size: 5.w, + color: isFavorite ? Colors.red : Colors.grey, + ), + ), + ), + ), + ], + ), + ); + }, + errorWidget: (context, url, error) { + return Container( + width: double.infinity, + decoration: BoxDecoration( + color: Colors.grey.shade100, + borderRadius: BorderRadius.vertical( + top: Radius.circular(2.5.w), + ), + ), + child: Stack( + children: [ + Center( + child: Icon( + Icons.image_outlined, + color: Colors.grey.shade300, + size: 10.w, + ), + ), + Positioned( + top: 1.w, + right: 1.w, + child: GestureDetector( + onTap: () { + ref + .read(favoriteControllerProvider.notifier) + .toggle(product, currentOutletId); + }, + child: CircleAvatar( + radius: 14, + backgroundColor: Colors.white.withOpacity(0.9), + child: Icon( + isFavorite + ? Icons.favorite + : Icons.favorite_outline, + size: 5.w, + color: isFavorite ? Colors.red : Colors.grey, + ), + ), + ), + ), + ], + ), + ); + }, + ), + ), + + Expanded( + flex: 2, + child: Padding( + padding: EdgeInsets.all(3.w), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + product.name, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: (AppFontSize.small - 1.25).sp, + color: const Color(0xFF102851), + ), + ), + + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "${product.totalSold.toCompact} Terjual", + style: TextStyle( + color: Colors.black87, + fontWeight: FontWeight.bold, + fontSize: (AppFontSize.small - 1.25).sp, + ), + ), + Row( + children: [ + Icon(Icons.star, color: Colors.orange, size: 3.5.w), + SizedBox(width: 1.w), + Text( + product.averageRating.toStringAsFixed(1), + style: TextStyle( + fontSize: (AppFontSize.small - 1.25).sp, + color: Colors.grey.shade600, + ), + ), + ], + ), + ], + ), + + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + CurrencyFormat.formatToIdr(product.sellingPrice, 0), + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: AppFontSize.small.sp, + color: AppColor.primaryColor, + ), + ), + Row( + children: [ + Icon( + Icons.thumb_up, + color: Colors.red, + size: 3.5.w, + ), + SizedBox(width: 1.w), + Text( + product.likes.toString(), + style: TextStyle( + fontSize: (AppFontSize.small - 1.25).sp, + color: Colors.red, + ), + ), + ], + ), + ], + ), + ], + ), + ), + ), + ], + ), + ), + ); + } + + Widget _buildBottomCartAction(List cartItems) { + return Container( + height: 10.h, + padding: EdgeInsets.symmetric(horizontal: 4.w, vertical: 1.5.h), + decoration: BoxDecoration( + color: Colors.white, + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.05), + blurRadius: 10, + offset: const Offset(0, -5), + ), + ], + ), + child: Row( + children: [ + Expanded( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Total Pesanan", + style: TextStyle( + fontSize: AppFontSize.small.sp, + color: Colors.grey.shade700, + ), + ), + Text( + CurrencyFormat.formatToIdr( + _calculateTotalOrder(cartItems), + 0, + ), + style: TextStyle( + fontSize: AppFontSize.small.sp, + fontWeight: FontWeight.bold, + color: AppColor.primaryColor, + ), + ), + ], + ), + ), + ElevatedButton( + onPressed: _processCheckout, + style: ElevatedButton.styleFrom( + backgroundColor: AppColor.primaryColor, + padding: EdgeInsets.symmetric(horizontal: 8.w, vertical: 1.5.h), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(2.w), + ), + elevation: 0, + ), + child: Text( + "Checkout Sekarang", + style: TextStyle( + fontSize: AppFontSize.small.sp, + fontWeight: FontWeight.bold, + color: Colors.white, + ), + ), + ), + ], + ), + ); + } + + Widget _buildEmptyCart() { + return Container( + width: double.infinity, + padding: EdgeInsets.symmetric(horizontal: 10.w), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Container( + width: 60.w, + height: 60.w, + decoration: BoxDecoration( + color: AppColor.primaryColor.withOpacity(0.05), + shape: BoxShape.circle, + ), + child: Icon( + Icons.shopping_cart_outlined, + size: 20.w, + color: AppColor.primaryColor.withOpacity(0.5), + ), + ), + SizedBox(height: 4.h), + + Text( + "Keranjangmu Kosong", + style: TextStyle( + fontSize: AppFontSize.small.sp, + fontWeight: FontWeight.bold, + color: Colors.black87, + ), + ), + SizedBox(height: 1.5.h), + Text( + "Sepertinya kamu belum menambahkan produk apa pun ke keranjang belanjaanmu.", + textAlign: TextAlign.center, + style: TextStyle( + fontSize: AppFontSize.small.sp, + color: Colors.grey.shade600, + height: 1.5, + ), + ), + SizedBox(height: 5.h), + SizedBox( + width: double.infinity, + child: ElevatedButton( + onPressed: () => context.goNamed(AppRoute.homeScreen), + style: ElevatedButton.styleFrom( + backgroundColor: AppColor.primaryColor, + padding: EdgeInsets.symmetric(vertical: 1.8.h), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(2.5.w), + ), + elevation: 0, + ), + child: Text( + "Mulai Belanja", + style: TextStyle( + fontSize: AppFontSize.small.sp, + fontWeight: FontWeight.bold, + color: Colors.white, + ), + ), + ), + ), + ], + ), + ); + } + + Widget _buildCartLoading() { + return Shimmer.fromColors( + baseColor: Colors.grey.shade300, + highlightColor: Colors.grey.shade100, + child: SingleChildScrollView( + physics: const NeverScrollableScrollPhysics(), + child: Column( + children: [ + Container( + width: double.infinity, + height: 10.h, + margin: EdgeInsets.all(4.w), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(2.5.w), + ), + ), + + Container( + margin: EdgeInsets.symmetric(horizontal: 4.w), + padding: EdgeInsets.all(4.w), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(2.5.w), + ), + child: Column( + children: List.generate( + 2, + (index) => Padding( + padding: EdgeInsets.only(bottom: index == 0 ? 2.h : 0), + child: Row( + children: [ + Container( + width: 18.w, + height: 18.w, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(2.w), + ), + ), + SizedBox(width: 3.w), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + width: 40.w, + height: 2.h, + color: Colors.white, + ), + SizedBox(height: 1.h), + Container( + width: 20.w, + height: 2.h, + color: Colors.white, + ), + ], + ), + ), + Container( + width: 20.w, + height: 4.h, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(20), + ), + ), + ], + ), + ), + ), + ), + ), + + SizedBox(height: 3.h), + + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: EdgeInsets.symmetric(horizontal: 4.w), + child: Container( + width: 35.w, + height: 2.h, + color: Colors.white, + ), + ), + SizedBox(height: 1.5.h), + SizedBox( + height: 25.h, + child: ListView.builder( + scrollDirection: Axis.horizontal, + padding: EdgeInsets.only(left: 4.w), + itemCount: 3, + itemBuilder: (_, __) => Container( + width: 45.w, + margin: EdgeInsets.only(right: 3.w), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(2.5.w), + ), + ), + ), + ), + ], + ), + ], + ), + ), + ); + } +} diff --git a/Mobile Commerce/lib/features/checkout/data/datasource/checkout_remote_datasource_impl.dart b/Mobile Commerce/lib/features/checkout/data/datasource/checkout_remote_datasource_impl.dart new file mode 100644 index 0000000..05281db --- /dev/null +++ b/Mobile Commerce/lib/features/checkout/data/datasource/checkout_remote_datasource_impl.dart @@ -0,0 +1,43 @@ +import 'dart:convert'; + +import 'package:dio/dio.dart'; +import 'package:niogu_ecommerce_v1/core/constant/app_url.dart'; +import 'package:niogu_ecommerce_v1/core/errors/exceptions.dart'; +import 'package:niogu_ecommerce_v1/core/models/api_response_model.dart'; +import 'package:niogu_ecommerce_v1/core/utils/log_message.dart'; +import 'package:niogu_ecommerce_v1/features/checkout/data/models/checkout_model.dart'; + +abstract class ICheckoutRemoteDatasource { + Future> checkout( + OnlineOrderRequest request, + ); +} + +class CheckoutRemoteDatasourceImpl implements ICheckoutRemoteDatasource { + final Dio _dio; + + CheckoutRemoteDatasourceImpl(this._dio); + + @override + Future> checkout( + OnlineOrderRequest request, + ) async { + try { + final formData = FormData.fromMap({ + "data": jsonEncode(request.toJson()), + "payment_proof_file": request.paymentProofFile, + }); + + final response = await _dio.post(AppUrl.CHECKOUT, data: formData); + + return ApiResponseModel.fromJson( + response.data, + (json) => CheckoutResponse.fromJson(json as Map), + ); + } on DioException catch (e, st) { + LogMessage.log.e(e.toString(), error: e, stackTrace: st); + + throw ServerException("Terjadi kesalahan koneksi"); + } + } +} diff --git a/Mobile Commerce/lib/features/checkout/data/models/checkout_model.dart b/Mobile Commerce/lib/features/checkout/data/models/checkout_model.dart new file mode 100644 index 0000000..6774d97 --- /dev/null +++ b/Mobile Commerce/lib/features/checkout/data/models/checkout_model.dart @@ -0,0 +1,204 @@ +import 'package:dio/dio.dart'; +import 'package:json_annotation/json_annotation.dart'; +import 'package:niogu_ecommerce_v1/core/utils/converter.dart'; +import 'package:niogu_ecommerce_v1/features/checkout/domain/entities/checkout.dart'; + +part 'checkout_model.g.dart'; + +@JsonSerializable() +class OnlineOrderRequest { + @JsonKey(name: 'outlet_id') + final String outletId; + + @JsonKey(name: 'outlet_name_snapshot') + final String outletNameSnapshot; + + @JsonKey(name: 'outlet_phone_number_snapshot') + final String? outletPhoneNumberSnapshot; + + @JsonKey(name: 'outlet_address_snapshot') + final OutletAddressSnapshotRequest? outletAddressSnapshot; + + @JsonKey(name: 'customer_id') + final String customerId; + + @JsonKey(name: 'customer_name_snapshot') + final String customerNameSnapshot; + + @JsonKey(name: 'customer_email_snapshot') + final String? customerEmailSnapshot; + + @JsonKey(name: 'customer_phone_number_snapshot') + final String customerPhoneNumberSnapshot; + + @JsonKey(name: 'customer_address_snapshot') + final CustomerAddressSnapshotRequest? customerAddressSnapshot; + + @JsonKey(name: 'order_status') + final String orderStatus; + + @JsonKey(name: 'delivery_type') + final String deliveryType; + + @JsonKey(name: 'delivery_preference') + final String deliveryPreference; + + @JsonKey(name: 'delivery_fee_type') + final String? deliveryFeeType; + + @JsonKey(name: 'delivery_fee') + final double deliveryFee; + + @JsonKey(name: 'total_delivery_fee') + final double totalDeliveryFee; + + @JsonKey(name: 'total_order') + final double totalOrder; + + @JsonKey(name: 'total_amount') + final double totalAmount; + + @JsonKey(name: 'notes') + final String? notes; + + @JsonKey(includeFromJson: false, includeToJson: false) + final MultipartFile? paymentProofFile; + + @JsonKey(name: 'payment_method') + final String paymentMethod; + + @JsonKey(name: 'items') + final List items; + + const OnlineOrderRequest({ + required this.outletId, + required this.outletNameSnapshot, + required this.outletPhoneNumberSnapshot, + required this.outletAddressSnapshot, + required this.customerId, + required this.customerNameSnapshot, + this.customerEmailSnapshot, + required this.customerPhoneNumberSnapshot, + required this.customerAddressSnapshot, + required this.orderStatus, + required this.deliveryType, + required this.deliveryPreference, + this.deliveryFeeType, + required this.deliveryFee, + required this.totalDeliveryFee, + required this.totalOrder, + required this.totalAmount, + this.notes, + this.paymentProofFile, + required this.paymentMethod, + required this.items, + }); + + factory OnlineOrderRequest.fromJson(Map json) => + _$OnlineOrderRequestFromJson(json); + + Map toJson() => _$OnlineOrderRequestToJson(this); +} + +@JsonSerializable() +class OutletAddressSnapshotRequest { + @JsonKey(name: 'full_address') + final String fullAddress; + + final double latitude; + + final double longitude; + + const OutletAddressSnapshotRequest({ + required this.fullAddress, + required this.latitude, + required this.longitude, + }); + + factory OutletAddressSnapshotRequest.fromJson(Map json) => + _$OutletAddressSnapshotRequestFromJson(json); + + Map toJson() => _$OutletAddressSnapshotRequestToJson(this); +} + +@JsonSerializable() +class CustomerAddressSnapshotRequest { + final String label; + + @JsonKey(name: 'full_address') + final String fullAddress; + + final double latitude; + + final double longitude; + + const CustomerAddressSnapshotRequest({ + required this.label, + required this.fullAddress, + required this.latitude, + required this.longitude, + }); + + factory CustomerAddressSnapshotRequest.fromJson(Map json) => + _$CustomerAddressSnapshotRequestFromJson(json); + + Map toJson() => _$CustomerAddressSnapshotRequestToJson(this); +} + +@JsonSerializable() +class OnlineOrderItemRequest { + @JsonKey(name: 'product_variant_id') + final String productVariantId; + + final int quantity; + + @JsonKey(name: 'product_image_url_snapshot') + final String? productImageUrlSnapshot; + + @JsonKey(name: 'product_name_snapshot') + final String productNameSnapshot; + + @JsonKey(name: 'product_variant_name_snapshot') + final String? productVariantNameSnapshot; + + @JsonKey(name: 'selling_price_snapshot', fromJson: toDouble) + final double sellingPriceSnapshot; + + @JsonKey(fromJson: toDouble) + final double subtotal; + + const OnlineOrderItemRequest({ + required this.productVariantId, + required this.quantity, + this.productImageUrlSnapshot, + required this.productNameSnapshot, + this.productVariantNameSnapshot, + required this.sellingPriceSnapshot, + required this.subtotal, + }); + + factory OnlineOrderItemRequest.fromJson(Map json) => + _$OnlineOrderItemRequestFromJson(json); + + Map toJson() => _$OnlineOrderItemRequestToJson(this); +} + +@JsonSerializable() +class CheckoutResponse { + @JsonKey(name: 'order_id') + final String orderId; + + @JsonKey(name: 'order_number') + final String orderNumber; + + const CheckoutResponse({required this.orderId, required this.orderNumber}); + + factory CheckoutResponse.fromJson(Map json) => + _$CheckoutResponseFromJson(json); + + Map toJson() => _$CheckoutResponseToJson(this); + + Checkout toCheckout() { + return Checkout(orderId: orderId, orderNumber: orderNumber); + } +} diff --git a/Mobile Commerce/lib/features/checkout/data/models/checkout_model.g.dart b/Mobile Commerce/lib/features/checkout/data/models/checkout_model.g.dart new file mode 100644 index 0000000..ac2ac57 --- /dev/null +++ b/Mobile Commerce/lib/features/checkout/data/models/checkout_model.g.dart @@ -0,0 +1,136 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'checkout_model.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +OnlineOrderRequest _$OnlineOrderRequestFromJson( + Map json, +) => OnlineOrderRequest( + outletId: json['outlet_id'] as String, + outletNameSnapshot: json['outlet_name_snapshot'] as String, + outletPhoneNumberSnapshot: json['outlet_phone_number_snapshot'] as String?, + outletAddressSnapshot: json['outlet_address_snapshot'] == null + ? null + : OutletAddressSnapshotRequest.fromJson( + json['outlet_address_snapshot'] as Map, + ), + customerId: json['customer_id'] as String, + customerNameSnapshot: json['customer_name_snapshot'] as String, + customerEmailSnapshot: json['customer_email_snapshot'] as String?, + customerPhoneNumberSnapshot: json['customer_phone_number_snapshot'] as String, + customerAddressSnapshot: json['customer_address_snapshot'] == null + ? null + : CustomerAddressSnapshotRequest.fromJson( + json['customer_address_snapshot'] as Map, + ), + orderStatus: json['order_status'] as String, + deliveryType: json['delivery_type'] as String, + deliveryPreference: json['delivery_preference'] as String, + deliveryFeeType: json['delivery_fee_type'] as String?, + deliveryFee: (json['delivery_fee'] as num).toDouble(), + totalDeliveryFee: (json['total_delivery_fee'] as num).toDouble(), + totalOrder: (json['total_order'] as num).toDouble(), + totalAmount: (json['total_amount'] as num).toDouble(), + notes: json['notes'] as String?, + paymentMethod: json['payment_method'] as String, + items: (json['items'] as List) + .map((e) => OnlineOrderItemRequest.fromJson(e as Map)) + .toList(), +); + +Map _$OnlineOrderRequestToJson(OnlineOrderRequest instance) => + { + 'outlet_id': instance.outletId, + 'outlet_name_snapshot': instance.outletNameSnapshot, + 'outlet_phone_number_snapshot': instance.outletPhoneNumberSnapshot, + 'outlet_address_snapshot': instance.outletAddressSnapshot, + 'customer_id': instance.customerId, + 'customer_name_snapshot': instance.customerNameSnapshot, + 'customer_email_snapshot': instance.customerEmailSnapshot, + 'customer_phone_number_snapshot': instance.customerPhoneNumberSnapshot, + 'customer_address_snapshot': instance.customerAddressSnapshot, + 'order_status': instance.orderStatus, + 'delivery_type': instance.deliveryType, + 'delivery_preference': instance.deliveryPreference, + 'delivery_fee_type': instance.deliveryFeeType, + 'delivery_fee': instance.deliveryFee, + 'total_delivery_fee': instance.totalDeliveryFee, + 'total_order': instance.totalOrder, + 'total_amount': instance.totalAmount, + 'notes': instance.notes, + 'payment_method': instance.paymentMethod, + 'items': instance.items, + }; + +OutletAddressSnapshotRequest _$OutletAddressSnapshotRequestFromJson( + Map json, +) => OutletAddressSnapshotRequest( + fullAddress: json['full_address'] as String, + latitude: (json['latitude'] as num).toDouble(), + longitude: (json['longitude'] as num).toDouble(), +); + +Map _$OutletAddressSnapshotRequestToJson( + OutletAddressSnapshotRequest instance, +) => { + 'full_address': instance.fullAddress, + 'latitude': instance.latitude, + 'longitude': instance.longitude, +}; + +CustomerAddressSnapshotRequest _$CustomerAddressSnapshotRequestFromJson( + Map json, +) => CustomerAddressSnapshotRequest( + label: json['label'] as String, + fullAddress: json['full_address'] as String, + latitude: (json['latitude'] as num).toDouble(), + longitude: (json['longitude'] as num).toDouble(), +); + +Map _$CustomerAddressSnapshotRequestToJson( + CustomerAddressSnapshotRequest instance, +) => { + 'label': instance.label, + 'full_address': instance.fullAddress, + 'latitude': instance.latitude, + 'longitude': instance.longitude, +}; + +OnlineOrderItemRequest _$OnlineOrderItemRequestFromJson( + Map json, +) => OnlineOrderItemRequest( + productVariantId: json['product_variant_id'] as String, + quantity: (json['quantity'] as num).toInt(), + productImageUrlSnapshot: json['product_image_url_snapshot'] as String?, + productNameSnapshot: json['product_name_snapshot'] as String, + productVariantNameSnapshot: json['product_variant_name_snapshot'] as String?, + sellingPriceSnapshot: toDouble(json['selling_price_snapshot']), + subtotal: toDouble(json['subtotal']), +); + +Map _$OnlineOrderItemRequestToJson( + OnlineOrderItemRequest instance, +) => { + 'product_variant_id': instance.productVariantId, + 'quantity': instance.quantity, + 'product_image_url_snapshot': instance.productImageUrlSnapshot, + 'product_name_snapshot': instance.productNameSnapshot, + 'product_variant_name_snapshot': instance.productVariantNameSnapshot, + 'selling_price_snapshot': instance.sellingPriceSnapshot, + 'subtotal': instance.subtotal, +}; + +CheckoutResponse _$CheckoutResponseFromJson(Map json) => + CheckoutResponse( + orderId: json['order_id'] as String, + orderNumber: json['order_number'] as String, + ); + +Map _$CheckoutResponseToJson(CheckoutResponse instance) => + { + 'order_id': instance.orderId, + 'order_number': instance.orderNumber, + }; diff --git a/Mobile Commerce/lib/features/checkout/data/repositories/checkout_repository_impl.dart b/Mobile Commerce/lib/features/checkout/data/repositories/checkout_repository_impl.dart new file mode 100644 index 0000000..0331ace --- /dev/null +++ b/Mobile Commerce/lib/features/checkout/data/repositories/checkout_repository_impl.dart @@ -0,0 +1,28 @@ +import 'package:dio/dio.dart'; +import 'package:niogu_ecommerce_v1/core/errors/exceptions.dart'; +import 'package:niogu_ecommerce_v1/core/utils/log_message.dart'; +import 'package:niogu_ecommerce_v1/features/checkout/data/datasource/checkout_remote_datasource_impl.dart'; +import 'package:niogu_ecommerce_v1/features/checkout/domain/entities/checkout.dart'; +import 'package:niogu_ecommerce_v1/features/checkout/domain/repositories/i_checkout_repository.dart'; + +class CheckoutRepositoryImpl implements ICheckoutRepository { + final ICheckoutRemoteDatasource _datasource; + + CheckoutRepositoryImpl(this._datasource); + + @override + Future checkout(OnlineOrder order) async { + try { + final response = await _datasource.checkout(order.toOnlineOrderRequest()); + + final data = response.data; + + if (response.status != 'success' || data == null) return null; + + return data.toCheckout(); + } on DioException catch (e, st) { + LogMessage.log.e(e.toString(), error: e, stackTrace: st); + throw ServerException("Terjadi kesalahan koneksi"); + } + } +} diff --git a/Mobile Commerce/lib/features/checkout/domain/entities/checkout.dart b/Mobile Commerce/lib/features/checkout/domain/entities/checkout.dart new file mode 100644 index 0000000..a1bd912 --- /dev/null +++ b/Mobile Commerce/lib/features/checkout/domain/entities/checkout.dart @@ -0,0 +1,194 @@ +import 'package:dio/dio.dart'; +import 'package:niogu_ecommerce_v1/core/enums/delivery_type.dart'; +import 'package:niogu_ecommerce_v1/core/enums/order_status.dart'; +import 'package:niogu_ecommerce_v1/features/checkout/data/models/checkout_model.dart'; + +class OnlineOrder { + final String outletId; + + final String outletNameSnapshot; + + final String? outletPhoneNumberSnapshot; + + final OutletAddressSnapshot? outletAddressSnapshot; + + final String customerId; + + final String customerNameSnapshot; + + final String? customerEmailSnapshot; + + final String customerPhoneNumberSnapshot; + + final CustomerAddressSnapshot? customerAddressSnapshot; + + final OrderStatus orderStatus; + + final DeliveryType deliveryType; + + final String deliveryPreference; + + final String? deliveryFeeType; + + final double deliveryFee; + + final double totalDeliveryFee; + + final double totalOrder; + + final double totalAmount; + + final String? notes; + + final MultipartFile? paymentProofFile; + + final String paymentMethod; + + final List items; + + const OnlineOrder({ + required this.outletId, + required this.outletNameSnapshot, + required this.outletPhoneNumberSnapshot, + required this.outletAddressSnapshot, + required this.customerId, + required this.customerNameSnapshot, + this.customerEmailSnapshot, + required this.customerPhoneNumberSnapshot, + required this.orderStatus, + required this.deliveryType, + required this.deliveryPreference, + this.deliveryFeeType, + required this.deliveryFee, + required this.totalDeliveryFee, + required this.totalOrder, + required this.totalAmount, + this.notes, + this.paymentProofFile, + required this.paymentMethod, + required this.customerAddressSnapshot, + required this.items, + }); + + OnlineOrderRequest toOnlineOrderRequest() { + return OnlineOrderRequest( + outletId: outletId, + outletNameSnapshot: outletNameSnapshot, + outletPhoneNumberSnapshot: outletPhoneNumberSnapshot, + outletAddressSnapshot: outletAddressSnapshot + ?.toOutletAddressSnapshotRequest(), + customerId: customerId, + customerNameSnapshot: customerNameSnapshot, + customerEmailSnapshot: customerEmailSnapshot, + customerPhoneNumberSnapshot: customerPhoneNumberSnapshot, + customerAddressSnapshot: customerAddressSnapshot + ?.toCustomerAddressSnapshotRequest(), + orderStatus: orderStatus.status, + deliveryType: deliveryType.type, + deliveryPreference: deliveryPreference, + deliveryFeeType: deliveryFeeType, + deliveryFee: deliveryFee, + totalDeliveryFee: totalDeliveryFee, + totalOrder: totalOrder, + totalAmount: totalAmount, + notes: notes, + paymentProofFile: paymentProofFile, + paymentMethod: paymentMethod, + items: items.map((item) => item.toOnlineOrderItemRequest()).toList(), + ); + } +} + +class OutletAddressSnapshot { + final String fullAddress; + + final double latitude; + + final double longitude; + + const OutletAddressSnapshot({ + required this.fullAddress, + required this.latitude, + required this.longitude, + }); + + OutletAddressSnapshotRequest toOutletAddressSnapshotRequest() { + return OutletAddressSnapshotRequest( + fullAddress: fullAddress, + latitude: latitude, + longitude: longitude, + ); + } +} + +class CustomerAddressSnapshot { + final String label; + + final String fullAddress; + + final double latitude; + + final double longitude; + + const CustomerAddressSnapshot({ + required this.label, + required this.fullAddress, + required this.latitude, + required this.longitude, + }); + + CustomerAddressSnapshotRequest toCustomerAddressSnapshotRequest() { + return CustomerAddressSnapshotRequest( + label: label, + fullAddress: fullAddress, + latitude: latitude, + longitude: longitude, + ); + } +} + +class OnlineOrderItem { + final String productVariantId; + + final int quantity; + + final String? productImageUrlSnapshot; + + final String productNameSnapshot; + + final String? productVariantNameSnapshot; + + final double sellingPriceSnapshot; + + final double subtotal; + + const OnlineOrderItem({ + required this.productVariantId, + required this.quantity, + this.productImageUrlSnapshot, + required this.productNameSnapshot, + this.productVariantNameSnapshot, + required this.sellingPriceSnapshot, + required this.subtotal, + }); + + OnlineOrderItemRequest toOnlineOrderItemRequest() { + return OnlineOrderItemRequest( + productVariantId: productVariantId, + quantity: quantity, + productImageUrlSnapshot: productImageUrlSnapshot, + productNameSnapshot: productNameSnapshot, + productVariantNameSnapshot: productVariantNameSnapshot, + sellingPriceSnapshot: sellingPriceSnapshot, + subtotal: subtotal, + ); + } +} + +class Checkout { + final String orderId; + + final String orderNumber; + + const Checkout({required this.orderId, required this.orderNumber}); +} diff --git a/Mobile Commerce/lib/features/checkout/domain/repositories/i_checkout_repository.dart b/Mobile Commerce/lib/features/checkout/domain/repositories/i_checkout_repository.dart new file mode 100644 index 0000000..1173172 --- /dev/null +++ b/Mobile Commerce/lib/features/checkout/domain/repositories/i_checkout_repository.dart @@ -0,0 +1,5 @@ +import 'package:niogu_ecommerce_v1/features/checkout/domain/entities/checkout.dart'; + +abstract class ICheckoutRepository { + Future checkout(OnlineOrder order); +} \ No newline at end of file diff --git a/Mobile Commerce/lib/features/checkout/presentation/controllers/checkout_controller.dart b/Mobile Commerce/lib/features/checkout/presentation/controllers/checkout_controller.dart new file mode 100644 index 0000000..b3f3ac3 --- /dev/null +++ b/Mobile Commerce/lib/features/checkout/presentation/controllers/checkout_controller.dart @@ -0,0 +1,30 @@ +import 'dart:async'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:niogu_ecommerce_v1/features/checkout/domain/entities/checkout.dart'; +import 'package:niogu_ecommerce_v1/features/checkout/domain/repositories/i_checkout_repository.dart'; +import 'package:niogu_ecommerce_v1/features/checkout/presentation/providers/checkout_provider.dart'; + +class CheckoutController extends AutoDisposeAsyncNotifier { + late final ICheckoutRepository _checkoutRepository; + + @override + FutureOr build() { + // TODO: implement build + _checkoutRepository = ref.read(checkoutRepositoryProvider); + } + + Future checkout(OnlineOrder order) async { + state = const AsyncValue.loading(); + + Checkout? data; + + final result = await AsyncValue.guard(() async { + data = await _checkoutRepository.checkout(order); + }); + + state = result; + + if (result is AsyncError) throw result.error!; + return data; + } +} diff --git a/Mobile Commerce/lib/features/checkout/presentation/providers/checkout_provider.dart b/Mobile Commerce/lib/features/checkout/presentation/providers/checkout_provider.dart new file mode 100644 index 0000000..b26d7d8 --- /dev/null +++ b/Mobile Commerce/lib/features/checkout/presentation/providers/checkout_provider.dart @@ -0,0 +1,26 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:niogu_ecommerce_v1/core/providers/app_provider.dart'; +import 'package:niogu_ecommerce_v1/features/checkout/data/datasource/checkout_remote_datasource_impl.dart'; +import 'package:niogu_ecommerce_v1/features/checkout/data/repositories/checkout_repository_impl.dart'; +import 'package:niogu_ecommerce_v1/features/checkout/domain/repositories/i_checkout_repository.dart'; +import 'package:niogu_ecommerce_v1/features/checkout/presentation/controllers/checkout_controller.dart'; + +final checkoutRemoteDatasourceProvider = + Provider.autoDispose((ref) { + final dio = ref.watch(dioProvider); + + return CheckoutRemoteDatasourceImpl(dio); + }); + +final checkoutRepositoryProvider = Provider.autoDispose(( + ref, +) { + final checkoutRemoteDatasource = ref.watch(checkoutRemoteDatasourceProvider); + + return CheckoutRepositoryImpl(checkoutRemoteDatasource); +}); + +final checkoutControllerProvider = + AsyncNotifierProvider.autoDispose( + CheckoutController.new, + ); \ No newline at end of file diff --git a/Mobile Commerce/lib/features/checkout/presentation/screens/checkout_screen.dart b/Mobile Commerce/lib/features/checkout/presentation/screens/checkout_screen.dart new file mode 100644 index 0000000..03aebee --- /dev/null +++ b/Mobile Commerce/lib/features/checkout/presentation/screens/checkout_screen.dart @@ -0,0 +1,1844 @@ +import 'dart:io'; +import 'dart:math'; +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:dio/dio.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; +import 'package:image_picker/image_picker.dart'; +import 'package:intl/intl.dart'; +import 'package:latlong2/latlong.dart'; +import 'package:niogu_ecommerce_v1/core/constant/app_asset.dart'; +import 'package:niogu_ecommerce_v1/core/constant/app_font_size.dart'; +import 'package:niogu_ecommerce_v1/core/enums/action_type.dart'; +import 'package:niogu_ecommerce_v1/core/enums/delivery_fee_type.dart'; +import 'package:niogu_ecommerce_v1/core/enums/delivery_type.dart'; +import 'package:niogu_ecommerce_v1/core/enums/order_status.dart'; +import 'package:niogu_ecommerce_v1/core/errors/exceptions.dart'; +import 'package:niogu_ecommerce_v1/core/models/tenant_payment_method_model.dart'; +import 'package:niogu_ecommerce_v1/core/providers/app_provider.dart'; +import 'package:niogu_ecommerce_v1/core/router/app_route.dart'; +import 'package:niogu_ecommerce_v1/core/system/system_setting.dart'; +import 'package:niogu_ecommerce_v1/core/utils/currency_format.dart'; +import 'package:niogu_ecommerce_v1/core/utils/extenstion_format.dart'; +import 'package:niogu_ecommerce_v1/core/utils/image_service.dart'; +import 'package:niogu_ecommerce_v1/core/utils/log_message.dart'; +import 'package:niogu_ecommerce_v1/core/utils/time_zone.dart'; +import 'package:niogu_ecommerce_v1/core/widgets/custom_snackbar.dart'; +import 'package:niogu_ecommerce_v1/features/account/domain/entities/account.dart'; +import 'package:niogu_ecommerce_v1/features/account/presentation/providers/account_provider.dart'; +import 'package:niogu_ecommerce_v1/features/cart/domain/entities/cart.dart'; +import 'package:niogu_ecommerce_v1/features/cart/presentation/providers/cart_provider.dart'; +import 'package:niogu_ecommerce_v1/features/checkout/domain/entities/checkout.dart'; +import 'package:niogu_ecommerce_v1/features/checkout/presentation/providers/checkout_provider.dart'; +import 'package:niogu_ecommerce_v1/features/home/presentation/providers/home_provider.dart'; +import 'package:sizer/sizer.dart'; +import 'package:niogu_ecommerce_v1/core/constant/app_color.dart'; + +class CheckoutScreen extends ConsumerStatefulWidget { + const CheckoutScreen({super.key}); + + @override + ConsumerState createState() => _CheckoutScreenState(); +} + +class _CheckoutScreenState extends ConsumerState { + final _noteController = TextEditingController(); + + DeliveryType _deliveryType = DeliveryType.delivery; + + String _selectedPaymentMethod = "COD"; + + String? _deliveryPreference; + + bool _isPaymentExpanded = false; + + final List _paymentProofPathTemps = []; + + final ImagePicker _picker = ImagePicker(); + + String? _paymentProofPath; + + @override + void initState() { + // TODO: implement initState + super.initState(); + + WidgetsBinding.instance.addPostFrameCallback((_) { + ref.invalidate(selectedAddressProvider); + }); + } + + @override + void dispose() { + // TODO: implement dispose + _noteController.dispose(); + super.dispose(); + } + + double _calculateTotalOrder(List cartItems) { + return cartItems.fold(0.0, (sum, cartItem) { + return sum + cartItem.sellingPrice * cartItem.quantity; + }); + } + + List _generateAvailableSlots({ + required int onlineOpenTime, + required int onlineCloseTime, + required int preparationTimeMinutes, + required int orderIntervalMinutes, + }) { + final slotDuration = Duration( + minutes: preparationTimeMinutes + orderIntervalMinutes, + ); + + final now = DateTime.now(); + + final List slots = []; + + final asap = _getEarliestDeliveryTime( + onlineOpenTime: onlineOpenTime, + preparationTimeMinutes: preparationTimeMinutes, + orderIntervalMinutes: orderIntervalMinutes, + ); + + final openingTime = DateTime( + now.year, + now.month, + now.day, + onlineOpenTime, + 0, + ); + + final closingTime = DateTime( + now.year, + now.month, + now.day, + onlineCloseTime, + 0, + ); + + DateTime currentSlot = now.isBefore(openingTime) + ? openingTime + : now.add(Duration(minutes: preparationTimeMinutes)); + + currentSlot = _roundToNextSlot( + currentSlot, + preparationTimeMinutes: preparationTimeMinutes, + orderIntervalMinutes: orderIntervalMinutes, + ); + + while (currentSlot.isBefore(closingTime)) { + final end = currentSlot.add(slotDuration); + + if (end.isAfter(_getLastAllowedEndTime(now, onlineCloseTime))) break; + + if (currentSlot.isAfter(asap)) { + slots.add(currentSlot); + } + currentSlot = currentSlot.add(slotDuration); + } + + return slots; + } + + DateTime _getLastAllowedEndTime(DateTime now, int onlineCloseTime) { + return DateTime(now.year, now.month, now.day, onlineCloseTime); + } + + DateTime _getEarliestDeliveryTime({ + required int onlineOpenTime, + required int preparationTimeMinutes, + required int orderIntervalMinutes, + }) { + final now = DateTime.now(); + + final openingTime = DateTime(now.year, now.month, now.day, onlineOpenTime); + + final baseTime = now.isBefore(openingTime) ? openingTime : now; + + final earliest = baseTime.add(Duration(minutes: preparationTimeMinutes)); + + return _roundToNextSlot( + earliest, + preparationTimeMinutes: preparationTimeMinutes, + orderIntervalMinutes: orderIntervalMinutes, + ); + } + + DateTime _roundToNextSlot( + DateTime time, { + required int preparationTimeMinutes, + required int orderIntervalMinutes, + }) { + final slotDuration = Duration( + minutes: preparationTimeMinutes + orderIntervalMinutes, + ); + final totalMinutes = time.hour * 60 + time.minute; + final slotMinutes = slotDuration.inMinutes; + + final remainder = totalMinutes % slotMinutes; + if (remainder == 0) return time; + + final minutesToAdd = slotMinutes - remainder; + + return time.add(Duration(minutes: minutesToAdd)); + } + + String _formatRange(DateTime start) { + final end = start.add(Duration(minutes: 25)); + final timeZone = TimeZone.getCurrentTimeZone(); + return "${DateFormat('HH:mm').format(start)} $timeZone - ${DateFormat('HH:mm').format(end)} $timeZone"; + } + + Future _showPickerOptions() async { + final bool isTablet = 100.w >= 600; + showModalBottomSheet( + context: context, + backgroundColor: Colors.white, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.vertical(top: Radius.circular(2.5.w)), + ), + constraints: const BoxConstraints(maxWidth: double.infinity), + builder: (BuildContext context) { + return SafeArea( + child: Container( + width: 100.w, + padding: EdgeInsets.symmetric(vertical: 2.h, horizontal: 5.w), + clipBehavior: Clip.hardEdge, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.vertical(top: Radius.circular(6.w)), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + ListTile( + leading: Icon(Icons.photo_library, size: 6.w), + title: Text( + 'Galeri', + style: TextStyle( + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + ), + ), + onTap: () async { + await _getImage(ImageSource.gallery); + context.pop(); + }, + ), + + SizedBox(height: 2.h), + + ListTile( + leading: Icon(Icons.photo_camera, size: 6.w), + title: Text( + 'Kamera', + style: TextStyle( + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + ), + ), + onTap: () async { + await _getImage(ImageSource.camera); + context.pop(); + }, + ), + ], + ), + ), + ); + }, + ); + } + + Future _getImage(ImageSource ImageSource) async { + try { + final XFile? pickedFile = await _picker.pickImage( + source: ImageSource, + imageQuality: 70, + ); + + if (pickedFile != null) { + File tempFile = File(pickedFile.path); + + String? imagePath = await ImageService.saveImageToLocalDirectory( + tempFile, + "payment_proof_orders", + ); + + if (imagePath != null) { + _paymentProofPathTemps.add(imagePath); + setState(() { + _paymentProofPath = imagePath; + }); + } + } + } catch (e, st) { + LogMessage.log.w(e.toString(), error: e, stackTrace: st); + CustomSnackbar.showWarning(context, "Akses ditolak"); + } + } + + Future _removeImage() async { + setState(() { + _paymentProofPath = null; + }); + } + + Future _getPaymentProofFile() async { + if (_paymentProofPath == null) return null; + + final file = File(_paymentProofPath!); + + if (!await file.exists()) { + LogMessage.log.e("File fisik tidak ditemukan: $_paymentProofPath"); + return null; + } + + return await MultipartFile.fromFile(_paymentProofPath!); + } + + String _fetchPaymentMethodAsset(String method) { + return switch (method) { + 'dana' => AppAsset.DANA_LOGO, + 'gopay' => AppAsset.GOPAY_LOGO, + 'shopeepay' => AppAsset.SHOPEEPAY_LOGO, + 'bri' => AppAsset.MANDIRI_LOGO, + 'mandiri' => AppAsset.MANDIRI_LOGO, + 'bni' => AppAsset.BNI_LOGO, + 'bca' => AppAsset.BCA_LOGO, + String() => 'error', + }; + } + + double _calculateDistance( + double lat1, + double lon1, + double lat2, + double lon2, + ) { + final p = 0.017453292519943295; + final c = cos; + final a = + 0.5 - + c((lat2 - lat1) * p) / 2 + + c(lat1 * p) * c(lat2 * p) * (1 - c((lon2 - lon1) * p)) / 2; + return 12742 * asin(sqrt(a)); + } + + Future _checkout({ + String? deliveryFeeType, + required double deliveryFee, + required double totalDeliveryFee, + required double maxDeliveryRadiusKm, + }) async { + final currentOutletId = ref.read(currentOutletIdProvider); + + final currentOutletName = ref.read(currentOutletNameProvider); + + final currentOutletPhoneNumber = ref.read(currentOutletPhoneProvider); + + final currentOutletLocation = ref.read(currentOutletLocationProvider); + + final currentOutletCoordinate = ref.read(currentOutletCoordinateProvider); + + final currentOutletHasLocation = + currentOutletLocation != null && currentOutletCoordinate != null; + + final currentCustomerId = ref.read(currentCustomerIdProvider); + + final currentCustomerName = ref.read(currentCustomerNameProvider); + + final currentCustomerPhone = ref.read(currentCustomerPhoneProvider); + + final selectedAddress = ref.read(selectedAddressProvider); + + if (_deliveryType == DeliveryType.delivery) { + if (!currentOutletHasLocation) { + CustomSnackbar.showError(context, "Metode pengiriman belum tersedia"); + return; + } + + if (selectedAddress == null) { + CustomSnackbar.showError(context, "Alamat pengiriman belum diatur"); + return; + } + + final distance = _calculateDistance( + currentOutletCoordinate.latitude, + currentOutletCoordinate.longitude, + selectedAddress.latitude, + selectedAddress.longitude, + ); + + if (maxDeliveryRadiusKm > 0 && distance > maxDeliveryRadiusKm) { + CustomSnackbar.showError( + context, + "Jangkauan pengiriman kami maks ${maxDeliveryRadiusKm.toStringWithoutTrailingZero()} Km", + ); + return; + } + } + + if (_deliveryPreference == null) { + CustomSnackbar.showError(context, "Waktu pengiriman belum dipilih"); + return; + } + + final isPayOnTheSpot = + ((_deliveryType == DeliveryType.delivery && + _selectedPaymentMethod == 'COD') || + (_deliveryType == DeliveryType.pick_up && + _selectedPaymentMethod == 'Bayar Ditoko')); + + if (!isPayOnTheSpot && _paymentProofPath == null) { + CustomSnackbar.showError(context, "Bukti pembayaran belum di upload"); + return; + } + + final deliveryPreference = _deliveryPreference == "Segera" + ? "(Segera)" + : "($_deliveryPreference)"; + + final cartItems = ref.watch(cartItemProvider); + + String? notes; + + if (_noteController.text.trim().isNotEmpty) { + notes = _noteController.text.trim(); + } + + final totalOrder = _calculateTotalOrder(cartItems); + + final order = OnlineOrder( + outletId: currentOutletId!, + outletNameSnapshot: currentOutletName!, + outletPhoneNumberSnapshot: currentOutletPhoneNumber, + outletAddressSnapshot: !currentOutletHasLocation + ? null + : OutletAddressSnapshot( + fullAddress: currentOutletLocation, + latitude: currentOutletCoordinate.latitude, + longitude: currentOutletCoordinate.longitude, + ), + customerId: currentCustomerId!, + customerNameSnapshot: currentCustomerName!, + customerPhoneNumberSnapshot: currentCustomerPhone!, + customerAddressSnapshot: selectedAddress == null + ? null + : CustomerAddressSnapshot( + label: selectedAddress.label!, + fullAddress: selectedAddress.fullAddress, + latitude: selectedAddress.latitude, + longitude: selectedAddress.longitude, + ), + orderStatus: isPayOnTheSpot + ? OrderStatus.pending_confirmation + : OrderStatus.pending_payment, + deliveryType: _deliveryType, + deliveryPreference: "Hari ini $deliveryPreference", + deliveryFeeType: deliveryFeeType, + deliveryFee: deliveryFee, + totalDeliveryFee: totalDeliveryFee, + totalOrder: totalOrder, + totalAmount: totalOrder + totalDeliveryFee, + notes: notes, + paymentProofFile: await _getPaymentProofFile(), + paymentMethod: _selectedPaymentMethod, + items: cartItems.map((item) { + return OnlineOrderItem( + productVariantId: item.id, + quantity: item.quantity, + productImageUrlSnapshot: item.image?.toRelativeImagePath(), + productNameSnapshot: item.name, + productVariantNameSnapshot: item.isProductVariant + ? item.variantName + : null, + sellingPriceSnapshot: item.sellingPrice, + subtotal: item.quantity * item.sellingPrice, + ); + }).toList(), + ); + try { + final data = await ref + .read(checkoutControllerProvider.notifier) + .checkout(order); + + if (!mounted) return; + + ref.read(cartItemControllerProvider.notifier).clear(); + + await SystemSetting.saveCartItemByOutlet([]); + + if (data != null) { + context.pushNamed( + AppRoute.customActionScreen, + extra: { + 'order_id': data.orderId, + 'order_number': data.orderNumber, + 'type': ActionType.checkout, + }, + ); + } + } on ServerException catch (e, st) { + LogMessage.log.e(e.toString(), error: e, stackTrace: st); + CustomSnackbar.showError(context, e.message); + } + } + + @override + Widget build(BuildContext context) { + return LayoutBuilder( + builder: (context, constraints) { + final currentCustomerName = ref.watch(currentCustomerNameProvider); + + final currentCustomerPhone = ref.watch(currentCustomerPhoneProvider); + + final currentOutletName = ref.watch(currentOutletNameProvider); + + final currentOutletCoordinate = ref.watch( + currentOutletCoordinateProvider, + ); + + final selectedAddress = ref.watch(selectedAddressProvider); + + final cartItems = ref.watch(cartItemProvider); + + final totalOrder = _calculateTotalOrder(cartItems); + + final isAddressSelected = selectedAddress != null; + + final configureState = ref.watch(configurationControllerProvider); + + final checkoutControllerState = ref.watch(checkoutControllerProvider); + + final isLoading = checkoutControllerState.isLoading; + + return SafeArea( + top: false, + bottom: true, + right: false, + left: false, + child: Scaffold( + backgroundColor: const Color(0xFFF5F5F5), + appBar: AppBar( + backgroundColor: Colors.white, + elevation: 0.5, + leading: IconButton( + icon: Icon( + Icons.arrow_back, + size: 7.w, + color: AppColor.primaryColor, + ), + onPressed: () => context.pop(), + ), + title: Text( + "Checkout", + style: TextStyle( + color: Colors.black, + fontSize: AppFontSize.medium.sp, + fontWeight: FontWeight.bold, + ), + ), + ), + body: Stack( + children: [ + SingleChildScrollView( + child: Column( + children: [ + _buildDeliveryAddressSection( + customerName: currentCustomerName!, + customerPhone: currentCustomerPhone!, + address: selectedAddress, + ), + + SizedBox(height: 1.25.h), + + _buildDeliveryTypeSection(), + + SizedBox(height: 1.25.h), + + _buildProductSummarySection( + cartItems, + currentOutletName!, + currentOutletCoordinate, + selectedAddress, + ), + + SizedBox(height: 1.25.h), + + configureState.maybeWhen( + data: (configure) { + if (configure == null) return const SizedBox(); + + var paymentMethods = configure.paymentMethods; + + if (configure.allowCod!) { + paymentMethods = [ + TenantPaymentMethodModel( + uuid: '', + sourceName: + _deliveryType == DeliveryType.delivery + ? 'COD' + : 'Bayar Ditoko', + accountNumber: '', + receipentName: '', + createdAt: '', + updatedAt: '', + ), + ...paymentMethods, + ]; + } + + final timeFormat = DateFormat('HH.mm'); + + final isCloseService = + configure.isCloseService ?? false; + + final onlineOpenTime = timeFormat + .parse(configure.onlineOpenTime!) + .hour; + + final onlineCloseTime = timeFormat + .parse(configure.onlineCloseTime!) + .hour; + + final preparationTimeMinutes = + configure.preparationTimeMinutes!; + + final orderIntervalMinutes = + configure.orderIntervalMinutes!; + + final deliveryFeeType = DeliveryFeeType.values.byName( + configure.deliveryFeeType!, + ); + + double deliveryFee = 0.0; + + double totalDeliveryFee = 0.0; + + double distance = 1; + + if (deliveryFeeType == DeliveryFeeType.fixed) { + deliveryFee = configure.deliveryFlatFee!; + totalDeliveryFee = deliveryFee; + } else if (currentOutletCoordinate != null && + selectedAddress != null) { + distance = _calculateDistance( + currentOutletCoordinate.latitude, + currentOutletCoordinate.longitude, + selectedAddress.latitude, + selectedAddress.longitude, + ); + deliveryFee = configure.deliveryFeePerKm!; + totalDeliveryFee = deliveryFee * distance; + } + + var strDistance = '${distance.toStringAsFixed(1)} Km'; + + if (distance < 1) { + strDistance = + '${(distance * 1000).toStringAsFixed(0)} meter'; + } + + var totalAmount = totalOrder + totalDeliveryFee; + + if (_deliveryType == DeliveryType.pick_up) + totalAmount = totalOrder; + + return Column( + children: [ + _buildDeliveryPreferenceSection( + isCloseService: isCloseService, + onlineOpenTime: onlineOpenTime, + onlineCloseTime: onlineCloseTime, + preparationTimeMinutes: preparationTimeMinutes, + orderIntervalMinutes: orderIntervalMinutes, + ), + + SizedBox(height: 1.25.h), + + _buildPaymentMethodSection(paymentMethods), + + SizedBox(height: 1.25.h), + + _buildPaymentDetailSection( + totalOrder: totalOrder, + isAddressSelcted: isAddressSelected, + deliveryFeeType: deliveryFeeType, + deliveryFee: deliveryFee, + strDistance: strDistance, + totalDeliveryFee: totalDeliveryFee, + totalAmount: totalAmount, + ), + ], + ); + }, + orElse: () => const SizedBox(), + ), + + SizedBox(height: 1.25.h), + + _buildNoteSection(), + + SizedBox(height: 1.25.h), + ], + ), + ), + + if (configureState.isRefreshing) + Container( + color: Colors.black.withOpacity(0.5), + child: Center( + child: const CircularProgressIndicator( + color: AppColor.primaryColor, + backgroundColor: Colors.white, + ), + ), + ), + + if (isLoading) + Container( + color: Colors.white54, + child: Center( + child: const CircularProgressIndicator( + color: AppColor.primaryColor, + backgroundColor: Colors.white, + ), + ), + ), + ], + ), + bottomNavigationBar: configureState.maybeWhen( + data: (configure) { + if (configure == null) return const SizedBox(); + + final deliveryType = DeliveryFeeType.values.byName( + configure.deliveryFeeType!, + ); + + double deliveryFee = 0.0; + + double totalDeliveryFee = 0.0; + + if (_deliveryType == DeliveryType.delivery) { + if (deliveryType == DeliveryFeeType.fixed) { + deliveryFee = configure.deliveryFlatFee!; + totalDeliveryFee = deliveryFee; + } else { + double distance = 1; + + if (currentOutletCoordinate != null && + selectedAddress != null) + distance = _calculateDistance( + currentOutletCoordinate.latitude, + currentOutletCoordinate.longitude, + selectedAddress.latitude, + selectedAddress.longitude, + ); + deliveryFee = configure.deliveryFeePerKm!; + totalDeliveryFee = deliveryFee * distance; + } + } + + final totalAmount = totalOrder + totalDeliveryFee; + + final maxDeliveryRadiusKm = configure.maxDeliveryRadiusKm; + + return _buildBottomCheckoutAction( + deliveryFeeType: deliveryType.type, + deliveryFee: deliveryFee, + isLoading: isLoading, + totalAmount: totalAmount, + totalDeliveryFee: totalDeliveryFee, + maxDeliveryRadiusKm: maxDeliveryRadiusKm ?? 0.0, + ); + }, + orElse: () => const SizedBox(), + ), + ), + ); + }, + ); + } + + Widget _buildDeliveryAddressSection({ + required String customerName, + required String customerPhone, + SelectedAddress? address, + }) { + if (address == null) + return GestureDetector( + onTap: () { + context.pushNamed(AppRoute.shippingAddressScreen); + }, + child: Container( + color: Colors.white, + padding: EdgeInsets.all(4.w), + child: Container( + padding: EdgeInsets.symmetric(vertical: 2.h, horizontal: 4.w), + decoration: BoxDecoration( + color: AppColor.primaryColor.withOpacity(0.05), + borderRadius: BorderRadius.circular(2.5.w), + border: Border.all( + color: AppColor.primaryColor.withOpacity(0.2), + style: BorderStyle.solid, + ), + ), + child: Row( + children: [ + CircleAvatar( + radius: 6.w, + backgroundColor: Colors.white, + child: Icon( + Icons.add_location_alt_outlined, + color: AppColor.primaryColor, + size: 7.w, + ), + ), + SizedBox(width: 4.w), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Alamat Belum Dipilih", + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: (AppFontSize.small - 0.5).sp, + color: Colors.black87, + ), + ), + SizedBox(height: 0.5.h), + Text( + "Klik di sini untuk memilih alamat pengiriman", + style: TextStyle( + fontSize: (AppFontSize.small - 1.5).sp, + color: Colors.grey.shade600, + ), + ), + ], + ), + ), + Icon( + Icons.arrow_forward_ios, + color: AppColor.primaryColor, + size: 4.w, + ), + ], + ), + ), + ), + ); + return Container( + color: Colors.white, + padding: EdgeInsets.all(4.w), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + Icon( + Icons.location_on, + color: AppColor.primaryColor, + size: 5.w, + ), + SizedBox(width: 2.w), + Text( + "Alamat Pengiriman", + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: AppFontSize.small.sp, + ), + ), + ], + ), + + GestureDetector( + onTap: () { + context.pushNamed(AppRoute.shippingAddressScreen); + }, + child: Text( + "Ubah", + style: TextStyle( + color: AppColor.primaryColor, + fontWeight: FontWeight.bold, + fontSize: (AppFontSize.small - 1.25).sp, + ), + ), + ), + ], + ), + + SizedBox(height: 1.5.h), + Container( + padding: EdgeInsets.symmetric(horizontal: 2.w, vertical: 1.h), + decoration: BoxDecoration( + color: Colors.grey.shade200, + borderRadius: BorderRadius.circular(1.5.w), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(Icons.home, size: 3.5.w, color: Colors.grey.shade500), + + SizedBox(width: 2.w), + + Text( + address.label!, + style: TextStyle( + color: Colors.grey.shade700, + fontSize: (AppFontSize.small - 1.25).sp, + ), + ), + ], + ), + ), + + SizedBox(height: 0.75.h), + Text( + "$customerName ($customerPhone)", + style: TextStyle( + fontSize: (AppFontSize.small - 1.25).sp, + fontWeight: FontWeight.bold, + ), + ), + Text( + address.fullAddress, + style: TextStyle( + fontSize: (AppFontSize.small - 1.25).sp, + color: Colors.black87, + height: 1.4, + ), + ), + ], + ), + ); + } + + Widget _buildDeliveryTypeSection() { + return Container( + color: Colors.white, + padding: EdgeInsets.all(4.w), + child: Column( + children: [ + Row( + children: [ + _buildChoiceChip( + "Pengiriman", + DeliveryType.delivery, + _deliveryType == DeliveryType.delivery, + ), + SizedBox(width: 3.w), + _buildChoiceChip( + "Ambil Ditoko", + DeliveryType.pick_up, + _deliveryType == DeliveryType.pick_up, + ), + ], + ), + /** + Divider(height: 3.h), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Waktu Pengiriman", + style: TextStyle(fontSize: (AppFontSize.small - 1.25).sp), + ), + Row( + children: [ + Icon(Icons.access_time, size: 14.sp, color: Colors.grey), + SizedBox(width: 1.w), + Text( + "Sekarang (30-40 Menit)", + style: TextStyle( + fontSize: (AppFontSize.small - 1.25).sp, + fontWeight: FontWeight.bold, + ), + ), + Icon(Icons.chevron_right, color: Colors.grey), + ], + ), + ], + ), + */ + ], + ), + ); + } + + Widget _buildChoiceChip( + String label, + DeliveryType deliveryType, + bool isSelected, + ) { + return Expanded( + child: GestureDetector( + onTap: () => setState(() => _deliveryType = deliveryType), + child: Container( + padding: EdgeInsets.symmetric(vertical: 1.2.h), + decoration: BoxDecoration( + color: isSelected ? AppColor.primaryColor : Colors.grey.shade100, + borderRadius: BorderRadius.circular(2.w), + ), + alignment: Alignment.center, + child: Text( + label, + style: TextStyle( + color: isSelected ? Colors.white : Colors.black87, + fontSize: AppFontSize.small.sp, + fontWeight: FontWeight.bold, + ), + ), + ), + ), + ); + } + + Widget _buildProductSummarySection( + List items, + String outletName, + LatLng? outletCoordinate, + SelectedAddress? address, + ) { + var distance = 0.0; + + if (outletCoordinate != null && address != null) { + distance = _calculateDistance( + outletCoordinate.latitude, + outletCoordinate.longitude, + address.latitude, + address.longitude, + ); + } + + var strDistance = '${distance.toStringAsFixed(1)} Km'; + + if (distance < 1) { + strDistance = '${(distance * 1000).toStringAsFixed(0)} meter'; + } + + return Container( + color: Colors.white, + padding: EdgeInsets.all(4.w), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + padding: EdgeInsets.all(3.w), + decoration: BoxDecoration( + color: AppColor.primaryColor.withOpacity(0.05), + borderRadius: BorderRadius.circular(2.w), + border: Border.all(color: AppColor.primaryColor.withOpacity(0.1)), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + Container( + padding: EdgeInsets.all(2.w), + decoration: const BoxDecoration( + color: Colors.white, + shape: BoxShape.circle, + ), + child: Icon( + Icons.storefront, + color: AppColor.primaryColor, + size: 5.w, + ), + ), + SizedBox(width: 3.w), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + _deliveryType == DeliveryType.delivery + ? "Pesanan dikirim dari:" + : "Tempat pengambilan:", + style: TextStyle( + fontSize: (AppFontSize.small - 1.25).sp, + color: Colors.grey.shade600, + ), + ), + + SizedBox(height: 0.5.h), + + Text( + outletName, + style: TextStyle( + fontSize: (AppFontSize.small - 1.25).sp, + fontWeight: FontWeight.bold, + color: Colors.black87, + ), + ), + if (distance > 0) ...[ + SizedBox(height: 0.5.h), + Text( + "Jarak \u00B1 $strDistance dari lokasi kamu", + style: TextStyle( + fontSize: (AppFontSize.small - 1.25).sp, + color: AppColor.primaryColor, + fontWeight: FontWeight.w500, + ), + ), + ], + ], + ), + ), + ], + ), + ), + + Divider(height: 4.h), + ...List.generate(items.length, (index) { + final item = items[index]; + return Padding( + padding: EdgeInsets.only(bottom: 2.h), + child: Row( + children: [ + CachedNetworkImage( + imageUrl: item.image ?? 'error', + imageBuilder: (context, imageProvider) { + return Container( + width: 15.w, + height: 15.w, + decoration: BoxDecoration( + color: Colors.grey.shade100, + border: BoxBorder.all(color: Colors.grey.shade300), + borderRadius: BorderRadius.circular(2.w), + image: DecorationImage( + image: imageProvider, + fit: BoxFit.cover, + ), + ), + ); + }, + errorWidget: (context, url, error) { + return Container( + width: 15.w, + height: 15.w, + decoration: BoxDecoration( + color: Colors.grey.shade100, + borderRadius: BorderRadius.circular(2.w), + ), + child: Icon(Icons.image, color: Colors.grey.shade300), + ); + }, + ), + + SizedBox(width: 3.w), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + item.name, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle(fontSize: AppFontSize.small.sp), + ), + if (item.isProductVariant) + Text( + item.variantName, + style: TextStyle( + fontSize: (AppFontSize.small - 1.25).sp, + color: Colors.grey.shade500, + ), + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + CurrencyFormat.formatToIdr(item.sellingPrice, 0), + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: (AppFontSize.small - 1.25).sp, + color: AppColor.primaryColor, + ), + ), + Text( + "x ${item.quantity}", + style: TextStyle( + color: Colors.grey.shade500, + fontSize: AppFontSize.small.sp, + ), + ), + ], + ), + ], + ), + ), + ], + ), + ); + }), + ], + ), + ); + } + + Widget _buildDeliveryPreferenceSection({ + required bool isCloseService, + required int onlineOpenTime, + required int onlineCloseTime, + required int preparationTimeMinutes, + required int orderIntervalMinutes, + }) { + var slots = []; + if (preparationTimeMinutes > 0 || orderIntervalMinutes > 0) + slots = _generateAvailableSlots( + onlineOpenTime: onlineOpenTime, + onlineCloseTime: onlineCloseTime, + preparationTimeMinutes: preparationTimeMinutes, + orderIntervalMinutes: orderIntervalMinutes, + ); + + return Container( + color: Colors.white, + padding: EdgeInsets.all(4.w), + child: isCloseService || DateTime.now().hour >= onlineCloseTime + ? Center( + child: Text( + "Waktu pengiriman tidak tersedia", + style: TextStyle( + color: Colors.grey.shade500, + fontSize: AppFontSize.small.sp, + ), + ), + ) + : Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + _deliveryType == DeliveryType.delivery + ? "Waktu Pengiriman" + : "Waktu Pengambilan", + style: TextStyle( + fontSize: AppFontSize.small.sp, + fontWeight: FontWeight.bold, + ), + ), + SizedBox(height: 1.h), + Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon( + Icons.info_outline, + size: 3.5.w, + color: AppColor.primaryColor, + ), + SizedBox(width: 0.75.w), + Text( + _deliveryType == DeliveryType.delivery + ? "Waktu produk kamu dikirim dari toko" + : "Waktu produk kamu di ambil di toko", + style: TextStyle( + color: AppColor.primaryColor, + fontSize: (AppFontSize.small - 1.25).sp, + ), + ), + ], + ), + ], + ), + /** + SizedBox(height: 2.h), + Row( + children: [ + _buildDayRadio("Sekarang"), + SizedBox(width: 5.w), + _buildDayRadio("Besok"), + ], + ), + */ + SizedBox(height: 2.h), + + _buildDayRadio(), + + SizedBox(height: 2.5.h), + + SingleChildScrollView( + clipBehavior: Clip.none, + scrollDirection: Axis.horizontal, + child: Row( + children: [ + if (DateTime.now().hour >= onlineOpenTime) + _buildTimeButton( + label: "Segera", + icon: Icons.history, + isActive: _deliveryPreference == "Segera", + onTap: () => + setState(() => _deliveryPreference = "Segera"), + ), + + if (slots.isNotEmpty) + ...List.generate(slots.length, (index) { + final label = _formatRange(slots[index]); + return _buildTimeButton( + label: label, + icon: Icons.access_time, + isActive: _deliveryPreference == label, + onTap: () { + setState(() => _deliveryPreference = label); + }, + ); + }), + ], + ), + ), + ], + ), + ); + } + + Widget _buildDayRadio() { + return Row( + children: [ + Icon( + Icons.radio_button_checked, + color: AppColor.primaryColor, + size: 5.w, + ), + SizedBox(width: 2.w), + Text( + "Hari Ini", + style: TextStyle( + fontSize: (AppFontSize.small - 1.25).sp, + color: AppColor.primaryColor, + fontWeight: FontWeight.bold, + ), + ), + ], + ); + } + + Widget _buildTimeButton({ + required String label, + required IconData icon, + required bool isActive, + required VoidCallback onTap, + }) { + return GestureDetector( + onTap: onTap, + child: Container( + width: 45.w, + padding: EdgeInsets.symmetric(vertical: 1.2.h, horizontal: 2.w), + margin: EdgeInsets.only(right: 3.w), + decoration: BoxDecoration( + color: isActive ? AppColor.primaryColor : Colors.grey.shade50, + borderRadius: BorderRadius.circular(2.w), + border: Border.all( + color: isActive ? AppColor.primaryColor : Colors.grey.shade300, + ), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(icon, size: 5.w, color: isActive ? Colors.white : Colors.grey), + SizedBox(width: 2.w), + Flexible( + child: Text( + label, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontSize: (AppFontSize.small - 1.25).sp, + fontWeight: FontWeight.bold, + color: isActive ? Colors.white : Colors.grey.shade700, + ), + ), + ), + ], + ), + ), + ); + } + + Widget _buildNoteSection() { + return Container( + color: Colors.white, + padding: EdgeInsets.all(4.w), + child: TextField( + controller: _noteController, + style: TextStyle(fontSize: AppFontSize.small.sp), + decoration: InputDecoration( + hintText: "Tambah Catatan", + hintStyle: TextStyle(fontSize: AppFontSize.small.sp), + prefixIcon: Icon( + Icons.note_alt_outlined, + color: Colors.grey, + size: 5.w, + ), + filled: true, + fillColor: Colors.grey.shade50, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + borderSide: BorderSide.none, + ), + ), + ), + ); + } + + Widget _buildPaymentMethodSection( + List paymentMethods, + ) { + if (_selectedPaymentMethod == "COD" || + _selectedPaymentMethod == 'Bayar Ditoko') { + _selectedPaymentMethod = _deliveryType == DeliveryType.delivery + ? "COD" + : "Bayar Ditoko"; + } + + final visibleMethods = _isPaymentExpanded + ? paymentMethods + : paymentMethods.take(3).toList(); + + return Container( + color: Colors.white, + padding: EdgeInsets.all(4.w), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Metode Pembayaran", + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: AppFontSize.small.sp, + ), + ), + SizedBox(height: 2.h), + + ...List.generate(visibleMethods.length, (index) { + final method = visibleMethods[index]; + return Column( + children: [ + RadioListTile( + value: method.sourceName.capitalize(), + groupValue: _selectedPaymentMethod, + onChanged: (val) { + setState(() { + _selectedPaymentMethod = val!; + _paymentProofPath = null; + }); + }, + activeColor: AppColor.primaryColor, + contentPadding: EdgeInsets.zero, + title: Row( + children: [ + method.sourceName == 'COD' || + method.sourceName == 'Bayar Ditoko' + ? Icon( + Icons.payments_outlined, + size: 7.w, + color: AppColor.primaryColor, + ) + : Image.asset( + _fetchPaymentMethodAsset(method.sourceName), + width: 7.w, + fit: BoxFit.contain, + ), + SizedBox(width: 2.5.w), + Text( + method.sourceName.capitalize(), + style: TextStyle( + fontSize: (AppFontSize.small - 1.25).sp, + ), + ), + ], + ), + ), + + if (_selectedPaymentMethod == method.sourceName.capitalize() && + method.sourceName != 'COD' && + method.sourceName != 'Bayar Ditoko') + _buildUploadProofSection(method), + ], + ); + }), + + if (paymentMethods.length > 3) + Center( + child: TextButton.icon( + style: TextButton.styleFrom(overlayColor: Colors.transparent), + onPressed: () => + setState(() => _isPaymentExpanded = !_isPaymentExpanded), + icon: Icon( + _isPaymentExpanded + ? Icons.keyboard_arrow_up + : Icons.keyboard_arrow_down, + color: Colors.grey, + size: 5.w, + ), + label: Text( + _isPaymentExpanded ? "Tutup" : "Selengkapnya", + style: TextStyle( + color: Colors.grey, + fontSize: (AppFontSize.small - 1.25).sp, + ), + ), + ), + ), + ], + ), + ); + } + + Widget _buildUploadProofSection(TenantPaymentMethodModel paymentMethod) { + final File imageFile = File(_paymentProofPath ?? "image not found"); + + final bool imageFileExists = imageFile.existsSync(); + + return Container( + margin: EdgeInsets.only(left: 12.w, bottom: 2.h, right: 2.w), + padding: EdgeInsets.all(3.w), + decoration: BoxDecoration( + color: Colors.grey.shade50, + borderRadius: BorderRadius.circular(2.w), + border: Border.all(color: Colors.grey.shade200), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Tujuan Transfer:", + style: TextStyle( + fontSize: (AppFontSize.small - 1.25).sp, + color: Colors.grey.shade700, + ), + ), + SizedBox(height: 0.5.h), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + paymentMethod.accountNumber, + style: TextStyle( + fontSize: (AppFontSize.small - 1.25).sp, + fontWeight: FontWeight.bold, + color: AppColor.primaryColor, + ), + ), + Text( + paymentMethod.receipentName, + style: TextStyle( + fontSize: (AppFontSize.small - 1.25).sp, + color: Colors.black87, + ), + ), + ], + ), + ), + IconButton( + onPressed: () { + Clipboard.setData( + ClipboardData(text: paymentMethod.accountNumber), + ); + CustomSnackbar.showSuccess( + context, + "Nomor akun berhasil disalin", + ); + }, + icon: Icon(Icons.copy, size: 5.w, color: Colors.grey), + padding: EdgeInsets.zero, + constraints: const BoxConstraints(), + ), + ], + ), + + Divider(height: 3.h, color: Colors.grey.shade200), + + Text( + "Upload Bukti Pembayaran", + style: TextStyle( + fontSize: (AppFontSize.small - 1.25).sp, + fontWeight: FontWeight.bold, + color: Colors.black87, + ), + ), + SizedBox(height: 1.5.h), + InkWell( + onTap: _showPickerOptions, + child: Stack( + children: [ + imageFileExists + ? GestureDetector( + onTap: () { + if (!imageFileExists) { + CustomSnackbar.showError( + context, + "Ups, terjadi kesalahan", + ); + return; + } + + showDialog( + context: context, + useRootNavigator: true, + builder: (context) => Dialog( + insetPadding: EdgeInsets.symmetric( + horizontal: 2.w, + vertical: 2.h, + ), + backgroundColor: Colors.transparent, + child: GestureDetector( + onTap: () => Navigator.pop(context), + child: Container( + width: MediaQuery.of(context).size.width, + constraints: BoxConstraints( + maxWidth: 85.w, + maxHeight: 85.h, + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(4.w), + child: InteractiveViewer( + child: Image.file( + imageFile, + fit: BoxFit.contain, + width: double.infinity, + ), + ), + ), + ), + ), + ), + ); + }, + child: Container( + width: double.infinity, + height: 12.h, + decoration: BoxDecoration( + border: BoxBorder.all(color: Colors.grey.shade300), + borderRadius: BorderRadius.circular(2.w), + image: DecorationImage( + image: FileImage(imageFile), + fit: BoxFit.cover, + ), + ), + ), + ) + : Container( + width: double.infinity, + height: 12.h, + decoration: BoxDecoration( + border: Border.all( + color: Colors.grey.shade300, + style: BorderStyle.solid, + ), + borderRadius: BorderRadius.circular(2.w), + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.cloud_upload_outlined, + color: AppColor.primaryColor, + size: 7.w, + ), + SizedBox(height: 1.h), + Text( + "Klik untuk upload gambar", + style: TextStyle( + fontSize: (AppFontSize.small - 1.25).sp, + color: Colors.grey, + ), + ), + ], + ), + ), + + if (_paymentProofPath != null) + Positioned( + top: 1.w, + right: 3.5.w, + child: Material( + color: Colors.transparent, + type: MaterialType.transparency, + child: InkWell( + onTap: _removeImage, + child: Container( + padding: EdgeInsets.all(1.w), + decoration: BoxDecoration( + color: Colors.red[50], + shape: BoxShape.circle, + border: Border.all( + color: Colors.red.withOpacity(0.2), + ), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.1), + blurRadius: 2, + offset: Offset(0, 1), + ), + ], + ), + child: Icon( + Icons.remove, + color: Colors.red, + size: 4.w, + ), + ), + ), + ), + ), + ], + ), + ), + ], + ), + ); + } + + Widget _buildPaymentDetailSection({ + required double totalOrder, + required bool isAddressSelcted, + required DeliveryFeeType deliveryFeeType, + required double deliveryFee, + required String strDistance, + required double totalDeliveryFee, + required double totalAmount, + }) { + return Container( + color: Colors.white, + padding: EdgeInsets.all(4.w), + child: Column( + children: [ + _buildRowDetail( + "Subtotal Pesanan", + CurrencyFormat.formatToIdr(totalOrder, 0), + ), + if (_deliveryType == DeliveryType.delivery && isAddressSelcted) ...[ + if (deliveryFeeType == DeliveryFeeType.per_km) ...[ + _buildRowDetail( + "Biaya Pengiriman Per Km", + CurrencyFormat.formatToIdr(deliveryFee, 0), + ), + _buildRowDetail( + "Jarak alamatmu dengan toko", + "\u00B1 $strDistance", + ), + ], + _buildRowDetail( + "Subtotal Pengiriman", + CurrencyFormat.formatToIdr(totalDeliveryFee, 0), + ), + ], + const Divider(), + _buildRowDetail( + "Total Pembayaran", + CurrencyFormat.formatToIdr(totalAmount, 0), + isBold: true, + ), + ], + ), + ); + } + + Widget _buildRowDetail(String label, String value, {bool isBold = false}) { + return Padding( + padding: EdgeInsets.symmetric(vertical: 0.5.h), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + label, + style: TextStyle( + fontSize: (AppFontSize.small - (isBold ? 0 : 1.25)).sp, + fontWeight: isBold ? FontWeight.bold : FontWeight.normal, + ), + ), + Text( + value, + style: TextStyle( + fontSize: (AppFontSize.small - (isBold ? 0 : 1.25)).sp, + fontWeight: isBold ? FontWeight.bold : FontWeight.normal, + color: isBold ? AppColor.primaryColor : Colors.black, + ), + ), + ], + ), + ); + } + + Widget _buildBottomCheckoutAction({ + String? deliveryFeeType, + required double deliveryFee, + required bool isLoading, + required double totalAmount, + required double totalDeliveryFee, + required double maxDeliveryRadiusKm, + }) { + return Container( + height: 8.h, + decoration: BoxDecoration( + color: Colors.white, + border: Border(top: BorderSide(color: Colors.grey.shade200)), + ), + child: Row( + children: [ + Expanded( + child: Padding( + padding: EdgeInsets.only(left: 4.w), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Total Pembayaran", + style: TextStyle(fontSize: AppFontSize.small.sp), + ), + Text( + CurrencyFormat.formatToIdr(totalAmount, 0), + style: TextStyle( + fontSize: AppFontSize.small.sp, + fontWeight: FontWeight.bold, + color: AppColor.primaryColor, + ), + ), + ], + ), + ), + ), + InkWell( + onTap: isLoading + ? null + : () async => await _checkout( + deliveryFeeType: deliveryFeeType, + deliveryFee: deliveryFee, + totalDeliveryFee: totalDeliveryFee, + maxDeliveryRadiusKm: maxDeliveryRadiusKm, + ), + child: Container( + width: 40.w, + height: double.infinity, + color: isLoading ? Colors.grey.shade300 : AppColor.primaryColor, + alignment: Alignment.center, + child: Text( + "Buat Pesanan", + style: TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + fontSize: AppFontSize.small.sp, + ), + ), + ), + ), + ], + ), + ); + } +} diff --git a/Mobile Commerce/lib/features/checkout/presentation/screens/shipping_address_screen.dart b/Mobile Commerce/lib/features/checkout/presentation/screens/shipping_address_screen.dart new file mode 100644 index 0000000..016d0f9 --- /dev/null +++ b/Mobile Commerce/lib/features/checkout/presentation/screens/shipping_address_screen.dart @@ -0,0 +1,251 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; +import 'package:niogu_ecommerce_v1/core/constant/app_color.dart'; +import 'package:niogu_ecommerce_v1/core/constant/app_font_size.dart'; +import 'package:niogu_ecommerce_v1/core/router/app_route.dart'; +import 'package:niogu_ecommerce_v1/core/widgets/custom_empty_screen.dart'; +import 'package:niogu_ecommerce_v1/features/account/domain/entities/account.dart'; +import 'package:niogu_ecommerce_v1/features/account/presentation/providers/account_provider.dart'; +import 'package:sizer/sizer.dart'; + +class ShippingAddressScreen extends ConsumerWidget { + const ShippingAddressScreen({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + return LayoutBuilder( + builder: (context, constraints) { + final customerAddressState = ref.watch( + customerAddressControllerProvider, + ); + + final selectedAddress = ref.watch(selectedAddressProvider); + + return SafeArea( + top: false, + bottom: true, + right: false, + left: false, + child: Scaffold( + backgroundColor: const Color(0xFFF8F9FA), + appBar: AppBar( + backgroundColor: Colors.white, + elevation: 0.5, + leading: IconButton( + icon: Icon( + Icons.arrow_back, + color: AppColor.primaryColor, + size: 7.w, + ), + onPressed: () => context.pop(), + ), + title: Text( + "Pilih Alamat Pengiriman", + style: TextStyle( + color: Colors.black, + fontSize: AppFontSize.medium.sp, + fontWeight: FontWeight.bold, + ), + ), + ), + body: Column( + children: [ + Expanded( + child: customerAddressState.when( + data: (addresses) { + if (addresses.isEmpty) { + return CustomEmptyScreen( + icon: Icons.search_off_outlined, + title: "Belum Ada Alamat", + subtitle: "Tambahkan alamat pertamamu", + height: 40.h, + ); + } + return ListView.builder( + padding: EdgeInsets.symmetric(vertical: 2.h), + itemCount: addresses.length, + itemBuilder: (context, index) { + final address = addresses[index]; + final isSelected = + selectedAddress?.id == address.uuid; + + return _buildSelectableAddressItem( + context, + ref, + address, + isSelected, + ); + }, + ); + }, + error: (error, stackTrace) => CustomEmptyScreen( + icon: Icons.cloud_off_outlined, + title: "Terjadi Kesalahan Koneksi", + height: 40.h, + ), + loading: () => const Center( + child: CircularProgressIndicator( + color: AppColor.primaryColor, + backgroundColor: Colors.white, + ), + ), + ), + ), + + if (!customerAddressState.isLoading) ...[ + if (customerAddressState.hasValue && + customerAddressState.value!.isNotEmpty) + _buildConfirmButton(context, ref, selectedAddress) + else + _buildAddAddressButton(context), + ], + ], + ), + ), + ); + }, + ); + } + + Widget _buildSelectableAddressItem( + BuildContext context, + WidgetRef ref, + CustomerAddress address, + bool isSelected, + ) { + final selectedAddress = SelectedAddress( + id: address.uuid, + label: address.label, + fullAddress: address.fullAddress, + latitude: address.latitude, + longitude: address.longitude, + ); + return GestureDetector( + onTap: () => + ref.read(selectedAddressProvider.notifier).state = selectedAddress, + child: Container( + margin: EdgeInsets.fromLTRB(4.w, 0, 4.w, 1.5.h), + padding: EdgeInsets.all(4.w), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(2.5.w), + border: Border.all(color: Colors.grey.shade200, width: 1), + ), + child: Row( + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + address.label, + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: AppFontSize.small.sp, + ), + ), + SizedBox(height: 1.h), + Text( + address.fullAddress, + style: TextStyle( + fontSize: (AppFontSize.small - 1).sp, + color: Colors.grey.shade600, + ), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + ], + ), + ), + if (isSelected) + Icon(Icons.check_circle, color: AppColor.primaryColor, size: 7.w) + else + Icon( + Icons.radio_button_unchecked, + color: Colors.grey.shade300, + size: 7.w, + ), + ], + ), + ), + ); + } + + Widget _buildConfirmButton( + BuildContext context, + WidgetRef ref, + SelectedAddress? selected, + ) { + return Container( + padding: EdgeInsets.all(4.w), + decoration: const BoxDecoration( + color: Colors.white, + boxShadow: [BoxShadow(color: Colors.black12, blurRadius: 4)], + ), + child: SafeArea( + child: SizedBox( + width: double.infinity, + child: ElevatedButton( + onPressed: selected == null + ? null + : () { + ref.read(selectedAddressProvider.notifier).state = selected; + context.pop(); + }, + style: ElevatedButton.styleFrom( + backgroundColor: AppColor.primaryColor, + padding: EdgeInsets.symmetric(vertical: 1.8.h), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(2.5.w), + ), + disabledBackgroundColor: Colors.grey.shade300, + ), + child: Text( + "Pilih Alamat Ini", + style: TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + fontSize: AppFontSize.medium.sp, + ), + ), + ), + ), + ), + ); + } + + Widget _buildAddAddressButton(BuildContext context) { + return Container( + padding: EdgeInsets.all(4.w), + child: SafeArea( + child: SizedBox( + width: double.infinity, + child: OutlinedButton.icon( + onPressed: () => context.pushNamed(AppRoute.mapAddressScreen), + icon: Icon( + Icons.add_circle_outline, + size: 5.w, + color: AppColor.primaryColor, + ), + label: Text( + "Tambah Alamat Baru", + style: TextStyle( + color: AppColor.primaryColor, + fontWeight: FontWeight.bold, + fontSize: AppFontSize.medium.sp, + ), + ), + style: OutlinedButton.styleFrom( + padding: EdgeInsets.symmetric(vertical: 1.8.h), + side: const BorderSide(color: AppColor.primaryColor), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(2.5.w), + ), + ), + ), + ), + ), + ); + } +} diff --git a/Mobile Commerce/lib/features/favorite/domain/entities/favorite.dart b/Mobile Commerce/lib/features/favorite/domain/entities/favorite.dart new file mode 100644 index 0000000..35f06bc --- /dev/null +++ b/Mobile Commerce/lib/features/favorite/domain/entities/favorite.dart @@ -0,0 +1,21 @@ +class SelectedFavorite { + final String id; + final String outletId; + final String? image; + final String name; + final double totalSold; + final double averageRating; + final double sellingPrice; + final int likes; + + const SelectedFavorite({ + required this.id, + required this.outletId, + this.image, + required this.name, + required this.totalSold, + required this.averageRating, + required this.sellingPrice, + required this.likes, + }); +} \ No newline at end of file diff --git a/Mobile Commerce/lib/features/favorite/presentation/controllers/favorite_controller.dart b/Mobile Commerce/lib/features/favorite/presentation/controllers/favorite_controller.dart new file mode 100644 index 0000000..fc81148 --- /dev/null +++ b/Mobile Commerce/lib/features/favorite/presentation/controllers/favorite_controller.dart @@ -0,0 +1,28 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:niogu_ecommerce_v1/features/favorite/domain/entities/favorite.dart'; +import 'package:niogu_ecommerce_v1/features/home/domain/entities/home.dart'; + +class FavoriteController extends StateNotifier> { + FavoriteController() : super({}); + + void toggle(ProductItem product, String outletId) { + final key = "$outletId-${product.id}"; + if (state.containsKey(key)) { + state = {...state}..remove(key); + } else { + state = { + ...state, + key: SelectedFavorite( + id: product.id, + image: product.image, + outletId: outletId, + name: product.name, + totalSold: product.totalSold, + averageRating: product.averageRating, + sellingPrice: product.sellingPrice, + likes: product.likes, + ), + }; + } + } +} diff --git a/Mobile Commerce/lib/features/favorite/presentation/providers/favorite_provider.dart b/Mobile Commerce/lib/features/favorite/presentation/providers/favorite_provider.dart new file mode 100644 index 0000000..fedbd2c --- /dev/null +++ b/Mobile Commerce/lib/features/favorite/presentation/providers/favorite_provider.dart @@ -0,0 +1,21 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:niogu_ecommerce_v1/core/providers/app_provider.dart'; +import 'package:niogu_ecommerce_v1/features/favorite/domain/entities/favorite.dart'; +import 'package:niogu_ecommerce_v1/features/favorite/presentation/controllers/favorite_controller.dart'; + +final favoriteControllerProvider = + StateNotifierProvider>(( + ref, + ) { + return FavoriteController(); + }); + +final favoriteProvider = Provider>((ref) { + final currentOutletId = ref.watch(currentOutletIdProvider); + + final favoriteController = ref.watch(favoriteControllerProvider); + + return favoriteController.values + .where((f) => f.outletId == currentOutletId) + .toList(); +}); diff --git a/Mobile Commerce/lib/features/favorite/presentation/screens/favorite_screen.dart b/Mobile Commerce/lib/features/favorite/presentation/screens/favorite_screen.dart new file mode 100644 index 0000000..a09c3bb --- /dev/null +++ b/Mobile Commerce/lib/features/favorite/presentation/screens/favorite_screen.dart @@ -0,0 +1,323 @@ +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; +import 'package:niogu_ecommerce_v1/core/constant/app_color.dart'; +import 'package:niogu_ecommerce_v1/core/constant/app_font_size.dart'; +import 'package:niogu_ecommerce_v1/core/errors/exceptions.dart'; +import 'package:niogu_ecommerce_v1/core/providers/app_provider.dart'; +import 'package:niogu_ecommerce_v1/core/router/app_route.dart'; +import 'package:niogu_ecommerce_v1/core/utils/currency_format.dart'; +import 'package:niogu_ecommerce_v1/core/utils/extenstion_format.dart'; +import 'package:niogu_ecommerce_v1/core/utils/log_message.dart'; +import 'package:niogu_ecommerce_v1/core/widgets/custom_empty_screen.dart'; +import 'package:niogu_ecommerce_v1/core/widgets/custom_snackbar.dart'; +import 'package:niogu_ecommerce_v1/features/favorite/presentation/providers/favorite_provider.dart'; +import 'package:niogu_ecommerce_v1/features/home/domain/entities/home.dart'; +import 'package:niogu_ecommerce_v1/features/home/presentation/providers/home_provider.dart'; +import 'package:niogu_ecommerce_v1/features/product/presentation/providers/product_provider.dart'; +import 'package:sizer/sizer.dart'; + +class FavoriteScreen extends ConsumerStatefulWidget { + const FavoriteScreen({super.key}); + + @override + ConsumerState createState() => _FavoriteScreenState(); +} + +class _FavoriteScreenState extends ConsumerState { + @override + Widget build(BuildContext context) { + return LayoutBuilder( + builder: (context, constraints) { + final currentOutletId = ref.watch(currentOutletIdProvider); + + final favoriteState = ref.watch(favoriteControllerProvider); + + final favoriteListState = ref.watch(favoriteProvider); + + return SafeArea( + top: false, + bottom: true, + right: false, + left: false, + child: Scaffold( + backgroundColor: Colors.white, + appBar: AppBar( + backgroundColor: Colors.white, + elevation: 0, + centerTitle: true, + title: Text( + "Favorit Saya", + style: TextStyle( + color: Colors.black, + fontSize: AppFontSize.medium.sp, + fontWeight: FontWeight.bold, + ), + ), + ), + body: favoriteListState.isEmpty + ? CustomEmptyScreen( + icon: Icons.search_off_outlined, + title: "Belum Ada Favorit Produk", + subtitle: "Tambahkan produk favorit pertamamu", + height: 40.h, + ) + : GridView.builder( + padding: EdgeInsets.all(4.w), + itemCount: favoriteListState.length, + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 2, + childAspectRatio: 0.7, + crossAxisSpacing: 3.w, + mainAxisSpacing: 3.w, + ), + itemBuilder: (context, index) { + final favorite = favoriteListState[index]; + + final product = ProductItem( + id: favorite.id, + image: favorite.image, + name: favorite.name, + totalSold: favorite.totalSold, + averageRating: favorite.averageRating, + sellingPrice: favorite.sellingPrice, + likes: favorite.likes, + ); + + final isFavorite = favoriteState.containsKey( + "$currentOutletId-${product.id}", + ); + + return _buildProductCard( + product, + currentOutletId!, + isFavorite, + ); + }, + ), + ), + ); + }, + ); + } + + Widget _buildProductCard( + ProductItem product, + String currentOutletId, + bool isFavorite, + ) { + return GestureDetector( + onTap: () async { + try { + final productDetail = await ref + .read(productRepositoryProvider) + .fetchProductById(product.id); + + if (productDetail == null) { + CustomSnackbar.showError(context, "Produk tidak ditemukan"); + ref.read(homeControllerProvider.notifier).refresh(); + return; + } + + context.pushNamed(AppRoute.productDetailScreen, extra: productDetail); + } on ServerException catch (e, st) { + LogMessage.log.i(e.toString(), error: e, stackTrace: st); + } + }, + child: Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(2.5.w), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.04), + blurRadius: 10, + offset: const Offset(0, 4), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + flex: 3, + child: CachedNetworkImage( + imageUrl: product.image ?? 'error', + imageBuilder: (context, imageProvider) { + return Container( + width: double.infinity, + decoration: BoxDecoration( + color: Colors.grey.shade100, + borderRadius: BorderRadius.vertical( + top: Radius.circular(2.5.w), + ), + image: DecorationImage( + image: imageProvider, + fit: BoxFit.cover, + ), + ), + child: Stack( + children: [ + Positioned( + top: 1.w, + right: 1.w, + child: GestureDetector( + onTap: () { + final currentOutletId = ref.read( + currentOutletIdProvider, + ); + ref + .read(favoriteControllerProvider.notifier) + .toggle(product, currentOutletId!); + }, + child: CircleAvatar( + radius: 14, + backgroundColor: Colors.white.withOpacity(0.9), + child: Icon( + isFavorite + ? Icons.favorite + : Icons.favorite_outline, + size: 5.w, + color: isFavorite ? Colors.red : Colors.grey, + ), + ), + ), + ), + ], + ), + ); + }, + errorWidget: (context, url, error) { + return Container( + width: double.infinity, + decoration: BoxDecoration( + color: Colors.grey.shade100, + borderRadius: BorderRadius.vertical( + top: Radius.circular(2.5.w), + ), + ), + child: Stack( + children: [ + Center( + child: Icon( + Icons.image_outlined, + color: Colors.grey.shade300, + size: 10.w, + ), + ), + Positioned( + top: 1.w, + right: 1.w, + child: GestureDetector( + onTap: () { + ref + .read(favoriteControllerProvider.notifier) + .toggle(product, currentOutletId); + }, + child: CircleAvatar( + radius: 14, + backgroundColor: Colors.white.withOpacity(0.9), + child: Icon( + isFavorite + ? Icons.favorite + : Icons.favorite_outline, + size: 5.w, + color: isFavorite ? Colors.red : Colors.grey, + ), + ), + ), + ), + ], + ), + ); + }, + ), + ), + + Expanded( + flex: 2, + child: Padding( + padding: EdgeInsets.all(3.w), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + product.name, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: (AppFontSize.small - 1.25).sp, + color: const Color(0xFF102851), + ), + ), + + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "${product.totalSold.toCompact} Terjual", + style: TextStyle( + color: Colors.black87, + fontWeight: FontWeight.bold, + fontSize: (AppFontSize.small - 1.25).sp, + ), + ), + Row( + children: [ + Icon(Icons.star, color: Colors.orange, size: 3.5.w), + SizedBox(width: 1.w), + Text( + product.averageRating.toStringAsFixed(1), + style: TextStyle( + fontSize: (AppFontSize.small - 1.25).sp, + color: Colors.grey.shade600, + ), + ), + ], + ), + ], + ), + + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + CurrencyFormat.formatToIdr(product.sellingPrice, 0), + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: AppFontSize.small.sp, + color: AppColor.primaryColor, + ), + ), + Row( + children: [ + Icon( + Icons.thumb_up, + color: Colors.red, + size: 3.5.w, + ), + SizedBox(width: 1.w), + Text( + product.likes.toString(), + style: TextStyle( + fontSize: (AppFontSize.small - 1.25).sp, + color: Colors.red, + ), + ), + ], + ), + ], + ), + ], + ), + ), + ), + ], + ), + ), + ); + } +} diff --git a/Mobile Commerce/lib/features/home/data/datasource/home_remote_datasource_impl.dart b/Mobile Commerce/lib/features/home/data/datasource/home_remote_datasource_impl.dart new file mode 100644 index 0000000..8b3e6f8 --- /dev/null +++ b/Mobile Commerce/lib/features/home/data/datasource/home_remote_datasource_impl.dart @@ -0,0 +1,120 @@ +import 'package:dio/dio.dart'; +import 'package:niogu_ecommerce_v1/core/constant/app_url.dart'; +import 'package:niogu_ecommerce_v1/core/models/api_response_model.dart'; +import 'package:niogu_ecommerce_v1/core/models/tenant_model.dart'; +import 'package:niogu_ecommerce_v1/core/system/system_setting.dart'; +import 'package:niogu_ecommerce_v1/features/home/data/models/home_model.dart'; + +abstract class IHomeRemoteDatasource { + Future> fetchConfiguration(); + + Future> fetchHome(); + + Future>> fetchProductByName( + String name, + ); + + Future>> fetchProductCategories( + String categoryId, + ); + + Future>> fetchOutlets(); + + Future> fetchMainOutlet(); + +} + +class HomeRemoteDatasourceImpl implements IHomeRemoteDatasource { + final Dio _dio; + + HomeRemoteDatasourceImpl(this._dio); + + @override + Future> fetchConfiguration() async { + final response = await _dio.get(AppUrl.TENANT); + + return ApiResponseModel.fromJson( + response.data, + (json) => TenantModel.fromJson(json as Map), + ); + } + + @override + Future> fetchHome() async { + final currentOutletId = await SystemSetting.getCurrentOutletId(); + + final response = await _dio.get("${AppUrl.HOME}/$currentOutletId"); + + return ApiResponseModel.fromJson( + response.data, + (json) => HomeResponse.fromJson(json as Map), + ); + } + + @override + Future>> fetchProductByName( + String name, + ) async { + final currentOutletId = await SystemSetting.getCurrentOutletId(); + + final response = await _dio.get( + "${AppUrl.PRODUCTS}/search/$currentOutletId?name=$name", + ); + + return ApiResponseModel>.fromJson( + response.data, + (json) => (json as List) + .map( + (item) => + ProductItemResponse.fromJson(item as Map), + ) + .toList(), + ); + } + + @override + Future>> fetchProductCategories( + String categoryId, + ) async { + final currentOutletId = await SystemSetting.getCurrentOutletId(); + + final response = await _dio.get( + "${AppUrl.PRODUCTS}/categories/$currentOutletId/$categoryId", + ); + + return ApiResponseModel>.fromJson( + response.data, + (json) => (json as List) + .map( + (item) => + ProductItemResponse.fromJson(item as Map), + ) + .toList(), + ); + } + + @override + Future>> fetchOutlets() async { + final response = await _dio.get(AppUrl.OUTLETS); + + return ApiResponseModel>.fromJson( + response.data, + (json) => (json as List) + .map( + (item) => + OtherOutletResponse.fromJson(item as Map), + ) + .toList(), + ); + } + + @override + Future> fetchMainOutlet() async { + final response = await _dio.get("${AppUrl.OUTLETS}/main"); + + return ApiResponseModel.fromJson( + response.data, + (json) => MainOutletResponse.fromJson(json as Map), + ); + } +} diff --git a/Mobile Commerce/lib/features/home/data/models/home_model.dart b/Mobile Commerce/lib/features/home/data/models/home_model.dart new file mode 100644 index 0000000..590e898 --- /dev/null +++ b/Mobile Commerce/lib/features/home/data/models/home_model.dart @@ -0,0 +1,349 @@ +import 'package:json_annotation/json_annotation.dart'; +import 'package:latlong2/latlong.dart'; +import 'package:niogu_ecommerce_v1/core/enums/campaign_type.dart'; +import 'package:niogu_ecommerce_v1/core/utils/converter.dart'; +import 'package:niogu_ecommerce_v1/features/home/domain/entities/home.dart'; + +part 'home_model.g.dart'; + +@JsonSerializable() +class HomeResponse { + @JsonKey(name: 'current_outlet') + final CurrentOutletResponse currentOutlet; + + @JsonKey(name: 'campaign_by_outlets') + final List campaignByOutlets; + + @JsonKey(name: 'all_categories') + final List allCategories; + + @JsonKey(name: 'other_outlets') + final List otherOutlets; + + @JsonKey(name: 'product_recommendations') + final List productRecommendations; + + @JsonKey(name: 'all_products') + final List allProducts; + + HomeResponse({ + required this.currentOutlet, + required this.campaignByOutlets, + required this.allCategories, + required this.otherOutlets, + required this.productRecommendations, + required this.allProducts, + }); + + factory HomeResponse.fromJson(Map json) => + _$HomeResponseFromJson(json); + + Map toJson() => _$HomeResponseToJson(this); + + Home toHome() { + return Home( + currentOutlet: currentOutlet.toCurrentOutlet(), + campaigns: campaignByOutlets + .map((campaign) => campaign.toCampaignByOutlet()) + .toList(), + categories: allCategories + .map((category) => category.toCategoryItem()) + .toList(), + otherOutlets: otherOutlets + .map((outlet) => outlet.toOtherOutlet()) + .toList(), + recommendations: productRecommendations + .map((product) => product.toProductItem()) + .toList(), + allProducts: allProducts + .map((product) => product.toProductItem()) + .toList(), + ); + } +} + +@JsonSerializable() +class OperationalServiceResponse { + @JsonKey(name: 'online_open_time') + final String onlineOpenTime; + + @JsonKey(name: 'online_close_time') + final String onlineCloseTime; + + @JsonKey(name: 'is_close_service') + final bool isCloseService; + + const OperationalServiceResponse({ + required this.onlineOpenTime, + required this.onlineCloseTime, + required this.isCloseService, + }); + + factory OperationalServiceResponse.fromJson(Map json) => + _$OperationalServiceResponseFromJson(json); + + Map toJson() => _$OperationalServiceResponseToJson(this); + + OperationalService toOperationalService() { + return OperationalService( + onlineOpenTime: onlineOpenTime, + onlineCloseTime: onlineCloseTime, + isCloseService: isCloseService, + ); + } +} + +@JsonSerializable() +class CurrentOutletResponse { + final String id; + + final String name; + + @JsonKey(fromJson: toDouble) + final double? latitude; + + @JsonKey(fromJson: toDouble) + final double? longitude; + + final String? location; + + @JsonKey(name: 'is_active') + final bool isActive; + + CurrentOutletResponse({ + required this.id, + required this.name, + required this.location, + required this.latitude, + required this.longitude, + required this.isActive, + }); + + factory CurrentOutletResponse.fromJson(Map json) => + _$CurrentOutletResponseFromJson(json); + + Map toJson() => _$CurrentOutletResponseToJson(this); + + CurrentOutlet toCurrentOutlet() { + LatLng? coordinate; + + if (latitude != null && longitude != null) { + coordinate = LatLng(latitude!, longitude!); + } + + return CurrentOutlet( + id: id, + name: name, + location: location, + coordinate: coordinate, + isActive: isActive, + ); + } +} + +@JsonSerializable() +class CampaignByOutletResponse { + @JsonKey(name: 'campaign_type') + final String? campaignType; + + @JsonKey(name: 'action_ref_id') + final String? actionRefId; + + @JsonKey(name: 'server_banner_url') + final String? serverBannerUrl; + + CampaignByOutletResponse({ + required this.campaignType, + required this.actionRefId, + this.serverBannerUrl, + }); + + factory CampaignByOutletResponse.fromJson(Map json) => + _$CampaignByOutletResponseFromJson(json); + + Map toJson() => _$CampaignByOutletResponseToJson(this); + + CampaignByOutlet toCampaignByOutlet() { + final campaignType = this.campaignType != null + ? CampaignType.values.byName(this.campaignType!) + : null; + return CampaignByOutlet( + campaignType: campaignType, + actionRefId: actionRefId, + image: serverBannerUrl, + ); + } +} + +@JsonSerializable() +class CategoryItemResponse { + final String id; + + @JsonKey(name: 'server_image_url') + final String? serverImageUrl; + + final String name; + + CategoryItemResponse({ + required this.id, + required this.serverImageUrl, + required this.name, + }); + + factory CategoryItemResponse.fromJson(Map json) => + _$CategoryItemResponseFromJson(json); + + Map toJson() => _$CategoryItemResponseToJson(this); + + CategoryItem toCategoryItem() { + return CategoryItem(id: id, image: serverImageUrl, name: name); + } +} + +@JsonSerializable() +class MainOutletResponse { + final String uuid; + + final String name; + + @JsonKey(name: 'phone_number') + final String? phoneNumber; + + @JsonKey(name: 'full_address') + final String? location; + + @JsonKey(fromJson: toDouble) + final double? latitude; + + @JsonKey(fromJson: toDouble) + final double? longitude; + + const MainOutletResponse({ + required this.uuid, + required this.name, + required this.phoneNumber, + required this.location, + required this.latitude, + required this.longitude, + }); + + factory MainOutletResponse.fromJson(Map json) => + _$MainOutletResponseFromJson(json); + + Map toJson() => _$MainOutletResponseToJson(this); + + MainOutlet toMainOutlet() { + return MainOutlet( + uuid: uuid, + name: name, + phoneNumber: phoneNumber, + location: location, + latitude: latitude, + longitude: longitude, + ); + } +} + +@JsonSerializable() +class OtherOutletResponse { + final String id; + + @JsonKey(name: 'server_banner_url') + final String? serverBannerUrl; + + final String name; + + @JsonKey(name: 'phone_number') + final String? phoneNumber; + + final String? location; + + @JsonKey(fromJson: toDouble) + final double? latitude; + + @JsonKey(fromJson: toDouble) + final double? longitude; + + @JsonKey(name: 'is_active') + final bool isActive; + + OtherOutletResponse({ + required this.id, + required this.serverBannerUrl, + required this.name, + required this.phoneNumber, + required this.location, + required this.latitude, + required this.longitude, + required this.isActive, + }); + + factory OtherOutletResponse.fromJson(Map json) => + _$OtherOutletResponseFromJson(json); + + Map toJson() => _$OtherOutletResponseToJson(this); + + OtherOutlet toOtherOutlet() { + LatLng? coordinate; + + if (latitude != null && longitude != null) { + coordinate = LatLng(latitude!, longitude!); + } + return OtherOutlet( + id: id, + image: serverBannerUrl, + name: name, + phoneNumber: phoneNumber, + location: location, + coordinate: coordinate, + isActive: isActive, + ); + } +} + +@JsonSerializable() +class ProductItemResponse { + final String id; + + @JsonKey(name: 'server_image_url') + final String? serverImageUrl; + + final String name; + @JsonKey(name: 'total_sold', fromJson: toDouble) + final double totalSold; + + @JsonKey(name: 'average_rating', fromJson: toDouble) + final double averageRating; + + @JsonKey(name: 'selling_price', fromJson: toDouble) + final double sellingPrice; + + final int likes; + + ProductItemResponse({ + required this.id, + required this.serverImageUrl, + required this.name, + required this.totalSold, + required this.averageRating, + required this.sellingPrice, + required this.likes, + }); + + factory ProductItemResponse.fromJson(Map json) => + _$ProductItemResponseFromJson(json); + + Map toJson() => _$ProductItemResponseToJson(this); + + ProductItem toProductItem() { + return ProductItem( + id: id, + image: serverImageUrl, + name: name, + totalSold: totalSold, + averageRating: averageRating, + sellingPrice: sellingPrice, + likes: likes, + ); + } +} diff --git a/Mobile Commerce/lib/features/home/data/models/home_model.g.dart b/Mobile Commerce/lib/features/home/data/models/home_model.g.dart new file mode 100644 index 0000000..b9a69d9 --- /dev/null +++ b/Mobile Commerce/lib/features/home/data/models/home_model.g.dart @@ -0,0 +1,176 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'home_model.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +HomeResponse _$HomeResponseFromJson(Map json) => HomeResponse( + currentOutlet: CurrentOutletResponse.fromJson( + json['current_outlet'] as Map, + ), + campaignByOutlets: (json['campaign_by_outlets'] as List) + .map((e) => CampaignByOutletResponse.fromJson(e as Map)) + .toList(), + allCategories: (json['all_categories'] as List) + .map((e) => CategoryItemResponse.fromJson(e as Map)) + .toList(), + otherOutlets: (json['other_outlets'] as List) + .map((e) => OtherOutletResponse.fromJson(e as Map)) + .toList(), + productRecommendations: (json['product_recommendations'] as List) + .map((e) => ProductItemResponse.fromJson(e as Map)) + .toList(), + allProducts: (json['all_products'] as List) + .map((e) => ProductItemResponse.fromJson(e as Map)) + .toList(), +); + +Map _$HomeResponseToJson(HomeResponse instance) => + { + 'current_outlet': instance.currentOutlet, + 'campaign_by_outlets': instance.campaignByOutlets, + 'all_categories': instance.allCategories, + 'other_outlets': instance.otherOutlets, + 'product_recommendations': instance.productRecommendations, + 'all_products': instance.allProducts, + }; + +OperationalServiceResponse _$OperationalServiceResponseFromJson( + Map json, +) => OperationalServiceResponse( + onlineOpenTime: json['online_open_time'] as String, + onlineCloseTime: json['online_close_time'] as String, + isCloseService: json['is_close_service'] as bool, +); + +Map _$OperationalServiceResponseToJson( + OperationalServiceResponse instance, +) => { + 'online_open_time': instance.onlineOpenTime, + 'online_close_time': instance.onlineCloseTime, + 'is_close_service': instance.isCloseService, +}; + +CurrentOutletResponse _$CurrentOutletResponseFromJson( + Map json, +) => CurrentOutletResponse( + id: json['id'] as String, + name: json['name'] as String, + location: json['location'] as String?, + latitude: toDouble(json['latitude']), + longitude: toDouble(json['longitude']), + isActive: json['is_active'] as bool, +); + +Map _$CurrentOutletResponseToJson( + CurrentOutletResponse instance, +) => { + 'id': instance.id, + 'name': instance.name, + 'latitude': instance.latitude, + 'longitude': instance.longitude, + 'location': instance.location, + 'is_active': instance.isActive, +}; + +CampaignByOutletResponse _$CampaignByOutletResponseFromJson( + Map json, +) => CampaignByOutletResponse( + campaignType: json['campaign_type'] as String?, + actionRefId: json['action_ref_id'] as String?, + serverBannerUrl: json['server_banner_url'] as String?, +); + +Map _$CampaignByOutletResponseToJson( + CampaignByOutletResponse instance, +) => { + 'campaign_type': instance.campaignType, + 'action_ref_id': instance.actionRefId, + 'server_banner_url': instance.serverBannerUrl, +}; + +CategoryItemResponse _$CategoryItemResponseFromJson( + Map json, +) => CategoryItemResponse( + id: json['id'] as String, + serverImageUrl: json['server_image_url'] as String?, + name: json['name'] as String, +); + +Map _$CategoryItemResponseToJson( + CategoryItemResponse instance, +) => { + 'id': instance.id, + 'server_image_url': instance.serverImageUrl, + 'name': instance.name, +}; + +MainOutletResponse _$MainOutletResponseFromJson(Map json) => + MainOutletResponse( + uuid: json['uuid'] as String, + name: json['name'] as String, + phoneNumber: json['phone_number'] as String?, + location: json['full_address'] as String?, + latitude: toDouble(json['latitude']), + longitude: toDouble(json['longitude']), + ); + +Map _$MainOutletResponseToJson(MainOutletResponse instance) => + { + 'uuid': instance.uuid, + 'name': instance.name, + 'phone_number': instance.phoneNumber, + 'full_address': instance.location, + 'latitude': instance.latitude, + 'longitude': instance.longitude, + }; + +OtherOutletResponse _$OtherOutletResponseFromJson(Map json) => + OtherOutletResponse( + id: json['id'] as String, + serverBannerUrl: json['server_banner_url'] as String?, + name: json['name'] as String, + phoneNumber: json['phone_number'] as String?, + location: json['location'] as String?, + latitude: toDouble(json['latitude']), + longitude: toDouble(json['longitude']), + isActive: json['is_active'] as bool, + ); + +Map _$OtherOutletResponseToJson( + OtherOutletResponse instance, +) => { + 'id': instance.id, + 'server_banner_url': instance.serverBannerUrl, + 'name': instance.name, + 'phone_number': instance.phoneNumber, + 'location': instance.location, + 'latitude': instance.latitude, + 'longitude': instance.longitude, + 'is_active': instance.isActive, +}; + +ProductItemResponse _$ProductItemResponseFromJson(Map json) => + ProductItemResponse( + id: json['id'] as String, + serverImageUrl: json['server_image_url'] as String?, + name: json['name'] as String, + totalSold: toDouble(json['total_sold']), + averageRating: toDouble(json['average_rating']), + sellingPrice: toDouble(json['selling_price']), + likes: (json['likes'] as num).toInt(), + ); + +Map _$ProductItemResponseToJson( + ProductItemResponse instance, +) => { + 'id': instance.id, + 'server_image_url': instance.serverImageUrl, + 'name': instance.name, + 'total_sold': instance.totalSold, + 'average_rating': instance.averageRating, + 'selling_price': instance.sellingPrice, + 'likes': instance.likes, +}; diff --git a/Mobile Commerce/lib/features/home/data/repositories/home_repository_impl.dart b/Mobile Commerce/lib/features/home/data/repositories/home_repository_impl.dart new file mode 100644 index 0000000..490ee2e --- /dev/null +++ b/Mobile Commerce/lib/features/home/data/repositories/home_repository_impl.dart @@ -0,0 +1,111 @@ +import 'package:dio/dio.dart'; +import 'package:niogu_ecommerce_v1/core/errors/exceptions.dart'; +import 'package:niogu_ecommerce_v1/core/models/tenant_model.dart'; +import 'package:niogu_ecommerce_v1/core/utils/log_message.dart'; +import 'package:niogu_ecommerce_v1/features/home/data/datasource/home_remote_datasource_impl.dart'; +import 'package:niogu_ecommerce_v1/features/home/domain/entities/home.dart'; +import 'package:niogu_ecommerce_v1/features/home/domain/repositories/i_home_repository.dart'; + +class HomeRepositoryImpl implements IHomeRepository { + final IHomeRemoteDatasource _datasource; + + HomeRepositoryImpl(this._datasource); + + @override + Future fetchConfiguration() async { + try { + final response = await _datasource.fetchConfiguration(); + + return response.data; + } on DioException catch (e, st) { + LogMessage.log.e(e.toString(), error: e, stackTrace: st); + + throw ServerException("Terjadi kesalahan koneksi"); + } + } + + @override + Future fetchHome() async { + try { + final response = await _datasource.fetchHome(); + + final data = response.data; + + if (response.status != 'success' || data == null) return null; + + return data.toHome(); + } on DioException catch (e, st) { + LogMessage.log.e(e.toString(), error: e, stackTrace: st); + + throw ServerException("Terjadi kesalahan koneksi"); + } + } + + @override + Future> fetchProductByName(String name) async { + try { + final response = await _datasource.fetchProductByName(name); + + final data = response.data; + + if (response.status != 'success' || data == null) return []; + + return data.map((product) => product.toProductItem()).toList(); + } on DioException catch (e, st) { + LogMessage.log.e(e.toString(), error: e, stackTrace: st); + + throw ServerException("Terjadi kesalahan koneksi"); + } + } + + @override + Future> fetchProductCategories(String categoryId) async { + try { + final response = await _datasource.fetchProductCategories(categoryId); + + final data = response.data; + + if (response.status != 'success' || data == null) return []; + + return data.map((product) => product.toProductItem()).toList(); + } on DioException catch (e, st) { + LogMessage.log.e(e.toString(), error: e, stackTrace: st); + + throw ServerException("Terjadi kesalahan koneksi"); + } + } + + @override + Future> fetchOutlets() async { + try { + final response = await _datasource.fetchOutlets(); + + final data = response.data; + + if (response.status != 'success' || data == null) return []; + + return data.map((outlet) => outlet.toOtherOutlet()).toList(); + } on DioException catch (e, st) { + LogMessage.log.e(e.toString(), error: e, stackTrace: st); + + throw ServerException("Terjadi kesalahan koneksi"); + } + } + + @override + Future fetchMainOutlet() async { + try { + final response = await _datasource.fetchMainOutlet(); + + final data = response.data; + + if (response.status != 'success' || data == null) return null; + + return data.toMainOutlet(); + } on DioException catch (e, st) { + LogMessage.log.e(e.toString(), error: e, stackTrace: st); + + throw ServerException("Terjadi kesalahan koneksi"); + } + } +} diff --git a/Mobile Commerce/lib/features/home/domain/entities/home.dart b/Mobile Commerce/lib/features/home/domain/entities/home.dart new file mode 100644 index 0000000..2a1adc2 --- /dev/null +++ b/Mobile Commerce/lib/features/home/domain/entities/home.dart @@ -0,0 +1,128 @@ +import 'package:latlong2/latlong.dart'; +import 'package:niogu_ecommerce_v1/core/enums/campaign_type.dart'; + +class Home { + final CurrentOutlet currentOutlet; + final List campaigns; + final List categories; + final List otherOutlets; + final List recommendations; + final List allProducts; + + const Home({ + required this.currentOutlet, + required this.campaigns, + required this.categories, + required this.otherOutlets, + required this.recommendations, + required this.allProducts, + }); +} + +class OperationalService { + final String onlineOpenTime; + final String onlineCloseTime; + final bool isCloseService; + const OperationalService({ + required this.onlineOpenTime, + required this.onlineCloseTime, + required this.isCloseService, + }); +} + +class CurrentOutlet { + final String id; + final String name; + final String? location; + final LatLng? coordinate; + final bool isActive; + + CurrentOutlet({ + required this.id, + required this.name, + required this.location, + this.coordinate, + required this.isActive, + }); +} + +class CampaignByOutlet { + final CampaignType? campaignType; + final String? actionRefId; + final String? image; + + const CampaignByOutlet({this.campaignType, this.actionRefId, this.image}); +} + +class CategoryItem { + final String id; + final String? image; + final String name; + + const CategoryItem({required this.id, this.image, required this.name}); +} + +class MainOutlet { + final String uuid; + + final String name; + + final String? phoneNumber; + + final String? location; + + final double? latitude; + + final double? longitude; + + const MainOutlet({ + required this.uuid, + required this.name, + this.phoneNumber, + this.location, + this.latitude, + this.longitude, + }); +} + +class OtherOutlet { + final String id; + final String? image; + final String name; + final String? phoneNumber; + final String? location; + final LatLng? coordinate; + final bool isActive; + + const OtherOutlet({ + required this.id, + this.image, + required this.name, + this.phoneNumber, + this.location, + this.coordinate, + required this.isActive, + }); +} + +class ProductItem { + final String id; + final String? image; + final String name; + final double totalSold; + final double averageRating; + final double sellingPrice; + final int likes; + + const ProductItem({ + required this.id, + this.image, + required this.name, + required this.totalSold, + required this.averageRating, + required this.sellingPrice, + required this.likes, + }); +} + +enum ProcessSearch { initial, suggestion, result } diff --git a/Mobile Commerce/lib/features/home/domain/repositories/i_home_repository.dart b/Mobile Commerce/lib/features/home/domain/repositories/i_home_repository.dart new file mode 100644 index 0000000..73d12c6 --- /dev/null +++ b/Mobile Commerce/lib/features/home/domain/repositories/i_home_repository.dart @@ -0,0 +1,16 @@ +import 'package:niogu_ecommerce_v1/core/models/tenant_model.dart'; +import 'package:niogu_ecommerce_v1/features/home/domain/entities/home.dart'; + +abstract class IHomeRepository { + Future fetchConfiguration(); + + Future fetchHome(); + + Future> fetchProductByName(String name); + + Future> fetchProductCategories(String categoryId); + + Future> fetchOutlets(); + + Future fetchMainOutlet(); +} diff --git a/Mobile Commerce/lib/features/home/presentation/controllers/home_controller.dart b/Mobile Commerce/lib/features/home/presentation/controllers/home_controller.dart new file mode 100644 index 0000000..5351ba3 --- /dev/null +++ b/Mobile Commerce/lib/features/home/presentation/controllers/home_controller.dart @@ -0,0 +1,77 @@ +import 'dart:async'; + +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:niogu_ecommerce_v1/core/models/tenant_model.dart'; +import 'package:niogu_ecommerce_v1/features/home/domain/entities/home.dart'; +import 'package:niogu_ecommerce_v1/features/home/presentation/providers/home_provider.dart'; + +class ConfigurationController extends AutoDisposeAsyncNotifier { + @override + FutureOr build() => _fetchConfiguration(); + + Future _fetchConfiguration() async { + final homeRepository = ref.read(homeRepositoryProvider); + + return await homeRepository.fetchConfiguration(); + } + + Future refresh() async { + ref.invalidateSelf(); + + await future; + } +} + +class HomeController extends AutoDisposeAsyncNotifier { + @override + FutureOr build() => _fetchHome(); + + Future _fetchHome() async { + final homeRepository = ref.read(homeRepositoryProvider); + + return await homeRepository.fetchHome(); + } + + Future refresh() async { + ref.invalidateSelf(); + + await future; + } +} + +class ProductByNameController + extends AutoDisposeFamilyAsyncNotifier, String> { + @override + FutureOr> build(String name) => _fetchProductByName(name); + + Future> _fetchProductByName(String name) async { + final homeRepository = ref.read(homeRepositoryProvider); + + return await homeRepository.fetchProductByName(name); + } + + Future refresh() async { + ref.invalidateSelf(); + + await future; + } +} + +class ProductCategoryController + extends AutoDisposeFamilyAsyncNotifier, String> { + @override + FutureOr> build(String categoryId) => + _fetchProductBestSellers(categoryId); + + Future> _fetchProductBestSellers(String categoryId) async { + final homeRepository = ref.read(homeRepositoryProvider); + + return await homeRepository.fetchProductCategories(categoryId); + } + + Future refresh() async { + ref.invalidateSelf(); + + await future; + } +} diff --git a/Mobile Commerce/lib/features/home/presentation/providers/home_provider.dart b/Mobile Commerce/lib/features/home/presentation/providers/home_provider.dart new file mode 100644 index 0000000..c8c3cd7 --- /dev/null +++ b/Mobile Commerce/lib/features/home/presentation/providers/home_provider.dart @@ -0,0 +1,73 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:niogu_ecommerce_v1/core/models/tenant_model.dart'; +import 'package:niogu_ecommerce_v1/core/providers/app_provider.dart'; +import 'package:niogu_ecommerce_v1/features/home/data/datasource/home_remote_datasource_impl.dart'; +import 'package:niogu_ecommerce_v1/features/home/data/repositories/home_repository_impl.dart'; +import 'package:niogu_ecommerce_v1/features/home/domain/entities/home.dart'; +import 'package:niogu_ecommerce_v1/features/home/domain/repositories/i_home_repository.dart'; +import 'package:niogu_ecommerce_v1/features/home/presentation/controllers/home_controller.dart'; + +final homeRemoteDataSourceProvider = + Provider.autoDispose((ref) { + final dio = ref.watch(dioProvider); + return HomeRemoteDatasourceImpl(dio); + }); + +final homeRepositoryProvider = Provider.autoDispose((ref) { + final homeRemoteDataSource = ref.watch(homeRemoteDataSourceProvider); + + return HomeRepositoryImpl(homeRemoteDataSource); +}); + +final configurationControllerProvider = + AsyncNotifierProvider.autoDispose( + ConfigurationController.new, + ); + +final homeControllerProvider = + AsyncNotifierProvider.autoDispose( + HomeController.new, + ); + +final productSearchProvider = StateProvider.autoDispose((ref) => ''); + +final productByNameControllerProvider = AsyncNotifierProvider.family + .autoDispose, String>( + ProductByNameController.new, + ); + +final productByNameProvider = + Provider.autoDispose>>((ref) { + final productSearch = ref.watch(productSearchProvider); + + if (productSearch.isEmpty) { + return AsyncValue.data([]); + } + + return ref.watch(productByNameControllerProvider(productSearch)); + }); + +final processSearchStateProvider = StateProvider.autoDispose( + (ref) => null, +); + +final processSearchProvider = Provider.autoDispose((ref) { + final productSearchAsync = ref.watch(productSearchProvider); + + if (productSearchAsync.isEmpty) { + return ProcessSearch.initial; + } + + final processSearchState = ref.watch(processSearchStateProvider); + + if (processSearchState != null) { + return processSearchState; + } + + return ProcessSearch.suggestion; +}); + +final productCategoriesControllerProvider = AsyncNotifierProvider.family + .autoDispose, String>( + ProductCategoryController.new, + ); diff --git a/Mobile Commerce/lib/features/home/presentation/screens/home_screen.dart b/Mobile Commerce/lib/features/home/presentation/screens/home_screen.dart new file mode 100644 index 0000000..867ec2f --- /dev/null +++ b/Mobile Commerce/lib/features/home/presentation/screens/home_screen.dart @@ -0,0 +1,1502 @@ +import 'dart:async'; +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:carousel_slider/carousel_slider.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:geolocator/geolocator.dart'; +import 'package:go_router/go_router.dart'; +import 'package:intl/intl.dart'; +import 'package:latlong2/latlong.dart'; +import 'package:niogu_ecommerce_v1/core/constant/app_asset.dart'; +import 'package:niogu_ecommerce_v1/core/constant/app_color.dart'; +import 'package:niogu_ecommerce_v1/core/constant/app_font_size.dart'; +import 'package:niogu_ecommerce_v1/core/enums/campaign_type.dart'; +import 'package:niogu_ecommerce_v1/core/errors/exceptions.dart'; +import 'package:niogu_ecommerce_v1/core/providers/app_provider.dart'; +import 'package:niogu_ecommerce_v1/core/router/app_route.dart'; +import 'package:niogu_ecommerce_v1/core/system/system_setting.dart'; +import 'package:niogu_ecommerce_v1/core/utils/currency_format.dart'; +import 'package:niogu_ecommerce_v1/core/utils/extenstion_format.dart'; +import 'package:niogu_ecommerce_v1/core/utils/log_message.dart'; +import 'package:niogu_ecommerce_v1/core/utils/time_zone.dart'; +import 'package:niogu_ecommerce_v1/core/widgets/custom_empty_screen.dart'; +import 'package:niogu_ecommerce_v1/core/widgets/custom_snackbar.dart'; +import 'package:niogu_ecommerce_v1/features/cart/presentation/providers/cart_provider.dart'; +import 'package:niogu_ecommerce_v1/features/favorite/domain/entities/favorite.dart'; +import 'package:niogu_ecommerce_v1/features/favorite/presentation/providers/favorite_provider.dart'; +import 'package:niogu_ecommerce_v1/features/home/domain/entities/home.dart'; +import 'package:niogu_ecommerce_v1/features/home/presentation/providers/home_provider.dart'; +import 'package:niogu_ecommerce_v1/features/order/presentation/providers/order_provider.dart'; +import 'package:niogu_ecommerce_v1/features/product/presentation/providers/product_provider.dart'; +import 'package:shimmer/shimmer.dart'; +import 'package:sizer/sizer.dart'; +import 'package:flutter/foundation.dart' show kIsWeb; +import 'package:url_launcher/url_launcher.dart'; + +class HomeScreen extends ConsumerStatefulWidget { + const HomeScreen({super.key}); + + @override + ConsumerState createState() => _HomeScreenState(); +} + +class _HomeScreenState extends ConsumerState { + late ScrollController _recommendedController; + + late Timer _recommendedTimer; + + int _currentBannerIndex = 0; + + @override + void initState() { + super.initState(); + _recommendedController = ScrollController(); + WidgetsBinding.instance.addPostFrameCallback((_) { + _instanceService(); + _startChefAutoScroll(); + }); + } + + @override + void dispose() { + _recommendedTimer.cancel(); + _recommendedController.dispose(); + super.dispose(); + } + + Future _instanceService() async { + final echoService = ref.read(echoServiceProvider); + + await echoService.init( + listener: (_, data) async { + final tables = List.from(data['changed_tables']); + + final currentRoute = GoRouter.of( + context, + ).routerDelegate.currentConfiguration.last.matchedLocation; + + final configureTables = ['tenants', 'tenant_payment_methods']; + + final changedTables = [ + 'outlets', + 'campaigns', + 'categories', + 'products', + 'product_variants', + 'outlet_inventories', + 'orders', + ]; + + if (tables.any((table) => configureTables.contains(table))) { + ref.read(configurationControllerProvider.notifier).refresh(); + } + + if (tables.any((table) => changedTables.contains(table))) { + if (currentRoute == '/home') { + ref.read(homeControllerProvider.notifier).refresh(); + } else if (currentRoute == '/carts') { + ref.read(productBestSellerControllerProvider.notifier).refresh(); + } else if (currentRoute == '/orders') { + ref.read(orderReportControllerProvider.notifier).refresh(); + } + } + }, + ); + } + + void _startChefAutoScroll() { + _recommendedTimer = Timer.periodic(const Duration(seconds: 3), (timer) { + if (_recommendedController.hasClients) { + final maxScroll = _recommendedController.position.maxScrollExtent; + final currentScroll = _recommendedController.position.pixels; + final delta = 70.w; + + if (currentScroll >= maxScroll - 10) { + _recommendedController.jumpTo(0); + } else { + _recommendedController.animateTo( + currentScroll + delta, + duration: const Duration(milliseconds: 800), + curve: Curves.easeInOut, + ); + } + } + }); + } + + Future _launchWhatsApp() async { + final phoneNumber = ref.read(currentOutletPhoneProvider)!; + + final Uri whatsappUri = Uri.parse( + "https://wa.me/${phoneNumber.normalizePhoneNumber()}", + ); + + try { + final bool launched = await launchUrl( + whatsappUri, + mode: LaunchMode.externalApplication, + ); + + if (!launched && mounted) { + CustomSnackbar.showError(context, "Tidak dapat membuka whatsApp"); + } + } catch (e) { + LogMessage.log.e("Error launching whatsApp: $e"); + if (mounted) { + CustomSnackbar.showError( + context, + "Terjadi kesalahan saat membuka whatsApp", + ); + } + } + } + + Future _fetchProductById(String id) async { + try { + final productDetail = await ref + .read(productRepositoryProvider) + .fetchProductById(id); + + if (productDetail == null) { + CustomSnackbar.showError(context, "Produk tidak ditemukan"); + ref.read(homeControllerProvider.notifier).refresh(); + return; + } + + context.pushNamed(AppRoute.productDetailScreen, extra: productDetail); + } on ServerException catch (e, st) { + LogMessage.log.i(e.toString(), error: e, stackTrace: st); + } + } + + @override + Widget build(BuildContext context) { + return LayoutBuilder( + builder: (context, constraints) { + final configureState = ref.watch(configurationControllerProvider); + + final homeState = ref.watch(homeControllerProvider); + + final currentOutletId = ref.watch(currentOutletIdProvider); + + final currentOutletPhone = ref.watch(currentOutletPhoneProvider); + + final favoriteState = ref.watch(favoriteControllerProvider); + + return SafeArea( + top: false, + bottom: true, + right: false, + left: false, + child: Scaffold( + backgroundColor: Colors.white, + body: Stack( + children: [ + homeState.when( + data: (home) { + if (home == null) return _buildErrorState(); + final currentOutlet = home.currentOutlet; + + final campaigns = home.campaigns; + + final categories = home.categories; + + final otherOutlets = home.otherOutlets; + + final recommendations = home.recommendations; + + final allProducts = home.allProducts; + + return RefreshIndicator( + onRefresh: () async { + await ref + .read(homeControllerProvider.notifier) + .refresh(); + }, + color: AppColor.primaryColor, + backgroundColor: Colors.white, + child: CustomScrollView( + physics: const AlwaysScrollableScrollPhysics(), + slivers: [ + _buildSliverAppBar( + location: currentOutlet.location, + name: currentOutlet.name, + ), + + _buildSliverSearch(categories), + + SliverToBoxAdapter( + child: Column( + children: [ + _buildCampaign( + campaigns: campaigns, + categories: categories, + ), + + _buildCategory(categories: categories), + + _buildOutletSection(outlets: otherOutlets), + + _buildRecommendation( + products: recommendations, + currentOutletId: currentOutletId!, + favorites: favoriteState, + ), + ], + ), + ), + + _buildAllProducts( + products: allProducts, + currentOutletId: currentOutletId, + favorites: favoriteState, + ), + + SliverToBoxAdapter(child: SizedBox(height: 15.h)), + ], + ), + ); + }, + error: (error, stackTrace) => _buildErrorState(), + loading: () => _buildHomeLoading(), + ), + + if (configureState.isRefreshing || homeState.isRefreshing) + Container( + color: Colors.black.withOpacity(0.5), + child: Center( + child: const CircularProgressIndicator( + color: AppColor.primaryColor, + backgroundColor: Colors.white, + ), + ), + ), + + if (configureState.hasValue && homeState.hasValue) + AnimatedPositioned( + duration: const Duration(milliseconds: 500), + curve: Curves.easeOutBack, + bottom: -1.85.h, + left: 0, + right: 0, + child: _buildClosedBanner( + onlineOpenTime: configureState.value!.onlineOpenTime!, + onlineCloseTime: configureState.value!.onlineCloseTime!, + isCloseService: configureState.value!.isCloseService!, + isActive: homeState.value!.currentOutlet.isActive, + ), + ), + ], + ), + floatingActionButton: currentOutletPhone == null + ? null + : Padding( + padding: EdgeInsets.only(bottom: 2.h), + child: FloatingActionButton( + onPressed: _launchWhatsApp, + backgroundColor: Colors.transparent, + shape: const CircleBorder(), + elevation: 0, + child: Image.asset( + AppAsset.WHATSAPP, + width: 12.5.w, + height: 12.5.w, + fit: BoxFit.cover, + ), + ), + ), + ), + ); + }, + ); + } + + Widget _buildSliverAppBar({String? location, required String name}) { + return SliverAppBar( + pinned: true, + backgroundColor: AppColor.primaryColor, + expandedHeight: kIsWeb ? 12.h : 8.5.h, + shadowColor: Colors.transparent, + surfaceTintColor: Colors.transparent, + flexibleSpace: FlexibleSpaceBar( + background: Padding( + padding: EdgeInsets.fromLTRB(4.w, 5.h, 4.w, 0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + if (location != null) + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Lokasi Outlet", + style: TextStyle( + color: Colors.white, + fontSize: AppFontSize.small.sp, + ), + ), + Row( + children: [ + Icon( + Icons.location_on, + color: Colors.white, + size: 5.w, + ), + Expanded( + child: Text( + location, + overflow: TextOverflow.ellipsis, + maxLines: 1, + style: TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + fontSize: AppFontSize.small.sp, + ), + ), + ), + ], + ), + ], + ), + ), + + SizedBox(width: 8.w), + + Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Text( + "Outlet Saat Ini", + style: TextStyle( + color: Colors.white, + fontSize: AppFontSize.small.sp, + ), + ), + Text( + name, + style: TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + fontSize: AppFontSize.small.sp, + ), + ), + ], + ), + ], + ), + ), + ), + ); + } + + Widget _buildSliverSearch(List categories) { + return SliverPersistentHeader( + pinned: true, + delegate: _SliverSearchDelegate( + child: Center( + child: Stack( + clipBehavior: Clip.none, + children: [ + Container(height: 6.h, color: AppColor.primaryColor), + Positioned( + top: 1.h, + left: 4.w, + right: 4.w, + child: Container( + height: 6.5.h, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(2.5.w), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.1), + blurRadius: 8, + offset: const Offset(0, 4), + ), + ], + ), + child: TextField( + onTap: () => context.pushNamed( + AppRoute.searchScreen, + extra: categories, + ), + style: TextStyle(fontSize: AppFontSize.small.sp), + textAlignVertical: TextAlignVertical.center, + decoration: InputDecoration( + hintText: "Cari produk", + hintStyle: TextStyle(fontSize: AppFontSize.small.sp), + prefixIcon: Icon(Icons.search, size: 3.5.w), + border: InputBorder.none, + contentPadding: EdgeInsets.zero, + ), + readOnly: true, + ), + ), + ), + ], + ), + ), + ), + ); + } + + Widget _buildCampaign({ + required List campaigns, + required List categories, + }) { + if (campaigns.isEmpty) { + campaigns.add(CampaignByOutlet(image: AppAsset.BANNER_MOCK_2)); + } + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: EdgeInsets.fromLTRB(4.w, 4.h, 4.w, 1.h), + child: Text( + "Informasi Hari Ini", + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: AppFontSize.small.sp, + color: Color(0xFF102851), + ), + ), + ), + CarouselSlider( + options: CarouselOptions( + height: 20.h, + viewportFraction: 0.92, + enlargeCenterPage: false, + autoPlay: true, + onPageChanged: (index, reason) { + setState(() { + _currentBannerIndex = index; + }); + }, + ), + items: campaigns.map((banner) { + return GestureDetector( + onTap: () async { + final campaignType = banner.campaignType; + + if (campaignType != null) { + switch (campaignType) { + case CampaignType.product: + await _fetchProductById(banner.actionRefId!); + break; + case CampaignType.category: + final category = categories.firstWhere( + (category) => category.id == banner.actionRefId, + ); + context.pushNamed( + AppRoute.productCategoryScreen, + extra: category, + ); + break; + } + } + }, + child: CachedNetworkImage( + imageUrl: banner.image ?? 'error', + imageBuilder: (context, imageProvider) { + return Container( + margin: EdgeInsets.symmetric(horizontal: 1.w), + decoration: BoxDecoration( + color: Colors.grey.shade100, + borderRadius: BorderRadius.circular(2.5.w), + image: DecorationImage( + image: imageProvider, + fit: BoxFit.cover, + ), + ), + ); + }, + placeholder: (context, url) { + return Shimmer.fromColors( + baseColor: Colors.grey.shade300, + highlightColor: Colors.grey.shade100, + child: Container( + margin: EdgeInsets.symmetric(horizontal: 1.w), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(2.5.w), + ), + ), + ); + }, + errorWidget: (context, url, error) { + return Container( + margin: EdgeInsets.symmetric(horizontal: 1.w), + decoration: BoxDecoration( + color: Colors.grey.shade100, + borderRadius: BorderRadius.circular(2.5.w), + image: DecorationImage( + image: AssetImage(AppAsset.BANNER_MOCK_2), + fit: BoxFit.cover, + ), + ), + ); + }, + ), + ); + }).toList(), + ), + + SizedBox(height: 2.h), + + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: campaigns.asMap().entries.map((entry) { + final index = entry.key; + return Container( + width: _currentBannerIndex == index ? 15.0 : 6.0, + height: 6.0, + margin: const EdgeInsets.symmetric(horizontal: 3.0), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(1.w), + color: _currentBannerIndex == index + ? AppColor.primaryColor + : Colors.grey.shade300, + ), + ); + }).toList(), + ), + ], + ); + } + + Widget _buildCategory({required List categories}) { + return Column( + children: [ + Padding( + padding: EdgeInsets.symmetric(vertical: 2.h), + child: Text( + "Kategori Produk", + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: AppFontSize.small.sp, + color: Color(0xFF102851), + ), + ), + ), + + if (categories.isEmpty) + CustomEmptyScreen( + icon: Icons.grid_off_outlined, + title: "Kategori Belum Tersedia", + subtitle: "Nantikan pilihan kategori menarik segera", + height: 18.h, + ) + else + SizedBox( + height: 14.h, + child: ListView.builder( + scrollDirection: Axis.horizontal, + padding: EdgeInsets.symmetric(horizontal: 4.w), + itemCount: categories.length, + itemBuilder: (context, index) { + final category = categories[index]; + return GestureDetector( + onTap: () { + context.pushNamed( + AppRoute.productCategoryScreen, + extra: category, + ); + }, + child: Padding( + padding: EdgeInsets.only(right: 5.w), + child: Column( + children: [ + CachedNetworkImage( + imageUrl: category.image ?? 'error', + imageBuilder: (context, imageProvider) { + return Container( + height: 18.w, + width: 18.w, + decoration: BoxDecoration( + color: Colors.white, + border: BoxBorder.all( + color: Colors.grey.shade300, + ), + borderRadius: BorderRadius.circular(2.5.w), + image: DecorationImage( + image: imageProvider, + fit: BoxFit.cover, + ), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.05), + blurRadius: 10, + ), + ], + ), + ); + }, + placeholder: (context, url) { + return Shimmer.fromColors( + baseColor: Colors.grey.shade300, + highlightColor: Colors.grey.shade100, + child: Container( + height: 18.w, + width: 18.w, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(2.5.w), + ), + ), + ); + }, + errorWidget: (context, url, error) { + return Container( + height: 18.w, + width: 18.w, + decoration: BoxDecoration( + color: Colors.white, + border: BoxBorder.all( + color: Colors.grey.shade300, + ), + borderRadius: BorderRadius.circular(2.5.w), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.05), + blurRadius: 10, + ), + ], + ), + child: Icon( + Icons.image_outlined, + size: 5.w, + color: Colors.grey.shade300, + ), + ); + }, + ), + + SizedBox(height: 1.h), + Text( + category.name, + style: TextStyle( + fontSize: (AppFontSize.small - 2).sp, + color: Color(0xFF102851), + ), + ), + ], + ), + ), + ); + }, + ), + ), + ], + ); + } + + Widget _buildOutletSection({required List outlets}) { + if (outlets.isEmpty) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: EdgeInsets.all(4.w), + child: Text( + "Kunjungi Outlet Lain", + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: AppFontSize.small.sp, + ), + ), + ), + CustomEmptyScreen( + icon: Icons.storefront_outlined, + title: "Hanya 1 Outlet Tersedia", + subtitle: "Belum ada outlet lain di wilayah ini", + ), + ], + ); + } + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: EdgeInsets.all(4.w), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Kunjungi Outlet Lain", + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: AppFontSize.small.sp, + ), + ), + + GestureDetector( + onTap: () async { + LocationPermission permission = + await Geolocator.checkPermission(); + if (permission == LocationPermission.denied) { + permission = await Geolocator.requestPermission(); + } + + Position position = await Geolocator.getCurrentPosition(); + + final userLocation = LatLng( + position.latitude, + position.longitude, + ); + + final outlets = await ref + .read(homeRepositoryProvider) + .fetchOutlets(); + + if (outlets.isEmpty) return; + + context.pushNamed( + AppRoute.outletMapScreen, + extra: {'user_location': userLocation, 'outlets': outlets}, + ); + }, + child: Container( + padding: EdgeInsets.all(1.5.w), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(1.w), + border: BoxBorder.all(color: AppColor.primaryColor), + ), + child: Row( + children: [ + Icon( + Icons.location_on_outlined, + size: 3.5.w, + color: AppColor.primaryColor, + ), + + SizedBox(width: 2.w), + + Text( + "Lihat Dipeta", + style: TextStyle( + color: AppColor.primaryColor, + fontWeight: FontWeight.bold, + fontSize: (AppFontSize.small - 1.25).sp, + ), + ), + ], + ), + ), + ), + ], + ), + ), + SizedBox( + height: 20.h, + child: ListView.builder( + scrollDirection: Axis.horizontal, + padding: EdgeInsets.symmetric(horizontal: 4.w), + itemCount: outlets.length, + itemBuilder: (context, index) { + final outlet = outlets[index]; + return GestureDetector( + onTap: () async { + await SystemSetting.switchOutlet( + outletId: outlet.id, + outletName: outlet.name, + outletPhone: outlet.phoneNumber, + outletLocation: outlet.location, + outletCoordinate: outlet.coordinate, + ); + + ref.read(currentOutletIdProvider.notifier).state = outlet.id; + + ref.read(currentOutletNameProvider.notifier).state = + outlet.name; + + ref.read(currentOutletPhoneProvider.notifier).state = + outlet.phoneNumber; + + ref.read(currentOutletLocationProvider.notifier).state = + outlet.location; + + ref.read(currentOutletCoordinateProvider.notifier).state = + outlet.coordinate; + + await ref.read(homeControllerProvider.notifier).refresh(); + + CustomSnackbar.showSuccess( + context, + "Berhasil mengunjungi ${outlet.name}", + ); + }, + child: CachedNetworkImage( + imageUrl: outlet.image ?? 'error', + imageBuilder: (context, imageProvider) { + return Stack( + children: [ + Container( + width: 75.w, + margin: EdgeInsets.only(right: 4.w), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(2.5.w), + image: DecorationImage( + image: imageProvider, + fit: BoxFit.cover, + ), + ), + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(2.5.w), + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [Colors.transparent, Colors.black87], + ), + ), + padding: EdgeInsets.all(4.w), + child: _buildBranchInformation( + name: outlet.name, + location: outlet.location, + ), + ), + ), + ], + ); + }, + placeholder: (context, url) { + return Shimmer.fromColors( + baseColor: Colors.grey.shade300, + highlightColor: Colors.grey.shade100, + child: Container( + width: 75.w, + margin: EdgeInsets.only(right: 4.w), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(2.5.w), + ), + ), + ); + }, + errorWidget: (context, url, error) { + return Container( + width: 75.w, + margin: EdgeInsets.only(right: 4.w), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(2.5.w), + image: DecorationImage( + image: AssetImage(AppAsset.OUTLET_MOCK), + fit: BoxFit.cover, + ), + ), + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(2.5.w), + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [Colors.transparent, Colors.black87], + ), + ), + padding: EdgeInsets.all(4.w), + child: _buildBranchInformation( + name: outlet.name, + location: outlet.location, + ), + ), + ); + }, + ), + ); + }, + ), + ), + ], + ); + } + + Widget _buildBranchInformation({required String name, String? location}) { + return Column( + mainAxisAlignment: MainAxisAlignment.end, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + name, + style: TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + fontSize: AppFontSize.small.sp, + ), + ), + if (location != null) + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Row( + children: [ + Icon(Icons.location_on, size: 4.w, color: Colors.white70), + + SizedBox(width: 0.75.w), + Expanded( + child: Text( + location, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + color: Colors.white70, + fontSize: (AppFontSize.small - 2).sp, + ), + ), + ), + ], + ), + ), + + /** + SizedBox(width: 2.5.w), + + Row( + children: [ + Icon(Icons.straighten, size: 4.w, color: Colors.white70), + + SizedBox(width: 0.75.w), + + Text( + "4km", + style: TextStyle( + color: Colors.white70, + fontSize: (AppFontSize.small - 2).sp, + ), + ), + ], + ), + */ + ], + ), + ], + ); + } + + Widget _buildRecommendation({ + required List products, + required String currentOutletId, + required Map favorites, + }) { + if (products.isEmpty) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: EdgeInsets.all(4.w), + child: Text( + "Rekomendasi Produk", + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: AppFontSize.small.sp, + ), + ), + ), + CustomEmptyScreen( + icon: Icons.auto_awesome_outlined, + title: "Belum Ada Rekomendasi", + subtitle: "Produk pilihan akan muncul di sini nanti", + ), + ], + ); + } + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: EdgeInsets.all(4.w), + child: Text( + "Rekomendasi Produk", + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: AppFontSize.small.sp, + color: Color(0xFF102851), + ), + ), + ), + SizedBox( + height: 32.h, + child: ListView.builder( + controller: _recommendedController, + scrollDirection: Axis.horizontal, + padding: EdgeInsets.symmetric(horizontal: 4.w), + itemCount: products.length, + itemBuilder: (context, index) { + final product = products[index]; + + final isFavorite = favorites.containsKey( + "$currentOutletId-${product.id}", + ); + return Container( + width: 70.w, + margin: EdgeInsets.only(right: 4.w), + child: _buildProductCard(product, currentOutletId, isFavorite), + ); + }, + ), + ), + ], + ); + } + + Widget _buildAllProducts({ + required List products, + required String currentOutletId, + required Map favorites, + }) { + if (products.isEmpty) { + return SliverToBoxAdapter( + child: CustomEmptyScreen( + icon: Icons.inventory_outlined, + title: "Katalog Kosong", + subtitle: "Nantikan produk-produk terbaru kami", + height: 40.h, + ), + ); + } + return SliverMainAxisGroup( + slivers: [ + SliverPersistentHeader( + pinned: true, + delegate: _SliverSearchDelegate( + child: Container( + color: Colors.white, + padding: EdgeInsets.all(4.w), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Semua Produk", + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: AppFontSize.small.sp, + ), + ), + Icon(Icons.filter_list), + ], + ), + ), + ), + ), + SliverPadding( + padding: EdgeInsets.symmetric(horizontal: 4.w), + sliver: SliverGrid( + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 2, + mainAxisSpacing: 10, + crossAxisSpacing: 10, + childAspectRatio: 0.75, + ), + delegate: SliverChildBuilderDelegate((context, index) { + final product = products[index]; + + final isFavorite = favorites.containsKey( + "$currentOutletId-${product.id}", + ); + + return _buildProductCard(product, currentOutletId, isFavorite); + }, childCount: products.length), + ), + ), + ], + ); + } + + Widget _buildProductCard( + ProductItem product, + String currentOutletId, + bool isFavorite, + ) { + return GestureDetector( + onTap: () async => await _fetchProductById(product.id), + child: Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(2.5.w), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.04), + blurRadius: 10, + offset: const Offset(0, 4), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + flex: 3, + child: CachedNetworkImage( + imageUrl: product.image ?? 'error', + imageBuilder: (context, imageProvider) { + return Container( + width: double.infinity, + decoration: BoxDecoration( + color: Colors.grey.shade100, + borderRadius: BorderRadius.vertical( + top: Radius.circular(2.5.w), + ), + image: DecorationImage( + image: imageProvider, + fit: BoxFit.cover, + ), + ), + child: Stack( + children: [ + Positioned( + top: 1.w, + right: 1.w, + child: GestureDetector( + onTap: () { + final currentOutletId = ref.read( + currentOutletIdProvider, + ); + ref + .read(favoriteControllerProvider.notifier) + .toggle(product, currentOutletId!); + }, + child: CircleAvatar( + radius: 14, + backgroundColor: Colors.white.withOpacity(0.9), + child: Icon( + isFavorite + ? Icons.favorite + : Icons.favorite_outline, + size: 5.w, + color: isFavorite ? Colors.red : Colors.grey, + ), + ), + ), + ), + ], + ), + ); + }, + placeholder: (context, url) { + return Shimmer.fromColors( + baseColor: Colors.grey.shade300, + highlightColor: Colors.grey.shade100, + child: Container( + width: double.infinity, + margin: EdgeInsets.only(right: 4.w), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.vertical( + top: Radius.circular(2.5.w), + ), + ), + ), + ); + }, + errorWidget: (context, url, error) { + return Container( + width: double.infinity, + decoration: BoxDecoration( + color: Colors.grey.shade100, + borderRadius: BorderRadius.vertical( + top: Radius.circular(2.5.w), + ), + ), + child: Stack( + children: [ + Center( + child: Icon( + Icons.image_outlined, + color: Colors.grey.shade300, + size: 10.w, + ), + ), + Positioned( + top: 1.w, + right: 1.w, + child: GestureDetector( + onTap: () { + ref + .read(favoriteControllerProvider.notifier) + .toggle(product, currentOutletId); + }, + child: CircleAvatar( + radius: 14, + backgroundColor: Colors.white.withOpacity(0.9), + child: Icon( + isFavorite + ? Icons.favorite + : Icons.favorite_outline, + size: 5.w, + color: isFavorite ? Colors.red : Colors.grey, + ), + ), + ), + ), + ], + ), + ); + }, + ), + ), + + Expanded( + flex: 2, + child: Padding( + padding: EdgeInsets.all(3.w), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + product.name, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: (AppFontSize.small - 1.25).sp, + color: const Color(0xFF102851), + ), + ), + + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "${product.totalSold.toCompact} Terjual", + style: TextStyle( + color: Colors.black87, + fontWeight: FontWeight.bold, + fontSize: (AppFontSize.small - 1.25).sp, + ), + ), + Row( + children: [ + Icon(Icons.star, color: Colors.orange, size: 3.5.w), + SizedBox(width: 1.w), + Text( + product.averageRating.toStringAsFixed(1), + style: TextStyle( + fontSize: (AppFontSize.small - 1.25).sp, + color: Colors.grey.shade600, + ), + ), + ], + ), + ], + ), + + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + CurrencyFormat.formatToIdr(product.sellingPrice, 0), + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: AppFontSize.small.sp, + color: AppColor.primaryColor, + ), + ), + Row( + children: [ + Icon( + Icons.thumb_up, + color: Colors.red, + size: 3.5.w, + ), + SizedBox(width: 1.w), + Text( + product.likes.toString(), + style: TextStyle( + fontSize: (AppFontSize.small - 1.25).sp, + color: Colors.red, + ), + ), + ], + ), + ], + ), + ], + ), + ), + ), + ], + ), + ), + ); + } + + Widget _buildErrorState() { + return RefreshIndicator( + onRefresh: () => ref.read(homeControllerProvider.notifier).refresh(), + color: AppColor.primaryColor, + backgroundColor: Colors.white, + child: SingleChildScrollView( + physics: const AlwaysScrollableScrollPhysics(), + child: SizedBox( + height: 80.h, + child: CustomEmptyScreen( + icon: Icons.cloud_off_outlined, + title: "Terjadi Kesalahan Koneksi", + subtitle: "Tarik ke bawah untuk mencoba lagi", + height: 40.h, + ), + ), + ), + ); + } + + Widget _buildHomeLoading() { + return Shimmer.fromColors( + baseColor: Colors.grey.shade300, + highlightColor: Colors.grey.shade100, + child: SingleChildScrollView( + physics: const NeverScrollableScrollPhysics(), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container(height: 8.5.h, color: Colors.white), + Padding( + padding: EdgeInsets.all(4.w), + child: Container( + height: 6.5.h, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(2.5.w), + ), + ), + ), + + Padding( + padding: EdgeInsets.symmetric(horizontal: 4.w), + child: Container( + height: 20.h, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(2.5.w), + ), + ), + ), + + SizedBox(height: 4.h), + + Padding( + padding: EdgeInsets.symmetric(horizontal: 4.w), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: List.generate( + 4, + (index) => Column( + children: [ + Container( + width: 15.w, + height: 15.w, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(2.5.w), + ), + ), + SizedBox(height: 1.h), + Container( + width: 12.w, + height: 1.5.h, + color: Colors.white, + ), + ], + ), + ), + ), + ), + + SizedBox(height: 4.h), + + Padding( + padding: EdgeInsets.symmetric(horizontal: 4.w), + child: Container( + height: 25.h, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(2.5.w), + ), + ), + ), + ], + ), + ), + ); + } + + Widget _buildClosedBanner({ + required String onlineOpenTime, + required String onlineCloseTime, + required bool isCloseService, + required bool isActive, + }) { + final timeFormat = DateFormat('HH.mm'); + + final open = timeFormat.parse(onlineOpenTime).hour; + + final close = timeFormat.parse(onlineCloseTime).hour; + + final now = DateTime.now().hour; + + if (!isCloseService && (now >= open && now < close) && isActive) + return const SizedBox(); + + final timeZone = TimeZone.getCurrentTimeZone(); + + final operatingHour = + "$onlineOpenTime $timeZone - $onlineCloseTime $timeZone"; + + return Container( + margin: EdgeInsets.symmetric(horizontal: 4.w, vertical: 1.h), + padding: EdgeInsets.symmetric(horizontal: 4.w, vertical: 1.5.h), + decoration: BoxDecoration( + color: Colors.red.withOpacity(0.85), + borderRadius: BorderRadius.circular(3.w), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.1), + blurRadius: 10, + offset: const Offset(0, -2), + ), + ], + ), + child: Row( + children: [ + Icon(Icons.info_outline, color: Colors.white, size: 5.w), + SizedBox(width: 3.w), + Expanded( + child: Text( + !isActive + ? "Outlet ini tutup sementara, kunjungi outlet lain kami" + : isCloseService + ? "Toko sedang tutup sementara" + : "Toko sedang tutup. Jam layanan pembelian online: $operatingHour", + style: TextStyle( + color: Colors.white, + fontSize: AppFontSize.small.sp, + fontWeight: FontWeight.w500, + ), + ), + ), + ], + ), + ); + } +} + +class _SliverSearchDelegate extends SliverPersistentHeaderDelegate { + final Widget child; + _SliverSearchDelegate({required this.child}); + + @override + Widget build(BuildContext context, shrinkOffset, bool overlapsContent) => + child; + + @override + double get maxExtent => 6.h; + @override + double get minExtent => 6.h; + + @override + bool shouldRebuild(covariant SliverPersistentHeaderDelegate oldDelegate) => + false; +} diff --git a/Mobile Commerce/lib/features/home/presentation/screens/outlet_map_screen.dart b/Mobile Commerce/lib/features/home/presentation/screens/outlet_map_screen.dart new file mode 100644 index 0000000..171a699 --- /dev/null +++ b/Mobile Commerce/lib/features/home/presentation/screens/outlet_map_screen.dart @@ -0,0 +1,438 @@ +import 'dart:math'; +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_map/flutter_map.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; +import 'package:latlong2/latlong.dart'; +import 'package:niogu_ecommerce_v1/core/constant/app_asset.dart'; +import 'package:niogu_ecommerce_v1/core/constant/app_font_size.dart'; +import 'package:niogu_ecommerce_v1/core/providers/app_provider.dart'; +import 'package:niogu_ecommerce_v1/core/system/system_setting.dart'; +import 'package:niogu_ecommerce_v1/core/widgets/custom_snackbar.dart'; +import 'package:niogu_ecommerce_v1/core/widgets/triangle_painter.dart'; +import 'package:niogu_ecommerce_v1/features/home/domain/entities/home.dart'; +import 'package:niogu_ecommerce_v1/features/home/presentation/providers/home_provider.dart'; +import 'package:sizer/sizer.dart'; +import 'package:niogu_ecommerce_v1/core/constant/app_color.dart'; + +class OutletMapScreen extends ConsumerStatefulWidget { + final LatLng userLocation; + final List outlets; + const OutletMapScreen({ + super.key, + required this.userLocation, + required this.outlets, + }); + + @override + ConsumerState createState() => _OutletMapScreenState(); +} + +class _OutletMapScreenState extends ConsumerState { + final MapController _mapController = MapController(); + + late final LatLng _userLocation; + + late final List _outlets; + + int _selectedOutletIndex = 0; + + @override + void initState() { + super.initState(); + _userLocation = widget.userLocation; + _outlets = widget.outlets; + WidgetsBinding.instance.addPostFrameCallback((_) { + _fitBounds(); + }); + } + + double _calculateDistance( + double lat1, + double lon1, + double lat2, + double lon2, + ) { + final p = 0.017453292519943295; + final c = cos; + final a = + 0.5 - + c((lat2 - lat1) * p) / 2 + + c(lat1 * p) * c(lat2 * p) * (1 - c((lon2 - lon1) * p)) / 2; + return 12742 * asin(sqrt(a)); + } + + void _fitBounds() { + _mapController.fitCamera( + CameraFit.bounds( + bounds: LatLngBounds( + _userLocation, + _outlets[_selectedOutletIndex].coordinate!, + ), + padding: EdgeInsets.symmetric(vertical: 15.h, horizontal: 10.w), + ), + ); + } + + @override + Widget build(BuildContext context) { + return LayoutBuilder( + builder: (context, constraints) { + return SafeArea( + top: false, + bottom: true, + right: false, + left: false, + child: Scaffold( + body: Stack( + children: [ + FlutterMap( + mapController: _mapController, + options: MapOptions( + initialCenter: _outlets[0].coordinate!, + initialZoom: 16.0, + minZoom: 3.0, + maxZoom: 18.0, + ), + children: [ + TileLayer( + urlTemplate: + 'https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png', + subdomains: const ['a', 'b', 'c', 'd'], + ), + + MarkerLayer( + markers: [ + ..._outlets.asMap().entries.map((entry) { + int idx = entry.key; + var data = entry.value; + final isSelected = _selectedOutletIndex == idx; + + final distance = _calculateDistance( + _userLocation.latitude, + _userLocation.longitude, + data.coordinate!.latitude, + data.coordinate!.longitude, + ); + + var strDistance = + '${distance.toStringAsFixed(1)} Km dari lokasimu'; + + if (distance < 1) { + strDistance = + '${(distance * 1000).toStringAsFixed(0)} meter dari lokasimu'; + } + + return Marker( + point: data.coordinate!, + width: 70.w, + height: 25.h, + alignment: Alignment.topCenter, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + AnimatedOpacity( + duration: const Duration(milliseconds: 300), + opacity: isSelected ? 1.0 : 0.0, + child: Container( + padding: EdgeInsets.symmetric( + horizontal: 3.w, + vertical: 0.8.h, + ), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(2.w), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.1), + blurRadius: 8, + offset: const Offset(0, 4), + ), + ], + border: Border.all( + color: isSelected + ? AppColor.primaryColor.withOpacity( + 0.5, + ) + : Colors.grey.shade200, + ), + ), + child: Text( + strDistance, + style: TextStyle( + fontSize: (AppFontSize.small - 1).sp, + fontWeight: FontWeight.bold, + color: AppColor.primaryColor, + ), + ), + ), + ), + if (isSelected) + CustomPaint( + size: Size(3.w, 1.5.w), + painter: TrianglePainter( + Colors.white, + ), // Reuse painter + ), + Icon( + Icons.location_on, + color: isSelected + ? AppColor.primaryColor + : Colors.grey.shade400, + size: 10.w, + ), + ], + ), + ); + }).toList(), + ], + ), + ], + ), + + Positioned( + top: 6.h, + left: 4.w, + child: CircleAvatar( + backgroundColor: Colors.white, + child: IconButton( + icon: Icon( + Icons.arrow_back, + color: Colors.black, + size: 7.w, + ), + onPressed: () => Navigator.pop(context), + ), + ), + ), + + Positioned( + bottom: 12.h, + left: 0, + right: 0, + child: SizedBox( + height: 20.h, + child: PageView.builder( + controller: PageController(viewportFraction: 0.85), + itemCount: _outlets.length, + onPageChanged: (index) { + setState(() => _selectedOutletIndex = index); + _fitBounds(); + }, + itemBuilder: (context, index) { + final outlet = _outlets[index]; + return CachedNetworkImage( + imageUrl: outlet.image ?? 'error', + imageBuilder: (context, imageProvider) { + return Container( + width: 75.w, + margin: EdgeInsets.only(right: 4.w), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(2.5.w), + image: DecorationImage( + image: imageProvider, + fit: BoxFit.cover, + ), + ), + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(2.5.w), + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + Colors.transparent, + Colors.black87, + ], + ), + ), + padding: EdgeInsets.all(4.w), + child: _buildBranchInformation( + name: outlet.name, + location: outlet.location, + ), + ), + ); + }, + errorWidget: (context, url, error) { + return Container( + width: 75.w, + margin: EdgeInsets.only(right: 4.w), + decoration: BoxDecoration( + border: BoxBorder.all( + color: _selectedOutletIndex == index + ? AppColor.primaryColor + : Colors.transparent, + ), + borderRadius: BorderRadius.circular(2.5.w), + image: DecorationImage( + image: AssetImage(AppAsset.OUTLET_MOCK), + fit: BoxFit.cover, + ), + ), + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(2.5.w), + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + Colors.transparent, + Colors.black87, + ], + ), + ), + padding: EdgeInsets.all(4.w), + child: _buildBranchInformation( + name: outlet.name, + location: outlet.location, + ), + ), + ); + }, + ); + }, + ), + ), + ), + + Align( + alignment: Alignment.bottomCenter, + child: Container( + padding: EdgeInsets.all(4.w), + color: Colors.white, + child: SafeArea( + top: false, + child: ElevatedButton( + onPressed: () async { + final outlet = _outlets[_selectedOutletIndex]; + + await SystemSetting.switchOutlet( + outletId: outlet.id, + outletName: outlet.name, + outletPhone: outlet.phoneNumber, + outletLocation: outlet.location, + outletCoordinate: outlet.coordinate, + ); + + ref.read(currentOutletIdProvider.notifier).state = + outlet.id; + + ref.read(currentOutletNameProvider.notifier).state = + outlet.name; + + ref.read(currentOutletPhoneProvider.notifier).state = + outlet.phoneNumber; + + ref + .read(currentOutletLocationProvider.notifier) + .state = + outlet.location; + + ref + .read(currentOutletCoordinateProvider.notifier) + .state = outlet + .coordinate; + + await ref + .read(homeControllerProvider.notifier) + .refresh(); + + CustomSnackbar.showSuccess( + context, + "Berhasil mengunjungi ${outlet.name}", + ); + + context.pop(); + }, + style: ElevatedButton.styleFrom( + backgroundColor: AppColor.primaryColor, + minimumSize: Size(double.infinity, 6.h), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(2.5.w), + ), + ), + child: Text( + "Pilih Outlet Ini", + style: TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + fontSize: AppFontSize.medium.sp, + ), + ), + ), + ), + ), + ), + ], + ), + ), + ); + }, + ); + } + + Widget _buildBranchInformation({required String name, String? location}) { + return Column( + mainAxisAlignment: MainAxisAlignment.end, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + name, + style: TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + fontSize: AppFontSize.small.sp, + ), + ), + if (location != null) + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Row( + children: [ + Icon(Icons.location_on, size: 4.w, color: Colors.white70), + + SizedBox(width: 0.75.w), + Expanded( + child: Text( + location, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + color: Colors.white70, + fontSize: (AppFontSize.small - 2).sp, + ), + ), + ), + ], + ), + ), + + /** + SizedBox(width: 2.5.w), + + Row( + children: [ + Icon(Icons.straighten, size: 4.w, color: Colors.white70), + + SizedBox(width: 0.75.w), + + Text( + "4km", + style: TextStyle( + color: Colors.white70, + fontSize: (AppFontSize.small - 2).sp, + ), + ), + ], + ), + */ + ], + ), + ], + ); + } +} diff --git a/Mobile Commerce/lib/features/home/presentation/screens/product_category_screen.dart b/Mobile Commerce/lib/features/home/presentation/screens/product_category_screen.dart new file mode 100644 index 0000000..3f605b1 --- /dev/null +++ b/Mobile Commerce/lib/features/home/presentation/screens/product_category_screen.dart @@ -0,0 +1,449 @@ +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; +import 'package:niogu_ecommerce_v1/core/constant/app_color.dart'; +import 'package:niogu_ecommerce_v1/core/constant/app_font_size.dart'; +import 'package:niogu_ecommerce_v1/core/errors/exceptions.dart'; +import 'package:niogu_ecommerce_v1/core/providers/app_provider.dart'; +import 'package:niogu_ecommerce_v1/core/router/app_route.dart'; +import 'package:niogu_ecommerce_v1/core/utils/currency_format.dart'; +import 'package:niogu_ecommerce_v1/core/utils/extenstion_format.dart'; +import 'package:niogu_ecommerce_v1/core/utils/log_message.dart'; +import 'package:niogu_ecommerce_v1/core/widgets/custom_empty_screen.dart'; +import 'package:niogu_ecommerce_v1/core/widgets/custom_snackbar.dart'; +import 'package:niogu_ecommerce_v1/features/favorite/presentation/providers/favorite_provider.dart'; +import 'package:niogu_ecommerce_v1/features/home/domain/entities/home.dart'; +import 'package:niogu_ecommerce_v1/features/home/presentation/providers/home_provider.dart'; +import 'package:niogu_ecommerce_v1/features/product/presentation/providers/product_provider.dart'; +import 'package:shimmer/shimmer.dart'; +import 'package:sizer/sizer.dart'; + +class ProductCategoryScreen extends ConsumerWidget { + final CategoryItem category; + const ProductCategoryScreen({super.key, required this.category}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + return LayoutBuilder( + builder: (context, constraints) { + final productState = ref.watch( + productCategoriesControllerProvider(category.id), + ); + + final currentOutletId = ref.watch(currentOutletIdProvider); + + final favoriteState = ref.watch(favoriteControllerProvider); + + return SafeArea( + top: false, + bottom: true, + right: false, + left: false, + child: Scaffold( + backgroundColor: Colors.white, + body: SingleChildScrollView( + child: Column( + children: [ + Stack( + children: [ + CachedNetworkImage( + imageUrl: category.image ?? 'error', + imageBuilder: (context, imageProvider) { + return Container( + width: 100.w, + height: 100.w, + decoration: BoxDecoration( + color: Colors.grey.shade200, + image: DecorationImage( + image: imageProvider, + fit: BoxFit.cover, + ), + ), + ); + }, + errorWidget: (context, url, error) { + return Container( + width: 100.w, + height: 100.w, + decoration: BoxDecoration( + color: Colors.grey.shade200, + ), + child: Icon( + Icons.image_outlined, + size: 30.w, + color: Colors.grey.shade300, + ), + ); + }, + ), + + Positioned( + top: 4.h, + left: 2.w, + child: GestureDetector( + onTap: () => context.pop(), + child: Padding( + padding: EdgeInsets.all(2.w), + child: CircleAvatar( + maxRadius: 5.w, + minRadius: 5.w, + backgroundColor: Colors.white.withOpacity(0.9), + child: Center( + child: Icon( + Icons.arrow_back, + color: Colors.black, + size: 6.w, + ), + ), + ), + ), + ), + ), + ], + ), + + Transform.translate( + offset: Offset(0, -4.h), + child: Container( + width: 100.w, + padding: EdgeInsets.symmetric( + horizontal: 4.w, + vertical: 3.h, + ), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.only( + topLeft: Radius.circular(4.w), + topRight: Radius.circular(4.w), + ), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Center( + child: Column( + children: [ + Container( + width: 10.w, + height: 5, + decoration: BoxDecoration( + color: Colors.grey.shade300, + borderRadius: BorderRadius.circular(2.5.w), + ), + ), + SizedBox(height: 2.h), + Text( + category.name, + style: TextStyle( + fontSize: AppFontSize.medium.sp, + fontWeight: FontWeight.bold, + color: Colors.black87, + ), + ), + ], + ), + ), + + SizedBox(height: 2.h), + + productState.when( + data: (products) { + if (products.isEmpty) { + return CustomEmptyScreen( + icon: Icons.search_off_outlined, + title: "Belum Tersedia", + subtitle: + "Nantikan untuk produk kategori ini segera", + height: 40.h, + ); + } + return GridView.builder( + padding: EdgeInsets.zero, + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: products.length, + gridDelegate: + SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 2, + mainAxisSpacing: 10, + crossAxisSpacing: 10, + childAspectRatio: 0.75, + ), + itemBuilder: (context, index) { + final product = products[index]; + + final isFavorite = favoriteState.containsKey( + "$currentOutletId-${product.id}", + ); + return _buildProductItem( + context, + ref, + product, + currentOutletId!, + isFavorite, + ); + }, + ); + }, + error: (error, stackTrace) => CustomEmptyScreen( + icon: Icons.cloud_off_outlined, + title: "Terjadi Kesalahan Koneksi", + subtitle: "Tarik ke bawah untuk mencoba lagi", + height: 40.h, + ), + loading: () => _buildProductShimmer(), + ), + ], + ), + ), + ), + ], + ), + ), + ), + ); + }, + ); + } + + Widget _buildProductItem( + BuildContext context, + WidgetRef ref, + ProductItem product, + String currentOutletId, + bool isFavorite, + ) { + return GestureDetector( + onTap: () async { + try { + final productDetail = await ref + .read(productRepositoryProvider) + .fetchProductById(product.id); + + if (productDetail == null) { + CustomSnackbar.showError(context, "Produk tidak ditemukan"); + ref.read(homeControllerProvider.notifier).refresh(); + return; + } + + context.pushNamed(AppRoute.productDetailScreen, extra: productDetail); + } on ServerException catch (e, st) { + LogMessage.log.i(e.toString(), error: e, stackTrace: st); + } + }, + child: Container( + width: 45.w, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(2.5.w), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.04), + blurRadius: 10, + offset: const Offset(0, 4), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + flex: 3, + child: CachedNetworkImage( + imageUrl: product.image ?? 'error', + imageBuilder: (context, imageProvider) { + return Container( + width: double.infinity, + decoration: BoxDecoration( + color: Colors.grey.shade100, + borderRadius: BorderRadius.vertical( + top: Radius.circular(2.5.w), + ), + image: DecorationImage( + image: imageProvider, + fit: BoxFit.cover, + ), + ), + child: Stack( + children: [ + Positioned( + top: 1.w, + right: 1.w, + child: GestureDetector( + onTap: () { + final currentOutletId = ref.read( + currentOutletIdProvider, + ); + ref + .read(favoriteControllerProvider.notifier) + .toggle(product, currentOutletId!); + }, + child: CircleAvatar( + radius: 14, + backgroundColor: Colors.white.withOpacity(0.9), + child: Icon( + isFavorite + ? Icons.favorite + : Icons.favorite_outline, + size: 5.w, + color: isFavorite ? Colors.red : Colors.grey, + ), + ), + ), + ), + ], + ), + ); + }, + errorWidget: (context, url, error) { + return Container( + width: double.infinity, + decoration: BoxDecoration( + color: Colors.grey.shade100, + borderRadius: BorderRadius.vertical( + top: Radius.circular(2.5.w), + ), + ), + child: Stack( + children: [ + Center( + child: Icon( + Icons.image_outlined, + color: Colors.grey.shade300, + size: 10.w, + ), + ), + Positioned( + top: 1.w, + right: 1.w, + child: CircleAvatar( + radius: 14, + backgroundColor: Colors.white.withOpacity(0.9), + child: Icon( + Icons.favorite_border, + size: 5.w, + color: Colors.grey, + ), + ), + ), + ], + ), + ); + }, + ), + ), + + Expanded( + flex: 2, + child: Padding( + padding: EdgeInsets.all(3.w), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + product.name, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: (AppFontSize.small - 1.25).sp, + color: const Color(0xFF102851), + ), + ), + + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "${product.totalSold.toCompact} Terjual", + style: TextStyle( + color: Colors.black87, + fontWeight: FontWeight.bold, + fontSize: (AppFontSize.small - 1.25).sp, + ), + ), + Row( + children: [ + Icon(Icons.star, color: Colors.orange, size: 3.5.w), + SizedBox(width: 1.w), + Text( + product.averageRating.toStringAsFixed(1), + style: TextStyle( + fontSize: (AppFontSize.small - 1.25).sp, + color: Colors.grey.shade600, + ), + ), + ], + ), + ], + ), + + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + CurrencyFormat.formatToIdr(product.sellingPrice, 0), + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: AppFontSize.small.sp, + color: AppColor.primaryColor, + ), + ), + Row( + children: [ + Icon( + Icons.thumb_up, + color: Colors.red, + size: 3.5.w, + ), + SizedBox(width: 1.w), + Text( + product.likes.toString(), + style: TextStyle( + fontSize: (AppFontSize.small - 1.25).sp, + color: Colors.red, + ), + ), + ], + ), + ], + ), + ], + ), + ), + ), + ], + ), + ), + ); + } + + Widget _buildProductShimmer() { + return Shimmer.fromColors( + baseColor: Colors.grey.shade300, + highlightColor: Colors.grey.shade100, + child: GridView.builder( + padding: EdgeInsets.zero, + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: 6, + gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 2, + mainAxisSpacing: 10, + crossAxisSpacing: 10, + childAspectRatio: 0.75, + ), + itemBuilder: (context, index) { + return Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(2.5.w), + ), + ); + }, + ), + ); + } +} diff --git a/Mobile Commerce/lib/features/home/presentation/screens/search_screen.dart b/Mobile Commerce/lib/features/home/presentation/screens/search_screen.dart new file mode 100644 index 0000000..a3a5915 --- /dev/null +++ b/Mobile Commerce/lib/features/home/presentation/screens/search_screen.dart @@ -0,0 +1,548 @@ +import 'dart:async'; +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; +import 'package:niogu_ecommerce_v1/core/constant/app_font_size.dart'; +import 'package:niogu_ecommerce_v1/core/errors/exceptions.dart'; +import 'package:niogu_ecommerce_v1/core/providers/app_provider.dart'; +import 'package:niogu_ecommerce_v1/core/router/app_route.dart'; +import 'package:niogu_ecommerce_v1/core/utils/currency_format.dart'; +import 'package:niogu_ecommerce_v1/core/utils/extenstion_format.dart'; +import 'package:niogu_ecommerce_v1/core/utils/log_message.dart'; +import 'package:niogu_ecommerce_v1/core/widgets/custom_empty_screen.dart'; +import 'package:niogu_ecommerce_v1/core/widgets/custom_snackbar.dart'; +import 'package:niogu_ecommerce_v1/features/favorite/domain/entities/favorite.dart'; +import 'package:niogu_ecommerce_v1/features/favorite/presentation/providers/favorite_provider.dart'; +import 'package:niogu_ecommerce_v1/features/home/domain/entities/home.dart'; +import 'package:niogu_ecommerce_v1/features/home/presentation/providers/home_provider.dart'; +import 'package:niogu_ecommerce_v1/features/product/presentation/providers/product_provider.dart'; +import 'package:sizer/sizer.dart'; +import 'package:niogu_ecommerce_v1/core/constant/app_color.dart'; + +class SearchScreen extends ConsumerStatefulWidget { + final List categories; + const SearchScreen({super.key, required this.categories}); + + @override + ConsumerState createState() => _SearchScreenState(); +} + +class _SearchScreenState extends ConsumerState { + late final List _categories; + + final TextEditingController _searchController = TextEditingController(); + + Timer? _debounce; + + @override + void initState() { + // TODO: implement initState + super.initState(); + + _categories = widget.categories; + } + + @override + void dispose() { + _searchController.dispose(); + _debounce?.cancel(); + super.dispose(); + } + + void _onSearchChanged(String query) { + if (_debounce?.isActive ?? false) _debounce?.cancel(); + + _debounce = Timer(const Duration(milliseconds: 800), () { + final processSerch = ref.read(processSearchStateProvider); + + if (processSerch != null) { + ref.read(processSearchStateProvider.notifier).state = null; + } + + ref.read(productSearchProvider.notifier).state = query; + }); + } + + @override + Widget build(BuildContext context) { + return LayoutBuilder( + builder: (context, constraints) { + final productByNameState = ref.watch(productByNameProvider); + + final processSearchState = ref.watch(processSearchProvider); + + final currentOutletId = ref.watch(currentOutletIdProvider); + + final favoriteState = ref.watch(favoriteControllerProvider); + + return SafeArea( + top: false, + bottom: true, + right: false, + left: false, + child: Scaffold( + backgroundColor: Colors.white, + appBar: AppBar( + backgroundColor: Colors.white, + elevation: 0.5, + leading: IconButton( + icon: Icon( + Icons.arrow_back, + size: 7.w, + color: AppColor.primaryColor, + ), + onPressed: () => context.pop(), + ), + title: Container( + height: 5.h, + decoration: BoxDecoration( + color: Colors.grey.shade100, + borderRadius: BorderRadius.circular(2.w), + ), + child: TextField( + controller: _searchController, + autofocus: true, + onChanged: _onSearchChanged, + style: TextStyle(fontSize: AppFontSize.small.sp), + decoration: InputDecoration( + hintText: "Ketik nama produk...", + hintStyle: TextStyle(fontSize: AppFontSize.small.sp), + prefixIcon: Icon(Icons.search, size: 5.w), + suffixIcon: _searchController.text.isNotEmpty + ? IconButton( + icon: Icon( + Icons.cancel, + color: Colors.grey, + size: 5.w, + ), + onPressed: () { + _searchController.clear(); + _onSearchChanged(""); + }, + ) + : null, + border: InputBorder.none, + contentPadding: EdgeInsets.symmetric(vertical: 1.h), + ), + ), + ), + ), + body: productByNameState.when( + data: (products) { + switch (processSearchState) { + case ProcessSearch.initial: + return _buildInitialRecommended(); + case ProcessSearch.suggestion: + return _buildSuggestionList(products); + case ProcessSearch.result: + return _buildProductResult( + products: products, + currentOutletId: currentOutletId!, + favorites: favoriteState, + ); + } + }, + error: (error, stackTrace) { + return SizedBox( + height: 80.h, + child: CustomEmptyScreen( + icon: Icons.cloud_off_outlined, + title: "Terjadi Kesalahan Koneksi", + subtitle: "Tarik ke bawah untuk mencoba lagi", + height: 80.h, + ), + ); + }, + loading: () => const Center( + child: CircularProgressIndicator(color: AppColor.primaryColor), + ), + ), + ), + ); + }, + ); + } + + Widget _buildInitialRecommended() { + return Padding( + padding: EdgeInsets.all(4.w), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Rekomendasi", + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: AppFontSize.small.sp, + ), + ), + SizedBox(height: 2.h), + GridView.builder( + shrinkWrap: true, + itemCount: _categories.length, + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 4, + mainAxisSpacing: 2.h, + childAspectRatio: 0.8, + ), + itemBuilder: (context, index) { + final category = _categories[index]; + return GestureDetector( + onTap: () { + context.pushNamed( + AppRoute.productCategoryScreen, + extra: category, + ); + }, + child: Padding( + padding: EdgeInsets.only(right: 5.w), + child: Column( + children: [ + CachedNetworkImage( + imageUrl: category.image ?? 'error', + imageBuilder: (context, imageProvider) { + return Container( + height: 18.w, + width: 18.w, + decoration: BoxDecoration( + color: Colors.white, + border: BoxBorder.all( + color: Colors.grey.shade300, + ), + borderRadius: BorderRadius.circular(2.5.w), + image: DecorationImage( + image: imageProvider, + fit: BoxFit.cover, + ), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.05), + blurRadius: 10, + ), + ], + ), + ); + }, + errorWidget: (context, url, error) { + return Container( + height: 18.w, + width: 18.w, + decoration: BoxDecoration( + color: Colors.white, + border: BoxBorder.all( + color: Colors.grey.shade300, + ), + borderRadius: BorderRadius.circular(2.5.w), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.05), + blurRadius: 10, + ), + ], + ), + child: Icon( + Icons.image_outlined, + size: 5.w, + color: Colors.grey.shade300, + ), + ); + }, + ), + + SizedBox(height: 1.h), + Text( + category.name, + style: TextStyle( + fontSize: (AppFontSize.small - 2).sp, + color: Color(0xFF102851), + ), + ), + ], + ), + ), + ); + }, + ), + ], + ), + ); + } + + Widget _buildSuggestionList(List products) { + if (products.isEmpty) { + return CustomEmptyScreen( + icon: Icons.search_off_outlined, + title: "Produk tidak tersedia", + height: 40.h, + ); + } + return ListView.builder( + itemCount: products.length, + itemBuilder: (context, index) { + final product = products[index]; + return ListTile( + leading: Icon(Icons.search, color: Colors.grey, size: 5.w), + title: Text( + product.name, + style: TextStyle( + color: Colors.grey.shade700, + fontSize: AppFontSize.small.sp, + ), + ), + trailing: Icon(Icons.north_west, color: Colors.grey, size: 5.w), + onTap: () { + ref.read(processSearchStateProvider.notifier).state = + ProcessSearch.result; + _searchController.text = product.name; + }, + ); + }, + ); + } + + Widget _buildProductResult({ + required List products, + required String currentOutletId, + required Map favorites, + }) { + return GridView.builder( + padding: EdgeInsets.all(4.w), + itemCount: products.length, + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 2, + childAspectRatio: 0.7, + crossAxisSpacing: 3.w, + mainAxisSpacing: 3.w, + ), + itemBuilder: (context, index) { + final product = products[index]; + + final isFavorite = favorites.containsKey( + "$currentOutletId-${product.id}", + ); + + return _buildProductCard(product, currentOutletId, isFavorite); + }, + ); + } + + Widget _buildProductCard( + ProductItem product, + String currentOutletId, + bool isFavorite, + ) { + return GestureDetector( + onTap: () async { + try { + final productDetail = await ref + .read(productRepositoryProvider) + .fetchProductById(product.id); + + if (productDetail == null) { + CustomSnackbar.showError(context, "Produk tidak ditemukan"); + ref.read(homeControllerProvider.notifier).refresh(); + return; + } + + context.pushNamed(AppRoute.productDetailScreen, extra: productDetail); + } on ServerException catch (e, st) { + LogMessage.log.i(e.toString(), error: e, stackTrace: st); + } + }, + child: Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(2.5.w), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.04), + blurRadius: 10, + offset: const Offset(0, 4), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + flex: 3, + child: CachedNetworkImage( + imageUrl: product.image ?? 'error', + imageBuilder: (context, imageProvider) { + return Container( + width: double.infinity, + decoration: BoxDecoration( + color: Colors.grey.shade100, + borderRadius: BorderRadius.vertical( + top: Radius.circular(2.5.w), + ), + image: DecorationImage( + image: imageProvider, + fit: BoxFit.cover, + ), + ), + child: Stack( + children: [ + Positioned( + top: 1.w, + right: 1.w, + child: GestureDetector( + onTap: () { + final currentOutletId = ref.read( + currentOutletIdProvider, + ); + ref + .read(favoriteControllerProvider.notifier) + .toggle(product, currentOutletId!); + }, + child: CircleAvatar( + radius: 14, + backgroundColor: Colors.white.withOpacity(0.9), + child: Icon( + isFavorite + ? Icons.favorite + : Icons.favorite_outline, + size: 5.w, + color: isFavorite ? Colors.red : Colors.grey, + ), + ), + ), + ), + ], + ), + ); + }, + errorWidget: (context, url, error) { + return Container( + width: double.infinity, + decoration: BoxDecoration( + color: Colors.grey.shade100, + borderRadius: BorderRadius.vertical( + top: Radius.circular(2.5.w), + ), + ), + child: Stack( + children: [ + Center( + child: Icon( + Icons.image_outlined, + color: Colors.grey.shade300, + size: 10.w, + ), + ), + Positioned( + top: 1.w, + right: 1.w, + child: GestureDetector( + onTap: () { + ref + .read(favoriteControllerProvider.notifier) + .toggle(product, currentOutletId); + }, + child: CircleAvatar( + radius: 14, + backgroundColor: Colors.white.withOpacity(0.9), + child: Icon( + isFavorite + ? Icons.favorite + : Icons.favorite_outline, + size: 5.w, + color: isFavorite ? Colors.red : Colors.grey, + ), + ), + ), + ), + ], + ), + ); + }, + ), + ), + + Expanded( + flex: 2, + child: Padding( + padding: EdgeInsets.all(3.w), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + product.name, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: (AppFontSize.small - 1.25).sp, + color: const Color(0xFF102851), + ), + ), + + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "${product.totalSold.toCompact} Terjual", + style: TextStyle( + color: Colors.black87, + fontWeight: FontWeight.bold, + fontSize: (AppFontSize.small - 1.25).sp, + ), + ), + Row( + children: [ + Icon(Icons.star, color: Colors.orange, size: 3.5.w), + SizedBox(width: 1.w), + Text( + product.averageRating.toStringAsFixed(1), + style: TextStyle( + fontSize: (AppFontSize.small - 1.25).sp, + color: Colors.grey.shade600, + ), + ), + ], + ), + ], + ), + + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + CurrencyFormat.formatToIdr(product.sellingPrice, 0), + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: AppFontSize.small.sp, + color: AppColor.primaryColor, + ), + ), + Row( + children: [ + Icon( + Icons.thumb_up, + color: Colors.red, + size: 3.5.w, + ), + SizedBox(width: 1.w), + Text( + product.likes.toString(), + style: TextStyle( + fontSize: (AppFontSize.small - 1.25).sp, + color: Colors.red, + ), + ), + ], + ), + ], + ), + ], + ), + ), + ), + ], + ), + ), + ); + } +} diff --git a/Mobile Commerce/lib/features/order/data/datasource/order_remote_datasource_impl.dart b/Mobile Commerce/lib/features/order/data/datasource/order_remote_datasource_impl.dart new file mode 100644 index 0000000..9a6da51 --- /dev/null +++ b/Mobile Commerce/lib/features/order/data/datasource/order_remote_datasource_impl.dart @@ -0,0 +1,111 @@ +import 'package:dio/dio.dart'; +import 'package:niogu_ecommerce_v1/core/constant/app_url.dart'; +import 'package:niogu_ecommerce_v1/core/models/api_response_model.dart'; +import 'package:niogu_ecommerce_v1/core/system/system_setting.dart'; +import 'package:niogu_ecommerce_v1/features/order/data/models/order_model.dart'; + +abstract class IOrderRemoteDatasource { + Future>> fetchOrders(); + + Future> fetchOrderById(String orderId); + + Future> cancelOrder(String orderId); + + Future> cancellationRequest(String orderId); + + Future>> + fetchProductReviewItem(String orderId); + + Future> reviewProduct( + String orderId, + List request, + ); +} + +class OrderRemoteDatasourceImpl implements IOrderRemoteDatasource { + final Dio _dio; + + OrderRemoteDatasourceImpl(this._dio); + + @override + Future>> fetchOrders() async { + final customerId = await SystemSetting.getCurrentCustomerId(); + + final response = await _dio.get("${AppUrl.ORDERS}/customer/$customerId"); + + return ApiResponseModel.fromJson( + response.data, + (json) => (json as List) + .map( + (item) => + OrderReportResponse.fromJson(item as Map), + ) + .toList(), + ); + } + + @override + Future> fetchOrderById( + String orderId, + ) async { + final customerId = await SystemSetting.getCurrentCustomerId(); + + final response = await _dio.get( + "${AppUrl.ORDERS}/customer/$customerId/$orderId", + ); + + return ApiResponseModel.fromJson( + response.data, + (json) => OrderInfoResponse.fromJson(json as Map), + ); + } + + @override + Future> cancelOrder(String orderId) async { + final customerId = await SystemSetting.getCurrentCustomerId(); + + final response = await _dio.patch( + "${AppUrl.ORDERS}/$customerId/$orderId/cancel", + ); + + return ApiResponseModel.fromJson(response.data, (json) => null); + } + + @override + Future> cancellationRequest(String orderId) async { + final customerId = await SystemSetting.getCurrentCustomerId(); + + final response = await _dio.patch( + "${AppUrl.ORDERS}/$customerId/$orderId/cancellation-request", + ); + + return ApiResponseModel.fromJson(response.data, (json) => null); + } + + @override + Future>> + fetchProductReviewItem(String orderId) async { + final response = await _dio.get("${AppUrl.REVIEW}/$orderId"); + + return ApiResponseModel.fromJson( + response.data, + (json) => (json as List) + .map((item) => ProductReviewItemResponse.fromJson(item)) + .toList(), + ); + } + + @override + Future> reviewProduct( + String orderId, + List request, + ) async { + final response = await _dio.post( + "${AppUrl.REVIEW}/$orderId", + data: request.map((item) => item.toJson()).toList(), + options: Options(contentType: Headers.jsonContentType), + ); + + return ApiResponseModel.fromJson(response.data, (json) => null); + } +} diff --git a/Mobile Commerce/lib/features/order/data/models/order_model.dart b/Mobile Commerce/lib/features/order/data/models/order_model.dart new file mode 100644 index 0000000..b603b5c --- /dev/null +++ b/Mobile Commerce/lib/features/order/data/models/order_model.dart @@ -0,0 +1,362 @@ +import 'package:json_annotation/json_annotation.dart'; +import 'package:niogu_ecommerce_v1/core/enums/approval_process.dart'; +import 'package:niogu_ecommerce_v1/core/enums/delivery_fee_type.dart'; +import 'package:niogu_ecommerce_v1/core/enums/delivery_type.dart'; +import 'package:niogu_ecommerce_v1/core/enums/order_status.dart'; +import 'package:niogu_ecommerce_v1/core/utils/converter.dart'; +import 'package:niogu_ecommerce_v1/features/checkout/domain/entities/checkout.dart'; +import 'package:niogu_ecommerce_v1/features/order/domain/entities/order.dart'; + +part 'order_model.g.dart'; + +@JsonSerializable() +class OrderReportResponse { + final String id; + + @JsonKey(name: 'order_number') + final String orderNumber; + + @JsonKey(name: 'total_items') + final int totalItems; + + @JsonKey(name: 'product_images') + final List productImages; + + @JsonKey(name: 'total_amount', fromJson: toDouble) + final double totalAmount; + + @JsonKey(name: 'order_status') + final String orderStatus; + + @JsonKey(name: 'delivery_type') + final String deliveryType; + + @JsonKey(name: 'has_been_assessed', defaultValue: false) + final bool hasBeenAssessed; + + @JsonKey(name: 'created_at') + final String createdAt; + + const OrderReportResponse({ + required this.id, + required this.orderNumber, + required this.productImages, + required this.totalItems, + required this.totalAmount, + required this.orderStatus, + required this.deliveryType, + required this.hasBeenAssessed, + required this.createdAt, + }); + + factory OrderReportResponse.fromJson(Map json) => + _$OrderReportResponseFromJson(json); + + Map toJson() => _$OrderReportResponseToJson(this); + + OrderReport toOrderReport() { + return OrderReport( + id: id, + orderNumber: orderNumber, + productImages: productImages, + totalItems: totalItems, + totalAmount: totalAmount, + orderStatus: OrderStatus.values.byName(orderStatus), + deliveryType: deliveryType, + hasBeenAssessed: hasBeenAssessed, + createdAt: DateTime.parse(createdAt), + ); + } +} + +@JsonSerializable() +class OrderInfoResponse { + final String id; + + @JsonKey(name: 'order_number') + final String orderNumber; + + @JsonKey(name: 'created_at') + final String createdAt; + + @JsonKey(name: 'order_status') + final String orderStatus; + + @JsonKey(name: 'is_cancellation') + final bool? isCancellation; + + @JsonKey(name: 'cancellation_accepted') + final bool? cancellationAccepted; + + @JsonKey(name: 'approval_process') + final String? approvalProcess; + + @JsonKey(name: 'delivery_type') + final String deliveryType; + + @JsonKey(name: 'delivery_preference') + final String deliveryPreference; + + @JsonKey(name: 'outlet_name_snapshot') + final String outletNameSnapshot; + + @JsonKey(name: 'outlet_address_snapshot') + final OutletAddressSnapshotResponse? outletAddressSnapshot; + + @JsonKey(name: 'customer_address_snapshot') + final CustomerAddressSnapshotResponse? customerAddressSnapshot; + + @JsonKey(name: 'order_items') + final List orderItems; + + @JsonKey(name: 'payment_method') + final String paymentMethod; + + @JsonKey(name: 'payment_proof_url') + final String? paymentProofUrl; + + @JsonKey(name: 'total_order', fromJson: toDouble) + final double totalOrder; + + @JsonKey(name: 'delivery_fee_type') + final String? deliveryFeeType; + + @JsonKey(name: 'delivery_fee') + final double deliveryFee; + + @JsonKey(name: 'total_delivery_fee', fromJson: toDouble) + final double totalDeliveryFee; + + @JsonKey(name: 'total_amount', fromJson: toDouble) + final double totalAmount; + + const OrderInfoResponse({ + required this.id, + required this.orderNumber, + required this.createdAt, + required this.orderStatus, + required this.isCancellation, + required this.cancellationAccepted, + required this.approvalProcess, + required this.deliveryType, + required this.deliveryPreference, + required this.outletNameSnapshot, + required this.outletAddressSnapshot, + required this.customerAddressSnapshot, + required this.orderItems, + required this.paymentMethod, + required this.paymentProofUrl, + required this.totalOrder, + required this.deliveryFeeType, + required this.deliveryFee, + required this.totalDeliveryFee, + required this.totalAmount, + }); + + factory OrderInfoResponse.fromJson(Map json) => + _$OrderInfoResponseFromJson(json); + + Map toJson() => _$OrderInfoResponseToJson(this); + + OrderInfo toOrderInfo() { + return OrderInfo( + id: id, + orderNumber: orderNumber, + createdAt: DateTime.parse(createdAt), + orderStatus: OrderStatus.values.byName(orderStatus), + isCancellation: isCancellation, + cancellationAccepted: cancellationAccepted, + approvalProcess: approvalProcess != null + ? ApprovalProcess.values.byName(approvalProcess!) + : null, + deliveryType: DeliveryType.values.byName(deliveryType), + deliveryPreference: deliveryPreference, + outletNameSnapshot: outletNameSnapshot, + outletAddressSnapshot: outletAddressSnapshot?.toOutletAdddressSnapshot(), + customerAddressSnapshot: customerAddressSnapshot + ?.toCustomerAddressSnapshot(), + orderItems: orderItems.map((item) => item.toOrderItemInfo()).toList(), + paymentMethod: paymentMethod, + paymentProofUrl: paymentProofUrl, + totalOrder: totalOrder, + deliveryFeeType: DeliveryFeeType.values.byName( + deliveryFeeType ?? 'fixed', + ), + deliveryFee: deliveryFee, + totalDeliveryFee: totalDeliveryFee, + totalAmount: totalAmount, + ); + } +} + +@JsonSerializable() +class OutletAddressSnapshotResponse { + @JsonKey(name: 'full_address') + final String fullAddress; + + @JsonKey(fromJson: toDouble) + final double latitude; + + @JsonKey(fromJson: toDouble) + final double longitude; + + const OutletAddressSnapshotResponse({ + required this.fullAddress, + required this.latitude, + required this.longitude, + }); + + factory OutletAddressSnapshotResponse.fromJson(Map json) => + _$OutletAddressSnapshotResponseFromJson(json); + + Map toJson() => _$OutletAddressSnapshotResponseToJson(this); + + OutletAddressSnapshot toOutletAdddressSnapshot() { + return OutletAddressSnapshot( + fullAddress: fullAddress, + latitude: latitude, + longitude: longitude, + ); + } +} + +@JsonSerializable() +class CustomerAddressSnapshotResponse { + final String label; + + @JsonKey(name: 'full_address') + final String fullAddress; + + @JsonKey(fromJson: toDouble) + final double latitude; + + @JsonKey(fromJson: toDouble) + final double longitude; + + const CustomerAddressSnapshotResponse({ + required this.label, + required this.fullAddress, + required this.latitude, + required this.longitude, + }); + + factory CustomerAddressSnapshotResponse.fromJson(Map json) => + _$CustomerAddressSnapshotResponseFromJson(json); + + Map toJson() => + _$CustomerAddressSnapshotResponseToJson(this); + + CustomerAddressSnapshot toCustomerAddressSnapshot() { + return CustomerAddressSnapshot( + label: label, + fullAddress: fullAddress, + latitude: latitude, + longitude: longitude, + ); + } +} + +@JsonSerializable() +class OrderItemInfoResponse { + final String id; + + @JsonKey(name: 'product_image_url_snapshot') + final String? productImageUrlSnapshot; + + @JsonKey(name: 'product_name_snapshot') + final String productNameSnapshot; + + @JsonKey(name: 'product_variant_name_snapshot') + final String? productVariantNameSnapshot; + + @JsonKey(name: 'selling_price_snapshot', fromJson: toDouble) + final double sellingPriceSnapshot; + + final int quantity; + + const OrderItemInfoResponse({ + required this.id, + required this.productImageUrlSnapshot, + required this.productNameSnapshot, + required this.productVariantNameSnapshot, + required this.sellingPriceSnapshot, + required this.quantity, + }); + + factory OrderItemInfoResponse.fromJson(Map json) => + _$OrderItemInfoResponseFromJson(json); + + Map toJson() => _$OrderItemInfoResponseToJson(this); + + OrderItemInfo toOrderItemInfo() { + return OrderItemInfo( + id: id, + productImageUrlSnapshot: productImageUrlSnapshot, + productNameSnapshot: productNameSnapshot, + productVariantNameSnapshot: productVariantNameSnapshot, + sellingPriceSnapshot: sellingPriceSnapshot, + quantity: quantity, + ); + } +} + +@JsonSerializable() +class ProductReviewItemResponse { + @JsonKey(name: 'product_variant_id') + final String productVariantId; + + @JsonKey(name: 'product_image_url_snapshot') + final String? productImageUrlSnapshot; + + @JsonKey(name: 'product_name_snapshot') + final String productNameSnapshot; + + @JsonKey(name: 'product_variant_name_snapshot') + final String? productVariantNameSnapshot; + + const ProductReviewItemResponse({ + required this.productVariantId, + this.productImageUrlSnapshot, + required this.productNameSnapshot, + this.productVariantNameSnapshot, + }); + + factory ProductReviewItemResponse.fromJson(Map json) => + _$ProductReviewItemResponseFromJson(json); + + Map toJson() => _$ProductReviewItemResponseToJson(this); + + ProductReviewItem toProductReviewItem() { + return ProductReviewItem( + id: productVariantId, + image: productImageUrlSnapshot, + name: productNameSnapshot, + variantName: productVariantNameSnapshot, + ); + } +} + +@JsonSerializable() +class ProductReviewRequest { + @JsonKey(name: 'customer_id') + final String customerId; + + @JsonKey(name: 'product_variant_id') + final String productVariantId; + + final int rating; + + final String? comment; + + const ProductReviewRequest({ + required this.customerId, + required this.productVariantId, + required this.rating, + this.comment, + }); + + factory ProductReviewRequest.fromJson(Map json) => + _$ProductReviewRequestFromJson(json); + + Map toJson() => _$ProductReviewRequestToJson(this); +} diff --git a/Mobile Commerce/lib/features/order/data/models/order_model.g.dart b/Mobile Commerce/lib/features/order/data/models/order_model.g.dart new file mode 100644 index 0000000..701d703 --- /dev/null +++ b/Mobile Commerce/lib/features/order/data/models/order_model.g.dart @@ -0,0 +1,186 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'order_model.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +OrderReportResponse _$OrderReportResponseFromJson(Map json) => + OrderReportResponse( + id: json['id'] as String, + orderNumber: json['order_number'] as String, + productImages: (json['product_images'] as List) + .map((e) => e as String?) + .toList(), + totalItems: (json['total_items'] as num).toInt(), + totalAmount: toDouble(json['total_amount']), + orderStatus: json['order_status'] as String, + deliveryType: json['delivery_type'] as String, + hasBeenAssessed: json['has_been_assessed'] as bool? ?? false, + createdAt: json['created_at'] as String, + ); + +Map _$OrderReportResponseToJson( + OrderReportResponse instance, +) => { + 'id': instance.id, + 'order_number': instance.orderNumber, + 'total_items': instance.totalItems, + 'product_images': instance.productImages, + 'total_amount': instance.totalAmount, + 'order_status': instance.orderStatus, + 'delivery_type': instance.deliveryType, + 'has_been_assessed': instance.hasBeenAssessed, + 'created_at': instance.createdAt, +}; + +OrderInfoResponse _$OrderInfoResponseFromJson(Map json) => + OrderInfoResponse( + id: json['id'] as String, + orderNumber: json['order_number'] as String, + createdAt: json['created_at'] as String, + orderStatus: json['order_status'] as String, + isCancellation: json['is_cancellation'] as bool?, + cancellationAccepted: json['cancellation_accepted'] as bool?, + approvalProcess: json['approval_process'] as String?, + deliveryType: json['delivery_type'] as String, + deliveryPreference: json['delivery_preference'] as String, + outletNameSnapshot: json['outlet_name_snapshot'] as String, + outletAddressSnapshot: json['outlet_address_snapshot'] == null + ? null + : OutletAddressSnapshotResponse.fromJson( + json['outlet_address_snapshot'] as Map, + ), + customerAddressSnapshot: json['customer_address_snapshot'] == null + ? null + : CustomerAddressSnapshotResponse.fromJson( + json['customer_address_snapshot'] as Map, + ), + orderItems: (json['order_items'] as List) + .map((e) => OrderItemInfoResponse.fromJson(e as Map)) + .toList(), + paymentMethod: json['payment_method'] as String, + paymentProofUrl: json['payment_proof_url'] as String?, + totalOrder: toDouble(json['total_order']), + deliveryFeeType: json['delivery_fee_type'] as String?, + deliveryFee: (json['delivery_fee'] as num).toDouble(), + totalDeliveryFee: toDouble(json['total_delivery_fee']), + totalAmount: toDouble(json['total_amount']), + ); + +Map _$OrderInfoResponseToJson(OrderInfoResponse instance) => + { + 'id': instance.id, + 'order_number': instance.orderNumber, + 'created_at': instance.createdAt, + 'order_status': instance.orderStatus, + 'is_cancellation': instance.isCancellation, + 'cancellation_accepted': instance.cancellationAccepted, + 'approval_process': instance.approvalProcess, + 'delivery_type': instance.deliveryType, + 'delivery_preference': instance.deliveryPreference, + 'outlet_name_snapshot': instance.outletNameSnapshot, + 'outlet_address_snapshot': instance.outletAddressSnapshot, + 'customer_address_snapshot': instance.customerAddressSnapshot, + 'order_items': instance.orderItems, + 'payment_method': instance.paymentMethod, + 'payment_proof_url': instance.paymentProofUrl, + 'total_order': instance.totalOrder, + 'delivery_fee_type': instance.deliveryFeeType, + 'delivery_fee': instance.deliveryFee, + 'total_delivery_fee': instance.totalDeliveryFee, + 'total_amount': instance.totalAmount, + }; + +OutletAddressSnapshotResponse _$OutletAddressSnapshotResponseFromJson( + Map json, +) => OutletAddressSnapshotResponse( + fullAddress: json['full_address'] as String, + latitude: toDouble(json['latitude']), + longitude: toDouble(json['longitude']), +); + +Map _$OutletAddressSnapshotResponseToJson( + OutletAddressSnapshotResponse instance, +) => { + 'full_address': instance.fullAddress, + 'latitude': instance.latitude, + 'longitude': instance.longitude, +}; + +CustomerAddressSnapshotResponse _$CustomerAddressSnapshotResponseFromJson( + Map json, +) => CustomerAddressSnapshotResponse( + label: json['label'] as String, + fullAddress: json['full_address'] as String, + latitude: toDouble(json['latitude']), + longitude: toDouble(json['longitude']), +); + +Map _$CustomerAddressSnapshotResponseToJson( + CustomerAddressSnapshotResponse instance, +) => { + 'label': instance.label, + 'full_address': instance.fullAddress, + 'latitude': instance.latitude, + 'longitude': instance.longitude, +}; + +OrderItemInfoResponse _$OrderItemInfoResponseFromJson( + Map json, +) => OrderItemInfoResponse( + id: json['id'] as String, + productImageUrlSnapshot: json['product_image_url_snapshot'] as String?, + productNameSnapshot: json['product_name_snapshot'] as String, + productVariantNameSnapshot: json['product_variant_name_snapshot'] as String?, + sellingPriceSnapshot: toDouble(json['selling_price_snapshot']), + quantity: (json['quantity'] as num).toInt(), +); + +Map _$OrderItemInfoResponseToJson( + OrderItemInfoResponse instance, +) => { + 'id': instance.id, + 'product_image_url_snapshot': instance.productImageUrlSnapshot, + 'product_name_snapshot': instance.productNameSnapshot, + 'product_variant_name_snapshot': instance.productVariantNameSnapshot, + 'selling_price_snapshot': instance.sellingPriceSnapshot, + 'quantity': instance.quantity, +}; + +ProductReviewItemResponse _$ProductReviewItemResponseFromJson( + Map json, +) => ProductReviewItemResponse( + productVariantId: json['product_variant_id'] as String, + productImageUrlSnapshot: json['product_image_url_snapshot'] as String?, + productNameSnapshot: json['product_name_snapshot'] as String, + productVariantNameSnapshot: json['product_variant_name_snapshot'] as String?, +); + +Map _$ProductReviewItemResponseToJson( + ProductReviewItemResponse instance, +) => { + 'product_variant_id': instance.productVariantId, + 'product_image_url_snapshot': instance.productImageUrlSnapshot, + 'product_name_snapshot': instance.productNameSnapshot, + 'product_variant_name_snapshot': instance.productVariantNameSnapshot, +}; + +ProductReviewRequest _$ProductReviewRequestFromJson( + Map json, +) => ProductReviewRequest( + customerId: json['customer_id'] as String, + productVariantId: json['product_variant_id'] as String, + rating: (json['rating'] as num).toInt(), + comment: json['comment'] as String?, +); + +Map _$ProductReviewRequestToJson( + ProductReviewRequest instance, +) => { + 'customer_id': instance.customerId, + 'product_variant_id': instance.productVariantId, + 'rating': instance.rating, + 'comment': instance.comment, +}; diff --git a/Mobile Commerce/lib/features/order/data/repositories/order_repository_impl.dart b/Mobile Commerce/lib/features/order/data/repositories/order_repository_impl.dart new file mode 100644 index 0000000..035eced --- /dev/null +++ b/Mobile Commerce/lib/features/order/data/repositories/order_repository_impl.dart @@ -0,0 +1,98 @@ +import 'package:dio/dio.dart'; +import 'package:niogu_ecommerce_v1/core/errors/exceptions.dart'; +import 'package:niogu_ecommerce_v1/core/utils/log_message.dart'; +import 'package:niogu_ecommerce_v1/features/order/data/datasource/order_remote_datasource_impl.dart'; +import 'package:niogu_ecommerce_v1/features/order/domain/entities/order.dart'; +import 'package:niogu_ecommerce_v1/features/order/domain/repositories/i_order_repository.dart'; + +class OrderRepositoryImpl implements IOrderRepository { + final IOrderRemoteDatasource _datasource; + + OrderRepositoryImpl(this._datasource); + + @override + Future> fetchOrders() async { + try { + final response = await _datasource.fetchOrders(); + + final data = response.data; + + if (response.status != 'success' || data == null) return []; + + if (data.isEmpty) return []; + + return data.map((order) => order.toOrderReport()).toList(); + } on DioException catch (e, st) { + LogMessage.log.e(e.toString(), error: e, stackTrace: st); + throw ServerException("Terjadi kesalahan koneksi"); + } + } + + @override + Future fetchOrderById(String orderId) async { + try { + final response = await _datasource.fetchOrderById(orderId); + + final order = response.data; + + if (response.status != 'success' || order == null) return null; + + return order.toOrderInfo(); + } on DioException catch (e, st) { + LogMessage.log.e(e.toString(), error: e, stackTrace: st); + throw ServerException("Terjadi kesalahan koneksi"); + } + } + + @override + Future cancelOrder(String orderId) async { + try { + await _datasource.cancelOrder(orderId); + } on DioException catch (e, st) { + LogMessage.log.e(e.toString(), error: e, stackTrace: st); + throw ServerException("Terjadi kesalahan koneksi"); + } + } + + @override + Future cancellationRequest(String orderId) async { + try { + await _datasource.cancellationRequest(orderId); + } on DioException catch (e, st) { + LogMessage.log.e(e.toString(), error: e, stackTrace: st); + throw ServerException("Terjadi kesalahan koneksi"); + } + } + + @override + Future> fetchProductReviewItem(String orderId) async { + try { + final response = await _datasource.fetchProductReviewItem(orderId); + + final products = response.data; + + if (response.status != 'success' || products == null) return []; + + return products.map((product) => product.toProductReviewItem()).toList(); + } on DioException catch (e, st) { + LogMessage.log.e(e.toString(), error: e, stackTrace: st); + throw ServerException("Terjadi kesalahan koneksi"); + } + } + + @override + Future reviewProduct( + String orderId, + List reviews, + ) async { + try { + await _datasource.reviewProduct( + orderId, + reviews.map((review) => review.toProductReviewRequest()).toList(), + ); + } on DioException catch (e, st) { + LogMessage.log.e(e.toString(), error: e, stackTrace: st); + throw ServerException("Terjadi kesalahan koneksi"); + } + } +} diff --git a/Mobile Commerce/lib/features/order/domain/entities/order.dart b/Mobile Commerce/lib/features/order/domain/entities/order.dart new file mode 100644 index 0000000..d629f18 --- /dev/null +++ b/Mobile Commerce/lib/features/order/domain/entities/order.dart @@ -0,0 +1,158 @@ +import 'package:niogu_ecommerce_v1/core/enums/approval_process.dart'; +import 'package:niogu_ecommerce_v1/core/enums/delivery_fee_type.dart'; +import 'package:niogu_ecommerce_v1/core/enums/delivery_type.dart'; +import 'package:niogu_ecommerce_v1/core/enums/order_status.dart'; +import 'package:niogu_ecommerce_v1/features/checkout/domain/entities/checkout.dart'; +import 'package:niogu_ecommerce_v1/features/order/data/models/order_model.dart'; + +class OrderReport { + final String id; + + final String orderNumber; + + final int totalItems; + + final List productImages; + + final double totalAmount; + + final OrderStatus orderStatus; + + final String deliveryType; + + final bool hasBeenAssessed; + + final DateTime createdAt; + + OrderReport({ + required this.id, + required this.orderNumber, + required this.productImages, + required this.totalItems, + required this.totalAmount, + required this.orderStatus, + required this.deliveryType, + required this.hasBeenAssessed, + required this.createdAt, + }); +} + +class OrderInfo { + final String id; + + final String orderNumber; + + final DateTime createdAt; + + final OrderStatus orderStatus; + + final bool? isCancellation; + + final bool? cancellationAccepted; + + final ApprovalProcess? approvalProcess; + + final DeliveryType deliveryType; + + final String deliveryPreference; + + final String outletNameSnapshot; + + final OutletAddressSnapshot? outletAddressSnapshot; + + final CustomerAddressSnapshot? customerAddressSnapshot; + + final List orderItems; + + final String paymentMethod; + + final String? paymentProofUrl; + + final double totalOrder; + + final DeliveryFeeType? deliveryFeeType; + + final double deliveryFee; + + final double totalDeliveryFee; + + final double totalAmount; + + const OrderInfo({ + required this.id, + required this.orderNumber, + required this.createdAt, + required this.orderStatus, + this.isCancellation, + this.cancellationAccepted, + this.approvalProcess, + required this.deliveryType, + required this.deliveryPreference, + required this.outletNameSnapshot, + this.outletAddressSnapshot, + this.customerAddressSnapshot, + required this.orderItems, + required this.paymentMethod, + required this.paymentProofUrl, + required this.totalOrder, + this.deliveryFeeType, + required this.deliveryFee, + required this.totalDeliveryFee, + required this.totalAmount, + }); +} + +class OrderItemInfo { + final String id; + final String? productImageUrlSnapshot; + final String productNameSnapshot; + final String? productVariantNameSnapshot; + final double sellingPriceSnapshot; + final int quantity; + + const OrderItemInfo({ + required this.id, + this.productImageUrlSnapshot, + required this.productNameSnapshot, + this.productVariantNameSnapshot, + required this.sellingPriceSnapshot, + required this.quantity, + }); +} + +class ProductReviewItem { + final String id; + final String? image; + final String name; + final String? variantName; + + const ProductReviewItem({ + required this.id, + this.image, + required this.name, + required this.variantName, + }); +} + +class ProductReview { + final String customerId; + final String productVariantId; + final int rating; + final String? comment; + + const ProductReview({ + required this.customerId, + required this.productVariantId, + required this.rating, + this.comment, + }); + + ProductReviewRequest toProductReviewRequest() { + return ProductReviewRequest( + customerId: customerId, + productVariantId: productVariantId, + rating: rating, + comment: comment, + ); + } +} diff --git a/Mobile Commerce/lib/features/order/domain/repositories/i_order_repository.dart b/Mobile Commerce/lib/features/order/domain/repositories/i_order_repository.dart new file mode 100644 index 0000000..10f799c --- /dev/null +++ b/Mobile Commerce/lib/features/order/domain/repositories/i_order_repository.dart @@ -0,0 +1,15 @@ +import 'package:niogu_ecommerce_v1/features/order/domain/entities/order.dart'; + +abstract class IOrderRepository { + Future> fetchOrders(); + + Future fetchOrderById(String orderId); + + Future cancelOrder(String orderId); + + Future cancellationRequest(String orderId); + + Future> fetchProductReviewItem(String orderId); + + Future reviewProduct(String orderId, List reviews); +} diff --git a/Mobile Commerce/lib/features/order/presentation/controllers/order_controller.dart b/Mobile Commerce/lib/features/order/presentation/controllers/order_controller.dart new file mode 100644 index 0000000..57fc10b --- /dev/null +++ b/Mobile Commerce/lib/features/order/presentation/controllers/order_controller.dart @@ -0,0 +1,68 @@ +import 'dart:async'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:niogu_ecommerce_v1/features/order/domain/entities/order.dart'; +import 'package:niogu_ecommerce_v1/features/order/domain/repositories/i_order_repository.dart'; +import 'package:niogu_ecommerce_v1/features/order/presentation/providers/order_provider.dart'; + +class OrderController extends AutoDisposeAsyncNotifier { + late final IOrderRepository _orderRepository; + + @override + FutureOr build() { + _orderRepository = ref.read(orderRepositoryProvider); + } + + Future cancelOrder(String orderId) async { + state = const AsyncValue.loading(); + + final result = await AsyncValue.guard(() async { + await _orderRepository.cancelOrder(orderId); + }); + + state = result; + + if (result is AsyncError) throw result.error!; + } + + Future cancellationRequest(String orderId) async { + state = const AsyncValue.loading(); + + final result = await AsyncValue.guard(() async { + await _orderRepository.cancellationRequest(orderId); + }); + + state = result; + + if (result is AsyncError) throw result.error!; + } + + Future reviewProduct(String orderId, List reviews) async { + state = const AsyncValue.loading(); + + final result = await AsyncValue.guard(() async { + await _orderRepository.reviewProduct(orderId, reviews); + }); + + state = result; + + if (result is AsyncError) throw result.error!; + } +} + +class OrderReportController + extends AutoDisposeAsyncNotifier> { + @override + FutureOr> build() => _fetchOrders(); + + Future> _fetchOrders() async { + final orderRepository = ref.read(orderRepositoryProvider); + + return await orderRepository.fetchOrders(); + } + + Future refresh() async { + ref.invalidateSelf(); + + await future; + } +} diff --git a/Mobile Commerce/lib/features/order/presentation/providers/order_provider.dart b/Mobile Commerce/lib/features/order/presentation/providers/order_provider.dart new file mode 100644 index 0000000..b1c3cef --- /dev/null +++ b/Mobile Commerce/lib/features/order/presentation/providers/order_provider.dart @@ -0,0 +1,57 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:niogu_ecommerce_v1/core/enums/order_status.dart'; +import 'package:niogu_ecommerce_v1/core/providers/app_provider.dart'; +import 'package:niogu_ecommerce_v1/features/order/data/datasource/order_remote_datasource_impl.dart'; +import 'package:niogu_ecommerce_v1/features/order/data/repositories/order_repository_impl.dart'; +import 'package:niogu_ecommerce_v1/features/order/domain/entities/order.dart'; +import 'package:niogu_ecommerce_v1/features/order/domain/repositories/i_order_repository.dart'; +import 'package:niogu_ecommerce_v1/features/order/presentation/controllers/order_controller.dart'; + +final orderRemoteDatasourceProvider = + Provider.autoDispose((ref) { + final dio = ref.watch(dioProvider); + + return OrderRemoteDatasourceImpl(dio); + }); + +final orderRepositoryProvider = Provider.autoDispose((ref) { + final orderRemoteDatasource = ref.watch(orderRemoteDatasourceProvider); + + return OrderRepositoryImpl(orderRemoteDatasource); +}); + +final orderReportControllerProvider = + AsyncNotifierProvider.autoDispose>( + OrderReportController.new, + ); + +final orderControllerProvider = + AsyncNotifierProvider.autoDispose( + OrderController.new, + ); + +final orderProvider = Provider.family + .autoDispose>, List>(( + ref, + orderStatuses, + ) { + final isLoggedIn = ref.watch(currentStatusLoginProvider); + + if (!isLoggedIn) return AsyncValue.data([]); + + final orderState = ref.watch(orderReportControllerProvider); + + return orderState.when( + data: (orders) { + if (orders.isEmpty) return AsyncValue.data([]); + + final filteredOrders = orders + .where((order) => orderStatuses.contains(order.orderStatus)) + .toList(); + + return AsyncValue.data(filteredOrders); + }, + error: (error, st) => AsyncValue.error(error, st), + loading: () => const AsyncValue.loading(), + ); + }); diff --git a/Mobile Commerce/lib/features/order/presentation/screens/order_detail_screen.dart b/Mobile Commerce/lib/features/order/presentation/screens/order_detail_screen.dart new file mode 100644 index 0000000..9c98ccb --- /dev/null +++ b/Mobile Commerce/lib/features/order/presentation/screens/order_detail_screen.dart @@ -0,0 +1,1008 @@ +import 'dart:math'; +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; +import 'package:intl/intl.dart'; +import 'package:niogu_ecommerce_v1/core/constant/app_asset.dart'; +import 'package:niogu_ecommerce_v1/core/constant/app_font_size.dart'; +import 'package:niogu_ecommerce_v1/core/enums/action_type.dart'; +import 'package:niogu_ecommerce_v1/core/enums/approval_process.dart'; +import 'package:niogu_ecommerce_v1/core/enums/delivery_fee_type.dart'; +import 'package:niogu_ecommerce_v1/core/enums/delivery_type.dart'; +import 'package:niogu_ecommerce_v1/core/enums/order_status.dart'; +import 'package:niogu_ecommerce_v1/core/errors/exceptions.dart'; +import 'package:niogu_ecommerce_v1/core/router/app_route.dart'; +import 'package:niogu_ecommerce_v1/core/utils/currency_format.dart'; +import 'package:niogu_ecommerce_v1/core/utils/extenstion_format.dart'; +import 'package:niogu_ecommerce_v1/core/utils/log_message.dart'; +import 'package:niogu_ecommerce_v1/core/utils/time_zone.dart'; +import 'package:niogu_ecommerce_v1/core/widgets/custom_snackbar.dart'; +import 'package:niogu_ecommerce_v1/features/checkout/domain/entities/checkout.dart'; +import 'package:niogu_ecommerce_v1/features/home/presentation/providers/home_provider.dart'; +import 'package:niogu_ecommerce_v1/features/order/domain/entities/order.dart'; +import 'package:niogu_ecommerce_v1/features/order/presentation/providers/order_provider.dart'; +import 'package:sizer/sizer.dart'; +import 'package:niogu_ecommerce_v1/core/constant/app_color.dart'; + +class OrderDetailScreen extends ConsumerStatefulWidget { + final OrderInfo orderInfo; + const OrderDetailScreen({super.key, required this.orderInfo}); + + @override + ConsumerState createState() => _OrderDetailScreenState(); +} + +class _OrderDetailScreenState extends ConsumerState { + late final OrderInfo _orderInfo; + + final timeZone = TimeZone.getCurrentTimeZone(); + + @override + void initState() { + // TODO: implement initState + super.initState(); + + _orderInfo = widget.orderInfo; + } + + @override + void dispose() { + // TODO: implement dispose + super.dispose(); + } + + double _calculateDistance( + double lat1, + double lon1, + double lat2, + double lon2, + ) { + final p = 0.017453292519943295; + final c = cos; + final a = + 0.5 - + c((lat2 - lat1) * p) / 2 + + c(lat1 * p) * c(lat2 * p) * (1 - c((lon2 - lon1) * p)) / 2; + return 12742 * asin(sqrt(a)); + } + + void _showPaymentProof() { + showDialog( + context: context, + useRootNavigator: true, + builder: (context) => Dialog( + insetPadding: EdgeInsets.symmetric(horizontal: 2.w, vertical: 2.h), + backgroundColor: Colors.transparent, + child: GestureDetector( + onTap: () => Navigator.pop(context), + child: CachedNetworkImage( + imageUrl: _orderInfo.paymentProofUrl ?? '', + imageBuilder: (context, imageProvider) { + return Container( + width: MediaQuery.of(context).size.width, + constraints: BoxConstraints(maxWidth: 85.w, maxHeight: 85.h), + decoration: BoxDecoration( + image: DecorationImage( + image: imageProvider, + fit: BoxFit.cover, + ), + ), + ); + }, + errorWidget: (context, url, error) { + return Container( + height: double.infinity, + width: MediaQuery.of(context).size.width, + color: Colors.white, + constraints: BoxConstraints(maxWidth: 85.w, maxHeight: 85.h), + child: Icon( + Icons.error_outline_outlined, + size: 10.w, + color: Colors.grey.shade500, + ), + ); + }, + errorListener: (value) => + CustomSnackbar.showError(context, "Ups, terjadi kesalahan"), + ), + ), + ), + ); + } + + void _showCancelConfirmation(bool canAutmaticCancelled) { + showDialog( + context: context, + builder: (BuildContext context) { + return Dialog( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(4.5.w), + ), + elevation: 0, + backgroundColor: Colors.transparent, + constraints: BoxConstraints(maxHeight: 80.h, maxWidth: 80.w), + child: _buildCancelModalContent(canAutmaticCancelled), + ); + }, + ); + } + + Widget _buildCancelModalContent(bool canAutmaticCancelled) { + final orderStatus = _orderInfo.orderStatus == OrderStatus.confirmed + ? 'dikonfirmasi' + : 'dikirim'; + + return Container( + padding: EdgeInsets.all(6.w), + decoration: BoxDecoration( + color: Colors.white, + shape: BoxShape.rectangle, + borderRadius: BorderRadius.circular(4.5.w), + boxShadow: const [ + BoxShadow( + color: Colors.black26, + blurRadius: 10.0, + offset: Offset(0.0, 10.0), + ), + ], + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + padding: EdgeInsets.all(4.w), + decoration: BoxDecoration( + color: Colors.red.shade50, + shape: BoxShape.circle, + ), + child: Icon(Icons.error_outline, color: Colors.red, size: 15.w), + ), + SizedBox(height: 3.h), + Text( + canAutmaticCancelled ? "Batalkan Pesanan?" : "Ajukan Pembatalan?", + style: TextStyle( + fontSize: AppFontSize.medium.sp, + fontWeight: FontWeight.bold, + ), + ), + SizedBox(height: 1.5.h), + Text( + canAutmaticCancelled + ? "Apakah anda yakin ingin membatalkan pesanan ini? Tindakan ini tidak dapat dibatalkan." + : "Pesanan anda sudah $orderStatus. Permintaan pembatalan memerlukan persetujuan dari admin.", + textAlign: TextAlign.center, + style: TextStyle( + fontSize: (AppFontSize.small - 1.25).sp, + color: Colors.grey.shade700, + height: 1.4, + ), + ), + SizedBox(height: 4.h), + + Row( + children: [ + Expanded( + child: OutlinedButton( + onPressed: () => Navigator.pop(context), + style: OutlinedButton.styleFrom( + side: BorderSide(color: Colors.grey.shade300), + padding: EdgeInsets.symmetric(vertical: 1.5.h), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(2.5.w), + ), + ), + child: Text( + "Kembali", + style: TextStyle( + color: Colors.black87, + fontSize: AppFontSize.small.sp, + ), + ), + ), + ), + SizedBox(width: 3.w), + + Expanded( + child: ElevatedButton( + onPressed: () async { + final orderId = _orderInfo.id; + + final orderNumber = _orderInfo.orderNumber; + if (canAutmaticCancelled) { + try { + await ref + .read(orderControllerProvider.notifier) + .cancelOrder(orderId); + } on ServerException catch (e, st) { + LogMessage.log.e( + e.toString(), + error: e, + stackTrace: st, + ); + CustomSnackbar.showError(context, e.toString()); + return; + } + + context.pushNamed( + AppRoute.customActionScreen, + extra: { + 'order_number': orderNumber, + 'type': ActionType.cancel, + }, + ); + } else { + try { + await ref + .read(orderControllerProvider.notifier) + .cancellationRequest(orderId); + } on ServerException catch (e, st) { + LogMessage.log.e( + e.toString(), + error: e, + stackTrace: st, + ); + CustomSnackbar.showError(context, e.toString()); + return; + } + + context.pushNamed( + AppRoute.customActionScreen, + extra: { + 'order_number': orderNumber, + 'type': ActionType.requestCancel, + }, + ); + } + }, + style: ElevatedButton.styleFrom( + backgroundColor: AppColor.primaryColor, + elevation: 0, + padding: EdgeInsets.symmetric(vertical: 1.5.h), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(2.5.w), + ), + ), + child: Text( + canAutmaticCancelled ? "Ya, Batalkan" : "Ajukan", + style: TextStyle( + color: Colors.white, + fontSize: AppFontSize.small.sp, + fontWeight: FontWeight.bold, + ), + ), + ), + ), + ], + ), + ], + ), + ); + } + + @override + Widget build(BuildContext context) { + return LayoutBuilder( + builder: (context, constraints) { + int preparationTimeMinutes = 0; + + final configurationState = ref.watch(configurationControllerProvider); + + configurationState.whenData((configure) { + if (configure != null) { + preparationTimeMinutes = configure.preparationTimeMinutes ?? 0; + } + }); + + final orderControllerState = ref.watch(orderControllerProvider); + + final isLoading = orderControllerState.isLoading; + + final isCancellation = _orderInfo.isCancellation; + + final cancellationAccepted = _orderInfo.cancellationAccepted; + + final approvalProcess = _orderInfo.approvalProcess; + + bool isProcess = false; + + bool isAccepted = false; + + if (approvalProcess != null) { + isProcess = + (isCancellation ?? false) && + approvalProcess == ApprovalProcess.waiting; + + isAccepted = + (isCancellation ?? false) && + (cancellationAccepted ?? false) && + approvalProcess == ApprovalProcess.done; + } + + final outletAddress = _orderInfo.outletAddressSnapshot; + + final customerAddress = _orderInfo.customerAddressSnapshot; + + var distance = 0.0; + + var strDistance = ''; + + final isDelivery = _orderInfo.deliveryType == DeliveryType.delivery; + + if (isDelivery && outletAddress != null && customerAddress != null) { + distance = _calculateDistance( + outletAddress.latitude, + outletAddress.longitude, + customerAddress.latitude, + customerAddress.longitude, + ); + + strDistance = '${distance.toStringAsFixed(1)} Km'; + + if (distance < 1) + strDistance = '${(distance * 1000).toStringAsFixed(0)} meter'; + } + + return SafeArea( + top: false, + bottom: true, + right: false, + left: false, + child: Scaffold( + backgroundColor: const Color(0xFFF8F9FA), + appBar: AppBar( + backgroundColor: Colors.white, + elevation: 0, + leading: IconButton( + icon: Icon( + Icons.arrow_back, + color: AppColor.primaryColor, + size: 7.w, + ), + onPressed: () => context.canPop() + ? context.pop() + : context.goNamed(AppRoute.homeScreen), + ), + title: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + _orderInfo.orderNumber.toUniqueNumberCleaner(), + style: TextStyle( + color: Colors.black, + fontSize: AppFontSize.small.sp, + fontWeight: FontWeight.bold, + ), + ), + + SizedBox(height: 0.5.h), + + Text( + "${DateFormat('dd MMMM yyyy HH:mm').format(_orderInfo.createdAt)} $timeZone", + style: TextStyle( + color: Colors.grey.shade700, + fontSize: (AppFontSize.small - 1.25).sp, + ), + ), + ], + ), + centerTitle: true, + ), + body: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildStatusHeader(preparationTimeMinutes), + + Padding( + padding: EdgeInsets.symmetric(horizontal: 4.w), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (approvalProcess != null) ...[ + SizedBox(height: 6.h), + _buildCancellationStatus(isProcess, isAccepted), + ], + SizedBox( + height: !(approvalProcess != null) ? 6.h : 3.h, + ), + _buildSectionTitle("Informasi Pengiriman"), + _buildDeliveryInfoCard( + outletAddress: outletAddress, + customerAddress: customerAddress, + isDelivery: isDelivery, + distance: distance, + strDistance: strDistance, + ), + + SizedBox(height: 3.h), + + _buildSectionTitle("Rincian Produk"), + _buildItemInfoCard(), + + SizedBox(height: 3.h), + + _buildSectionTitle("Informasi Pembayaran"), + _buildPaymentInfoCard(), + + SizedBox(height: 3.h), + + _buildOrderSummaryCard(strDistance), + + SizedBox(height: 4.h), + ], + ), + ), + ], + ), + ), + bottomNavigationBar: + _orderInfo.orderStatus == OrderStatus.canceled || + _orderInfo.orderStatus == OrderStatus.completed + ? null + : _buildBottomAction(isLoading), + ), + ); + }, + ); + } + + Widget _buildSectionTitle(String title) { + return Padding( + padding: EdgeInsets.only(bottom: 1.5.h), + child: Text( + title, + style: TextStyle( + fontSize: AppFontSize.small.sp, + fontWeight: FontWeight.bold, + color: Colors.black87, + ), + ), + ); + } + + Widget _buildStatusHeader(int preparationTimeMinutes) { + final orderStatus = _orderInfo.orderStatus; + + final status = switch (orderStatus) { + OrderStatus.pending_payment => 'Diproses', + OrderStatus.pending_confirmation => 'Diproses', + OrderStatus.confirmed => 'Dikonfirmasi', + OrderStatus.in_progress => 'Dikirim', + OrderStatus.completed => 'Selesai', + OrderStatus.canceled => 'Dibatalkan', + }; + + final animation = switch (orderStatus) { + OrderStatus.pending_payment => AppAsset.PENDING_ORDER, + OrderStatus.pending_confirmation => AppAsset.PENDING_ORDER, + OrderStatus.confirmed => AppAsset.PENDING_ORDER, + OrderStatus.in_progress => AppAsset.IN_PROGRESS_ORDER, + OrderStatus.completed => AppAsset.COMPLETED_ORDER, + OrderStatus.canceled => AppAsset.FAILED_ORDER, + }; + + return Stack( + clipBehavior: Clip.none, + alignment: Alignment.center, + children: [ + Container( + width: double.infinity, + height: 25.h, + color: orderStatus == OrderStatus.in_progress + ? Colors.white + : AppColor.primaryColor.withOpacity(0.05), + child: Center(child: Image.asset(animation)), + ), + Positioned( + bottom: -4.h, + child: Container( + width: 85.w, + padding: EdgeInsets.all(4.w), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(2.5.w), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.05), + blurRadius: 10, + offset: const Offset(0, 5), + ), + ], + ), + child: Column( + children: [ + Text( + orderStatus == OrderStatus.completed || + orderStatus == OrderStatus.canceled + ? "Pesanan $status" + : "Pesananmu Telah $status", + style: TextStyle( + color: AppColor.primaryColor, + fontWeight: FontWeight.bold, + fontSize: AppFontSize.small.sp, + ), + ), + + if ((_orderInfo.deliveryType == DeliveryType.delivery) && + orderStatus == OrderStatus.pending_payment || + orderStatus == OrderStatus.pending_confirmation || + orderStatus == OrderStatus.confirmed) ...[ + SizedBox(height: 1.h), + Text( + _orderInfo.deliveryPreference == "Hari ini (Segera)" + ? "Pesananmu segera dikirim" + : "Pesananmu dikirim pada ${_orderInfo.deliveryPreference}", + style: TextStyle( + color: Colors.black87, + fontSize: (AppFontSize.small - 1.25).sp, + ), + ), + ] else if (orderStatus == OrderStatus.pending_payment || + orderStatus == OrderStatus.pending_confirmation) ...[ + SizedBox(height: 1.h), + Text( + _orderInfo.deliveryPreference == "Hari ini (Segera)" + ? "Dapat di ambil dalam $preparationTimeMinutes menit lagi" + : "Dapat di ambil ${_orderInfo.deliveryPreference}", + style: TextStyle( + color: Colors.black87, + fontSize: (AppFontSize.small - 1.25).sp, + ), + ), + ], + ], + ), + ), + ), + ], + ); + } + + Widget _buildCancellationStatus(bool isProcess, bool isAccepted) { + Color primaryColor; + + IconData statusIcon; + + String title; + + if (isProcess) { + primaryColor = AppColor.primaryColor; + statusIcon = Icons.hourglass_empty; + title = "Permintaan pembatalan sedang diproses"; + } else if (isAccepted) { + primaryColor = Colors.red; + statusIcon = Icons.cancel_outlined; + title = "Permintaan pembatalan diterima"; + } else { + primaryColor = Colors.orange; + statusIcon = Icons.error_outline; + title = "Permintaan pembatalan ditolak"; + } + + return Container( + margin: EdgeInsets.only(top: 2.h), + padding: EdgeInsets.all(4.w), + decoration: BoxDecoration( + color: primaryColor.withOpacity(0.05), + borderRadius: BorderRadius.circular(2.5.w), + border: Border.all(color: primaryColor.withOpacity(0.2)), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(statusIcon, color: primaryColor, size: 5.w), + SizedBox(width: 3.w), + + Text( + title, + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: AppFontSize.small.sp, + color: primaryColor, + ), + ), + ], + ), + ); + } + + Widget _buildDeliveryInfoCard({ + required OutletAddressSnapshot? outletAddress, + required CustomerAddressSnapshot? customerAddress, + required bool isDelivery, + required double distance, + required String strDistance, + }) { + return _buildWhiteCard( + child: Column( + children: [ + _buildLocationRow( + icon: Icons.location_on_outlined, + color: Colors.red, + label: isDelivery ? "Dari" : "Tempat Pengambilan", + place: _orderInfo.outletNameSnapshot, + address: outletAddress?.fullAddress, + ), + + if (distance > 0) + Padding( + padding: EdgeInsets.only(left: 2.5.w), + child: Row( + children: [ + Container(width: 1, height: 6.h, color: Colors.grey.shade300), + SizedBox(width: 4.w), + Container( + padding: EdgeInsets.symmetric( + horizontal: 2.w, + vertical: 0.5.h, + ), + decoration: BoxDecoration( + color: AppColor.primaryColor.withOpacity(0.1), + borderRadius: BorderRadius.circular(1.w), + ), + child: Text( + "Jarak : $strDistance", + style: TextStyle( + fontSize: (AppFontSize.small - 1.25).sp, + fontWeight: FontWeight.bold, + color: AppColor.primaryColor, + ), + ), + ), + ], + ), + ), + if (isDelivery) ...[ + SizedBox(height: 3.h), + + _buildLocationRow( + icon: Icons.location_on, + color: Colors.green, + label: "Tujuan", + place: customerAddress!.label, + address: customerAddress.fullAddress, + ), + ], + ], + ), + ); + } + + Widget _buildLocationRow({ + required IconData icon, + required Color color, + required String label, + required String place, + String? address, + }) { + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Icon(icon, color: color, size: 5.w), + SizedBox(width: 3.w), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + label, + style: TextStyle( + color: Colors.grey.shade700, + fontSize: (AppFontSize.small - 1.25).sp, + ), + ), + SizedBox(height: 0.5.h), + Text( + place, + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: (AppFontSize.small - 1.25).sp, + ), + ), + if (address != null) ...[ + SizedBox(height: 0.5.h), + Text( + address, + style: TextStyle( + color: Colors.black87, + fontSize: (AppFontSize.small - 1.25).sp, + ), + ), + ], + ], + ), + ), + ], + ); + } + + Widget _buildItemInfoCard() { + return _buildWhiteCard( + child: ListView.separated( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: _orderInfo.orderItems.length, + separatorBuilder: (context, index) => + Divider(height: 3.h, color: Colors.grey.shade100), + itemBuilder: (context, index) { + final item = _orderInfo.orderItems[index]; + return Row( + children: [ + CachedNetworkImage( + imageUrl: item.productImageUrlSnapshot ?? 'error', + imageBuilder: (context, imageProvider) { + return Container( + width: 15.w, + height: 15.w, + decoration: BoxDecoration( + color: Colors.grey.shade100, + borderRadius: BorderRadius.circular(2.w), + image: DecorationImage( + image: imageProvider, + fit: BoxFit.cover, + ), + ), + ); + }, + errorWidget: (context, url, error) { + return Container( + width: 15.w, + height: 15.w, + decoration: BoxDecoration( + color: Colors.grey.shade100, + borderRadius: BorderRadius.circular(2.w), + ), + child: Icon(Icons.image, color: Colors.grey.shade300), + ); + }, + ), + SizedBox(width: 4.w), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + item.productNameSnapshot, + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: (AppFontSize.small - 1.25).sp, + ), + ), + + if (item.productVariantNameSnapshot != null && + item.productVariantNameSnapshot!.isNotEmpty) ...[ + SizedBox(height: 0.75.h), + + Text( + item.productVariantNameSnapshot!, + style: TextStyle( + color: Colors.grey.shade700, + fontSize: (AppFontSize.small - 1.25).sp, + ), + ), + ], + + SizedBox(height: 0.75.h), + + Text( + CurrencyFormat.formatToIdr(item.sellingPriceSnapshot, 0), + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: (AppFontSize.small - 1.25).sp, + ), + ), + ], + ), + ), + Text( + "x ${item.quantity}", + style: TextStyle( + color: Colors.grey.shade700, + fontSize: (AppFontSize.small - 1.25).sp, + ), + ), + ], + ); + }, + ), + ); + } + + Widget _buildPaymentInfoCard() { + final isPayOnTheSpot = + (_orderInfo.deliveryType == DeliveryType.delivery && + _orderInfo.paymentMethod == 'COD') || + (_orderInfo.deliveryType == DeliveryType.pick_up && + _orderInfo.paymentMethod == 'Bayar Ditoko'); + return Material( + color: Colors.transparent, + child: InkWell( + onTap: isPayOnTheSpot ? null : _showPaymentProof, + child: _buildWhiteCard( + child: Row( + children: [ + Container( + padding: EdgeInsets.all(2.w), + decoration: BoxDecoration( + color: Colors.white, + shape: BoxShape.circle, + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.05), + blurRadius: 5, + ), + ], + ), + child: Icon( + Icons.receipt_long_outlined, + color: AppColor.primaryColor, + size: 5.w, + ), + ), + SizedBox(width: 3.w), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + _orderInfo.paymentMethod, + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: (AppFontSize.small - 1.25).sp, + ), + ), + if (!isPayOnTheSpot) + Text( + "Ketuk untuk lihat bukti pembayaran", + style: TextStyle( + fontSize: (AppFontSize.small - 1.25).sp, + color: Colors.grey.shade600, + ), + ), + ], + ), + ), + if (!isPayOnTheSpot) + Icon( + Icons.arrow_forward_ios, + size: 3.5.w, + color: Colors.grey.shade400, + ), + ], + ), + ), + ), + ); + } + + Widget _buildOrderSummaryCard(String distance) { + return _buildWhiteCard( + child: Column( + children: [ + _buildSummaryRow( + "Subtotal Pesanan", + CurrencyFormat.formatToIdr(_orderInfo.totalOrder, 0), + ), + if (_orderInfo.deliveryType == DeliveryType.delivery) ...[ + if (_orderInfo.deliveryFeeType == DeliveryFeeType.per_km) ...[ + _buildSummaryRow( + "Biaya Pengiriman Per Km", + CurrencyFormat.formatToIdr(_orderInfo.deliveryFee, 0), + ), + + _buildSummaryRow( + "Jarak Alamatmu Dengan Toko", + "\u00B1 $distance", + ), + ], + _buildSummaryRow( + "Subtotal Pengiriman", + CurrencyFormat.formatToIdr(_orderInfo.totalDeliveryFee, 0), + ), + ], + const Divider(), + _buildSummaryRow( + "Total Pembayaran", + CurrencyFormat.formatToIdr(_orderInfo.totalAmount, 0), + isTotal: true, + ), + ], + ), + ); + } + + Widget _buildSummaryRow(String label, String value, {bool isTotal = false}) { + return Padding( + padding: EdgeInsets.symmetric(vertical: 0.8.h), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + label, + style: TextStyle( + fontSize: (AppFontSize.small - 1.25).sp, + fontWeight: isTotal ? FontWeight.bold : FontWeight.normal, + color: isTotal ? Colors.black : Colors.grey.shade700, + ), + ), + Text( + value, + style: TextStyle( + fontSize: (AppFontSize.small - 1.25).sp, + fontWeight: isTotal ? FontWeight.bold : FontWeight.normal, + color: isTotal ? AppColor.primaryColor : Colors.black, + ), + ), + ], + ), + ); + } + + Widget _buildWhiteCard({required Widget child}) { + return Container( + width: double.infinity, + padding: EdgeInsets.all(4.w), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(2.5.w), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.02), + blurRadius: 5, + offset: const Offset(0, 2), + ), + ], + ), + child: child, + ); + } + + Widget _buildBottomAction(bool isLoading) { + final canCanceled = + _orderInfo.orderStatus != OrderStatus.completed || + _orderInfo.orderStatus != OrderStatus.canceled; + + final canAutomaticCanceled = + _orderInfo.orderStatus == OrderStatus.pending_payment || + _orderInfo.orderStatus == OrderStatus.pending_confirmation; + + final approvalProcess = _orderInfo.approvalProcess; + + return Container( + padding: EdgeInsets.all(4.w), + decoration: BoxDecoration( + color: Colors.white, + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.05), + blurRadius: 10, + offset: const Offset(0, -5), + ), + ], + ), + child: ElevatedButton( + onPressed: !canCanceled || (approvalProcess != null) || isLoading + ? null + : () => _showCancelConfirmation(canAutomaticCanceled), + style: ElevatedButton.styleFrom( + backgroundColor: AppColor.primaryColor, + elevation: 0, + minimumSize: Size(double.infinity, 6.h), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(2.5.w), + ), + disabledBackgroundColor: Colors.grey.shade300, + ), + child: Text( + canAutomaticCanceled ? "Batalkan Pesanan" : "Ajukan Pembatalan", + style: TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + fontSize: AppFontSize.medium.sp, + ), + ), + ), + ); + } +} diff --git a/Mobile Commerce/lib/features/order/presentation/screens/order_screen.dart b/Mobile Commerce/lib/features/order/presentation/screens/order_screen.dart new file mode 100644 index 0000000..22c940a --- /dev/null +++ b/Mobile Commerce/lib/features/order/presentation/screens/order_screen.dart @@ -0,0 +1,548 @@ +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; +import 'package:intl/intl.dart'; +import 'package:niogu_ecommerce_v1/core/constant/app_font_size.dart'; +import 'package:niogu_ecommerce_v1/core/enums/delivery_type.dart'; +import 'package:niogu_ecommerce_v1/core/enums/order_status.dart'; +import 'package:niogu_ecommerce_v1/core/router/app_route.dart'; +import 'package:niogu_ecommerce_v1/core/utils/currency_format.dart'; +import 'package:niogu_ecommerce_v1/core/utils/extenstion_format.dart'; +import 'package:niogu_ecommerce_v1/core/utils/log_message.dart'; +import 'package:niogu_ecommerce_v1/core/widgets/custom_empty_screen.dart'; +import 'package:niogu_ecommerce_v1/core/widgets/custom_snackbar.dart'; +import 'package:niogu_ecommerce_v1/features/order/domain/entities/order.dart'; +import 'package:niogu_ecommerce_v1/features/order/presentation/providers/order_provider.dart'; +import 'package:shimmer/shimmer.dart'; +import 'package:sizer/sizer.dart'; +import 'package:niogu_ecommerce_v1/core/constant/app_color.dart'; + +class OrderScreen extends ConsumerStatefulWidget { + const OrderScreen({super.key}); + + @override + ConsumerState createState() => _OrderScreenState(); +} + +class _OrderScreenState extends ConsumerState + with SingleTickerProviderStateMixin { + late TabController _tabController; + + @override + void initState() { + super.initState(); + _tabController = TabController(length: 2, vsync: this); + } + + @override + Widget build(BuildContext context) { + return LayoutBuilder( + builder: (context, constraints) { + final onGoingOrderState = ref.watch( + orderProvider([ + OrderStatus.pending_payment, + OrderStatus.pending_confirmation, + OrderStatus.confirmed, + OrderStatus.in_progress, + ]), + ); + + final historyOrderState = ref.watch( + orderProvider([OrderStatus.completed, OrderStatus.canceled]), + ); + + return SafeArea( + top: false, + bottom: true, + right: false, + left: false, + child: Scaffold( + backgroundColor: Colors.white, + appBar: AppBar( + backgroundColor: Colors.white, + elevation: 0, + centerTitle: true, + title: Text( + "Pesanan Saya", + style: TextStyle( + color: Colors.black, + fontSize: AppFontSize.medium.sp, + fontWeight: FontWeight.bold, + ), + ), + ), + body: Column( + children: [ + Container( + margin: EdgeInsets.all(4.w), + padding: EdgeInsets.all(1.5.w), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(2.5.w), + ), + child: TabBar( + controller: _tabController, + dividerColor: Colors.transparent, + indicatorSize: TabBarIndicatorSize.tab, + indicator: BoxDecoration( + color: AppColor.primaryColor, + borderRadius: BorderRadius.circular(2.w), + ), + labelColor: Colors.white, + unselectedLabelColor: Colors.grey, + labelStyle: TextStyle( + fontWeight: FontWeight.bold, + fontSize: AppFontSize.small.sp, + ), + tabs: [ + Tab(text: "Berlangsung"), + Tab(text: "Riwayat"), + ], + ), + ), + + Expanded( + child: TabBarView( + controller: _tabController, + children: [ + RefreshIndicator( + onRefresh: () async { + await ref + .read(orderReportControllerProvider.notifier) + .refresh(); + }, + color: AppColor.primaryColor, + backgroundColor: Colors.white, + child: SingleChildScrollView( + physics: const AlwaysScrollableScrollPhysics(), + child: onGoingOrderState.when( + data: (orders) { + if (orders.isEmpty) { + return CustomEmptyScreen( + icon: Icons.hourglass_empty_outlined, + title: "Belum Ada Pesanan Berlangsung", + subtitle: "Yuk, buat pesanan pertamamu", + height: 40.h, + ); + } + return _buildOrderList(orders); + }, + error: (error, st) { + LogMessage.log.e( + error.toString(), + error: error, + stackTrace: st, + ); + return CustomEmptyScreen( + icon: Icons.cloud_off_outlined, + title: "Terjadi Kesalahan Koneksi", + subtitle: "Tarik ke bawah untuk mencoba lagi", + height: 40.h, + ); + }, + loading: () => _buildOrderItemShimmer(), + ), + ), + ), + RefreshIndicator( + onRefresh: () async { + await ref + .read(orderReportControllerProvider.notifier) + .refresh(); + }, + color: AppColor.primaryColor, + backgroundColor: Colors.white, + child: SingleChildScrollView( + physics: const AlwaysScrollableScrollPhysics(), + child: historyOrderState.when( + data: (orders) { + if (orders.isEmpty) { + return CustomEmptyScreen( + icon: Icons.history_outlined, + title: "Belum Ada Riwayat Pesanan", + subtitle: "Yuk, buat pesanan pertamamu", + height: 40.h, + ); + } + return _buildOrderList(orders); + }, + error: (error, st) { + return CustomEmptyScreen( + icon: Icons.cloud_off_outlined, + title: "Terjadi Kesalahan Koneksi", + subtitle: "Tarik ke bawah untuk mencoba lagi", + height: 40.h, + ); + }, + loading: () => _buildOrderItemShimmer(), + ), + ), + ), + ], + ), + ), + ], + ), + ), + ); + }, + ); + } + + Widget _buildOrderList(List orders) { + Map> groupedOrders = {}; + for (final order in orders) { + final date = DateFormat('dd MMMM yyyy').format(order.createdAt); + if (groupedOrders[date] == null) { + groupedOrders[date] = []; + } + groupedOrders[date]!.add(order); + } + + return Column( + children: groupedOrders.keys.map((date) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: EdgeInsets.fromLTRB(5.w, 2.h, 5.w, 1.h), + child: Text( + date, + style: TextStyle( + fontSize: AppFontSize.small.sp, + fontWeight: FontWeight.bold, + color: Colors.grey.shade600, + ), + ), + ), + + Container( + margin: EdgeInsets.symmetric(horizontal: 3.w), + child: Column( + children: List.generate(groupedOrders[date]!.length, (index) { + final order = groupedOrders[date]![index]; + return Column( + children: [ + _buildOrderItem(order), + if (index != groupedOrders[date]!.length - 1) + Divider( + height: 0, + indent: 4.w, + endIndent: 4.w, + color: Colors.grey.shade100, + ), + ], + ); + }), + ), + ), + SizedBox(height: 1.h), + ], + ); + }).toList(), + ); + } + + Widget _buildOrderItem(OrderReport order) { + final orderStatus = switch (order.orderStatus) { + OrderStatus.pending_payment => "Diproses", + OrderStatus.pending_confirmation => "Diproses", + OrderStatus.confirmed => "Dikonfirmasi", + OrderStatus.in_progress => "Dikirim", + OrderStatus.completed => "Selesai", + OrderStatus.canceled => "Dibatalkan", + }; + + final colorStatus = switch (order.orderStatus) { + OrderStatus.pending_payment => Colors.grey.shade700, + OrderStatus.pending_confirmation => Colors.grey.shade700, + OrderStatus.confirmed => Colors.blue.shade700, + OrderStatus.in_progress => Colors.orange.shade700, + OrderStatus.completed => Colors.green.shade700, + OrderStatus.canceled => Colors.red.shade700, + }; + + final type = DeliveryType.values.byName(order.deliveryType); + + final deliveryType = switch (type) { + DeliveryType.pick_up => "Ambil Ditoko", + DeliveryType.delivery => "Pengiriman", + }; + + return InkWell( + onTap: () async { + final orderInfo = await ref + .read(orderRepositoryProvider) + .fetchOrderById(order.id); + + if (orderInfo == null) { + CustomSnackbar.showError(context, "Ups, terjadi kesalahan"); + return; + } + + context.pushNamed(AppRoute.orderDetailScreen, extra: orderInfo); + }, + borderRadius: BorderRadius.circular(2.5.w), + child: Padding( + padding: EdgeInsets.all(4.w), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildOrderImages(order.totalItems, order.productImages), + + SizedBox(width: 4.w), + + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + order.orderNumber.toUniqueNumberCleaner(), + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: (AppFontSize.small - 1.25).sp, + color: AppColor.primaryColor, + ), + ), + SizedBox(height: 0.5.h), + Text( + "${order.totalItems} Produk", + style: TextStyle( + color: Colors.grey.shade700, + fontSize: (AppFontSize.small - 1.25).sp, + ), + ), + SizedBox(height: 1.2.h), + Text( + CurrencyFormat.formatToIdr(order.totalAmount, 0), + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: (AppFontSize.small - 1.25).sp, + color: Colors.black87, + ), + ), + ], + ), + ), + + Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Container( + padding: EdgeInsets.symmetric( + horizontal: 2.5.w, + vertical: 0.6.h, + ), + decoration: BoxDecoration( + color: colorStatus.withOpacity(0.1), + borderRadius: BorderRadius.circular(1.5.w), + ), + child: Text( + orderStatus, + style: TextStyle( + color: colorStatus, + fontWeight: FontWeight.bold, + fontSize: (AppFontSize.small - 1.25).sp, + ), + ), + ), + SizedBox(height: 2.h), + Text( + deliveryType, + style: TextStyle( + fontSize: (AppFontSize.small - 1.25).sp, + color: Colors.black87, + ), + ), + if (order.orderStatus == OrderStatus.completed && + !order.hasBeenAssessed) ...[ + SizedBox(height: 2.h), + GestureDetector( + onTap: () async { + final products = await ref + .read(orderRepositoryProvider) + .fetchProductReviewItem(order.id); + + await Future.delayed(const Duration(milliseconds: 500)); + + context.pushNamed( + AppRoute.ratingScreen, + pathParameters: {'id': order.id}, + extra: products, + ); + }, + child: Container( + padding: EdgeInsets.symmetric( + horizontal: 2.5.w, + vertical: 0.6.h, + ), + decoration: BoxDecoration(color: AppColor.primaryColor), + child: Text( + "Nilai Pesanan", + style: TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + fontSize: (AppFontSize.small - 1.25).sp, + ), + ), + ), + ), + ], + ], + ), + ], + ), + ), + ); + } + + Widget _buildOrderImages(int totalItems, List productImages) { + if (productImages.isEmpty) { + return Container( + width: 12.w, + height: 12.w, + decoration: BoxDecoration( + color: Colors.grey.shade100, + borderRadius: BorderRadius.circular(2.w), + ), + child: Icon(Icons.image, color: Colors.grey.shade300, size: 5.w), + ); + } + if (totalItems > 1 && productImages.length > 1) { + return Row( + children: [ + _imageBox(productImages[0]), + SizedBox(width: 1.w), + _imageBox(productImages[1]), + ], + ); + } + return _imageBox(productImages[0]); + } + + Widget _imageBox(String? img) { + return CachedNetworkImage( + imageUrl: img ?? 'error', + imageBuilder: (context, imageProvider) { + return Container( + width: 12.w, + height: 12.w, + decoration: BoxDecoration( + color: Colors.grey.shade100, + borderRadius: BorderRadius.circular(2.w), + image: DecorationImage(image: imageProvider, fit: BoxFit.cover), + ), + ); + }, + errorWidget: (context, url, error) { + return Container( + width: 12.w, + height: 12.w, + decoration: BoxDecoration( + color: Colors.grey.shade100, + borderRadius: BorderRadius.circular(2.w), + ), + child: Icon(Icons.image, color: Colors.grey.shade300, size: 5.w), + ); + }, + ); + } + + Widget _buildOrderItemShimmer() { + return Container( + margin: EdgeInsets.symmetric(horizontal: 4.w, vertical: 1.h), + padding: EdgeInsets.all(4.w), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(15), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.03), + blurRadius: 10, + offset: const Offset(0, 5), + ), + ], + ), + child: Shimmer.fromColors( + baseColor: Colors.grey.shade300, + highlightColor: Colors.grey.shade100, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Container( + width: 8.w, + height: 8.w, + decoration: BoxDecoration( + color: Colors.white, + shape: BoxShape.circle, + ), + ), + SizedBox(width: 3.w), + Container(width: 30.w, height: 2.h, color: Colors.white), + const Spacer(), + Container( + width: 20.w, + height: 3.h, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(4.5.w), + ), + ), + ], + ), + + Divider(height: 3.h, color: Colors.grey.shade200), + Row( + children: [ + Container( + width: 15.w, + height: 15.w, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(2.5.w), + ), + ), + SizedBox(width: 4.w), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container(width: 40.w, height: 2.h, color: Colors.white), + SizedBox(height: 1.h), + Container(width: 25.w, height: 1.5.h, color: Colors.white), + ], + ), + ], + ), + + SizedBox(height: 2.h), + + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container(width: 20.w, height: 1.5.h, color: Colors.white), + SizedBox(height: 0.5.h), + // Skeleton Harga Total + Container(width: 30.w, height: 2.5.h, color: Colors.white), + ], + ), + Container( + width: 25.w, + height: 5.h, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(2.5.w), + ), + ), + ], + ), + ], + ), + ), + ); + } +} diff --git a/Mobile Commerce/lib/features/order/presentation/screens/rating_screen.dart b/Mobile Commerce/lib/features/order/presentation/screens/rating_screen.dart new file mode 100644 index 0000000..1cc6624 --- /dev/null +++ b/Mobile Commerce/lib/features/order/presentation/screens/rating_screen.dart @@ -0,0 +1,337 @@ +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; +import 'package:niogu_ecommerce_v1/core/constant/app_color.dart'; +import 'package:niogu_ecommerce_v1/core/constant/app_font_size.dart'; +import 'package:niogu_ecommerce_v1/core/errors/exceptions.dart'; +import 'package:niogu_ecommerce_v1/core/providers/app_provider.dart'; +import 'package:niogu_ecommerce_v1/core/utils/log_message.dart'; +import 'package:niogu_ecommerce_v1/core/widgets/custom_snackbar.dart'; +import 'package:niogu_ecommerce_v1/features/home/presentation/providers/home_provider.dart'; +import 'package:niogu_ecommerce_v1/features/order/domain/entities/order.dart'; +import 'package:niogu_ecommerce_v1/features/order/presentation/providers/order_provider.dart'; +import 'package:sizer/sizer.dart'; + +class RatingScreen extends ConsumerStatefulWidget { + final String orderId; + final List products; + const RatingScreen({ + super.key, + required this.orderId, + required this.products, + }); + + @override + ConsumerState createState() => _RatingScreenState(); +} + +class _RatingScreenState extends ConsumerState { + late final List _products; + + final Map _ratings = {}; + + final Map _controllers = {}; + + @override + void initState() { + super.initState(); + _products = widget.products; + + for (int i = 0; i < _products.length; i++) { + _ratings[i] = 5; + _controllers[i] = TextEditingController(); + } + } + + @override + void dispose() { + // TODO: implement dispose + for (int i = 0; i < _products.length; i++) { + _controllers[i]?.dispose(); + } + super.dispose(); + } + + String _getRatingLabel(int rating) { + if (rating == 1) return "Sangat Buruk"; + if (rating == 2) return "Buruk"; + if (rating == 3) return "Cukup"; + if (rating == 4) return "Puas"; + return "Sangat Puas"; + } + + Future _reviewProduct() async { + final customerId = ref.read(currentCustomerIdProvider); + + final List reviews = []; + + for (int i = 0; i < _products.length; i++) { + final product = _products[i]; + + final rating = _ratings[i] ?? 5; + + final comment = _controllers[i]?.text; + + reviews.add( + ProductReview( + customerId: customerId!, + productVariantId: product.id, + rating: rating, + comment: comment, + ), + ); + } + + try { + await ref + .read(orderControllerProvider.notifier) + .reviewProduct(widget.orderId, reviews); + + if (!mounted) return; + + await ref.read(orderReportControllerProvider.notifier).refresh(); + + await ref.read(homeControllerProvider.notifier).refresh(); + + CustomSnackbar.showSuccess(context, "Berhasil menilai pesanan"); + + context.pop(); + } on ServerException catch (e, st) { + LogMessage.log.e(e.toString(), error: e, stackTrace: st); + CustomSnackbar.showError(context, "Terjadi kesalahan koneksi"); + } + } + + @override + Widget build(BuildContext context) { + return LayoutBuilder( + builder: (context, constraints) { + final orderState = ref.watch(orderControllerProvider); + + final isLoading = orderState.isLoading; + + return SafeArea( + top: false, + bottom: true, + right: false, + left: false, + child: Scaffold( + backgroundColor: const Color(0xFFF8F9FA), + appBar: AppBar( + backgroundColor: Colors.white, + elevation: 0, + centerTitle: true, + leading: IconButton( + icon: Icon( + Icons.arrow_back, + size: 7.w, + color: AppColor.primaryColor, + ), + onPressed: () => context.pop(), + ), + title: Text( + "Nilai Produk", + style: TextStyle( + color: Colors.black, + fontSize: AppFontSize.medium.sp, + fontWeight: FontWeight.bold, + ), + ), + ), + body: Column( + children: [ + Expanded( + child: ListView.builder( + padding: EdgeInsets.all(4.w), + itemCount: _products.length, + itemBuilder: (context, index) { + return _buildRatingCard(index); + }, + ), + ), + _buildSubmitButton(isLoading), + ], + ), + ), + ); + }, + ); + } + + Widget _buildRatingCard(int index) { + final controller = _controllers[index]; + + final product = _products[index]; + + final rating = _ratings[index]; + + return Container( + margin: EdgeInsets.only(bottom: 2.h), + padding: EdgeInsets.all(4.w), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(3.w), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.03), + blurRadius: 10, + offset: const Offset(0, 5), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + CachedNetworkImage( + imageUrl: product.image ?? 'error', + imageBuilder: (context, imageProvider) { + return Container( + width: 15.w, + height: 15.w, + decoration: BoxDecoration( + color: Colors.grey.shade100, + borderRadius: BorderRadius.circular(2.w), + image: DecorationImage( + image: imageProvider, + fit: BoxFit.cover, + ), + ), + ); + }, + errorWidget: (context, url, error) { + return Container( + width: 15.w, + height: 15.w, + decoration: BoxDecoration( + color: Colors.grey.shade100, + borderRadius: BorderRadius.circular(2.w), + ), + child: Icon( + Icons.image, + color: Colors.grey.shade300, + size: 5.w, + ), + ); + }, + ), + SizedBox(width: 3.w), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + product.name, + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: AppFontSize.small.sp, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + + if (product.variantName != null) ...[ + SizedBox(height: 0.75.h), + Text( + product.variantName!, + style: TextStyle( + color: Colors.grey.shade700, + fontSize: (AppFontSize.small - 1.25).sp, + ), + ), + ], + ], + ), + ), + ], + ), + Divider(height: 4.h, color: Colors.grey.shade300), + + Center( + child: Column( + children: [ + Text( + _getRatingLabel(rating!), + style: TextStyle( + fontWeight: FontWeight.bold, + color: AppColor.primaryColor, + fontSize: (AppFontSize.small + 1).sp, + ), + ), + SizedBox(height: 1.h), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: List.generate(5, (starIndex) { + return GestureDetector( + onTap: () => + setState(() => _ratings[index] = starIndex + 1), + child: Icon( + starIndex < rating ? Icons.star : Icons.star_border, + color: Colors.orange, + size: 7.w, + ), + ); + }), + ), + ], + ), + ), + + SizedBox(height: 3.h), + + TextField( + controller: controller, + maxLines: 6, + style: TextStyle(fontSize: (AppFontSize.small - 1.25).sp), + decoration: InputDecoration( + hintText: "Bagikan penilaianmu tentang produk ini...", + hintStyle: TextStyle(fontSize: (AppFontSize.small - 1.25).sp), + filled: true, + fillColor: Colors.grey.shade50, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(2.w), + borderSide: BorderSide.none, + ), + ), + ), + ], + ), + ); + } + + Widget _buildSubmitButton(bool isLoading) { + return Container( + padding: EdgeInsets.all(4.w), + decoration: BoxDecoration( + color: Colors.white, + boxShadow: [BoxShadow(color: Colors.black12, blurRadius: 5)], + ), + child: SafeArea( + child: SizedBox( + width: double.infinity, + child: ElevatedButton( + onPressed: isLoading ? null : _reviewProduct, + style: ElevatedButton.styleFrom( + backgroundColor: AppColor.primaryColor, + padding: EdgeInsets.symmetric(vertical: 1.8.h), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(2.w), + ), + disabledBackgroundColor: Colors.grey.shade300, + ), + child: Text( + "Kirim Penilaian", + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: AppFontSize.medium.sp, + color: Colors.white, + ), + ), + ), + ), + ), + ); + } +} diff --git a/Mobile Commerce/lib/features/product/data/datasource/product_datasource_remote_impl.dart b/Mobile Commerce/lib/features/product/data/datasource/product_datasource_remote_impl.dart new file mode 100644 index 0000000..5e83a6d --- /dev/null +++ b/Mobile Commerce/lib/features/product/data/datasource/product_datasource_remote_impl.dart @@ -0,0 +1,29 @@ +import 'package:dio/dio.dart'; +import 'package:niogu_ecommerce_v1/core/constant/app_url.dart'; +import 'package:niogu_ecommerce_v1/core/models/api_response_model.dart'; +import 'package:niogu_ecommerce_v1/features/product/data/models/product_response_model.dart'; + +abstract class IProductDatasourceRemote { + Future> fetchProductById( + String id, + ); +} + +class ProductDatasourceRemoteImpl implements IProductDatasourceRemote { + final Dio _dio; + + ProductDatasourceRemoteImpl(this._dio); + + @override + Future> fetchProductById( + String id, + ) async { + final response = await _dio.get('${AppUrl.PRODUCTS}/$id'); + + return ApiResponseModel.fromJson( + response.data, + (json) => + ProductDetailResponse.fromJson(json as Map), + ); + } +} diff --git a/Mobile Commerce/lib/features/product/data/models/product_response_model.dart b/Mobile Commerce/lib/features/product/data/models/product_response_model.dart new file mode 100644 index 0000000..48f7b7c --- /dev/null +++ b/Mobile Commerce/lib/features/product/data/models/product_response_model.dart @@ -0,0 +1,176 @@ +import 'package:json_annotation/json_annotation.dart'; +import 'package:niogu_ecommerce_v1/core/enums/review_display_policy.dart'; +import 'package:niogu_ecommerce_v1/core/enums/review_display_type.dart'; +import 'package:niogu_ecommerce_v1/core/enums/stock_type.dart'; +import 'package:niogu_ecommerce_v1/core/utils/converter.dart'; +import 'package:niogu_ecommerce_v1/features/product/domain/entities/product.dart'; + +part 'product_response_model.g.dart'; + +@JsonSerializable() +class ProductDetailResponse { + @JsonKey(name: 'review_display_type') + final String reviewDisplayType; + + @JsonKey(name: 'review_display_policy') + final String reviewDisplayPolicy; + + @JsonKey(name: 'product_info') + final ProductInfoResponse productInfo; + + const ProductDetailResponse({ + required this.reviewDisplayType, + required this.reviewDisplayPolicy, + required this.productInfo, + }); + + factory ProductDetailResponse.fromJson(Map json) => + _$ProductDetailResponseFromJson(json); + + Map toJson() => _$ProductDetailResponseToJson(this); + + ProductDetail toProductDetail() { + return ProductDetail( + reviewDisplayType: ReviewDisplayType.values.byName(reviewDisplayType), + reviewDisplayPolicy: ReviewDisplayPolicy.values.byName(reviewDisplayPolicy), + productInfo: productInfo.toProductInfo(), + ); + } +} + +@JsonSerializable() +class ProductInfoResponse { + final String id; + + @JsonKey(name: 'server_image_url') + final List serverImageUrls; + + final String name; + + @JsonKey(name: 'has_variant') + final bool hasVariant; + + final String unit; + + @JsonKey(name: 'variants') + final List variants; + + final String? description; + + const ProductInfoResponse({ + required this.id, + required this.serverImageUrls, + required this.name, + required this.hasVariant, + required this.unit, + required this.variants, + required this.description, + }); + + factory ProductInfoResponse.fromJson(Map json) => _$ProductInfoResponseFromJson(json); + + Map toJson() => _$ProductInfoResponseToJson(this); + + ProductInfo toProductInfo() { + return ProductInfo( + id: id, + images: serverImageUrls, + name: name, + hasVariant: hasVariant, + unit: unit, + variants: variants.map((v) => v.toProductVariant()).toList(), + description: description, + ); + } +} + +@JsonSerializable() +class ProductVariantResponse { + final String id; + + @JsonKey(name: 'server_image_url') + final String? serverImageUrl; + + final String name; + + @JsonKey(name: 'is_product_variant') + final bool isProductVariant; + + @JsonKey(name: 'selling_price') + final double sellingPrice; + + @JsonKey(name: 'stock_type') + final String stockType; + + @JsonKey(fromJson: toDouble) + final double stock; + + @JsonKey(name: 'total_sold', fromJson: toDouble) + final double totalSold; + + @JsonKey(name: 'reviews') + final List reviews; + + const ProductVariantResponse({ + required this.id, + required this.serverImageUrl, + required this.name, + required this.isProductVariant, + required this.sellingPrice, + required this.stockType, + required this.stock, + required this.totalSold, + required this.reviews, + }); + + factory ProductVariantResponse.fromJson(Map json) => _$ProductVariantResponseFromJson(json); + + Map toJson() => _$ProductVariantResponseToJson(this); + + ProductVariant toProductVariant() { + return ProductVariant( + id: id, + image: serverImageUrl, + name: name, + isProductVariant: isProductVariant, + sellingPrice: sellingPrice, + stockType: StockType.values.byName(stockType), + stock: stock, + totalSold: totalSold, + reviews: reviews.map((r) => r.toProductReview()).toList(), + ); + } +} + +@JsonSerializable() +class ProductReviewResponse { + final String id; + + @JsonKey(name: 'customer_name') + final String customerName; + + @JsonKey(name: 'rating_count') + final int ratingCount; + + final String? comment; + + const ProductReviewResponse({ + required this.id, + required this.customerName, + required this.ratingCount, + required this.comment, + }); + + factory ProductReviewResponse.fromJson(Map json) => _$ProductReviewResponseFromJson(json); + + Map toJson() => _$ProductReviewResponseToJson(this); + + ProductReview toProductReview() { + return ProductReview( + id: id, + customerName: customerName, + ratingCount: ratingCount, + comment: comment, + ); + } +} diff --git a/Mobile Commerce/lib/features/product/data/models/product_response_model.g.dart b/Mobile Commerce/lib/features/product/data/models/product_response_model.g.dart new file mode 100644 index 0000000..a2ef30d --- /dev/null +++ b/Mobile Commerce/lib/features/product/data/models/product_response_model.g.dart @@ -0,0 +1,102 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'product_response_model.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +ProductDetailResponse _$ProductDetailResponseFromJson( + Map json, +) => ProductDetailResponse( + reviewDisplayType: json['review_display_type'] as String, + reviewDisplayPolicy: json['review_display_policy'] as String, + productInfo: ProductInfoResponse.fromJson( + json['product_info'] as Map, + ), +); + +Map _$ProductDetailResponseToJson( + ProductDetailResponse instance, +) => { + 'review_display_type': instance.reviewDisplayType, + 'review_display_policy': instance.reviewDisplayPolicy, + 'product_info': instance.productInfo, +}; + +ProductInfoResponse _$ProductInfoResponseFromJson(Map json) => + ProductInfoResponse( + id: json['id'] as String, + serverImageUrls: (json['server_image_url'] as List) + .map((e) => e as String) + .toList(), + name: json['name'] as String, + hasVariant: json['has_variant'] as bool, + unit: json['unit'] as String, + variants: (json['variants'] as List) + .map( + (e) => ProductVariantResponse.fromJson(e as Map), + ) + .toList(), + description: json['description'] as String?, + ); + +Map _$ProductInfoResponseToJson( + ProductInfoResponse instance, +) => { + 'id': instance.id, + 'server_image_url': instance.serverImageUrls, + 'name': instance.name, + 'has_variant': instance.hasVariant, + 'unit': instance.unit, + 'variants': instance.variants, + 'description': instance.description, +}; + +ProductVariantResponse _$ProductVariantResponseFromJson( + Map json, +) => ProductVariantResponse( + id: json['id'] as String, + serverImageUrl: json['server_image_url'] as String?, + name: json['name'] as String, + isProductVariant: json['is_product_variant'] as bool, + sellingPrice: (json['selling_price'] as num).toDouble(), + stockType: json['stock_type'] as String, + stock: toDouble(json['stock']), + totalSold: toDouble(json['total_sold']), + reviews: (json['reviews'] as List) + .map((e) => ProductReviewResponse.fromJson(e as Map)) + .toList(), +); + +Map _$ProductVariantResponseToJson( + ProductVariantResponse instance, +) => { + 'id': instance.id, + 'server_image_url': instance.serverImageUrl, + 'name': instance.name, + 'is_product_variant': instance.isProductVariant, + 'selling_price': instance.sellingPrice, + 'stock_type': instance.stockType, + 'stock': instance.stock, + 'total_sold': instance.totalSold, + 'reviews': instance.reviews, +}; + +ProductReviewResponse _$ProductReviewResponseFromJson( + Map json, +) => ProductReviewResponse( + id: json['id'] as String, + customerName: json['customer_name'] as String, + ratingCount: (json['rating_count'] as num).toInt(), + comment: json['comment'] as String?, +); + +Map _$ProductReviewResponseToJson( + ProductReviewResponse instance, +) => { + 'id': instance.id, + 'customer_name': instance.customerName, + 'rating_count': instance.ratingCount, + 'comment': instance.comment, +}; diff --git a/Mobile Commerce/lib/features/product/data/repositories/product_repository_impl.dart b/Mobile Commerce/lib/features/product/data/repositories/product_repository_impl.dart new file mode 100644 index 0000000..5c2545e --- /dev/null +++ b/Mobile Commerce/lib/features/product/data/repositories/product_repository_impl.dart @@ -0,0 +1,29 @@ +import 'package:dio/dio.dart'; +import 'package:niogu_ecommerce_v1/core/errors/exceptions.dart'; +import 'package:niogu_ecommerce_v1/core/utils/log_message.dart'; +import 'package:niogu_ecommerce_v1/features/product/data/datasource/product_datasource_remote_impl.dart'; +import 'package:niogu_ecommerce_v1/features/product/domain/entities/product.dart'; +import 'package:niogu_ecommerce_v1/features/product/domain/repositories/i_product_repository.dart'; + +class ProductRepositoryImpl implements IProductRepository { + final IProductDatasourceRemote _datasource; + + ProductRepositoryImpl(this._datasource); + + @override + Future fetchProductById(String id) async { + try { + final response = await _datasource.fetchProductById(id); + + final productDetail = response.data; + + if (response.status != 'success' || productDetail == null) return null; + + return productDetail.toProductDetail(); + } on DioException catch (e, st) { + LogMessage.log.e(e.toString(), error: e, stackTrace: st); + + throw ServerException("Terjadi kesalahan koneksi"); + } + } +} diff --git a/Mobile Commerce/lib/features/product/domain/entities/product.dart b/Mobile Commerce/lib/features/product/domain/entities/product.dart new file mode 100644 index 0000000..f58bee8 --- /dev/null +++ b/Mobile Commerce/lib/features/product/domain/entities/product.dart @@ -0,0 +1,73 @@ +import 'package:niogu_ecommerce_v1/core/enums/review_display_policy.dart'; +import 'package:niogu_ecommerce_v1/core/enums/review_display_type.dart'; +import 'package:niogu_ecommerce_v1/core/enums/stock_type.dart'; + +class ProductDetail { + final ReviewDisplayType reviewDisplayType; + final ReviewDisplayPolicy reviewDisplayPolicy; + final ProductInfo productInfo; + + const ProductDetail({ + required this.reviewDisplayType, + required this.reviewDisplayPolicy, + required this.productInfo, + }); +} + +class ProductInfo { + final String id; + final List images; + final String name; + final bool hasVariant; + final String unit; + final List variants; + final String? description; + + const ProductInfo({ + required this.id, + required this.images, + required this.name, + required this.hasVariant, + required this.unit, + required this.variants, + this.description, + }); +} + +class ProductVariant { + final String id; + final String? image; + final String name; + final bool isProductVariant; + final double sellingPrice; + final StockType stockType; + final double stock; + final double totalSold; + final List reviews; + + const ProductVariant({ + required this.id, + this.image, + required this.name, + required this.isProductVariant, + required this.sellingPrice, + required this.stockType, + required this.stock, + required this.totalSold, + required this.reviews, + }); +} + +class ProductReview { + final String id; + final String customerName; + final int ratingCount; + final String? comment; + + const ProductReview({ + required this.id, + required this.customerName, + required this.ratingCount, + this.comment, + }); +} \ No newline at end of file diff --git a/Mobile Commerce/lib/features/product/domain/repositories/i_product_repository.dart b/Mobile Commerce/lib/features/product/domain/repositories/i_product_repository.dart new file mode 100644 index 0000000..a9187d0 --- /dev/null +++ b/Mobile Commerce/lib/features/product/domain/repositories/i_product_repository.dart @@ -0,0 +1,5 @@ +import 'package:niogu_ecommerce_v1/features/product/domain/entities/product.dart'; + +abstract class IProductRepository { + Future fetchProductById(String id); +} \ No newline at end of file diff --git a/Mobile Commerce/lib/features/product/presentation/providers/product_provider.dart b/Mobile Commerce/lib/features/product/presentation/providers/product_provider.dart new file mode 100644 index 0000000..3b3f809 --- /dev/null +++ b/Mobile Commerce/lib/features/product/presentation/providers/product_provider.dart @@ -0,0 +1,20 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:niogu_ecommerce_v1/core/providers/app_provider.dart'; +import 'package:niogu_ecommerce_v1/features/product/data/datasource/product_datasource_remote_impl.dart'; +import 'package:niogu_ecommerce_v1/features/product/data/repositories/product_repository_impl.dart'; +import 'package:niogu_ecommerce_v1/features/product/domain/repositories/i_product_repository.dart'; + +final productRemoteDatasourceProvider = + Provider.autoDispose((ref) { + final dio = ref.watch(dioProvider); + + return ProductDatasourceRemoteImpl(dio); + }); + +final productRepositoryProvider = Provider.autoDispose(( + ref, +) { + final productRemoteDatasource = ref.watch(productRemoteDatasourceProvider); + + return ProductRepositoryImpl(productRemoteDatasource); +}); diff --git a/Mobile Commerce/lib/features/product/presentation/screens/product_detail_screen.dart b/Mobile Commerce/lib/features/product/presentation/screens/product_detail_screen.dart new file mode 100644 index 0000000..00c383f --- /dev/null +++ b/Mobile Commerce/lib/features/product/presentation/screens/product_detail_screen.dart @@ -0,0 +1,1065 @@ +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; +import 'package:niogu_ecommerce_v1/core/constant/app_font_size.dart'; +import 'package:niogu_ecommerce_v1/core/enums/review_display_policy.dart'; +import 'package:niogu_ecommerce_v1/core/enums/review_display_type.dart'; +import 'package:niogu_ecommerce_v1/core/enums/stock_type.dart'; +import 'package:niogu_ecommerce_v1/core/providers/app_provider.dart'; +import 'package:niogu_ecommerce_v1/core/router/app_route.dart'; +import 'package:niogu_ecommerce_v1/core/system/system_setting.dart'; +import 'package:niogu_ecommerce_v1/core/utils/currency_format.dart'; +import 'package:niogu_ecommerce_v1/core/utils/extenstion_format.dart'; +import 'package:niogu_ecommerce_v1/core/widgets/custom_snackbar.dart'; +import 'package:niogu_ecommerce_v1/features/account/presentation/widgets/login_auth_modal.dart'; +import 'package:niogu_ecommerce_v1/features/cart/domain/entities/cart.dart'; +import 'package:niogu_ecommerce_v1/features/cart/presentation/providers/cart_provider.dart'; +import 'package:niogu_ecommerce_v1/features/product/domain/entities/product.dart'; +import 'package:sizer/sizer.dart'; +import 'package:niogu_ecommerce_v1/core/constant/app_color.dart'; + +class ProductDetailScreen extends ConsumerStatefulWidget { + final ProductDetail productDetail; + const ProductDetailScreen({super.key, required this.productDetail}); + + @override + ConsumerState createState() => + _ProductDetailScreenState(); +} + +class _ProductDetailScreenState extends ConsumerState { + final PageController _imageController = PageController(); + + late final ProductDetail _productDetail; + + late final ReviewDisplayType _displayType; + + late final ReviewDisplayPolicy _displayPolicy; + + late final ProductInfo _productInfo; + + final List _productImages = []; + + final List _variants = []; + + final List _reviews = []; + + final List _allImages = []; + + double _totalSold = 0; + + bool _isDescriptionExpanded = false; + + int _selectedVariationIndex = 0; + + @override + void initState() { + // TODO: implement initState + super.initState(); + + _productDetail = widget.productDetail; + + _displayType = _productDetail.reviewDisplayType; + + _displayPolicy = _productDetail.reviewDisplayPolicy; + + _productInfo = _productDetail.productInfo; + + _productImages.addAll(_productInfo.images); + + _variants.addAll(_productInfo.variants); + + _allImages.addAll(_productImages); + + for (final variant in _variants) { + _totalSold += variant.totalSold; + final image = variant.image; + if (image != null) { + _allImages.add(image); + } + + if (_displayPolicy == ReviewDisplayPolicy.all_rating) { + _reviews.addAll(variant.reviews); + } else { + for (final review in variant.reviews) { + if (review.ratingCount >= 4) { + _reviews.add(review); + } + } + } + } + } + + Future _checkoutOrAddToCart({ + required int tempQuantity, + required double sellingPrice, + required bool isBuyNow, + }) async { + final outletId = ref.read(currentOutletIdProvider); + + var image = _productInfo.hasVariant + ? _productInfo.variants[_selectedVariationIndex].image + : _productInfo.images.lastOrNull; + + if (image == null) { + image = _productInfo.images.lastOrNull; + } + + final stockType = _variants[_selectedVariationIndex].stockType; + + final stock = _variants[_selectedVariationIndex].stock; + + final unit = _productInfo.unit; + + final cartItem = CartItem( + id: _productInfo.variants[_selectedVariationIndex].id, + outletId: outletId!, + image: image, + name: _productInfo.name, + variantName: _variants[_selectedVariationIndex].name, + isProductVariant: _variants[_selectedVariationIndex].isProductVariant, + sellingPrice: sellingPrice, + stockType: stockType.type, + stock: stock, + unit: unit, + quantity: tempQuantity, + ); + + if (isBuyNow) { + final isLoggedIn = ref.read(currentStatusLoginProvider); + + if (!isLoggedIn) { + showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: Colors.transparent, + constraints: BoxConstraints(maxWidth: double.infinity), + builder: (context) => const LoginAuthModal(), + ); + return; + } + + ref.read(cartItemControllerProvider.notifier).clear(); + ref + .read(cartItemControllerProvider.notifier) + .increment(cartItem, quantity: tempQuantity); + + final cartItems = ref.read(cartItemProvider); + + await SystemSetting.saveCartItemByOutlet(cartItems); + + context.pushNamed(AppRoute.checkoutScreen); + } else { + ref + .read(cartItemControllerProvider.notifier) + .increment(cartItem, quantity: tempQuantity); + + final cartItems = ref.read(cartItemProvider); + + await SystemSetting.saveCartItemByOutlet(cartItems); + + CustomSnackbar.showSuccess(context, "Berhasil ditambahkan ke keranjang"); + + context.goNamed(AppRoute.cartScreen); + } + } + + @override + void dispose() { + // TODO: implement dispose + _imageController.dispose(); + super.dispose(); + } + + void _showSelectedModal({required bool isBuyNow}) { + bool showLimitMessage = false; + + bool showMaksMessage = false; + + int tempQuantity = 1; + + final hasVariant = _productInfo.hasVariant; + + showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: Colors.transparent, + constraints: BoxConstraints(maxWidth: double.infinity), + builder: (context) { + return StatefulBuilder( + builder: (context, setModalState) { + final image = hasVariant + ? _variants[_selectedVariationIndex].image + : _productInfo.images.firstOrNull; + + final sellingPrice = + _variants[_selectedVariationIndex].sellingPrice; + + final totalOrder = sellingPrice * tempQuantity; + + final stockType = _variants[_selectedVariationIndex].stockType; + + final stock = _variants[_selectedVariationIndex].stock; + + final isAvailable = + stockType == StockType.unlimited || + (stockType == StockType.fixed && stock > 0); + return SafeArea( + top: false, + bottom: true, + right: false, + left: false, + child: Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.vertical( + top: Radius.circular(4.5.w), + ), + ), + constraints: BoxConstraints(maxWidth: double.infinity), + padding: EdgeInsets.fromLTRB(4.w, 2.h, 4.w, 4.h), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + CachedNetworkImage( + imageUrl: image ?? '', + imageBuilder: (context, imageProvider) { + return Container( + width: 25.w, + height: 25.w, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(2.5.w), + image: DecorationImage( + image: imageProvider, + fit: BoxFit.cover, + ), + ), + ); + }, + errorWidget: (context, url, error) { + return Container( + width: 25.w, + height: 25.w, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(2.5.w), + ), + child: Center( + child: Icon( + Icons.image_outlined, + color: Colors.grey.shade300, + size: 25.w, + ), + ), + ); + }, + ), + + SizedBox(width: 4.w), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + CurrencyFormat.formatToIdr(totalOrder, 0), + style: TextStyle( + color: AppColor.primaryColor, + fontSize: AppFontSize.medium.sp, + fontWeight: FontWeight.bold, + ), + ), + + if (stockType == StockType.fixed) ...[ + SizedBox(height: 1.5.h), + + Text( + stock <= 0 + ? "Stok Habis" + : "Stok: " + + stock + .toStringWithoutTrailingZero() + + " ${_productInfo.unit}", + style: TextStyle( + fontSize: (AppFontSize.small - 1.25).sp, + color: Colors.grey.shade700, + ), + ), + ], + ], + ), + ), + ], + ), + if (_productInfo.hasVariant) ...[ + Divider(height: 4.h), + Text( + "Varian", + style: TextStyle( + fontSize: (AppFontSize.small - 1.25).sp, + fontWeight: FontWeight.bold, + ), + ), + SizedBox(height: 1.5.h), + Wrap( + spacing: 2.w, + runSpacing: 1.h, + children: List.generate(_variants.length, (index) { + final variant = _variants[index]; + final isSelected = _selectedVariationIndex == index; + return GestureDetector( + onTap: () { + setModalState( + () => _selectedVariationIndex = index, + ); + }, + child: Container( + padding: EdgeInsets.symmetric( + horizontal: 4.w, + vertical: 1.5.h, + ), + decoration: BoxDecoration( + color: isSelected + ? AppColor.primaryColor.withOpacity(0.15) + : Colors.grey.shade100, + borderRadius: BorderRadius.circular(2.w), + ), + child: Text( + variant.name, + style: TextStyle( + color: isSelected + ? AppColor.primaryColor + : Colors.black87, + fontSize: (AppFontSize.small - 1.25).sp, + fontWeight: isSelected + ? FontWeight.bold + : FontWeight.normal, + ), + ), + ), + ); + }), + ), + ], + Divider(height: 4.h), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Jumlah", + style: TextStyle( + fontSize: (AppFontSize.small - 1.25).sp, + fontWeight: FontWeight.bold, + ), + ), + Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Container( + decoration: BoxDecoration( + border: Border.all(color: Colors.grey.shade300), + borderRadius: BorderRadius.circular(1.5.w), + ), + child: Row( + children: [ + _buildQtyBtn(Icons.remove, () { + if (tempQuantity > 1) + setModalState(() { + if (showMaksMessage) { + showMaksMessage = false; + } + tempQuantity--; + }); + }), + Container( + width: 12.w, + alignment: Alignment.center, + child: Text( + "$tempQuantity", + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: (AppFontSize.small - 1.25).sp, + ), + ), + ), + _buildQtyBtn(Icons.add, () { + if (showMaksMessage) return; + + if (stockType == StockType.fixed && + tempQuantity >= stock) { + setModalState( + () => showMaksMessage = true, + ); + return; + } + setModalState(() => tempQuantity++); + }), + ], + ), + ), + if (showMaksMessage) ...[ + SizedBox(height: 0.75.h), + Text( + "Kuantitas mencapai batas maksimum", + style: TextStyle( + color: Colors.red, + fontSize: (AppFontSize.small - 1.25).sp, + ), + ), + ], + ], + ), + ], + ), + if (showLimitMessage) ...[ + SizedBox(height: 1.h), + Text( + "Keranjang mencapai batas maksimum", + style: TextStyle( + color: Colors.red, + fontSize: (AppFontSize.small - 1.25).sp, + ), + ), + ], + SizedBox(height: 4.h), + + SizedBox( + width: double.infinity, + height: 6.h, + child: ElevatedButton( + onPressed: !isAvailable + ? null + : () async { + final cartItems = ref.read(cartItemProvider); + + final totalQuantity = cartItems.fold(0, ( + sum, + cartItem, + ) { + if (cartItem.id == + _variants[_selectedVariationIndex].id) { + return sum + cartItem.quantity; + } + return 0; + }); + + if (!isBuyNow && + stockType == StockType.fixed && + ((totalQuantity + tempQuantity) > stock)) { + setModalState(() => showLimitMessage = true); + return; + } + + await _checkoutOrAddToCart( + tempQuantity: tempQuantity, + sellingPrice: sellingPrice, + isBuyNow: isBuyNow, + ); + }, + style: ElevatedButton.styleFrom( + backgroundColor: AppColor.primaryColor, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(1.5.w), + ), + disabledBackgroundColor: Colors.grey.shade300, + ), + child: Text( + isBuyNow ? "Beli Sekarang" : "Masukkan Keranjang", + style: TextStyle( + color: Colors.white, + fontSize: AppFontSize.medium.sp, + fontWeight: FontWeight.bold, + ), + ), + ), + ), + ], + ), + ), + ); + }, + ); + }, + ); + } + + @override + Widget build(BuildContext context) { + return LayoutBuilder( + builder: (context, constraints) { + return SafeArea( + top: false, + bottom: true, + right: false, + left: false, + child: Scaffold( + backgroundColor: const Color(0xFFF5F5F5), + body: Stack( + children: [ + SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildProductImageSlider(), + + Container( + color: Colors.white, + padding: EdgeInsets.all(4.w), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildPriceAndActions(), + SizedBox(height: 1.h), + Text( + _productInfo.name, + style: TextStyle( + fontSize: AppFontSize.small.sp, + fontWeight: FontWeight.w500, + height: 1.4, + ), + ), + ], + ), + ), + if (_productInfo.hasVariant) _buildVariationSection(), + + if (_productInfo.description != null && + _productInfo.description!.isNotEmpty) + _buildDescriptionSection(), + + _reviews.isEmpty + ? _buildEmptyReview() + : _buildReviewSection(), + SizedBox(height: 5.h), + ], + ), + ), + + _buildTopOverlayButtons(), + ], + ), + bottomNavigationBar: _buildBottomAppBar(), + ), + ); + }, + ); + } + + Widget _buildQtyBtn(IconData icon, VoidCallback onTap) { + return InkWell( + onTap: onTap, + child: Container( + padding: EdgeInsets.all(1.h), + child: Icon(icon, size: 5.w, color: Colors.grey.shade700), + ), + ); + } + + Widget _buildProductImageSlider() { + if (_allImages.isEmpty) { + return AspectRatio( + aspectRatio: 1 / 1, + child: Container( + color: Colors.grey.shade100, + child: Center( + child: Icon( + Icons.image_outlined, + color: Colors.grey.shade300, + size: 25.w, + ), + ), + ), + ); + } + + final stockType = _variants[_selectedVariationIndex].stockType; + + final stock = _variants[_selectedVariationIndex].stock; + + return AspectRatio( + aspectRatio: 1 / 1, + child: PageView.builder( + controller: _imageController, + itemCount: _allImages.length, + onPageChanged: (index) { + if (index >= _productImages.length) { + setState(() { + _selectedVariationIndex = index - _productImages.length; + }); + } + }, + itemBuilder: (context, index) { + return Stack( + children: [ + CachedNetworkImage( + imageUrl: _allImages[index], + imageBuilder: (context, imageProvider) { + return Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(2.5.w), + image: DecorationImage( + image: imageProvider, + fit: BoxFit.cover, + ), + ), + ); + }, + errorWidget: (context, url, error) { + return Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(2.5.w), + ), + child: Center( + child: Icon( + Icons.image_outlined, + color: Colors.grey.shade300, + size: 25.w, + ), + ), + ); + }, + ), + + if (stockType == StockType.fixed && stock <= 0) + Center( + child: Container( + width: 20.w, + height: 20.w, + decoration: BoxDecoration( + color: Colors.black.withOpacity(0.75), + shape: BoxShape.circle, + ), + alignment: Alignment.center, + child: Text( + "HABIS", + style: TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + fontSize: AppFontSize.small.sp, + letterSpacing: 1.5, + ), + ), + ), + ), + ], + ); + }, + ), + ); + } + + Widget _buildPriceAndActions() { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + CurrencyFormat.formatToIdr( + _productInfo.variants[_selectedVariationIndex].sellingPrice, + 0, + ), + style: TextStyle( + color: AppColor.primaryColor, + fontSize: AppFontSize.large.sp, + fontWeight: FontWeight.bold, + ), + ), + Text( + "${_totalSold.toCompact} Terjual", + style: TextStyle( + fontSize: (AppFontSize.small - 1.25).sp, + color: Colors.grey.shade700, + ), + ), + ], + ); + } + + Widget _buildVariationSection() { + return Container( + color: Colors.white, + padding: EdgeInsets.all(4.w), + width: double.infinity, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Pilihan Variasi", + style: TextStyle( + fontSize: (AppFontSize.small - 1.25).sp, + fontWeight: FontWeight.bold, + ), + ), + SizedBox(height: 2.h), + SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Row( + children: List.generate(_variants.length, (index) { + final variant = _variants[index]; + final isSelected = _selectedVariationIndex == index; + return GestureDetector( + onTap: () { + setState(() => _selectedVariationIndex = index); + if (_variants[_selectedVariationIndex].image == null) + return; + _imageController.animateToPage( + _productImages.length + index, + duration: const Duration(milliseconds: 300), + curve: Curves.easeInOut, + ); + }, + child: _buildVariantCard(variant, isSelected), + ); + }), + ), + ), + ], + ), + ); + } + + Widget _buildVariantCard(ProductVariant variant, bool isSelected) { + return Container( + margin: EdgeInsets.only(right: 3.w), + padding: EdgeInsets.all(0.5.w), + decoration: BoxDecoration( + border: Border.all( + color: isSelected ? AppColor.primaryColor : Colors.grey.shade300, + width: 1, + ), + borderRadius: BorderRadius.circular(1.w), + ), + child: Row( + children: [ + CachedNetworkImage( + imageUrl: variant.image ?? '', + imageBuilder: (context, imageProvider) { + return Container( + width: 12.5.w, + height: 12.5.w, + decoration: BoxDecoration( + color: Colors.grey.shade100, + image: DecorationImage( + image: imageProvider, + fit: BoxFit.cover, + ), + ), + ); + }, + errorWidget: (context, url, error) { + return Container( + width: 12.5.w, + height: 12.5.w, + decoration: BoxDecoration(color: Colors.grey.shade100), + child: Center( + child: Icon( + Icons.image_outlined, + color: Colors.grey.shade300, + size: 5.w, + ), + ), + ); + }, + ), + SizedBox(width: 2.5.w), + Text( + variant.name, + style: TextStyle(fontSize: (AppFontSize.small - 1.25).sp), + ), + SizedBox(width: 2.5.w), + ], + ), + ); + } + + Widget _buildDescriptionSection() { + return Container( + color: Colors.white, + padding: EdgeInsets.all(4.w), + width: double.infinity, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Deskripsi Produk", + style: TextStyle( + fontSize: (AppFontSize.small - 1.25).sp, + fontWeight: FontWeight.bold, + ), + ), + SizedBox(height: 1.h), + Text( + _productInfo.description ?? 'Deskripsi produk belum tersedia', + maxLines: _isDescriptionExpanded ? 100 : 3, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontSize: (AppFontSize.small - 1.25).sp, + color: Colors.black87, + height: 1.5, + ), + ), + if (_productInfo.description != null) + Center( + child: TextButton.icon( + style: TextButton.styleFrom(overlayColor: Colors.transparent), + onPressed: () => setState( + () => _isDescriptionExpanded = !_isDescriptionExpanded, + ), + icon: Icon( + _isDescriptionExpanded + ? Icons.keyboard_arrow_up + : Icons.keyboard_arrow_down, + size: 5.w, + color: AppColor.primaryColor, + ), + label: Text( + _isDescriptionExpanded ? "Tutup" : "Selengkapnya", + style: TextStyle( + color: AppColor.primaryColor, + fontSize: AppFontSize.small.sp, + ), + ), + ), + ), + ], + ), + ); + } + + Widget _buildReviewSection() { + return Container( + color: Colors.white, + padding: EdgeInsets.all(4.w), + width: double.infinity, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Penilaian Produk", + style: TextStyle( + fontSize: (AppFontSize.small - 1.25).sp, + fontWeight: FontWeight.bold, + ), + ), + /** + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Penilaian Produk", + style: TextStyle( + fontSize: (AppFontSize.small - 1.25).sp, + fontWeight: FontWeight.bold, + ), + ), + Text( + "Lihat Semua", + style: TextStyle( + fontSize: (AppFontSize.small - 1.25).sp, + color: AppColor.primaryColor, + ), + ), + ], + ), + */ + ListView.separated( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: _reviews.length, + separatorBuilder: (context, index) => + Divider(color: Colors.grey.shade200, thickness: 1), + itemBuilder: (context, index) { + final review = _reviews[index]; + return Padding( + padding: EdgeInsets.symmetric(vertical: 1.h), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + CircleAvatar( + radius: 5.w, + backgroundColor: Colors.grey.shade100, + child: Icon( + Icons.person, + color: Colors.grey.shade400, + size: 5.w, + ), + ), + SizedBox(width: 3.w), + + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + review.customerName, + style: TextStyle( + fontSize: (AppFontSize.small - 1.25).sp, + fontWeight: FontWeight.w500, + ), + ), + SizedBox(height: 0.5.h), + + Row( + children: List.generate(5, (starIndex) { + return Icon( + Icons.star, + size: 3.w, + color: starIndex < review.ratingCount + ? Colors.orange + : Colors.grey.shade300, + ); + }), + ), + if (_displayType == + ReviewDisplayType.rating_and_review && + review.comment != null) ...[ + SizedBox(height: 1.h), + Text( + review.comment!, + style: TextStyle( + fontSize: (AppFontSize.small - 1.25).sp, + color: Colors.black87, + height: 1.4, + ), + ), + ], + ], + ), + ), + ], + ), + ); + }, + ), + ], + ), + ); + } + + Widget _buildEmptyReview() { + return Container( + width: double.infinity, + padding: EdgeInsets.symmetric(vertical: 4.h, horizontal: 4.w), + decoration: BoxDecoration( + color: Colors.white, + border: Border(top: BorderSide(color: Colors.grey.shade100)), + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.rate_review_outlined, + size: AppFontSize.extraLarge.sp, + color: Colors.grey.shade300, + ), + SizedBox(height: 2.h), + Text( + "Belum ada penilaian untuk produk ini", + style: TextStyle( + fontSize: AppFontSize.small.sp, + color: Colors.grey.shade500, + fontWeight: FontWeight.w400, + ), + ), + SizedBox(height: 1.h), + Text( + "Jadilah yang pertama memberikan ulasan!", + style: TextStyle( + fontSize: (AppFontSize.small - 2).sp, + color: Colors.grey.shade500, + ), + ), + ], + ), + ); + } + + Widget _buildTopOverlayButtons() { + return SafeArea( + child: Padding( + padding: EdgeInsets.symmetric(horizontal: 4.w, vertical: 1.h), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + CircleAvatar( + backgroundColor: Colors.black.withOpacity(0.3), + radius: 5.w, + child: IconButton( + icon: Icon(Icons.arrow_back, size: 5.w, color: Colors.white), + onPressed: () => context.pop(), + ), + ), + /** + Row( + children: [ + CircleAvatar( + backgroundColor: Colors.black.withOpacity(0.3), + radius: 5.w, + child: Icon(Icons.share, size: 5.w, color: Colors.white), + ), + SizedBox(width: 3.w), + CircleAvatar( + backgroundColor: Colors.black.withOpacity(0.3), + radius: 5.w, + child: Icon( + Icons.shopping_cart, + size: 5.w, + color: Colors.white, + ), + ), + ], + ), + */ + ], + ), + ), + ); + } + + Widget _buildBottomAppBar() { + return Container( + height: 8.h, + decoration: BoxDecoration( + color: Colors.white, + border: Border(top: BorderSide(color: Colors.grey.shade200)), + ), + child: Row( + children: [ + Expanded( + flex: 2, + child: InkWell( + onTap: () => _showSelectedModal(isBuyNow: false), + child: Icon( + Icons.add_shopping_cart, + color: AppColor.primaryColor, + size: 5.w, + ), + ), + ), + Container(width: 1, height: 5.h, color: Colors.grey.shade200), + Expanded( + flex: 3, + child: Container( + color: AppColor.primaryColor, + child: InkWell( + onTap: () => _showSelectedModal(isBuyNow: true), + child: Center( + child: Text( + "Beli Sekarang", + style: TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + fontSize: AppFontSize.medium.sp, + ), + ), + ), + ), + ), + ), + ], + ), + ); + } +} diff --git a/Mobile Commerce/lib/features/splash/presentation/screens/splash_screen.dart b/Mobile Commerce/lib/features/splash/presentation/screens/splash_screen.dart new file mode 100644 index 0000000..6a72851 --- /dev/null +++ b/Mobile Commerce/lib/features/splash/presentation/screens/splash_screen.dart @@ -0,0 +1,175 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; +import 'package:latlong2/latlong.dart'; +import 'package:niogu_ecommerce_v1/core/constant/app_asset.dart'; +import 'package:niogu_ecommerce_v1/core/constant/app_font_size.dart'; +import 'package:niogu_ecommerce_v1/core/providers/app_provider.dart'; +import 'package:niogu_ecommerce_v1/core/router/app_route.dart'; +import 'package:niogu_ecommerce_v1/core/system/system_setting.dart'; +import 'package:niogu_ecommerce_v1/features/cart/presentation/providers/cart_provider.dart'; +import 'package:niogu_ecommerce_v1/features/home/domain/entities/home.dart'; +import 'package:niogu_ecommerce_v1/features/home/presentation/providers/home_provider.dart'; +import 'package:sizer/sizer.dart'; + +class SplashScreen extends ConsumerStatefulWidget { + const SplashScreen({super.key}); + + @override + ConsumerState createState() => _SplashScreenState(); +} + +class _SplashScreenState extends ConsumerState { + @override + void initState() { + super.initState(); + + WidgetsBinding.instance.addPostFrameCallback((_) async { + final currentOutlet = await _setCurrentOutlet(); + + _setCustomerInfo(); + + _setCartItems(); + + if (!mounted) return; + + if (currentOutlet == null) return; + + Future.delayed(const Duration(seconds: 3), () { + context.goNamed(AppRoute.homeScreen); + }); + }); + } + + @override + void dispose() { + // TODO: implement dispose + super.dispose(); + } + + Future _setCurrentOutlet() async { + final mainOutlet = await ref.read(homeRepositoryProvider).fetchMainOutlet(); + + if (mainOutlet == null) return null; + + await SystemSetting.switchOutlet( + outletId: mainOutlet.uuid, + outletName: mainOutlet.name, + outletPhone: mainOutlet.phoneNumber, + outletLocation: mainOutlet.location, + outletCoordinate: LatLng( + mainOutlet.latitude ?? 0.0, + mainOutlet.longitude ?? 0.0, + ), + ); + + final currentOutletId = await SystemSetting.getCurrentOutletId(); + + final currentOutletName = await SystemSetting.getCurrentOutletName(); + + final currentOutletPhone = await SystemSetting.getCurrentOutletPhone(); + + final currentOutletLocation = + await SystemSetting.getCurrentOutletLocation(); + + final currentOutletCoordinate = + await SystemSetting.getCurrentOutletCoordinate(); + + ref.read(currentOutletIdProvider.notifier).state = currentOutletId; + + ref.read(currentOutletNameProvider.notifier).state = currentOutletName; + + ref.read(currentOutletPhoneProvider.notifier).state = currentOutletPhone; + + ref.read(currentOutletLocationProvider.notifier).state = + currentOutletLocation; + + ref.read(currentOutletCoordinateProvider.notifier).state = + currentOutletCoordinate; + + return mainOutlet; + } + + Future _setCustomerInfo() async { + final customerId = await SystemSetting.getCurrentCustomerId(); + + final customerName = await SystemSetting.getCurrentCustomerName(); + + final customerEmail = await SystemSetting.getCurrentCustomerEmail(); + + final customerPhone = await SystemSetting.getCurrentCustomerPhone(); + + final isLoggedIn = await SystemSetting.isLoggedIn(); + + ref.read(currentCustomerIdProvider.notifier).state = customerId; + + ref.read(currentCustomerNameProvider.notifier).state = customerName; + + ref.read(currentCustomerEmailProvider.notifier).state = customerEmail; + + ref.read(currentCustomerPhoneProvider.notifier).state = customerPhone; + + ref.read(currentStatusLoginProvider.notifier).state = isLoggedIn; + } + + Future _setCartItems() async { + final cartItems = await SystemSetting.getCartItemByOutlet(); + + ref.read(cartItemControllerProvider.notifier).hydrate(cartItems); + } + + @override + Widget build(BuildContext context) { + return LayoutBuilder( + builder: (context, constraints) { + return SafeArea( + top: false, + bottom: true, + right: false, + left: false, + child: Scaffold( + backgroundColor: Colors.white, + body: Stack( + alignment: Alignment.center, + children: [ + Center( + child: Image.asset( + AppAsset.LOGO, + fit: BoxFit.cover, + height: 45.w, + width: 45.w, + ), + ), + + Positioned( + bottom: 5.h, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + "powered by ", + style: TextStyle( + fontSize: (AppFontSize.small - 2).sp, + color: Colors.grey.shade500, + ), + ), + Text( + "Niogu", + style: TextStyle( + fontSize: (AppFontSize.small - 2).sp, + fontWeight: FontWeight.bold, + color: Colors.grey.shade500, + letterSpacing: 1, + ), + ), + ], + ), + ), + ], + ), + ), + ); + }, + ); + } +} diff --git a/Mobile Commerce/lib/main_app.dart b/Mobile Commerce/lib/main_app.dart new file mode 100644 index 0000000..700f84e --- /dev/null +++ b/Mobile Commerce/lib/main_app.dart @@ -0,0 +1,63 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_localizations/flutter_localizations.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:intl/date_symbol_data_local.dart'; +import 'package:intl/intl.dart'; +import 'package:niogu_ecommerce_v1/core/router/app_route.dart'; +import 'package:sizer/sizer.dart'; + +void main(List args) async { + WidgetsFlutterBinding.ensureInitialized(); + + await initializeDateFormatting('id_ID', null); + + Intl.defaultLocale = "id_ID"; + + await SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]); + + runApp(const MainApp()); +} + +class MainApp extends StatelessWidget { + const MainApp({super.key}); + + @override + Widget build(BuildContext context) { + return ProviderScope( + child: Sizer( + builder: (context, orientation, deviceType) { + return MaterialApp.router( + title: "Niogu", + localizationsDelegates: const [ + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + ], + supportedLocales: const [ + const Locale('id', 'ID'), + const Locale('en', 'US'), + ], + locale: const Locale('id', 'ID'), + debugShowCheckedModeBanner: false, + theme: ThemeData(fontFamily: "Poppins", useMaterial3: true), + builder: (context, child) { + final mediaQueryData = MediaQuery.of(context); + + final scale = mediaQueryData.textScaler.clamp( + minScaleFactor: 1.0, + maxScaleFactor: 1.1, + ); + + return MediaQuery( + data: mediaQueryData.copyWith(textScaler: scale), + child: child!, + ); + }, + routerConfig: AppRoute.router, + ); + }, + ), + ); + } +} diff --git a/Mobile Commerce/lib/main_wrapper.dart b/Mobile Commerce/lib/main_wrapper.dart new file mode 100644 index 0000000..d17ff14 --- /dev/null +++ b/Mobile Commerce/lib/main_wrapper.dart @@ -0,0 +1,48 @@ +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'package:niogu_ecommerce_v1/core/components/bottom_bar_app.dart'; + +class MainWrapper extends StatelessWidget { + final StatefulNavigationShell navigationShell; + + const MainWrapper({super.key, required this.navigationShell}); + + static final GlobalKey scaffoldKey = + GlobalKey(); + + @override + Widget build(BuildContext context) { + return LayoutBuilder( + builder: (context, constraints) { + return SafeArea( + top: false, + bottom: true, + right: false, + left: false, + child: Stack( + children: [ + Scaffold( + key: scaffoldKey, + extendBody: true, + resizeToAvoidBottomInset: false, + body: navigationShell, + bottomNavigationBar: navigationShell.currentIndex == 2 + ? null + : BottomBarApp( + currentIndex: navigationShell.currentIndex, + onTap: (index) { + navigationShell.goBranch( + index, + initialLocation: + index == navigationShell.currentIndex, + ); + }, + ), + ), + ], + ), + ); + }, + ); + } +} diff --git a/Mobile Commerce/linux/.gitignore b/Mobile Commerce/linux/.gitignore new file mode 100644 index 0000000..d3896c9 --- /dev/null +++ b/Mobile Commerce/linux/.gitignore @@ -0,0 +1 @@ +flutter/ephemeral diff --git a/Mobile Commerce/linux/CMakeLists.txt b/Mobile Commerce/linux/CMakeLists.txt new file mode 100644 index 0000000..7760e67 --- /dev/null +++ b/Mobile Commerce/linux/CMakeLists.txt @@ -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 "niogu_ecommerce_v1") +# The unique GTK application identifier for this application. See: +# https://wiki.gnome.org/HowDoI/ChooseApplicationID +set(APPLICATION_ID "com.example.niogu_ecommerce_v1") + +# 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 "$<$>:-O3>") + target_compile_definitions(${TARGET} PRIVATE "$<$>: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() diff --git a/Mobile Commerce/linux/flutter/CMakeLists.txt b/Mobile Commerce/linux/flutter/CMakeLists.txt new file mode 100644 index 0000000..d5bd016 --- /dev/null +++ b/Mobile Commerce/linux/flutter/CMakeLists.txt @@ -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} +) diff --git a/Mobile Commerce/linux/flutter/generated_plugin_registrant.cc b/Mobile Commerce/linux/flutter/generated_plugin_registrant.cc new file mode 100644 index 0000000..3ccd551 --- /dev/null +++ b/Mobile Commerce/linux/flutter/generated_plugin_registrant.cc @@ -0,0 +1,23 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#include "generated_plugin_registrant.h" + +#include +#include +#include + +void fl_register_plugins(FlPluginRegistry* registry) { + g_autoptr(FlPluginRegistrar) file_selector_linux_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "FileSelectorPlugin"); + file_selector_plugin_register_with_registrar(file_selector_linux_registrar); + g_autoptr(FlPluginRegistrar) flutter_secure_storage_linux_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterSecureStorageLinuxPlugin"); + flutter_secure_storage_linux_plugin_register_with_registrar(flutter_secure_storage_linux_registrar); + g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin"); + url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar); +} diff --git a/Mobile Commerce/linux/flutter/generated_plugin_registrant.h b/Mobile Commerce/linux/flutter/generated_plugin_registrant.h new file mode 100644 index 0000000..e0f0a47 --- /dev/null +++ b/Mobile Commerce/linux/flutter/generated_plugin_registrant.h @@ -0,0 +1,15 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#ifndef GENERATED_PLUGIN_REGISTRANT_ +#define GENERATED_PLUGIN_REGISTRANT_ + +#include + +// Registers Flutter plugins. +void fl_register_plugins(FlPluginRegistry* registry); + +#endif // GENERATED_PLUGIN_REGISTRANT_ diff --git a/Mobile Commerce/linux/flutter/generated_plugins.cmake b/Mobile Commerce/linux/flutter/generated_plugins.cmake new file mode 100644 index 0000000..9ce94c4 --- /dev/null +++ b/Mobile Commerce/linux/flutter/generated_plugins.cmake @@ -0,0 +1,26 @@ +# +# Generated file, do not edit. +# + +list(APPEND FLUTTER_PLUGIN_LIST + file_selector_linux + flutter_secure_storage_linux + url_launcher_linux +) + +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 $) + 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) diff --git a/Mobile Commerce/linux/runner/CMakeLists.txt b/Mobile Commerce/linux/runner/CMakeLists.txt new file mode 100644 index 0000000..e97dabc --- /dev/null +++ b/Mobile Commerce/linux/runner/CMakeLists.txt @@ -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}") diff --git a/Mobile Commerce/linux/runner/main.cc b/Mobile Commerce/linux/runner/main.cc new file mode 100644 index 0000000..e7c5c54 --- /dev/null +++ b/Mobile Commerce/linux/runner/main.cc @@ -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); +} diff --git a/Mobile Commerce/linux/runner/my_application.cc b/Mobile Commerce/linux/runner/my_application.cc new file mode 100644 index 0000000..60d026e --- /dev/null +++ b/Mobile Commerce/linux/runner/my_application.cc @@ -0,0 +1,148 @@ +#include "my_application.h" + +#include +#ifdef GDK_WINDOWING_X11 +#include +#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) + +// Called when first Flutter frame received. +static void first_frame_cb(MyApplication* self, FlView* view) { + gtk_widget_show(gtk_widget_get_toplevel(GTK_WIDGET(view))); +} + +// 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, "niogu_ecommerce_v1"); + 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, "niogu_ecommerce_v1"); + } + + gtk_window_set_default_size(window, 1280, 720); + + 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); + GdkRGBA background_color; + // Background defaults to black, override it here if necessary, e.g. #00000000 + // for transparent. + gdk_rgba_parse(&background_color, "#000000"); + fl_view_set_background_color(view, &background_color); + gtk_widget_show(GTK_WIDGET(view)); + gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view)); + + // Show the window when Flutter renders. + // Requires the view to be realized so we can start rendering. + g_signal_connect_swapped(view, "first-frame", G_CALLBACK(first_frame_cb), + self); + gtk_widget_realize(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)); +} diff --git a/Mobile Commerce/linux/runner/my_application.h b/Mobile Commerce/linux/runner/my_application.h new file mode 100644 index 0000000..db16367 --- /dev/null +++ b/Mobile Commerce/linux/runner/my_application.h @@ -0,0 +1,21 @@ +#ifndef FLUTTER_MY_APPLICATION_H_ +#define FLUTTER_MY_APPLICATION_H_ + +#include + +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_ diff --git a/Mobile Commerce/macos/.gitignore b/Mobile Commerce/macos/.gitignore new file mode 100644 index 0000000..746adbb --- /dev/null +++ b/Mobile Commerce/macos/.gitignore @@ -0,0 +1,7 @@ +# Flutter-related +**/Flutter/ephemeral/ +**/Pods/ + +# Xcode-related +**/dgph +**/xcuserdata/ diff --git a/Mobile Commerce/macos/Flutter/Flutter-Debug.xcconfig b/Mobile Commerce/macos/Flutter/Flutter-Debug.xcconfig new file mode 100644 index 0000000..c2efd0b --- /dev/null +++ b/Mobile Commerce/macos/Flutter/Flutter-Debug.xcconfig @@ -0,0 +1 @@ +#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/Mobile Commerce/macos/Flutter/Flutter-Release.xcconfig b/Mobile Commerce/macos/Flutter/Flutter-Release.xcconfig new file mode 100644 index 0000000..c2efd0b --- /dev/null +++ b/Mobile Commerce/macos/Flutter/Flutter-Release.xcconfig @@ -0,0 +1 @@ +#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/Mobile Commerce/macos/Flutter/GeneratedPluginRegistrant.swift b/Mobile Commerce/macos/Flutter/GeneratedPluginRegistrant.swift new file mode 100644 index 0000000..88714e0 --- /dev/null +++ b/Mobile Commerce/macos/Flutter/GeneratedPluginRegistrant.swift @@ -0,0 +1,26 @@ +// +// Generated file. Do not edit. +// + +import FlutterMacOS +import Foundation + +import file_selector_macos +import flutter_secure_storage_darwin +import geolocator_apple +import package_info_plus +import path_provider_foundation +import shared_preferences_foundation +import sqflite_darwin +import url_launcher_macos + +func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { + FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin")) + FlutterSecureStorageDarwinPlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStorageDarwinPlugin")) + GeolocatorPlugin.register(with: registry.registrar(forPlugin: "GeolocatorPlugin")) + FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin")) + PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) + SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) + SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin")) + UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) +} diff --git a/Mobile Commerce/macos/Runner.xcodeproj/project.pbxproj b/Mobile Commerce/macos/Runner.xcodeproj/project.pbxproj new file mode 100644 index 0000000..235e28b --- /dev/null +++ b/Mobile Commerce/macos/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,705 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 54; + objects = { + +/* Begin PBXAggregateTarget section */ + 33CC111A2044C6BA0003C045 /* Flutter Assemble */ = { + isa = PBXAggregateTarget; + buildConfigurationList = 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */; + buildPhases = ( + 33CC111E2044C6BF0003C045 /* ShellScript */, + ); + dependencies = ( + ); + name = "Flutter Assemble"; + productName = FLX; + }; +/* End PBXAggregateTarget section */ + +/* Begin PBXBuildFile section */ + 331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C80D7294CF71000263BE5 /* RunnerTests.swift */; }; + 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */; }; + 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC10F02044A3C60003C045 /* AppDelegate.swift */; }; + 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; }; + 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; }; + 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 331C80D9294CF71000263BE5 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 33CC10E52044A3C60003C045 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 33CC10EC2044A3C60003C045; + remoteInfo = Runner; + }; + 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 33CC10E52044A3C60003C045 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 33CC111A2044C6BA0003C045; + remoteInfo = FLX; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 33CC110E2044A8840003C045 /* Bundle Framework */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Bundle Framework"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 331C80D5294CF71000263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 331C80D7294CF71000263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; + 333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = ""; }; + 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = ""; }; + 33CC10ED2044A3C60003C045 /* niogu_ecommerce_v1.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "niogu_ecommerce_v1.app"; sourceTree = BUILT_PRODUCTS_DIR; }; + 33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = ""; }; + 33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; }; + 33CC10F72044A3C60003C045 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Info.plist; path = Runner/Info.plist; sourceTree = ""; }; + 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainFlutterWindow.swift; sourceTree = ""; }; + 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Debug.xcconfig"; sourceTree = ""; }; + 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Release.xcconfig"; sourceTree = ""; }; + 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = "Flutter-Generated.xcconfig"; path = "ephemeral/Flutter-Generated.xcconfig"; sourceTree = ""; }; + 33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = ""; }; + 33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = ""; }; + 33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 331C80D2294CF70F00263BE5 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 33CC10EA2044A3C60003C045 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 331C80D6294CF71000263BE5 /* RunnerTests */ = { + isa = PBXGroup; + children = ( + 331C80D7294CF71000263BE5 /* RunnerTests.swift */, + ); + path = RunnerTests; + sourceTree = ""; + }; + 33BA886A226E78AF003329D5 /* Configs */ = { + isa = PBXGroup; + children = ( + 33E5194F232828860026EE4D /* AppInfo.xcconfig */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 333000ED22D3DE5D00554162 /* Warnings.xcconfig */, + ); + path = Configs; + sourceTree = ""; + }; + 33CC10E42044A3C60003C045 = { + isa = PBXGroup; + children = ( + 33FAB671232836740065AC1E /* Runner */, + 33CEB47122A05771004F2AC0 /* Flutter */, + 331C80D6294CF71000263BE5 /* RunnerTests */, + 33CC10EE2044A3C60003C045 /* Products */, + D73912EC22F37F3D000D13A0 /* Frameworks */, + ); + sourceTree = ""; + }; + 33CC10EE2044A3C60003C045 /* Products */ = { + isa = PBXGroup; + children = ( + 33CC10ED2044A3C60003C045 /* niogu_ecommerce_v1.app */, + 331C80D5294CF71000263BE5 /* RunnerTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 33CC11242044D66E0003C045 /* Resources */ = { + isa = PBXGroup; + children = ( + 33CC10F22044A3C60003C045 /* Assets.xcassets */, + 33CC10F42044A3C60003C045 /* MainMenu.xib */, + 33CC10F72044A3C60003C045 /* Info.plist */, + ); + name = Resources; + path = ..; + sourceTree = ""; + }; + 33CEB47122A05771004F2AC0 /* Flutter */ = { + isa = PBXGroup; + children = ( + 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */, + 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */, + 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */, + 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */, + ); + path = Flutter; + sourceTree = ""; + }; + 33FAB671232836740065AC1E /* Runner */ = { + isa = PBXGroup; + children = ( + 33CC10F02044A3C60003C045 /* AppDelegate.swift */, + 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */, + 33E51913231747F40026EE4D /* DebugProfile.entitlements */, + 33E51914231749380026EE4D /* Release.entitlements */, + 33CC11242044D66E0003C045 /* Resources */, + 33BA886A226E78AF003329D5 /* Configs */, + ); + path = Runner; + sourceTree = ""; + }; + D73912EC22F37F3D000D13A0 /* Frameworks */ = { + isa = PBXGroup; + children = ( + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 331C80D4294CF70F00263BE5 /* RunnerTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; + buildPhases = ( + 331C80D1294CF70F00263BE5 /* Sources */, + 331C80D2294CF70F00263BE5 /* Frameworks */, + 331C80D3294CF70F00263BE5 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 331C80DA294CF71000263BE5 /* PBXTargetDependency */, + ); + name = RunnerTests; + productName = RunnerTests; + productReference = 331C80D5294CF71000263BE5 /* RunnerTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 33CC10EC2044A3C60003C045 /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + 33CC10E92044A3C60003C045 /* Sources */, + 33CC10EA2044A3C60003C045 /* Frameworks */, + 33CC10EB2044A3C60003C045 /* Resources */, + 33CC110E2044A8840003C045 /* Bundle Framework */, + 3399D490228B24CF009A79C7 /* ShellScript */, + ); + buildRules = ( + ); + dependencies = ( + 33CC11202044C79F0003C045 /* PBXTargetDependency */, + ); + name = Runner; + productName = Runner; + productReference = 33CC10ED2044A3C60003C045 /* niogu_ecommerce_v1.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 33CC10E52044A3C60003C045 /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = YES; + LastSwiftUpdateCheck = 0920; + LastUpgradeCheck = 1510; + ORGANIZATIONNAME = ""; + TargetAttributes = { + 331C80D4294CF70F00263BE5 = { + CreatedOnToolsVersion = 14.0; + TestTargetID = 33CC10EC2044A3C60003C045; + }; + 33CC10EC2044A3C60003C045 = { + CreatedOnToolsVersion = 9.2; + LastSwiftMigration = 1100; + ProvisioningStyle = Automatic; + SystemCapabilities = { + com.apple.Sandbox = { + enabled = 1; + }; + }; + }; + 33CC111A2044C6BA0003C045 = { + CreatedOnToolsVersion = 9.2; + ProvisioningStyle = Manual; + }; + }; + }; + buildConfigurationList = 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 33CC10E42044A3C60003C045; + productRefGroup = 33CC10EE2044A3C60003C045 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 33CC10EC2044A3C60003C045 /* Runner */, + 331C80D4294CF70F00263BE5 /* RunnerTests */, + 33CC111A2044C6BA0003C045 /* Flutter Assemble */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 331C80D3294CF70F00263BE5 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 33CC10EB2044A3C60003C045 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */, + 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 3399D490228B24CF009A79C7 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "echo \"$PRODUCT_NAME.app\" > \"$PROJECT_DIR\"/Flutter/ephemeral/.app_filename && \"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh embed\n"; + }; + 33CC111E2044C6BF0003C045 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + Flutter/ephemeral/FlutterInputs.xcfilelist, + ); + inputPaths = ( + Flutter/ephemeral/tripwire, + ); + outputFileListPaths = ( + Flutter/ephemeral/FlutterOutputs.xcfilelist, + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire"; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 331C80D1294CF70F00263BE5 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 33CC10E92044A3C60003C045 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */, + 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */, + 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 331C80DA294CF71000263BE5 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 33CC10EC2044A3C60003C045 /* Runner */; + targetProxy = 331C80D9294CF71000263BE5 /* PBXContainerItemProxy */; + }; + 33CC11202044C79F0003C045 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 33CC111A2044C6BA0003C045 /* Flutter Assemble */; + targetProxy = 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 33CC10F42044A3C60003C045 /* MainMenu.xib */ = { + isa = PBXVariantGroup; + children = ( + 33CC10F52044A3C60003C045 /* Base */, + ); + name = MainMenu.xib; + path = Runner; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 331C80DB294CF71000263BE5 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.nioguEcommerceV1.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/niogu_ecommerce_v1.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/niogu_ecommerce_v1"; + }; + name = Debug; + }; + 331C80DC294CF71000263BE5 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.nioguEcommerceV1.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/niogu_ecommerce_v1.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/niogu_ecommerce_v1"; + }; + name = Release; + }; + 331C80DD294CF71000263BE5 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.nioguEcommerceV1.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/niogu_ecommerce_v1.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/niogu_ecommerce_v1"; + }; + name = Profile; + }; + 338D0CE9231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + 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_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + 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_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = YES; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.15; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + }; + name = Profile; + }; + 338D0CEA231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_VERSION = 5.0; + }; + name = Profile; + }; + 338D0CEB231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Manual; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Profile; + }; + 33CC10F92044A3C60003C045 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + 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_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + 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_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = YES; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu11; + 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_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.15; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = macosx; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 33CC10FA2044A3C60003C045 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + 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_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + 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_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = YES; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.15; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + }; + name = Release; + }; + 33CC10FC2044A3C60003C045 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + }; + name = Debug; + }; + 33CC10FD2044A3C60003C045 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_VERSION = 5.0; + }; + name = Release; + }; + 33CC111C2044C6BA0003C045 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Manual; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Debug; + }; + 33CC111D2044C6BA0003C045 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 331C80DB294CF71000263BE5 /* Debug */, + 331C80DC294CF71000263BE5 /* Release */, + 331C80DD294CF71000263BE5 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC10F92044A3C60003C045 /* Debug */, + 33CC10FA2044A3C60003C045 /* Release */, + 338D0CE9231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC10FC2044A3C60003C045 /* Debug */, + 33CC10FD2044A3C60003C045 /* Release */, + 338D0CEA231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC111C2044C6BA0003C045 /* Debug */, + 33CC111D2044C6BA0003C045 /* Release */, + 338D0CEB231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 33CC10E52044A3C60003C045 /* Project object */; +} diff --git a/Mobile Commerce/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/Mobile Commerce/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/Mobile Commerce/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/Mobile Commerce/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/Mobile Commerce/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 0000000..de43c87 --- /dev/null +++ b/Mobile Commerce/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,99 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Mobile Commerce/macos/Runner.xcworkspace/contents.xcworkspacedata b/Mobile Commerce/macos/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..1d526a1 --- /dev/null +++ b/Mobile Commerce/macos/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/Mobile Commerce/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/Mobile Commerce/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/Mobile Commerce/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/Mobile Commerce/macos/Runner/AppDelegate.swift b/Mobile Commerce/macos/Runner/AppDelegate.swift new file mode 100644 index 0000000..b3c1761 --- /dev/null +++ b/Mobile Commerce/macos/Runner/AppDelegate.swift @@ -0,0 +1,13 @@ +import Cocoa +import FlutterMacOS + +@main +class AppDelegate: FlutterAppDelegate { + override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { + return true + } + + override func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool { + return true + } +} diff --git a/Mobile Commerce/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/Mobile Commerce/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..a2ec33f --- /dev/null +++ b/Mobile Commerce/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,68 @@ +{ + "images" : [ + { + "size" : "16x16", + "idiom" : "mac", + "filename" : "app_icon_16.png", + "scale" : "1x" + }, + { + "size" : "16x16", + "idiom" : "mac", + "filename" : "app_icon_32.png", + "scale" : "2x" + }, + { + "size" : "32x32", + "idiom" : "mac", + "filename" : "app_icon_32.png", + "scale" : "1x" + }, + { + "size" : "32x32", + "idiom" : "mac", + "filename" : "app_icon_64.png", + "scale" : "2x" + }, + { + "size" : "128x128", + "idiom" : "mac", + "filename" : "app_icon_128.png", + "scale" : "1x" + }, + { + "size" : "128x128", + "idiom" : "mac", + "filename" : "app_icon_256.png", + "scale" : "2x" + }, + { + "size" : "256x256", + "idiom" : "mac", + "filename" : "app_icon_256.png", + "scale" : "1x" + }, + { + "size" : "256x256", + "idiom" : "mac", + "filename" : "app_icon_512.png", + "scale" : "2x" + }, + { + "size" : "512x512", + "idiom" : "mac", + "filename" : "app_icon_512.png", + "scale" : "1x" + }, + { + "size" : "512x512", + "idiom" : "mac", + "filename" : "app_icon_1024.png", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/Mobile Commerce/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png b/Mobile Commerce/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png new file mode 100644 index 0000000..82b6f9d Binary files /dev/null and b/Mobile Commerce/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png differ diff --git a/Mobile Commerce/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png b/Mobile Commerce/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png new file mode 100644 index 0000000..13b35eb Binary files /dev/null and b/Mobile Commerce/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png differ diff --git a/Mobile Commerce/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png b/Mobile Commerce/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png new file mode 100644 index 0000000..0a3f5fa Binary files /dev/null and b/Mobile Commerce/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png differ diff --git a/Mobile Commerce/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png b/Mobile Commerce/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png new file mode 100644 index 0000000..bdb5722 Binary files /dev/null and b/Mobile Commerce/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png differ diff --git a/Mobile Commerce/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png b/Mobile Commerce/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png new file mode 100644 index 0000000..f083318 Binary files /dev/null and b/Mobile Commerce/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png differ diff --git a/Mobile Commerce/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png b/Mobile Commerce/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png new file mode 100644 index 0000000..326c0e7 Binary files /dev/null and b/Mobile Commerce/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png differ diff --git a/Mobile Commerce/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png b/Mobile Commerce/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png new file mode 100644 index 0000000..2f1632c Binary files /dev/null and b/Mobile Commerce/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png differ diff --git a/Mobile Commerce/macos/Runner/Base.lproj/MainMenu.xib b/Mobile Commerce/macos/Runner/Base.lproj/MainMenu.xib new file mode 100644 index 0000000..80e867a --- /dev/null +++ b/Mobile Commerce/macos/Runner/Base.lproj/MainMenu.xib @@ -0,0 +1,343 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Mobile Commerce/macos/Runner/Configs/AppInfo.xcconfig b/Mobile Commerce/macos/Runner/Configs/AppInfo.xcconfig new file mode 100644 index 0000000..4137eaf --- /dev/null +++ b/Mobile Commerce/macos/Runner/Configs/AppInfo.xcconfig @@ -0,0 +1,14 @@ +// Application-level settings for the Runner target. +// +// This may be replaced with something auto-generated from metadata (e.g., pubspec.yaml) in the +// future. If not, the values below would default to using the project name when this becomes a +// 'flutter create' template. + +// The application's name. By default this is also the title of the Flutter window. +PRODUCT_NAME = niogu_ecommerce_v1 + +// The application's bundle identifier +PRODUCT_BUNDLE_IDENTIFIER = com.example.nioguEcommerceV1 + +// The copyright displayed in application information +PRODUCT_COPYRIGHT = Copyright © 2026 com.example. All rights reserved. diff --git a/Mobile Commerce/macos/Runner/Configs/Debug.xcconfig b/Mobile Commerce/macos/Runner/Configs/Debug.xcconfig new file mode 100644 index 0000000..36b0fd9 --- /dev/null +++ b/Mobile Commerce/macos/Runner/Configs/Debug.xcconfig @@ -0,0 +1,2 @@ +#include "../../Flutter/Flutter-Debug.xcconfig" +#include "Warnings.xcconfig" diff --git a/Mobile Commerce/macos/Runner/Configs/Release.xcconfig b/Mobile Commerce/macos/Runner/Configs/Release.xcconfig new file mode 100644 index 0000000..dff4f49 --- /dev/null +++ b/Mobile Commerce/macos/Runner/Configs/Release.xcconfig @@ -0,0 +1,2 @@ +#include "../../Flutter/Flutter-Release.xcconfig" +#include "Warnings.xcconfig" diff --git a/Mobile Commerce/macos/Runner/Configs/Warnings.xcconfig b/Mobile Commerce/macos/Runner/Configs/Warnings.xcconfig new file mode 100644 index 0000000..42bcbf4 --- /dev/null +++ b/Mobile Commerce/macos/Runner/Configs/Warnings.xcconfig @@ -0,0 +1,13 @@ +WARNING_CFLAGS = -Wall -Wconditional-uninitialized -Wnullable-to-nonnull-conversion -Wmissing-method-return-type -Woverlength-strings +GCC_WARN_UNDECLARED_SELECTOR = YES +CLANG_UNDEFINED_BEHAVIOR_SANITIZER_NULLABILITY = YES +CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE +CLANG_WARN__DUPLICATE_METHOD_MATCH = YES +CLANG_WARN_PRAGMA_PACK = YES +CLANG_WARN_STRICT_PROTOTYPES = YES +CLANG_WARN_COMMA = YES +GCC_WARN_STRICT_SELECTOR_MATCH = YES +CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES +CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES +GCC_WARN_SHADOW = YES +CLANG_WARN_UNREACHABLE_CODE = YES diff --git a/Mobile Commerce/macos/Runner/DebugProfile.entitlements b/Mobile Commerce/macos/Runner/DebugProfile.entitlements new file mode 100644 index 0000000..dddb8a3 --- /dev/null +++ b/Mobile Commerce/macos/Runner/DebugProfile.entitlements @@ -0,0 +1,12 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.cs.allow-jit + + com.apple.security.network.server + + + diff --git a/Mobile Commerce/macos/Runner/Info.plist b/Mobile Commerce/macos/Runner/Info.plist new file mode 100644 index 0000000..4789daa --- /dev/null +++ b/Mobile Commerce/macos/Runner/Info.plist @@ -0,0 +1,32 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIconFile + + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSMinimumSystemVersion + $(MACOSX_DEPLOYMENT_TARGET) + NSHumanReadableCopyright + $(PRODUCT_COPYRIGHT) + NSMainNibFile + MainMenu + NSPrincipalClass + NSApplication + + diff --git a/Mobile Commerce/macos/Runner/MainFlutterWindow.swift b/Mobile Commerce/macos/Runner/MainFlutterWindow.swift new file mode 100644 index 0000000..3cc05eb --- /dev/null +++ b/Mobile Commerce/macos/Runner/MainFlutterWindow.swift @@ -0,0 +1,15 @@ +import Cocoa +import FlutterMacOS + +class MainFlutterWindow: NSWindow { + override func awakeFromNib() { + let flutterViewController = FlutterViewController() + let windowFrame = self.frame + self.contentViewController = flutterViewController + self.setFrame(windowFrame, display: true) + + RegisterGeneratedPlugins(registry: flutterViewController) + + super.awakeFromNib() + } +} diff --git a/Mobile Commerce/macos/Runner/Release.entitlements b/Mobile Commerce/macos/Runner/Release.entitlements new file mode 100644 index 0000000..852fa1a --- /dev/null +++ b/Mobile Commerce/macos/Runner/Release.entitlements @@ -0,0 +1,8 @@ + + + + + com.apple.security.app-sandbox + + + diff --git a/Mobile Commerce/macos/RunnerTests/RunnerTests.swift b/Mobile Commerce/macos/RunnerTests/RunnerTests.swift new file mode 100644 index 0000000..61f3bd1 --- /dev/null +++ b/Mobile Commerce/macos/RunnerTests/RunnerTests.swift @@ -0,0 +1,12 @@ +import Cocoa +import FlutterMacOS +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. + } + +} diff --git a/Mobile Commerce/pubspec.lock b/Mobile Commerce/pubspec.lock new file mode 100644 index 0000000..807814a --- /dev/null +++ b/Mobile Commerce/pubspec.lock @@ -0,0 +1,1359 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + _fe_analyzer_shared: + dependency: transitive + description: + name: _fe_analyzer_shared + sha256: "8d7ff3948166b8ec5da0fbb5962000926b8e02f2ed9b3e51d1738905fbd4c98d" + url: "https://pub.dev" + source: hosted + version: "93.0.0" + analyzer: + dependency: transitive + description: + name: analyzer + sha256: de7148ed2fcec579b19f122c1800933dfa028f6d9fd38a152b04b1516cec120b + url: "https://pub.dev" + source: hosted + version: "10.0.1" + args: + dependency: transitive + description: + name: args + sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04 + url: "https://pub.dev" + source: hosted + version: "2.7.0" + async: + dependency: transitive + description: + name: async + sha256: e2eb0491ba5ddb6177742d2da23904574082139b07c1e33b8503b9f46f3e1a37 + url: "https://pub.dev" + source: hosted + version: "2.13.1" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + build: + dependency: transitive + description: + name: build + sha256: aadd943f4f8cc946882c954c187e6115a84c98c81ad1d9c6cbf0895a8c85da9c + url: "https://pub.dev" + source: hosted + version: "4.0.5" + build_config: + dependency: transitive + description: + name: build_config + sha256: "4070d2a59f8eec34c97c86ceb44403834899075f66e8a9d59706f8e7834f6f71" + url: "https://pub.dev" + source: hosted + version: "1.3.0" + build_daemon: + dependency: transitive + description: + name: build_daemon + sha256: bf05f6e12cfea92d3c09308d7bcdab1906cd8a179b023269eed00c071004b957 + url: "https://pub.dev" + source: hosted + version: "4.1.1" + build_runner: + dependency: "direct dev" + description: + name: build_runner + sha256: "521daf8d189deb79ba474e43a696b41c49fb3987818dbacf3308f1e03673a75e" + url: "https://pub.dev" + source: hosted + version: "2.13.1" + built_collection: + dependency: transitive + description: + name: built_collection + sha256: "376e3dd27b51ea877c28d525560790aee2e6fbb5f20e2f85d5081027d94e2100" + url: "https://pub.dev" + source: hosted + version: "5.1.1" + built_value: + dependency: transitive + description: + name: built_value + sha256: "0730c18c770d05636a8f945c32a4d7d81cb6e0f0148c8db4ad12e7748f7e49af" + url: "https://pub.dev" + source: hosted + version: "8.12.5" + cached_network_image: + dependency: "direct main" + description: + name: cached_network_image + sha256: "7c1183e361e5c8b0a0f21a28401eecdbde252441106a9816400dd4c2b2424916" + url: "https://pub.dev" + source: hosted + version: "3.4.1" + cached_network_image_platform_interface: + dependency: transitive + description: + name: cached_network_image_platform_interface + sha256: "35814b016e37fbdc91f7ae18c8caf49ba5c88501813f73ce8a07027a395e2829" + url: "https://pub.dev" + source: hosted + version: "4.1.1" + cached_network_image_web: + dependency: transitive + description: + name: cached_network_image_web + sha256: "980842f4e8e2535b8dbd3d5ca0b1f0ba66bf61d14cc3a17a9b4788a3685ba062" + url: "https://pub.dev" + source: hosted + version: "1.3.1" + carousel_slider: + dependency: "direct main" + description: + name: carousel_slider + sha256: febf4b0163e0242adc13d7a863b04965351f59e7dfea56675c7c2caa7bcd7476 + url: "https://pub.dev" + source: hosted + version: "5.1.2" + characters: + dependency: transitive + description: + name: characters + sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 + url: "https://pub.dev" + source: hosted + version: "1.4.0" + checked_yaml: + dependency: transitive + description: + name: checked_yaml + sha256: "959525d3162f249993882720d52b7e0c833978df229be20702b33d48d91de70f" + url: "https://pub.dev" + source: hosted + version: "2.0.4" + clock: + dependency: transitive + description: + name: clock + sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b + url: "https://pub.dev" + source: hosted + version: "1.1.2" + code_builder: + dependency: transitive + description: + name: code_builder + sha256: "6a6cab2ba4680d6423f34a9b972a4c9a94ebe1b62ecec4e1a1f2cba91fd1319d" + url: "https://pub.dev" + source: hosted + version: "4.11.1" + collection: + dependency: transitive + description: + name: collection + sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" + url: "https://pub.dev" + source: hosted + version: "1.19.1" + convert: + dependency: transitive + description: + name: convert + sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68 + url: "https://pub.dev" + source: hosted + version: "3.1.2" + cross_file: + dependency: transitive + description: + name: cross_file + sha256: "28bb3ae56f117b5aec029d702a90f57d285cd975c3c5c281eaca38dbc47c5937" + url: "https://pub.dev" + source: hosted + version: "0.3.5+2" + crypto: + dependency: transitive + description: + name: crypto + sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf + url: "https://pub.dev" + source: hosted + version: "3.0.7" + cupertino_icons: + dependency: "direct main" + description: + name: cupertino_icons + sha256: "41e005c33bd814be4d3096aff55b1908d419fde52ca656c8c47719ec745873cd" + url: "https://pub.dev" + source: hosted + version: "1.0.9" + dart_earcut: + dependency: transitive + description: + name: dart_earcut + sha256: e485001bfc05dcbc437d7bfb666316182e3522d4c3f9668048e004d0eb2ce43b + url: "https://pub.dev" + source: hosted + version: "1.2.0" + dart_polylabel2: + dependency: transitive + description: + name: dart_polylabel2 + sha256: "7eeab15ce72894e4bdba6a8765712231fc81be0bd95247de4ad9966abc57adc6" + url: "https://pub.dev" + source: hosted + version: "1.0.0" + dart_style: + dependency: transitive + description: + name: dart_style + sha256: "29f7ecc274a86d32920b1d9cfc7502fa87220da41ec60b55f329559d5732e2b2" + url: "https://pub.dev" + source: hosted + version: "3.1.7" + dbus: + dependency: transitive + description: + name: dbus + sha256: d0c98dcd4f5169878b6cf8f6e0a52403a9dff371a3e2f019697accbf6f44a270 + url: "https://pub.dev" + source: hosted + version: "0.7.12" + dio: + dependency: "direct main" + description: + name: dio + sha256: aff32c08f92787a557dd5c0145ac91536481831a01b4648136373cddb0e64f8c + url: "https://pub.dev" + source: hosted + version: "5.9.2" + dio_web_adapter: + dependency: transitive + description: + name: dio_web_adapter + sha256: "2f9e64323a7c3c7ef69567d5c800424a11f8337b8b228bad02524c9fb3c1f340" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + fake_async: + dependency: transitive + description: + name: fake_async + sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" + url: "https://pub.dev" + source: hosted + version: "1.3.3" + ffi: + dependency: transitive + description: + name: ffi + sha256: "6d7fd89431262d8f3125e81b50d3847a091d846eafcd4fdb88dd06f36d705a45" + url: "https://pub.dev" + source: hosted + version: "2.2.0" + file: + dependency: transitive + description: + name: file + sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 + url: "https://pub.dev" + source: hosted + version: "7.0.1" + file_selector_linux: + dependency: transitive + description: + name: file_selector_linux + sha256: "2567f398e06ac72dcf2e98a0c95df2a9edd03c2c2e0cacd4780f20cdf56263a0" + url: "https://pub.dev" + source: hosted + version: "0.9.4" + file_selector_macos: + dependency: transitive + description: + name: file_selector_macos + sha256: "5e0bbe9c312416f1787a68259ea1505b52f258c587f12920422671807c4d618a" + url: "https://pub.dev" + source: hosted + version: "0.9.5" + file_selector_platform_interface: + dependency: transitive + description: + name: file_selector_platform_interface + sha256: "35e0bd61ebcdb91a3505813b055b09b79dfdc7d0aee9c09a7ba59ae4bb13dc85" + url: "https://pub.dev" + source: hosted + version: "2.7.0" + file_selector_windows: + dependency: transitive + description: + name: file_selector_windows + sha256: "62197474ae75893a62df75939c777763d39c2bc5f73ce5b88497208bc269abfd" + url: "https://pub.dev" + source: hosted + version: "0.9.3+5" + fixnum: + dependency: transitive + description: + name: fixnum + sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be + url: "https://pub.dev" + source: hosted + version: "1.1.1" + flutter: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_cache_manager: + dependency: transitive + description: + name: flutter_cache_manager + sha256: "400b6592f16a4409a7f2bb929a9a7e38c72cceb8ffb99ee57bbf2cb2cecf8386" + url: "https://pub.dev" + source: hosted + version: "3.4.1" + flutter_localizations: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_map: + dependency: "direct main" + description: + name: flutter_map + sha256: "391e7dc95cc3f5190748210a69d4cfeb5d8f84dcdfa9c3235d0a9d7742ccb3f8" + url: "https://pub.dev" + source: hosted + version: "8.2.2" + flutter_plugin_android_lifecycle: + dependency: transitive + description: + name: flutter_plugin_android_lifecycle + sha256: "38d1c268de9097ff59cf0e844ac38759fc78f76836d37edad06fa21e182055a0" + url: "https://pub.dev" + source: hosted + version: "2.0.34" + flutter_riverpod: + dependency: "direct main" + description: + name: flutter_riverpod + sha256: "9532ee6db4a943a1ed8383072a2e3eeda041db5657cdf6d2acecf3c21ecbe7e1" + url: "https://pub.dev" + source: hosted + version: "2.6.1" + flutter_secure_storage: + dependency: "direct main" + description: + name: flutter_secure_storage + sha256: da922f2aab2d733db7e011a6bcc4a825b844892d4edd6df83ff156b09a9b2e40 + url: "https://pub.dev" + source: hosted + version: "10.0.0" + flutter_secure_storage_darwin: + dependency: transitive + description: + name: flutter_secure_storage_darwin + sha256: "8878c25136a79def1668c75985e8e193d9d7d095453ec28730da0315dc69aee3" + url: "https://pub.dev" + source: hosted + version: "0.2.0" + flutter_secure_storage_linux: + dependency: transitive + description: + name: flutter_secure_storage_linux + sha256: "2b5c76dce569ab752d55a1cee6a2242bcc11fdba927078fb88c503f150767cda" + url: "https://pub.dev" + source: hosted + version: "3.0.0" + flutter_secure_storage_platform_interface: + dependency: transitive + description: + name: flutter_secure_storage_platform_interface + sha256: "8ceea1223bee3c6ac1a22dabd8feefc550e4729b3675de4b5900f55afcb435d6" + url: "https://pub.dev" + source: hosted + version: "2.0.1" + flutter_secure_storage_web: + dependency: transitive + description: + name: flutter_secure_storage_web + sha256: "6a1137df62b84b54261dca582c1c09ea72f4f9a4b2fcee21b025964132d5d0c3" + url: "https://pub.dev" + source: hosted + version: "2.1.0" + flutter_secure_storage_windows: + dependency: transitive + description: + name: flutter_secure_storage_windows + sha256: "3b7c8e068875dfd46719ff57c90d8c459c87f2302ed6b00ff006b3c9fcad1613" + url: "https://pub.dev" + source: hosted + version: "4.1.0" + flutter_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" + flutter_web_plugins: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + geoclue: + dependency: transitive + description: + name: geoclue + sha256: c2a998c77474fc57aa00c6baa2928e58f4b267649057a1c76738656e9dbd2a7f + url: "https://pub.dev" + source: hosted + version: "0.1.1" + geocoding: + dependency: "direct main" + description: + name: geocoding + sha256: "606be036287842d779d7ec4e2f6c9435fc29bbbd3c6da6589710f981d8852895" + url: "https://pub.dev" + source: hosted + version: "4.0.0" + geocoding_android: + dependency: transitive + description: + name: geocoding_android + sha256: ba810da90d6633cbb82bbab630e5b4a3b7d23503263c00ae7f1ef0316dcae5b9 + url: "https://pub.dev" + source: hosted + version: "4.0.1" + geocoding_ios: + dependency: transitive + description: + name: geocoding_ios + sha256: "18ab1c8369e2b0dcb3a8ccc907319334f35ee8cf4cfef4d9c8e23b13c65cb825" + url: "https://pub.dev" + source: hosted + version: "3.1.0" + geocoding_platform_interface: + dependency: transitive + description: + name: geocoding_platform_interface + sha256: "8c2c8226e5c276594c2e18bfe88b19110ed770aeb7c1ab50ede570be8b92229b" + url: "https://pub.dev" + source: hosted + version: "3.2.0" + geolocator: + dependency: "direct main" + description: + name: geolocator + sha256: "79939537046c9025be47ec645f35c8090ecadb6fe98eba146a0d25e8c1357516" + url: "https://pub.dev" + source: hosted + version: "14.0.2" + geolocator_android: + dependency: transitive + description: + name: geolocator_android + sha256: "179c3cb66dfa674fc9ccbf2be872a02658724d1c067634e2c427cf6df7df901a" + url: "https://pub.dev" + source: hosted + version: "5.0.2" + geolocator_apple: + dependency: transitive + description: + name: geolocator_apple + sha256: dbdd8789d5aaf14cf69f74d4925ad1336b4433a6efdf2fce91e8955dc921bf22 + url: "https://pub.dev" + source: hosted + version: "2.3.13" + geolocator_linux: + dependency: transitive + description: + name: geolocator_linux + sha256: d64112a205931926f4363bb6bd48f14cb38e7326833041d170615586cd143797 + url: "https://pub.dev" + source: hosted + version: "0.2.4" + geolocator_platform_interface: + dependency: transitive + description: + name: geolocator_platform_interface + sha256: "30cb64f0b9adcc0fb36f628b4ebf4f731a2961a0ebd849f4b56200205056fe67" + url: "https://pub.dev" + source: hosted + version: "4.2.6" + geolocator_web: + dependency: transitive + description: + name: geolocator_web + sha256: b1ae9bdfd90f861fde8fd4f209c37b953d65e92823cb73c7dee1fa021b06f172 + url: "https://pub.dev" + source: hosted + version: "4.1.3" + geolocator_windows: + dependency: transitive + description: + name: geolocator_windows + sha256: "175435404d20278ffd220de83c2ca293b73db95eafbdc8131fe8609be1421eb6" + url: "https://pub.dev" + source: hosted + version: "0.2.5" + glob: + dependency: transitive + description: + name: glob + sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de + url: "https://pub.dev" + source: hosted + version: "2.1.3" + go_router: + dependency: "direct main" + description: + name: go_router + sha256: d8f590a69729f719177ea68eb1e598295e8dbc41bbc247fed78b2c8a25660d7c + url: "https://pub.dev" + source: hosted + version: "16.3.0" + graphs: + dependency: transitive + description: + name: graphs + sha256: "741bbf84165310a68ff28fe9e727332eef1407342fca52759cb21ad8177bb8d0" + url: "https://pub.dev" + source: hosted + version: "2.3.2" + gsettings: + dependency: transitive + description: + name: gsettings + sha256: "1b0ce661f5436d2db1e51f3c4295a49849f03d304003a7ba177d01e3a858249c" + url: "https://pub.dev" + source: hosted + version: "0.2.8" + http: + dependency: transitive + description: + name: http + sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412" + url: "https://pub.dev" + source: hosted + version: "1.6.0" + http_multi_server: + dependency: transitive + description: + name: http_multi_server + sha256: aa6199f908078bb1c5efb8d8638d4ae191aac11b311132c3ef48ce352fb52ef8 + url: "https://pub.dev" + source: hosted + version: "3.2.2" + http_parser: + dependency: transitive + description: + name: http_parser + sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" + url: "https://pub.dev" + source: hosted + version: "4.1.2" + image_picker: + dependency: "direct main" + description: + name: image_picker + sha256: "784210112be18ea55f69d7076e2c656a4e24949fa9e76429fe53af0c0f4fa320" + url: "https://pub.dev" + source: hosted + version: "1.2.1" + image_picker_android: + dependency: transitive + description: + name: image_picker_android + sha256: "66810af8e99b2657ee98e5c6f02064f69bb63f7a70e343937f70946c5f8c6622" + url: "https://pub.dev" + source: hosted + version: "0.8.13+16" + image_picker_for_web: + dependency: transitive + description: + name: image_picker_for_web + sha256: "66257a3191ab360d23a55c8241c91a6e329d31e94efa7be9cf7a212e65850214" + url: "https://pub.dev" + source: hosted + version: "3.1.1" + image_picker_ios: + dependency: transitive + description: + name: image_picker_ios + sha256: b9c4a438a9ff4f60808c9cf0039b93a42bb6c2211ef6ebb647394b2b3fa84588 + url: "https://pub.dev" + source: hosted + version: "0.8.13+6" + image_picker_linux: + dependency: transitive + description: + name: image_picker_linux + sha256: "1f81c5f2046b9ab724f85523e4af65be1d47b038160a8c8deed909762c308ed4" + url: "https://pub.dev" + source: hosted + version: "0.2.2" + image_picker_macos: + dependency: transitive + description: + name: image_picker_macos + sha256: "86f0f15a309de7e1a552c12df9ce5b59fe927e71385329355aec4776c6a8ec91" + url: "https://pub.dev" + source: hosted + version: "0.2.2+1" + image_picker_platform_interface: + dependency: transitive + description: + name: image_picker_platform_interface + sha256: "567e056716333a1647c64bb6bd873cff7622233a5c3f694be28a583d4715690c" + url: "https://pub.dev" + source: hosted + version: "2.11.1" + image_picker_windows: + dependency: transitive + description: + name: image_picker_windows + sha256: d248c86554a72b5495a31c56f060cf73a41c7ff541689327b1a7dbccc33adfae + url: "https://pub.dev" + source: hosted + version: "0.2.2" + intl: + dependency: "direct main" + description: + name: intl + sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5" + url: "https://pub.dev" + source: hosted + version: "0.20.2" + io: + dependency: transitive + description: + name: io + sha256: dfd5a80599cf0165756e3181807ed3e77daf6dd4137caaad72d0b7931597650b + url: "https://pub.dev" + source: hosted + version: "1.0.5" + json_annotation: + dependency: "direct main" + description: + name: json_annotation + sha256: cb09e7dac6210041fad964ed7fbee004f14258b4eca4040f72d1234062ace4c8 + url: "https://pub.dev" + source: hosted + version: "4.11.0" + json_serializable: + dependency: "direct dev" + description: + name: json_serializable + sha256: fbcf404b03520e6e795f6b9b39badb2b788407dfc0a50cf39158a6ae1ca78925 + url: "https://pub.dev" + source: hosted + version: "6.13.1" + laravel_echo_null: + dependency: "direct main" + description: + name: laravel_echo_null + sha256: "73f303fae15d203ada127500a2b2708422e83fa6d36307b652b709e25fa7f1d7" + url: "https://pub.dev" + source: hosted + version: "0.2.0" + latlong2: + dependency: "direct main" + description: + name: latlong2 + sha256: "98227922caf49e6056f91b6c56945ea1c7b166f28ffcd5fb8e72fc0b453cc8fe" + url: "https://pub.dev" + source: hosted + version: "0.9.1" + leak_tracker: + dependency: transitive + description: + name: leak_tracker + sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de" + url: "https://pub.dev" + source: hosted + version: "11.0.2" + leak_tracker_flutter_testing: + dependency: transitive + description: + name: leak_tracker_flutter_testing + sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1" + url: "https://pub.dev" + source: hosted + version: "3.0.10" + leak_tracker_testing: + dependency: transitive + description: + name: leak_tracker_testing + sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1" + url: "https://pub.dev" + source: hosted + version: "3.0.2" + lists: + dependency: transitive + description: + name: lists + sha256: "4ca5c19ae4350de036a7e996cdd1ee39c93ac0a2b840f4915459b7d0a7d4ab27" + url: "https://pub.dev" + source: hosted + version: "1.0.1" + logger: + dependency: "direct main" + description: + name: logger + sha256: "25aee487596a6257655a1e091ec2ae66bc30e7af663592cc3a27e6591e05035c" + url: "https://pub.dev" + source: hosted + version: "2.7.0" + logging: + dependency: transitive + description: + name: logging + sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61 + url: "https://pub.dev" + source: hosted + version: "1.3.0" + matcher: + dependency: transitive + description: + name: matcher + sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 + url: "https://pub.dev" + source: hosted + version: "0.12.17" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec + url: "https://pub.dev" + source: hosted + version: "0.11.1" + meta: + dependency: transitive + description: + name: meta + sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" + url: "https://pub.dev" + source: hosted + version: "1.17.0" + mgrs_dart: + dependency: transitive + description: + name: mgrs_dart + sha256: fb89ae62f05fa0bb90f70c31fc870bcbcfd516c843fb554452ab3396f78586f7 + url: "https://pub.dev" + source: hosted + version: "2.0.0" + mime: + dependency: transitive + description: + name: mime + sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6" + url: "https://pub.dev" + source: hosted + version: "2.0.0" + octo_image: + dependency: transitive + description: + name: octo_image + sha256: "34faa6639a78c7e3cbe79be6f9f96535867e879748ade7d17c9b1ae7536293bd" + url: "https://pub.dev" + source: hosted + version: "2.1.0" + package_config: + dependency: transitive + description: + name: package_config + sha256: f096c55ebb7deb7e384101542bfba8c52696c1b56fca2eb62827989ef2353bbc + url: "https://pub.dev" + source: hosted + version: "2.2.0" + package_info_plus: + dependency: transitive + description: + name: package_info_plus + sha256: "468c26b4254ab01979fa5e4a98cb343ea3631b9acee6f21028997419a80e1a20" + url: "https://pub.dev" + source: hosted + version: "9.0.1" + package_info_plus_platform_interface: + dependency: transitive + description: + name: package_info_plus_platform_interface + sha256: "202a487f08836a592a6bd4f901ac69b3a8f146af552bbd14407b6b41e1c3f086" + url: "https://pub.dev" + source: hosted + version: "3.2.1" + path: + dependency: "direct main" + description: + name: path + sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" + url: "https://pub.dev" + source: hosted + version: "1.9.1" + path_provider: + dependency: transitive + description: + name: path_provider + sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd" + url: "https://pub.dev" + source: hosted + version: "2.1.5" + path_provider_android: + dependency: transitive + description: + name: path_provider_android + sha256: "149441ca6e4f38193b2e004c0ca6376a3d11f51fa5a77552d8bd4d2b0c0912ba" + url: "https://pub.dev" + source: hosted + version: "2.2.23" + path_provider_foundation: + dependency: transitive + description: + name: path_provider_foundation + sha256: "6d13aece7b3f5c5a9731eaf553ff9dcbc2eff41087fd2df587fd0fed9a3eb0c4" + url: "https://pub.dev" + source: hosted + version: "2.5.1" + path_provider_linux: + dependency: transitive + description: + name: path_provider_linux + sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 + url: "https://pub.dev" + source: hosted + version: "2.2.1" + path_provider_platform_interface: + dependency: transitive + description: + name: path_provider_platform_interface + sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + path_provider_windows: + dependency: transitive + description: + name: path_provider_windows + sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 + url: "https://pub.dev" + source: hosted + version: "2.3.0" + petitparser: + dependency: transitive + description: + name: petitparser + sha256: "91bd59303e9f769f108f8df05e371341b15d59e995e6806aefab827b58336675" + url: "https://pub.dev" + source: hosted + version: "7.0.2" + pinenacl: + dependency: transitive + description: + name: pinenacl + sha256: "57e907beaacbc3c024a098910b6240758e899674de07d6949a67b52fd984cbdf" + url: "https://pub.dev" + source: hosted + version: "0.6.0" + platform: + dependency: transitive + description: + name: platform + sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" + url: "https://pub.dev" + source: hosted + version: "3.1.6" + plugin_platform_interface: + dependency: transitive + description: + name: plugin_platform_interface + sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" + url: "https://pub.dev" + source: hosted + version: "2.1.8" + pool: + dependency: transitive + description: + name: pool + sha256: "978783255c543aa3586a1b3c21f6e9d720eb315376a915872c61ef8b5c20177d" + url: "https://pub.dev" + source: hosted + version: "1.5.2" + proj4dart: + dependency: transitive + description: + name: proj4dart + sha256: c8a659ac9b6864aa47c171e78d41bbe6f5e1d7bd790a5814249e6b68bc44324e + url: "https://pub.dev" + source: hosted + version: "2.1.0" + pub_semver: + dependency: transitive + description: + name: pub_semver + sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585" + url: "https://pub.dev" + source: hosted + version: "2.2.0" + pubspec_parse: + dependency: transitive + description: + name: pubspec_parse + sha256: "0560ba233314abbed0a48a2956f7f022cce7c3e1e73df540277da7544cad4082" + url: "https://pub.dev" + source: hosted + version: "1.5.0" + pusher_client_socket: + dependency: transitive + description: + name: pusher_client_socket + sha256: ad05f9419263c817ba27eaf96c1bd1c1916359f91edc18b5c67ac4acb0aa0145 + url: "https://pub.dev" + source: hosted + version: "0.0.7" + riverpod: + dependency: transitive + description: + name: riverpod + sha256: "59062512288d3056b2321804332a13ffdd1bf16df70dcc8e506e411280a72959" + url: "https://pub.dev" + source: hosted + version: "2.6.1" + rxdart: + dependency: transitive + description: + name: rxdart + sha256: "5c3004a4a8dbb94bd4bf5412a4def4acdaa12e12f269737a5751369e12d1a962" + url: "https://pub.dev" + source: hosted + version: "0.28.0" + shared_preferences: + dependency: "direct main" + description: + name: shared_preferences + sha256: c3025c5534b01739267eb7d76959bbc25a6d10f6988e1c2a3036940133dd10bf + url: "https://pub.dev" + source: hosted + version: "2.5.5" + shared_preferences_android: + dependency: transitive + description: + name: shared_preferences_android + sha256: e8d4762b1e2e8578fc4d0fd548cebf24afd24f49719c08974df92834565e2c53 + url: "https://pub.dev" + source: hosted + version: "2.4.23" + shared_preferences_foundation: + dependency: transitive + description: + name: shared_preferences_foundation + sha256: "4e7eaffc2b17ba398759f1151415869a34771ba11ebbccd1b0145472a619a64f" + url: "https://pub.dev" + source: hosted + version: "2.5.6" + shared_preferences_linux: + dependency: transitive + description: + name: shared_preferences_linux + sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + shared_preferences_platform_interface: + dependency: transitive + description: + name: shared_preferences_platform_interface + sha256: "649dc798a33931919ea356c4305c2d1f81619ea6e92244070b520187b5140ef9" + url: "https://pub.dev" + source: hosted + version: "2.4.2" + shared_preferences_web: + dependency: transitive + description: + name: shared_preferences_web + sha256: c49bd060261c9a3f0ff445892695d6212ff603ef3115edbb448509d407600019 + url: "https://pub.dev" + source: hosted + version: "2.4.3" + shared_preferences_windows: + dependency: transitive + description: + name: shared_preferences_windows + sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + shelf: + dependency: transitive + description: + name: shelf + sha256: e7dd780a7ffb623c57850b33f43309312fc863fb6aa3d276a754bb299839ef12 + url: "https://pub.dev" + source: hosted + version: "1.4.2" + shelf_web_socket: + dependency: transitive + description: + name: shelf_web_socket + sha256: "3632775c8e90d6c9712f883e633716432a27758216dfb61bd86a8321c0580925" + url: "https://pub.dev" + source: hosted + version: "3.0.0" + shimmer: + dependency: "direct main" + description: + name: shimmer + sha256: "5f88c883a22e9f9f299e5ba0e4f7e6054857224976a5d9f839d4ebdc94a14ac9" + url: "https://pub.dev" + source: hosted + version: "3.0.0" + sizer: + dependency: "direct main" + description: + name: sizer + sha256: "9963c89e4d30d7c2108de3eafc0a7e6a4a8009799376ea6be5ef0a9ad87cfbad" + url: "https://pub.dev" + source: hosted + version: "3.1.3" + sky_engine: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + socket_io_client: + dependency: transitive + description: + name: socket_io_client + sha256: ef6c989e5eee8d04baf18482ec3d7699b91bc41e279794a99d8e3bef897b074a + url: "https://pub.dev" + source: hosted + version: "3.1.4" + socket_io_common: + dependency: transitive + description: + name: socket_io_common + sha256: "162fbaecbf4bf9a9372a62a341b3550b51dcef2f02f3e5830a297fd48203d45b" + url: "https://pub.dev" + source: hosted + version: "3.1.1" + source_gen: + dependency: transitive + description: + name: source_gen + sha256: "732792cfd197d2161a65bb029606a46e0a18ff30ef9e141a7a82172b05ea8ecd" + url: "https://pub.dev" + source: hosted + version: "4.2.2" + source_helper: + dependency: transitive + description: + name: source_helper + sha256: "1d3b229b2934034fb2e691fbb3d53e0f75a4af7b1407f88425ed8f209bcb1b8f" + url: "https://pub.dev" + source: hosted + version: "1.3.11" + source_span: + dependency: transitive + description: + name: source_span + sha256: "56a02f1f4cd1a2d96303c0144c93bd6d909eea6bee6bf5a0e0b685edbd4c47ab" + url: "https://pub.dev" + source: hosted + version: "1.10.2" + sqflite: + dependency: transitive + description: + name: sqflite + sha256: e2297b1da52f127bc7a3da11439985d9b536f75070f3325e62ada69a5c585d03 + url: "https://pub.dev" + source: hosted + version: "2.4.2" + sqflite_android: + dependency: transitive + description: + name: sqflite_android + sha256: "881e28efdcc9950fd8e9bb42713dcf1103e62a2e7168f23c9338d82db13dec40" + url: "https://pub.dev" + source: hosted + version: "2.4.2+3" + sqflite_common: + dependency: transitive + description: + name: sqflite_common + sha256: "6ef422a4525ecc601db6c0a2233ff448c731307906e92cabc9ba292afaae16a6" + url: "https://pub.dev" + source: hosted + version: "2.5.6" + sqflite_darwin: + dependency: transitive + description: + name: sqflite_darwin + sha256: "279832e5cde3fe99e8571879498c9211f3ca6391b0d818df4e17d9fff5c6ccb3" + url: "https://pub.dev" + source: hosted + version: "2.4.2" + sqflite_platform_interface: + dependency: transitive + description: + name: sqflite_platform_interface + sha256: "8dd4515c7bdcae0a785b0062859336de775e8c65db81ae33dd5445f35be61920" + url: "https://pub.dev" + source: hosted + version: "2.4.0" + stack_trace: + dependency: transitive + description: + name: stack_trace + sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" + url: "https://pub.dev" + source: hosted + version: "1.12.1" + state_notifier: + dependency: transitive + description: + name: state_notifier + sha256: b8677376aa54f2d7c58280d5a007f9e8774f1968d1fb1c096adcb4792fba29bb + url: "https://pub.dev" + source: hosted + version: "1.0.0" + stream_channel: + dependency: transitive + description: + name: stream_channel + sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + stream_transform: + dependency: transitive + description: + name: stream_transform + sha256: ad47125e588cfd37a9a7f86c7d6356dde8dfe89d071d293f80ca9e9273a33871 + url: "https://pub.dev" + source: hosted + version: "2.1.1" + string_scanner: + dependency: transitive + description: + name: string_scanner + sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" + url: "https://pub.dev" + source: hosted + version: "1.4.1" + synchronized: + dependency: transitive + description: + name: synchronized + sha256: c254ade258ec8282947a0acbbc90b9575b4f19673533ee46f2f6e9b3aeefd7c0 + url: "https://pub.dev" + source: hosted + version: "3.4.0" + term_glyph: + dependency: transitive + description: + name: term_glyph + sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" + url: "https://pub.dev" + source: hosted + version: "1.2.2" + test_api: + dependency: transitive + description: + name: test_api + sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55 + url: "https://pub.dev" + source: hosted + version: "0.7.7" + typed_data: + dependency: transitive + description: + name: typed_data + sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 + url: "https://pub.dev" + source: hosted + version: "1.4.0" + unicode: + dependency: transitive + description: + name: unicode + sha256: "0f69e46593d65245774d4f17125c6084d2c20b4e473a983f6e21b7d7762218f1" + url: "https://pub.dev" + source: hosted + version: "0.3.1" + url_launcher: + dependency: "direct main" + description: + name: url_launcher + sha256: f6a7e5c4835bb4e3026a04793a4199ca2d14c739ec378fdfe23fc8075d0439f8 + url: "https://pub.dev" + source: hosted + version: "6.3.2" + url_launcher_android: + dependency: transitive + description: + name: url_launcher_android + sha256: "3bb000251e55d4a209aa0e2e563309dc9bb2befea2295fd0cec1f51760aac572" + url: "https://pub.dev" + source: hosted + version: "6.3.29" + url_launcher_ios: + dependency: transitive + description: + name: url_launcher_ios + sha256: "580fe5dfb51671ae38191d316e027f6b76272b026370708c2d898799750a02b0" + url: "https://pub.dev" + source: hosted + version: "6.4.1" + url_launcher_linux: + dependency: transitive + description: + name: url_launcher_linux + sha256: d5e14138b3bc193a0f63c10a53c94b91d399df0512b1f29b94a043db7482384a + url: "https://pub.dev" + source: hosted + version: "3.2.2" + url_launcher_macos: + dependency: transitive + description: + name: url_launcher_macos + sha256: "368adf46f71ad3c21b8f06614adb38346f193f3a59ba8fe9a2fd74133070ba18" + url: "https://pub.dev" + source: hosted + version: "3.2.5" + url_launcher_platform_interface: + dependency: transitive + description: + name: url_launcher_platform_interface + sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029" + url: "https://pub.dev" + source: hosted + version: "2.3.2" + url_launcher_web: + dependency: transitive + description: + name: url_launcher_web + sha256: d0412fcf4c6b31ecfdb7762359b7206ffba3bbffd396c6d9f9c4616ece476c1f + url: "https://pub.dev" + source: hosted + version: "2.4.2" + url_launcher_windows: + dependency: transitive + description: + name: url_launcher_windows + sha256: "712c70ab1b99744ff066053cbe3e80c73332b38d46e5e945c98689b2e66fc15f" + url: "https://pub.dev" + source: hosted + version: "3.1.5" + uuid: + dependency: "direct main" + description: + name: uuid + sha256: "1fef9e8e11e2991bb773070d4656b7bd5d850967a2456cfc83cf47925ba79489" + url: "https://pub.dev" + source: hosted + version: "4.5.3" + vector_math: + dependency: transitive + description: + name: vector_math + sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b + url: "https://pub.dev" + source: hosted + version: "2.2.0" + vm_service: + dependency: transitive + description: + name: vm_service + sha256: "45caa6c5917fa127b5dbcfbd1fa60b14e583afdc08bfc96dda38886ca252eb60" + url: "https://pub.dev" + source: hosted + version: "15.0.2" + watcher: + dependency: transitive + description: + name: watcher + sha256: "1398c9f081a753f9226febe8900fce8f7d0a67163334e1c94a2438339d79d635" + url: "https://pub.dev" + source: hosted + version: "1.2.1" + web: + dependency: transitive + description: + name: web + sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" + url: "https://pub.dev" + source: hosted + version: "1.1.1" + web_socket: + dependency: transitive + description: + name: web_socket + sha256: "34d64019aa8e36bf9842ac014bb5d2f5586ca73df5e4d9bf5c936975cae6982c" + url: "https://pub.dev" + source: hosted + version: "1.0.1" + web_socket_channel: + dependency: transitive + description: + name: web_socket_channel + sha256: d645757fb0f4773d602444000a8131ff5d48c9e47adfe9772652dd1a4f2d45c8 + url: "https://pub.dev" + source: hosted + version: "3.0.3" + web_socket_client: + dependency: transitive + description: + name: web_socket_client + sha256: "394789177aa3bc1b7b071622a1dbf52a4631d7ce23c555c39bb2523e92316b07" + url: "https://pub.dev" + source: hosted + version: "0.2.1" + win32: + dependency: transitive + description: + name: win32 + sha256: d7cb55e04cd34096cd3a79b3330245f54cb96a370a1c27adb3c84b917de8b08e + url: "https://pub.dev" + source: hosted + version: "5.15.0" + wkt_parser: + dependency: transitive + description: + name: wkt_parser + sha256: "8a555fc60de3116c00aad67891bcab20f81a958e4219cc106e3c037aa3937f13" + url: "https://pub.dev" + source: hosted + version: "2.0.0" + xdg_directories: + dependency: transitive + description: + name: xdg_directories + sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15" + url: "https://pub.dev" + source: hosted + version: "1.1.0" + xml: + dependency: transitive + description: + name: xml + sha256: "971043b3a0d3da28727e40ed3e0b5d18b742fa5a68665cca88e74b7876d5e025" + url: "https://pub.dev" + source: hosted + version: "6.6.1" + yaml: + dependency: transitive + description: + name: yaml + sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce + url: "https://pub.dev" + source: hosted + version: "3.1.3" +sdks: + dart: ">=3.10.1 <4.0.0" + flutter: ">=3.38.0" diff --git a/Mobile Commerce/pubspec.yaml b/Mobile Commerce/pubspec.yaml new file mode 100644 index 0000000..01842fd --- /dev/null +++ b/Mobile Commerce/pubspec.yaml @@ -0,0 +1,101 @@ +name: niogu_ecommerce_v1 +description: "A new Flutter project." +# The following line prevents the package from being accidentally published to +# pub.dev using `flutter pub publish`. This is preferred for private packages. +publish_to: "none" # Remove this line if you wish to publish to pub.dev + +# The following defines the version and build number for your application. +# A version number is three numbers separated by dots, like 1.2.43 +# followed by an optional build number separated by a +. +# Both the version and the builder number may be overridden in flutter +# build by specifying --build-name and --build-number, respectively. +# In Android, build-name is used as versionName while build-number used as versionCode. +# Read more about Android versioning at https://developer.android.com/studio/publish/versioning +# In iOS, build-name is used as CFBundleShortVersionString while build-number is used as CFBundleVersion. +# Read more about iOS versioning at +# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html +# In Windows, build-name is used as the major, minor, and patch parts +# of the product and file versions while build-number is used as the build suffix. +version: 1.0.0+1 + +environment: + sdk: ^3.10.1 + +# Dependencies specify other packages that your package needs in order to work. +# To automatically upgrade your package dependencies to the latest versions +# consider running `flutter pub upgrade --major-versions`. Alternatively, +# dependencies can be manually updated by changing the version numbers below to +# the latest version available on pub.dev. To see which dependencies have newer +# versions available, run `flutter pub outdated`. +dependencies: + flutter: + sdk: flutter + + # The following adds the Cupertino Icons font to your application. + # Use with the CupertinoIcons class for iOS style icons. + cupertino_icons: ^1.0.8 + sizer: ^3.1.3 + carousel_slider: ^5.1.2 + go_router: ^16.1.0 + intl: ^0.20.2 + flutter_riverpod: ^2.6.1 + flutter_localizations: + sdk: flutter + logger: ^2.6.2 + cached_network_image: ^3.4.1 + dio: ^5.9.2 + json_annotation: ^4.11.0 + shared_preferences: ^2.5.4 + flutter_secure_storage: ^10.0.0 + image_picker: ^1.2.1 + path: ^1.9.0 + flutter_map: ^8.2.2 + latlong2: ^0.9.1 + geolocator: ^14.0.2 + geocoding: ^4.0.0 + shimmer: ^3.0.0 + uuid: ^4.5.2 + laravel_echo_null: ^0.2.0 + url_launcher: ^6.3.2 + +dev_dependencies: + flutter_test: + sdk: flutter + + # For information on the generic Dart part of this file, see the + # following page: https://dart.dev/tools/pub/pubspec + build_runner: ^2.4.15 + json_serializable: ^6.13.1 + +# The following section is specific to Flutter packages. +flutter: + # The following line ensures that the Material Icons font is + # included with your application, so that you can use the icons in + # the material Icons class. + uses-material-design: true + + # To add assets to your application, add an assets section, like this: + assets: + - assets/gifs/ + - assets/images/ + + # An image asset can refer to one or more resolution-specific "variants", see + # https://flutter.dev/to/resolution-aware-images + + # For details regarding adding assets from package dependencies, see + # https://flutter.dev/to/asset-from-package + + # To add custom fonts to your application, add a fonts section here, + # in this "flutter" section. Each entry in this list should have a + # "family" key with the font family name, and a "fonts" key with a + # list giving the asset and other descriptors for the font. For + # example: + fonts: + - family: Poppins + fonts: + - asset: assets/fonts/Poppins-Regular.ttf + - asset: assets/fonts/Poppins-Bold.ttf + weight: 700 + # + # For details regarding fonts from package dependencies, + # see https://flutter.dev/to/font-from-package diff --git a/Mobile Commerce/web/favicon.png b/Mobile Commerce/web/favicon.png new file mode 100644 index 0000000..8aaa46a Binary files /dev/null and b/Mobile Commerce/web/favicon.png differ diff --git a/Mobile Commerce/web/icons/Icon-192.png b/Mobile Commerce/web/icons/Icon-192.png new file mode 100644 index 0000000..b749bfe Binary files /dev/null and b/Mobile Commerce/web/icons/Icon-192.png differ diff --git a/Mobile Commerce/web/icons/Icon-512.png b/Mobile Commerce/web/icons/Icon-512.png new file mode 100644 index 0000000..88cfd48 Binary files /dev/null and b/Mobile Commerce/web/icons/Icon-512.png differ diff --git a/Mobile Commerce/web/icons/Icon-maskable-192.png b/Mobile Commerce/web/icons/Icon-maskable-192.png new file mode 100644 index 0000000..eb9b4d7 Binary files /dev/null and b/Mobile Commerce/web/icons/Icon-maskable-192.png differ diff --git a/Mobile Commerce/web/icons/Icon-maskable-512.png b/Mobile Commerce/web/icons/Icon-maskable-512.png new file mode 100644 index 0000000..d69c566 Binary files /dev/null and b/Mobile Commerce/web/icons/Icon-maskable-512.png differ diff --git a/Mobile Commerce/web/index.html b/Mobile Commerce/web/index.html new file mode 100644 index 0000000..1ad9eda --- /dev/null +++ b/Mobile Commerce/web/index.html @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + niogu_ecommerce_v1 + + + + + + diff --git a/Mobile Commerce/web/manifest.json b/Mobile Commerce/web/manifest.json new file mode 100644 index 0000000..8ca778a --- /dev/null +++ b/Mobile Commerce/web/manifest.json @@ -0,0 +1,35 @@ +{ + "name": "niogu_ecommerce_v1", + "short_name": "niogu_ecommerce_v1", + "start_url": ".", + "display": "standalone", + "background_color": "#0175C2", + "theme_color": "#0175C2", + "description": "A new Flutter project.", + "orientation": "portrait-primary", + "prefer_related_applications": false, + "icons": [ + { + "src": "icons/Icon-192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "icons/Icon-512.png", + "sizes": "512x512", + "type": "image/png" + }, + { + "src": "icons/Icon-maskable-192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "maskable" + }, + { + "src": "icons/Icon-maskable-512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "maskable" + } + ] +} diff --git a/Mobile Commerce/windows/.gitignore b/Mobile Commerce/windows/.gitignore new file mode 100644 index 0000000..d492d0d --- /dev/null +++ b/Mobile Commerce/windows/.gitignore @@ -0,0 +1,17 @@ +flutter/ephemeral/ + +# Visual Studio user-specific files. +*.suo +*.user +*.userosscache +*.sln.docstates + +# Visual Studio build-related files. +x64/ +x86/ + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ diff --git a/Mobile Commerce/windows/CMakeLists.txt b/Mobile Commerce/windows/CMakeLists.txt new file mode 100644 index 0000000..1965553 --- /dev/null +++ b/Mobile Commerce/windows/CMakeLists.txt @@ -0,0 +1,108 @@ +# Project-level configuration. +cmake_minimum_required(VERSION 3.14) +project(niogu_ecommerce_v1 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 "niogu_ecommerce_v1") + +# Explicitly opt in to modern CMake behaviors to avoid warnings with recent +# versions of CMake. +cmake_policy(VERSION 3.14...3.25) + +# Define build configuration option. +get_property(IS_MULTICONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG) +if(IS_MULTICONFIG) + set(CMAKE_CONFIGURATION_TYPES "Debug;Profile;Release" + CACHE STRING "" FORCE) +else() + 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() +endif() +# Define settings for the Profile build mode. +set(CMAKE_EXE_LINKER_FLAGS_PROFILE "${CMAKE_EXE_LINKER_FLAGS_RELEASE}") +set(CMAKE_SHARED_LINKER_FLAGS_PROFILE "${CMAKE_SHARED_LINKER_FLAGS_RELEASE}") +set(CMAKE_C_FLAGS_PROFILE "${CMAKE_C_FLAGS_RELEASE}") +set(CMAKE_CXX_FLAGS_PROFILE "${CMAKE_CXX_FLAGS_RELEASE}") + +# Use Unicode for all projects. +add_definitions(-DUNICODE -D_UNICODE) + +# 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_17) + target_compile_options(${TARGET} PRIVATE /W4 /WX /wd"4100") + target_compile_options(${TARGET} PRIVATE /EHsc) + target_compile_definitions(${TARGET} PRIVATE "_HAS_EXCEPTIONS=0") + target_compile_definitions(${TARGET} PRIVATE "$<$:_DEBUG>") +endfunction() + +# Flutter library and tool build rules. +set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") +add_subdirectory(${FLUTTER_MANAGED_DIR}) + +# Application build; see runner/CMakeLists.txt. +add_subdirectory("runner") + + +# Generated plugin build rules, which manage building the plugins and adding +# them to the application. +include(flutter/generated_plugins.cmake) + + +# === Installation === +# Support files are copied into place next to the executable, so that it can +# run in place. This is done instead of making a separate bundle (as on Linux) +# so that building and running from within Visual Studio will work. +set(BUILD_BUNDLE_DIR "$") +# Make the "install" step default, as it's required to run. +set(CMAKE_VS_INCLUDE_INSTALL_TO_DEFAULT_BUILD 1) +if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) + set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) +endif() + +set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") +set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}") + +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) + +if(PLUGIN_BUNDLED_LIBRARIES) + install(FILES "${PLUGIN_BUNDLED_LIBRARIES}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endif() + +# Copy the native assets provided by the build.dart from all packages. +set(NATIVE_ASSETS_DIR "${PROJECT_BUILD_DIR}native_assets/windows/") +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. +install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + CONFIGURATIONS Profile;Release + COMPONENT Runtime) diff --git a/Mobile Commerce/windows/flutter/CMakeLists.txt b/Mobile Commerce/windows/flutter/CMakeLists.txt new file mode 100644 index 0000000..903f489 --- /dev/null +++ b/Mobile Commerce/windows/flutter/CMakeLists.txt @@ -0,0 +1,109 @@ +# This file controls Flutter-level build steps. It should not be edited. +cmake_minimum_required(VERSION 3.14) + +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. +set(WRAPPER_ROOT "${EPHEMERAL_DIR}/cpp_client_wrapper") + +# Set fallback configurations for older versions of the flutter tool. +if (NOT DEFINED FLUTTER_TARGET_PLATFORM) + set(FLUTTER_TARGET_PLATFORM "windows-x64") +endif() + +# === Flutter Library === +set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/flutter_windows.dll") + +# 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/windows/app.so" PARENT_SCOPE) + +list(APPEND FLUTTER_LIBRARY_HEADERS + "flutter_export.h" + "flutter_windows.h" + "flutter_messenger.h" + "flutter_plugin_registrar.h" + "flutter_texture_registrar.h" +) +list(TRANSFORM FLUTTER_LIBRARY_HEADERS PREPEND "${EPHEMERAL_DIR}/") +add_library(flutter INTERFACE) +target_include_directories(flutter INTERFACE + "${EPHEMERAL_DIR}" +) +target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}.lib") +add_dependencies(flutter flutter_assemble) + +# === Wrapper === +list(APPEND CPP_WRAPPER_SOURCES_CORE + "core_implementations.cc" + "standard_codec.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_CORE PREPEND "${WRAPPER_ROOT}/") +list(APPEND CPP_WRAPPER_SOURCES_PLUGIN + "plugin_registrar.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_PLUGIN PREPEND "${WRAPPER_ROOT}/") +list(APPEND CPP_WRAPPER_SOURCES_APP + "flutter_engine.cc" + "flutter_view_controller.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_APP PREPEND "${WRAPPER_ROOT}/") + +# Wrapper sources needed for a plugin. +add_library(flutter_wrapper_plugin STATIC + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_PLUGIN} +) +apply_standard_settings(flutter_wrapper_plugin) +set_target_properties(flutter_wrapper_plugin PROPERTIES + POSITION_INDEPENDENT_CODE ON) +set_target_properties(flutter_wrapper_plugin PROPERTIES + CXX_VISIBILITY_PRESET hidden) +target_link_libraries(flutter_wrapper_plugin PUBLIC flutter) +target_include_directories(flutter_wrapper_plugin PUBLIC + "${WRAPPER_ROOT}/include" +) +add_dependencies(flutter_wrapper_plugin flutter_assemble) + +# Wrapper sources needed for the runner. +add_library(flutter_wrapper_app STATIC + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_APP} +) +apply_standard_settings(flutter_wrapper_app) +target_link_libraries(flutter_wrapper_app PUBLIC flutter) +target_include_directories(flutter_wrapper_app PUBLIC + "${WRAPPER_ROOT}/include" +) +add_dependencies(flutter_wrapper_app 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. +set(PHONY_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/_phony_") +set_source_files_properties("${PHONY_OUTPUT}" PROPERTIES SYMBOLIC TRUE) +add_custom_command( + OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} + ${CPP_WRAPPER_SOURCES_CORE} ${CPP_WRAPPER_SOURCES_PLUGIN} + ${CPP_WRAPPER_SOURCES_APP} + ${PHONY_OUTPUT} + COMMAND ${CMAKE_COMMAND} -E env + ${FLUTTER_TOOL_ENVIRONMENT} + "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat" + ${FLUTTER_TARGET_PLATFORM} $ + VERBATIM +) +add_custom_target(flutter_assemble DEPENDS + "${FLUTTER_LIBRARY}" + ${FLUTTER_LIBRARY_HEADERS} + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_PLUGIN} + ${CPP_WRAPPER_SOURCES_APP} +) diff --git a/Mobile Commerce/windows/flutter/generated_plugin_registrant.cc b/Mobile Commerce/windows/flutter/generated_plugin_registrant.cc new file mode 100644 index 0000000..b51ed9b --- /dev/null +++ b/Mobile Commerce/windows/flutter/generated_plugin_registrant.cc @@ -0,0 +1,23 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#include "generated_plugin_registrant.h" + +#include +#include +#include +#include + +void RegisterPlugins(flutter::PluginRegistry* registry) { + FileSelectorWindowsRegisterWithRegistrar( + registry->GetRegistrarForPlugin("FileSelectorWindows")); + FlutterSecureStorageWindowsPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("FlutterSecureStorageWindowsPlugin")); + GeolocatorWindowsRegisterWithRegistrar( + registry->GetRegistrarForPlugin("GeolocatorWindows")); + UrlLauncherWindowsRegisterWithRegistrar( + registry->GetRegistrarForPlugin("UrlLauncherWindows")); +} diff --git a/Mobile Commerce/windows/flutter/generated_plugin_registrant.h b/Mobile Commerce/windows/flutter/generated_plugin_registrant.h new file mode 100644 index 0000000..dc139d8 --- /dev/null +++ b/Mobile Commerce/windows/flutter/generated_plugin_registrant.h @@ -0,0 +1,15 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#ifndef GENERATED_PLUGIN_REGISTRANT_ +#define GENERATED_PLUGIN_REGISTRANT_ + +#include + +// Registers Flutter plugins. +void RegisterPlugins(flutter::PluginRegistry* registry); + +#endif // GENERATED_PLUGIN_REGISTRANT_ diff --git a/Mobile Commerce/windows/flutter/generated_plugins.cmake b/Mobile Commerce/windows/flutter/generated_plugins.cmake new file mode 100644 index 0000000..2ab2c6a --- /dev/null +++ b/Mobile Commerce/windows/flutter/generated_plugins.cmake @@ -0,0 +1,27 @@ +# +# Generated file, do not edit. +# + +list(APPEND FLUTTER_PLUGIN_LIST + file_selector_windows + flutter_secure_storage_windows + geolocator_windows + url_launcher_windows +) + +list(APPEND FLUTTER_FFI_PLUGIN_LIST +) + +set(PLUGIN_BUNDLED_LIBRARIES) + +foreach(plugin ${FLUTTER_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/windows plugins/${plugin}) + target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) + list(APPEND PLUGIN_BUNDLED_LIBRARIES $) + 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}/windows plugins/${ffi_plugin}) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) +endforeach(ffi_plugin) diff --git a/Mobile Commerce/windows/runner/CMakeLists.txt b/Mobile Commerce/windows/runner/CMakeLists.txt new file mode 100644 index 0000000..394917c --- /dev/null +++ b/Mobile Commerce/windows/runner/CMakeLists.txt @@ -0,0 +1,40 @@ +cmake_minimum_required(VERSION 3.14) +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} WIN32 + "flutter_window.cpp" + "main.cpp" + "utils.cpp" + "win32_window.cpp" + "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" + "Runner.rc" + "runner.exe.manifest" +) + +# 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 build version. +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION=\"${FLUTTER_VERSION}\"") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MAJOR=${FLUTTER_VERSION_MAJOR}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MINOR=${FLUTTER_VERSION_MINOR}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_PATCH=${FLUTTER_VERSION_PATCH}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_BUILD=${FLUTTER_VERSION_BUILD}") + +# Disable Windows macros that collide with C++ standard library functions. +target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX") + +# Add dependency libraries and include directories. Add any application-specific +# dependencies here. +target_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app) +target_link_libraries(${BINARY_NAME} PRIVATE "dwmapi.lib") +target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}") + +# Run the Flutter tool portions of the build. This must not be removed. +add_dependencies(${BINARY_NAME} flutter_assemble) diff --git a/Mobile Commerce/windows/runner/Runner.rc b/Mobile Commerce/windows/runner/Runner.rc new file mode 100644 index 0000000..caeba63 --- /dev/null +++ b/Mobile Commerce/windows/runner/Runner.rc @@ -0,0 +1,121 @@ +// Microsoft Visual C++ generated resource script. +// +#pragma code_page(65001) +#include "resource.h" + +#define APSTUDIO_READONLY_SYMBOLS +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 2 resource. +// +#include "winres.h" + +///////////////////////////////////////////////////////////////////////////// +#undef APSTUDIO_READONLY_SYMBOLS + +///////////////////////////////////////////////////////////////////////////// +// English (United States) resources + +#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) +LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US + +#ifdef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// TEXTINCLUDE +// + +1 TEXTINCLUDE +BEGIN + "resource.h\0" +END + +2 TEXTINCLUDE +BEGIN + "#include ""winres.h""\r\n" + "\0" +END + +3 TEXTINCLUDE +BEGIN + "\r\n" + "\0" +END + +#endif // APSTUDIO_INVOKED + + +///////////////////////////////////////////////////////////////////////////// +// +// Icon +// + +// Icon with lowest ID value placed first to ensure application icon +// remains consistent on all systems. +IDI_APP_ICON ICON "resources\\app_icon.ico" + + +///////////////////////////////////////////////////////////////////////////// +// +// Version +// + +#if defined(FLUTTER_VERSION_MAJOR) && defined(FLUTTER_VERSION_MINOR) && defined(FLUTTER_VERSION_PATCH) && defined(FLUTTER_VERSION_BUILD) +#define VERSION_AS_NUMBER FLUTTER_VERSION_MAJOR,FLUTTER_VERSION_MINOR,FLUTTER_VERSION_PATCH,FLUTTER_VERSION_BUILD +#else +#define VERSION_AS_NUMBER 1,0,0,0 +#endif + +#if defined(FLUTTER_VERSION) +#define VERSION_AS_STRING FLUTTER_VERSION +#else +#define VERSION_AS_STRING "1.0.0" +#endif + +VS_VERSION_INFO VERSIONINFO + FILEVERSION VERSION_AS_NUMBER + PRODUCTVERSION VERSION_AS_NUMBER + FILEFLAGSMASK VS_FFI_FILEFLAGSMASK +#ifdef _DEBUG + FILEFLAGS VS_FF_DEBUG +#else + FILEFLAGS 0x0L +#endif + FILEOS VOS__WINDOWS32 + FILETYPE VFT_APP + FILESUBTYPE 0x0L +BEGIN + BLOCK "StringFileInfo" + BEGIN + BLOCK "040904e4" + BEGIN + VALUE "CompanyName", "com.example" "\0" + VALUE "FileDescription", "niogu_ecommerce_v1" "\0" + VALUE "FileVersion", VERSION_AS_STRING "\0" + VALUE "InternalName", "niogu_ecommerce_v1" "\0" + VALUE "LegalCopyright", "Copyright (C) 2026 com.example. All rights reserved." "\0" + VALUE "OriginalFilename", "niogu_ecommerce_v1.exe" "\0" + VALUE "ProductName", "niogu_ecommerce_v1" "\0" + VALUE "ProductVersion", VERSION_AS_STRING "\0" + END + END + BLOCK "VarFileInfo" + BEGIN + VALUE "Translation", 0x409, 1252 + END +END + +#endif // English (United States) resources +///////////////////////////////////////////////////////////////////////////// + + + +#ifndef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 3 resource. +// + + +///////////////////////////////////////////////////////////////////////////// +#endif // not APSTUDIO_INVOKED diff --git a/Mobile Commerce/windows/runner/flutter_window.cpp b/Mobile Commerce/windows/runner/flutter_window.cpp new file mode 100644 index 0000000..955ee30 --- /dev/null +++ b/Mobile Commerce/windows/runner/flutter_window.cpp @@ -0,0 +1,71 @@ +#include "flutter_window.h" + +#include + +#include "flutter/generated_plugin_registrant.h" + +FlutterWindow::FlutterWindow(const flutter::DartProject& project) + : project_(project) {} + +FlutterWindow::~FlutterWindow() {} + +bool FlutterWindow::OnCreate() { + if (!Win32Window::OnCreate()) { + return false; + } + + RECT frame = GetClientArea(); + + // The size here must match the window dimensions to avoid unnecessary surface + // creation / destruction in the startup path. + flutter_controller_ = std::make_unique( + frame.right - frame.left, frame.bottom - frame.top, project_); + // Ensure that basic setup of the controller was successful. + if (!flutter_controller_->engine() || !flutter_controller_->view()) { + return false; + } + RegisterPlugins(flutter_controller_->engine()); + SetChildContent(flutter_controller_->view()->GetNativeWindow()); + + flutter_controller_->engine()->SetNextFrameCallback([&]() { + this->Show(); + }); + + // Flutter can complete the first frame before the "show window" callback is + // registered. The following call ensures a frame is pending to ensure the + // window is shown. It is a no-op if the first frame hasn't completed yet. + flutter_controller_->ForceRedraw(); + + return true; +} + +void FlutterWindow::OnDestroy() { + if (flutter_controller_) { + flutter_controller_ = nullptr; + } + + Win32Window::OnDestroy(); +} + +LRESULT +FlutterWindow::MessageHandler(HWND hwnd, UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + // Give Flutter, including plugins, an opportunity to handle window messages. + if (flutter_controller_) { + std::optional result = + flutter_controller_->HandleTopLevelWindowProc(hwnd, message, wparam, + lparam); + if (result) { + return *result; + } + } + + switch (message) { + case WM_FONTCHANGE: + flutter_controller_->engine()->ReloadSystemFonts(); + break; + } + + return Win32Window::MessageHandler(hwnd, message, wparam, lparam); +} diff --git a/Mobile Commerce/windows/runner/flutter_window.h b/Mobile Commerce/windows/runner/flutter_window.h new file mode 100644 index 0000000..6da0652 --- /dev/null +++ b/Mobile Commerce/windows/runner/flutter_window.h @@ -0,0 +1,33 @@ +#ifndef RUNNER_FLUTTER_WINDOW_H_ +#define RUNNER_FLUTTER_WINDOW_H_ + +#include +#include + +#include + +#include "win32_window.h" + +// A window that does nothing but host a Flutter view. +class FlutterWindow : public Win32Window { + public: + // Creates a new FlutterWindow hosting a Flutter view running |project|. + explicit FlutterWindow(const flutter::DartProject& project); + virtual ~FlutterWindow(); + + protected: + // Win32Window: + bool OnCreate() override; + void OnDestroy() override; + LRESULT MessageHandler(HWND window, UINT const message, WPARAM const wparam, + LPARAM const lparam) noexcept override; + + private: + // The project to run. + flutter::DartProject project_; + + // The Flutter instance hosted by this window. + std::unique_ptr flutter_controller_; +}; + +#endif // RUNNER_FLUTTER_WINDOW_H_ diff --git a/Mobile Commerce/windows/runner/main.cpp b/Mobile Commerce/windows/runner/main.cpp new file mode 100644 index 0000000..b99d97b --- /dev/null +++ b/Mobile Commerce/windows/runner/main.cpp @@ -0,0 +1,43 @@ +#include +#include +#include + +#include "flutter_window.h" +#include "utils.h" + +int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, + _In_ wchar_t *command_line, _In_ int show_command) { + // Attach to console when present (e.g., 'flutter run') or create a + // new console when running with a debugger. + if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) { + CreateAndAttachConsole(); + } + + // Initialize COM, so that it is available for use in the library and/or + // plugins. + ::CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED); + + flutter::DartProject project(L"data"); + + std::vector command_line_arguments = + GetCommandLineArguments(); + + project.set_dart_entrypoint_arguments(std::move(command_line_arguments)); + + FlutterWindow window(project); + Win32Window::Point origin(10, 10); + Win32Window::Size size(1280, 720); + if (!window.Create(L"niogu_ecommerce_v1", origin, size)) { + return EXIT_FAILURE; + } + window.SetQuitOnClose(true); + + ::MSG msg; + while (::GetMessage(&msg, nullptr, 0, 0)) { + ::TranslateMessage(&msg); + ::DispatchMessage(&msg); + } + + ::CoUninitialize(); + return EXIT_SUCCESS; +} diff --git a/Mobile Commerce/windows/runner/resource.h b/Mobile Commerce/windows/runner/resource.h new file mode 100644 index 0000000..66a65d1 --- /dev/null +++ b/Mobile Commerce/windows/runner/resource.h @@ -0,0 +1,16 @@ +//{{NO_DEPENDENCIES}} +// Microsoft Visual C++ generated include file. +// Used by Runner.rc +// +#define IDI_APP_ICON 101 + +// Next default values for new objects +// +#ifdef APSTUDIO_INVOKED +#ifndef APSTUDIO_READONLY_SYMBOLS +#define _APS_NEXT_RESOURCE_VALUE 102 +#define _APS_NEXT_COMMAND_VALUE 40001 +#define _APS_NEXT_CONTROL_VALUE 1001 +#define _APS_NEXT_SYMED_VALUE 101 +#endif +#endif diff --git a/Mobile Commerce/windows/runner/resources/app_icon.ico b/Mobile Commerce/windows/runner/resources/app_icon.ico new file mode 100644 index 0000000..c04e20c Binary files /dev/null and b/Mobile Commerce/windows/runner/resources/app_icon.ico differ diff --git a/Mobile Commerce/windows/runner/runner.exe.manifest b/Mobile Commerce/windows/runner/runner.exe.manifest new file mode 100644 index 0000000..153653e --- /dev/null +++ b/Mobile Commerce/windows/runner/runner.exe.manifest @@ -0,0 +1,14 @@ + + + + + PerMonitorV2 + + + + + + + + + diff --git a/Mobile Commerce/windows/runner/utils.cpp b/Mobile Commerce/windows/runner/utils.cpp new file mode 100644 index 0000000..3a0b465 --- /dev/null +++ b/Mobile Commerce/windows/runner/utils.cpp @@ -0,0 +1,65 @@ +#include "utils.h" + +#include +#include +#include +#include + +#include + +void CreateAndAttachConsole() { + if (::AllocConsole()) { + FILE *unused; + if (freopen_s(&unused, "CONOUT$", "w", stdout)) { + _dup2(_fileno(stdout), 1); + } + if (freopen_s(&unused, "CONOUT$", "w", stderr)) { + _dup2(_fileno(stdout), 2); + } + std::ios::sync_with_stdio(); + FlutterDesktopResyncOutputStreams(); + } +} + +std::vector GetCommandLineArguments() { + // Convert the UTF-16 command line arguments to UTF-8 for the Engine to use. + int argc; + wchar_t** argv = ::CommandLineToArgvW(::GetCommandLineW(), &argc); + if (argv == nullptr) { + return std::vector(); + } + + std::vector command_line_arguments; + + // Skip the first argument as it's the binary name. + for (int i = 1; i < argc; i++) { + command_line_arguments.push_back(Utf8FromUtf16(argv[i])); + } + + ::LocalFree(argv); + + return command_line_arguments; +} + +std::string Utf8FromUtf16(const wchar_t* utf16_string) { + if (utf16_string == nullptr) { + return std::string(); + } + unsigned int target_length = ::WideCharToMultiByte( + CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, + -1, nullptr, 0, nullptr, nullptr) + -1; // remove the trailing null character + int input_length = (int)wcslen(utf16_string); + std::string utf8_string; + if (target_length == 0 || target_length > utf8_string.max_size()) { + return utf8_string; + } + utf8_string.resize(target_length); + int converted_length = ::WideCharToMultiByte( + CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, + input_length, utf8_string.data(), target_length, nullptr, nullptr); + if (converted_length == 0) { + return std::string(); + } + return utf8_string; +} diff --git a/Mobile Commerce/windows/runner/utils.h b/Mobile Commerce/windows/runner/utils.h new file mode 100644 index 0000000..3879d54 --- /dev/null +++ b/Mobile Commerce/windows/runner/utils.h @@ -0,0 +1,19 @@ +#ifndef RUNNER_UTILS_H_ +#define RUNNER_UTILS_H_ + +#include +#include + +// Creates a console for the process, and redirects stdout and stderr to +// it for both the runner and the Flutter library. +void CreateAndAttachConsole(); + +// Takes a null-terminated wchar_t* encoded in UTF-16 and returns a std::string +// encoded in UTF-8. Returns an empty std::string on failure. +std::string Utf8FromUtf16(const wchar_t* utf16_string); + +// Gets the command line arguments passed in as a std::vector, +// encoded in UTF-8. Returns an empty std::vector on failure. +std::vector GetCommandLineArguments(); + +#endif // RUNNER_UTILS_H_ diff --git a/Mobile Commerce/windows/runner/win32_window.cpp b/Mobile Commerce/windows/runner/win32_window.cpp new file mode 100644 index 0000000..60608d0 --- /dev/null +++ b/Mobile Commerce/windows/runner/win32_window.cpp @@ -0,0 +1,288 @@ +#include "win32_window.h" + +#include +#include + +#include "resource.h" + +namespace { + +/// Window attribute that enables dark mode window decorations. +/// +/// Redefined in case the developer's machine has a Windows SDK older than +/// version 10.0.22000.0. +/// See: https://docs.microsoft.com/windows/win32/api/dwmapi/ne-dwmapi-dwmwindowattribute +#ifndef DWMWA_USE_IMMERSIVE_DARK_MODE +#define DWMWA_USE_IMMERSIVE_DARK_MODE 20 +#endif + +constexpr const wchar_t kWindowClassName[] = L"FLUTTER_RUNNER_WIN32_WINDOW"; + +/// Registry key for app theme preference. +/// +/// A value of 0 indicates apps should use dark mode. A non-zero or missing +/// value indicates apps should use light mode. +constexpr const wchar_t kGetPreferredBrightnessRegKey[] = + L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize"; +constexpr const wchar_t kGetPreferredBrightnessRegValue[] = L"AppsUseLightTheme"; + +// The number of Win32Window objects that currently exist. +static int g_active_window_count = 0; + +using EnableNonClientDpiScaling = BOOL __stdcall(HWND hwnd); + +// Scale helper to convert logical scaler values to physical using passed in +// scale factor +int Scale(int source, double scale_factor) { + return static_cast(source * scale_factor); +} + +// Dynamically loads the |EnableNonClientDpiScaling| from the User32 module. +// This API is only needed for PerMonitor V1 awareness mode. +void EnableFullDpiSupportIfAvailable(HWND hwnd) { + HMODULE user32_module = LoadLibraryA("User32.dll"); + if (!user32_module) { + return; + } + auto enable_non_client_dpi_scaling = + reinterpret_cast( + GetProcAddress(user32_module, "EnableNonClientDpiScaling")); + if (enable_non_client_dpi_scaling != nullptr) { + enable_non_client_dpi_scaling(hwnd); + } + FreeLibrary(user32_module); +} + +} // namespace + +// Manages the Win32Window's window class registration. +class WindowClassRegistrar { + public: + ~WindowClassRegistrar() = default; + + // Returns the singleton registrar instance. + static WindowClassRegistrar* GetInstance() { + if (!instance_) { + instance_ = new WindowClassRegistrar(); + } + return instance_; + } + + // Returns the name of the window class, registering the class if it hasn't + // previously been registered. + const wchar_t* GetWindowClass(); + + // Unregisters the window class. Should only be called if there are no + // instances of the window. + void UnregisterWindowClass(); + + private: + WindowClassRegistrar() = default; + + static WindowClassRegistrar* instance_; + + bool class_registered_ = false; +}; + +WindowClassRegistrar* WindowClassRegistrar::instance_ = nullptr; + +const wchar_t* WindowClassRegistrar::GetWindowClass() { + if (!class_registered_) { + WNDCLASS window_class{}; + window_class.hCursor = LoadCursor(nullptr, IDC_ARROW); + window_class.lpszClassName = kWindowClassName; + window_class.style = CS_HREDRAW | CS_VREDRAW; + window_class.cbClsExtra = 0; + window_class.cbWndExtra = 0; + window_class.hInstance = GetModuleHandle(nullptr); + window_class.hIcon = + LoadIcon(window_class.hInstance, MAKEINTRESOURCE(IDI_APP_ICON)); + window_class.hbrBackground = 0; + window_class.lpszMenuName = nullptr; + window_class.lpfnWndProc = Win32Window::WndProc; + RegisterClass(&window_class); + class_registered_ = true; + } + return kWindowClassName; +} + +void WindowClassRegistrar::UnregisterWindowClass() { + UnregisterClass(kWindowClassName, nullptr); + class_registered_ = false; +} + +Win32Window::Win32Window() { + ++g_active_window_count; +} + +Win32Window::~Win32Window() { + --g_active_window_count; + Destroy(); +} + +bool Win32Window::Create(const std::wstring& title, + const Point& origin, + const Size& size) { + Destroy(); + + const wchar_t* window_class = + WindowClassRegistrar::GetInstance()->GetWindowClass(); + + const POINT target_point = {static_cast(origin.x), + static_cast(origin.y)}; + HMONITOR monitor = MonitorFromPoint(target_point, MONITOR_DEFAULTTONEAREST); + UINT dpi = FlutterDesktopGetDpiForMonitor(monitor); + double scale_factor = dpi / 96.0; + + HWND window = CreateWindow( + window_class, title.c_str(), WS_OVERLAPPEDWINDOW, + Scale(origin.x, scale_factor), Scale(origin.y, scale_factor), + Scale(size.width, scale_factor), Scale(size.height, scale_factor), + nullptr, nullptr, GetModuleHandle(nullptr), this); + + if (!window) { + return false; + } + + UpdateTheme(window); + + return OnCreate(); +} + +bool Win32Window::Show() { + return ShowWindow(window_handle_, SW_SHOWNORMAL); +} + +// static +LRESULT CALLBACK Win32Window::WndProc(HWND const window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + if (message == WM_NCCREATE) { + auto window_struct = reinterpret_cast(lparam); + SetWindowLongPtr(window, GWLP_USERDATA, + reinterpret_cast(window_struct->lpCreateParams)); + + auto that = static_cast(window_struct->lpCreateParams); + EnableFullDpiSupportIfAvailable(window); + that->window_handle_ = window; + } else if (Win32Window* that = GetThisFromHandle(window)) { + return that->MessageHandler(window, message, wparam, lparam); + } + + return DefWindowProc(window, message, wparam, lparam); +} + +LRESULT +Win32Window::MessageHandler(HWND hwnd, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + switch (message) { + case WM_DESTROY: + window_handle_ = nullptr; + Destroy(); + if (quit_on_close_) { + PostQuitMessage(0); + } + return 0; + + case WM_DPICHANGED: { + auto newRectSize = reinterpret_cast(lparam); + LONG newWidth = newRectSize->right - newRectSize->left; + LONG newHeight = newRectSize->bottom - newRectSize->top; + + SetWindowPos(hwnd, nullptr, newRectSize->left, newRectSize->top, newWidth, + newHeight, SWP_NOZORDER | SWP_NOACTIVATE); + + return 0; + } + case WM_SIZE: { + RECT rect = GetClientArea(); + if (child_content_ != nullptr) { + // Size and position the child window. + MoveWindow(child_content_, rect.left, rect.top, rect.right - rect.left, + rect.bottom - rect.top, TRUE); + } + return 0; + } + + case WM_ACTIVATE: + if (child_content_ != nullptr) { + SetFocus(child_content_); + } + return 0; + + case WM_DWMCOLORIZATIONCOLORCHANGED: + UpdateTheme(hwnd); + return 0; + } + + return DefWindowProc(window_handle_, message, wparam, lparam); +} + +void Win32Window::Destroy() { + OnDestroy(); + + if (window_handle_) { + DestroyWindow(window_handle_); + window_handle_ = nullptr; + } + if (g_active_window_count == 0) { + WindowClassRegistrar::GetInstance()->UnregisterWindowClass(); + } +} + +Win32Window* Win32Window::GetThisFromHandle(HWND const window) noexcept { + return reinterpret_cast( + GetWindowLongPtr(window, GWLP_USERDATA)); +} + +void Win32Window::SetChildContent(HWND content) { + child_content_ = content; + SetParent(content, window_handle_); + RECT frame = GetClientArea(); + + MoveWindow(content, frame.left, frame.top, frame.right - frame.left, + frame.bottom - frame.top, true); + + SetFocus(child_content_); +} + +RECT Win32Window::GetClientArea() { + RECT frame; + GetClientRect(window_handle_, &frame); + return frame; +} + +HWND Win32Window::GetHandle() { + return window_handle_; +} + +void Win32Window::SetQuitOnClose(bool quit_on_close) { + quit_on_close_ = quit_on_close; +} + +bool Win32Window::OnCreate() { + // No-op; provided for subclasses. + return true; +} + +void Win32Window::OnDestroy() { + // No-op; provided for subclasses. +} + +void Win32Window::UpdateTheme(HWND const window) { + DWORD light_mode; + DWORD light_mode_size = sizeof(light_mode); + LSTATUS result = RegGetValue(HKEY_CURRENT_USER, kGetPreferredBrightnessRegKey, + kGetPreferredBrightnessRegValue, + RRF_RT_REG_DWORD, nullptr, &light_mode, + &light_mode_size); + + if (result == ERROR_SUCCESS) { + BOOL enable_dark_mode = light_mode == 0; + DwmSetWindowAttribute(window, DWMWA_USE_IMMERSIVE_DARK_MODE, + &enable_dark_mode, sizeof(enable_dark_mode)); + } +} diff --git a/Mobile Commerce/windows/runner/win32_window.h b/Mobile Commerce/windows/runner/win32_window.h new file mode 100644 index 0000000..e901dde --- /dev/null +++ b/Mobile Commerce/windows/runner/win32_window.h @@ -0,0 +1,102 @@ +#ifndef RUNNER_WIN32_WINDOW_H_ +#define RUNNER_WIN32_WINDOW_H_ + +#include + +#include +#include +#include + +// A class abstraction for a high DPI-aware Win32 Window. Intended to be +// inherited from by classes that wish to specialize with custom +// rendering and input handling +class Win32Window { + public: + struct Point { + unsigned int x; + unsigned int y; + Point(unsigned int x, unsigned int y) : x(x), y(y) {} + }; + + struct Size { + unsigned int width; + unsigned int height; + Size(unsigned int width, unsigned int height) + : width(width), height(height) {} + }; + + Win32Window(); + virtual ~Win32Window(); + + // Creates a win32 window with |title| that is positioned and sized using + // |origin| and |size|. New windows are created on the default monitor. Window + // sizes are specified to the OS in physical pixels, hence to ensure a + // consistent size this function will scale the inputted width and height as + // as appropriate for the default monitor. The window is invisible until + // |Show| is called. Returns true if the window was created successfully. + bool Create(const std::wstring& title, const Point& origin, const Size& size); + + // Show the current window. Returns true if the window was successfully shown. + bool Show(); + + // Release OS resources associated with window. + void Destroy(); + + // Inserts |content| into the window tree. + void SetChildContent(HWND content); + + // Returns the backing Window handle to enable clients to set icon and other + // window properties. Returns nullptr if the window has been destroyed. + HWND GetHandle(); + + // If true, closing this window will quit the application. + void SetQuitOnClose(bool quit_on_close); + + // Return a RECT representing the bounds of the current client area. + RECT GetClientArea(); + + protected: + // Processes and route salient window messages for mouse handling, + // size change and DPI. Delegates handling of these to member overloads that + // inheriting classes can handle. + virtual LRESULT MessageHandler(HWND window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept; + + // Called when CreateAndShow is called, allowing subclass window-related + // setup. Subclasses should return false if setup fails. + virtual bool OnCreate(); + + // Called when Destroy is called. + virtual void OnDestroy(); + + private: + friend class WindowClassRegistrar; + + // OS callback called by message pump. Handles the WM_NCCREATE message which + // is passed when the non-client area is being created and enables automatic + // non-client DPI scaling so that the non-client area automatically + // responds to changes in DPI. All other messages are handled by + // MessageHandler. + static LRESULT CALLBACK WndProc(HWND const window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept; + + // Retrieves a class instance pointer for |window| + static Win32Window* GetThisFromHandle(HWND const window) noexcept; + + // Update the window frame's theme to match the system theme. + static void UpdateTheme(HWND const window); + + bool quit_on_close_ = false; + + // window handle for top level window. + HWND window_handle_ = nullptr; + + // window handle for hosted content. + HWND child_content_ = nullptr; +}; + +#endif // RUNNER_WIN32_WINDOW_H_ diff --git a/Mobile Operasional/.gitignore b/Mobile Operasional/.gitignore new file mode 100644 index 0000000..3820a95 --- /dev/null +++ b/Mobile Operasional/.gitignore @@ -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-dependencies +.pub-cache/ +.pub/ +/build/ +/coverage/ + +# 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 diff --git a/Mobile Operasional/.metadata b/Mobile Operasional/.metadata new file mode 100644 index 0000000..e679346 --- /dev/null +++ b/Mobile Operasional/.metadata @@ -0,0 +1,30 @@ +# 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: "19074d12f7eaf6a8180cd4036a430c1d76de904e" + channel: "stable" + +project_type: app + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: 19074d12f7eaf6a8180cd4036a430c1d76de904e + base_revision: 19074d12f7eaf6a8180cd4036a430c1d76de904e + - platform: web + create_revision: 19074d12f7eaf6a8180cd4036a430c1d76de904e + base_revision: 19074d12f7eaf6a8180cd4036a430c1d76de904e + + # 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' diff --git a/Mobile Operasional/.vscode/settings.json b/Mobile Operasional/.vscode/settings.json new file mode 100644 index 0000000..385f27a --- /dev/null +++ b/Mobile Operasional/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "java.configuration.updateBuildConfiguration": "interactive" +} diff --git a/Mobile Operasional/README.md b/Mobile Operasional/README.md new file mode 100644 index 0000000..e69de29 diff --git a/Mobile Operasional/analysis_options.yaml b/Mobile Operasional/analysis_options.yaml new file mode 100644 index 0000000..6d17993 --- /dev/null +++ b/Mobile Operasional/analysis_options.yaml @@ -0,0 +1,24 @@ +# 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`. + +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 diff --git a/Mobile Operasional/android/.gitignore b/Mobile Operasional/android/.gitignore new file mode 100644 index 0000000..be3943c --- /dev/null +++ b/Mobile Operasional/android/.gitignore @@ -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 diff --git a/Mobile Operasional/android/app/build.gradle.kts b/Mobile Operasional/android/app/build.gradle.kts new file mode 100644 index 0000000..fa87329 --- /dev/null +++ b/Mobile Operasional/android/app/build.gradle.kts @@ -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.niogu_app" + compileSdk = flutter.compileSdkVersion + ndkVersion = flutter.ndkVersion + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + + kotlinOptions { + jvmTarget = JavaVersion.VERSION_17.toString() + } + + defaultConfig { + // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). + applicationId = "com.example.niogu_app" + // You can update the following values to match your application needs. + // For more information, see: https://flutter.dev/to/review-gradle-config. + minSdk = flutter.minSdkVersion + targetSdk = flutter.targetSdkVersion + versionCode = flutter.versionCode + versionName = flutter.versionName + } + + buildTypes { + release { + // TODO: Add your own signing config for the release build. + // Signing with the debug keys for now, so `flutter run --release` works. + signingConfig = signingConfigs.getByName("debug") + } + } +} + +flutter { + source = "../.." +} diff --git a/Mobile Operasional/android/app/src/debug/AndroidManifest.xml b/Mobile Operasional/android/app/src/debug/AndroidManifest.xml new file mode 100644 index 0000000..399f698 --- /dev/null +++ b/Mobile Operasional/android/app/src/debug/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/Mobile Operasional/android/app/src/main/AndroidManifest.xml b/Mobile Operasional/android/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..0b73d45 --- /dev/null +++ b/Mobile Operasional/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Mobile Operasional/android/app/src/main/kotlin/com/example/niogu_app/MainActivity.kt b/Mobile Operasional/android/app/src/main/kotlin/com/example/niogu_app/MainActivity.kt new file mode 100644 index 0000000..dd5837b --- /dev/null +++ b/Mobile Operasional/android/app/src/main/kotlin/com/example/niogu_app/MainActivity.kt @@ -0,0 +1,5 @@ +package com.example.niogu_app + +import io.flutter.embedding.android.FlutterActivity + +class MainActivity : FlutterActivity() diff --git a/Mobile Operasional/android/app/src/main/res/drawable-v21/launch_background.xml b/Mobile Operasional/android/app/src/main/res/drawable-v21/launch_background.xml new file mode 100644 index 0000000..f74085f --- /dev/null +++ b/Mobile Operasional/android/app/src/main/res/drawable-v21/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/Mobile Operasional/android/app/src/main/res/drawable/launch_background.xml b/Mobile Operasional/android/app/src/main/res/drawable/launch_background.xml new file mode 100644 index 0000000..304732f --- /dev/null +++ b/Mobile Operasional/android/app/src/main/res/drawable/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/Mobile Operasional/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/Mobile Operasional/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000..db77bb4 Binary files /dev/null and b/Mobile Operasional/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/Mobile Operasional/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/Mobile Operasional/android/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000..17987b7 Binary files /dev/null and b/Mobile Operasional/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/Mobile Operasional/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/Mobile Operasional/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000..09d4391 Binary files /dev/null and b/Mobile Operasional/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/Mobile Operasional/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/Mobile Operasional/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000..d5f1c8d Binary files /dev/null and b/Mobile Operasional/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/Mobile Operasional/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/Mobile Operasional/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000..4d6372e Binary files /dev/null and b/Mobile Operasional/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/Mobile Operasional/android/app/src/main/res/values-night/styles.xml b/Mobile Operasional/android/app/src/main/res/values-night/styles.xml new file mode 100644 index 0000000..06952be --- /dev/null +++ b/Mobile Operasional/android/app/src/main/res/values-night/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/Mobile Operasional/android/app/src/main/res/values/styles.xml b/Mobile Operasional/android/app/src/main/res/values/styles.xml new file mode 100644 index 0000000..cb1ef88 --- /dev/null +++ b/Mobile Operasional/android/app/src/main/res/values/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/Mobile Operasional/android/app/src/profile/AndroidManifest.xml b/Mobile Operasional/android/app/src/profile/AndroidManifest.xml new file mode 100644 index 0000000..399f698 --- /dev/null +++ b/Mobile Operasional/android/app/src/profile/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/Mobile Operasional/android/build.gradle.kts b/Mobile Operasional/android/build.gradle.kts new file mode 100644 index 0000000..dbee657 --- /dev/null +++ b/Mobile Operasional/android/build.gradle.kts @@ -0,0 +1,24 @@ +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("clean") { + delete(rootProject.layout.buildDirectory) +} diff --git a/Mobile Operasional/android/build/reports/problems/problems-report.html b/Mobile Operasional/android/build/reports/problems/problems-report.html new file mode 100644 index 0000000..9990879 --- /dev/null +++ b/Mobile Operasional/android/build/reports/problems/problems-report.html @@ -0,0 +1,663 @@ + + + + + + + + + + + + + Gradle Configuration Cache + + + +
+ +
+ Loading... +
+ + + + + + diff --git a/Mobile Operasional/android/gradle.properties b/Mobile Operasional/android/gradle.properties new file mode 100644 index 0000000..fbee1d8 --- /dev/null +++ b/Mobile Operasional/android/gradle.properties @@ -0,0 +1,2 @@ +org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError +android.useAndroidX=true diff --git a/Mobile Operasional/android/gradle/wrapper/gradle-wrapper.properties b/Mobile Operasional/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..e4ef43f --- /dev/null +++ b/Mobile Operasional/android/gradle/wrapper/gradle-wrapper.properties @@ -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.14-all.zip diff --git a/Mobile Operasional/android/settings.gradle.kts b/Mobile Operasional/android/settings.gradle.kts new file mode 100644 index 0000000..ca7fe06 --- /dev/null +++ b/Mobile Operasional/android/settings.gradle.kts @@ -0,0 +1,26 @@ +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.11.1" apply false + id("org.jetbrains.kotlin.android") version "2.2.20" apply false +} + +include(":app") diff --git a/Mobile Operasional/assets/fonts/Poppins-Bold.ttf b/Mobile Operasional/assets/fonts/Poppins-Bold.ttf new file mode 100644 index 0000000..00559ee Binary files /dev/null and b/Mobile Operasional/assets/fonts/Poppins-Bold.ttf differ diff --git a/Mobile Operasional/assets/fonts/Poppins-Regular.ttf b/Mobile Operasional/assets/fonts/Poppins-Regular.ttf new file mode 100644 index 0000000..9f0c71b Binary files /dev/null and b/Mobile Operasional/assets/fonts/Poppins-Regular.ttf differ diff --git a/Mobile Operasional/assets/images/Niogu_Logo.png b/Mobile Operasional/assets/images/Niogu_Logo.png new file mode 100644 index 0000000..fa67d10 Binary files /dev/null and b/Mobile Operasional/assets/images/Niogu_Logo.png differ diff --git a/Mobile Operasional/assets/images/Niogu_Logo_Update_1.0.png b/Mobile Operasional/assets/images/Niogu_Logo_Update_1.0.png new file mode 100644 index 0000000..1b0d512 Binary files /dev/null and b/Mobile Operasional/assets/images/Niogu_Logo_Update_1.0.png differ diff --git a/Mobile Operasional/assets/images/bca.png b/Mobile Operasional/assets/images/bca.png new file mode 100644 index 0000000..09a6318 Binary files /dev/null and b/Mobile Operasional/assets/images/bca.png differ diff --git a/Mobile Operasional/assets/images/bni.png b/Mobile Operasional/assets/images/bni.png new file mode 100644 index 0000000..cd7bcea Binary files /dev/null and b/Mobile Operasional/assets/images/bni.png differ diff --git a/Mobile Operasional/assets/images/bri.png b/Mobile Operasional/assets/images/bri.png new file mode 100644 index 0000000..cf524b0 Binary files /dev/null and b/Mobile Operasional/assets/images/bri.png differ diff --git a/Mobile Operasional/assets/images/dana.png b/Mobile Operasional/assets/images/dana.png new file mode 100644 index 0000000..16845d8 Binary files /dev/null and b/Mobile Operasional/assets/images/dana.png differ diff --git a/Mobile Operasional/assets/images/gopay.png b/Mobile Operasional/assets/images/gopay.png new file mode 100644 index 0000000..ff52ea5 Binary files /dev/null and b/Mobile Operasional/assets/images/gopay.png differ diff --git a/Mobile Operasional/assets/images/mandiri.png b/Mobile Operasional/assets/images/mandiri.png new file mode 100644 index 0000000..a333a95 Binary files /dev/null and b/Mobile Operasional/assets/images/mandiri.png differ diff --git a/Mobile Operasional/assets/images/shopeepay.png b/Mobile Operasional/assets/images/shopeepay.png new file mode 100644 index 0000000..50aedf4 Binary files /dev/null and b/Mobile Operasional/assets/images/shopeepay.png differ diff --git a/Mobile Operasional/devtools_options.yaml b/Mobile Operasional/devtools_options.yaml new file mode 100644 index 0000000..fa0b357 --- /dev/null +++ b/Mobile Operasional/devtools_options.yaml @@ -0,0 +1,3 @@ +description: This file stores settings for Dart & Flutter DevTools. +documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states +extensions: diff --git a/Mobile Operasional/ios/.gitignore b/Mobile Operasional/ios/.gitignore new file mode 100644 index 0000000..7a7f987 --- /dev/null +++ b/Mobile Operasional/ios/.gitignore @@ -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 diff --git a/Mobile Operasional/ios/Flutter/AppFrameworkInfo.plist b/Mobile Operasional/ios/Flutter/AppFrameworkInfo.plist new file mode 100644 index 0000000..1dc6cf7 --- /dev/null +++ b/Mobile Operasional/ios/Flutter/AppFrameworkInfo.plist @@ -0,0 +1,26 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + App + CFBundleIdentifier + io.flutter.flutter.app + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + App + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1.0 + MinimumOSVersion + 13.0 + + diff --git a/Mobile Operasional/ios/Flutter/Debug.xcconfig b/Mobile Operasional/ios/Flutter/Debug.xcconfig new file mode 100644 index 0000000..592ceee --- /dev/null +++ b/Mobile Operasional/ios/Flutter/Debug.xcconfig @@ -0,0 +1 @@ +#include "Generated.xcconfig" diff --git a/Mobile Operasional/ios/Flutter/Release.xcconfig b/Mobile Operasional/ios/Flutter/Release.xcconfig new file mode 100644 index 0000000..592ceee --- /dev/null +++ b/Mobile Operasional/ios/Flutter/Release.xcconfig @@ -0,0 +1 @@ +#include "Generated.xcconfig" diff --git a/Mobile Operasional/ios/Runner.xcodeproj/project.pbxproj b/Mobile Operasional/ios/Runner.xcodeproj/project.pbxproj new file mode 100644 index 0000000..d902458 --- /dev/null +++ b/Mobile Operasional/ios/Runner.xcodeproj/project.pbxproj @@ -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 = ""; }; + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; + 331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; + 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 = ""; }; + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; + 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; + 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 = ""; }; + 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; +/* 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 = ""; + }; + 9740EEB11CF90186004384FC /* Flutter */ = { + isa = PBXGroup; + children = ( + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 9740EEB31CF90195004384FC /* Generated.xcconfig */, + ); + name = Flutter; + sourceTree = ""; + }; + 97C146E51CF9000F007C117D = { + isa = PBXGroup; + children = ( + 9740EEB11CF90186004384FC /* Flutter */, + 97C146F01CF9000F007C117D /* Runner */, + 97C146EF1CF9000F007C117D /* Products */, + 331C8082294A63A400263BE5 /* RunnerTests */, + ); + sourceTree = ""; + }; + 97C146EF1CF9000F007C117D /* Products */ = { + isa = PBXGroup; + children = ( + 97C146EE1CF9000F007C117D /* Runner.app */, + 331C8081294A63A400263BE5 /* RunnerTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 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 = ""; + }; +/* 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 = ""; + }; + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C147001CF9000F007C117D /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* 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 = 13.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.niogooApp; + 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.niogooApp.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.niogooApp.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.niogooApp.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 = 13.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 = 13.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.niogooApp; + 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.niogooApp; + 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 */; +} diff --git a/Mobile Operasional/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/Mobile Operasional/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..919434a --- /dev/null +++ b/Mobile Operasional/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/Mobile Operasional/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/Mobile Operasional/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/Mobile Operasional/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/Mobile Operasional/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/Mobile Operasional/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..f9b0d7c --- /dev/null +++ b/Mobile Operasional/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/Mobile Operasional/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/Mobile Operasional/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 0000000..e3773d4 --- /dev/null +++ b/Mobile Operasional/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,101 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Mobile Operasional/ios/Runner.xcworkspace/contents.xcworkspacedata b/Mobile Operasional/ios/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..1d526a1 --- /dev/null +++ b/Mobile Operasional/ios/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/Mobile Operasional/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/Mobile Operasional/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/Mobile Operasional/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/Mobile Operasional/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/Mobile Operasional/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..f9b0d7c --- /dev/null +++ b/Mobile Operasional/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/Mobile Operasional/ios/Runner/AppDelegate.swift b/Mobile Operasional/ios/Runner/AppDelegate.swift new file mode 100644 index 0000000..6266644 --- /dev/null +++ b/Mobile Operasional/ios/Runner/AppDelegate.swift @@ -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) + } +} diff --git a/Mobile Operasional/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/Mobile Operasional/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..d36b1fa --- /dev/null +++ b/Mobile Operasional/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -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" + } +} diff --git a/Mobile Operasional/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/Mobile Operasional/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png new file mode 100644 index 0000000..dc9ada4 Binary files /dev/null and b/Mobile Operasional/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png differ diff --git a/Mobile Operasional/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/Mobile Operasional/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png new file mode 100644 index 0000000..7353c41 Binary files /dev/null and b/Mobile Operasional/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png differ diff --git a/Mobile Operasional/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/Mobile Operasional/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png new file mode 100644 index 0000000..797d452 Binary files /dev/null and b/Mobile Operasional/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png differ diff --git a/Mobile Operasional/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/Mobile Operasional/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png new file mode 100644 index 0000000..6ed2d93 Binary files /dev/null and b/Mobile Operasional/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png differ diff --git a/Mobile Operasional/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/Mobile Operasional/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png new file mode 100644 index 0000000..4cd7b00 Binary files /dev/null and b/Mobile Operasional/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png differ diff --git a/Mobile Operasional/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/Mobile Operasional/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png new file mode 100644 index 0000000..fe73094 Binary files /dev/null and b/Mobile Operasional/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png differ diff --git a/Mobile Operasional/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/Mobile Operasional/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png new file mode 100644 index 0000000..321773c Binary files /dev/null and b/Mobile Operasional/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png differ diff --git a/Mobile Operasional/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/Mobile Operasional/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png new file mode 100644 index 0000000..797d452 Binary files /dev/null and b/Mobile Operasional/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png differ diff --git a/Mobile Operasional/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/Mobile Operasional/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png new file mode 100644 index 0000000..502f463 Binary files /dev/null and b/Mobile Operasional/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png differ diff --git a/Mobile Operasional/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/Mobile Operasional/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png new file mode 100644 index 0000000..0ec3034 Binary files /dev/null and b/Mobile Operasional/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png differ diff --git a/Mobile Operasional/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/Mobile Operasional/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png new file mode 100644 index 0000000..0ec3034 Binary files /dev/null and b/Mobile Operasional/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png differ diff --git a/Mobile Operasional/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/Mobile Operasional/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png new file mode 100644 index 0000000..e9f5fea Binary files /dev/null and b/Mobile Operasional/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png differ diff --git a/Mobile Operasional/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/Mobile Operasional/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png new file mode 100644 index 0000000..84ac32a Binary files /dev/null and b/Mobile Operasional/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png differ diff --git a/Mobile Operasional/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/Mobile Operasional/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png new file mode 100644 index 0000000..8953cba Binary files /dev/null and b/Mobile Operasional/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png differ diff --git a/Mobile Operasional/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/Mobile Operasional/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png new file mode 100644 index 0000000..0467bf1 Binary files /dev/null and b/Mobile Operasional/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png differ diff --git a/Mobile Operasional/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/Mobile Operasional/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json new file mode 100644 index 0000000..0bedcf2 --- /dev/null +++ b/Mobile Operasional/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json @@ -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" + } +} diff --git a/Mobile Operasional/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/Mobile Operasional/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/Mobile Operasional/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png differ diff --git a/Mobile Operasional/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/Mobile Operasional/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/Mobile Operasional/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png differ diff --git a/Mobile Operasional/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/Mobile Operasional/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/Mobile Operasional/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png differ diff --git a/Mobile Operasional/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/Mobile Operasional/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md new file mode 100644 index 0000000..89c2725 --- /dev/null +++ b/Mobile Operasional/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md @@ -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. \ No newline at end of file diff --git a/Mobile Operasional/ios/Runner/Base.lproj/LaunchScreen.storyboard b/Mobile Operasional/ios/Runner/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 0000000..f2e259c --- /dev/null +++ b/Mobile Operasional/ios/Runner/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Mobile Operasional/ios/Runner/Base.lproj/Main.storyboard b/Mobile Operasional/ios/Runner/Base.lproj/Main.storyboard new file mode 100644 index 0000000..f3c2851 --- /dev/null +++ b/Mobile Operasional/ios/Runner/Base.lproj/Main.storyboard @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Mobile Operasional/ios/Runner/Info.plist b/Mobile Operasional/ios/Runner/Info.plist new file mode 100644 index 0000000..ad27b70 --- /dev/null +++ b/Mobile Operasional/ios/Runner/Info.plist @@ -0,0 +1,53 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + Niogoo App + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + niogoo_app + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleSignature + ???? + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSRequiresIPhoneOS + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + CADisableMinimumFrameDurationOnPhone + + UIApplicationSupportsIndirectInputEvents + + NSPhotoLibraryUsageDescription + Izinkan aplikasi untuk mengakses galeri + NSCameraUsageDescription + Izinkan aplikasi untuk mengakses kamera + + diff --git a/Mobile Operasional/ios/Runner/Runner-Bridging-Header.h b/Mobile Operasional/ios/Runner/Runner-Bridging-Header.h new file mode 100644 index 0000000..308a2a5 --- /dev/null +++ b/Mobile Operasional/ios/Runner/Runner-Bridging-Header.h @@ -0,0 +1 @@ +#import "GeneratedPluginRegistrant.h" diff --git a/Mobile Operasional/ios/RunnerTests/RunnerTests.swift b/Mobile Operasional/ios/RunnerTests/RunnerTests.swift new file mode 100644 index 0000000..86a7c3b --- /dev/null +++ b/Mobile Operasional/ios/RunnerTests/RunnerTests.swift @@ -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. + } + +} diff --git a/Mobile Operasional/lib/core/.gitignore b/Mobile Operasional/lib/core/.gitignore new file mode 100644 index 0000000..3573899 --- /dev/null +++ b/Mobile Operasional/lib/core/.gitignore @@ -0,0 +1 @@ +/templates \ No newline at end of file diff --git a/Mobile Operasional/lib/core/components/app_bar_with_tab.dart b/Mobile Operasional/lib/core/components/app_bar_with_tab.dart new file mode 100644 index 0000000..f83ac3f --- /dev/null +++ b/Mobile Operasional/lib/core/components/app_bar_with_tab.dart @@ -0,0 +1,236 @@ +import 'package:flutter/material.dart'; +import 'package:niogu_app/core/components/modal_outlet_bottom.dart'; +import 'package:niogu_app/core/constants/app_color.dart'; +import 'package:niogu_app/core/constants/app_font_size.dart'; +import 'package:sizer/sizer.dart'; + +class AppBarWithTab extends StatelessWidget implements PreferredSizeWidget { + final String hintText; + final Color searchIconColor; + final bool canPop; + final VoidCallback? onPop; + final bool showSwitchOutlet; + final bool showNotifIcon; + final FocusNode searchFocusNode; + final TabController tabController; + final Function(int)? onTap; + final Function(String)? onSearchChanged; + final List tabs; + + const AppBarWithTab({ + super.key, + required this.hintText, + required this.searchIconColor, + this.canPop = false, + this.onPop, + this.showSwitchOutlet = true, + this.showNotifIcon = true, + required this.searchFocusNode, + required this.tabController, + required this.onTap, + required this.onSearchChanged, + required this.tabs, + }); + + @override + Size get preferredSize { + return Size.fromHeight(10.h + 8.h); + } + + @override + Widget build(BuildContext context) { + final bool isTablet = 100.w >= 600; + const greyColor = Color(0xFFF5F5F5); + + return AppBar( + backgroundColor: Colors.white, + surfaceTintColor: Colors.white, + elevation: 0, + titleSpacing: 0, + toolbarHeight: 10.h, + automaticallyImplyLeading: false, + title: Padding( + padding: EdgeInsets.symmetric(horizontal: 4.w), + child: Row( + children: [ + if (canPop) ...[ + Center( + child: Material( + color: Colors.transparent, + type: MaterialType.canvas, + child: InkWell( + onTap: onPop, + borderRadius: BorderRadius.circular(2.5.w), + child: Container( + width: 10.w, + height: 10.w, + decoration: BoxDecoration( + color: Colors.white, + border: Border.all(color: Colors.grey.shade200), + borderRadius: BorderRadius.circular(2.5.w), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.05), + blurRadius: 5, + offset: const Offset(0, 2), + ), + ], + ), + child: Icon( + Icons.arrow_back_ios_new_rounded, + color: Colors.black87, + size: 5.w, + ), + ), + ), + ), + ), + SizedBox(width: 3.w), + ] else if (showSwitchOutlet) ...[ + IconButton( + onPressed: () { + showModalBottomSheet( + context: context, + useRootNavigator: true, + isScrollControlled: true, + constraints: const BoxConstraints( + maxWidth: double.infinity, + ), + builder: (context) => const ModalOutletBottom(), + ); + }, + icon: Icon( + Icons.keyboard_arrow_down_rounded, + color: AppColor.primaryColor, + size: 7.w, + ), + padding: EdgeInsets.zero, + constraints: const BoxConstraints(), + ), + SizedBox(width: 3.w), + ], + + Expanded( + child: Container( + height: 6.h, + padding: isTablet + ? EdgeInsets.symmetric(vertical: 1.h, horizontal: 2.w) + : EdgeInsets.zero, + decoration: BoxDecoration( + color: greyColor, + borderRadius: BorderRadius.circular(2.5.w), + ), + child: TextField( + focusNode: searchFocusNode, + onChanged: onSearchChanged, + textAlignVertical: TextAlignVertical.center, + style: TextStyle( + color: Colors.black87, + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + ), + decoration: InputDecoration( + hintText: hintText, + hintStyle: TextStyle( + color: searchIconColor, + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + ), + prefixIcon: Icon( + Icons.search, + color: searchIconColor, + size: 5.w, + ), + border: InputBorder.none, + contentPadding: EdgeInsets.zero, + isDense: true, + ), + ), + ), + ), + + /** Notification + if (showNotifIcon) ...[ + SizedBox(width: 3.w), + Stack( + children: [ + IconButton( + onPressed: () {}, + icon: Icon( + Icons.notifications_outlined, + color: AppColor.primaryColor, + size: 7.w, + ), + padding: EdgeInsets.zero, + ), + Positioned( + right: isTablet + ? 10 + : 100.w > 360 + ? 12.5 + : 15, + top: isTablet + ? 10 + : 100.w > 360 + ? 12.5 + : 15, + child: Container( + padding: EdgeInsets.all(0.5.w), + decoration: const BoxDecoration( + color: Colors.red, + shape: BoxShape.circle, + ), + constraints: BoxConstraints( + minWidth: 2.5.w, + minHeight: 2.5.w, + ), + ), + ), + ], + ), + ], + */ + ], + ), + ), + bottom: PreferredSize( + preferredSize: Size.fromHeight(8.h), + child: Container( + height: 6.h, + margin: EdgeInsets.fromLTRB(4.w, 0, 4.w, 1.5.h), + decoration: BoxDecoration( + color: greyColor, + borderRadius: BorderRadius.circular(2.5.w), + ), + child: TabBar( + onTap: onTap, + controller: tabController, + indicator: BoxDecoration( + color: AppColor.primaryColor, + borderRadius: BorderRadius.circular(2.05.w), + boxShadow: [ + BoxShadow( + color: AppColor.primaryColor.withOpacity(0.3), + blurRadius: 4, + offset: const Offset(0, 2), + ), + ], + ), + indicatorSize: TabBarIndicatorSize.tab, + dividerColor: Colors.transparent, + labelColor: Colors.white, + unselectedLabelColor: Colors.grey[600], + labelStyle: TextStyle( + fontWeight: FontWeight.bold, + fontSize: AppFontSize.medium.sp, + ), + padding: EdgeInsets.all(1.w), + tabs: tabs, + ), + ), + ), + ); + } +} diff --git a/Mobile Operasional/lib/core/components/bottom_bar_app.dart b/Mobile Operasional/lib/core/components/bottom_bar_app.dart new file mode 100644 index 0000000..26ad250 --- /dev/null +++ b/Mobile Operasional/lib/core/components/bottom_bar_app.dart @@ -0,0 +1,107 @@ +import 'package:flutter/material.dart'; +import 'package:niogu_app/core/constants/app_color.dart'; +import 'package:niogu_app/core/constants/app_font_size.dart'; +import 'package:sizer/sizer.dart'; + +class BottomBarApp extends StatelessWidget { + final int currentIndex; + final void Function(int) onTap; + + const BottomBarApp({ + super.key, + required this.currentIndex, + required this.onTap, + }); + + @override + Widget build(BuildContext context) { + final bool isTablet = 100.w >= 600; + final double height = isTablet ? 14.h : 12.h; + return Container( + height: height, + decoration: BoxDecoration( + color: Colors.white, + border: BoxBorder.fromLTRB( + top: BorderSide(color: Colors.grey.shade300), + ), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + _buildNavItem(Icons.grid_view_rounded, "Menu", 0), + _buildNavItem(Icons.inventory_2_rounded, "Barang", 1), + + _buildCenterPosButton(2), + + _buildNavItem(Icons.description_rounded, "Laporan", 3), + _buildNavItem(Icons.person_rounded, "Profil", 4), + ], + ), + ); + } + + Widget _buildNavItem(IconData icon, String label, int index) { + final bool isActive = currentIndex == index; + + return InkWell( + onTap: () => onTap(index), + borderRadius: BorderRadius.circular(2.5.w), + child: AnimatedContainer( + duration: const Duration(milliseconds: 200), + padding: EdgeInsets.symmetric(horizontal: 3.w, vertical: 1.h), + decoration: BoxDecoration( + color: isActive + ? AppColor.primaryColor.withOpacity(0.08) + : Colors.transparent, + borderRadius: BorderRadius.circular(2.5.w), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + icon, + color: isActive ? AppColor.primaryColor : Colors.grey, + size: 6.w, + ), + SizedBox(height: 0.5.h), + Text( + label, + style: TextStyle( + fontSize: AppFontSize.small.sp, + fontWeight: isActive ? FontWeight.bold : FontWeight.w500, + color: isActive ? AppColor.primaryColor : Colors.grey, + ), + ), + ], + ), + ), + ); + } + + Widget _buildCenterPosButton(int index) { + return InkWell( + onTap: () => onTap(index), + borderRadius: BorderRadius.circular(4.w), + child: Container( + width: 15.w, + height: 15.w, + decoration: BoxDecoration( + color: AppColor.primaryColor, + borderRadius: BorderRadius.circular(4.w), + boxShadow: [ + BoxShadow( + color: AppColor.primaryColor.withOpacity(0.3), + blurRadius: 10, + offset: const Offset(0, 4), + ), + ], + ), + child: Icon( + Icons.point_of_sale_rounded, + color: Colors.white, + size: 12.w, + ), + ), + ); + } +} diff --git a/Mobile Operasional/lib/core/components/modal_outlet_bottom.dart b/Mobile Operasional/lib/core/components/modal_outlet_bottom.dart new file mode 100644 index 0000000..013efa8 --- /dev/null +++ b/Mobile Operasional/lib/core/components/modal_outlet_bottom.dart @@ -0,0 +1,376 @@ +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; +import 'package:niogu_app/core/constants/app_color.dart'; +import 'package:niogu_app/core/constants/app_font_size.dart'; +import 'package:niogu_app/core/widgets/custom_error_screen.dart'; +import 'package:niogu_app/core/providers/app_provider.dart'; +import 'package:niogu_app/core/system/system_setting.dart'; +import 'package:niogu_app/core/widgets/custom_snackbar.dart'; +import 'package:niogu_app/core/components/modal_outlet_bottom_shimmer.dart'; +import 'package:niogu_app/features/outlets/presentation/providers/outlet_provider.dart'; +import 'package:sizer/sizer.dart'; + +class ModalOutletBottom extends ConsumerStatefulWidget { + const ModalOutletBottom({super.key}); + + @override + ConsumerState createState() => _ModalOutletBottomState(); +} + +class _ModalOutletBottomState extends ConsumerState { + String? _activeOutletId; + + String? _tempSelectedId; + + String? _currentOutletName; + + @override + void initState() { + // TODO: implement initState + super.initState(); + + WidgetsBinding.instance.addPostFrameCallback((_) { + Future.delayed(const Duration(milliseconds: 400), () { + initCurrentOutlet(); + }); + }); + } + + Future initCurrentOutlet() async { + _activeOutletId = await SystemSetting.getCurrentOutletId(); + + _currentOutletName = await SystemSetting.getCurrentOutletName(); + + setState(() { + _tempSelectedId = _activeOutletId; + }); + } + + @override + void dispose() { + // TODO: implement dispose + super.dispose(); + } + + Future _handleSwicthOutlet() async { + await SystemSetting.switchOutlet( + outletId: _tempSelectedId!, + outletName: _currentOutletName!, + ); + + _activeOutletId = await SystemSetting.getCurrentOutletId(); + + _currentOutletName = await SystemSetting.getCurrentOutletName(); + + ref.read(currentOutletIdProvider.notifier).state = _activeOutletId; + + ref.read(currentOutletNameProvider.notifier).state = _currentOutletName; + + context.pop(); + + CustomSnackbar.showSuccess( + context, + "Berhasil masuk ke ${_currentOutletName!}", + ); + } + + @override + Widget build(BuildContext context) { + final bool isTablet = 100.w >= 600; + + final outletState = ref.watch(outletStreamProvider); + + return Container( + height: 85.h, + width: 100.w, + padding: EdgeInsets.fromLTRB(5.w, 2.h, 5.w, 5.h), + clipBehavior: Clip.hardEdge, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.vertical(top: Radius.circular(6.w)), + ), + constraints: BoxConstraints(maxHeight: 85.h), + child: Material( + color: Colors.white, + type: MaterialType.card, + child: outletState.when( + data: (outlets) { + return Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Center( + child: Container( + width: 15.w, + height: 0.6.h, + decoration: BoxDecoration( + color: Colors.grey[300], + borderRadius: BorderRadius.circular(2.25.w), + ), + ), + ), + SizedBox(height: 3.h), + + Text( + "Pilih Outlet", + style: TextStyle( + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + fontWeight: FontWeight.bold, + color: Colors.black, + ), + ), + SizedBox(height: 2.h), + + Expanded( + child: ListView.builder( + physics: const AlwaysScrollableScrollPhysics(), + itemCount: outlets.length, + itemBuilder: (context, index) { + final outlet = outlets[index]; + + final File imageFile = File( + outlet.photoPath ?? "image not found", + ); + + final bool imageFileExists = imageFile.existsSync(); + + final isSelected = outlet.id == _tempSelectedId; + + final isActiveReal = outlet.id == _activeOutletId; + + return Padding( + padding: EdgeInsets.only(bottom: 1.5.h), + child: InkWell( + onTap: () { + setState(() { + _tempSelectedId = outlet.id; + _currentOutletName = outlet.name; + }); + }, + borderRadius: BorderRadius.circular(2.5.w), + child: AnimatedContainer( + duration: const Duration(milliseconds: 200), + padding: EdgeInsets.symmetric( + horizontal: 4.w, + vertical: 2.h, + ), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(2.5.w), + border: Border.all( + color: isSelected + ? AppColor.primaryColor + : Colors.grey.shade300, + width: isSelected ? 2 : 1, + ), + boxShadow: isSelected + ? [ + BoxShadow( + color: AppColor.primaryColor.withValues( + alpha: 0.1, + ), + blurRadius: 8, + offset: Offset(0, 4), + ), + ] + : [], + ), + child: Row( + children: [ + Container( + width: 15.w, + height: 15.w, + clipBehavior: Clip.hardEdge, + + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(2.5.w), + border: Border.all( + color: Colors.grey.shade300, + width: 1, + ), + ), + + child: Center( + child: imageFileExists + ? Image.file( + imageFile, + fit: BoxFit.cover, + width: double.infinity, + height: double.infinity, + errorBuilder: + (context, error, stackTrace) { + return Icon( + Icons.error, + color: Colors.grey, + size: 8.w, + ); + }, + ) + : Icon( + Icons.store_outlined, + color: Colors.grey, + size: 8.w, + ), + ), + ), + + SizedBox(width: 2.w), + + Expanded( + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text( + outlet.name, + style: TextStyle( + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + fontWeight: isSelected + ? FontWeight.bold + : FontWeight.normal, + color: isSelected + ? Colors.black87 + : Colors.grey[800], + ), + ), + + if (outlet.isMainOutlet) ...[ + SizedBox(height: 0.75.h), + Text( + "(Outlet Utama)", + style: TextStyle( + fontSize: isTablet + ? (AppFontSize.medium - 2).sp + : (AppFontSize.small - 2).sp, + fontWeight: isSelected + ? FontWeight.bold + : FontWeight.normal, + color: isSelected + ? Colors.black87 + : Colors.grey[800], + ), + ), + ], + + if (isActiveReal) ...[ + SizedBox(height: 0.75.h), + Container( + padding: EdgeInsets.symmetric( + horizontal: 2.w, + vertical: 0.2.h, + ), + decoration: BoxDecoration( + color: Colors.green, + borderRadius: BorderRadius.circular( + 4, + ), + ), + child: Text( + "Sedang Aktif", + style: TextStyle( + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + color: Colors.white, + ), + ), + ), + ], + + if (!outlet.isActive) ...[ + SizedBox(height: 0.75), + Row( + mainAxisAlignment: + MainAxisAlignment.end, + children: [ + Text( + "Non Aktif / Tutup", + style: TextStyle( + color: Colors.red, + fontSize: isTablet + ? (AppFontSize.medium - 2) + .sp + : (AppFontSize.small - 2) + .sp, + fontWeight: isSelected + ? FontWeight.bold + : FontWeight.normal, + ), + ), + ], + ), + ], + ], + ), + ), + if (isSelected) + Icon( + Icons.check_circle, + color: AppColor.primaryColor, + size: 6.w, + ), + ], + ), + ), + ), + ); + }, + ), + ), + + SizedBox(height: 1.h), + + SizedBox( + width: double.infinity, + height: 6.5.h, + child: ElevatedButton( + onPressed: + !(_tempSelectedId != null && + outlets + .firstWhere( + (outlet) => outlet.id == _tempSelectedId, + ) + .isActive) + ? null + : _handleSwicthOutlet, + style: ElevatedButton.styleFrom( + backgroundColor: AppColor.primaryColor, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(2.5.w), + ), + elevation: 4, + ), + child: Text( + "Masuk Ke Outlet Ini", + style: TextStyle( + color: Colors.white, + fontSize: AppFontSize.medium.sp, + fontWeight: FontWeight.bold, + ), + ), + ), + ), + + SizedBox(height: MediaQuery.of(context).padding.bottom), + ], + ); + }, + error: (error, stackTrace) { + return CustomErrorScreen( + message: "Ups, terjadi kesalahan", + onRefresh: () {}, + ); + }, + loading: () => const ModalOutletBottomShimmer(), + ), + ), + ); + } +} diff --git a/Mobile Operasional/lib/core/components/modal_outlet_bottom_shimmer.dart b/Mobile Operasional/lib/core/components/modal_outlet_bottom_shimmer.dart new file mode 100644 index 0000000..5c17296 --- /dev/null +++ b/Mobile Operasional/lib/core/components/modal_outlet_bottom_shimmer.dart @@ -0,0 +1,118 @@ +import 'package:flutter/material.dart'; +import 'package:shimmer/shimmer.dart'; +import 'package:sizer/sizer.dart'; + +class ModalOutletBottomShimmer extends StatelessWidget { + const ModalOutletBottomShimmer({super.key}); + + @override + Widget build(BuildContext context) { + final baseColor = Colors.grey[300]!; + final highlightColor = Colors.grey[100]!; + + return Container( + decoration: const BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.vertical(top: Radius.circular(25)), + ), + child: Shimmer.fromColors( + baseColor: baseColor, + highlightColor: highlightColor, + child: SingleChildScrollView( + padding: EdgeInsets.fromLTRB(5.w, 2.h, 5.w, 5.h), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Center( + child: Container( + width: 15.w, + height: 0.6.h, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(2.25.w), + ), + ), + ), + SizedBox(height: 3.h), + + Container( + width: 35.w, + height: 2.h, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(4), + ), + ), + SizedBox(height: 2.h), + + ...List.generate(4, (index) => _buildOutletCardShimmer()), + + SizedBox(height: 2.h), + + Container( + width: double.infinity, + height: 6.5.h, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(2.25.w), + border: Border.all(color: Colors.white, width: 2), + ), + ), + + SizedBox(height: 2.h), + ], + ), + ), + ), + ); + } + + Widget _buildOutletCardShimmer() { + return Container( + margin: EdgeInsets.only(bottom: 1.5.h), + padding: EdgeInsets.symmetric(horizontal: 4.w, vertical: 2.h), + decoration: BoxDecoration( + border: Border.all(color: Colors.white), + borderRadius: BorderRadius.circular(12), + ), + child: Row( + children: [ + Container( + width: 15.w, + height: 15.w, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(2.25.w), + ), + ), + SizedBox(width: 3.w), + + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + width: 40.w, + height: 1.5.h, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(2.w), + ), + ), + SizedBox(height: 1.h), + Container( + width: 20.w, + height: 1.2.h, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(2.w), + ), + ), + ], + ), + ), + ], + ), + ); + } +} \ No newline at end of file diff --git a/Mobile Operasional/lib/core/components/top_back_bar_app.dart b/Mobile Operasional/lib/core/components/top_back_bar_app.dart new file mode 100644 index 0000000..d545254 --- /dev/null +++ b/Mobile Operasional/lib/core/components/top_back_bar_app.dart @@ -0,0 +1,83 @@ +import 'package:flutter/material.dart'; +import 'package:niogu_app/core/constants/app_font_size.dart'; +import 'package:sizer/sizer.dart'; + +class TopBackBarApp extends StatelessWidget implements PreferredSizeWidget { + final String title; + final VoidCallback? onTap; + + const TopBackBarApp({super.key, required this.title, this.onTap}); + + @override + // TODO: implement preferredSize + Size get preferredSize => + Size.fromHeight(100.w >= 600 ? 5.h : kToolbarHeight); + + @override + Widget build(BuildContext context) { + return AppBar( + backgroundColor: Colors.white, + surfaceTintColor: Colors.white, + elevation: 0, + centerTitle: true, + leading: Center( + child: Material( + color: Colors.transparent, + type: MaterialType.canvas, + child: InkWell( + onTap: onTap, + borderRadius: BorderRadius.circular(2.5.w), + child: Container( + width: 10.w, + height: 10.w, + decoration: BoxDecoration( + color: Colors.white, + border: Border.all(color: Colors.grey.shade200), + borderRadius: BorderRadius.circular(2.5.w), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.05), + blurRadius: 5, + offset: const Offset(0, 2), + ), + ], + ), + child: Icon( + Icons.arrow_back_ios_new_rounded, + color: Colors.black87, + size: 5.w, + ), + ), + ), + ), + ), + leadingWidth: 18.w, + title: Text( + title, + style: TextStyle( + color: Colors.black87, + fontSize: AppFontSize.medium.sp, + fontWeight: FontWeight.bold, + letterSpacing: 0.5, + ), + ), + /** + actions: [ + IconButton( + onPressed: onPressed, + icon: Icon( + Icons.help_outline_rounded, + color: Colors.grey[400], + size: 4.w, + ), + ), + SizedBox(width: 3.w), + ], + */ + bottom: PreferredSize( + preferredSize: Size.fromHeight(2), + child: Container(color: Colors.grey.shade100, height: 1), + ), + ); + } +} diff --git a/Mobile Operasional/lib/core/constants/.gitignore b/Mobile Operasional/lib/core/constants/.gitignore new file mode 100644 index 0000000..c71d382 --- /dev/null +++ b/Mobile Operasional/lib/core/constants/.gitignore @@ -0,0 +1,2 @@ +app_temp.dart +app_debug.dart \ No newline at end of file diff --git a/Mobile Operasional/lib/core/constants/app_asset.dart b/Mobile Operasional/lib/core/constants/app_asset.dart new file mode 100644 index 0000000..f2fed2b --- /dev/null +++ b/Mobile Operasional/lib/core/constants/app_asset.dart @@ -0,0 +1,11 @@ +class AppAsset { + static const String LOGO = 'assets/images/Niogu_Logo.png'; + static const String LOGO_UPDATE = 'assets/images/Niogu_Logo_Update_1.0.png'; + static const String SHOPEEPAY_LOGO = 'assets/images/shopeepay.png'; + static const String GOPAY_LOGO = 'assets/images/gopay.png'; + static const String DANA_LOGO = 'assets/images/dana.png'; + static const String BRI_LOGO = 'assets/images/bri.png'; + static const String MANDIRI_LOGO = 'assets/images/mandiri.png'; + static const String BNI_LOGO = 'assets/images/bni.png'; + static const String BCA_LOGO = 'assets/images/bca.png'; +} \ No newline at end of file diff --git a/Mobile Operasional/lib/core/constants/app_color.dart b/Mobile Operasional/lib/core/constants/app_color.dart new file mode 100644 index 0000000..bfeddf0 --- /dev/null +++ b/Mobile Operasional/lib/core/constants/app_color.dart @@ -0,0 +1,6 @@ +import 'package:flutter/material.dart'; + +class AppColor { + static const Color primaryColor = Color(0xFF000080); + static const Color secondaryColor = Color(0xFF009ADE); +} \ No newline at end of file diff --git a/Mobile Operasional/lib/core/constants/app_config.dart b/Mobile Operasional/lib/core/constants/app_config.dart new file mode 100644 index 0000000..f8b7079 --- /dev/null +++ b/Mobile Operasional/lib/core/constants/app_config.dart @@ -0,0 +1,16 @@ +class AppConfig { + // Cukup ganti IP di sini sekali saja + static const String _localIp = "192.168.137.1"; + static const String _port = "8000"; + + // URL untuk Development (Laptop) + static const String _baseUrlDev = "http://$_localIp:$_port/api"; + + // URL untuk Production (Server asli nantinya) + static const String _baseUrlProd = "https://api.niogu.com/api"; + + // Gunakan flag untuk menentukan environment + static const bool _isDevelopment = true; + + static String get BASE_URL => _isDevelopment ? _baseUrlDev : _baseUrlProd; +} diff --git a/Mobile Operasional/lib/core/constants/app_font_size.dart b/Mobile Operasional/lib/core/constants/app_font_size.dart new file mode 100644 index 0000000..68d0f3b --- /dev/null +++ b/Mobile Operasional/lib/core/constants/app_font_size.dart @@ -0,0 +1,6 @@ +class AppFontSize { + static const double extraLarge = 40.0; + static const double large = 18.0; + static const double medium = 16.0; + static const double small = 14.0; +} \ No newline at end of file diff --git a/Mobile Operasional/lib/core/constants/app_generator.dart b/Mobile Operasional/lib/core/constants/app_generator.dart new file mode 100644 index 0000000..fba5cd3 --- /dev/null +++ b/Mobile Operasional/lib/core/constants/app_generator.dart @@ -0,0 +1,19 @@ +import 'dart:math'; + +class AppGenerator { + static String generateBusinessCode() { + const chars = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789'; + final random = Random.secure(); + + final String randomPart = List.generate( + 6, + (index) => chars[random.nextInt(chars.length)], + ).join(); + + final int ms = DateTime.now().millisecondsSinceEpoch; + final String timePart = + chars[ms % chars.length] + chars[(ms ~/ chars.length) % chars.length]; + + return randomPart + timePart; + } +} diff --git a/Mobile Operasional/lib/core/constants/app_key.dart b/Mobile Operasional/lib/core/constants/app_key.dart new file mode 100644 index 0000000..db8c6ab --- /dev/null +++ b/Mobile Operasional/lib/core/constants/app_key.dart @@ -0,0 +1,16 @@ +class AppKey { + static const String DATABASE_ENCRYPTION = 'database_encryption'; + static const String AUTH_TOKEN = 'auth_token'; + static const String TENANT_ID = 'tenant_id'; + static const String TENANT_CODE = 'tenant_code'; + static const String TENANT_NAME = 'tenant_name'; + static const String OUTLET_ID = 'outlet_id'; + static const String OUTLET_ID_BY_OWNER = 'outlet_id_by_owner'; + static const String OUTLET_NAME = 'outlet_name'; + static const String USER_ID = 'user_id'; + static const String USER_NAME = 'user_name'; + static const String USER_EMAIL = 'user_email'; + static const String USER_ROLE = 'user_role'; + static const String USER_PASSWORD = 'user_password'; + static const String USER_LOGIN = 'user_login'; +} \ No newline at end of file diff --git a/Mobile Operasional/lib/core/constants/app_url.dart b/Mobile Operasional/lib/core/constants/app_url.dart new file mode 100644 index 0000000..ec8971c --- /dev/null +++ b/Mobile Operasional/lib/core/constants/app_url.dart @@ -0,0 +1,6 @@ +class AppUrl { + static final String LOGIN = '/login'; + static final String REGISTER = '/register'; + static final String SYNC_UP = '/sync/batch'; + static final String UPLOAD_IMAGE = '/upload/image'; +} diff --git a/Mobile Operasional/lib/core/database/app_database.dart b/Mobile Operasional/lib/core/database/app_database.dart new file mode 100644 index 0000000..4a56707 --- /dev/null +++ b/Mobile Operasional/lib/core/database/app_database.dart @@ -0,0 +1,228 @@ +import 'dart:io'; + +import 'package:drift/drift.dart'; +import 'package:drift/native.dart'; +import 'package:flutter/foundation.dart'; +import 'package:niogu_app/core/database/daos/campaign_dao.dart'; +import 'package:niogu_app/core/database/daos/category_dao.dart'; +import 'package:niogu_app/core/database/daos/customer_address_dao.dart'; +import 'package:niogu_app/core/database/daos/customer_dao.dart'; +import 'package:niogu_app/core/database/daos/order_dao.dart'; +import 'package:niogu_app/core/database/daos/order_item_dao.dart'; +import 'package:niogu_app/core/database/daos/outlet_campaign_dao.dart'; +import 'package:niogu_app/core/database/daos/outlet_dao.dart'; +import 'package:niogu_app/core/database/daos/outlet_inventory_dao.dart'; +import 'package:niogu_app/core/database/daos/product_dao.dart'; +import 'package:niogu_app/core/database/daos/product_like_dao.dart'; +import 'package:niogu_app/core/database/daos/product_review_dao.dart'; +import 'package:niogu_app/core/database/daos/product_variant_dao.dart'; +import 'package:niogu_app/core/database/daos/purchase_dao.dart'; +import 'package:niogu_app/core/database/daos/purchase_item_dao.dart'; +import 'package:niogu_app/core/database/daos/raw_material_dao.dart'; +import 'package:niogu_app/core/database/daos/stock_card_dao.dart'; +import 'package:niogu_app/core/database/daos/supplier_dao.dart'; +import 'package:niogu_app/core/database/daos/sync_queue_dao.dart'; +import 'package:niogu_app/core/database/daos/tenant_dao.dart'; +import 'package:niogu_app/core/database/daos/tenant_payment_method_dao.dart'; +import 'package:niogu_app/core/database/daos/transaction_dao.dart'; +import 'package:niogu_app/core/database/daos/unit_dao.dart'; +import 'package:niogu_app/core/database/daos/receipt_item_dao.dart'; +import 'package:niogu_app/core/database/daos/user_dao.dart'; +import 'package:niogu_app/core/database/tables/campaigns.dart'; +import 'package:niogu_app/core/database/tables/categories.dart'; +import 'package:niogu_app/core/database/tables/customer_addresses.dart'; +import 'package:niogu_app/core/database/tables/customers.dart'; +import 'package:niogu_app/core/database/tables/order_items.dart'; +import 'package:niogu_app/core/database/tables/orders.dart'; +import 'package:niogu_app/core/database/tables/outlet_campaigns.dart'; +import 'package:niogu_app/core/database/tables/outlet_inventories.dart'; +import 'package:niogu_app/core/database/tables/outlets.dart'; +import 'package:niogu_app/core/database/tables/product_likes.dart'; +import 'package:niogu_app/core/database/tables/product_reviews.dart'; +import 'package:niogu_app/core/database/tables/product_variants.dart'; +import 'package:niogu_app/core/database/tables/products.dart'; +import 'package:niogu_app/core/database/tables/purchase_items.dart'; +import 'package:niogu_app/core/database/tables/purchases.dart'; +import 'package:niogu_app/core/database/tables/raw_materials.dart'; +import 'package:niogu_app/core/database/tables/receipt_items.dart'; +import 'package:niogu_app/core/database/tables/stock_cards.dart'; +import 'package:niogu_app/core/database/tables/suppliers.dart'; +import 'package:niogu_app/core/database/tables/sync_queues.dart'; +import 'package:niogu_app/core/database/tables/tenant_payment_methods.dart'; +import 'package:niogu_app/core/database/tables/tenants.dart'; +import 'package:niogu_app/core/database/tables/transactions.dart'; +import 'package:niogu_app/core/database/tables/units.dart'; +import 'package:niogu_app/core/database/tables/users.dart'; +import 'package:niogu_app/core/system/system_secure.dart'; +import 'package:niogu_app/core/utils/string_list_converter.dart'; +import 'package:niogu_app/core/utils/string_map_converter.dart'; +import 'package:niogu_app/core/system/system_setting.dart'; +import 'package:path/path.dart' as path; +import 'package:path_provider/path_provider.dart'; +import 'package:uuid/uuid.dart'; + +part 'app_database.g.dart'; + +@DriftDatabase( + tables: [ + Tenants, + TenantPaymentMethods, + Users, + Customers, + CustomerAddresses, + Outlets, + Categories, + Units, + Products, + ProductVariants, + ProductReviews, + ProductLikes, + RawMaterials, + OutletInventories, + ReceiptItems, + StockCards, + Orders, + OrderItems, + Purchases, + PurchaseItems, + Suppliers, + Transactions, + Campaigns, + OutletCampaigns, + SyncQueues + ], + daos: [ + TenantDao, + TenantPaymentMethodDao, + UserDao, + CustomerDao, + CustomerAddressDao, + OutletDao, + CategoryDao, + UnitDao, + ProductDao, + ProductVariantDao, + ProductReviewDao, + ProductLikeDao, + RawMaterialDao, + OutletInventoryDao, + ReceiptItemDao, + StockCardDao, + OrderDao, + OrderItemDao, + SupplierDao, + PurchaseDao, + PurchaseItemDao, + TransactionDao, + CampaignDao, + OutletCampaignDao, + SyncQueueDao + ], +) +class AppDatabase extends _$AppDatabase { + AppDatabase() : super(_openConnection()); + + @override + // TODO: implement schemaVersion + int get schemaVersion => 1; + + TenantDao get tenantDao => TenantDao(this); + TenantPaymentMethodDao get tenantPaymentMethodDao => + TenantPaymentMethodDao(this); + UserDao get userDao => UserDao(this); + CustomerDao get customerDao => CustomerDao(this); + CustomerAddressDao get customerAddressDao => CustomerAddressDao(this); + OutletDao get outletDao => OutletDao(this); + CategoryDao get categoryDao => CategoryDao(this); + UnitDao get unitDao => UnitDao(this); + ProductDao get productDao => ProductDao(this); + ProductVariantDao get productVariantDao => ProductVariantDao(this); + ProductReviewDao get productReviewDao => ProductReviewDao(this); + ProductLikeDao get productLikeDao => ProductLikeDao(this); + RawMaterialDao get rawMaterialDao => RawMaterialDao(this); + OutletInventoryDao get outletInventoryDao => OutletInventoryDao(this); + ReceiptItemDao get receiptItemDao => ReceiptItemDao(this); + StockCardDao get stockCardDao => StockCardDao(this); + OrderDao get orderDao => OrderDao(this); + OrderItemDao get orderItemDao => OrderItemDao(this); + PurchaseDao get purchaseDao => PurchaseDao(this); + PurchaseItemDao get purchaseItemDao => PurchaseItemDao(this); + SupplierDao get supplierDao => SupplierDao(this); + CampaignDao get campaignDao => CampaignDao(this); + OutletCampaignDao get outletCampaignDao => OutletCampaignDao(this); + SyncQueueDao get syncQueueDao => SyncQueueDao(this); + + @override + // TODO: implement migration + MigrationStrategy get migration => MigrationStrategy( + onCreate: (m) async { + await m.createAll(); + + await customStatement( + 'CREATE UNIQUE INDEX outlet_email_unique_active ON outlets(email) WHERE deleted_at IS NULL', + ); + + await customStatement( + 'CREATE UNIQUE INDEX outlet_phone_unique_active ON outlets(phone_number) WHERE deleted_at IS NULL', + ); + + await customStatement( + 'CREATE UNIQUE INDEX users_email_unique_active ON users(email) WHERE deleted_at IS NULL', + ); + + await customStatement( + 'CREATE UNIQUE INDEX users_phone_unique_active ON users(phone_number) WHERE deleted_at IS NULL', + ); + + await customStatement( + 'CREATE INDEX IF NOT EXISTS idx_products_created_at ON products (created_at)', + ); + + await customStatement( + 'CREATE INDEX IF NOT EXISTS idx_products_deleted_at ON products (deleted_at)', + ); + + await customStatement( + 'CREATE INDEX IF NOT EXISTS idx_outlet_inventories_lookup ON outlet_inventories (item_type, item_id)', + ); + + await customStatement( + 'CREATE INDEX IF NOT EXISTS idx_outlet_inventories_deleted_at ON outlet_inventories (deleted_at)', + ); + + await customStatement( + 'CREATE INDEX IF NOT EXISTS idx_transaction_lookup ON transactions (reference_type, reference_id)', + ); + }, + onUpgrade: (m, from, to) async { + // Handle database upgrades if needed + }, + ); +} + +LazyDatabase _openConnection() { + return LazyDatabase(() async { + final tenantCode = await SystemSetting.getTenantCode(); + + final dbFolder = await getApplicationDocumentsDirectory(); + + final file = File(path.join(dbFolder.path, 'NIOGU_$tenantCode.sqlite')); + + var databaseKey = await SystemSecure.getDatabaseKey(tenantCode!); + + if (databaseKey == null) { + await SystemSecure.generateDatabaseKey(tenantCode); + + databaseKey = await SystemSecure.getDatabaseKey(tenantCode); + } + + return NativeDatabase.createInBackground( + file, + setup: (database) { + database.execute('PRAGMA foreign_keys = ON'); + database.execute("PRAGMA key = '$databaseKey';"); + }, + logStatements: kDebugMode, + ); + }); +} diff --git a/Mobile Operasional/lib/core/database/app_database.g.dart b/Mobile Operasional/lib/core/database/app_database.g.dart new file mode 100644 index 0000000..df7f0cd --- /dev/null +++ b/Mobile Operasional/lib/core/database/app_database.g.dart @@ -0,0 +1,37375 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'app_database.dart'; + +// ignore_for_file: type=lint +class $TenantsTable extends Tenants with TableInfo<$TenantsTable, Tenant> { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + $TenantsTable(this.attachedDatabase, [this._alias]); + static const VerificationMeta _localIdMeta = const VerificationMeta( + 'localId', + ); + @override + late final GeneratedColumn localId = GeneratedColumn( + 'local_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: false, + clientDefault: () => const Uuid().v7().toString(), + ); + static const VerificationMeta _serverIdMeta = const VerificationMeta( + 'serverId', + ); + @override + late final GeneratedColumn serverId = GeneratedColumn( + 'server_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _businessCodeMeta = const VerificationMeta( + 'businessCode', + ); + @override + late final GeneratedColumn businessCode = GeneratedColumn( + 'business_code', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways('UNIQUE'), + ); + static const VerificationMeta _businessNameMeta = const VerificationMeta( + 'businessName', + ); + @override + late final GeneratedColumn businessName = GeneratedColumn( + 'business_name', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + static const VerificationMeta _businessEmailMeta = const VerificationMeta( + 'businessEmail', + ); + @override + late final GeneratedColumn businessEmail = GeneratedColumn( + 'business_email', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways('UNIQUE'), + ); + static const VerificationMeta _businessPhoneMeta = const VerificationMeta( + 'businessPhone', + ); + @override + late final GeneratedColumn businessPhone = GeneratedColumn( + 'business_phone', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways('UNIQUE'), + ); + static const VerificationMeta _businessTypeMeta = const VerificationMeta( + 'businessType', + ); + @override + late final GeneratedColumn businessType = GeneratedColumn( + 'business_type', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _localLogoPathMeta = const VerificationMeta( + 'localLogoPath', + ); + @override + late final GeneratedColumn localLogoPath = GeneratedColumn( + 'local_logo_path', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _serverLogoUrlMeta = const VerificationMeta( + 'serverLogoUrl', + ); + @override + late final GeneratedColumn serverLogoUrl = GeneratedColumn( + 'server_logo_url', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _subdomainMeta = const VerificationMeta( + 'subdomain', + ); + @override + late final GeneratedColumn subdomain = GeneratedColumn( + 'subdomain', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways('UNIQUE'), + ); + static const VerificationMeta _onlinStoreNameMeta = const VerificationMeta( + 'onlinStoreName', + ); + @override + late final GeneratedColumn onlinStoreName = GeneratedColumn( + 'onlin_store_name', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _onlineOpenTimeMeta = const VerificationMeta( + 'onlineOpenTime', + ); + @override + late final GeneratedColumn onlineOpenTime = GeneratedColumn( + 'online_open_time', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _onlineCloseTimeMeta = const VerificationMeta( + 'onlineCloseTime', + ); + @override + late final GeneratedColumn onlineCloseTime = GeneratedColumn( + 'online_close_time', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _isCloseServiceMeta = const VerificationMeta( + 'isCloseService', + ); + @override + late final GeneratedColumn isCloseService = GeneratedColumn( + 'is_close_service', + aliasedName, + false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("is_close_service" IN (0, 1))', + ), + defaultValue: const Constant(false), + ); + static const VerificationMeta _preparationTimeMinutesMeta = + const VerificationMeta('preparationTimeMinutes'); + @override + late final GeneratedColumn preparationTimeMinutes = GeneratedColumn( + 'preparation_time_minutes', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultValue: const Constant(0), + ); + static const VerificationMeta _orderIntervalMinutesMeta = + const VerificationMeta('orderIntervalMinutes'); + @override + late final GeneratedColumn orderIntervalMinutes = GeneratedColumn( + 'order_interval_minutes', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultValue: const Constant(0), + ); + static const VerificationMeta _allowCodMeta = const VerificationMeta( + 'allowCod', + ); + @override + late final GeneratedColumn allowCod = GeneratedColumn( + 'allow_cod', + aliasedName, + false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("allow_cod" IN (0, 1))', + ), + defaultValue: const Constant(false), + ); + static const VerificationMeta _maxDeliveryRadiusKmMeta = + const VerificationMeta('maxDeliveryRadiusKm'); + @override + late final GeneratedColumn maxDeliveryRadiusKm = + GeneratedColumn( + 'max_delivery_radius_km', + aliasedName, + false, + type: DriftSqlType.double, + requiredDuringInsert: false, + defaultValue: const Constant(0.0), + ); + static const VerificationMeta _deliveryFeeTypeMeta = const VerificationMeta( + 'deliveryFeeType', + ); + @override + late final GeneratedColumn deliveryFeeType = GeneratedColumn( + 'delivery_fee_type', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultValue: const Constant('fixed'), + ); + static const VerificationMeta _deliveryFlatFeeMeta = const VerificationMeta( + 'deliveryFlatFee', + ); + @override + late final GeneratedColumn deliveryFlatFee = GeneratedColumn( + 'delivery_flat_fee', + aliasedName, + false, + type: DriftSqlType.double, + requiredDuringInsert: false, + defaultValue: const Constant(0.0), + ); + static const VerificationMeta _deliveryFeePerKmMeta = const VerificationMeta( + 'deliveryFeePerKm', + ); + @override + late final GeneratedColumn deliveryFeePerKm = GeneratedColumn( + 'delivery_fee_per_km', + aliasedName, + false, + type: DriftSqlType.double, + requiredDuringInsert: false, + defaultValue: const Constant(0.0), + ); + static const VerificationMeta _deliveryBaseFeeMeta = const VerificationMeta( + 'deliveryBaseFee', + ); + @override + late final GeneratedColumn deliveryBaseFee = GeneratedColumn( + 'delivery_base_fee', + aliasedName, + false, + type: DriftSqlType.double, + requiredDuringInsert: false, + defaultValue: const Constant(0.0), + ); + static const VerificationMeta _reviewDisplayTypeMeta = const VerificationMeta( + 'reviewDisplayType', + ); + @override + late final GeneratedColumn reviewDisplayType = + GeneratedColumn( + 'review_display_type', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultValue: const Constant('rating_only'), + ); + static const VerificationMeta _reviewDisplayPolicyMeta = + const VerificationMeta('reviewDisplayPolicy'); + @override + late final GeneratedColumn reviewDisplayPolicy = + GeneratedColumn( + 'review_display_policy', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultValue: const Constant('four_and_five_star_only'), + ); + static const VerificationMeta _subscriptionLevelMeta = const VerificationMeta( + 'subscriptionLevel', + ); + @override + late final GeneratedColumn subscriptionLevel = + GeneratedColumn( + 'subscription_level', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultValue: const Constant('standart'), + ); + static const VerificationMeta _limitOutletMeta = const VerificationMeta( + 'limitOutlet', + ); + @override + late final GeneratedColumn limitOutlet = GeneratedColumn( + 'limit_outlet', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultValue: const Constant(5), + ); + static const VerificationMeta _subscriptionExpiresAtMeta = + const VerificationMeta('subscriptionExpiresAt'); + @override + late final GeneratedColumn subscriptionExpiresAt = + GeneratedColumn( + 'subscription_expires_at', + aliasedName, + true, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + ); + static const VerificationMeta _syncStatusMeta = const VerificationMeta( + 'syncStatus', + ); + @override + late final GeneratedColumn syncStatus = GeneratedColumn( + 'sync_status', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultValue: const Constant('dirty'), + ); + static const VerificationMeta _createdAtMeta = const VerificationMeta( + 'createdAt', + ); + @override + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: currentDateAndTime, + ); + static const VerificationMeta _updatedAtMeta = const VerificationMeta( + 'updatedAt', + ); + @override + late final GeneratedColumn updatedAt = GeneratedColumn( + 'updated_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: currentDateAndTime, + ); + @override + List get $columns => [ + localId, + serverId, + businessCode, + businessName, + businessEmail, + businessPhone, + businessType, + localLogoPath, + serverLogoUrl, + subdomain, + onlinStoreName, + onlineOpenTime, + onlineCloseTime, + isCloseService, + preparationTimeMinutes, + orderIntervalMinutes, + allowCod, + maxDeliveryRadiusKm, + deliveryFeeType, + deliveryFlatFee, + deliveryFeePerKm, + deliveryBaseFee, + reviewDisplayType, + reviewDisplayPolicy, + subscriptionLevel, + limitOutlet, + subscriptionExpiresAt, + syncStatus, + createdAt, + updatedAt, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'tenants'; + @override + VerificationContext validateIntegrity( + Insertable instance, { + bool isInserting = false, + }) { + final context = VerificationContext(); + final data = instance.toColumns(true); + if (data.containsKey('local_id')) { + context.handle( + _localIdMeta, + localId.isAcceptableOrUnknown(data['local_id']!, _localIdMeta), + ); + } + if (data.containsKey('server_id')) { + context.handle( + _serverIdMeta, + serverId.isAcceptableOrUnknown(data['server_id']!, _serverIdMeta), + ); + } + if (data.containsKey('business_code')) { + context.handle( + _businessCodeMeta, + businessCode.isAcceptableOrUnknown( + data['business_code']!, + _businessCodeMeta, + ), + ); + } else if (isInserting) { + context.missing(_businessCodeMeta); + } + if (data.containsKey('business_name')) { + context.handle( + _businessNameMeta, + businessName.isAcceptableOrUnknown( + data['business_name']!, + _businessNameMeta, + ), + ); + } else if (isInserting) { + context.missing(_businessNameMeta); + } + if (data.containsKey('business_email')) { + context.handle( + _businessEmailMeta, + businessEmail.isAcceptableOrUnknown( + data['business_email']!, + _businessEmailMeta, + ), + ); + } + if (data.containsKey('business_phone')) { + context.handle( + _businessPhoneMeta, + businessPhone.isAcceptableOrUnknown( + data['business_phone']!, + _businessPhoneMeta, + ), + ); + } + if (data.containsKey('business_type')) { + context.handle( + _businessTypeMeta, + businessType.isAcceptableOrUnknown( + data['business_type']!, + _businessTypeMeta, + ), + ); + } + if (data.containsKey('local_logo_path')) { + context.handle( + _localLogoPathMeta, + localLogoPath.isAcceptableOrUnknown( + data['local_logo_path']!, + _localLogoPathMeta, + ), + ); + } + if (data.containsKey('server_logo_url')) { + context.handle( + _serverLogoUrlMeta, + serverLogoUrl.isAcceptableOrUnknown( + data['server_logo_url']!, + _serverLogoUrlMeta, + ), + ); + } + if (data.containsKey('subdomain')) { + context.handle( + _subdomainMeta, + subdomain.isAcceptableOrUnknown(data['subdomain']!, _subdomainMeta), + ); + } + if (data.containsKey('onlin_store_name')) { + context.handle( + _onlinStoreNameMeta, + onlinStoreName.isAcceptableOrUnknown( + data['onlin_store_name']!, + _onlinStoreNameMeta, + ), + ); + } + if (data.containsKey('online_open_time')) { + context.handle( + _onlineOpenTimeMeta, + onlineOpenTime.isAcceptableOrUnknown( + data['online_open_time']!, + _onlineOpenTimeMeta, + ), + ); + } + if (data.containsKey('online_close_time')) { + context.handle( + _onlineCloseTimeMeta, + onlineCloseTime.isAcceptableOrUnknown( + data['online_close_time']!, + _onlineCloseTimeMeta, + ), + ); + } + if (data.containsKey('is_close_service')) { + context.handle( + _isCloseServiceMeta, + isCloseService.isAcceptableOrUnknown( + data['is_close_service']!, + _isCloseServiceMeta, + ), + ); + } + if (data.containsKey('preparation_time_minutes')) { + context.handle( + _preparationTimeMinutesMeta, + preparationTimeMinutes.isAcceptableOrUnknown( + data['preparation_time_minutes']!, + _preparationTimeMinutesMeta, + ), + ); + } + if (data.containsKey('order_interval_minutes')) { + context.handle( + _orderIntervalMinutesMeta, + orderIntervalMinutes.isAcceptableOrUnknown( + data['order_interval_minutes']!, + _orderIntervalMinutesMeta, + ), + ); + } + if (data.containsKey('allow_cod')) { + context.handle( + _allowCodMeta, + allowCod.isAcceptableOrUnknown(data['allow_cod']!, _allowCodMeta), + ); + } + if (data.containsKey('max_delivery_radius_km')) { + context.handle( + _maxDeliveryRadiusKmMeta, + maxDeliveryRadiusKm.isAcceptableOrUnknown( + data['max_delivery_radius_km']!, + _maxDeliveryRadiusKmMeta, + ), + ); + } + if (data.containsKey('delivery_fee_type')) { + context.handle( + _deliveryFeeTypeMeta, + deliveryFeeType.isAcceptableOrUnknown( + data['delivery_fee_type']!, + _deliveryFeeTypeMeta, + ), + ); + } + if (data.containsKey('delivery_flat_fee')) { + context.handle( + _deliveryFlatFeeMeta, + deliveryFlatFee.isAcceptableOrUnknown( + data['delivery_flat_fee']!, + _deliveryFlatFeeMeta, + ), + ); + } + if (data.containsKey('delivery_fee_per_km')) { + context.handle( + _deliveryFeePerKmMeta, + deliveryFeePerKm.isAcceptableOrUnknown( + data['delivery_fee_per_km']!, + _deliveryFeePerKmMeta, + ), + ); + } + if (data.containsKey('delivery_base_fee')) { + context.handle( + _deliveryBaseFeeMeta, + deliveryBaseFee.isAcceptableOrUnknown( + data['delivery_base_fee']!, + _deliveryBaseFeeMeta, + ), + ); + } + if (data.containsKey('review_display_type')) { + context.handle( + _reviewDisplayTypeMeta, + reviewDisplayType.isAcceptableOrUnknown( + data['review_display_type']!, + _reviewDisplayTypeMeta, + ), + ); + } + if (data.containsKey('review_display_policy')) { + context.handle( + _reviewDisplayPolicyMeta, + reviewDisplayPolicy.isAcceptableOrUnknown( + data['review_display_policy']!, + _reviewDisplayPolicyMeta, + ), + ); + } + if (data.containsKey('subscription_level')) { + context.handle( + _subscriptionLevelMeta, + subscriptionLevel.isAcceptableOrUnknown( + data['subscription_level']!, + _subscriptionLevelMeta, + ), + ); + } + if (data.containsKey('limit_outlet')) { + context.handle( + _limitOutletMeta, + limitOutlet.isAcceptableOrUnknown( + data['limit_outlet']!, + _limitOutletMeta, + ), + ); + } + if (data.containsKey('subscription_expires_at')) { + context.handle( + _subscriptionExpiresAtMeta, + subscriptionExpiresAt.isAcceptableOrUnknown( + data['subscription_expires_at']!, + _subscriptionExpiresAtMeta, + ), + ); + } + if (data.containsKey('sync_status')) { + context.handle( + _syncStatusMeta, + syncStatus.isAcceptableOrUnknown(data['sync_status']!, _syncStatusMeta), + ); + } + if (data.containsKey('created_at')) { + context.handle( + _createdAtMeta, + createdAt.isAcceptableOrUnknown(data['created_at']!, _createdAtMeta), + ); + } + if (data.containsKey('updated_at')) { + context.handle( + _updatedAtMeta, + updatedAt.isAcceptableOrUnknown(data['updated_at']!, _updatedAtMeta), + ); + } + return context; + } + + @override + Set get $primaryKey => {localId}; + @override + Tenant map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return Tenant( + localId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}local_id'], + )!, + serverId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}server_id'], + ), + businessCode: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}business_code'], + )!, + businessName: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}business_name'], + )!, + businessEmail: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}business_email'], + ), + businessPhone: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}business_phone'], + ), + businessType: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}business_type'], + ), + localLogoPath: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}local_logo_path'], + ), + serverLogoUrl: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}server_logo_url'], + ), + subdomain: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}subdomain'], + ), + onlinStoreName: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}onlin_store_name'], + ), + onlineOpenTime: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}online_open_time'], + ), + onlineCloseTime: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}online_close_time'], + ), + isCloseService: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}is_close_service'], + )!, + preparationTimeMinutes: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}preparation_time_minutes'], + )!, + orderIntervalMinutes: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}order_interval_minutes'], + )!, + allowCod: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}allow_cod'], + )!, + maxDeliveryRadiusKm: attachedDatabase.typeMapping.read( + DriftSqlType.double, + data['${effectivePrefix}max_delivery_radius_km'], + )!, + deliveryFeeType: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}delivery_fee_type'], + )!, + deliveryFlatFee: attachedDatabase.typeMapping.read( + DriftSqlType.double, + data['${effectivePrefix}delivery_flat_fee'], + )!, + deliveryFeePerKm: attachedDatabase.typeMapping.read( + DriftSqlType.double, + data['${effectivePrefix}delivery_fee_per_km'], + )!, + deliveryBaseFee: attachedDatabase.typeMapping.read( + DriftSqlType.double, + data['${effectivePrefix}delivery_base_fee'], + )!, + reviewDisplayType: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}review_display_type'], + )!, + reviewDisplayPolicy: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}review_display_policy'], + )!, + subscriptionLevel: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}subscription_level'], + )!, + limitOutlet: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}limit_outlet'], + )!, + subscriptionExpiresAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}subscription_expires_at'], + ), + syncStatus: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}sync_status'], + )!, + createdAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}created_at'], + )!, + updatedAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}updated_at'], + )!, + ); + } + + @override + $TenantsTable createAlias(String alias) { + return $TenantsTable(attachedDatabase, alias); + } +} + +class Tenant extends DataClass implements Insertable { + final String localId; + final String? serverId; + final String businessCode; + final String businessName; + final String? businessEmail; + final String? businessPhone; + final String? businessType; + final String? localLogoPath; + final String? serverLogoUrl; + final String? subdomain; + final String? onlinStoreName; + final String? onlineOpenTime; + final String? onlineCloseTime; + final bool isCloseService; + final int preparationTimeMinutes; + final int orderIntervalMinutes; + final bool allowCod; + final double maxDeliveryRadiusKm; + final String deliveryFeeType; + final double deliveryFlatFee; + final double deliveryFeePerKm; + final double deliveryBaseFee; + final String reviewDisplayType; + final String reviewDisplayPolicy; + final String subscriptionLevel; + final int limitOutlet; + final DateTime? subscriptionExpiresAt; + final String syncStatus; + final DateTime createdAt; + final DateTime updatedAt; + const Tenant({ + required this.localId, + this.serverId, + required this.businessCode, + required this.businessName, + this.businessEmail, + this.businessPhone, + this.businessType, + this.localLogoPath, + this.serverLogoUrl, + this.subdomain, + this.onlinStoreName, + this.onlineOpenTime, + this.onlineCloseTime, + required this.isCloseService, + required this.preparationTimeMinutes, + required this.orderIntervalMinutes, + required this.allowCod, + required this.maxDeliveryRadiusKm, + required this.deliveryFeeType, + required this.deliveryFlatFee, + required this.deliveryFeePerKm, + required this.deliveryBaseFee, + required this.reviewDisplayType, + required this.reviewDisplayPolicy, + required this.subscriptionLevel, + required this.limitOutlet, + this.subscriptionExpiresAt, + required this.syncStatus, + required this.createdAt, + required this.updatedAt, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['local_id'] = Variable(localId); + if (!nullToAbsent || serverId != null) { + map['server_id'] = Variable(serverId); + } + map['business_code'] = Variable(businessCode); + map['business_name'] = Variable(businessName); + if (!nullToAbsent || businessEmail != null) { + map['business_email'] = Variable(businessEmail); + } + if (!nullToAbsent || businessPhone != null) { + map['business_phone'] = Variable(businessPhone); + } + if (!nullToAbsent || businessType != null) { + map['business_type'] = Variable(businessType); + } + if (!nullToAbsent || localLogoPath != null) { + map['local_logo_path'] = Variable(localLogoPath); + } + if (!nullToAbsent || serverLogoUrl != null) { + map['server_logo_url'] = Variable(serverLogoUrl); + } + if (!nullToAbsent || subdomain != null) { + map['subdomain'] = Variable(subdomain); + } + if (!nullToAbsent || onlinStoreName != null) { + map['onlin_store_name'] = Variable(onlinStoreName); + } + if (!nullToAbsent || onlineOpenTime != null) { + map['online_open_time'] = Variable(onlineOpenTime); + } + if (!nullToAbsent || onlineCloseTime != null) { + map['online_close_time'] = Variable(onlineCloseTime); + } + map['is_close_service'] = Variable(isCloseService); + map['preparation_time_minutes'] = Variable(preparationTimeMinutes); + map['order_interval_minutes'] = Variable(orderIntervalMinutes); + map['allow_cod'] = Variable(allowCod); + map['max_delivery_radius_km'] = Variable(maxDeliveryRadiusKm); + map['delivery_fee_type'] = Variable(deliveryFeeType); + map['delivery_flat_fee'] = Variable(deliveryFlatFee); + map['delivery_fee_per_km'] = Variable(deliveryFeePerKm); + map['delivery_base_fee'] = Variable(deliveryBaseFee); + map['review_display_type'] = Variable(reviewDisplayType); + map['review_display_policy'] = Variable(reviewDisplayPolicy); + map['subscription_level'] = Variable(subscriptionLevel); + map['limit_outlet'] = Variable(limitOutlet); + if (!nullToAbsent || subscriptionExpiresAt != null) { + map['subscription_expires_at'] = Variable( + subscriptionExpiresAt, + ); + } + map['sync_status'] = Variable(syncStatus); + map['created_at'] = Variable(createdAt); + map['updated_at'] = Variable(updatedAt); + return map; + } + + TenantsCompanion toCompanion(bool nullToAbsent) { + return TenantsCompanion( + localId: Value(localId), + serverId: serverId == null && nullToAbsent + ? const Value.absent() + : Value(serverId), + businessCode: Value(businessCode), + businessName: Value(businessName), + businessEmail: businessEmail == null && nullToAbsent + ? const Value.absent() + : Value(businessEmail), + businessPhone: businessPhone == null && nullToAbsent + ? const Value.absent() + : Value(businessPhone), + businessType: businessType == null && nullToAbsent + ? const Value.absent() + : Value(businessType), + localLogoPath: localLogoPath == null && nullToAbsent + ? const Value.absent() + : Value(localLogoPath), + serverLogoUrl: serverLogoUrl == null && nullToAbsent + ? const Value.absent() + : Value(serverLogoUrl), + subdomain: subdomain == null && nullToAbsent + ? const Value.absent() + : Value(subdomain), + onlinStoreName: onlinStoreName == null && nullToAbsent + ? const Value.absent() + : Value(onlinStoreName), + onlineOpenTime: onlineOpenTime == null && nullToAbsent + ? const Value.absent() + : Value(onlineOpenTime), + onlineCloseTime: onlineCloseTime == null && nullToAbsent + ? const Value.absent() + : Value(onlineCloseTime), + isCloseService: Value(isCloseService), + preparationTimeMinutes: Value(preparationTimeMinutes), + orderIntervalMinutes: Value(orderIntervalMinutes), + allowCod: Value(allowCod), + maxDeliveryRadiusKm: Value(maxDeliveryRadiusKm), + deliveryFeeType: Value(deliveryFeeType), + deliveryFlatFee: Value(deliveryFlatFee), + deliveryFeePerKm: Value(deliveryFeePerKm), + deliveryBaseFee: Value(deliveryBaseFee), + reviewDisplayType: Value(reviewDisplayType), + reviewDisplayPolicy: Value(reviewDisplayPolicy), + subscriptionLevel: Value(subscriptionLevel), + limitOutlet: Value(limitOutlet), + subscriptionExpiresAt: subscriptionExpiresAt == null && nullToAbsent + ? const Value.absent() + : Value(subscriptionExpiresAt), + syncStatus: Value(syncStatus), + createdAt: Value(createdAt), + updatedAt: Value(updatedAt), + ); + } + + factory Tenant.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return Tenant( + localId: serializer.fromJson(json['localId']), + serverId: serializer.fromJson(json['serverId']), + businessCode: serializer.fromJson(json['businessCode']), + businessName: serializer.fromJson(json['businessName']), + businessEmail: serializer.fromJson(json['businessEmail']), + businessPhone: serializer.fromJson(json['businessPhone']), + businessType: serializer.fromJson(json['businessType']), + localLogoPath: serializer.fromJson(json['localLogoPath']), + serverLogoUrl: serializer.fromJson(json['serverLogoUrl']), + subdomain: serializer.fromJson(json['subdomain']), + onlinStoreName: serializer.fromJson(json['onlinStoreName']), + onlineOpenTime: serializer.fromJson(json['onlineOpenTime']), + onlineCloseTime: serializer.fromJson(json['onlineCloseTime']), + isCloseService: serializer.fromJson(json['isCloseService']), + preparationTimeMinutes: serializer.fromJson( + json['preparationTimeMinutes'], + ), + orderIntervalMinutes: serializer.fromJson( + json['orderIntervalMinutes'], + ), + allowCod: serializer.fromJson(json['allowCod']), + maxDeliveryRadiusKm: serializer.fromJson( + json['maxDeliveryRadiusKm'], + ), + deliveryFeeType: serializer.fromJson(json['deliveryFeeType']), + deliveryFlatFee: serializer.fromJson(json['deliveryFlatFee']), + deliveryFeePerKm: serializer.fromJson(json['deliveryFeePerKm']), + deliveryBaseFee: serializer.fromJson(json['deliveryBaseFee']), + reviewDisplayType: serializer.fromJson(json['reviewDisplayType']), + reviewDisplayPolicy: serializer.fromJson( + json['reviewDisplayPolicy'], + ), + subscriptionLevel: serializer.fromJson(json['subscriptionLevel']), + limitOutlet: serializer.fromJson(json['limitOutlet']), + subscriptionExpiresAt: serializer.fromJson( + json['subscriptionExpiresAt'], + ), + syncStatus: serializer.fromJson(json['syncStatus']), + createdAt: serializer.fromJson(json['createdAt']), + updatedAt: serializer.fromJson(json['updatedAt']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'localId': serializer.toJson(localId), + 'serverId': serializer.toJson(serverId), + 'businessCode': serializer.toJson(businessCode), + 'businessName': serializer.toJson(businessName), + 'businessEmail': serializer.toJson(businessEmail), + 'businessPhone': serializer.toJson(businessPhone), + 'businessType': serializer.toJson(businessType), + 'localLogoPath': serializer.toJson(localLogoPath), + 'serverLogoUrl': serializer.toJson(serverLogoUrl), + 'subdomain': serializer.toJson(subdomain), + 'onlinStoreName': serializer.toJson(onlinStoreName), + 'onlineOpenTime': serializer.toJson(onlineOpenTime), + 'onlineCloseTime': serializer.toJson(onlineCloseTime), + 'isCloseService': serializer.toJson(isCloseService), + 'preparationTimeMinutes': serializer.toJson(preparationTimeMinutes), + 'orderIntervalMinutes': serializer.toJson(orderIntervalMinutes), + 'allowCod': serializer.toJson(allowCod), + 'maxDeliveryRadiusKm': serializer.toJson(maxDeliveryRadiusKm), + 'deliveryFeeType': serializer.toJson(deliveryFeeType), + 'deliveryFlatFee': serializer.toJson(deliveryFlatFee), + 'deliveryFeePerKm': serializer.toJson(deliveryFeePerKm), + 'deliveryBaseFee': serializer.toJson(deliveryBaseFee), + 'reviewDisplayType': serializer.toJson(reviewDisplayType), + 'reviewDisplayPolicy': serializer.toJson(reviewDisplayPolicy), + 'subscriptionLevel': serializer.toJson(subscriptionLevel), + 'limitOutlet': serializer.toJson(limitOutlet), + 'subscriptionExpiresAt': serializer.toJson( + subscriptionExpiresAt, + ), + 'syncStatus': serializer.toJson(syncStatus), + 'createdAt': serializer.toJson(createdAt), + 'updatedAt': serializer.toJson(updatedAt), + }; + } + + Tenant copyWith({ + String? localId, + Value serverId = const Value.absent(), + String? businessCode, + String? businessName, + Value businessEmail = const Value.absent(), + Value businessPhone = const Value.absent(), + Value businessType = const Value.absent(), + Value localLogoPath = const Value.absent(), + Value serverLogoUrl = const Value.absent(), + Value subdomain = const Value.absent(), + Value onlinStoreName = const Value.absent(), + Value onlineOpenTime = const Value.absent(), + Value onlineCloseTime = const Value.absent(), + bool? isCloseService, + int? preparationTimeMinutes, + int? orderIntervalMinutes, + bool? allowCod, + double? maxDeliveryRadiusKm, + String? deliveryFeeType, + double? deliveryFlatFee, + double? deliveryFeePerKm, + double? deliveryBaseFee, + String? reviewDisplayType, + String? reviewDisplayPolicy, + String? subscriptionLevel, + int? limitOutlet, + Value subscriptionExpiresAt = const Value.absent(), + String? syncStatus, + DateTime? createdAt, + DateTime? updatedAt, + }) => Tenant( + localId: localId ?? this.localId, + serverId: serverId.present ? serverId.value : this.serverId, + businessCode: businessCode ?? this.businessCode, + businessName: businessName ?? this.businessName, + businessEmail: businessEmail.present + ? businessEmail.value + : this.businessEmail, + businessPhone: businessPhone.present + ? businessPhone.value + : this.businessPhone, + businessType: businessType.present ? businessType.value : this.businessType, + localLogoPath: localLogoPath.present + ? localLogoPath.value + : this.localLogoPath, + serverLogoUrl: serverLogoUrl.present + ? serverLogoUrl.value + : this.serverLogoUrl, + subdomain: subdomain.present ? subdomain.value : this.subdomain, + onlinStoreName: onlinStoreName.present + ? onlinStoreName.value + : this.onlinStoreName, + onlineOpenTime: onlineOpenTime.present + ? onlineOpenTime.value + : this.onlineOpenTime, + onlineCloseTime: onlineCloseTime.present + ? onlineCloseTime.value + : this.onlineCloseTime, + isCloseService: isCloseService ?? this.isCloseService, + preparationTimeMinutes: + preparationTimeMinutes ?? this.preparationTimeMinutes, + orderIntervalMinutes: orderIntervalMinutes ?? this.orderIntervalMinutes, + allowCod: allowCod ?? this.allowCod, + maxDeliveryRadiusKm: maxDeliveryRadiusKm ?? this.maxDeliveryRadiusKm, + deliveryFeeType: deliveryFeeType ?? this.deliveryFeeType, + deliveryFlatFee: deliveryFlatFee ?? this.deliveryFlatFee, + deliveryFeePerKm: deliveryFeePerKm ?? this.deliveryFeePerKm, + deliveryBaseFee: deliveryBaseFee ?? this.deliveryBaseFee, + reviewDisplayType: reviewDisplayType ?? this.reviewDisplayType, + reviewDisplayPolicy: reviewDisplayPolicy ?? this.reviewDisplayPolicy, + subscriptionLevel: subscriptionLevel ?? this.subscriptionLevel, + limitOutlet: limitOutlet ?? this.limitOutlet, + subscriptionExpiresAt: subscriptionExpiresAt.present + ? subscriptionExpiresAt.value + : this.subscriptionExpiresAt, + syncStatus: syncStatus ?? this.syncStatus, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + ); + Tenant copyWithCompanion(TenantsCompanion data) { + return Tenant( + localId: data.localId.present ? data.localId.value : this.localId, + serverId: data.serverId.present ? data.serverId.value : this.serverId, + businessCode: data.businessCode.present + ? data.businessCode.value + : this.businessCode, + businessName: data.businessName.present + ? data.businessName.value + : this.businessName, + businessEmail: data.businessEmail.present + ? data.businessEmail.value + : this.businessEmail, + businessPhone: data.businessPhone.present + ? data.businessPhone.value + : this.businessPhone, + businessType: data.businessType.present + ? data.businessType.value + : this.businessType, + localLogoPath: data.localLogoPath.present + ? data.localLogoPath.value + : this.localLogoPath, + serverLogoUrl: data.serverLogoUrl.present + ? data.serverLogoUrl.value + : this.serverLogoUrl, + subdomain: data.subdomain.present ? data.subdomain.value : this.subdomain, + onlinStoreName: data.onlinStoreName.present + ? data.onlinStoreName.value + : this.onlinStoreName, + onlineOpenTime: data.onlineOpenTime.present + ? data.onlineOpenTime.value + : this.onlineOpenTime, + onlineCloseTime: data.onlineCloseTime.present + ? data.onlineCloseTime.value + : this.onlineCloseTime, + isCloseService: data.isCloseService.present + ? data.isCloseService.value + : this.isCloseService, + preparationTimeMinutes: data.preparationTimeMinutes.present + ? data.preparationTimeMinutes.value + : this.preparationTimeMinutes, + orderIntervalMinutes: data.orderIntervalMinutes.present + ? data.orderIntervalMinutes.value + : this.orderIntervalMinutes, + allowCod: data.allowCod.present ? data.allowCod.value : this.allowCod, + maxDeliveryRadiusKm: data.maxDeliveryRadiusKm.present + ? data.maxDeliveryRadiusKm.value + : this.maxDeliveryRadiusKm, + deliveryFeeType: data.deliveryFeeType.present + ? data.deliveryFeeType.value + : this.deliveryFeeType, + deliveryFlatFee: data.deliveryFlatFee.present + ? data.deliveryFlatFee.value + : this.deliveryFlatFee, + deliveryFeePerKm: data.deliveryFeePerKm.present + ? data.deliveryFeePerKm.value + : this.deliveryFeePerKm, + deliveryBaseFee: data.deliveryBaseFee.present + ? data.deliveryBaseFee.value + : this.deliveryBaseFee, + reviewDisplayType: data.reviewDisplayType.present + ? data.reviewDisplayType.value + : this.reviewDisplayType, + reviewDisplayPolicy: data.reviewDisplayPolicy.present + ? data.reviewDisplayPolicy.value + : this.reviewDisplayPolicy, + subscriptionLevel: data.subscriptionLevel.present + ? data.subscriptionLevel.value + : this.subscriptionLevel, + limitOutlet: data.limitOutlet.present + ? data.limitOutlet.value + : this.limitOutlet, + subscriptionExpiresAt: data.subscriptionExpiresAt.present + ? data.subscriptionExpiresAt.value + : this.subscriptionExpiresAt, + syncStatus: data.syncStatus.present + ? data.syncStatus.value + : this.syncStatus, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, + ); + } + + @override + String toString() { + return (StringBuffer('Tenant(') + ..write('localId: $localId, ') + ..write('serverId: $serverId, ') + ..write('businessCode: $businessCode, ') + ..write('businessName: $businessName, ') + ..write('businessEmail: $businessEmail, ') + ..write('businessPhone: $businessPhone, ') + ..write('businessType: $businessType, ') + ..write('localLogoPath: $localLogoPath, ') + ..write('serverLogoUrl: $serverLogoUrl, ') + ..write('subdomain: $subdomain, ') + ..write('onlinStoreName: $onlinStoreName, ') + ..write('onlineOpenTime: $onlineOpenTime, ') + ..write('onlineCloseTime: $onlineCloseTime, ') + ..write('isCloseService: $isCloseService, ') + ..write('preparationTimeMinutes: $preparationTimeMinutes, ') + ..write('orderIntervalMinutes: $orderIntervalMinutes, ') + ..write('allowCod: $allowCod, ') + ..write('maxDeliveryRadiusKm: $maxDeliveryRadiusKm, ') + ..write('deliveryFeeType: $deliveryFeeType, ') + ..write('deliveryFlatFee: $deliveryFlatFee, ') + ..write('deliveryFeePerKm: $deliveryFeePerKm, ') + ..write('deliveryBaseFee: $deliveryBaseFee, ') + ..write('reviewDisplayType: $reviewDisplayType, ') + ..write('reviewDisplayPolicy: $reviewDisplayPolicy, ') + ..write('subscriptionLevel: $subscriptionLevel, ') + ..write('limitOutlet: $limitOutlet, ') + ..write('subscriptionExpiresAt: $subscriptionExpiresAt, ') + ..write('syncStatus: $syncStatus, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hashAll([ + localId, + serverId, + businessCode, + businessName, + businessEmail, + businessPhone, + businessType, + localLogoPath, + serverLogoUrl, + subdomain, + onlinStoreName, + onlineOpenTime, + onlineCloseTime, + isCloseService, + preparationTimeMinutes, + orderIntervalMinutes, + allowCod, + maxDeliveryRadiusKm, + deliveryFeeType, + deliveryFlatFee, + deliveryFeePerKm, + deliveryBaseFee, + reviewDisplayType, + reviewDisplayPolicy, + subscriptionLevel, + limitOutlet, + subscriptionExpiresAt, + syncStatus, + createdAt, + updatedAt, + ]); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is Tenant && + other.localId == this.localId && + other.serverId == this.serverId && + other.businessCode == this.businessCode && + other.businessName == this.businessName && + other.businessEmail == this.businessEmail && + other.businessPhone == this.businessPhone && + other.businessType == this.businessType && + other.localLogoPath == this.localLogoPath && + other.serverLogoUrl == this.serverLogoUrl && + other.subdomain == this.subdomain && + other.onlinStoreName == this.onlinStoreName && + other.onlineOpenTime == this.onlineOpenTime && + other.onlineCloseTime == this.onlineCloseTime && + other.isCloseService == this.isCloseService && + other.preparationTimeMinutes == this.preparationTimeMinutes && + other.orderIntervalMinutes == this.orderIntervalMinutes && + other.allowCod == this.allowCod && + other.maxDeliveryRadiusKm == this.maxDeliveryRadiusKm && + other.deliveryFeeType == this.deliveryFeeType && + other.deliveryFlatFee == this.deliveryFlatFee && + other.deliveryFeePerKm == this.deliveryFeePerKm && + other.deliveryBaseFee == this.deliveryBaseFee && + other.reviewDisplayType == this.reviewDisplayType && + other.reviewDisplayPolicy == this.reviewDisplayPolicy && + other.subscriptionLevel == this.subscriptionLevel && + other.limitOutlet == this.limitOutlet && + other.subscriptionExpiresAt == this.subscriptionExpiresAt && + other.syncStatus == this.syncStatus && + other.createdAt == this.createdAt && + other.updatedAt == this.updatedAt); +} + +class TenantsCompanion extends UpdateCompanion { + final Value localId; + final Value serverId; + final Value businessCode; + final Value businessName; + final Value businessEmail; + final Value businessPhone; + final Value businessType; + final Value localLogoPath; + final Value serverLogoUrl; + final Value subdomain; + final Value onlinStoreName; + final Value onlineOpenTime; + final Value onlineCloseTime; + final Value isCloseService; + final Value preparationTimeMinutes; + final Value orderIntervalMinutes; + final Value allowCod; + final Value maxDeliveryRadiusKm; + final Value deliveryFeeType; + final Value deliveryFlatFee; + final Value deliveryFeePerKm; + final Value deliveryBaseFee; + final Value reviewDisplayType; + final Value reviewDisplayPolicy; + final Value subscriptionLevel; + final Value limitOutlet; + final Value subscriptionExpiresAt; + final Value syncStatus; + final Value createdAt; + final Value updatedAt; + final Value rowid; + const TenantsCompanion({ + this.localId = const Value.absent(), + this.serverId = const Value.absent(), + this.businessCode = const Value.absent(), + this.businessName = const Value.absent(), + this.businessEmail = const Value.absent(), + this.businessPhone = const Value.absent(), + this.businessType = const Value.absent(), + this.localLogoPath = const Value.absent(), + this.serverLogoUrl = const Value.absent(), + this.subdomain = const Value.absent(), + this.onlinStoreName = const Value.absent(), + this.onlineOpenTime = const Value.absent(), + this.onlineCloseTime = const Value.absent(), + this.isCloseService = const Value.absent(), + this.preparationTimeMinutes = const Value.absent(), + this.orderIntervalMinutes = const Value.absent(), + this.allowCod = const Value.absent(), + this.maxDeliveryRadiusKm = const Value.absent(), + this.deliveryFeeType = const Value.absent(), + this.deliveryFlatFee = const Value.absent(), + this.deliveryFeePerKm = const Value.absent(), + this.deliveryBaseFee = const Value.absent(), + this.reviewDisplayType = const Value.absent(), + this.reviewDisplayPolicy = const Value.absent(), + this.subscriptionLevel = const Value.absent(), + this.limitOutlet = const Value.absent(), + this.subscriptionExpiresAt = const Value.absent(), + this.syncStatus = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.rowid = const Value.absent(), + }); + TenantsCompanion.insert({ + this.localId = const Value.absent(), + this.serverId = const Value.absent(), + required String businessCode, + required String businessName, + this.businessEmail = const Value.absent(), + this.businessPhone = const Value.absent(), + this.businessType = const Value.absent(), + this.localLogoPath = const Value.absent(), + this.serverLogoUrl = const Value.absent(), + this.subdomain = const Value.absent(), + this.onlinStoreName = const Value.absent(), + this.onlineOpenTime = const Value.absent(), + this.onlineCloseTime = const Value.absent(), + this.isCloseService = const Value.absent(), + this.preparationTimeMinutes = const Value.absent(), + this.orderIntervalMinutes = const Value.absent(), + this.allowCod = const Value.absent(), + this.maxDeliveryRadiusKm = const Value.absent(), + this.deliveryFeeType = const Value.absent(), + this.deliveryFlatFee = const Value.absent(), + this.deliveryFeePerKm = const Value.absent(), + this.deliveryBaseFee = const Value.absent(), + this.reviewDisplayType = const Value.absent(), + this.reviewDisplayPolicy = const Value.absent(), + this.subscriptionLevel = const Value.absent(), + this.limitOutlet = const Value.absent(), + this.subscriptionExpiresAt = const Value.absent(), + this.syncStatus = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.rowid = const Value.absent(), + }) : businessCode = Value(businessCode), + businessName = Value(businessName); + static Insertable custom({ + Expression? localId, + Expression? serverId, + Expression? businessCode, + Expression? businessName, + Expression? businessEmail, + Expression? businessPhone, + Expression? businessType, + Expression? localLogoPath, + Expression? serverLogoUrl, + Expression? subdomain, + Expression? onlinStoreName, + Expression? onlineOpenTime, + Expression? onlineCloseTime, + Expression? isCloseService, + Expression? preparationTimeMinutes, + Expression? orderIntervalMinutes, + Expression? allowCod, + Expression? maxDeliveryRadiusKm, + Expression? deliveryFeeType, + Expression? deliveryFlatFee, + Expression? deliveryFeePerKm, + Expression? deliveryBaseFee, + Expression? reviewDisplayType, + Expression? reviewDisplayPolicy, + Expression? subscriptionLevel, + Expression? limitOutlet, + Expression? subscriptionExpiresAt, + Expression? syncStatus, + Expression? createdAt, + Expression? updatedAt, + Expression? rowid, + }) { + return RawValuesInsertable({ + if (localId != null) 'local_id': localId, + if (serverId != null) 'server_id': serverId, + if (businessCode != null) 'business_code': businessCode, + if (businessName != null) 'business_name': businessName, + if (businessEmail != null) 'business_email': businessEmail, + if (businessPhone != null) 'business_phone': businessPhone, + if (businessType != null) 'business_type': businessType, + if (localLogoPath != null) 'local_logo_path': localLogoPath, + if (serverLogoUrl != null) 'server_logo_url': serverLogoUrl, + if (subdomain != null) 'subdomain': subdomain, + if (onlinStoreName != null) 'onlin_store_name': onlinStoreName, + if (onlineOpenTime != null) 'online_open_time': onlineOpenTime, + if (onlineCloseTime != null) 'online_close_time': onlineCloseTime, + if (isCloseService != null) 'is_close_service': isCloseService, + if (preparationTimeMinutes != null) + 'preparation_time_minutes': preparationTimeMinutes, + if (orderIntervalMinutes != null) + 'order_interval_minutes': orderIntervalMinutes, + if (allowCod != null) 'allow_cod': allowCod, + if (maxDeliveryRadiusKm != null) + 'max_delivery_radius_km': maxDeliveryRadiusKm, + if (deliveryFeeType != null) 'delivery_fee_type': deliveryFeeType, + if (deliveryFlatFee != null) 'delivery_flat_fee': deliveryFlatFee, + if (deliveryFeePerKm != null) 'delivery_fee_per_km': deliveryFeePerKm, + if (deliveryBaseFee != null) 'delivery_base_fee': deliveryBaseFee, + if (reviewDisplayType != null) 'review_display_type': reviewDisplayType, + if (reviewDisplayPolicy != null) + 'review_display_policy': reviewDisplayPolicy, + if (subscriptionLevel != null) 'subscription_level': subscriptionLevel, + if (limitOutlet != null) 'limit_outlet': limitOutlet, + if (subscriptionExpiresAt != null) + 'subscription_expires_at': subscriptionExpiresAt, + if (syncStatus != null) 'sync_status': syncStatus, + if (createdAt != null) 'created_at': createdAt, + if (updatedAt != null) 'updated_at': updatedAt, + if (rowid != null) 'rowid': rowid, + }); + } + + TenantsCompanion copyWith({ + Value? localId, + Value? serverId, + Value? businessCode, + Value? businessName, + Value? businessEmail, + Value? businessPhone, + Value? businessType, + Value? localLogoPath, + Value? serverLogoUrl, + Value? subdomain, + Value? onlinStoreName, + Value? onlineOpenTime, + Value? onlineCloseTime, + Value? isCloseService, + Value? preparationTimeMinutes, + Value? orderIntervalMinutes, + Value? allowCod, + Value? maxDeliveryRadiusKm, + Value? deliveryFeeType, + Value? deliveryFlatFee, + Value? deliveryFeePerKm, + Value? deliveryBaseFee, + Value? reviewDisplayType, + Value? reviewDisplayPolicy, + Value? subscriptionLevel, + Value? limitOutlet, + Value? subscriptionExpiresAt, + Value? syncStatus, + Value? createdAt, + Value? updatedAt, + Value? rowid, + }) { + return TenantsCompanion( + localId: localId ?? this.localId, + serverId: serverId ?? this.serverId, + businessCode: businessCode ?? this.businessCode, + businessName: businessName ?? this.businessName, + businessEmail: businessEmail ?? this.businessEmail, + businessPhone: businessPhone ?? this.businessPhone, + businessType: businessType ?? this.businessType, + localLogoPath: localLogoPath ?? this.localLogoPath, + serverLogoUrl: serverLogoUrl ?? this.serverLogoUrl, + subdomain: subdomain ?? this.subdomain, + onlinStoreName: onlinStoreName ?? this.onlinStoreName, + onlineOpenTime: onlineOpenTime ?? this.onlineOpenTime, + onlineCloseTime: onlineCloseTime ?? this.onlineCloseTime, + isCloseService: isCloseService ?? this.isCloseService, + preparationTimeMinutes: + preparationTimeMinutes ?? this.preparationTimeMinutes, + orderIntervalMinutes: orderIntervalMinutes ?? this.orderIntervalMinutes, + allowCod: allowCod ?? this.allowCod, + maxDeliveryRadiusKm: maxDeliveryRadiusKm ?? this.maxDeliveryRadiusKm, + deliveryFeeType: deliveryFeeType ?? this.deliveryFeeType, + deliveryFlatFee: deliveryFlatFee ?? this.deliveryFlatFee, + deliveryFeePerKm: deliveryFeePerKm ?? this.deliveryFeePerKm, + deliveryBaseFee: deliveryBaseFee ?? this.deliveryBaseFee, + reviewDisplayType: reviewDisplayType ?? this.reviewDisplayType, + reviewDisplayPolicy: reviewDisplayPolicy ?? this.reviewDisplayPolicy, + subscriptionLevel: subscriptionLevel ?? this.subscriptionLevel, + limitOutlet: limitOutlet ?? this.limitOutlet, + subscriptionExpiresAt: + subscriptionExpiresAt ?? this.subscriptionExpiresAt, + syncStatus: syncStatus ?? this.syncStatus, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (localId.present) { + map['local_id'] = Variable(localId.value); + } + if (serverId.present) { + map['server_id'] = Variable(serverId.value); + } + if (businessCode.present) { + map['business_code'] = Variable(businessCode.value); + } + if (businessName.present) { + map['business_name'] = Variable(businessName.value); + } + if (businessEmail.present) { + map['business_email'] = Variable(businessEmail.value); + } + if (businessPhone.present) { + map['business_phone'] = Variable(businessPhone.value); + } + if (businessType.present) { + map['business_type'] = Variable(businessType.value); + } + if (localLogoPath.present) { + map['local_logo_path'] = Variable(localLogoPath.value); + } + if (serverLogoUrl.present) { + map['server_logo_url'] = Variable(serverLogoUrl.value); + } + if (subdomain.present) { + map['subdomain'] = Variable(subdomain.value); + } + if (onlinStoreName.present) { + map['onlin_store_name'] = Variable(onlinStoreName.value); + } + if (onlineOpenTime.present) { + map['online_open_time'] = Variable(onlineOpenTime.value); + } + if (onlineCloseTime.present) { + map['online_close_time'] = Variable(onlineCloseTime.value); + } + if (isCloseService.present) { + map['is_close_service'] = Variable(isCloseService.value); + } + if (preparationTimeMinutes.present) { + map['preparation_time_minutes'] = Variable( + preparationTimeMinutes.value, + ); + } + if (orderIntervalMinutes.present) { + map['order_interval_minutes'] = Variable(orderIntervalMinutes.value); + } + if (allowCod.present) { + map['allow_cod'] = Variable(allowCod.value); + } + if (maxDeliveryRadiusKm.present) { + map['max_delivery_radius_km'] = Variable( + maxDeliveryRadiusKm.value, + ); + } + if (deliveryFeeType.present) { + map['delivery_fee_type'] = Variable(deliveryFeeType.value); + } + if (deliveryFlatFee.present) { + map['delivery_flat_fee'] = Variable(deliveryFlatFee.value); + } + if (deliveryFeePerKm.present) { + map['delivery_fee_per_km'] = Variable(deliveryFeePerKm.value); + } + if (deliveryBaseFee.present) { + map['delivery_base_fee'] = Variable(deliveryBaseFee.value); + } + if (reviewDisplayType.present) { + map['review_display_type'] = Variable(reviewDisplayType.value); + } + if (reviewDisplayPolicy.present) { + map['review_display_policy'] = Variable( + reviewDisplayPolicy.value, + ); + } + if (subscriptionLevel.present) { + map['subscription_level'] = Variable(subscriptionLevel.value); + } + if (limitOutlet.present) { + map['limit_outlet'] = Variable(limitOutlet.value); + } + if (subscriptionExpiresAt.present) { + map['subscription_expires_at'] = Variable( + subscriptionExpiresAt.value, + ); + } + if (syncStatus.present) { + map['sync_status'] = Variable(syncStatus.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (updatedAt.present) { + map['updated_at'] = Variable(updatedAt.value); + } + if (rowid.present) { + map['rowid'] = Variable(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('TenantsCompanion(') + ..write('localId: $localId, ') + ..write('serverId: $serverId, ') + ..write('businessCode: $businessCode, ') + ..write('businessName: $businessName, ') + ..write('businessEmail: $businessEmail, ') + ..write('businessPhone: $businessPhone, ') + ..write('businessType: $businessType, ') + ..write('localLogoPath: $localLogoPath, ') + ..write('serverLogoUrl: $serverLogoUrl, ') + ..write('subdomain: $subdomain, ') + ..write('onlinStoreName: $onlinStoreName, ') + ..write('onlineOpenTime: $onlineOpenTime, ') + ..write('onlineCloseTime: $onlineCloseTime, ') + ..write('isCloseService: $isCloseService, ') + ..write('preparationTimeMinutes: $preparationTimeMinutes, ') + ..write('orderIntervalMinutes: $orderIntervalMinutes, ') + ..write('allowCod: $allowCod, ') + ..write('maxDeliveryRadiusKm: $maxDeliveryRadiusKm, ') + ..write('deliveryFeeType: $deliveryFeeType, ') + ..write('deliveryFlatFee: $deliveryFlatFee, ') + ..write('deliveryFeePerKm: $deliveryFeePerKm, ') + ..write('deliveryBaseFee: $deliveryBaseFee, ') + ..write('reviewDisplayType: $reviewDisplayType, ') + ..write('reviewDisplayPolicy: $reviewDisplayPolicy, ') + ..write('subscriptionLevel: $subscriptionLevel, ') + ..write('limitOutlet: $limitOutlet, ') + ..write('subscriptionExpiresAt: $subscriptionExpiresAt, ') + ..write('syncStatus: $syncStatus, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + +class $TenantPaymentMethodsTable extends TenantPaymentMethods + with TableInfo<$TenantPaymentMethodsTable, TenantPaymentMethod> { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + $TenantPaymentMethodsTable(this.attachedDatabase, [this._alias]); + static const VerificationMeta _localIdMeta = const VerificationMeta( + 'localId', + ); + @override + late final GeneratedColumn localId = GeneratedColumn( + 'local_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: false, + clientDefault: () => const Uuid().v7().toString(), + ); + static const VerificationMeta _serverIdMeta = const VerificationMeta( + 'serverId', + ); + @override + late final GeneratedColumn serverId = GeneratedColumn( + 'server_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _sourceNameMeta = const VerificationMeta( + 'sourceName', + ); + @override + late final GeneratedColumn sourceName = GeneratedColumn( + 'source_name', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _accountNumberMeta = const VerificationMeta( + 'accountNumber', + ); + @override + late final GeneratedColumn accountNumber = GeneratedColumn( + 'account_number', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _receipentNameMeta = const VerificationMeta( + 'receipentName', + ); + @override + late final GeneratedColumn receipentName = GeneratedColumn( + 'receipent_name', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _syncStatusMeta = const VerificationMeta( + 'syncStatus', + ); + @override + late final GeneratedColumn syncStatus = GeneratedColumn( + 'sync_status', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultValue: const Constant('dirty'), + ); + static const VerificationMeta _createdAtMeta = const VerificationMeta( + 'createdAt', + ); + @override + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: currentDateAndTime, + ); + static const VerificationMeta _updatedAtMeta = const VerificationMeta( + 'updatedAt', + ); + @override + late final GeneratedColumn updatedAt = GeneratedColumn( + 'updated_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: currentDateAndTime, + ); + @override + List get $columns => [ + localId, + serverId, + sourceName, + accountNumber, + receipentName, + syncStatus, + createdAt, + updatedAt, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'tenant_payment_methods'; + @override + VerificationContext validateIntegrity( + Insertable instance, { + bool isInserting = false, + }) { + final context = VerificationContext(); + final data = instance.toColumns(true); + if (data.containsKey('local_id')) { + context.handle( + _localIdMeta, + localId.isAcceptableOrUnknown(data['local_id']!, _localIdMeta), + ); + } + if (data.containsKey('server_id')) { + context.handle( + _serverIdMeta, + serverId.isAcceptableOrUnknown(data['server_id']!, _serverIdMeta), + ); + } + if (data.containsKey('source_name')) { + context.handle( + _sourceNameMeta, + sourceName.isAcceptableOrUnknown(data['source_name']!, _sourceNameMeta), + ); + } + if (data.containsKey('account_number')) { + context.handle( + _accountNumberMeta, + accountNumber.isAcceptableOrUnknown( + data['account_number']!, + _accountNumberMeta, + ), + ); + } + if (data.containsKey('receipent_name')) { + context.handle( + _receipentNameMeta, + receipentName.isAcceptableOrUnknown( + data['receipent_name']!, + _receipentNameMeta, + ), + ); + } + if (data.containsKey('sync_status')) { + context.handle( + _syncStatusMeta, + syncStatus.isAcceptableOrUnknown(data['sync_status']!, _syncStatusMeta), + ); + } + if (data.containsKey('created_at')) { + context.handle( + _createdAtMeta, + createdAt.isAcceptableOrUnknown(data['created_at']!, _createdAtMeta), + ); + } + if (data.containsKey('updated_at')) { + context.handle( + _updatedAtMeta, + updatedAt.isAcceptableOrUnknown(data['updated_at']!, _updatedAtMeta), + ); + } + return context; + } + + @override + Set get $primaryKey => {localId}; + @override + TenantPaymentMethod map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return TenantPaymentMethod( + localId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}local_id'], + )!, + serverId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}server_id'], + ), + sourceName: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}source_name'], + ), + accountNumber: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}account_number'], + ), + receipentName: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}receipent_name'], + ), + syncStatus: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}sync_status'], + )!, + createdAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}created_at'], + )!, + updatedAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}updated_at'], + )!, + ); + } + + @override + $TenantPaymentMethodsTable createAlias(String alias) { + return $TenantPaymentMethodsTable(attachedDatabase, alias); + } +} + +class TenantPaymentMethod extends DataClass + implements Insertable { + final String localId; + final String? serverId; + final String? sourceName; + final String? accountNumber; + final String? receipentName; + final String syncStatus; + final DateTime createdAt; + final DateTime updatedAt; + const TenantPaymentMethod({ + required this.localId, + this.serverId, + this.sourceName, + this.accountNumber, + this.receipentName, + required this.syncStatus, + required this.createdAt, + required this.updatedAt, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['local_id'] = Variable(localId); + if (!nullToAbsent || serverId != null) { + map['server_id'] = Variable(serverId); + } + if (!nullToAbsent || sourceName != null) { + map['source_name'] = Variable(sourceName); + } + if (!nullToAbsent || accountNumber != null) { + map['account_number'] = Variable(accountNumber); + } + if (!nullToAbsent || receipentName != null) { + map['receipent_name'] = Variable(receipentName); + } + map['sync_status'] = Variable(syncStatus); + map['created_at'] = Variable(createdAt); + map['updated_at'] = Variable(updatedAt); + return map; + } + + TenantPaymentMethodsCompanion toCompanion(bool nullToAbsent) { + return TenantPaymentMethodsCompanion( + localId: Value(localId), + serverId: serverId == null && nullToAbsent + ? const Value.absent() + : Value(serverId), + sourceName: sourceName == null && nullToAbsent + ? const Value.absent() + : Value(sourceName), + accountNumber: accountNumber == null && nullToAbsent + ? const Value.absent() + : Value(accountNumber), + receipentName: receipentName == null && nullToAbsent + ? const Value.absent() + : Value(receipentName), + syncStatus: Value(syncStatus), + createdAt: Value(createdAt), + updatedAt: Value(updatedAt), + ); + } + + factory TenantPaymentMethod.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return TenantPaymentMethod( + localId: serializer.fromJson(json['localId']), + serverId: serializer.fromJson(json['serverId']), + sourceName: serializer.fromJson(json['sourceName']), + accountNumber: serializer.fromJson(json['accountNumber']), + receipentName: serializer.fromJson(json['receipentName']), + syncStatus: serializer.fromJson(json['syncStatus']), + createdAt: serializer.fromJson(json['createdAt']), + updatedAt: serializer.fromJson(json['updatedAt']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'localId': serializer.toJson(localId), + 'serverId': serializer.toJson(serverId), + 'sourceName': serializer.toJson(sourceName), + 'accountNumber': serializer.toJson(accountNumber), + 'receipentName': serializer.toJson(receipentName), + 'syncStatus': serializer.toJson(syncStatus), + 'createdAt': serializer.toJson(createdAt), + 'updatedAt': serializer.toJson(updatedAt), + }; + } + + TenantPaymentMethod copyWith({ + String? localId, + Value serverId = const Value.absent(), + Value sourceName = const Value.absent(), + Value accountNumber = const Value.absent(), + Value receipentName = const Value.absent(), + String? syncStatus, + DateTime? createdAt, + DateTime? updatedAt, + }) => TenantPaymentMethod( + localId: localId ?? this.localId, + serverId: serverId.present ? serverId.value : this.serverId, + sourceName: sourceName.present ? sourceName.value : this.sourceName, + accountNumber: accountNumber.present + ? accountNumber.value + : this.accountNumber, + receipentName: receipentName.present + ? receipentName.value + : this.receipentName, + syncStatus: syncStatus ?? this.syncStatus, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + ); + TenantPaymentMethod copyWithCompanion(TenantPaymentMethodsCompanion data) { + return TenantPaymentMethod( + localId: data.localId.present ? data.localId.value : this.localId, + serverId: data.serverId.present ? data.serverId.value : this.serverId, + sourceName: data.sourceName.present + ? data.sourceName.value + : this.sourceName, + accountNumber: data.accountNumber.present + ? data.accountNumber.value + : this.accountNumber, + receipentName: data.receipentName.present + ? data.receipentName.value + : this.receipentName, + syncStatus: data.syncStatus.present + ? data.syncStatus.value + : this.syncStatus, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, + ); + } + + @override + String toString() { + return (StringBuffer('TenantPaymentMethod(') + ..write('localId: $localId, ') + ..write('serverId: $serverId, ') + ..write('sourceName: $sourceName, ') + ..write('accountNumber: $accountNumber, ') + ..write('receipentName: $receipentName, ') + ..write('syncStatus: $syncStatus, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + localId, + serverId, + sourceName, + accountNumber, + receipentName, + syncStatus, + createdAt, + updatedAt, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is TenantPaymentMethod && + other.localId == this.localId && + other.serverId == this.serverId && + other.sourceName == this.sourceName && + other.accountNumber == this.accountNumber && + other.receipentName == this.receipentName && + other.syncStatus == this.syncStatus && + other.createdAt == this.createdAt && + other.updatedAt == this.updatedAt); +} + +class TenantPaymentMethodsCompanion + extends UpdateCompanion { + final Value localId; + final Value serverId; + final Value sourceName; + final Value accountNumber; + final Value receipentName; + final Value syncStatus; + final Value createdAt; + final Value updatedAt; + final Value rowid; + const TenantPaymentMethodsCompanion({ + this.localId = const Value.absent(), + this.serverId = const Value.absent(), + this.sourceName = const Value.absent(), + this.accountNumber = const Value.absent(), + this.receipentName = const Value.absent(), + this.syncStatus = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.rowid = const Value.absent(), + }); + TenantPaymentMethodsCompanion.insert({ + this.localId = const Value.absent(), + this.serverId = const Value.absent(), + this.sourceName = const Value.absent(), + this.accountNumber = const Value.absent(), + this.receipentName = const Value.absent(), + this.syncStatus = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.rowid = const Value.absent(), + }); + static Insertable custom({ + Expression? localId, + Expression? serverId, + Expression? sourceName, + Expression? accountNumber, + Expression? receipentName, + Expression? syncStatus, + Expression? createdAt, + Expression? updatedAt, + Expression? rowid, + }) { + return RawValuesInsertable({ + if (localId != null) 'local_id': localId, + if (serverId != null) 'server_id': serverId, + if (sourceName != null) 'source_name': sourceName, + if (accountNumber != null) 'account_number': accountNumber, + if (receipentName != null) 'receipent_name': receipentName, + if (syncStatus != null) 'sync_status': syncStatus, + if (createdAt != null) 'created_at': createdAt, + if (updatedAt != null) 'updated_at': updatedAt, + if (rowid != null) 'rowid': rowid, + }); + } + + TenantPaymentMethodsCompanion copyWith({ + Value? localId, + Value? serverId, + Value? sourceName, + Value? accountNumber, + Value? receipentName, + Value? syncStatus, + Value? createdAt, + Value? updatedAt, + Value? rowid, + }) { + return TenantPaymentMethodsCompanion( + localId: localId ?? this.localId, + serverId: serverId ?? this.serverId, + sourceName: sourceName ?? this.sourceName, + accountNumber: accountNumber ?? this.accountNumber, + receipentName: receipentName ?? this.receipentName, + syncStatus: syncStatus ?? this.syncStatus, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (localId.present) { + map['local_id'] = Variable(localId.value); + } + if (serverId.present) { + map['server_id'] = Variable(serverId.value); + } + if (sourceName.present) { + map['source_name'] = Variable(sourceName.value); + } + if (accountNumber.present) { + map['account_number'] = Variable(accountNumber.value); + } + if (receipentName.present) { + map['receipent_name'] = Variable(receipentName.value); + } + if (syncStatus.present) { + map['sync_status'] = Variable(syncStatus.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (updatedAt.present) { + map['updated_at'] = Variable(updatedAt.value); + } + if (rowid.present) { + map['rowid'] = Variable(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('TenantPaymentMethodsCompanion(') + ..write('localId: $localId, ') + ..write('serverId: $serverId, ') + ..write('sourceName: $sourceName, ') + ..write('accountNumber: $accountNumber, ') + ..write('receipentName: $receipentName, ') + ..write('syncStatus: $syncStatus, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + +class $OutletsTable extends Outlets with TableInfo<$OutletsTable, Outlet> { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + $OutletsTable(this.attachedDatabase, [this._alias]); + static const VerificationMeta _localIdMeta = const VerificationMeta( + 'localId', + ); + @override + late final GeneratedColumn localId = GeneratedColumn( + 'local_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: false, + clientDefault: () => const Uuid().v7().toString(), + ); + static const VerificationMeta _serverIdMeta = const VerificationMeta( + 'serverId', + ); + @override + late final GeneratedColumn serverId = GeneratedColumn( + 'server_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _nameMeta = const VerificationMeta('name'); + @override + late final GeneratedColumn name = GeneratedColumn( + 'name', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + static const VerificationMeta _emailMeta = const VerificationMeta('email'); + @override + late final GeneratedColumn email = GeneratedColumn( + 'email', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _phoneNumberMeta = const VerificationMeta( + 'phoneNumber', + ); + @override + late final GeneratedColumn phoneNumber = GeneratedColumn( + 'phone_number', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _fullAddressMeta = const VerificationMeta( + 'fullAddress', + ); + @override + late final GeneratedColumn fullAddress = GeneratedColumn( + 'full_address', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _latitudeMeta = const VerificationMeta( + 'latitude', + ); + @override + late final GeneratedColumn latitude = GeneratedColumn( + 'latitude', + aliasedName, + true, + type: DriftSqlType.double, + requiredDuringInsert: false, + ); + static const VerificationMeta _longitudeMeta = const VerificationMeta( + 'longitude', + ); + @override + late final GeneratedColumn longitude = GeneratedColumn( + 'longitude', + aliasedName, + true, + type: DriftSqlType.double, + requiredDuringInsert: false, + ); + static const VerificationMeta _localPhotoPathMeta = const VerificationMeta( + 'localPhotoPath', + ); + @override + late final GeneratedColumn localPhotoPath = GeneratedColumn( + 'local_photo_path', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _localBannerPathMeta = const VerificationMeta( + 'localBannerPath', + ); + @override + late final GeneratedColumn localBannerPath = GeneratedColumn( + 'local_banner_path', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _serverPhotoUrlMeta = const VerificationMeta( + 'serverPhotoUrl', + ); + @override + late final GeneratedColumn serverPhotoUrl = GeneratedColumn( + 'server_photo_url', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _serverBannerUrlMeta = const VerificationMeta( + 'serverBannerUrl', + ); + @override + late final GeneratedColumn serverBannerUrl = GeneratedColumn( + 'server_banner_url', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _isMainOutletMeta = const VerificationMeta( + 'isMainOutlet', + ); + @override + late final GeneratedColumn isMainOutlet = GeneratedColumn( + 'is_main_outlet', + aliasedName, + false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("is_main_outlet" IN (0, 1))', + ), + defaultValue: const Constant(false), + ); + static const VerificationMeta _isActiveMeta = const VerificationMeta( + 'isActive', + ); + @override + late final GeneratedColumn isActive = GeneratedColumn( + 'is_active', + aliasedName, + false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("is_active" IN (0, 1))', + ), + defaultValue: const Constant(true), + ); + static const VerificationMeta _syncStatusMeta = const VerificationMeta( + 'syncStatus', + ); + @override + late final GeneratedColumn syncStatus = GeneratedColumn( + 'sync_status', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultValue: const Constant('dirty'), + ); + static const VerificationMeta _createdAtMeta = const VerificationMeta( + 'createdAt', + ); + @override + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: currentDateAndTime, + ); + static const VerificationMeta _updatedAtMeta = const VerificationMeta( + 'updatedAt', + ); + @override + late final GeneratedColumn updatedAt = GeneratedColumn( + 'updated_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: currentDateAndTime, + ); + static const VerificationMeta _deletedAtMeta = const VerificationMeta( + 'deletedAt', + ); + @override + late final GeneratedColumn deletedAt = GeneratedColumn( + 'deleted_at', + aliasedName, + true, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + ); + @override + List get $columns => [ + localId, + serverId, + name, + email, + phoneNumber, + fullAddress, + latitude, + longitude, + localPhotoPath, + localBannerPath, + serverPhotoUrl, + serverBannerUrl, + isMainOutlet, + isActive, + syncStatus, + createdAt, + updatedAt, + deletedAt, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'outlets'; + @override + VerificationContext validateIntegrity( + Insertable instance, { + bool isInserting = false, + }) { + final context = VerificationContext(); + final data = instance.toColumns(true); + if (data.containsKey('local_id')) { + context.handle( + _localIdMeta, + localId.isAcceptableOrUnknown(data['local_id']!, _localIdMeta), + ); + } + if (data.containsKey('server_id')) { + context.handle( + _serverIdMeta, + serverId.isAcceptableOrUnknown(data['server_id']!, _serverIdMeta), + ); + } + if (data.containsKey('name')) { + context.handle( + _nameMeta, + name.isAcceptableOrUnknown(data['name']!, _nameMeta), + ); + } else if (isInserting) { + context.missing(_nameMeta); + } + if (data.containsKey('email')) { + context.handle( + _emailMeta, + email.isAcceptableOrUnknown(data['email']!, _emailMeta), + ); + } + if (data.containsKey('phone_number')) { + context.handle( + _phoneNumberMeta, + phoneNumber.isAcceptableOrUnknown( + data['phone_number']!, + _phoneNumberMeta, + ), + ); + } + if (data.containsKey('full_address')) { + context.handle( + _fullAddressMeta, + fullAddress.isAcceptableOrUnknown( + data['full_address']!, + _fullAddressMeta, + ), + ); + } + if (data.containsKey('latitude')) { + context.handle( + _latitudeMeta, + latitude.isAcceptableOrUnknown(data['latitude']!, _latitudeMeta), + ); + } + if (data.containsKey('longitude')) { + context.handle( + _longitudeMeta, + longitude.isAcceptableOrUnknown(data['longitude']!, _longitudeMeta), + ); + } + if (data.containsKey('local_photo_path')) { + context.handle( + _localPhotoPathMeta, + localPhotoPath.isAcceptableOrUnknown( + data['local_photo_path']!, + _localPhotoPathMeta, + ), + ); + } + if (data.containsKey('local_banner_path')) { + context.handle( + _localBannerPathMeta, + localBannerPath.isAcceptableOrUnknown( + data['local_banner_path']!, + _localBannerPathMeta, + ), + ); + } + if (data.containsKey('server_photo_url')) { + context.handle( + _serverPhotoUrlMeta, + serverPhotoUrl.isAcceptableOrUnknown( + data['server_photo_url']!, + _serverPhotoUrlMeta, + ), + ); + } + if (data.containsKey('server_banner_url')) { + context.handle( + _serverBannerUrlMeta, + serverBannerUrl.isAcceptableOrUnknown( + data['server_banner_url']!, + _serverBannerUrlMeta, + ), + ); + } + if (data.containsKey('is_main_outlet')) { + context.handle( + _isMainOutletMeta, + isMainOutlet.isAcceptableOrUnknown( + data['is_main_outlet']!, + _isMainOutletMeta, + ), + ); + } + if (data.containsKey('is_active')) { + context.handle( + _isActiveMeta, + isActive.isAcceptableOrUnknown(data['is_active']!, _isActiveMeta), + ); + } + if (data.containsKey('sync_status')) { + context.handle( + _syncStatusMeta, + syncStatus.isAcceptableOrUnknown(data['sync_status']!, _syncStatusMeta), + ); + } + if (data.containsKey('created_at')) { + context.handle( + _createdAtMeta, + createdAt.isAcceptableOrUnknown(data['created_at']!, _createdAtMeta), + ); + } + if (data.containsKey('updated_at')) { + context.handle( + _updatedAtMeta, + updatedAt.isAcceptableOrUnknown(data['updated_at']!, _updatedAtMeta), + ); + } + if (data.containsKey('deleted_at')) { + context.handle( + _deletedAtMeta, + deletedAt.isAcceptableOrUnknown(data['deleted_at']!, _deletedAtMeta), + ); + } + return context; + } + + @override + Set get $primaryKey => {localId}; + @override + Outlet map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return Outlet( + localId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}local_id'], + )!, + serverId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}server_id'], + ), + name: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}name'], + )!, + email: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}email'], + ), + phoneNumber: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}phone_number'], + ), + fullAddress: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}full_address'], + ), + latitude: attachedDatabase.typeMapping.read( + DriftSqlType.double, + data['${effectivePrefix}latitude'], + ), + longitude: attachedDatabase.typeMapping.read( + DriftSqlType.double, + data['${effectivePrefix}longitude'], + ), + localPhotoPath: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}local_photo_path'], + ), + localBannerPath: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}local_banner_path'], + ), + serverPhotoUrl: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}server_photo_url'], + ), + serverBannerUrl: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}server_banner_url'], + ), + isMainOutlet: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}is_main_outlet'], + )!, + isActive: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}is_active'], + )!, + syncStatus: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}sync_status'], + )!, + createdAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}created_at'], + )!, + updatedAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}updated_at'], + )!, + deletedAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}deleted_at'], + ), + ); + } + + @override + $OutletsTable createAlias(String alias) { + return $OutletsTable(attachedDatabase, alias); + } +} + +class Outlet extends DataClass implements Insertable { + final String localId; + final String? serverId; + final String name; + final String? email; + final String? phoneNumber; + final String? fullAddress; + final double? latitude; + final double? longitude; + final String? localPhotoPath; + final String? localBannerPath; + final String? serverPhotoUrl; + final String? serverBannerUrl; + final bool isMainOutlet; + final bool isActive; + final String syncStatus; + final DateTime createdAt; + final DateTime updatedAt; + final DateTime? deletedAt; + const Outlet({ + required this.localId, + this.serverId, + required this.name, + this.email, + this.phoneNumber, + this.fullAddress, + this.latitude, + this.longitude, + this.localPhotoPath, + this.localBannerPath, + this.serverPhotoUrl, + this.serverBannerUrl, + required this.isMainOutlet, + required this.isActive, + required this.syncStatus, + required this.createdAt, + required this.updatedAt, + this.deletedAt, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['local_id'] = Variable(localId); + if (!nullToAbsent || serverId != null) { + map['server_id'] = Variable(serverId); + } + map['name'] = Variable(name); + if (!nullToAbsent || email != null) { + map['email'] = Variable(email); + } + if (!nullToAbsent || phoneNumber != null) { + map['phone_number'] = Variable(phoneNumber); + } + if (!nullToAbsent || fullAddress != null) { + map['full_address'] = Variable(fullAddress); + } + if (!nullToAbsent || latitude != null) { + map['latitude'] = Variable(latitude); + } + if (!nullToAbsent || longitude != null) { + map['longitude'] = Variable(longitude); + } + if (!nullToAbsent || localPhotoPath != null) { + map['local_photo_path'] = Variable(localPhotoPath); + } + if (!nullToAbsent || localBannerPath != null) { + map['local_banner_path'] = Variable(localBannerPath); + } + if (!nullToAbsent || serverPhotoUrl != null) { + map['server_photo_url'] = Variable(serverPhotoUrl); + } + if (!nullToAbsent || serverBannerUrl != null) { + map['server_banner_url'] = Variable(serverBannerUrl); + } + map['is_main_outlet'] = Variable(isMainOutlet); + map['is_active'] = Variable(isActive); + map['sync_status'] = Variable(syncStatus); + map['created_at'] = Variable(createdAt); + map['updated_at'] = Variable(updatedAt); + if (!nullToAbsent || deletedAt != null) { + map['deleted_at'] = Variable(deletedAt); + } + return map; + } + + OutletsCompanion toCompanion(bool nullToAbsent) { + return OutletsCompanion( + localId: Value(localId), + serverId: serverId == null && nullToAbsent + ? const Value.absent() + : Value(serverId), + name: Value(name), + email: email == null && nullToAbsent + ? const Value.absent() + : Value(email), + phoneNumber: phoneNumber == null && nullToAbsent + ? const Value.absent() + : Value(phoneNumber), + fullAddress: fullAddress == null && nullToAbsent + ? const Value.absent() + : Value(fullAddress), + latitude: latitude == null && nullToAbsent + ? const Value.absent() + : Value(latitude), + longitude: longitude == null && nullToAbsent + ? const Value.absent() + : Value(longitude), + localPhotoPath: localPhotoPath == null && nullToAbsent + ? const Value.absent() + : Value(localPhotoPath), + localBannerPath: localBannerPath == null && nullToAbsent + ? const Value.absent() + : Value(localBannerPath), + serverPhotoUrl: serverPhotoUrl == null && nullToAbsent + ? const Value.absent() + : Value(serverPhotoUrl), + serverBannerUrl: serverBannerUrl == null && nullToAbsent + ? const Value.absent() + : Value(serverBannerUrl), + isMainOutlet: Value(isMainOutlet), + isActive: Value(isActive), + syncStatus: Value(syncStatus), + createdAt: Value(createdAt), + updatedAt: Value(updatedAt), + deletedAt: deletedAt == null && nullToAbsent + ? const Value.absent() + : Value(deletedAt), + ); + } + + factory Outlet.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return Outlet( + localId: serializer.fromJson(json['localId']), + serverId: serializer.fromJson(json['serverId']), + name: serializer.fromJson(json['name']), + email: serializer.fromJson(json['email']), + phoneNumber: serializer.fromJson(json['phoneNumber']), + fullAddress: serializer.fromJson(json['fullAddress']), + latitude: serializer.fromJson(json['latitude']), + longitude: serializer.fromJson(json['longitude']), + localPhotoPath: serializer.fromJson(json['localPhotoPath']), + localBannerPath: serializer.fromJson(json['localBannerPath']), + serverPhotoUrl: serializer.fromJson(json['serverPhotoUrl']), + serverBannerUrl: serializer.fromJson(json['serverBannerUrl']), + isMainOutlet: serializer.fromJson(json['isMainOutlet']), + isActive: serializer.fromJson(json['isActive']), + syncStatus: serializer.fromJson(json['syncStatus']), + createdAt: serializer.fromJson(json['createdAt']), + updatedAt: serializer.fromJson(json['updatedAt']), + deletedAt: serializer.fromJson(json['deletedAt']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'localId': serializer.toJson(localId), + 'serverId': serializer.toJson(serverId), + 'name': serializer.toJson(name), + 'email': serializer.toJson(email), + 'phoneNumber': serializer.toJson(phoneNumber), + 'fullAddress': serializer.toJson(fullAddress), + 'latitude': serializer.toJson(latitude), + 'longitude': serializer.toJson(longitude), + 'localPhotoPath': serializer.toJson(localPhotoPath), + 'localBannerPath': serializer.toJson(localBannerPath), + 'serverPhotoUrl': serializer.toJson(serverPhotoUrl), + 'serverBannerUrl': serializer.toJson(serverBannerUrl), + 'isMainOutlet': serializer.toJson(isMainOutlet), + 'isActive': serializer.toJson(isActive), + 'syncStatus': serializer.toJson(syncStatus), + 'createdAt': serializer.toJson(createdAt), + 'updatedAt': serializer.toJson(updatedAt), + 'deletedAt': serializer.toJson(deletedAt), + }; + } + + Outlet copyWith({ + String? localId, + Value serverId = const Value.absent(), + String? name, + Value email = const Value.absent(), + Value phoneNumber = const Value.absent(), + Value fullAddress = const Value.absent(), + Value latitude = const Value.absent(), + Value longitude = const Value.absent(), + Value localPhotoPath = const Value.absent(), + Value localBannerPath = const Value.absent(), + Value serverPhotoUrl = const Value.absent(), + Value serverBannerUrl = const Value.absent(), + bool? isMainOutlet, + bool? isActive, + String? syncStatus, + DateTime? createdAt, + DateTime? updatedAt, + Value deletedAt = const Value.absent(), + }) => Outlet( + localId: localId ?? this.localId, + serverId: serverId.present ? serverId.value : this.serverId, + name: name ?? this.name, + email: email.present ? email.value : this.email, + phoneNumber: phoneNumber.present ? phoneNumber.value : this.phoneNumber, + fullAddress: fullAddress.present ? fullAddress.value : this.fullAddress, + latitude: latitude.present ? latitude.value : this.latitude, + longitude: longitude.present ? longitude.value : this.longitude, + localPhotoPath: localPhotoPath.present + ? localPhotoPath.value + : this.localPhotoPath, + localBannerPath: localBannerPath.present + ? localBannerPath.value + : this.localBannerPath, + serverPhotoUrl: serverPhotoUrl.present + ? serverPhotoUrl.value + : this.serverPhotoUrl, + serverBannerUrl: serverBannerUrl.present + ? serverBannerUrl.value + : this.serverBannerUrl, + isMainOutlet: isMainOutlet ?? this.isMainOutlet, + isActive: isActive ?? this.isActive, + syncStatus: syncStatus ?? this.syncStatus, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + deletedAt: deletedAt.present ? deletedAt.value : this.deletedAt, + ); + Outlet copyWithCompanion(OutletsCompanion data) { + return Outlet( + localId: data.localId.present ? data.localId.value : this.localId, + serverId: data.serverId.present ? data.serverId.value : this.serverId, + name: data.name.present ? data.name.value : this.name, + email: data.email.present ? data.email.value : this.email, + phoneNumber: data.phoneNumber.present + ? data.phoneNumber.value + : this.phoneNumber, + fullAddress: data.fullAddress.present + ? data.fullAddress.value + : this.fullAddress, + latitude: data.latitude.present ? data.latitude.value : this.latitude, + longitude: data.longitude.present ? data.longitude.value : this.longitude, + localPhotoPath: data.localPhotoPath.present + ? data.localPhotoPath.value + : this.localPhotoPath, + localBannerPath: data.localBannerPath.present + ? data.localBannerPath.value + : this.localBannerPath, + serverPhotoUrl: data.serverPhotoUrl.present + ? data.serverPhotoUrl.value + : this.serverPhotoUrl, + serverBannerUrl: data.serverBannerUrl.present + ? data.serverBannerUrl.value + : this.serverBannerUrl, + isMainOutlet: data.isMainOutlet.present + ? data.isMainOutlet.value + : this.isMainOutlet, + isActive: data.isActive.present ? data.isActive.value : this.isActive, + syncStatus: data.syncStatus.present + ? data.syncStatus.value + : this.syncStatus, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, + deletedAt: data.deletedAt.present ? data.deletedAt.value : this.deletedAt, + ); + } + + @override + String toString() { + return (StringBuffer('Outlet(') + ..write('localId: $localId, ') + ..write('serverId: $serverId, ') + ..write('name: $name, ') + ..write('email: $email, ') + ..write('phoneNumber: $phoneNumber, ') + ..write('fullAddress: $fullAddress, ') + ..write('latitude: $latitude, ') + ..write('longitude: $longitude, ') + ..write('localPhotoPath: $localPhotoPath, ') + ..write('localBannerPath: $localBannerPath, ') + ..write('serverPhotoUrl: $serverPhotoUrl, ') + ..write('serverBannerUrl: $serverBannerUrl, ') + ..write('isMainOutlet: $isMainOutlet, ') + ..write('isActive: $isActive, ') + ..write('syncStatus: $syncStatus, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('deletedAt: $deletedAt') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + localId, + serverId, + name, + email, + phoneNumber, + fullAddress, + latitude, + longitude, + localPhotoPath, + localBannerPath, + serverPhotoUrl, + serverBannerUrl, + isMainOutlet, + isActive, + syncStatus, + createdAt, + updatedAt, + deletedAt, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is Outlet && + other.localId == this.localId && + other.serverId == this.serverId && + other.name == this.name && + other.email == this.email && + other.phoneNumber == this.phoneNumber && + other.fullAddress == this.fullAddress && + other.latitude == this.latitude && + other.longitude == this.longitude && + other.localPhotoPath == this.localPhotoPath && + other.localBannerPath == this.localBannerPath && + other.serverPhotoUrl == this.serverPhotoUrl && + other.serverBannerUrl == this.serverBannerUrl && + other.isMainOutlet == this.isMainOutlet && + other.isActive == this.isActive && + other.syncStatus == this.syncStatus && + other.createdAt == this.createdAt && + other.updatedAt == this.updatedAt && + other.deletedAt == this.deletedAt); +} + +class OutletsCompanion extends UpdateCompanion { + final Value localId; + final Value serverId; + final Value name; + final Value email; + final Value phoneNumber; + final Value fullAddress; + final Value latitude; + final Value longitude; + final Value localPhotoPath; + final Value localBannerPath; + final Value serverPhotoUrl; + final Value serverBannerUrl; + final Value isMainOutlet; + final Value isActive; + final Value syncStatus; + final Value createdAt; + final Value updatedAt; + final Value deletedAt; + final Value rowid; + const OutletsCompanion({ + this.localId = const Value.absent(), + this.serverId = const Value.absent(), + this.name = const Value.absent(), + this.email = const Value.absent(), + this.phoneNumber = const Value.absent(), + this.fullAddress = const Value.absent(), + this.latitude = const Value.absent(), + this.longitude = const Value.absent(), + this.localPhotoPath = const Value.absent(), + this.localBannerPath = const Value.absent(), + this.serverPhotoUrl = const Value.absent(), + this.serverBannerUrl = const Value.absent(), + this.isMainOutlet = const Value.absent(), + this.isActive = const Value.absent(), + this.syncStatus = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.deletedAt = const Value.absent(), + this.rowid = const Value.absent(), + }); + OutletsCompanion.insert({ + this.localId = const Value.absent(), + this.serverId = const Value.absent(), + required String name, + this.email = const Value.absent(), + this.phoneNumber = const Value.absent(), + this.fullAddress = const Value.absent(), + this.latitude = const Value.absent(), + this.longitude = const Value.absent(), + this.localPhotoPath = const Value.absent(), + this.localBannerPath = const Value.absent(), + this.serverPhotoUrl = const Value.absent(), + this.serverBannerUrl = const Value.absent(), + this.isMainOutlet = const Value.absent(), + this.isActive = const Value.absent(), + this.syncStatus = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.deletedAt = const Value.absent(), + this.rowid = const Value.absent(), + }) : name = Value(name); + static Insertable custom({ + Expression? localId, + Expression? serverId, + Expression? name, + Expression? email, + Expression? phoneNumber, + Expression? fullAddress, + Expression? latitude, + Expression? longitude, + Expression? localPhotoPath, + Expression? localBannerPath, + Expression? serverPhotoUrl, + Expression? serverBannerUrl, + Expression? isMainOutlet, + Expression? isActive, + Expression? syncStatus, + Expression? createdAt, + Expression? updatedAt, + Expression? deletedAt, + Expression? rowid, + }) { + return RawValuesInsertable({ + if (localId != null) 'local_id': localId, + if (serverId != null) 'server_id': serverId, + if (name != null) 'name': name, + if (email != null) 'email': email, + if (phoneNumber != null) 'phone_number': phoneNumber, + if (fullAddress != null) 'full_address': fullAddress, + if (latitude != null) 'latitude': latitude, + if (longitude != null) 'longitude': longitude, + if (localPhotoPath != null) 'local_photo_path': localPhotoPath, + if (localBannerPath != null) 'local_banner_path': localBannerPath, + if (serverPhotoUrl != null) 'server_photo_url': serverPhotoUrl, + if (serverBannerUrl != null) 'server_banner_url': serverBannerUrl, + if (isMainOutlet != null) 'is_main_outlet': isMainOutlet, + if (isActive != null) 'is_active': isActive, + if (syncStatus != null) 'sync_status': syncStatus, + if (createdAt != null) 'created_at': createdAt, + if (updatedAt != null) 'updated_at': updatedAt, + if (deletedAt != null) 'deleted_at': deletedAt, + if (rowid != null) 'rowid': rowid, + }); + } + + OutletsCompanion copyWith({ + Value? localId, + Value? serverId, + Value? name, + Value? email, + Value? phoneNumber, + Value? fullAddress, + Value? latitude, + Value? longitude, + Value? localPhotoPath, + Value? localBannerPath, + Value? serverPhotoUrl, + Value? serverBannerUrl, + Value? isMainOutlet, + Value? isActive, + Value? syncStatus, + Value? createdAt, + Value? updatedAt, + Value? deletedAt, + Value? rowid, + }) { + return OutletsCompanion( + localId: localId ?? this.localId, + serverId: serverId ?? this.serverId, + name: name ?? this.name, + email: email ?? this.email, + phoneNumber: phoneNumber ?? this.phoneNumber, + fullAddress: fullAddress ?? this.fullAddress, + latitude: latitude ?? this.latitude, + longitude: longitude ?? this.longitude, + localPhotoPath: localPhotoPath ?? this.localPhotoPath, + localBannerPath: localBannerPath ?? this.localBannerPath, + serverPhotoUrl: serverPhotoUrl ?? this.serverPhotoUrl, + serverBannerUrl: serverBannerUrl ?? this.serverBannerUrl, + isMainOutlet: isMainOutlet ?? this.isMainOutlet, + isActive: isActive ?? this.isActive, + syncStatus: syncStatus ?? this.syncStatus, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + deletedAt: deletedAt ?? this.deletedAt, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (localId.present) { + map['local_id'] = Variable(localId.value); + } + if (serverId.present) { + map['server_id'] = Variable(serverId.value); + } + if (name.present) { + map['name'] = Variable(name.value); + } + if (email.present) { + map['email'] = Variable(email.value); + } + if (phoneNumber.present) { + map['phone_number'] = Variable(phoneNumber.value); + } + if (fullAddress.present) { + map['full_address'] = Variable(fullAddress.value); + } + if (latitude.present) { + map['latitude'] = Variable(latitude.value); + } + if (longitude.present) { + map['longitude'] = Variable(longitude.value); + } + if (localPhotoPath.present) { + map['local_photo_path'] = Variable(localPhotoPath.value); + } + if (localBannerPath.present) { + map['local_banner_path'] = Variable(localBannerPath.value); + } + if (serverPhotoUrl.present) { + map['server_photo_url'] = Variable(serverPhotoUrl.value); + } + if (serverBannerUrl.present) { + map['server_banner_url'] = Variable(serverBannerUrl.value); + } + if (isMainOutlet.present) { + map['is_main_outlet'] = Variable(isMainOutlet.value); + } + if (isActive.present) { + map['is_active'] = Variable(isActive.value); + } + if (syncStatus.present) { + map['sync_status'] = Variable(syncStatus.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (updatedAt.present) { + map['updated_at'] = Variable(updatedAt.value); + } + if (deletedAt.present) { + map['deleted_at'] = Variable(deletedAt.value); + } + if (rowid.present) { + map['rowid'] = Variable(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('OutletsCompanion(') + ..write('localId: $localId, ') + ..write('serverId: $serverId, ') + ..write('name: $name, ') + ..write('email: $email, ') + ..write('phoneNumber: $phoneNumber, ') + ..write('fullAddress: $fullAddress, ') + ..write('latitude: $latitude, ') + ..write('longitude: $longitude, ') + ..write('localPhotoPath: $localPhotoPath, ') + ..write('localBannerPath: $localBannerPath, ') + ..write('serverPhotoUrl: $serverPhotoUrl, ') + ..write('serverBannerUrl: $serverBannerUrl, ') + ..write('isMainOutlet: $isMainOutlet, ') + ..write('isActive: $isActive, ') + ..write('syncStatus: $syncStatus, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('deletedAt: $deletedAt, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + +class $UsersTable extends Users with TableInfo<$UsersTable, User> { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + $UsersTable(this.attachedDatabase, [this._alias]); + static const VerificationMeta _localIdMeta = const VerificationMeta( + 'localId', + ); + @override + late final GeneratedColumn localId = GeneratedColumn( + 'local_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: false, + clientDefault: () => const Uuid().v7().toString(), + ); + static const VerificationMeta _serverIdMeta = const VerificationMeta( + 'serverId', + ); + @override + late final GeneratedColumn serverId = GeneratedColumn( + 'server_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _outletIdMeta = const VerificationMeta( + 'outletId', + ); + @override + late final GeneratedColumn outletId = GeneratedColumn( + 'outlet_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES outlets (local_id)', + ), + ); + static const VerificationMeta _nameMeta = const VerificationMeta('name'); + @override + late final GeneratedColumn name = GeneratedColumn( + 'name', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + static const VerificationMeta _emailMeta = const VerificationMeta('email'); + @override + late final GeneratedColumn email = GeneratedColumn( + 'email', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _phoneNumberMeta = const VerificationMeta( + 'phoneNumber', + ); + @override + late final GeneratedColumn phoneNumber = GeneratedColumn( + 'phone_number', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _passwordHashMeta = const VerificationMeta( + 'passwordHash', + ); + @override + late final GeneratedColumn passwordHash = GeneratedColumn( + 'password_hash', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _roleMeta = const VerificationMeta('role'); + @override + late final GeneratedColumn role = GeneratedColumn( + 'role', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + static const VerificationMeta _placeOfBirthMeta = const VerificationMeta( + 'placeOfBirth', + ); + @override + late final GeneratedColumn placeOfBirth = GeneratedColumn( + 'place_of_birth', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _dateOfBirthMeta = const VerificationMeta( + 'dateOfBirth', + ); + @override + late final GeneratedColumn dateOfBirth = GeneratedColumn( + 'date_of_birth', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _shiftNameMeta = const VerificationMeta( + 'shiftName', + ); + @override + late final GeneratedColumn shiftName = GeneratedColumn( + 'shift_name', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _shiftStartTimeMeta = const VerificationMeta( + 'shiftStartTime', + ); + @override + late final GeneratedColumn shiftStartTime = GeneratedColumn( + 'shift_start_time', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _shiftEndTimeMeta = const VerificationMeta( + 'shiftEndTime', + ); + @override + late final GeneratedColumn shiftEndTime = GeneratedColumn( + 'shift_end_time', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _isActiveMeta = const VerificationMeta( + 'isActive', + ); + @override + late final GeneratedColumn isActive = GeneratedColumn( + 'is_active', + aliasedName, + false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("is_active" IN (0, 1))', + ), + defaultValue: const Constant(true), + ); + static const VerificationMeta _syncStatusMeta = const VerificationMeta( + 'syncStatus', + ); + @override + late final GeneratedColumn syncStatus = GeneratedColumn( + 'sync_status', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultValue: const Constant('dirty'), + ); + static const VerificationMeta _createdAtMeta = const VerificationMeta( + 'createdAt', + ); + @override + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: currentDateAndTime, + ); + static const VerificationMeta _updatedAtMeta = const VerificationMeta( + 'updatedAt', + ); + @override + late final GeneratedColumn updatedAt = GeneratedColumn( + 'updated_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: currentDateAndTime, + ); + static const VerificationMeta _deletedAtMeta = const VerificationMeta( + 'deletedAt', + ); + @override + late final GeneratedColumn deletedAt = GeneratedColumn( + 'deleted_at', + aliasedName, + true, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + ); + @override + List get $columns => [ + localId, + serverId, + outletId, + name, + email, + phoneNumber, + passwordHash, + role, + placeOfBirth, + dateOfBirth, + shiftName, + shiftStartTime, + shiftEndTime, + isActive, + syncStatus, + createdAt, + updatedAt, + deletedAt, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'users'; + @override + VerificationContext validateIntegrity( + Insertable instance, { + bool isInserting = false, + }) { + final context = VerificationContext(); + final data = instance.toColumns(true); + if (data.containsKey('local_id')) { + context.handle( + _localIdMeta, + localId.isAcceptableOrUnknown(data['local_id']!, _localIdMeta), + ); + } + if (data.containsKey('server_id')) { + context.handle( + _serverIdMeta, + serverId.isAcceptableOrUnknown(data['server_id']!, _serverIdMeta), + ); + } + if (data.containsKey('outlet_id')) { + context.handle( + _outletIdMeta, + outletId.isAcceptableOrUnknown(data['outlet_id']!, _outletIdMeta), + ); + } else if (isInserting) { + context.missing(_outletIdMeta); + } + if (data.containsKey('name')) { + context.handle( + _nameMeta, + name.isAcceptableOrUnknown(data['name']!, _nameMeta), + ); + } else if (isInserting) { + context.missing(_nameMeta); + } + if (data.containsKey('email')) { + context.handle( + _emailMeta, + email.isAcceptableOrUnknown(data['email']!, _emailMeta), + ); + } + if (data.containsKey('phone_number')) { + context.handle( + _phoneNumberMeta, + phoneNumber.isAcceptableOrUnknown( + data['phone_number']!, + _phoneNumberMeta, + ), + ); + } + if (data.containsKey('password_hash')) { + context.handle( + _passwordHashMeta, + passwordHash.isAcceptableOrUnknown( + data['password_hash']!, + _passwordHashMeta, + ), + ); + } + if (data.containsKey('role')) { + context.handle( + _roleMeta, + role.isAcceptableOrUnknown(data['role']!, _roleMeta), + ); + } else if (isInserting) { + context.missing(_roleMeta); + } + if (data.containsKey('place_of_birth')) { + context.handle( + _placeOfBirthMeta, + placeOfBirth.isAcceptableOrUnknown( + data['place_of_birth']!, + _placeOfBirthMeta, + ), + ); + } + if (data.containsKey('date_of_birth')) { + context.handle( + _dateOfBirthMeta, + dateOfBirth.isAcceptableOrUnknown( + data['date_of_birth']!, + _dateOfBirthMeta, + ), + ); + } + if (data.containsKey('shift_name')) { + context.handle( + _shiftNameMeta, + shiftName.isAcceptableOrUnknown(data['shift_name']!, _shiftNameMeta), + ); + } + if (data.containsKey('shift_start_time')) { + context.handle( + _shiftStartTimeMeta, + shiftStartTime.isAcceptableOrUnknown( + data['shift_start_time']!, + _shiftStartTimeMeta, + ), + ); + } + if (data.containsKey('shift_end_time')) { + context.handle( + _shiftEndTimeMeta, + shiftEndTime.isAcceptableOrUnknown( + data['shift_end_time']!, + _shiftEndTimeMeta, + ), + ); + } + if (data.containsKey('is_active')) { + context.handle( + _isActiveMeta, + isActive.isAcceptableOrUnknown(data['is_active']!, _isActiveMeta), + ); + } + if (data.containsKey('sync_status')) { + context.handle( + _syncStatusMeta, + syncStatus.isAcceptableOrUnknown(data['sync_status']!, _syncStatusMeta), + ); + } + if (data.containsKey('created_at')) { + context.handle( + _createdAtMeta, + createdAt.isAcceptableOrUnknown(data['created_at']!, _createdAtMeta), + ); + } + if (data.containsKey('updated_at')) { + context.handle( + _updatedAtMeta, + updatedAt.isAcceptableOrUnknown(data['updated_at']!, _updatedAtMeta), + ); + } + if (data.containsKey('deleted_at')) { + context.handle( + _deletedAtMeta, + deletedAt.isAcceptableOrUnknown(data['deleted_at']!, _deletedAtMeta), + ); + } + return context; + } + + @override + Set get $primaryKey => {localId}; + @override + User map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return User( + localId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}local_id'], + )!, + serverId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}server_id'], + ), + outletId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}outlet_id'], + )!, + name: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}name'], + )!, + email: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}email'], + ), + phoneNumber: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}phone_number'], + ), + passwordHash: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}password_hash'], + ), + role: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}role'], + )!, + placeOfBirth: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}place_of_birth'], + ), + dateOfBirth: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}date_of_birth'], + ), + shiftName: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}shift_name'], + ), + shiftStartTime: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}shift_start_time'], + ), + shiftEndTime: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}shift_end_time'], + ), + isActive: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}is_active'], + )!, + syncStatus: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}sync_status'], + )!, + createdAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}created_at'], + )!, + updatedAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}updated_at'], + )!, + deletedAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}deleted_at'], + ), + ); + } + + @override + $UsersTable createAlias(String alias) { + return $UsersTable(attachedDatabase, alias); + } +} + +class User extends DataClass implements Insertable { + final String localId; + final String? serverId; + final String outletId; + final String name; + final String? email; + final String? phoneNumber; + final String? passwordHash; + final String role; + final String? placeOfBirth; + final String? dateOfBirth; + final String? shiftName; + final String? shiftStartTime; + final String? shiftEndTime; + final bool isActive; + final String syncStatus; + final DateTime createdAt; + final DateTime updatedAt; + final DateTime? deletedAt; + const User({ + required this.localId, + this.serverId, + required this.outletId, + required this.name, + this.email, + this.phoneNumber, + this.passwordHash, + required this.role, + this.placeOfBirth, + this.dateOfBirth, + this.shiftName, + this.shiftStartTime, + this.shiftEndTime, + required this.isActive, + required this.syncStatus, + required this.createdAt, + required this.updatedAt, + this.deletedAt, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['local_id'] = Variable(localId); + if (!nullToAbsent || serverId != null) { + map['server_id'] = Variable(serverId); + } + map['outlet_id'] = Variable(outletId); + map['name'] = Variable(name); + if (!nullToAbsent || email != null) { + map['email'] = Variable(email); + } + if (!nullToAbsent || phoneNumber != null) { + map['phone_number'] = Variable(phoneNumber); + } + if (!nullToAbsent || passwordHash != null) { + map['password_hash'] = Variable(passwordHash); + } + map['role'] = Variable(role); + if (!nullToAbsent || placeOfBirth != null) { + map['place_of_birth'] = Variable(placeOfBirth); + } + if (!nullToAbsent || dateOfBirth != null) { + map['date_of_birth'] = Variable(dateOfBirth); + } + if (!nullToAbsent || shiftName != null) { + map['shift_name'] = Variable(shiftName); + } + if (!nullToAbsent || shiftStartTime != null) { + map['shift_start_time'] = Variable(shiftStartTime); + } + if (!nullToAbsent || shiftEndTime != null) { + map['shift_end_time'] = Variable(shiftEndTime); + } + map['is_active'] = Variable(isActive); + map['sync_status'] = Variable(syncStatus); + map['created_at'] = Variable(createdAt); + map['updated_at'] = Variable(updatedAt); + if (!nullToAbsent || deletedAt != null) { + map['deleted_at'] = Variable(deletedAt); + } + return map; + } + + UsersCompanion toCompanion(bool nullToAbsent) { + return UsersCompanion( + localId: Value(localId), + serverId: serverId == null && nullToAbsent + ? const Value.absent() + : Value(serverId), + outletId: Value(outletId), + name: Value(name), + email: email == null && nullToAbsent + ? const Value.absent() + : Value(email), + phoneNumber: phoneNumber == null && nullToAbsent + ? const Value.absent() + : Value(phoneNumber), + passwordHash: passwordHash == null && nullToAbsent + ? const Value.absent() + : Value(passwordHash), + role: Value(role), + placeOfBirth: placeOfBirth == null && nullToAbsent + ? const Value.absent() + : Value(placeOfBirth), + dateOfBirth: dateOfBirth == null && nullToAbsent + ? const Value.absent() + : Value(dateOfBirth), + shiftName: shiftName == null && nullToAbsent + ? const Value.absent() + : Value(shiftName), + shiftStartTime: shiftStartTime == null && nullToAbsent + ? const Value.absent() + : Value(shiftStartTime), + shiftEndTime: shiftEndTime == null && nullToAbsent + ? const Value.absent() + : Value(shiftEndTime), + isActive: Value(isActive), + syncStatus: Value(syncStatus), + createdAt: Value(createdAt), + updatedAt: Value(updatedAt), + deletedAt: deletedAt == null && nullToAbsent + ? const Value.absent() + : Value(deletedAt), + ); + } + + factory User.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return User( + localId: serializer.fromJson(json['localId']), + serverId: serializer.fromJson(json['serverId']), + outletId: serializer.fromJson(json['outletId']), + name: serializer.fromJson(json['name']), + email: serializer.fromJson(json['email']), + phoneNumber: serializer.fromJson(json['phoneNumber']), + passwordHash: serializer.fromJson(json['passwordHash']), + role: serializer.fromJson(json['role']), + placeOfBirth: serializer.fromJson(json['placeOfBirth']), + dateOfBirth: serializer.fromJson(json['dateOfBirth']), + shiftName: serializer.fromJson(json['shiftName']), + shiftStartTime: serializer.fromJson(json['shiftStartTime']), + shiftEndTime: serializer.fromJson(json['shiftEndTime']), + isActive: serializer.fromJson(json['isActive']), + syncStatus: serializer.fromJson(json['syncStatus']), + createdAt: serializer.fromJson(json['createdAt']), + updatedAt: serializer.fromJson(json['updatedAt']), + deletedAt: serializer.fromJson(json['deletedAt']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'localId': serializer.toJson(localId), + 'serverId': serializer.toJson(serverId), + 'outletId': serializer.toJson(outletId), + 'name': serializer.toJson(name), + 'email': serializer.toJson(email), + 'phoneNumber': serializer.toJson(phoneNumber), + 'passwordHash': serializer.toJson(passwordHash), + 'role': serializer.toJson(role), + 'placeOfBirth': serializer.toJson(placeOfBirth), + 'dateOfBirth': serializer.toJson(dateOfBirth), + 'shiftName': serializer.toJson(shiftName), + 'shiftStartTime': serializer.toJson(shiftStartTime), + 'shiftEndTime': serializer.toJson(shiftEndTime), + 'isActive': serializer.toJson(isActive), + 'syncStatus': serializer.toJson(syncStatus), + 'createdAt': serializer.toJson(createdAt), + 'updatedAt': serializer.toJson(updatedAt), + 'deletedAt': serializer.toJson(deletedAt), + }; + } + + User copyWith({ + String? localId, + Value serverId = const Value.absent(), + String? outletId, + String? name, + Value email = const Value.absent(), + Value phoneNumber = const Value.absent(), + Value passwordHash = const Value.absent(), + String? role, + Value placeOfBirth = const Value.absent(), + Value dateOfBirth = const Value.absent(), + Value shiftName = const Value.absent(), + Value shiftStartTime = const Value.absent(), + Value shiftEndTime = const Value.absent(), + bool? isActive, + String? syncStatus, + DateTime? createdAt, + DateTime? updatedAt, + Value deletedAt = const Value.absent(), + }) => User( + localId: localId ?? this.localId, + serverId: serverId.present ? serverId.value : this.serverId, + outletId: outletId ?? this.outletId, + name: name ?? this.name, + email: email.present ? email.value : this.email, + phoneNumber: phoneNumber.present ? phoneNumber.value : this.phoneNumber, + passwordHash: passwordHash.present ? passwordHash.value : this.passwordHash, + role: role ?? this.role, + placeOfBirth: placeOfBirth.present ? placeOfBirth.value : this.placeOfBirth, + dateOfBirth: dateOfBirth.present ? dateOfBirth.value : this.dateOfBirth, + shiftName: shiftName.present ? shiftName.value : this.shiftName, + shiftStartTime: shiftStartTime.present + ? shiftStartTime.value + : this.shiftStartTime, + shiftEndTime: shiftEndTime.present ? shiftEndTime.value : this.shiftEndTime, + isActive: isActive ?? this.isActive, + syncStatus: syncStatus ?? this.syncStatus, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + deletedAt: deletedAt.present ? deletedAt.value : this.deletedAt, + ); + User copyWithCompanion(UsersCompanion data) { + return User( + localId: data.localId.present ? data.localId.value : this.localId, + serverId: data.serverId.present ? data.serverId.value : this.serverId, + outletId: data.outletId.present ? data.outletId.value : this.outletId, + name: data.name.present ? data.name.value : this.name, + email: data.email.present ? data.email.value : this.email, + phoneNumber: data.phoneNumber.present + ? data.phoneNumber.value + : this.phoneNumber, + passwordHash: data.passwordHash.present + ? data.passwordHash.value + : this.passwordHash, + role: data.role.present ? data.role.value : this.role, + placeOfBirth: data.placeOfBirth.present + ? data.placeOfBirth.value + : this.placeOfBirth, + dateOfBirth: data.dateOfBirth.present + ? data.dateOfBirth.value + : this.dateOfBirth, + shiftName: data.shiftName.present ? data.shiftName.value : this.shiftName, + shiftStartTime: data.shiftStartTime.present + ? data.shiftStartTime.value + : this.shiftStartTime, + shiftEndTime: data.shiftEndTime.present + ? data.shiftEndTime.value + : this.shiftEndTime, + isActive: data.isActive.present ? data.isActive.value : this.isActive, + syncStatus: data.syncStatus.present + ? data.syncStatus.value + : this.syncStatus, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, + deletedAt: data.deletedAt.present ? data.deletedAt.value : this.deletedAt, + ); + } + + @override + String toString() { + return (StringBuffer('User(') + ..write('localId: $localId, ') + ..write('serverId: $serverId, ') + ..write('outletId: $outletId, ') + ..write('name: $name, ') + ..write('email: $email, ') + ..write('phoneNumber: $phoneNumber, ') + ..write('passwordHash: $passwordHash, ') + ..write('role: $role, ') + ..write('placeOfBirth: $placeOfBirth, ') + ..write('dateOfBirth: $dateOfBirth, ') + ..write('shiftName: $shiftName, ') + ..write('shiftStartTime: $shiftStartTime, ') + ..write('shiftEndTime: $shiftEndTime, ') + ..write('isActive: $isActive, ') + ..write('syncStatus: $syncStatus, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('deletedAt: $deletedAt') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + localId, + serverId, + outletId, + name, + email, + phoneNumber, + passwordHash, + role, + placeOfBirth, + dateOfBirth, + shiftName, + shiftStartTime, + shiftEndTime, + isActive, + syncStatus, + createdAt, + updatedAt, + deletedAt, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is User && + other.localId == this.localId && + other.serverId == this.serverId && + other.outletId == this.outletId && + other.name == this.name && + other.email == this.email && + other.phoneNumber == this.phoneNumber && + other.passwordHash == this.passwordHash && + other.role == this.role && + other.placeOfBirth == this.placeOfBirth && + other.dateOfBirth == this.dateOfBirth && + other.shiftName == this.shiftName && + other.shiftStartTime == this.shiftStartTime && + other.shiftEndTime == this.shiftEndTime && + other.isActive == this.isActive && + other.syncStatus == this.syncStatus && + other.createdAt == this.createdAt && + other.updatedAt == this.updatedAt && + other.deletedAt == this.deletedAt); +} + +class UsersCompanion extends UpdateCompanion { + final Value localId; + final Value serverId; + final Value outletId; + final Value name; + final Value email; + final Value phoneNumber; + final Value passwordHash; + final Value role; + final Value placeOfBirth; + final Value dateOfBirth; + final Value shiftName; + final Value shiftStartTime; + final Value shiftEndTime; + final Value isActive; + final Value syncStatus; + final Value createdAt; + final Value updatedAt; + final Value deletedAt; + final Value rowid; + const UsersCompanion({ + this.localId = const Value.absent(), + this.serverId = const Value.absent(), + this.outletId = const Value.absent(), + this.name = const Value.absent(), + this.email = const Value.absent(), + this.phoneNumber = const Value.absent(), + this.passwordHash = const Value.absent(), + this.role = const Value.absent(), + this.placeOfBirth = const Value.absent(), + this.dateOfBirth = const Value.absent(), + this.shiftName = const Value.absent(), + this.shiftStartTime = const Value.absent(), + this.shiftEndTime = const Value.absent(), + this.isActive = const Value.absent(), + this.syncStatus = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.deletedAt = const Value.absent(), + this.rowid = const Value.absent(), + }); + UsersCompanion.insert({ + this.localId = const Value.absent(), + this.serverId = const Value.absent(), + required String outletId, + required String name, + this.email = const Value.absent(), + this.phoneNumber = const Value.absent(), + this.passwordHash = const Value.absent(), + required String role, + this.placeOfBirth = const Value.absent(), + this.dateOfBirth = const Value.absent(), + this.shiftName = const Value.absent(), + this.shiftStartTime = const Value.absent(), + this.shiftEndTime = const Value.absent(), + this.isActive = const Value.absent(), + this.syncStatus = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.deletedAt = const Value.absent(), + this.rowid = const Value.absent(), + }) : outletId = Value(outletId), + name = Value(name), + role = Value(role); + static Insertable custom({ + Expression? localId, + Expression? serverId, + Expression? outletId, + Expression? name, + Expression? email, + Expression? phoneNumber, + Expression? passwordHash, + Expression? role, + Expression? placeOfBirth, + Expression? dateOfBirth, + Expression? shiftName, + Expression? shiftStartTime, + Expression? shiftEndTime, + Expression? isActive, + Expression? syncStatus, + Expression? createdAt, + Expression? updatedAt, + Expression? deletedAt, + Expression? rowid, + }) { + return RawValuesInsertable({ + if (localId != null) 'local_id': localId, + if (serverId != null) 'server_id': serverId, + if (outletId != null) 'outlet_id': outletId, + if (name != null) 'name': name, + if (email != null) 'email': email, + if (phoneNumber != null) 'phone_number': phoneNumber, + if (passwordHash != null) 'password_hash': passwordHash, + if (role != null) 'role': role, + if (placeOfBirth != null) 'place_of_birth': placeOfBirth, + if (dateOfBirth != null) 'date_of_birth': dateOfBirth, + if (shiftName != null) 'shift_name': shiftName, + if (shiftStartTime != null) 'shift_start_time': shiftStartTime, + if (shiftEndTime != null) 'shift_end_time': shiftEndTime, + if (isActive != null) 'is_active': isActive, + if (syncStatus != null) 'sync_status': syncStatus, + if (createdAt != null) 'created_at': createdAt, + if (updatedAt != null) 'updated_at': updatedAt, + if (deletedAt != null) 'deleted_at': deletedAt, + if (rowid != null) 'rowid': rowid, + }); + } + + UsersCompanion copyWith({ + Value? localId, + Value? serverId, + Value? outletId, + Value? name, + Value? email, + Value? phoneNumber, + Value? passwordHash, + Value? role, + Value? placeOfBirth, + Value? dateOfBirth, + Value? shiftName, + Value? shiftStartTime, + Value? shiftEndTime, + Value? isActive, + Value? syncStatus, + Value? createdAt, + Value? updatedAt, + Value? deletedAt, + Value? rowid, + }) { + return UsersCompanion( + localId: localId ?? this.localId, + serverId: serverId ?? this.serverId, + outletId: outletId ?? this.outletId, + name: name ?? this.name, + email: email ?? this.email, + phoneNumber: phoneNumber ?? this.phoneNumber, + passwordHash: passwordHash ?? this.passwordHash, + role: role ?? this.role, + placeOfBirth: placeOfBirth ?? this.placeOfBirth, + dateOfBirth: dateOfBirth ?? this.dateOfBirth, + shiftName: shiftName ?? this.shiftName, + shiftStartTime: shiftStartTime ?? this.shiftStartTime, + shiftEndTime: shiftEndTime ?? this.shiftEndTime, + isActive: isActive ?? this.isActive, + syncStatus: syncStatus ?? this.syncStatus, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + deletedAt: deletedAt ?? this.deletedAt, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (localId.present) { + map['local_id'] = Variable(localId.value); + } + if (serverId.present) { + map['server_id'] = Variable(serverId.value); + } + if (outletId.present) { + map['outlet_id'] = Variable(outletId.value); + } + if (name.present) { + map['name'] = Variable(name.value); + } + if (email.present) { + map['email'] = Variable(email.value); + } + if (phoneNumber.present) { + map['phone_number'] = Variable(phoneNumber.value); + } + if (passwordHash.present) { + map['password_hash'] = Variable(passwordHash.value); + } + if (role.present) { + map['role'] = Variable(role.value); + } + if (placeOfBirth.present) { + map['place_of_birth'] = Variable(placeOfBirth.value); + } + if (dateOfBirth.present) { + map['date_of_birth'] = Variable(dateOfBirth.value); + } + if (shiftName.present) { + map['shift_name'] = Variable(shiftName.value); + } + if (shiftStartTime.present) { + map['shift_start_time'] = Variable(shiftStartTime.value); + } + if (shiftEndTime.present) { + map['shift_end_time'] = Variable(shiftEndTime.value); + } + if (isActive.present) { + map['is_active'] = Variable(isActive.value); + } + if (syncStatus.present) { + map['sync_status'] = Variable(syncStatus.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (updatedAt.present) { + map['updated_at'] = Variable(updatedAt.value); + } + if (deletedAt.present) { + map['deleted_at'] = Variable(deletedAt.value); + } + if (rowid.present) { + map['rowid'] = Variable(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('UsersCompanion(') + ..write('localId: $localId, ') + ..write('serverId: $serverId, ') + ..write('outletId: $outletId, ') + ..write('name: $name, ') + ..write('email: $email, ') + ..write('phoneNumber: $phoneNumber, ') + ..write('passwordHash: $passwordHash, ') + ..write('role: $role, ') + ..write('placeOfBirth: $placeOfBirth, ') + ..write('dateOfBirth: $dateOfBirth, ') + ..write('shiftName: $shiftName, ') + ..write('shiftStartTime: $shiftStartTime, ') + ..write('shiftEndTime: $shiftEndTime, ') + ..write('isActive: $isActive, ') + ..write('syncStatus: $syncStatus, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('deletedAt: $deletedAt, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + +class $CustomersTable extends Customers + with TableInfo<$CustomersTable, Customer> { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + $CustomersTable(this.attachedDatabase, [this._alias]); + static const VerificationMeta _localIdMeta = const VerificationMeta( + 'localId', + ); + @override + late final GeneratedColumn localId = GeneratedColumn( + 'local_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: false, + clientDefault: () => const Uuid().v7().toString(), + ); + static const VerificationMeta _serverIdMeta = const VerificationMeta( + 'serverId', + ); + @override + late final GeneratedColumn serverId = GeneratedColumn( + 'server_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _outletIdMeta = const VerificationMeta( + 'outletId', + ); + @override + late final GeneratedColumn outletId = GeneratedColumn( + 'outlet_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES outlets (local_id)', + ), + ); + static const VerificationMeta _nameMeta = const VerificationMeta('name'); + @override + late final GeneratedColumn name = GeneratedColumn( + 'name', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + static const VerificationMeta _emailMeta = const VerificationMeta('email'); + @override + late final GeneratedColumn email = GeneratedColumn( + 'email', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _phoneNumberMeta = const VerificationMeta( + 'phoneNumber', + ); + @override + late final GeneratedColumn phoneNumber = GeneratedColumn( + 'phone_number', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _passwordHashMeta = const VerificationMeta( + 'passwordHash', + ); + @override + late final GeneratedColumn passwordHash = GeneratedColumn( + 'password_hash', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _sourceMeta = const VerificationMeta('source'); + @override + late final GeneratedColumn source = GeneratedColumn( + 'source', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + static const VerificationMeta _syncStatusMeta = const VerificationMeta( + 'syncStatus', + ); + @override + late final GeneratedColumn syncStatus = GeneratedColumn( + 'sync_status', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultValue: const Constant('dirty'), + ); + static const VerificationMeta _createdAtMeta = const VerificationMeta( + 'createdAt', + ); + @override + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: currentDateAndTime, + ); + static const VerificationMeta _updatedAtMeta = const VerificationMeta( + 'updatedAt', + ); + @override + late final GeneratedColumn updatedAt = GeneratedColumn( + 'updated_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: currentDateAndTime, + ); + static const VerificationMeta _deletedAtMeta = const VerificationMeta( + 'deletedAt', + ); + @override + late final GeneratedColumn deletedAt = GeneratedColumn( + 'deleted_at', + aliasedName, + true, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + ); + @override + List get $columns => [ + localId, + serverId, + outletId, + name, + email, + phoneNumber, + passwordHash, + source, + syncStatus, + createdAt, + updatedAt, + deletedAt, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'customers'; + @override + VerificationContext validateIntegrity( + Insertable instance, { + bool isInserting = false, + }) { + final context = VerificationContext(); + final data = instance.toColumns(true); + if (data.containsKey('local_id')) { + context.handle( + _localIdMeta, + localId.isAcceptableOrUnknown(data['local_id']!, _localIdMeta), + ); + } + if (data.containsKey('server_id')) { + context.handle( + _serverIdMeta, + serverId.isAcceptableOrUnknown(data['server_id']!, _serverIdMeta), + ); + } + if (data.containsKey('outlet_id')) { + context.handle( + _outletIdMeta, + outletId.isAcceptableOrUnknown(data['outlet_id']!, _outletIdMeta), + ); + } else if (isInserting) { + context.missing(_outletIdMeta); + } + if (data.containsKey('name')) { + context.handle( + _nameMeta, + name.isAcceptableOrUnknown(data['name']!, _nameMeta), + ); + } else if (isInserting) { + context.missing(_nameMeta); + } + if (data.containsKey('email')) { + context.handle( + _emailMeta, + email.isAcceptableOrUnknown(data['email']!, _emailMeta), + ); + } + if (data.containsKey('phone_number')) { + context.handle( + _phoneNumberMeta, + phoneNumber.isAcceptableOrUnknown( + data['phone_number']!, + _phoneNumberMeta, + ), + ); + } + if (data.containsKey('password_hash')) { + context.handle( + _passwordHashMeta, + passwordHash.isAcceptableOrUnknown( + data['password_hash']!, + _passwordHashMeta, + ), + ); + } + if (data.containsKey('source')) { + context.handle( + _sourceMeta, + source.isAcceptableOrUnknown(data['source']!, _sourceMeta), + ); + } else if (isInserting) { + context.missing(_sourceMeta); + } + if (data.containsKey('sync_status')) { + context.handle( + _syncStatusMeta, + syncStatus.isAcceptableOrUnknown(data['sync_status']!, _syncStatusMeta), + ); + } + if (data.containsKey('created_at')) { + context.handle( + _createdAtMeta, + createdAt.isAcceptableOrUnknown(data['created_at']!, _createdAtMeta), + ); + } + if (data.containsKey('updated_at')) { + context.handle( + _updatedAtMeta, + updatedAt.isAcceptableOrUnknown(data['updated_at']!, _updatedAtMeta), + ); + } + if (data.containsKey('deleted_at')) { + context.handle( + _deletedAtMeta, + deletedAt.isAcceptableOrUnknown(data['deleted_at']!, _deletedAtMeta), + ); + } + return context; + } + + @override + Set get $primaryKey => {localId}; + @override + Customer map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return Customer( + localId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}local_id'], + )!, + serverId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}server_id'], + ), + outletId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}outlet_id'], + )!, + name: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}name'], + )!, + email: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}email'], + ), + phoneNumber: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}phone_number'], + ), + passwordHash: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}password_hash'], + ), + source: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}source'], + )!, + syncStatus: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}sync_status'], + )!, + createdAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}created_at'], + )!, + updatedAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}updated_at'], + )!, + deletedAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}deleted_at'], + ), + ); + } + + @override + $CustomersTable createAlias(String alias) { + return $CustomersTable(attachedDatabase, alias); + } +} + +class Customer extends DataClass implements Insertable { + final String localId; + final String? serverId; + final String outletId; + final String name; + final String? email; + final String? phoneNumber; + final String? passwordHash; + final String source; + final String syncStatus; + final DateTime createdAt; + final DateTime updatedAt; + final DateTime? deletedAt; + const Customer({ + required this.localId, + this.serverId, + required this.outletId, + required this.name, + this.email, + this.phoneNumber, + this.passwordHash, + required this.source, + required this.syncStatus, + required this.createdAt, + required this.updatedAt, + this.deletedAt, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['local_id'] = Variable(localId); + if (!nullToAbsent || serverId != null) { + map['server_id'] = Variable(serverId); + } + map['outlet_id'] = Variable(outletId); + map['name'] = Variable(name); + if (!nullToAbsent || email != null) { + map['email'] = Variable(email); + } + if (!nullToAbsent || phoneNumber != null) { + map['phone_number'] = Variable(phoneNumber); + } + if (!nullToAbsent || passwordHash != null) { + map['password_hash'] = Variable(passwordHash); + } + map['source'] = Variable(source); + map['sync_status'] = Variable(syncStatus); + map['created_at'] = Variable(createdAt); + map['updated_at'] = Variable(updatedAt); + if (!nullToAbsent || deletedAt != null) { + map['deleted_at'] = Variable(deletedAt); + } + return map; + } + + CustomersCompanion toCompanion(bool nullToAbsent) { + return CustomersCompanion( + localId: Value(localId), + serverId: serverId == null && nullToAbsent + ? const Value.absent() + : Value(serverId), + outletId: Value(outletId), + name: Value(name), + email: email == null && nullToAbsent + ? const Value.absent() + : Value(email), + phoneNumber: phoneNumber == null && nullToAbsent + ? const Value.absent() + : Value(phoneNumber), + passwordHash: passwordHash == null && nullToAbsent + ? const Value.absent() + : Value(passwordHash), + source: Value(source), + syncStatus: Value(syncStatus), + createdAt: Value(createdAt), + updatedAt: Value(updatedAt), + deletedAt: deletedAt == null && nullToAbsent + ? const Value.absent() + : Value(deletedAt), + ); + } + + factory Customer.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return Customer( + localId: serializer.fromJson(json['localId']), + serverId: serializer.fromJson(json['serverId']), + outletId: serializer.fromJson(json['outletId']), + name: serializer.fromJson(json['name']), + email: serializer.fromJson(json['email']), + phoneNumber: serializer.fromJson(json['phoneNumber']), + passwordHash: serializer.fromJson(json['passwordHash']), + source: serializer.fromJson(json['source']), + syncStatus: serializer.fromJson(json['syncStatus']), + createdAt: serializer.fromJson(json['createdAt']), + updatedAt: serializer.fromJson(json['updatedAt']), + deletedAt: serializer.fromJson(json['deletedAt']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'localId': serializer.toJson(localId), + 'serverId': serializer.toJson(serverId), + 'outletId': serializer.toJson(outletId), + 'name': serializer.toJson(name), + 'email': serializer.toJson(email), + 'phoneNumber': serializer.toJson(phoneNumber), + 'passwordHash': serializer.toJson(passwordHash), + 'source': serializer.toJson(source), + 'syncStatus': serializer.toJson(syncStatus), + 'createdAt': serializer.toJson(createdAt), + 'updatedAt': serializer.toJson(updatedAt), + 'deletedAt': serializer.toJson(deletedAt), + }; + } + + Customer copyWith({ + String? localId, + Value serverId = const Value.absent(), + String? outletId, + String? name, + Value email = const Value.absent(), + Value phoneNumber = const Value.absent(), + Value passwordHash = const Value.absent(), + String? source, + String? syncStatus, + DateTime? createdAt, + DateTime? updatedAt, + Value deletedAt = const Value.absent(), + }) => Customer( + localId: localId ?? this.localId, + serverId: serverId.present ? serverId.value : this.serverId, + outletId: outletId ?? this.outletId, + name: name ?? this.name, + email: email.present ? email.value : this.email, + phoneNumber: phoneNumber.present ? phoneNumber.value : this.phoneNumber, + passwordHash: passwordHash.present ? passwordHash.value : this.passwordHash, + source: source ?? this.source, + syncStatus: syncStatus ?? this.syncStatus, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + deletedAt: deletedAt.present ? deletedAt.value : this.deletedAt, + ); + Customer copyWithCompanion(CustomersCompanion data) { + return Customer( + localId: data.localId.present ? data.localId.value : this.localId, + serverId: data.serverId.present ? data.serverId.value : this.serverId, + outletId: data.outletId.present ? data.outletId.value : this.outletId, + name: data.name.present ? data.name.value : this.name, + email: data.email.present ? data.email.value : this.email, + phoneNumber: data.phoneNumber.present + ? data.phoneNumber.value + : this.phoneNumber, + passwordHash: data.passwordHash.present + ? data.passwordHash.value + : this.passwordHash, + source: data.source.present ? data.source.value : this.source, + syncStatus: data.syncStatus.present + ? data.syncStatus.value + : this.syncStatus, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, + deletedAt: data.deletedAt.present ? data.deletedAt.value : this.deletedAt, + ); + } + + @override + String toString() { + return (StringBuffer('Customer(') + ..write('localId: $localId, ') + ..write('serverId: $serverId, ') + ..write('outletId: $outletId, ') + ..write('name: $name, ') + ..write('email: $email, ') + ..write('phoneNumber: $phoneNumber, ') + ..write('passwordHash: $passwordHash, ') + ..write('source: $source, ') + ..write('syncStatus: $syncStatus, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('deletedAt: $deletedAt') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + localId, + serverId, + outletId, + name, + email, + phoneNumber, + passwordHash, + source, + syncStatus, + createdAt, + updatedAt, + deletedAt, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is Customer && + other.localId == this.localId && + other.serverId == this.serverId && + other.outletId == this.outletId && + other.name == this.name && + other.email == this.email && + other.phoneNumber == this.phoneNumber && + other.passwordHash == this.passwordHash && + other.source == this.source && + other.syncStatus == this.syncStatus && + other.createdAt == this.createdAt && + other.updatedAt == this.updatedAt && + other.deletedAt == this.deletedAt); +} + +class CustomersCompanion extends UpdateCompanion { + final Value localId; + final Value serverId; + final Value outletId; + final Value name; + final Value email; + final Value phoneNumber; + final Value passwordHash; + final Value source; + final Value syncStatus; + final Value createdAt; + final Value updatedAt; + final Value deletedAt; + final Value rowid; + const CustomersCompanion({ + this.localId = const Value.absent(), + this.serverId = const Value.absent(), + this.outletId = const Value.absent(), + this.name = const Value.absent(), + this.email = const Value.absent(), + this.phoneNumber = const Value.absent(), + this.passwordHash = const Value.absent(), + this.source = const Value.absent(), + this.syncStatus = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.deletedAt = const Value.absent(), + this.rowid = const Value.absent(), + }); + CustomersCompanion.insert({ + this.localId = const Value.absent(), + this.serverId = const Value.absent(), + required String outletId, + required String name, + this.email = const Value.absent(), + this.phoneNumber = const Value.absent(), + this.passwordHash = const Value.absent(), + required String source, + this.syncStatus = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.deletedAt = const Value.absent(), + this.rowid = const Value.absent(), + }) : outletId = Value(outletId), + name = Value(name), + source = Value(source); + static Insertable custom({ + Expression? localId, + Expression? serverId, + Expression? outletId, + Expression? name, + Expression? email, + Expression? phoneNumber, + Expression? passwordHash, + Expression? source, + Expression? syncStatus, + Expression? createdAt, + Expression? updatedAt, + Expression? deletedAt, + Expression? rowid, + }) { + return RawValuesInsertable({ + if (localId != null) 'local_id': localId, + if (serverId != null) 'server_id': serverId, + if (outletId != null) 'outlet_id': outletId, + if (name != null) 'name': name, + if (email != null) 'email': email, + if (phoneNumber != null) 'phone_number': phoneNumber, + if (passwordHash != null) 'password_hash': passwordHash, + if (source != null) 'source': source, + if (syncStatus != null) 'sync_status': syncStatus, + if (createdAt != null) 'created_at': createdAt, + if (updatedAt != null) 'updated_at': updatedAt, + if (deletedAt != null) 'deleted_at': deletedAt, + if (rowid != null) 'rowid': rowid, + }); + } + + CustomersCompanion copyWith({ + Value? localId, + Value? serverId, + Value? outletId, + Value? name, + Value? email, + Value? phoneNumber, + Value? passwordHash, + Value? source, + Value? syncStatus, + Value? createdAt, + Value? updatedAt, + Value? deletedAt, + Value? rowid, + }) { + return CustomersCompanion( + localId: localId ?? this.localId, + serverId: serverId ?? this.serverId, + outletId: outletId ?? this.outletId, + name: name ?? this.name, + email: email ?? this.email, + phoneNumber: phoneNumber ?? this.phoneNumber, + passwordHash: passwordHash ?? this.passwordHash, + source: source ?? this.source, + syncStatus: syncStatus ?? this.syncStatus, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + deletedAt: deletedAt ?? this.deletedAt, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (localId.present) { + map['local_id'] = Variable(localId.value); + } + if (serverId.present) { + map['server_id'] = Variable(serverId.value); + } + if (outletId.present) { + map['outlet_id'] = Variable(outletId.value); + } + if (name.present) { + map['name'] = Variable(name.value); + } + if (email.present) { + map['email'] = Variable(email.value); + } + if (phoneNumber.present) { + map['phone_number'] = Variable(phoneNumber.value); + } + if (passwordHash.present) { + map['password_hash'] = Variable(passwordHash.value); + } + if (source.present) { + map['source'] = Variable(source.value); + } + if (syncStatus.present) { + map['sync_status'] = Variable(syncStatus.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (updatedAt.present) { + map['updated_at'] = Variable(updatedAt.value); + } + if (deletedAt.present) { + map['deleted_at'] = Variable(deletedAt.value); + } + if (rowid.present) { + map['rowid'] = Variable(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('CustomersCompanion(') + ..write('localId: $localId, ') + ..write('serverId: $serverId, ') + ..write('outletId: $outletId, ') + ..write('name: $name, ') + ..write('email: $email, ') + ..write('phoneNumber: $phoneNumber, ') + ..write('passwordHash: $passwordHash, ') + ..write('source: $source, ') + ..write('syncStatus: $syncStatus, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('deletedAt: $deletedAt, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + +class $CustomerAddressesTable extends CustomerAddresses + with TableInfo<$CustomerAddressesTable, CustomerAddressesData> { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + $CustomerAddressesTable(this.attachedDatabase, [this._alias]); + static const VerificationMeta _localIdMeta = const VerificationMeta( + 'localId', + ); + @override + late final GeneratedColumn localId = GeneratedColumn( + 'local_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: false, + clientDefault: () => const Uuid().v7().toString(), + ); + static const VerificationMeta _serverIdMeta = const VerificationMeta( + 'serverId', + ); + @override + late final GeneratedColumn serverId = GeneratedColumn( + 'server_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _customerIdMeta = const VerificationMeta( + 'customerId', + ); + @override + late final GeneratedColumn customerId = GeneratedColumn( + 'customer_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES customers (local_id)', + ), + ); + static const VerificationMeta _labelMeta = const VerificationMeta('label'); + @override + late final GeneratedColumn label = GeneratedColumn( + 'label', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _fullAddressMeta = const VerificationMeta( + 'fullAddress', + ); + @override + late final GeneratedColumn fullAddress = GeneratedColumn( + 'full_address', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _latitudeMeta = const VerificationMeta( + 'latitude', + ); + @override + late final GeneratedColumn latitude = GeneratedColumn( + 'latitude', + aliasedName, + true, + type: DriftSqlType.double, + requiredDuringInsert: false, + ); + static const VerificationMeta _longitudeMeta = const VerificationMeta( + 'longitude', + ); + @override + late final GeneratedColumn longitude = GeneratedColumn( + 'longitude', + aliasedName, + true, + type: DriftSqlType.double, + requiredDuringInsert: false, + ); + static const VerificationMeta _notesMeta = const VerificationMeta('notes'); + @override + late final GeneratedColumn notes = GeneratedColumn( + 'notes', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _syncStatusMeta = const VerificationMeta( + 'syncStatus', + ); + @override + late final GeneratedColumn syncStatus = GeneratedColumn( + 'sync_status', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultValue: const Constant('dirty'), + ); + static const VerificationMeta _createdAtMeta = const VerificationMeta( + 'createdAt', + ); + @override + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: currentDateAndTime, + ); + static const VerificationMeta _updatedAtMeta = const VerificationMeta( + 'updatedAt', + ); + @override + late final GeneratedColumn updatedAt = GeneratedColumn( + 'updated_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: currentDateAndTime, + ); + static const VerificationMeta _deletedAtMeta = const VerificationMeta( + 'deletedAt', + ); + @override + late final GeneratedColumn deletedAt = GeneratedColumn( + 'deleted_at', + aliasedName, + true, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + ); + @override + List get $columns => [ + localId, + serverId, + customerId, + label, + fullAddress, + latitude, + longitude, + notes, + syncStatus, + createdAt, + updatedAt, + deletedAt, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'customer_addresses'; + @override + VerificationContext validateIntegrity( + Insertable instance, { + bool isInserting = false, + }) { + final context = VerificationContext(); + final data = instance.toColumns(true); + if (data.containsKey('local_id')) { + context.handle( + _localIdMeta, + localId.isAcceptableOrUnknown(data['local_id']!, _localIdMeta), + ); + } + if (data.containsKey('server_id')) { + context.handle( + _serverIdMeta, + serverId.isAcceptableOrUnknown(data['server_id']!, _serverIdMeta), + ); + } + if (data.containsKey('customer_id')) { + context.handle( + _customerIdMeta, + customerId.isAcceptableOrUnknown(data['customer_id']!, _customerIdMeta), + ); + } else if (isInserting) { + context.missing(_customerIdMeta); + } + if (data.containsKey('label')) { + context.handle( + _labelMeta, + label.isAcceptableOrUnknown(data['label']!, _labelMeta), + ); + } + if (data.containsKey('full_address')) { + context.handle( + _fullAddressMeta, + fullAddress.isAcceptableOrUnknown( + data['full_address']!, + _fullAddressMeta, + ), + ); + } + if (data.containsKey('latitude')) { + context.handle( + _latitudeMeta, + latitude.isAcceptableOrUnknown(data['latitude']!, _latitudeMeta), + ); + } + if (data.containsKey('longitude')) { + context.handle( + _longitudeMeta, + longitude.isAcceptableOrUnknown(data['longitude']!, _longitudeMeta), + ); + } + if (data.containsKey('notes')) { + context.handle( + _notesMeta, + notes.isAcceptableOrUnknown(data['notes']!, _notesMeta), + ); + } + if (data.containsKey('sync_status')) { + context.handle( + _syncStatusMeta, + syncStatus.isAcceptableOrUnknown(data['sync_status']!, _syncStatusMeta), + ); + } + if (data.containsKey('created_at')) { + context.handle( + _createdAtMeta, + createdAt.isAcceptableOrUnknown(data['created_at']!, _createdAtMeta), + ); + } + if (data.containsKey('updated_at')) { + context.handle( + _updatedAtMeta, + updatedAt.isAcceptableOrUnknown(data['updated_at']!, _updatedAtMeta), + ); + } + if (data.containsKey('deleted_at')) { + context.handle( + _deletedAtMeta, + deletedAt.isAcceptableOrUnknown(data['deleted_at']!, _deletedAtMeta), + ); + } + return context; + } + + @override + Set get $primaryKey => {localId}; + @override + CustomerAddressesData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return CustomerAddressesData( + localId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}local_id'], + )!, + serverId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}server_id'], + ), + customerId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}customer_id'], + )!, + label: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}label'], + ), + fullAddress: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}full_address'], + ), + latitude: attachedDatabase.typeMapping.read( + DriftSqlType.double, + data['${effectivePrefix}latitude'], + ), + longitude: attachedDatabase.typeMapping.read( + DriftSqlType.double, + data['${effectivePrefix}longitude'], + ), + notes: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}notes'], + ), + syncStatus: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}sync_status'], + )!, + createdAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}created_at'], + )!, + updatedAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}updated_at'], + )!, + deletedAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}deleted_at'], + ), + ); + } + + @override + $CustomerAddressesTable createAlias(String alias) { + return $CustomerAddressesTable(attachedDatabase, alias); + } +} + +class CustomerAddressesData extends DataClass + implements Insertable { + final String localId; + final String? serverId; + final String customerId; + final String? label; + final String? fullAddress; + final double? latitude; + final double? longitude; + final String? notes; + final String syncStatus; + final DateTime createdAt; + final DateTime updatedAt; + final DateTime? deletedAt; + const CustomerAddressesData({ + required this.localId, + this.serverId, + required this.customerId, + this.label, + this.fullAddress, + this.latitude, + this.longitude, + this.notes, + required this.syncStatus, + required this.createdAt, + required this.updatedAt, + this.deletedAt, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['local_id'] = Variable(localId); + if (!nullToAbsent || serverId != null) { + map['server_id'] = Variable(serverId); + } + map['customer_id'] = Variable(customerId); + if (!nullToAbsent || label != null) { + map['label'] = Variable(label); + } + if (!nullToAbsent || fullAddress != null) { + map['full_address'] = Variable(fullAddress); + } + if (!nullToAbsent || latitude != null) { + map['latitude'] = Variable(latitude); + } + if (!nullToAbsent || longitude != null) { + map['longitude'] = Variable(longitude); + } + if (!nullToAbsent || notes != null) { + map['notes'] = Variable(notes); + } + map['sync_status'] = Variable(syncStatus); + map['created_at'] = Variable(createdAt); + map['updated_at'] = Variable(updatedAt); + if (!nullToAbsent || deletedAt != null) { + map['deleted_at'] = Variable(deletedAt); + } + return map; + } + + CustomerAddressesCompanion toCompanion(bool nullToAbsent) { + return CustomerAddressesCompanion( + localId: Value(localId), + serverId: serverId == null && nullToAbsent + ? const Value.absent() + : Value(serverId), + customerId: Value(customerId), + label: label == null && nullToAbsent + ? const Value.absent() + : Value(label), + fullAddress: fullAddress == null && nullToAbsent + ? const Value.absent() + : Value(fullAddress), + latitude: latitude == null && nullToAbsent + ? const Value.absent() + : Value(latitude), + longitude: longitude == null && nullToAbsent + ? const Value.absent() + : Value(longitude), + notes: notes == null && nullToAbsent + ? const Value.absent() + : Value(notes), + syncStatus: Value(syncStatus), + createdAt: Value(createdAt), + updatedAt: Value(updatedAt), + deletedAt: deletedAt == null && nullToAbsent + ? const Value.absent() + : Value(deletedAt), + ); + } + + factory CustomerAddressesData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return CustomerAddressesData( + localId: serializer.fromJson(json['localId']), + serverId: serializer.fromJson(json['serverId']), + customerId: serializer.fromJson(json['customerId']), + label: serializer.fromJson(json['label']), + fullAddress: serializer.fromJson(json['fullAddress']), + latitude: serializer.fromJson(json['latitude']), + longitude: serializer.fromJson(json['longitude']), + notes: serializer.fromJson(json['notes']), + syncStatus: serializer.fromJson(json['syncStatus']), + createdAt: serializer.fromJson(json['createdAt']), + updatedAt: serializer.fromJson(json['updatedAt']), + deletedAt: serializer.fromJson(json['deletedAt']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'localId': serializer.toJson(localId), + 'serverId': serializer.toJson(serverId), + 'customerId': serializer.toJson(customerId), + 'label': serializer.toJson(label), + 'fullAddress': serializer.toJson(fullAddress), + 'latitude': serializer.toJson(latitude), + 'longitude': serializer.toJson(longitude), + 'notes': serializer.toJson(notes), + 'syncStatus': serializer.toJson(syncStatus), + 'createdAt': serializer.toJson(createdAt), + 'updatedAt': serializer.toJson(updatedAt), + 'deletedAt': serializer.toJson(deletedAt), + }; + } + + CustomerAddressesData copyWith({ + String? localId, + Value serverId = const Value.absent(), + String? customerId, + Value label = const Value.absent(), + Value fullAddress = const Value.absent(), + Value latitude = const Value.absent(), + Value longitude = const Value.absent(), + Value notes = const Value.absent(), + String? syncStatus, + DateTime? createdAt, + DateTime? updatedAt, + Value deletedAt = const Value.absent(), + }) => CustomerAddressesData( + localId: localId ?? this.localId, + serverId: serverId.present ? serverId.value : this.serverId, + customerId: customerId ?? this.customerId, + label: label.present ? label.value : this.label, + fullAddress: fullAddress.present ? fullAddress.value : this.fullAddress, + latitude: latitude.present ? latitude.value : this.latitude, + longitude: longitude.present ? longitude.value : this.longitude, + notes: notes.present ? notes.value : this.notes, + syncStatus: syncStatus ?? this.syncStatus, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + deletedAt: deletedAt.present ? deletedAt.value : this.deletedAt, + ); + CustomerAddressesData copyWithCompanion(CustomerAddressesCompanion data) { + return CustomerAddressesData( + localId: data.localId.present ? data.localId.value : this.localId, + serverId: data.serverId.present ? data.serverId.value : this.serverId, + customerId: data.customerId.present + ? data.customerId.value + : this.customerId, + label: data.label.present ? data.label.value : this.label, + fullAddress: data.fullAddress.present + ? data.fullAddress.value + : this.fullAddress, + latitude: data.latitude.present ? data.latitude.value : this.latitude, + longitude: data.longitude.present ? data.longitude.value : this.longitude, + notes: data.notes.present ? data.notes.value : this.notes, + syncStatus: data.syncStatus.present + ? data.syncStatus.value + : this.syncStatus, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, + deletedAt: data.deletedAt.present ? data.deletedAt.value : this.deletedAt, + ); + } + + @override + String toString() { + return (StringBuffer('CustomerAddressesData(') + ..write('localId: $localId, ') + ..write('serverId: $serverId, ') + ..write('customerId: $customerId, ') + ..write('label: $label, ') + ..write('fullAddress: $fullAddress, ') + ..write('latitude: $latitude, ') + ..write('longitude: $longitude, ') + ..write('notes: $notes, ') + ..write('syncStatus: $syncStatus, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('deletedAt: $deletedAt') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + localId, + serverId, + customerId, + label, + fullAddress, + latitude, + longitude, + notes, + syncStatus, + createdAt, + updatedAt, + deletedAt, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is CustomerAddressesData && + other.localId == this.localId && + other.serverId == this.serverId && + other.customerId == this.customerId && + other.label == this.label && + other.fullAddress == this.fullAddress && + other.latitude == this.latitude && + other.longitude == this.longitude && + other.notes == this.notes && + other.syncStatus == this.syncStatus && + other.createdAt == this.createdAt && + other.updatedAt == this.updatedAt && + other.deletedAt == this.deletedAt); +} + +class CustomerAddressesCompanion + extends UpdateCompanion { + final Value localId; + final Value serverId; + final Value customerId; + final Value label; + final Value fullAddress; + final Value latitude; + final Value longitude; + final Value notes; + final Value syncStatus; + final Value createdAt; + final Value updatedAt; + final Value deletedAt; + final Value rowid; + const CustomerAddressesCompanion({ + this.localId = const Value.absent(), + this.serverId = const Value.absent(), + this.customerId = const Value.absent(), + this.label = const Value.absent(), + this.fullAddress = const Value.absent(), + this.latitude = const Value.absent(), + this.longitude = const Value.absent(), + this.notes = const Value.absent(), + this.syncStatus = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.deletedAt = const Value.absent(), + this.rowid = const Value.absent(), + }); + CustomerAddressesCompanion.insert({ + this.localId = const Value.absent(), + this.serverId = const Value.absent(), + required String customerId, + this.label = const Value.absent(), + this.fullAddress = const Value.absent(), + this.latitude = const Value.absent(), + this.longitude = const Value.absent(), + this.notes = const Value.absent(), + this.syncStatus = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.deletedAt = const Value.absent(), + this.rowid = const Value.absent(), + }) : customerId = Value(customerId); + static Insertable custom({ + Expression? localId, + Expression? serverId, + Expression? customerId, + Expression? label, + Expression? fullAddress, + Expression? latitude, + Expression? longitude, + Expression? notes, + Expression? syncStatus, + Expression? createdAt, + Expression? updatedAt, + Expression? deletedAt, + Expression? rowid, + }) { + return RawValuesInsertable({ + if (localId != null) 'local_id': localId, + if (serverId != null) 'server_id': serverId, + if (customerId != null) 'customer_id': customerId, + if (label != null) 'label': label, + if (fullAddress != null) 'full_address': fullAddress, + if (latitude != null) 'latitude': latitude, + if (longitude != null) 'longitude': longitude, + if (notes != null) 'notes': notes, + if (syncStatus != null) 'sync_status': syncStatus, + if (createdAt != null) 'created_at': createdAt, + if (updatedAt != null) 'updated_at': updatedAt, + if (deletedAt != null) 'deleted_at': deletedAt, + if (rowid != null) 'rowid': rowid, + }); + } + + CustomerAddressesCompanion copyWith({ + Value? localId, + Value? serverId, + Value? customerId, + Value? label, + Value? fullAddress, + Value? latitude, + Value? longitude, + Value? notes, + Value? syncStatus, + Value? createdAt, + Value? updatedAt, + Value? deletedAt, + Value? rowid, + }) { + return CustomerAddressesCompanion( + localId: localId ?? this.localId, + serverId: serverId ?? this.serverId, + customerId: customerId ?? this.customerId, + label: label ?? this.label, + fullAddress: fullAddress ?? this.fullAddress, + latitude: latitude ?? this.latitude, + longitude: longitude ?? this.longitude, + notes: notes ?? this.notes, + syncStatus: syncStatus ?? this.syncStatus, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + deletedAt: deletedAt ?? this.deletedAt, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (localId.present) { + map['local_id'] = Variable(localId.value); + } + if (serverId.present) { + map['server_id'] = Variable(serverId.value); + } + if (customerId.present) { + map['customer_id'] = Variable(customerId.value); + } + if (label.present) { + map['label'] = Variable(label.value); + } + if (fullAddress.present) { + map['full_address'] = Variable(fullAddress.value); + } + if (latitude.present) { + map['latitude'] = Variable(latitude.value); + } + if (longitude.present) { + map['longitude'] = Variable(longitude.value); + } + if (notes.present) { + map['notes'] = Variable(notes.value); + } + if (syncStatus.present) { + map['sync_status'] = Variable(syncStatus.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (updatedAt.present) { + map['updated_at'] = Variable(updatedAt.value); + } + if (deletedAt.present) { + map['deleted_at'] = Variable(deletedAt.value); + } + if (rowid.present) { + map['rowid'] = Variable(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('CustomerAddressesCompanion(') + ..write('localId: $localId, ') + ..write('serverId: $serverId, ') + ..write('customerId: $customerId, ') + ..write('label: $label, ') + ..write('fullAddress: $fullAddress, ') + ..write('latitude: $latitude, ') + ..write('longitude: $longitude, ') + ..write('notes: $notes, ') + ..write('syncStatus: $syncStatus, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('deletedAt: $deletedAt, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + +class $CategoriesTable extends Categories + with TableInfo<$CategoriesTable, Category> { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + $CategoriesTable(this.attachedDatabase, [this._alias]); + static const VerificationMeta _localIdMeta = const VerificationMeta( + 'localId', + ); + @override + late final GeneratedColumn localId = GeneratedColumn( + 'local_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: false, + clientDefault: () => const Uuid().v7().toString(), + ); + static const VerificationMeta _serverIdMeta = const VerificationMeta( + 'serverId', + ); + @override + late final GeneratedColumn serverId = GeneratedColumn( + 'server_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _nameMeta = const VerificationMeta('name'); + @override + late final GeneratedColumn name = GeneratedColumn( + 'name', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways('UNIQUE'), + ); + static const VerificationMeta _localImagePathMeta = const VerificationMeta( + 'localImagePath', + ); + @override + late final GeneratedColumn localImagePath = GeneratedColumn( + 'local_image_path', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _serverImageUrlMeta = const VerificationMeta( + 'serverImageUrl', + ); + @override + late final GeneratedColumn serverImageUrl = GeneratedColumn( + 'server_image_url', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _syncStatusMeta = const VerificationMeta( + 'syncStatus', + ); + @override + late final GeneratedColumn syncStatus = GeneratedColumn( + 'sync_status', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultValue: const Constant('dirty'), + ); + static const VerificationMeta _createdAtMeta = const VerificationMeta( + 'createdAt', + ); + @override + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: currentDateAndTime, + ); + static const VerificationMeta _updatedAtMeta = const VerificationMeta( + 'updatedAt', + ); + @override + late final GeneratedColumn updatedAt = GeneratedColumn( + 'updated_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: currentDateAndTime, + ); + @override + List get $columns => [ + localId, + serverId, + name, + localImagePath, + serverImageUrl, + syncStatus, + createdAt, + updatedAt, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'categories'; + @override + VerificationContext validateIntegrity( + Insertable instance, { + bool isInserting = false, + }) { + final context = VerificationContext(); + final data = instance.toColumns(true); + if (data.containsKey('local_id')) { + context.handle( + _localIdMeta, + localId.isAcceptableOrUnknown(data['local_id']!, _localIdMeta), + ); + } + if (data.containsKey('server_id')) { + context.handle( + _serverIdMeta, + serverId.isAcceptableOrUnknown(data['server_id']!, _serverIdMeta), + ); + } + if (data.containsKey('name')) { + context.handle( + _nameMeta, + name.isAcceptableOrUnknown(data['name']!, _nameMeta), + ); + } else if (isInserting) { + context.missing(_nameMeta); + } + if (data.containsKey('local_image_path')) { + context.handle( + _localImagePathMeta, + localImagePath.isAcceptableOrUnknown( + data['local_image_path']!, + _localImagePathMeta, + ), + ); + } + if (data.containsKey('server_image_url')) { + context.handle( + _serverImageUrlMeta, + serverImageUrl.isAcceptableOrUnknown( + data['server_image_url']!, + _serverImageUrlMeta, + ), + ); + } + if (data.containsKey('sync_status')) { + context.handle( + _syncStatusMeta, + syncStatus.isAcceptableOrUnknown(data['sync_status']!, _syncStatusMeta), + ); + } + if (data.containsKey('created_at')) { + context.handle( + _createdAtMeta, + createdAt.isAcceptableOrUnknown(data['created_at']!, _createdAtMeta), + ); + } + if (data.containsKey('updated_at')) { + context.handle( + _updatedAtMeta, + updatedAt.isAcceptableOrUnknown(data['updated_at']!, _updatedAtMeta), + ); + } + return context; + } + + @override + Set get $primaryKey => {localId}; + @override + Category map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return Category( + localId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}local_id'], + )!, + serverId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}server_id'], + ), + name: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}name'], + )!, + localImagePath: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}local_image_path'], + ), + serverImageUrl: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}server_image_url'], + ), + syncStatus: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}sync_status'], + )!, + createdAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}created_at'], + )!, + updatedAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}updated_at'], + )!, + ); + } + + @override + $CategoriesTable createAlias(String alias) { + return $CategoriesTable(attachedDatabase, alias); + } +} + +class Category extends DataClass implements Insertable { + final String localId; + final String? serverId; + final String name; + final String? localImagePath; + final String? serverImageUrl; + final String syncStatus; + final DateTime createdAt; + final DateTime updatedAt; + const Category({ + required this.localId, + this.serverId, + required this.name, + this.localImagePath, + this.serverImageUrl, + required this.syncStatus, + required this.createdAt, + required this.updatedAt, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['local_id'] = Variable(localId); + if (!nullToAbsent || serverId != null) { + map['server_id'] = Variable(serverId); + } + map['name'] = Variable(name); + if (!nullToAbsent || localImagePath != null) { + map['local_image_path'] = Variable(localImagePath); + } + if (!nullToAbsent || serverImageUrl != null) { + map['server_image_url'] = Variable(serverImageUrl); + } + map['sync_status'] = Variable(syncStatus); + map['created_at'] = Variable(createdAt); + map['updated_at'] = Variable(updatedAt); + return map; + } + + CategoriesCompanion toCompanion(bool nullToAbsent) { + return CategoriesCompanion( + localId: Value(localId), + serverId: serverId == null && nullToAbsent + ? const Value.absent() + : Value(serverId), + name: Value(name), + localImagePath: localImagePath == null && nullToAbsent + ? const Value.absent() + : Value(localImagePath), + serverImageUrl: serverImageUrl == null && nullToAbsent + ? const Value.absent() + : Value(serverImageUrl), + syncStatus: Value(syncStatus), + createdAt: Value(createdAt), + updatedAt: Value(updatedAt), + ); + } + + factory Category.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return Category( + localId: serializer.fromJson(json['localId']), + serverId: serializer.fromJson(json['serverId']), + name: serializer.fromJson(json['name']), + localImagePath: serializer.fromJson(json['localImagePath']), + serverImageUrl: serializer.fromJson(json['serverImageUrl']), + syncStatus: serializer.fromJson(json['syncStatus']), + createdAt: serializer.fromJson(json['createdAt']), + updatedAt: serializer.fromJson(json['updatedAt']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'localId': serializer.toJson(localId), + 'serverId': serializer.toJson(serverId), + 'name': serializer.toJson(name), + 'localImagePath': serializer.toJson(localImagePath), + 'serverImageUrl': serializer.toJson(serverImageUrl), + 'syncStatus': serializer.toJson(syncStatus), + 'createdAt': serializer.toJson(createdAt), + 'updatedAt': serializer.toJson(updatedAt), + }; + } + + Category copyWith({ + String? localId, + Value serverId = const Value.absent(), + String? name, + Value localImagePath = const Value.absent(), + Value serverImageUrl = const Value.absent(), + String? syncStatus, + DateTime? createdAt, + DateTime? updatedAt, + }) => Category( + localId: localId ?? this.localId, + serverId: serverId.present ? serverId.value : this.serverId, + name: name ?? this.name, + localImagePath: localImagePath.present + ? localImagePath.value + : this.localImagePath, + serverImageUrl: serverImageUrl.present + ? serverImageUrl.value + : this.serverImageUrl, + syncStatus: syncStatus ?? this.syncStatus, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + ); + Category copyWithCompanion(CategoriesCompanion data) { + return Category( + localId: data.localId.present ? data.localId.value : this.localId, + serverId: data.serverId.present ? data.serverId.value : this.serverId, + name: data.name.present ? data.name.value : this.name, + localImagePath: data.localImagePath.present + ? data.localImagePath.value + : this.localImagePath, + serverImageUrl: data.serverImageUrl.present + ? data.serverImageUrl.value + : this.serverImageUrl, + syncStatus: data.syncStatus.present + ? data.syncStatus.value + : this.syncStatus, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, + ); + } + + @override + String toString() { + return (StringBuffer('Category(') + ..write('localId: $localId, ') + ..write('serverId: $serverId, ') + ..write('name: $name, ') + ..write('localImagePath: $localImagePath, ') + ..write('serverImageUrl: $serverImageUrl, ') + ..write('syncStatus: $syncStatus, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + localId, + serverId, + name, + localImagePath, + serverImageUrl, + syncStatus, + createdAt, + updatedAt, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is Category && + other.localId == this.localId && + other.serverId == this.serverId && + other.name == this.name && + other.localImagePath == this.localImagePath && + other.serverImageUrl == this.serverImageUrl && + other.syncStatus == this.syncStatus && + other.createdAt == this.createdAt && + other.updatedAt == this.updatedAt); +} + +class CategoriesCompanion extends UpdateCompanion { + final Value localId; + final Value serverId; + final Value name; + final Value localImagePath; + final Value serverImageUrl; + final Value syncStatus; + final Value createdAt; + final Value updatedAt; + final Value rowid; + const CategoriesCompanion({ + this.localId = const Value.absent(), + this.serverId = const Value.absent(), + this.name = const Value.absent(), + this.localImagePath = const Value.absent(), + this.serverImageUrl = const Value.absent(), + this.syncStatus = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.rowid = const Value.absent(), + }); + CategoriesCompanion.insert({ + this.localId = const Value.absent(), + this.serverId = const Value.absent(), + required String name, + this.localImagePath = const Value.absent(), + this.serverImageUrl = const Value.absent(), + this.syncStatus = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.rowid = const Value.absent(), + }) : name = Value(name); + static Insertable custom({ + Expression? localId, + Expression? serverId, + Expression? name, + Expression? localImagePath, + Expression? serverImageUrl, + Expression? syncStatus, + Expression? createdAt, + Expression? updatedAt, + Expression? rowid, + }) { + return RawValuesInsertable({ + if (localId != null) 'local_id': localId, + if (serverId != null) 'server_id': serverId, + if (name != null) 'name': name, + if (localImagePath != null) 'local_image_path': localImagePath, + if (serverImageUrl != null) 'server_image_url': serverImageUrl, + if (syncStatus != null) 'sync_status': syncStatus, + if (createdAt != null) 'created_at': createdAt, + if (updatedAt != null) 'updated_at': updatedAt, + if (rowid != null) 'rowid': rowid, + }); + } + + CategoriesCompanion copyWith({ + Value? localId, + Value? serverId, + Value? name, + Value? localImagePath, + Value? serverImageUrl, + Value? syncStatus, + Value? createdAt, + Value? updatedAt, + Value? rowid, + }) { + return CategoriesCompanion( + localId: localId ?? this.localId, + serverId: serverId ?? this.serverId, + name: name ?? this.name, + localImagePath: localImagePath ?? this.localImagePath, + serverImageUrl: serverImageUrl ?? this.serverImageUrl, + syncStatus: syncStatus ?? this.syncStatus, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (localId.present) { + map['local_id'] = Variable(localId.value); + } + if (serverId.present) { + map['server_id'] = Variable(serverId.value); + } + if (name.present) { + map['name'] = Variable(name.value); + } + if (localImagePath.present) { + map['local_image_path'] = Variable(localImagePath.value); + } + if (serverImageUrl.present) { + map['server_image_url'] = Variable(serverImageUrl.value); + } + if (syncStatus.present) { + map['sync_status'] = Variable(syncStatus.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (updatedAt.present) { + map['updated_at'] = Variable(updatedAt.value); + } + if (rowid.present) { + map['rowid'] = Variable(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('CategoriesCompanion(') + ..write('localId: $localId, ') + ..write('serverId: $serverId, ') + ..write('name: $name, ') + ..write('localImagePath: $localImagePath, ') + ..write('serverImageUrl: $serverImageUrl, ') + ..write('syncStatus: $syncStatus, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + +class $UnitsTable extends Units with TableInfo<$UnitsTable, Unit> { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + $UnitsTable(this.attachedDatabase, [this._alias]); + static const VerificationMeta _localIdMeta = const VerificationMeta( + 'localId', + ); + @override + late final GeneratedColumn localId = GeneratedColumn( + 'local_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: false, + clientDefault: () => const Uuid().v7().toString(), + ); + static const VerificationMeta _serverIdMeta = const VerificationMeta( + 'serverId', + ); + @override + late final GeneratedColumn serverId = GeneratedColumn( + 'server_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _nameMeta = const VerificationMeta('name'); + @override + late final GeneratedColumn name = GeneratedColumn( + 'name', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways('UNIQUE'), + ); + static const VerificationMeta _syncStatusMeta = const VerificationMeta( + 'syncStatus', + ); + @override + late final GeneratedColumn syncStatus = GeneratedColumn( + 'sync_status', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultValue: const Constant('dirty'), + ); + static const VerificationMeta _createdAtMeta = const VerificationMeta( + 'createdAt', + ); + @override + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: currentDateAndTime, + ); + static const VerificationMeta _updatedAtMeta = const VerificationMeta( + 'updatedAt', + ); + @override + late final GeneratedColumn updatedAt = GeneratedColumn( + 'updated_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: currentDateAndTime, + ); + @override + List get $columns => [ + localId, + serverId, + name, + syncStatus, + createdAt, + updatedAt, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'units'; + @override + VerificationContext validateIntegrity( + Insertable instance, { + bool isInserting = false, + }) { + final context = VerificationContext(); + final data = instance.toColumns(true); + if (data.containsKey('local_id')) { + context.handle( + _localIdMeta, + localId.isAcceptableOrUnknown(data['local_id']!, _localIdMeta), + ); + } + if (data.containsKey('server_id')) { + context.handle( + _serverIdMeta, + serverId.isAcceptableOrUnknown(data['server_id']!, _serverIdMeta), + ); + } + if (data.containsKey('name')) { + context.handle( + _nameMeta, + name.isAcceptableOrUnknown(data['name']!, _nameMeta), + ); + } else if (isInserting) { + context.missing(_nameMeta); + } + if (data.containsKey('sync_status')) { + context.handle( + _syncStatusMeta, + syncStatus.isAcceptableOrUnknown(data['sync_status']!, _syncStatusMeta), + ); + } + if (data.containsKey('created_at')) { + context.handle( + _createdAtMeta, + createdAt.isAcceptableOrUnknown(data['created_at']!, _createdAtMeta), + ); + } + if (data.containsKey('updated_at')) { + context.handle( + _updatedAtMeta, + updatedAt.isAcceptableOrUnknown(data['updated_at']!, _updatedAtMeta), + ); + } + return context; + } + + @override + Set get $primaryKey => {localId}; + @override + Unit map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return Unit( + localId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}local_id'], + )!, + serverId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}server_id'], + ), + name: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}name'], + )!, + syncStatus: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}sync_status'], + )!, + createdAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}created_at'], + )!, + updatedAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}updated_at'], + )!, + ); + } + + @override + $UnitsTable createAlias(String alias) { + return $UnitsTable(attachedDatabase, alias); + } +} + +class Unit extends DataClass implements Insertable { + final String localId; + final String? serverId; + final String name; + final String syncStatus; + final DateTime createdAt; + final DateTime updatedAt; + const Unit({ + required this.localId, + this.serverId, + required this.name, + required this.syncStatus, + required this.createdAt, + required this.updatedAt, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['local_id'] = Variable(localId); + if (!nullToAbsent || serverId != null) { + map['server_id'] = Variable(serverId); + } + map['name'] = Variable(name); + map['sync_status'] = Variable(syncStatus); + map['created_at'] = Variable(createdAt); + map['updated_at'] = Variable(updatedAt); + return map; + } + + UnitsCompanion toCompanion(bool nullToAbsent) { + return UnitsCompanion( + localId: Value(localId), + serverId: serverId == null && nullToAbsent + ? const Value.absent() + : Value(serverId), + name: Value(name), + syncStatus: Value(syncStatus), + createdAt: Value(createdAt), + updatedAt: Value(updatedAt), + ); + } + + factory Unit.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return Unit( + localId: serializer.fromJson(json['localId']), + serverId: serializer.fromJson(json['serverId']), + name: serializer.fromJson(json['name']), + syncStatus: serializer.fromJson(json['syncStatus']), + createdAt: serializer.fromJson(json['createdAt']), + updatedAt: serializer.fromJson(json['updatedAt']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'localId': serializer.toJson(localId), + 'serverId': serializer.toJson(serverId), + 'name': serializer.toJson(name), + 'syncStatus': serializer.toJson(syncStatus), + 'createdAt': serializer.toJson(createdAt), + 'updatedAt': serializer.toJson(updatedAt), + }; + } + + Unit copyWith({ + String? localId, + Value serverId = const Value.absent(), + String? name, + String? syncStatus, + DateTime? createdAt, + DateTime? updatedAt, + }) => Unit( + localId: localId ?? this.localId, + serverId: serverId.present ? serverId.value : this.serverId, + name: name ?? this.name, + syncStatus: syncStatus ?? this.syncStatus, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + ); + Unit copyWithCompanion(UnitsCompanion data) { + return Unit( + localId: data.localId.present ? data.localId.value : this.localId, + serverId: data.serverId.present ? data.serverId.value : this.serverId, + name: data.name.present ? data.name.value : this.name, + syncStatus: data.syncStatus.present + ? data.syncStatus.value + : this.syncStatus, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, + ); + } + + @override + String toString() { + return (StringBuffer('Unit(') + ..write('localId: $localId, ') + ..write('serverId: $serverId, ') + ..write('name: $name, ') + ..write('syncStatus: $syncStatus, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt') + ..write(')')) + .toString(); + } + + @override + int get hashCode => + Object.hash(localId, serverId, name, syncStatus, createdAt, updatedAt); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is Unit && + other.localId == this.localId && + other.serverId == this.serverId && + other.name == this.name && + other.syncStatus == this.syncStatus && + other.createdAt == this.createdAt && + other.updatedAt == this.updatedAt); +} + +class UnitsCompanion extends UpdateCompanion { + final Value localId; + final Value serverId; + final Value name; + final Value syncStatus; + final Value createdAt; + final Value updatedAt; + final Value rowid; + const UnitsCompanion({ + this.localId = const Value.absent(), + this.serverId = const Value.absent(), + this.name = const Value.absent(), + this.syncStatus = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.rowid = const Value.absent(), + }); + UnitsCompanion.insert({ + this.localId = const Value.absent(), + this.serverId = const Value.absent(), + required String name, + this.syncStatus = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.rowid = const Value.absent(), + }) : name = Value(name); + static Insertable custom({ + Expression? localId, + Expression? serverId, + Expression? name, + Expression? syncStatus, + Expression? createdAt, + Expression? updatedAt, + Expression? rowid, + }) { + return RawValuesInsertable({ + if (localId != null) 'local_id': localId, + if (serverId != null) 'server_id': serverId, + if (name != null) 'name': name, + if (syncStatus != null) 'sync_status': syncStatus, + if (createdAt != null) 'created_at': createdAt, + if (updatedAt != null) 'updated_at': updatedAt, + if (rowid != null) 'rowid': rowid, + }); + } + + UnitsCompanion copyWith({ + Value? localId, + Value? serverId, + Value? name, + Value? syncStatus, + Value? createdAt, + Value? updatedAt, + Value? rowid, + }) { + return UnitsCompanion( + localId: localId ?? this.localId, + serverId: serverId ?? this.serverId, + name: name ?? this.name, + syncStatus: syncStatus ?? this.syncStatus, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (localId.present) { + map['local_id'] = Variable(localId.value); + } + if (serverId.present) { + map['server_id'] = Variable(serverId.value); + } + if (name.present) { + map['name'] = Variable(name.value); + } + if (syncStatus.present) { + map['sync_status'] = Variable(syncStatus.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (updatedAt.present) { + map['updated_at'] = Variable(updatedAt.value); + } + if (rowid.present) { + map['rowid'] = Variable(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('UnitsCompanion(') + ..write('localId: $localId, ') + ..write('serverId: $serverId, ') + ..write('name: $name, ') + ..write('syncStatus: $syncStatus, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + +class $ProductsTable extends Products with TableInfo<$ProductsTable, Product> { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + $ProductsTable(this.attachedDatabase, [this._alias]); + static const VerificationMeta _localIdMeta = const VerificationMeta( + 'localId', + ); + @override + late final GeneratedColumn localId = GeneratedColumn( + 'local_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: false, + clientDefault: () => const Uuid().v7().toString(), + ); + static const VerificationMeta _serverIdMeta = const VerificationMeta( + 'serverId', + ); + @override + late final GeneratedColumn serverId = GeneratedColumn( + 'server_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _categoryIdMeta = const VerificationMeta( + 'categoryId', + ); + @override + late final GeneratedColumn categoryId = GeneratedColumn( + 'category_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES categories (local_id)', + ), + ); + static const VerificationMeta _unitIdMeta = const VerificationMeta('unitId'); + @override + late final GeneratedColumn unitId = GeneratedColumn( + 'unit_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES units (local_id)', + ), + ); + static const VerificationMeta _nameMeta = const VerificationMeta('name'); + @override + late final GeneratedColumn name = GeneratedColumn( + 'name', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + @override + late final GeneratedColumnWithTypeConverter?, String> + localImagePath = GeneratedColumn( + 'local_image_path', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ).withConverter?>($ProductsTable.$converterlocalImagePathn); + @override + late final GeneratedColumnWithTypeConverter?, String> + serverImageUrl = GeneratedColumn( + 'server_image_url', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ).withConverter?>($ProductsTable.$converterserverImageUrln); + static const VerificationMeta _hasVariantMeta = const VerificationMeta( + 'hasVariant', + ); + @override + late final GeneratedColumn hasVariant = GeneratedColumn( + 'has_variant', + aliasedName, + false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("has_variant" IN (0, 1))', + ), + defaultValue: const Constant(false), + ); + static const VerificationMeta _syncStatusMeta = const VerificationMeta( + 'syncStatus', + ); + @override + late final GeneratedColumn syncStatus = GeneratedColumn( + 'sync_status', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultValue: const Constant('dirty'), + ); + static const VerificationMeta _createdAtMeta = const VerificationMeta( + 'createdAt', + ); + @override + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: currentDateAndTime, + ); + static const VerificationMeta _updatedAtMeta = const VerificationMeta( + 'updatedAt', + ); + @override + late final GeneratedColumn updatedAt = GeneratedColumn( + 'updated_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: currentDateAndTime, + ); + static const VerificationMeta _deletedAtMeta = const VerificationMeta( + 'deletedAt', + ); + @override + late final GeneratedColumn deletedAt = GeneratedColumn( + 'deleted_at', + aliasedName, + true, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + ); + @override + List get $columns => [ + localId, + serverId, + categoryId, + unitId, + name, + localImagePath, + serverImageUrl, + hasVariant, + syncStatus, + createdAt, + updatedAt, + deletedAt, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'products'; + @override + VerificationContext validateIntegrity( + Insertable instance, { + bool isInserting = false, + }) { + final context = VerificationContext(); + final data = instance.toColumns(true); + if (data.containsKey('local_id')) { + context.handle( + _localIdMeta, + localId.isAcceptableOrUnknown(data['local_id']!, _localIdMeta), + ); + } + if (data.containsKey('server_id')) { + context.handle( + _serverIdMeta, + serverId.isAcceptableOrUnknown(data['server_id']!, _serverIdMeta), + ); + } + if (data.containsKey('category_id')) { + context.handle( + _categoryIdMeta, + categoryId.isAcceptableOrUnknown(data['category_id']!, _categoryIdMeta), + ); + } else if (isInserting) { + context.missing(_categoryIdMeta); + } + if (data.containsKey('unit_id')) { + context.handle( + _unitIdMeta, + unitId.isAcceptableOrUnknown(data['unit_id']!, _unitIdMeta), + ); + } else if (isInserting) { + context.missing(_unitIdMeta); + } + if (data.containsKey('name')) { + context.handle( + _nameMeta, + name.isAcceptableOrUnknown(data['name']!, _nameMeta), + ); + } else if (isInserting) { + context.missing(_nameMeta); + } + if (data.containsKey('has_variant')) { + context.handle( + _hasVariantMeta, + hasVariant.isAcceptableOrUnknown(data['has_variant']!, _hasVariantMeta), + ); + } + if (data.containsKey('sync_status')) { + context.handle( + _syncStatusMeta, + syncStatus.isAcceptableOrUnknown(data['sync_status']!, _syncStatusMeta), + ); + } + if (data.containsKey('created_at')) { + context.handle( + _createdAtMeta, + createdAt.isAcceptableOrUnknown(data['created_at']!, _createdAtMeta), + ); + } + if (data.containsKey('updated_at')) { + context.handle( + _updatedAtMeta, + updatedAt.isAcceptableOrUnknown(data['updated_at']!, _updatedAtMeta), + ); + } + if (data.containsKey('deleted_at')) { + context.handle( + _deletedAtMeta, + deletedAt.isAcceptableOrUnknown(data['deleted_at']!, _deletedAtMeta), + ); + } + return context; + } + + @override + Set get $primaryKey => {localId}; + @override + Product map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return Product( + localId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}local_id'], + )!, + serverId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}server_id'], + ), + categoryId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}category_id'], + )!, + unitId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}unit_id'], + )!, + name: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}name'], + )!, + localImagePath: $ProductsTable.$converterlocalImagePathn.fromSql( + attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}local_image_path'], + ), + ), + serverImageUrl: $ProductsTable.$converterserverImageUrln.fromSql( + attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}server_image_url'], + ), + ), + hasVariant: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}has_variant'], + )!, + syncStatus: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}sync_status'], + )!, + createdAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}created_at'], + )!, + updatedAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}updated_at'], + )!, + deletedAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}deleted_at'], + ), + ); + } + + @override + $ProductsTable createAlias(String alias) { + return $ProductsTable(attachedDatabase, alias); + } + + static TypeConverter, String> $converterlocalImagePath = + StringListConverter(); + static TypeConverter?, String?> $converterlocalImagePathn = + NullAwareTypeConverter.wrap($converterlocalImagePath); + static TypeConverter, String> $converterserverImageUrl = + StringListConverter(); + static TypeConverter?, String?> $converterserverImageUrln = + NullAwareTypeConverter.wrap($converterserverImageUrl); +} + +class Product extends DataClass implements Insertable { + final String localId; + final String? serverId; + final String categoryId; + final String unitId; + final String name; + final List? localImagePath; + final List? serverImageUrl; + final bool hasVariant; + final String syncStatus; + final DateTime createdAt; + final DateTime updatedAt; + final DateTime? deletedAt; + const Product({ + required this.localId, + this.serverId, + required this.categoryId, + required this.unitId, + required this.name, + this.localImagePath, + this.serverImageUrl, + required this.hasVariant, + required this.syncStatus, + required this.createdAt, + required this.updatedAt, + this.deletedAt, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['local_id'] = Variable(localId); + if (!nullToAbsent || serverId != null) { + map['server_id'] = Variable(serverId); + } + map['category_id'] = Variable(categoryId); + map['unit_id'] = Variable(unitId); + map['name'] = Variable(name); + if (!nullToAbsent || localImagePath != null) { + map['local_image_path'] = Variable( + $ProductsTable.$converterlocalImagePathn.toSql(localImagePath), + ); + } + if (!nullToAbsent || serverImageUrl != null) { + map['server_image_url'] = Variable( + $ProductsTable.$converterserverImageUrln.toSql(serverImageUrl), + ); + } + map['has_variant'] = Variable(hasVariant); + map['sync_status'] = Variable(syncStatus); + map['created_at'] = Variable(createdAt); + map['updated_at'] = Variable(updatedAt); + if (!nullToAbsent || deletedAt != null) { + map['deleted_at'] = Variable(deletedAt); + } + return map; + } + + ProductsCompanion toCompanion(bool nullToAbsent) { + return ProductsCompanion( + localId: Value(localId), + serverId: serverId == null && nullToAbsent + ? const Value.absent() + : Value(serverId), + categoryId: Value(categoryId), + unitId: Value(unitId), + name: Value(name), + localImagePath: localImagePath == null && nullToAbsent + ? const Value.absent() + : Value(localImagePath), + serverImageUrl: serverImageUrl == null && nullToAbsent + ? const Value.absent() + : Value(serverImageUrl), + hasVariant: Value(hasVariant), + syncStatus: Value(syncStatus), + createdAt: Value(createdAt), + updatedAt: Value(updatedAt), + deletedAt: deletedAt == null && nullToAbsent + ? const Value.absent() + : Value(deletedAt), + ); + } + + factory Product.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return Product( + localId: serializer.fromJson(json['localId']), + serverId: serializer.fromJson(json['serverId']), + categoryId: serializer.fromJson(json['categoryId']), + unitId: serializer.fromJson(json['unitId']), + name: serializer.fromJson(json['name']), + localImagePath: serializer.fromJson?>( + json['localImagePath'], + ), + serverImageUrl: serializer.fromJson?>( + json['serverImageUrl'], + ), + hasVariant: serializer.fromJson(json['hasVariant']), + syncStatus: serializer.fromJson(json['syncStatus']), + createdAt: serializer.fromJson(json['createdAt']), + updatedAt: serializer.fromJson(json['updatedAt']), + deletedAt: serializer.fromJson(json['deletedAt']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'localId': serializer.toJson(localId), + 'serverId': serializer.toJson(serverId), + 'categoryId': serializer.toJson(categoryId), + 'unitId': serializer.toJson(unitId), + 'name': serializer.toJson(name), + 'localImagePath': serializer.toJson?>(localImagePath), + 'serverImageUrl': serializer.toJson?>(serverImageUrl), + 'hasVariant': serializer.toJson(hasVariant), + 'syncStatus': serializer.toJson(syncStatus), + 'createdAt': serializer.toJson(createdAt), + 'updatedAt': serializer.toJson(updatedAt), + 'deletedAt': serializer.toJson(deletedAt), + }; + } + + Product copyWith({ + String? localId, + Value serverId = const Value.absent(), + String? categoryId, + String? unitId, + String? name, + Value?> localImagePath = const Value.absent(), + Value?> serverImageUrl = const Value.absent(), + bool? hasVariant, + String? syncStatus, + DateTime? createdAt, + DateTime? updatedAt, + Value deletedAt = const Value.absent(), + }) => Product( + localId: localId ?? this.localId, + serverId: serverId.present ? serverId.value : this.serverId, + categoryId: categoryId ?? this.categoryId, + unitId: unitId ?? this.unitId, + name: name ?? this.name, + localImagePath: localImagePath.present + ? localImagePath.value + : this.localImagePath, + serverImageUrl: serverImageUrl.present + ? serverImageUrl.value + : this.serverImageUrl, + hasVariant: hasVariant ?? this.hasVariant, + syncStatus: syncStatus ?? this.syncStatus, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + deletedAt: deletedAt.present ? deletedAt.value : this.deletedAt, + ); + Product copyWithCompanion(ProductsCompanion data) { + return Product( + localId: data.localId.present ? data.localId.value : this.localId, + serverId: data.serverId.present ? data.serverId.value : this.serverId, + categoryId: data.categoryId.present + ? data.categoryId.value + : this.categoryId, + unitId: data.unitId.present ? data.unitId.value : this.unitId, + name: data.name.present ? data.name.value : this.name, + localImagePath: data.localImagePath.present + ? data.localImagePath.value + : this.localImagePath, + serverImageUrl: data.serverImageUrl.present + ? data.serverImageUrl.value + : this.serverImageUrl, + hasVariant: data.hasVariant.present + ? data.hasVariant.value + : this.hasVariant, + syncStatus: data.syncStatus.present + ? data.syncStatus.value + : this.syncStatus, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, + deletedAt: data.deletedAt.present ? data.deletedAt.value : this.deletedAt, + ); + } + + @override + String toString() { + return (StringBuffer('Product(') + ..write('localId: $localId, ') + ..write('serverId: $serverId, ') + ..write('categoryId: $categoryId, ') + ..write('unitId: $unitId, ') + ..write('name: $name, ') + ..write('localImagePath: $localImagePath, ') + ..write('serverImageUrl: $serverImageUrl, ') + ..write('hasVariant: $hasVariant, ') + ..write('syncStatus: $syncStatus, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('deletedAt: $deletedAt') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + localId, + serverId, + categoryId, + unitId, + name, + localImagePath, + serverImageUrl, + hasVariant, + syncStatus, + createdAt, + updatedAt, + deletedAt, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is Product && + other.localId == this.localId && + other.serverId == this.serverId && + other.categoryId == this.categoryId && + other.unitId == this.unitId && + other.name == this.name && + other.localImagePath == this.localImagePath && + other.serverImageUrl == this.serverImageUrl && + other.hasVariant == this.hasVariant && + other.syncStatus == this.syncStatus && + other.createdAt == this.createdAt && + other.updatedAt == this.updatedAt && + other.deletedAt == this.deletedAt); +} + +class ProductsCompanion extends UpdateCompanion { + final Value localId; + final Value serverId; + final Value categoryId; + final Value unitId; + final Value name; + final Value?> localImagePath; + final Value?> serverImageUrl; + final Value hasVariant; + final Value syncStatus; + final Value createdAt; + final Value updatedAt; + final Value deletedAt; + final Value rowid; + const ProductsCompanion({ + this.localId = const Value.absent(), + this.serverId = const Value.absent(), + this.categoryId = const Value.absent(), + this.unitId = const Value.absent(), + this.name = const Value.absent(), + this.localImagePath = const Value.absent(), + this.serverImageUrl = const Value.absent(), + this.hasVariant = const Value.absent(), + this.syncStatus = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.deletedAt = const Value.absent(), + this.rowid = const Value.absent(), + }); + ProductsCompanion.insert({ + this.localId = const Value.absent(), + this.serverId = const Value.absent(), + required String categoryId, + required String unitId, + required String name, + this.localImagePath = const Value.absent(), + this.serverImageUrl = const Value.absent(), + this.hasVariant = const Value.absent(), + this.syncStatus = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.deletedAt = const Value.absent(), + this.rowid = const Value.absent(), + }) : categoryId = Value(categoryId), + unitId = Value(unitId), + name = Value(name); + static Insertable custom({ + Expression? localId, + Expression? serverId, + Expression? categoryId, + Expression? unitId, + Expression? name, + Expression? localImagePath, + Expression? serverImageUrl, + Expression? hasVariant, + Expression? syncStatus, + Expression? createdAt, + Expression? updatedAt, + Expression? deletedAt, + Expression? rowid, + }) { + return RawValuesInsertable({ + if (localId != null) 'local_id': localId, + if (serverId != null) 'server_id': serverId, + if (categoryId != null) 'category_id': categoryId, + if (unitId != null) 'unit_id': unitId, + if (name != null) 'name': name, + if (localImagePath != null) 'local_image_path': localImagePath, + if (serverImageUrl != null) 'server_image_url': serverImageUrl, + if (hasVariant != null) 'has_variant': hasVariant, + if (syncStatus != null) 'sync_status': syncStatus, + if (createdAt != null) 'created_at': createdAt, + if (updatedAt != null) 'updated_at': updatedAt, + if (deletedAt != null) 'deleted_at': deletedAt, + if (rowid != null) 'rowid': rowid, + }); + } + + ProductsCompanion copyWith({ + Value? localId, + Value? serverId, + Value? categoryId, + Value? unitId, + Value? name, + Value?>? localImagePath, + Value?>? serverImageUrl, + Value? hasVariant, + Value? syncStatus, + Value? createdAt, + Value? updatedAt, + Value? deletedAt, + Value? rowid, + }) { + return ProductsCompanion( + localId: localId ?? this.localId, + serverId: serverId ?? this.serverId, + categoryId: categoryId ?? this.categoryId, + unitId: unitId ?? this.unitId, + name: name ?? this.name, + localImagePath: localImagePath ?? this.localImagePath, + serverImageUrl: serverImageUrl ?? this.serverImageUrl, + hasVariant: hasVariant ?? this.hasVariant, + syncStatus: syncStatus ?? this.syncStatus, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + deletedAt: deletedAt ?? this.deletedAt, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (localId.present) { + map['local_id'] = Variable(localId.value); + } + if (serverId.present) { + map['server_id'] = Variable(serverId.value); + } + if (categoryId.present) { + map['category_id'] = Variable(categoryId.value); + } + if (unitId.present) { + map['unit_id'] = Variable(unitId.value); + } + if (name.present) { + map['name'] = Variable(name.value); + } + if (localImagePath.present) { + map['local_image_path'] = Variable( + $ProductsTable.$converterlocalImagePathn.toSql(localImagePath.value), + ); + } + if (serverImageUrl.present) { + map['server_image_url'] = Variable( + $ProductsTable.$converterserverImageUrln.toSql(serverImageUrl.value), + ); + } + if (hasVariant.present) { + map['has_variant'] = Variable(hasVariant.value); + } + if (syncStatus.present) { + map['sync_status'] = Variable(syncStatus.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (updatedAt.present) { + map['updated_at'] = Variable(updatedAt.value); + } + if (deletedAt.present) { + map['deleted_at'] = Variable(deletedAt.value); + } + if (rowid.present) { + map['rowid'] = Variable(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('ProductsCompanion(') + ..write('localId: $localId, ') + ..write('serverId: $serverId, ') + ..write('categoryId: $categoryId, ') + ..write('unitId: $unitId, ') + ..write('name: $name, ') + ..write('localImagePath: $localImagePath, ') + ..write('serverImageUrl: $serverImageUrl, ') + ..write('hasVariant: $hasVariant, ') + ..write('syncStatus: $syncStatus, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('deletedAt: $deletedAt, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + +class $ProductVariantsTable extends ProductVariants + with TableInfo<$ProductVariantsTable, ProductVariant> { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + $ProductVariantsTable(this.attachedDatabase, [this._alias]); + static const VerificationMeta _localIdMeta = const VerificationMeta( + 'localId', + ); + @override + late final GeneratedColumn localId = GeneratedColumn( + 'local_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: false, + clientDefault: () => const Uuid().v7().toString(), + ); + static const VerificationMeta _serverIdMeta = const VerificationMeta( + 'serverId', + ); + @override + late final GeneratedColumn serverId = GeneratedColumn( + 'server_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _productIdMeta = const VerificationMeta( + 'productId', + ); + @override + late final GeneratedColumn productId = GeneratedColumn( + 'product_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES products (local_id)', + ), + ); + static const VerificationMeta _nameMeta = const VerificationMeta('name'); + @override + late final GeneratedColumn name = GeneratedColumn( + 'name', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + static const VerificationMeta _localImagePathMeta = const VerificationMeta( + 'localImagePath', + ); + @override + late final GeneratedColumn localImagePath = GeneratedColumn( + 'local_image_path', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _serverImageUrlMeta = const VerificationMeta( + 'serverImageUrl', + ); + @override + late final GeneratedColumn serverImageUrl = GeneratedColumn( + 'server_image_url', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _isVisibleOnlineMeta = const VerificationMeta( + 'isVisibleOnline', + ); + @override + late final GeneratedColumn isVisibleOnline = GeneratedColumn( + 'is_visible_online', + aliasedName, + false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("is_visible_online" IN (0, 1))', + ), + defaultValue: const Constant(false), + ); + static const VerificationMeta _totalRatingMeta = const VerificationMeta( + 'totalRating', + ); + @override + late final GeneratedColumn totalRating = GeneratedColumn( + 'total_rating', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultValue: const Constant(0), + ); + static const VerificationMeta _averageRatingMeta = const VerificationMeta( + 'averageRating', + ); + @override + late final GeneratedColumn averageRating = GeneratedColumn( + 'average_rating', + aliasedName, + false, + type: DriftSqlType.double, + requiredDuringInsert: false, + defaultValue: const Constant(0.0), + ); + static const VerificationMeta _totalSoldMeta = const VerificationMeta( + 'totalSold', + ); + @override + late final GeneratedColumn totalSold = GeneratedColumn( + 'total_sold', + aliasedName, + false, + type: DriftSqlType.double, + requiredDuringInsert: false, + defaultValue: const Constant(0.0), + ); + static const VerificationMeta _isProductVariantMeta = const VerificationMeta( + 'isProductVariant', + ); + @override + late final GeneratedColumn isProductVariant = GeneratedColumn( + 'is_product_variant', + aliasedName, + false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("is_product_variant" IN (0, 1))', + ), + defaultValue: const Constant(false), + ); + static const VerificationMeta _syncStatusMeta = const VerificationMeta( + 'syncStatus', + ); + @override + late final GeneratedColumn syncStatus = GeneratedColumn( + 'sync_status', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultValue: const Constant('dirty'), + ); + static const VerificationMeta _createdAtMeta = const VerificationMeta( + 'createdAt', + ); + @override + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: currentDateAndTime, + ); + static const VerificationMeta _updatedAtMeta = const VerificationMeta( + 'updatedAt', + ); + @override + late final GeneratedColumn updatedAt = GeneratedColumn( + 'updated_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: currentDateAndTime, + ); + static const VerificationMeta _deletedAtMeta = const VerificationMeta( + 'deletedAt', + ); + @override + late final GeneratedColumn deletedAt = GeneratedColumn( + 'deleted_at', + aliasedName, + true, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + ); + @override + List get $columns => [ + localId, + serverId, + productId, + name, + localImagePath, + serverImageUrl, + isVisibleOnline, + totalRating, + averageRating, + totalSold, + isProductVariant, + syncStatus, + createdAt, + updatedAt, + deletedAt, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'product_variants'; + @override + VerificationContext validateIntegrity( + Insertable instance, { + bool isInserting = false, + }) { + final context = VerificationContext(); + final data = instance.toColumns(true); + if (data.containsKey('local_id')) { + context.handle( + _localIdMeta, + localId.isAcceptableOrUnknown(data['local_id']!, _localIdMeta), + ); + } + if (data.containsKey('server_id')) { + context.handle( + _serverIdMeta, + serverId.isAcceptableOrUnknown(data['server_id']!, _serverIdMeta), + ); + } + if (data.containsKey('product_id')) { + context.handle( + _productIdMeta, + productId.isAcceptableOrUnknown(data['product_id']!, _productIdMeta), + ); + } else if (isInserting) { + context.missing(_productIdMeta); + } + if (data.containsKey('name')) { + context.handle( + _nameMeta, + name.isAcceptableOrUnknown(data['name']!, _nameMeta), + ); + } else if (isInserting) { + context.missing(_nameMeta); + } + if (data.containsKey('local_image_path')) { + context.handle( + _localImagePathMeta, + localImagePath.isAcceptableOrUnknown( + data['local_image_path']!, + _localImagePathMeta, + ), + ); + } + if (data.containsKey('server_image_url')) { + context.handle( + _serverImageUrlMeta, + serverImageUrl.isAcceptableOrUnknown( + data['server_image_url']!, + _serverImageUrlMeta, + ), + ); + } + if (data.containsKey('is_visible_online')) { + context.handle( + _isVisibleOnlineMeta, + isVisibleOnline.isAcceptableOrUnknown( + data['is_visible_online']!, + _isVisibleOnlineMeta, + ), + ); + } + if (data.containsKey('total_rating')) { + context.handle( + _totalRatingMeta, + totalRating.isAcceptableOrUnknown( + data['total_rating']!, + _totalRatingMeta, + ), + ); + } + if (data.containsKey('average_rating')) { + context.handle( + _averageRatingMeta, + averageRating.isAcceptableOrUnknown( + data['average_rating']!, + _averageRatingMeta, + ), + ); + } + if (data.containsKey('total_sold')) { + context.handle( + _totalSoldMeta, + totalSold.isAcceptableOrUnknown(data['total_sold']!, _totalSoldMeta), + ); + } + if (data.containsKey('is_product_variant')) { + context.handle( + _isProductVariantMeta, + isProductVariant.isAcceptableOrUnknown( + data['is_product_variant']!, + _isProductVariantMeta, + ), + ); + } + if (data.containsKey('sync_status')) { + context.handle( + _syncStatusMeta, + syncStatus.isAcceptableOrUnknown(data['sync_status']!, _syncStatusMeta), + ); + } + if (data.containsKey('created_at')) { + context.handle( + _createdAtMeta, + createdAt.isAcceptableOrUnknown(data['created_at']!, _createdAtMeta), + ); + } + if (data.containsKey('updated_at')) { + context.handle( + _updatedAtMeta, + updatedAt.isAcceptableOrUnknown(data['updated_at']!, _updatedAtMeta), + ); + } + if (data.containsKey('deleted_at')) { + context.handle( + _deletedAtMeta, + deletedAt.isAcceptableOrUnknown(data['deleted_at']!, _deletedAtMeta), + ); + } + return context; + } + + @override + Set get $primaryKey => {localId}; + @override + ProductVariant map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return ProductVariant( + localId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}local_id'], + )!, + serverId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}server_id'], + ), + productId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}product_id'], + )!, + name: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}name'], + )!, + localImagePath: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}local_image_path'], + ), + serverImageUrl: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}server_image_url'], + ), + isVisibleOnline: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}is_visible_online'], + )!, + totalRating: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}total_rating'], + )!, + averageRating: attachedDatabase.typeMapping.read( + DriftSqlType.double, + data['${effectivePrefix}average_rating'], + )!, + totalSold: attachedDatabase.typeMapping.read( + DriftSqlType.double, + data['${effectivePrefix}total_sold'], + )!, + isProductVariant: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}is_product_variant'], + )!, + syncStatus: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}sync_status'], + )!, + createdAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}created_at'], + )!, + updatedAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}updated_at'], + )!, + deletedAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}deleted_at'], + ), + ); + } + + @override + $ProductVariantsTable createAlias(String alias) { + return $ProductVariantsTable(attachedDatabase, alias); + } +} + +class ProductVariant extends DataClass implements Insertable { + final String localId; + final String? serverId; + final String productId; + final String name; + final String? localImagePath; + final String? serverImageUrl; + final bool isVisibleOnline; + final int totalRating; + final double averageRating; + final double totalSold; + final bool isProductVariant; + final String syncStatus; + final DateTime createdAt; + final DateTime updatedAt; + final DateTime? deletedAt; + const ProductVariant({ + required this.localId, + this.serverId, + required this.productId, + required this.name, + this.localImagePath, + this.serverImageUrl, + required this.isVisibleOnline, + required this.totalRating, + required this.averageRating, + required this.totalSold, + required this.isProductVariant, + required this.syncStatus, + required this.createdAt, + required this.updatedAt, + this.deletedAt, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['local_id'] = Variable(localId); + if (!nullToAbsent || serverId != null) { + map['server_id'] = Variable(serverId); + } + map['product_id'] = Variable(productId); + map['name'] = Variable(name); + if (!nullToAbsent || localImagePath != null) { + map['local_image_path'] = Variable(localImagePath); + } + if (!nullToAbsent || serverImageUrl != null) { + map['server_image_url'] = Variable(serverImageUrl); + } + map['is_visible_online'] = Variable(isVisibleOnline); + map['total_rating'] = Variable(totalRating); + map['average_rating'] = Variable(averageRating); + map['total_sold'] = Variable(totalSold); + map['is_product_variant'] = Variable(isProductVariant); + map['sync_status'] = Variable(syncStatus); + map['created_at'] = Variable(createdAt); + map['updated_at'] = Variable(updatedAt); + if (!nullToAbsent || deletedAt != null) { + map['deleted_at'] = Variable(deletedAt); + } + return map; + } + + ProductVariantsCompanion toCompanion(bool nullToAbsent) { + return ProductVariantsCompanion( + localId: Value(localId), + serverId: serverId == null && nullToAbsent + ? const Value.absent() + : Value(serverId), + productId: Value(productId), + name: Value(name), + localImagePath: localImagePath == null && nullToAbsent + ? const Value.absent() + : Value(localImagePath), + serverImageUrl: serverImageUrl == null && nullToAbsent + ? const Value.absent() + : Value(serverImageUrl), + isVisibleOnline: Value(isVisibleOnline), + totalRating: Value(totalRating), + averageRating: Value(averageRating), + totalSold: Value(totalSold), + isProductVariant: Value(isProductVariant), + syncStatus: Value(syncStatus), + createdAt: Value(createdAt), + updatedAt: Value(updatedAt), + deletedAt: deletedAt == null && nullToAbsent + ? const Value.absent() + : Value(deletedAt), + ); + } + + factory ProductVariant.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return ProductVariant( + localId: serializer.fromJson(json['localId']), + serverId: serializer.fromJson(json['serverId']), + productId: serializer.fromJson(json['productId']), + name: serializer.fromJson(json['name']), + localImagePath: serializer.fromJson(json['localImagePath']), + serverImageUrl: serializer.fromJson(json['serverImageUrl']), + isVisibleOnline: serializer.fromJson(json['isVisibleOnline']), + totalRating: serializer.fromJson(json['totalRating']), + averageRating: serializer.fromJson(json['averageRating']), + totalSold: serializer.fromJson(json['totalSold']), + isProductVariant: serializer.fromJson(json['isProductVariant']), + syncStatus: serializer.fromJson(json['syncStatus']), + createdAt: serializer.fromJson(json['createdAt']), + updatedAt: serializer.fromJson(json['updatedAt']), + deletedAt: serializer.fromJson(json['deletedAt']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'localId': serializer.toJson(localId), + 'serverId': serializer.toJson(serverId), + 'productId': serializer.toJson(productId), + 'name': serializer.toJson(name), + 'localImagePath': serializer.toJson(localImagePath), + 'serverImageUrl': serializer.toJson(serverImageUrl), + 'isVisibleOnline': serializer.toJson(isVisibleOnline), + 'totalRating': serializer.toJson(totalRating), + 'averageRating': serializer.toJson(averageRating), + 'totalSold': serializer.toJson(totalSold), + 'isProductVariant': serializer.toJson(isProductVariant), + 'syncStatus': serializer.toJson(syncStatus), + 'createdAt': serializer.toJson(createdAt), + 'updatedAt': serializer.toJson(updatedAt), + 'deletedAt': serializer.toJson(deletedAt), + }; + } + + ProductVariant copyWith({ + String? localId, + Value serverId = const Value.absent(), + String? productId, + String? name, + Value localImagePath = const Value.absent(), + Value serverImageUrl = const Value.absent(), + bool? isVisibleOnline, + int? totalRating, + double? averageRating, + double? totalSold, + bool? isProductVariant, + String? syncStatus, + DateTime? createdAt, + DateTime? updatedAt, + Value deletedAt = const Value.absent(), + }) => ProductVariant( + localId: localId ?? this.localId, + serverId: serverId.present ? serverId.value : this.serverId, + productId: productId ?? this.productId, + name: name ?? this.name, + localImagePath: localImagePath.present + ? localImagePath.value + : this.localImagePath, + serverImageUrl: serverImageUrl.present + ? serverImageUrl.value + : this.serverImageUrl, + isVisibleOnline: isVisibleOnline ?? this.isVisibleOnline, + totalRating: totalRating ?? this.totalRating, + averageRating: averageRating ?? this.averageRating, + totalSold: totalSold ?? this.totalSold, + isProductVariant: isProductVariant ?? this.isProductVariant, + syncStatus: syncStatus ?? this.syncStatus, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + deletedAt: deletedAt.present ? deletedAt.value : this.deletedAt, + ); + ProductVariant copyWithCompanion(ProductVariantsCompanion data) { + return ProductVariant( + localId: data.localId.present ? data.localId.value : this.localId, + serverId: data.serverId.present ? data.serverId.value : this.serverId, + productId: data.productId.present ? data.productId.value : this.productId, + name: data.name.present ? data.name.value : this.name, + localImagePath: data.localImagePath.present + ? data.localImagePath.value + : this.localImagePath, + serverImageUrl: data.serverImageUrl.present + ? data.serverImageUrl.value + : this.serverImageUrl, + isVisibleOnline: data.isVisibleOnline.present + ? data.isVisibleOnline.value + : this.isVisibleOnline, + totalRating: data.totalRating.present + ? data.totalRating.value + : this.totalRating, + averageRating: data.averageRating.present + ? data.averageRating.value + : this.averageRating, + totalSold: data.totalSold.present ? data.totalSold.value : this.totalSold, + isProductVariant: data.isProductVariant.present + ? data.isProductVariant.value + : this.isProductVariant, + syncStatus: data.syncStatus.present + ? data.syncStatus.value + : this.syncStatus, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, + deletedAt: data.deletedAt.present ? data.deletedAt.value : this.deletedAt, + ); + } + + @override + String toString() { + return (StringBuffer('ProductVariant(') + ..write('localId: $localId, ') + ..write('serverId: $serverId, ') + ..write('productId: $productId, ') + ..write('name: $name, ') + ..write('localImagePath: $localImagePath, ') + ..write('serverImageUrl: $serverImageUrl, ') + ..write('isVisibleOnline: $isVisibleOnline, ') + ..write('totalRating: $totalRating, ') + ..write('averageRating: $averageRating, ') + ..write('totalSold: $totalSold, ') + ..write('isProductVariant: $isProductVariant, ') + ..write('syncStatus: $syncStatus, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('deletedAt: $deletedAt') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + localId, + serverId, + productId, + name, + localImagePath, + serverImageUrl, + isVisibleOnline, + totalRating, + averageRating, + totalSold, + isProductVariant, + syncStatus, + createdAt, + updatedAt, + deletedAt, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is ProductVariant && + other.localId == this.localId && + other.serverId == this.serverId && + other.productId == this.productId && + other.name == this.name && + other.localImagePath == this.localImagePath && + other.serverImageUrl == this.serverImageUrl && + other.isVisibleOnline == this.isVisibleOnline && + other.totalRating == this.totalRating && + other.averageRating == this.averageRating && + other.totalSold == this.totalSold && + other.isProductVariant == this.isProductVariant && + other.syncStatus == this.syncStatus && + other.createdAt == this.createdAt && + other.updatedAt == this.updatedAt && + other.deletedAt == this.deletedAt); +} + +class ProductVariantsCompanion extends UpdateCompanion { + final Value localId; + final Value serverId; + final Value productId; + final Value name; + final Value localImagePath; + final Value serverImageUrl; + final Value isVisibleOnline; + final Value totalRating; + final Value averageRating; + final Value totalSold; + final Value isProductVariant; + final Value syncStatus; + final Value createdAt; + final Value updatedAt; + final Value deletedAt; + final Value rowid; + const ProductVariantsCompanion({ + this.localId = const Value.absent(), + this.serverId = const Value.absent(), + this.productId = const Value.absent(), + this.name = const Value.absent(), + this.localImagePath = const Value.absent(), + this.serverImageUrl = const Value.absent(), + this.isVisibleOnline = const Value.absent(), + this.totalRating = const Value.absent(), + this.averageRating = const Value.absent(), + this.totalSold = const Value.absent(), + this.isProductVariant = const Value.absent(), + this.syncStatus = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.deletedAt = const Value.absent(), + this.rowid = const Value.absent(), + }); + ProductVariantsCompanion.insert({ + this.localId = const Value.absent(), + this.serverId = const Value.absent(), + required String productId, + required String name, + this.localImagePath = const Value.absent(), + this.serverImageUrl = const Value.absent(), + this.isVisibleOnline = const Value.absent(), + this.totalRating = const Value.absent(), + this.averageRating = const Value.absent(), + this.totalSold = const Value.absent(), + this.isProductVariant = const Value.absent(), + this.syncStatus = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.deletedAt = const Value.absent(), + this.rowid = const Value.absent(), + }) : productId = Value(productId), + name = Value(name); + static Insertable custom({ + Expression? localId, + Expression? serverId, + Expression? productId, + Expression? name, + Expression? localImagePath, + Expression? serverImageUrl, + Expression? isVisibleOnline, + Expression? totalRating, + Expression? averageRating, + Expression? totalSold, + Expression? isProductVariant, + Expression? syncStatus, + Expression? createdAt, + Expression? updatedAt, + Expression? deletedAt, + Expression? rowid, + }) { + return RawValuesInsertable({ + if (localId != null) 'local_id': localId, + if (serverId != null) 'server_id': serverId, + if (productId != null) 'product_id': productId, + if (name != null) 'name': name, + if (localImagePath != null) 'local_image_path': localImagePath, + if (serverImageUrl != null) 'server_image_url': serverImageUrl, + if (isVisibleOnline != null) 'is_visible_online': isVisibleOnline, + if (totalRating != null) 'total_rating': totalRating, + if (averageRating != null) 'average_rating': averageRating, + if (totalSold != null) 'total_sold': totalSold, + if (isProductVariant != null) 'is_product_variant': isProductVariant, + if (syncStatus != null) 'sync_status': syncStatus, + if (createdAt != null) 'created_at': createdAt, + if (updatedAt != null) 'updated_at': updatedAt, + if (deletedAt != null) 'deleted_at': deletedAt, + if (rowid != null) 'rowid': rowid, + }); + } + + ProductVariantsCompanion copyWith({ + Value? localId, + Value? serverId, + Value? productId, + Value? name, + Value? localImagePath, + Value? serverImageUrl, + Value? isVisibleOnline, + Value? totalRating, + Value? averageRating, + Value? totalSold, + Value? isProductVariant, + Value? syncStatus, + Value? createdAt, + Value? updatedAt, + Value? deletedAt, + Value? rowid, + }) { + return ProductVariantsCompanion( + localId: localId ?? this.localId, + serverId: serverId ?? this.serverId, + productId: productId ?? this.productId, + name: name ?? this.name, + localImagePath: localImagePath ?? this.localImagePath, + serverImageUrl: serverImageUrl ?? this.serverImageUrl, + isVisibleOnline: isVisibleOnline ?? this.isVisibleOnline, + totalRating: totalRating ?? this.totalRating, + averageRating: averageRating ?? this.averageRating, + totalSold: totalSold ?? this.totalSold, + isProductVariant: isProductVariant ?? this.isProductVariant, + syncStatus: syncStatus ?? this.syncStatus, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + deletedAt: deletedAt ?? this.deletedAt, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (localId.present) { + map['local_id'] = Variable(localId.value); + } + if (serverId.present) { + map['server_id'] = Variable(serverId.value); + } + if (productId.present) { + map['product_id'] = Variable(productId.value); + } + if (name.present) { + map['name'] = Variable(name.value); + } + if (localImagePath.present) { + map['local_image_path'] = Variable(localImagePath.value); + } + if (serverImageUrl.present) { + map['server_image_url'] = Variable(serverImageUrl.value); + } + if (isVisibleOnline.present) { + map['is_visible_online'] = Variable(isVisibleOnline.value); + } + if (totalRating.present) { + map['total_rating'] = Variable(totalRating.value); + } + if (averageRating.present) { + map['average_rating'] = Variable(averageRating.value); + } + if (totalSold.present) { + map['total_sold'] = Variable(totalSold.value); + } + if (isProductVariant.present) { + map['is_product_variant'] = Variable(isProductVariant.value); + } + if (syncStatus.present) { + map['sync_status'] = Variable(syncStatus.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (updatedAt.present) { + map['updated_at'] = Variable(updatedAt.value); + } + if (deletedAt.present) { + map['deleted_at'] = Variable(deletedAt.value); + } + if (rowid.present) { + map['rowid'] = Variable(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('ProductVariantsCompanion(') + ..write('localId: $localId, ') + ..write('serverId: $serverId, ') + ..write('productId: $productId, ') + ..write('name: $name, ') + ..write('localImagePath: $localImagePath, ') + ..write('serverImageUrl: $serverImageUrl, ') + ..write('isVisibleOnline: $isVisibleOnline, ') + ..write('totalRating: $totalRating, ') + ..write('averageRating: $averageRating, ') + ..write('totalSold: $totalSold, ') + ..write('isProductVariant: $isProductVariant, ') + ..write('syncStatus: $syncStatus, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('deletedAt: $deletedAt, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + +class $ProductReviewsTable extends ProductReviews + with TableInfo<$ProductReviewsTable, ProductReview> { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + $ProductReviewsTable(this.attachedDatabase, [this._alias]); + static const VerificationMeta _localIdMeta = const VerificationMeta( + 'localId', + ); + @override + late final GeneratedColumn localId = GeneratedColumn( + 'local_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: false, + clientDefault: () => const Uuid().v7().toString(), + ); + static const VerificationMeta _serverIdMeta = const VerificationMeta( + 'serverId', + ); + @override + late final GeneratedColumn serverId = GeneratedColumn( + 'server_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _customerIdMeta = const VerificationMeta( + 'customerId', + ); + @override + late final GeneratedColumn customerId = GeneratedColumn( + 'customer_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES customers (local_id)', + ), + ); + static const VerificationMeta _productVariantIdMeta = const VerificationMeta( + 'productVariantId', + ); + @override + late final GeneratedColumn productVariantId = GeneratedColumn( + 'product_variant_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES product_variants (local_id)', + ), + ); + static const VerificationMeta _customerNameSnapshotMeta = + const VerificationMeta('customerNameSnapshot'); + @override + late final GeneratedColumn customerNameSnapshot = + GeneratedColumn( + 'customer_name_snapshot', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _ratingMeta = const VerificationMeta('rating'); + @override + late final GeneratedColumn rating = GeneratedColumn( + 'rating', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultValue: const Constant(0), + ); + static const VerificationMeta _commentMeta = const VerificationMeta( + 'comment', + ); + @override + late final GeneratedColumn comment = GeneratedColumn( + 'comment', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _syncStatusMeta = const VerificationMeta( + 'syncStatus', + ); + @override + late final GeneratedColumn syncStatus = GeneratedColumn( + 'sync_status', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultValue: const Constant('dirty'), + ); + static const VerificationMeta _createdAtMeta = const VerificationMeta( + 'createdAt', + ); + @override + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: currentDateAndTime, + ); + static const VerificationMeta _updatedAtMeta = const VerificationMeta( + 'updatedAt', + ); + @override + late final GeneratedColumn updatedAt = GeneratedColumn( + 'updated_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: currentDateAndTime, + ); + @override + List get $columns => [ + localId, + serverId, + customerId, + productVariantId, + customerNameSnapshot, + rating, + comment, + syncStatus, + createdAt, + updatedAt, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'product_reviews'; + @override + VerificationContext validateIntegrity( + Insertable instance, { + bool isInserting = false, + }) { + final context = VerificationContext(); + final data = instance.toColumns(true); + if (data.containsKey('local_id')) { + context.handle( + _localIdMeta, + localId.isAcceptableOrUnknown(data['local_id']!, _localIdMeta), + ); + } + if (data.containsKey('server_id')) { + context.handle( + _serverIdMeta, + serverId.isAcceptableOrUnknown(data['server_id']!, _serverIdMeta), + ); + } + if (data.containsKey('customer_id')) { + context.handle( + _customerIdMeta, + customerId.isAcceptableOrUnknown(data['customer_id']!, _customerIdMeta), + ); + } + if (data.containsKey('product_variant_id')) { + context.handle( + _productVariantIdMeta, + productVariantId.isAcceptableOrUnknown( + data['product_variant_id']!, + _productVariantIdMeta, + ), + ); + } else if (isInserting) { + context.missing(_productVariantIdMeta); + } + if (data.containsKey('customer_name_snapshot')) { + context.handle( + _customerNameSnapshotMeta, + customerNameSnapshot.isAcceptableOrUnknown( + data['customer_name_snapshot']!, + _customerNameSnapshotMeta, + ), + ); + } + if (data.containsKey('rating')) { + context.handle( + _ratingMeta, + rating.isAcceptableOrUnknown(data['rating']!, _ratingMeta), + ); + } + if (data.containsKey('comment')) { + context.handle( + _commentMeta, + comment.isAcceptableOrUnknown(data['comment']!, _commentMeta), + ); + } + if (data.containsKey('sync_status')) { + context.handle( + _syncStatusMeta, + syncStatus.isAcceptableOrUnknown(data['sync_status']!, _syncStatusMeta), + ); + } + if (data.containsKey('created_at')) { + context.handle( + _createdAtMeta, + createdAt.isAcceptableOrUnknown(data['created_at']!, _createdAtMeta), + ); + } + if (data.containsKey('updated_at')) { + context.handle( + _updatedAtMeta, + updatedAt.isAcceptableOrUnknown(data['updated_at']!, _updatedAtMeta), + ); + } + return context; + } + + @override + Set get $primaryKey => {localId}; + @override + ProductReview map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return ProductReview( + localId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}local_id'], + )!, + serverId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}server_id'], + ), + customerId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}customer_id'], + ), + productVariantId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}product_variant_id'], + )!, + customerNameSnapshot: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}customer_name_snapshot'], + ), + rating: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}rating'], + )!, + comment: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}comment'], + ), + syncStatus: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}sync_status'], + )!, + createdAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}created_at'], + )!, + updatedAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}updated_at'], + )!, + ); + } + + @override + $ProductReviewsTable createAlias(String alias) { + return $ProductReviewsTable(attachedDatabase, alias); + } +} + +class ProductReview extends DataClass implements Insertable { + final String localId; + final String? serverId; + final String? customerId; + final String productVariantId; + final String? customerNameSnapshot; + final int rating; + final String? comment; + final String syncStatus; + final DateTime createdAt; + final DateTime updatedAt; + const ProductReview({ + required this.localId, + this.serverId, + this.customerId, + required this.productVariantId, + this.customerNameSnapshot, + required this.rating, + this.comment, + required this.syncStatus, + required this.createdAt, + required this.updatedAt, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['local_id'] = Variable(localId); + if (!nullToAbsent || serverId != null) { + map['server_id'] = Variable(serverId); + } + if (!nullToAbsent || customerId != null) { + map['customer_id'] = Variable(customerId); + } + map['product_variant_id'] = Variable(productVariantId); + if (!nullToAbsent || customerNameSnapshot != null) { + map['customer_name_snapshot'] = Variable(customerNameSnapshot); + } + map['rating'] = Variable(rating); + if (!nullToAbsent || comment != null) { + map['comment'] = Variable(comment); + } + map['sync_status'] = Variable(syncStatus); + map['created_at'] = Variable(createdAt); + map['updated_at'] = Variable(updatedAt); + return map; + } + + ProductReviewsCompanion toCompanion(bool nullToAbsent) { + return ProductReviewsCompanion( + localId: Value(localId), + serverId: serverId == null && nullToAbsent + ? const Value.absent() + : Value(serverId), + customerId: customerId == null && nullToAbsent + ? const Value.absent() + : Value(customerId), + productVariantId: Value(productVariantId), + customerNameSnapshot: customerNameSnapshot == null && nullToAbsent + ? const Value.absent() + : Value(customerNameSnapshot), + rating: Value(rating), + comment: comment == null && nullToAbsent + ? const Value.absent() + : Value(comment), + syncStatus: Value(syncStatus), + createdAt: Value(createdAt), + updatedAt: Value(updatedAt), + ); + } + + factory ProductReview.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return ProductReview( + localId: serializer.fromJson(json['localId']), + serverId: serializer.fromJson(json['serverId']), + customerId: serializer.fromJson(json['customerId']), + productVariantId: serializer.fromJson(json['productVariantId']), + customerNameSnapshot: serializer.fromJson( + json['customerNameSnapshot'], + ), + rating: serializer.fromJson(json['rating']), + comment: serializer.fromJson(json['comment']), + syncStatus: serializer.fromJson(json['syncStatus']), + createdAt: serializer.fromJson(json['createdAt']), + updatedAt: serializer.fromJson(json['updatedAt']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'localId': serializer.toJson(localId), + 'serverId': serializer.toJson(serverId), + 'customerId': serializer.toJson(customerId), + 'productVariantId': serializer.toJson(productVariantId), + 'customerNameSnapshot': serializer.toJson(customerNameSnapshot), + 'rating': serializer.toJson(rating), + 'comment': serializer.toJson(comment), + 'syncStatus': serializer.toJson(syncStatus), + 'createdAt': serializer.toJson(createdAt), + 'updatedAt': serializer.toJson(updatedAt), + }; + } + + ProductReview copyWith({ + String? localId, + Value serverId = const Value.absent(), + Value customerId = const Value.absent(), + String? productVariantId, + Value customerNameSnapshot = const Value.absent(), + int? rating, + Value comment = const Value.absent(), + String? syncStatus, + DateTime? createdAt, + DateTime? updatedAt, + }) => ProductReview( + localId: localId ?? this.localId, + serverId: serverId.present ? serverId.value : this.serverId, + customerId: customerId.present ? customerId.value : this.customerId, + productVariantId: productVariantId ?? this.productVariantId, + customerNameSnapshot: customerNameSnapshot.present + ? customerNameSnapshot.value + : this.customerNameSnapshot, + rating: rating ?? this.rating, + comment: comment.present ? comment.value : this.comment, + syncStatus: syncStatus ?? this.syncStatus, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + ); + ProductReview copyWithCompanion(ProductReviewsCompanion data) { + return ProductReview( + localId: data.localId.present ? data.localId.value : this.localId, + serverId: data.serverId.present ? data.serverId.value : this.serverId, + customerId: data.customerId.present + ? data.customerId.value + : this.customerId, + productVariantId: data.productVariantId.present + ? data.productVariantId.value + : this.productVariantId, + customerNameSnapshot: data.customerNameSnapshot.present + ? data.customerNameSnapshot.value + : this.customerNameSnapshot, + rating: data.rating.present ? data.rating.value : this.rating, + comment: data.comment.present ? data.comment.value : this.comment, + syncStatus: data.syncStatus.present + ? data.syncStatus.value + : this.syncStatus, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, + ); + } + + @override + String toString() { + return (StringBuffer('ProductReview(') + ..write('localId: $localId, ') + ..write('serverId: $serverId, ') + ..write('customerId: $customerId, ') + ..write('productVariantId: $productVariantId, ') + ..write('customerNameSnapshot: $customerNameSnapshot, ') + ..write('rating: $rating, ') + ..write('comment: $comment, ') + ..write('syncStatus: $syncStatus, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + localId, + serverId, + customerId, + productVariantId, + customerNameSnapshot, + rating, + comment, + syncStatus, + createdAt, + updatedAt, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is ProductReview && + other.localId == this.localId && + other.serverId == this.serverId && + other.customerId == this.customerId && + other.productVariantId == this.productVariantId && + other.customerNameSnapshot == this.customerNameSnapshot && + other.rating == this.rating && + other.comment == this.comment && + other.syncStatus == this.syncStatus && + other.createdAt == this.createdAt && + other.updatedAt == this.updatedAt); +} + +class ProductReviewsCompanion extends UpdateCompanion { + final Value localId; + final Value serverId; + final Value customerId; + final Value productVariantId; + final Value customerNameSnapshot; + final Value rating; + final Value comment; + final Value syncStatus; + final Value createdAt; + final Value updatedAt; + final Value rowid; + const ProductReviewsCompanion({ + this.localId = const Value.absent(), + this.serverId = const Value.absent(), + this.customerId = const Value.absent(), + this.productVariantId = const Value.absent(), + this.customerNameSnapshot = const Value.absent(), + this.rating = const Value.absent(), + this.comment = const Value.absent(), + this.syncStatus = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.rowid = const Value.absent(), + }); + ProductReviewsCompanion.insert({ + this.localId = const Value.absent(), + this.serverId = const Value.absent(), + this.customerId = const Value.absent(), + required String productVariantId, + this.customerNameSnapshot = const Value.absent(), + this.rating = const Value.absent(), + this.comment = const Value.absent(), + this.syncStatus = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.rowid = const Value.absent(), + }) : productVariantId = Value(productVariantId); + static Insertable custom({ + Expression? localId, + Expression? serverId, + Expression? customerId, + Expression? productVariantId, + Expression? customerNameSnapshot, + Expression? rating, + Expression? comment, + Expression? syncStatus, + Expression? createdAt, + Expression? updatedAt, + Expression? rowid, + }) { + return RawValuesInsertable({ + if (localId != null) 'local_id': localId, + if (serverId != null) 'server_id': serverId, + if (customerId != null) 'customer_id': customerId, + if (productVariantId != null) 'product_variant_id': productVariantId, + if (customerNameSnapshot != null) + 'customer_name_snapshot': customerNameSnapshot, + if (rating != null) 'rating': rating, + if (comment != null) 'comment': comment, + if (syncStatus != null) 'sync_status': syncStatus, + if (createdAt != null) 'created_at': createdAt, + if (updatedAt != null) 'updated_at': updatedAt, + if (rowid != null) 'rowid': rowid, + }); + } + + ProductReviewsCompanion copyWith({ + Value? localId, + Value? serverId, + Value? customerId, + Value? productVariantId, + Value? customerNameSnapshot, + Value? rating, + Value? comment, + Value? syncStatus, + Value? createdAt, + Value? updatedAt, + Value? rowid, + }) { + return ProductReviewsCompanion( + localId: localId ?? this.localId, + serverId: serverId ?? this.serverId, + customerId: customerId ?? this.customerId, + productVariantId: productVariantId ?? this.productVariantId, + customerNameSnapshot: customerNameSnapshot ?? this.customerNameSnapshot, + rating: rating ?? this.rating, + comment: comment ?? this.comment, + syncStatus: syncStatus ?? this.syncStatus, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (localId.present) { + map['local_id'] = Variable(localId.value); + } + if (serverId.present) { + map['server_id'] = Variable(serverId.value); + } + if (customerId.present) { + map['customer_id'] = Variable(customerId.value); + } + if (productVariantId.present) { + map['product_variant_id'] = Variable(productVariantId.value); + } + if (customerNameSnapshot.present) { + map['customer_name_snapshot'] = Variable( + customerNameSnapshot.value, + ); + } + if (rating.present) { + map['rating'] = Variable(rating.value); + } + if (comment.present) { + map['comment'] = Variable(comment.value); + } + if (syncStatus.present) { + map['sync_status'] = Variable(syncStatus.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (updatedAt.present) { + map['updated_at'] = Variable(updatedAt.value); + } + if (rowid.present) { + map['rowid'] = Variable(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('ProductReviewsCompanion(') + ..write('localId: $localId, ') + ..write('serverId: $serverId, ') + ..write('customerId: $customerId, ') + ..write('productVariantId: $productVariantId, ') + ..write('customerNameSnapshot: $customerNameSnapshot, ') + ..write('rating: $rating, ') + ..write('comment: $comment, ') + ..write('syncStatus: $syncStatus, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + +class $ProductLikesTable extends ProductLikes + with TableInfo<$ProductLikesTable, ProductLike> { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + $ProductLikesTable(this.attachedDatabase, [this._alias]); + static const VerificationMeta _localIdMeta = const VerificationMeta( + 'localId', + ); + @override + late final GeneratedColumn localId = GeneratedColumn( + 'local_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: false, + clientDefault: () => const Uuid().v7().toString(), + ); + static const VerificationMeta _serverIdMeta = const VerificationMeta( + 'serverId', + ); + @override + late final GeneratedColumn serverId = GeneratedColumn( + 'server_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _customerIdMeta = const VerificationMeta( + 'customerId', + ); + @override + late final GeneratedColumn customerId = GeneratedColumn( + 'customer_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES customers (local_id)', + ), + ); + static const VerificationMeta _productIdMeta = const VerificationMeta( + 'productId', + ); + @override + late final GeneratedColumn productId = GeneratedColumn( + 'product_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES products (local_id)', + ), + ); + static const VerificationMeta _isLikeMeta = const VerificationMeta('isLike'); + @override + late final GeneratedColumn isLike = GeneratedColumn( + 'is_like', + aliasedName, + false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("is_like" IN (0, 1))', + ), + defaultValue: const Constant(false), + ); + static const VerificationMeta _syncStatusMeta = const VerificationMeta( + 'syncStatus', + ); + @override + late final GeneratedColumn syncStatus = GeneratedColumn( + 'sync_status', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultValue: const Constant('dirty'), + ); + static const VerificationMeta _createdAtMeta = const VerificationMeta( + 'createdAt', + ); + @override + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: currentDateAndTime, + ); + static const VerificationMeta _updatedAtMeta = const VerificationMeta( + 'updatedAt', + ); + @override + late final GeneratedColumn updatedAt = GeneratedColumn( + 'updated_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: currentDateAndTime, + ); + @override + List get $columns => [ + localId, + serverId, + customerId, + productId, + isLike, + syncStatus, + createdAt, + updatedAt, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'product_likes'; + @override + VerificationContext validateIntegrity( + Insertable instance, { + bool isInserting = false, + }) { + final context = VerificationContext(); + final data = instance.toColumns(true); + if (data.containsKey('local_id')) { + context.handle( + _localIdMeta, + localId.isAcceptableOrUnknown(data['local_id']!, _localIdMeta), + ); + } + if (data.containsKey('server_id')) { + context.handle( + _serverIdMeta, + serverId.isAcceptableOrUnknown(data['server_id']!, _serverIdMeta), + ); + } + if (data.containsKey('customer_id')) { + context.handle( + _customerIdMeta, + customerId.isAcceptableOrUnknown(data['customer_id']!, _customerIdMeta), + ); + } + if (data.containsKey('product_id')) { + context.handle( + _productIdMeta, + productId.isAcceptableOrUnknown(data['product_id']!, _productIdMeta), + ); + } else if (isInserting) { + context.missing(_productIdMeta); + } + if (data.containsKey('is_like')) { + context.handle( + _isLikeMeta, + isLike.isAcceptableOrUnknown(data['is_like']!, _isLikeMeta), + ); + } + if (data.containsKey('sync_status')) { + context.handle( + _syncStatusMeta, + syncStatus.isAcceptableOrUnknown(data['sync_status']!, _syncStatusMeta), + ); + } + if (data.containsKey('created_at')) { + context.handle( + _createdAtMeta, + createdAt.isAcceptableOrUnknown(data['created_at']!, _createdAtMeta), + ); + } + if (data.containsKey('updated_at')) { + context.handle( + _updatedAtMeta, + updatedAt.isAcceptableOrUnknown(data['updated_at']!, _updatedAtMeta), + ); + } + return context; + } + + @override + Set get $primaryKey => {localId}; + @override + ProductLike map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return ProductLike( + localId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}local_id'], + )!, + serverId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}server_id'], + ), + customerId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}customer_id'], + ), + productId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}product_id'], + )!, + isLike: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}is_like'], + )!, + syncStatus: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}sync_status'], + )!, + createdAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}created_at'], + )!, + updatedAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}updated_at'], + )!, + ); + } + + @override + $ProductLikesTable createAlias(String alias) { + return $ProductLikesTable(attachedDatabase, alias); + } +} + +class ProductLike extends DataClass implements Insertable { + final String localId; + final String? serverId; + final String? customerId; + final String productId; + final bool isLike; + final String syncStatus; + final DateTime createdAt; + final DateTime updatedAt; + const ProductLike({ + required this.localId, + this.serverId, + this.customerId, + required this.productId, + required this.isLike, + required this.syncStatus, + required this.createdAt, + required this.updatedAt, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['local_id'] = Variable(localId); + if (!nullToAbsent || serverId != null) { + map['server_id'] = Variable(serverId); + } + if (!nullToAbsent || customerId != null) { + map['customer_id'] = Variable(customerId); + } + map['product_id'] = Variable(productId); + map['is_like'] = Variable(isLike); + map['sync_status'] = Variable(syncStatus); + map['created_at'] = Variable(createdAt); + map['updated_at'] = Variable(updatedAt); + return map; + } + + ProductLikesCompanion toCompanion(bool nullToAbsent) { + return ProductLikesCompanion( + localId: Value(localId), + serverId: serverId == null && nullToAbsent + ? const Value.absent() + : Value(serverId), + customerId: customerId == null && nullToAbsent + ? const Value.absent() + : Value(customerId), + productId: Value(productId), + isLike: Value(isLike), + syncStatus: Value(syncStatus), + createdAt: Value(createdAt), + updatedAt: Value(updatedAt), + ); + } + + factory ProductLike.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return ProductLike( + localId: serializer.fromJson(json['localId']), + serverId: serializer.fromJson(json['serverId']), + customerId: serializer.fromJson(json['customerId']), + productId: serializer.fromJson(json['productId']), + isLike: serializer.fromJson(json['isLike']), + syncStatus: serializer.fromJson(json['syncStatus']), + createdAt: serializer.fromJson(json['createdAt']), + updatedAt: serializer.fromJson(json['updatedAt']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'localId': serializer.toJson(localId), + 'serverId': serializer.toJson(serverId), + 'customerId': serializer.toJson(customerId), + 'productId': serializer.toJson(productId), + 'isLike': serializer.toJson(isLike), + 'syncStatus': serializer.toJson(syncStatus), + 'createdAt': serializer.toJson(createdAt), + 'updatedAt': serializer.toJson(updatedAt), + }; + } + + ProductLike copyWith({ + String? localId, + Value serverId = const Value.absent(), + Value customerId = const Value.absent(), + String? productId, + bool? isLike, + String? syncStatus, + DateTime? createdAt, + DateTime? updatedAt, + }) => ProductLike( + localId: localId ?? this.localId, + serverId: serverId.present ? serverId.value : this.serverId, + customerId: customerId.present ? customerId.value : this.customerId, + productId: productId ?? this.productId, + isLike: isLike ?? this.isLike, + syncStatus: syncStatus ?? this.syncStatus, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + ); + ProductLike copyWithCompanion(ProductLikesCompanion data) { + return ProductLike( + localId: data.localId.present ? data.localId.value : this.localId, + serverId: data.serverId.present ? data.serverId.value : this.serverId, + customerId: data.customerId.present + ? data.customerId.value + : this.customerId, + productId: data.productId.present ? data.productId.value : this.productId, + isLike: data.isLike.present ? data.isLike.value : this.isLike, + syncStatus: data.syncStatus.present + ? data.syncStatus.value + : this.syncStatus, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, + ); + } + + @override + String toString() { + return (StringBuffer('ProductLike(') + ..write('localId: $localId, ') + ..write('serverId: $serverId, ') + ..write('customerId: $customerId, ') + ..write('productId: $productId, ') + ..write('isLike: $isLike, ') + ..write('syncStatus: $syncStatus, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + localId, + serverId, + customerId, + productId, + isLike, + syncStatus, + createdAt, + updatedAt, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is ProductLike && + other.localId == this.localId && + other.serverId == this.serverId && + other.customerId == this.customerId && + other.productId == this.productId && + other.isLike == this.isLike && + other.syncStatus == this.syncStatus && + other.createdAt == this.createdAt && + other.updatedAt == this.updatedAt); +} + +class ProductLikesCompanion extends UpdateCompanion { + final Value localId; + final Value serverId; + final Value customerId; + final Value productId; + final Value isLike; + final Value syncStatus; + final Value createdAt; + final Value updatedAt; + final Value rowid; + const ProductLikesCompanion({ + this.localId = const Value.absent(), + this.serverId = const Value.absent(), + this.customerId = const Value.absent(), + this.productId = const Value.absent(), + this.isLike = const Value.absent(), + this.syncStatus = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.rowid = const Value.absent(), + }); + ProductLikesCompanion.insert({ + this.localId = const Value.absent(), + this.serverId = const Value.absent(), + this.customerId = const Value.absent(), + required String productId, + this.isLike = const Value.absent(), + this.syncStatus = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.rowid = const Value.absent(), + }) : productId = Value(productId); + static Insertable custom({ + Expression? localId, + Expression? serverId, + Expression? customerId, + Expression? productId, + Expression? isLike, + Expression? syncStatus, + Expression? createdAt, + Expression? updatedAt, + Expression? rowid, + }) { + return RawValuesInsertable({ + if (localId != null) 'local_id': localId, + if (serverId != null) 'server_id': serverId, + if (customerId != null) 'customer_id': customerId, + if (productId != null) 'product_id': productId, + if (isLike != null) 'is_like': isLike, + if (syncStatus != null) 'sync_status': syncStatus, + if (createdAt != null) 'created_at': createdAt, + if (updatedAt != null) 'updated_at': updatedAt, + if (rowid != null) 'rowid': rowid, + }); + } + + ProductLikesCompanion copyWith({ + Value? localId, + Value? serverId, + Value? customerId, + Value? productId, + Value? isLike, + Value? syncStatus, + Value? createdAt, + Value? updatedAt, + Value? rowid, + }) { + return ProductLikesCompanion( + localId: localId ?? this.localId, + serverId: serverId ?? this.serverId, + customerId: customerId ?? this.customerId, + productId: productId ?? this.productId, + isLike: isLike ?? this.isLike, + syncStatus: syncStatus ?? this.syncStatus, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (localId.present) { + map['local_id'] = Variable(localId.value); + } + if (serverId.present) { + map['server_id'] = Variable(serverId.value); + } + if (customerId.present) { + map['customer_id'] = Variable(customerId.value); + } + if (productId.present) { + map['product_id'] = Variable(productId.value); + } + if (isLike.present) { + map['is_like'] = Variable(isLike.value); + } + if (syncStatus.present) { + map['sync_status'] = Variable(syncStatus.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (updatedAt.present) { + map['updated_at'] = Variable(updatedAt.value); + } + if (rowid.present) { + map['rowid'] = Variable(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('ProductLikesCompanion(') + ..write('localId: $localId, ') + ..write('serverId: $serverId, ') + ..write('customerId: $customerId, ') + ..write('productId: $productId, ') + ..write('isLike: $isLike, ') + ..write('syncStatus: $syncStatus, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + +class $RawMaterialsTable extends RawMaterials + with TableInfo<$RawMaterialsTable, RawMaterial> { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + $RawMaterialsTable(this.attachedDatabase, [this._alias]); + static const VerificationMeta _localIdMeta = const VerificationMeta( + 'localId', + ); + @override + late final GeneratedColumn localId = GeneratedColumn( + 'local_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: false, + clientDefault: () => const Uuid().v7().toString(), + ); + static const VerificationMeta _serverIdMeta = const VerificationMeta( + 'serverId', + ); + @override + late final GeneratedColumn serverId = GeneratedColumn( + 'server_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _unitIdMeta = const VerificationMeta('unitId'); + @override + late final GeneratedColumn unitId = GeneratedColumn( + 'unit_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES units (local_id)', + ), + ); + static const VerificationMeta _nameMeta = const VerificationMeta('name'); + @override + late final GeneratedColumn name = GeneratedColumn( + 'name', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + static const VerificationMeta _syncStatusMeta = const VerificationMeta( + 'syncStatus', + ); + @override + late final GeneratedColumn syncStatus = GeneratedColumn( + 'sync_status', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultValue: const Constant('dirty'), + ); + static const VerificationMeta _createdAtMeta = const VerificationMeta( + 'createdAt', + ); + @override + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: currentDateAndTime, + ); + static const VerificationMeta _updatedAtMeta = const VerificationMeta( + 'updatedAt', + ); + @override + late final GeneratedColumn updatedAt = GeneratedColumn( + 'updated_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: currentDateAndTime, + ); + static const VerificationMeta _deletedAtMeta = const VerificationMeta( + 'deletedAt', + ); + @override + late final GeneratedColumn deletedAt = GeneratedColumn( + 'deleted_at', + aliasedName, + true, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + ); + @override + List get $columns => [ + localId, + serverId, + unitId, + name, + syncStatus, + createdAt, + updatedAt, + deletedAt, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'raw_materials'; + @override + VerificationContext validateIntegrity( + Insertable instance, { + bool isInserting = false, + }) { + final context = VerificationContext(); + final data = instance.toColumns(true); + if (data.containsKey('local_id')) { + context.handle( + _localIdMeta, + localId.isAcceptableOrUnknown(data['local_id']!, _localIdMeta), + ); + } + if (data.containsKey('server_id')) { + context.handle( + _serverIdMeta, + serverId.isAcceptableOrUnknown(data['server_id']!, _serverIdMeta), + ); + } + if (data.containsKey('unit_id')) { + context.handle( + _unitIdMeta, + unitId.isAcceptableOrUnknown(data['unit_id']!, _unitIdMeta), + ); + } else if (isInserting) { + context.missing(_unitIdMeta); + } + if (data.containsKey('name')) { + context.handle( + _nameMeta, + name.isAcceptableOrUnknown(data['name']!, _nameMeta), + ); + } else if (isInserting) { + context.missing(_nameMeta); + } + if (data.containsKey('sync_status')) { + context.handle( + _syncStatusMeta, + syncStatus.isAcceptableOrUnknown(data['sync_status']!, _syncStatusMeta), + ); + } + if (data.containsKey('created_at')) { + context.handle( + _createdAtMeta, + createdAt.isAcceptableOrUnknown(data['created_at']!, _createdAtMeta), + ); + } + if (data.containsKey('updated_at')) { + context.handle( + _updatedAtMeta, + updatedAt.isAcceptableOrUnknown(data['updated_at']!, _updatedAtMeta), + ); + } + if (data.containsKey('deleted_at')) { + context.handle( + _deletedAtMeta, + deletedAt.isAcceptableOrUnknown(data['deleted_at']!, _deletedAtMeta), + ); + } + return context; + } + + @override + Set get $primaryKey => {localId}; + @override + RawMaterial map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return RawMaterial( + localId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}local_id'], + )!, + serverId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}server_id'], + ), + unitId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}unit_id'], + )!, + name: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}name'], + )!, + syncStatus: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}sync_status'], + )!, + createdAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}created_at'], + )!, + updatedAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}updated_at'], + )!, + deletedAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}deleted_at'], + ), + ); + } + + @override + $RawMaterialsTable createAlias(String alias) { + return $RawMaterialsTable(attachedDatabase, alias); + } +} + +class RawMaterial extends DataClass implements Insertable { + final String localId; + final String? serverId; + final String unitId; + final String name; + final String syncStatus; + final DateTime createdAt; + final DateTime updatedAt; + final DateTime? deletedAt; + const RawMaterial({ + required this.localId, + this.serverId, + required this.unitId, + required this.name, + required this.syncStatus, + required this.createdAt, + required this.updatedAt, + this.deletedAt, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['local_id'] = Variable(localId); + if (!nullToAbsent || serverId != null) { + map['server_id'] = Variable(serverId); + } + map['unit_id'] = Variable(unitId); + map['name'] = Variable(name); + map['sync_status'] = Variable(syncStatus); + map['created_at'] = Variable(createdAt); + map['updated_at'] = Variable(updatedAt); + if (!nullToAbsent || deletedAt != null) { + map['deleted_at'] = Variable(deletedAt); + } + return map; + } + + RawMaterialsCompanion toCompanion(bool nullToAbsent) { + return RawMaterialsCompanion( + localId: Value(localId), + serverId: serverId == null && nullToAbsent + ? const Value.absent() + : Value(serverId), + unitId: Value(unitId), + name: Value(name), + syncStatus: Value(syncStatus), + createdAt: Value(createdAt), + updatedAt: Value(updatedAt), + deletedAt: deletedAt == null && nullToAbsent + ? const Value.absent() + : Value(deletedAt), + ); + } + + factory RawMaterial.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return RawMaterial( + localId: serializer.fromJson(json['localId']), + serverId: serializer.fromJson(json['serverId']), + unitId: serializer.fromJson(json['unitId']), + name: serializer.fromJson(json['name']), + syncStatus: serializer.fromJson(json['syncStatus']), + createdAt: serializer.fromJson(json['createdAt']), + updatedAt: serializer.fromJson(json['updatedAt']), + deletedAt: serializer.fromJson(json['deletedAt']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'localId': serializer.toJson(localId), + 'serverId': serializer.toJson(serverId), + 'unitId': serializer.toJson(unitId), + 'name': serializer.toJson(name), + 'syncStatus': serializer.toJson(syncStatus), + 'createdAt': serializer.toJson(createdAt), + 'updatedAt': serializer.toJson(updatedAt), + 'deletedAt': serializer.toJson(deletedAt), + }; + } + + RawMaterial copyWith({ + String? localId, + Value serverId = const Value.absent(), + String? unitId, + String? name, + String? syncStatus, + DateTime? createdAt, + DateTime? updatedAt, + Value deletedAt = const Value.absent(), + }) => RawMaterial( + localId: localId ?? this.localId, + serverId: serverId.present ? serverId.value : this.serverId, + unitId: unitId ?? this.unitId, + name: name ?? this.name, + syncStatus: syncStatus ?? this.syncStatus, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + deletedAt: deletedAt.present ? deletedAt.value : this.deletedAt, + ); + RawMaterial copyWithCompanion(RawMaterialsCompanion data) { + return RawMaterial( + localId: data.localId.present ? data.localId.value : this.localId, + serverId: data.serverId.present ? data.serverId.value : this.serverId, + unitId: data.unitId.present ? data.unitId.value : this.unitId, + name: data.name.present ? data.name.value : this.name, + syncStatus: data.syncStatus.present + ? data.syncStatus.value + : this.syncStatus, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, + deletedAt: data.deletedAt.present ? data.deletedAt.value : this.deletedAt, + ); + } + + @override + String toString() { + return (StringBuffer('RawMaterial(') + ..write('localId: $localId, ') + ..write('serverId: $serverId, ') + ..write('unitId: $unitId, ') + ..write('name: $name, ') + ..write('syncStatus: $syncStatus, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('deletedAt: $deletedAt') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + localId, + serverId, + unitId, + name, + syncStatus, + createdAt, + updatedAt, + deletedAt, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is RawMaterial && + other.localId == this.localId && + other.serverId == this.serverId && + other.unitId == this.unitId && + other.name == this.name && + other.syncStatus == this.syncStatus && + other.createdAt == this.createdAt && + other.updatedAt == this.updatedAt && + other.deletedAt == this.deletedAt); +} + +class RawMaterialsCompanion extends UpdateCompanion { + final Value localId; + final Value serverId; + final Value unitId; + final Value name; + final Value syncStatus; + final Value createdAt; + final Value updatedAt; + final Value deletedAt; + final Value rowid; + const RawMaterialsCompanion({ + this.localId = const Value.absent(), + this.serverId = const Value.absent(), + this.unitId = const Value.absent(), + this.name = const Value.absent(), + this.syncStatus = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.deletedAt = const Value.absent(), + this.rowid = const Value.absent(), + }); + RawMaterialsCompanion.insert({ + this.localId = const Value.absent(), + this.serverId = const Value.absent(), + required String unitId, + required String name, + this.syncStatus = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.deletedAt = const Value.absent(), + this.rowid = const Value.absent(), + }) : unitId = Value(unitId), + name = Value(name); + static Insertable custom({ + Expression? localId, + Expression? serverId, + Expression? unitId, + Expression? name, + Expression? syncStatus, + Expression? createdAt, + Expression? updatedAt, + Expression? deletedAt, + Expression? rowid, + }) { + return RawValuesInsertable({ + if (localId != null) 'local_id': localId, + if (serverId != null) 'server_id': serverId, + if (unitId != null) 'unit_id': unitId, + if (name != null) 'name': name, + if (syncStatus != null) 'sync_status': syncStatus, + if (createdAt != null) 'created_at': createdAt, + if (updatedAt != null) 'updated_at': updatedAt, + if (deletedAt != null) 'deleted_at': deletedAt, + if (rowid != null) 'rowid': rowid, + }); + } + + RawMaterialsCompanion copyWith({ + Value? localId, + Value? serverId, + Value? unitId, + Value? name, + Value? syncStatus, + Value? createdAt, + Value? updatedAt, + Value? deletedAt, + Value? rowid, + }) { + return RawMaterialsCompanion( + localId: localId ?? this.localId, + serverId: serverId ?? this.serverId, + unitId: unitId ?? this.unitId, + name: name ?? this.name, + syncStatus: syncStatus ?? this.syncStatus, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + deletedAt: deletedAt ?? this.deletedAt, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (localId.present) { + map['local_id'] = Variable(localId.value); + } + if (serverId.present) { + map['server_id'] = Variable(serverId.value); + } + if (unitId.present) { + map['unit_id'] = Variable(unitId.value); + } + if (name.present) { + map['name'] = Variable(name.value); + } + if (syncStatus.present) { + map['sync_status'] = Variable(syncStatus.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (updatedAt.present) { + map['updated_at'] = Variable(updatedAt.value); + } + if (deletedAt.present) { + map['deleted_at'] = Variable(deletedAt.value); + } + if (rowid.present) { + map['rowid'] = Variable(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('RawMaterialsCompanion(') + ..write('localId: $localId, ') + ..write('serverId: $serverId, ') + ..write('unitId: $unitId, ') + ..write('name: $name, ') + ..write('syncStatus: $syncStatus, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('deletedAt: $deletedAt, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + +class $OutletInventoriesTable extends OutletInventories + with TableInfo<$OutletInventoriesTable, OutletInventory> { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + $OutletInventoriesTable(this.attachedDatabase, [this._alias]); + static const VerificationMeta _localIdMeta = const VerificationMeta( + 'localId', + ); + @override + late final GeneratedColumn localId = GeneratedColumn( + 'local_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: false, + clientDefault: () => const Uuid().v7().toString(), + ); + static const VerificationMeta _serverIdMeta = const VerificationMeta( + 'serverId', + ); + @override + late final GeneratedColumn serverId = GeneratedColumn( + 'server_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _outletIdMeta = const VerificationMeta( + 'outletId', + ); + @override + late final GeneratedColumn outletId = GeneratedColumn( + 'outlet_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES outlets (local_id)', + ), + ); + static const VerificationMeta _itemTypeMeta = const VerificationMeta( + 'itemType', + ); + @override + late final GeneratedColumn itemType = GeneratedColumn( + 'item_type', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + static const VerificationMeta _itemIdMeta = const VerificationMeta('itemId'); + @override + late final GeneratedColumn itemId = GeneratedColumn( + 'item_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + static const VerificationMeta _stockTypeMeta = const VerificationMeta( + 'stockType', + ); + @override + late final GeneratedColumn stockType = GeneratedColumn( + 'stock_type', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultValue: const Constant("fixed"), + ); + static const VerificationMeta _stockMeta = const VerificationMeta('stock'); + @override + late final GeneratedColumn stock = GeneratedColumn( + 'stock', + aliasedName, + false, + type: DriftSqlType.double, + requiredDuringInsert: false, + defaultValue: const Constant(0.0), + ); + static const VerificationMeta _sellingPriceMeta = const VerificationMeta( + 'sellingPrice', + ); + @override + late final GeneratedColumn sellingPrice = GeneratedColumn( + 'selling_price', + aliasedName, + false, + type: DriftSqlType.double, + requiredDuringInsert: false, + defaultValue: const Constant(0.0), + ); + static const VerificationMeta _costPriceMeta = const VerificationMeta( + 'costPrice', + ); + @override + late final GeneratedColumn costPrice = GeneratedColumn( + 'cost_price', + aliasedName, + false, + type: DriftSqlType.double, + requiredDuringInsert: false, + defaultValue: const Constant(0.0), + ); + static const VerificationMeta _purchasePriceMeta = const VerificationMeta( + 'purchasePrice', + ); + @override + late final GeneratedColumn purchasePrice = GeneratedColumn( + 'purchase_price', + aliasedName, + false, + type: DriftSqlType.double, + requiredDuringInsert: false, + defaultValue: const Constant(0.0), + ); + static const VerificationMeta _isDeleteByOwnerMeta = const VerificationMeta( + 'isDeleteByOwner', + ); + @override + late final GeneratedColumn isDeleteByOwner = GeneratedColumn( + 'is_delete_by_owner', + aliasedName, + true, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("is_delete_by_owner" IN (0, 1))', + ), + ); + static const VerificationMeta _syncStatusMeta = const VerificationMeta( + 'syncStatus', + ); + @override + late final GeneratedColumn syncStatus = GeneratedColumn( + 'sync_status', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultValue: const Constant("dirty"), + ); + static const VerificationMeta _createdAtMeta = const VerificationMeta( + 'createdAt', + ); + @override + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: currentDateAndTime, + ); + static const VerificationMeta _updatedAtMeta = const VerificationMeta( + 'updatedAt', + ); + @override + late final GeneratedColumn updatedAt = GeneratedColumn( + 'updated_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: currentDateAndTime, + ); + static const VerificationMeta _deletedAtMeta = const VerificationMeta( + 'deletedAt', + ); + @override + late final GeneratedColumn deletedAt = GeneratedColumn( + 'deleted_at', + aliasedName, + true, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + ); + @override + List get $columns => [ + localId, + serverId, + outletId, + itemType, + itemId, + stockType, + stock, + sellingPrice, + costPrice, + purchasePrice, + isDeleteByOwner, + syncStatus, + createdAt, + updatedAt, + deletedAt, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'outlet_inventories'; + @override + VerificationContext validateIntegrity( + Insertable instance, { + bool isInserting = false, + }) { + final context = VerificationContext(); + final data = instance.toColumns(true); + if (data.containsKey('local_id')) { + context.handle( + _localIdMeta, + localId.isAcceptableOrUnknown(data['local_id']!, _localIdMeta), + ); + } + if (data.containsKey('server_id')) { + context.handle( + _serverIdMeta, + serverId.isAcceptableOrUnknown(data['server_id']!, _serverIdMeta), + ); + } + if (data.containsKey('outlet_id')) { + context.handle( + _outletIdMeta, + outletId.isAcceptableOrUnknown(data['outlet_id']!, _outletIdMeta), + ); + } else if (isInserting) { + context.missing(_outletIdMeta); + } + if (data.containsKey('item_type')) { + context.handle( + _itemTypeMeta, + itemType.isAcceptableOrUnknown(data['item_type']!, _itemTypeMeta), + ); + } else if (isInserting) { + context.missing(_itemTypeMeta); + } + if (data.containsKey('item_id')) { + context.handle( + _itemIdMeta, + itemId.isAcceptableOrUnknown(data['item_id']!, _itemIdMeta), + ); + } else if (isInserting) { + context.missing(_itemIdMeta); + } + if (data.containsKey('stock_type')) { + context.handle( + _stockTypeMeta, + stockType.isAcceptableOrUnknown(data['stock_type']!, _stockTypeMeta), + ); + } + if (data.containsKey('stock')) { + context.handle( + _stockMeta, + stock.isAcceptableOrUnknown(data['stock']!, _stockMeta), + ); + } + if (data.containsKey('selling_price')) { + context.handle( + _sellingPriceMeta, + sellingPrice.isAcceptableOrUnknown( + data['selling_price']!, + _sellingPriceMeta, + ), + ); + } + if (data.containsKey('cost_price')) { + context.handle( + _costPriceMeta, + costPrice.isAcceptableOrUnknown(data['cost_price']!, _costPriceMeta), + ); + } + if (data.containsKey('purchase_price')) { + context.handle( + _purchasePriceMeta, + purchasePrice.isAcceptableOrUnknown( + data['purchase_price']!, + _purchasePriceMeta, + ), + ); + } + if (data.containsKey('is_delete_by_owner')) { + context.handle( + _isDeleteByOwnerMeta, + isDeleteByOwner.isAcceptableOrUnknown( + data['is_delete_by_owner']!, + _isDeleteByOwnerMeta, + ), + ); + } + if (data.containsKey('sync_status')) { + context.handle( + _syncStatusMeta, + syncStatus.isAcceptableOrUnknown(data['sync_status']!, _syncStatusMeta), + ); + } + if (data.containsKey('created_at')) { + context.handle( + _createdAtMeta, + createdAt.isAcceptableOrUnknown(data['created_at']!, _createdAtMeta), + ); + } + if (data.containsKey('updated_at')) { + context.handle( + _updatedAtMeta, + updatedAt.isAcceptableOrUnknown(data['updated_at']!, _updatedAtMeta), + ); + } + if (data.containsKey('deleted_at')) { + context.handle( + _deletedAtMeta, + deletedAt.isAcceptableOrUnknown(data['deleted_at']!, _deletedAtMeta), + ); + } + return context; + } + + @override + Set get $primaryKey => {localId}; + @override + OutletInventory map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return OutletInventory( + localId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}local_id'], + )!, + serverId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}server_id'], + ), + outletId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}outlet_id'], + )!, + itemType: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}item_type'], + )!, + itemId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}item_id'], + )!, + stockType: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}stock_type'], + )!, + stock: attachedDatabase.typeMapping.read( + DriftSqlType.double, + data['${effectivePrefix}stock'], + )!, + sellingPrice: attachedDatabase.typeMapping.read( + DriftSqlType.double, + data['${effectivePrefix}selling_price'], + )!, + costPrice: attachedDatabase.typeMapping.read( + DriftSqlType.double, + data['${effectivePrefix}cost_price'], + )!, + purchasePrice: attachedDatabase.typeMapping.read( + DriftSqlType.double, + data['${effectivePrefix}purchase_price'], + )!, + isDeleteByOwner: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}is_delete_by_owner'], + ), + syncStatus: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}sync_status'], + )!, + createdAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}created_at'], + )!, + updatedAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}updated_at'], + )!, + deletedAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}deleted_at'], + ), + ); + } + + @override + $OutletInventoriesTable createAlias(String alias) { + return $OutletInventoriesTable(attachedDatabase, alias); + } +} + +class OutletInventory extends DataClass implements Insertable { + final String localId; + final String? serverId; + final String outletId; + final String itemType; + final String itemId; + final String stockType; + final double stock; + final double sellingPrice; + final double costPrice; + final double purchasePrice; + final bool? isDeleteByOwner; + final String syncStatus; + final DateTime createdAt; + final DateTime updatedAt; + final DateTime? deletedAt; + const OutletInventory({ + required this.localId, + this.serverId, + required this.outletId, + required this.itemType, + required this.itemId, + required this.stockType, + required this.stock, + required this.sellingPrice, + required this.costPrice, + required this.purchasePrice, + this.isDeleteByOwner, + required this.syncStatus, + required this.createdAt, + required this.updatedAt, + this.deletedAt, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['local_id'] = Variable(localId); + if (!nullToAbsent || serverId != null) { + map['server_id'] = Variable(serverId); + } + map['outlet_id'] = Variable(outletId); + map['item_type'] = Variable(itemType); + map['item_id'] = Variable(itemId); + map['stock_type'] = Variable(stockType); + map['stock'] = Variable(stock); + map['selling_price'] = Variable(sellingPrice); + map['cost_price'] = Variable(costPrice); + map['purchase_price'] = Variable(purchasePrice); + if (!nullToAbsent || isDeleteByOwner != null) { + map['is_delete_by_owner'] = Variable(isDeleteByOwner); + } + map['sync_status'] = Variable(syncStatus); + map['created_at'] = Variable(createdAt); + map['updated_at'] = Variable(updatedAt); + if (!nullToAbsent || deletedAt != null) { + map['deleted_at'] = Variable(deletedAt); + } + return map; + } + + OutletInventoriesCompanion toCompanion(bool nullToAbsent) { + return OutletInventoriesCompanion( + localId: Value(localId), + serverId: serverId == null && nullToAbsent + ? const Value.absent() + : Value(serverId), + outletId: Value(outletId), + itemType: Value(itemType), + itemId: Value(itemId), + stockType: Value(stockType), + stock: Value(stock), + sellingPrice: Value(sellingPrice), + costPrice: Value(costPrice), + purchasePrice: Value(purchasePrice), + isDeleteByOwner: isDeleteByOwner == null && nullToAbsent + ? const Value.absent() + : Value(isDeleteByOwner), + syncStatus: Value(syncStatus), + createdAt: Value(createdAt), + updatedAt: Value(updatedAt), + deletedAt: deletedAt == null && nullToAbsent + ? const Value.absent() + : Value(deletedAt), + ); + } + + factory OutletInventory.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return OutletInventory( + localId: serializer.fromJson(json['localId']), + serverId: serializer.fromJson(json['serverId']), + outletId: serializer.fromJson(json['outletId']), + itemType: serializer.fromJson(json['itemType']), + itemId: serializer.fromJson(json['itemId']), + stockType: serializer.fromJson(json['stockType']), + stock: serializer.fromJson(json['stock']), + sellingPrice: serializer.fromJson(json['sellingPrice']), + costPrice: serializer.fromJson(json['costPrice']), + purchasePrice: serializer.fromJson(json['purchasePrice']), + isDeleteByOwner: serializer.fromJson(json['isDeleteByOwner']), + syncStatus: serializer.fromJson(json['syncStatus']), + createdAt: serializer.fromJson(json['createdAt']), + updatedAt: serializer.fromJson(json['updatedAt']), + deletedAt: serializer.fromJson(json['deletedAt']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'localId': serializer.toJson(localId), + 'serverId': serializer.toJson(serverId), + 'outletId': serializer.toJson(outletId), + 'itemType': serializer.toJson(itemType), + 'itemId': serializer.toJson(itemId), + 'stockType': serializer.toJson(stockType), + 'stock': serializer.toJson(stock), + 'sellingPrice': serializer.toJson(sellingPrice), + 'costPrice': serializer.toJson(costPrice), + 'purchasePrice': serializer.toJson(purchasePrice), + 'isDeleteByOwner': serializer.toJson(isDeleteByOwner), + 'syncStatus': serializer.toJson(syncStatus), + 'createdAt': serializer.toJson(createdAt), + 'updatedAt': serializer.toJson(updatedAt), + 'deletedAt': serializer.toJson(deletedAt), + }; + } + + OutletInventory copyWith({ + String? localId, + Value serverId = const Value.absent(), + String? outletId, + String? itemType, + String? itemId, + String? stockType, + double? stock, + double? sellingPrice, + double? costPrice, + double? purchasePrice, + Value isDeleteByOwner = const Value.absent(), + String? syncStatus, + DateTime? createdAt, + DateTime? updatedAt, + Value deletedAt = const Value.absent(), + }) => OutletInventory( + localId: localId ?? this.localId, + serverId: serverId.present ? serverId.value : this.serverId, + outletId: outletId ?? this.outletId, + itemType: itemType ?? this.itemType, + itemId: itemId ?? this.itemId, + stockType: stockType ?? this.stockType, + stock: stock ?? this.stock, + sellingPrice: sellingPrice ?? this.sellingPrice, + costPrice: costPrice ?? this.costPrice, + purchasePrice: purchasePrice ?? this.purchasePrice, + isDeleteByOwner: isDeleteByOwner.present + ? isDeleteByOwner.value + : this.isDeleteByOwner, + syncStatus: syncStatus ?? this.syncStatus, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + deletedAt: deletedAt.present ? deletedAt.value : this.deletedAt, + ); + OutletInventory copyWithCompanion(OutletInventoriesCompanion data) { + return OutletInventory( + localId: data.localId.present ? data.localId.value : this.localId, + serverId: data.serverId.present ? data.serverId.value : this.serverId, + outletId: data.outletId.present ? data.outletId.value : this.outletId, + itemType: data.itemType.present ? data.itemType.value : this.itemType, + itemId: data.itemId.present ? data.itemId.value : this.itemId, + stockType: data.stockType.present ? data.stockType.value : this.stockType, + stock: data.stock.present ? data.stock.value : this.stock, + sellingPrice: data.sellingPrice.present + ? data.sellingPrice.value + : this.sellingPrice, + costPrice: data.costPrice.present ? data.costPrice.value : this.costPrice, + purchasePrice: data.purchasePrice.present + ? data.purchasePrice.value + : this.purchasePrice, + isDeleteByOwner: data.isDeleteByOwner.present + ? data.isDeleteByOwner.value + : this.isDeleteByOwner, + syncStatus: data.syncStatus.present + ? data.syncStatus.value + : this.syncStatus, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, + deletedAt: data.deletedAt.present ? data.deletedAt.value : this.deletedAt, + ); + } + + @override + String toString() { + return (StringBuffer('OutletInventory(') + ..write('localId: $localId, ') + ..write('serverId: $serverId, ') + ..write('outletId: $outletId, ') + ..write('itemType: $itemType, ') + ..write('itemId: $itemId, ') + ..write('stockType: $stockType, ') + ..write('stock: $stock, ') + ..write('sellingPrice: $sellingPrice, ') + ..write('costPrice: $costPrice, ') + ..write('purchasePrice: $purchasePrice, ') + ..write('isDeleteByOwner: $isDeleteByOwner, ') + ..write('syncStatus: $syncStatus, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('deletedAt: $deletedAt') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + localId, + serverId, + outletId, + itemType, + itemId, + stockType, + stock, + sellingPrice, + costPrice, + purchasePrice, + isDeleteByOwner, + syncStatus, + createdAt, + updatedAt, + deletedAt, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is OutletInventory && + other.localId == this.localId && + other.serverId == this.serverId && + other.outletId == this.outletId && + other.itemType == this.itemType && + other.itemId == this.itemId && + other.stockType == this.stockType && + other.stock == this.stock && + other.sellingPrice == this.sellingPrice && + other.costPrice == this.costPrice && + other.purchasePrice == this.purchasePrice && + other.isDeleteByOwner == this.isDeleteByOwner && + other.syncStatus == this.syncStatus && + other.createdAt == this.createdAt && + other.updatedAt == this.updatedAt && + other.deletedAt == this.deletedAt); +} + +class OutletInventoriesCompanion extends UpdateCompanion { + final Value localId; + final Value serverId; + final Value outletId; + final Value itemType; + final Value itemId; + final Value stockType; + final Value stock; + final Value sellingPrice; + final Value costPrice; + final Value purchasePrice; + final Value isDeleteByOwner; + final Value syncStatus; + final Value createdAt; + final Value updatedAt; + final Value deletedAt; + final Value rowid; + const OutletInventoriesCompanion({ + this.localId = const Value.absent(), + this.serverId = const Value.absent(), + this.outletId = const Value.absent(), + this.itemType = const Value.absent(), + this.itemId = const Value.absent(), + this.stockType = const Value.absent(), + this.stock = const Value.absent(), + this.sellingPrice = const Value.absent(), + this.costPrice = const Value.absent(), + this.purchasePrice = const Value.absent(), + this.isDeleteByOwner = const Value.absent(), + this.syncStatus = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.deletedAt = const Value.absent(), + this.rowid = const Value.absent(), + }); + OutletInventoriesCompanion.insert({ + this.localId = const Value.absent(), + this.serverId = const Value.absent(), + required String outletId, + required String itemType, + required String itemId, + this.stockType = const Value.absent(), + this.stock = const Value.absent(), + this.sellingPrice = const Value.absent(), + this.costPrice = const Value.absent(), + this.purchasePrice = const Value.absent(), + this.isDeleteByOwner = const Value.absent(), + this.syncStatus = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.deletedAt = const Value.absent(), + this.rowid = const Value.absent(), + }) : outletId = Value(outletId), + itemType = Value(itemType), + itemId = Value(itemId); + static Insertable custom({ + Expression? localId, + Expression? serverId, + Expression? outletId, + Expression? itemType, + Expression? itemId, + Expression? stockType, + Expression? stock, + Expression? sellingPrice, + Expression? costPrice, + Expression? purchasePrice, + Expression? isDeleteByOwner, + Expression? syncStatus, + Expression? createdAt, + Expression? updatedAt, + Expression? deletedAt, + Expression? rowid, + }) { + return RawValuesInsertable({ + if (localId != null) 'local_id': localId, + if (serverId != null) 'server_id': serverId, + if (outletId != null) 'outlet_id': outletId, + if (itemType != null) 'item_type': itemType, + if (itemId != null) 'item_id': itemId, + if (stockType != null) 'stock_type': stockType, + if (stock != null) 'stock': stock, + if (sellingPrice != null) 'selling_price': sellingPrice, + if (costPrice != null) 'cost_price': costPrice, + if (purchasePrice != null) 'purchase_price': purchasePrice, + if (isDeleteByOwner != null) 'is_delete_by_owner': isDeleteByOwner, + if (syncStatus != null) 'sync_status': syncStatus, + if (createdAt != null) 'created_at': createdAt, + if (updatedAt != null) 'updated_at': updatedAt, + if (deletedAt != null) 'deleted_at': deletedAt, + if (rowid != null) 'rowid': rowid, + }); + } + + OutletInventoriesCompanion copyWith({ + Value? localId, + Value? serverId, + Value? outletId, + Value? itemType, + Value? itemId, + Value? stockType, + Value? stock, + Value? sellingPrice, + Value? costPrice, + Value? purchasePrice, + Value? isDeleteByOwner, + Value? syncStatus, + Value? createdAt, + Value? updatedAt, + Value? deletedAt, + Value? rowid, + }) { + return OutletInventoriesCompanion( + localId: localId ?? this.localId, + serverId: serverId ?? this.serverId, + outletId: outletId ?? this.outletId, + itemType: itemType ?? this.itemType, + itemId: itemId ?? this.itemId, + stockType: stockType ?? this.stockType, + stock: stock ?? this.stock, + sellingPrice: sellingPrice ?? this.sellingPrice, + costPrice: costPrice ?? this.costPrice, + purchasePrice: purchasePrice ?? this.purchasePrice, + isDeleteByOwner: isDeleteByOwner ?? this.isDeleteByOwner, + syncStatus: syncStatus ?? this.syncStatus, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + deletedAt: deletedAt ?? this.deletedAt, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (localId.present) { + map['local_id'] = Variable(localId.value); + } + if (serverId.present) { + map['server_id'] = Variable(serverId.value); + } + if (outletId.present) { + map['outlet_id'] = Variable(outletId.value); + } + if (itemType.present) { + map['item_type'] = Variable(itemType.value); + } + if (itemId.present) { + map['item_id'] = Variable(itemId.value); + } + if (stockType.present) { + map['stock_type'] = Variable(stockType.value); + } + if (stock.present) { + map['stock'] = Variable(stock.value); + } + if (sellingPrice.present) { + map['selling_price'] = Variable(sellingPrice.value); + } + if (costPrice.present) { + map['cost_price'] = Variable(costPrice.value); + } + if (purchasePrice.present) { + map['purchase_price'] = Variable(purchasePrice.value); + } + if (isDeleteByOwner.present) { + map['is_delete_by_owner'] = Variable(isDeleteByOwner.value); + } + if (syncStatus.present) { + map['sync_status'] = Variable(syncStatus.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (updatedAt.present) { + map['updated_at'] = Variable(updatedAt.value); + } + if (deletedAt.present) { + map['deleted_at'] = Variable(deletedAt.value); + } + if (rowid.present) { + map['rowid'] = Variable(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('OutletInventoriesCompanion(') + ..write('localId: $localId, ') + ..write('serverId: $serverId, ') + ..write('outletId: $outletId, ') + ..write('itemType: $itemType, ') + ..write('itemId: $itemId, ') + ..write('stockType: $stockType, ') + ..write('stock: $stock, ') + ..write('sellingPrice: $sellingPrice, ') + ..write('costPrice: $costPrice, ') + ..write('purchasePrice: $purchasePrice, ') + ..write('isDeleteByOwner: $isDeleteByOwner, ') + ..write('syncStatus: $syncStatus, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('deletedAt: $deletedAt, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + +class $ReceiptItemsTable extends ReceiptItems + with TableInfo<$ReceiptItemsTable, ReceiptItem> { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + $ReceiptItemsTable(this.attachedDatabase, [this._alias]); + static const VerificationMeta _localIdMeta = const VerificationMeta( + 'localId', + ); + @override + late final GeneratedColumn localId = GeneratedColumn( + 'local_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: false, + clientDefault: () => const Uuid().v7().toString(), + ); + static const VerificationMeta _serverIdMeta = const VerificationMeta( + 'serverId', + ); + @override + late final GeneratedColumn serverId = GeneratedColumn( + 'server_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _productVariantIdMeta = const VerificationMeta( + 'productVariantId', + ); + @override + late final GeneratedColumn productVariantId = GeneratedColumn( + 'product_variant_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES product_variants (local_id)', + ), + ); + static const VerificationMeta _rawMaterialIdMeta = const VerificationMeta( + 'rawMaterialId', + ); + @override + late final GeneratedColumn rawMaterialId = GeneratedColumn( + 'raw_material_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES raw_materials (local_id)', + ), + ); + static const VerificationMeta _quantityMeta = const VerificationMeta( + 'quantity', + ); + @override + late final GeneratedColumn quantity = GeneratedColumn( + 'quantity', + aliasedName, + false, + type: DriftSqlType.double, + requiredDuringInsert: false, + defaultValue: const Constant(0.0), + ); + static const VerificationMeta _syncStatusMeta = const VerificationMeta( + 'syncStatus', + ); + @override + late final GeneratedColumn syncStatus = GeneratedColumn( + 'sync_status', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultValue: const Constant('dirty'), + ); + static const VerificationMeta _createdAtMeta = const VerificationMeta( + 'createdAt', + ); + @override + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: currentDateAndTime, + ); + static const VerificationMeta _updatedAtMeta = const VerificationMeta( + 'updatedAt', + ); + @override + late final GeneratedColumn updatedAt = GeneratedColumn( + 'updated_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: currentDateAndTime, + ); + static const VerificationMeta _deletedAtMeta = const VerificationMeta( + 'deletedAt', + ); + @override + late final GeneratedColumn deletedAt = GeneratedColumn( + 'deleted_at', + aliasedName, + true, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + ); + @override + List get $columns => [ + localId, + serverId, + productVariantId, + rawMaterialId, + quantity, + syncStatus, + createdAt, + updatedAt, + deletedAt, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'receipt_items'; + @override + VerificationContext validateIntegrity( + Insertable instance, { + bool isInserting = false, + }) { + final context = VerificationContext(); + final data = instance.toColumns(true); + if (data.containsKey('local_id')) { + context.handle( + _localIdMeta, + localId.isAcceptableOrUnknown(data['local_id']!, _localIdMeta), + ); + } + if (data.containsKey('server_id')) { + context.handle( + _serverIdMeta, + serverId.isAcceptableOrUnknown(data['server_id']!, _serverIdMeta), + ); + } + if (data.containsKey('product_variant_id')) { + context.handle( + _productVariantIdMeta, + productVariantId.isAcceptableOrUnknown( + data['product_variant_id']!, + _productVariantIdMeta, + ), + ); + } + if (data.containsKey('raw_material_id')) { + context.handle( + _rawMaterialIdMeta, + rawMaterialId.isAcceptableOrUnknown( + data['raw_material_id']!, + _rawMaterialIdMeta, + ), + ); + } + if (data.containsKey('quantity')) { + context.handle( + _quantityMeta, + quantity.isAcceptableOrUnknown(data['quantity']!, _quantityMeta), + ); + } + if (data.containsKey('sync_status')) { + context.handle( + _syncStatusMeta, + syncStatus.isAcceptableOrUnknown(data['sync_status']!, _syncStatusMeta), + ); + } + if (data.containsKey('created_at')) { + context.handle( + _createdAtMeta, + createdAt.isAcceptableOrUnknown(data['created_at']!, _createdAtMeta), + ); + } + if (data.containsKey('updated_at')) { + context.handle( + _updatedAtMeta, + updatedAt.isAcceptableOrUnknown(data['updated_at']!, _updatedAtMeta), + ); + } + if (data.containsKey('deleted_at')) { + context.handle( + _deletedAtMeta, + deletedAt.isAcceptableOrUnknown(data['deleted_at']!, _deletedAtMeta), + ); + } + return context; + } + + @override + Set get $primaryKey => {localId}; + @override + ReceiptItem map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return ReceiptItem( + localId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}local_id'], + )!, + serverId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}server_id'], + ), + productVariantId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}product_variant_id'], + ), + rawMaterialId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}raw_material_id'], + ), + quantity: attachedDatabase.typeMapping.read( + DriftSqlType.double, + data['${effectivePrefix}quantity'], + )!, + syncStatus: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}sync_status'], + )!, + createdAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}created_at'], + )!, + updatedAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}updated_at'], + )!, + deletedAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}deleted_at'], + ), + ); + } + + @override + $ReceiptItemsTable createAlias(String alias) { + return $ReceiptItemsTable(attachedDatabase, alias); + } +} + +class ReceiptItem extends DataClass implements Insertable { + final String localId; + final String? serverId; + final String? productVariantId; + final String? rawMaterialId; + final double quantity; + final String syncStatus; + final DateTime createdAt; + final DateTime updatedAt; + final DateTime? deletedAt; + const ReceiptItem({ + required this.localId, + this.serverId, + this.productVariantId, + this.rawMaterialId, + required this.quantity, + required this.syncStatus, + required this.createdAt, + required this.updatedAt, + this.deletedAt, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['local_id'] = Variable(localId); + if (!nullToAbsent || serverId != null) { + map['server_id'] = Variable(serverId); + } + if (!nullToAbsent || productVariantId != null) { + map['product_variant_id'] = Variable(productVariantId); + } + if (!nullToAbsent || rawMaterialId != null) { + map['raw_material_id'] = Variable(rawMaterialId); + } + map['quantity'] = Variable(quantity); + map['sync_status'] = Variable(syncStatus); + map['created_at'] = Variable(createdAt); + map['updated_at'] = Variable(updatedAt); + if (!nullToAbsent || deletedAt != null) { + map['deleted_at'] = Variable(deletedAt); + } + return map; + } + + ReceiptItemsCompanion toCompanion(bool nullToAbsent) { + return ReceiptItemsCompanion( + localId: Value(localId), + serverId: serverId == null && nullToAbsent + ? const Value.absent() + : Value(serverId), + productVariantId: productVariantId == null && nullToAbsent + ? const Value.absent() + : Value(productVariantId), + rawMaterialId: rawMaterialId == null && nullToAbsent + ? const Value.absent() + : Value(rawMaterialId), + quantity: Value(quantity), + syncStatus: Value(syncStatus), + createdAt: Value(createdAt), + updatedAt: Value(updatedAt), + deletedAt: deletedAt == null && nullToAbsent + ? const Value.absent() + : Value(deletedAt), + ); + } + + factory ReceiptItem.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return ReceiptItem( + localId: serializer.fromJson(json['localId']), + serverId: serializer.fromJson(json['serverId']), + productVariantId: serializer.fromJson(json['productVariantId']), + rawMaterialId: serializer.fromJson(json['rawMaterialId']), + quantity: serializer.fromJson(json['quantity']), + syncStatus: serializer.fromJson(json['syncStatus']), + createdAt: serializer.fromJson(json['createdAt']), + updatedAt: serializer.fromJson(json['updatedAt']), + deletedAt: serializer.fromJson(json['deletedAt']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'localId': serializer.toJson(localId), + 'serverId': serializer.toJson(serverId), + 'productVariantId': serializer.toJson(productVariantId), + 'rawMaterialId': serializer.toJson(rawMaterialId), + 'quantity': serializer.toJson(quantity), + 'syncStatus': serializer.toJson(syncStatus), + 'createdAt': serializer.toJson(createdAt), + 'updatedAt': serializer.toJson(updatedAt), + 'deletedAt': serializer.toJson(deletedAt), + }; + } + + ReceiptItem copyWith({ + String? localId, + Value serverId = const Value.absent(), + Value productVariantId = const Value.absent(), + Value rawMaterialId = const Value.absent(), + double? quantity, + String? syncStatus, + DateTime? createdAt, + DateTime? updatedAt, + Value deletedAt = const Value.absent(), + }) => ReceiptItem( + localId: localId ?? this.localId, + serverId: serverId.present ? serverId.value : this.serverId, + productVariantId: productVariantId.present + ? productVariantId.value + : this.productVariantId, + rawMaterialId: rawMaterialId.present + ? rawMaterialId.value + : this.rawMaterialId, + quantity: quantity ?? this.quantity, + syncStatus: syncStatus ?? this.syncStatus, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + deletedAt: deletedAt.present ? deletedAt.value : this.deletedAt, + ); + ReceiptItem copyWithCompanion(ReceiptItemsCompanion data) { + return ReceiptItem( + localId: data.localId.present ? data.localId.value : this.localId, + serverId: data.serverId.present ? data.serverId.value : this.serverId, + productVariantId: data.productVariantId.present + ? data.productVariantId.value + : this.productVariantId, + rawMaterialId: data.rawMaterialId.present + ? data.rawMaterialId.value + : this.rawMaterialId, + quantity: data.quantity.present ? data.quantity.value : this.quantity, + syncStatus: data.syncStatus.present + ? data.syncStatus.value + : this.syncStatus, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, + deletedAt: data.deletedAt.present ? data.deletedAt.value : this.deletedAt, + ); + } + + @override + String toString() { + return (StringBuffer('ReceiptItem(') + ..write('localId: $localId, ') + ..write('serverId: $serverId, ') + ..write('productVariantId: $productVariantId, ') + ..write('rawMaterialId: $rawMaterialId, ') + ..write('quantity: $quantity, ') + ..write('syncStatus: $syncStatus, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('deletedAt: $deletedAt') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + localId, + serverId, + productVariantId, + rawMaterialId, + quantity, + syncStatus, + createdAt, + updatedAt, + deletedAt, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is ReceiptItem && + other.localId == this.localId && + other.serverId == this.serverId && + other.productVariantId == this.productVariantId && + other.rawMaterialId == this.rawMaterialId && + other.quantity == this.quantity && + other.syncStatus == this.syncStatus && + other.createdAt == this.createdAt && + other.updatedAt == this.updatedAt && + other.deletedAt == this.deletedAt); +} + +class ReceiptItemsCompanion extends UpdateCompanion { + final Value localId; + final Value serverId; + final Value productVariantId; + final Value rawMaterialId; + final Value quantity; + final Value syncStatus; + final Value createdAt; + final Value updatedAt; + final Value deletedAt; + final Value rowid; + const ReceiptItemsCompanion({ + this.localId = const Value.absent(), + this.serverId = const Value.absent(), + this.productVariantId = const Value.absent(), + this.rawMaterialId = const Value.absent(), + this.quantity = const Value.absent(), + this.syncStatus = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.deletedAt = const Value.absent(), + this.rowid = const Value.absent(), + }); + ReceiptItemsCompanion.insert({ + this.localId = const Value.absent(), + this.serverId = const Value.absent(), + this.productVariantId = const Value.absent(), + this.rawMaterialId = const Value.absent(), + this.quantity = const Value.absent(), + this.syncStatus = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.deletedAt = const Value.absent(), + this.rowid = const Value.absent(), + }); + static Insertable custom({ + Expression? localId, + Expression? serverId, + Expression? productVariantId, + Expression? rawMaterialId, + Expression? quantity, + Expression? syncStatus, + Expression? createdAt, + Expression? updatedAt, + Expression? deletedAt, + Expression? rowid, + }) { + return RawValuesInsertable({ + if (localId != null) 'local_id': localId, + if (serverId != null) 'server_id': serverId, + if (productVariantId != null) 'product_variant_id': productVariantId, + if (rawMaterialId != null) 'raw_material_id': rawMaterialId, + if (quantity != null) 'quantity': quantity, + if (syncStatus != null) 'sync_status': syncStatus, + if (createdAt != null) 'created_at': createdAt, + if (updatedAt != null) 'updated_at': updatedAt, + if (deletedAt != null) 'deleted_at': deletedAt, + if (rowid != null) 'rowid': rowid, + }); + } + + ReceiptItemsCompanion copyWith({ + Value? localId, + Value? serverId, + Value? productVariantId, + Value? rawMaterialId, + Value? quantity, + Value? syncStatus, + Value? createdAt, + Value? updatedAt, + Value? deletedAt, + Value? rowid, + }) { + return ReceiptItemsCompanion( + localId: localId ?? this.localId, + serverId: serverId ?? this.serverId, + productVariantId: productVariantId ?? this.productVariantId, + rawMaterialId: rawMaterialId ?? this.rawMaterialId, + quantity: quantity ?? this.quantity, + syncStatus: syncStatus ?? this.syncStatus, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + deletedAt: deletedAt ?? this.deletedAt, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (localId.present) { + map['local_id'] = Variable(localId.value); + } + if (serverId.present) { + map['server_id'] = Variable(serverId.value); + } + if (productVariantId.present) { + map['product_variant_id'] = Variable(productVariantId.value); + } + if (rawMaterialId.present) { + map['raw_material_id'] = Variable(rawMaterialId.value); + } + if (quantity.present) { + map['quantity'] = Variable(quantity.value); + } + if (syncStatus.present) { + map['sync_status'] = Variable(syncStatus.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (updatedAt.present) { + map['updated_at'] = Variable(updatedAt.value); + } + if (deletedAt.present) { + map['deleted_at'] = Variable(deletedAt.value); + } + if (rowid.present) { + map['rowid'] = Variable(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('ReceiptItemsCompanion(') + ..write('localId: $localId, ') + ..write('serverId: $serverId, ') + ..write('productVariantId: $productVariantId, ') + ..write('rawMaterialId: $rawMaterialId, ') + ..write('quantity: $quantity, ') + ..write('syncStatus: $syncStatus, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('deletedAt: $deletedAt, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + +class $StockCardsTable extends StockCards + with TableInfo<$StockCardsTable, StockCard> { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + $StockCardsTable(this.attachedDatabase, [this._alias]); + static const VerificationMeta _localIdMeta = const VerificationMeta( + 'localId', + ); + @override + late final GeneratedColumn localId = GeneratedColumn( + 'local_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: false, + clientDefault: () => const Uuid().v7().toString(), + ); + static const VerificationMeta _serverIdMeta = const VerificationMeta( + 'serverId', + ); + @override + late final GeneratedColumn serverId = GeneratedColumn( + 'server_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _outletInventoryIdMeta = const VerificationMeta( + 'outletInventoryId', + ); + @override + late final GeneratedColumn outletInventoryId = + GeneratedColumn( + 'outlet_inventory_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES outlet_inventories (local_id)', + ), + ); + static const VerificationMeta _userIdMeta = const VerificationMeta('userId'); + @override + late final GeneratedColumn userId = GeneratedColumn( + 'user_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES users (local_id)', + ), + ); + static const VerificationMeta _typeMeta = const VerificationMeta('type'); + @override + late final GeneratedColumn type = GeneratedColumn( + 'type', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + static const VerificationMeta _receiveFromIdMeta = const VerificationMeta( + 'receiveFromId', + ); + @override + late final GeneratedColumn receiveFromId = GeneratedColumn( + 'receive_from_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES outlets (local_id)', + ), + ); + static const VerificationMeta _qtyChangeMeta = const VerificationMeta( + 'qtyChange', + ); + @override + late final GeneratedColumn qtyChange = GeneratedColumn( + 'qty_change', + aliasedName, + false, + type: DriftSqlType.double, + requiredDuringInsert: false, + defaultValue: const Constant(0.0), + ); + static const VerificationMeta _currentStockMeta = const VerificationMeta( + 'currentStock', + ); + @override + late final GeneratedColumn currentStock = GeneratedColumn( + 'current_stock', + aliasedName, + false, + type: DriftSqlType.double, + requiredDuringInsert: false, + defaultValue: const Constant(0.0), + ); + static const VerificationMeta _stockTypeMeta = const VerificationMeta( + 'stockType', + ); + @override + late final GeneratedColumn stockType = GeneratedColumn( + 'stock_type', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultValue: const Constant("fixed"), + ); + static const VerificationMeta _modeChangeMeta = const VerificationMeta( + 'modeChange', + ); + @override + late final GeneratedColumn modeChange = GeneratedColumn( + 'mode_change', + aliasedName, + false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("mode_change" IN (0, 1))', + ), + defaultValue: const Constant(false), + ); + static const VerificationMeta _modeBeforeChangeMeta = const VerificationMeta( + 'modeBeforeChange', + ); + @override + late final GeneratedColumn modeBeforeChange = GeneratedColumn( + 'mode_before_change', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _modeAfterChangeMeta = const VerificationMeta( + 'modeAfterChange', + ); + @override + late final GeneratedColumn modeAfterChange = GeneratedColumn( + 'mode_after_change', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _costPriceMeta = const VerificationMeta( + 'costPrice', + ); + @override + late final GeneratedColumn costPrice = GeneratedColumn( + 'cost_price', + aliasedName, + false, + type: DriftSqlType.double, + requiredDuringInsert: false, + defaultValue: const Constant(0.0), + ); + static const VerificationMeta _referenceIdMeta = const VerificationMeta( + 'referenceId', + ); + @override + late final GeneratedColumn referenceId = GeneratedColumn( + 'reference_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _documentNumberMeta = const VerificationMeta( + 'documentNumber', + ); + @override + late final GeneratedColumn documentNumber = GeneratedColumn( + 'document_number', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _notesMeta = const VerificationMeta('notes'); + @override + late final GeneratedColumn notes = GeneratedColumn( + 'notes', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _syncStatusMeta = const VerificationMeta( + 'syncStatus', + ); + @override + late final GeneratedColumn syncStatus = GeneratedColumn( + 'sync_status', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultValue: const Constant("dirty"), + ); + static const VerificationMeta _createdAtMeta = const VerificationMeta( + 'createdAt', + ); + @override + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: currentDateAndTime, + ); + @override + List get $columns => [ + localId, + serverId, + outletInventoryId, + userId, + type, + receiveFromId, + qtyChange, + currentStock, + stockType, + modeChange, + modeBeforeChange, + modeAfterChange, + costPrice, + referenceId, + documentNumber, + notes, + syncStatus, + createdAt, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'stock_cards'; + @override + VerificationContext validateIntegrity( + Insertable instance, { + bool isInserting = false, + }) { + final context = VerificationContext(); + final data = instance.toColumns(true); + if (data.containsKey('local_id')) { + context.handle( + _localIdMeta, + localId.isAcceptableOrUnknown(data['local_id']!, _localIdMeta), + ); + } + if (data.containsKey('server_id')) { + context.handle( + _serverIdMeta, + serverId.isAcceptableOrUnknown(data['server_id']!, _serverIdMeta), + ); + } + if (data.containsKey('outlet_inventory_id')) { + context.handle( + _outletInventoryIdMeta, + outletInventoryId.isAcceptableOrUnknown( + data['outlet_inventory_id']!, + _outletInventoryIdMeta, + ), + ); + } else if (isInserting) { + context.missing(_outletInventoryIdMeta); + } + if (data.containsKey('user_id')) { + context.handle( + _userIdMeta, + userId.isAcceptableOrUnknown(data['user_id']!, _userIdMeta), + ); + } else if (isInserting) { + context.missing(_userIdMeta); + } + if (data.containsKey('type')) { + context.handle( + _typeMeta, + type.isAcceptableOrUnknown(data['type']!, _typeMeta), + ); + } else if (isInserting) { + context.missing(_typeMeta); + } + if (data.containsKey('receive_from_id')) { + context.handle( + _receiveFromIdMeta, + receiveFromId.isAcceptableOrUnknown( + data['receive_from_id']!, + _receiveFromIdMeta, + ), + ); + } + if (data.containsKey('qty_change')) { + context.handle( + _qtyChangeMeta, + qtyChange.isAcceptableOrUnknown(data['qty_change']!, _qtyChangeMeta), + ); + } + if (data.containsKey('current_stock')) { + context.handle( + _currentStockMeta, + currentStock.isAcceptableOrUnknown( + data['current_stock']!, + _currentStockMeta, + ), + ); + } + if (data.containsKey('stock_type')) { + context.handle( + _stockTypeMeta, + stockType.isAcceptableOrUnknown(data['stock_type']!, _stockTypeMeta), + ); + } + if (data.containsKey('mode_change')) { + context.handle( + _modeChangeMeta, + modeChange.isAcceptableOrUnknown(data['mode_change']!, _modeChangeMeta), + ); + } + if (data.containsKey('mode_before_change')) { + context.handle( + _modeBeforeChangeMeta, + modeBeforeChange.isAcceptableOrUnknown( + data['mode_before_change']!, + _modeBeforeChangeMeta, + ), + ); + } + if (data.containsKey('mode_after_change')) { + context.handle( + _modeAfterChangeMeta, + modeAfterChange.isAcceptableOrUnknown( + data['mode_after_change']!, + _modeAfterChangeMeta, + ), + ); + } + if (data.containsKey('cost_price')) { + context.handle( + _costPriceMeta, + costPrice.isAcceptableOrUnknown(data['cost_price']!, _costPriceMeta), + ); + } + if (data.containsKey('reference_id')) { + context.handle( + _referenceIdMeta, + referenceId.isAcceptableOrUnknown( + data['reference_id']!, + _referenceIdMeta, + ), + ); + } + if (data.containsKey('document_number')) { + context.handle( + _documentNumberMeta, + documentNumber.isAcceptableOrUnknown( + data['document_number']!, + _documentNumberMeta, + ), + ); + } + if (data.containsKey('notes')) { + context.handle( + _notesMeta, + notes.isAcceptableOrUnknown(data['notes']!, _notesMeta), + ); + } + if (data.containsKey('sync_status')) { + context.handle( + _syncStatusMeta, + syncStatus.isAcceptableOrUnknown(data['sync_status']!, _syncStatusMeta), + ); + } + if (data.containsKey('created_at')) { + context.handle( + _createdAtMeta, + createdAt.isAcceptableOrUnknown(data['created_at']!, _createdAtMeta), + ); + } + return context; + } + + @override + Set get $primaryKey => {localId}; + @override + StockCard map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return StockCard( + localId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}local_id'], + )!, + serverId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}server_id'], + ), + outletInventoryId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}outlet_inventory_id'], + )!, + userId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}user_id'], + )!, + type: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}type'], + )!, + receiveFromId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}receive_from_id'], + ), + qtyChange: attachedDatabase.typeMapping.read( + DriftSqlType.double, + data['${effectivePrefix}qty_change'], + )!, + currentStock: attachedDatabase.typeMapping.read( + DriftSqlType.double, + data['${effectivePrefix}current_stock'], + )!, + stockType: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}stock_type'], + )!, + modeChange: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}mode_change'], + )!, + modeBeforeChange: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}mode_before_change'], + ), + modeAfterChange: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}mode_after_change'], + ), + costPrice: attachedDatabase.typeMapping.read( + DriftSqlType.double, + data['${effectivePrefix}cost_price'], + )!, + referenceId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}reference_id'], + ), + documentNumber: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}document_number'], + ), + notes: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}notes'], + ), + syncStatus: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}sync_status'], + )!, + createdAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}created_at'], + )!, + ); + } + + @override + $StockCardsTable createAlias(String alias) { + return $StockCardsTable(attachedDatabase, alias); + } +} + +class StockCard extends DataClass implements Insertable { + final String localId; + final String? serverId; + final String outletInventoryId; + final String userId; + final String type; + final String? receiveFromId; + final double qtyChange; + final double currentStock; + final String stockType; + final bool modeChange; + final String? modeBeforeChange; + final String? modeAfterChange; + final double costPrice; + final String? referenceId; + final String? documentNumber; + final String? notes; + final String syncStatus; + final DateTime createdAt; + const StockCard({ + required this.localId, + this.serverId, + required this.outletInventoryId, + required this.userId, + required this.type, + this.receiveFromId, + required this.qtyChange, + required this.currentStock, + required this.stockType, + required this.modeChange, + this.modeBeforeChange, + this.modeAfterChange, + required this.costPrice, + this.referenceId, + this.documentNumber, + this.notes, + required this.syncStatus, + required this.createdAt, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['local_id'] = Variable(localId); + if (!nullToAbsent || serverId != null) { + map['server_id'] = Variable(serverId); + } + map['outlet_inventory_id'] = Variable(outletInventoryId); + map['user_id'] = Variable(userId); + map['type'] = Variable(type); + if (!nullToAbsent || receiveFromId != null) { + map['receive_from_id'] = Variable(receiveFromId); + } + map['qty_change'] = Variable(qtyChange); + map['current_stock'] = Variable(currentStock); + map['stock_type'] = Variable(stockType); + map['mode_change'] = Variable(modeChange); + if (!nullToAbsent || modeBeforeChange != null) { + map['mode_before_change'] = Variable(modeBeforeChange); + } + if (!nullToAbsent || modeAfterChange != null) { + map['mode_after_change'] = Variable(modeAfterChange); + } + map['cost_price'] = Variable(costPrice); + if (!nullToAbsent || referenceId != null) { + map['reference_id'] = Variable(referenceId); + } + if (!nullToAbsent || documentNumber != null) { + map['document_number'] = Variable(documentNumber); + } + if (!nullToAbsent || notes != null) { + map['notes'] = Variable(notes); + } + map['sync_status'] = Variable(syncStatus); + map['created_at'] = Variable(createdAt); + return map; + } + + StockCardsCompanion toCompanion(bool nullToAbsent) { + return StockCardsCompanion( + localId: Value(localId), + serverId: serverId == null && nullToAbsent + ? const Value.absent() + : Value(serverId), + outletInventoryId: Value(outletInventoryId), + userId: Value(userId), + type: Value(type), + receiveFromId: receiveFromId == null && nullToAbsent + ? const Value.absent() + : Value(receiveFromId), + qtyChange: Value(qtyChange), + currentStock: Value(currentStock), + stockType: Value(stockType), + modeChange: Value(modeChange), + modeBeforeChange: modeBeforeChange == null && nullToAbsent + ? const Value.absent() + : Value(modeBeforeChange), + modeAfterChange: modeAfterChange == null && nullToAbsent + ? const Value.absent() + : Value(modeAfterChange), + costPrice: Value(costPrice), + referenceId: referenceId == null && nullToAbsent + ? const Value.absent() + : Value(referenceId), + documentNumber: documentNumber == null && nullToAbsent + ? const Value.absent() + : Value(documentNumber), + notes: notes == null && nullToAbsent + ? const Value.absent() + : Value(notes), + syncStatus: Value(syncStatus), + createdAt: Value(createdAt), + ); + } + + factory StockCard.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return StockCard( + localId: serializer.fromJson(json['localId']), + serverId: serializer.fromJson(json['serverId']), + outletInventoryId: serializer.fromJson(json['outletInventoryId']), + userId: serializer.fromJson(json['userId']), + type: serializer.fromJson(json['type']), + receiveFromId: serializer.fromJson(json['receiveFromId']), + qtyChange: serializer.fromJson(json['qtyChange']), + currentStock: serializer.fromJson(json['currentStock']), + stockType: serializer.fromJson(json['stockType']), + modeChange: serializer.fromJson(json['modeChange']), + modeBeforeChange: serializer.fromJson(json['modeBeforeChange']), + modeAfterChange: serializer.fromJson(json['modeAfterChange']), + costPrice: serializer.fromJson(json['costPrice']), + referenceId: serializer.fromJson(json['referenceId']), + documentNumber: serializer.fromJson(json['documentNumber']), + notes: serializer.fromJson(json['notes']), + syncStatus: serializer.fromJson(json['syncStatus']), + createdAt: serializer.fromJson(json['createdAt']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'localId': serializer.toJson(localId), + 'serverId': serializer.toJson(serverId), + 'outletInventoryId': serializer.toJson(outletInventoryId), + 'userId': serializer.toJson(userId), + 'type': serializer.toJson(type), + 'receiveFromId': serializer.toJson(receiveFromId), + 'qtyChange': serializer.toJson(qtyChange), + 'currentStock': serializer.toJson(currentStock), + 'stockType': serializer.toJson(stockType), + 'modeChange': serializer.toJson(modeChange), + 'modeBeforeChange': serializer.toJson(modeBeforeChange), + 'modeAfterChange': serializer.toJson(modeAfterChange), + 'costPrice': serializer.toJson(costPrice), + 'referenceId': serializer.toJson(referenceId), + 'documentNumber': serializer.toJson(documentNumber), + 'notes': serializer.toJson(notes), + 'syncStatus': serializer.toJson(syncStatus), + 'createdAt': serializer.toJson(createdAt), + }; + } + + StockCard copyWith({ + String? localId, + Value serverId = const Value.absent(), + String? outletInventoryId, + String? userId, + String? type, + Value receiveFromId = const Value.absent(), + double? qtyChange, + double? currentStock, + String? stockType, + bool? modeChange, + Value modeBeforeChange = const Value.absent(), + Value modeAfterChange = const Value.absent(), + double? costPrice, + Value referenceId = const Value.absent(), + Value documentNumber = const Value.absent(), + Value notes = const Value.absent(), + String? syncStatus, + DateTime? createdAt, + }) => StockCard( + localId: localId ?? this.localId, + serverId: serverId.present ? serverId.value : this.serverId, + outletInventoryId: outletInventoryId ?? this.outletInventoryId, + userId: userId ?? this.userId, + type: type ?? this.type, + receiveFromId: receiveFromId.present + ? receiveFromId.value + : this.receiveFromId, + qtyChange: qtyChange ?? this.qtyChange, + currentStock: currentStock ?? this.currentStock, + stockType: stockType ?? this.stockType, + modeChange: modeChange ?? this.modeChange, + modeBeforeChange: modeBeforeChange.present + ? modeBeforeChange.value + : this.modeBeforeChange, + modeAfterChange: modeAfterChange.present + ? modeAfterChange.value + : this.modeAfterChange, + costPrice: costPrice ?? this.costPrice, + referenceId: referenceId.present ? referenceId.value : this.referenceId, + documentNumber: documentNumber.present + ? documentNumber.value + : this.documentNumber, + notes: notes.present ? notes.value : this.notes, + syncStatus: syncStatus ?? this.syncStatus, + createdAt: createdAt ?? this.createdAt, + ); + StockCard copyWithCompanion(StockCardsCompanion data) { + return StockCard( + localId: data.localId.present ? data.localId.value : this.localId, + serverId: data.serverId.present ? data.serverId.value : this.serverId, + outletInventoryId: data.outletInventoryId.present + ? data.outletInventoryId.value + : this.outletInventoryId, + userId: data.userId.present ? data.userId.value : this.userId, + type: data.type.present ? data.type.value : this.type, + receiveFromId: data.receiveFromId.present + ? data.receiveFromId.value + : this.receiveFromId, + qtyChange: data.qtyChange.present ? data.qtyChange.value : this.qtyChange, + currentStock: data.currentStock.present + ? data.currentStock.value + : this.currentStock, + stockType: data.stockType.present ? data.stockType.value : this.stockType, + modeChange: data.modeChange.present + ? data.modeChange.value + : this.modeChange, + modeBeforeChange: data.modeBeforeChange.present + ? data.modeBeforeChange.value + : this.modeBeforeChange, + modeAfterChange: data.modeAfterChange.present + ? data.modeAfterChange.value + : this.modeAfterChange, + costPrice: data.costPrice.present ? data.costPrice.value : this.costPrice, + referenceId: data.referenceId.present + ? data.referenceId.value + : this.referenceId, + documentNumber: data.documentNumber.present + ? data.documentNumber.value + : this.documentNumber, + notes: data.notes.present ? data.notes.value : this.notes, + syncStatus: data.syncStatus.present + ? data.syncStatus.value + : this.syncStatus, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + ); + } + + @override + String toString() { + return (StringBuffer('StockCard(') + ..write('localId: $localId, ') + ..write('serverId: $serverId, ') + ..write('outletInventoryId: $outletInventoryId, ') + ..write('userId: $userId, ') + ..write('type: $type, ') + ..write('receiveFromId: $receiveFromId, ') + ..write('qtyChange: $qtyChange, ') + ..write('currentStock: $currentStock, ') + ..write('stockType: $stockType, ') + ..write('modeChange: $modeChange, ') + ..write('modeBeforeChange: $modeBeforeChange, ') + ..write('modeAfterChange: $modeAfterChange, ') + ..write('costPrice: $costPrice, ') + ..write('referenceId: $referenceId, ') + ..write('documentNumber: $documentNumber, ') + ..write('notes: $notes, ') + ..write('syncStatus: $syncStatus, ') + ..write('createdAt: $createdAt') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + localId, + serverId, + outletInventoryId, + userId, + type, + receiveFromId, + qtyChange, + currentStock, + stockType, + modeChange, + modeBeforeChange, + modeAfterChange, + costPrice, + referenceId, + documentNumber, + notes, + syncStatus, + createdAt, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is StockCard && + other.localId == this.localId && + other.serverId == this.serverId && + other.outletInventoryId == this.outletInventoryId && + other.userId == this.userId && + other.type == this.type && + other.receiveFromId == this.receiveFromId && + other.qtyChange == this.qtyChange && + other.currentStock == this.currentStock && + other.stockType == this.stockType && + other.modeChange == this.modeChange && + other.modeBeforeChange == this.modeBeforeChange && + other.modeAfterChange == this.modeAfterChange && + other.costPrice == this.costPrice && + other.referenceId == this.referenceId && + other.documentNumber == this.documentNumber && + other.notes == this.notes && + other.syncStatus == this.syncStatus && + other.createdAt == this.createdAt); +} + +class StockCardsCompanion extends UpdateCompanion { + final Value localId; + final Value serverId; + final Value outletInventoryId; + final Value userId; + final Value type; + final Value receiveFromId; + final Value qtyChange; + final Value currentStock; + final Value stockType; + final Value modeChange; + final Value modeBeforeChange; + final Value modeAfterChange; + final Value costPrice; + final Value referenceId; + final Value documentNumber; + final Value notes; + final Value syncStatus; + final Value createdAt; + final Value rowid; + const StockCardsCompanion({ + this.localId = const Value.absent(), + this.serverId = const Value.absent(), + this.outletInventoryId = const Value.absent(), + this.userId = const Value.absent(), + this.type = const Value.absent(), + this.receiveFromId = const Value.absent(), + this.qtyChange = const Value.absent(), + this.currentStock = const Value.absent(), + this.stockType = const Value.absent(), + this.modeChange = const Value.absent(), + this.modeBeforeChange = const Value.absent(), + this.modeAfterChange = const Value.absent(), + this.costPrice = const Value.absent(), + this.referenceId = const Value.absent(), + this.documentNumber = const Value.absent(), + this.notes = const Value.absent(), + this.syncStatus = const Value.absent(), + this.createdAt = const Value.absent(), + this.rowid = const Value.absent(), + }); + StockCardsCompanion.insert({ + this.localId = const Value.absent(), + this.serverId = const Value.absent(), + required String outletInventoryId, + required String userId, + required String type, + this.receiveFromId = const Value.absent(), + this.qtyChange = const Value.absent(), + this.currentStock = const Value.absent(), + this.stockType = const Value.absent(), + this.modeChange = const Value.absent(), + this.modeBeforeChange = const Value.absent(), + this.modeAfterChange = const Value.absent(), + this.costPrice = const Value.absent(), + this.referenceId = const Value.absent(), + this.documentNumber = const Value.absent(), + this.notes = const Value.absent(), + this.syncStatus = const Value.absent(), + this.createdAt = const Value.absent(), + this.rowid = const Value.absent(), + }) : outletInventoryId = Value(outletInventoryId), + userId = Value(userId), + type = Value(type); + static Insertable custom({ + Expression? localId, + Expression? serverId, + Expression? outletInventoryId, + Expression? userId, + Expression? type, + Expression? receiveFromId, + Expression? qtyChange, + Expression? currentStock, + Expression? stockType, + Expression? modeChange, + Expression? modeBeforeChange, + Expression? modeAfterChange, + Expression? costPrice, + Expression? referenceId, + Expression? documentNumber, + Expression? notes, + Expression? syncStatus, + Expression? createdAt, + Expression? rowid, + }) { + return RawValuesInsertable({ + if (localId != null) 'local_id': localId, + if (serverId != null) 'server_id': serverId, + if (outletInventoryId != null) 'outlet_inventory_id': outletInventoryId, + if (userId != null) 'user_id': userId, + if (type != null) 'type': type, + if (receiveFromId != null) 'receive_from_id': receiveFromId, + if (qtyChange != null) 'qty_change': qtyChange, + if (currentStock != null) 'current_stock': currentStock, + if (stockType != null) 'stock_type': stockType, + if (modeChange != null) 'mode_change': modeChange, + if (modeBeforeChange != null) 'mode_before_change': modeBeforeChange, + if (modeAfterChange != null) 'mode_after_change': modeAfterChange, + if (costPrice != null) 'cost_price': costPrice, + if (referenceId != null) 'reference_id': referenceId, + if (documentNumber != null) 'document_number': documentNumber, + if (notes != null) 'notes': notes, + if (syncStatus != null) 'sync_status': syncStatus, + if (createdAt != null) 'created_at': createdAt, + if (rowid != null) 'rowid': rowid, + }); + } + + StockCardsCompanion copyWith({ + Value? localId, + Value? serverId, + Value? outletInventoryId, + Value? userId, + Value? type, + Value? receiveFromId, + Value? qtyChange, + Value? currentStock, + Value? stockType, + Value? modeChange, + Value? modeBeforeChange, + Value? modeAfterChange, + Value? costPrice, + Value? referenceId, + Value? documentNumber, + Value? notes, + Value? syncStatus, + Value? createdAt, + Value? rowid, + }) { + return StockCardsCompanion( + localId: localId ?? this.localId, + serverId: serverId ?? this.serverId, + outletInventoryId: outletInventoryId ?? this.outletInventoryId, + userId: userId ?? this.userId, + type: type ?? this.type, + receiveFromId: receiveFromId ?? this.receiveFromId, + qtyChange: qtyChange ?? this.qtyChange, + currentStock: currentStock ?? this.currentStock, + stockType: stockType ?? this.stockType, + modeChange: modeChange ?? this.modeChange, + modeBeforeChange: modeBeforeChange ?? this.modeBeforeChange, + modeAfterChange: modeAfterChange ?? this.modeAfterChange, + costPrice: costPrice ?? this.costPrice, + referenceId: referenceId ?? this.referenceId, + documentNumber: documentNumber ?? this.documentNumber, + notes: notes ?? this.notes, + syncStatus: syncStatus ?? this.syncStatus, + createdAt: createdAt ?? this.createdAt, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (localId.present) { + map['local_id'] = Variable(localId.value); + } + if (serverId.present) { + map['server_id'] = Variable(serverId.value); + } + if (outletInventoryId.present) { + map['outlet_inventory_id'] = Variable(outletInventoryId.value); + } + if (userId.present) { + map['user_id'] = Variable(userId.value); + } + if (type.present) { + map['type'] = Variable(type.value); + } + if (receiveFromId.present) { + map['receive_from_id'] = Variable(receiveFromId.value); + } + if (qtyChange.present) { + map['qty_change'] = Variable(qtyChange.value); + } + if (currentStock.present) { + map['current_stock'] = Variable(currentStock.value); + } + if (stockType.present) { + map['stock_type'] = Variable(stockType.value); + } + if (modeChange.present) { + map['mode_change'] = Variable(modeChange.value); + } + if (modeBeforeChange.present) { + map['mode_before_change'] = Variable(modeBeforeChange.value); + } + if (modeAfterChange.present) { + map['mode_after_change'] = Variable(modeAfterChange.value); + } + if (costPrice.present) { + map['cost_price'] = Variable(costPrice.value); + } + if (referenceId.present) { + map['reference_id'] = Variable(referenceId.value); + } + if (documentNumber.present) { + map['document_number'] = Variable(documentNumber.value); + } + if (notes.present) { + map['notes'] = Variable(notes.value); + } + if (syncStatus.present) { + map['sync_status'] = Variable(syncStatus.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (rowid.present) { + map['rowid'] = Variable(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('StockCardsCompanion(') + ..write('localId: $localId, ') + ..write('serverId: $serverId, ') + ..write('outletInventoryId: $outletInventoryId, ') + ..write('userId: $userId, ') + ..write('type: $type, ') + ..write('receiveFromId: $receiveFromId, ') + ..write('qtyChange: $qtyChange, ') + ..write('currentStock: $currentStock, ') + ..write('stockType: $stockType, ') + ..write('modeChange: $modeChange, ') + ..write('modeBeforeChange: $modeBeforeChange, ') + ..write('modeAfterChange: $modeAfterChange, ') + ..write('costPrice: $costPrice, ') + ..write('referenceId: $referenceId, ') + ..write('documentNumber: $documentNumber, ') + ..write('notes: $notes, ') + ..write('syncStatus: $syncStatus, ') + ..write('createdAt: $createdAt, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + +class $OrdersTable extends Orders with TableInfo<$OrdersTable, Order> { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + $OrdersTable(this.attachedDatabase, [this._alias]); + static const VerificationMeta _localIdMeta = const VerificationMeta( + 'localId', + ); + @override + late final GeneratedColumn localId = GeneratedColumn( + 'local_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: false, + clientDefault: () => const Uuid().v7().toString(), + ); + static const VerificationMeta _serverIdMeta = const VerificationMeta( + 'serverId', + ); + @override + late final GeneratedColumn serverId = GeneratedColumn( + 'server_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _orderNumberMeta = const VerificationMeta( + 'orderNumber', + ); + @override + late final GeneratedColumn orderNumber = GeneratedColumn( + 'order_number', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways('UNIQUE'), + ); + static const VerificationMeta _outletIdMeta = const VerificationMeta( + 'outletId', + ); + @override + late final GeneratedColumn outletId = GeneratedColumn( + 'outlet_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES outlets (local_id)', + ), + ); + static const VerificationMeta _outletNameSnapshotMeta = + const VerificationMeta('outletNameSnapshot'); + @override + late final GeneratedColumn outletNameSnapshot = + GeneratedColumn( + 'outlet_name_snapshot', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + static const VerificationMeta _customerIdMeta = const VerificationMeta( + 'customerId', + ); + @override + late final GeneratedColumn customerId = GeneratedColumn( + 'customer_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES customers (local_id)', + ), + ); + static const VerificationMeta _customerNameSnapshotMeta = + const VerificationMeta('customerNameSnapshot'); + @override + late final GeneratedColumn customerNameSnapshot = + GeneratedColumn( + 'customer_name_snapshot', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _customerEmailSnapshotMeta = + const VerificationMeta('customerEmailSnapshot'); + @override + late final GeneratedColumn customerEmailSnapshot = + GeneratedColumn( + 'customer_email_snapshot', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _customerPhoneNumberSnapshotMeta = + const VerificationMeta('customerPhoneNumberSnapshot'); + @override + late final GeneratedColumn customerPhoneNumberSnapshot = + GeneratedColumn( + 'customer_phone_number_snapshot', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _userIdMeta = const VerificationMeta('userId'); + @override + late final GeneratedColumn userId = GeneratedColumn( + 'user_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES users (local_id)', + ), + ); + static const VerificationMeta _adminNameSnapshotMeta = const VerificationMeta( + 'adminNameSnapshot', + ); + @override + late final GeneratedColumn adminNameSnapshot = + GeneratedColumn( + 'admin_name_snapshot', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + static const VerificationMeta _sourceMeta = const VerificationMeta('source'); + @override + late final GeneratedColumn source = GeneratedColumn( + 'source', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + static const VerificationMeta _alreadyReadMeta = const VerificationMeta( + 'alreadyRead', + ); + @override + late final GeneratedColumn alreadyRead = GeneratedColumn( + 'already_read', + aliasedName, + true, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("already_read" IN (0, 1))', + ), + ); + static const VerificationMeta _orderStatusMeta = const VerificationMeta( + 'orderStatus', + ); + @override + late final GeneratedColumn orderStatus = GeneratedColumn( + 'order_status', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + static const VerificationMeta _isCancellationMeta = const VerificationMeta( + 'isCancellation', + ); + @override + late final GeneratedColumn isCancellation = GeneratedColumn( + 'is_cancellation', + aliasedName, + true, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("is_cancellation" IN (0, 1))', + ), + ); + static const VerificationMeta _cancellationAcceptedMeta = + const VerificationMeta('cancellationAccepted'); + @override + late final GeneratedColumn cancellationAccepted = GeneratedColumn( + 'cancellation_accepted', + aliasedName, + true, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("cancellation_accepted" IN (0, 1))', + ), + ); + static const VerificationMeta _approvalProccessMeta = const VerificationMeta( + 'approvalProccess', + ); + @override + late final GeneratedColumn approvalProccess = GeneratedColumn( + 'approval_proccess', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _canceledByMeta = const VerificationMeta( + 'canceledBy', + ); + @override + late final GeneratedColumn canceledBy = GeneratedColumn( + 'canceled_by', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _deliveryTypeMeta = const VerificationMeta( + 'deliveryType', + ); + @override + late final GeneratedColumn deliveryType = GeneratedColumn( + 'delivery_type', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _deliveryPrefrenceMeta = const VerificationMeta( + 'deliveryPrefrence', + ); + @override + late final GeneratedColumn deliveryPrefrence = + GeneratedColumn( + 'delivery_prefrence', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _shippingCostMeta = const VerificationMeta( + 'shippingCost', + ); + @override + late final GeneratedColumn shippingCost = GeneratedColumn( + 'shipping_cost', + aliasedName, + false, + type: DriftSqlType.double, + requiredDuringInsert: false, + defaultValue: const Constant(0.0), + ); + static const VerificationMeta _totalOrderMeta = const VerificationMeta( + 'totalOrder', + ); + @override + late final GeneratedColumn totalOrder = GeneratedColumn( + 'total_order', + aliasedName, + false, + type: DriftSqlType.double, + requiredDuringInsert: false, + defaultValue: const Constant(0.0), + ); + static const VerificationMeta _discountMeta = const VerificationMeta( + 'discount', + ); + @override + late final GeneratedColumn discount = GeneratedColumn( + 'discount', + aliasedName, + false, + type: DriftSqlType.double, + requiredDuringInsert: false, + defaultValue: const Constant(0.0), + ); + static const VerificationMeta _taxMeta = const VerificationMeta('tax'); + @override + late final GeneratedColumn tax = GeneratedColumn( + 'tax', + aliasedName, + false, + type: DriftSqlType.double, + requiredDuringInsert: false, + defaultValue: const Constant(0.0), + ); + static const VerificationMeta _totalAmountMeta = const VerificationMeta( + 'totalAmount', + ); + @override + late final GeneratedColumn totalAmount = GeneratedColumn( + 'total_amount', + aliasedName, + false, + type: DriftSqlType.double, + requiredDuringInsert: false, + defaultValue: const Constant(0.0), + ); + static const VerificationMeta _amountPaidMeta = const VerificationMeta( + 'amountPaid', + ); + @override + late final GeneratedColumn amountPaid = GeneratedColumn( + 'amount_paid', + aliasedName, + false, + type: DriftSqlType.double, + requiredDuringInsert: false, + defaultValue: const Constant(0.0), + ); + static const VerificationMeta _changeAmountMeta = const VerificationMeta( + 'changeAmount', + ); + @override + late final GeneratedColumn changeAmount = GeneratedColumn( + 'change_amount', + aliasedName, + false, + type: DriftSqlType.double, + requiredDuringInsert: false, + defaultValue: const Constant(0.0), + ); + static const VerificationMeta _notesMeta = const VerificationMeta('notes'); + @override + late final GeneratedColumn notes = GeneratedColumn( + 'notes', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _paymentProofPathMeta = const VerificationMeta( + 'paymentProofPath', + ); + @override + late final GeneratedColumn paymentProofPath = GeneratedColumn( + 'payment_proof_path', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _paymentProofUrlMeta = const VerificationMeta( + 'paymentProofUrl', + ); + @override + late final GeneratedColumn paymentProofUrl = GeneratedColumn( + 'payment_proof_url', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _paymentMethodMeta = const VerificationMeta( + 'paymentMethod', + ); + @override + late final GeneratedColumn paymentMethod = GeneratedColumn( + 'payment_method', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + @override + late final GeneratedColumnWithTypeConverter?, String> + customerAddressSnapshot = + GeneratedColumn( + 'customer_address_snapshot', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ).withConverter?>( + $OrdersTable.$convertercustomerAddressSnapshotn, + ); + static const VerificationMeta _syncStatusMeta = const VerificationMeta( + 'syncStatus', + ); + @override + late final GeneratedColumn syncStatus = GeneratedColumn( + 'sync_status', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultValue: const Constant('dirty'), + ); + static const VerificationMeta _createdAtMeta = const VerificationMeta( + 'createdAt', + ); + @override + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: currentDateAndTime, + ); + static const VerificationMeta _updatedAtMeta = const VerificationMeta( + 'updatedAt', + ); + @override + late final GeneratedColumn updatedAt = GeneratedColumn( + 'updated_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: currentDateAndTime, + ); + @override + List get $columns => [ + localId, + serverId, + orderNumber, + outletId, + outletNameSnapshot, + customerId, + customerNameSnapshot, + customerEmailSnapshot, + customerPhoneNumberSnapshot, + userId, + adminNameSnapshot, + source, + alreadyRead, + orderStatus, + isCancellation, + cancellationAccepted, + approvalProccess, + canceledBy, + deliveryType, + deliveryPrefrence, + shippingCost, + totalOrder, + discount, + tax, + totalAmount, + amountPaid, + changeAmount, + notes, + paymentProofPath, + paymentProofUrl, + paymentMethod, + customerAddressSnapshot, + syncStatus, + createdAt, + updatedAt, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'orders'; + @override + VerificationContext validateIntegrity( + Insertable instance, { + bool isInserting = false, + }) { + final context = VerificationContext(); + final data = instance.toColumns(true); + if (data.containsKey('local_id')) { + context.handle( + _localIdMeta, + localId.isAcceptableOrUnknown(data['local_id']!, _localIdMeta), + ); + } + if (data.containsKey('server_id')) { + context.handle( + _serverIdMeta, + serverId.isAcceptableOrUnknown(data['server_id']!, _serverIdMeta), + ); + } + if (data.containsKey('order_number')) { + context.handle( + _orderNumberMeta, + orderNumber.isAcceptableOrUnknown( + data['order_number']!, + _orderNumberMeta, + ), + ); + } else if (isInserting) { + context.missing(_orderNumberMeta); + } + if (data.containsKey('outlet_id')) { + context.handle( + _outletIdMeta, + outletId.isAcceptableOrUnknown(data['outlet_id']!, _outletIdMeta), + ); + } else if (isInserting) { + context.missing(_outletIdMeta); + } + if (data.containsKey('outlet_name_snapshot')) { + context.handle( + _outletNameSnapshotMeta, + outletNameSnapshot.isAcceptableOrUnknown( + data['outlet_name_snapshot']!, + _outletNameSnapshotMeta, + ), + ); + } else if (isInserting) { + context.missing(_outletNameSnapshotMeta); + } + if (data.containsKey('customer_id')) { + context.handle( + _customerIdMeta, + customerId.isAcceptableOrUnknown(data['customer_id']!, _customerIdMeta), + ); + } + if (data.containsKey('customer_name_snapshot')) { + context.handle( + _customerNameSnapshotMeta, + customerNameSnapshot.isAcceptableOrUnknown( + data['customer_name_snapshot']!, + _customerNameSnapshotMeta, + ), + ); + } + if (data.containsKey('customer_email_snapshot')) { + context.handle( + _customerEmailSnapshotMeta, + customerEmailSnapshot.isAcceptableOrUnknown( + data['customer_email_snapshot']!, + _customerEmailSnapshotMeta, + ), + ); + } + if (data.containsKey('customer_phone_number_snapshot')) { + context.handle( + _customerPhoneNumberSnapshotMeta, + customerPhoneNumberSnapshot.isAcceptableOrUnknown( + data['customer_phone_number_snapshot']!, + _customerPhoneNumberSnapshotMeta, + ), + ); + } + if (data.containsKey('user_id')) { + context.handle( + _userIdMeta, + userId.isAcceptableOrUnknown(data['user_id']!, _userIdMeta), + ); + } else if (isInserting) { + context.missing(_userIdMeta); + } + if (data.containsKey('admin_name_snapshot')) { + context.handle( + _adminNameSnapshotMeta, + adminNameSnapshot.isAcceptableOrUnknown( + data['admin_name_snapshot']!, + _adminNameSnapshotMeta, + ), + ); + } else if (isInserting) { + context.missing(_adminNameSnapshotMeta); + } + if (data.containsKey('source')) { + context.handle( + _sourceMeta, + source.isAcceptableOrUnknown(data['source']!, _sourceMeta), + ); + } else if (isInserting) { + context.missing(_sourceMeta); + } + if (data.containsKey('already_read')) { + context.handle( + _alreadyReadMeta, + alreadyRead.isAcceptableOrUnknown( + data['already_read']!, + _alreadyReadMeta, + ), + ); + } + if (data.containsKey('order_status')) { + context.handle( + _orderStatusMeta, + orderStatus.isAcceptableOrUnknown( + data['order_status']!, + _orderStatusMeta, + ), + ); + } else if (isInserting) { + context.missing(_orderStatusMeta); + } + if (data.containsKey('is_cancellation')) { + context.handle( + _isCancellationMeta, + isCancellation.isAcceptableOrUnknown( + data['is_cancellation']!, + _isCancellationMeta, + ), + ); + } + if (data.containsKey('cancellation_accepted')) { + context.handle( + _cancellationAcceptedMeta, + cancellationAccepted.isAcceptableOrUnknown( + data['cancellation_accepted']!, + _cancellationAcceptedMeta, + ), + ); + } + if (data.containsKey('approval_proccess')) { + context.handle( + _approvalProccessMeta, + approvalProccess.isAcceptableOrUnknown( + data['approval_proccess']!, + _approvalProccessMeta, + ), + ); + } + if (data.containsKey('canceled_by')) { + context.handle( + _canceledByMeta, + canceledBy.isAcceptableOrUnknown(data['canceled_by']!, _canceledByMeta), + ); + } + if (data.containsKey('delivery_type')) { + context.handle( + _deliveryTypeMeta, + deliveryType.isAcceptableOrUnknown( + data['delivery_type']!, + _deliveryTypeMeta, + ), + ); + } + if (data.containsKey('delivery_prefrence')) { + context.handle( + _deliveryPrefrenceMeta, + deliveryPrefrence.isAcceptableOrUnknown( + data['delivery_prefrence']!, + _deliveryPrefrenceMeta, + ), + ); + } + if (data.containsKey('shipping_cost')) { + context.handle( + _shippingCostMeta, + shippingCost.isAcceptableOrUnknown( + data['shipping_cost']!, + _shippingCostMeta, + ), + ); + } + if (data.containsKey('total_order')) { + context.handle( + _totalOrderMeta, + totalOrder.isAcceptableOrUnknown(data['total_order']!, _totalOrderMeta), + ); + } + if (data.containsKey('discount')) { + context.handle( + _discountMeta, + discount.isAcceptableOrUnknown(data['discount']!, _discountMeta), + ); + } + if (data.containsKey('tax')) { + context.handle( + _taxMeta, + tax.isAcceptableOrUnknown(data['tax']!, _taxMeta), + ); + } + if (data.containsKey('total_amount')) { + context.handle( + _totalAmountMeta, + totalAmount.isAcceptableOrUnknown( + data['total_amount']!, + _totalAmountMeta, + ), + ); + } + if (data.containsKey('amount_paid')) { + context.handle( + _amountPaidMeta, + amountPaid.isAcceptableOrUnknown(data['amount_paid']!, _amountPaidMeta), + ); + } + if (data.containsKey('change_amount')) { + context.handle( + _changeAmountMeta, + changeAmount.isAcceptableOrUnknown( + data['change_amount']!, + _changeAmountMeta, + ), + ); + } + if (data.containsKey('notes')) { + context.handle( + _notesMeta, + notes.isAcceptableOrUnknown(data['notes']!, _notesMeta), + ); + } + if (data.containsKey('payment_proof_path')) { + context.handle( + _paymentProofPathMeta, + paymentProofPath.isAcceptableOrUnknown( + data['payment_proof_path']!, + _paymentProofPathMeta, + ), + ); + } + if (data.containsKey('payment_proof_url')) { + context.handle( + _paymentProofUrlMeta, + paymentProofUrl.isAcceptableOrUnknown( + data['payment_proof_url']!, + _paymentProofUrlMeta, + ), + ); + } + if (data.containsKey('payment_method')) { + context.handle( + _paymentMethodMeta, + paymentMethod.isAcceptableOrUnknown( + data['payment_method']!, + _paymentMethodMeta, + ), + ); + } + if (data.containsKey('sync_status')) { + context.handle( + _syncStatusMeta, + syncStatus.isAcceptableOrUnknown(data['sync_status']!, _syncStatusMeta), + ); + } + if (data.containsKey('created_at')) { + context.handle( + _createdAtMeta, + createdAt.isAcceptableOrUnknown(data['created_at']!, _createdAtMeta), + ); + } + if (data.containsKey('updated_at')) { + context.handle( + _updatedAtMeta, + updatedAt.isAcceptableOrUnknown(data['updated_at']!, _updatedAtMeta), + ); + } + return context; + } + + @override + Set get $primaryKey => {localId}; + @override + Order map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return Order( + localId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}local_id'], + )!, + serverId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}server_id'], + ), + orderNumber: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}order_number'], + )!, + outletId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}outlet_id'], + )!, + outletNameSnapshot: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}outlet_name_snapshot'], + )!, + customerId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}customer_id'], + ), + customerNameSnapshot: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}customer_name_snapshot'], + ), + customerEmailSnapshot: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}customer_email_snapshot'], + ), + customerPhoneNumberSnapshot: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}customer_phone_number_snapshot'], + ), + userId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}user_id'], + )!, + adminNameSnapshot: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}admin_name_snapshot'], + )!, + source: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}source'], + )!, + alreadyRead: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}already_read'], + ), + orderStatus: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}order_status'], + )!, + isCancellation: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}is_cancellation'], + ), + cancellationAccepted: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}cancellation_accepted'], + ), + approvalProccess: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}approval_proccess'], + ), + canceledBy: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}canceled_by'], + ), + deliveryType: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}delivery_type'], + ), + deliveryPrefrence: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}delivery_prefrence'], + ), + shippingCost: attachedDatabase.typeMapping.read( + DriftSqlType.double, + data['${effectivePrefix}shipping_cost'], + )!, + totalOrder: attachedDatabase.typeMapping.read( + DriftSqlType.double, + data['${effectivePrefix}total_order'], + )!, + discount: attachedDatabase.typeMapping.read( + DriftSqlType.double, + data['${effectivePrefix}discount'], + )!, + tax: attachedDatabase.typeMapping.read( + DriftSqlType.double, + data['${effectivePrefix}tax'], + )!, + totalAmount: attachedDatabase.typeMapping.read( + DriftSqlType.double, + data['${effectivePrefix}total_amount'], + )!, + amountPaid: attachedDatabase.typeMapping.read( + DriftSqlType.double, + data['${effectivePrefix}amount_paid'], + )!, + changeAmount: attachedDatabase.typeMapping.read( + DriftSqlType.double, + data['${effectivePrefix}change_amount'], + )!, + notes: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}notes'], + ), + paymentProofPath: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}payment_proof_path'], + ), + paymentProofUrl: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}payment_proof_url'], + ), + paymentMethod: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}payment_method'], + ), + customerAddressSnapshot: $OrdersTable.$convertercustomerAddressSnapshotn + .fromSql( + attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}customer_address_snapshot'], + ), + ), + syncStatus: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}sync_status'], + )!, + createdAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}created_at'], + )!, + updatedAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}updated_at'], + )!, + ); + } + + @override + $OrdersTable createAlias(String alias) { + return $OrdersTable(attachedDatabase, alias); + } + + static TypeConverter, String> + $convertercustomerAddressSnapshot = StringMapConverter(); + static TypeConverter?, String?> + $convertercustomerAddressSnapshotn = NullAwareTypeConverter.wrap( + $convertercustomerAddressSnapshot, + ); +} + +class Order extends DataClass implements Insertable { + final String localId; + final String? serverId; + final String orderNumber; + final String outletId; + final String outletNameSnapshot; + final String? customerId; + final String? customerNameSnapshot; + final String? customerEmailSnapshot; + final String? customerPhoneNumberSnapshot; + final String userId; + final String adminNameSnapshot; + final String source; + final bool? alreadyRead; + final String orderStatus; + final bool? isCancellation; + final bool? cancellationAccepted; + final String? approvalProccess; + final String? canceledBy; + final String? deliveryType; + final String? deliveryPrefrence; + final double shippingCost; + final double totalOrder; + final double discount; + final double tax; + final double totalAmount; + final double amountPaid; + final double changeAmount; + final String? notes; + final String? paymentProofPath; + final String? paymentProofUrl; + final String? paymentMethod; + final Map? customerAddressSnapshot; + final String syncStatus; + final DateTime createdAt; + final DateTime updatedAt; + const Order({ + required this.localId, + this.serverId, + required this.orderNumber, + required this.outletId, + required this.outletNameSnapshot, + this.customerId, + this.customerNameSnapshot, + this.customerEmailSnapshot, + this.customerPhoneNumberSnapshot, + required this.userId, + required this.adminNameSnapshot, + required this.source, + this.alreadyRead, + required this.orderStatus, + this.isCancellation, + this.cancellationAccepted, + this.approvalProccess, + this.canceledBy, + this.deliveryType, + this.deliveryPrefrence, + required this.shippingCost, + required this.totalOrder, + required this.discount, + required this.tax, + required this.totalAmount, + required this.amountPaid, + required this.changeAmount, + this.notes, + this.paymentProofPath, + this.paymentProofUrl, + this.paymentMethod, + this.customerAddressSnapshot, + required this.syncStatus, + required this.createdAt, + required this.updatedAt, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['local_id'] = Variable(localId); + if (!nullToAbsent || serverId != null) { + map['server_id'] = Variable(serverId); + } + map['order_number'] = Variable(orderNumber); + map['outlet_id'] = Variable(outletId); + map['outlet_name_snapshot'] = Variable(outletNameSnapshot); + if (!nullToAbsent || customerId != null) { + map['customer_id'] = Variable(customerId); + } + if (!nullToAbsent || customerNameSnapshot != null) { + map['customer_name_snapshot'] = Variable(customerNameSnapshot); + } + if (!nullToAbsent || customerEmailSnapshot != null) { + map['customer_email_snapshot'] = Variable(customerEmailSnapshot); + } + if (!nullToAbsent || customerPhoneNumberSnapshot != null) { + map['customer_phone_number_snapshot'] = Variable( + customerPhoneNumberSnapshot, + ); + } + map['user_id'] = Variable(userId); + map['admin_name_snapshot'] = Variable(adminNameSnapshot); + map['source'] = Variable(source); + if (!nullToAbsent || alreadyRead != null) { + map['already_read'] = Variable(alreadyRead); + } + map['order_status'] = Variable(orderStatus); + if (!nullToAbsent || isCancellation != null) { + map['is_cancellation'] = Variable(isCancellation); + } + if (!nullToAbsent || cancellationAccepted != null) { + map['cancellation_accepted'] = Variable(cancellationAccepted); + } + if (!nullToAbsent || approvalProccess != null) { + map['approval_proccess'] = Variable(approvalProccess); + } + if (!nullToAbsent || canceledBy != null) { + map['canceled_by'] = Variable(canceledBy); + } + if (!nullToAbsent || deliveryType != null) { + map['delivery_type'] = Variable(deliveryType); + } + if (!nullToAbsent || deliveryPrefrence != null) { + map['delivery_prefrence'] = Variable(deliveryPrefrence); + } + map['shipping_cost'] = Variable(shippingCost); + map['total_order'] = Variable(totalOrder); + map['discount'] = Variable(discount); + map['tax'] = Variable(tax); + map['total_amount'] = Variable(totalAmount); + map['amount_paid'] = Variable(amountPaid); + map['change_amount'] = Variable(changeAmount); + if (!nullToAbsent || notes != null) { + map['notes'] = Variable(notes); + } + if (!nullToAbsent || paymentProofPath != null) { + map['payment_proof_path'] = Variable(paymentProofPath); + } + if (!nullToAbsent || paymentProofUrl != null) { + map['payment_proof_url'] = Variable(paymentProofUrl); + } + if (!nullToAbsent || paymentMethod != null) { + map['payment_method'] = Variable(paymentMethod); + } + if (!nullToAbsent || customerAddressSnapshot != null) { + map['customer_address_snapshot'] = Variable( + $OrdersTable.$convertercustomerAddressSnapshotn.toSql( + customerAddressSnapshot, + ), + ); + } + map['sync_status'] = Variable(syncStatus); + map['created_at'] = Variable(createdAt); + map['updated_at'] = Variable(updatedAt); + return map; + } + + OrdersCompanion toCompanion(bool nullToAbsent) { + return OrdersCompanion( + localId: Value(localId), + serverId: serverId == null && nullToAbsent + ? const Value.absent() + : Value(serverId), + orderNumber: Value(orderNumber), + outletId: Value(outletId), + outletNameSnapshot: Value(outletNameSnapshot), + customerId: customerId == null && nullToAbsent + ? const Value.absent() + : Value(customerId), + customerNameSnapshot: customerNameSnapshot == null && nullToAbsent + ? const Value.absent() + : Value(customerNameSnapshot), + customerEmailSnapshot: customerEmailSnapshot == null && nullToAbsent + ? const Value.absent() + : Value(customerEmailSnapshot), + customerPhoneNumberSnapshot: + customerPhoneNumberSnapshot == null && nullToAbsent + ? const Value.absent() + : Value(customerPhoneNumberSnapshot), + userId: Value(userId), + adminNameSnapshot: Value(adminNameSnapshot), + source: Value(source), + alreadyRead: alreadyRead == null && nullToAbsent + ? const Value.absent() + : Value(alreadyRead), + orderStatus: Value(orderStatus), + isCancellation: isCancellation == null && nullToAbsent + ? const Value.absent() + : Value(isCancellation), + cancellationAccepted: cancellationAccepted == null && nullToAbsent + ? const Value.absent() + : Value(cancellationAccepted), + approvalProccess: approvalProccess == null && nullToAbsent + ? const Value.absent() + : Value(approvalProccess), + canceledBy: canceledBy == null && nullToAbsent + ? const Value.absent() + : Value(canceledBy), + deliveryType: deliveryType == null && nullToAbsent + ? const Value.absent() + : Value(deliveryType), + deliveryPrefrence: deliveryPrefrence == null && nullToAbsent + ? const Value.absent() + : Value(deliveryPrefrence), + shippingCost: Value(shippingCost), + totalOrder: Value(totalOrder), + discount: Value(discount), + tax: Value(tax), + totalAmount: Value(totalAmount), + amountPaid: Value(amountPaid), + changeAmount: Value(changeAmount), + notes: notes == null && nullToAbsent + ? const Value.absent() + : Value(notes), + paymentProofPath: paymentProofPath == null && nullToAbsent + ? const Value.absent() + : Value(paymentProofPath), + paymentProofUrl: paymentProofUrl == null && nullToAbsent + ? const Value.absent() + : Value(paymentProofUrl), + paymentMethod: paymentMethod == null && nullToAbsent + ? const Value.absent() + : Value(paymentMethod), + customerAddressSnapshot: customerAddressSnapshot == null && nullToAbsent + ? const Value.absent() + : Value(customerAddressSnapshot), + syncStatus: Value(syncStatus), + createdAt: Value(createdAt), + updatedAt: Value(updatedAt), + ); + } + + factory Order.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return Order( + localId: serializer.fromJson(json['localId']), + serverId: serializer.fromJson(json['serverId']), + orderNumber: serializer.fromJson(json['orderNumber']), + outletId: serializer.fromJson(json['outletId']), + outletNameSnapshot: serializer.fromJson( + json['outletNameSnapshot'], + ), + customerId: serializer.fromJson(json['customerId']), + customerNameSnapshot: serializer.fromJson( + json['customerNameSnapshot'], + ), + customerEmailSnapshot: serializer.fromJson( + json['customerEmailSnapshot'], + ), + customerPhoneNumberSnapshot: serializer.fromJson( + json['customerPhoneNumberSnapshot'], + ), + userId: serializer.fromJson(json['userId']), + adminNameSnapshot: serializer.fromJson(json['adminNameSnapshot']), + source: serializer.fromJson(json['source']), + alreadyRead: serializer.fromJson(json['alreadyRead']), + orderStatus: serializer.fromJson(json['orderStatus']), + isCancellation: serializer.fromJson(json['isCancellation']), + cancellationAccepted: serializer.fromJson( + json['cancellationAccepted'], + ), + approvalProccess: serializer.fromJson(json['approvalProccess']), + canceledBy: serializer.fromJson(json['canceledBy']), + deliveryType: serializer.fromJson(json['deliveryType']), + deliveryPrefrence: serializer.fromJson( + json['deliveryPrefrence'], + ), + shippingCost: serializer.fromJson(json['shippingCost']), + totalOrder: serializer.fromJson(json['totalOrder']), + discount: serializer.fromJson(json['discount']), + tax: serializer.fromJson(json['tax']), + totalAmount: serializer.fromJson(json['totalAmount']), + amountPaid: serializer.fromJson(json['amountPaid']), + changeAmount: serializer.fromJson(json['changeAmount']), + notes: serializer.fromJson(json['notes']), + paymentProofPath: serializer.fromJson(json['paymentProofPath']), + paymentProofUrl: serializer.fromJson(json['paymentProofUrl']), + paymentMethod: serializer.fromJson(json['paymentMethod']), + customerAddressSnapshot: serializer.fromJson?>( + json['customerAddressSnapshot'], + ), + syncStatus: serializer.fromJson(json['syncStatus']), + createdAt: serializer.fromJson(json['createdAt']), + updatedAt: serializer.fromJson(json['updatedAt']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'localId': serializer.toJson(localId), + 'serverId': serializer.toJson(serverId), + 'orderNumber': serializer.toJson(orderNumber), + 'outletId': serializer.toJson(outletId), + 'outletNameSnapshot': serializer.toJson(outletNameSnapshot), + 'customerId': serializer.toJson(customerId), + 'customerNameSnapshot': serializer.toJson(customerNameSnapshot), + 'customerEmailSnapshot': serializer.toJson( + customerEmailSnapshot, + ), + 'customerPhoneNumberSnapshot': serializer.toJson( + customerPhoneNumberSnapshot, + ), + 'userId': serializer.toJson(userId), + 'adminNameSnapshot': serializer.toJson(adminNameSnapshot), + 'source': serializer.toJson(source), + 'alreadyRead': serializer.toJson(alreadyRead), + 'orderStatus': serializer.toJson(orderStatus), + 'isCancellation': serializer.toJson(isCancellation), + 'cancellationAccepted': serializer.toJson(cancellationAccepted), + 'approvalProccess': serializer.toJson(approvalProccess), + 'canceledBy': serializer.toJson(canceledBy), + 'deliveryType': serializer.toJson(deliveryType), + 'deliveryPrefrence': serializer.toJson(deliveryPrefrence), + 'shippingCost': serializer.toJson(shippingCost), + 'totalOrder': serializer.toJson(totalOrder), + 'discount': serializer.toJson(discount), + 'tax': serializer.toJson(tax), + 'totalAmount': serializer.toJson(totalAmount), + 'amountPaid': serializer.toJson(amountPaid), + 'changeAmount': serializer.toJson(changeAmount), + 'notes': serializer.toJson(notes), + 'paymentProofPath': serializer.toJson(paymentProofPath), + 'paymentProofUrl': serializer.toJson(paymentProofUrl), + 'paymentMethod': serializer.toJson(paymentMethod), + 'customerAddressSnapshot': serializer.toJson?>( + customerAddressSnapshot, + ), + 'syncStatus': serializer.toJson(syncStatus), + 'createdAt': serializer.toJson(createdAt), + 'updatedAt': serializer.toJson(updatedAt), + }; + } + + Order copyWith({ + String? localId, + Value serverId = const Value.absent(), + String? orderNumber, + String? outletId, + String? outletNameSnapshot, + Value customerId = const Value.absent(), + Value customerNameSnapshot = const Value.absent(), + Value customerEmailSnapshot = const Value.absent(), + Value customerPhoneNumberSnapshot = const Value.absent(), + String? userId, + String? adminNameSnapshot, + String? source, + Value alreadyRead = const Value.absent(), + String? orderStatus, + Value isCancellation = const Value.absent(), + Value cancellationAccepted = const Value.absent(), + Value approvalProccess = const Value.absent(), + Value canceledBy = const Value.absent(), + Value deliveryType = const Value.absent(), + Value deliveryPrefrence = const Value.absent(), + double? shippingCost, + double? totalOrder, + double? discount, + double? tax, + double? totalAmount, + double? amountPaid, + double? changeAmount, + Value notes = const Value.absent(), + Value paymentProofPath = const Value.absent(), + Value paymentProofUrl = const Value.absent(), + Value paymentMethod = const Value.absent(), + Value?> customerAddressSnapshot = const Value.absent(), + String? syncStatus, + DateTime? createdAt, + DateTime? updatedAt, + }) => Order( + localId: localId ?? this.localId, + serverId: serverId.present ? serverId.value : this.serverId, + orderNumber: orderNumber ?? this.orderNumber, + outletId: outletId ?? this.outletId, + outletNameSnapshot: outletNameSnapshot ?? this.outletNameSnapshot, + customerId: customerId.present ? customerId.value : this.customerId, + customerNameSnapshot: customerNameSnapshot.present + ? customerNameSnapshot.value + : this.customerNameSnapshot, + customerEmailSnapshot: customerEmailSnapshot.present + ? customerEmailSnapshot.value + : this.customerEmailSnapshot, + customerPhoneNumberSnapshot: customerPhoneNumberSnapshot.present + ? customerPhoneNumberSnapshot.value + : this.customerPhoneNumberSnapshot, + userId: userId ?? this.userId, + adminNameSnapshot: adminNameSnapshot ?? this.adminNameSnapshot, + source: source ?? this.source, + alreadyRead: alreadyRead.present ? alreadyRead.value : this.alreadyRead, + orderStatus: orderStatus ?? this.orderStatus, + isCancellation: isCancellation.present + ? isCancellation.value + : this.isCancellation, + cancellationAccepted: cancellationAccepted.present + ? cancellationAccepted.value + : this.cancellationAccepted, + approvalProccess: approvalProccess.present + ? approvalProccess.value + : this.approvalProccess, + canceledBy: canceledBy.present ? canceledBy.value : this.canceledBy, + deliveryType: deliveryType.present ? deliveryType.value : this.deliveryType, + deliveryPrefrence: deliveryPrefrence.present + ? deliveryPrefrence.value + : this.deliveryPrefrence, + shippingCost: shippingCost ?? this.shippingCost, + totalOrder: totalOrder ?? this.totalOrder, + discount: discount ?? this.discount, + tax: tax ?? this.tax, + totalAmount: totalAmount ?? this.totalAmount, + amountPaid: amountPaid ?? this.amountPaid, + changeAmount: changeAmount ?? this.changeAmount, + notes: notes.present ? notes.value : this.notes, + paymentProofPath: paymentProofPath.present + ? paymentProofPath.value + : this.paymentProofPath, + paymentProofUrl: paymentProofUrl.present + ? paymentProofUrl.value + : this.paymentProofUrl, + paymentMethod: paymentMethod.present + ? paymentMethod.value + : this.paymentMethod, + customerAddressSnapshot: customerAddressSnapshot.present + ? customerAddressSnapshot.value + : this.customerAddressSnapshot, + syncStatus: syncStatus ?? this.syncStatus, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + ); + Order copyWithCompanion(OrdersCompanion data) { + return Order( + localId: data.localId.present ? data.localId.value : this.localId, + serverId: data.serverId.present ? data.serverId.value : this.serverId, + orderNumber: data.orderNumber.present + ? data.orderNumber.value + : this.orderNumber, + outletId: data.outletId.present ? data.outletId.value : this.outletId, + outletNameSnapshot: data.outletNameSnapshot.present + ? data.outletNameSnapshot.value + : this.outletNameSnapshot, + customerId: data.customerId.present + ? data.customerId.value + : this.customerId, + customerNameSnapshot: data.customerNameSnapshot.present + ? data.customerNameSnapshot.value + : this.customerNameSnapshot, + customerEmailSnapshot: data.customerEmailSnapshot.present + ? data.customerEmailSnapshot.value + : this.customerEmailSnapshot, + customerPhoneNumberSnapshot: data.customerPhoneNumberSnapshot.present + ? data.customerPhoneNumberSnapshot.value + : this.customerPhoneNumberSnapshot, + userId: data.userId.present ? data.userId.value : this.userId, + adminNameSnapshot: data.adminNameSnapshot.present + ? data.adminNameSnapshot.value + : this.adminNameSnapshot, + source: data.source.present ? data.source.value : this.source, + alreadyRead: data.alreadyRead.present + ? data.alreadyRead.value + : this.alreadyRead, + orderStatus: data.orderStatus.present + ? data.orderStatus.value + : this.orderStatus, + isCancellation: data.isCancellation.present + ? data.isCancellation.value + : this.isCancellation, + cancellationAccepted: data.cancellationAccepted.present + ? data.cancellationAccepted.value + : this.cancellationAccepted, + approvalProccess: data.approvalProccess.present + ? data.approvalProccess.value + : this.approvalProccess, + canceledBy: data.canceledBy.present + ? data.canceledBy.value + : this.canceledBy, + deliveryType: data.deliveryType.present + ? data.deliveryType.value + : this.deliveryType, + deliveryPrefrence: data.deliveryPrefrence.present + ? data.deliveryPrefrence.value + : this.deliveryPrefrence, + shippingCost: data.shippingCost.present + ? data.shippingCost.value + : this.shippingCost, + totalOrder: data.totalOrder.present + ? data.totalOrder.value + : this.totalOrder, + discount: data.discount.present ? data.discount.value : this.discount, + tax: data.tax.present ? data.tax.value : this.tax, + totalAmount: data.totalAmount.present + ? data.totalAmount.value + : this.totalAmount, + amountPaid: data.amountPaid.present + ? data.amountPaid.value + : this.amountPaid, + changeAmount: data.changeAmount.present + ? data.changeAmount.value + : this.changeAmount, + notes: data.notes.present ? data.notes.value : this.notes, + paymentProofPath: data.paymentProofPath.present + ? data.paymentProofPath.value + : this.paymentProofPath, + paymentProofUrl: data.paymentProofUrl.present + ? data.paymentProofUrl.value + : this.paymentProofUrl, + paymentMethod: data.paymentMethod.present + ? data.paymentMethod.value + : this.paymentMethod, + customerAddressSnapshot: data.customerAddressSnapshot.present + ? data.customerAddressSnapshot.value + : this.customerAddressSnapshot, + syncStatus: data.syncStatus.present + ? data.syncStatus.value + : this.syncStatus, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, + ); + } + + @override + String toString() { + return (StringBuffer('Order(') + ..write('localId: $localId, ') + ..write('serverId: $serverId, ') + ..write('orderNumber: $orderNumber, ') + ..write('outletId: $outletId, ') + ..write('outletNameSnapshot: $outletNameSnapshot, ') + ..write('customerId: $customerId, ') + ..write('customerNameSnapshot: $customerNameSnapshot, ') + ..write('customerEmailSnapshot: $customerEmailSnapshot, ') + ..write('customerPhoneNumberSnapshot: $customerPhoneNumberSnapshot, ') + ..write('userId: $userId, ') + ..write('adminNameSnapshot: $adminNameSnapshot, ') + ..write('source: $source, ') + ..write('alreadyRead: $alreadyRead, ') + ..write('orderStatus: $orderStatus, ') + ..write('isCancellation: $isCancellation, ') + ..write('cancellationAccepted: $cancellationAccepted, ') + ..write('approvalProccess: $approvalProccess, ') + ..write('canceledBy: $canceledBy, ') + ..write('deliveryType: $deliveryType, ') + ..write('deliveryPrefrence: $deliveryPrefrence, ') + ..write('shippingCost: $shippingCost, ') + ..write('totalOrder: $totalOrder, ') + ..write('discount: $discount, ') + ..write('tax: $tax, ') + ..write('totalAmount: $totalAmount, ') + ..write('amountPaid: $amountPaid, ') + ..write('changeAmount: $changeAmount, ') + ..write('notes: $notes, ') + ..write('paymentProofPath: $paymentProofPath, ') + ..write('paymentProofUrl: $paymentProofUrl, ') + ..write('paymentMethod: $paymentMethod, ') + ..write('customerAddressSnapshot: $customerAddressSnapshot, ') + ..write('syncStatus: $syncStatus, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hashAll([ + localId, + serverId, + orderNumber, + outletId, + outletNameSnapshot, + customerId, + customerNameSnapshot, + customerEmailSnapshot, + customerPhoneNumberSnapshot, + userId, + adminNameSnapshot, + source, + alreadyRead, + orderStatus, + isCancellation, + cancellationAccepted, + approvalProccess, + canceledBy, + deliveryType, + deliveryPrefrence, + shippingCost, + totalOrder, + discount, + tax, + totalAmount, + amountPaid, + changeAmount, + notes, + paymentProofPath, + paymentProofUrl, + paymentMethod, + customerAddressSnapshot, + syncStatus, + createdAt, + updatedAt, + ]); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is Order && + other.localId == this.localId && + other.serverId == this.serverId && + other.orderNumber == this.orderNumber && + other.outletId == this.outletId && + other.outletNameSnapshot == this.outletNameSnapshot && + other.customerId == this.customerId && + other.customerNameSnapshot == this.customerNameSnapshot && + other.customerEmailSnapshot == this.customerEmailSnapshot && + other.customerPhoneNumberSnapshot == + this.customerPhoneNumberSnapshot && + other.userId == this.userId && + other.adminNameSnapshot == this.adminNameSnapshot && + other.source == this.source && + other.alreadyRead == this.alreadyRead && + other.orderStatus == this.orderStatus && + other.isCancellation == this.isCancellation && + other.cancellationAccepted == this.cancellationAccepted && + other.approvalProccess == this.approvalProccess && + other.canceledBy == this.canceledBy && + other.deliveryType == this.deliveryType && + other.deliveryPrefrence == this.deliveryPrefrence && + other.shippingCost == this.shippingCost && + other.totalOrder == this.totalOrder && + other.discount == this.discount && + other.tax == this.tax && + other.totalAmount == this.totalAmount && + other.amountPaid == this.amountPaid && + other.changeAmount == this.changeAmount && + other.notes == this.notes && + other.paymentProofPath == this.paymentProofPath && + other.paymentProofUrl == this.paymentProofUrl && + other.paymentMethod == this.paymentMethod && + other.customerAddressSnapshot == this.customerAddressSnapshot && + other.syncStatus == this.syncStatus && + other.createdAt == this.createdAt && + other.updatedAt == this.updatedAt); +} + +class OrdersCompanion extends UpdateCompanion { + final Value localId; + final Value serverId; + final Value orderNumber; + final Value outletId; + final Value outletNameSnapshot; + final Value customerId; + final Value customerNameSnapshot; + final Value customerEmailSnapshot; + final Value customerPhoneNumberSnapshot; + final Value userId; + final Value adminNameSnapshot; + final Value source; + final Value alreadyRead; + final Value orderStatus; + final Value isCancellation; + final Value cancellationAccepted; + final Value approvalProccess; + final Value canceledBy; + final Value deliveryType; + final Value deliveryPrefrence; + final Value shippingCost; + final Value totalOrder; + final Value discount; + final Value tax; + final Value totalAmount; + final Value amountPaid; + final Value changeAmount; + final Value notes; + final Value paymentProofPath; + final Value paymentProofUrl; + final Value paymentMethod; + final Value?> customerAddressSnapshot; + final Value syncStatus; + final Value createdAt; + final Value updatedAt; + final Value rowid; + const OrdersCompanion({ + this.localId = const Value.absent(), + this.serverId = const Value.absent(), + this.orderNumber = const Value.absent(), + this.outletId = const Value.absent(), + this.outletNameSnapshot = const Value.absent(), + this.customerId = const Value.absent(), + this.customerNameSnapshot = const Value.absent(), + this.customerEmailSnapshot = const Value.absent(), + this.customerPhoneNumberSnapshot = const Value.absent(), + this.userId = const Value.absent(), + this.adminNameSnapshot = const Value.absent(), + this.source = const Value.absent(), + this.alreadyRead = const Value.absent(), + this.orderStatus = const Value.absent(), + this.isCancellation = const Value.absent(), + this.cancellationAccepted = const Value.absent(), + this.approvalProccess = const Value.absent(), + this.canceledBy = const Value.absent(), + this.deliveryType = const Value.absent(), + this.deliveryPrefrence = const Value.absent(), + this.shippingCost = const Value.absent(), + this.totalOrder = const Value.absent(), + this.discount = const Value.absent(), + this.tax = const Value.absent(), + this.totalAmount = const Value.absent(), + this.amountPaid = const Value.absent(), + this.changeAmount = const Value.absent(), + this.notes = const Value.absent(), + this.paymentProofPath = const Value.absent(), + this.paymentProofUrl = const Value.absent(), + this.paymentMethod = const Value.absent(), + this.customerAddressSnapshot = const Value.absent(), + this.syncStatus = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.rowid = const Value.absent(), + }); + OrdersCompanion.insert({ + this.localId = const Value.absent(), + this.serverId = const Value.absent(), + required String orderNumber, + required String outletId, + required String outletNameSnapshot, + this.customerId = const Value.absent(), + this.customerNameSnapshot = const Value.absent(), + this.customerEmailSnapshot = const Value.absent(), + this.customerPhoneNumberSnapshot = const Value.absent(), + required String userId, + required String adminNameSnapshot, + required String source, + this.alreadyRead = const Value.absent(), + required String orderStatus, + this.isCancellation = const Value.absent(), + this.cancellationAccepted = const Value.absent(), + this.approvalProccess = const Value.absent(), + this.canceledBy = const Value.absent(), + this.deliveryType = const Value.absent(), + this.deliveryPrefrence = const Value.absent(), + this.shippingCost = const Value.absent(), + this.totalOrder = const Value.absent(), + this.discount = const Value.absent(), + this.tax = const Value.absent(), + this.totalAmount = const Value.absent(), + this.amountPaid = const Value.absent(), + this.changeAmount = const Value.absent(), + this.notes = const Value.absent(), + this.paymentProofPath = const Value.absent(), + this.paymentProofUrl = const Value.absent(), + this.paymentMethod = const Value.absent(), + this.customerAddressSnapshot = const Value.absent(), + this.syncStatus = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.rowid = const Value.absent(), + }) : orderNumber = Value(orderNumber), + outletId = Value(outletId), + outletNameSnapshot = Value(outletNameSnapshot), + userId = Value(userId), + adminNameSnapshot = Value(adminNameSnapshot), + source = Value(source), + orderStatus = Value(orderStatus); + static Insertable custom({ + Expression? localId, + Expression? serverId, + Expression? orderNumber, + Expression? outletId, + Expression? outletNameSnapshot, + Expression? customerId, + Expression? customerNameSnapshot, + Expression? customerEmailSnapshot, + Expression? customerPhoneNumberSnapshot, + Expression? userId, + Expression? adminNameSnapshot, + Expression? source, + Expression? alreadyRead, + Expression? orderStatus, + Expression? isCancellation, + Expression? cancellationAccepted, + Expression? approvalProccess, + Expression? canceledBy, + Expression? deliveryType, + Expression? deliveryPrefrence, + Expression? shippingCost, + Expression? totalOrder, + Expression? discount, + Expression? tax, + Expression? totalAmount, + Expression? amountPaid, + Expression? changeAmount, + Expression? notes, + Expression? paymentProofPath, + Expression? paymentProofUrl, + Expression? paymentMethod, + Expression? customerAddressSnapshot, + Expression? syncStatus, + Expression? createdAt, + Expression? updatedAt, + Expression? rowid, + }) { + return RawValuesInsertable({ + if (localId != null) 'local_id': localId, + if (serverId != null) 'server_id': serverId, + if (orderNumber != null) 'order_number': orderNumber, + if (outletId != null) 'outlet_id': outletId, + if (outletNameSnapshot != null) + 'outlet_name_snapshot': outletNameSnapshot, + if (customerId != null) 'customer_id': customerId, + if (customerNameSnapshot != null) + 'customer_name_snapshot': customerNameSnapshot, + if (customerEmailSnapshot != null) + 'customer_email_snapshot': customerEmailSnapshot, + if (customerPhoneNumberSnapshot != null) + 'customer_phone_number_snapshot': customerPhoneNumberSnapshot, + if (userId != null) 'user_id': userId, + if (adminNameSnapshot != null) 'admin_name_snapshot': adminNameSnapshot, + if (source != null) 'source': source, + if (alreadyRead != null) 'already_read': alreadyRead, + if (orderStatus != null) 'order_status': orderStatus, + if (isCancellation != null) 'is_cancellation': isCancellation, + if (cancellationAccepted != null) + 'cancellation_accepted': cancellationAccepted, + if (approvalProccess != null) 'approval_proccess': approvalProccess, + if (canceledBy != null) 'canceled_by': canceledBy, + if (deliveryType != null) 'delivery_type': deliveryType, + if (deliveryPrefrence != null) 'delivery_prefrence': deliveryPrefrence, + if (shippingCost != null) 'shipping_cost': shippingCost, + if (totalOrder != null) 'total_order': totalOrder, + if (discount != null) 'discount': discount, + if (tax != null) 'tax': tax, + if (totalAmount != null) 'total_amount': totalAmount, + if (amountPaid != null) 'amount_paid': amountPaid, + if (changeAmount != null) 'change_amount': changeAmount, + if (notes != null) 'notes': notes, + if (paymentProofPath != null) 'payment_proof_path': paymentProofPath, + if (paymentProofUrl != null) 'payment_proof_url': paymentProofUrl, + if (paymentMethod != null) 'payment_method': paymentMethod, + if (customerAddressSnapshot != null) + 'customer_address_snapshot': customerAddressSnapshot, + if (syncStatus != null) 'sync_status': syncStatus, + if (createdAt != null) 'created_at': createdAt, + if (updatedAt != null) 'updated_at': updatedAt, + if (rowid != null) 'rowid': rowid, + }); + } + + OrdersCompanion copyWith({ + Value? localId, + Value? serverId, + Value? orderNumber, + Value? outletId, + Value? outletNameSnapshot, + Value? customerId, + Value? customerNameSnapshot, + Value? customerEmailSnapshot, + Value? customerPhoneNumberSnapshot, + Value? userId, + Value? adminNameSnapshot, + Value? source, + Value? alreadyRead, + Value? orderStatus, + Value? isCancellation, + Value? cancellationAccepted, + Value? approvalProccess, + Value? canceledBy, + Value? deliveryType, + Value? deliveryPrefrence, + Value? shippingCost, + Value? totalOrder, + Value? discount, + Value? tax, + Value? totalAmount, + Value? amountPaid, + Value? changeAmount, + Value? notes, + Value? paymentProofPath, + Value? paymentProofUrl, + Value? paymentMethod, + Value?>? customerAddressSnapshot, + Value? syncStatus, + Value? createdAt, + Value? updatedAt, + Value? rowid, + }) { + return OrdersCompanion( + localId: localId ?? this.localId, + serverId: serverId ?? this.serverId, + orderNumber: orderNumber ?? this.orderNumber, + outletId: outletId ?? this.outletId, + outletNameSnapshot: outletNameSnapshot ?? this.outletNameSnapshot, + customerId: customerId ?? this.customerId, + customerNameSnapshot: customerNameSnapshot ?? this.customerNameSnapshot, + customerEmailSnapshot: + customerEmailSnapshot ?? this.customerEmailSnapshot, + customerPhoneNumberSnapshot: + customerPhoneNumberSnapshot ?? this.customerPhoneNumberSnapshot, + userId: userId ?? this.userId, + adminNameSnapshot: adminNameSnapshot ?? this.adminNameSnapshot, + source: source ?? this.source, + alreadyRead: alreadyRead ?? this.alreadyRead, + orderStatus: orderStatus ?? this.orderStatus, + isCancellation: isCancellation ?? this.isCancellation, + cancellationAccepted: cancellationAccepted ?? this.cancellationAccepted, + approvalProccess: approvalProccess ?? this.approvalProccess, + canceledBy: canceledBy ?? this.canceledBy, + deliveryType: deliveryType ?? this.deliveryType, + deliveryPrefrence: deliveryPrefrence ?? this.deliveryPrefrence, + shippingCost: shippingCost ?? this.shippingCost, + totalOrder: totalOrder ?? this.totalOrder, + discount: discount ?? this.discount, + tax: tax ?? this.tax, + totalAmount: totalAmount ?? this.totalAmount, + amountPaid: amountPaid ?? this.amountPaid, + changeAmount: changeAmount ?? this.changeAmount, + notes: notes ?? this.notes, + paymentProofPath: paymentProofPath ?? this.paymentProofPath, + paymentProofUrl: paymentProofUrl ?? this.paymentProofUrl, + paymentMethod: paymentMethod ?? this.paymentMethod, + customerAddressSnapshot: + customerAddressSnapshot ?? this.customerAddressSnapshot, + syncStatus: syncStatus ?? this.syncStatus, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (localId.present) { + map['local_id'] = Variable(localId.value); + } + if (serverId.present) { + map['server_id'] = Variable(serverId.value); + } + if (orderNumber.present) { + map['order_number'] = Variable(orderNumber.value); + } + if (outletId.present) { + map['outlet_id'] = Variable(outletId.value); + } + if (outletNameSnapshot.present) { + map['outlet_name_snapshot'] = Variable(outletNameSnapshot.value); + } + if (customerId.present) { + map['customer_id'] = Variable(customerId.value); + } + if (customerNameSnapshot.present) { + map['customer_name_snapshot'] = Variable( + customerNameSnapshot.value, + ); + } + if (customerEmailSnapshot.present) { + map['customer_email_snapshot'] = Variable( + customerEmailSnapshot.value, + ); + } + if (customerPhoneNumberSnapshot.present) { + map['customer_phone_number_snapshot'] = Variable( + customerPhoneNumberSnapshot.value, + ); + } + if (userId.present) { + map['user_id'] = Variable(userId.value); + } + if (adminNameSnapshot.present) { + map['admin_name_snapshot'] = Variable(adminNameSnapshot.value); + } + if (source.present) { + map['source'] = Variable(source.value); + } + if (alreadyRead.present) { + map['already_read'] = Variable(alreadyRead.value); + } + if (orderStatus.present) { + map['order_status'] = Variable(orderStatus.value); + } + if (isCancellation.present) { + map['is_cancellation'] = Variable(isCancellation.value); + } + if (cancellationAccepted.present) { + map['cancellation_accepted'] = Variable(cancellationAccepted.value); + } + if (approvalProccess.present) { + map['approval_proccess'] = Variable(approvalProccess.value); + } + if (canceledBy.present) { + map['canceled_by'] = Variable(canceledBy.value); + } + if (deliveryType.present) { + map['delivery_type'] = Variable(deliveryType.value); + } + if (deliveryPrefrence.present) { + map['delivery_prefrence'] = Variable(deliveryPrefrence.value); + } + if (shippingCost.present) { + map['shipping_cost'] = Variable(shippingCost.value); + } + if (totalOrder.present) { + map['total_order'] = Variable(totalOrder.value); + } + if (discount.present) { + map['discount'] = Variable(discount.value); + } + if (tax.present) { + map['tax'] = Variable(tax.value); + } + if (totalAmount.present) { + map['total_amount'] = Variable(totalAmount.value); + } + if (amountPaid.present) { + map['amount_paid'] = Variable(amountPaid.value); + } + if (changeAmount.present) { + map['change_amount'] = Variable(changeAmount.value); + } + if (notes.present) { + map['notes'] = Variable(notes.value); + } + if (paymentProofPath.present) { + map['payment_proof_path'] = Variable(paymentProofPath.value); + } + if (paymentProofUrl.present) { + map['payment_proof_url'] = Variable(paymentProofUrl.value); + } + if (paymentMethod.present) { + map['payment_method'] = Variable(paymentMethod.value); + } + if (customerAddressSnapshot.present) { + map['customer_address_snapshot'] = Variable( + $OrdersTable.$convertercustomerAddressSnapshotn.toSql( + customerAddressSnapshot.value, + ), + ); + } + if (syncStatus.present) { + map['sync_status'] = Variable(syncStatus.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (updatedAt.present) { + map['updated_at'] = Variable(updatedAt.value); + } + if (rowid.present) { + map['rowid'] = Variable(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('OrdersCompanion(') + ..write('localId: $localId, ') + ..write('serverId: $serverId, ') + ..write('orderNumber: $orderNumber, ') + ..write('outletId: $outletId, ') + ..write('outletNameSnapshot: $outletNameSnapshot, ') + ..write('customerId: $customerId, ') + ..write('customerNameSnapshot: $customerNameSnapshot, ') + ..write('customerEmailSnapshot: $customerEmailSnapshot, ') + ..write('customerPhoneNumberSnapshot: $customerPhoneNumberSnapshot, ') + ..write('userId: $userId, ') + ..write('adminNameSnapshot: $adminNameSnapshot, ') + ..write('source: $source, ') + ..write('alreadyRead: $alreadyRead, ') + ..write('orderStatus: $orderStatus, ') + ..write('isCancellation: $isCancellation, ') + ..write('cancellationAccepted: $cancellationAccepted, ') + ..write('approvalProccess: $approvalProccess, ') + ..write('canceledBy: $canceledBy, ') + ..write('deliveryType: $deliveryType, ') + ..write('deliveryPrefrence: $deliveryPrefrence, ') + ..write('shippingCost: $shippingCost, ') + ..write('totalOrder: $totalOrder, ') + ..write('discount: $discount, ') + ..write('tax: $tax, ') + ..write('totalAmount: $totalAmount, ') + ..write('amountPaid: $amountPaid, ') + ..write('changeAmount: $changeAmount, ') + ..write('notes: $notes, ') + ..write('paymentProofPath: $paymentProofPath, ') + ..write('paymentProofUrl: $paymentProofUrl, ') + ..write('paymentMethod: $paymentMethod, ') + ..write('customerAddressSnapshot: $customerAddressSnapshot, ') + ..write('syncStatus: $syncStatus, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + +class $OrderItemsTable extends OrderItems + with TableInfo<$OrderItemsTable, OrderItem> { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + $OrderItemsTable(this.attachedDatabase, [this._alias]); + static const VerificationMeta _localIdMeta = const VerificationMeta( + 'localId', + ); + @override + late final GeneratedColumn localId = GeneratedColumn( + 'local_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: false, + clientDefault: () => const Uuid().v7().toString(), + ); + static const VerificationMeta _serverIdMeta = const VerificationMeta( + 'serverId', + ); + @override + late final GeneratedColumn serverId = GeneratedColumn( + 'server_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _orderIdMeta = const VerificationMeta( + 'orderId', + ); + @override + late final GeneratedColumn orderId = GeneratedColumn( + 'order_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES orders (local_id)', + ), + ); + static const VerificationMeta _productVariantIdMeta = const VerificationMeta( + 'productVariantId', + ); + @override + late final GeneratedColumn productVariantId = GeneratedColumn( + 'product_variant_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES product_variants (local_id)', + ), + ); + static const VerificationMeta _quantityMeta = const VerificationMeta( + 'quantity', + ); + @override + late final GeneratedColumn quantity = GeneratedColumn( + 'quantity', + aliasedName, + false, + type: DriftSqlType.double, + requiredDuringInsert: false, + defaultValue: const Constant(0), + ); + static const VerificationMeta _productImageSnapshotMeta = + const VerificationMeta('productImageSnapshot'); + @override + late final GeneratedColumn productImageSnapshot = + GeneratedColumn( + 'product_image_snapshot', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _productImageUrlSnapshotMeta = + const VerificationMeta('productImageUrlSnapshot'); + @override + late final GeneratedColumn productImageUrlSnapshot = + GeneratedColumn( + 'product_image_url_snapshot', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _productNameSnapshotMeta = + const VerificationMeta('productNameSnapshot'); + @override + late final GeneratedColumn productNameSnapshot = + GeneratedColumn( + 'product_name_snapshot', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + static const VerificationMeta _productVariantNameSnapshotMeta = + const VerificationMeta('productVariantNameSnapshot'); + @override + late final GeneratedColumn productVariantNameSnapshot = + GeneratedColumn( + 'product_variant_name_snapshot', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _sellingPriceSnapshotMeta = + const VerificationMeta('sellingPriceSnapshot'); + @override + late final GeneratedColumn sellingPriceSnapshot = + GeneratedColumn( + 'selling_price_snapshot', + aliasedName, + false, + type: DriftSqlType.double, + requiredDuringInsert: false, + defaultValue: const Constant(0), + ); + static const VerificationMeta _subtotalMeta = const VerificationMeta( + 'subtotal', + ); + @override + late final GeneratedColumn subtotal = GeneratedColumn( + 'subtotal', + aliasedName, + false, + type: DriftSqlType.double, + requiredDuringInsert: false, + defaultValue: const Constant(0), + ); + static const VerificationMeta _syncStatusMeta = const VerificationMeta( + 'syncStatus', + ); + @override + late final GeneratedColumn syncStatus = GeneratedColumn( + 'sync_status', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultValue: const Constant('dirty'), + ); + static const VerificationMeta _createdAtMeta = const VerificationMeta( + 'createdAt', + ); + @override + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: currentDateAndTime, + ); + static const VerificationMeta _updatedAtMeta = const VerificationMeta( + 'updatedAt', + ); + @override + late final GeneratedColumn updatedAt = GeneratedColumn( + 'updated_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: currentDateAndTime, + ); + @override + List get $columns => [ + localId, + serverId, + orderId, + productVariantId, + quantity, + productImageSnapshot, + productImageUrlSnapshot, + productNameSnapshot, + productVariantNameSnapshot, + sellingPriceSnapshot, + subtotal, + syncStatus, + createdAt, + updatedAt, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'order_items'; + @override + VerificationContext validateIntegrity( + Insertable instance, { + bool isInserting = false, + }) { + final context = VerificationContext(); + final data = instance.toColumns(true); + if (data.containsKey('local_id')) { + context.handle( + _localIdMeta, + localId.isAcceptableOrUnknown(data['local_id']!, _localIdMeta), + ); + } + if (data.containsKey('server_id')) { + context.handle( + _serverIdMeta, + serverId.isAcceptableOrUnknown(data['server_id']!, _serverIdMeta), + ); + } + if (data.containsKey('order_id')) { + context.handle( + _orderIdMeta, + orderId.isAcceptableOrUnknown(data['order_id']!, _orderIdMeta), + ); + } else if (isInserting) { + context.missing(_orderIdMeta); + } + if (data.containsKey('product_variant_id')) { + context.handle( + _productVariantIdMeta, + productVariantId.isAcceptableOrUnknown( + data['product_variant_id']!, + _productVariantIdMeta, + ), + ); + } else if (isInserting) { + context.missing(_productVariantIdMeta); + } + if (data.containsKey('quantity')) { + context.handle( + _quantityMeta, + quantity.isAcceptableOrUnknown(data['quantity']!, _quantityMeta), + ); + } + if (data.containsKey('product_image_snapshot')) { + context.handle( + _productImageSnapshotMeta, + productImageSnapshot.isAcceptableOrUnknown( + data['product_image_snapshot']!, + _productImageSnapshotMeta, + ), + ); + } + if (data.containsKey('product_image_url_snapshot')) { + context.handle( + _productImageUrlSnapshotMeta, + productImageUrlSnapshot.isAcceptableOrUnknown( + data['product_image_url_snapshot']!, + _productImageUrlSnapshotMeta, + ), + ); + } + if (data.containsKey('product_name_snapshot')) { + context.handle( + _productNameSnapshotMeta, + productNameSnapshot.isAcceptableOrUnknown( + data['product_name_snapshot']!, + _productNameSnapshotMeta, + ), + ); + } else if (isInserting) { + context.missing(_productNameSnapshotMeta); + } + if (data.containsKey('product_variant_name_snapshot')) { + context.handle( + _productVariantNameSnapshotMeta, + productVariantNameSnapshot.isAcceptableOrUnknown( + data['product_variant_name_snapshot']!, + _productVariantNameSnapshotMeta, + ), + ); + } + if (data.containsKey('selling_price_snapshot')) { + context.handle( + _sellingPriceSnapshotMeta, + sellingPriceSnapshot.isAcceptableOrUnknown( + data['selling_price_snapshot']!, + _sellingPriceSnapshotMeta, + ), + ); + } + if (data.containsKey('subtotal')) { + context.handle( + _subtotalMeta, + subtotal.isAcceptableOrUnknown(data['subtotal']!, _subtotalMeta), + ); + } + if (data.containsKey('sync_status')) { + context.handle( + _syncStatusMeta, + syncStatus.isAcceptableOrUnknown(data['sync_status']!, _syncStatusMeta), + ); + } + if (data.containsKey('created_at')) { + context.handle( + _createdAtMeta, + createdAt.isAcceptableOrUnknown(data['created_at']!, _createdAtMeta), + ); + } + if (data.containsKey('updated_at')) { + context.handle( + _updatedAtMeta, + updatedAt.isAcceptableOrUnknown(data['updated_at']!, _updatedAtMeta), + ); + } + return context; + } + + @override + Set get $primaryKey => {localId}; + @override + OrderItem map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return OrderItem( + localId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}local_id'], + )!, + serverId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}server_id'], + ), + orderId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}order_id'], + )!, + productVariantId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}product_variant_id'], + )!, + quantity: attachedDatabase.typeMapping.read( + DriftSqlType.double, + data['${effectivePrefix}quantity'], + )!, + productImageSnapshot: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}product_image_snapshot'], + ), + productImageUrlSnapshot: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}product_image_url_snapshot'], + ), + productNameSnapshot: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}product_name_snapshot'], + )!, + productVariantNameSnapshot: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}product_variant_name_snapshot'], + ), + sellingPriceSnapshot: attachedDatabase.typeMapping.read( + DriftSqlType.double, + data['${effectivePrefix}selling_price_snapshot'], + )!, + subtotal: attachedDatabase.typeMapping.read( + DriftSqlType.double, + data['${effectivePrefix}subtotal'], + )!, + syncStatus: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}sync_status'], + )!, + createdAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}created_at'], + )!, + updatedAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}updated_at'], + )!, + ); + } + + @override + $OrderItemsTable createAlias(String alias) { + return $OrderItemsTable(attachedDatabase, alias); + } +} + +class OrderItem extends DataClass implements Insertable { + final String localId; + final String? serverId; + final String orderId; + final String productVariantId; + final double quantity; + final String? productImageSnapshot; + final String? productImageUrlSnapshot; + final String productNameSnapshot; + final String? productVariantNameSnapshot; + final double sellingPriceSnapshot; + final double subtotal; + final String syncStatus; + final DateTime createdAt; + final DateTime updatedAt; + const OrderItem({ + required this.localId, + this.serverId, + required this.orderId, + required this.productVariantId, + required this.quantity, + this.productImageSnapshot, + this.productImageUrlSnapshot, + required this.productNameSnapshot, + this.productVariantNameSnapshot, + required this.sellingPriceSnapshot, + required this.subtotal, + required this.syncStatus, + required this.createdAt, + required this.updatedAt, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['local_id'] = Variable(localId); + if (!nullToAbsent || serverId != null) { + map['server_id'] = Variable(serverId); + } + map['order_id'] = Variable(orderId); + map['product_variant_id'] = Variable(productVariantId); + map['quantity'] = Variable(quantity); + if (!nullToAbsent || productImageSnapshot != null) { + map['product_image_snapshot'] = Variable(productImageSnapshot); + } + if (!nullToAbsent || productImageUrlSnapshot != null) { + map['product_image_url_snapshot'] = Variable( + productImageUrlSnapshot, + ); + } + map['product_name_snapshot'] = Variable(productNameSnapshot); + if (!nullToAbsent || productVariantNameSnapshot != null) { + map['product_variant_name_snapshot'] = Variable( + productVariantNameSnapshot, + ); + } + map['selling_price_snapshot'] = Variable(sellingPriceSnapshot); + map['subtotal'] = Variable(subtotal); + map['sync_status'] = Variable(syncStatus); + map['created_at'] = Variable(createdAt); + map['updated_at'] = Variable(updatedAt); + return map; + } + + OrderItemsCompanion toCompanion(bool nullToAbsent) { + return OrderItemsCompanion( + localId: Value(localId), + serverId: serverId == null && nullToAbsent + ? const Value.absent() + : Value(serverId), + orderId: Value(orderId), + productVariantId: Value(productVariantId), + quantity: Value(quantity), + productImageSnapshot: productImageSnapshot == null && nullToAbsent + ? const Value.absent() + : Value(productImageSnapshot), + productImageUrlSnapshot: productImageUrlSnapshot == null && nullToAbsent + ? const Value.absent() + : Value(productImageUrlSnapshot), + productNameSnapshot: Value(productNameSnapshot), + productVariantNameSnapshot: + productVariantNameSnapshot == null && nullToAbsent + ? const Value.absent() + : Value(productVariantNameSnapshot), + sellingPriceSnapshot: Value(sellingPriceSnapshot), + subtotal: Value(subtotal), + syncStatus: Value(syncStatus), + createdAt: Value(createdAt), + updatedAt: Value(updatedAt), + ); + } + + factory OrderItem.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return OrderItem( + localId: serializer.fromJson(json['localId']), + serverId: serializer.fromJson(json['serverId']), + orderId: serializer.fromJson(json['orderId']), + productVariantId: serializer.fromJson(json['productVariantId']), + quantity: serializer.fromJson(json['quantity']), + productImageSnapshot: serializer.fromJson( + json['productImageSnapshot'], + ), + productImageUrlSnapshot: serializer.fromJson( + json['productImageUrlSnapshot'], + ), + productNameSnapshot: serializer.fromJson( + json['productNameSnapshot'], + ), + productVariantNameSnapshot: serializer.fromJson( + json['productVariantNameSnapshot'], + ), + sellingPriceSnapshot: serializer.fromJson( + json['sellingPriceSnapshot'], + ), + subtotal: serializer.fromJson(json['subtotal']), + syncStatus: serializer.fromJson(json['syncStatus']), + createdAt: serializer.fromJson(json['createdAt']), + updatedAt: serializer.fromJson(json['updatedAt']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'localId': serializer.toJson(localId), + 'serverId': serializer.toJson(serverId), + 'orderId': serializer.toJson(orderId), + 'productVariantId': serializer.toJson(productVariantId), + 'quantity': serializer.toJson(quantity), + 'productImageSnapshot': serializer.toJson(productImageSnapshot), + 'productImageUrlSnapshot': serializer.toJson( + productImageUrlSnapshot, + ), + 'productNameSnapshot': serializer.toJson(productNameSnapshot), + 'productVariantNameSnapshot': serializer.toJson( + productVariantNameSnapshot, + ), + 'sellingPriceSnapshot': serializer.toJson(sellingPriceSnapshot), + 'subtotal': serializer.toJson(subtotal), + 'syncStatus': serializer.toJson(syncStatus), + 'createdAt': serializer.toJson(createdAt), + 'updatedAt': serializer.toJson(updatedAt), + }; + } + + OrderItem copyWith({ + String? localId, + Value serverId = const Value.absent(), + String? orderId, + String? productVariantId, + double? quantity, + Value productImageSnapshot = const Value.absent(), + Value productImageUrlSnapshot = const Value.absent(), + String? productNameSnapshot, + Value productVariantNameSnapshot = const Value.absent(), + double? sellingPriceSnapshot, + double? subtotal, + String? syncStatus, + DateTime? createdAt, + DateTime? updatedAt, + }) => OrderItem( + localId: localId ?? this.localId, + serverId: serverId.present ? serverId.value : this.serverId, + orderId: orderId ?? this.orderId, + productVariantId: productVariantId ?? this.productVariantId, + quantity: quantity ?? this.quantity, + productImageSnapshot: productImageSnapshot.present + ? productImageSnapshot.value + : this.productImageSnapshot, + productImageUrlSnapshot: productImageUrlSnapshot.present + ? productImageUrlSnapshot.value + : this.productImageUrlSnapshot, + productNameSnapshot: productNameSnapshot ?? this.productNameSnapshot, + productVariantNameSnapshot: productVariantNameSnapshot.present + ? productVariantNameSnapshot.value + : this.productVariantNameSnapshot, + sellingPriceSnapshot: sellingPriceSnapshot ?? this.sellingPriceSnapshot, + subtotal: subtotal ?? this.subtotal, + syncStatus: syncStatus ?? this.syncStatus, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + ); + OrderItem copyWithCompanion(OrderItemsCompanion data) { + return OrderItem( + localId: data.localId.present ? data.localId.value : this.localId, + serverId: data.serverId.present ? data.serverId.value : this.serverId, + orderId: data.orderId.present ? data.orderId.value : this.orderId, + productVariantId: data.productVariantId.present + ? data.productVariantId.value + : this.productVariantId, + quantity: data.quantity.present ? data.quantity.value : this.quantity, + productImageSnapshot: data.productImageSnapshot.present + ? data.productImageSnapshot.value + : this.productImageSnapshot, + productImageUrlSnapshot: data.productImageUrlSnapshot.present + ? data.productImageUrlSnapshot.value + : this.productImageUrlSnapshot, + productNameSnapshot: data.productNameSnapshot.present + ? data.productNameSnapshot.value + : this.productNameSnapshot, + productVariantNameSnapshot: data.productVariantNameSnapshot.present + ? data.productVariantNameSnapshot.value + : this.productVariantNameSnapshot, + sellingPriceSnapshot: data.sellingPriceSnapshot.present + ? data.sellingPriceSnapshot.value + : this.sellingPriceSnapshot, + subtotal: data.subtotal.present ? data.subtotal.value : this.subtotal, + syncStatus: data.syncStatus.present + ? data.syncStatus.value + : this.syncStatus, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, + ); + } + + @override + String toString() { + return (StringBuffer('OrderItem(') + ..write('localId: $localId, ') + ..write('serverId: $serverId, ') + ..write('orderId: $orderId, ') + ..write('productVariantId: $productVariantId, ') + ..write('quantity: $quantity, ') + ..write('productImageSnapshot: $productImageSnapshot, ') + ..write('productImageUrlSnapshot: $productImageUrlSnapshot, ') + ..write('productNameSnapshot: $productNameSnapshot, ') + ..write('productVariantNameSnapshot: $productVariantNameSnapshot, ') + ..write('sellingPriceSnapshot: $sellingPriceSnapshot, ') + ..write('subtotal: $subtotal, ') + ..write('syncStatus: $syncStatus, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + localId, + serverId, + orderId, + productVariantId, + quantity, + productImageSnapshot, + productImageUrlSnapshot, + productNameSnapshot, + productVariantNameSnapshot, + sellingPriceSnapshot, + subtotal, + syncStatus, + createdAt, + updatedAt, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is OrderItem && + other.localId == this.localId && + other.serverId == this.serverId && + other.orderId == this.orderId && + other.productVariantId == this.productVariantId && + other.quantity == this.quantity && + other.productImageSnapshot == this.productImageSnapshot && + other.productImageUrlSnapshot == this.productImageUrlSnapshot && + other.productNameSnapshot == this.productNameSnapshot && + other.productVariantNameSnapshot == this.productVariantNameSnapshot && + other.sellingPriceSnapshot == this.sellingPriceSnapshot && + other.subtotal == this.subtotal && + other.syncStatus == this.syncStatus && + other.createdAt == this.createdAt && + other.updatedAt == this.updatedAt); +} + +class OrderItemsCompanion extends UpdateCompanion { + final Value localId; + final Value serverId; + final Value orderId; + final Value productVariantId; + final Value quantity; + final Value productImageSnapshot; + final Value productImageUrlSnapshot; + final Value productNameSnapshot; + final Value productVariantNameSnapshot; + final Value sellingPriceSnapshot; + final Value subtotal; + final Value syncStatus; + final Value createdAt; + final Value updatedAt; + final Value rowid; + const OrderItemsCompanion({ + this.localId = const Value.absent(), + this.serverId = const Value.absent(), + this.orderId = const Value.absent(), + this.productVariantId = const Value.absent(), + this.quantity = const Value.absent(), + this.productImageSnapshot = const Value.absent(), + this.productImageUrlSnapshot = const Value.absent(), + this.productNameSnapshot = const Value.absent(), + this.productVariantNameSnapshot = const Value.absent(), + this.sellingPriceSnapshot = const Value.absent(), + this.subtotal = const Value.absent(), + this.syncStatus = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.rowid = const Value.absent(), + }); + OrderItemsCompanion.insert({ + this.localId = const Value.absent(), + this.serverId = const Value.absent(), + required String orderId, + required String productVariantId, + this.quantity = const Value.absent(), + this.productImageSnapshot = const Value.absent(), + this.productImageUrlSnapshot = const Value.absent(), + required String productNameSnapshot, + this.productVariantNameSnapshot = const Value.absent(), + this.sellingPriceSnapshot = const Value.absent(), + this.subtotal = const Value.absent(), + this.syncStatus = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.rowid = const Value.absent(), + }) : orderId = Value(orderId), + productVariantId = Value(productVariantId), + productNameSnapshot = Value(productNameSnapshot); + static Insertable custom({ + Expression? localId, + Expression? serverId, + Expression? orderId, + Expression? productVariantId, + Expression? quantity, + Expression? productImageSnapshot, + Expression? productImageUrlSnapshot, + Expression? productNameSnapshot, + Expression? productVariantNameSnapshot, + Expression? sellingPriceSnapshot, + Expression? subtotal, + Expression? syncStatus, + Expression? createdAt, + Expression? updatedAt, + Expression? rowid, + }) { + return RawValuesInsertable({ + if (localId != null) 'local_id': localId, + if (serverId != null) 'server_id': serverId, + if (orderId != null) 'order_id': orderId, + if (productVariantId != null) 'product_variant_id': productVariantId, + if (quantity != null) 'quantity': quantity, + if (productImageSnapshot != null) + 'product_image_snapshot': productImageSnapshot, + if (productImageUrlSnapshot != null) + 'product_image_url_snapshot': productImageUrlSnapshot, + if (productNameSnapshot != null) + 'product_name_snapshot': productNameSnapshot, + if (productVariantNameSnapshot != null) + 'product_variant_name_snapshot': productVariantNameSnapshot, + if (sellingPriceSnapshot != null) + 'selling_price_snapshot': sellingPriceSnapshot, + if (subtotal != null) 'subtotal': subtotal, + if (syncStatus != null) 'sync_status': syncStatus, + if (createdAt != null) 'created_at': createdAt, + if (updatedAt != null) 'updated_at': updatedAt, + if (rowid != null) 'rowid': rowid, + }); + } + + OrderItemsCompanion copyWith({ + Value? localId, + Value? serverId, + Value? orderId, + Value? productVariantId, + Value? quantity, + Value? productImageSnapshot, + Value? productImageUrlSnapshot, + Value? productNameSnapshot, + Value? productVariantNameSnapshot, + Value? sellingPriceSnapshot, + Value? subtotal, + Value? syncStatus, + Value? createdAt, + Value? updatedAt, + Value? rowid, + }) { + return OrderItemsCompanion( + localId: localId ?? this.localId, + serverId: serverId ?? this.serverId, + orderId: orderId ?? this.orderId, + productVariantId: productVariantId ?? this.productVariantId, + quantity: quantity ?? this.quantity, + productImageSnapshot: productImageSnapshot ?? this.productImageSnapshot, + productImageUrlSnapshot: + productImageUrlSnapshot ?? this.productImageUrlSnapshot, + productNameSnapshot: productNameSnapshot ?? this.productNameSnapshot, + productVariantNameSnapshot: + productVariantNameSnapshot ?? this.productVariantNameSnapshot, + sellingPriceSnapshot: sellingPriceSnapshot ?? this.sellingPriceSnapshot, + subtotal: subtotal ?? this.subtotal, + syncStatus: syncStatus ?? this.syncStatus, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (localId.present) { + map['local_id'] = Variable(localId.value); + } + if (serverId.present) { + map['server_id'] = Variable(serverId.value); + } + if (orderId.present) { + map['order_id'] = Variable(orderId.value); + } + if (productVariantId.present) { + map['product_variant_id'] = Variable(productVariantId.value); + } + if (quantity.present) { + map['quantity'] = Variable(quantity.value); + } + if (productImageSnapshot.present) { + map['product_image_snapshot'] = Variable( + productImageSnapshot.value, + ); + } + if (productImageUrlSnapshot.present) { + map['product_image_url_snapshot'] = Variable( + productImageUrlSnapshot.value, + ); + } + if (productNameSnapshot.present) { + map['product_name_snapshot'] = Variable( + productNameSnapshot.value, + ); + } + if (productVariantNameSnapshot.present) { + map['product_variant_name_snapshot'] = Variable( + productVariantNameSnapshot.value, + ); + } + if (sellingPriceSnapshot.present) { + map['selling_price_snapshot'] = Variable( + sellingPriceSnapshot.value, + ); + } + if (subtotal.present) { + map['subtotal'] = Variable(subtotal.value); + } + if (syncStatus.present) { + map['sync_status'] = Variable(syncStatus.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (updatedAt.present) { + map['updated_at'] = Variable(updatedAt.value); + } + if (rowid.present) { + map['rowid'] = Variable(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('OrderItemsCompanion(') + ..write('localId: $localId, ') + ..write('serverId: $serverId, ') + ..write('orderId: $orderId, ') + ..write('productVariantId: $productVariantId, ') + ..write('quantity: $quantity, ') + ..write('productImageSnapshot: $productImageSnapshot, ') + ..write('productImageUrlSnapshot: $productImageUrlSnapshot, ') + ..write('productNameSnapshot: $productNameSnapshot, ') + ..write('productVariantNameSnapshot: $productVariantNameSnapshot, ') + ..write('sellingPriceSnapshot: $sellingPriceSnapshot, ') + ..write('subtotal: $subtotal, ') + ..write('syncStatus: $syncStatus, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + +class $SuppliersTable extends Suppliers + with TableInfo<$SuppliersTable, Supplier> { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + $SuppliersTable(this.attachedDatabase, [this._alias]); + static const VerificationMeta _localIdMeta = const VerificationMeta( + 'localId', + ); + @override + late final GeneratedColumn localId = GeneratedColumn( + 'local_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: false, + clientDefault: () => const Uuid().v7().toString(), + ); + static const VerificationMeta _serverIdMeta = const VerificationMeta( + 'serverId', + ); + @override + late final GeneratedColumn serverId = GeneratedColumn( + 'server_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _nameMeta = const VerificationMeta('name'); + @override + late final GeneratedColumn name = GeneratedColumn( + 'name', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + static const VerificationMeta _emailMeta = const VerificationMeta('email'); + @override + late final GeneratedColumn email = GeneratedColumn( + 'email', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _phoneNumberMeta = const VerificationMeta( + 'phoneNumber', + ); + @override + late final GeneratedColumn phoneNumber = GeneratedColumn( + 'phone_number', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _addressMeta = const VerificationMeta( + 'address', + ); + @override + late final GeneratedColumn address = GeneratedColumn( + 'address', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _syncStatusMeta = const VerificationMeta( + 'syncStatus', + ); + @override + late final GeneratedColumn syncStatus = GeneratedColumn( + 'sync_status', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultValue: const Constant('dirty'), + ); + static const VerificationMeta _createdAtMeta = const VerificationMeta( + 'createdAt', + ); + @override + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: currentDateAndTime, + ); + static const VerificationMeta _updatedAtMeta = const VerificationMeta( + 'updatedAt', + ); + @override + late final GeneratedColumn updatedAt = GeneratedColumn( + 'updated_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: currentDateAndTime, + ); + static const VerificationMeta _deletedAtMeta = const VerificationMeta( + 'deletedAt', + ); + @override + late final GeneratedColumn deletedAt = GeneratedColumn( + 'deleted_at', + aliasedName, + true, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + ); + @override + List get $columns => [ + localId, + serverId, + name, + email, + phoneNumber, + address, + syncStatus, + createdAt, + updatedAt, + deletedAt, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'suppliers'; + @override + VerificationContext validateIntegrity( + Insertable instance, { + bool isInserting = false, + }) { + final context = VerificationContext(); + final data = instance.toColumns(true); + if (data.containsKey('local_id')) { + context.handle( + _localIdMeta, + localId.isAcceptableOrUnknown(data['local_id']!, _localIdMeta), + ); + } + if (data.containsKey('server_id')) { + context.handle( + _serverIdMeta, + serverId.isAcceptableOrUnknown(data['server_id']!, _serverIdMeta), + ); + } + if (data.containsKey('name')) { + context.handle( + _nameMeta, + name.isAcceptableOrUnknown(data['name']!, _nameMeta), + ); + } else if (isInserting) { + context.missing(_nameMeta); + } + if (data.containsKey('email')) { + context.handle( + _emailMeta, + email.isAcceptableOrUnknown(data['email']!, _emailMeta), + ); + } + if (data.containsKey('phone_number')) { + context.handle( + _phoneNumberMeta, + phoneNumber.isAcceptableOrUnknown( + data['phone_number']!, + _phoneNumberMeta, + ), + ); + } + if (data.containsKey('address')) { + context.handle( + _addressMeta, + address.isAcceptableOrUnknown(data['address']!, _addressMeta), + ); + } + if (data.containsKey('sync_status')) { + context.handle( + _syncStatusMeta, + syncStatus.isAcceptableOrUnknown(data['sync_status']!, _syncStatusMeta), + ); + } + if (data.containsKey('created_at')) { + context.handle( + _createdAtMeta, + createdAt.isAcceptableOrUnknown(data['created_at']!, _createdAtMeta), + ); + } + if (data.containsKey('updated_at')) { + context.handle( + _updatedAtMeta, + updatedAt.isAcceptableOrUnknown(data['updated_at']!, _updatedAtMeta), + ); + } + if (data.containsKey('deleted_at')) { + context.handle( + _deletedAtMeta, + deletedAt.isAcceptableOrUnknown(data['deleted_at']!, _deletedAtMeta), + ); + } + return context; + } + + @override + Set get $primaryKey => {localId}; + @override + Supplier map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return Supplier( + localId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}local_id'], + )!, + serverId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}server_id'], + ), + name: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}name'], + )!, + email: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}email'], + ), + phoneNumber: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}phone_number'], + ), + address: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}address'], + ), + syncStatus: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}sync_status'], + )!, + createdAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}created_at'], + )!, + updatedAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}updated_at'], + )!, + deletedAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}deleted_at'], + ), + ); + } + + @override + $SuppliersTable createAlias(String alias) { + return $SuppliersTable(attachedDatabase, alias); + } +} + +class Supplier extends DataClass implements Insertable { + final String localId; + final String? serverId; + final String name; + final String? email; + final String? phoneNumber; + final String? address; + final String syncStatus; + final DateTime createdAt; + final DateTime updatedAt; + final DateTime? deletedAt; + const Supplier({ + required this.localId, + this.serverId, + required this.name, + this.email, + this.phoneNumber, + this.address, + required this.syncStatus, + required this.createdAt, + required this.updatedAt, + this.deletedAt, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['local_id'] = Variable(localId); + if (!nullToAbsent || serverId != null) { + map['server_id'] = Variable(serverId); + } + map['name'] = Variable(name); + if (!nullToAbsent || email != null) { + map['email'] = Variable(email); + } + if (!nullToAbsent || phoneNumber != null) { + map['phone_number'] = Variable(phoneNumber); + } + if (!nullToAbsent || address != null) { + map['address'] = Variable(address); + } + map['sync_status'] = Variable(syncStatus); + map['created_at'] = Variable(createdAt); + map['updated_at'] = Variable(updatedAt); + if (!nullToAbsent || deletedAt != null) { + map['deleted_at'] = Variable(deletedAt); + } + return map; + } + + SuppliersCompanion toCompanion(bool nullToAbsent) { + return SuppliersCompanion( + localId: Value(localId), + serverId: serverId == null && nullToAbsent + ? const Value.absent() + : Value(serverId), + name: Value(name), + email: email == null && nullToAbsent + ? const Value.absent() + : Value(email), + phoneNumber: phoneNumber == null && nullToAbsent + ? const Value.absent() + : Value(phoneNumber), + address: address == null && nullToAbsent + ? const Value.absent() + : Value(address), + syncStatus: Value(syncStatus), + createdAt: Value(createdAt), + updatedAt: Value(updatedAt), + deletedAt: deletedAt == null && nullToAbsent + ? const Value.absent() + : Value(deletedAt), + ); + } + + factory Supplier.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return Supplier( + localId: serializer.fromJson(json['localId']), + serverId: serializer.fromJson(json['serverId']), + name: serializer.fromJson(json['name']), + email: serializer.fromJson(json['email']), + phoneNumber: serializer.fromJson(json['phoneNumber']), + address: serializer.fromJson(json['address']), + syncStatus: serializer.fromJson(json['syncStatus']), + createdAt: serializer.fromJson(json['createdAt']), + updatedAt: serializer.fromJson(json['updatedAt']), + deletedAt: serializer.fromJson(json['deletedAt']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'localId': serializer.toJson(localId), + 'serverId': serializer.toJson(serverId), + 'name': serializer.toJson(name), + 'email': serializer.toJson(email), + 'phoneNumber': serializer.toJson(phoneNumber), + 'address': serializer.toJson(address), + 'syncStatus': serializer.toJson(syncStatus), + 'createdAt': serializer.toJson(createdAt), + 'updatedAt': serializer.toJson(updatedAt), + 'deletedAt': serializer.toJson(deletedAt), + }; + } + + Supplier copyWith({ + String? localId, + Value serverId = const Value.absent(), + String? name, + Value email = const Value.absent(), + Value phoneNumber = const Value.absent(), + Value address = const Value.absent(), + String? syncStatus, + DateTime? createdAt, + DateTime? updatedAt, + Value deletedAt = const Value.absent(), + }) => Supplier( + localId: localId ?? this.localId, + serverId: serverId.present ? serverId.value : this.serverId, + name: name ?? this.name, + email: email.present ? email.value : this.email, + phoneNumber: phoneNumber.present ? phoneNumber.value : this.phoneNumber, + address: address.present ? address.value : this.address, + syncStatus: syncStatus ?? this.syncStatus, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + deletedAt: deletedAt.present ? deletedAt.value : this.deletedAt, + ); + Supplier copyWithCompanion(SuppliersCompanion data) { + return Supplier( + localId: data.localId.present ? data.localId.value : this.localId, + serverId: data.serverId.present ? data.serverId.value : this.serverId, + name: data.name.present ? data.name.value : this.name, + email: data.email.present ? data.email.value : this.email, + phoneNumber: data.phoneNumber.present + ? data.phoneNumber.value + : this.phoneNumber, + address: data.address.present ? data.address.value : this.address, + syncStatus: data.syncStatus.present + ? data.syncStatus.value + : this.syncStatus, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, + deletedAt: data.deletedAt.present ? data.deletedAt.value : this.deletedAt, + ); + } + + @override + String toString() { + return (StringBuffer('Supplier(') + ..write('localId: $localId, ') + ..write('serverId: $serverId, ') + ..write('name: $name, ') + ..write('email: $email, ') + ..write('phoneNumber: $phoneNumber, ') + ..write('address: $address, ') + ..write('syncStatus: $syncStatus, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('deletedAt: $deletedAt') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + localId, + serverId, + name, + email, + phoneNumber, + address, + syncStatus, + createdAt, + updatedAt, + deletedAt, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is Supplier && + other.localId == this.localId && + other.serverId == this.serverId && + other.name == this.name && + other.email == this.email && + other.phoneNumber == this.phoneNumber && + other.address == this.address && + other.syncStatus == this.syncStatus && + other.createdAt == this.createdAt && + other.updatedAt == this.updatedAt && + other.deletedAt == this.deletedAt); +} + +class SuppliersCompanion extends UpdateCompanion { + final Value localId; + final Value serverId; + final Value name; + final Value email; + final Value phoneNumber; + final Value address; + final Value syncStatus; + final Value createdAt; + final Value updatedAt; + final Value deletedAt; + final Value rowid; + const SuppliersCompanion({ + this.localId = const Value.absent(), + this.serverId = const Value.absent(), + this.name = const Value.absent(), + this.email = const Value.absent(), + this.phoneNumber = const Value.absent(), + this.address = const Value.absent(), + this.syncStatus = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.deletedAt = const Value.absent(), + this.rowid = const Value.absent(), + }); + SuppliersCompanion.insert({ + this.localId = const Value.absent(), + this.serverId = const Value.absent(), + required String name, + this.email = const Value.absent(), + this.phoneNumber = const Value.absent(), + this.address = const Value.absent(), + this.syncStatus = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.deletedAt = const Value.absent(), + this.rowid = const Value.absent(), + }) : name = Value(name); + static Insertable custom({ + Expression? localId, + Expression? serverId, + Expression? name, + Expression? email, + Expression? phoneNumber, + Expression? address, + Expression? syncStatus, + Expression? createdAt, + Expression? updatedAt, + Expression? deletedAt, + Expression? rowid, + }) { + return RawValuesInsertable({ + if (localId != null) 'local_id': localId, + if (serverId != null) 'server_id': serverId, + if (name != null) 'name': name, + if (email != null) 'email': email, + if (phoneNumber != null) 'phone_number': phoneNumber, + if (address != null) 'address': address, + if (syncStatus != null) 'sync_status': syncStatus, + if (createdAt != null) 'created_at': createdAt, + if (updatedAt != null) 'updated_at': updatedAt, + if (deletedAt != null) 'deleted_at': deletedAt, + if (rowid != null) 'rowid': rowid, + }); + } + + SuppliersCompanion copyWith({ + Value? localId, + Value? serverId, + Value? name, + Value? email, + Value? phoneNumber, + Value? address, + Value? syncStatus, + Value? createdAt, + Value? updatedAt, + Value? deletedAt, + Value? rowid, + }) { + return SuppliersCompanion( + localId: localId ?? this.localId, + serverId: serverId ?? this.serverId, + name: name ?? this.name, + email: email ?? this.email, + phoneNumber: phoneNumber ?? this.phoneNumber, + address: address ?? this.address, + syncStatus: syncStatus ?? this.syncStatus, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + deletedAt: deletedAt ?? this.deletedAt, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (localId.present) { + map['local_id'] = Variable(localId.value); + } + if (serverId.present) { + map['server_id'] = Variable(serverId.value); + } + if (name.present) { + map['name'] = Variable(name.value); + } + if (email.present) { + map['email'] = Variable(email.value); + } + if (phoneNumber.present) { + map['phone_number'] = Variable(phoneNumber.value); + } + if (address.present) { + map['address'] = Variable(address.value); + } + if (syncStatus.present) { + map['sync_status'] = Variable(syncStatus.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (updatedAt.present) { + map['updated_at'] = Variable(updatedAt.value); + } + if (deletedAt.present) { + map['deleted_at'] = Variable(deletedAt.value); + } + if (rowid.present) { + map['rowid'] = Variable(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('SuppliersCompanion(') + ..write('localId: $localId, ') + ..write('serverId: $serverId, ') + ..write('name: $name, ') + ..write('email: $email, ') + ..write('phoneNumber: $phoneNumber, ') + ..write('address: $address, ') + ..write('syncStatus: $syncStatus, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('deletedAt: $deletedAt, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + +class $PurchasesTable extends Purchases + with TableInfo<$PurchasesTable, Purchase> { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + $PurchasesTable(this.attachedDatabase, [this._alias]); + static const VerificationMeta _localIdMeta = const VerificationMeta( + 'localId', + ); + @override + late final GeneratedColumn localId = GeneratedColumn( + 'local_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: false, + clientDefault: () => const Uuid().v7().toString(), + ); + static const VerificationMeta _serverIdMeta = const VerificationMeta( + 'serverId', + ); + @override + late final GeneratedColumn serverId = GeneratedColumn( + 'server_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _purchaseNumberMeta = const VerificationMeta( + 'purchaseNumber', + ); + @override + late final GeneratedColumn purchaseNumber = GeneratedColumn( + 'purchase_number', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways('UNIQUE'), + ); + static const VerificationMeta _outletIdMeta = const VerificationMeta( + 'outletId', + ); + @override + late final GeneratedColumn outletId = GeneratedColumn( + 'outlet_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES outlets (local_id)', + ), + ); + static const VerificationMeta _outletNameSnapshotMeta = + const VerificationMeta('outletNameSnapshot'); + @override + late final GeneratedColumn outletNameSnapshot = + GeneratedColumn( + 'outlet_name_snapshot', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + static const VerificationMeta _supplierIdMeta = const VerificationMeta( + 'supplierId', + ); + @override + late final GeneratedColumn supplierId = GeneratedColumn( + 'supplier_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES suppliers (local_id)', + ), + ); + static const VerificationMeta _supplierNameSnapshotMeta = + const VerificationMeta('supplierNameSnapshot'); + @override + late final GeneratedColumn supplierNameSnapshot = + GeneratedColumn( + 'supplier_name_snapshot', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _supplierEmailSnapshotMeta = + const VerificationMeta('supplierEmailSnapshot'); + @override + late final GeneratedColumn supplierEmailSnapshot = + GeneratedColumn( + 'supplier_email_snapshot', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _supplierPhoneNumberSnapshotMeta = + const VerificationMeta('supplierPhoneNumberSnapshot'); + @override + late final GeneratedColumn supplierPhoneNumberSnapshot = + GeneratedColumn( + 'supplier_phone_number_snapshot', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _userIdMeta = const VerificationMeta('userId'); + @override + late final GeneratedColumn userId = GeneratedColumn( + 'user_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES users (local_id)', + ), + ); + static const VerificationMeta _adminNameSnapshotMeta = const VerificationMeta( + 'adminNameSnapshot', + ); + @override + late final GeneratedColumn adminNameSnapshot = + GeneratedColumn( + 'admin_name_snapshot', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + static const VerificationMeta _purchaseStatusMeta = const VerificationMeta( + 'purchaseStatus', + ); + @override + late final GeneratedColumn purchaseStatus = GeneratedColumn( + 'purchase_status', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + static const VerificationMeta _totalPurchaseMeta = const VerificationMeta( + 'totalPurchase', + ); + @override + late final GeneratedColumn totalPurchase = GeneratedColumn( + 'total_purchase', + aliasedName, + false, + type: DriftSqlType.double, + requiredDuringInsert: false, + defaultValue: const Constant(0.0), + ); + static const VerificationMeta _discountMeta = const VerificationMeta( + 'discount', + ); + @override + late final GeneratedColumn discount = GeneratedColumn( + 'discount', + aliasedName, + false, + type: DriftSqlType.double, + requiredDuringInsert: false, + defaultValue: const Constant(0.0), + ); + static const VerificationMeta _taxMeta = const VerificationMeta('tax'); + @override + late final GeneratedColumn tax = GeneratedColumn( + 'tax', + aliasedName, + false, + type: DriftSqlType.double, + requiredDuringInsert: false, + defaultValue: const Constant(0.0), + ); + static const VerificationMeta _paymentStatusMeta = const VerificationMeta( + 'paymentStatus', + ); + @override + late final GeneratedColumn paymentStatus = GeneratedColumn( + 'payment_status', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + static const VerificationMeta _totalAmountMeta = const VerificationMeta( + 'totalAmount', + ); + @override + late final GeneratedColumn totalAmount = GeneratedColumn( + 'total_amount', + aliasedName, + false, + type: DriftSqlType.double, + requiredDuringInsert: false, + defaultValue: const Constant(0.0), + ); + static const VerificationMeta _amountPaidMeta = const VerificationMeta( + 'amountPaid', + ); + @override + late final GeneratedColumn amountPaid = GeneratedColumn( + 'amount_paid', + aliasedName, + false, + type: DriftSqlType.double, + requiredDuringInsert: false, + defaultValue: const Constant(0.0), + ); + static const VerificationMeta _changeAmountMeta = const VerificationMeta( + 'changeAmount', + ); + @override + late final GeneratedColumn changeAmount = GeneratedColumn( + 'change_amount', + aliasedName, + false, + type: DriftSqlType.double, + requiredDuringInsert: false, + defaultValue: const Constant(0.0), + ); + static const VerificationMeta _underPaymentAmountMeta = + const VerificationMeta('underPaymentAmount'); + @override + late final GeneratedColumn underPaymentAmount = + GeneratedColumn( + 'under_payment_amount', + aliasedName, + false, + type: DriftSqlType.double, + requiredDuringInsert: false, + defaultValue: const Constant(0.0), + ); + static const VerificationMeta _debtAmountMeta = const VerificationMeta( + 'debtAmount', + ); + @override + late final GeneratedColumn debtAmount = GeneratedColumn( + 'debt_amount', + aliasedName, + false, + type: DriftSqlType.double, + requiredDuringInsert: false, + defaultValue: const Constant(0.0), + ); + static const VerificationMeta _paymentProofPathMeta = const VerificationMeta( + 'paymentProofPath', + ); + @override + late final GeneratedColumn paymentProofPath = GeneratedColumn( + 'payment_proof_path', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _paymentProofUrlMeta = const VerificationMeta( + 'paymentProofUrl', + ); + @override + late final GeneratedColumn paymentProofUrl = GeneratedColumn( + 'payment_proof_url', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _notesMeta = const VerificationMeta('notes'); + @override + late final GeneratedColumn notes = GeneratedColumn( + 'notes', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _syncStatusMeta = const VerificationMeta( + 'syncStatus', + ); + @override + late final GeneratedColumn syncStatus = GeneratedColumn( + 'sync_status', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultValue: const Constant('dirty'), + ); + static const VerificationMeta _createdAtMeta = const VerificationMeta( + 'createdAt', + ); + @override + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: currentDateAndTime, + ); + static const VerificationMeta _updatedAtMeta = const VerificationMeta( + 'updatedAt', + ); + @override + late final GeneratedColumn updatedAt = GeneratedColumn( + 'updated_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: currentDateAndTime, + ); + @override + List get $columns => [ + localId, + serverId, + purchaseNumber, + outletId, + outletNameSnapshot, + supplierId, + supplierNameSnapshot, + supplierEmailSnapshot, + supplierPhoneNumberSnapshot, + userId, + adminNameSnapshot, + purchaseStatus, + totalPurchase, + discount, + tax, + paymentStatus, + totalAmount, + amountPaid, + changeAmount, + underPaymentAmount, + debtAmount, + paymentProofPath, + paymentProofUrl, + notes, + syncStatus, + createdAt, + updatedAt, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'purchases'; + @override + VerificationContext validateIntegrity( + Insertable instance, { + bool isInserting = false, + }) { + final context = VerificationContext(); + final data = instance.toColumns(true); + if (data.containsKey('local_id')) { + context.handle( + _localIdMeta, + localId.isAcceptableOrUnknown(data['local_id']!, _localIdMeta), + ); + } + if (data.containsKey('server_id')) { + context.handle( + _serverIdMeta, + serverId.isAcceptableOrUnknown(data['server_id']!, _serverIdMeta), + ); + } + if (data.containsKey('purchase_number')) { + context.handle( + _purchaseNumberMeta, + purchaseNumber.isAcceptableOrUnknown( + data['purchase_number']!, + _purchaseNumberMeta, + ), + ); + } else if (isInserting) { + context.missing(_purchaseNumberMeta); + } + if (data.containsKey('outlet_id')) { + context.handle( + _outletIdMeta, + outletId.isAcceptableOrUnknown(data['outlet_id']!, _outletIdMeta), + ); + } else if (isInserting) { + context.missing(_outletIdMeta); + } + if (data.containsKey('outlet_name_snapshot')) { + context.handle( + _outletNameSnapshotMeta, + outletNameSnapshot.isAcceptableOrUnknown( + data['outlet_name_snapshot']!, + _outletNameSnapshotMeta, + ), + ); + } else if (isInserting) { + context.missing(_outletNameSnapshotMeta); + } + if (data.containsKey('supplier_id')) { + context.handle( + _supplierIdMeta, + supplierId.isAcceptableOrUnknown(data['supplier_id']!, _supplierIdMeta), + ); + } + if (data.containsKey('supplier_name_snapshot')) { + context.handle( + _supplierNameSnapshotMeta, + supplierNameSnapshot.isAcceptableOrUnknown( + data['supplier_name_snapshot']!, + _supplierNameSnapshotMeta, + ), + ); + } + if (data.containsKey('supplier_email_snapshot')) { + context.handle( + _supplierEmailSnapshotMeta, + supplierEmailSnapshot.isAcceptableOrUnknown( + data['supplier_email_snapshot']!, + _supplierEmailSnapshotMeta, + ), + ); + } + if (data.containsKey('supplier_phone_number_snapshot')) { + context.handle( + _supplierPhoneNumberSnapshotMeta, + supplierPhoneNumberSnapshot.isAcceptableOrUnknown( + data['supplier_phone_number_snapshot']!, + _supplierPhoneNumberSnapshotMeta, + ), + ); + } + if (data.containsKey('user_id')) { + context.handle( + _userIdMeta, + userId.isAcceptableOrUnknown(data['user_id']!, _userIdMeta), + ); + } else if (isInserting) { + context.missing(_userIdMeta); + } + if (data.containsKey('admin_name_snapshot')) { + context.handle( + _adminNameSnapshotMeta, + adminNameSnapshot.isAcceptableOrUnknown( + data['admin_name_snapshot']!, + _adminNameSnapshotMeta, + ), + ); + } else if (isInserting) { + context.missing(_adminNameSnapshotMeta); + } + if (data.containsKey('purchase_status')) { + context.handle( + _purchaseStatusMeta, + purchaseStatus.isAcceptableOrUnknown( + data['purchase_status']!, + _purchaseStatusMeta, + ), + ); + } else if (isInserting) { + context.missing(_purchaseStatusMeta); + } + if (data.containsKey('total_purchase')) { + context.handle( + _totalPurchaseMeta, + totalPurchase.isAcceptableOrUnknown( + data['total_purchase']!, + _totalPurchaseMeta, + ), + ); + } + if (data.containsKey('discount')) { + context.handle( + _discountMeta, + discount.isAcceptableOrUnknown(data['discount']!, _discountMeta), + ); + } + if (data.containsKey('tax')) { + context.handle( + _taxMeta, + tax.isAcceptableOrUnknown(data['tax']!, _taxMeta), + ); + } + if (data.containsKey('payment_status')) { + context.handle( + _paymentStatusMeta, + paymentStatus.isAcceptableOrUnknown( + data['payment_status']!, + _paymentStatusMeta, + ), + ); + } else if (isInserting) { + context.missing(_paymentStatusMeta); + } + if (data.containsKey('total_amount')) { + context.handle( + _totalAmountMeta, + totalAmount.isAcceptableOrUnknown( + data['total_amount']!, + _totalAmountMeta, + ), + ); + } + if (data.containsKey('amount_paid')) { + context.handle( + _amountPaidMeta, + amountPaid.isAcceptableOrUnknown(data['amount_paid']!, _amountPaidMeta), + ); + } + if (data.containsKey('change_amount')) { + context.handle( + _changeAmountMeta, + changeAmount.isAcceptableOrUnknown( + data['change_amount']!, + _changeAmountMeta, + ), + ); + } + if (data.containsKey('under_payment_amount')) { + context.handle( + _underPaymentAmountMeta, + underPaymentAmount.isAcceptableOrUnknown( + data['under_payment_amount']!, + _underPaymentAmountMeta, + ), + ); + } + if (data.containsKey('debt_amount')) { + context.handle( + _debtAmountMeta, + debtAmount.isAcceptableOrUnknown(data['debt_amount']!, _debtAmountMeta), + ); + } + if (data.containsKey('payment_proof_path')) { + context.handle( + _paymentProofPathMeta, + paymentProofPath.isAcceptableOrUnknown( + data['payment_proof_path']!, + _paymentProofPathMeta, + ), + ); + } + if (data.containsKey('payment_proof_url')) { + context.handle( + _paymentProofUrlMeta, + paymentProofUrl.isAcceptableOrUnknown( + data['payment_proof_url']!, + _paymentProofUrlMeta, + ), + ); + } + if (data.containsKey('notes')) { + context.handle( + _notesMeta, + notes.isAcceptableOrUnknown(data['notes']!, _notesMeta), + ); + } + if (data.containsKey('sync_status')) { + context.handle( + _syncStatusMeta, + syncStatus.isAcceptableOrUnknown(data['sync_status']!, _syncStatusMeta), + ); + } + if (data.containsKey('created_at')) { + context.handle( + _createdAtMeta, + createdAt.isAcceptableOrUnknown(data['created_at']!, _createdAtMeta), + ); + } + if (data.containsKey('updated_at')) { + context.handle( + _updatedAtMeta, + updatedAt.isAcceptableOrUnknown(data['updated_at']!, _updatedAtMeta), + ); + } + return context; + } + + @override + Set get $primaryKey => {localId}; + @override + Purchase map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return Purchase( + localId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}local_id'], + )!, + serverId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}server_id'], + ), + purchaseNumber: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}purchase_number'], + )!, + outletId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}outlet_id'], + )!, + outletNameSnapshot: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}outlet_name_snapshot'], + )!, + supplierId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}supplier_id'], + ), + supplierNameSnapshot: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}supplier_name_snapshot'], + ), + supplierEmailSnapshot: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}supplier_email_snapshot'], + ), + supplierPhoneNumberSnapshot: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}supplier_phone_number_snapshot'], + ), + userId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}user_id'], + )!, + adminNameSnapshot: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}admin_name_snapshot'], + )!, + purchaseStatus: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}purchase_status'], + )!, + totalPurchase: attachedDatabase.typeMapping.read( + DriftSqlType.double, + data['${effectivePrefix}total_purchase'], + )!, + discount: attachedDatabase.typeMapping.read( + DriftSqlType.double, + data['${effectivePrefix}discount'], + )!, + tax: attachedDatabase.typeMapping.read( + DriftSqlType.double, + data['${effectivePrefix}tax'], + )!, + paymentStatus: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}payment_status'], + )!, + totalAmount: attachedDatabase.typeMapping.read( + DriftSqlType.double, + data['${effectivePrefix}total_amount'], + )!, + amountPaid: attachedDatabase.typeMapping.read( + DriftSqlType.double, + data['${effectivePrefix}amount_paid'], + )!, + changeAmount: attachedDatabase.typeMapping.read( + DriftSqlType.double, + data['${effectivePrefix}change_amount'], + )!, + underPaymentAmount: attachedDatabase.typeMapping.read( + DriftSqlType.double, + data['${effectivePrefix}under_payment_amount'], + )!, + debtAmount: attachedDatabase.typeMapping.read( + DriftSqlType.double, + data['${effectivePrefix}debt_amount'], + )!, + paymentProofPath: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}payment_proof_path'], + ), + paymentProofUrl: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}payment_proof_url'], + ), + notes: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}notes'], + ), + syncStatus: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}sync_status'], + )!, + createdAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}created_at'], + )!, + updatedAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}updated_at'], + )!, + ); + } + + @override + $PurchasesTable createAlias(String alias) { + return $PurchasesTable(attachedDatabase, alias); + } +} + +class Purchase extends DataClass implements Insertable { + final String localId; + final String? serverId; + final String purchaseNumber; + final String outletId; + final String outletNameSnapshot; + final String? supplierId; + final String? supplierNameSnapshot; + final String? supplierEmailSnapshot; + final String? supplierPhoneNumberSnapshot; + final String userId; + final String adminNameSnapshot; + final String purchaseStatus; + final double totalPurchase; + final double discount; + final double tax; + final String paymentStatus; + final double totalAmount; + final double amountPaid; + final double changeAmount; + final double underPaymentAmount; + final double debtAmount; + final String? paymentProofPath; + final String? paymentProofUrl; + final String? notes; + final String syncStatus; + final DateTime createdAt; + final DateTime updatedAt; + const Purchase({ + required this.localId, + this.serverId, + required this.purchaseNumber, + required this.outletId, + required this.outletNameSnapshot, + this.supplierId, + this.supplierNameSnapshot, + this.supplierEmailSnapshot, + this.supplierPhoneNumberSnapshot, + required this.userId, + required this.adminNameSnapshot, + required this.purchaseStatus, + required this.totalPurchase, + required this.discount, + required this.tax, + required this.paymentStatus, + required this.totalAmount, + required this.amountPaid, + required this.changeAmount, + required this.underPaymentAmount, + required this.debtAmount, + this.paymentProofPath, + this.paymentProofUrl, + this.notes, + required this.syncStatus, + required this.createdAt, + required this.updatedAt, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['local_id'] = Variable(localId); + if (!nullToAbsent || serverId != null) { + map['server_id'] = Variable(serverId); + } + map['purchase_number'] = Variable(purchaseNumber); + map['outlet_id'] = Variable(outletId); + map['outlet_name_snapshot'] = Variable(outletNameSnapshot); + if (!nullToAbsent || supplierId != null) { + map['supplier_id'] = Variable(supplierId); + } + if (!nullToAbsent || supplierNameSnapshot != null) { + map['supplier_name_snapshot'] = Variable(supplierNameSnapshot); + } + if (!nullToAbsent || supplierEmailSnapshot != null) { + map['supplier_email_snapshot'] = Variable(supplierEmailSnapshot); + } + if (!nullToAbsent || supplierPhoneNumberSnapshot != null) { + map['supplier_phone_number_snapshot'] = Variable( + supplierPhoneNumberSnapshot, + ); + } + map['user_id'] = Variable(userId); + map['admin_name_snapshot'] = Variable(adminNameSnapshot); + map['purchase_status'] = Variable(purchaseStatus); + map['total_purchase'] = Variable(totalPurchase); + map['discount'] = Variable(discount); + map['tax'] = Variable(tax); + map['payment_status'] = Variable(paymentStatus); + map['total_amount'] = Variable(totalAmount); + map['amount_paid'] = Variable(amountPaid); + map['change_amount'] = Variable(changeAmount); + map['under_payment_amount'] = Variable(underPaymentAmount); + map['debt_amount'] = Variable(debtAmount); + if (!nullToAbsent || paymentProofPath != null) { + map['payment_proof_path'] = Variable(paymentProofPath); + } + if (!nullToAbsent || paymentProofUrl != null) { + map['payment_proof_url'] = Variable(paymentProofUrl); + } + if (!nullToAbsent || notes != null) { + map['notes'] = Variable(notes); + } + map['sync_status'] = Variable(syncStatus); + map['created_at'] = Variable(createdAt); + map['updated_at'] = Variable(updatedAt); + return map; + } + + PurchasesCompanion toCompanion(bool nullToAbsent) { + return PurchasesCompanion( + localId: Value(localId), + serverId: serverId == null && nullToAbsent + ? const Value.absent() + : Value(serverId), + purchaseNumber: Value(purchaseNumber), + outletId: Value(outletId), + outletNameSnapshot: Value(outletNameSnapshot), + supplierId: supplierId == null && nullToAbsent + ? const Value.absent() + : Value(supplierId), + supplierNameSnapshot: supplierNameSnapshot == null && nullToAbsent + ? const Value.absent() + : Value(supplierNameSnapshot), + supplierEmailSnapshot: supplierEmailSnapshot == null && nullToAbsent + ? const Value.absent() + : Value(supplierEmailSnapshot), + supplierPhoneNumberSnapshot: + supplierPhoneNumberSnapshot == null && nullToAbsent + ? const Value.absent() + : Value(supplierPhoneNumberSnapshot), + userId: Value(userId), + adminNameSnapshot: Value(adminNameSnapshot), + purchaseStatus: Value(purchaseStatus), + totalPurchase: Value(totalPurchase), + discount: Value(discount), + tax: Value(tax), + paymentStatus: Value(paymentStatus), + totalAmount: Value(totalAmount), + amountPaid: Value(amountPaid), + changeAmount: Value(changeAmount), + underPaymentAmount: Value(underPaymentAmount), + debtAmount: Value(debtAmount), + paymentProofPath: paymentProofPath == null && nullToAbsent + ? const Value.absent() + : Value(paymentProofPath), + paymentProofUrl: paymentProofUrl == null && nullToAbsent + ? const Value.absent() + : Value(paymentProofUrl), + notes: notes == null && nullToAbsent + ? const Value.absent() + : Value(notes), + syncStatus: Value(syncStatus), + createdAt: Value(createdAt), + updatedAt: Value(updatedAt), + ); + } + + factory Purchase.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return Purchase( + localId: serializer.fromJson(json['localId']), + serverId: serializer.fromJson(json['serverId']), + purchaseNumber: serializer.fromJson(json['purchaseNumber']), + outletId: serializer.fromJson(json['outletId']), + outletNameSnapshot: serializer.fromJson( + json['outletNameSnapshot'], + ), + supplierId: serializer.fromJson(json['supplierId']), + supplierNameSnapshot: serializer.fromJson( + json['supplierNameSnapshot'], + ), + supplierEmailSnapshot: serializer.fromJson( + json['supplierEmailSnapshot'], + ), + supplierPhoneNumberSnapshot: serializer.fromJson( + json['supplierPhoneNumberSnapshot'], + ), + userId: serializer.fromJson(json['userId']), + adminNameSnapshot: serializer.fromJson(json['adminNameSnapshot']), + purchaseStatus: serializer.fromJson(json['purchaseStatus']), + totalPurchase: serializer.fromJson(json['totalPurchase']), + discount: serializer.fromJson(json['discount']), + tax: serializer.fromJson(json['tax']), + paymentStatus: serializer.fromJson(json['paymentStatus']), + totalAmount: serializer.fromJson(json['totalAmount']), + amountPaid: serializer.fromJson(json['amountPaid']), + changeAmount: serializer.fromJson(json['changeAmount']), + underPaymentAmount: serializer.fromJson( + json['underPaymentAmount'], + ), + debtAmount: serializer.fromJson(json['debtAmount']), + paymentProofPath: serializer.fromJson(json['paymentProofPath']), + paymentProofUrl: serializer.fromJson(json['paymentProofUrl']), + notes: serializer.fromJson(json['notes']), + syncStatus: serializer.fromJson(json['syncStatus']), + createdAt: serializer.fromJson(json['createdAt']), + updatedAt: serializer.fromJson(json['updatedAt']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'localId': serializer.toJson(localId), + 'serverId': serializer.toJson(serverId), + 'purchaseNumber': serializer.toJson(purchaseNumber), + 'outletId': serializer.toJson(outletId), + 'outletNameSnapshot': serializer.toJson(outletNameSnapshot), + 'supplierId': serializer.toJson(supplierId), + 'supplierNameSnapshot': serializer.toJson(supplierNameSnapshot), + 'supplierEmailSnapshot': serializer.toJson( + supplierEmailSnapshot, + ), + 'supplierPhoneNumberSnapshot': serializer.toJson( + supplierPhoneNumberSnapshot, + ), + 'userId': serializer.toJson(userId), + 'adminNameSnapshot': serializer.toJson(adminNameSnapshot), + 'purchaseStatus': serializer.toJson(purchaseStatus), + 'totalPurchase': serializer.toJson(totalPurchase), + 'discount': serializer.toJson(discount), + 'tax': serializer.toJson(tax), + 'paymentStatus': serializer.toJson(paymentStatus), + 'totalAmount': serializer.toJson(totalAmount), + 'amountPaid': serializer.toJson(amountPaid), + 'changeAmount': serializer.toJson(changeAmount), + 'underPaymentAmount': serializer.toJson(underPaymentAmount), + 'debtAmount': serializer.toJson(debtAmount), + 'paymentProofPath': serializer.toJson(paymentProofPath), + 'paymentProofUrl': serializer.toJson(paymentProofUrl), + 'notes': serializer.toJson(notes), + 'syncStatus': serializer.toJson(syncStatus), + 'createdAt': serializer.toJson(createdAt), + 'updatedAt': serializer.toJson(updatedAt), + }; + } + + Purchase copyWith({ + String? localId, + Value serverId = const Value.absent(), + String? purchaseNumber, + String? outletId, + String? outletNameSnapshot, + Value supplierId = const Value.absent(), + Value supplierNameSnapshot = const Value.absent(), + Value supplierEmailSnapshot = const Value.absent(), + Value supplierPhoneNumberSnapshot = const Value.absent(), + String? userId, + String? adminNameSnapshot, + String? purchaseStatus, + double? totalPurchase, + double? discount, + double? tax, + String? paymentStatus, + double? totalAmount, + double? amountPaid, + double? changeAmount, + double? underPaymentAmount, + double? debtAmount, + Value paymentProofPath = const Value.absent(), + Value paymentProofUrl = const Value.absent(), + Value notes = const Value.absent(), + String? syncStatus, + DateTime? createdAt, + DateTime? updatedAt, + }) => Purchase( + localId: localId ?? this.localId, + serverId: serverId.present ? serverId.value : this.serverId, + purchaseNumber: purchaseNumber ?? this.purchaseNumber, + outletId: outletId ?? this.outletId, + outletNameSnapshot: outletNameSnapshot ?? this.outletNameSnapshot, + supplierId: supplierId.present ? supplierId.value : this.supplierId, + supplierNameSnapshot: supplierNameSnapshot.present + ? supplierNameSnapshot.value + : this.supplierNameSnapshot, + supplierEmailSnapshot: supplierEmailSnapshot.present + ? supplierEmailSnapshot.value + : this.supplierEmailSnapshot, + supplierPhoneNumberSnapshot: supplierPhoneNumberSnapshot.present + ? supplierPhoneNumberSnapshot.value + : this.supplierPhoneNumberSnapshot, + userId: userId ?? this.userId, + adminNameSnapshot: adminNameSnapshot ?? this.adminNameSnapshot, + purchaseStatus: purchaseStatus ?? this.purchaseStatus, + totalPurchase: totalPurchase ?? this.totalPurchase, + discount: discount ?? this.discount, + tax: tax ?? this.tax, + paymentStatus: paymentStatus ?? this.paymentStatus, + totalAmount: totalAmount ?? this.totalAmount, + amountPaid: amountPaid ?? this.amountPaid, + changeAmount: changeAmount ?? this.changeAmount, + underPaymentAmount: underPaymentAmount ?? this.underPaymentAmount, + debtAmount: debtAmount ?? this.debtAmount, + paymentProofPath: paymentProofPath.present + ? paymentProofPath.value + : this.paymentProofPath, + paymentProofUrl: paymentProofUrl.present + ? paymentProofUrl.value + : this.paymentProofUrl, + notes: notes.present ? notes.value : this.notes, + syncStatus: syncStatus ?? this.syncStatus, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + ); + Purchase copyWithCompanion(PurchasesCompanion data) { + return Purchase( + localId: data.localId.present ? data.localId.value : this.localId, + serverId: data.serverId.present ? data.serverId.value : this.serverId, + purchaseNumber: data.purchaseNumber.present + ? data.purchaseNumber.value + : this.purchaseNumber, + outletId: data.outletId.present ? data.outletId.value : this.outletId, + outletNameSnapshot: data.outletNameSnapshot.present + ? data.outletNameSnapshot.value + : this.outletNameSnapshot, + supplierId: data.supplierId.present + ? data.supplierId.value + : this.supplierId, + supplierNameSnapshot: data.supplierNameSnapshot.present + ? data.supplierNameSnapshot.value + : this.supplierNameSnapshot, + supplierEmailSnapshot: data.supplierEmailSnapshot.present + ? data.supplierEmailSnapshot.value + : this.supplierEmailSnapshot, + supplierPhoneNumberSnapshot: data.supplierPhoneNumberSnapshot.present + ? data.supplierPhoneNumberSnapshot.value + : this.supplierPhoneNumberSnapshot, + userId: data.userId.present ? data.userId.value : this.userId, + adminNameSnapshot: data.adminNameSnapshot.present + ? data.adminNameSnapshot.value + : this.adminNameSnapshot, + purchaseStatus: data.purchaseStatus.present + ? data.purchaseStatus.value + : this.purchaseStatus, + totalPurchase: data.totalPurchase.present + ? data.totalPurchase.value + : this.totalPurchase, + discount: data.discount.present ? data.discount.value : this.discount, + tax: data.tax.present ? data.tax.value : this.tax, + paymentStatus: data.paymentStatus.present + ? data.paymentStatus.value + : this.paymentStatus, + totalAmount: data.totalAmount.present + ? data.totalAmount.value + : this.totalAmount, + amountPaid: data.amountPaid.present + ? data.amountPaid.value + : this.amountPaid, + changeAmount: data.changeAmount.present + ? data.changeAmount.value + : this.changeAmount, + underPaymentAmount: data.underPaymentAmount.present + ? data.underPaymentAmount.value + : this.underPaymentAmount, + debtAmount: data.debtAmount.present + ? data.debtAmount.value + : this.debtAmount, + paymentProofPath: data.paymentProofPath.present + ? data.paymentProofPath.value + : this.paymentProofPath, + paymentProofUrl: data.paymentProofUrl.present + ? data.paymentProofUrl.value + : this.paymentProofUrl, + notes: data.notes.present ? data.notes.value : this.notes, + syncStatus: data.syncStatus.present + ? data.syncStatus.value + : this.syncStatus, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, + ); + } + + @override + String toString() { + return (StringBuffer('Purchase(') + ..write('localId: $localId, ') + ..write('serverId: $serverId, ') + ..write('purchaseNumber: $purchaseNumber, ') + ..write('outletId: $outletId, ') + ..write('outletNameSnapshot: $outletNameSnapshot, ') + ..write('supplierId: $supplierId, ') + ..write('supplierNameSnapshot: $supplierNameSnapshot, ') + ..write('supplierEmailSnapshot: $supplierEmailSnapshot, ') + ..write('supplierPhoneNumberSnapshot: $supplierPhoneNumberSnapshot, ') + ..write('userId: $userId, ') + ..write('adminNameSnapshot: $adminNameSnapshot, ') + ..write('purchaseStatus: $purchaseStatus, ') + ..write('totalPurchase: $totalPurchase, ') + ..write('discount: $discount, ') + ..write('tax: $tax, ') + ..write('paymentStatus: $paymentStatus, ') + ..write('totalAmount: $totalAmount, ') + ..write('amountPaid: $amountPaid, ') + ..write('changeAmount: $changeAmount, ') + ..write('underPaymentAmount: $underPaymentAmount, ') + ..write('debtAmount: $debtAmount, ') + ..write('paymentProofPath: $paymentProofPath, ') + ..write('paymentProofUrl: $paymentProofUrl, ') + ..write('notes: $notes, ') + ..write('syncStatus: $syncStatus, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hashAll([ + localId, + serverId, + purchaseNumber, + outletId, + outletNameSnapshot, + supplierId, + supplierNameSnapshot, + supplierEmailSnapshot, + supplierPhoneNumberSnapshot, + userId, + adminNameSnapshot, + purchaseStatus, + totalPurchase, + discount, + tax, + paymentStatus, + totalAmount, + amountPaid, + changeAmount, + underPaymentAmount, + debtAmount, + paymentProofPath, + paymentProofUrl, + notes, + syncStatus, + createdAt, + updatedAt, + ]); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is Purchase && + other.localId == this.localId && + other.serverId == this.serverId && + other.purchaseNumber == this.purchaseNumber && + other.outletId == this.outletId && + other.outletNameSnapshot == this.outletNameSnapshot && + other.supplierId == this.supplierId && + other.supplierNameSnapshot == this.supplierNameSnapshot && + other.supplierEmailSnapshot == this.supplierEmailSnapshot && + other.supplierPhoneNumberSnapshot == + this.supplierPhoneNumberSnapshot && + other.userId == this.userId && + other.adminNameSnapshot == this.adminNameSnapshot && + other.purchaseStatus == this.purchaseStatus && + other.totalPurchase == this.totalPurchase && + other.discount == this.discount && + other.tax == this.tax && + other.paymentStatus == this.paymentStatus && + other.totalAmount == this.totalAmount && + other.amountPaid == this.amountPaid && + other.changeAmount == this.changeAmount && + other.underPaymentAmount == this.underPaymentAmount && + other.debtAmount == this.debtAmount && + other.paymentProofPath == this.paymentProofPath && + other.paymentProofUrl == this.paymentProofUrl && + other.notes == this.notes && + other.syncStatus == this.syncStatus && + other.createdAt == this.createdAt && + other.updatedAt == this.updatedAt); +} + +class PurchasesCompanion extends UpdateCompanion { + final Value localId; + final Value serverId; + final Value purchaseNumber; + final Value outletId; + final Value outletNameSnapshot; + final Value supplierId; + final Value supplierNameSnapshot; + final Value supplierEmailSnapshot; + final Value supplierPhoneNumberSnapshot; + final Value userId; + final Value adminNameSnapshot; + final Value purchaseStatus; + final Value totalPurchase; + final Value discount; + final Value tax; + final Value paymentStatus; + final Value totalAmount; + final Value amountPaid; + final Value changeAmount; + final Value underPaymentAmount; + final Value debtAmount; + final Value paymentProofPath; + final Value paymentProofUrl; + final Value notes; + final Value syncStatus; + final Value createdAt; + final Value updatedAt; + final Value rowid; + const PurchasesCompanion({ + this.localId = const Value.absent(), + this.serverId = const Value.absent(), + this.purchaseNumber = const Value.absent(), + this.outletId = const Value.absent(), + this.outletNameSnapshot = const Value.absent(), + this.supplierId = const Value.absent(), + this.supplierNameSnapshot = const Value.absent(), + this.supplierEmailSnapshot = const Value.absent(), + this.supplierPhoneNumberSnapshot = const Value.absent(), + this.userId = const Value.absent(), + this.adminNameSnapshot = const Value.absent(), + this.purchaseStatus = const Value.absent(), + this.totalPurchase = const Value.absent(), + this.discount = const Value.absent(), + this.tax = const Value.absent(), + this.paymentStatus = const Value.absent(), + this.totalAmount = const Value.absent(), + this.amountPaid = const Value.absent(), + this.changeAmount = const Value.absent(), + this.underPaymentAmount = const Value.absent(), + this.debtAmount = const Value.absent(), + this.paymentProofPath = const Value.absent(), + this.paymentProofUrl = const Value.absent(), + this.notes = const Value.absent(), + this.syncStatus = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.rowid = const Value.absent(), + }); + PurchasesCompanion.insert({ + this.localId = const Value.absent(), + this.serverId = const Value.absent(), + required String purchaseNumber, + required String outletId, + required String outletNameSnapshot, + this.supplierId = const Value.absent(), + this.supplierNameSnapshot = const Value.absent(), + this.supplierEmailSnapshot = const Value.absent(), + this.supplierPhoneNumberSnapshot = const Value.absent(), + required String userId, + required String adminNameSnapshot, + required String purchaseStatus, + this.totalPurchase = const Value.absent(), + this.discount = const Value.absent(), + this.tax = const Value.absent(), + required String paymentStatus, + this.totalAmount = const Value.absent(), + this.amountPaid = const Value.absent(), + this.changeAmount = const Value.absent(), + this.underPaymentAmount = const Value.absent(), + this.debtAmount = const Value.absent(), + this.paymentProofPath = const Value.absent(), + this.paymentProofUrl = const Value.absent(), + this.notes = const Value.absent(), + this.syncStatus = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.rowid = const Value.absent(), + }) : purchaseNumber = Value(purchaseNumber), + outletId = Value(outletId), + outletNameSnapshot = Value(outletNameSnapshot), + userId = Value(userId), + adminNameSnapshot = Value(adminNameSnapshot), + purchaseStatus = Value(purchaseStatus), + paymentStatus = Value(paymentStatus); + static Insertable custom({ + Expression? localId, + Expression? serverId, + Expression? purchaseNumber, + Expression? outletId, + Expression? outletNameSnapshot, + Expression? supplierId, + Expression? supplierNameSnapshot, + Expression? supplierEmailSnapshot, + Expression? supplierPhoneNumberSnapshot, + Expression? userId, + Expression? adminNameSnapshot, + Expression? purchaseStatus, + Expression? totalPurchase, + Expression? discount, + Expression? tax, + Expression? paymentStatus, + Expression? totalAmount, + Expression? amountPaid, + Expression? changeAmount, + Expression? underPaymentAmount, + Expression? debtAmount, + Expression? paymentProofPath, + Expression? paymentProofUrl, + Expression? notes, + Expression? syncStatus, + Expression? createdAt, + Expression? updatedAt, + Expression? rowid, + }) { + return RawValuesInsertable({ + if (localId != null) 'local_id': localId, + if (serverId != null) 'server_id': serverId, + if (purchaseNumber != null) 'purchase_number': purchaseNumber, + if (outletId != null) 'outlet_id': outletId, + if (outletNameSnapshot != null) + 'outlet_name_snapshot': outletNameSnapshot, + if (supplierId != null) 'supplier_id': supplierId, + if (supplierNameSnapshot != null) + 'supplier_name_snapshot': supplierNameSnapshot, + if (supplierEmailSnapshot != null) + 'supplier_email_snapshot': supplierEmailSnapshot, + if (supplierPhoneNumberSnapshot != null) + 'supplier_phone_number_snapshot': supplierPhoneNumberSnapshot, + if (userId != null) 'user_id': userId, + if (adminNameSnapshot != null) 'admin_name_snapshot': adminNameSnapshot, + if (purchaseStatus != null) 'purchase_status': purchaseStatus, + if (totalPurchase != null) 'total_purchase': totalPurchase, + if (discount != null) 'discount': discount, + if (tax != null) 'tax': tax, + if (paymentStatus != null) 'payment_status': paymentStatus, + if (totalAmount != null) 'total_amount': totalAmount, + if (amountPaid != null) 'amount_paid': amountPaid, + if (changeAmount != null) 'change_amount': changeAmount, + if (underPaymentAmount != null) + 'under_payment_amount': underPaymentAmount, + if (debtAmount != null) 'debt_amount': debtAmount, + if (paymentProofPath != null) 'payment_proof_path': paymentProofPath, + if (paymentProofUrl != null) 'payment_proof_url': paymentProofUrl, + if (notes != null) 'notes': notes, + if (syncStatus != null) 'sync_status': syncStatus, + if (createdAt != null) 'created_at': createdAt, + if (updatedAt != null) 'updated_at': updatedAt, + if (rowid != null) 'rowid': rowid, + }); + } + + PurchasesCompanion copyWith({ + Value? localId, + Value? serverId, + Value? purchaseNumber, + Value? outletId, + Value? outletNameSnapshot, + Value? supplierId, + Value? supplierNameSnapshot, + Value? supplierEmailSnapshot, + Value? supplierPhoneNumberSnapshot, + Value? userId, + Value? adminNameSnapshot, + Value? purchaseStatus, + Value? totalPurchase, + Value? discount, + Value? tax, + Value? paymentStatus, + Value? totalAmount, + Value? amountPaid, + Value? changeAmount, + Value? underPaymentAmount, + Value? debtAmount, + Value? paymentProofPath, + Value? paymentProofUrl, + Value? notes, + Value? syncStatus, + Value? createdAt, + Value? updatedAt, + Value? rowid, + }) { + return PurchasesCompanion( + localId: localId ?? this.localId, + serverId: serverId ?? this.serverId, + purchaseNumber: purchaseNumber ?? this.purchaseNumber, + outletId: outletId ?? this.outletId, + outletNameSnapshot: outletNameSnapshot ?? this.outletNameSnapshot, + supplierId: supplierId ?? this.supplierId, + supplierNameSnapshot: supplierNameSnapshot ?? this.supplierNameSnapshot, + supplierEmailSnapshot: + supplierEmailSnapshot ?? this.supplierEmailSnapshot, + supplierPhoneNumberSnapshot: + supplierPhoneNumberSnapshot ?? this.supplierPhoneNumberSnapshot, + userId: userId ?? this.userId, + adminNameSnapshot: adminNameSnapshot ?? this.adminNameSnapshot, + purchaseStatus: purchaseStatus ?? this.purchaseStatus, + totalPurchase: totalPurchase ?? this.totalPurchase, + discount: discount ?? this.discount, + tax: tax ?? this.tax, + paymentStatus: paymentStatus ?? this.paymentStatus, + totalAmount: totalAmount ?? this.totalAmount, + amountPaid: amountPaid ?? this.amountPaid, + changeAmount: changeAmount ?? this.changeAmount, + underPaymentAmount: underPaymentAmount ?? this.underPaymentAmount, + debtAmount: debtAmount ?? this.debtAmount, + paymentProofPath: paymentProofPath ?? this.paymentProofPath, + paymentProofUrl: paymentProofUrl ?? this.paymentProofUrl, + notes: notes ?? this.notes, + syncStatus: syncStatus ?? this.syncStatus, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (localId.present) { + map['local_id'] = Variable(localId.value); + } + if (serverId.present) { + map['server_id'] = Variable(serverId.value); + } + if (purchaseNumber.present) { + map['purchase_number'] = Variable(purchaseNumber.value); + } + if (outletId.present) { + map['outlet_id'] = Variable(outletId.value); + } + if (outletNameSnapshot.present) { + map['outlet_name_snapshot'] = Variable(outletNameSnapshot.value); + } + if (supplierId.present) { + map['supplier_id'] = Variable(supplierId.value); + } + if (supplierNameSnapshot.present) { + map['supplier_name_snapshot'] = Variable( + supplierNameSnapshot.value, + ); + } + if (supplierEmailSnapshot.present) { + map['supplier_email_snapshot'] = Variable( + supplierEmailSnapshot.value, + ); + } + if (supplierPhoneNumberSnapshot.present) { + map['supplier_phone_number_snapshot'] = Variable( + supplierPhoneNumberSnapshot.value, + ); + } + if (userId.present) { + map['user_id'] = Variable(userId.value); + } + if (adminNameSnapshot.present) { + map['admin_name_snapshot'] = Variable(adminNameSnapshot.value); + } + if (purchaseStatus.present) { + map['purchase_status'] = Variable(purchaseStatus.value); + } + if (totalPurchase.present) { + map['total_purchase'] = Variable(totalPurchase.value); + } + if (discount.present) { + map['discount'] = Variable(discount.value); + } + if (tax.present) { + map['tax'] = Variable(tax.value); + } + if (paymentStatus.present) { + map['payment_status'] = Variable(paymentStatus.value); + } + if (totalAmount.present) { + map['total_amount'] = Variable(totalAmount.value); + } + if (amountPaid.present) { + map['amount_paid'] = Variable(amountPaid.value); + } + if (changeAmount.present) { + map['change_amount'] = Variable(changeAmount.value); + } + if (underPaymentAmount.present) { + map['under_payment_amount'] = Variable(underPaymentAmount.value); + } + if (debtAmount.present) { + map['debt_amount'] = Variable(debtAmount.value); + } + if (paymentProofPath.present) { + map['payment_proof_path'] = Variable(paymentProofPath.value); + } + if (paymentProofUrl.present) { + map['payment_proof_url'] = Variable(paymentProofUrl.value); + } + if (notes.present) { + map['notes'] = Variable(notes.value); + } + if (syncStatus.present) { + map['sync_status'] = Variable(syncStatus.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (updatedAt.present) { + map['updated_at'] = Variable(updatedAt.value); + } + if (rowid.present) { + map['rowid'] = Variable(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('PurchasesCompanion(') + ..write('localId: $localId, ') + ..write('serverId: $serverId, ') + ..write('purchaseNumber: $purchaseNumber, ') + ..write('outletId: $outletId, ') + ..write('outletNameSnapshot: $outletNameSnapshot, ') + ..write('supplierId: $supplierId, ') + ..write('supplierNameSnapshot: $supplierNameSnapshot, ') + ..write('supplierEmailSnapshot: $supplierEmailSnapshot, ') + ..write('supplierPhoneNumberSnapshot: $supplierPhoneNumberSnapshot, ') + ..write('userId: $userId, ') + ..write('adminNameSnapshot: $adminNameSnapshot, ') + ..write('purchaseStatus: $purchaseStatus, ') + ..write('totalPurchase: $totalPurchase, ') + ..write('discount: $discount, ') + ..write('tax: $tax, ') + ..write('paymentStatus: $paymentStatus, ') + ..write('totalAmount: $totalAmount, ') + ..write('amountPaid: $amountPaid, ') + ..write('changeAmount: $changeAmount, ') + ..write('underPaymentAmount: $underPaymentAmount, ') + ..write('debtAmount: $debtAmount, ') + ..write('paymentProofPath: $paymentProofPath, ') + ..write('paymentProofUrl: $paymentProofUrl, ') + ..write('notes: $notes, ') + ..write('syncStatus: $syncStatus, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + +class $PurchaseItemsTable extends PurchaseItems + with TableInfo<$PurchaseItemsTable, PurchaseItem> { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + $PurchaseItemsTable(this.attachedDatabase, [this._alias]); + static const VerificationMeta _localIdMeta = const VerificationMeta( + 'localId', + ); + @override + late final GeneratedColumn localId = GeneratedColumn( + 'local_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: false, + clientDefault: () => const Uuid().v7().toString(), + ); + static const VerificationMeta _serverIdMeta = const VerificationMeta( + 'serverId', + ); + @override + late final GeneratedColumn serverId = GeneratedColumn( + 'server_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _purchaseIdMeta = const VerificationMeta( + 'purchaseId', + ); + @override + late final GeneratedColumn purchaseId = GeneratedColumn( + 'purchase_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES purchases (local_id)', + ), + ); + static const VerificationMeta _itemTypeMeta = const VerificationMeta( + 'itemType', + ); + @override + late final GeneratedColumn itemType = GeneratedColumn( + 'item_type', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + static const VerificationMeta _itemIdMeta = const VerificationMeta('itemId'); + @override + late final GeneratedColumn itemId = GeneratedColumn( + 'item_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + static const VerificationMeta _quantityMeta = const VerificationMeta( + 'quantity', + ); + @override + late final GeneratedColumn quantity = GeneratedColumn( + 'quantity', + aliasedName, + false, + type: DriftSqlType.double, + requiredDuringInsert: false, + defaultValue: const Constant(0.0), + ); + static const VerificationMeta _itemImageSnapshotMeta = const VerificationMeta( + 'itemImageSnapshot', + ); + @override + late final GeneratedColumn itemImageSnapshot = + GeneratedColumn( + 'item_image_snapshot', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _itemImageUrlSnapshotMeta = + const VerificationMeta('itemImageUrlSnapshot'); + @override + late final GeneratedColumn itemImageUrlSnapshot = + GeneratedColumn( + 'item_image_url_snapshot', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _itemNameSnapshotMeta = const VerificationMeta( + 'itemNameSnapshot', + ); + @override + late final GeneratedColumn itemNameSnapshot = GeneratedColumn( + 'item_name_snapshot', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + static const VerificationMeta _itemVariantNameSnapshotMeta = + const VerificationMeta('itemVariantNameSnapshot'); + @override + late final GeneratedColumn itemVariantNameSnapshot = + GeneratedColumn( + 'item_variant_name_snapshot', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _purchasePriceSnaphotMeta = + const VerificationMeta('purchasePriceSnaphot'); + @override + late final GeneratedColumn purchasePriceSnaphot = + GeneratedColumn( + 'purchase_price_snaphot', + aliasedName, + false, + type: DriftSqlType.double, + requiredDuringInsert: false, + defaultValue: const Constant(0.0), + ); + static const VerificationMeta _subtotalMeta = const VerificationMeta( + 'subtotal', + ); + @override + late final GeneratedColumn subtotal = GeneratedColumn( + 'subtotal', + aliasedName, + false, + type: DriftSqlType.double, + requiredDuringInsert: false, + defaultValue: const Constant(0.0), + ); + static const VerificationMeta _syncStatusMeta = const VerificationMeta( + 'syncStatus', + ); + @override + late final GeneratedColumn syncStatus = GeneratedColumn( + 'sync_status', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultValue: const Constant('dirty'), + ); + static const VerificationMeta _createdAtMeta = const VerificationMeta( + 'createdAt', + ); + @override + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: currentDateAndTime, + ); + static const VerificationMeta _updatedAtMeta = const VerificationMeta( + 'updatedAt', + ); + @override + late final GeneratedColumn updatedAt = GeneratedColumn( + 'updated_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: currentDateAndTime, + ); + @override + List get $columns => [ + localId, + serverId, + purchaseId, + itemType, + itemId, + quantity, + itemImageSnapshot, + itemImageUrlSnapshot, + itemNameSnapshot, + itemVariantNameSnapshot, + purchasePriceSnaphot, + subtotal, + syncStatus, + createdAt, + updatedAt, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'purchase_items'; + @override + VerificationContext validateIntegrity( + Insertable instance, { + bool isInserting = false, + }) { + final context = VerificationContext(); + final data = instance.toColumns(true); + if (data.containsKey('local_id')) { + context.handle( + _localIdMeta, + localId.isAcceptableOrUnknown(data['local_id']!, _localIdMeta), + ); + } + if (data.containsKey('server_id')) { + context.handle( + _serverIdMeta, + serverId.isAcceptableOrUnknown(data['server_id']!, _serverIdMeta), + ); + } + if (data.containsKey('purchase_id')) { + context.handle( + _purchaseIdMeta, + purchaseId.isAcceptableOrUnknown(data['purchase_id']!, _purchaseIdMeta), + ); + } else if (isInserting) { + context.missing(_purchaseIdMeta); + } + if (data.containsKey('item_type')) { + context.handle( + _itemTypeMeta, + itemType.isAcceptableOrUnknown(data['item_type']!, _itemTypeMeta), + ); + } else if (isInserting) { + context.missing(_itemTypeMeta); + } + if (data.containsKey('item_id')) { + context.handle( + _itemIdMeta, + itemId.isAcceptableOrUnknown(data['item_id']!, _itemIdMeta), + ); + } else if (isInserting) { + context.missing(_itemIdMeta); + } + if (data.containsKey('quantity')) { + context.handle( + _quantityMeta, + quantity.isAcceptableOrUnknown(data['quantity']!, _quantityMeta), + ); + } + if (data.containsKey('item_image_snapshot')) { + context.handle( + _itemImageSnapshotMeta, + itemImageSnapshot.isAcceptableOrUnknown( + data['item_image_snapshot']!, + _itemImageSnapshotMeta, + ), + ); + } + if (data.containsKey('item_image_url_snapshot')) { + context.handle( + _itemImageUrlSnapshotMeta, + itemImageUrlSnapshot.isAcceptableOrUnknown( + data['item_image_url_snapshot']!, + _itemImageUrlSnapshotMeta, + ), + ); + } + if (data.containsKey('item_name_snapshot')) { + context.handle( + _itemNameSnapshotMeta, + itemNameSnapshot.isAcceptableOrUnknown( + data['item_name_snapshot']!, + _itemNameSnapshotMeta, + ), + ); + } else if (isInserting) { + context.missing(_itemNameSnapshotMeta); + } + if (data.containsKey('item_variant_name_snapshot')) { + context.handle( + _itemVariantNameSnapshotMeta, + itemVariantNameSnapshot.isAcceptableOrUnknown( + data['item_variant_name_snapshot']!, + _itemVariantNameSnapshotMeta, + ), + ); + } + if (data.containsKey('purchase_price_snaphot')) { + context.handle( + _purchasePriceSnaphotMeta, + purchasePriceSnaphot.isAcceptableOrUnknown( + data['purchase_price_snaphot']!, + _purchasePriceSnaphotMeta, + ), + ); + } + if (data.containsKey('subtotal')) { + context.handle( + _subtotalMeta, + subtotal.isAcceptableOrUnknown(data['subtotal']!, _subtotalMeta), + ); + } + if (data.containsKey('sync_status')) { + context.handle( + _syncStatusMeta, + syncStatus.isAcceptableOrUnknown(data['sync_status']!, _syncStatusMeta), + ); + } + if (data.containsKey('created_at')) { + context.handle( + _createdAtMeta, + createdAt.isAcceptableOrUnknown(data['created_at']!, _createdAtMeta), + ); + } + if (data.containsKey('updated_at')) { + context.handle( + _updatedAtMeta, + updatedAt.isAcceptableOrUnknown(data['updated_at']!, _updatedAtMeta), + ); + } + return context; + } + + @override + Set get $primaryKey => {localId}; + @override + PurchaseItem map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return PurchaseItem( + localId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}local_id'], + )!, + serverId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}server_id'], + ), + purchaseId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}purchase_id'], + )!, + itemType: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}item_type'], + )!, + itemId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}item_id'], + )!, + quantity: attachedDatabase.typeMapping.read( + DriftSqlType.double, + data['${effectivePrefix}quantity'], + )!, + itemImageSnapshot: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}item_image_snapshot'], + ), + itemImageUrlSnapshot: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}item_image_url_snapshot'], + ), + itemNameSnapshot: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}item_name_snapshot'], + )!, + itemVariantNameSnapshot: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}item_variant_name_snapshot'], + ), + purchasePriceSnaphot: attachedDatabase.typeMapping.read( + DriftSqlType.double, + data['${effectivePrefix}purchase_price_snaphot'], + )!, + subtotal: attachedDatabase.typeMapping.read( + DriftSqlType.double, + data['${effectivePrefix}subtotal'], + )!, + syncStatus: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}sync_status'], + )!, + createdAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}created_at'], + )!, + updatedAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}updated_at'], + )!, + ); + } + + @override + $PurchaseItemsTable createAlias(String alias) { + return $PurchaseItemsTable(attachedDatabase, alias); + } +} + +class PurchaseItem extends DataClass implements Insertable { + final String localId; + final String? serverId; + final String purchaseId; + final String itemType; + final String itemId; + final double quantity; + final String? itemImageSnapshot; + final String? itemImageUrlSnapshot; + final String itemNameSnapshot; + final String? itemVariantNameSnapshot; + final double purchasePriceSnaphot; + final double subtotal; + final String syncStatus; + final DateTime createdAt; + final DateTime updatedAt; + const PurchaseItem({ + required this.localId, + this.serverId, + required this.purchaseId, + required this.itemType, + required this.itemId, + required this.quantity, + this.itemImageSnapshot, + this.itemImageUrlSnapshot, + required this.itemNameSnapshot, + this.itemVariantNameSnapshot, + required this.purchasePriceSnaphot, + required this.subtotal, + required this.syncStatus, + required this.createdAt, + required this.updatedAt, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['local_id'] = Variable(localId); + if (!nullToAbsent || serverId != null) { + map['server_id'] = Variable(serverId); + } + map['purchase_id'] = Variable(purchaseId); + map['item_type'] = Variable(itemType); + map['item_id'] = Variable(itemId); + map['quantity'] = Variable(quantity); + if (!nullToAbsent || itemImageSnapshot != null) { + map['item_image_snapshot'] = Variable(itemImageSnapshot); + } + if (!nullToAbsent || itemImageUrlSnapshot != null) { + map['item_image_url_snapshot'] = Variable(itemImageUrlSnapshot); + } + map['item_name_snapshot'] = Variable(itemNameSnapshot); + if (!nullToAbsent || itemVariantNameSnapshot != null) { + map['item_variant_name_snapshot'] = Variable( + itemVariantNameSnapshot, + ); + } + map['purchase_price_snaphot'] = Variable(purchasePriceSnaphot); + map['subtotal'] = Variable(subtotal); + map['sync_status'] = Variable(syncStatus); + map['created_at'] = Variable(createdAt); + map['updated_at'] = Variable(updatedAt); + return map; + } + + PurchaseItemsCompanion toCompanion(bool nullToAbsent) { + return PurchaseItemsCompanion( + localId: Value(localId), + serverId: serverId == null && nullToAbsent + ? const Value.absent() + : Value(serverId), + purchaseId: Value(purchaseId), + itemType: Value(itemType), + itemId: Value(itemId), + quantity: Value(quantity), + itemImageSnapshot: itemImageSnapshot == null && nullToAbsent + ? const Value.absent() + : Value(itemImageSnapshot), + itemImageUrlSnapshot: itemImageUrlSnapshot == null && nullToAbsent + ? const Value.absent() + : Value(itemImageUrlSnapshot), + itemNameSnapshot: Value(itemNameSnapshot), + itemVariantNameSnapshot: itemVariantNameSnapshot == null && nullToAbsent + ? const Value.absent() + : Value(itemVariantNameSnapshot), + purchasePriceSnaphot: Value(purchasePriceSnaphot), + subtotal: Value(subtotal), + syncStatus: Value(syncStatus), + createdAt: Value(createdAt), + updatedAt: Value(updatedAt), + ); + } + + factory PurchaseItem.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return PurchaseItem( + localId: serializer.fromJson(json['localId']), + serverId: serializer.fromJson(json['serverId']), + purchaseId: serializer.fromJson(json['purchaseId']), + itemType: serializer.fromJson(json['itemType']), + itemId: serializer.fromJson(json['itemId']), + quantity: serializer.fromJson(json['quantity']), + itemImageSnapshot: serializer.fromJson( + json['itemImageSnapshot'], + ), + itemImageUrlSnapshot: serializer.fromJson( + json['itemImageUrlSnapshot'], + ), + itemNameSnapshot: serializer.fromJson(json['itemNameSnapshot']), + itemVariantNameSnapshot: serializer.fromJson( + json['itemVariantNameSnapshot'], + ), + purchasePriceSnaphot: serializer.fromJson( + json['purchasePriceSnaphot'], + ), + subtotal: serializer.fromJson(json['subtotal']), + syncStatus: serializer.fromJson(json['syncStatus']), + createdAt: serializer.fromJson(json['createdAt']), + updatedAt: serializer.fromJson(json['updatedAt']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'localId': serializer.toJson(localId), + 'serverId': serializer.toJson(serverId), + 'purchaseId': serializer.toJson(purchaseId), + 'itemType': serializer.toJson(itemType), + 'itemId': serializer.toJson(itemId), + 'quantity': serializer.toJson(quantity), + 'itemImageSnapshot': serializer.toJson(itemImageSnapshot), + 'itemImageUrlSnapshot': serializer.toJson(itemImageUrlSnapshot), + 'itemNameSnapshot': serializer.toJson(itemNameSnapshot), + 'itemVariantNameSnapshot': serializer.toJson( + itemVariantNameSnapshot, + ), + 'purchasePriceSnaphot': serializer.toJson(purchasePriceSnaphot), + 'subtotal': serializer.toJson(subtotal), + 'syncStatus': serializer.toJson(syncStatus), + 'createdAt': serializer.toJson(createdAt), + 'updatedAt': serializer.toJson(updatedAt), + }; + } + + PurchaseItem copyWith({ + String? localId, + Value serverId = const Value.absent(), + String? purchaseId, + String? itemType, + String? itemId, + double? quantity, + Value itemImageSnapshot = const Value.absent(), + Value itemImageUrlSnapshot = const Value.absent(), + String? itemNameSnapshot, + Value itemVariantNameSnapshot = const Value.absent(), + double? purchasePriceSnaphot, + double? subtotal, + String? syncStatus, + DateTime? createdAt, + DateTime? updatedAt, + }) => PurchaseItem( + localId: localId ?? this.localId, + serverId: serverId.present ? serverId.value : this.serverId, + purchaseId: purchaseId ?? this.purchaseId, + itemType: itemType ?? this.itemType, + itemId: itemId ?? this.itemId, + quantity: quantity ?? this.quantity, + itemImageSnapshot: itemImageSnapshot.present + ? itemImageSnapshot.value + : this.itemImageSnapshot, + itemImageUrlSnapshot: itemImageUrlSnapshot.present + ? itemImageUrlSnapshot.value + : this.itemImageUrlSnapshot, + itemNameSnapshot: itemNameSnapshot ?? this.itemNameSnapshot, + itemVariantNameSnapshot: itemVariantNameSnapshot.present + ? itemVariantNameSnapshot.value + : this.itemVariantNameSnapshot, + purchasePriceSnaphot: purchasePriceSnaphot ?? this.purchasePriceSnaphot, + subtotal: subtotal ?? this.subtotal, + syncStatus: syncStatus ?? this.syncStatus, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + ); + PurchaseItem copyWithCompanion(PurchaseItemsCompanion data) { + return PurchaseItem( + localId: data.localId.present ? data.localId.value : this.localId, + serverId: data.serverId.present ? data.serverId.value : this.serverId, + purchaseId: data.purchaseId.present + ? data.purchaseId.value + : this.purchaseId, + itemType: data.itemType.present ? data.itemType.value : this.itemType, + itemId: data.itemId.present ? data.itemId.value : this.itemId, + quantity: data.quantity.present ? data.quantity.value : this.quantity, + itemImageSnapshot: data.itemImageSnapshot.present + ? data.itemImageSnapshot.value + : this.itemImageSnapshot, + itemImageUrlSnapshot: data.itemImageUrlSnapshot.present + ? data.itemImageUrlSnapshot.value + : this.itemImageUrlSnapshot, + itemNameSnapshot: data.itemNameSnapshot.present + ? data.itemNameSnapshot.value + : this.itemNameSnapshot, + itemVariantNameSnapshot: data.itemVariantNameSnapshot.present + ? data.itemVariantNameSnapshot.value + : this.itemVariantNameSnapshot, + purchasePriceSnaphot: data.purchasePriceSnaphot.present + ? data.purchasePriceSnaphot.value + : this.purchasePriceSnaphot, + subtotal: data.subtotal.present ? data.subtotal.value : this.subtotal, + syncStatus: data.syncStatus.present + ? data.syncStatus.value + : this.syncStatus, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, + ); + } + + @override + String toString() { + return (StringBuffer('PurchaseItem(') + ..write('localId: $localId, ') + ..write('serverId: $serverId, ') + ..write('purchaseId: $purchaseId, ') + ..write('itemType: $itemType, ') + ..write('itemId: $itemId, ') + ..write('quantity: $quantity, ') + ..write('itemImageSnapshot: $itemImageSnapshot, ') + ..write('itemImageUrlSnapshot: $itemImageUrlSnapshot, ') + ..write('itemNameSnapshot: $itemNameSnapshot, ') + ..write('itemVariantNameSnapshot: $itemVariantNameSnapshot, ') + ..write('purchasePriceSnaphot: $purchasePriceSnaphot, ') + ..write('subtotal: $subtotal, ') + ..write('syncStatus: $syncStatus, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + localId, + serverId, + purchaseId, + itemType, + itemId, + quantity, + itemImageSnapshot, + itemImageUrlSnapshot, + itemNameSnapshot, + itemVariantNameSnapshot, + purchasePriceSnaphot, + subtotal, + syncStatus, + createdAt, + updatedAt, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is PurchaseItem && + other.localId == this.localId && + other.serverId == this.serverId && + other.purchaseId == this.purchaseId && + other.itemType == this.itemType && + other.itemId == this.itemId && + other.quantity == this.quantity && + other.itemImageSnapshot == this.itemImageSnapshot && + other.itemImageUrlSnapshot == this.itemImageUrlSnapshot && + other.itemNameSnapshot == this.itemNameSnapshot && + other.itemVariantNameSnapshot == this.itemVariantNameSnapshot && + other.purchasePriceSnaphot == this.purchasePriceSnaphot && + other.subtotal == this.subtotal && + other.syncStatus == this.syncStatus && + other.createdAt == this.createdAt && + other.updatedAt == this.updatedAt); +} + +class PurchaseItemsCompanion extends UpdateCompanion { + final Value localId; + final Value serverId; + final Value purchaseId; + final Value itemType; + final Value itemId; + final Value quantity; + final Value itemImageSnapshot; + final Value itemImageUrlSnapshot; + final Value itemNameSnapshot; + final Value itemVariantNameSnapshot; + final Value purchasePriceSnaphot; + final Value subtotal; + final Value syncStatus; + final Value createdAt; + final Value updatedAt; + final Value rowid; + const PurchaseItemsCompanion({ + this.localId = const Value.absent(), + this.serverId = const Value.absent(), + this.purchaseId = const Value.absent(), + this.itemType = const Value.absent(), + this.itemId = const Value.absent(), + this.quantity = const Value.absent(), + this.itemImageSnapshot = const Value.absent(), + this.itemImageUrlSnapshot = const Value.absent(), + this.itemNameSnapshot = const Value.absent(), + this.itemVariantNameSnapshot = const Value.absent(), + this.purchasePriceSnaphot = const Value.absent(), + this.subtotal = const Value.absent(), + this.syncStatus = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.rowid = const Value.absent(), + }); + PurchaseItemsCompanion.insert({ + this.localId = const Value.absent(), + this.serverId = const Value.absent(), + required String purchaseId, + required String itemType, + required String itemId, + this.quantity = const Value.absent(), + this.itemImageSnapshot = const Value.absent(), + this.itemImageUrlSnapshot = const Value.absent(), + required String itemNameSnapshot, + this.itemVariantNameSnapshot = const Value.absent(), + this.purchasePriceSnaphot = const Value.absent(), + this.subtotal = const Value.absent(), + this.syncStatus = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.rowid = const Value.absent(), + }) : purchaseId = Value(purchaseId), + itemType = Value(itemType), + itemId = Value(itemId), + itemNameSnapshot = Value(itemNameSnapshot); + static Insertable custom({ + Expression? localId, + Expression? serverId, + Expression? purchaseId, + Expression? itemType, + Expression? itemId, + Expression? quantity, + Expression? itemImageSnapshot, + Expression? itemImageUrlSnapshot, + Expression? itemNameSnapshot, + Expression? itemVariantNameSnapshot, + Expression? purchasePriceSnaphot, + Expression? subtotal, + Expression? syncStatus, + Expression? createdAt, + Expression? updatedAt, + Expression? rowid, + }) { + return RawValuesInsertable({ + if (localId != null) 'local_id': localId, + if (serverId != null) 'server_id': serverId, + if (purchaseId != null) 'purchase_id': purchaseId, + if (itemType != null) 'item_type': itemType, + if (itemId != null) 'item_id': itemId, + if (quantity != null) 'quantity': quantity, + if (itemImageSnapshot != null) 'item_image_snapshot': itemImageSnapshot, + if (itemImageUrlSnapshot != null) + 'item_image_url_snapshot': itemImageUrlSnapshot, + if (itemNameSnapshot != null) 'item_name_snapshot': itemNameSnapshot, + if (itemVariantNameSnapshot != null) + 'item_variant_name_snapshot': itemVariantNameSnapshot, + if (purchasePriceSnaphot != null) + 'purchase_price_snaphot': purchasePriceSnaphot, + if (subtotal != null) 'subtotal': subtotal, + if (syncStatus != null) 'sync_status': syncStatus, + if (createdAt != null) 'created_at': createdAt, + if (updatedAt != null) 'updated_at': updatedAt, + if (rowid != null) 'rowid': rowid, + }); + } + + PurchaseItemsCompanion copyWith({ + Value? localId, + Value? serverId, + Value? purchaseId, + Value? itemType, + Value? itemId, + Value? quantity, + Value? itemImageSnapshot, + Value? itemImageUrlSnapshot, + Value? itemNameSnapshot, + Value? itemVariantNameSnapshot, + Value? purchasePriceSnaphot, + Value? subtotal, + Value? syncStatus, + Value? createdAt, + Value? updatedAt, + Value? rowid, + }) { + return PurchaseItemsCompanion( + localId: localId ?? this.localId, + serverId: serverId ?? this.serverId, + purchaseId: purchaseId ?? this.purchaseId, + itemType: itemType ?? this.itemType, + itemId: itemId ?? this.itemId, + quantity: quantity ?? this.quantity, + itemImageSnapshot: itemImageSnapshot ?? this.itemImageSnapshot, + itemImageUrlSnapshot: itemImageUrlSnapshot ?? this.itemImageUrlSnapshot, + itemNameSnapshot: itemNameSnapshot ?? this.itemNameSnapshot, + itemVariantNameSnapshot: + itemVariantNameSnapshot ?? this.itemVariantNameSnapshot, + purchasePriceSnaphot: purchasePriceSnaphot ?? this.purchasePriceSnaphot, + subtotal: subtotal ?? this.subtotal, + syncStatus: syncStatus ?? this.syncStatus, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (localId.present) { + map['local_id'] = Variable(localId.value); + } + if (serverId.present) { + map['server_id'] = Variable(serverId.value); + } + if (purchaseId.present) { + map['purchase_id'] = Variable(purchaseId.value); + } + if (itemType.present) { + map['item_type'] = Variable(itemType.value); + } + if (itemId.present) { + map['item_id'] = Variable(itemId.value); + } + if (quantity.present) { + map['quantity'] = Variable(quantity.value); + } + if (itemImageSnapshot.present) { + map['item_image_snapshot'] = Variable(itemImageSnapshot.value); + } + if (itemImageUrlSnapshot.present) { + map['item_image_url_snapshot'] = Variable( + itemImageUrlSnapshot.value, + ); + } + if (itemNameSnapshot.present) { + map['item_name_snapshot'] = Variable(itemNameSnapshot.value); + } + if (itemVariantNameSnapshot.present) { + map['item_variant_name_snapshot'] = Variable( + itemVariantNameSnapshot.value, + ); + } + if (purchasePriceSnaphot.present) { + map['purchase_price_snaphot'] = Variable( + purchasePriceSnaphot.value, + ); + } + if (subtotal.present) { + map['subtotal'] = Variable(subtotal.value); + } + if (syncStatus.present) { + map['sync_status'] = Variable(syncStatus.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (updatedAt.present) { + map['updated_at'] = Variable(updatedAt.value); + } + if (rowid.present) { + map['rowid'] = Variable(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('PurchaseItemsCompanion(') + ..write('localId: $localId, ') + ..write('serverId: $serverId, ') + ..write('purchaseId: $purchaseId, ') + ..write('itemType: $itemType, ') + ..write('itemId: $itemId, ') + ..write('quantity: $quantity, ') + ..write('itemImageSnapshot: $itemImageSnapshot, ') + ..write('itemImageUrlSnapshot: $itemImageUrlSnapshot, ') + ..write('itemNameSnapshot: $itemNameSnapshot, ') + ..write('itemVariantNameSnapshot: $itemVariantNameSnapshot, ') + ..write('purchasePriceSnaphot: $purchasePriceSnaphot, ') + ..write('subtotal: $subtotal, ') + ..write('syncStatus: $syncStatus, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + +class $TransactionsTable extends Transactions + with TableInfo<$TransactionsTable, Transaction> { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + $TransactionsTable(this.attachedDatabase, [this._alias]); + static const VerificationMeta _localIdMeta = const VerificationMeta( + 'localId', + ); + @override + late final GeneratedColumn localId = GeneratedColumn( + 'local_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: false, + clientDefault: () => const Uuid().v7().toString(), + ); + static const VerificationMeta _serverIdMeta = const VerificationMeta( + 'serverId', + ); + @override + late final GeneratedColumn serverId = GeneratedColumn( + 'server_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _outletIdMeta = const VerificationMeta( + 'outletId', + ); + @override + late final GeneratedColumn outletId = GeneratedColumn( + 'outlet_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES outlets (local_id)', + ), + ); + static const VerificationMeta _userIdMeta = const VerificationMeta('userId'); + @override + late final GeneratedColumn userId = GeneratedColumn( + 'user_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES users (local_id)', + ), + ); + static const VerificationMeta _referenceTypeMeta = const VerificationMeta( + 'referenceType', + ); + @override + late final GeneratedColumn referenceType = GeneratedColumn( + 'reference_type', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + static const VerificationMeta _referenceIdMeta = const VerificationMeta( + 'referenceId', + ); + @override + late final GeneratedColumn referenceId = GeneratedColumn( + 'reference_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + static const VerificationMeta _referenceNumberMeta = const VerificationMeta( + 'referenceNumber', + ); + @override + late final GeneratedColumn referenceNumber = GeneratedColumn( + 'reference_number', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + static const VerificationMeta _stakeholderMeta = const VerificationMeta( + 'stakeholder', + ); + @override + late final GeneratedColumn stakeholder = GeneratedColumn( + 'stakeholder', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _transactionTypeMeta = const VerificationMeta( + 'transactionType', + ); + @override + late final GeneratedColumn transactionType = GeneratedColumn( + 'transaction_type', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + static const VerificationMeta _categoryMeta = const VerificationMeta( + 'category', + ); + @override + late final GeneratedColumn category = GeneratedColumn( + 'category', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + static const VerificationMeta _amountMeta = const VerificationMeta('amount'); + @override + late final GeneratedColumn amount = GeneratedColumn( + 'amount', + aliasedName, + false, + type: DriftSqlType.double, + requiredDuringInsert: false, + defaultValue: const Constant(0.0), + ); + static const VerificationMeta _noteMeta = const VerificationMeta('note'); + @override + late final GeneratedColumn note = GeneratedColumn( + 'note', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _syncStatusMeta = const VerificationMeta( + 'syncStatus', + ); + @override + late final GeneratedColumn syncStatus = GeneratedColumn( + 'sync_status', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultValue: const Constant('dirty'), + ); + static const VerificationMeta _createdAtMeta = const VerificationMeta( + 'createdAt', + ); + @override + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: currentDateAndTime, + ); + static const VerificationMeta _updatedAtMeta = const VerificationMeta( + 'updatedAt', + ); + @override + late final GeneratedColumn updatedAt = GeneratedColumn( + 'updated_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: currentDateAndTime, + ); + @override + List get $columns => [ + localId, + serverId, + outletId, + userId, + referenceType, + referenceId, + referenceNumber, + stakeholder, + transactionType, + category, + amount, + note, + syncStatus, + createdAt, + updatedAt, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'transactions'; + @override + VerificationContext validateIntegrity( + Insertable instance, { + bool isInserting = false, + }) { + final context = VerificationContext(); + final data = instance.toColumns(true); + if (data.containsKey('local_id')) { + context.handle( + _localIdMeta, + localId.isAcceptableOrUnknown(data['local_id']!, _localIdMeta), + ); + } + if (data.containsKey('server_id')) { + context.handle( + _serverIdMeta, + serverId.isAcceptableOrUnknown(data['server_id']!, _serverIdMeta), + ); + } + if (data.containsKey('outlet_id')) { + context.handle( + _outletIdMeta, + outletId.isAcceptableOrUnknown(data['outlet_id']!, _outletIdMeta), + ); + } else if (isInserting) { + context.missing(_outletIdMeta); + } + if (data.containsKey('user_id')) { + context.handle( + _userIdMeta, + userId.isAcceptableOrUnknown(data['user_id']!, _userIdMeta), + ); + } else if (isInserting) { + context.missing(_userIdMeta); + } + if (data.containsKey('reference_type')) { + context.handle( + _referenceTypeMeta, + referenceType.isAcceptableOrUnknown( + data['reference_type']!, + _referenceTypeMeta, + ), + ); + } else if (isInserting) { + context.missing(_referenceTypeMeta); + } + if (data.containsKey('reference_id')) { + context.handle( + _referenceIdMeta, + referenceId.isAcceptableOrUnknown( + data['reference_id']!, + _referenceIdMeta, + ), + ); + } else if (isInserting) { + context.missing(_referenceIdMeta); + } + if (data.containsKey('reference_number')) { + context.handle( + _referenceNumberMeta, + referenceNumber.isAcceptableOrUnknown( + data['reference_number']!, + _referenceNumberMeta, + ), + ); + } else if (isInserting) { + context.missing(_referenceNumberMeta); + } + if (data.containsKey('stakeholder')) { + context.handle( + _stakeholderMeta, + stakeholder.isAcceptableOrUnknown( + data['stakeholder']!, + _stakeholderMeta, + ), + ); + } + if (data.containsKey('transaction_type')) { + context.handle( + _transactionTypeMeta, + transactionType.isAcceptableOrUnknown( + data['transaction_type']!, + _transactionTypeMeta, + ), + ); + } else if (isInserting) { + context.missing(_transactionTypeMeta); + } + if (data.containsKey('category')) { + context.handle( + _categoryMeta, + category.isAcceptableOrUnknown(data['category']!, _categoryMeta), + ); + } else if (isInserting) { + context.missing(_categoryMeta); + } + if (data.containsKey('amount')) { + context.handle( + _amountMeta, + amount.isAcceptableOrUnknown(data['amount']!, _amountMeta), + ); + } + if (data.containsKey('note')) { + context.handle( + _noteMeta, + note.isAcceptableOrUnknown(data['note']!, _noteMeta), + ); + } + if (data.containsKey('sync_status')) { + context.handle( + _syncStatusMeta, + syncStatus.isAcceptableOrUnknown(data['sync_status']!, _syncStatusMeta), + ); + } + if (data.containsKey('created_at')) { + context.handle( + _createdAtMeta, + createdAt.isAcceptableOrUnknown(data['created_at']!, _createdAtMeta), + ); + } + if (data.containsKey('updated_at')) { + context.handle( + _updatedAtMeta, + updatedAt.isAcceptableOrUnknown(data['updated_at']!, _updatedAtMeta), + ); + } + return context; + } + + @override + Set get $primaryKey => {localId}; + @override + Transaction map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return Transaction( + localId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}local_id'], + )!, + serverId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}server_id'], + ), + outletId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}outlet_id'], + )!, + userId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}user_id'], + )!, + referenceType: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}reference_type'], + )!, + referenceId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}reference_id'], + )!, + referenceNumber: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}reference_number'], + )!, + stakeholder: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}stakeholder'], + ), + transactionType: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}transaction_type'], + )!, + category: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}category'], + )!, + amount: attachedDatabase.typeMapping.read( + DriftSqlType.double, + data['${effectivePrefix}amount'], + )!, + note: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}note'], + ), + syncStatus: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}sync_status'], + )!, + createdAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}created_at'], + )!, + updatedAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}updated_at'], + )!, + ); + } + + @override + $TransactionsTable createAlias(String alias) { + return $TransactionsTable(attachedDatabase, alias); + } +} + +class Transaction extends DataClass implements Insertable { + final String localId; + final String? serverId; + final String outletId; + final String userId; + final String referenceType; + final String referenceId; + final String referenceNumber; + final String? stakeholder; + final String transactionType; + final String category; + final double amount; + final String? note; + final String syncStatus; + final DateTime createdAt; + final DateTime updatedAt; + const Transaction({ + required this.localId, + this.serverId, + required this.outletId, + required this.userId, + required this.referenceType, + required this.referenceId, + required this.referenceNumber, + this.stakeholder, + required this.transactionType, + required this.category, + required this.amount, + this.note, + required this.syncStatus, + required this.createdAt, + required this.updatedAt, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['local_id'] = Variable(localId); + if (!nullToAbsent || serverId != null) { + map['server_id'] = Variable(serverId); + } + map['outlet_id'] = Variable(outletId); + map['user_id'] = Variable(userId); + map['reference_type'] = Variable(referenceType); + map['reference_id'] = Variable(referenceId); + map['reference_number'] = Variable(referenceNumber); + if (!nullToAbsent || stakeholder != null) { + map['stakeholder'] = Variable(stakeholder); + } + map['transaction_type'] = Variable(transactionType); + map['category'] = Variable(category); + map['amount'] = Variable(amount); + if (!nullToAbsent || note != null) { + map['note'] = Variable(note); + } + map['sync_status'] = Variable(syncStatus); + map['created_at'] = Variable(createdAt); + map['updated_at'] = Variable(updatedAt); + return map; + } + + TransactionsCompanion toCompanion(bool nullToAbsent) { + return TransactionsCompanion( + localId: Value(localId), + serverId: serverId == null && nullToAbsent + ? const Value.absent() + : Value(serverId), + outletId: Value(outletId), + userId: Value(userId), + referenceType: Value(referenceType), + referenceId: Value(referenceId), + referenceNumber: Value(referenceNumber), + stakeholder: stakeholder == null && nullToAbsent + ? const Value.absent() + : Value(stakeholder), + transactionType: Value(transactionType), + category: Value(category), + amount: Value(amount), + note: note == null && nullToAbsent ? const Value.absent() : Value(note), + syncStatus: Value(syncStatus), + createdAt: Value(createdAt), + updatedAt: Value(updatedAt), + ); + } + + factory Transaction.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return Transaction( + localId: serializer.fromJson(json['localId']), + serverId: serializer.fromJson(json['serverId']), + outletId: serializer.fromJson(json['outletId']), + userId: serializer.fromJson(json['userId']), + referenceType: serializer.fromJson(json['referenceType']), + referenceId: serializer.fromJson(json['referenceId']), + referenceNumber: serializer.fromJson(json['referenceNumber']), + stakeholder: serializer.fromJson(json['stakeholder']), + transactionType: serializer.fromJson(json['transactionType']), + category: serializer.fromJson(json['category']), + amount: serializer.fromJson(json['amount']), + note: serializer.fromJson(json['note']), + syncStatus: serializer.fromJson(json['syncStatus']), + createdAt: serializer.fromJson(json['createdAt']), + updatedAt: serializer.fromJson(json['updatedAt']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'localId': serializer.toJson(localId), + 'serverId': serializer.toJson(serverId), + 'outletId': serializer.toJson(outletId), + 'userId': serializer.toJson(userId), + 'referenceType': serializer.toJson(referenceType), + 'referenceId': serializer.toJson(referenceId), + 'referenceNumber': serializer.toJson(referenceNumber), + 'stakeholder': serializer.toJson(stakeholder), + 'transactionType': serializer.toJson(transactionType), + 'category': serializer.toJson(category), + 'amount': serializer.toJson(amount), + 'note': serializer.toJson(note), + 'syncStatus': serializer.toJson(syncStatus), + 'createdAt': serializer.toJson(createdAt), + 'updatedAt': serializer.toJson(updatedAt), + }; + } + + Transaction copyWith({ + String? localId, + Value serverId = const Value.absent(), + String? outletId, + String? userId, + String? referenceType, + String? referenceId, + String? referenceNumber, + Value stakeholder = const Value.absent(), + String? transactionType, + String? category, + double? amount, + Value note = const Value.absent(), + String? syncStatus, + DateTime? createdAt, + DateTime? updatedAt, + }) => Transaction( + localId: localId ?? this.localId, + serverId: serverId.present ? serverId.value : this.serverId, + outletId: outletId ?? this.outletId, + userId: userId ?? this.userId, + referenceType: referenceType ?? this.referenceType, + referenceId: referenceId ?? this.referenceId, + referenceNumber: referenceNumber ?? this.referenceNumber, + stakeholder: stakeholder.present ? stakeholder.value : this.stakeholder, + transactionType: transactionType ?? this.transactionType, + category: category ?? this.category, + amount: amount ?? this.amount, + note: note.present ? note.value : this.note, + syncStatus: syncStatus ?? this.syncStatus, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + ); + Transaction copyWithCompanion(TransactionsCompanion data) { + return Transaction( + localId: data.localId.present ? data.localId.value : this.localId, + serverId: data.serverId.present ? data.serverId.value : this.serverId, + outletId: data.outletId.present ? data.outletId.value : this.outletId, + userId: data.userId.present ? data.userId.value : this.userId, + referenceType: data.referenceType.present + ? data.referenceType.value + : this.referenceType, + referenceId: data.referenceId.present + ? data.referenceId.value + : this.referenceId, + referenceNumber: data.referenceNumber.present + ? data.referenceNumber.value + : this.referenceNumber, + stakeholder: data.stakeholder.present + ? data.stakeholder.value + : this.stakeholder, + transactionType: data.transactionType.present + ? data.transactionType.value + : this.transactionType, + category: data.category.present ? data.category.value : this.category, + amount: data.amount.present ? data.amount.value : this.amount, + note: data.note.present ? data.note.value : this.note, + syncStatus: data.syncStatus.present + ? data.syncStatus.value + : this.syncStatus, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, + ); + } + + @override + String toString() { + return (StringBuffer('Transaction(') + ..write('localId: $localId, ') + ..write('serverId: $serverId, ') + ..write('outletId: $outletId, ') + ..write('userId: $userId, ') + ..write('referenceType: $referenceType, ') + ..write('referenceId: $referenceId, ') + ..write('referenceNumber: $referenceNumber, ') + ..write('stakeholder: $stakeholder, ') + ..write('transactionType: $transactionType, ') + ..write('category: $category, ') + ..write('amount: $amount, ') + ..write('note: $note, ') + ..write('syncStatus: $syncStatus, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + localId, + serverId, + outletId, + userId, + referenceType, + referenceId, + referenceNumber, + stakeholder, + transactionType, + category, + amount, + note, + syncStatus, + createdAt, + updatedAt, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is Transaction && + other.localId == this.localId && + other.serverId == this.serverId && + other.outletId == this.outletId && + other.userId == this.userId && + other.referenceType == this.referenceType && + other.referenceId == this.referenceId && + other.referenceNumber == this.referenceNumber && + other.stakeholder == this.stakeholder && + other.transactionType == this.transactionType && + other.category == this.category && + other.amount == this.amount && + other.note == this.note && + other.syncStatus == this.syncStatus && + other.createdAt == this.createdAt && + other.updatedAt == this.updatedAt); +} + +class TransactionsCompanion extends UpdateCompanion { + final Value localId; + final Value serverId; + final Value outletId; + final Value userId; + final Value referenceType; + final Value referenceId; + final Value referenceNumber; + final Value stakeholder; + final Value transactionType; + final Value category; + final Value amount; + final Value note; + final Value syncStatus; + final Value createdAt; + final Value updatedAt; + final Value rowid; + const TransactionsCompanion({ + this.localId = const Value.absent(), + this.serverId = const Value.absent(), + this.outletId = const Value.absent(), + this.userId = const Value.absent(), + this.referenceType = const Value.absent(), + this.referenceId = const Value.absent(), + this.referenceNumber = const Value.absent(), + this.stakeholder = const Value.absent(), + this.transactionType = const Value.absent(), + this.category = const Value.absent(), + this.amount = const Value.absent(), + this.note = const Value.absent(), + this.syncStatus = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.rowid = const Value.absent(), + }); + TransactionsCompanion.insert({ + this.localId = const Value.absent(), + this.serverId = const Value.absent(), + required String outletId, + required String userId, + required String referenceType, + required String referenceId, + required String referenceNumber, + this.stakeholder = const Value.absent(), + required String transactionType, + required String category, + this.amount = const Value.absent(), + this.note = const Value.absent(), + this.syncStatus = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.rowid = const Value.absent(), + }) : outletId = Value(outletId), + userId = Value(userId), + referenceType = Value(referenceType), + referenceId = Value(referenceId), + referenceNumber = Value(referenceNumber), + transactionType = Value(transactionType), + category = Value(category); + static Insertable custom({ + Expression? localId, + Expression? serverId, + Expression? outletId, + Expression? userId, + Expression? referenceType, + Expression? referenceId, + Expression? referenceNumber, + Expression? stakeholder, + Expression? transactionType, + Expression? category, + Expression? amount, + Expression? note, + Expression? syncStatus, + Expression? createdAt, + Expression? updatedAt, + Expression? rowid, + }) { + return RawValuesInsertable({ + if (localId != null) 'local_id': localId, + if (serverId != null) 'server_id': serverId, + if (outletId != null) 'outlet_id': outletId, + if (userId != null) 'user_id': userId, + if (referenceType != null) 'reference_type': referenceType, + if (referenceId != null) 'reference_id': referenceId, + if (referenceNumber != null) 'reference_number': referenceNumber, + if (stakeholder != null) 'stakeholder': stakeholder, + if (transactionType != null) 'transaction_type': transactionType, + if (category != null) 'category': category, + if (amount != null) 'amount': amount, + if (note != null) 'note': note, + if (syncStatus != null) 'sync_status': syncStatus, + if (createdAt != null) 'created_at': createdAt, + if (updatedAt != null) 'updated_at': updatedAt, + if (rowid != null) 'rowid': rowid, + }); + } + + TransactionsCompanion copyWith({ + Value? localId, + Value? serverId, + Value? outletId, + Value? userId, + Value? referenceType, + Value? referenceId, + Value? referenceNumber, + Value? stakeholder, + Value? transactionType, + Value? category, + Value? amount, + Value? note, + Value? syncStatus, + Value? createdAt, + Value? updatedAt, + Value? rowid, + }) { + return TransactionsCompanion( + localId: localId ?? this.localId, + serverId: serverId ?? this.serverId, + outletId: outletId ?? this.outletId, + userId: userId ?? this.userId, + referenceType: referenceType ?? this.referenceType, + referenceId: referenceId ?? this.referenceId, + referenceNumber: referenceNumber ?? this.referenceNumber, + stakeholder: stakeholder ?? this.stakeholder, + transactionType: transactionType ?? this.transactionType, + category: category ?? this.category, + amount: amount ?? this.amount, + note: note ?? this.note, + syncStatus: syncStatus ?? this.syncStatus, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (localId.present) { + map['local_id'] = Variable(localId.value); + } + if (serverId.present) { + map['server_id'] = Variable(serverId.value); + } + if (outletId.present) { + map['outlet_id'] = Variable(outletId.value); + } + if (userId.present) { + map['user_id'] = Variable(userId.value); + } + if (referenceType.present) { + map['reference_type'] = Variable(referenceType.value); + } + if (referenceId.present) { + map['reference_id'] = Variable(referenceId.value); + } + if (referenceNumber.present) { + map['reference_number'] = Variable(referenceNumber.value); + } + if (stakeholder.present) { + map['stakeholder'] = Variable(stakeholder.value); + } + if (transactionType.present) { + map['transaction_type'] = Variable(transactionType.value); + } + if (category.present) { + map['category'] = Variable(category.value); + } + if (amount.present) { + map['amount'] = Variable(amount.value); + } + if (note.present) { + map['note'] = Variable(note.value); + } + if (syncStatus.present) { + map['sync_status'] = Variable(syncStatus.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (updatedAt.present) { + map['updated_at'] = Variable(updatedAt.value); + } + if (rowid.present) { + map['rowid'] = Variable(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('TransactionsCompanion(') + ..write('localId: $localId, ') + ..write('serverId: $serverId, ') + ..write('outletId: $outletId, ') + ..write('userId: $userId, ') + ..write('referenceType: $referenceType, ') + ..write('referenceId: $referenceId, ') + ..write('referenceNumber: $referenceNumber, ') + ..write('stakeholder: $stakeholder, ') + ..write('transactionType: $transactionType, ') + ..write('category: $category, ') + ..write('amount: $amount, ') + ..write('note: $note, ') + ..write('syncStatus: $syncStatus, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + +class $CampaignsTable extends Campaigns + with TableInfo<$CampaignsTable, Campaign> { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + $CampaignsTable(this.attachedDatabase, [this._alias]); + static const VerificationMeta _localIdMeta = const VerificationMeta( + 'localId', + ); + @override + late final GeneratedColumn localId = GeneratedColumn( + 'local_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: false, + clientDefault: () => const Uuid().v7().toString(), + ); + static const VerificationMeta _serverIdMeta = const VerificationMeta( + 'serverId', + ); + @override + late final GeneratedColumn serverId = GeneratedColumn( + 'server_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _titleMeta = const VerificationMeta('title'); + @override + late final GeneratedColumn title = GeneratedColumn( + 'title', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + static const VerificationMeta _localBannerPathMeta = const VerificationMeta( + 'localBannerPath', + ); + @override + late final GeneratedColumn localBannerPath = GeneratedColumn( + 'local_banner_path', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + static const VerificationMeta _serverBannerUrlMeta = const VerificationMeta( + 'serverBannerUrl', + ); + @override + late final GeneratedColumn serverBannerUrl = GeneratedColumn( + 'server_banner_url', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _startDateMeta = const VerificationMeta( + 'startDate', + ); + @override + late final GeneratedColumn startDate = GeneratedColumn( + 'start_date', + aliasedName, + true, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + ); + static const VerificationMeta _endDateMeta = const VerificationMeta( + 'endDate', + ); + @override + late final GeneratedColumn endDate = GeneratedColumn( + 'end_date', + aliasedName, + true, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + ); + static const VerificationMeta _actionTypeMeta = const VerificationMeta( + 'actionType', + ); + @override + late final GeneratedColumn actionType = GeneratedColumn( + 'action_type', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _actionReferenceIdMeta = const VerificationMeta( + 'actionReferenceId', + ); + @override + late final GeneratedColumn actionReferenceId = + GeneratedColumn( + 'action_reference_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _isActiveMeta = const VerificationMeta( + 'isActive', + ); + @override + late final GeneratedColumn isActive = GeneratedColumn( + 'is_active', + aliasedName, + false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("is_active" IN (0, 1))', + ), + defaultValue: const Constant(true), + ); + static const VerificationMeta _syncStatusMeta = const VerificationMeta( + 'syncStatus', + ); + @override + late final GeneratedColumn syncStatus = GeneratedColumn( + 'sync_status', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultValue: const Constant('dirty'), + ); + static const VerificationMeta _createdAtMeta = const VerificationMeta( + 'createdAt', + ); + @override + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: currentDateAndTime, + ); + static const VerificationMeta _updatedAtMeta = const VerificationMeta( + 'updatedAt', + ); + @override + late final GeneratedColumn updatedAt = GeneratedColumn( + 'updated_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: currentDateAndTime, + ); + static const VerificationMeta _deletedAtMeta = const VerificationMeta( + 'deletedAt', + ); + @override + late final GeneratedColumn deletedAt = GeneratedColumn( + 'deleted_at', + aliasedName, + true, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + ); + @override + List get $columns => [ + localId, + serverId, + title, + localBannerPath, + serverBannerUrl, + startDate, + endDate, + actionType, + actionReferenceId, + isActive, + syncStatus, + createdAt, + updatedAt, + deletedAt, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'campaigns'; + @override + VerificationContext validateIntegrity( + Insertable instance, { + bool isInserting = false, + }) { + final context = VerificationContext(); + final data = instance.toColumns(true); + if (data.containsKey('local_id')) { + context.handle( + _localIdMeta, + localId.isAcceptableOrUnknown(data['local_id']!, _localIdMeta), + ); + } + if (data.containsKey('server_id')) { + context.handle( + _serverIdMeta, + serverId.isAcceptableOrUnknown(data['server_id']!, _serverIdMeta), + ); + } + if (data.containsKey('title')) { + context.handle( + _titleMeta, + title.isAcceptableOrUnknown(data['title']!, _titleMeta), + ); + } else if (isInserting) { + context.missing(_titleMeta); + } + if (data.containsKey('local_banner_path')) { + context.handle( + _localBannerPathMeta, + localBannerPath.isAcceptableOrUnknown( + data['local_banner_path']!, + _localBannerPathMeta, + ), + ); + } else if (isInserting) { + context.missing(_localBannerPathMeta); + } + if (data.containsKey('server_banner_url')) { + context.handle( + _serverBannerUrlMeta, + serverBannerUrl.isAcceptableOrUnknown( + data['server_banner_url']!, + _serverBannerUrlMeta, + ), + ); + } + if (data.containsKey('start_date')) { + context.handle( + _startDateMeta, + startDate.isAcceptableOrUnknown(data['start_date']!, _startDateMeta), + ); + } + if (data.containsKey('end_date')) { + context.handle( + _endDateMeta, + endDate.isAcceptableOrUnknown(data['end_date']!, _endDateMeta), + ); + } + if (data.containsKey('action_type')) { + context.handle( + _actionTypeMeta, + actionType.isAcceptableOrUnknown(data['action_type']!, _actionTypeMeta), + ); + } + if (data.containsKey('action_reference_id')) { + context.handle( + _actionReferenceIdMeta, + actionReferenceId.isAcceptableOrUnknown( + data['action_reference_id']!, + _actionReferenceIdMeta, + ), + ); + } + if (data.containsKey('is_active')) { + context.handle( + _isActiveMeta, + isActive.isAcceptableOrUnknown(data['is_active']!, _isActiveMeta), + ); + } + if (data.containsKey('sync_status')) { + context.handle( + _syncStatusMeta, + syncStatus.isAcceptableOrUnknown(data['sync_status']!, _syncStatusMeta), + ); + } + if (data.containsKey('created_at')) { + context.handle( + _createdAtMeta, + createdAt.isAcceptableOrUnknown(data['created_at']!, _createdAtMeta), + ); + } + if (data.containsKey('updated_at')) { + context.handle( + _updatedAtMeta, + updatedAt.isAcceptableOrUnknown(data['updated_at']!, _updatedAtMeta), + ); + } + if (data.containsKey('deleted_at')) { + context.handle( + _deletedAtMeta, + deletedAt.isAcceptableOrUnknown(data['deleted_at']!, _deletedAtMeta), + ); + } + return context; + } + + @override + Set get $primaryKey => {localId}; + @override + Campaign map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return Campaign( + localId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}local_id'], + )!, + serverId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}server_id'], + ), + title: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}title'], + )!, + localBannerPath: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}local_banner_path'], + )!, + serverBannerUrl: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}server_banner_url'], + ), + startDate: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}start_date'], + ), + endDate: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}end_date'], + ), + actionType: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}action_type'], + ), + actionReferenceId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}action_reference_id'], + ), + isActive: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}is_active'], + )!, + syncStatus: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}sync_status'], + )!, + createdAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}created_at'], + )!, + updatedAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}updated_at'], + )!, + deletedAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}deleted_at'], + ), + ); + } + + @override + $CampaignsTable createAlias(String alias) { + return $CampaignsTable(attachedDatabase, alias); + } +} + +class Campaign extends DataClass implements Insertable { + final String localId; + final String? serverId; + final String title; + final String localBannerPath; + final String? serverBannerUrl; + final DateTime? startDate; + final DateTime? endDate; + final String? actionType; + final String? actionReferenceId; + final bool isActive; + final String syncStatus; + final DateTime createdAt; + final DateTime updatedAt; + final DateTime? deletedAt; + const Campaign({ + required this.localId, + this.serverId, + required this.title, + required this.localBannerPath, + this.serverBannerUrl, + this.startDate, + this.endDate, + this.actionType, + this.actionReferenceId, + required this.isActive, + required this.syncStatus, + required this.createdAt, + required this.updatedAt, + this.deletedAt, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['local_id'] = Variable(localId); + if (!nullToAbsent || serverId != null) { + map['server_id'] = Variable(serverId); + } + map['title'] = Variable(title); + map['local_banner_path'] = Variable(localBannerPath); + if (!nullToAbsent || serverBannerUrl != null) { + map['server_banner_url'] = Variable(serverBannerUrl); + } + if (!nullToAbsent || startDate != null) { + map['start_date'] = Variable(startDate); + } + if (!nullToAbsent || endDate != null) { + map['end_date'] = Variable(endDate); + } + if (!nullToAbsent || actionType != null) { + map['action_type'] = Variable(actionType); + } + if (!nullToAbsent || actionReferenceId != null) { + map['action_reference_id'] = Variable(actionReferenceId); + } + map['is_active'] = Variable(isActive); + map['sync_status'] = Variable(syncStatus); + map['created_at'] = Variable(createdAt); + map['updated_at'] = Variable(updatedAt); + if (!nullToAbsent || deletedAt != null) { + map['deleted_at'] = Variable(deletedAt); + } + return map; + } + + CampaignsCompanion toCompanion(bool nullToAbsent) { + return CampaignsCompanion( + localId: Value(localId), + serverId: serverId == null && nullToAbsent + ? const Value.absent() + : Value(serverId), + title: Value(title), + localBannerPath: Value(localBannerPath), + serverBannerUrl: serverBannerUrl == null && nullToAbsent + ? const Value.absent() + : Value(serverBannerUrl), + startDate: startDate == null && nullToAbsent + ? const Value.absent() + : Value(startDate), + endDate: endDate == null && nullToAbsent + ? const Value.absent() + : Value(endDate), + actionType: actionType == null && nullToAbsent + ? const Value.absent() + : Value(actionType), + actionReferenceId: actionReferenceId == null && nullToAbsent + ? const Value.absent() + : Value(actionReferenceId), + isActive: Value(isActive), + syncStatus: Value(syncStatus), + createdAt: Value(createdAt), + updatedAt: Value(updatedAt), + deletedAt: deletedAt == null && nullToAbsent + ? const Value.absent() + : Value(deletedAt), + ); + } + + factory Campaign.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return Campaign( + localId: serializer.fromJson(json['localId']), + serverId: serializer.fromJson(json['serverId']), + title: serializer.fromJson(json['title']), + localBannerPath: serializer.fromJson(json['localBannerPath']), + serverBannerUrl: serializer.fromJson(json['serverBannerUrl']), + startDate: serializer.fromJson(json['startDate']), + endDate: serializer.fromJson(json['endDate']), + actionType: serializer.fromJson(json['actionType']), + actionReferenceId: serializer.fromJson( + json['actionReferenceId'], + ), + isActive: serializer.fromJson(json['isActive']), + syncStatus: serializer.fromJson(json['syncStatus']), + createdAt: serializer.fromJson(json['createdAt']), + updatedAt: serializer.fromJson(json['updatedAt']), + deletedAt: serializer.fromJson(json['deletedAt']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'localId': serializer.toJson(localId), + 'serverId': serializer.toJson(serverId), + 'title': serializer.toJson(title), + 'localBannerPath': serializer.toJson(localBannerPath), + 'serverBannerUrl': serializer.toJson(serverBannerUrl), + 'startDate': serializer.toJson(startDate), + 'endDate': serializer.toJson(endDate), + 'actionType': serializer.toJson(actionType), + 'actionReferenceId': serializer.toJson(actionReferenceId), + 'isActive': serializer.toJson(isActive), + 'syncStatus': serializer.toJson(syncStatus), + 'createdAt': serializer.toJson(createdAt), + 'updatedAt': serializer.toJson(updatedAt), + 'deletedAt': serializer.toJson(deletedAt), + }; + } + + Campaign copyWith({ + String? localId, + Value serverId = const Value.absent(), + String? title, + String? localBannerPath, + Value serverBannerUrl = const Value.absent(), + Value startDate = const Value.absent(), + Value endDate = const Value.absent(), + Value actionType = const Value.absent(), + Value actionReferenceId = const Value.absent(), + bool? isActive, + String? syncStatus, + DateTime? createdAt, + DateTime? updatedAt, + Value deletedAt = const Value.absent(), + }) => Campaign( + localId: localId ?? this.localId, + serverId: serverId.present ? serverId.value : this.serverId, + title: title ?? this.title, + localBannerPath: localBannerPath ?? this.localBannerPath, + serverBannerUrl: serverBannerUrl.present + ? serverBannerUrl.value + : this.serverBannerUrl, + startDate: startDate.present ? startDate.value : this.startDate, + endDate: endDate.present ? endDate.value : this.endDate, + actionType: actionType.present ? actionType.value : this.actionType, + actionReferenceId: actionReferenceId.present + ? actionReferenceId.value + : this.actionReferenceId, + isActive: isActive ?? this.isActive, + syncStatus: syncStatus ?? this.syncStatus, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + deletedAt: deletedAt.present ? deletedAt.value : this.deletedAt, + ); + Campaign copyWithCompanion(CampaignsCompanion data) { + return Campaign( + localId: data.localId.present ? data.localId.value : this.localId, + serverId: data.serverId.present ? data.serverId.value : this.serverId, + title: data.title.present ? data.title.value : this.title, + localBannerPath: data.localBannerPath.present + ? data.localBannerPath.value + : this.localBannerPath, + serverBannerUrl: data.serverBannerUrl.present + ? data.serverBannerUrl.value + : this.serverBannerUrl, + startDate: data.startDate.present ? data.startDate.value : this.startDate, + endDate: data.endDate.present ? data.endDate.value : this.endDate, + actionType: data.actionType.present + ? data.actionType.value + : this.actionType, + actionReferenceId: data.actionReferenceId.present + ? data.actionReferenceId.value + : this.actionReferenceId, + isActive: data.isActive.present ? data.isActive.value : this.isActive, + syncStatus: data.syncStatus.present + ? data.syncStatus.value + : this.syncStatus, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, + deletedAt: data.deletedAt.present ? data.deletedAt.value : this.deletedAt, + ); + } + + @override + String toString() { + return (StringBuffer('Campaign(') + ..write('localId: $localId, ') + ..write('serverId: $serverId, ') + ..write('title: $title, ') + ..write('localBannerPath: $localBannerPath, ') + ..write('serverBannerUrl: $serverBannerUrl, ') + ..write('startDate: $startDate, ') + ..write('endDate: $endDate, ') + ..write('actionType: $actionType, ') + ..write('actionReferenceId: $actionReferenceId, ') + ..write('isActive: $isActive, ') + ..write('syncStatus: $syncStatus, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('deletedAt: $deletedAt') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + localId, + serverId, + title, + localBannerPath, + serverBannerUrl, + startDate, + endDate, + actionType, + actionReferenceId, + isActive, + syncStatus, + createdAt, + updatedAt, + deletedAt, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is Campaign && + other.localId == this.localId && + other.serverId == this.serverId && + other.title == this.title && + other.localBannerPath == this.localBannerPath && + other.serverBannerUrl == this.serverBannerUrl && + other.startDate == this.startDate && + other.endDate == this.endDate && + other.actionType == this.actionType && + other.actionReferenceId == this.actionReferenceId && + other.isActive == this.isActive && + other.syncStatus == this.syncStatus && + other.createdAt == this.createdAt && + other.updatedAt == this.updatedAt && + other.deletedAt == this.deletedAt); +} + +class CampaignsCompanion extends UpdateCompanion { + final Value localId; + final Value serverId; + final Value title; + final Value localBannerPath; + final Value serverBannerUrl; + final Value startDate; + final Value endDate; + final Value actionType; + final Value actionReferenceId; + final Value isActive; + final Value syncStatus; + final Value createdAt; + final Value updatedAt; + final Value deletedAt; + final Value rowid; + const CampaignsCompanion({ + this.localId = const Value.absent(), + this.serverId = const Value.absent(), + this.title = const Value.absent(), + this.localBannerPath = const Value.absent(), + this.serverBannerUrl = const Value.absent(), + this.startDate = const Value.absent(), + this.endDate = const Value.absent(), + this.actionType = const Value.absent(), + this.actionReferenceId = const Value.absent(), + this.isActive = const Value.absent(), + this.syncStatus = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.deletedAt = const Value.absent(), + this.rowid = const Value.absent(), + }); + CampaignsCompanion.insert({ + this.localId = const Value.absent(), + this.serverId = const Value.absent(), + required String title, + required String localBannerPath, + this.serverBannerUrl = const Value.absent(), + this.startDate = const Value.absent(), + this.endDate = const Value.absent(), + this.actionType = const Value.absent(), + this.actionReferenceId = const Value.absent(), + this.isActive = const Value.absent(), + this.syncStatus = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.deletedAt = const Value.absent(), + this.rowid = const Value.absent(), + }) : title = Value(title), + localBannerPath = Value(localBannerPath); + static Insertable custom({ + Expression? localId, + Expression? serverId, + Expression? title, + Expression? localBannerPath, + Expression? serverBannerUrl, + Expression? startDate, + Expression? endDate, + Expression? actionType, + Expression? actionReferenceId, + Expression? isActive, + Expression? syncStatus, + Expression? createdAt, + Expression? updatedAt, + Expression? deletedAt, + Expression? rowid, + }) { + return RawValuesInsertable({ + if (localId != null) 'local_id': localId, + if (serverId != null) 'server_id': serverId, + if (title != null) 'title': title, + if (localBannerPath != null) 'local_banner_path': localBannerPath, + if (serverBannerUrl != null) 'server_banner_url': serverBannerUrl, + if (startDate != null) 'start_date': startDate, + if (endDate != null) 'end_date': endDate, + if (actionType != null) 'action_type': actionType, + if (actionReferenceId != null) 'action_reference_id': actionReferenceId, + if (isActive != null) 'is_active': isActive, + if (syncStatus != null) 'sync_status': syncStatus, + if (createdAt != null) 'created_at': createdAt, + if (updatedAt != null) 'updated_at': updatedAt, + if (deletedAt != null) 'deleted_at': deletedAt, + if (rowid != null) 'rowid': rowid, + }); + } + + CampaignsCompanion copyWith({ + Value? localId, + Value? serverId, + Value? title, + Value? localBannerPath, + Value? serverBannerUrl, + Value? startDate, + Value? endDate, + Value? actionType, + Value? actionReferenceId, + Value? isActive, + Value? syncStatus, + Value? createdAt, + Value? updatedAt, + Value? deletedAt, + Value? rowid, + }) { + return CampaignsCompanion( + localId: localId ?? this.localId, + serverId: serverId ?? this.serverId, + title: title ?? this.title, + localBannerPath: localBannerPath ?? this.localBannerPath, + serverBannerUrl: serverBannerUrl ?? this.serverBannerUrl, + startDate: startDate ?? this.startDate, + endDate: endDate ?? this.endDate, + actionType: actionType ?? this.actionType, + actionReferenceId: actionReferenceId ?? this.actionReferenceId, + isActive: isActive ?? this.isActive, + syncStatus: syncStatus ?? this.syncStatus, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + deletedAt: deletedAt ?? this.deletedAt, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (localId.present) { + map['local_id'] = Variable(localId.value); + } + if (serverId.present) { + map['server_id'] = Variable(serverId.value); + } + if (title.present) { + map['title'] = Variable(title.value); + } + if (localBannerPath.present) { + map['local_banner_path'] = Variable(localBannerPath.value); + } + if (serverBannerUrl.present) { + map['server_banner_url'] = Variable(serverBannerUrl.value); + } + if (startDate.present) { + map['start_date'] = Variable(startDate.value); + } + if (endDate.present) { + map['end_date'] = Variable(endDate.value); + } + if (actionType.present) { + map['action_type'] = Variable(actionType.value); + } + if (actionReferenceId.present) { + map['action_reference_id'] = Variable(actionReferenceId.value); + } + if (isActive.present) { + map['is_active'] = Variable(isActive.value); + } + if (syncStatus.present) { + map['sync_status'] = Variable(syncStatus.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (updatedAt.present) { + map['updated_at'] = Variable(updatedAt.value); + } + if (deletedAt.present) { + map['deleted_at'] = Variable(deletedAt.value); + } + if (rowid.present) { + map['rowid'] = Variable(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('CampaignsCompanion(') + ..write('localId: $localId, ') + ..write('serverId: $serverId, ') + ..write('title: $title, ') + ..write('localBannerPath: $localBannerPath, ') + ..write('serverBannerUrl: $serverBannerUrl, ') + ..write('startDate: $startDate, ') + ..write('endDate: $endDate, ') + ..write('actionType: $actionType, ') + ..write('actionReferenceId: $actionReferenceId, ') + ..write('isActive: $isActive, ') + ..write('syncStatus: $syncStatus, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('deletedAt: $deletedAt, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + +class $OutletCampaignsTable extends OutletCampaigns + with TableInfo<$OutletCampaignsTable, OutletCampaign> { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + $OutletCampaignsTable(this.attachedDatabase, [this._alias]); + static const VerificationMeta _localIdMeta = const VerificationMeta( + 'localId', + ); + @override + late final GeneratedColumn localId = GeneratedColumn( + 'local_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: false, + clientDefault: () => const Uuid().v7().toString(), + ); + static const VerificationMeta _serverIdMeta = const VerificationMeta( + 'serverId', + ); + @override + late final GeneratedColumn serverId = GeneratedColumn( + 'server_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _campaignIdMeta = const VerificationMeta( + 'campaignId', + ); + @override + late final GeneratedColumn campaignId = GeneratedColumn( + 'campaign_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES campaigns (local_id)', + ), + ); + static const VerificationMeta _outletIdMeta = const VerificationMeta( + 'outletId', + ); + @override + late final GeneratedColumn outletId = GeneratedColumn( + 'outlet_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES outlets (local_id)', + ), + ); + static const VerificationMeta _syncStatusMeta = const VerificationMeta( + 'syncStatus', + ); + @override + late final GeneratedColumn syncStatus = GeneratedColumn( + 'sync_status', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultValue: const Constant('dirty'), + ); + static const VerificationMeta _createdAtMeta = const VerificationMeta( + 'createdAt', + ); + @override + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: currentDateAndTime, + ); + static const VerificationMeta _updatedAtMeta = const VerificationMeta( + 'updatedAt', + ); + @override + late final GeneratedColumn updatedAt = GeneratedColumn( + 'updated_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: currentDateAndTime, + ); + static const VerificationMeta _deletedAtMeta = const VerificationMeta( + 'deletedAt', + ); + @override + late final GeneratedColumn deletedAt = GeneratedColumn( + 'deleted_at', + aliasedName, + true, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + ); + @override + List get $columns => [ + localId, + serverId, + campaignId, + outletId, + syncStatus, + createdAt, + updatedAt, + deletedAt, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'outlet_campaigns'; + @override + VerificationContext validateIntegrity( + Insertable instance, { + bool isInserting = false, + }) { + final context = VerificationContext(); + final data = instance.toColumns(true); + if (data.containsKey('local_id')) { + context.handle( + _localIdMeta, + localId.isAcceptableOrUnknown(data['local_id']!, _localIdMeta), + ); + } + if (data.containsKey('server_id')) { + context.handle( + _serverIdMeta, + serverId.isAcceptableOrUnknown(data['server_id']!, _serverIdMeta), + ); + } + if (data.containsKey('campaign_id')) { + context.handle( + _campaignIdMeta, + campaignId.isAcceptableOrUnknown(data['campaign_id']!, _campaignIdMeta), + ); + } else if (isInserting) { + context.missing(_campaignIdMeta); + } + if (data.containsKey('outlet_id')) { + context.handle( + _outletIdMeta, + outletId.isAcceptableOrUnknown(data['outlet_id']!, _outletIdMeta), + ); + } + if (data.containsKey('sync_status')) { + context.handle( + _syncStatusMeta, + syncStatus.isAcceptableOrUnknown(data['sync_status']!, _syncStatusMeta), + ); + } + if (data.containsKey('created_at')) { + context.handle( + _createdAtMeta, + createdAt.isAcceptableOrUnknown(data['created_at']!, _createdAtMeta), + ); + } + if (data.containsKey('updated_at')) { + context.handle( + _updatedAtMeta, + updatedAt.isAcceptableOrUnknown(data['updated_at']!, _updatedAtMeta), + ); + } + if (data.containsKey('deleted_at')) { + context.handle( + _deletedAtMeta, + deletedAt.isAcceptableOrUnknown(data['deleted_at']!, _deletedAtMeta), + ); + } + return context; + } + + @override + Set get $primaryKey => {localId}; + @override + OutletCampaign map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return OutletCampaign( + localId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}local_id'], + )!, + serverId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}server_id'], + ), + campaignId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}campaign_id'], + )!, + outletId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}outlet_id'], + ), + syncStatus: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}sync_status'], + )!, + createdAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}created_at'], + )!, + updatedAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}updated_at'], + )!, + deletedAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}deleted_at'], + ), + ); + } + + @override + $OutletCampaignsTable createAlias(String alias) { + return $OutletCampaignsTable(attachedDatabase, alias); + } +} + +class OutletCampaign extends DataClass implements Insertable { + final String localId; + final String? serverId; + final String campaignId; + final String? outletId; + final String syncStatus; + final DateTime createdAt; + final DateTime updatedAt; + final DateTime? deletedAt; + const OutletCampaign({ + required this.localId, + this.serverId, + required this.campaignId, + this.outletId, + required this.syncStatus, + required this.createdAt, + required this.updatedAt, + this.deletedAt, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['local_id'] = Variable(localId); + if (!nullToAbsent || serverId != null) { + map['server_id'] = Variable(serverId); + } + map['campaign_id'] = Variable(campaignId); + if (!nullToAbsent || outletId != null) { + map['outlet_id'] = Variable(outletId); + } + map['sync_status'] = Variable(syncStatus); + map['created_at'] = Variable(createdAt); + map['updated_at'] = Variable(updatedAt); + if (!nullToAbsent || deletedAt != null) { + map['deleted_at'] = Variable(deletedAt); + } + return map; + } + + OutletCampaignsCompanion toCompanion(bool nullToAbsent) { + return OutletCampaignsCompanion( + localId: Value(localId), + serverId: serverId == null && nullToAbsent + ? const Value.absent() + : Value(serverId), + campaignId: Value(campaignId), + outletId: outletId == null && nullToAbsent + ? const Value.absent() + : Value(outletId), + syncStatus: Value(syncStatus), + createdAt: Value(createdAt), + updatedAt: Value(updatedAt), + deletedAt: deletedAt == null && nullToAbsent + ? const Value.absent() + : Value(deletedAt), + ); + } + + factory OutletCampaign.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return OutletCampaign( + localId: serializer.fromJson(json['localId']), + serverId: serializer.fromJson(json['serverId']), + campaignId: serializer.fromJson(json['campaignId']), + outletId: serializer.fromJson(json['outletId']), + syncStatus: serializer.fromJson(json['syncStatus']), + createdAt: serializer.fromJson(json['createdAt']), + updatedAt: serializer.fromJson(json['updatedAt']), + deletedAt: serializer.fromJson(json['deletedAt']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'localId': serializer.toJson(localId), + 'serverId': serializer.toJson(serverId), + 'campaignId': serializer.toJson(campaignId), + 'outletId': serializer.toJson(outletId), + 'syncStatus': serializer.toJson(syncStatus), + 'createdAt': serializer.toJson(createdAt), + 'updatedAt': serializer.toJson(updatedAt), + 'deletedAt': serializer.toJson(deletedAt), + }; + } + + OutletCampaign copyWith({ + String? localId, + Value serverId = const Value.absent(), + String? campaignId, + Value outletId = const Value.absent(), + String? syncStatus, + DateTime? createdAt, + DateTime? updatedAt, + Value deletedAt = const Value.absent(), + }) => OutletCampaign( + localId: localId ?? this.localId, + serverId: serverId.present ? serverId.value : this.serverId, + campaignId: campaignId ?? this.campaignId, + outletId: outletId.present ? outletId.value : this.outletId, + syncStatus: syncStatus ?? this.syncStatus, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + deletedAt: deletedAt.present ? deletedAt.value : this.deletedAt, + ); + OutletCampaign copyWithCompanion(OutletCampaignsCompanion data) { + return OutletCampaign( + localId: data.localId.present ? data.localId.value : this.localId, + serverId: data.serverId.present ? data.serverId.value : this.serverId, + campaignId: data.campaignId.present + ? data.campaignId.value + : this.campaignId, + outletId: data.outletId.present ? data.outletId.value : this.outletId, + syncStatus: data.syncStatus.present + ? data.syncStatus.value + : this.syncStatus, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, + deletedAt: data.deletedAt.present ? data.deletedAt.value : this.deletedAt, + ); + } + + @override + String toString() { + return (StringBuffer('OutletCampaign(') + ..write('localId: $localId, ') + ..write('serverId: $serverId, ') + ..write('campaignId: $campaignId, ') + ..write('outletId: $outletId, ') + ..write('syncStatus: $syncStatus, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('deletedAt: $deletedAt') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + localId, + serverId, + campaignId, + outletId, + syncStatus, + createdAt, + updatedAt, + deletedAt, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is OutletCampaign && + other.localId == this.localId && + other.serverId == this.serverId && + other.campaignId == this.campaignId && + other.outletId == this.outletId && + other.syncStatus == this.syncStatus && + other.createdAt == this.createdAt && + other.updatedAt == this.updatedAt && + other.deletedAt == this.deletedAt); +} + +class OutletCampaignsCompanion extends UpdateCompanion { + final Value localId; + final Value serverId; + final Value campaignId; + final Value outletId; + final Value syncStatus; + final Value createdAt; + final Value updatedAt; + final Value deletedAt; + final Value rowid; + const OutletCampaignsCompanion({ + this.localId = const Value.absent(), + this.serverId = const Value.absent(), + this.campaignId = const Value.absent(), + this.outletId = const Value.absent(), + this.syncStatus = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.deletedAt = const Value.absent(), + this.rowid = const Value.absent(), + }); + OutletCampaignsCompanion.insert({ + this.localId = const Value.absent(), + this.serverId = const Value.absent(), + required String campaignId, + this.outletId = const Value.absent(), + this.syncStatus = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.deletedAt = const Value.absent(), + this.rowid = const Value.absent(), + }) : campaignId = Value(campaignId); + static Insertable custom({ + Expression? localId, + Expression? serverId, + Expression? campaignId, + Expression? outletId, + Expression? syncStatus, + Expression? createdAt, + Expression? updatedAt, + Expression? deletedAt, + Expression? rowid, + }) { + return RawValuesInsertable({ + if (localId != null) 'local_id': localId, + if (serverId != null) 'server_id': serverId, + if (campaignId != null) 'campaign_id': campaignId, + if (outletId != null) 'outlet_id': outletId, + if (syncStatus != null) 'sync_status': syncStatus, + if (createdAt != null) 'created_at': createdAt, + if (updatedAt != null) 'updated_at': updatedAt, + if (deletedAt != null) 'deleted_at': deletedAt, + if (rowid != null) 'rowid': rowid, + }); + } + + OutletCampaignsCompanion copyWith({ + Value? localId, + Value? serverId, + Value? campaignId, + Value? outletId, + Value? syncStatus, + Value? createdAt, + Value? updatedAt, + Value? deletedAt, + Value? rowid, + }) { + return OutletCampaignsCompanion( + localId: localId ?? this.localId, + serverId: serverId ?? this.serverId, + campaignId: campaignId ?? this.campaignId, + outletId: outletId ?? this.outletId, + syncStatus: syncStatus ?? this.syncStatus, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + deletedAt: deletedAt ?? this.deletedAt, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (localId.present) { + map['local_id'] = Variable(localId.value); + } + if (serverId.present) { + map['server_id'] = Variable(serverId.value); + } + if (campaignId.present) { + map['campaign_id'] = Variable(campaignId.value); + } + if (outletId.present) { + map['outlet_id'] = Variable(outletId.value); + } + if (syncStatus.present) { + map['sync_status'] = Variable(syncStatus.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (updatedAt.present) { + map['updated_at'] = Variable(updatedAt.value); + } + if (deletedAt.present) { + map['deleted_at'] = Variable(deletedAt.value); + } + if (rowid.present) { + map['rowid'] = Variable(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('OutletCampaignsCompanion(') + ..write('localId: $localId, ') + ..write('serverId: $serverId, ') + ..write('campaignId: $campaignId, ') + ..write('outletId: $outletId, ') + ..write('syncStatus: $syncStatus, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('deletedAt: $deletedAt, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + +class $SyncQueuesTable extends SyncQueues + with TableInfo<$SyncQueuesTable, SyncQueue> { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + $SyncQueuesTable(this.attachedDatabase, [this._alias]); + static const VerificationMeta _idMeta = const VerificationMeta('id'); + @override + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + hasAutoIncrement: true, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'PRIMARY KEY AUTOINCREMENT', + ), + ); + static const VerificationMeta _tableTypeMeta = const VerificationMeta( + 'tableType', + ); + @override + late final GeneratedColumn tableType = GeneratedColumn( + 'table_type', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + static const VerificationMeta _tableLocalIdMeta = const VerificationMeta( + 'tableLocalId', + ); + @override + late final GeneratedColumn tableLocalId = GeneratedColumn( + 'table_local_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + static const VerificationMeta _actionMeta = const VerificationMeta('action'); + @override + late final GeneratedColumn action = GeneratedColumn( + 'action', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + static const VerificationMeta _statusMeta = const VerificationMeta('status'); + @override + late final GeneratedColumn status = GeneratedColumn( + 'status', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + @override + late final GeneratedColumnWithTypeConverter, String> + payload = GeneratedColumn( + 'payload', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ).withConverter>($SyncQueuesTable.$converterpayload); + static const VerificationMeta _batchIdMeta = const VerificationMeta( + 'batchId', + ); + @override + late final GeneratedColumn batchId = GeneratedColumn( + 'batch_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _lastErrorMeta = const VerificationMeta( + 'lastError', + ); + @override + late final GeneratedColumn lastError = GeneratedColumn( + 'last_error', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _retryCountMeta = const VerificationMeta( + 'retryCount', + ); + @override + late final GeneratedColumn retryCount = GeneratedColumn( + 'retry_count', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultValue: const Constant(0), + ); + static const VerificationMeta _createdAtMeta = const VerificationMeta( + 'createdAt', + ); + @override + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: currentDateAndTime, + ); + static const VerificationMeta _lastAttemptMeta = const VerificationMeta( + 'lastAttempt', + ); + @override + late final GeneratedColumn lastAttempt = GeneratedColumn( + 'last_attempt', + aliasedName, + true, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + ); + @override + List get $columns => [ + id, + tableType, + tableLocalId, + action, + status, + payload, + batchId, + lastError, + retryCount, + createdAt, + lastAttempt, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'sync_queues'; + @override + VerificationContext validateIntegrity( + Insertable instance, { + bool isInserting = false, + }) { + final context = VerificationContext(); + final data = instance.toColumns(true); + if (data.containsKey('id')) { + context.handle(_idMeta, id.isAcceptableOrUnknown(data['id']!, _idMeta)); + } + if (data.containsKey('table_type')) { + context.handle( + _tableTypeMeta, + tableType.isAcceptableOrUnknown(data['table_type']!, _tableTypeMeta), + ); + } else if (isInserting) { + context.missing(_tableTypeMeta); + } + if (data.containsKey('table_local_id')) { + context.handle( + _tableLocalIdMeta, + tableLocalId.isAcceptableOrUnknown( + data['table_local_id']!, + _tableLocalIdMeta, + ), + ); + } else if (isInserting) { + context.missing(_tableLocalIdMeta); + } + if (data.containsKey('action')) { + context.handle( + _actionMeta, + action.isAcceptableOrUnknown(data['action']!, _actionMeta), + ); + } else if (isInserting) { + context.missing(_actionMeta); + } + if (data.containsKey('status')) { + context.handle( + _statusMeta, + status.isAcceptableOrUnknown(data['status']!, _statusMeta), + ); + } else if (isInserting) { + context.missing(_statusMeta); + } + if (data.containsKey('batch_id')) { + context.handle( + _batchIdMeta, + batchId.isAcceptableOrUnknown(data['batch_id']!, _batchIdMeta), + ); + } + if (data.containsKey('last_error')) { + context.handle( + _lastErrorMeta, + lastError.isAcceptableOrUnknown(data['last_error']!, _lastErrorMeta), + ); + } + if (data.containsKey('retry_count')) { + context.handle( + _retryCountMeta, + retryCount.isAcceptableOrUnknown(data['retry_count']!, _retryCountMeta), + ); + } + if (data.containsKey('created_at')) { + context.handle( + _createdAtMeta, + createdAt.isAcceptableOrUnknown(data['created_at']!, _createdAtMeta), + ); + } + if (data.containsKey('last_attempt')) { + context.handle( + _lastAttemptMeta, + lastAttempt.isAcceptableOrUnknown( + data['last_attempt']!, + _lastAttemptMeta, + ), + ); + } + return context; + } + + @override + Set get $primaryKey => {id}; + @override + SyncQueue map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return SyncQueue( + id: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}id'], + )!, + tableType: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}table_type'], + )!, + tableLocalId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}table_local_id'], + )!, + action: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}action'], + )!, + status: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}status'], + )!, + payload: $SyncQueuesTable.$converterpayload.fromSql( + attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}payload'], + )!, + ), + batchId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}batch_id'], + ), + lastError: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}last_error'], + ), + retryCount: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}retry_count'], + )!, + createdAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}created_at'], + )!, + lastAttempt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}last_attempt'], + ), + ); + } + + @override + $SyncQueuesTable createAlias(String alias) { + return $SyncQueuesTable(attachedDatabase, alias); + } + + static TypeConverter, String> $converterpayload = + StringMapConverter(); +} + +class SyncQueue extends DataClass implements Insertable { + final int id; + final String tableType; + final String tableLocalId; + final String action; + final String status; + final Map payload; + final String? batchId; + final String? lastError; + final int retryCount; + final DateTime createdAt; + final DateTime? lastAttempt; + const SyncQueue({ + required this.id, + required this.tableType, + required this.tableLocalId, + required this.action, + required this.status, + required this.payload, + this.batchId, + this.lastError, + required this.retryCount, + required this.createdAt, + this.lastAttempt, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['table_type'] = Variable(tableType); + map['table_local_id'] = Variable(tableLocalId); + map['action'] = Variable(action); + map['status'] = Variable(status); + { + map['payload'] = Variable( + $SyncQueuesTable.$converterpayload.toSql(payload), + ); + } + if (!nullToAbsent || batchId != null) { + map['batch_id'] = Variable(batchId); + } + if (!nullToAbsent || lastError != null) { + map['last_error'] = Variable(lastError); + } + map['retry_count'] = Variable(retryCount); + map['created_at'] = Variable(createdAt); + if (!nullToAbsent || lastAttempt != null) { + map['last_attempt'] = Variable(lastAttempt); + } + return map; + } + + SyncQueuesCompanion toCompanion(bool nullToAbsent) { + return SyncQueuesCompanion( + id: Value(id), + tableType: Value(tableType), + tableLocalId: Value(tableLocalId), + action: Value(action), + status: Value(status), + payload: Value(payload), + batchId: batchId == null && nullToAbsent + ? const Value.absent() + : Value(batchId), + lastError: lastError == null && nullToAbsent + ? const Value.absent() + : Value(lastError), + retryCount: Value(retryCount), + createdAt: Value(createdAt), + lastAttempt: lastAttempt == null && nullToAbsent + ? const Value.absent() + : Value(lastAttempt), + ); + } + + factory SyncQueue.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return SyncQueue( + id: serializer.fromJson(json['id']), + tableType: serializer.fromJson(json['tableType']), + tableLocalId: serializer.fromJson(json['tableLocalId']), + action: serializer.fromJson(json['action']), + status: serializer.fromJson(json['status']), + payload: serializer.fromJson>(json['payload']), + batchId: serializer.fromJson(json['batchId']), + lastError: serializer.fromJson(json['lastError']), + retryCount: serializer.fromJson(json['retryCount']), + createdAt: serializer.fromJson(json['createdAt']), + lastAttempt: serializer.fromJson(json['lastAttempt']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'tableType': serializer.toJson(tableType), + 'tableLocalId': serializer.toJson(tableLocalId), + 'action': serializer.toJson(action), + 'status': serializer.toJson(status), + 'payload': serializer.toJson>(payload), + 'batchId': serializer.toJson(batchId), + 'lastError': serializer.toJson(lastError), + 'retryCount': serializer.toJson(retryCount), + 'createdAt': serializer.toJson(createdAt), + 'lastAttempt': serializer.toJson(lastAttempt), + }; + } + + SyncQueue copyWith({ + int? id, + String? tableType, + String? tableLocalId, + String? action, + String? status, + Map? payload, + Value batchId = const Value.absent(), + Value lastError = const Value.absent(), + int? retryCount, + DateTime? createdAt, + Value lastAttempt = const Value.absent(), + }) => SyncQueue( + id: id ?? this.id, + tableType: tableType ?? this.tableType, + tableLocalId: tableLocalId ?? this.tableLocalId, + action: action ?? this.action, + status: status ?? this.status, + payload: payload ?? this.payload, + batchId: batchId.present ? batchId.value : this.batchId, + lastError: lastError.present ? lastError.value : this.lastError, + retryCount: retryCount ?? this.retryCount, + createdAt: createdAt ?? this.createdAt, + lastAttempt: lastAttempt.present ? lastAttempt.value : this.lastAttempt, + ); + SyncQueue copyWithCompanion(SyncQueuesCompanion data) { + return SyncQueue( + id: data.id.present ? data.id.value : this.id, + tableType: data.tableType.present ? data.tableType.value : this.tableType, + tableLocalId: data.tableLocalId.present + ? data.tableLocalId.value + : this.tableLocalId, + action: data.action.present ? data.action.value : this.action, + status: data.status.present ? data.status.value : this.status, + payload: data.payload.present ? data.payload.value : this.payload, + batchId: data.batchId.present ? data.batchId.value : this.batchId, + lastError: data.lastError.present ? data.lastError.value : this.lastError, + retryCount: data.retryCount.present + ? data.retryCount.value + : this.retryCount, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + lastAttempt: data.lastAttempt.present + ? data.lastAttempt.value + : this.lastAttempt, + ); + } + + @override + String toString() { + return (StringBuffer('SyncQueue(') + ..write('id: $id, ') + ..write('tableType: $tableType, ') + ..write('tableLocalId: $tableLocalId, ') + ..write('action: $action, ') + ..write('status: $status, ') + ..write('payload: $payload, ') + ..write('batchId: $batchId, ') + ..write('lastError: $lastError, ') + ..write('retryCount: $retryCount, ') + ..write('createdAt: $createdAt, ') + ..write('lastAttempt: $lastAttempt') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + id, + tableType, + tableLocalId, + action, + status, + payload, + batchId, + lastError, + retryCount, + createdAt, + lastAttempt, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is SyncQueue && + other.id == this.id && + other.tableType == this.tableType && + other.tableLocalId == this.tableLocalId && + other.action == this.action && + other.status == this.status && + other.payload == this.payload && + other.batchId == this.batchId && + other.lastError == this.lastError && + other.retryCount == this.retryCount && + other.createdAt == this.createdAt && + other.lastAttempt == this.lastAttempt); +} + +class SyncQueuesCompanion extends UpdateCompanion { + final Value id; + final Value tableType; + final Value tableLocalId; + final Value action; + final Value status; + final Value> payload; + final Value batchId; + final Value lastError; + final Value retryCount; + final Value createdAt; + final Value lastAttempt; + const SyncQueuesCompanion({ + this.id = const Value.absent(), + this.tableType = const Value.absent(), + this.tableLocalId = const Value.absent(), + this.action = const Value.absent(), + this.status = const Value.absent(), + this.payload = const Value.absent(), + this.batchId = const Value.absent(), + this.lastError = const Value.absent(), + this.retryCount = const Value.absent(), + this.createdAt = const Value.absent(), + this.lastAttempt = const Value.absent(), + }); + SyncQueuesCompanion.insert({ + this.id = const Value.absent(), + required String tableType, + required String tableLocalId, + required String action, + required String status, + required Map payload, + this.batchId = const Value.absent(), + this.lastError = const Value.absent(), + this.retryCount = const Value.absent(), + this.createdAt = const Value.absent(), + this.lastAttempt = const Value.absent(), + }) : tableType = Value(tableType), + tableLocalId = Value(tableLocalId), + action = Value(action), + status = Value(status), + payload = Value(payload); + static Insertable custom({ + Expression? id, + Expression? tableType, + Expression? tableLocalId, + Expression? action, + Expression? status, + Expression? payload, + Expression? batchId, + Expression? lastError, + Expression? retryCount, + Expression? createdAt, + Expression? lastAttempt, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (tableType != null) 'table_type': tableType, + if (tableLocalId != null) 'table_local_id': tableLocalId, + if (action != null) 'action': action, + if (status != null) 'status': status, + if (payload != null) 'payload': payload, + if (batchId != null) 'batch_id': batchId, + if (lastError != null) 'last_error': lastError, + if (retryCount != null) 'retry_count': retryCount, + if (createdAt != null) 'created_at': createdAt, + if (lastAttempt != null) 'last_attempt': lastAttempt, + }); + } + + SyncQueuesCompanion copyWith({ + Value? id, + Value? tableType, + Value? tableLocalId, + Value? action, + Value? status, + Value>? payload, + Value? batchId, + Value? lastError, + Value? retryCount, + Value? createdAt, + Value? lastAttempt, + }) { + return SyncQueuesCompanion( + id: id ?? this.id, + tableType: tableType ?? this.tableType, + tableLocalId: tableLocalId ?? this.tableLocalId, + action: action ?? this.action, + status: status ?? this.status, + payload: payload ?? this.payload, + batchId: batchId ?? this.batchId, + lastError: lastError ?? this.lastError, + retryCount: retryCount ?? this.retryCount, + createdAt: createdAt ?? this.createdAt, + lastAttempt: lastAttempt ?? this.lastAttempt, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (tableType.present) { + map['table_type'] = Variable(tableType.value); + } + if (tableLocalId.present) { + map['table_local_id'] = Variable(tableLocalId.value); + } + if (action.present) { + map['action'] = Variable(action.value); + } + if (status.present) { + map['status'] = Variable(status.value); + } + if (payload.present) { + map['payload'] = Variable( + $SyncQueuesTable.$converterpayload.toSql(payload.value), + ); + } + if (batchId.present) { + map['batch_id'] = Variable(batchId.value); + } + if (lastError.present) { + map['last_error'] = Variable(lastError.value); + } + if (retryCount.present) { + map['retry_count'] = Variable(retryCount.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (lastAttempt.present) { + map['last_attempt'] = Variable(lastAttempt.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('SyncQueuesCompanion(') + ..write('id: $id, ') + ..write('tableType: $tableType, ') + ..write('tableLocalId: $tableLocalId, ') + ..write('action: $action, ') + ..write('status: $status, ') + ..write('payload: $payload, ') + ..write('batchId: $batchId, ') + ..write('lastError: $lastError, ') + ..write('retryCount: $retryCount, ') + ..write('createdAt: $createdAt, ') + ..write('lastAttempt: $lastAttempt') + ..write(')')) + .toString(); + } +} + +abstract class _$AppDatabase extends GeneratedDatabase { + _$AppDatabase(QueryExecutor e) : super(e); + $AppDatabaseManager get managers => $AppDatabaseManager(this); + late final $TenantsTable tenants = $TenantsTable(this); + late final $TenantPaymentMethodsTable tenantPaymentMethods = + $TenantPaymentMethodsTable(this); + late final $OutletsTable outlets = $OutletsTable(this); + late final $UsersTable users = $UsersTable(this); + late final $CustomersTable customers = $CustomersTable(this); + late final $CustomerAddressesTable customerAddresses = + $CustomerAddressesTable(this); + late final $CategoriesTable categories = $CategoriesTable(this); + late final $UnitsTable units = $UnitsTable(this); + late final $ProductsTable products = $ProductsTable(this); + late final $ProductVariantsTable productVariants = $ProductVariantsTable( + this, + ); + late final $ProductReviewsTable productReviews = $ProductReviewsTable(this); + late final $ProductLikesTable productLikes = $ProductLikesTable(this); + late final $RawMaterialsTable rawMaterials = $RawMaterialsTable(this); + late final $OutletInventoriesTable outletInventories = + $OutletInventoriesTable(this); + late final $ReceiptItemsTable receiptItems = $ReceiptItemsTable(this); + late final $StockCardsTable stockCards = $StockCardsTable(this); + late final $OrdersTable orders = $OrdersTable(this); + late final $OrderItemsTable orderItems = $OrderItemsTable(this); + late final $SuppliersTable suppliers = $SuppliersTable(this); + late final $PurchasesTable purchases = $PurchasesTable(this); + late final $PurchaseItemsTable purchaseItems = $PurchaseItemsTable(this); + late final $TransactionsTable transactions = $TransactionsTable(this); + late final $CampaignsTable campaigns = $CampaignsTable(this); + late final $OutletCampaignsTable outletCampaigns = $OutletCampaignsTable( + this, + ); + late final $SyncQueuesTable syncQueues = $SyncQueuesTable(this); + late final TenantDao tenantDao = TenantDao(this as AppDatabase); + late final TenantPaymentMethodDao tenantPaymentMethodDao = + TenantPaymentMethodDao(this as AppDatabase); + late final UserDao userDao = UserDao(this as AppDatabase); + late final CustomerDao customerDao = CustomerDao(this as AppDatabase); + late final CustomerAddressDao customerAddressDao = CustomerAddressDao( + this as AppDatabase, + ); + late final OutletDao outletDao = OutletDao(this as AppDatabase); + late final CategoryDao categoryDao = CategoryDao(this as AppDatabase); + late final UnitDao unitDao = UnitDao(this as AppDatabase); + late final ProductDao productDao = ProductDao(this as AppDatabase); + late final ProductVariantDao productVariantDao = ProductVariantDao( + this as AppDatabase, + ); + late final ProductReviewDao productReviewDao = ProductReviewDao( + this as AppDatabase, + ); + late final ProductLikeDao productLikeDao = ProductLikeDao( + this as AppDatabase, + ); + late final RawMaterialDao rawMaterialDao = RawMaterialDao( + this as AppDatabase, + ); + late final OutletInventoryDao outletInventoryDao = OutletInventoryDao( + this as AppDatabase, + ); + late final ReceiptItemDao receiptItemDao = ReceiptItemDao( + this as AppDatabase, + ); + late final StockCardDao stockCardDao = StockCardDao(this as AppDatabase); + late final OrderDao orderDao = OrderDao(this as AppDatabase); + late final OrderItemDao orderItemDao = OrderItemDao(this as AppDatabase); + late final SupplierDao supplierDao = SupplierDao(this as AppDatabase); + late final PurchaseDao purchaseDao = PurchaseDao(this as AppDatabase); + late final PurchaseItemDao purchaseItemDao = PurchaseItemDao( + this as AppDatabase, + ); + late final TransactionDao transactionDao = TransactionDao( + this as AppDatabase, + ); + late final CampaignDao campaignDao = CampaignDao(this as AppDatabase); + late final OutletCampaignDao outletCampaignDao = OutletCampaignDao( + this as AppDatabase, + ); + late final SyncQueueDao syncQueueDao = SyncQueueDao(this as AppDatabase); + @override + Iterable> get allTables => + allSchemaEntities.whereType>(); + @override + List get allSchemaEntities => [ + tenants, + tenantPaymentMethods, + outlets, + users, + customers, + customerAddresses, + categories, + units, + products, + productVariants, + productReviews, + productLikes, + rawMaterials, + outletInventories, + receiptItems, + stockCards, + orders, + orderItems, + suppliers, + purchases, + purchaseItems, + transactions, + campaigns, + outletCampaigns, + syncQueues, + ]; +} + +typedef $$TenantsTableCreateCompanionBuilder = + TenantsCompanion Function({ + Value localId, + Value serverId, + required String businessCode, + required String businessName, + Value businessEmail, + Value businessPhone, + Value businessType, + Value localLogoPath, + Value serverLogoUrl, + Value subdomain, + Value onlinStoreName, + Value onlineOpenTime, + Value onlineCloseTime, + Value isCloseService, + Value preparationTimeMinutes, + Value orderIntervalMinutes, + Value allowCod, + Value maxDeliveryRadiusKm, + Value deliveryFeeType, + Value deliveryFlatFee, + Value deliveryFeePerKm, + Value deliveryBaseFee, + Value reviewDisplayType, + Value reviewDisplayPolicy, + Value subscriptionLevel, + Value limitOutlet, + Value subscriptionExpiresAt, + Value syncStatus, + Value createdAt, + Value updatedAt, + Value rowid, + }); +typedef $$TenantsTableUpdateCompanionBuilder = + TenantsCompanion Function({ + Value localId, + Value serverId, + Value businessCode, + Value businessName, + Value businessEmail, + Value businessPhone, + Value businessType, + Value localLogoPath, + Value serverLogoUrl, + Value subdomain, + Value onlinStoreName, + Value onlineOpenTime, + Value onlineCloseTime, + Value isCloseService, + Value preparationTimeMinutes, + Value orderIntervalMinutes, + Value allowCod, + Value maxDeliveryRadiusKm, + Value deliveryFeeType, + Value deliveryFlatFee, + Value deliveryFeePerKm, + Value deliveryBaseFee, + Value reviewDisplayType, + Value reviewDisplayPolicy, + Value subscriptionLevel, + Value limitOutlet, + Value subscriptionExpiresAt, + Value syncStatus, + Value createdAt, + Value updatedAt, + Value rowid, + }); + +class $$TenantsTableFilterComposer + extends Composer<_$AppDatabase, $TenantsTable> { + $$TenantsTableFilterComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnFilters get localId => $composableBuilder( + column: $table.localId, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get serverId => $composableBuilder( + column: $table.serverId, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get businessCode => $composableBuilder( + column: $table.businessCode, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get businessName => $composableBuilder( + column: $table.businessName, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get businessEmail => $composableBuilder( + column: $table.businessEmail, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get businessPhone => $composableBuilder( + column: $table.businessPhone, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get businessType => $composableBuilder( + column: $table.businessType, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get localLogoPath => $composableBuilder( + column: $table.localLogoPath, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get serverLogoUrl => $composableBuilder( + column: $table.serverLogoUrl, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get subdomain => $composableBuilder( + column: $table.subdomain, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get onlinStoreName => $composableBuilder( + column: $table.onlinStoreName, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get onlineOpenTime => $composableBuilder( + column: $table.onlineOpenTime, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get onlineCloseTime => $composableBuilder( + column: $table.onlineCloseTime, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get isCloseService => $composableBuilder( + column: $table.isCloseService, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get preparationTimeMinutes => $composableBuilder( + column: $table.preparationTimeMinutes, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get orderIntervalMinutes => $composableBuilder( + column: $table.orderIntervalMinutes, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get allowCod => $composableBuilder( + column: $table.allowCod, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get maxDeliveryRadiusKm => $composableBuilder( + column: $table.maxDeliveryRadiusKm, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get deliveryFeeType => $composableBuilder( + column: $table.deliveryFeeType, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get deliveryFlatFee => $composableBuilder( + column: $table.deliveryFlatFee, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get deliveryFeePerKm => $composableBuilder( + column: $table.deliveryFeePerKm, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get deliveryBaseFee => $composableBuilder( + column: $table.deliveryBaseFee, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get reviewDisplayType => $composableBuilder( + column: $table.reviewDisplayType, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get reviewDisplayPolicy => $composableBuilder( + column: $table.reviewDisplayPolicy, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get subscriptionLevel => $composableBuilder( + column: $table.subscriptionLevel, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get limitOutlet => $composableBuilder( + column: $table.limitOutlet, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get subscriptionExpiresAt => $composableBuilder( + column: $table.subscriptionExpiresAt, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get syncStatus => $composableBuilder( + column: $table.syncStatus, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get createdAt => $composableBuilder( + column: $table.createdAt, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get updatedAt => $composableBuilder( + column: $table.updatedAt, + builder: (column) => ColumnFilters(column), + ); +} + +class $$TenantsTableOrderingComposer + extends Composer<_$AppDatabase, $TenantsTable> { + $$TenantsTableOrderingComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnOrderings get localId => $composableBuilder( + column: $table.localId, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get serverId => $composableBuilder( + column: $table.serverId, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get businessCode => $composableBuilder( + column: $table.businessCode, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get businessName => $composableBuilder( + column: $table.businessName, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get businessEmail => $composableBuilder( + column: $table.businessEmail, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get businessPhone => $composableBuilder( + column: $table.businessPhone, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get businessType => $composableBuilder( + column: $table.businessType, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get localLogoPath => $composableBuilder( + column: $table.localLogoPath, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get serverLogoUrl => $composableBuilder( + column: $table.serverLogoUrl, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get subdomain => $composableBuilder( + column: $table.subdomain, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get onlinStoreName => $composableBuilder( + column: $table.onlinStoreName, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get onlineOpenTime => $composableBuilder( + column: $table.onlineOpenTime, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get onlineCloseTime => $composableBuilder( + column: $table.onlineCloseTime, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get isCloseService => $composableBuilder( + column: $table.isCloseService, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get preparationTimeMinutes => $composableBuilder( + column: $table.preparationTimeMinutes, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get orderIntervalMinutes => $composableBuilder( + column: $table.orderIntervalMinutes, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get allowCod => $composableBuilder( + column: $table.allowCod, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get maxDeliveryRadiusKm => $composableBuilder( + column: $table.maxDeliveryRadiusKm, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get deliveryFeeType => $composableBuilder( + column: $table.deliveryFeeType, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get deliveryFlatFee => $composableBuilder( + column: $table.deliveryFlatFee, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get deliveryFeePerKm => $composableBuilder( + column: $table.deliveryFeePerKm, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get deliveryBaseFee => $composableBuilder( + column: $table.deliveryBaseFee, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get reviewDisplayType => $composableBuilder( + column: $table.reviewDisplayType, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get reviewDisplayPolicy => $composableBuilder( + column: $table.reviewDisplayPolicy, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get subscriptionLevel => $composableBuilder( + column: $table.subscriptionLevel, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get limitOutlet => $composableBuilder( + column: $table.limitOutlet, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get subscriptionExpiresAt => $composableBuilder( + column: $table.subscriptionExpiresAt, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get syncStatus => $composableBuilder( + column: $table.syncStatus, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get createdAt => $composableBuilder( + column: $table.createdAt, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get updatedAt => $composableBuilder( + column: $table.updatedAt, + builder: (column) => ColumnOrderings(column), + ); +} + +class $$TenantsTableAnnotationComposer + extends Composer<_$AppDatabase, $TenantsTable> { + $$TenantsTableAnnotationComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + GeneratedColumn get localId => + $composableBuilder(column: $table.localId, builder: (column) => column); + + GeneratedColumn get serverId => + $composableBuilder(column: $table.serverId, builder: (column) => column); + + GeneratedColumn get businessCode => $composableBuilder( + column: $table.businessCode, + builder: (column) => column, + ); + + GeneratedColumn get businessName => $composableBuilder( + column: $table.businessName, + builder: (column) => column, + ); + + GeneratedColumn get businessEmail => $composableBuilder( + column: $table.businessEmail, + builder: (column) => column, + ); + + GeneratedColumn get businessPhone => $composableBuilder( + column: $table.businessPhone, + builder: (column) => column, + ); + + GeneratedColumn get businessType => $composableBuilder( + column: $table.businessType, + builder: (column) => column, + ); + + GeneratedColumn get localLogoPath => $composableBuilder( + column: $table.localLogoPath, + builder: (column) => column, + ); + + GeneratedColumn get serverLogoUrl => $composableBuilder( + column: $table.serverLogoUrl, + builder: (column) => column, + ); + + GeneratedColumn get subdomain => + $composableBuilder(column: $table.subdomain, builder: (column) => column); + + GeneratedColumn get onlinStoreName => $composableBuilder( + column: $table.onlinStoreName, + builder: (column) => column, + ); + + GeneratedColumn get onlineOpenTime => $composableBuilder( + column: $table.onlineOpenTime, + builder: (column) => column, + ); + + GeneratedColumn get onlineCloseTime => $composableBuilder( + column: $table.onlineCloseTime, + builder: (column) => column, + ); + + GeneratedColumn get isCloseService => $composableBuilder( + column: $table.isCloseService, + builder: (column) => column, + ); + + GeneratedColumn get preparationTimeMinutes => $composableBuilder( + column: $table.preparationTimeMinutes, + builder: (column) => column, + ); + + GeneratedColumn get orderIntervalMinutes => $composableBuilder( + column: $table.orderIntervalMinutes, + builder: (column) => column, + ); + + GeneratedColumn get allowCod => + $composableBuilder(column: $table.allowCod, builder: (column) => column); + + GeneratedColumn get maxDeliveryRadiusKm => $composableBuilder( + column: $table.maxDeliveryRadiusKm, + builder: (column) => column, + ); + + GeneratedColumn get deliveryFeeType => $composableBuilder( + column: $table.deliveryFeeType, + builder: (column) => column, + ); + + GeneratedColumn get deliveryFlatFee => $composableBuilder( + column: $table.deliveryFlatFee, + builder: (column) => column, + ); + + GeneratedColumn get deliveryFeePerKm => $composableBuilder( + column: $table.deliveryFeePerKm, + builder: (column) => column, + ); + + GeneratedColumn get deliveryBaseFee => $composableBuilder( + column: $table.deliveryBaseFee, + builder: (column) => column, + ); + + GeneratedColumn get reviewDisplayType => $composableBuilder( + column: $table.reviewDisplayType, + builder: (column) => column, + ); + + GeneratedColumn get reviewDisplayPolicy => $composableBuilder( + column: $table.reviewDisplayPolicy, + builder: (column) => column, + ); + + GeneratedColumn get subscriptionLevel => $composableBuilder( + column: $table.subscriptionLevel, + builder: (column) => column, + ); + + GeneratedColumn get limitOutlet => $composableBuilder( + column: $table.limitOutlet, + builder: (column) => column, + ); + + GeneratedColumn get subscriptionExpiresAt => $composableBuilder( + column: $table.subscriptionExpiresAt, + builder: (column) => column, + ); + + GeneratedColumn get syncStatus => $composableBuilder( + column: $table.syncStatus, + builder: (column) => column, + ); + + GeneratedColumn get createdAt => + $composableBuilder(column: $table.createdAt, builder: (column) => column); + + GeneratedColumn get updatedAt => + $composableBuilder(column: $table.updatedAt, builder: (column) => column); +} + +class $$TenantsTableTableManager + extends + RootTableManager< + _$AppDatabase, + $TenantsTable, + Tenant, + $$TenantsTableFilterComposer, + $$TenantsTableOrderingComposer, + $$TenantsTableAnnotationComposer, + $$TenantsTableCreateCompanionBuilder, + $$TenantsTableUpdateCompanionBuilder, + (Tenant, BaseReferences<_$AppDatabase, $TenantsTable, Tenant>), + Tenant, + PrefetchHooks Function() + > { + $$TenantsTableTableManager(_$AppDatabase db, $TenantsTable table) + : super( + TableManagerState( + db: db, + table: table, + createFilteringComposer: () => + $$TenantsTableFilterComposer($db: db, $table: table), + createOrderingComposer: () => + $$TenantsTableOrderingComposer($db: db, $table: table), + createComputedFieldComposer: () => + $$TenantsTableAnnotationComposer($db: db, $table: table), + updateCompanionCallback: + ({ + Value localId = const Value.absent(), + Value serverId = const Value.absent(), + Value businessCode = const Value.absent(), + Value businessName = const Value.absent(), + Value businessEmail = const Value.absent(), + Value businessPhone = const Value.absent(), + Value businessType = const Value.absent(), + Value localLogoPath = const Value.absent(), + Value serverLogoUrl = const Value.absent(), + Value subdomain = const Value.absent(), + Value onlinStoreName = const Value.absent(), + Value onlineOpenTime = const Value.absent(), + Value onlineCloseTime = const Value.absent(), + Value isCloseService = const Value.absent(), + Value preparationTimeMinutes = const Value.absent(), + Value orderIntervalMinutes = const Value.absent(), + Value allowCod = const Value.absent(), + Value maxDeliveryRadiusKm = const Value.absent(), + Value deliveryFeeType = const Value.absent(), + Value deliveryFlatFee = const Value.absent(), + Value deliveryFeePerKm = const Value.absent(), + Value deliveryBaseFee = const Value.absent(), + Value reviewDisplayType = const Value.absent(), + Value reviewDisplayPolicy = const Value.absent(), + Value subscriptionLevel = const Value.absent(), + Value limitOutlet = const Value.absent(), + Value subscriptionExpiresAt = const Value.absent(), + Value syncStatus = const Value.absent(), + Value createdAt = const Value.absent(), + Value updatedAt = const Value.absent(), + Value rowid = const Value.absent(), + }) => TenantsCompanion( + localId: localId, + serverId: serverId, + businessCode: businessCode, + businessName: businessName, + businessEmail: businessEmail, + businessPhone: businessPhone, + businessType: businessType, + localLogoPath: localLogoPath, + serverLogoUrl: serverLogoUrl, + subdomain: subdomain, + onlinStoreName: onlinStoreName, + onlineOpenTime: onlineOpenTime, + onlineCloseTime: onlineCloseTime, + isCloseService: isCloseService, + preparationTimeMinutes: preparationTimeMinutes, + orderIntervalMinutes: orderIntervalMinutes, + allowCod: allowCod, + maxDeliveryRadiusKm: maxDeliveryRadiusKm, + deliveryFeeType: deliveryFeeType, + deliveryFlatFee: deliveryFlatFee, + deliveryFeePerKm: deliveryFeePerKm, + deliveryBaseFee: deliveryBaseFee, + reviewDisplayType: reviewDisplayType, + reviewDisplayPolicy: reviewDisplayPolicy, + subscriptionLevel: subscriptionLevel, + limitOutlet: limitOutlet, + subscriptionExpiresAt: subscriptionExpiresAt, + syncStatus: syncStatus, + createdAt: createdAt, + updatedAt: updatedAt, + rowid: rowid, + ), + createCompanionCallback: + ({ + Value localId = const Value.absent(), + Value serverId = const Value.absent(), + required String businessCode, + required String businessName, + Value businessEmail = const Value.absent(), + Value businessPhone = const Value.absent(), + Value businessType = const Value.absent(), + Value localLogoPath = const Value.absent(), + Value serverLogoUrl = const Value.absent(), + Value subdomain = const Value.absent(), + Value onlinStoreName = const Value.absent(), + Value onlineOpenTime = const Value.absent(), + Value onlineCloseTime = const Value.absent(), + Value isCloseService = const Value.absent(), + Value preparationTimeMinutes = const Value.absent(), + Value orderIntervalMinutes = const Value.absent(), + Value allowCod = const Value.absent(), + Value maxDeliveryRadiusKm = const Value.absent(), + Value deliveryFeeType = const Value.absent(), + Value deliveryFlatFee = const Value.absent(), + Value deliveryFeePerKm = const Value.absent(), + Value deliveryBaseFee = const Value.absent(), + Value reviewDisplayType = const Value.absent(), + Value reviewDisplayPolicy = const Value.absent(), + Value subscriptionLevel = const Value.absent(), + Value limitOutlet = const Value.absent(), + Value subscriptionExpiresAt = const Value.absent(), + Value syncStatus = const Value.absent(), + Value createdAt = const Value.absent(), + Value updatedAt = const Value.absent(), + Value rowid = const Value.absent(), + }) => TenantsCompanion.insert( + localId: localId, + serverId: serverId, + businessCode: businessCode, + businessName: businessName, + businessEmail: businessEmail, + businessPhone: businessPhone, + businessType: businessType, + localLogoPath: localLogoPath, + serverLogoUrl: serverLogoUrl, + subdomain: subdomain, + onlinStoreName: onlinStoreName, + onlineOpenTime: onlineOpenTime, + onlineCloseTime: onlineCloseTime, + isCloseService: isCloseService, + preparationTimeMinutes: preparationTimeMinutes, + orderIntervalMinutes: orderIntervalMinutes, + allowCod: allowCod, + maxDeliveryRadiusKm: maxDeliveryRadiusKm, + deliveryFeeType: deliveryFeeType, + deliveryFlatFee: deliveryFlatFee, + deliveryFeePerKm: deliveryFeePerKm, + deliveryBaseFee: deliveryBaseFee, + reviewDisplayType: reviewDisplayType, + reviewDisplayPolicy: reviewDisplayPolicy, + subscriptionLevel: subscriptionLevel, + limitOutlet: limitOutlet, + subscriptionExpiresAt: subscriptionExpiresAt, + syncStatus: syncStatus, + createdAt: createdAt, + updatedAt: updatedAt, + rowid: rowid, + ), + withReferenceMapper: (p0) => p0 + .map((e) => (e.readTable(table), BaseReferences(db, table, e))) + .toList(), + prefetchHooksCallback: null, + ), + ); +} + +typedef $$TenantsTableProcessedTableManager = + ProcessedTableManager< + _$AppDatabase, + $TenantsTable, + Tenant, + $$TenantsTableFilterComposer, + $$TenantsTableOrderingComposer, + $$TenantsTableAnnotationComposer, + $$TenantsTableCreateCompanionBuilder, + $$TenantsTableUpdateCompanionBuilder, + (Tenant, BaseReferences<_$AppDatabase, $TenantsTable, Tenant>), + Tenant, + PrefetchHooks Function() + >; +typedef $$TenantPaymentMethodsTableCreateCompanionBuilder = + TenantPaymentMethodsCompanion Function({ + Value localId, + Value serverId, + Value sourceName, + Value accountNumber, + Value receipentName, + Value syncStatus, + Value createdAt, + Value updatedAt, + Value rowid, + }); +typedef $$TenantPaymentMethodsTableUpdateCompanionBuilder = + TenantPaymentMethodsCompanion Function({ + Value localId, + Value serverId, + Value sourceName, + Value accountNumber, + Value receipentName, + Value syncStatus, + Value createdAt, + Value updatedAt, + Value rowid, + }); + +class $$TenantPaymentMethodsTableFilterComposer + extends Composer<_$AppDatabase, $TenantPaymentMethodsTable> { + $$TenantPaymentMethodsTableFilterComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnFilters get localId => $composableBuilder( + column: $table.localId, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get serverId => $composableBuilder( + column: $table.serverId, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get sourceName => $composableBuilder( + column: $table.sourceName, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get accountNumber => $composableBuilder( + column: $table.accountNumber, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get receipentName => $composableBuilder( + column: $table.receipentName, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get syncStatus => $composableBuilder( + column: $table.syncStatus, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get createdAt => $composableBuilder( + column: $table.createdAt, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get updatedAt => $composableBuilder( + column: $table.updatedAt, + builder: (column) => ColumnFilters(column), + ); +} + +class $$TenantPaymentMethodsTableOrderingComposer + extends Composer<_$AppDatabase, $TenantPaymentMethodsTable> { + $$TenantPaymentMethodsTableOrderingComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnOrderings get localId => $composableBuilder( + column: $table.localId, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get serverId => $composableBuilder( + column: $table.serverId, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get sourceName => $composableBuilder( + column: $table.sourceName, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get accountNumber => $composableBuilder( + column: $table.accountNumber, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get receipentName => $composableBuilder( + column: $table.receipentName, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get syncStatus => $composableBuilder( + column: $table.syncStatus, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get createdAt => $composableBuilder( + column: $table.createdAt, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get updatedAt => $composableBuilder( + column: $table.updatedAt, + builder: (column) => ColumnOrderings(column), + ); +} + +class $$TenantPaymentMethodsTableAnnotationComposer + extends Composer<_$AppDatabase, $TenantPaymentMethodsTable> { + $$TenantPaymentMethodsTableAnnotationComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + GeneratedColumn get localId => + $composableBuilder(column: $table.localId, builder: (column) => column); + + GeneratedColumn get serverId => + $composableBuilder(column: $table.serverId, builder: (column) => column); + + GeneratedColumn get sourceName => $composableBuilder( + column: $table.sourceName, + builder: (column) => column, + ); + + GeneratedColumn get accountNumber => $composableBuilder( + column: $table.accountNumber, + builder: (column) => column, + ); + + GeneratedColumn get receipentName => $composableBuilder( + column: $table.receipentName, + builder: (column) => column, + ); + + GeneratedColumn get syncStatus => $composableBuilder( + column: $table.syncStatus, + builder: (column) => column, + ); + + GeneratedColumn get createdAt => + $composableBuilder(column: $table.createdAt, builder: (column) => column); + + GeneratedColumn get updatedAt => + $composableBuilder(column: $table.updatedAt, builder: (column) => column); +} + +class $$TenantPaymentMethodsTableTableManager + extends + RootTableManager< + _$AppDatabase, + $TenantPaymentMethodsTable, + TenantPaymentMethod, + $$TenantPaymentMethodsTableFilterComposer, + $$TenantPaymentMethodsTableOrderingComposer, + $$TenantPaymentMethodsTableAnnotationComposer, + $$TenantPaymentMethodsTableCreateCompanionBuilder, + $$TenantPaymentMethodsTableUpdateCompanionBuilder, + ( + TenantPaymentMethod, + BaseReferences< + _$AppDatabase, + $TenantPaymentMethodsTable, + TenantPaymentMethod + >, + ), + TenantPaymentMethod, + PrefetchHooks Function() + > { + $$TenantPaymentMethodsTableTableManager( + _$AppDatabase db, + $TenantPaymentMethodsTable table, + ) : super( + TableManagerState( + db: db, + table: table, + createFilteringComposer: () => + $$TenantPaymentMethodsTableFilterComposer($db: db, $table: table), + createOrderingComposer: () => + $$TenantPaymentMethodsTableOrderingComposer( + $db: db, + $table: table, + ), + createComputedFieldComposer: () => + $$TenantPaymentMethodsTableAnnotationComposer( + $db: db, + $table: table, + ), + updateCompanionCallback: + ({ + Value localId = const Value.absent(), + Value serverId = const Value.absent(), + Value sourceName = const Value.absent(), + Value accountNumber = const Value.absent(), + Value receipentName = const Value.absent(), + Value syncStatus = const Value.absent(), + Value createdAt = const Value.absent(), + Value updatedAt = const Value.absent(), + Value rowid = const Value.absent(), + }) => TenantPaymentMethodsCompanion( + localId: localId, + serverId: serverId, + sourceName: sourceName, + accountNumber: accountNumber, + receipentName: receipentName, + syncStatus: syncStatus, + createdAt: createdAt, + updatedAt: updatedAt, + rowid: rowid, + ), + createCompanionCallback: + ({ + Value localId = const Value.absent(), + Value serverId = const Value.absent(), + Value sourceName = const Value.absent(), + Value accountNumber = const Value.absent(), + Value receipentName = const Value.absent(), + Value syncStatus = const Value.absent(), + Value createdAt = const Value.absent(), + Value updatedAt = const Value.absent(), + Value rowid = const Value.absent(), + }) => TenantPaymentMethodsCompanion.insert( + localId: localId, + serverId: serverId, + sourceName: sourceName, + accountNumber: accountNumber, + receipentName: receipentName, + syncStatus: syncStatus, + createdAt: createdAt, + updatedAt: updatedAt, + rowid: rowid, + ), + withReferenceMapper: (p0) => p0 + .map((e) => (e.readTable(table), BaseReferences(db, table, e))) + .toList(), + prefetchHooksCallback: null, + ), + ); +} + +typedef $$TenantPaymentMethodsTableProcessedTableManager = + ProcessedTableManager< + _$AppDatabase, + $TenantPaymentMethodsTable, + TenantPaymentMethod, + $$TenantPaymentMethodsTableFilterComposer, + $$TenantPaymentMethodsTableOrderingComposer, + $$TenantPaymentMethodsTableAnnotationComposer, + $$TenantPaymentMethodsTableCreateCompanionBuilder, + $$TenantPaymentMethodsTableUpdateCompanionBuilder, + ( + TenantPaymentMethod, + BaseReferences< + _$AppDatabase, + $TenantPaymentMethodsTable, + TenantPaymentMethod + >, + ), + TenantPaymentMethod, + PrefetchHooks Function() + >; +typedef $$OutletsTableCreateCompanionBuilder = + OutletsCompanion Function({ + Value localId, + Value serverId, + required String name, + Value email, + Value phoneNumber, + Value fullAddress, + Value latitude, + Value longitude, + Value localPhotoPath, + Value localBannerPath, + Value serverPhotoUrl, + Value serverBannerUrl, + Value isMainOutlet, + Value isActive, + Value syncStatus, + Value createdAt, + Value updatedAt, + Value deletedAt, + Value rowid, + }); +typedef $$OutletsTableUpdateCompanionBuilder = + OutletsCompanion Function({ + Value localId, + Value serverId, + Value name, + Value email, + Value phoneNumber, + Value fullAddress, + Value latitude, + Value longitude, + Value localPhotoPath, + Value localBannerPath, + Value serverPhotoUrl, + Value serverBannerUrl, + Value isMainOutlet, + Value isActive, + Value syncStatus, + Value createdAt, + Value updatedAt, + Value deletedAt, + Value rowid, + }); + +final class $$OutletsTableReferences + extends BaseReferences<_$AppDatabase, $OutletsTable, Outlet> { + $$OutletsTableReferences(super.$_db, super.$_table, super.$_typedResult); + + static MultiTypedResultKey<$UsersTable, List> _usersRefsTable( + _$AppDatabase db, + ) => MultiTypedResultKey.fromTable( + db.users, + aliasName: $_aliasNameGenerator(db.outlets.localId, db.users.outletId), + ); + + $$UsersTableProcessedTableManager get usersRefs { + final manager = $$UsersTableTableManager($_db, $_db.users).filter( + (f) => f.outletId.localId.sqlEquals($_itemColumn('local_id')!), + ); + + final cache = $_typedResult.readTableOrNull(_usersRefsTable($_db)); + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: cache), + ); + } + + static MultiTypedResultKey<$CustomersTable, List> + _customersRefsTable(_$AppDatabase db) => MultiTypedResultKey.fromTable( + db.customers, + aliasName: $_aliasNameGenerator(db.outlets.localId, db.customers.outletId), + ); + + $$CustomersTableProcessedTableManager get customersRefs { + final manager = $$CustomersTableTableManager($_db, $_db.customers).filter( + (f) => f.outletId.localId.sqlEquals($_itemColumn('local_id')!), + ); + + final cache = $_typedResult.readTableOrNull(_customersRefsTable($_db)); + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: cache), + ); + } + + static MultiTypedResultKey<$OutletInventoriesTable, List> + _outletInventoriesRefsTable(_$AppDatabase db) => + MultiTypedResultKey.fromTable( + db.outletInventories, + aliasName: $_aliasNameGenerator( + db.outlets.localId, + db.outletInventories.outletId, + ), + ); + + $$OutletInventoriesTableProcessedTableManager get outletInventoriesRefs { + final manager = + $$OutletInventoriesTableTableManager( + $_db, + $_db.outletInventories, + ).filter( + (f) => + f.outletId.localId.sqlEquals($_itemColumn('local_id')!), + ); + + final cache = $_typedResult.readTableOrNull( + _outletInventoriesRefsTable($_db), + ); + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: cache), + ); + } + + static MultiTypedResultKey<$StockCardsTable, List> + _stockCardsRefsTable(_$AppDatabase db) => MultiTypedResultKey.fromTable( + db.stockCards, + aliasName: $_aliasNameGenerator( + db.outlets.localId, + db.stockCards.receiveFromId, + ), + ); + + $$StockCardsTableProcessedTableManager get stockCardsRefs { + final manager = $$StockCardsTableTableManager($_db, $_db.stockCards).filter( + (f) => + f.receiveFromId.localId.sqlEquals($_itemColumn('local_id')!), + ); + + final cache = $_typedResult.readTableOrNull(_stockCardsRefsTable($_db)); + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: cache), + ); + } + + static MultiTypedResultKey<$OrdersTable, List> _ordersRefsTable( + _$AppDatabase db, + ) => MultiTypedResultKey.fromTable( + db.orders, + aliasName: $_aliasNameGenerator(db.outlets.localId, db.orders.outletId), + ); + + $$OrdersTableProcessedTableManager get ordersRefs { + final manager = $$OrdersTableTableManager($_db, $_db.orders).filter( + (f) => f.outletId.localId.sqlEquals($_itemColumn('local_id')!), + ); + + final cache = $_typedResult.readTableOrNull(_ordersRefsTable($_db)); + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: cache), + ); + } + + static MultiTypedResultKey<$PurchasesTable, List> + _purchasesRefsTable(_$AppDatabase db) => MultiTypedResultKey.fromTable( + db.purchases, + aliasName: $_aliasNameGenerator(db.outlets.localId, db.purchases.outletId), + ); + + $$PurchasesTableProcessedTableManager get purchasesRefs { + final manager = $$PurchasesTableTableManager($_db, $_db.purchases).filter( + (f) => f.outletId.localId.sqlEquals($_itemColumn('local_id')!), + ); + + final cache = $_typedResult.readTableOrNull(_purchasesRefsTable($_db)); + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: cache), + ); + } + + static MultiTypedResultKey<$TransactionsTable, List> + _transactionsRefsTable(_$AppDatabase db) => MultiTypedResultKey.fromTable( + db.transactions, + aliasName: $_aliasNameGenerator( + db.outlets.localId, + db.transactions.outletId, + ), + ); + + $$TransactionsTableProcessedTableManager get transactionsRefs { + final manager = $$TransactionsTableTableManager($_db, $_db.transactions) + .filter( + (f) => + f.outletId.localId.sqlEquals($_itemColumn('local_id')!), + ); + + final cache = $_typedResult.readTableOrNull(_transactionsRefsTable($_db)); + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: cache), + ); + } + + static MultiTypedResultKey<$OutletCampaignsTable, List> + _outletCampaignsRefsTable(_$AppDatabase db) => MultiTypedResultKey.fromTable( + db.outletCampaigns, + aliasName: $_aliasNameGenerator( + db.outlets.localId, + db.outletCampaigns.outletId, + ), + ); + + $$OutletCampaignsTableProcessedTableManager get outletCampaignsRefs { + final manager = + $$OutletCampaignsTableTableManager($_db, $_db.outletCampaigns).filter( + (f) => + f.outletId.localId.sqlEquals($_itemColumn('local_id')!), + ); + + final cache = $_typedResult.readTableOrNull( + _outletCampaignsRefsTable($_db), + ); + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: cache), + ); + } +} + +class $$OutletsTableFilterComposer + extends Composer<_$AppDatabase, $OutletsTable> { + $$OutletsTableFilterComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnFilters get localId => $composableBuilder( + column: $table.localId, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get serverId => $composableBuilder( + column: $table.serverId, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get name => $composableBuilder( + column: $table.name, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get email => $composableBuilder( + column: $table.email, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get phoneNumber => $composableBuilder( + column: $table.phoneNumber, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get fullAddress => $composableBuilder( + column: $table.fullAddress, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get latitude => $composableBuilder( + column: $table.latitude, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get longitude => $composableBuilder( + column: $table.longitude, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get localPhotoPath => $composableBuilder( + column: $table.localPhotoPath, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get localBannerPath => $composableBuilder( + column: $table.localBannerPath, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get serverPhotoUrl => $composableBuilder( + column: $table.serverPhotoUrl, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get serverBannerUrl => $composableBuilder( + column: $table.serverBannerUrl, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get isMainOutlet => $composableBuilder( + column: $table.isMainOutlet, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get isActive => $composableBuilder( + column: $table.isActive, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get syncStatus => $composableBuilder( + column: $table.syncStatus, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get createdAt => $composableBuilder( + column: $table.createdAt, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get updatedAt => $composableBuilder( + column: $table.updatedAt, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get deletedAt => $composableBuilder( + column: $table.deletedAt, + builder: (column) => ColumnFilters(column), + ); + + Expression usersRefs( + Expression Function($$UsersTableFilterComposer f) f, + ) { + final $$UsersTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.localId, + referencedTable: $db.users, + getReferencedColumn: (t) => t.outletId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$UsersTableFilterComposer( + $db: $db, + $table: $db.users, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return f(composer); + } + + Expression customersRefs( + Expression Function($$CustomersTableFilterComposer f) f, + ) { + final $$CustomersTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.localId, + referencedTable: $db.customers, + getReferencedColumn: (t) => t.outletId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$CustomersTableFilterComposer( + $db: $db, + $table: $db.customers, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return f(composer); + } + + Expression outletInventoriesRefs( + Expression Function($$OutletInventoriesTableFilterComposer f) f, + ) { + final $$OutletInventoriesTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.localId, + referencedTable: $db.outletInventories, + getReferencedColumn: (t) => t.outletId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$OutletInventoriesTableFilterComposer( + $db: $db, + $table: $db.outletInventories, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return f(composer); + } + + Expression stockCardsRefs( + Expression Function($$StockCardsTableFilterComposer f) f, + ) { + final $$StockCardsTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.localId, + referencedTable: $db.stockCards, + getReferencedColumn: (t) => t.receiveFromId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$StockCardsTableFilterComposer( + $db: $db, + $table: $db.stockCards, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return f(composer); + } + + Expression ordersRefs( + Expression Function($$OrdersTableFilterComposer f) f, + ) { + final $$OrdersTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.localId, + referencedTable: $db.orders, + getReferencedColumn: (t) => t.outletId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$OrdersTableFilterComposer( + $db: $db, + $table: $db.orders, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return f(composer); + } + + Expression purchasesRefs( + Expression Function($$PurchasesTableFilterComposer f) f, + ) { + final $$PurchasesTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.localId, + referencedTable: $db.purchases, + getReferencedColumn: (t) => t.outletId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$PurchasesTableFilterComposer( + $db: $db, + $table: $db.purchases, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return f(composer); + } + + Expression transactionsRefs( + Expression Function($$TransactionsTableFilterComposer f) f, + ) { + final $$TransactionsTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.localId, + referencedTable: $db.transactions, + getReferencedColumn: (t) => t.outletId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$TransactionsTableFilterComposer( + $db: $db, + $table: $db.transactions, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return f(composer); + } + + Expression outletCampaignsRefs( + Expression Function($$OutletCampaignsTableFilterComposer f) f, + ) { + final $$OutletCampaignsTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.localId, + referencedTable: $db.outletCampaigns, + getReferencedColumn: (t) => t.outletId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$OutletCampaignsTableFilterComposer( + $db: $db, + $table: $db.outletCampaigns, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return f(composer); + } +} + +class $$OutletsTableOrderingComposer + extends Composer<_$AppDatabase, $OutletsTable> { + $$OutletsTableOrderingComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnOrderings get localId => $composableBuilder( + column: $table.localId, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get serverId => $composableBuilder( + column: $table.serverId, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get name => $composableBuilder( + column: $table.name, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get email => $composableBuilder( + column: $table.email, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get phoneNumber => $composableBuilder( + column: $table.phoneNumber, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get fullAddress => $composableBuilder( + column: $table.fullAddress, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get latitude => $composableBuilder( + column: $table.latitude, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get longitude => $composableBuilder( + column: $table.longitude, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get localPhotoPath => $composableBuilder( + column: $table.localPhotoPath, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get localBannerPath => $composableBuilder( + column: $table.localBannerPath, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get serverPhotoUrl => $composableBuilder( + column: $table.serverPhotoUrl, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get serverBannerUrl => $composableBuilder( + column: $table.serverBannerUrl, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get isMainOutlet => $composableBuilder( + column: $table.isMainOutlet, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get isActive => $composableBuilder( + column: $table.isActive, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get syncStatus => $composableBuilder( + column: $table.syncStatus, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get createdAt => $composableBuilder( + column: $table.createdAt, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get updatedAt => $composableBuilder( + column: $table.updatedAt, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get deletedAt => $composableBuilder( + column: $table.deletedAt, + builder: (column) => ColumnOrderings(column), + ); +} + +class $$OutletsTableAnnotationComposer + extends Composer<_$AppDatabase, $OutletsTable> { + $$OutletsTableAnnotationComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + GeneratedColumn get localId => + $composableBuilder(column: $table.localId, builder: (column) => column); + + GeneratedColumn get serverId => + $composableBuilder(column: $table.serverId, builder: (column) => column); + + GeneratedColumn get name => + $composableBuilder(column: $table.name, builder: (column) => column); + + GeneratedColumn get email => + $composableBuilder(column: $table.email, builder: (column) => column); + + GeneratedColumn get phoneNumber => $composableBuilder( + column: $table.phoneNumber, + builder: (column) => column, + ); + + GeneratedColumn get fullAddress => $composableBuilder( + column: $table.fullAddress, + builder: (column) => column, + ); + + GeneratedColumn get latitude => + $composableBuilder(column: $table.latitude, builder: (column) => column); + + GeneratedColumn get longitude => + $composableBuilder(column: $table.longitude, builder: (column) => column); + + GeneratedColumn get localPhotoPath => $composableBuilder( + column: $table.localPhotoPath, + builder: (column) => column, + ); + + GeneratedColumn get localBannerPath => $composableBuilder( + column: $table.localBannerPath, + builder: (column) => column, + ); + + GeneratedColumn get serverPhotoUrl => $composableBuilder( + column: $table.serverPhotoUrl, + builder: (column) => column, + ); + + GeneratedColumn get serverBannerUrl => $composableBuilder( + column: $table.serverBannerUrl, + builder: (column) => column, + ); + + GeneratedColumn get isMainOutlet => $composableBuilder( + column: $table.isMainOutlet, + builder: (column) => column, + ); + + GeneratedColumn get isActive => + $composableBuilder(column: $table.isActive, builder: (column) => column); + + GeneratedColumn get syncStatus => $composableBuilder( + column: $table.syncStatus, + builder: (column) => column, + ); + + GeneratedColumn get createdAt => + $composableBuilder(column: $table.createdAt, builder: (column) => column); + + GeneratedColumn get updatedAt => + $composableBuilder(column: $table.updatedAt, builder: (column) => column); + + GeneratedColumn get deletedAt => + $composableBuilder(column: $table.deletedAt, builder: (column) => column); + + Expression usersRefs( + Expression Function($$UsersTableAnnotationComposer a) f, + ) { + final $$UsersTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.localId, + referencedTable: $db.users, + getReferencedColumn: (t) => t.outletId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$UsersTableAnnotationComposer( + $db: $db, + $table: $db.users, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return f(composer); + } + + Expression customersRefs( + Expression Function($$CustomersTableAnnotationComposer a) f, + ) { + final $$CustomersTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.localId, + referencedTable: $db.customers, + getReferencedColumn: (t) => t.outletId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$CustomersTableAnnotationComposer( + $db: $db, + $table: $db.customers, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return f(composer); + } + + Expression outletInventoriesRefs( + Expression Function($$OutletInventoriesTableAnnotationComposer a) f, + ) { + final $$OutletInventoriesTableAnnotationComposer composer = + $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.localId, + referencedTable: $db.outletInventories, + getReferencedColumn: (t) => t.outletId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$OutletInventoriesTableAnnotationComposer( + $db: $db, + $table: $db.outletInventories, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return f(composer); + } + + Expression stockCardsRefs( + Expression Function($$StockCardsTableAnnotationComposer a) f, + ) { + final $$StockCardsTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.localId, + referencedTable: $db.stockCards, + getReferencedColumn: (t) => t.receiveFromId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$StockCardsTableAnnotationComposer( + $db: $db, + $table: $db.stockCards, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return f(composer); + } + + Expression ordersRefs( + Expression Function($$OrdersTableAnnotationComposer a) f, + ) { + final $$OrdersTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.localId, + referencedTable: $db.orders, + getReferencedColumn: (t) => t.outletId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$OrdersTableAnnotationComposer( + $db: $db, + $table: $db.orders, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return f(composer); + } + + Expression purchasesRefs( + Expression Function($$PurchasesTableAnnotationComposer a) f, + ) { + final $$PurchasesTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.localId, + referencedTable: $db.purchases, + getReferencedColumn: (t) => t.outletId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$PurchasesTableAnnotationComposer( + $db: $db, + $table: $db.purchases, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return f(composer); + } + + Expression transactionsRefs( + Expression Function($$TransactionsTableAnnotationComposer a) f, + ) { + final $$TransactionsTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.localId, + referencedTable: $db.transactions, + getReferencedColumn: (t) => t.outletId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$TransactionsTableAnnotationComposer( + $db: $db, + $table: $db.transactions, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return f(composer); + } + + Expression outletCampaignsRefs( + Expression Function($$OutletCampaignsTableAnnotationComposer a) f, + ) { + final $$OutletCampaignsTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.localId, + referencedTable: $db.outletCampaigns, + getReferencedColumn: (t) => t.outletId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$OutletCampaignsTableAnnotationComposer( + $db: $db, + $table: $db.outletCampaigns, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return f(composer); + } +} + +class $$OutletsTableTableManager + extends + RootTableManager< + _$AppDatabase, + $OutletsTable, + Outlet, + $$OutletsTableFilterComposer, + $$OutletsTableOrderingComposer, + $$OutletsTableAnnotationComposer, + $$OutletsTableCreateCompanionBuilder, + $$OutletsTableUpdateCompanionBuilder, + (Outlet, $$OutletsTableReferences), + Outlet, + PrefetchHooks Function({ + bool usersRefs, + bool customersRefs, + bool outletInventoriesRefs, + bool stockCardsRefs, + bool ordersRefs, + bool purchasesRefs, + bool transactionsRefs, + bool outletCampaignsRefs, + }) + > { + $$OutletsTableTableManager(_$AppDatabase db, $OutletsTable table) + : super( + TableManagerState( + db: db, + table: table, + createFilteringComposer: () => + $$OutletsTableFilterComposer($db: db, $table: table), + createOrderingComposer: () => + $$OutletsTableOrderingComposer($db: db, $table: table), + createComputedFieldComposer: () => + $$OutletsTableAnnotationComposer($db: db, $table: table), + updateCompanionCallback: + ({ + Value localId = const Value.absent(), + Value serverId = const Value.absent(), + Value name = const Value.absent(), + Value email = const Value.absent(), + Value phoneNumber = const Value.absent(), + Value fullAddress = const Value.absent(), + Value latitude = const Value.absent(), + Value longitude = const Value.absent(), + Value localPhotoPath = const Value.absent(), + Value localBannerPath = const Value.absent(), + Value serverPhotoUrl = const Value.absent(), + Value serverBannerUrl = const Value.absent(), + Value isMainOutlet = const Value.absent(), + Value isActive = const Value.absent(), + Value syncStatus = const Value.absent(), + Value createdAt = const Value.absent(), + Value updatedAt = const Value.absent(), + Value deletedAt = const Value.absent(), + Value rowid = const Value.absent(), + }) => OutletsCompanion( + localId: localId, + serverId: serverId, + name: name, + email: email, + phoneNumber: phoneNumber, + fullAddress: fullAddress, + latitude: latitude, + longitude: longitude, + localPhotoPath: localPhotoPath, + localBannerPath: localBannerPath, + serverPhotoUrl: serverPhotoUrl, + serverBannerUrl: serverBannerUrl, + isMainOutlet: isMainOutlet, + isActive: isActive, + syncStatus: syncStatus, + createdAt: createdAt, + updatedAt: updatedAt, + deletedAt: deletedAt, + rowid: rowid, + ), + createCompanionCallback: + ({ + Value localId = const Value.absent(), + Value serverId = const Value.absent(), + required String name, + Value email = const Value.absent(), + Value phoneNumber = const Value.absent(), + Value fullAddress = const Value.absent(), + Value latitude = const Value.absent(), + Value longitude = const Value.absent(), + Value localPhotoPath = const Value.absent(), + Value localBannerPath = const Value.absent(), + Value serverPhotoUrl = const Value.absent(), + Value serverBannerUrl = const Value.absent(), + Value isMainOutlet = const Value.absent(), + Value isActive = const Value.absent(), + Value syncStatus = const Value.absent(), + Value createdAt = const Value.absent(), + Value updatedAt = const Value.absent(), + Value deletedAt = const Value.absent(), + Value rowid = const Value.absent(), + }) => OutletsCompanion.insert( + localId: localId, + serverId: serverId, + name: name, + email: email, + phoneNumber: phoneNumber, + fullAddress: fullAddress, + latitude: latitude, + longitude: longitude, + localPhotoPath: localPhotoPath, + localBannerPath: localBannerPath, + serverPhotoUrl: serverPhotoUrl, + serverBannerUrl: serverBannerUrl, + isMainOutlet: isMainOutlet, + isActive: isActive, + syncStatus: syncStatus, + createdAt: createdAt, + updatedAt: updatedAt, + deletedAt: deletedAt, + rowid: rowid, + ), + withReferenceMapper: (p0) => p0 + .map( + (e) => ( + e.readTable(table), + $$OutletsTableReferences(db, table, e), + ), + ) + .toList(), + prefetchHooksCallback: + ({ + usersRefs = false, + customersRefs = false, + outletInventoriesRefs = false, + stockCardsRefs = false, + ordersRefs = false, + purchasesRefs = false, + transactionsRefs = false, + outletCampaignsRefs = false, + }) { + return PrefetchHooks( + db: db, + explicitlyWatchedTables: [ + if (usersRefs) db.users, + if (customersRefs) db.customers, + if (outletInventoriesRefs) db.outletInventories, + if (stockCardsRefs) db.stockCards, + if (ordersRefs) db.orders, + if (purchasesRefs) db.purchases, + if (transactionsRefs) db.transactions, + if (outletCampaignsRefs) db.outletCampaigns, + ], + addJoins: null, + getPrefetchedDataCallback: (items) async { + return [ + if (usersRefs) + await $_getPrefetchedData( + currentTable: table, + referencedTable: $$OutletsTableReferences + ._usersRefsTable(db), + managerFromTypedResult: (p0) => + $$OutletsTableReferences(db, table, p0).usersRefs, + referencedItemsForCurrentItem: + (item, referencedItems) => referencedItems.where( + (e) => e.outletId == item.localId, + ), + typedResults: items, + ), + if (customersRefs) + await $_getPrefetchedData< + Outlet, + $OutletsTable, + Customer + >( + currentTable: table, + referencedTable: $$OutletsTableReferences + ._customersRefsTable(db), + managerFromTypedResult: (p0) => + $$OutletsTableReferences( + db, + table, + p0, + ).customersRefs, + referencedItemsForCurrentItem: + (item, referencedItems) => referencedItems.where( + (e) => e.outletId == item.localId, + ), + typedResults: items, + ), + if (outletInventoriesRefs) + await $_getPrefetchedData< + Outlet, + $OutletsTable, + OutletInventory + >( + currentTable: table, + referencedTable: $$OutletsTableReferences + ._outletInventoriesRefsTable(db), + managerFromTypedResult: (p0) => + $$OutletsTableReferences( + db, + table, + p0, + ).outletInventoriesRefs, + referencedItemsForCurrentItem: + (item, referencedItems) => referencedItems.where( + (e) => e.outletId == item.localId, + ), + typedResults: items, + ), + if (stockCardsRefs) + await $_getPrefetchedData< + Outlet, + $OutletsTable, + StockCard + >( + currentTable: table, + referencedTable: $$OutletsTableReferences + ._stockCardsRefsTable(db), + managerFromTypedResult: (p0) => + $$OutletsTableReferences( + db, + table, + p0, + ).stockCardsRefs, + referencedItemsForCurrentItem: + (item, referencedItems) => referencedItems.where( + (e) => e.receiveFromId == item.localId, + ), + typedResults: items, + ), + if (ordersRefs) + await $_getPrefetchedData( + currentTable: table, + referencedTable: $$OutletsTableReferences + ._ordersRefsTable(db), + managerFromTypedResult: (p0) => + $$OutletsTableReferences( + db, + table, + p0, + ).ordersRefs, + referencedItemsForCurrentItem: + (item, referencedItems) => referencedItems.where( + (e) => e.outletId == item.localId, + ), + typedResults: items, + ), + if (purchasesRefs) + await $_getPrefetchedData< + Outlet, + $OutletsTable, + Purchase + >( + currentTable: table, + referencedTable: $$OutletsTableReferences + ._purchasesRefsTable(db), + managerFromTypedResult: (p0) => + $$OutletsTableReferences( + db, + table, + p0, + ).purchasesRefs, + referencedItemsForCurrentItem: + (item, referencedItems) => referencedItems.where( + (e) => e.outletId == item.localId, + ), + typedResults: items, + ), + if (transactionsRefs) + await $_getPrefetchedData< + Outlet, + $OutletsTable, + Transaction + >( + currentTable: table, + referencedTable: $$OutletsTableReferences + ._transactionsRefsTable(db), + managerFromTypedResult: (p0) => + $$OutletsTableReferences( + db, + table, + p0, + ).transactionsRefs, + referencedItemsForCurrentItem: + (item, referencedItems) => referencedItems.where( + (e) => e.outletId == item.localId, + ), + typedResults: items, + ), + if (outletCampaignsRefs) + await $_getPrefetchedData< + Outlet, + $OutletsTable, + OutletCampaign + >( + currentTable: table, + referencedTable: $$OutletsTableReferences + ._outletCampaignsRefsTable(db), + managerFromTypedResult: (p0) => + $$OutletsTableReferences( + db, + table, + p0, + ).outletCampaignsRefs, + referencedItemsForCurrentItem: + (item, referencedItems) => referencedItems.where( + (e) => e.outletId == item.localId, + ), + typedResults: items, + ), + ]; + }, + ); + }, + ), + ); +} + +typedef $$OutletsTableProcessedTableManager = + ProcessedTableManager< + _$AppDatabase, + $OutletsTable, + Outlet, + $$OutletsTableFilterComposer, + $$OutletsTableOrderingComposer, + $$OutletsTableAnnotationComposer, + $$OutletsTableCreateCompanionBuilder, + $$OutletsTableUpdateCompanionBuilder, + (Outlet, $$OutletsTableReferences), + Outlet, + PrefetchHooks Function({ + bool usersRefs, + bool customersRefs, + bool outletInventoriesRefs, + bool stockCardsRefs, + bool ordersRefs, + bool purchasesRefs, + bool transactionsRefs, + bool outletCampaignsRefs, + }) + >; +typedef $$UsersTableCreateCompanionBuilder = + UsersCompanion Function({ + Value localId, + Value serverId, + required String outletId, + required String name, + Value email, + Value phoneNumber, + Value passwordHash, + required String role, + Value placeOfBirth, + Value dateOfBirth, + Value shiftName, + Value shiftStartTime, + Value shiftEndTime, + Value isActive, + Value syncStatus, + Value createdAt, + Value updatedAt, + Value deletedAt, + Value rowid, + }); +typedef $$UsersTableUpdateCompanionBuilder = + UsersCompanion Function({ + Value localId, + Value serverId, + Value outletId, + Value name, + Value email, + Value phoneNumber, + Value passwordHash, + Value role, + Value placeOfBirth, + Value dateOfBirth, + Value shiftName, + Value shiftStartTime, + Value shiftEndTime, + Value isActive, + Value syncStatus, + Value createdAt, + Value updatedAt, + Value deletedAt, + Value rowid, + }); + +final class $$UsersTableReferences + extends BaseReferences<_$AppDatabase, $UsersTable, User> { + $$UsersTableReferences(super.$_db, super.$_table, super.$_typedResult); + + static $OutletsTable _outletIdTable(_$AppDatabase db) => db.outlets + .createAlias($_aliasNameGenerator(db.users.outletId, db.outlets.localId)); + + $$OutletsTableProcessedTableManager get outletId { + final $_column = $_itemColumn('outlet_id')!; + + final manager = $$OutletsTableTableManager( + $_db, + $_db.outlets, + ).filter((f) => f.localId.sqlEquals($_column)); + final item = $_typedResult.readTableOrNull(_outletIdTable($_db)); + if (item == null) return manager; + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: [item]), + ); + } + + static MultiTypedResultKey<$StockCardsTable, List> + _stockCardsRefsTable(_$AppDatabase db) => MultiTypedResultKey.fromTable( + db.stockCards, + aliasName: $_aliasNameGenerator(db.users.localId, db.stockCards.userId), + ); + + $$StockCardsTableProcessedTableManager get stockCardsRefs { + final manager = $$StockCardsTableTableManager($_db, $_db.stockCards).filter( + (f) => f.userId.localId.sqlEquals($_itemColumn('local_id')!), + ); + + final cache = $_typedResult.readTableOrNull(_stockCardsRefsTable($_db)); + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: cache), + ); + } + + static MultiTypedResultKey<$OrdersTable, List> _ordersRefsTable( + _$AppDatabase db, + ) => MultiTypedResultKey.fromTable( + db.orders, + aliasName: $_aliasNameGenerator(db.users.localId, db.orders.userId), + ); + + $$OrdersTableProcessedTableManager get ordersRefs { + final manager = $$OrdersTableTableManager($_db, $_db.orders).filter( + (f) => f.userId.localId.sqlEquals($_itemColumn('local_id')!), + ); + + final cache = $_typedResult.readTableOrNull(_ordersRefsTable($_db)); + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: cache), + ); + } + + static MultiTypedResultKey<$PurchasesTable, List> + _purchasesRefsTable(_$AppDatabase db) => MultiTypedResultKey.fromTable( + db.purchases, + aliasName: $_aliasNameGenerator(db.users.localId, db.purchases.userId), + ); + + $$PurchasesTableProcessedTableManager get purchasesRefs { + final manager = $$PurchasesTableTableManager($_db, $_db.purchases).filter( + (f) => f.userId.localId.sqlEquals($_itemColumn('local_id')!), + ); + + final cache = $_typedResult.readTableOrNull(_purchasesRefsTable($_db)); + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: cache), + ); + } + + static MultiTypedResultKey<$TransactionsTable, List> + _transactionsRefsTable(_$AppDatabase db) => MultiTypedResultKey.fromTable( + db.transactions, + aliasName: $_aliasNameGenerator(db.users.localId, db.transactions.userId), + ); + + $$TransactionsTableProcessedTableManager get transactionsRefs { + final manager = $$TransactionsTableTableManager($_db, $_db.transactions) + .filter( + (f) => f.userId.localId.sqlEquals($_itemColumn('local_id')!), + ); + + final cache = $_typedResult.readTableOrNull(_transactionsRefsTable($_db)); + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: cache), + ); + } +} + +class $$UsersTableFilterComposer extends Composer<_$AppDatabase, $UsersTable> { + $$UsersTableFilterComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnFilters get localId => $composableBuilder( + column: $table.localId, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get serverId => $composableBuilder( + column: $table.serverId, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get name => $composableBuilder( + column: $table.name, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get email => $composableBuilder( + column: $table.email, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get phoneNumber => $composableBuilder( + column: $table.phoneNumber, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get passwordHash => $composableBuilder( + column: $table.passwordHash, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get role => $composableBuilder( + column: $table.role, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get placeOfBirth => $composableBuilder( + column: $table.placeOfBirth, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get dateOfBirth => $composableBuilder( + column: $table.dateOfBirth, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get shiftName => $composableBuilder( + column: $table.shiftName, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get shiftStartTime => $composableBuilder( + column: $table.shiftStartTime, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get shiftEndTime => $composableBuilder( + column: $table.shiftEndTime, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get isActive => $composableBuilder( + column: $table.isActive, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get syncStatus => $composableBuilder( + column: $table.syncStatus, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get createdAt => $composableBuilder( + column: $table.createdAt, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get updatedAt => $composableBuilder( + column: $table.updatedAt, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get deletedAt => $composableBuilder( + column: $table.deletedAt, + builder: (column) => ColumnFilters(column), + ); + + $$OutletsTableFilterComposer get outletId { + final $$OutletsTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.outletId, + referencedTable: $db.outlets, + getReferencedColumn: (t) => t.localId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$OutletsTableFilterComposer( + $db: $db, + $table: $db.outlets, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } + + Expression stockCardsRefs( + Expression Function($$StockCardsTableFilterComposer f) f, + ) { + final $$StockCardsTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.localId, + referencedTable: $db.stockCards, + getReferencedColumn: (t) => t.userId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$StockCardsTableFilterComposer( + $db: $db, + $table: $db.stockCards, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return f(composer); + } + + Expression ordersRefs( + Expression Function($$OrdersTableFilterComposer f) f, + ) { + final $$OrdersTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.localId, + referencedTable: $db.orders, + getReferencedColumn: (t) => t.userId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$OrdersTableFilterComposer( + $db: $db, + $table: $db.orders, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return f(composer); + } + + Expression purchasesRefs( + Expression Function($$PurchasesTableFilterComposer f) f, + ) { + final $$PurchasesTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.localId, + referencedTable: $db.purchases, + getReferencedColumn: (t) => t.userId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$PurchasesTableFilterComposer( + $db: $db, + $table: $db.purchases, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return f(composer); + } + + Expression transactionsRefs( + Expression Function($$TransactionsTableFilterComposer f) f, + ) { + final $$TransactionsTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.localId, + referencedTable: $db.transactions, + getReferencedColumn: (t) => t.userId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$TransactionsTableFilterComposer( + $db: $db, + $table: $db.transactions, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return f(composer); + } +} + +class $$UsersTableOrderingComposer + extends Composer<_$AppDatabase, $UsersTable> { + $$UsersTableOrderingComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnOrderings get localId => $composableBuilder( + column: $table.localId, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get serverId => $composableBuilder( + column: $table.serverId, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get name => $composableBuilder( + column: $table.name, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get email => $composableBuilder( + column: $table.email, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get phoneNumber => $composableBuilder( + column: $table.phoneNumber, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get passwordHash => $composableBuilder( + column: $table.passwordHash, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get role => $composableBuilder( + column: $table.role, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get placeOfBirth => $composableBuilder( + column: $table.placeOfBirth, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get dateOfBirth => $composableBuilder( + column: $table.dateOfBirth, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get shiftName => $composableBuilder( + column: $table.shiftName, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get shiftStartTime => $composableBuilder( + column: $table.shiftStartTime, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get shiftEndTime => $composableBuilder( + column: $table.shiftEndTime, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get isActive => $composableBuilder( + column: $table.isActive, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get syncStatus => $composableBuilder( + column: $table.syncStatus, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get createdAt => $composableBuilder( + column: $table.createdAt, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get updatedAt => $composableBuilder( + column: $table.updatedAt, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get deletedAt => $composableBuilder( + column: $table.deletedAt, + builder: (column) => ColumnOrderings(column), + ); + + $$OutletsTableOrderingComposer get outletId { + final $$OutletsTableOrderingComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.outletId, + referencedTable: $db.outlets, + getReferencedColumn: (t) => t.localId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$OutletsTableOrderingComposer( + $db: $db, + $table: $db.outlets, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } +} + +class $$UsersTableAnnotationComposer + extends Composer<_$AppDatabase, $UsersTable> { + $$UsersTableAnnotationComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + GeneratedColumn get localId => + $composableBuilder(column: $table.localId, builder: (column) => column); + + GeneratedColumn get serverId => + $composableBuilder(column: $table.serverId, builder: (column) => column); + + GeneratedColumn get name => + $composableBuilder(column: $table.name, builder: (column) => column); + + GeneratedColumn get email => + $composableBuilder(column: $table.email, builder: (column) => column); + + GeneratedColumn get phoneNumber => $composableBuilder( + column: $table.phoneNumber, + builder: (column) => column, + ); + + GeneratedColumn get passwordHash => $composableBuilder( + column: $table.passwordHash, + builder: (column) => column, + ); + + GeneratedColumn get role => + $composableBuilder(column: $table.role, builder: (column) => column); + + GeneratedColumn get placeOfBirth => $composableBuilder( + column: $table.placeOfBirth, + builder: (column) => column, + ); + + GeneratedColumn get dateOfBirth => $composableBuilder( + column: $table.dateOfBirth, + builder: (column) => column, + ); + + GeneratedColumn get shiftName => + $composableBuilder(column: $table.shiftName, builder: (column) => column); + + GeneratedColumn get shiftStartTime => $composableBuilder( + column: $table.shiftStartTime, + builder: (column) => column, + ); + + GeneratedColumn get shiftEndTime => $composableBuilder( + column: $table.shiftEndTime, + builder: (column) => column, + ); + + GeneratedColumn get isActive => + $composableBuilder(column: $table.isActive, builder: (column) => column); + + GeneratedColumn get syncStatus => $composableBuilder( + column: $table.syncStatus, + builder: (column) => column, + ); + + GeneratedColumn get createdAt => + $composableBuilder(column: $table.createdAt, builder: (column) => column); + + GeneratedColumn get updatedAt => + $composableBuilder(column: $table.updatedAt, builder: (column) => column); + + GeneratedColumn get deletedAt => + $composableBuilder(column: $table.deletedAt, builder: (column) => column); + + $$OutletsTableAnnotationComposer get outletId { + final $$OutletsTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.outletId, + referencedTable: $db.outlets, + getReferencedColumn: (t) => t.localId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$OutletsTableAnnotationComposer( + $db: $db, + $table: $db.outlets, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } + + Expression stockCardsRefs( + Expression Function($$StockCardsTableAnnotationComposer a) f, + ) { + final $$StockCardsTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.localId, + referencedTable: $db.stockCards, + getReferencedColumn: (t) => t.userId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$StockCardsTableAnnotationComposer( + $db: $db, + $table: $db.stockCards, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return f(composer); + } + + Expression ordersRefs( + Expression Function($$OrdersTableAnnotationComposer a) f, + ) { + final $$OrdersTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.localId, + referencedTable: $db.orders, + getReferencedColumn: (t) => t.userId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$OrdersTableAnnotationComposer( + $db: $db, + $table: $db.orders, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return f(composer); + } + + Expression purchasesRefs( + Expression Function($$PurchasesTableAnnotationComposer a) f, + ) { + final $$PurchasesTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.localId, + referencedTable: $db.purchases, + getReferencedColumn: (t) => t.userId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$PurchasesTableAnnotationComposer( + $db: $db, + $table: $db.purchases, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return f(composer); + } + + Expression transactionsRefs( + Expression Function($$TransactionsTableAnnotationComposer a) f, + ) { + final $$TransactionsTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.localId, + referencedTable: $db.transactions, + getReferencedColumn: (t) => t.userId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$TransactionsTableAnnotationComposer( + $db: $db, + $table: $db.transactions, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return f(composer); + } +} + +class $$UsersTableTableManager + extends + RootTableManager< + _$AppDatabase, + $UsersTable, + User, + $$UsersTableFilterComposer, + $$UsersTableOrderingComposer, + $$UsersTableAnnotationComposer, + $$UsersTableCreateCompanionBuilder, + $$UsersTableUpdateCompanionBuilder, + (User, $$UsersTableReferences), + User, + PrefetchHooks Function({ + bool outletId, + bool stockCardsRefs, + bool ordersRefs, + bool purchasesRefs, + bool transactionsRefs, + }) + > { + $$UsersTableTableManager(_$AppDatabase db, $UsersTable table) + : super( + TableManagerState( + db: db, + table: table, + createFilteringComposer: () => + $$UsersTableFilterComposer($db: db, $table: table), + createOrderingComposer: () => + $$UsersTableOrderingComposer($db: db, $table: table), + createComputedFieldComposer: () => + $$UsersTableAnnotationComposer($db: db, $table: table), + updateCompanionCallback: + ({ + Value localId = const Value.absent(), + Value serverId = const Value.absent(), + Value outletId = const Value.absent(), + Value name = const Value.absent(), + Value email = const Value.absent(), + Value phoneNumber = const Value.absent(), + Value passwordHash = const Value.absent(), + Value role = const Value.absent(), + Value placeOfBirth = const Value.absent(), + Value dateOfBirth = const Value.absent(), + Value shiftName = const Value.absent(), + Value shiftStartTime = const Value.absent(), + Value shiftEndTime = const Value.absent(), + Value isActive = const Value.absent(), + Value syncStatus = const Value.absent(), + Value createdAt = const Value.absent(), + Value updatedAt = const Value.absent(), + Value deletedAt = const Value.absent(), + Value rowid = const Value.absent(), + }) => UsersCompanion( + localId: localId, + serverId: serverId, + outletId: outletId, + name: name, + email: email, + phoneNumber: phoneNumber, + passwordHash: passwordHash, + role: role, + placeOfBirth: placeOfBirth, + dateOfBirth: dateOfBirth, + shiftName: shiftName, + shiftStartTime: shiftStartTime, + shiftEndTime: shiftEndTime, + isActive: isActive, + syncStatus: syncStatus, + createdAt: createdAt, + updatedAt: updatedAt, + deletedAt: deletedAt, + rowid: rowid, + ), + createCompanionCallback: + ({ + Value localId = const Value.absent(), + Value serverId = const Value.absent(), + required String outletId, + required String name, + Value email = const Value.absent(), + Value phoneNumber = const Value.absent(), + Value passwordHash = const Value.absent(), + required String role, + Value placeOfBirth = const Value.absent(), + Value dateOfBirth = const Value.absent(), + Value shiftName = const Value.absent(), + Value shiftStartTime = const Value.absent(), + Value shiftEndTime = const Value.absent(), + Value isActive = const Value.absent(), + Value syncStatus = const Value.absent(), + Value createdAt = const Value.absent(), + Value updatedAt = const Value.absent(), + Value deletedAt = const Value.absent(), + Value rowid = const Value.absent(), + }) => UsersCompanion.insert( + localId: localId, + serverId: serverId, + outletId: outletId, + name: name, + email: email, + phoneNumber: phoneNumber, + passwordHash: passwordHash, + role: role, + placeOfBirth: placeOfBirth, + dateOfBirth: dateOfBirth, + shiftName: shiftName, + shiftStartTime: shiftStartTime, + shiftEndTime: shiftEndTime, + isActive: isActive, + syncStatus: syncStatus, + createdAt: createdAt, + updatedAt: updatedAt, + deletedAt: deletedAt, + rowid: rowid, + ), + withReferenceMapper: (p0) => p0 + .map( + (e) => + (e.readTable(table), $$UsersTableReferences(db, table, e)), + ) + .toList(), + prefetchHooksCallback: + ({ + outletId = false, + stockCardsRefs = false, + ordersRefs = false, + purchasesRefs = false, + transactionsRefs = false, + }) { + return PrefetchHooks( + db: db, + explicitlyWatchedTables: [ + if (stockCardsRefs) db.stockCards, + if (ordersRefs) db.orders, + if (purchasesRefs) db.purchases, + if (transactionsRefs) db.transactions, + ], + addJoins: + < + T extends TableManagerState< + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic + > + >(state) { + if (outletId) { + state = + state.withJoin( + currentTable: table, + currentColumn: table.outletId, + referencedTable: $$UsersTableReferences + ._outletIdTable(db), + referencedColumn: $$UsersTableReferences + ._outletIdTable(db) + .localId, + ) + as T; + } + + return state; + }, + getPrefetchedDataCallback: (items) async { + return [ + if (stockCardsRefs) + await $_getPrefetchedData( + currentTable: table, + referencedTable: $$UsersTableReferences + ._stockCardsRefsTable(db), + managerFromTypedResult: (p0) => + $$UsersTableReferences( + db, + table, + p0, + ).stockCardsRefs, + referencedItemsForCurrentItem: + (item, referencedItems) => referencedItems.where( + (e) => e.userId == item.localId, + ), + typedResults: items, + ), + if (ordersRefs) + await $_getPrefetchedData( + currentTable: table, + referencedTable: $$UsersTableReferences + ._ordersRefsTable(db), + managerFromTypedResult: (p0) => + $$UsersTableReferences(db, table, p0).ordersRefs, + referencedItemsForCurrentItem: + (item, referencedItems) => referencedItems.where( + (e) => e.userId == item.localId, + ), + typedResults: items, + ), + if (purchasesRefs) + await $_getPrefetchedData( + currentTable: table, + referencedTable: $$UsersTableReferences + ._purchasesRefsTable(db), + managerFromTypedResult: (p0) => + $$UsersTableReferences( + db, + table, + p0, + ).purchasesRefs, + referencedItemsForCurrentItem: + (item, referencedItems) => referencedItems.where( + (e) => e.userId == item.localId, + ), + typedResults: items, + ), + if (transactionsRefs) + await $_getPrefetchedData< + User, + $UsersTable, + Transaction + >( + currentTable: table, + referencedTable: $$UsersTableReferences + ._transactionsRefsTable(db), + managerFromTypedResult: (p0) => + $$UsersTableReferences( + db, + table, + p0, + ).transactionsRefs, + referencedItemsForCurrentItem: + (item, referencedItems) => referencedItems.where( + (e) => e.userId == item.localId, + ), + typedResults: items, + ), + ]; + }, + ); + }, + ), + ); +} + +typedef $$UsersTableProcessedTableManager = + ProcessedTableManager< + _$AppDatabase, + $UsersTable, + User, + $$UsersTableFilterComposer, + $$UsersTableOrderingComposer, + $$UsersTableAnnotationComposer, + $$UsersTableCreateCompanionBuilder, + $$UsersTableUpdateCompanionBuilder, + (User, $$UsersTableReferences), + User, + PrefetchHooks Function({ + bool outletId, + bool stockCardsRefs, + bool ordersRefs, + bool purchasesRefs, + bool transactionsRefs, + }) + >; +typedef $$CustomersTableCreateCompanionBuilder = + CustomersCompanion Function({ + Value localId, + Value serverId, + required String outletId, + required String name, + Value email, + Value phoneNumber, + Value passwordHash, + required String source, + Value syncStatus, + Value createdAt, + Value updatedAt, + Value deletedAt, + Value rowid, + }); +typedef $$CustomersTableUpdateCompanionBuilder = + CustomersCompanion Function({ + Value localId, + Value serverId, + Value outletId, + Value name, + Value email, + Value phoneNumber, + Value passwordHash, + Value source, + Value syncStatus, + Value createdAt, + Value updatedAt, + Value deletedAt, + Value rowid, + }); + +final class $$CustomersTableReferences + extends BaseReferences<_$AppDatabase, $CustomersTable, Customer> { + $$CustomersTableReferences(super.$_db, super.$_table, super.$_typedResult); + + static $OutletsTable _outletIdTable(_$AppDatabase db) => + db.outlets.createAlias( + $_aliasNameGenerator(db.customers.outletId, db.outlets.localId), + ); + + $$OutletsTableProcessedTableManager get outletId { + final $_column = $_itemColumn('outlet_id')!; + + final manager = $$OutletsTableTableManager( + $_db, + $_db.outlets, + ).filter((f) => f.localId.sqlEquals($_column)); + final item = $_typedResult.readTableOrNull(_outletIdTable($_db)); + if (item == null) return manager; + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: [item]), + ); + } + + static MultiTypedResultKey< + $CustomerAddressesTable, + List + > + _customerAddressesRefsTable(_$AppDatabase db) => + MultiTypedResultKey.fromTable( + db.customerAddresses, + aliasName: $_aliasNameGenerator( + db.customers.localId, + db.customerAddresses.customerId, + ), + ); + + $$CustomerAddressesTableProcessedTableManager get customerAddressesRefs { + final manager = + $$CustomerAddressesTableTableManager( + $_db, + $_db.customerAddresses, + ).filter( + (f) => + f.customerId.localId.sqlEquals($_itemColumn('local_id')!), + ); + + final cache = $_typedResult.readTableOrNull( + _customerAddressesRefsTable($_db), + ); + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: cache), + ); + } + + static MultiTypedResultKey<$ProductReviewsTable, List> + _productReviewsRefsTable(_$AppDatabase db) => MultiTypedResultKey.fromTable( + db.productReviews, + aliasName: $_aliasNameGenerator( + db.customers.localId, + db.productReviews.customerId, + ), + ); + + $$ProductReviewsTableProcessedTableManager get productReviewsRefs { + final manager = $$ProductReviewsTableTableManager($_db, $_db.productReviews) + .filter( + (f) => + f.customerId.localId.sqlEquals($_itemColumn('local_id')!), + ); + + final cache = $_typedResult.readTableOrNull(_productReviewsRefsTable($_db)); + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: cache), + ); + } + + static MultiTypedResultKey<$ProductLikesTable, List> + _productLikesRefsTable(_$AppDatabase db) => MultiTypedResultKey.fromTable( + db.productLikes, + aliasName: $_aliasNameGenerator( + db.customers.localId, + db.productLikes.customerId, + ), + ); + + $$ProductLikesTableProcessedTableManager get productLikesRefs { + final manager = $$ProductLikesTableTableManager($_db, $_db.productLikes) + .filter( + (f) => + f.customerId.localId.sqlEquals($_itemColumn('local_id')!), + ); + + final cache = $_typedResult.readTableOrNull(_productLikesRefsTable($_db)); + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: cache), + ); + } + + static MultiTypedResultKey<$OrdersTable, List> _ordersRefsTable( + _$AppDatabase db, + ) => MultiTypedResultKey.fromTable( + db.orders, + aliasName: $_aliasNameGenerator(db.customers.localId, db.orders.customerId), + ); + + $$OrdersTableProcessedTableManager get ordersRefs { + final manager = $$OrdersTableTableManager($_db, $_db.orders).filter( + (f) => f.customerId.localId.sqlEquals($_itemColumn('local_id')!), + ); + + final cache = $_typedResult.readTableOrNull(_ordersRefsTable($_db)); + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: cache), + ); + } +} + +class $$CustomersTableFilterComposer + extends Composer<_$AppDatabase, $CustomersTable> { + $$CustomersTableFilterComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnFilters get localId => $composableBuilder( + column: $table.localId, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get serverId => $composableBuilder( + column: $table.serverId, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get name => $composableBuilder( + column: $table.name, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get email => $composableBuilder( + column: $table.email, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get phoneNumber => $composableBuilder( + column: $table.phoneNumber, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get passwordHash => $composableBuilder( + column: $table.passwordHash, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get source => $composableBuilder( + column: $table.source, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get syncStatus => $composableBuilder( + column: $table.syncStatus, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get createdAt => $composableBuilder( + column: $table.createdAt, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get updatedAt => $composableBuilder( + column: $table.updatedAt, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get deletedAt => $composableBuilder( + column: $table.deletedAt, + builder: (column) => ColumnFilters(column), + ); + + $$OutletsTableFilterComposer get outletId { + final $$OutletsTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.outletId, + referencedTable: $db.outlets, + getReferencedColumn: (t) => t.localId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$OutletsTableFilterComposer( + $db: $db, + $table: $db.outlets, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } + + Expression customerAddressesRefs( + Expression Function($$CustomerAddressesTableFilterComposer f) f, + ) { + final $$CustomerAddressesTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.localId, + referencedTable: $db.customerAddresses, + getReferencedColumn: (t) => t.customerId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$CustomerAddressesTableFilterComposer( + $db: $db, + $table: $db.customerAddresses, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return f(composer); + } + + Expression productReviewsRefs( + Expression Function($$ProductReviewsTableFilterComposer f) f, + ) { + final $$ProductReviewsTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.localId, + referencedTable: $db.productReviews, + getReferencedColumn: (t) => t.customerId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$ProductReviewsTableFilterComposer( + $db: $db, + $table: $db.productReviews, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return f(composer); + } + + Expression productLikesRefs( + Expression Function($$ProductLikesTableFilterComposer f) f, + ) { + final $$ProductLikesTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.localId, + referencedTable: $db.productLikes, + getReferencedColumn: (t) => t.customerId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$ProductLikesTableFilterComposer( + $db: $db, + $table: $db.productLikes, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return f(composer); + } + + Expression ordersRefs( + Expression Function($$OrdersTableFilterComposer f) f, + ) { + final $$OrdersTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.localId, + referencedTable: $db.orders, + getReferencedColumn: (t) => t.customerId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$OrdersTableFilterComposer( + $db: $db, + $table: $db.orders, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return f(composer); + } +} + +class $$CustomersTableOrderingComposer + extends Composer<_$AppDatabase, $CustomersTable> { + $$CustomersTableOrderingComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnOrderings get localId => $composableBuilder( + column: $table.localId, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get serverId => $composableBuilder( + column: $table.serverId, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get name => $composableBuilder( + column: $table.name, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get email => $composableBuilder( + column: $table.email, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get phoneNumber => $composableBuilder( + column: $table.phoneNumber, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get passwordHash => $composableBuilder( + column: $table.passwordHash, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get source => $composableBuilder( + column: $table.source, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get syncStatus => $composableBuilder( + column: $table.syncStatus, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get createdAt => $composableBuilder( + column: $table.createdAt, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get updatedAt => $composableBuilder( + column: $table.updatedAt, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get deletedAt => $composableBuilder( + column: $table.deletedAt, + builder: (column) => ColumnOrderings(column), + ); + + $$OutletsTableOrderingComposer get outletId { + final $$OutletsTableOrderingComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.outletId, + referencedTable: $db.outlets, + getReferencedColumn: (t) => t.localId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$OutletsTableOrderingComposer( + $db: $db, + $table: $db.outlets, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } +} + +class $$CustomersTableAnnotationComposer + extends Composer<_$AppDatabase, $CustomersTable> { + $$CustomersTableAnnotationComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + GeneratedColumn get localId => + $composableBuilder(column: $table.localId, builder: (column) => column); + + GeneratedColumn get serverId => + $composableBuilder(column: $table.serverId, builder: (column) => column); + + GeneratedColumn get name => + $composableBuilder(column: $table.name, builder: (column) => column); + + GeneratedColumn get email => + $composableBuilder(column: $table.email, builder: (column) => column); + + GeneratedColumn get phoneNumber => $composableBuilder( + column: $table.phoneNumber, + builder: (column) => column, + ); + + GeneratedColumn get passwordHash => $composableBuilder( + column: $table.passwordHash, + builder: (column) => column, + ); + + GeneratedColumn get source => + $composableBuilder(column: $table.source, builder: (column) => column); + + GeneratedColumn get syncStatus => $composableBuilder( + column: $table.syncStatus, + builder: (column) => column, + ); + + GeneratedColumn get createdAt => + $composableBuilder(column: $table.createdAt, builder: (column) => column); + + GeneratedColumn get updatedAt => + $composableBuilder(column: $table.updatedAt, builder: (column) => column); + + GeneratedColumn get deletedAt => + $composableBuilder(column: $table.deletedAt, builder: (column) => column); + + $$OutletsTableAnnotationComposer get outletId { + final $$OutletsTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.outletId, + referencedTable: $db.outlets, + getReferencedColumn: (t) => t.localId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$OutletsTableAnnotationComposer( + $db: $db, + $table: $db.outlets, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } + + Expression customerAddressesRefs( + Expression Function($$CustomerAddressesTableAnnotationComposer a) f, + ) { + final $$CustomerAddressesTableAnnotationComposer composer = + $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.localId, + referencedTable: $db.customerAddresses, + getReferencedColumn: (t) => t.customerId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$CustomerAddressesTableAnnotationComposer( + $db: $db, + $table: $db.customerAddresses, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return f(composer); + } + + Expression productReviewsRefs( + Expression Function($$ProductReviewsTableAnnotationComposer a) f, + ) { + final $$ProductReviewsTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.localId, + referencedTable: $db.productReviews, + getReferencedColumn: (t) => t.customerId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$ProductReviewsTableAnnotationComposer( + $db: $db, + $table: $db.productReviews, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return f(composer); + } + + Expression productLikesRefs( + Expression Function($$ProductLikesTableAnnotationComposer a) f, + ) { + final $$ProductLikesTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.localId, + referencedTable: $db.productLikes, + getReferencedColumn: (t) => t.customerId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$ProductLikesTableAnnotationComposer( + $db: $db, + $table: $db.productLikes, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return f(composer); + } + + Expression ordersRefs( + Expression Function($$OrdersTableAnnotationComposer a) f, + ) { + final $$OrdersTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.localId, + referencedTable: $db.orders, + getReferencedColumn: (t) => t.customerId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$OrdersTableAnnotationComposer( + $db: $db, + $table: $db.orders, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return f(composer); + } +} + +class $$CustomersTableTableManager + extends + RootTableManager< + _$AppDatabase, + $CustomersTable, + Customer, + $$CustomersTableFilterComposer, + $$CustomersTableOrderingComposer, + $$CustomersTableAnnotationComposer, + $$CustomersTableCreateCompanionBuilder, + $$CustomersTableUpdateCompanionBuilder, + (Customer, $$CustomersTableReferences), + Customer, + PrefetchHooks Function({ + bool outletId, + bool customerAddressesRefs, + bool productReviewsRefs, + bool productLikesRefs, + bool ordersRefs, + }) + > { + $$CustomersTableTableManager(_$AppDatabase db, $CustomersTable table) + : super( + TableManagerState( + db: db, + table: table, + createFilteringComposer: () => + $$CustomersTableFilterComposer($db: db, $table: table), + createOrderingComposer: () => + $$CustomersTableOrderingComposer($db: db, $table: table), + createComputedFieldComposer: () => + $$CustomersTableAnnotationComposer($db: db, $table: table), + updateCompanionCallback: + ({ + Value localId = const Value.absent(), + Value serverId = const Value.absent(), + Value outletId = const Value.absent(), + Value name = const Value.absent(), + Value email = const Value.absent(), + Value phoneNumber = const Value.absent(), + Value passwordHash = const Value.absent(), + Value source = const Value.absent(), + Value syncStatus = const Value.absent(), + Value createdAt = const Value.absent(), + Value updatedAt = const Value.absent(), + Value deletedAt = const Value.absent(), + Value rowid = const Value.absent(), + }) => CustomersCompanion( + localId: localId, + serverId: serverId, + outletId: outletId, + name: name, + email: email, + phoneNumber: phoneNumber, + passwordHash: passwordHash, + source: source, + syncStatus: syncStatus, + createdAt: createdAt, + updatedAt: updatedAt, + deletedAt: deletedAt, + rowid: rowid, + ), + createCompanionCallback: + ({ + Value localId = const Value.absent(), + Value serverId = const Value.absent(), + required String outletId, + required String name, + Value email = const Value.absent(), + Value phoneNumber = const Value.absent(), + Value passwordHash = const Value.absent(), + required String source, + Value syncStatus = const Value.absent(), + Value createdAt = const Value.absent(), + Value updatedAt = const Value.absent(), + Value deletedAt = const Value.absent(), + Value rowid = const Value.absent(), + }) => CustomersCompanion.insert( + localId: localId, + serverId: serverId, + outletId: outletId, + name: name, + email: email, + phoneNumber: phoneNumber, + passwordHash: passwordHash, + source: source, + syncStatus: syncStatus, + createdAt: createdAt, + updatedAt: updatedAt, + deletedAt: deletedAt, + rowid: rowid, + ), + withReferenceMapper: (p0) => p0 + .map( + (e) => ( + e.readTable(table), + $$CustomersTableReferences(db, table, e), + ), + ) + .toList(), + prefetchHooksCallback: + ({ + outletId = false, + customerAddressesRefs = false, + productReviewsRefs = false, + productLikesRefs = false, + ordersRefs = false, + }) { + return PrefetchHooks( + db: db, + explicitlyWatchedTables: [ + if (customerAddressesRefs) db.customerAddresses, + if (productReviewsRefs) db.productReviews, + if (productLikesRefs) db.productLikes, + if (ordersRefs) db.orders, + ], + addJoins: + < + T extends TableManagerState< + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic + > + >(state) { + if (outletId) { + state = + state.withJoin( + currentTable: table, + currentColumn: table.outletId, + referencedTable: $$CustomersTableReferences + ._outletIdTable(db), + referencedColumn: $$CustomersTableReferences + ._outletIdTable(db) + .localId, + ) + as T; + } + + return state; + }, + getPrefetchedDataCallback: (items) async { + return [ + if (customerAddressesRefs) + await $_getPrefetchedData< + Customer, + $CustomersTable, + CustomerAddressesData + >( + currentTable: table, + referencedTable: $$CustomersTableReferences + ._customerAddressesRefsTable(db), + managerFromTypedResult: (p0) => + $$CustomersTableReferences( + db, + table, + p0, + ).customerAddressesRefs, + referencedItemsForCurrentItem: + (item, referencedItems) => referencedItems.where( + (e) => e.customerId == item.localId, + ), + typedResults: items, + ), + if (productReviewsRefs) + await $_getPrefetchedData< + Customer, + $CustomersTable, + ProductReview + >( + currentTable: table, + referencedTable: $$CustomersTableReferences + ._productReviewsRefsTable(db), + managerFromTypedResult: (p0) => + $$CustomersTableReferences( + db, + table, + p0, + ).productReviewsRefs, + referencedItemsForCurrentItem: + (item, referencedItems) => referencedItems.where( + (e) => e.customerId == item.localId, + ), + typedResults: items, + ), + if (productLikesRefs) + await $_getPrefetchedData< + Customer, + $CustomersTable, + ProductLike + >( + currentTable: table, + referencedTable: $$CustomersTableReferences + ._productLikesRefsTable(db), + managerFromTypedResult: (p0) => + $$CustomersTableReferences( + db, + table, + p0, + ).productLikesRefs, + referencedItemsForCurrentItem: + (item, referencedItems) => referencedItems.where( + (e) => e.customerId == item.localId, + ), + typedResults: items, + ), + if (ordersRefs) + await $_getPrefetchedData< + Customer, + $CustomersTable, + Order + >( + currentTable: table, + referencedTable: $$CustomersTableReferences + ._ordersRefsTable(db), + managerFromTypedResult: (p0) => + $$CustomersTableReferences( + db, + table, + p0, + ).ordersRefs, + referencedItemsForCurrentItem: + (item, referencedItems) => referencedItems.where( + (e) => e.customerId == item.localId, + ), + typedResults: items, + ), + ]; + }, + ); + }, + ), + ); +} + +typedef $$CustomersTableProcessedTableManager = + ProcessedTableManager< + _$AppDatabase, + $CustomersTable, + Customer, + $$CustomersTableFilterComposer, + $$CustomersTableOrderingComposer, + $$CustomersTableAnnotationComposer, + $$CustomersTableCreateCompanionBuilder, + $$CustomersTableUpdateCompanionBuilder, + (Customer, $$CustomersTableReferences), + Customer, + PrefetchHooks Function({ + bool outletId, + bool customerAddressesRefs, + bool productReviewsRefs, + bool productLikesRefs, + bool ordersRefs, + }) + >; +typedef $$CustomerAddressesTableCreateCompanionBuilder = + CustomerAddressesCompanion Function({ + Value localId, + Value serverId, + required String customerId, + Value label, + Value fullAddress, + Value latitude, + Value longitude, + Value notes, + Value syncStatus, + Value createdAt, + Value updatedAt, + Value deletedAt, + Value rowid, + }); +typedef $$CustomerAddressesTableUpdateCompanionBuilder = + CustomerAddressesCompanion Function({ + Value localId, + Value serverId, + Value customerId, + Value label, + Value fullAddress, + Value latitude, + Value longitude, + Value notes, + Value syncStatus, + Value createdAt, + Value updatedAt, + Value deletedAt, + Value rowid, + }); + +final class $$CustomerAddressesTableReferences + extends + BaseReferences< + _$AppDatabase, + $CustomerAddressesTable, + CustomerAddressesData + > { + $$CustomerAddressesTableReferences( + super.$_db, + super.$_table, + super.$_typedResult, + ); + + static $CustomersTable _customerIdTable(_$AppDatabase db) => + db.customers.createAlias( + $_aliasNameGenerator( + db.customerAddresses.customerId, + db.customers.localId, + ), + ); + + $$CustomersTableProcessedTableManager get customerId { + final $_column = $_itemColumn('customer_id')!; + + final manager = $$CustomersTableTableManager( + $_db, + $_db.customers, + ).filter((f) => f.localId.sqlEquals($_column)); + final item = $_typedResult.readTableOrNull(_customerIdTable($_db)); + if (item == null) return manager; + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: [item]), + ); + } +} + +class $$CustomerAddressesTableFilterComposer + extends Composer<_$AppDatabase, $CustomerAddressesTable> { + $$CustomerAddressesTableFilterComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnFilters get localId => $composableBuilder( + column: $table.localId, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get serverId => $composableBuilder( + column: $table.serverId, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get label => $composableBuilder( + column: $table.label, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get fullAddress => $composableBuilder( + column: $table.fullAddress, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get latitude => $composableBuilder( + column: $table.latitude, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get longitude => $composableBuilder( + column: $table.longitude, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get notes => $composableBuilder( + column: $table.notes, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get syncStatus => $composableBuilder( + column: $table.syncStatus, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get createdAt => $composableBuilder( + column: $table.createdAt, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get updatedAt => $composableBuilder( + column: $table.updatedAt, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get deletedAt => $composableBuilder( + column: $table.deletedAt, + builder: (column) => ColumnFilters(column), + ); + + $$CustomersTableFilterComposer get customerId { + final $$CustomersTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.customerId, + referencedTable: $db.customers, + getReferencedColumn: (t) => t.localId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$CustomersTableFilterComposer( + $db: $db, + $table: $db.customers, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } +} + +class $$CustomerAddressesTableOrderingComposer + extends Composer<_$AppDatabase, $CustomerAddressesTable> { + $$CustomerAddressesTableOrderingComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnOrderings get localId => $composableBuilder( + column: $table.localId, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get serverId => $composableBuilder( + column: $table.serverId, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get label => $composableBuilder( + column: $table.label, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get fullAddress => $composableBuilder( + column: $table.fullAddress, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get latitude => $composableBuilder( + column: $table.latitude, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get longitude => $composableBuilder( + column: $table.longitude, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get notes => $composableBuilder( + column: $table.notes, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get syncStatus => $composableBuilder( + column: $table.syncStatus, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get createdAt => $composableBuilder( + column: $table.createdAt, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get updatedAt => $composableBuilder( + column: $table.updatedAt, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get deletedAt => $composableBuilder( + column: $table.deletedAt, + builder: (column) => ColumnOrderings(column), + ); + + $$CustomersTableOrderingComposer get customerId { + final $$CustomersTableOrderingComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.customerId, + referencedTable: $db.customers, + getReferencedColumn: (t) => t.localId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$CustomersTableOrderingComposer( + $db: $db, + $table: $db.customers, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } +} + +class $$CustomerAddressesTableAnnotationComposer + extends Composer<_$AppDatabase, $CustomerAddressesTable> { + $$CustomerAddressesTableAnnotationComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + GeneratedColumn get localId => + $composableBuilder(column: $table.localId, builder: (column) => column); + + GeneratedColumn get serverId => + $composableBuilder(column: $table.serverId, builder: (column) => column); + + GeneratedColumn get label => + $composableBuilder(column: $table.label, builder: (column) => column); + + GeneratedColumn get fullAddress => $composableBuilder( + column: $table.fullAddress, + builder: (column) => column, + ); + + GeneratedColumn get latitude => + $composableBuilder(column: $table.latitude, builder: (column) => column); + + GeneratedColumn get longitude => + $composableBuilder(column: $table.longitude, builder: (column) => column); + + GeneratedColumn get notes => + $composableBuilder(column: $table.notes, builder: (column) => column); + + GeneratedColumn get syncStatus => $composableBuilder( + column: $table.syncStatus, + builder: (column) => column, + ); + + GeneratedColumn get createdAt => + $composableBuilder(column: $table.createdAt, builder: (column) => column); + + GeneratedColumn get updatedAt => + $composableBuilder(column: $table.updatedAt, builder: (column) => column); + + GeneratedColumn get deletedAt => + $composableBuilder(column: $table.deletedAt, builder: (column) => column); + + $$CustomersTableAnnotationComposer get customerId { + final $$CustomersTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.customerId, + referencedTable: $db.customers, + getReferencedColumn: (t) => t.localId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$CustomersTableAnnotationComposer( + $db: $db, + $table: $db.customers, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } +} + +class $$CustomerAddressesTableTableManager + extends + RootTableManager< + _$AppDatabase, + $CustomerAddressesTable, + CustomerAddressesData, + $$CustomerAddressesTableFilterComposer, + $$CustomerAddressesTableOrderingComposer, + $$CustomerAddressesTableAnnotationComposer, + $$CustomerAddressesTableCreateCompanionBuilder, + $$CustomerAddressesTableUpdateCompanionBuilder, + (CustomerAddressesData, $$CustomerAddressesTableReferences), + CustomerAddressesData, + PrefetchHooks Function({bool customerId}) + > { + $$CustomerAddressesTableTableManager( + _$AppDatabase db, + $CustomerAddressesTable table, + ) : super( + TableManagerState( + db: db, + table: table, + createFilteringComposer: () => + $$CustomerAddressesTableFilterComposer($db: db, $table: table), + createOrderingComposer: () => + $$CustomerAddressesTableOrderingComposer($db: db, $table: table), + createComputedFieldComposer: () => + $$CustomerAddressesTableAnnotationComposer( + $db: db, + $table: table, + ), + updateCompanionCallback: + ({ + Value localId = const Value.absent(), + Value serverId = const Value.absent(), + Value customerId = const Value.absent(), + Value label = const Value.absent(), + Value fullAddress = const Value.absent(), + Value latitude = const Value.absent(), + Value longitude = const Value.absent(), + Value notes = const Value.absent(), + Value syncStatus = const Value.absent(), + Value createdAt = const Value.absent(), + Value updatedAt = const Value.absent(), + Value deletedAt = const Value.absent(), + Value rowid = const Value.absent(), + }) => CustomerAddressesCompanion( + localId: localId, + serverId: serverId, + customerId: customerId, + label: label, + fullAddress: fullAddress, + latitude: latitude, + longitude: longitude, + notes: notes, + syncStatus: syncStatus, + createdAt: createdAt, + updatedAt: updatedAt, + deletedAt: deletedAt, + rowid: rowid, + ), + createCompanionCallback: + ({ + Value localId = const Value.absent(), + Value serverId = const Value.absent(), + required String customerId, + Value label = const Value.absent(), + Value fullAddress = const Value.absent(), + Value latitude = const Value.absent(), + Value longitude = const Value.absent(), + Value notes = const Value.absent(), + Value syncStatus = const Value.absent(), + Value createdAt = const Value.absent(), + Value updatedAt = const Value.absent(), + Value deletedAt = const Value.absent(), + Value rowid = const Value.absent(), + }) => CustomerAddressesCompanion.insert( + localId: localId, + serverId: serverId, + customerId: customerId, + label: label, + fullAddress: fullAddress, + latitude: latitude, + longitude: longitude, + notes: notes, + syncStatus: syncStatus, + createdAt: createdAt, + updatedAt: updatedAt, + deletedAt: deletedAt, + rowid: rowid, + ), + withReferenceMapper: (p0) => p0 + .map( + (e) => ( + e.readTable(table), + $$CustomerAddressesTableReferences(db, table, e), + ), + ) + .toList(), + prefetchHooksCallback: ({customerId = false}) { + return PrefetchHooks( + db: db, + explicitlyWatchedTables: [], + addJoins: + < + T extends TableManagerState< + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic + > + >(state) { + if (customerId) { + state = + state.withJoin( + currentTable: table, + currentColumn: table.customerId, + referencedTable: + $$CustomerAddressesTableReferences + ._customerIdTable(db), + referencedColumn: + $$CustomerAddressesTableReferences + ._customerIdTable(db) + .localId, + ) + as T; + } + + return state; + }, + getPrefetchedDataCallback: (items) async { + return []; + }, + ); + }, + ), + ); +} + +typedef $$CustomerAddressesTableProcessedTableManager = + ProcessedTableManager< + _$AppDatabase, + $CustomerAddressesTable, + CustomerAddressesData, + $$CustomerAddressesTableFilterComposer, + $$CustomerAddressesTableOrderingComposer, + $$CustomerAddressesTableAnnotationComposer, + $$CustomerAddressesTableCreateCompanionBuilder, + $$CustomerAddressesTableUpdateCompanionBuilder, + (CustomerAddressesData, $$CustomerAddressesTableReferences), + CustomerAddressesData, + PrefetchHooks Function({bool customerId}) + >; +typedef $$CategoriesTableCreateCompanionBuilder = + CategoriesCompanion Function({ + Value localId, + Value serverId, + required String name, + Value localImagePath, + Value serverImageUrl, + Value syncStatus, + Value createdAt, + Value updatedAt, + Value rowid, + }); +typedef $$CategoriesTableUpdateCompanionBuilder = + CategoriesCompanion Function({ + Value localId, + Value serverId, + Value name, + Value localImagePath, + Value serverImageUrl, + Value syncStatus, + Value createdAt, + Value updatedAt, + Value rowid, + }); + +final class $$CategoriesTableReferences + extends BaseReferences<_$AppDatabase, $CategoriesTable, Category> { + $$CategoriesTableReferences(super.$_db, super.$_table, super.$_typedResult); + + static MultiTypedResultKey<$ProductsTable, List> _productsRefsTable( + _$AppDatabase db, + ) => MultiTypedResultKey.fromTable( + db.products, + aliasName: $_aliasNameGenerator( + db.categories.localId, + db.products.categoryId, + ), + ); + + $$ProductsTableProcessedTableManager get productsRefs { + final manager = $$ProductsTableTableManager($_db, $_db.products).filter( + (f) => f.categoryId.localId.sqlEquals($_itemColumn('local_id')!), + ); + + final cache = $_typedResult.readTableOrNull(_productsRefsTable($_db)); + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: cache), + ); + } +} + +class $$CategoriesTableFilterComposer + extends Composer<_$AppDatabase, $CategoriesTable> { + $$CategoriesTableFilterComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnFilters get localId => $composableBuilder( + column: $table.localId, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get serverId => $composableBuilder( + column: $table.serverId, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get name => $composableBuilder( + column: $table.name, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get localImagePath => $composableBuilder( + column: $table.localImagePath, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get serverImageUrl => $composableBuilder( + column: $table.serverImageUrl, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get syncStatus => $composableBuilder( + column: $table.syncStatus, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get createdAt => $composableBuilder( + column: $table.createdAt, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get updatedAt => $composableBuilder( + column: $table.updatedAt, + builder: (column) => ColumnFilters(column), + ); + + Expression productsRefs( + Expression Function($$ProductsTableFilterComposer f) f, + ) { + final $$ProductsTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.localId, + referencedTable: $db.products, + getReferencedColumn: (t) => t.categoryId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$ProductsTableFilterComposer( + $db: $db, + $table: $db.products, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return f(composer); + } +} + +class $$CategoriesTableOrderingComposer + extends Composer<_$AppDatabase, $CategoriesTable> { + $$CategoriesTableOrderingComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnOrderings get localId => $composableBuilder( + column: $table.localId, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get serverId => $composableBuilder( + column: $table.serverId, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get name => $composableBuilder( + column: $table.name, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get localImagePath => $composableBuilder( + column: $table.localImagePath, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get serverImageUrl => $composableBuilder( + column: $table.serverImageUrl, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get syncStatus => $composableBuilder( + column: $table.syncStatus, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get createdAt => $composableBuilder( + column: $table.createdAt, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get updatedAt => $composableBuilder( + column: $table.updatedAt, + builder: (column) => ColumnOrderings(column), + ); +} + +class $$CategoriesTableAnnotationComposer + extends Composer<_$AppDatabase, $CategoriesTable> { + $$CategoriesTableAnnotationComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + GeneratedColumn get localId => + $composableBuilder(column: $table.localId, builder: (column) => column); + + GeneratedColumn get serverId => + $composableBuilder(column: $table.serverId, builder: (column) => column); + + GeneratedColumn get name => + $composableBuilder(column: $table.name, builder: (column) => column); + + GeneratedColumn get localImagePath => $composableBuilder( + column: $table.localImagePath, + builder: (column) => column, + ); + + GeneratedColumn get serverImageUrl => $composableBuilder( + column: $table.serverImageUrl, + builder: (column) => column, + ); + + GeneratedColumn get syncStatus => $composableBuilder( + column: $table.syncStatus, + builder: (column) => column, + ); + + GeneratedColumn get createdAt => + $composableBuilder(column: $table.createdAt, builder: (column) => column); + + GeneratedColumn get updatedAt => + $composableBuilder(column: $table.updatedAt, builder: (column) => column); + + Expression productsRefs( + Expression Function($$ProductsTableAnnotationComposer a) f, + ) { + final $$ProductsTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.localId, + referencedTable: $db.products, + getReferencedColumn: (t) => t.categoryId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$ProductsTableAnnotationComposer( + $db: $db, + $table: $db.products, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return f(composer); + } +} + +class $$CategoriesTableTableManager + extends + RootTableManager< + _$AppDatabase, + $CategoriesTable, + Category, + $$CategoriesTableFilterComposer, + $$CategoriesTableOrderingComposer, + $$CategoriesTableAnnotationComposer, + $$CategoriesTableCreateCompanionBuilder, + $$CategoriesTableUpdateCompanionBuilder, + (Category, $$CategoriesTableReferences), + Category, + PrefetchHooks Function({bool productsRefs}) + > { + $$CategoriesTableTableManager(_$AppDatabase db, $CategoriesTable table) + : super( + TableManagerState( + db: db, + table: table, + createFilteringComposer: () => + $$CategoriesTableFilterComposer($db: db, $table: table), + createOrderingComposer: () => + $$CategoriesTableOrderingComposer($db: db, $table: table), + createComputedFieldComposer: () => + $$CategoriesTableAnnotationComposer($db: db, $table: table), + updateCompanionCallback: + ({ + Value localId = const Value.absent(), + Value serverId = const Value.absent(), + Value name = const Value.absent(), + Value localImagePath = const Value.absent(), + Value serverImageUrl = const Value.absent(), + Value syncStatus = const Value.absent(), + Value createdAt = const Value.absent(), + Value updatedAt = const Value.absent(), + Value rowid = const Value.absent(), + }) => CategoriesCompanion( + localId: localId, + serverId: serverId, + name: name, + localImagePath: localImagePath, + serverImageUrl: serverImageUrl, + syncStatus: syncStatus, + createdAt: createdAt, + updatedAt: updatedAt, + rowid: rowid, + ), + createCompanionCallback: + ({ + Value localId = const Value.absent(), + Value serverId = const Value.absent(), + required String name, + Value localImagePath = const Value.absent(), + Value serverImageUrl = const Value.absent(), + Value syncStatus = const Value.absent(), + Value createdAt = const Value.absent(), + Value updatedAt = const Value.absent(), + Value rowid = const Value.absent(), + }) => CategoriesCompanion.insert( + localId: localId, + serverId: serverId, + name: name, + localImagePath: localImagePath, + serverImageUrl: serverImageUrl, + syncStatus: syncStatus, + createdAt: createdAt, + updatedAt: updatedAt, + rowid: rowid, + ), + withReferenceMapper: (p0) => p0 + .map( + (e) => ( + e.readTable(table), + $$CategoriesTableReferences(db, table, e), + ), + ) + .toList(), + prefetchHooksCallback: ({productsRefs = false}) { + return PrefetchHooks( + db: db, + explicitlyWatchedTables: [if (productsRefs) db.products], + addJoins: null, + getPrefetchedDataCallback: (items) async { + return [ + if (productsRefs) + await $_getPrefetchedData< + Category, + $CategoriesTable, + Product + >( + currentTable: table, + referencedTable: $$CategoriesTableReferences + ._productsRefsTable(db), + managerFromTypedResult: (p0) => + $$CategoriesTableReferences( + db, + table, + p0, + ).productsRefs, + referencedItemsForCurrentItem: (item, referencedItems) => + referencedItems.where( + (e) => e.categoryId == item.localId, + ), + typedResults: items, + ), + ]; + }, + ); + }, + ), + ); +} + +typedef $$CategoriesTableProcessedTableManager = + ProcessedTableManager< + _$AppDatabase, + $CategoriesTable, + Category, + $$CategoriesTableFilterComposer, + $$CategoriesTableOrderingComposer, + $$CategoriesTableAnnotationComposer, + $$CategoriesTableCreateCompanionBuilder, + $$CategoriesTableUpdateCompanionBuilder, + (Category, $$CategoriesTableReferences), + Category, + PrefetchHooks Function({bool productsRefs}) + >; +typedef $$UnitsTableCreateCompanionBuilder = + UnitsCompanion Function({ + Value localId, + Value serverId, + required String name, + Value syncStatus, + Value createdAt, + Value updatedAt, + Value rowid, + }); +typedef $$UnitsTableUpdateCompanionBuilder = + UnitsCompanion Function({ + Value localId, + Value serverId, + Value name, + Value syncStatus, + Value createdAt, + Value updatedAt, + Value rowid, + }); + +final class $$UnitsTableReferences + extends BaseReferences<_$AppDatabase, $UnitsTable, Unit> { + $$UnitsTableReferences(super.$_db, super.$_table, super.$_typedResult); + + static MultiTypedResultKey<$ProductsTable, List> _productsRefsTable( + _$AppDatabase db, + ) => MultiTypedResultKey.fromTable( + db.products, + aliasName: $_aliasNameGenerator(db.units.localId, db.products.unitId), + ); + + $$ProductsTableProcessedTableManager get productsRefs { + final manager = $$ProductsTableTableManager($_db, $_db.products).filter( + (f) => f.unitId.localId.sqlEquals($_itemColumn('local_id')!), + ); + + final cache = $_typedResult.readTableOrNull(_productsRefsTable($_db)); + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: cache), + ); + } + + static MultiTypedResultKey<$RawMaterialsTable, List> + _rawMaterialsRefsTable(_$AppDatabase db) => MultiTypedResultKey.fromTable( + db.rawMaterials, + aliasName: $_aliasNameGenerator(db.units.localId, db.rawMaterials.unitId), + ); + + $$RawMaterialsTableProcessedTableManager get rawMaterialsRefs { + final manager = $$RawMaterialsTableTableManager($_db, $_db.rawMaterials) + .filter( + (f) => f.unitId.localId.sqlEquals($_itemColumn('local_id')!), + ); + + final cache = $_typedResult.readTableOrNull(_rawMaterialsRefsTable($_db)); + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: cache), + ); + } +} + +class $$UnitsTableFilterComposer extends Composer<_$AppDatabase, $UnitsTable> { + $$UnitsTableFilterComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnFilters get localId => $composableBuilder( + column: $table.localId, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get serverId => $composableBuilder( + column: $table.serverId, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get name => $composableBuilder( + column: $table.name, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get syncStatus => $composableBuilder( + column: $table.syncStatus, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get createdAt => $composableBuilder( + column: $table.createdAt, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get updatedAt => $composableBuilder( + column: $table.updatedAt, + builder: (column) => ColumnFilters(column), + ); + + Expression productsRefs( + Expression Function($$ProductsTableFilterComposer f) f, + ) { + final $$ProductsTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.localId, + referencedTable: $db.products, + getReferencedColumn: (t) => t.unitId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$ProductsTableFilterComposer( + $db: $db, + $table: $db.products, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return f(composer); + } + + Expression rawMaterialsRefs( + Expression Function($$RawMaterialsTableFilterComposer f) f, + ) { + final $$RawMaterialsTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.localId, + referencedTable: $db.rawMaterials, + getReferencedColumn: (t) => t.unitId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$RawMaterialsTableFilterComposer( + $db: $db, + $table: $db.rawMaterials, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return f(composer); + } +} + +class $$UnitsTableOrderingComposer + extends Composer<_$AppDatabase, $UnitsTable> { + $$UnitsTableOrderingComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnOrderings get localId => $composableBuilder( + column: $table.localId, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get serverId => $composableBuilder( + column: $table.serverId, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get name => $composableBuilder( + column: $table.name, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get syncStatus => $composableBuilder( + column: $table.syncStatus, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get createdAt => $composableBuilder( + column: $table.createdAt, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get updatedAt => $composableBuilder( + column: $table.updatedAt, + builder: (column) => ColumnOrderings(column), + ); +} + +class $$UnitsTableAnnotationComposer + extends Composer<_$AppDatabase, $UnitsTable> { + $$UnitsTableAnnotationComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + GeneratedColumn get localId => + $composableBuilder(column: $table.localId, builder: (column) => column); + + GeneratedColumn get serverId => + $composableBuilder(column: $table.serverId, builder: (column) => column); + + GeneratedColumn get name => + $composableBuilder(column: $table.name, builder: (column) => column); + + GeneratedColumn get syncStatus => $composableBuilder( + column: $table.syncStatus, + builder: (column) => column, + ); + + GeneratedColumn get createdAt => + $composableBuilder(column: $table.createdAt, builder: (column) => column); + + GeneratedColumn get updatedAt => + $composableBuilder(column: $table.updatedAt, builder: (column) => column); + + Expression productsRefs( + Expression Function($$ProductsTableAnnotationComposer a) f, + ) { + final $$ProductsTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.localId, + referencedTable: $db.products, + getReferencedColumn: (t) => t.unitId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$ProductsTableAnnotationComposer( + $db: $db, + $table: $db.products, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return f(composer); + } + + Expression rawMaterialsRefs( + Expression Function($$RawMaterialsTableAnnotationComposer a) f, + ) { + final $$RawMaterialsTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.localId, + referencedTable: $db.rawMaterials, + getReferencedColumn: (t) => t.unitId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$RawMaterialsTableAnnotationComposer( + $db: $db, + $table: $db.rawMaterials, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return f(composer); + } +} + +class $$UnitsTableTableManager + extends + RootTableManager< + _$AppDatabase, + $UnitsTable, + Unit, + $$UnitsTableFilterComposer, + $$UnitsTableOrderingComposer, + $$UnitsTableAnnotationComposer, + $$UnitsTableCreateCompanionBuilder, + $$UnitsTableUpdateCompanionBuilder, + (Unit, $$UnitsTableReferences), + Unit, + PrefetchHooks Function({bool productsRefs, bool rawMaterialsRefs}) + > { + $$UnitsTableTableManager(_$AppDatabase db, $UnitsTable table) + : super( + TableManagerState( + db: db, + table: table, + createFilteringComposer: () => + $$UnitsTableFilterComposer($db: db, $table: table), + createOrderingComposer: () => + $$UnitsTableOrderingComposer($db: db, $table: table), + createComputedFieldComposer: () => + $$UnitsTableAnnotationComposer($db: db, $table: table), + updateCompanionCallback: + ({ + Value localId = const Value.absent(), + Value serverId = const Value.absent(), + Value name = const Value.absent(), + Value syncStatus = const Value.absent(), + Value createdAt = const Value.absent(), + Value updatedAt = const Value.absent(), + Value rowid = const Value.absent(), + }) => UnitsCompanion( + localId: localId, + serverId: serverId, + name: name, + syncStatus: syncStatus, + createdAt: createdAt, + updatedAt: updatedAt, + rowid: rowid, + ), + createCompanionCallback: + ({ + Value localId = const Value.absent(), + Value serverId = const Value.absent(), + required String name, + Value syncStatus = const Value.absent(), + Value createdAt = const Value.absent(), + Value updatedAt = const Value.absent(), + Value rowid = const Value.absent(), + }) => UnitsCompanion.insert( + localId: localId, + serverId: serverId, + name: name, + syncStatus: syncStatus, + createdAt: createdAt, + updatedAt: updatedAt, + rowid: rowid, + ), + withReferenceMapper: (p0) => p0 + .map( + (e) => + (e.readTable(table), $$UnitsTableReferences(db, table, e)), + ) + .toList(), + prefetchHooksCallback: + ({productsRefs = false, rawMaterialsRefs = false}) { + return PrefetchHooks( + db: db, + explicitlyWatchedTables: [ + if (productsRefs) db.products, + if (rawMaterialsRefs) db.rawMaterials, + ], + addJoins: null, + getPrefetchedDataCallback: (items) async { + return [ + if (productsRefs) + await $_getPrefetchedData( + currentTable: table, + referencedTable: $$UnitsTableReferences + ._productsRefsTable(db), + managerFromTypedResult: (p0) => + $$UnitsTableReferences( + db, + table, + p0, + ).productsRefs, + referencedItemsForCurrentItem: + (item, referencedItems) => referencedItems.where( + (e) => e.unitId == item.localId, + ), + typedResults: items, + ), + if (rawMaterialsRefs) + await $_getPrefetchedData< + Unit, + $UnitsTable, + RawMaterial + >( + currentTable: table, + referencedTable: $$UnitsTableReferences + ._rawMaterialsRefsTable(db), + managerFromTypedResult: (p0) => + $$UnitsTableReferences( + db, + table, + p0, + ).rawMaterialsRefs, + referencedItemsForCurrentItem: + (item, referencedItems) => referencedItems.where( + (e) => e.unitId == item.localId, + ), + typedResults: items, + ), + ]; + }, + ); + }, + ), + ); +} + +typedef $$UnitsTableProcessedTableManager = + ProcessedTableManager< + _$AppDatabase, + $UnitsTable, + Unit, + $$UnitsTableFilterComposer, + $$UnitsTableOrderingComposer, + $$UnitsTableAnnotationComposer, + $$UnitsTableCreateCompanionBuilder, + $$UnitsTableUpdateCompanionBuilder, + (Unit, $$UnitsTableReferences), + Unit, + PrefetchHooks Function({bool productsRefs, bool rawMaterialsRefs}) + >; +typedef $$ProductsTableCreateCompanionBuilder = + ProductsCompanion Function({ + Value localId, + Value serverId, + required String categoryId, + required String unitId, + required String name, + Value?> localImagePath, + Value?> serverImageUrl, + Value hasVariant, + Value syncStatus, + Value createdAt, + Value updatedAt, + Value deletedAt, + Value rowid, + }); +typedef $$ProductsTableUpdateCompanionBuilder = + ProductsCompanion Function({ + Value localId, + Value serverId, + Value categoryId, + Value unitId, + Value name, + Value?> localImagePath, + Value?> serverImageUrl, + Value hasVariant, + Value syncStatus, + Value createdAt, + Value updatedAt, + Value deletedAt, + Value rowid, + }); + +final class $$ProductsTableReferences + extends BaseReferences<_$AppDatabase, $ProductsTable, Product> { + $$ProductsTableReferences(super.$_db, super.$_table, super.$_typedResult); + + static $CategoriesTable _categoryIdTable(_$AppDatabase db) => + db.categories.createAlias( + $_aliasNameGenerator(db.products.categoryId, db.categories.localId), + ); + + $$CategoriesTableProcessedTableManager get categoryId { + final $_column = $_itemColumn('category_id')!; + + final manager = $$CategoriesTableTableManager( + $_db, + $_db.categories, + ).filter((f) => f.localId.sqlEquals($_column)); + final item = $_typedResult.readTableOrNull(_categoryIdTable($_db)); + if (item == null) return manager; + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: [item]), + ); + } + + static $UnitsTable _unitIdTable(_$AppDatabase db) => db.units.createAlias( + $_aliasNameGenerator(db.products.unitId, db.units.localId), + ); + + $$UnitsTableProcessedTableManager get unitId { + final $_column = $_itemColumn('unit_id')!; + + final manager = $$UnitsTableTableManager( + $_db, + $_db.units, + ).filter((f) => f.localId.sqlEquals($_column)); + final item = $_typedResult.readTableOrNull(_unitIdTable($_db)); + if (item == null) return manager; + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: [item]), + ); + } + + static MultiTypedResultKey<$ProductVariantsTable, List> + _productVariantsRefsTable(_$AppDatabase db) => MultiTypedResultKey.fromTable( + db.productVariants, + aliasName: $_aliasNameGenerator( + db.products.localId, + db.productVariants.productId, + ), + ); + + $$ProductVariantsTableProcessedTableManager get productVariantsRefs { + final manager = + $$ProductVariantsTableTableManager($_db, $_db.productVariants).filter( + (f) => + f.productId.localId.sqlEquals($_itemColumn('local_id')!), + ); + + final cache = $_typedResult.readTableOrNull( + _productVariantsRefsTable($_db), + ); + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: cache), + ); + } + + static MultiTypedResultKey<$ProductLikesTable, List> + _productLikesRefsTable(_$AppDatabase db) => MultiTypedResultKey.fromTable( + db.productLikes, + aliasName: $_aliasNameGenerator( + db.products.localId, + db.productLikes.productId, + ), + ); + + $$ProductLikesTableProcessedTableManager get productLikesRefs { + final manager = $$ProductLikesTableTableManager($_db, $_db.productLikes) + .filter( + (f) => + f.productId.localId.sqlEquals($_itemColumn('local_id')!), + ); + + final cache = $_typedResult.readTableOrNull(_productLikesRefsTable($_db)); + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: cache), + ); + } +} + +class $$ProductsTableFilterComposer + extends Composer<_$AppDatabase, $ProductsTable> { + $$ProductsTableFilterComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnFilters get localId => $composableBuilder( + column: $table.localId, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get serverId => $composableBuilder( + column: $table.serverId, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get name => $composableBuilder( + column: $table.name, + builder: (column) => ColumnFilters(column), + ); + + ColumnWithTypeConverterFilters?, List, String> + get localImagePath => $composableBuilder( + column: $table.localImagePath, + builder: (column) => ColumnWithTypeConverterFilters(column), + ); + + ColumnWithTypeConverterFilters?, List, String> + get serverImageUrl => $composableBuilder( + column: $table.serverImageUrl, + builder: (column) => ColumnWithTypeConverterFilters(column), + ); + + ColumnFilters get hasVariant => $composableBuilder( + column: $table.hasVariant, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get syncStatus => $composableBuilder( + column: $table.syncStatus, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get createdAt => $composableBuilder( + column: $table.createdAt, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get updatedAt => $composableBuilder( + column: $table.updatedAt, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get deletedAt => $composableBuilder( + column: $table.deletedAt, + builder: (column) => ColumnFilters(column), + ); + + $$CategoriesTableFilterComposer get categoryId { + final $$CategoriesTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.categoryId, + referencedTable: $db.categories, + getReferencedColumn: (t) => t.localId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$CategoriesTableFilterComposer( + $db: $db, + $table: $db.categories, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } + + $$UnitsTableFilterComposer get unitId { + final $$UnitsTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.unitId, + referencedTable: $db.units, + getReferencedColumn: (t) => t.localId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$UnitsTableFilterComposer( + $db: $db, + $table: $db.units, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } + + Expression productVariantsRefs( + Expression Function($$ProductVariantsTableFilterComposer f) f, + ) { + final $$ProductVariantsTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.localId, + referencedTable: $db.productVariants, + getReferencedColumn: (t) => t.productId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$ProductVariantsTableFilterComposer( + $db: $db, + $table: $db.productVariants, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return f(composer); + } + + Expression productLikesRefs( + Expression Function($$ProductLikesTableFilterComposer f) f, + ) { + final $$ProductLikesTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.localId, + referencedTable: $db.productLikes, + getReferencedColumn: (t) => t.productId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$ProductLikesTableFilterComposer( + $db: $db, + $table: $db.productLikes, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return f(composer); + } +} + +class $$ProductsTableOrderingComposer + extends Composer<_$AppDatabase, $ProductsTable> { + $$ProductsTableOrderingComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnOrderings get localId => $composableBuilder( + column: $table.localId, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get serverId => $composableBuilder( + column: $table.serverId, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get name => $composableBuilder( + column: $table.name, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get localImagePath => $composableBuilder( + column: $table.localImagePath, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get serverImageUrl => $composableBuilder( + column: $table.serverImageUrl, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get hasVariant => $composableBuilder( + column: $table.hasVariant, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get syncStatus => $composableBuilder( + column: $table.syncStatus, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get createdAt => $composableBuilder( + column: $table.createdAt, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get updatedAt => $composableBuilder( + column: $table.updatedAt, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get deletedAt => $composableBuilder( + column: $table.deletedAt, + builder: (column) => ColumnOrderings(column), + ); + + $$CategoriesTableOrderingComposer get categoryId { + final $$CategoriesTableOrderingComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.categoryId, + referencedTable: $db.categories, + getReferencedColumn: (t) => t.localId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$CategoriesTableOrderingComposer( + $db: $db, + $table: $db.categories, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } + + $$UnitsTableOrderingComposer get unitId { + final $$UnitsTableOrderingComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.unitId, + referencedTable: $db.units, + getReferencedColumn: (t) => t.localId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$UnitsTableOrderingComposer( + $db: $db, + $table: $db.units, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } +} + +class $$ProductsTableAnnotationComposer + extends Composer<_$AppDatabase, $ProductsTable> { + $$ProductsTableAnnotationComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + GeneratedColumn get localId => + $composableBuilder(column: $table.localId, builder: (column) => column); + + GeneratedColumn get serverId => + $composableBuilder(column: $table.serverId, builder: (column) => column); + + GeneratedColumn get name => + $composableBuilder(column: $table.name, builder: (column) => column); + + GeneratedColumnWithTypeConverter?, String> get localImagePath => + $composableBuilder( + column: $table.localImagePath, + builder: (column) => column, + ); + + GeneratedColumnWithTypeConverter?, String> get serverImageUrl => + $composableBuilder( + column: $table.serverImageUrl, + builder: (column) => column, + ); + + GeneratedColumn get hasVariant => $composableBuilder( + column: $table.hasVariant, + builder: (column) => column, + ); + + GeneratedColumn get syncStatus => $composableBuilder( + column: $table.syncStatus, + builder: (column) => column, + ); + + GeneratedColumn get createdAt => + $composableBuilder(column: $table.createdAt, builder: (column) => column); + + GeneratedColumn get updatedAt => + $composableBuilder(column: $table.updatedAt, builder: (column) => column); + + GeneratedColumn get deletedAt => + $composableBuilder(column: $table.deletedAt, builder: (column) => column); + + $$CategoriesTableAnnotationComposer get categoryId { + final $$CategoriesTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.categoryId, + referencedTable: $db.categories, + getReferencedColumn: (t) => t.localId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$CategoriesTableAnnotationComposer( + $db: $db, + $table: $db.categories, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } + + $$UnitsTableAnnotationComposer get unitId { + final $$UnitsTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.unitId, + referencedTable: $db.units, + getReferencedColumn: (t) => t.localId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$UnitsTableAnnotationComposer( + $db: $db, + $table: $db.units, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } + + Expression productVariantsRefs( + Expression Function($$ProductVariantsTableAnnotationComposer a) f, + ) { + final $$ProductVariantsTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.localId, + referencedTable: $db.productVariants, + getReferencedColumn: (t) => t.productId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$ProductVariantsTableAnnotationComposer( + $db: $db, + $table: $db.productVariants, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return f(composer); + } + + Expression productLikesRefs( + Expression Function($$ProductLikesTableAnnotationComposer a) f, + ) { + final $$ProductLikesTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.localId, + referencedTable: $db.productLikes, + getReferencedColumn: (t) => t.productId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$ProductLikesTableAnnotationComposer( + $db: $db, + $table: $db.productLikes, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return f(composer); + } +} + +class $$ProductsTableTableManager + extends + RootTableManager< + _$AppDatabase, + $ProductsTable, + Product, + $$ProductsTableFilterComposer, + $$ProductsTableOrderingComposer, + $$ProductsTableAnnotationComposer, + $$ProductsTableCreateCompanionBuilder, + $$ProductsTableUpdateCompanionBuilder, + (Product, $$ProductsTableReferences), + Product, + PrefetchHooks Function({ + bool categoryId, + bool unitId, + bool productVariantsRefs, + bool productLikesRefs, + }) + > { + $$ProductsTableTableManager(_$AppDatabase db, $ProductsTable table) + : super( + TableManagerState( + db: db, + table: table, + createFilteringComposer: () => + $$ProductsTableFilterComposer($db: db, $table: table), + createOrderingComposer: () => + $$ProductsTableOrderingComposer($db: db, $table: table), + createComputedFieldComposer: () => + $$ProductsTableAnnotationComposer($db: db, $table: table), + updateCompanionCallback: + ({ + Value localId = const Value.absent(), + Value serverId = const Value.absent(), + Value categoryId = const Value.absent(), + Value unitId = const Value.absent(), + Value name = const Value.absent(), + Value?> localImagePath = const Value.absent(), + Value?> serverImageUrl = const Value.absent(), + Value hasVariant = const Value.absent(), + Value syncStatus = const Value.absent(), + Value createdAt = const Value.absent(), + Value updatedAt = const Value.absent(), + Value deletedAt = const Value.absent(), + Value rowid = const Value.absent(), + }) => ProductsCompanion( + localId: localId, + serverId: serverId, + categoryId: categoryId, + unitId: unitId, + name: name, + localImagePath: localImagePath, + serverImageUrl: serverImageUrl, + hasVariant: hasVariant, + syncStatus: syncStatus, + createdAt: createdAt, + updatedAt: updatedAt, + deletedAt: deletedAt, + rowid: rowid, + ), + createCompanionCallback: + ({ + Value localId = const Value.absent(), + Value serverId = const Value.absent(), + required String categoryId, + required String unitId, + required String name, + Value?> localImagePath = const Value.absent(), + Value?> serverImageUrl = const Value.absent(), + Value hasVariant = const Value.absent(), + Value syncStatus = const Value.absent(), + Value createdAt = const Value.absent(), + Value updatedAt = const Value.absent(), + Value deletedAt = const Value.absent(), + Value rowid = const Value.absent(), + }) => ProductsCompanion.insert( + localId: localId, + serverId: serverId, + categoryId: categoryId, + unitId: unitId, + name: name, + localImagePath: localImagePath, + serverImageUrl: serverImageUrl, + hasVariant: hasVariant, + syncStatus: syncStatus, + createdAt: createdAt, + updatedAt: updatedAt, + deletedAt: deletedAt, + rowid: rowid, + ), + withReferenceMapper: (p0) => p0 + .map( + (e) => ( + e.readTable(table), + $$ProductsTableReferences(db, table, e), + ), + ) + .toList(), + prefetchHooksCallback: + ({ + categoryId = false, + unitId = false, + productVariantsRefs = false, + productLikesRefs = false, + }) { + return PrefetchHooks( + db: db, + explicitlyWatchedTables: [ + if (productVariantsRefs) db.productVariants, + if (productLikesRefs) db.productLikes, + ], + addJoins: + < + T extends TableManagerState< + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic + > + >(state) { + if (categoryId) { + state = + state.withJoin( + currentTable: table, + currentColumn: table.categoryId, + referencedTable: $$ProductsTableReferences + ._categoryIdTable(db), + referencedColumn: $$ProductsTableReferences + ._categoryIdTable(db) + .localId, + ) + as T; + } + if (unitId) { + state = + state.withJoin( + currentTable: table, + currentColumn: table.unitId, + referencedTable: $$ProductsTableReferences + ._unitIdTable(db), + referencedColumn: $$ProductsTableReferences + ._unitIdTable(db) + .localId, + ) + as T; + } + + return state; + }, + getPrefetchedDataCallback: (items) async { + return [ + if (productVariantsRefs) + await $_getPrefetchedData< + Product, + $ProductsTable, + ProductVariant + >( + currentTable: table, + referencedTable: $$ProductsTableReferences + ._productVariantsRefsTable(db), + managerFromTypedResult: (p0) => + $$ProductsTableReferences( + db, + table, + p0, + ).productVariantsRefs, + referencedItemsForCurrentItem: + (item, referencedItems) => referencedItems.where( + (e) => e.productId == item.localId, + ), + typedResults: items, + ), + if (productLikesRefs) + await $_getPrefetchedData< + Product, + $ProductsTable, + ProductLike + >( + currentTable: table, + referencedTable: $$ProductsTableReferences + ._productLikesRefsTable(db), + managerFromTypedResult: (p0) => + $$ProductsTableReferences( + db, + table, + p0, + ).productLikesRefs, + referencedItemsForCurrentItem: + (item, referencedItems) => referencedItems.where( + (e) => e.productId == item.localId, + ), + typedResults: items, + ), + ]; + }, + ); + }, + ), + ); +} + +typedef $$ProductsTableProcessedTableManager = + ProcessedTableManager< + _$AppDatabase, + $ProductsTable, + Product, + $$ProductsTableFilterComposer, + $$ProductsTableOrderingComposer, + $$ProductsTableAnnotationComposer, + $$ProductsTableCreateCompanionBuilder, + $$ProductsTableUpdateCompanionBuilder, + (Product, $$ProductsTableReferences), + Product, + PrefetchHooks Function({ + bool categoryId, + bool unitId, + bool productVariantsRefs, + bool productLikesRefs, + }) + >; +typedef $$ProductVariantsTableCreateCompanionBuilder = + ProductVariantsCompanion Function({ + Value localId, + Value serverId, + required String productId, + required String name, + Value localImagePath, + Value serverImageUrl, + Value isVisibleOnline, + Value totalRating, + Value averageRating, + Value totalSold, + Value isProductVariant, + Value syncStatus, + Value createdAt, + Value updatedAt, + Value deletedAt, + Value rowid, + }); +typedef $$ProductVariantsTableUpdateCompanionBuilder = + ProductVariantsCompanion Function({ + Value localId, + Value serverId, + Value productId, + Value name, + Value localImagePath, + Value serverImageUrl, + Value isVisibleOnline, + Value totalRating, + Value averageRating, + Value totalSold, + Value isProductVariant, + Value syncStatus, + Value createdAt, + Value updatedAt, + Value deletedAt, + Value rowid, + }); + +final class $$ProductVariantsTableReferences + extends + BaseReferences<_$AppDatabase, $ProductVariantsTable, ProductVariant> { + $$ProductVariantsTableReferences( + super.$_db, + super.$_table, + super.$_typedResult, + ); + + static $ProductsTable _productIdTable(_$AppDatabase db) => + db.products.createAlias( + $_aliasNameGenerator(db.productVariants.productId, db.products.localId), + ); + + $$ProductsTableProcessedTableManager get productId { + final $_column = $_itemColumn('product_id')!; + + final manager = $$ProductsTableTableManager( + $_db, + $_db.products, + ).filter((f) => f.localId.sqlEquals($_column)); + final item = $_typedResult.readTableOrNull(_productIdTable($_db)); + if (item == null) return manager; + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: [item]), + ); + } + + static MultiTypedResultKey<$ProductReviewsTable, List> + _productReviewsRefsTable(_$AppDatabase db) => MultiTypedResultKey.fromTable( + db.productReviews, + aliasName: $_aliasNameGenerator( + db.productVariants.localId, + db.productReviews.productVariantId, + ), + ); + + $$ProductReviewsTableProcessedTableManager get productReviewsRefs { + final manager = $$ProductReviewsTableTableManager($_db, $_db.productReviews) + .filter( + (f) => f.productVariantId.localId.sqlEquals( + $_itemColumn('local_id')!, + ), + ); + + final cache = $_typedResult.readTableOrNull(_productReviewsRefsTable($_db)); + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: cache), + ); + } + + static MultiTypedResultKey<$ReceiptItemsTable, List> + _receiptItemsRefsTable(_$AppDatabase db) => MultiTypedResultKey.fromTable( + db.receiptItems, + aliasName: $_aliasNameGenerator( + db.productVariants.localId, + db.receiptItems.productVariantId, + ), + ); + + $$ReceiptItemsTableProcessedTableManager get receiptItemsRefs { + final manager = $$ReceiptItemsTableTableManager($_db, $_db.receiptItems) + .filter( + (f) => f.productVariantId.localId.sqlEquals( + $_itemColumn('local_id')!, + ), + ); + + final cache = $_typedResult.readTableOrNull(_receiptItemsRefsTable($_db)); + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: cache), + ); + } + + static MultiTypedResultKey<$OrderItemsTable, List> + _orderItemsRefsTable(_$AppDatabase db) => MultiTypedResultKey.fromTable( + db.orderItems, + aliasName: $_aliasNameGenerator( + db.productVariants.localId, + db.orderItems.productVariantId, + ), + ); + + $$OrderItemsTableProcessedTableManager get orderItemsRefs { + final manager = $$OrderItemsTableTableManager($_db, $_db.orderItems).filter( + (f) => f.productVariantId.localId.sqlEquals( + $_itemColumn('local_id')!, + ), + ); + + final cache = $_typedResult.readTableOrNull(_orderItemsRefsTable($_db)); + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: cache), + ); + } +} + +class $$ProductVariantsTableFilterComposer + extends Composer<_$AppDatabase, $ProductVariantsTable> { + $$ProductVariantsTableFilterComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnFilters get localId => $composableBuilder( + column: $table.localId, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get serverId => $composableBuilder( + column: $table.serverId, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get name => $composableBuilder( + column: $table.name, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get localImagePath => $composableBuilder( + column: $table.localImagePath, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get serverImageUrl => $composableBuilder( + column: $table.serverImageUrl, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get isVisibleOnline => $composableBuilder( + column: $table.isVisibleOnline, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get totalRating => $composableBuilder( + column: $table.totalRating, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get averageRating => $composableBuilder( + column: $table.averageRating, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get totalSold => $composableBuilder( + column: $table.totalSold, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get isProductVariant => $composableBuilder( + column: $table.isProductVariant, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get syncStatus => $composableBuilder( + column: $table.syncStatus, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get createdAt => $composableBuilder( + column: $table.createdAt, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get updatedAt => $composableBuilder( + column: $table.updatedAt, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get deletedAt => $composableBuilder( + column: $table.deletedAt, + builder: (column) => ColumnFilters(column), + ); + + $$ProductsTableFilterComposer get productId { + final $$ProductsTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.productId, + referencedTable: $db.products, + getReferencedColumn: (t) => t.localId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$ProductsTableFilterComposer( + $db: $db, + $table: $db.products, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } + + Expression productReviewsRefs( + Expression Function($$ProductReviewsTableFilterComposer f) f, + ) { + final $$ProductReviewsTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.localId, + referencedTable: $db.productReviews, + getReferencedColumn: (t) => t.productVariantId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$ProductReviewsTableFilterComposer( + $db: $db, + $table: $db.productReviews, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return f(composer); + } + + Expression receiptItemsRefs( + Expression Function($$ReceiptItemsTableFilterComposer f) f, + ) { + final $$ReceiptItemsTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.localId, + referencedTable: $db.receiptItems, + getReferencedColumn: (t) => t.productVariantId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$ReceiptItemsTableFilterComposer( + $db: $db, + $table: $db.receiptItems, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return f(composer); + } + + Expression orderItemsRefs( + Expression Function($$OrderItemsTableFilterComposer f) f, + ) { + final $$OrderItemsTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.localId, + referencedTable: $db.orderItems, + getReferencedColumn: (t) => t.productVariantId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$OrderItemsTableFilterComposer( + $db: $db, + $table: $db.orderItems, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return f(composer); + } +} + +class $$ProductVariantsTableOrderingComposer + extends Composer<_$AppDatabase, $ProductVariantsTable> { + $$ProductVariantsTableOrderingComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnOrderings get localId => $composableBuilder( + column: $table.localId, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get serverId => $composableBuilder( + column: $table.serverId, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get name => $composableBuilder( + column: $table.name, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get localImagePath => $composableBuilder( + column: $table.localImagePath, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get serverImageUrl => $composableBuilder( + column: $table.serverImageUrl, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get isVisibleOnline => $composableBuilder( + column: $table.isVisibleOnline, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get totalRating => $composableBuilder( + column: $table.totalRating, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get averageRating => $composableBuilder( + column: $table.averageRating, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get totalSold => $composableBuilder( + column: $table.totalSold, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get isProductVariant => $composableBuilder( + column: $table.isProductVariant, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get syncStatus => $composableBuilder( + column: $table.syncStatus, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get createdAt => $composableBuilder( + column: $table.createdAt, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get updatedAt => $composableBuilder( + column: $table.updatedAt, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get deletedAt => $composableBuilder( + column: $table.deletedAt, + builder: (column) => ColumnOrderings(column), + ); + + $$ProductsTableOrderingComposer get productId { + final $$ProductsTableOrderingComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.productId, + referencedTable: $db.products, + getReferencedColumn: (t) => t.localId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$ProductsTableOrderingComposer( + $db: $db, + $table: $db.products, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } +} + +class $$ProductVariantsTableAnnotationComposer + extends Composer<_$AppDatabase, $ProductVariantsTable> { + $$ProductVariantsTableAnnotationComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + GeneratedColumn get localId => + $composableBuilder(column: $table.localId, builder: (column) => column); + + GeneratedColumn get serverId => + $composableBuilder(column: $table.serverId, builder: (column) => column); + + GeneratedColumn get name => + $composableBuilder(column: $table.name, builder: (column) => column); + + GeneratedColumn get localImagePath => $composableBuilder( + column: $table.localImagePath, + builder: (column) => column, + ); + + GeneratedColumn get serverImageUrl => $composableBuilder( + column: $table.serverImageUrl, + builder: (column) => column, + ); + + GeneratedColumn get isVisibleOnline => $composableBuilder( + column: $table.isVisibleOnline, + builder: (column) => column, + ); + + GeneratedColumn get totalRating => $composableBuilder( + column: $table.totalRating, + builder: (column) => column, + ); + + GeneratedColumn get averageRating => $composableBuilder( + column: $table.averageRating, + builder: (column) => column, + ); + + GeneratedColumn get totalSold => + $composableBuilder(column: $table.totalSold, builder: (column) => column); + + GeneratedColumn get isProductVariant => $composableBuilder( + column: $table.isProductVariant, + builder: (column) => column, + ); + + GeneratedColumn get syncStatus => $composableBuilder( + column: $table.syncStatus, + builder: (column) => column, + ); + + GeneratedColumn get createdAt => + $composableBuilder(column: $table.createdAt, builder: (column) => column); + + GeneratedColumn get updatedAt => + $composableBuilder(column: $table.updatedAt, builder: (column) => column); + + GeneratedColumn get deletedAt => + $composableBuilder(column: $table.deletedAt, builder: (column) => column); + + $$ProductsTableAnnotationComposer get productId { + final $$ProductsTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.productId, + referencedTable: $db.products, + getReferencedColumn: (t) => t.localId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$ProductsTableAnnotationComposer( + $db: $db, + $table: $db.products, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } + + Expression productReviewsRefs( + Expression Function($$ProductReviewsTableAnnotationComposer a) f, + ) { + final $$ProductReviewsTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.localId, + referencedTable: $db.productReviews, + getReferencedColumn: (t) => t.productVariantId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$ProductReviewsTableAnnotationComposer( + $db: $db, + $table: $db.productReviews, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return f(composer); + } + + Expression receiptItemsRefs( + Expression Function($$ReceiptItemsTableAnnotationComposer a) f, + ) { + final $$ReceiptItemsTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.localId, + referencedTable: $db.receiptItems, + getReferencedColumn: (t) => t.productVariantId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$ReceiptItemsTableAnnotationComposer( + $db: $db, + $table: $db.receiptItems, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return f(composer); + } + + Expression orderItemsRefs( + Expression Function($$OrderItemsTableAnnotationComposer a) f, + ) { + final $$OrderItemsTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.localId, + referencedTable: $db.orderItems, + getReferencedColumn: (t) => t.productVariantId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$OrderItemsTableAnnotationComposer( + $db: $db, + $table: $db.orderItems, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return f(composer); + } +} + +class $$ProductVariantsTableTableManager + extends + RootTableManager< + _$AppDatabase, + $ProductVariantsTable, + ProductVariant, + $$ProductVariantsTableFilterComposer, + $$ProductVariantsTableOrderingComposer, + $$ProductVariantsTableAnnotationComposer, + $$ProductVariantsTableCreateCompanionBuilder, + $$ProductVariantsTableUpdateCompanionBuilder, + (ProductVariant, $$ProductVariantsTableReferences), + ProductVariant, + PrefetchHooks Function({ + bool productId, + bool productReviewsRefs, + bool receiptItemsRefs, + bool orderItemsRefs, + }) + > { + $$ProductVariantsTableTableManager( + _$AppDatabase db, + $ProductVariantsTable table, + ) : super( + TableManagerState( + db: db, + table: table, + createFilteringComposer: () => + $$ProductVariantsTableFilterComposer($db: db, $table: table), + createOrderingComposer: () => + $$ProductVariantsTableOrderingComposer($db: db, $table: table), + createComputedFieldComposer: () => + $$ProductVariantsTableAnnotationComposer($db: db, $table: table), + updateCompanionCallback: + ({ + Value localId = const Value.absent(), + Value serverId = const Value.absent(), + Value productId = const Value.absent(), + Value name = const Value.absent(), + Value localImagePath = const Value.absent(), + Value serverImageUrl = const Value.absent(), + Value isVisibleOnline = const Value.absent(), + Value totalRating = const Value.absent(), + Value averageRating = const Value.absent(), + Value totalSold = const Value.absent(), + Value isProductVariant = const Value.absent(), + Value syncStatus = const Value.absent(), + Value createdAt = const Value.absent(), + Value updatedAt = const Value.absent(), + Value deletedAt = const Value.absent(), + Value rowid = const Value.absent(), + }) => ProductVariantsCompanion( + localId: localId, + serverId: serverId, + productId: productId, + name: name, + localImagePath: localImagePath, + serverImageUrl: serverImageUrl, + isVisibleOnline: isVisibleOnline, + totalRating: totalRating, + averageRating: averageRating, + totalSold: totalSold, + isProductVariant: isProductVariant, + syncStatus: syncStatus, + createdAt: createdAt, + updatedAt: updatedAt, + deletedAt: deletedAt, + rowid: rowid, + ), + createCompanionCallback: + ({ + Value localId = const Value.absent(), + Value serverId = const Value.absent(), + required String productId, + required String name, + Value localImagePath = const Value.absent(), + Value serverImageUrl = const Value.absent(), + Value isVisibleOnline = const Value.absent(), + Value totalRating = const Value.absent(), + Value averageRating = const Value.absent(), + Value totalSold = const Value.absent(), + Value isProductVariant = const Value.absent(), + Value syncStatus = const Value.absent(), + Value createdAt = const Value.absent(), + Value updatedAt = const Value.absent(), + Value deletedAt = const Value.absent(), + Value rowid = const Value.absent(), + }) => ProductVariantsCompanion.insert( + localId: localId, + serverId: serverId, + productId: productId, + name: name, + localImagePath: localImagePath, + serverImageUrl: serverImageUrl, + isVisibleOnline: isVisibleOnline, + totalRating: totalRating, + averageRating: averageRating, + totalSold: totalSold, + isProductVariant: isProductVariant, + syncStatus: syncStatus, + createdAt: createdAt, + updatedAt: updatedAt, + deletedAt: deletedAt, + rowid: rowid, + ), + withReferenceMapper: (p0) => p0 + .map( + (e) => ( + e.readTable(table), + $$ProductVariantsTableReferences(db, table, e), + ), + ) + .toList(), + prefetchHooksCallback: + ({ + productId = false, + productReviewsRefs = false, + receiptItemsRefs = false, + orderItemsRefs = false, + }) { + return PrefetchHooks( + db: db, + explicitlyWatchedTables: [ + if (productReviewsRefs) db.productReviews, + if (receiptItemsRefs) db.receiptItems, + if (orderItemsRefs) db.orderItems, + ], + addJoins: + < + T extends TableManagerState< + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic + > + >(state) { + if (productId) { + state = + state.withJoin( + currentTable: table, + currentColumn: table.productId, + referencedTable: + $$ProductVariantsTableReferences + ._productIdTable(db), + referencedColumn: + $$ProductVariantsTableReferences + ._productIdTable(db) + .localId, + ) + as T; + } + + return state; + }, + getPrefetchedDataCallback: (items) async { + return [ + if (productReviewsRefs) + await $_getPrefetchedData< + ProductVariant, + $ProductVariantsTable, + ProductReview + >( + currentTable: table, + referencedTable: $$ProductVariantsTableReferences + ._productReviewsRefsTable(db), + managerFromTypedResult: (p0) => + $$ProductVariantsTableReferences( + db, + table, + p0, + ).productReviewsRefs, + referencedItemsForCurrentItem: + (item, referencedItems) => referencedItems.where( + (e) => e.productVariantId == item.localId, + ), + typedResults: items, + ), + if (receiptItemsRefs) + await $_getPrefetchedData< + ProductVariant, + $ProductVariantsTable, + ReceiptItem + >( + currentTable: table, + referencedTable: $$ProductVariantsTableReferences + ._receiptItemsRefsTable(db), + managerFromTypedResult: (p0) => + $$ProductVariantsTableReferences( + db, + table, + p0, + ).receiptItemsRefs, + referencedItemsForCurrentItem: + (item, referencedItems) => referencedItems.where( + (e) => e.productVariantId == item.localId, + ), + typedResults: items, + ), + if (orderItemsRefs) + await $_getPrefetchedData< + ProductVariant, + $ProductVariantsTable, + OrderItem + >( + currentTable: table, + referencedTable: $$ProductVariantsTableReferences + ._orderItemsRefsTable(db), + managerFromTypedResult: (p0) => + $$ProductVariantsTableReferences( + db, + table, + p0, + ).orderItemsRefs, + referencedItemsForCurrentItem: + (item, referencedItems) => referencedItems.where( + (e) => e.productVariantId == item.localId, + ), + typedResults: items, + ), + ]; + }, + ); + }, + ), + ); +} + +typedef $$ProductVariantsTableProcessedTableManager = + ProcessedTableManager< + _$AppDatabase, + $ProductVariantsTable, + ProductVariant, + $$ProductVariantsTableFilterComposer, + $$ProductVariantsTableOrderingComposer, + $$ProductVariantsTableAnnotationComposer, + $$ProductVariantsTableCreateCompanionBuilder, + $$ProductVariantsTableUpdateCompanionBuilder, + (ProductVariant, $$ProductVariantsTableReferences), + ProductVariant, + PrefetchHooks Function({ + bool productId, + bool productReviewsRefs, + bool receiptItemsRefs, + bool orderItemsRefs, + }) + >; +typedef $$ProductReviewsTableCreateCompanionBuilder = + ProductReviewsCompanion Function({ + Value localId, + Value serverId, + Value customerId, + required String productVariantId, + Value customerNameSnapshot, + Value rating, + Value comment, + Value syncStatus, + Value createdAt, + Value updatedAt, + Value rowid, + }); +typedef $$ProductReviewsTableUpdateCompanionBuilder = + ProductReviewsCompanion Function({ + Value localId, + Value serverId, + Value customerId, + Value productVariantId, + Value customerNameSnapshot, + Value rating, + Value comment, + Value syncStatus, + Value createdAt, + Value updatedAt, + Value rowid, + }); + +final class $$ProductReviewsTableReferences + extends BaseReferences<_$AppDatabase, $ProductReviewsTable, ProductReview> { + $$ProductReviewsTableReferences( + super.$_db, + super.$_table, + super.$_typedResult, + ); + + static $CustomersTable _customerIdTable(_$AppDatabase db) => + db.customers.createAlias( + $_aliasNameGenerator( + db.productReviews.customerId, + db.customers.localId, + ), + ); + + $$CustomersTableProcessedTableManager? get customerId { + final $_column = $_itemColumn('customer_id'); + if ($_column == null) return null; + final manager = $$CustomersTableTableManager( + $_db, + $_db.customers, + ).filter((f) => f.localId.sqlEquals($_column)); + final item = $_typedResult.readTableOrNull(_customerIdTable($_db)); + if (item == null) return manager; + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: [item]), + ); + } + + static $ProductVariantsTable _productVariantIdTable(_$AppDatabase db) => + db.productVariants.createAlias( + $_aliasNameGenerator( + db.productReviews.productVariantId, + db.productVariants.localId, + ), + ); + + $$ProductVariantsTableProcessedTableManager get productVariantId { + final $_column = $_itemColumn('product_variant_id')!; + + final manager = $$ProductVariantsTableTableManager( + $_db, + $_db.productVariants, + ).filter((f) => f.localId.sqlEquals($_column)); + final item = $_typedResult.readTableOrNull(_productVariantIdTable($_db)); + if (item == null) return manager; + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: [item]), + ); + } +} + +class $$ProductReviewsTableFilterComposer + extends Composer<_$AppDatabase, $ProductReviewsTable> { + $$ProductReviewsTableFilterComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnFilters get localId => $composableBuilder( + column: $table.localId, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get serverId => $composableBuilder( + column: $table.serverId, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get customerNameSnapshot => $composableBuilder( + column: $table.customerNameSnapshot, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get rating => $composableBuilder( + column: $table.rating, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get comment => $composableBuilder( + column: $table.comment, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get syncStatus => $composableBuilder( + column: $table.syncStatus, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get createdAt => $composableBuilder( + column: $table.createdAt, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get updatedAt => $composableBuilder( + column: $table.updatedAt, + builder: (column) => ColumnFilters(column), + ); + + $$CustomersTableFilterComposer get customerId { + final $$CustomersTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.customerId, + referencedTable: $db.customers, + getReferencedColumn: (t) => t.localId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$CustomersTableFilterComposer( + $db: $db, + $table: $db.customers, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } + + $$ProductVariantsTableFilterComposer get productVariantId { + final $$ProductVariantsTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.productVariantId, + referencedTable: $db.productVariants, + getReferencedColumn: (t) => t.localId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$ProductVariantsTableFilterComposer( + $db: $db, + $table: $db.productVariants, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } +} + +class $$ProductReviewsTableOrderingComposer + extends Composer<_$AppDatabase, $ProductReviewsTable> { + $$ProductReviewsTableOrderingComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnOrderings get localId => $composableBuilder( + column: $table.localId, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get serverId => $composableBuilder( + column: $table.serverId, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get customerNameSnapshot => $composableBuilder( + column: $table.customerNameSnapshot, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get rating => $composableBuilder( + column: $table.rating, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get comment => $composableBuilder( + column: $table.comment, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get syncStatus => $composableBuilder( + column: $table.syncStatus, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get createdAt => $composableBuilder( + column: $table.createdAt, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get updatedAt => $composableBuilder( + column: $table.updatedAt, + builder: (column) => ColumnOrderings(column), + ); + + $$CustomersTableOrderingComposer get customerId { + final $$CustomersTableOrderingComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.customerId, + referencedTable: $db.customers, + getReferencedColumn: (t) => t.localId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$CustomersTableOrderingComposer( + $db: $db, + $table: $db.customers, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } + + $$ProductVariantsTableOrderingComposer get productVariantId { + final $$ProductVariantsTableOrderingComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.productVariantId, + referencedTable: $db.productVariants, + getReferencedColumn: (t) => t.localId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$ProductVariantsTableOrderingComposer( + $db: $db, + $table: $db.productVariants, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } +} + +class $$ProductReviewsTableAnnotationComposer + extends Composer<_$AppDatabase, $ProductReviewsTable> { + $$ProductReviewsTableAnnotationComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + GeneratedColumn get localId => + $composableBuilder(column: $table.localId, builder: (column) => column); + + GeneratedColumn get serverId => + $composableBuilder(column: $table.serverId, builder: (column) => column); + + GeneratedColumn get customerNameSnapshot => $composableBuilder( + column: $table.customerNameSnapshot, + builder: (column) => column, + ); + + GeneratedColumn get rating => + $composableBuilder(column: $table.rating, builder: (column) => column); + + GeneratedColumn get comment => + $composableBuilder(column: $table.comment, builder: (column) => column); + + GeneratedColumn get syncStatus => $composableBuilder( + column: $table.syncStatus, + builder: (column) => column, + ); + + GeneratedColumn get createdAt => + $composableBuilder(column: $table.createdAt, builder: (column) => column); + + GeneratedColumn get updatedAt => + $composableBuilder(column: $table.updatedAt, builder: (column) => column); + + $$CustomersTableAnnotationComposer get customerId { + final $$CustomersTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.customerId, + referencedTable: $db.customers, + getReferencedColumn: (t) => t.localId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$CustomersTableAnnotationComposer( + $db: $db, + $table: $db.customers, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } + + $$ProductVariantsTableAnnotationComposer get productVariantId { + final $$ProductVariantsTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.productVariantId, + referencedTable: $db.productVariants, + getReferencedColumn: (t) => t.localId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$ProductVariantsTableAnnotationComposer( + $db: $db, + $table: $db.productVariants, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } +} + +class $$ProductReviewsTableTableManager + extends + RootTableManager< + _$AppDatabase, + $ProductReviewsTable, + ProductReview, + $$ProductReviewsTableFilterComposer, + $$ProductReviewsTableOrderingComposer, + $$ProductReviewsTableAnnotationComposer, + $$ProductReviewsTableCreateCompanionBuilder, + $$ProductReviewsTableUpdateCompanionBuilder, + (ProductReview, $$ProductReviewsTableReferences), + ProductReview, + PrefetchHooks Function({bool customerId, bool productVariantId}) + > { + $$ProductReviewsTableTableManager( + _$AppDatabase db, + $ProductReviewsTable table, + ) : super( + TableManagerState( + db: db, + table: table, + createFilteringComposer: () => + $$ProductReviewsTableFilterComposer($db: db, $table: table), + createOrderingComposer: () => + $$ProductReviewsTableOrderingComposer($db: db, $table: table), + createComputedFieldComposer: () => + $$ProductReviewsTableAnnotationComposer($db: db, $table: table), + updateCompanionCallback: + ({ + Value localId = const Value.absent(), + Value serverId = const Value.absent(), + Value customerId = const Value.absent(), + Value productVariantId = const Value.absent(), + Value customerNameSnapshot = const Value.absent(), + Value rating = const Value.absent(), + Value comment = const Value.absent(), + Value syncStatus = const Value.absent(), + Value createdAt = const Value.absent(), + Value updatedAt = const Value.absent(), + Value rowid = const Value.absent(), + }) => ProductReviewsCompanion( + localId: localId, + serverId: serverId, + customerId: customerId, + productVariantId: productVariantId, + customerNameSnapshot: customerNameSnapshot, + rating: rating, + comment: comment, + syncStatus: syncStatus, + createdAt: createdAt, + updatedAt: updatedAt, + rowid: rowid, + ), + createCompanionCallback: + ({ + Value localId = const Value.absent(), + Value serverId = const Value.absent(), + Value customerId = const Value.absent(), + required String productVariantId, + Value customerNameSnapshot = const Value.absent(), + Value rating = const Value.absent(), + Value comment = const Value.absent(), + Value syncStatus = const Value.absent(), + Value createdAt = const Value.absent(), + Value updatedAt = const Value.absent(), + Value rowid = const Value.absent(), + }) => ProductReviewsCompanion.insert( + localId: localId, + serverId: serverId, + customerId: customerId, + productVariantId: productVariantId, + customerNameSnapshot: customerNameSnapshot, + rating: rating, + comment: comment, + syncStatus: syncStatus, + createdAt: createdAt, + updatedAt: updatedAt, + rowid: rowid, + ), + withReferenceMapper: (p0) => p0 + .map( + (e) => ( + e.readTable(table), + $$ProductReviewsTableReferences(db, table, e), + ), + ) + .toList(), + prefetchHooksCallback: + ({customerId = false, productVariantId = false}) { + return PrefetchHooks( + db: db, + explicitlyWatchedTables: [], + addJoins: + < + T extends TableManagerState< + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic + > + >(state) { + if (customerId) { + state = + state.withJoin( + currentTable: table, + currentColumn: table.customerId, + referencedTable: + $$ProductReviewsTableReferences + ._customerIdTable(db), + referencedColumn: + $$ProductReviewsTableReferences + ._customerIdTable(db) + .localId, + ) + as T; + } + if (productVariantId) { + state = + state.withJoin( + currentTable: table, + currentColumn: table.productVariantId, + referencedTable: + $$ProductReviewsTableReferences + ._productVariantIdTable(db), + referencedColumn: + $$ProductReviewsTableReferences + ._productVariantIdTable(db) + .localId, + ) + as T; + } + + return state; + }, + getPrefetchedDataCallback: (items) async { + return []; + }, + ); + }, + ), + ); +} + +typedef $$ProductReviewsTableProcessedTableManager = + ProcessedTableManager< + _$AppDatabase, + $ProductReviewsTable, + ProductReview, + $$ProductReviewsTableFilterComposer, + $$ProductReviewsTableOrderingComposer, + $$ProductReviewsTableAnnotationComposer, + $$ProductReviewsTableCreateCompanionBuilder, + $$ProductReviewsTableUpdateCompanionBuilder, + (ProductReview, $$ProductReviewsTableReferences), + ProductReview, + PrefetchHooks Function({bool customerId, bool productVariantId}) + >; +typedef $$ProductLikesTableCreateCompanionBuilder = + ProductLikesCompanion Function({ + Value localId, + Value serverId, + Value customerId, + required String productId, + Value isLike, + Value syncStatus, + Value createdAt, + Value updatedAt, + Value rowid, + }); +typedef $$ProductLikesTableUpdateCompanionBuilder = + ProductLikesCompanion Function({ + Value localId, + Value serverId, + Value customerId, + Value productId, + Value isLike, + Value syncStatus, + Value createdAt, + Value updatedAt, + Value rowid, + }); + +final class $$ProductLikesTableReferences + extends BaseReferences<_$AppDatabase, $ProductLikesTable, ProductLike> { + $$ProductLikesTableReferences(super.$_db, super.$_table, super.$_typedResult); + + static $CustomersTable _customerIdTable(_$AppDatabase db) => + db.customers.createAlias( + $_aliasNameGenerator(db.productLikes.customerId, db.customers.localId), + ); + + $$CustomersTableProcessedTableManager? get customerId { + final $_column = $_itemColumn('customer_id'); + if ($_column == null) return null; + final manager = $$CustomersTableTableManager( + $_db, + $_db.customers, + ).filter((f) => f.localId.sqlEquals($_column)); + final item = $_typedResult.readTableOrNull(_customerIdTable($_db)); + if (item == null) return manager; + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: [item]), + ); + } + + static $ProductsTable _productIdTable(_$AppDatabase db) => + db.products.createAlias( + $_aliasNameGenerator(db.productLikes.productId, db.products.localId), + ); + + $$ProductsTableProcessedTableManager get productId { + final $_column = $_itemColumn('product_id')!; + + final manager = $$ProductsTableTableManager( + $_db, + $_db.products, + ).filter((f) => f.localId.sqlEquals($_column)); + final item = $_typedResult.readTableOrNull(_productIdTable($_db)); + if (item == null) return manager; + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: [item]), + ); + } +} + +class $$ProductLikesTableFilterComposer + extends Composer<_$AppDatabase, $ProductLikesTable> { + $$ProductLikesTableFilterComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnFilters get localId => $composableBuilder( + column: $table.localId, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get serverId => $composableBuilder( + column: $table.serverId, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get isLike => $composableBuilder( + column: $table.isLike, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get syncStatus => $composableBuilder( + column: $table.syncStatus, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get createdAt => $composableBuilder( + column: $table.createdAt, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get updatedAt => $composableBuilder( + column: $table.updatedAt, + builder: (column) => ColumnFilters(column), + ); + + $$CustomersTableFilterComposer get customerId { + final $$CustomersTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.customerId, + referencedTable: $db.customers, + getReferencedColumn: (t) => t.localId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$CustomersTableFilterComposer( + $db: $db, + $table: $db.customers, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } + + $$ProductsTableFilterComposer get productId { + final $$ProductsTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.productId, + referencedTable: $db.products, + getReferencedColumn: (t) => t.localId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$ProductsTableFilterComposer( + $db: $db, + $table: $db.products, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } +} + +class $$ProductLikesTableOrderingComposer + extends Composer<_$AppDatabase, $ProductLikesTable> { + $$ProductLikesTableOrderingComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnOrderings get localId => $composableBuilder( + column: $table.localId, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get serverId => $composableBuilder( + column: $table.serverId, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get isLike => $composableBuilder( + column: $table.isLike, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get syncStatus => $composableBuilder( + column: $table.syncStatus, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get createdAt => $composableBuilder( + column: $table.createdAt, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get updatedAt => $composableBuilder( + column: $table.updatedAt, + builder: (column) => ColumnOrderings(column), + ); + + $$CustomersTableOrderingComposer get customerId { + final $$CustomersTableOrderingComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.customerId, + referencedTable: $db.customers, + getReferencedColumn: (t) => t.localId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$CustomersTableOrderingComposer( + $db: $db, + $table: $db.customers, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } + + $$ProductsTableOrderingComposer get productId { + final $$ProductsTableOrderingComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.productId, + referencedTable: $db.products, + getReferencedColumn: (t) => t.localId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$ProductsTableOrderingComposer( + $db: $db, + $table: $db.products, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } +} + +class $$ProductLikesTableAnnotationComposer + extends Composer<_$AppDatabase, $ProductLikesTable> { + $$ProductLikesTableAnnotationComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + GeneratedColumn get localId => + $composableBuilder(column: $table.localId, builder: (column) => column); + + GeneratedColumn get serverId => + $composableBuilder(column: $table.serverId, builder: (column) => column); + + GeneratedColumn get isLike => + $composableBuilder(column: $table.isLike, builder: (column) => column); + + GeneratedColumn get syncStatus => $composableBuilder( + column: $table.syncStatus, + builder: (column) => column, + ); + + GeneratedColumn get createdAt => + $composableBuilder(column: $table.createdAt, builder: (column) => column); + + GeneratedColumn get updatedAt => + $composableBuilder(column: $table.updatedAt, builder: (column) => column); + + $$CustomersTableAnnotationComposer get customerId { + final $$CustomersTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.customerId, + referencedTable: $db.customers, + getReferencedColumn: (t) => t.localId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$CustomersTableAnnotationComposer( + $db: $db, + $table: $db.customers, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } + + $$ProductsTableAnnotationComposer get productId { + final $$ProductsTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.productId, + referencedTable: $db.products, + getReferencedColumn: (t) => t.localId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$ProductsTableAnnotationComposer( + $db: $db, + $table: $db.products, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } +} + +class $$ProductLikesTableTableManager + extends + RootTableManager< + _$AppDatabase, + $ProductLikesTable, + ProductLike, + $$ProductLikesTableFilterComposer, + $$ProductLikesTableOrderingComposer, + $$ProductLikesTableAnnotationComposer, + $$ProductLikesTableCreateCompanionBuilder, + $$ProductLikesTableUpdateCompanionBuilder, + (ProductLike, $$ProductLikesTableReferences), + ProductLike, + PrefetchHooks Function({bool customerId, bool productId}) + > { + $$ProductLikesTableTableManager(_$AppDatabase db, $ProductLikesTable table) + : super( + TableManagerState( + db: db, + table: table, + createFilteringComposer: () => + $$ProductLikesTableFilterComposer($db: db, $table: table), + createOrderingComposer: () => + $$ProductLikesTableOrderingComposer($db: db, $table: table), + createComputedFieldComposer: () => + $$ProductLikesTableAnnotationComposer($db: db, $table: table), + updateCompanionCallback: + ({ + Value localId = const Value.absent(), + Value serverId = const Value.absent(), + Value customerId = const Value.absent(), + Value productId = const Value.absent(), + Value isLike = const Value.absent(), + Value syncStatus = const Value.absent(), + Value createdAt = const Value.absent(), + Value updatedAt = const Value.absent(), + Value rowid = const Value.absent(), + }) => ProductLikesCompanion( + localId: localId, + serverId: serverId, + customerId: customerId, + productId: productId, + isLike: isLike, + syncStatus: syncStatus, + createdAt: createdAt, + updatedAt: updatedAt, + rowid: rowid, + ), + createCompanionCallback: + ({ + Value localId = const Value.absent(), + Value serverId = const Value.absent(), + Value customerId = const Value.absent(), + required String productId, + Value isLike = const Value.absent(), + Value syncStatus = const Value.absent(), + Value createdAt = const Value.absent(), + Value updatedAt = const Value.absent(), + Value rowid = const Value.absent(), + }) => ProductLikesCompanion.insert( + localId: localId, + serverId: serverId, + customerId: customerId, + productId: productId, + isLike: isLike, + syncStatus: syncStatus, + createdAt: createdAt, + updatedAt: updatedAt, + rowid: rowid, + ), + withReferenceMapper: (p0) => p0 + .map( + (e) => ( + e.readTable(table), + $$ProductLikesTableReferences(db, table, e), + ), + ) + .toList(), + prefetchHooksCallback: ({customerId = false, productId = false}) { + return PrefetchHooks( + db: db, + explicitlyWatchedTables: [], + addJoins: + < + T extends TableManagerState< + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic + > + >(state) { + if (customerId) { + state = + state.withJoin( + currentTable: table, + currentColumn: table.customerId, + referencedTable: $$ProductLikesTableReferences + ._customerIdTable(db), + referencedColumn: $$ProductLikesTableReferences + ._customerIdTable(db) + .localId, + ) + as T; + } + if (productId) { + state = + state.withJoin( + currentTable: table, + currentColumn: table.productId, + referencedTable: $$ProductLikesTableReferences + ._productIdTable(db), + referencedColumn: $$ProductLikesTableReferences + ._productIdTable(db) + .localId, + ) + as T; + } + + return state; + }, + getPrefetchedDataCallback: (items) async { + return []; + }, + ); + }, + ), + ); +} + +typedef $$ProductLikesTableProcessedTableManager = + ProcessedTableManager< + _$AppDatabase, + $ProductLikesTable, + ProductLike, + $$ProductLikesTableFilterComposer, + $$ProductLikesTableOrderingComposer, + $$ProductLikesTableAnnotationComposer, + $$ProductLikesTableCreateCompanionBuilder, + $$ProductLikesTableUpdateCompanionBuilder, + (ProductLike, $$ProductLikesTableReferences), + ProductLike, + PrefetchHooks Function({bool customerId, bool productId}) + >; +typedef $$RawMaterialsTableCreateCompanionBuilder = + RawMaterialsCompanion Function({ + Value localId, + Value serverId, + required String unitId, + required String name, + Value syncStatus, + Value createdAt, + Value updatedAt, + Value deletedAt, + Value rowid, + }); +typedef $$RawMaterialsTableUpdateCompanionBuilder = + RawMaterialsCompanion Function({ + Value localId, + Value serverId, + Value unitId, + Value name, + Value syncStatus, + Value createdAt, + Value updatedAt, + Value deletedAt, + Value rowid, + }); + +final class $$RawMaterialsTableReferences + extends BaseReferences<_$AppDatabase, $RawMaterialsTable, RawMaterial> { + $$RawMaterialsTableReferences(super.$_db, super.$_table, super.$_typedResult); + + static $UnitsTable _unitIdTable(_$AppDatabase db) => db.units.createAlias( + $_aliasNameGenerator(db.rawMaterials.unitId, db.units.localId), + ); + + $$UnitsTableProcessedTableManager get unitId { + final $_column = $_itemColumn('unit_id')!; + + final manager = $$UnitsTableTableManager( + $_db, + $_db.units, + ).filter((f) => f.localId.sqlEquals($_column)); + final item = $_typedResult.readTableOrNull(_unitIdTable($_db)); + if (item == null) return manager; + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: [item]), + ); + } + + static MultiTypedResultKey<$ReceiptItemsTable, List> + _receiptItemsRefsTable(_$AppDatabase db) => MultiTypedResultKey.fromTable( + db.receiptItems, + aliasName: $_aliasNameGenerator( + db.rawMaterials.localId, + db.receiptItems.rawMaterialId, + ), + ); + + $$ReceiptItemsTableProcessedTableManager get receiptItemsRefs { + final manager = $$ReceiptItemsTableTableManager($_db, $_db.receiptItems) + .filter( + (f) => f.rawMaterialId.localId.sqlEquals( + $_itemColumn('local_id')!, + ), + ); + + final cache = $_typedResult.readTableOrNull(_receiptItemsRefsTable($_db)); + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: cache), + ); + } +} + +class $$RawMaterialsTableFilterComposer + extends Composer<_$AppDatabase, $RawMaterialsTable> { + $$RawMaterialsTableFilterComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnFilters get localId => $composableBuilder( + column: $table.localId, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get serverId => $composableBuilder( + column: $table.serverId, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get name => $composableBuilder( + column: $table.name, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get syncStatus => $composableBuilder( + column: $table.syncStatus, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get createdAt => $composableBuilder( + column: $table.createdAt, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get updatedAt => $composableBuilder( + column: $table.updatedAt, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get deletedAt => $composableBuilder( + column: $table.deletedAt, + builder: (column) => ColumnFilters(column), + ); + + $$UnitsTableFilterComposer get unitId { + final $$UnitsTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.unitId, + referencedTable: $db.units, + getReferencedColumn: (t) => t.localId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$UnitsTableFilterComposer( + $db: $db, + $table: $db.units, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } + + Expression receiptItemsRefs( + Expression Function($$ReceiptItemsTableFilterComposer f) f, + ) { + final $$ReceiptItemsTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.localId, + referencedTable: $db.receiptItems, + getReferencedColumn: (t) => t.rawMaterialId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$ReceiptItemsTableFilterComposer( + $db: $db, + $table: $db.receiptItems, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return f(composer); + } +} + +class $$RawMaterialsTableOrderingComposer + extends Composer<_$AppDatabase, $RawMaterialsTable> { + $$RawMaterialsTableOrderingComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnOrderings get localId => $composableBuilder( + column: $table.localId, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get serverId => $composableBuilder( + column: $table.serverId, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get name => $composableBuilder( + column: $table.name, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get syncStatus => $composableBuilder( + column: $table.syncStatus, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get createdAt => $composableBuilder( + column: $table.createdAt, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get updatedAt => $composableBuilder( + column: $table.updatedAt, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get deletedAt => $composableBuilder( + column: $table.deletedAt, + builder: (column) => ColumnOrderings(column), + ); + + $$UnitsTableOrderingComposer get unitId { + final $$UnitsTableOrderingComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.unitId, + referencedTable: $db.units, + getReferencedColumn: (t) => t.localId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$UnitsTableOrderingComposer( + $db: $db, + $table: $db.units, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } +} + +class $$RawMaterialsTableAnnotationComposer + extends Composer<_$AppDatabase, $RawMaterialsTable> { + $$RawMaterialsTableAnnotationComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + GeneratedColumn get localId => + $composableBuilder(column: $table.localId, builder: (column) => column); + + GeneratedColumn get serverId => + $composableBuilder(column: $table.serverId, builder: (column) => column); + + GeneratedColumn get name => + $composableBuilder(column: $table.name, builder: (column) => column); + + GeneratedColumn get syncStatus => $composableBuilder( + column: $table.syncStatus, + builder: (column) => column, + ); + + GeneratedColumn get createdAt => + $composableBuilder(column: $table.createdAt, builder: (column) => column); + + GeneratedColumn get updatedAt => + $composableBuilder(column: $table.updatedAt, builder: (column) => column); + + GeneratedColumn get deletedAt => + $composableBuilder(column: $table.deletedAt, builder: (column) => column); + + $$UnitsTableAnnotationComposer get unitId { + final $$UnitsTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.unitId, + referencedTable: $db.units, + getReferencedColumn: (t) => t.localId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$UnitsTableAnnotationComposer( + $db: $db, + $table: $db.units, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } + + Expression receiptItemsRefs( + Expression Function($$ReceiptItemsTableAnnotationComposer a) f, + ) { + final $$ReceiptItemsTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.localId, + referencedTable: $db.receiptItems, + getReferencedColumn: (t) => t.rawMaterialId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$ReceiptItemsTableAnnotationComposer( + $db: $db, + $table: $db.receiptItems, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return f(composer); + } +} + +class $$RawMaterialsTableTableManager + extends + RootTableManager< + _$AppDatabase, + $RawMaterialsTable, + RawMaterial, + $$RawMaterialsTableFilterComposer, + $$RawMaterialsTableOrderingComposer, + $$RawMaterialsTableAnnotationComposer, + $$RawMaterialsTableCreateCompanionBuilder, + $$RawMaterialsTableUpdateCompanionBuilder, + (RawMaterial, $$RawMaterialsTableReferences), + RawMaterial, + PrefetchHooks Function({bool unitId, bool receiptItemsRefs}) + > { + $$RawMaterialsTableTableManager(_$AppDatabase db, $RawMaterialsTable table) + : super( + TableManagerState( + db: db, + table: table, + createFilteringComposer: () => + $$RawMaterialsTableFilterComposer($db: db, $table: table), + createOrderingComposer: () => + $$RawMaterialsTableOrderingComposer($db: db, $table: table), + createComputedFieldComposer: () => + $$RawMaterialsTableAnnotationComposer($db: db, $table: table), + updateCompanionCallback: + ({ + Value localId = const Value.absent(), + Value serverId = const Value.absent(), + Value unitId = const Value.absent(), + Value name = const Value.absent(), + Value syncStatus = const Value.absent(), + Value createdAt = const Value.absent(), + Value updatedAt = const Value.absent(), + Value deletedAt = const Value.absent(), + Value rowid = const Value.absent(), + }) => RawMaterialsCompanion( + localId: localId, + serverId: serverId, + unitId: unitId, + name: name, + syncStatus: syncStatus, + createdAt: createdAt, + updatedAt: updatedAt, + deletedAt: deletedAt, + rowid: rowid, + ), + createCompanionCallback: + ({ + Value localId = const Value.absent(), + Value serverId = const Value.absent(), + required String unitId, + required String name, + Value syncStatus = const Value.absent(), + Value createdAt = const Value.absent(), + Value updatedAt = const Value.absent(), + Value deletedAt = const Value.absent(), + Value rowid = const Value.absent(), + }) => RawMaterialsCompanion.insert( + localId: localId, + serverId: serverId, + unitId: unitId, + name: name, + syncStatus: syncStatus, + createdAt: createdAt, + updatedAt: updatedAt, + deletedAt: deletedAt, + rowid: rowid, + ), + withReferenceMapper: (p0) => p0 + .map( + (e) => ( + e.readTable(table), + $$RawMaterialsTableReferences(db, table, e), + ), + ) + .toList(), + prefetchHooksCallback: ({unitId = false, receiptItemsRefs = false}) { + return PrefetchHooks( + db: db, + explicitlyWatchedTables: [if (receiptItemsRefs) db.receiptItems], + addJoins: + < + T extends TableManagerState< + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic + > + >(state) { + if (unitId) { + state = + state.withJoin( + currentTable: table, + currentColumn: table.unitId, + referencedTable: $$RawMaterialsTableReferences + ._unitIdTable(db), + referencedColumn: $$RawMaterialsTableReferences + ._unitIdTable(db) + .localId, + ) + as T; + } + + return state; + }, + getPrefetchedDataCallback: (items) async { + return [ + if (receiptItemsRefs) + await $_getPrefetchedData< + RawMaterial, + $RawMaterialsTable, + ReceiptItem + >( + currentTable: table, + referencedTable: $$RawMaterialsTableReferences + ._receiptItemsRefsTable(db), + managerFromTypedResult: (p0) => + $$RawMaterialsTableReferences( + db, + table, + p0, + ).receiptItemsRefs, + referencedItemsForCurrentItem: (item, referencedItems) => + referencedItems.where( + (e) => e.rawMaterialId == item.localId, + ), + typedResults: items, + ), + ]; + }, + ); + }, + ), + ); +} + +typedef $$RawMaterialsTableProcessedTableManager = + ProcessedTableManager< + _$AppDatabase, + $RawMaterialsTable, + RawMaterial, + $$RawMaterialsTableFilterComposer, + $$RawMaterialsTableOrderingComposer, + $$RawMaterialsTableAnnotationComposer, + $$RawMaterialsTableCreateCompanionBuilder, + $$RawMaterialsTableUpdateCompanionBuilder, + (RawMaterial, $$RawMaterialsTableReferences), + RawMaterial, + PrefetchHooks Function({bool unitId, bool receiptItemsRefs}) + >; +typedef $$OutletInventoriesTableCreateCompanionBuilder = + OutletInventoriesCompanion Function({ + Value localId, + Value serverId, + required String outletId, + required String itemType, + required String itemId, + Value stockType, + Value stock, + Value sellingPrice, + Value costPrice, + Value purchasePrice, + Value isDeleteByOwner, + Value syncStatus, + Value createdAt, + Value updatedAt, + Value deletedAt, + Value rowid, + }); +typedef $$OutletInventoriesTableUpdateCompanionBuilder = + OutletInventoriesCompanion Function({ + Value localId, + Value serverId, + Value outletId, + Value itemType, + Value itemId, + Value stockType, + Value stock, + Value sellingPrice, + Value costPrice, + Value purchasePrice, + Value isDeleteByOwner, + Value syncStatus, + Value createdAt, + Value updatedAt, + Value deletedAt, + Value rowid, + }); + +final class $$OutletInventoriesTableReferences + extends + BaseReferences< + _$AppDatabase, + $OutletInventoriesTable, + OutletInventory + > { + $$OutletInventoriesTableReferences( + super.$_db, + super.$_table, + super.$_typedResult, + ); + + static $OutletsTable _outletIdTable(_$AppDatabase db) => + db.outlets.createAlias( + $_aliasNameGenerator(db.outletInventories.outletId, db.outlets.localId), + ); + + $$OutletsTableProcessedTableManager get outletId { + final $_column = $_itemColumn('outlet_id')!; + + final manager = $$OutletsTableTableManager( + $_db, + $_db.outlets, + ).filter((f) => f.localId.sqlEquals($_column)); + final item = $_typedResult.readTableOrNull(_outletIdTable($_db)); + if (item == null) return manager; + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: [item]), + ); + } + + static MultiTypedResultKey<$StockCardsTable, List> + _stockCardsRefsTable(_$AppDatabase db) => MultiTypedResultKey.fromTable( + db.stockCards, + aliasName: $_aliasNameGenerator( + db.outletInventories.localId, + db.stockCards.outletInventoryId, + ), + ); + + $$StockCardsTableProcessedTableManager get stockCardsRefs { + final manager = $$StockCardsTableTableManager($_db, $_db.stockCards).filter( + (f) => f.outletInventoryId.localId.sqlEquals( + $_itemColumn('local_id')!, + ), + ); + + final cache = $_typedResult.readTableOrNull(_stockCardsRefsTable($_db)); + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: cache), + ); + } +} + +class $$OutletInventoriesTableFilterComposer + extends Composer<_$AppDatabase, $OutletInventoriesTable> { + $$OutletInventoriesTableFilterComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnFilters get localId => $composableBuilder( + column: $table.localId, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get serverId => $composableBuilder( + column: $table.serverId, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get itemType => $composableBuilder( + column: $table.itemType, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get itemId => $composableBuilder( + column: $table.itemId, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get stockType => $composableBuilder( + column: $table.stockType, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get stock => $composableBuilder( + column: $table.stock, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get sellingPrice => $composableBuilder( + column: $table.sellingPrice, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get costPrice => $composableBuilder( + column: $table.costPrice, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get purchasePrice => $composableBuilder( + column: $table.purchasePrice, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get isDeleteByOwner => $composableBuilder( + column: $table.isDeleteByOwner, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get syncStatus => $composableBuilder( + column: $table.syncStatus, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get createdAt => $composableBuilder( + column: $table.createdAt, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get updatedAt => $composableBuilder( + column: $table.updatedAt, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get deletedAt => $composableBuilder( + column: $table.deletedAt, + builder: (column) => ColumnFilters(column), + ); + + $$OutletsTableFilterComposer get outletId { + final $$OutletsTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.outletId, + referencedTable: $db.outlets, + getReferencedColumn: (t) => t.localId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$OutletsTableFilterComposer( + $db: $db, + $table: $db.outlets, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } + + Expression stockCardsRefs( + Expression Function($$StockCardsTableFilterComposer f) f, + ) { + final $$StockCardsTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.localId, + referencedTable: $db.stockCards, + getReferencedColumn: (t) => t.outletInventoryId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$StockCardsTableFilterComposer( + $db: $db, + $table: $db.stockCards, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return f(composer); + } +} + +class $$OutletInventoriesTableOrderingComposer + extends Composer<_$AppDatabase, $OutletInventoriesTable> { + $$OutletInventoriesTableOrderingComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnOrderings get localId => $composableBuilder( + column: $table.localId, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get serverId => $composableBuilder( + column: $table.serverId, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get itemType => $composableBuilder( + column: $table.itemType, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get itemId => $composableBuilder( + column: $table.itemId, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get stockType => $composableBuilder( + column: $table.stockType, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get stock => $composableBuilder( + column: $table.stock, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get sellingPrice => $composableBuilder( + column: $table.sellingPrice, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get costPrice => $composableBuilder( + column: $table.costPrice, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get purchasePrice => $composableBuilder( + column: $table.purchasePrice, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get isDeleteByOwner => $composableBuilder( + column: $table.isDeleteByOwner, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get syncStatus => $composableBuilder( + column: $table.syncStatus, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get createdAt => $composableBuilder( + column: $table.createdAt, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get updatedAt => $composableBuilder( + column: $table.updatedAt, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get deletedAt => $composableBuilder( + column: $table.deletedAt, + builder: (column) => ColumnOrderings(column), + ); + + $$OutletsTableOrderingComposer get outletId { + final $$OutletsTableOrderingComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.outletId, + referencedTable: $db.outlets, + getReferencedColumn: (t) => t.localId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$OutletsTableOrderingComposer( + $db: $db, + $table: $db.outlets, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } +} + +class $$OutletInventoriesTableAnnotationComposer + extends Composer<_$AppDatabase, $OutletInventoriesTable> { + $$OutletInventoriesTableAnnotationComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + GeneratedColumn get localId => + $composableBuilder(column: $table.localId, builder: (column) => column); + + GeneratedColumn get serverId => + $composableBuilder(column: $table.serverId, builder: (column) => column); + + GeneratedColumn get itemType => + $composableBuilder(column: $table.itemType, builder: (column) => column); + + GeneratedColumn get itemId => + $composableBuilder(column: $table.itemId, builder: (column) => column); + + GeneratedColumn get stockType => + $composableBuilder(column: $table.stockType, builder: (column) => column); + + GeneratedColumn get stock => + $composableBuilder(column: $table.stock, builder: (column) => column); + + GeneratedColumn get sellingPrice => $composableBuilder( + column: $table.sellingPrice, + builder: (column) => column, + ); + + GeneratedColumn get costPrice => + $composableBuilder(column: $table.costPrice, builder: (column) => column); + + GeneratedColumn get purchasePrice => $composableBuilder( + column: $table.purchasePrice, + builder: (column) => column, + ); + + GeneratedColumn get isDeleteByOwner => $composableBuilder( + column: $table.isDeleteByOwner, + builder: (column) => column, + ); + + GeneratedColumn get syncStatus => $composableBuilder( + column: $table.syncStatus, + builder: (column) => column, + ); + + GeneratedColumn get createdAt => + $composableBuilder(column: $table.createdAt, builder: (column) => column); + + GeneratedColumn get updatedAt => + $composableBuilder(column: $table.updatedAt, builder: (column) => column); + + GeneratedColumn get deletedAt => + $composableBuilder(column: $table.deletedAt, builder: (column) => column); + + $$OutletsTableAnnotationComposer get outletId { + final $$OutletsTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.outletId, + referencedTable: $db.outlets, + getReferencedColumn: (t) => t.localId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$OutletsTableAnnotationComposer( + $db: $db, + $table: $db.outlets, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } + + Expression stockCardsRefs( + Expression Function($$StockCardsTableAnnotationComposer a) f, + ) { + final $$StockCardsTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.localId, + referencedTable: $db.stockCards, + getReferencedColumn: (t) => t.outletInventoryId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$StockCardsTableAnnotationComposer( + $db: $db, + $table: $db.stockCards, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return f(composer); + } +} + +class $$OutletInventoriesTableTableManager + extends + RootTableManager< + _$AppDatabase, + $OutletInventoriesTable, + OutletInventory, + $$OutletInventoriesTableFilterComposer, + $$OutletInventoriesTableOrderingComposer, + $$OutletInventoriesTableAnnotationComposer, + $$OutletInventoriesTableCreateCompanionBuilder, + $$OutletInventoriesTableUpdateCompanionBuilder, + (OutletInventory, $$OutletInventoriesTableReferences), + OutletInventory, + PrefetchHooks Function({bool outletId, bool stockCardsRefs}) + > { + $$OutletInventoriesTableTableManager( + _$AppDatabase db, + $OutletInventoriesTable table, + ) : super( + TableManagerState( + db: db, + table: table, + createFilteringComposer: () => + $$OutletInventoriesTableFilterComposer($db: db, $table: table), + createOrderingComposer: () => + $$OutletInventoriesTableOrderingComposer($db: db, $table: table), + createComputedFieldComposer: () => + $$OutletInventoriesTableAnnotationComposer( + $db: db, + $table: table, + ), + updateCompanionCallback: + ({ + Value localId = const Value.absent(), + Value serverId = const Value.absent(), + Value outletId = const Value.absent(), + Value itemType = const Value.absent(), + Value itemId = const Value.absent(), + Value stockType = const Value.absent(), + Value stock = const Value.absent(), + Value sellingPrice = const Value.absent(), + Value costPrice = const Value.absent(), + Value purchasePrice = const Value.absent(), + Value isDeleteByOwner = const Value.absent(), + Value syncStatus = const Value.absent(), + Value createdAt = const Value.absent(), + Value updatedAt = const Value.absent(), + Value deletedAt = const Value.absent(), + Value rowid = const Value.absent(), + }) => OutletInventoriesCompanion( + localId: localId, + serverId: serverId, + outletId: outletId, + itemType: itemType, + itemId: itemId, + stockType: stockType, + stock: stock, + sellingPrice: sellingPrice, + costPrice: costPrice, + purchasePrice: purchasePrice, + isDeleteByOwner: isDeleteByOwner, + syncStatus: syncStatus, + createdAt: createdAt, + updatedAt: updatedAt, + deletedAt: deletedAt, + rowid: rowid, + ), + createCompanionCallback: + ({ + Value localId = const Value.absent(), + Value serverId = const Value.absent(), + required String outletId, + required String itemType, + required String itemId, + Value stockType = const Value.absent(), + Value stock = const Value.absent(), + Value sellingPrice = const Value.absent(), + Value costPrice = const Value.absent(), + Value purchasePrice = const Value.absent(), + Value isDeleteByOwner = const Value.absent(), + Value syncStatus = const Value.absent(), + Value createdAt = const Value.absent(), + Value updatedAt = const Value.absent(), + Value deletedAt = const Value.absent(), + Value rowid = const Value.absent(), + }) => OutletInventoriesCompanion.insert( + localId: localId, + serverId: serverId, + outletId: outletId, + itemType: itemType, + itemId: itemId, + stockType: stockType, + stock: stock, + sellingPrice: sellingPrice, + costPrice: costPrice, + purchasePrice: purchasePrice, + isDeleteByOwner: isDeleteByOwner, + syncStatus: syncStatus, + createdAt: createdAt, + updatedAt: updatedAt, + deletedAt: deletedAt, + rowid: rowid, + ), + withReferenceMapper: (p0) => p0 + .map( + (e) => ( + e.readTable(table), + $$OutletInventoriesTableReferences(db, table, e), + ), + ) + .toList(), + prefetchHooksCallback: ({outletId = false, stockCardsRefs = false}) { + return PrefetchHooks( + db: db, + explicitlyWatchedTables: [if (stockCardsRefs) db.stockCards], + addJoins: + < + T extends TableManagerState< + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic + > + >(state) { + if (outletId) { + state = + state.withJoin( + currentTable: table, + currentColumn: table.outletId, + referencedTable: + $$OutletInventoriesTableReferences + ._outletIdTable(db), + referencedColumn: + $$OutletInventoriesTableReferences + ._outletIdTable(db) + .localId, + ) + as T; + } + + return state; + }, + getPrefetchedDataCallback: (items) async { + return [ + if (stockCardsRefs) + await $_getPrefetchedData< + OutletInventory, + $OutletInventoriesTable, + StockCard + >( + currentTable: table, + referencedTable: $$OutletInventoriesTableReferences + ._stockCardsRefsTable(db), + managerFromTypedResult: (p0) => + $$OutletInventoriesTableReferences( + db, + table, + p0, + ).stockCardsRefs, + referencedItemsForCurrentItem: (item, referencedItems) => + referencedItems.where( + (e) => e.outletInventoryId == item.localId, + ), + typedResults: items, + ), + ]; + }, + ); + }, + ), + ); +} + +typedef $$OutletInventoriesTableProcessedTableManager = + ProcessedTableManager< + _$AppDatabase, + $OutletInventoriesTable, + OutletInventory, + $$OutletInventoriesTableFilterComposer, + $$OutletInventoriesTableOrderingComposer, + $$OutletInventoriesTableAnnotationComposer, + $$OutletInventoriesTableCreateCompanionBuilder, + $$OutletInventoriesTableUpdateCompanionBuilder, + (OutletInventory, $$OutletInventoriesTableReferences), + OutletInventory, + PrefetchHooks Function({bool outletId, bool stockCardsRefs}) + >; +typedef $$ReceiptItemsTableCreateCompanionBuilder = + ReceiptItemsCompanion Function({ + Value localId, + Value serverId, + Value productVariantId, + Value rawMaterialId, + Value quantity, + Value syncStatus, + Value createdAt, + Value updatedAt, + Value deletedAt, + Value rowid, + }); +typedef $$ReceiptItemsTableUpdateCompanionBuilder = + ReceiptItemsCompanion Function({ + Value localId, + Value serverId, + Value productVariantId, + Value rawMaterialId, + Value quantity, + Value syncStatus, + Value createdAt, + Value updatedAt, + Value deletedAt, + Value rowid, + }); + +final class $$ReceiptItemsTableReferences + extends BaseReferences<_$AppDatabase, $ReceiptItemsTable, ReceiptItem> { + $$ReceiptItemsTableReferences(super.$_db, super.$_table, super.$_typedResult); + + static $ProductVariantsTable _productVariantIdTable(_$AppDatabase db) => + db.productVariants.createAlias( + $_aliasNameGenerator( + db.receiptItems.productVariantId, + db.productVariants.localId, + ), + ); + + $$ProductVariantsTableProcessedTableManager? get productVariantId { + final $_column = $_itemColumn('product_variant_id'); + if ($_column == null) return null; + final manager = $$ProductVariantsTableTableManager( + $_db, + $_db.productVariants, + ).filter((f) => f.localId.sqlEquals($_column)); + final item = $_typedResult.readTableOrNull(_productVariantIdTable($_db)); + if (item == null) return manager; + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: [item]), + ); + } + + static $RawMaterialsTable _rawMaterialIdTable(_$AppDatabase db) => + db.rawMaterials.createAlias( + $_aliasNameGenerator( + db.receiptItems.rawMaterialId, + db.rawMaterials.localId, + ), + ); + + $$RawMaterialsTableProcessedTableManager? get rawMaterialId { + final $_column = $_itemColumn('raw_material_id'); + if ($_column == null) return null; + final manager = $$RawMaterialsTableTableManager( + $_db, + $_db.rawMaterials, + ).filter((f) => f.localId.sqlEquals($_column)); + final item = $_typedResult.readTableOrNull(_rawMaterialIdTable($_db)); + if (item == null) return manager; + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: [item]), + ); + } +} + +class $$ReceiptItemsTableFilterComposer + extends Composer<_$AppDatabase, $ReceiptItemsTable> { + $$ReceiptItemsTableFilterComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnFilters get localId => $composableBuilder( + column: $table.localId, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get serverId => $composableBuilder( + column: $table.serverId, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get quantity => $composableBuilder( + column: $table.quantity, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get syncStatus => $composableBuilder( + column: $table.syncStatus, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get createdAt => $composableBuilder( + column: $table.createdAt, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get updatedAt => $composableBuilder( + column: $table.updatedAt, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get deletedAt => $composableBuilder( + column: $table.deletedAt, + builder: (column) => ColumnFilters(column), + ); + + $$ProductVariantsTableFilterComposer get productVariantId { + final $$ProductVariantsTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.productVariantId, + referencedTable: $db.productVariants, + getReferencedColumn: (t) => t.localId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$ProductVariantsTableFilterComposer( + $db: $db, + $table: $db.productVariants, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } + + $$RawMaterialsTableFilterComposer get rawMaterialId { + final $$RawMaterialsTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.rawMaterialId, + referencedTable: $db.rawMaterials, + getReferencedColumn: (t) => t.localId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$RawMaterialsTableFilterComposer( + $db: $db, + $table: $db.rawMaterials, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } +} + +class $$ReceiptItemsTableOrderingComposer + extends Composer<_$AppDatabase, $ReceiptItemsTable> { + $$ReceiptItemsTableOrderingComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnOrderings get localId => $composableBuilder( + column: $table.localId, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get serverId => $composableBuilder( + column: $table.serverId, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get quantity => $composableBuilder( + column: $table.quantity, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get syncStatus => $composableBuilder( + column: $table.syncStatus, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get createdAt => $composableBuilder( + column: $table.createdAt, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get updatedAt => $composableBuilder( + column: $table.updatedAt, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get deletedAt => $composableBuilder( + column: $table.deletedAt, + builder: (column) => ColumnOrderings(column), + ); + + $$ProductVariantsTableOrderingComposer get productVariantId { + final $$ProductVariantsTableOrderingComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.productVariantId, + referencedTable: $db.productVariants, + getReferencedColumn: (t) => t.localId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$ProductVariantsTableOrderingComposer( + $db: $db, + $table: $db.productVariants, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } + + $$RawMaterialsTableOrderingComposer get rawMaterialId { + final $$RawMaterialsTableOrderingComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.rawMaterialId, + referencedTable: $db.rawMaterials, + getReferencedColumn: (t) => t.localId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$RawMaterialsTableOrderingComposer( + $db: $db, + $table: $db.rawMaterials, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } +} + +class $$ReceiptItemsTableAnnotationComposer + extends Composer<_$AppDatabase, $ReceiptItemsTable> { + $$ReceiptItemsTableAnnotationComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + GeneratedColumn get localId => + $composableBuilder(column: $table.localId, builder: (column) => column); + + GeneratedColumn get serverId => + $composableBuilder(column: $table.serverId, builder: (column) => column); + + GeneratedColumn get quantity => + $composableBuilder(column: $table.quantity, builder: (column) => column); + + GeneratedColumn get syncStatus => $composableBuilder( + column: $table.syncStatus, + builder: (column) => column, + ); + + GeneratedColumn get createdAt => + $composableBuilder(column: $table.createdAt, builder: (column) => column); + + GeneratedColumn get updatedAt => + $composableBuilder(column: $table.updatedAt, builder: (column) => column); + + GeneratedColumn get deletedAt => + $composableBuilder(column: $table.deletedAt, builder: (column) => column); + + $$ProductVariantsTableAnnotationComposer get productVariantId { + final $$ProductVariantsTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.productVariantId, + referencedTable: $db.productVariants, + getReferencedColumn: (t) => t.localId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$ProductVariantsTableAnnotationComposer( + $db: $db, + $table: $db.productVariants, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } + + $$RawMaterialsTableAnnotationComposer get rawMaterialId { + final $$RawMaterialsTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.rawMaterialId, + referencedTable: $db.rawMaterials, + getReferencedColumn: (t) => t.localId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$RawMaterialsTableAnnotationComposer( + $db: $db, + $table: $db.rawMaterials, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } +} + +class $$ReceiptItemsTableTableManager + extends + RootTableManager< + _$AppDatabase, + $ReceiptItemsTable, + ReceiptItem, + $$ReceiptItemsTableFilterComposer, + $$ReceiptItemsTableOrderingComposer, + $$ReceiptItemsTableAnnotationComposer, + $$ReceiptItemsTableCreateCompanionBuilder, + $$ReceiptItemsTableUpdateCompanionBuilder, + (ReceiptItem, $$ReceiptItemsTableReferences), + ReceiptItem, + PrefetchHooks Function({bool productVariantId, bool rawMaterialId}) + > { + $$ReceiptItemsTableTableManager(_$AppDatabase db, $ReceiptItemsTable table) + : super( + TableManagerState( + db: db, + table: table, + createFilteringComposer: () => + $$ReceiptItemsTableFilterComposer($db: db, $table: table), + createOrderingComposer: () => + $$ReceiptItemsTableOrderingComposer($db: db, $table: table), + createComputedFieldComposer: () => + $$ReceiptItemsTableAnnotationComposer($db: db, $table: table), + updateCompanionCallback: + ({ + Value localId = const Value.absent(), + Value serverId = const Value.absent(), + Value productVariantId = const Value.absent(), + Value rawMaterialId = const Value.absent(), + Value quantity = const Value.absent(), + Value syncStatus = const Value.absent(), + Value createdAt = const Value.absent(), + Value updatedAt = const Value.absent(), + Value deletedAt = const Value.absent(), + Value rowid = const Value.absent(), + }) => ReceiptItemsCompanion( + localId: localId, + serverId: serverId, + productVariantId: productVariantId, + rawMaterialId: rawMaterialId, + quantity: quantity, + syncStatus: syncStatus, + createdAt: createdAt, + updatedAt: updatedAt, + deletedAt: deletedAt, + rowid: rowid, + ), + createCompanionCallback: + ({ + Value localId = const Value.absent(), + Value serverId = const Value.absent(), + Value productVariantId = const Value.absent(), + Value rawMaterialId = const Value.absent(), + Value quantity = const Value.absent(), + Value syncStatus = const Value.absent(), + Value createdAt = const Value.absent(), + Value updatedAt = const Value.absent(), + Value deletedAt = const Value.absent(), + Value rowid = const Value.absent(), + }) => ReceiptItemsCompanion.insert( + localId: localId, + serverId: serverId, + productVariantId: productVariantId, + rawMaterialId: rawMaterialId, + quantity: quantity, + syncStatus: syncStatus, + createdAt: createdAt, + updatedAt: updatedAt, + deletedAt: deletedAt, + rowid: rowid, + ), + withReferenceMapper: (p0) => p0 + .map( + (e) => ( + e.readTable(table), + $$ReceiptItemsTableReferences(db, table, e), + ), + ) + .toList(), + prefetchHooksCallback: + ({productVariantId = false, rawMaterialId = false}) { + return PrefetchHooks( + db: db, + explicitlyWatchedTables: [], + addJoins: + < + T extends TableManagerState< + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic + > + >(state) { + if (productVariantId) { + state = + state.withJoin( + currentTable: table, + currentColumn: table.productVariantId, + referencedTable: + $$ReceiptItemsTableReferences + ._productVariantIdTable(db), + referencedColumn: + $$ReceiptItemsTableReferences + ._productVariantIdTable(db) + .localId, + ) + as T; + } + if (rawMaterialId) { + state = + state.withJoin( + currentTable: table, + currentColumn: table.rawMaterialId, + referencedTable: + $$ReceiptItemsTableReferences + ._rawMaterialIdTable(db), + referencedColumn: + $$ReceiptItemsTableReferences + ._rawMaterialIdTable(db) + .localId, + ) + as T; + } + + return state; + }, + getPrefetchedDataCallback: (items) async { + return []; + }, + ); + }, + ), + ); +} + +typedef $$ReceiptItemsTableProcessedTableManager = + ProcessedTableManager< + _$AppDatabase, + $ReceiptItemsTable, + ReceiptItem, + $$ReceiptItemsTableFilterComposer, + $$ReceiptItemsTableOrderingComposer, + $$ReceiptItemsTableAnnotationComposer, + $$ReceiptItemsTableCreateCompanionBuilder, + $$ReceiptItemsTableUpdateCompanionBuilder, + (ReceiptItem, $$ReceiptItemsTableReferences), + ReceiptItem, + PrefetchHooks Function({bool productVariantId, bool rawMaterialId}) + >; +typedef $$StockCardsTableCreateCompanionBuilder = + StockCardsCompanion Function({ + Value localId, + Value serverId, + required String outletInventoryId, + required String userId, + required String type, + Value receiveFromId, + Value qtyChange, + Value currentStock, + Value stockType, + Value modeChange, + Value modeBeforeChange, + Value modeAfterChange, + Value costPrice, + Value referenceId, + Value documentNumber, + Value notes, + Value syncStatus, + Value createdAt, + Value rowid, + }); +typedef $$StockCardsTableUpdateCompanionBuilder = + StockCardsCompanion Function({ + Value localId, + Value serverId, + Value outletInventoryId, + Value userId, + Value type, + Value receiveFromId, + Value qtyChange, + Value currentStock, + Value stockType, + Value modeChange, + Value modeBeforeChange, + Value modeAfterChange, + Value costPrice, + Value referenceId, + Value documentNumber, + Value notes, + Value syncStatus, + Value createdAt, + Value rowid, + }); + +final class $$StockCardsTableReferences + extends BaseReferences<_$AppDatabase, $StockCardsTable, StockCard> { + $$StockCardsTableReferences(super.$_db, super.$_table, super.$_typedResult); + + static $OutletInventoriesTable _outletInventoryIdTable(_$AppDatabase db) => + db.outletInventories.createAlias( + $_aliasNameGenerator( + db.stockCards.outletInventoryId, + db.outletInventories.localId, + ), + ); + + $$OutletInventoriesTableProcessedTableManager get outletInventoryId { + final $_column = $_itemColumn('outlet_inventory_id')!; + + final manager = $$OutletInventoriesTableTableManager( + $_db, + $_db.outletInventories, + ).filter((f) => f.localId.sqlEquals($_column)); + final item = $_typedResult.readTableOrNull(_outletInventoryIdTable($_db)); + if (item == null) return manager; + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: [item]), + ); + } + + static $UsersTable _userIdTable(_$AppDatabase db) => db.users.createAlias( + $_aliasNameGenerator(db.stockCards.userId, db.users.localId), + ); + + $$UsersTableProcessedTableManager get userId { + final $_column = $_itemColumn('user_id')!; + + final manager = $$UsersTableTableManager( + $_db, + $_db.users, + ).filter((f) => f.localId.sqlEquals($_column)); + final item = $_typedResult.readTableOrNull(_userIdTable($_db)); + if (item == null) return manager; + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: [item]), + ); + } + + static $OutletsTable _receiveFromIdTable(_$AppDatabase db) => + db.outlets.createAlias( + $_aliasNameGenerator(db.stockCards.receiveFromId, db.outlets.localId), + ); + + $$OutletsTableProcessedTableManager? get receiveFromId { + final $_column = $_itemColumn('receive_from_id'); + if ($_column == null) return null; + final manager = $$OutletsTableTableManager( + $_db, + $_db.outlets, + ).filter((f) => f.localId.sqlEquals($_column)); + final item = $_typedResult.readTableOrNull(_receiveFromIdTable($_db)); + if (item == null) return manager; + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: [item]), + ); + } +} + +class $$StockCardsTableFilterComposer + extends Composer<_$AppDatabase, $StockCardsTable> { + $$StockCardsTableFilterComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnFilters get localId => $composableBuilder( + column: $table.localId, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get serverId => $composableBuilder( + column: $table.serverId, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get type => $composableBuilder( + column: $table.type, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get qtyChange => $composableBuilder( + column: $table.qtyChange, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get currentStock => $composableBuilder( + column: $table.currentStock, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get stockType => $composableBuilder( + column: $table.stockType, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get modeChange => $composableBuilder( + column: $table.modeChange, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get modeBeforeChange => $composableBuilder( + column: $table.modeBeforeChange, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get modeAfterChange => $composableBuilder( + column: $table.modeAfterChange, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get costPrice => $composableBuilder( + column: $table.costPrice, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get referenceId => $composableBuilder( + column: $table.referenceId, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get documentNumber => $composableBuilder( + column: $table.documentNumber, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get notes => $composableBuilder( + column: $table.notes, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get syncStatus => $composableBuilder( + column: $table.syncStatus, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get createdAt => $composableBuilder( + column: $table.createdAt, + builder: (column) => ColumnFilters(column), + ); + + $$OutletInventoriesTableFilterComposer get outletInventoryId { + final $$OutletInventoriesTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.outletInventoryId, + referencedTable: $db.outletInventories, + getReferencedColumn: (t) => t.localId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$OutletInventoriesTableFilterComposer( + $db: $db, + $table: $db.outletInventories, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } + + $$UsersTableFilterComposer get userId { + final $$UsersTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.userId, + referencedTable: $db.users, + getReferencedColumn: (t) => t.localId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$UsersTableFilterComposer( + $db: $db, + $table: $db.users, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } + + $$OutletsTableFilterComposer get receiveFromId { + final $$OutletsTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.receiveFromId, + referencedTable: $db.outlets, + getReferencedColumn: (t) => t.localId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$OutletsTableFilterComposer( + $db: $db, + $table: $db.outlets, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } +} + +class $$StockCardsTableOrderingComposer + extends Composer<_$AppDatabase, $StockCardsTable> { + $$StockCardsTableOrderingComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnOrderings get localId => $composableBuilder( + column: $table.localId, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get serverId => $composableBuilder( + column: $table.serverId, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get type => $composableBuilder( + column: $table.type, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get qtyChange => $composableBuilder( + column: $table.qtyChange, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get currentStock => $composableBuilder( + column: $table.currentStock, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get stockType => $composableBuilder( + column: $table.stockType, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get modeChange => $composableBuilder( + column: $table.modeChange, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get modeBeforeChange => $composableBuilder( + column: $table.modeBeforeChange, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get modeAfterChange => $composableBuilder( + column: $table.modeAfterChange, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get costPrice => $composableBuilder( + column: $table.costPrice, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get referenceId => $composableBuilder( + column: $table.referenceId, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get documentNumber => $composableBuilder( + column: $table.documentNumber, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get notes => $composableBuilder( + column: $table.notes, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get syncStatus => $composableBuilder( + column: $table.syncStatus, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get createdAt => $composableBuilder( + column: $table.createdAt, + builder: (column) => ColumnOrderings(column), + ); + + $$OutletInventoriesTableOrderingComposer get outletInventoryId { + final $$OutletInventoriesTableOrderingComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.outletInventoryId, + referencedTable: $db.outletInventories, + getReferencedColumn: (t) => t.localId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$OutletInventoriesTableOrderingComposer( + $db: $db, + $table: $db.outletInventories, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } + + $$UsersTableOrderingComposer get userId { + final $$UsersTableOrderingComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.userId, + referencedTable: $db.users, + getReferencedColumn: (t) => t.localId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$UsersTableOrderingComposer( + $db: $db, + $table: $db.users, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } + + $$OutletsTableOrderingComposer get receiveFromId { + final $$OutletsTableOrderingComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.receiveFromId, + referencedTable: $db.outlets, + getReferencedColumn: (t) => t.localId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$OutletsTableOrderingComposer( + $db: $db, + $table: $db.outlets, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } +} + +class $$StockCardsTableAnnotationComposer + extends Composer<_$AppDatabase, $StockCardsTable> { + $$StockCardsTableAnnotationComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + GeneratedColumn get localId => + $composableBuilder(column: $table.localId, builder: (column) => column); + + GeneratedColumn get serverId => + $composableBuilder(column: $table.serverId, builder: (column) => column); + + GeneratedColumn get type => + $composableBuilder(column: $table.type, builder: (column) => column); + + GeneratedColumn get qtyChange => + $composableBuilder(column: $table.qtyChange, builder: (column) => column); + + GeneratedColumn get currentStock => $composableBuilder( + column: $table.currentStock, + builder: (column) => column, + ); + + GeneratedColumn get stockType => + $composableBuilder(column: $table.stockType, builder: (column) => column); + + GeneratedColumn get modeChange => $composableBuilder( + column: $table.modeChange, + builder: (column) => column, + ); + + GeneratedColumn get modeBeforeChange => $composableBuilder( + column: $table.modeBeforeChange, + builder: (column) => column, + ); + + GeneratedColumn get modeAfterChange => $composableBuilder( + column: $table.modeAfterChange, + builder: (column) => column, + ); + + GeneratedColumn get costPrice => + $composableBuilder(column: $table.costPrice, builder: (column) => column); + + GeneratedColumn get referenceId => $composableBuilder( + column: $table.referenceId, + builder: (column) => column, + ); + + GeneratedColumn get documentNumber => $composableBuilder( + column: $table.documentNumber, + builder: (column) => column, + ); + + GeneratedColumn get notes => + $composableBuilder(column: $table.notes, builder: (column) => column); + + GeneratedColumn get syncStatus => $composableBuilder( + column: $table.syncStatus, + builder: (column) => column, + ); + + GeneratedColumn get createdAt => + $composableBuilder(column: $table.createdAt, builder: (column) => column); + + $$OutletInventoriesTableAnnotationComposer get outletInventoryId { + final $$OutletInventoriesTableAnnotationComposer composer = + $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.outletInventoryId, + referencedTable: $db.outletInventories, + getReferencedColumn: (t) => t.localId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$OutletInventoriesTableAnnotationComposer( + $db: $db, + $table: $db.outletInventories, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } + + $$UsersTableAnnotationComposer get userId { + final $$UsersTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.userId, + referencedTable: $db.users, + getReferencedColumn: (t) => t.localId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$UsersTableAnnotationComposer( + $db: $db, + $table: $db.users, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } + + $$OutletsTableAnnotationComposer get receiveFromId { + final $$OutletsTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.receiveFromId, + referencedTable: $db.outlets, + getReferencedColumn: (t) => t.localId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$OutletsTableAnnotationComposer( + $db: $db, + $table: $db.outlets, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } +} + +class $$StockCardsTableTableManager + extends + RootTableManager< + _$AppDatabase, + $StockCardsTable, + StockCard, + $$StockCardsTableFilterComposer, + $$StockCardsTableOrderingComposer, + $$StockCardsTableAnnotationComposer, + $$StockCardsTableCreateCompanionBuilder, + $$StockCardsTableUpdateCompanionBuilder, + (StockCard, $$StockCardsTableReferences), + StockCard, + PrefetchHooks Function({ + bool outletInventoryId, + bool userId, + bool receiveFromId, + }) + > { + $$StockCardsTableTableManager(_$AppDatabase db, $StockCardsTable table) + : super( + TableManagerState( + db: db, + table: table, + createFilteringComposer: () => + $$StockCardsTableFilterComposer($db: db, $table: table), + createOrderingComposer: () => + $$StockCardsTableOrderingComposer($db: db, $table: table), + createComputedFieldComposer: () => + $$StockCardsTableAnnotationComposer($db: db, $table: table), + updateCompanionCallback: + ({ + Value localId = const Value.absent(), + Value serverId = const Value.absent(), + Value outletInventoryId = const Value.absent(), + Value userId = const Value.absent(), + Value type = const Value.absent(), + Value receiveFromId = const Value.absent(), + Value qtyChange = const Value.absent(), + Value currentStock = const Value.absent(), + Value stockType = const Value.absent(), + Value modeChange = const Value.absent(), + Value modeBeforeChange = const Value.absent(), + Value modeAfterChange = const Value.absent(), + Value costPrice = const Value.absent(), + Value referenceId = const Value.absent(), + Value documentNumber = const Value.absent(), + Value notes = const Value.absent(), + Value syncStatus = const Value.absent(), + Value createdAt = const Value.absent(), + Value rowid = const Value.absent(), + }) => StockCardsCompanion( + localId: localId, + serverId: serverId, + outletInventoryId: outletInventoryId, + userId: userId, + type: type, + receiveFromId: receiveFromId, + qtyChange: qtyChange, + currentStock: currentStock, + stockType: stockType, + modeChange: modeChange, + modeBeforeChange: modeBeforeChange, + modeAfterChange: modeAfterChange, + costPrice: costPrice, + referenceId: referenceId, + documentNumber: documentNumber, + notes: notes, + syncStatus: syncStatus, + createdAt: createdAt, + rowid: rowid, + ), + createCompanionCallback: + ({ + Value localId = const Value.absent(), + Value serverId = const Value.absent(), + required String outletInventoryId, + required String userId, + required String type, + Value receiveFromId = const Value.absent(), + Value qtyChange = const Value.absent(), + Value currentStock = const Value.absent(), + Value stockType = const Value.absent(), + Value modeChange = const Value.absent(), + Value modeBeforeChange = const Value.absent(), + Value modeAfterChange = const Value.absent(), + Value costPrice = const Value.absent(), + Value referenceId = const Value.absent(), + Value documentNumber = const Value.absent(), + Value notes = const Value.absent(), + Value syncStatus = const Value.absent(), + Value createdAt = const Value.absent(), + Value rowid = const Value.absent(), + }) => StockCardsCompanion.insert( + localId: localId, + serverId: serverId, + outletInventoryId: outletInventoryId, + userId: userId, + type: type, + receiveFromId: receiveFromId, + qtyChange: qtyChange, + currentStock: currentStock, + stockType: stockType, + modeChange: modeChange, + modeBeforeChange: modeBeforeChange, + modeAfterChange: modeAfterChange, + costPrice: costPrice, + referenceId: referenceId, + documentNumber: documentNumber, + notes: notes, + syncStatus: syncStatus, + createdAt: createdAt, + rowid: rowid, + ), + withReferenceMapper: (p0) => p0 + .map( + (e) => ( + e.readTable(table), + $$StockCardsTableReferences(db, table, e), + ), + ) + .toList(), + prefetchHooksCallback: + ({ + outletInventoryId = false, + userId = false, + receiveFromId = false, + }) { + return PrefetchHooks( + db: db, + explicitlyWatchedTables: [], + addJoins: + < + T extends TableManagerState< + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic + > + >(state) { + if (outletInventoryId) { + state = + state.withJoin( + currentTable: table, + currentColumn: table.outletInventoryId, + referencedTable: $$StockCardsTableReferences + ._outletInventoryIdTable(db), + referencedColumn: + $$StockCardsTableReferences + ._outletInventoryIdTable(db) + .localId, + ) + as T; + } + if (userId) { + state = + state.withJoin( + currentTable: table, + currentColumn: table.userId, + referencedTable: $$StockCardsTableReferences + ._userIdTable(db), + referencedColumn: + $$StockCardsTableReferences + ._userIdTable(db) + .localId, + ) + as T; + } + if (receiveFromId) { + state = + state.withJoin( + currentTable: table, + currentColumn: table.receiveFromId, + referencedTable: $$StockCardsTableReferences + ._receiveFromIdTable(db), + referencedColumn: + $$StockCardsTableReferences + ._receiveFromIdTable(db) + .localId, + ) + as T; + } + + return state; + }, + getPrefetchedDataCallback: (items) async { + return []; + }, + ); + }, + ), + ); +} + +typedef $$StockCardsTableProcessedTableManager = + ProcessedTableManager< + _$AppDatabase, + $StockCardsTable, + StockCard, + $$StockCardsTableFilterComposer, + $$StockCardsTableOrderingComposer, + $$StockCardsTableAnnotationComposer, + $$StockCardsTableCreateCompanionBuilder, + $$StockCardsTableUpdateCompanionBuilder, + (StockCard, $$StockCardsTableReferences), + StockCard, + PrefetchHooks Function({ + bool outletInventoryId, + bool userId, + bool receiveFromId, + }) + >; +typedef $$OrdersTableCreateCompanionBuilder = + OrdersCompanion Function({ + Value localId, + Value serverId, + required String orderNumber, + required String outletId, + required String outletNameSnapshot, + Value customerId, + Value customerNameSnapshot, + Value customerEmailSnapshot, + Value customerPhoneNumberSnapshot, + required String userId, + required String adminNameSnapshot, + required String source, + Value alreadyRead, + required String orderStatus, + Value isCancellation, + Value cancellationAccepted, + Value approvalProccess, + Value canceledBy, + Value deliveryType, + Value deliveryPrefrence, + Value shippingCost, + Value totalOrder, + Value discount, + Value tax, + Value totalAmount, + Value amountPaid, + Value changeAmount, + Value notes, + Value paymentProofPath, + Value paymentProofUrl, + Value paymentMethod, + Value?> customerAddressSnapshot, + Value syncStatus, + Value createdAt, + Value updatedAt, + Value rowid, + }); +typedef $$OrdersTableUpdateCompanionBuilder = + OrdersCompanion Function({ + Value localId, + Value serverId, + Value orderNumber, + Value outletId, + Value outletNameSnapshot, + Value customerId, + Value customerNameSnapshot, + Value customerEmailSnapshot, + Value customerPhoneNumberSnapshot, + Value userId, + Value adminNameSnapshot, + Value source, + Value alreadyRead, + Value orderStatus, + Value isCancellation, + Value cancellationAccepted, + Value approvalProccess, + Value canceledBy, + Value deliveryType, + Value deliveryPrefrence, + Value shippingCost, + Value totalOrder, + Value discount, + Value tax, + Value totalAmount, + Value amountPaid, + Value changeAmount, + Value notes, + Value paymentProofPath, + Value paymentProofUrl, + Value paymentMethod, + Value?> customerAddressSnapshot, + Value syncStatus, + Value createdAt, + Value updatedAt, + Value rowid, + }); + +final class $$OrdersTableReferences + extends BaseReferences<_$AppDatabase, $OrdersTable, Order> { + $$OrdersTableReferences(super.$_db, super.$_table, super.$_typedResult); + + static $OutletsTable _outletIdTable(_$AppDatabase db) => + db.outlets.createAlias( + $_aliasNameGenerator(db.orders.outletId, db.outlets.localId), + ); + + $$OutletsTableProcessedTableManager get outletId { + final $_column = $_itemColumn('outlet_id')!; + + final manager = $$OutletsTableTableManager( + $_db, + $_db.outlets, + ).filter((f) => f.localId.sqlEquals($_column)); + final item = $_typedResult.readTableOrNull(_outletIdTable($_db)); + if (item == null) return manager; + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: [item]), + ); + } + + static $CustomersTable _customerIdTable(_$AppDatabase db) => + db.customers.createAlias( + $_aliasNameGenerator(db.orders.customerId, db.customers.localId), + ); + + $$CustomersTableProcessedTableManager? get customerId { + final $_column = $_itemColumn('customer_id'); + if ($_column == null) return null; + final manager = $$CustomersTableTableManager( + $_db, + $_db.customers, + ).filter((f) => f.localId.sqlEquals($_column)); + final item = $_typedResult.readTableOrNull(_customerIdTable($_db)); + if (item == null) return manager; + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: [item]), + ); + } + + static $UsersTable _userIdTable(_$AppDatabase db) => db.users.createAlias( + $_aliasNameGenerator(db.orders.userId, db.users.localId), + ); + + $$UsersTableProcessedTableManager get userId { + final $_column = $_itemColumn('user_id')!; + + final manager = $$UsersTableTableManager( + $_db, + $_db.users, + ).filter((f) => f.localId.sqlEquals($_column)); + final item = $_typedResult.readTableOrNull(_userIdTable($_db)); + if (item == null) return manager; + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: [item]), + ); + } + + static MultiTypedResultKey<$OrderItemsTable, List> + _orderItemsRefsTable(_$AppDatabase db) => MultiTypedResultKey.fromTable( + db.orderItems, + aliasName: $_aliasNameGenerator(db.orders.localId, db.orderItems.orderId), + ); + + $$OrderItemsTableProcessedTableManager get orderItemsRefs { + final manager = $$OrderItemsTableTableManager($_db, $_db.orderItems).filter( + (f) => f.orderId.localId.sqlEquals($_itemColumn('local_id')!), + ); + + final cache = $_typedResult.readTableOrNull(_orderItemsRefsTable($_db)); + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: cache), + ); + } +} + +class $$OrdersTableFilterComposer + extends Composer<_$AppDatabase, $OrdersTable> { + $$OrdersTableFilterComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnFilters get localId => $composableBuilder( + column: $table.localId, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get serverId => $composableBuilder( + column: $table.serverId, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get orderNumber => $composableBuilder( + column: $table.orderNumber, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get outletNameSnapshot => $composableBuilder( + column: $table.outletNameSnapshot, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get customerNameSnapshot => $composableBuilder( + column: $table.customerNameSnapshot, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get customerEmailSnapshot => $composableBuilder( + column: $table.customerEmailSnapshot, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get customerPhoneNumberSnapshot => $composableBuilder( + column: $table.customerPhoneNumberSnapshot, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get adminNameSnapshot => $composableBuilder( + column: $table.adminNameSnapshot, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get source => $composableBuilder( + column: $table.source, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get alreadyRead => $composableBuilder( + column: $table.alreadyRead, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get orderStatus => $composableBuilder( + column: $table.orderStatus, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get isCancellation => $composableBuilder( + column: $table.isCancellation, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get cancellationAccepted => $composableBuilder( + column: $table.cancellationAccepted, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get approvalProccess => $composableBuilder( + column: $table.approvalProccess, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get canceledBy => $composableBuilder( + column: $table.canceledBy, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get deliveryType => $composableBuilder( + column: $table.deliveryType, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get deliveryPrefrence => $composableBuilder( + column: $table.deliveryPrefrence, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get shippingCost => $composableBuilder( + column: $table.shippingCost, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get totalOrder => $composableBuilder( + column: $table.totalOrder, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get discount => $composableBuilder( + column: $table.discount, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get tax => $composableBuilder( + column: $table.tax, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get totalAmount => $composableBuilder( + column: $table.totalAmount, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get amountPaid => $composableBuilder( + column: $table.amountPaid, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get changeAmount => $composableBuilder( + column: $table.changeAmount, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get notes => $composableBuilder( + column: $table.notes, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get paymentProofPath => $composableBuilder( + column: $table.paymentProofPath, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get paymentProofUrl => $composableBuilder( + column: $table.paymentProofUrl, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get paymentMethod => $composableBuilder( + column: $table.paymentMethod, + builder: (column) => ColumnFilters(column), + ); + + ColumnWithTypeConverterFilters< + Map?, + Map, + String + > + get customerAddressSnapshot => $composableBuilder( + column: $table.customerAddressSnapshot, + builder: (column) => ColumnWithTypeConverterFilters(column), + ); + + ColumnFilters get syncStatus => $composableBuilder( + column: $table.syncStatus, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get createdAt => $composableBuilder( + column: $table.createdAt, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get updatedAt => $composableBuilder( + column: $table.updatedAt, + builder: (column) => ColumnFilters(column), + ); + + $$OutletsTableFilterComposer get outletId { + final $$OutletsTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.outletId, + referencedTable: $db.outlets, + getReferencedColumn: (t) => t.localId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$OutletsTableFilterComposer( + $db: $db, + $table: $db.outlets, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } + + $$CustomersTableFilterComposer get customerId { + final $$CustomersTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.customerId, + referencedTable: $db.customers, + getReferencedColumn: (t) => t.localId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$CustomersTableFilterComposer( + $db: $db, + $table: $db.customers, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } + + $$UsersTableFilterComposer get userId { + final $$UsersTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.userId, + referencedTable: $db.users, + getReferencedColumn: (t) => t.localId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$UsersTableFilterComposer( + $db: $db, + $table: $db.users, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } + + Expression orderItemsRefs( + Expression Function($$OrderItemsTableFilterComposer f) f, + ) { + final $$OrderItemsTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.localId, + referencedTable: $db.orderItems, + getReferencedColumn: (t) => t.orderId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$OrderItemsTableFilterComposer( + $db: $db, + $table: $db.orderItems, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return f(composer); + } +} + +class $$OrdersTableOrderingComposer + extends Composer<_$AppDatabase, $OrdersTable> { + $$OrdersTableOrderingComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnOrderings get localId => $composableBuilder( + column: $table.localId, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get serverId => $composableBuilder( + column: $table.serverId, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get orderNumber => $composableBuilder( + column: $table.orderNumber, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get outletNameSnapshot => $composableBuilder( + column: $table.outletNameSnapshot, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get customerNameSnapshot => $composableBuilder( + column: $table.customerNameSnapshot, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get customerEmailSnapshot => $composableBuilder( + column: $table.customerEmailSnapshot, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get customerPhoneNumberSnapshot => $composableBuilder( + column: $table.customerPhoneNumberSnapshot, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get adminNameSnapshot => $composableBuilder( + column: $table.adminNameSnapshot, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get source => $composableBuilder( + column: $table.source, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get alreadyRead => $composableBuilder( + column: $table.alreadyRead, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get orderStatus => $composableBuilder( + column: $table.orderStatus, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get isCancellation => $composableBuilder( + column: $table.isCancellation, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get cancellationAccepted => $composableBuilder( + column: $table.cancellationAccepted, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get approvalProccess => $composableBuilder( + column: $table.approvalProccess, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get canceledBy => $composableBuilder( + column: $table.canceledBy, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get deliveryType => $composableBuilder( + column: $table.deliveryType, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get deliveryPrefrence => $composableBuilder( + column: $table.deliveryPrefrence, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get shippingCost => $composableBuilder( + column: $table.shippingCost, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get totalOrder => $composableBuilder( + column: $table.totalOrder, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get discount => $composableBuilder( + column: $table.discount, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get tax => $composableBuilder( + column: $table.tax, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get totalAmount => $composableBuilder( + column: $table.totalAmount, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get amountPaid => $composableBuilder( + column: $table.amountPaid, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get changeAmount => $composableBuilder( + column: $table.changeAmount, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get notes => $composableBuilder( + column: $table.notes, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get paymentProofPath => $composableBuilder( + column: $table.paymentProofPath, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get paymentProofUrl => $composableBuilder( + column: $table.paymentProofUrl, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get paymentMethod => $composableBuilder( + column: $table.paymentMethod, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get customerAddressSnapshot => $composableBuilder( + column: $table.customerAddressSnapshot, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get syncStatus => $composableBuilder( + column: $table.syncStatus, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get createdAt => $composableBuilder( + column: $table.createdAt, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get updatedAt => $composableBuilder( + column: $table.updatedAt, + builder: (column) => ColumnOrderings(column), + ); + + $$OutletsTableOrderingComposer get outletId { + final $$OutletsTableOrderingComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.outletId, + referencedTable: $db.outlets, + getReferencedColumn: (t) => t.localId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$OutletsTableOrderingComposer( + $db: $db, + $table: $db.outlets, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } + + $$CustomersTableOrderingComposer get customerId { + final $$CustomersTableOrderingComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.customerId, + referencedTable: $db.customers, + getReferencedColumn: (t) => t.localId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$CustomersTableOrderingComposer( + $db: $db, + $table: $db.customers, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } + + $$UsersTableOrderingComposer get userId { + final $$UsersTableOrderingComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.userId, + referencedTable: $db.users, + getReferencedColumn: (t) => t.localId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$UsersTableOrderingComposer( + $db: $db, + $table: $db.users, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } +} + +class $$OrdersTableAnnotationComposer + extends Composer<_$AppDatabase, $OrdersTable> { + $$OrdersTableAnnotationComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + GeneratedColumn get localId => + $composableBuilder(column: $table.localId, builder: (column) => column); + + GeneratedColumn get serverId => + $composableBuilder(column: $table.serverId, builder: (column) => column); + + GeneratedColumn get orderNumber => $composableBuilder( + column: $table.orderNumber, + builder: (column) => column, + ); + + GeneratedColumn get outletNameSnapshot => $composableBuilder( + column: $table.outletNameSnapshot, + builder: (column) => column, + ); + + GeneratedColumn get customerNameSnapshot => $composableBuilder( + column: $table.customerNameSnapshot, + builder: (column) => column, + ); + + GeneratedColumn get customerEmailSnapshot => $composableBuilder( + column: $table.customerEmailSnapshot, + builder: (column) => column, + ); + + GeneratedColumn get customerPhoneNumberSnapshot => $composableBuilder( + column: $table.customerPhoneNumberSnapshot, + builder: (column) => column, + ); + + GeneratedColumn get adminNameSnapshot => $composableBuilder( + column: $table.adminNameSnapshot, + builder: (column) => column, + ); + + GeneratedColumn get source => + $composableBuilder(column: $table.source, builder: (column) => column); + + GeneratedColumn get alreadyRead => $composableBuilder( + column: $table.alreadyRead, + builder: (column) => column, + ); + + GeneratedColumn get orderStatus => $composableBuilder( + column: $table.orderStatus, + builder: (column) => column, + ); + + GeneratedColumn get isCancellation => $composableBuilder( + column: $table.isCancellation, + builder: (column) => column, + ); + + GeneratedColumn get cancellationAccepted => $composableBuilder( + column: $table.cancellationAccepted, + builder: (column) => column, + ); + + GeneratedColumn get approvalProccess => $composableBuilder( + column: $table.approvalProccess, + builder: (column) => column, + ); + + GeneratedColumn get canceledBy => $composableBuilder( + column: $table.canceledBy, + builder: (column) => column, + ); + + GeneratedColumn get deliveryType => $composableBuilder( + column: $table.deliveryType, + builder: (column) => column, + ); + + GeneratedColumn get deliveryPrefrence => $composableBuilder( + column: $table.deliveryPrefrence, + builder: (column) => column, + ); + + GeneratedColumn get shippingCost => $composableBuilder( + column: $table.shippingCost, + builder: (column) => column, + ); + + GeneratedColumn get totalOrder => $composableBuilder( + column: $table.totalOrder, + builder: (column) => column, + ); + + GeneratedColumn get discount => + $composableBuilder(column: $table.discount, builder: (column) => column); + + GeneratedColumn get tax => + $composableBuilder(column: $table.tax, builder: (column) => column); + + GeneratedColumn get totalAmount => $composableBuilder( + column: $table.totalAmount, + builder: (column) => column, + ); + + GeneratedColumn get amountPaid => $composableBuilder( + column: $table.amountPaid, + builder: (column) => column, + ); + + GeneratedColumn get changeAmount => $composableBuilder( + column: $table.changeAmount, + builder: (column) => column, + ); + + GeneratedColumn get notes => + $composableBuilder(column: $table.notes, builder: (column) => column); + + GeneratedColumn get paymentProofPath => $composableBuilder( + column: $table.paymentProofPath, + builder: (column) => column, + ); + + GeneratedColumn get paymentProofUrl => $composableBuilder( + column: $table.paymentProofUrl, + builder: (column) => column, + ); + + GeneratedColumn get paymentMethod => $composableBuilder( + column: $table.paymentMethod, + builder: (column) => column, + ); + + GeneratedColumnWithTypeConverter?, String> + get customerAddressSnapshot => $composableBuilder( + column: $table.customerAddressSnapshot, + builder: (column) => column, + ); + + GeneratedColumn get syncStatus => $composableBuilder( + column: $table.syncStatus, + builder: (column) => column, + ); + + GeneratedColumn get createdAt => + $composableBuilder(column: $table.createdAt, builder: (column) => column); + + GeneratedColumn get updatedAt => + $composableBuilder(column: $table.updatedAt, builder: (column) => column); + + $$OutletsTableAnnotationComposer get outletId { + final $$OutletsTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.outletId, + referencedTable: $db.outlets, + getReferencedColumn: (t) => t.localId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$OutletsTableAnnotationComposer( + $db: $db, + $table: $db.outlets, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } + + $$CustomersTableAnnotationComposer get customerId { + final $$CustomersTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.customerId, + referencedTable: $db.customers, + getReferencedColumn: (t) => t.localId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$CustomersTableAnnotationComposer( + $db: $db, + $table: $db.customers, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } + + $$UsersTableAnnotationComposer get userId { + final $$UsersTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.userId, + referencedTable: $db.users, + getReferencedColumn: (t) => t.localId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$UsersTableAnnotationComposer( + $db: $db, + $table: $db.users, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } + + Expression orderItemsRefs( + Expression Function($$OrderItemsTableAnnotationComposer a) f, + ) { + final $$OrderItemsTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.localId, + referencedTable: $db.orderItems, + getReferencedColumn: (t) => t.orderId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$OrderItemsTableAnnotationComposer( + $db: $db, + $table: $db.orderItems, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return f(composer); + } +} + +class $$OrdersTableTableManager + extends + RootTableManager< + _$AppDatabase, + $OrdersTable, + Order, + $$OrdersTableFilterComposer, + $$OrdersTableOrderingComposer, + $$OrdersTableAnnotationComposer, + $$OrdersTableCreateCompanionBuilder, + $$OrdersTableUpdateCompanionBuilder, + (Order, $$OrdersTableReferences), + Order, + PrefetchHooks Function({ + bool outletId, + bool customerId, + bool userId, + bool orderItemsRefs, + }) + > { + $$OrdersTableTableManager(_$AppDatabase db, $OrdersTable table) + : super( + TableManagerState( + db: db, + table: table, + createFilteringComposer: () => + $$OrdersTableFilterComposer($db: db, $table: table), + createOrderingComposer: () => + $$OrdersTableOrderingComposer($db: db, $table: table), + createComputedFieldComposer: () => + $$OrdersTableAnnotationComposer($db: db, $table: table), + updateCompanionCallback: + ({ + Value localId = const Value.absent(), + Value serverId = const Value.absent(), + Value orderNumber = const Value.absent(), + Value outletId = const Value.absent(), + Value outletNameSnapshot = const Value.absent(), + Value customerId = const Value.absent(), + Value customerNameSnapshot = const Value.absent(), + Value customerEmailSnapshot = const Value.absent(), + Value customerPhoneNumberSnapshot = + const Value.absent(), + Value userId = const Value.absent(), + Value adminNameSnapshot = const Value.absent(), + Value source = const Value.absent(), + Value alreadyRead = const Value.absent(), + Value orderStatus = const Value.absent(), + Value isCancellation = const Value.absent(), + Value cancellationAccepted = const Value.absent(), + Value approvalProccess = const Value.absent(), + Value canceledBy = const Value.absent(), + Value deliveryType = const Value.absent(), + Value deliveryPrefrence = const Value.absent(), + Value shippingCost = const Value.absent(), + Value totalOrder = const Value.absent(), + Value discount = const Value.absent(), + Value tax = const Value.absent(), + Value totalAmount = const Value.absent(), + Value amountPaid = const Value.absent(), + Value changeAmount = const Value.absent(), + Value notes = const Value.absent(), + Value paymentProofPath = const Value.absent(), + Value paymentProofUrl = const Value.absent(), + Value paymentMethod = const Value.absent(), + Value?> customerAddressSnapshot = + const Value.absent(), + Value syncStatus = const Value.absent(), + Value createdAt = const Value.absent(), + Value updatedAt = const Value.absent(), + Value rowid = const Value.absent(), + }) => OrdersCompanion( + localId: localId, + serverId: serverId, + orderNumber: orderNumber, + outletId: outletId, + outletNameSnapshot: outletNameSnapshot, + customerId: customerId, + customerNameSnapshot: customerNameSnapshot, + customerEmailSnapshot: customerEmailSnapshot, + customerPhoneNumberSnapshot: customerPhoneNumberSnapshot, + userId: userId, + adminNameSnapshot: adminNameSnapshot, + source: source, + alreadyRead: alreadyRead, + orderStatus: orderStatus, + isCancellation: isCancellation, + cancellationAccepted: cancellationAccepted, + approvalProccess: approvalProccess, + canceledBy: canceledBy, + deliveryType: deliveryType, + deliveryPrefrence: deliveryPrefrence, + shippingCost: shippingCost, + totalOrder: totalOrder, + discount: discount, + tax: tax, + totalAmount: totalAmount, + amountPaid: amountPaid, + changeAmount: changeAmount, + notes: notes, + paymentProofPath: paymentProofPath, + paymentProofUrl: paymentProofUrl, + paymentMethod: paymentMethod, + customerAddressSnapshot: customerAddressSnapshot, + syncStatus: syncStatus, + createdAt: createdAt, + updatedAt: updatedAt, + rowid: rowid, + ), + createCompanionCallback: + ({ + Value localId = const Value.absent(), + Value serverId = const Value.absent(), + required String orderNumber, + required String outletId, + required String outletNameSnapshot, + Value customerId = const Value.absent(), + Value customerNameSnapshot = const Value.absent(), + Value customerEmailSnapshot = const Value.absent(), + Value customerPhoneNumberSnapshot = + const Value.absent(), + required String userId, + required String adminNameSnapshot, + required String source, + Value alreadyRead = const Value.absent(), + required String orderStatus, + Value isCancellation = const Value.absent(), + Value cancellationAccepted = const Value.absent(), + Value approvalProccess = const Value.absent(), + Value canceledBy = const Value.absent(), + Value deliveryType = const Value.absent(), + Value deliveryPrefrence = const Value.absent(), + Value shippingCost = const Value.absent(), + Value totalOrder = const Value.absent(), + Value discount = const Value.absent(), + Value tax = const Value.absent(), + Value totalAmount = const Value.absent(), + Value amountPaid = const Value.absent(), + Value changeAmount = const Value.absent(), + Value notes = const Value.absent(), + Value paymentProofPath = const Value.absent(), + Value paymentProofUrl = const Value.absent(), + Value paymentMethod = const Value.absent(), + Value?> customerAddressSnapshot = + const Value.absent(), + Value syncStatus = const Value.absent(), + Value createdAt = const Value.absent(), + Value updatedAt = const Value.absent(), + Value rowid = const Value.absent(), + }) => OrdersCompanion.insert( + localId: localId, + serverId: serverId, + orderNumber: orderNumber, + outletId: outletId, + outletNameSnapshot: outletNameSnapshot, + customerId: customerId, + customerNameSnapshot: customerNameSnapshot, + customerEmailSnapshot: customerEmailSnapshot, + customerPhoneNumberSnapshot: customerPhoneNumberSnapshot, + userId: userId, + adminNameSnapshot: adminNameSnapshot, + source: source, + alreadyRead: alreadyRead, + orderStatus: orderStatus, + isCancellation: isCancellation, + cancellationAccepted: cancellationAccepted, + approvalProccess: approvalProccess, + canceledBy: canceledBy, + deliveryType: deliveryType, + deliveryPrefrence: deliveryPrefrence, + shippingCost: shippingCost, + totalOrder: totalOrder, + discount: discount, + tax: tax, + totalAmount: totalAmount, + amountPaid: amountPaid, + changeAmount: changeAmount, + notes: notes, + paymentProofPath: paymentProofPath, + paymentProofUrl: paymentProofUrl, + paymentMethod: paymentMethod, + customerAddressSnapshot: customerAddressSnapshot, + syncStatus: syncStatus, + createdAt: createdAt, + updatedAt: updatedAt, + rowid: rowid, + ), + withReferenceMapper: (p0) => p0 + .map( + (e) => + (e.readTable(table), $$OrdersTableReferences(db, table, e)), + ) + .toList(), + prefetchHooksCallback: + ({ + outletId = false, + customerId = false, + userId = false, + orderItemsRefs = false, + }) { + return PrefetchHooks( + db: db, + explicitlyWatchedTables: [if (orderItemsRefs) db.orderItems], + addJoins: + < + T extends TableManagerState< + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic + > + >(state) { + if (outletId) { + state = + state.withJoin( + currentTable: table, + currentColumn: table.outletId, + referencedTable: $$OrdersTableReferences + ._outletIdTable(db), + referencedColumn: $$OrdersTableReferences + ._outletIdTable(db) + .localId, + ) + as T; + } + if (customerId) { + state = + state.withJoin( + currentTable: table, + currentColumn: table.customerId, + referencedTable: $$OrdersTableReferences + ._customerIdTable(db), + referencedColumn: $$OrdersTableReferences + ._customerIdTable(db) + .localId, + ) + as T; + } + if (userId) { + state = + state.withJoin( + currentTable: table, + currentColumn: table.userId, + referencedTable: $$OrdersTableReferences + ._userIdTable(db), + referencedColumn: $$OrdersTableReferences + ._userIdTable(db) + .localId, + ) + as T; + } + + return state; + }, + getPrefetchedDataCallback: (items) async { + return [ + if (orderItemsRefs) + await $_getPrefetchedData< + Order, + $OrdersTable, + OrderItem + >( + currentTable: table, + referencedTable: $$OrdersTableReferences + ._orderItemsRefsTable(db), + managerFromTypedResult: (p0) => + $$OrdersTableReferences( + db, + table, + p0, + ).orderItemsRefs, + referencedItemsForCurrentItem: + (item, referencedItems) => referencedItems.where( + (e) => e.orderId == item.localId, + ), + typedResults: items, + ), + ]; + }, + ); + }, + ), + ); +} + +typedef $$OrdersTableProcessedTableManager = + ProcessedTableManager< + _$AppDatabase, + $OrdersTable, + Order, + $$OrdersTableFilterComposer, + $$OrdersTableOrderingComposer, + $$OrdersTableAnnotationComposer, + $$OrdersTableCreateCompanionBuilder, + $$OrdersTableUpdateCompanionBuilder, + (Order, $$OrdersTableReferences), + Order, + PrefetchHooks Function({ + bool outletId, + bool customerId, + bool userId, + bool orderItemsRefs, + }) + >; +typedef $$OrderItemsTableCreateCompanionBuilder = + OrderItemsCompanion Function({ + Value localId, + Value serverId, + required String orderId, + required String productVariantId, + Value quantity, + Value productImageSnapshot, + Value productImageUrlSnapshot, + required String productNameSnapshot, + Value productVariantNameSnapshot, + Value sellingPriceSnapshot, + Value subtotal, + Value syncStatus, + Value createdAt, + Value updatedAt, + Value rowid, + }); +typedef $$OrderItemsTableUpdateCompanionBuilder = + OrderItemsCompanion Function({ + Value localId, + Value serverId, + Value orderId, + Value productVariantId, + Value quantity, + Value productImageSnapshot, + Value productImageUrlSnapshot, + Value productNameSnapshot, + Value productVariantNameSnapshot, + Value sellingPriceSnapshot, + Value subtotal, + Value syncStatus, + Value createdAt, + Value updatedAt, + Value rowid, + }); + +final class $$OrderItemsTableReferences + extends BaseReferences<_$AppDatabase, $OrderItemsTable, OrderItem> { + $$OrderItemsTableReferences(super.$_db, super.$_table, super.$_typedResult); + + static $OrdersTable _orderIdTable(_$AppDatabase db) => db.orders.createAlias( + $_aliasNameGenerator(db.orderItems.orderId, db.orders.localId), + ); + + $$OrdersTableProcessedTableManager get orderId { + final $_column = $_itemColumn('order_id')!; + + final manager = $$OrdersTableTableManager( + $_db, + $_db.orders, + ).filter((f) => f.localId.sqlEquals($_column)); + final item = $_typedResult.readTableOrNull(_orderIdTable($_db)); + if (item == null) return manager; + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: [item]), + ); + } + + static $ProductVariantsTable _productVariantIdTable(_$AppDatabase db) => + db.productVariants.createAlias( + $_aliasNameGenerator( + db.orderItems.productVariantId, + db.productVariants.localId, + ), + ); + + $$ProductVariantsTableProcessedTableManager get productVariantId { + final $_column = $_itemColumn('product_variant_id')!; + + final manager = $$ProductVariantsTableTableManager( + $_db, + $_db.productVariants, + ).filter((f) => f.localId.sqlEquals($_column)); + final item = $_typedResult.readTableOrNull(_productVariantIdTable($_db)); + if (item == null) return manager; + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: [item]), + ); + } +} + +class $$OrderItemsTableFilterComposer + extends Composer<_$AppDatabase, $OrderItemsTable> { + $$OrderItemsTableFilterComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnFilters get localId => $composableBuilder( + column: $table.localId, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get serverId => $composableBuilder( + column: $table.serverId, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get quantity => $composableBuilder( + column: $table.quantity, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get productImageSnapshot => $composableBuilder( + column: $table.productImageSnapshot, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get productImageUrlSnapshot => $composableBuilder( + column: $table.productImageUrlSnapshot, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get productNameSnapshot => $composableBuilder( + column: $table.productNameSnapshot, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get productVariantNameSnapshot => $composableBuilder( + column: $table.productVariantNameSnapshot, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get sellingPriceSnapshot => $composableBuilder( + column: $table.sellingPriceSnapshot, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get subtotal => $composableBuilder( + column: $table.subtotal, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get syncStatus => $composableBuilder( + column: $table.syncStatus, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get createdAt => $composableBuilder( + column: $table.createdAt, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get updatedAt => $composableBuilder( + column: $table.updatedAt, + builder: (column) => ColumnFilters(column), + ); + + $$OrdersTableFilterComposer get orderId { + final $$OrdersTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.orderId, + referencedTable: $db.orders, + getReferencedColumn: (t) => t.localId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$OrdersTableFilterComposer( + $db: $db, + $table: $db.orders, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } + + $$ProductVariantsTableFilterComposer get productVariantId { + final $$ProductVariantsTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.productVariantId, + referencedTable: $db.productVariants, + getReferencedColumn: (t) => t.localId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$ProductVariantsTableFilterComposer( + $db: $db, + $table: $db.productVariants, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } +} + +class $$OrderItemsTableOrderingComposer + extends Composer<_$AppDatabase, $OrderItemsTable> { + $$OrderItemsTableOrderingComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnOrderings get localId => $composableBuilder( + column: $table.localId, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get serverId => $composableBuilder( + column: $table.serverId, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get quantity => $composableBuilder( + column: $table.quantity, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get productImageSnapshot => $composableBuilder( + column: $table.productImageSnapshot, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get productImageUrlSnapshot => $composableBuilder( + column: $table.productImageUrlSnapshot, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get productNameSnapshot => $composableBuilder( + column: $table.productNameSnapshot, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get productVariantNameSnapshot => $composableBuilder( + column: $table.productVariantNameSnapshot, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get sellingPriceSnapshot => $composableBuilder( + column: $table.sellingPriceSnapshot, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get subtotal => $composableBuilder( + column: $table.subtotal, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get syncStatus => $composableBuilder( + column: $table.syncStatus, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get createdAt => $composableBuilder( + column: $table.createdAt, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get updatedAt => $composableBuilder( + column: $table.updatedAt, + builder: (column) => ColumnOrderings(column), + ); + + $$OrdersTableOrderingComposer get orderId { + final $$OrdersTableOrderingComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.orderId, + referencedTable: $db.orders, + getReferencedColumn: (t) => t.localId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$OrdersTableOrderingComposer( + $db: $db, + $table: $db.orders, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } + + $$ProductVariantsTableOrderingComposer get productVariantId { + final $$ProductVariantsTableOrderingComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.productVariantId, + referencedTable: $db.productVariants, + getReferencedColumn: (t) => t.localId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$ProductVariantsTableOrderingComposer( + $db: $db, + $table: $db.productVariants, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } +} + +class $$OrderItemsTableAnnotationComposer + extends Composer<_$AppDatabase, $OrderItemsTable> { + $$OrderItemsTableAnnotationComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + GeneratedColumn get localId => + $composableBuilder(column: $table.localId, builder: (column) => column); + + GeneratedColumn get serverId => + $composableBuilder(column: $table.serverId, builder: (column) => column); + + GeneratedColumn get quantity => + $composableBuilder(column: $table.quantity, builder: (column) => column); + + GeneratedColumn get productImageSnapshot => $composableBuilder( + column: $table.productImageSnapshot, + builder: (column) => column, + ); + + GeneratedColumn get productImageUrlSnapshot => $composableBuilder( + column: $table.productImageUrlSnapshot, + builder: (column) => column, + ); + + GeneratedColumn get productNameSnapshot => $composableBuilder( + column: $table.productNameSnapshot, + builder: (column) => column, + ); + + GeneratedColumn get productVariantNameSnapshot => $composableBuilder( + column: $table.productVariantNameSnapshot, + builder: (column) => column, + ); + + GeneratedColumn get sellingPriceSnapshot => $composableBuilder( + column: $table.sellingPriceSnapshot, + builder: (column) => column, + ); + + GeneratedColumn get subtotal => + $composableBuilder(column: $table.subtotal, builder: (column) => column); + + GeneratedColumn get syncStatus => $composableBuilder( + column: $table.syncStatus, + builder: (column) => column, + ); + + GeneratedColumn get createdAt => + $composableBuilder(column: $table.createdAt, builder: (column) => column); + + GeneratedColumn get updatedAt => + $composableBuilder(column: $table.updatedAt, builder: (column) => column); + + $$OrdersTableAnnotationComposer get orderId { + final $$OrdersTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.orderId, + referencedTable: $db.orders, + getReferencedColumn: (t) => t.localId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$OrdersTableAnnotationComposer( + $db: $db, + $table: $db.orders, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } + + $$ProductVariantsTableAnnotationComposer get productVariantId { + final $$ProductVariantsTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.productVariantId, + referencedTable: $db.productVariants, + getReferencedColumn: (t) => t.localId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$ProductVariantsTableAnnotationComposer( + $db: $db, + $table: $db.productVariants, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } +} + +class $$OrderItemsTableTableManager + extends + RootTableManager< + _$AppDatabase, + $OrderItemsTable, + OrderItem, + $$OrderItemsTableFilterComposer, + $$OrderItemsTableOrderingComposer, + $$OrderItemsTableAnnotationComposer, + $$OrderItemsTableCreateCompanionBuilder, + $$OrderItemsTableUpdateCompanionBuilder, + (OrderItem, $$OrderItemsTableReferences), + OrderItem, + PrefetchHooks Function({bool orderId, bool productVariantId}) + > { + $$OrderItemsTableTableManager(_$AppDatabase db, $OrderItemsTable table) + : super( + TableManagerState( + db: db, + table: table, + createFilteringComposer: () => + $$OrderItemsTableFilterComposer($db: db, $table: table), + createOrderingComposer: () => + $$OrderItemsTableOrderingComposer($db: db, $table: table), + createComputedFieldComposer: () => + $$OrderItemsTableAnnotationComposer($db: db, $table: table), + updateCompanionCallback: + ({ + Value localId = const Value.absent(), + Value serverId = const Value.absent(), + Value orderId = const Value.absent(), + Value productVariantId = const Value.absent(), + Value quantity = const Value.absent(), + Value productImageSnapshot = const Value.absent(), + Value productImageUrlSnapshot = const Value.absent(), + Value productNameSnapshot = const Value.absent(), + Value productVariantNameSnapshot = + const Value.absent(), + Value sellingPriceSnapshot = const Value.absent(), + Value subtotal = const Value.absent(), + Value syncStatus = const Value.absent(), + Value createdAt = const Value.absent(), + Value updatedAt = const Value.absent(), + Value rowid = const Value.absent(), + }) => OrderItemsCompanion( + localId: localId, + serverId: serverId, + orderId: orderId, + productVariantId: productVariantId, + quantity: quantity, + productImageSnapshot: productImageSnapshot, + productImageUrlSnapshot: productImageUrlSnapshot, + productNameSnapshot: productNameSnapshot, + productVariantNameSnapshot: productVariantNameSnapshot, + sellingPriceSnapshot: sellingPriceSnapshot, + subtotal: subtotal, + syncStatus: syncStatus, + createdAt: createdAt, + updatedAt: updatedAt, + rowid: rowid, + ), + createCompanionCallback: + ({ + Value localId = const Value.absent(), + Value serverId = const Value.absent(), + required String orderId, + required String productVariantId, + Value quantity = const Value.absent(), + Value productImageSnapshot = const Value.absent(), + Value productImageUrlSnapshot = const Value.absent(), + required String productNameSnapshot, + Value productVariantNameSnapshot = + const Value.absent(), + Value sellingPriceSnapshot = const Value.absent(), + Value subtotal = const Value.absent(), + Value syncStatus = const Value.absent(), + Value createdAt = const Value.absent(), + Value updatedAt = const Value.absent(), + Value rowid = const Value.absent(), + }) => OrderItemsCompanion.insert( + localId: localId, + serverId: serverId, + orderId: orderId, + productVariantId: productVariantId, + quantity: quantity, + productImageSnapshot: productImageSnapshot, + productImageUrlSnapshot: productImageUrlSnapshot, + productNameSnapshot: productNameSnapshot, + productVariantNameSnapshot: productVariantNameSnapshot, + sellingPriceSnapshot: sellingPriceSnapshot, + subtotal: subtotal, + syncStatus: syncStatus, + createdAt: createdAt, + updatedAt: updatedAt, + rowid: rowid, + ), + withReferenceMapper: (p0) => p0 + .map( + (e) => ( + e.readTable(table), + $$OrderItemsTableReferences(db, table, e), + ), + ) + .toList(), + prefetchHooksCallback: ({orderId = false, productVariantId = false}) { + return PrefetchHooks( + db: db, + explicitlyWatchedTables: [], + addJoins: + < + T extends TableManagerState< + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic + > + >(state) { + if (orderId) { + state = + state.withJoin( + currentTable: table, + currentColumn: table.orderId, + referencedTable: $$OrderItemsTableReferences + ._orderIdTable(db), + referencedColumn: $$OrderItemsTableReferences + ._orderIdTable(db) + .localId, + ) + as T; + } + if (productVariantId) { + state = + state.withJoin( + currentTable: table, + currentColumn: table.productVariantId, + referencedTable: $$OrderItemsTableReferences + ._productVariantIdTable(db), + referencedColumn: $$OrderItemsTableReferences + ._productVariantIdTable(db) + .localId, + ) + as T; + } + + return state; + }, + getPrefetchedDataCallback: (items) async { + return []; + }, + ); + }, + ), + ); +} + +typedef $$OrderItemsTableProcessedTableManager = + ProcessedTableManager< + _$AppDatabase, + $OrderItemsTable, + OrderItem, + $$OrderItemsTableFilterComposer, + $$OrderItemsTableOrderingComposer, + $$OrderItemsTableAnnotationComposer, + $$OrderItemsTableCreateCompanionBuilder, + $$OrderItemsTableUpdateCompanionBuilder, + (OrderItem, $$OrderItemsTableReferences), + OrderItem, + PrefetchHooks Function({bool orderId, bool productVariantId}) + >; +typedef $$SuppliersTableCreateCompanionBuilder = + SuppliersCompanion Function({ + Value localId, + Value serverId, + required String name, + Value email, + Value phoneNumber, + Value address, + Value syncStatus, + Value createdAt, + Value updatedAt, + Value deletedAt, + Value rowid, + }); +typedef $$SuppliersTableUpdateCompanionBuilder = + SuppliersCompanion Function({ + Value localId, + Value serverId, + Value name, + Value email, + Value phoneNumber, + Value address, + Value syncStatus, + Value createdAt, + Value updatedAt, + Value deletedAt, + Value rowid, + }); + +final class $$SuppliersTableReferences + extends BaseReferences<_$AppDatabase, $SuppliersTable, Supplier> { + $$SuppliersTableReferences(super.$_db, super.$_table, super.$_typedResult); + + static MultiTypedResultKey<$PurchasesTable, List> + _purchasesRefsTable(_$AppDatabase db) => MultiTypedResultKey.fromTable( + db.purchases, + aliasName: $_aliasNameGenerator( + db.suppliers.localId, + db.purchases.supplierId, + ), + ); + + $$PurchasesTableProcessedTableManager get purchasesRefs { + final manager = $$PurchasesTableTableManager($_db, $_db.purchases).filter( + (f) => f.supplierId.localId.sqlEquals($_itemColumn('local_id')!), + ); + + final cache = $_typedResult.readTableOrNull(_purchasesRefsTable($_db)); + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: cache), + ); + } +} + +class $$SuppliersTableFilterComposer + extends Composer<_$AppDatabase, $SuppliersTable> { + $$SuppliersTableFilterComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnFilters get localId => $composableBuilder( + column: $table.localId, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get serverId => $composableBuilder( + column: $table.serverId, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get name => $composableBuilder( + column: $table.name, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get email => $composableBuilder( + column: $table.email, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get phoneNumber => $composableBuilder( + column: $table.phoneNumber, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get address => $composableBuilder( + column: $table.address, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get syncStatus => $composableBuilder( + column: $table.syncStatus, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get createdAt => $composableBuilder( + column: $table.createdAt, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get updatedAt => $composableBuilder( + column: $table.updatedAt, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get deletedAt => $composableBuilder( + column: $table.deletedAt, + builder: (column) => ColumnFilters(column), + ); + + Expression purchasesRefs( + Expression Function($$PurchasesTableFilterComposer f) f, + ) { + final $$PurchasesTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.localId, + referencedTable: $db.purchases, + getReferencedColumn: (t) => t.supplierId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$PurchasesTableFilterComposer( + $db: $db, + $table: $db.purchases, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return f(composer); + } +} + +class $$SuppliersTableOrderingComposer + extends Composer<_$AppDatabase, $SuppliersTable> { + $$SuppliersTableOrderingComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnOrderings get localId => $composableBuilder( + column: $table.localId, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get serverId => $composableBuilder( + column: $table.serverId, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get name => $composableBuilder( + column: $table.name, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get email => $composableBuilder( + column: $table.email, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get phoneNumber => $composableBuilder( + column: $table.phoneNumber, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get address => $composableBuilder( + column: $table.address, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get syncStatus => $composableBuilder( + column: $table.syncStatus, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get createdAt => $composableBuilder( + column: $table.createdAt, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get updatedAt => $composableBuilder( + column: $table.updatedAt, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get deletedAt => $composableBuilder( + column: $table.deletedAt, + builder: (column) => ColumnOrderings(column), + ); +} + +class $$SuppliersTableAnnotationComposer + extends Composer<_$AppDatabase, $SuppliersTable> { + $$SuppliersTableAnnotationComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + GeneratedColumn get localId => + $composableBuilder(column: $table.localId, builder: (column) => column); + + GeneratedColumn get serverId => + $composableBuilder(column: $table.serverId, builder: (column) => column); + + GeneratedColumn get name => + $composableBuilder(column: $table.name, builder: (column) => column); + + GeneratedColumn get email => + $composableBuilder(column: $table.email, builder: (column) => column); + + GeneratedColumn get phoneNumber => $composableBuilder( + column: $table.phoneNumber, + builder: (column) => column, + ); + + GeneratedColumn get address => + $composableBuilder(column: $table.address, builder: (column) => column); + + GeneratedColumn get syncStatus => $composableBuilder( + column: $table.syncStatus, + builder: (column) => column, + ); + + GeneratedColumn get createdAt => + $composableBuilder(column: $table.createdAt, builder: (column) => column); + + GeneratedColumn get updatedAt => + $composableBuilder(column: $table.updatedAt, builder: (column) => column); + + GeneratedColumn get deletedAt => + $composableBuilder(column: $table.deletedAt, builder: (column) => column); + + Expression purchasesRefs( + Expression Function($$PurchasesTableAnnotationComposer a) f, + ) { + final $$PurchasesTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.localId, + referencedTable: $db.purchases, + getReferencedColumn: (t) => t.supplierId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$PurchasesTableAnnotationComposer( + $db: $db, + $table: $db.purchases, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return f(composer); + } +} + +class $$SuppliersTableTableManager + extends + RootTableManager< + _$AppDatabase, + $SuppliersTable, + Supplier, + $$SuppliersTableFilterComposer, + $$SuppliersTableOrderingComposer, + $$SuppliersTableAnnotationComposer, + $$SuppliersTableCreateCompanionBuilder, + $$SuppliersTableUpdateCompanionBuilder, + (Supplier, $$SuppliersTableReferences), + Supplier, + PrefetchHooks Function({bool purchasesRefs}) + > { + $$SuppliersTableTableManager(_$AppDatabase db, $SuppliersTable table) + : super( + TableManagerState( + db: db, + table: table, + createFilteringComposer: () => + $$SuppliersTableFilterComposer($db: db, $table: table), + createOrderingComposer: () => + $$SuppliersTableOrderingComposer($db: db, $table: table), + createComputedFieldComposer: () => + $$SuppliersTableAnnotationComposer($db: db, $table: table), + updateCompanionCallback: + ({ + Value localId = const Value.absent(), + Value serverId = const Value.absent(), + Value name = const Value.absent(), + Value email = const Value.absent(), + Value phoneNumber = const Value.absent(), + Value address = const Value.absent(), + Value syncStatus = const Value.absent(), + Value createdAt = const Value.absent(), + Value updatedAt = const Value.absent(), + Value deletedAt = const Value.absent(), + Value rowid = const Value.absent(), + }) => SuppliersCompanion( + localId: localId, + serverId: serverId, + name: name, + email: email, + phoneNumber: phoneNumber, + address: address, + syncStatus: syncStatus, + createdAt: createdAt, + updatedAt: updatedAt, + deletedAt: deletedAt, + rowid: rowid, + ), + createCompanionCallback: + ({ + Value localId = const Value.absent(), + Value serverId = const Value.absent(), + required String name, + Value email = const Value.absent(), + Value phoneNumber = const Value.absent(), + Value address = const Value.absent(), + Value syncStatus = const Value.absent(), + Value createdAt = const Value.absent(), + Value updatedAt = const Value.absent(), + Value deletedAt = const Value.absent(), + Value rowid = const Value.absent(), + }) => SuppliersCompanion.insert( + localId: localId, + serverId: serverId, + name: name, + email: email, + phoneNumber: phoneNumber, + address: address, + syncStatus: syncStatus, + createdAt: createdAt, + updatedAt: updatedAt, + deletedAt: deletedAt, + rowid: rowid, + ), + withReferenceMapper: (p0) => p0 + .map( + (e) => ( + e.readTable(table), + $$SuppliersTableReferences(db, table, e), + ), + ) + .toList(), + prefetchHooksCallback: ({purchasesRefs = false}) { + return PrefetchHooks( + db: db, + explicitlyWatchedTables: [if (purchasesRefs) db.purchases], + addJoins: null, + getPrefetchedDataCallback: (items) async { + return [ + if (purchasesRefs) + await $_getPrefetchedData< + Supplier, + $SuppliersTable, + Purchase + >( + currentTable: table, + referencedTable: $$SuppliersTableReferences + ._purchasesRefsTable(db), + managerFromTypedResult: (p0) => + $$SuppliersTableReferences( + db, + table, + p0, + ).purchasesRefs, + referencedItemsForCurrentItem: (item, referencedItems) => + referencedItems.where( + (e) => e.supplierId == item.localId, + ), + typedResults: items, + ), + ]; + }, + ); + }, + ), + ); +} + +typedef $$SuppliersTableProcessedTableManager = + ProcessedTableManager< + _$AppDatabase, + $SuppliersTable, + Supplier, + $$SuppliersTableFilterComposer, + $$SuppliersTableOrderingComposer, + $$SuppliersTableAnnotationComposer, + $$SuppliersTableCreateCompanionBuilder, + $$SuppliersTableUpdateCompanionBuilder, + (Supplier, $$SuppliersTableReferences), + Supplier, + PrefetchHooks Function({bool purchasesRefs}) + >; +typedef $$PurchasesTableCreateCompanionBuilder = + PurchasesCompanion Function({ + Value localId, + Value serverId, + required String purchaseNumber, + required String outletId, + required String outletNameSnapshot, + Value supplierId, + Value supplierNameSnapshot, + Value supplierEmailSnapshot, + Value supplierPhoneNumberSnapshot, + required String userId, + required String adminNameSnapshot, + required String purchaseStatus, + Value totalPurchase, + Value discount, + Value tax, + required String paymentStatus, + Value totalAmount, + Value amountPaid, + Value changeAmount, + Value underPaymentAmount, + Value debtAmount, + Value paymentProofPath, + Value paymentProofUrl, + Value notes, + Value syncStatus, + Value createdAt, + Value updatedAt, + Value rowid, + }); +typedef $$PurchasesTableUpdateCompanionBuilder = + PurchasesCompanion Function({ + Value localId, + Value serverId, + Value purchaseNumber, + Value outletId, + Value outletNameSnapshot, + Value supplierId, + Value supplierNameSnapshot, + Value supplierEmailSnapshot, + Value supplierPhoneNumberSnapshot, + Value userId, + Value adminNameSnapshot, + Value purchaseStatus, + Value totalPurchase, + Value discount, + Value tax, + Value paymentStatus, + Value totalAmount, + Value amountPaid, + Value changeAmount, + Value underPaymentAmount, + Value debtAmount, + Value paymentProofPath, + Value paymentProofUrl, + Value notes, + Value syncStatus, + Value createdAt, + Value updatedAt, + Value rowid, + }); + +final class $$PurchasesTableReferences + extends BaseReferences<_$AppDatabase, $PurchasesTable, Purchase> { + $$PurchasesTableReferences(super.$_db, super.$_table, super.$_typedResult); + + static $OutletsTable _outletIdTable(_$AppDatabase db) => + db.outlets.createAlias( + $_aliasNameGenerator(db.purchases.outletId, db.outlets.localId), + ); + + $$OutletsTableProcessedTableManager get outletId { + final $_column = $_itemColumn('outlet_id')!; + + final manager = $$OutletsTableTableManager( + $_db, + $_db.outlets, + ).filter((f) => f.localId.sqlEquals($_column)); + final item = $_typedResult.readTableOrNull(_outletIdTable($_db)); + if (item == null) return manager; + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: [item]), + ); + } + + static $SuppliersTable _supplierIdTable(_$AppDatabase db) => + db.suppliers.createAlias( + $_aliasNameGenerator(db.purchases.supplierId, db.suppliers.localId), + ); + + $$SuppliersTableProcessedTableManager? get supplierId { + final $_column = $_itemColumn('supplier_id'); + if ($_column == null) return null; + final manager = $$SuppliersTableTableManager( + $_db, + $_db.suppliers, + ).filter((f) => f.localId.sqlEquals($_column)); + final item = $_typedResult.readTableOrNull(_supplierIdTable($_db)); + if (item == null) return manager; + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: [item]), + ); + } + + static $UsersTable _userIdTable(_$AppDatabase db) => db.users.createAlias( + $_aliasNameGenerator(db.purchases.userId, db.users.localId), + ); + + $$UsersTableProcessedTableManager get userId { + final $_column = $_itemColumn('user_id')!; + + final manager = $$UsersTableTableManager( + $_db, + $_db.users, + ).filter((f) => f.localId.sqlEquals($_column)); + final item = $_typedResult.readTableOrNull(_userIdTable($_db)); + if (item == null) return manager; + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: [item]), + ); + } + + static MultiTypedResultKey<$PurchaseItemsTable, List> + _purchaseItemsRefsTable(_$AppDatabase db) => MultiTypedResultKey.fromTable( + db.purchaseItems, + aliasName: $_aliasNameGenerator( + db.purchases.localId, + db.purchaseItems.purchaseId, + ), + ); + + $$PurchaseItemsTableProcessedTableManager get purchaseItemsRefs { + final manager = $$PurchaseItemsTableTableManager($_db, $_db.purchaseItems) + .filter( + (f) => + f.purchaseId.localId.sqlEquals($_itemColumn('local_id')!), + ); + + final cache = $_typedResult.readTableOrNull(_purchaseItemsRefsTable($_db)); + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: cache), + ); + } +} + +class $$PurchasesTableFilterComposer + extends Composer<_$AppDatabase, $PurchasesTable> { + $$PurchasesTableFilterComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnFilters get localId => $composableBuilder( + column: $table.localId, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get serverId => $composableBuilder( + column: $table.serverId, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get purchaseNumber => $composableBuilder( + column: $table.purchaseNumber, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get outletNameSnapshot => $composableBuilder( + column: $table.outletNameSnapshot, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get supplierNameSnapshot => $composableBuilder( + column: $table.supplierNameSnapshot, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get supplierEmailSnapshot => $composableBuilder( + column: $table.supplierEmailSnapshot, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get supplierPhoneNumberSnapshot => $composableBuilder( + column: $table.supplierPhoneNumberSnapshot, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get adminNameSnapshot => $composableBuilder( + column: $table.adminNameSnapshot, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get purchaseStatus => $composableBuilder( + column: $table.purchaseStatus, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get totalPurchase => $composableBuilder( + column: $table.totalPurchase, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get discount => $composableBuilder( + column: $table.discount, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get tax => $composableBuilder( + column: $table.tax, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get paymentStatus => $composableBuilder( + column: $table.paymentStatus, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get totalAmount => $composableBuilder( + column: $table.totalAmount, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get amountPaid => $composableBuilder( + column: $table.amountPaid, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get changeAmount => $composableBuilder( + column: $table.changeAmount, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get underPaymentAmount => $composableBuilder( + column: $table.underPaymentAmount, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get debtAmount => $composableBuilder( + column: $table.debtAmount, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get paymentProofPath => $composableBuilder( + column: $table.paymentProofPath, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get paymentProofUrl => $composableBuilder( + column: $table.paymentProofUrl, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get notes => $composableBuilder( + column: $table.notes, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get syncStatus => $composableBuilder( + column: $table.syncStatus, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get createdAt => $composableBuilder( + column: $table.createdAt, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get updatedAt => $composableBuilder( + column: $table.updatedAt, + builder: (column) => ColumnFilters(column), + ); + + $$OutletsTableFilterComposer get outletId { + final $$OutletsTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.outletId, + referencedTable: $db.outlets, + getReferencedColumn: (t) => t.localId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$OutletsTableFilterComposer( + $db: $db, + $table: $db.outlets, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } + + $$SuppliersTableFilterComposer get supplierId { + final $$SuppliersTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.supplierId, + referencedTable: $db.suppliers, + getReferencedColumn: (t) => t.localId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$SuppliersTableFilterComposer( + $db: $db, + $table: $db.suppliers, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } + + $$UsersTableFilterComposer get userId { + final $$UsersTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.userId, + referencedTable: $db.users, + getReferencedColumn: (t) => t.localId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$UsersTableFilterComposer( + $db: $db, + $table: $db.users, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } + + Expression purchaseItemsRefs( + Expression Function($$PurchaseItemsTableFilterComposer f) f, + ) { + final $$PurchaseItemsTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.localId, + referencedTable: $db.purchaseItems, + getReferencedColumn: (t) => t.purchaseId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$PurchaseItemsTableFilterComposer( + $db: $db, + $table: $db.purchaseItems, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return f(composer); + } +} + +class $$PurchasesTableOrderingComposer + extends Composer<_$AppDatabase, $PurchasesTable> { + $$PurchasesTableOrderingComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnOrderings get localId => $composableBuilder( + column: $table.localId, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get serverId => $composableBuilder( + column: $table.serverId, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get purchaseNumber => $composableBuilder( + column: $table.purchaseNumber, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get outletNameSnapshot => $composableBuilder( + column: $table.outletNameSnapshot, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get supplierNameSnapshot => $composableBuilder( + column: $table.supplierNameSnapshot, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get supplierEmailSnapshot => $composableBuilder( + column: $table.supplierEmailSnapshot, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get supplierPhoneNumberSnapshot => $composableBuilder( + column: $table.supplierPhoneNumberSnapshot, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get adminNameSnapshot => $composableBuilder( + column: $table.adminNameSnapshot, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get purchaseStatus => $composableBuilder( + column: $table.purchaseStatus, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get totalPurchase => $composableBuilder( + column: $table.totalPurchase, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get discount => $composableBuilder( + column: $table.discount, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get tax => $composableBuilder( + column: $table.tax, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get paymentStatus => $composableBuilder( + column: $table.paymentStatus, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get totalAmount => $composableBuilder( + column: $table.totalAmount, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get amountPaid => $composableBuilder( + column: $table.amountPaid, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get changeAmount => $composableBuilder( + column: $table.changeAmount, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get underPaymentAmount => $composableBuilder( + column: $table.underPaymentAmount, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get debtAmount => $composableBuilder( + column: $table.debtAmount, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get paymentProofPath => $composableBuilder( + column: $table.paymentProofPath, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get paymentProofUrl => $composableBuilder( + column: $table.paymentProofUrl, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get notes => $composableBuilder( + column: $table.notes, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get syncStatus => $composableBuilder( + column: $table.syncStatus, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get createdAt => $composableBuilder( + column: $table.createdAt, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get updatedAt => $composableBuilder( + column: $table.updatedAt, + builder: (column) => ColumnOrderings(column), + ); + + $$OutletsTableOrderingComposer get outletId { + final $$OutletsTableOrderingComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.outletId, + referencedTable: $db.outlets, + getReferencedColumn: (t) => t.localId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$OutletsTableOrderingComposer( + $db: $db, + $table: $db.outlets, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } + + $$SuppliersTableOrderingComposer get supplierId { + final $$SuppliersTableOrderingComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.supplierId, + referencedTable: $db.suppliers, + getReferencedColumn: (t) => t.localId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$SuppliersTableOrderingComposer( + $db: $db, + $table: $db.suppliers, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } + + $$UsersTableOrderingComposer get userId { + final $$UsersTableOrderingComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.userId, + referencedTable: $db.users, + getReferencedColumn: (t) => t.localId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$UsersTableOrderingComposer( + $db: $db, + $table: $db.users, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } +} + +class $$PurchasesTableAnnotationComposer + extends Composer<_$AppDatabase, $PurchasesTable> { + $$PurchasesTableAnnotationComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + GeneratedColumn get localId => + $composableBuilder(column: $table.localId, builder: (column) => column); + + GeneratedColumn get serverId => + $composableBuilder(column: $table.serverId, builder: (column) => column); + + GeneratedColumn get purchaseNumber => $composableBuilder( + column: $table.purchaseNumber, + builder: (column) => column, + ); + + GeneratedColumn get outletNameSnapshot => $composableBuilder( + column: $table.outletNameSnapshot, + builder: (column) => column, + ); + + GeneratedColumn get supplierNameSnapshot => $composableBuilder( + column: $table.supplierNameSnapshot, + builder: (column) => column, + ); + + GeneratedColumn get supplierEmailSnapshot => $composableBuilder( + column: $table.supplierEmailSnapshot, + builder: (column) => column, + ); + + GeneratedColumn get supplierPhoneNumberSnapshot => $composableBuilder( + column: $table.supplierPhoneNumberSnapshot, + builder: (column) => column, + ); + + GeneratedColumn get adminNameSnapshot => $composableBuilder( + column: $table.adminNameSnapshot, + builder: (column) => column, + ); + + GeneratedColumn get purchaseStatus => $composableBuilder( + column: $table.purchaseStatus, + builder: (column) => column, + ); + + GeneratedColumn get totalPurchase => $composableBuilder( + column: $table.totalPurchase, + builder: (column) => column, + ); + + GeneratedColumn get discount => + $composableBuilder(column: $table.discount, builder: (column) => column); + + GeneratedColumn get tax => + $composableBuilder(column: $table.tax, builder: (column) => column); + + GeneratedColumn get paymentStatus => $composableBuilder( + column: $table.paymentStatus, + builder: (column) => column, + ); + + GeneratedColumn get totalAmount => $composableBuilder( + column: $table.totalAmount, + builder: (column) => column, + ); + + GeneratedColumn get amountPaid => $composableBuilder( + column: $table.amountPaid, + builder: (column) => column, + ); + + GeneratedColumn get changeAmount => $composableBuilder( + column: $table.changeAmount, + builder: (column) => column, + ); + + GeneratedColumn get underPaymentAmount => $composableBuilder( + column: $table.underPaymentAmount, + builder: (column) => column, + ); + + GeneratedColumn get debtAmount => $composableBuilder( + column: $table.debtAmount, + builder: (column) => column, + ); + + GeneratedColumn get paymentProofPath => $composableBuilder( + column: $table.paymentProofPath, + builder: (column) => column, + ); + + GeneratedColumn get paymentProofUrl => $composableBuilder( + column: $table.paymentProofUrl, + builder: (column) => column, + ); + + GeneratedColumn get notes => + $composableBuilder(column: $table.notes, builder: (column) => column); + + GeneratedColumn get syncStatus => $composableBuilder( + column: $table.syncStatus, + builder: (column) => column, + ); + + GeneratedColumn get createdAt => + $composableBuilder(column: $table.createdAt, builder: (column) => column); + + GeneratedColumn get updatedAt => + $composableBuilder(column: $table.updatedAt, builder: (column) => column); + + $$OutletsTableAnnotationComposer get outletId { + final $$OutletsTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.outletId, + referencedTable: $db.outlets, + getReferencedColumn: (t) => t.localId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$OutletsTableAnnotationComposer( + $db: $db, + $table: $db.outlets, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } + + $$SuppliersTableAnnotationComposer get supplierId { + final $$SuppliersTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.supplierId, + referencedTable: $db.suppliers, + getReferencedColumn: (t) => t.localId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$SuppliersTableAnnotationComposer( + $db: $db, + $table: $db.suppliers, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } + + $$UsersTableAnnotationComposer get userId { + final $$UsersTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.userId, + referencedTable: $db.users, + getReferencedColumn: (t) => t.localId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$UsersTableAnnotationComposer( + $db: $db, + $table: $db.users, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } + + Expression purchaseItemsRefs( + Expression Function($$PurchaseItemsTableAnnotationComposer a) f, + ) { + final $$PurchaseItemsTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.localId, + referencedTable: $db.purchaseItems, + getReferencedColumn: (t) => t.purchaseId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$PurchaseItemsTableAnnotationComposer( + $db: $db, + $table: $db.purchaseItems, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return f(composer); + } +} + +class $$PurchasesTableTableManager + extends + RootTableManager< + _$AppDatabase, + $PurchasesTable, + Purchase, + $$PurchasesTableFilterComposer, + $$PurchasesTableOrderingComposer, + $$PurchasesTableAnnotationComposer, + $$PurchasesTableCreateCompanionBuilder, + $$PurchasesTableUpdateCompanionBuilder, + (Purchase, $$PurchasesTableReferences), + Purchase, + PrefetchHooks Function({ + bool outletId, + bool supplierId, + bool userId, + bool purchaseItemsRefs, + }) + > { + $$PurchasesTableTableManager(_$AppDatabase db, $PurchasesTable table) + : super( + TableManagerState( + db: db, + table: table, + createFilteringComposer: () => + $$PurchasesTableFilterComposer($db: db, $table: table), + createOrderingComposer: () => + $$PurchasesTableOrderingComposer($db: db, $table: table), + createComputedFieldComposer: () => + $$PurchasesTableAnnotationComposer($db: db, $table: table), + updateCompanionCallback: + ({ + Value localId = const Value.absent(), + Value serverId = const Value.absent(), + Value purchaseNumber = const Value.absent(), + Value outletId = const Value.absent(), + Value outletNameSnapshot = const Value.absent(), + Value supplierId = const Value.absent(), + Value supplierNameSnapshot = const Value.absent(), + Value supplierEmailSnapshot = const Value.absent(), + Value supplierPhoneNumberSnapshot = + const Value.absent(), + Value userId = const Value.absent(), + Value adminNameSnapshot = const Value.absent(), + Value purchaseStatus = const Value.absent(), + Value totalPurchase = const Value.absent(), + Value discount = const Value.absent(), + Value tax = const Value.absent(), + Value paymentStatus = const Value.absent(), + Value totalAmount = const Value.absent(), + Value amountPaid = const Value.absent(), + Value changeAmount = const Value.absent(), + Value underPaymentAmount = const Value.absent(), + Value debtAmount = const Value.absent(), + Value paymentProofPath = const Value.absent(), + Value paymentProofUrl = const Value.absent(), + Value notes = const Value.absent(), + Value syncStatus = const Value.absent(), + Value createdAt = const Value.absent(), + Value updatedAt = const Value.absent(), + Value rowid = const Value.absent(), + }) => PurchasesCompanion( + localId: localId, + serverId: serverId, + purchaseNumber: purchaseNumber, + outletId: outletId, + outletNameSnapshot: outletNameSnapshot, + supplierId: supplierId, + supplierNameSnapshot: supplierNameSnapshot, + supplierEmailSnapshot: supplierEmailSnapshot, + supplierPhoneNumberSnapshot: supplierPhoneNumberSnapshot, + userId: userId, + adminNameSnapshot: adminNameSnapshot, + purchaseStatus: purchaseStatus, + totalPurchase: totalPurchase, + discount: discount, + tax: tax, + paymentStatus: paymentStatus, + totalAmount: totalAmount, + amountPaid: amountPaid, + changeAmount: changeAmount, + underPaymentAmount: underPaymentAmount, + debtAmount: debtAmount, + paymentProofPath: paymentProofPath, + paymentProofUrl: paymentProofUrl, + notes: notes, + syncStatus: syncStatus, + createdAt: createdAt, + updatedAt: updatedAt, + rowid: rowid, + ), + createCompanionCallback: + ({ + Value localId = const Value.absent(), + Value serverId = const Value.absent(), + required String purchaseNumber, + required String outletId, + required String outletNameSnapshot, + Value supplierId = const Value.absent(), + Value supplierNameSnapshot = const Value.absent(), + Value supplierEmailSnapshot = const Value.absent(), + Value supplierPhoneNumberSnapshot = + const Value.absent(), + required String userId, + required String adminNameSnapshot, + required String purchaseStatus, + Value totalPurchase = const Value.absent(), + Value discount = const Value.absent(), + Value tax = const Value.absent(), + required String paymentStatus, + Value totalAmount = const Value.absent(), + Value amountPaid = const Value.absent(), + Value changeAmount = const Value.absent(), + Value underPaymentAmount = const Value.absent(), + Value debtAmount = const Value.absent(), + Value paymentProofPath = const Value.absent(), + Value paymentProofUrl = const Value.absent(), + Value notes = const Value.absent(), + Value syncStatus = const Value.absent(), + Value createdAt = const Value.absent(), + Value updatedAt = const Value.absent(), + Value rowid = const Value.absent(), + }) => PurchasesCompanion.insert( + localId: localId, + serverId: serverId, + purchaseNumber: purchaseNumber, + outletId: outletId, + outletNameSnapshot: outletNameSnapshot, + supplierId: supplierId, + supplierNameSnapshot: supplierNameSnapshot, + supplierEmailSnapshot: supplierEmailSnapshot, + supplierPhoneNumberSnapshot: supplierPhoneNumberSnapshot, + userId: userId, + adminNameSnapshot: adminNameSnapshot, + purchaseStatus: purchaseStatus, + totalPurchase: totalPurchase, + discount: discount, + tax: tax, + paymentStatus: paymentStatus, + totalAmount: totalAmount, + amountPaid: amountPaid, + changeAmount: changeAmount, + underPaymentAmount: underPaymentAmount, + debtAmount: debtAmount, + paymentProofPath: paymentProofPath, + paymentProofUrl: paymentProofUrl, + notes: notes, + syncStatus: syncStatus, + createdAt: createdAt, + updatedAt: updatedAt, + rowid: rowid, + ), + withReferenceMapper: (p0) => p0 + .map( + (e) => ( + e.readTable(table), + $$PurchasesTableReferences(db, table, e), + ), + ) + .toList(), + prefetchHooksCallback: + ({ + outletId = false, + supplierId = false, + userId = false, + purchaseItemsRefs = false, + }) { + return PrefetchHooks( + db: db, + explicitlyWatchedTables: [ + if (purchaseItemsRefs) db.purchaseItems, + ], + addJoins: + < + T extends TableManagerState< + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic + > + >(state) { + if (outletId) { + state = + state.withJoin( + currentTable: table, + currentColumn: table.outletId, + referencedTable: $$PurchasesTableReferences + ._outletIdTable(db), + referencedColumn: $$PurchasesTableReferences + ._outletIdTable(db) + .localId, + ) + as T; + } + if (supplierId) { + state = + state.withJoin( + currentTable: table, + currentColumn: table.supplierId, + referencedTable: $$PurchasesTableReferences + ._supplierIdTable(db), + referencedColumn: $$PurchasesTableReferences + ._supplierIdTable(db) + .localId, + ) + as T; + } + if (userId) { + state = + state.withJoin( + currentTable: table, + currentColumn: table.userId, + referencedTable: $$PurchasesTableReferences + ._userIdTable(db), + referencedColumn: $$PurchasesTableReferences + ._userIdTable(db) + .localId, + ) + as T; + } + + return state; + }, + getPrefetchedDataCallback: (items) async { + return [ + if (purchaseItemsRefs) + await $_getPrefetchedData< + Purchase, + $PurchasesTable, + PurchaseItem + >( + currentTable: table, + referencedTable: $$PurchasesTableReferences + ._purchaseItemsRefsTable(db), + managerFromTypedResult: (p0) => + $$PurchasesTableReferences( + db, + table, + p0, + ).purchaseItemsRefs, + referencedItemsForCurrentItem: + (item, referencedItems) => referencedItems.where( + (e) => e.purchaseId == item.localId, + ), + typedResults: items, + ), + ]; + }, + ); + }, + ), + ); +} + +typedef $$PurchasesTableProcessedTableManager = + ProcessedTableManager< + _$AppDatabase, + $PurchasesTable, + Purchase, + $$PurchasesTableFilterComposer, + $$PurchasesTableOrderingComposer, + $$PurchasesTableAnnotationComposer, + $$PurchasesTableCreateCompanionBuilder, + $$PurchasesTableUpdateCompanionBuilder, + (Purchase, $$PurchasesTableReferences), + Purchase, + PrefetchHooks Function({ + bool outletId, + bool supplierId, + bool userId, + bool purchaseItemsRefs, + }) + >; +typedef $$PurchaseItemsTableCreateCompanionBuilder = + PurchaseItemsCompanion Function({ + Value localId, + Value serverId, + required String purchaseId, + required String itemType, + required String itemId, + Value quantity, + Value itemImageSnapshot, + Value itemImageUrlSnapshot, + required String itemNameSnapshot, + Value itemVariantNameSnapshot, + Value purchasePriceSnaphot, + Value subtotal, + Value syncStatus, + Value createdAt, + Value updatedAt, + Value rowid, + }); +typedef $$PurchaseItemsTableUpdateCompanionBuilder = + PurchaseItemsCompanion Function({ + Value localId, + Value serverId, + Value purchaseId, + Value itemType, + Value itemId, + Value quantity, + Value itemImageSnapshot, + Value itemImageUrlSnapshot, + Value itemNameSnapshot, + Value itemVariantNameSnapshot, + Value purchasePriceSnaphot, + Value subtotal, + Value syncStatus, + Value createdAt, + Value updatedAt, + Value rowid, + }); + +final class $$PurchaseItemsTableReferences + extends BaseReferences<_$AppDatabase, $PurchaseItemsTable, PurchaseItem> { + $$PurchaseItemsTableReferences( + super.$_db, + super.$_table, + super.$_typedResult, + ); + + static $PurchasesTable _purchaseIdTable(_$AppDatabase db) => + db.purchases.createAlias( + $_aliasNameGenerator(db.purchaseItems.purchaseId, db.purchases.localId), + ); + + $$PurchasesTableProcessedTableManager get purchaseId { + final $_column = $_itemColumn('purchase_id')!; + + final manager = $$PurchasesTableTableManager( + $_db, + $_db.purchases, + ).filter((f) => f.localId.sqlEquals($_column)); + final item = $_typedResult.readTableOrNull(_purchaseIdTable($_db)); + if (item == null) return manager; + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: [item]), + ); + } +} + +class $$PurchaseItemsTableFilterComposer + extends Composer<_$AppDatabase, $PurchaseItemsTable> { + $$PurchaseItemsTableFilterComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnFilters get localId => $composableBuilder( + column: $table.localId, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get serverId => $composableBuilder( + column: $table.serverId, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get itemType => $composableBuilder( + column: $table.itemType, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get itemId => $composableBuilder( + column: $table.itemId, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get quantity => $composableBuilder( + column: $table.quantity, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get itemImageSnapshot => $composableBuilder( + column: $table.itemImageSnapshot, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get itemImageUrlSnapshot => $composableBuilder( + column: $table.itemImageUrlSnapshot, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get itemNameSnapshot => $composableBuilder( + column: $table.itemNameSnapshot, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get itemVariantNameSnapshot => $composableBuilder( + column: $table.itemVariantNameSnapshot, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get purchasePriceSnaphot => $composableBuilder( + column: $table.purchasePriceSnaphot, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get subtotal => $composableBuilder( + column: $table.subtotal, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get syncStatus => $composableBuilder( + column: $table.syncStatus, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get createdAt => $composableBuilder( + column: $table.createdAt, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get updatedAt => $composableBuilder( + column: $table.updatedAt, + builder: (column) => ColumnFilters(column), + ); + + $$PurchasesTableFilterComposer get purchaseId { + final $$PurchasesTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.purchaseId, + referencedTable: $db.purchases, + getReferencedColumn: (t) => t.localId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$PurchasesTableFilterComposer( + $db: $db, + $table: $db.purchases, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } +} + +class $$PurchaseItemsTableOrderingComposer + extends Composer<_$AppDatabase, $PurchaseItemsTable> { + $$PurchaseItemsTableOrderingComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnOrderings get localId => $composableBuilder( + column: $table.localId, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get serverId => $composableBuilder( + column: $table.serverId, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get itemType => $composableBuilder( + column: $table.itemType, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get itemId => $composableBuilder( + column: $table.itemId, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get quantity => $composableBuilder( + column: $table.quantity, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get itemImageSnapshot => $composableBuilder( + column: $table.itemImageSnapshot, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get itemImageUrlSnapshot => $composableBuilder( + column: $table.itemImageUrlSnapshot, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get itemNameSnapshot => $composableBuilder( + column: $table.itemNameSnapshot, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get itemVariantNameSnapshot => $composableBuilder( + column: $table.itemVariantNameSnapshot, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get purchasePriceSnaphot => $composableBuilder( + column: $table.purchasePriceSnaphot, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get subtotal => $composableBuilder( + column: $table.subtotal, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get syncStatus => $composableBuilder( + column: $table.syncStatus, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get createdAt => $composableBuilder( + column: $table.createdAt, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get updatedAt => $composableBuilder( + column: $table.updatedAt, + builder: (column) => ColumnOrderings(column), + ); + + $$PurchasesTableOrderingComposer get purchaseId { + final $$PurchasesTableOrderingComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.purchaseId, + referencedTable: $db.purchases, + getReferencedColumn: (t) => t.localId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$PurchasesTableOrderingComposer( + $db: $db, + $table: $db.purchases, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } +} + +class $$PurchaseItemsTableAnnotationComposer + extends Composer<_$AppDatabase, $PurchaseItemsTable> { + $$PurchaseItemsTableAnnotationComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + GeneratedColumn get localId => + $composableBuilder(column: $table.localId, builder: (column) => column); + + GeneratedColumn get serverId => + $composableBuilder(column: $table.serverId, builder: (column) => column); + + GeneratedColumn get itemType => + $composableBuilder(column: $table.itemType, builder: (column) => column); + + GeneratedColumn get itemId => + $composableBuilder(column: $table.itemId, builder: (column) => column); + + GeneratedColumn get quantity => + $composableBuilder(column: $table.quantity, builder: (column) => column); + + GeneratedColumn get itemImageSnapshot => $composableBuilder( + column: $table.itemImageSnapshot, + builder: (column) => column, + ); + + GeneratedColumn get itemImageUrlSnapshot => $composableBuilder( + column: $table.itemImageUrlSnapshot, + builder: (column) => column, + ); + + GeneratedColumn get itemNameSnapshot => $composableBuilder( + column: $table.itemNameSnapshot, + builder: (column) => column, + ); + + GeneratedColumn get itemVariantNameSnapshot => $composableBuilder( + column: $table.itemVariantNameSnapshot, + builder: (column) => column, + ); + + GeneratedColumn get purchasePriceSnaphot => $composableBuilder( + column: $table.purchasePriceSnaphot, + builder: (column) => column, + ); + + GeneratedColumn get subtotal => + $composableBuilder(column: $table.subtotal, builder: (column) => column); + + GeneratedColumn get syncStatus => $composableBuilder( + column: $table.syncStatus, + builder: (column) => column, + ); + + GeneratedColumn get createdAt => + $composableBuilder(column: $table.createdAt, builder: (column) => column); + + GeneratedColumn get updatedAt => + $composableBuilder(column: $table.updatedAt, builder: (column) => column); + + $$PurchasesTableAnnotationComposer get purchaseId { + final $$PurchasesTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.purchaseId, + referencedTable: $db.purchases, + getReferencedColumn: (t) => t.localId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$PurchasesTableAnnotationComposer( + $db: $db, + $table: $db.purchases, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } +} + +class $$PurchaseItemsTableTableManager + extends + RootTableManager< + _$AppDatabase, + $PurchaseItemsTable, + PurchaseItem, + $$PurchaseItemsTableFilterComposer, + $$PurchaseItemsTableOrderingComposer, + $$PurchaseItemsTableAnnotationComposer, + $$PurchaseItemsTableCreateCompanionBuilder, + $$PurchaseItemsTableUpdateCompanionBuilder, + (PurchaseItem, $$PurchaseItemsTableReferences), + PurchaseItem, + PrefetchHooks Function({bool purchaseId}) + > { + $$PurchaseItemsTableTableManager(_$AppDatabase db, $PurchaseItemsTable table) + : super( + TableManagerState( + db: db, + table: table, + createFilteringComposer: () => + $$PurchaseItemsTableFilterComposer($db: db, $table: table), + createOrderingComposer: () => + $$PurchaseItemsTableOrderingComposer($db: db, $table: table), + createComputedFieldComposer: () => + $$PurchaseItemsTableAnnotationComposer($db: db, $table: table), + updateCompanionCallback: + ({ + Value localId = const Value.absent(), + Value serverId = const Value.absent(), + Value purchaseId = const Value.absent(), + Value itemType = const Value.absent(), + Value itemId = const Value.absent(), + Value quantity = const Value.absent(), + Value itemImageSnapshot = const Value.absent(), + Value itemImageUrlSnapshot = const Value.absent(), + Value itemNameSnapshot = const Value.absent(), + Value itemVariantNameSnapshot = const Value.absent(), + Value purchasePriceSnaphot = const Value.absent(), + Value subtotal = const Value.absent(), + Value syncStatus = const Value.absent(), + Value createdAt = const Value.absent(), + Value updatedAt = const Value.absent(), + Value rowid = const Value.absent(), + }) => PurchaseItemsCompanion( + localId: localId, + serverId: serverId, + purchaseId: purchaseId, + itemType: itemType, + itemId: itemId, + quantity: quantity, + itemImageSnapshot: itemImageSnapshot, + itemImageUrlSnapshot: itemImageUrlSnapshot, + itemNameSnapshot: itemNameSnapshot, + itemVariantNameSnapshot: itemVariantNameSnapshot, + purchasePriceSnaphot: purchasePriceSnaphot, + subtotal: subtotal, + syncStatus: syncStatus, + createdAt: createdAt, + updatedAt: updatedAt, + rowid: rowid, + ), + createCompanionCallback: + ({ + Value localId = const Value.absent(), + Value serverId = const Value.absent(), + required String purchaseId, + required String itemType, + required String itemId, + Value quantity = const Value.absent(), + Value itemImageSnapshot = const Value.absent(), + Value itemImageUrlSnapshot = const Value.absent(), + required String itemNameSnapshot, + Value itemVariantNameSnapshot = const Value.absent(), + Value purchasePriceSnaphot = const Value.absent(), + Value subtotal = const Value.absent(), + Value syncStatus = const Value.absent(), + Value createdAt = const Value.absent(), + Value updatedAt = const Value.absent(), + Value rowid = const Value.absent(), + }) => PurchaseItemsCompanion.insert( + localId: localId, + serverId: serverId, + purchaseId: purchaseId, + itemType: itemType, + itemId: itemId, + quantity: quantity, + itemImageSnapshot: itemImageSnapshot, + itemImageUrlSnapshot: itemImageUrlSnapshot, + itemNameSnapshot: itemNameSnapshot, + itemVariantNameSnapshot: itemVariantNameSnapshot, + purchasePriceSnaphot: purchasePriceSnaphot, + subtotal: subtotal, + syncStatus: syncStatus, + createdAt: createdAt, + updatedAt: updatedAt, + rowid: rowid, + ), + withReferenceMapper: (p0) => p0 + .map( + (e) => ( + e.readTable(table), + $$PurchaseItemsTableReferences(db, table, e), + ), + ) + .toList(), + prefetchHooksCallback: ({purchaseId = false}) { + return PrefetchHooks( + db: db, + explicitlyWatchedTables: [], + addJoins: + < + T extends TableManagerState< + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic + > + >(state) { + if (purchaseId) { + state = + state.withJoin( + currentTable: table, + currentColumn: table.purchaseId, + referencedTable: $$PurchaseItemsTableReferences + ._purchaseIdTable(db), + referencedColumn: $$PurchaseItemsTableReferences + ._purchaseIdTable(db) + .localId, + ) + as T; + } + + return state; + }, + getPrefetchedDataCallback: (items) async { + return []; + }, + ); + }, + ), + ); +} + +typedef $$PurchaseItemsTableProcessedTableManager = + ProcessedTableManager< + _$AppDatabase, + $PurchaseItemsTable, + PurchaseItem, + $$PurchaseItemsTableFilterComposer, + $$PurchaseItemsTableOrderingComposer, + $$PurchaseItemsTableAnnotationComposer, + $$PurchaseItemsTableCreateCompanionBuilder, + $$PurchaseItemsTableUpdateCompanionBuilder, + (PurchaseItem, $$PurchaseItemsTableReferences), + PurchaseItem, + PrefetchHooks Function({bool purchaseId}) + >; +typedef $$TransactionsTableCreateCompanionBuilder = + TransactionsCompanion Function({ + Value localId, + Value serverId, + required String outletId, + required String userId, + required String referenceType, + required String referenceId, + required String referenceNumber, + Value stakeholder, + required String transactionType, + required String category, + Value amount, + Value note, + Value syncStatus, + Value createdAt, + Value updatedAt, + Value rowid, + }); +typedef $$TransactionsTableUpdateCompanionBuilder = + TransactionsCompanion Function({ + Value localId, + Value serverId, + Value outletId, + Value userId, + Value referenceType, + Value referenceId, + Value referenceNumber, + Value stakeholder, + Value transactionType, + Value category, + Value amount, + Value note, + Value syncStatus, + Value createdAt, + Value updatedAt, + Value rowid, + }); + +final class $$TransactionsTableReferences + extends BaseReferences<_$AppDatabase, $TransactionsTable, Transaction> { + $$TransactionsTableReferences(super.$_db, super.$_table, super.$_typedResult); + + static $OutletsTable _outletIdTable(_$AppDatabase db) => + db.outlets.createAlias( + $_aliasNameGenerator(db.transactions.outletId, db.outlets.localId), + ); + + $$OutletsTableProcessedTableManager get outletId { + final $_column = $_itemColumn('outlet_id')!; + + final manager = $$OutletsTableTableManager( + $_db, + $_db.outlets, + ).filter((f) => f.localId.sqlEquals($_column)); + final item = $_typedResult.readTableOrNull(_outletIdTable($_db)); + if (item == null) return manager; + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: [item]), + ); + } + + static $UsersTable _userIdTable(_$AppDatabase db) => db.users.createAlias( + $_aliasNameGenerator(db.transactions.userId, db.users.localId), + ); + + $$UsersTableProcessedTableManager get userId { + final $_column = $_itemColumn('user_id')!; + + final manager = $$UsersTableTableManager( + $_db, + $_db.users, + ).filter((f) => f.localId.sqlEquals($_column)); + final item = $_typedResult.readTableOrNull(_userIdTable($_db)); + if (item == null) return manager; + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: [item]), + ); + } +} + +class $$TransactionsTableFilterComposer + extends Composer<_$AppDatabase, $TransactionsTable> { + $$TransactionsTableFilterComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnFilters get localId => $composableBuilder( + column: $table.localId, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get serverId => $composableBuilder( + column: $table.serverId, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get referenceType => $composableBuilder( + column: $table.referenceType, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get referenceId => $composableBuilder( + column: $table.referenceId, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get referenceNumber => $composableBuilder( + column: $table.referenceNumber, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get stakeholder => $composableBuilder( + column: $table.stakeholder, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get transactionType => $composableBuilder( + column: $table.transactionType, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get category => $composableBuilder( + column: $table.category, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get amount => $composableBuilder( + column: $table.amount, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get note => $composableBuilder( + column: $table.note, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get syncStatus => $composableBuilder( + column: $table.syncStatus, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get createdAt => $composableBuilder( + column: $table.createdAt, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get updatedAt => $composableBuilder( + column: $table.updatedAt, + builder: (column) => ColumnFilters(column), + ); + + $$OutletsTableFilterComposer get outletId { + final $$OutletsTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.outletId, + referencedTable: $db.outlets, + getReferencedColumn: (t) => t.localId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$OutletsTableFilterComposer( + $db: $db, + $table: $db.outlets, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } + + $$UsersTableFilterComposer get userId { + final $$UsersTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.userId, + referencedTable: $db.users, + getReferencedColumn: (t) => t.localId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$UsersTableFilterComposer( + $db: $db, + $table: $db.users, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } +} + +class $$TransactionsTableOrderingComposer + extends Composer<_$AppDatabase, $TransactionsTable> { + $$TransactionsTableOrderingComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnOrderings get localId => $composableBuilder( + column: $table.localId, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get serverId => $composableBuilder( + column: $table.serverId, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get referenceType => $composableBuilder( + column: $table.referenceType, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get referenceId => $composableBuilder( + column: $table.referenceId, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get referenceNumber => $composableBuilder( + column: $table.referenceNumber, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get stakeholder => $composableBuilder( + column: $table.stakeholder, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get transactionType => $composableBuilder( + column: $table.transactionType, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get category => $composableBuilder( + column: $table.category, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get amount => $composableBuilder( + column: $table.amount, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get note => $composableBuilder( + column: $table.note, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get syncStatus => $composableBuilder( + column: $table.syncStatus, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get createdAt => $composableBuilder( + column: $table.createdAt, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get updatedAt => $composableBuilder( + column: $table.updatedAt, + builder: (column) => ColumnOrderings(column), + ); + + $$OutletsTableOrderingComposer get outletId { + final $$OutletsTableOrderingComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.outletId, + referencedTable: $db.outlets, + getReferencedColumn: (t) => t.localId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$OutletsTableOrderingComposer( + $db: $db, + $table: $db.outlets, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } + + $$UsersTableOrderingComposer get userId { + final $$UsersTableOrderingComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.userId, + referencedTable: $db.users, + getReferencedColumn: (t) => t.localId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$UsersTableOrderingComposer( + $db: $db, + $table: $db.users, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } +} + +class $$TransactionsTableAnnotationComposer + extends Composer<_$AppDatabase, $TransactionsTable> { + $$TransactionsTableAnnotationComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + GeneratedColumn get localId => + $composableBuilder(column: $table.localId, builder: (column) => column); + + GeneratedColumn get serverId => + $composableBuilder(column: $table.serverId, builder: (column) => column); + + GeneratedColumn get referenceType => $composableBuilder( + column: $table.referenceType, + builder: (column) => column, + ); + + GeneratedColumn get referenceId => $composableBuilder( + column: $table.referenceId, + builder: (column) => column, + ); + + GeneratedColumn get referenceNumber => $composableBuilder( + column: $table.referenceNumber, + builder: (column) => column, + ); + + GeneratedColumn get stakeholder => $composableBuilder( + column: $table.stakeholder, + builder: (column) => column, + ); + + GeneratedColumn get transactionType => $composableBuilder( + column: $table.transactionType, + builder: (column) => column, + ); + + GeneratedColumn get category => + $composableBuilder(column: $table.category, builder: (column) => column); + + GeneratedColumn get amount => + $composableBuilder(column: $table.amount, builder: (column) => column); + + GeneratedColumn get note => + $composableBuilder(column: $table.note, builder: (column) => column); + + GeneratedColumn get syncStatus => $composableBuilder( + column: $table.syncStatus, + builder: (column) => column, + ); + + GeneratedColumn get createdAt => + $composableBuilder(column: $table.createdAt, builder: (column) => column); + + GeneratedColumn get updatedAt => + $composableBuilder(column: $table.updatedAt, builder: (column) => column); + + $$OutletsTableAnnotationComposer get outletId { + final $$OutletsTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.outletId, + referencedTable: $db.outlets, + getReferencedColumn: (t) => t.localId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$OutletsTableAnnotationComposer( + $db: $db, + $table: $db.outlets, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } + + $$UsersTableAnnotationComposer get userId { + final $$UsersTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.userId, + referencedTable: $db.users, + getReferencedColumn: (t) => t.localId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$UsersTableAnnotationComposer( + $db: $db, + $table: $db.users, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } +} + +class $$TransactionsTableTableManager + extends + RootTableManager< + _$AppDatabase, + $TransactionsTable, + Transaction, + $$TransactionsTableFilterComposer, + $$TransactionsTableOrderingComposer, + $$TransactionsTableAnnotationComposer, + $$TransactionsTableCreateCompanionBuilder, + $$TransactionsTableUpdateCompanionBuilder, + (Transaction, $$TransactionsTableReferences), + Transaction, + PrefetchHooks Function({bool outletId, bool userId}) + > { + $$TransactionsTableTableManager(_$AppDatabase db, $TransactionsTable table) + : super( + TableManagerState( + db: db, + table: table, + createFilteringComposer: () => + $$TransactionsTableFilterComposer($db: db, $table: table), + createOrderingComposer: () => + $$TransactionsTableOrderingComposer($db: db, $table: table), + createComputedFieldComposer: () => + $$TransactionsTableAnnotationComposer($db: db, $table: table), + updateCompanionCallback: + ({ + Value localId = const Value.absent(), + Value serverId = const Value.absent(), + Value outletId = const Value.absent(), + Value userId = const Value.absent(), + Value referenceType = const Value.absent(), + Value referenceId = const Value.absent(), + Value referenceNumber = const Value.absent(), + Value stakeholder = const Value.absent(), + Value transactionType = const Value.absent(), + Value category = const Value.absent(), + Value amount = const Value.absent(), + Value note = const Value.absent(), + Value syncStatus = const Value.absent(), + Value createdAt = const Value.absent(), + Value updatedAt = const Value.absent(), + Value rowid = const Value.absent(), + }) => TransactionsCompanion( + localId: localId, + serverId: serverId, + outletId: outletId, + userId: userId, + referenceType: referenceType, + referenceId: referenceId, + referenceNumber: referenceNumber, + stakeholder: stakeholder, + transactionType: transactionType, + category: category, + amount: amount, + note: note, + syncStatus: syncStatus, + createdAt: createdAt, + updatedAt: updatedAt, + rowid: rowid, + ), + createCompanionCallback: + ({ + Value localId = const Value.absent(), + Value serverId = const Value.absent(), + required String outletId, + required String userId, + required String referenceType, + required String referenceId, + required String referenceNumber, + Value stakeholder = const Value.absent(), + required String transactionType, + required String category, + Value amount = const Value.absent(), + Value note = const Value.absent(), + Value syncStatus = const Value.absent(), + Value createdAt = const Value.absent(), + Value updatedAt = const Value.absent(), + Value rowid = const Value.absent(), + }) => TransactionsCompanion.insert( + localId: localId, + serverId: serverId, + outletId: outletId, + userId: userId, + referenceType: referenceType, + referenceId: referenceId, + referenceNumber: referenceNumber, + stakeholder: stakeholder, + transactionType: transactionType, + category: category, + amount: amount, + note: note, + syncStatus: syncStatus, + createdAt: createdAt, + updatedAt: updatedAt, + rowid: rowid, + ), + withReferenceMapper: (p0) => p0 + .map( + (e) => ( + e.readTable(table), + $$TransactionsTableReferences(db, table, e), + ), + ) + .toList(), + prefetchHooksCallback: ({outletId = false, userId = false}) { + return PrefetchHooks( + db: db, + explicitlyWatchedTables: [], + addJoins: + < + T extends TableManagerState< + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic + > + >(state) { + if (outletId) { + state = + state.withJoin( + currentTable: table, + currentColumn: table.outletId, + referencedTable: $$TransactionsTableReferences + ._outletIdTable(db), + referencedColumn: $$TransactionsTableReferences + ._outletIdTable(db) + .localId, + ) + as T; + } + if (userId) { + state = + state.withJoin( + currentTable: table, + currentColumn: table.userId, + referencedTable: $$TransactionsTableReferences + ._userIdTable(db), + referencedColumn: $$TransactionsTableReferences + ._userIdTable(db) + .localId, + ) + as T; + } + + return state; + }, + getPrefetchedDataCallback: (items) async { + return []; + }, + ); + }, + ), + ); +} + +typedef $$TransactionsTableProcessedTableManager = + ProcessedTableManager< + _$AppDatabase, + $TransactionsTable, + Transaction, + $$TransactionsTableFilterComposer, + $$TransactionsTableOrderingComposer, + $$TransactionsTableAnnotationComposer, + $$TransactionsTableCreateCompanionBuilder, + $$TransactionsTableUpdateCompanionBuilder, + (Transaction, $$TransactionsTableReferences), + Transaction, + PrefetchHooks Function({bool outletId, bool userId}) + >; +typedef $$CampaignsTableCreateCompanionBuilder = + CampaignsCompanion Function({ + Value localId, + Value serverId, + required String title, + required String localBannerPath, + Value serverBannerUrl, + Value startDate, + Value endDate, + Value actionType, + Value actionReferenceId, + Value isActive, + Value syncStatus, + Value createdAt, + Value updatedAt, + Value deletedAt, + Value rowid, + }); +typedef $$CampaignsTableUpdateCompanionBuilder = + CampaignsCompanion Function({ + Value localId, + Value serverId, + Value title, + Value localBannerPath, + Value serverBannerUrl, + Value startDate, + Value endDate, + Value actionType, + Value actionReferenceId, + Value isActive, + Value syncStatus, + Value createdAt, + Value updatedAt, + Value deletedAt, + Value rowid, + }); + +final class $$CampaignsTableReferences + extends BaseReferences<_$AppDatabase, $CampaignsTable, Campaign> { + $$CampaignsTableReferences(super.$_db, super.$_table, super.$_typedResult); + + static MultiTypedResultKey<$OutletCampaignsTable, List> + _outletCampaignsRefsTable(_$AppDatabase db) => MultiTypedResultKey.fromTable( + db.outletCampaigns, + aliasName: $_aliasNameGenerator( + db.campaigns.localId, + db.outletCampaigns.campaignId, + ), + ); + + $$OutletCampaignsTableProcessedTableManager get outletCampaignsRefs { + final manager = + $$OutletCampaignsTableTableManager($_db, $_db.outletCampaigns).filter( + (f) => + f.campaignId.localId.sqlEquals($_itemColumn('local_id')!), + ); + + final cache = $_typedResult.readTableOrNull( + _outletCampaignsRefsTable($_db), + ); + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: cache), + ); + } +} + +class $$CampaignsTableFilterComposer + extends Composer<_$AppDatabase, $CampaignsTable> { + $$CampaignsTableFilterComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnFilters get localId => $composableBuilder( + column: $table.localId, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get serverId => $composableBuilder( + column: $table.serverId, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get title => $composableBuilder( + column: $table.title, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get localBannerPath => $composableBuilder( + column: $table.localBannerPath, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get serverBannerUrl => $composableBuilder( + column: $table.serverBannerUrl, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get startDate => $composableBuilder( + column: $table.startDate, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get endDate => $composableBuilder( + column: $table.endDate, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get actionType => $composableBuilder( + column: $table.actionType, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get actionReferenceId => $composableBuilder( + column: $table.actionReferenceId, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get isActive => $composableBuilder( + column: $table.isActive, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get syncStatus => $composableBuilder( + column: $table.syncStatus, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get createdAt => $composableBuilder( + column: $table.createdAt, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get updatedAt => $composableBuilder( + column: $table.updatedAt, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get deletedAt => $composableBuilder( + column: $table.deletedAt, + builder: (column) => ColumnFilters(column), + ); + + Expression outletCampaignsRefs( + Expression Function($$OutletCampaignsTableFilterComposer f) f, + ) { + final $$OutletCampaignsTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.localId, + referencedTable: $db.outletCampaigns, + getReferencedColumn: (t) => t.campaignId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$OutletCampaignsTableFilterComposer( + $db: $db, + $table: $db.outletCampaigns, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return f(composer); + } +} + +class $$CampaignsTableOrderingComposer + extends Composer<_$AppDatabase, $CampaignsTable> { + $$CampaignsTableOrderingComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnOrderings get localId => $composableBuilder( + column: $table.localId, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get serverId => $composableBuilder( + column: $table.serverId, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get title => $composableBuilder( + column: $table.title, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get localBannerPath => $composableBuilder( + column: $table.localBannerPath, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get serverBannerUrl => $composableBuilder( + column: $table.serverBannerUrl, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get startDate => $composableBuilder( + column: $table.startDate, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get endDate => $composableBuilder( + column: $table.endDate, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get actionType => $composableBuilder( + column: $table.actionType, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get actionReferenceId => $composableBuilder( + column: $table.actionReferenceId, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get isActive => $composableBuilder( + column: $table.isActive, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get syncStatus => $composableBuilder( + column: $table.syncStatus, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get createdAt => $composableBuilder( + column: $table.createdAt, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get updatedAt => $composableBuilder( + column: $table.updatedAt, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get deletedAt => $composableBuilder( + column: $table.deletedAt, + builder: (column) => ColumnOrderings(column), + ); +} + +class $$CampaignsTableAnnotationComposer + extends Composer<_$AppDatabase, $CampaignsTable> { + $$CampaignsTableAnnotationComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + GeneratedColumn get localId => + $composableBuilder(column: $table.localId, builder: (column) => column); + + GeneratedColumn get serverId => + $composableBuilder(column: $table.serverId, builder: (column) => column); + + GeneratedColumn get title => + $composableBuilder(column: $table.title, builder: (column) => column); + + GeneratedColumn get localBannerPath => $composableBuilder( + column: $table.localBannerPath, + builder: (column) => column, + ); + + GeneratedColumn get serverBannerUrl => $composableBuilder( + column: $table.serverBannerUrl, + builder: (column) => column, + ); + + GeneratedColumn get startDate => + $composableBuilder(column: $table.startDate, builder: (column) => column); + + GeneratedColumn get endDate => + $composableBuilder(column: $table.endDate, builder: (column) => column); + + GeneratedColumn get actionType => $composableBuilder( + column: $table.actionType, + builder: (column) => column, + ); + + GeneratedColumn get actionReferenceId => $composableBuilder( + column: $table.actionReferenceId, + builder: (column) => column, + ); + + GeneratedColumn get isActive => + $composableBuilder(column: $table.isActive, builder: (column) => column); + + GeneratedColumn get syncStatus => $composableBuilder( + column: $table.syncStatus, + builder: (column) => column, + ); + + GeneratedColumn get createdAt => + $composableBuilder(column: $table.createdAt, builder: (column) => column); + + GeneratedColumn get updatedAt => + $composableBuilder(column: $table.updatedAt, builder: (column) => column); + + GeneratedColumn get deletedAt => + $composableBuilder(column: $table.deletedAt, builder: (column) => column); + + Expression outletCampaignsRefs( + Expression Function($$OutletCampaignsTableAnnotationComposer a) f, + ) { + final $$OutletCampaignsTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.localId, + referencedTable: $db.outletCampaigns, + getReferencedColumn: (t) => t.campaignId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$OutletCampaignsTableAnnotationComposer( + $db: $db, + $table: $db.outletCampaigns, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return f(composer); + } +} + +class $$CampaignsTableTableManager + extends + RootTableManager< + _$AppDatabase, + $CampaignsTable, + Campaign, + $$CampaignsTableFilterComposer, + $$CampaignsTableOrderingComposer, + $$CampaignsTableAnnotationComposer, + $$CampaignsTableCreateCompanionBuilder, + $$CampaignsTableUpdateCompanionBuilder, + (Campaign, $$CampaignsTableReferences), + Campaign, + PrefetchHooks Function({bool outletCampaignsRefs}) + > { + $$CampaignsTableTableManager(_$AppDatabase db, $CampaignsTable table) + : super( + TableManagerState( + db: db, + table: table, + createFilteringComposer: () => + $$CampaignsTableFilterComposer($db: db, $table: table), + createOrderingComposer: () => + $$CampaignsTableOrderingComposer($db: db, $table: table), + createComputedFieldComposer: () => + $$CampaignsTableAnnotationComposer($db: db, $table: table), + updateCompanionCallback: + ({ + Value localId = const Value.absent(), + Value serverId = const Value.absent(), + Value title = const Value.absent(), + Value localBannerPath = const Value.absent(), + Value serverBannerUrl = const Value.absent(), + Value startDate = const Value.absent(), + Value endDate = const Value.absent(), + Value actionType = const Value.absent(), + Value actionReferenceId = const Value.absent(), + Value isActive = const Value.absent(), + Value syncStatus = const Value.absent(), + Value createdAt = const Value.absent(), + Value updatedAt = const Value.absent(), + Value deletedAt = const Value.absent(), + Value rowid = const Value.absent(), + }) => CampaignsCompanion( + localId: localId, + serverId: serverId, + title: title, + localBannerPath: localBannerPath, + serverBannerUrl: serverBannerUrl, + startDate: startDate, + endDate: endDate, + actionType: actionType, + actionReferenceId: actionReferenceId, + isActive: isActive, + syncStatus: syncStatus, + createdAt: createdAt, + updatedAt: updatedAt, + deletedAt: deletedAt, + rowid: rowid, + ), + createCompanionCallback: + ({ + Value localId = const Value.absent(), + Value serverId = const Value.absent(), + required String title, + required String localBannerPath, + Value serverBannerUrl = const Value.absent(), + Value startDate = const Value.absent(), + Value endDate = const Value.absent(), + Value actionType = const Value.absent(), + Value actionReferenceId = const Value.absent(), + Value isActive = const Value.absent(), + Value syncStatus = const Value.absent(), + Value createdAt = const Value.absent(), + Value updatedAt = const Value.absent(), + Value deletedAt = const Value.absent(), + Value rowid = const Value.absent(), + }) => CampaignsCompanion.insert( + localId: localId, + serverId: serverId, + title: title, + localBannerPath: localBannerPath, + serverBannerUrl: serverBannerUrl, + startDate: startDate, + endDate: endDate, + actionType: actionType, + actionReferenceId: actionReferenceId, + isActive: isActive, + syncStatus: syncStatus, + createdAt: createdAt, + updatedAt: updatedAt, + deletedAt: deletedAt, + rowid: rowid, + ), + withReferenceMapper: (p0) => p0 + .map( + (e) => ( + e.readTable(table), + $$CampaignsTableReferences(db, table, e), + ), + ) + .toList(), + prefetchHooksCallback: ({outletCampaignsRefs = false}) { + return PrefetchHooks( + db: db, + explicitlyWatchedTables: [ + if (outletCampaignsRefs) db.outletCampaigns, + ], + addJoins: null, + getPrefetchedDataCallback: (items) async { + return [ + if (outletCampaignsRefs) + await $_getPrefetchedData< + Campaign, + $CampaignsTable, + OutletCampaign + >( + currentTable: table, + referencedTable: $$CampaignsTableReferences + ._outletCampaignsRefsTable(db), + managerFromTypedResult: (p0) => + $$CampaignsTableReferences( + db, + table, + p0, + ).outletCampaignsRefs, + referencedItemsForCurrentItem: (item, referencedItems) => + referencedItems.where( + (e) => e.campaignId == item.localId, + ), + typedResults: items, + ), + ]; + }, + ); + }, + ), + ); +} + +typedef $$CampaignsTableProcessedTableManager = + ProcessedTableManager< + _$AppDatabase, + $CampaignsTable, + Campaign, + $$CampaignsTableFilterComposer, + $$CampaignsTableOrderingComposer, + $$CampaignsTableAnnotationComposer, + $$CampaignsTableCreateCompanionBuilder, + $$CampaignsTableUpdateCompanionBuilder, + (Campaign, $$CampaignsTableReferences), + Campaign, + PrefetchHooks Function({bool outletCampaignsRefs}) + >; +typedef $$OutletCampaignsTableCreateCompanionBuilder = + OutletCampaignsCompanion Function({ + Value localId, + Value serverId, + required String campaignId, + Value outletId, + Value syncStatus, + Value createdAt, + Value updatedAt, + Value deletedAt, + Value rowid, + }); +typedef $$OutletCampaignsTableUpdateCompanionBuilder = + OutletCampaignsCompanion Function({ + Value localId, + Value serverId, + Value campaignId, + Value outletId, + Value syncStatus, + Value createdAt, + Value updatedAt, + Value deletedAt, + Value rowid, + }); + +final class $$OutletCampaignsTableReferences + extends + BaseReferences<_$AppDatabase, $OutletCampaignsTable, OutletCampaign> { + $$OutletCampaignsTableReferences( + super.$_db, + super.$_table, + super.$_typedResult, + ); + + static $CampaignsTable _campaignIdTable(_$AppDatabase db) => + db.campaigns.createAlias( + $_aliasNameGenerator( + db.outletCampaigns.campaignId, + db.campaigns.localId, + ), + ); + + $$CampaignsTableProcessedTableManager get campaignId { + final $_column = $_itemColumn('campaign_id')!; + + final manager = $$CampaignsTableTableManager( + $_db, + $_db.campaigns, + ).filter((f) => f.localId.sqlEquals($_column)); + final item = $_typedResult.readTableOrNull(_campaignIdTable($_db)); + if (item == null) return manager; + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: [item]), + ); + } + + static $OutletsTable _outletIdTable(_$AppDatabase db) => + db.outlets.createAlias( + $_aliasNameGenerator(db.outletCampaigns.outletId, db.outlets.localId), + ); + + $$OutletsTableProcessedTableManager? get outletId { + final $_column = $_itemColumn('outlet_id'); + if ($_column == null) return null; + final manager = $$OutletsTableTableManager( + $_db, + $_db.outlets, + ).filter((f) => f.localId.sqlEquals($_column)); + final item = $_typedResult.readTableOrNull(_outletIdTable($_db)); + if (item == null) return manager; + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: [item]), + ); + } +} + +class $$OutletCampaignsTableFilterComposer + extends Composer<_$AppDatabase, $OutletCampaignsTable> { + $$OutletCampaignsTableFilterComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnFilters get localId => $composableBuilder( + column: $table.localId, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get serverId => $composableBuilder( + column: $table.serverId, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get syncStatus => $composableBuilder( + column: $table.syncStatus, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get createdAt => $composableBuilder( + column: $table.createdAt, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get updatedAt => $composableBuilder( + column: $table.updatedAt, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get deletedAt => $composableBuilder( + column: $table.deletedAt, + builder: (column) => ColumnFilters(column), + ); + + $$CampaignsTableFilterComposer get campaignId { + final $$CampaignsTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.campaignId, + referencedTable: $db.campaigns, + getReferencedColumn: (t) => t.localId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$CampaignsTableFilterComposer( + $db: $db, + $table: $db.campaigns, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } + + $$OutletsTableFilterComposer get outletId { + final $$OutletsTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.outletId, + referencedTable: $db.outlets, + getReferencedColumn: (t) => t.localId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$OutletsTableFilterComposer( + $db: $db, + $table: $db.outlets, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } +} + +class $$OutletCampaignsTableOrderingComposer + extends Composer<_$AppDatabase, $OutletCampaignsTable> { + $$OutletCampaignsTableOrderingComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnOrderings get localId => $composableBuilder( + column: $table.localId, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get serverId => $composableBuilder( + column: $table.serverId, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get syncStatus => $composableBuilder( + column: $table.syncStatus, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get createdAt => $composableBuilder( + column: $table.createdAt, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get updatedAt => $composableBuilder( + column: $table.updatedAt, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get deletedAt => $composableBuilder( + column: $table.deletedAt, + builder: (column) => ColumnOrderings(column), + ); + + $$CampaignsTableOrderingComposer get campaignId { + final $$CampaignsTableOrderingComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.campaignId, + referencedTable: $db.campaigns, + getReferencedColumn: (t) => t.localId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$CampaignsTableOrderingComposer( + $db: $db, + $table: $db.campaigns, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } + + $$OutletsTableOrderingComposer get outletId { + final $$OutletsTableOrderingComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.outletId, + referencedTable: $db.outlets, + getReferencedColumn: (t) => t.localId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$OutletsTableOrderingComposer( + $db: $db, + $table: $db.outlets, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } +} + +class $$OutletCampaignsTableAnnotationComposer + extends Composer<_$AppDatabase, $OutletCampaignsTable> { + $$OutletCampaignsTableAnnotationComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + GeneratedColumn get localId => + $composableBuilder(column: $table.localId, builder: (column) => column); + + GeneratedColumn get serverId => + $composableBuilder(column: $table.serverId, builder: (column) => column); + + GeneratedColumn get syncStatus => $composableBuilder( + column: $table.syncStatus, + builder: (column) => column, + ); + + GeneratedColumn get createdAt => + $composableBuilder(column: $table.createdAt, builder: (column) => column); + + GeneratedColumn get updatedAt => + $composableBuilder(column: $table.updatedAt, builder: (column) => column); + + GeneratedColumn get deletedAt => + $composableBuilder(column: $table.deletedAt, builder: (column) => column); + + $$CampaignsTableAnnotationComposer get campaignId { + final $$CampaignsTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.campaignId, + referencedTable: $db.campaigns, + getReferencedColumn: (t) => t.localId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$CampaignsTableAnnotationComposer( + $db: $db, + $table: $db.campaigns, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } + + $$OutletsTableAnnotationComposer get outletId { + final $$OutletsTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.outletId, + referencedTable: $db.outlets, + getReferencedColumn: (t) => t.localId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$OutletsTableAnnotationComposer( + $db: $db, + $table: $db.outlets, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } +} + +class $$OutletCampaignsTableTableManager + extends + RootTableManager< + _$AppDatabase, + $OutletCampaignsTable, + OutletCampaign, + $$OutletCampaignsTableFilterComposer, + $$OutletCampaignsTableOrderingComposer, + $$OutletCampaignsTableAnnotationComposer, + $$OutletCampaignsTableCreateCompanionBuilder, + $$OutletCampaignsTableUpdateCompanionBuilder, + (OutletCampaign, $$OutletCampaignsTableReferences), + OutletCampaign, + PrefetchHooks Function({bool campaignId, bool outletId}) + > { + $$OutletCampaignsTableTableManager( + _$AppDatabase db, + $OutletCampaignsTable table, + ) : super( + TableManagerState( + db: db, + table: table, + createFilteringComposer: () => + $$OutletCampaignsTableFilterComposer($db: db, $table: table), + createOrderingComposer: () => + $$OutletCampaignsTableOrderingComposer($db: db, $table: table), + createComputedFieldComposer: () => + $$OutletCampaignsTableAnnotationComposer($db: db, $table: table), + updateCompanionCallback: + ({ + Value localId = const Value.absent(), + Value serverId = const Value.absent(), + Value campaignId = const Value.absent(), + Value outletId = const Value.absent(), + Value syncStatus = const Value.absent(), + Value createdAt = const Value.absent(), + Value updatedAt = const Value.absent(), + Value deletedAt = const Value.absent(), + Value rowid = const Value.absent(), + }) => OutletCampaignsCompanion( + localId: localId, + serverId: serverId, + campaignId: campaignId, + outletId: outletId, + syncStatus: syncStatus, + createdAt: createdAt, + updatedAt: updatedAt, + deletedAt: deletedAt, + rowid: rowid, + ), + createCompanionCallback: + ({ + Value localId = const Value.absent(), + Value serverId = const Value.absent(), + required String campaignId, + Value outletId = const Value.absent(), + Value syncStatus = const Value.absent(), + Value createdAt = const Value.absent(), + Value updatedAt = const Value.absent(), + Value deletedAt = const Value.absent(), + Value rowid = const Value.absent(), + }) => OutletCampaignsCompanion.insert( + localId: localId, + serverId: serverId, + campaignId: campaignId, + outletId: outletId, + syncStatus: syncStatus, + createdAt: createdAt, + updatedAt: updatedAt, + deletedAt: deletedAt, + rowid: rowid, + ), + withReferenceMapper: (p0) => p0 + .map( + (e) => ( + e.readTable(table), + $$OutletCampaignsTableReferences(db, table, e), + ), + ) + .toList(), + prefetchHooksCallback: ({campaignId = false, outletId = false}) { + return PrefetchHooks( + db: db, + explicitlyWatchedTables: [], + addJoins: + < + T extends TableManagerState< + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic + > + >(state) { + if (campaignId) { + state = + state.withJoin( + currentTable: table, + currentColumn: table.campaignId, + referencedTable: + $$OutletCampaignsTableReferences + ._campaignIdTable(db), + referencedColumn: + $$OutletCampaignsTableReferences + ._campaignIdTable(db) + .localId, + ) + as T; + } + if (outletId) { + state = + state.withJoin( + currentTable: table, + currentColumn: table.outletId, + referencedTable: + $$OutletCampaignsTableReferences + ._outletIdTable(db), + referencedColumn: + $$OutletCampaignsTableReferences + ._outletIdTable(db) + .localId, + ) + as T; + } + + return state; + }, + getPrefetchedDataCallback: (items) async { + return []; + }, + ); + }, + ), + ); +} + +typedef $$OutletCampaignsTableProcessedTableManager = + ProcessedTableManager< + _$AppDatabase, + $OutletCampaignsTable, + OutletCampaign, + $$OutletCampaignsTableFilterComposer, + $$OutletCampaignsTableOrderingComposer, + $$OutletCampaignsTableAnnotationComposer, + $$OutletCampaignsTableCreateCompanionBuilder, + $$OutletCampaignsTableUpdateCompanionBuilder, + (OutletCampaign, $$OutletCampaignsTableReferences), + OutletCampaign, + PrefetchHooks Function({bool campaignId, bool outletId}) + >; +typedef $$SyncQueuesTableCreateCompanionBuilder = + SyncQueuesCompanion Function({ + Value id, + required String tableType, + required String tableLocalId, + required String action, + required String status, + required Map payload, + Value batchId, + Value lastError, + Value retryCount, + Value createdAt, + Value lastAttempt, + }); +typedef $$SyncQueuesTableUpdateCompanionBuilder = + SyncQueuesCompanion Function({ + Value id, + Value tableType, + Value tableLocalId, + Value action, + Value status, + Value> payload, + Value batchId, + Value lastError, + Value retryCount, + Value createdAt, + Value lastAttempt, + }); + +class $$SyncQueuesTableFilterComposer + extends Composer<_$AppDatabase, $SyncQueuesTable> { + $$SyncQueuesTableFilterComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnFilters get id => $composableBuilder( + column: $table.id, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get tableType => $composableBuilder( + column: $table.tableType, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get tableLocalId => $composableBuilder( + column: $table.tableLocalId, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get action => $composableBuilder( + column: $table.action, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get status => $composableBuilder( + column: $table.status, + builder: (column) => ColumnFilters(column), + ); + + ColumnWithTypeConverterFilters< + Map, + Map, + String + > + get payload => $composableBuilder( + column: $table.payload, + builder: (column) => ColumnWithTypeConverterFilters(column), + ); + + ColumnFilters get batchId => $composableBuilder( + column: $table.batchId, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get lastError => $composableBuilder( + column: $table.lastError, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get retryCount => $composableBuilder( + column: $table.retryCount, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get createdAt => $composableBuilder( + column: $table.createdAt, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get lastAttempt => $composableBuilder( + column: $table.lastAttempt, + builder: (column) => ColumnFilters(column), + ); +} + +class $$SyncQueuesTableOrderingComposer + extends Composer<_$AppDatabase, $SyncQueuesTable> { + $$SyncQueuesTableOrderingComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnOrderings get id => $composableBuilder( + column: $table.id, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get tableType => $composableBuilder( + column: $table.tableType, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get tableLocalId => $composableBuilder( + column: $table.tableLocalId, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get action => $composableBuilder( + column: $table.action, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get status => $composableBuilder( + column: $table.status, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get payload => $composableBuilder( + column: $table.payload, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get batchId => $composableBuilder( + column: $table.batchId, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get lastError => $composableBuilder( + column: $table.lastError, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get retryCount => $composableBuilder( + column: $table.retryCount, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get createdAt => $composableBuilder( + column: $table.createdAt, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get lastAttempt => $composableBuilder( + column: $table.lastAttempt, + builder: (column) => ColumnOrderings(column), + ); +} + +class $$SyncQueuesTableAnnotationComposer + extends Composer<_$AppDatabase, $SyncQueuesTable> { + $$SyncQueuesTableAnnotationComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + GeneratedColumn get id => + $composableBuilder(column: $table.id, builder: (column) => column); + + GeneratedColumn get tableType => + $composableBuilder(column: $table.tableType, builder: (column) => column); + + GeneratedColumn get tableLocalId => $composableBuilder( + column: $table.tableLocalId, + builder: (column) => column, + ); + + GeneratedColumn get action => + $composableBuilder(column: $table.action, builder: (column) => column); + + GeneratedColumn get status => + $composableBuilder(column: $table.status, builder: (column) => column); + + GeneratedColumnWithTypeConverter, String> get payload => + $composableBuilder(column: $table.payload, builder: (column) => column); + + GeneratedColumn get batchId => + $composableBuilder(column: $table.batchId, builder: (column) => column); + + GeneratedColumn get lastError => + $composableBuilder(column: $table.lastError, builder: (column) => column); + + GeneratedColumn get retryCount => $composableBuilder( + column: $table.retryCount, + builder: (column) => column, + ); + + GeneratedColumn get createdAt => + $composableBuilder(column: $table.createdAt, builder: (column) => column); + + GeneratedColumn get lastAttempt => $composableBuilder( + column: $table.lastAttempt, + builder: (column) => column, + ); +} + +class $$SyncQueuesTableTableManager + extends + RootTableManager< + _$AppDatabase, + $SyncQueuesTable, + SyncQueue, + $$SyncQueuesTableFilterComposer, + $$SyncQueuesTableOrderingComposer, + $$SyncQueuesTableAnnotationComposer, + $$SyncQueuesTableCreateCompanionBuilder, + $$SyncQueuesTableUpdateCompanionBuilder, + ( + SyncQueue, + BaseReferences<_$AppDatabase, $SyncQueuesTable, SyncQueue>, + ), + SyncQueue, + PrefetchHooks Function() + > { + $$SyncQueuesTableTableManager(_$AppDatabase db, $SyncQueuesTable table) + : super( + TableManagerState( + db: db, + table: table, + createFilteringComposer: () => + $$SyncQueuesTableFilterComposer($db: db, $table: table), + createOrderingComposer: () => + $$SyncQueuesTableOrderingComposer($db: db, $table: table), + createComputedFieldComposer: () => + $$SyncQueuesTableAnnotationComposer($db: db, $table: table), + updateCompanionCallback: + ({ + Value id = const Value.absent(), + Value tableType = const Value.absent(), + Value tableLocalId = const Value.absent(), + Value action = const Value.absent(), + Value status = const Value.absent(), + Value> payload = const Value.absent(), + Value batchId = const Value.absent(), + Value lastError = const Value.absent(), + Value retryCount = const Value.absent(), + Value createdAt = const Value.absent(), + Value lastAttempt = const Value.absent(), + }) => SyncQueuesCompanion( + id: id, + tableType: tableType, + tableLocalId: tableLocalId, + action: action, + status: status, + payload: payload, + batchId: batchId, + lastError: lastError, + retryCount: retryCount, + createdAt: createdAt, + lastAttempt: lastAttempt, + ), + createCompanionCallback: + ({ + Value id = const Value.absent(), + required String tableType, + required String tableLocalId, + required String action, + required String status, + required Map payload, + Value batchId = const Value.absent(), + Value lastError = const Value.absent(), + Value retryCount = const Value.absent(), + Value createdAt = const Value.absent(), + Value lastAttempt = const Value.absent(), + }) => SyncQueuesCompanion.insert( + id: id, + tableType: tableType, + tableLocalId: tableLocalId, + action: action, + status: status, + payload: payload, + batchId: batchId, + lastError: lastError, + retryCount: retryCount, + createdAt: createdAt, + lastAttempt: lastAttempt, + ), + withReferenceMapper: (p0) => p0 + .map((e) => (e.readTable(table), BaseReferences(db, table, e))) + .toList(), + prefetchHooksCallback: null, + ), + ); +} + +typedef $$SyncQueuesTableProcessedTableManager = + ProcessedTableManager< + _$AppDatabase, + $SyncQueuesTable, + SyncQueue, + $$SyncQueuesTableFilterComposer, + $$SyncQueuesTableOrderingComposer, + $$SyncQueuesTableAnnotationComposer, + $$SyncQueuesTableCreateCompanionBuilder, + $$SyncQueuesTableUpdateCompanionBuilder, + (SyncQueue, BaseReferences<_$AppDatabase, $SyncQueuesTable, SyncQueue>), + SyncQueue, + PrefetchHooks Function() + >; + +class $AppDatabaseManager { + final _$AppDatabase _db; + $AppDatabaseManager(this._db); + $$TenantsTableTableManager get tenants => + $$TenantsTableTableManager(_db, _db.tenants); + $$TenantPaymentMethodsTableTableManager get tenantPaymentMethods => + $$TenantPaymentMethodsTableTableManager(_db, _db.tenantPaymentMethods); + $$OutletsTableTableManager get outlets => + $$OutletsTableTableManager(_db, _db.outlets); + $$UsersTableTableManager get users => + $$UsersTableTableManager(_db, _db.users); + $$CustomersTableTableManager get customers => + $$CustomersTableTableManager(_db, _db.customers); + $$CustomerAddressesTableTableManager get customerAddresses => + $$CustomerAddressesTableTableManager(_db, _db.customerAddresses); + $$CategoriesTableTableManager get categories => + $$CategoriesTableTableManager(_db, _db.categories); + $$UnitsTableTableManager get units => + $$UnitsTableTableManager(_db, _db.units); + $$ProductsTableTableManager get products => + $$ProductsTableTableManager(_db, _db.products); + $$ProductVariantsTableTableManager get productVariants => + $$ProductVariantsTableTableManager(_db, _db.productVariants); + $$ProductReviewsTableTableManager get productReviews => + $$ProductReviewsTableTableManager(_db, _db.productReviews); + $$ProductLikesTableTableManager get productLikes => + $$ProductLikesTableTableManager(_db, _db.productLikes); + $$RawMaterialsTableTableManager get rawMaterials => + $$RawMaterialsTableTableManager(_db, _db.rawMaterials); + $$OutletInventoriesTableTableManager get outletInventories => + $$OutletInventoriesTableTableManager(_db, _db.outletInventories); + $$ReceiptItemsTableTableManager get receiptItems => + $$ReceiptItemsTableTableManager(_db, _db.receiptItems); + $$StockCardsTableTableManager get stockCards => + $$StockCardsTableTableManager(_db, _db.stockCards); + $$OrdersTableTableManager get orders => + $$OrdersTableTableManager(_db, _db.orders); + $$OrderItemsTableTableManager get orderItems => + $$OrderItemsTableTableManager(_db, _db.orderItems); + $$SuppliersTableTableManager get suppliers => + $$SuppliersTableTableManager(_db, _db.suppliers); + $$PurchasesTableTableManager get purchases => + $$PurchasesTableTableManager(_db, _db.purchases); + $$PurchaseItemsTableTableManager get purchaseItems => + $$PurchaseItemsTableTableManager(_db, _db.purchaseItems); + $$TransactionsTableTableManager get transactions => + $$TransactionsTableTableManager(_db, _db.transactions); + $$CampaignsTableTableManager get campaigns => + $$CampaignsTableTableManager(_db, _db.campaigns); + $$OutletCampaignsTableTableManager get outletCampaigns => + $$OutletCampaignsTableTableManager(_db, _db.outletCampaigns); + $$SyncQueuesTableTableManager get syncQueues => + $$SyncQueuesTableTableManager(_db, _db.syncQueues); +} diff --git a/Mobile Operasional/lib/core/database/daos/campaign_dao.dart b/Mobile Operasional/lib/core/database/daos/campaign_dao.dart new file mode 100644 index 0000000..a169ffe --- /dev/null +++ b/Mobile Operasional/lib/core/database/daos/campaign_dao.dart @@ -0,0 +1,361 @@ +import 'package:drift/drift.dart'; +import 'package:niogu_app/core/database/app_database.dart'; +import 'package:niogu_app/core/database/tables/campaigns.dart'; +import 'package:niogu_app/core/database/tables/categories.dart'; +import 'package:niogu_app/core/database/tables/outlet_campaigns.dart'; +import 'package:niogu_app/core/database/tables/products.dart'; +import 'package:niogu_app/core/database/tables/sync_queues.dart'; +import 'package:niogu_app/core/entities/custom_sync_queue.dart'; +import 'package:niogu_app/core/enums/campaign_type.dart'; +import 'package:niogu_app/core/enums/sync_status.dart'; +import 'package:niogu_app/core/enums/table_type.dart'; +import 'package:niogu_app/core/utils/log_message.dart'; +import 'package:niogu_app/features/online_store/domain/entities/online_store.dart'; + +part 'campaign_dao.g.dart'; + +@DriftAccessor( + tables: [Products, Categories, Campaigns, OutletCampaigns, SyncQueues], +) +class CampaignDao extends DatabaseAccessor + with _$CampaignDaoMixin { + CampaignDao(AppDatabase db) : super(db); + + Stream> watchAllCampaigns() async* { + final query = + selectOnly(campaigns).join([ + innerJoin( + outletCampaigns, + outletCampaigns.campaignId.equalsExp(campaigns.localId), + ), + innerJoin( + outlets, + outlets.localId.equalsExp(outletCampaigns.outletId) & + outlets.deletedAt.isNull(), + ), + ]) + ..addColumns([ + campaigns.localId, + campaigns.localBannerPath, + campaigns.title, + campaigns.startDate, + campaigns.endDate, + campaigns.isActive, + outletCampaigns.localId.count(), + ]) + ..groupBy([campaigns.localId]) + ..where(campaigns.deletedAt.isNull()) + ..where(outletCampaigns.deletedAt.isNull()); + + yield* query.watch().map((rows) { + return rows.map((row) { + return DisplayCampaign( + id: row.read(campaigns.localId)!, + imagePath: row.read(campaigns.localBannerPath)!, + name: row.read(campaigns.title)!, + startDate: row.read(campaigns.startDate), + endDate: row.read(campaigns.endDate), + isActive: row.read(campaigns.isActive) ?? false, + outletUseAmount: row.read(outletCampaigns.localId.count()) ?? 0, + ); + }).toList(); + }); + } + + Future getCampaignById(String id) async { + final campaignQuery = selectOnly(campaigns) + ..addColumns([ + campaigns.localId, + campaigns.localBannerPath, + campaigns.title, + campaigns.startDate, + campaigns.endDate, + campaigns.actionType, + campaigns.actionReferenceId, + campaigns.isActive, + ]) + ..where(campaigns.localId.equals(id)) + ..limit(1); + + final campaignRow = await campaignQuery.getSingle(); + + final campaignId = campaignRow.read(campaigns.localId)!; + + final actionType = campaignRow.read(campaigns.actionType); + + final actionId = campaignRow.read(campaigns.actionReferenceId); + + CampaignType? campaignType; + + String? linkId; + + String? linkName; + + if (actionType != null && actionId != null) { + campaignType = CampaignType.values.byName(actionType); + + if (campaignType == CampaignType.product) { + final linkQuery = selectOnly(products) + ..addColumns([products.localId, products.name]) + ..where(products.localId.equals(actionId)) + ..limit(1); + + final linkRow = await linkQuery.getSingle(); + + linkId = linkRow.read(products.localId)!; + + linkName = linkRow.read(products.name) ?? ''; + } else { + final linkQuery = selectOnly(categories) + ..addColumns([categories.localId, categories.name]) + ..where(categories.localId.equals(actionId)) + ..limit(1); + + final linkRow = await linkQuery.getSingle(); + + linkId = linkRow.read(categories.localId)!; + + linkName = linkRow.read(categories.name) ?? ''; + } + } + + final outletCampaignQuery = + selectOnly(outletCampaigns).join([ + innerJoin( + outlets, + outlets.localId.equalsExp(outletCampaigns.outletId), + ), + ]) + ..addColumns([ + outletCampaigns.localId, + outletCampaigns.outletId, + outlets.name, + ]) + ..where(outletCampaigns.campaignId.equals(campaignId)) + ..where(outletCampaigns.deletedAt.isNull()); + + final outletCampaignRows = await outletCampaignQuery.get(); + + final targetOutlets = outletCampaignRows.map((row) { + return SelectedTargetOutlet( + id: row.read(outletCampaigns.localId)!, + outletId: row.read(outletCampaigns.outletId)!, + name: row.read(outlets.name) ?? '', + ); + }).toList(); + + return CampaignDetail( + id: campaignId, + imagePath: campaignRow.read(campaigns.localBannerPath)!, + name: campaignRow.read(campaigns.title) ?? '', + startDate: campaignRow.read(campaigns.startDate), + endDate: campaignRow.read(campaigns.endDate), + campaignType: campaignType, + linkId: linkId, + linkName: linkName, + targetOutlets: targetOutlets, + isActive: campaignRow.read(campaigns.isActive) ?? false, + ); + } + + Future saveCampaign( + UpsertCampaign campaign, + List? deleteTargetOutlets, + ) async { + try { + await transaction(() async { + final now = DateTime.now(); + + await batch((batch) async { + batch.insert( + campaigns, + CampaignsCompanion( + localId: Value(campaign.id), + title: Value(campaign.name), + localBannerPath: Value(campaign.imagePath), + startDate: Value(campaign.startDate), + endDate: Value(campaign.endDate), + actionType: Value(campaign.campaignType?.type), + actionReferenceId: Value(campaign.linkId), + ), + onConflict: DoUpdate((_) { + return CampaignsCompanion( + title: Value(campaign.name), + localBannerPath: Value(campaign.imagePath), + startDate: Value(campaign.startDate), + endDate: Value(campaign.endDate), + actionType: Value(campaign.campaignType?.type), + actionReferenceId: Value(campaign.linkId), + isActive: Value(campaign.isActive), + syncStatus: Value(SyncStatus.dirty.status), + updatedAt: Value(now), + ); + }), + ); + + final existing = + await (select(syncQueues)..where( + (tbl) => + tbl.tableType.equals(TableType.campaigns.type) & + tbl.tableLocalId.equals(campaign.id), + )) + .getSingleOrNull(); + + if (existing != null) { + CustomSyncQueue.updateSyncQueue( + type: TableType.campaigns, + id: campaign.id, + action: SyncAction.update, + batch: batch, + syncQueues: syncQueues, + data: { + 'uuid': campaign.id, + 'title': campaign.name, + 'server_banner_url': campaign.imagePath, + 'start_date': campaign.startDate, + 'end_date': campaign.endDate, + 'action_type': campaign.campaignType?.type, + 'action_reference_id': campaign.linkId, + 'is_active': campaign.isActive, + 'updated_at': now.toIso8601String(), + }, + ); + } else { + CustomSyncQueue.addToSyncQueue( + type: TableType.campaigns, + id: campaign.id, + action: SyncAction.create, + batch: batch, + syncQueues: syncQueues, + data: { + 'uuid': campaign.id, + 'title': campaign.name, + 'server_banner_url': campaign.imagePath, + 'start_date': campaign.startDate, + 'end_date': campaign.endDate, + 'action_type': campaign.campaignType?.type, + 'action_reference_id': campaign.linkId, + 'created_at': now.toIso8601String(), + 'updated_at': now.toIso8601String(), + }, + ); + } + + if (deleteTargetOutlets != null && deleteTargetOutlets.isNotEmpty) { + batch.update( + outletCampaigns, + OutletCampaignsCompanion( + syncStatus: Value(SyncStatus.dirty.status), + deletedAt: Value(now), + ), + where: (tbl) => tbl.localId.isIn(deleteTargetOutlets), + ); + + CustomSyncQueue.addToSyncQueue( + type: TableType.outlet_campaigns, + id: 'BULK_DELETE_${now.millisecondsSinceEpoch}', + action: SyncAction.bulk_update, + batch: batch, + syncQueues: syncQueues, + data: { + 'outlet_ids': deleteTargetOutlets, + 'deleted_at': now.toIso8601String(), + }, + ); + } + + for (final outlet in campaign.targetOutlets) { + final outletCampign = OutletCampaignsCompanion( + localId: Value(outlet.id), + campaignId: Value(campaign.id), + outletId: Value(outlet.outletId), + ); + batch.insert( + outletCampaigns, + outletCampign, + onConflict: DoUpdate((_) { + return outletCampign.copyWith( + syncStatus: Value(SyncStatus.dirty.status), + updatedAt: Value(now), + ); + }), + ); + + CustomSyncQueue.addToSyncQueue( + type: TableType.outlet_campaigns, + id: outlet.id, + action: SyncAction.update_or_create, + batch: batch, + syncQueues: syncQueues, + data: { + 'uuid': outlet.id, + 'campaign_id': campaign.id, + 'outlet_id': outlet.outletId, + 'updated_at': now.toIso8601String(), + }, + ); + } + }); + }); + } catch (e, st) { + LogMessage.log.e(e.toString(), error: e, stackTrace: st); + rethrow; + } + } + + Future deleteCampaign(String campaignId) async { + try { + await transaction(() async { + final now = DateTime.now(); + + await batch((batch) { + batch.update( + campaigns, + CampaignsCompanion( + syncStatus: Value(SyncStatus.dirty.status), + deletedAt: Value(now), + ), + where: (tbl) => tbl.localId.equals(campaignId), + ); + + CustomSyncQueue.addToSyncQueue( + type: TableType.campaigns, + id: campaignId, + action: SyncAction.delete, + batch: batch, + syncQueues: syncQueues, + data: { + 'uuid': campaignId, + 'deleted_at': now.toIso8601String(), + }, + ); + + batch.update( + outletCampaigns, + OutletCampaignsCompanion( + syncStatus: Value(SyncStatus.dirty.status), + deletedAt: Value(now), + ), + where: (tbl) => tbl.campaignId.equals(campaignId), + ); + + CustomSyncQueue.addToSyncQueue( + type: TableType.outlet_campaigns, + id: campaignId, + action: SyncAction.delete, + batch: batch, + syncQueues: syncQueues, + data: { + 'campaign_id': campaignId, + 'deleted_at': now.toIso8601String(), + }, + ); + + }); + }); + } catch (e, st) { + LogMessage.log.e(e.toString(), error: e, stackTrace: st); + rethrow; + } + } +} diff --git a/Mobile Operasional/lib/core/database/daos/campaign_dao.g.dart b/Mobile Operasional/lib/core/database/daos/campaign_dao.g.dart new file mode 100644 index 0000000..3d1d04b --- /dev/null +++ b/Mobile Operasional/lib/core/database/daos/campaign_dao.g.dart @@ -0,0 +1,37 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'campaign_dao.dart'; + +// ignore_for_file: type=lint +mixin _$CampaignDaoMixin on DatabaseAccessor { + $CategoriesTable get categories => attachedDatabase.categories; + $UnitsTable get units => attachedDatabase.units; + $ProductsTable get products => attachedDatabase.products; + $CampaignsTable get campaigns => attachedDatabase.campaigns; + $OutletsTable get outlets => attachedDatabase.outlets; + $OutletCampaignsTable get outletCampaigns => attachedDatabase.outletCampaigns; + $SyncQueuesTable get syncQueues => attachedDatabase.syncQueues; + CampaignDaoManager get managers => CampaignDaoManager(this); +} + +class CampaignDaoManager { + final _$CampaignDaoMixin _db; + CampaignDaoManager(this._db); + $$CategoriesTableTableManager get categories => + $$CategoriesTableTableManager(_db.attachedDatabase, _db.categories); + $$UnitsTableTableManager get units => + $$UnitsTableTableManager(_db.attachedDatabase, _db.units); + $$ProductsTableTableManager get products => + $$ProductsTableTableManager(_db.attachedDatabase, _db.products); + $$CampaignsTableTableManager get campaigns => + $$CampaignsTableTableManager(_db.attachedDatabase, _db.campaigns); + $$OutletsTableTableManager get outlets => + $$OutletsTableTableManager(_db.attachedDatabase, _db.outlets); + $$OutletCampaignsTableTableManager get outletCampaigns => + $$OutletCampaignsTableTableManager( + _db.attachedDatabase, + _db.outletCampaigns, + ); + $$SyncQueuesTableTableManager get syncQueues => + $$SyncQueuesTableTableManager(_db.attachedDatabase, _db.syncQueues); +} diff --git a/Mobile Operasional/lib/core/database/daos/category_dao.dart b/Mobile Operasional/lib/core/database/daos/category_dao.dart new file mode 100644 index 0000000..6d96edd --- /dev/null +++ b/Mobile Operasional/lib/core/database/daos/category_dao.dart @@ -0,0 +1,149 @@ +import 'package:drift/drift.dart'; +import 'package:niogu_app/core/database/app_database.dart' hide Category; +import 'package:niogu_app/core/database/tables/categories.dart'; +import 'package:niogu_app/core/database/tables/sync_queues.dart'; +import 'package:niogu_app/core/entities/custom_sync_queue.dart'; +import 'package:niogu_app/core/enums/sync_status.dart'; +import 'package:niogu_app/core/enums/table_type.dart'; +import 'package:niogu_app/features/goods/products/domain/entities/category.dart'; + +part 'category_dao.g.dart'; + +@DriftAccessor(tables: [Categories, SyncQueues]) +class CategoryDao extends DatabaseAccessor + with _$CategoryDaoMixin { + CategoryDao(AppDatabase db) : super(db); + + Stream> watchAllCategories() async* { + final query = selectOnly(categories) + ..addColumns([categories.localId, categories.name]) + ..orderBy([OrderingTerm.desc(categories.createdAt)]); + + yield* query.watch().map((rows) { + return rows.map((row) { + final String id = row.read(categories.localId) ?? ''; + final String name = row.read(categories.name) ?? ''; + return Category(id: id, name: name); + }).toList(); + }); + } + + Future> getAllCategories() async { + final query = selectOnly(categories) + ..addColumns([categories.localId, categories.name]) + ..orderBy([OrderingTerm.desc(categories.createdAt)]); + + final results = await query.get(); + + return results.map((row) { + final String id = row.read(categories.localId) ?? ''; + final String name = row.read(categories.name) ?? ''; + return Category(id: id, name: name); + }).toList(); + } + + Future getCategoryById(String id) async { + final query = selectOnly(categories) + ..addColumns([ + categories.localId, + categories.name, + categories.localImagePath, + ]) + ..where(categories.localId.equals(id)) + ..limit(1); + + final result = await query.getSingle(); + final String localId = result.read(categories.localId) ?? ''; + final String name = result.read(categories.name) ?? ''; + final String imagePath = result.read(categories.localImagePath) ?? ''; + return Category(id: localId, name: name, imagePath: imagePath); + } + + Future insertCategory(CategoriesCompanion category) async { + return await transaction(() async { + final now = DateTime.now(); + + final id = await into(categories).insert(category); + + final payload = SyncPayload( + table: TableType.categories.type, + localId: category.localId.value, + action: SyncAction.create.action, + data: { + 'uuid': category.localId.value, + 'name': category.name.value, + 'server_image_url': category.localImagePath.value, + 'created_at': now.toIso8601String(), + 'updated_at': now.toIso8601String(), + }, + ).toJson(); + + await into(syncQueues).insert( + SyncQueuesCompanion( + tableType: Value(TableType.categories.type), + tableLocalId: category.localId, + action: Value(SyncAction.create.action), + status: Value(SyncStatus.dirty.status), + payload: Value(payload), + ), + ); + + return id; + }); + } + + Future updateCategory(String id, String name, String? imagePath) async { + return await transaction(() async { + final now = DateTime.now(); + + final rowsAffected = + await (update( + categories, + )..where((category) => category.localId.equals(id))).write( + CategoriesCompanion( + name: Value(name), + localImagePath: Value(imagePath), + syncStatus: Value(SyncStatus.dirty.status), + updatedAt: Value(now), + ), + ); + + final payload = SyncPayload( + table: TableType.categories.type, + localId: id, + action: SyncAction.update.action, + data: { + 'uuid': id, + 'name': name, + 'server_image_url': imagePath, + 'updated_at': now.toIso8601String(), + }, + ).toJson(); + + final syncQueueComp = SyncQueuesCompanion( + tableType: Value(TableType.categories.type), + tableLocalId: Value(id), + action: Value(SyncAction.update.action), + status: Value(SyncStatus.dirty.status), + payload: Value(payload), + ); + + final existing = + await (select(syncQueues)..where( + (t) => + t.tableType.equals(TableType.categories.type) & + t.tableLocalId.equals(id), + )) + .getSingleOrNull(); + + if (existing != null) { + await (update( + syncQueues, + )..where((q) => q.id.equals(existing.id))).write(syncQueueComp); + } else { + await into(syncQueues).insert(syncQueueComp); + } + return rowsAffected; + }); + } +} diff --git a/Mobile Operasional/lib/core/database/daos/category_dao.g.dart b/Mobile Operasional/lib/core/database/daos/category_dao.g.dart new file mode 100644 index 0000000..128ed7f --- /dev/null +++ b/Mobile Operasional/lib/core/database/daos/category_dao.g.dart @@ -0,0 +1,19 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'category_dao.dart'; + +// ignore_for_file: type=lint +mixin _$CategoryDaoMixin on DatabaseAccessor { + $CategoriesTable get categories => attachedDatabase.categories; + $SyncQueuesTable get syncQueues => attachedDatabase.syncQueues; + CategoryDaoManager get managers => CategoryDaoManager(this); +} + +class CategoryDaoManager { + final _$CategoryDaoMixin _db; + CategoryDaoManager(this._db); + $$CategoriesTableTableManager get categories => + $$CategoriesTableTableManager(_db.attachedDatabase, _db.categories); + $$SyncQueuesTableTableManager get syncQueues => + $$SyncQueuesTableTableManager(_db.attachedDatabase, _db.syncQueues); +} diff --git a/Mobile Operasional/lib/core/database/daos/customer_address_dao.dart b/Mobile Operasional/lib/core/database/daos/customer_address_dao.dart new file mode 100644 index 0000000..570d340 --- /dev/null +++ b/Mobile Operasional/lib/core/database/daos/customer_address_dao.dart @@ -0,0 +1,11 @@ +import 'package:drift/drift.dart'; +import 'package:niogu_app/core/database/app_database.dart'; +import 'package:niogu_app/core/database/tables/customer_addresses.dart'; + +part 'customer_address_dao.g.dart'; + +@DriftAccessor(tables: [CustomerAddresses]) +class CustomerAddressDao extends DatabaseAccessor with _$CustomerAddressDaoMixin { + + CustomerAddressDao(AppDatabase db) : super(db); +} \ No newline at end of file diff --git a/Mobile Operasional/lib/core/database/daos/customer_address_dao.g.dart b/Mobile Operasional/lib/core/database/daos/customer_address_dao.g.dart new file mode 100644 index 0000000..4871db7 --- /dev/null +++ b/Mobile Operasional/lib/core/database/daos/customer_address_dao.g.dart @@ -0,0 +1,26 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'customer_address_dao.dart'; + +// ignore_for_file: type=lint +mixin _$CustomerAddressDaoMixin on DatabaseAccessor { + $OutletsTable get outlets => attachedDatabase.outlets; + $CustomersTable get customers => attachedDatabase.customers; + $CustomerAddressesTable get customerAddresses => + attachedDatabase.customerAddresses; + CustomerAddressDaoManager get managers => CustomerAddressDaoManager(this); +} + +class CustomerAddressDaoManager { + final _$CustomerAddressDaoMixin _db; + CustomerAddressDaoManager(this._db); + $$OutletsTableTableManager get outlets => + $$OutletsTableTableManager(_db.attachedDatabase, _db.outlets); + $$CustomersTableTableManager get customers => + $$CustomersTableTableManager(_db.attachedDatabase, _db.customers); + $$CustomerAddressesTableTableManager get customerAddresses => + $$CustomerAddressesTableTableManager( + _db.attachedDatabase, + _db.customerAddresses, + ); +} diff --git a/Mobile Operasional/lib/core/database/daos/customer_dao.dart b/Mobile Operasional/lib/core/database/daos/customer_dao.dart new file mode 100644 index 0000000..8d29c56 --- /dev/null +++ b/Mobile Operasional/lib/core/database/daos/customer_dao.dart @@ -0,0 +1,323 @@ +import 'package:drift/drift.dart'; +import 'package:latlong2/latlong.dart'; +import 'package:niogu_app/core/database/app_database.dart'; +import 'package:niogu_app/core/database/tables/customer_addresses.dart'; +import 'package:niogu_app/core/database/tables/customers.dart'; +import 'package:niogu_app/core/database/tables/orders.dart'; +import 'package:niogu_app/core/database/tables/sync_queues.dart'; +import 'package:niogu_app/core/entities/custom_sync_queue.dart'; +import 'package:niogu_app/core/enums/customer_source.dart'; +import 'package:niogu_app/core/enums/table_type.dart'; +import 'package:niogu_app/core/utils/extension_format.dart'; +import 'package:niogu_app/core/enums/order_source.dart'; +import 'package:niogu_app/core/enums/order_status.dart'; +import 'package:niogu_app/core/enums/sync_status.dart'; +import 'package:niogu_app/core/system/system_setting.dart'; +import 'package:niogu_app/features/customer/domain/entities/customer.dart'; +import 'package:niogu_app/features/pos/domain/entities/pos.dart'; + +part 'customer_dao.g.dart'; + +@DriftAccessor(tables: [Customers, CustomerAddresses, Orders, SyncQueues]) +class CustomerDao extends DatabaseAccessor + with _$CustomerDaoMixin { + CustomerDao(AppDatabase db) : super(db); + + Stream> watchAllCustomers() async* { + final query = selectOnly(customers) + ..addColumns([customers.localId, customers.name]) + ..where(customers.deletedAt.isNull()); + + yield* query.watch().map((rows) { + return rows.map((row) { + final String id = row.read(customers.localId)!; + + final String name = row.read(customers.name) ?? ''; + + return DisplayCustomers(id: id, name: name); + }).toList(); + }); + } + + Future getCustomerById(String id) async { + final query = selectOnly(customers) + ..addColumns([ + customers.localId, + customers.name, + customers.email, + customers.phoneNumber, + customers.source, + ]) + ..where(customers.localId.equals(id) & customers.deletedAt.isNull()) + ..limit(1); + /** + final customerAddressQuery = selectOnly(customerAddresses) + ..addColumns([ + customerAddresses.label, + customerAddresses.fullAddress, + customerAddresses.latitude, + customerAddresses.longitude, + ]) + ..orderBy([OrderingTerm.desc(customerAddresses.createdAt)]) + ..where( + customerAddresses.customerId.equals(id) & + customerAddresses.deletedAt.isNull(), + ); + + final orderQuery = selectOnly(orders) + ..addColumns([ + orders.localId, + orders.orderNumber, + orders.source, + orders.orderStatus, + orders.createdAt, + ]) + ..orderBy([OrderingTerm.desc(orders.createdAt)]) + ..where(orders.customerId.equals(id)); + */ + + final result = await query.getSingle(); + + final String localId = result.read(customers.localId) ?? ''; + + final String name = result.read(customers.name) ?? ''; + + final String email = result.read(customers.email) ?? ''; + + final String phoneNumber = result.read(customers.phoneNumber) ?? ''; + + final CustomerSource customerSource = CustomerSource.values.byName( + (result.read(customers.source) ?? ''), + ); + + return CustomerDetail( + id: localId, + name: name, + email: email, + phoneNumber: phoneNumber, + customerSource: customerSource, + ); + } + + Stream> watchAllCustomerAddresses( + String customerId, + ) async* { + final query = selectOnly(customerAddresses) + ..addColumns([ + customerAddresses.label, + customerAddresses.fullAddress, + customerAddresses.latitude, + customerAddresses.longitude, + ]) + ..orderBy([OrderingTerm.desc(customerAddresses.createdAt)]) + ..where( + customerAddresses.customerId.equals(customerId) & + customerAddresses.deletedAt.isNull(), + ); + + yield* query.watch().map((rows) { + return rows.map((row) { + final String label = row.read(customerAddresses.label) ?? ''; + + final String fullAddress = + row.read(customerAddresses.fullAddress) ?? ''; + + final double latitude = row.read(customerAddresses.latitude) ?? 0.0; + + final double longitude = row.read(customerAddresses.longitude) ?? 0.0; + + return CustomerAddress( + label: label, + fullAddress: fullAddress, + coordinate: LatLng(latitude, longitude), + ); + }).toList(); + }); + } + + Stream> watchAllCustomerActivities( + String customerId, + ) async* { + final query = selectOnly(orders) + ..addColumns([ + orders.localId, + orders.orderNumber, + orders.source, + orders.orderStatus, + orders.createdAt, + ]) + ..orderBy([OrderingTerm.desc(orders.createdAt)]) + ..where(orders.customerId.equals(customerId)); + + yield* query.watch().map((rows) { + return rows.map((row) { + final String localId = row.read(orders.localId) ?? ''; + + final String orderNumber = (row.read(orders.orderNumber) ?? '') + .toUniqueNumberCleaner(); + + final OrderSource orderSource = OrderSource.values.byName( + (row.read(orders.source) ?? ''), + ); + + final OrderStatus orderStatus = OrderStatus.values.byName( + (row.read(orders.orderStatus) ?? ''), + ); + + final orderDate = row.read(orders.createdAt) ?? DateTime.now(); + + return CustomerActivities( + id: localId, + orderNumber: orderNumber, + orderSource: orderSource, + orderStatus: orderStatus, + orderDate: orderDate, + ); + }).toList(); + }); + } + + Future saveCustomer(CustomersCompanion customer) async { + return await transaction(() async { + final now = DateTime.now(); + + final rowsAffected = await into(customers).insert( + customer, + onConflict: DoUpdate((old) { + return customer.copyWith( + syncStatus: Value(SyncStatus.dirty.status), + createdAt: Value.absent(), + updatedAt: Value(now), + ); + }), + ); + + final payload = SyncPayload( + table: TableType.customers.type, + localId: customer.localId.value, + action: SyncAction.update_or_create.action, + data: { + 'uuid': customer.localId.value, + 'outlet_id': customer.outletId.value, + 'name': customer.name.value, + 'email': customer.email.value, + 'phone_number': customer.phoneNumber.value, + 'source': customer.source.value, + 'created_at': now.toIso8601String(), + 'updated_at': now.toIso8601String(), + }, + ).toJson(); + + final syncQueueComp = SyncQueuesCompanion( + tableType: Value(TableType.customers.type), + tableLocalId: customer.localId, + action: Value(SyncAction.update_or_create.action), + status: Value(SyncStatus.dirty.status), + payload: Value(payload), + ); + + final existing = + await (select(syncQueues)..where( + (t) => + t.tableType.equals(TableType.customers.type) & + t.tableLocalId.equals(customer.localId.value), + )) + .getSingleOrNull(); + + if (existing != null) { + await (update( + syncQueues, + )..where((t) => t.id.equals(existing.id))).write(syncQueueComp); + } else { + await into(syncQueues).insert(syncQueueComp); + } + + return rowsAffected; + }); + } + + Future deleteCustomer(String id) async { + return transaction(() async { + final now = DateTime.now(); + + final rowsAffected = + (await update(customers) + ..where((customer) => customer.localId.equals(id))) + .write( + CustomersCompanion( + syncStatus: Value(SyncStatus.dirty.status), + deletedAt: Value(now), + ), + ); + + final payload = SyncPayload( + table: TableType.customers.type, + localId: id, + action: SyncAction.delete.action, + data: {'uuid': id, 'deleted_at': now.toIso8601String()}, + ).toJson(); + + await into(syncQueues).insert( + SyncQueuesCompanion( + tableType: Value(TableType.customers.type), + tableLocalId: Value(id), + action: Value(SyncAction.delete.action), + status: Value(SyncStatus.dirty.status), + payload: Value(payload), + ), + ); + + return rowsAffected; + }); + } + + Stream> watchAllAlreadyCustomers() async* { + final outletId = await SystemSetting.getCurrentOutletId(); + + final query = + selectOnly(customers).join([ + leftOuterJoin( + customerAddresses, + customerAddresses.customerId.equalsExp(customers.localId), + ), + ]) + ..addColumns([ + customers.localId, + customers.name, + customers.phoneNumber, + customerAddresses.fullAddress, + ]) + ..where( + customers.outletId.equals(outletId!) & customers.deletedAt.isNull(), + ) + ..where(customerAddresses.deletedAt.isNull()); + + yield* query.watch().map((rows) { + final Map groupedMap = {}; + + for (final row in rows) { + final customerId = row.read(customers.localId)!; + + final name = row.read(customers.name) ?? ''; + + final phoneNumber = row.read(customers.phoneNumber) ?? ''; + + groupedMap.putIfAbsent( + customerId, + () => AlreadyCustomer( + id: customerId, + name: name, + phoneNumber: phoneNumber, + addresses: [], + ), + ); + + final address = row.read(customerAddresses.fullAddress) ?? ''; + + groupedMap[customerId]!.addresses.add(address); + } + + return groupedMap.values.toList(); + }); + } +} diff --git a/Mobile Operasional/lib/core/database/daos/customer_dao.g.dart b/Mobile Operasional/lib/core/database/daos/customer_dao.g.dart new file mode 100644 index 0000000..6bcf207 --- /dev/null +++ b/Mobile Operasional/lib/core/database/daos/customer_dao.g.dart @@ -0,0 +1,35 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'customer_dao.dart'; + +// ignore_for_file: type=lint +mixin _$CustomerDaoMixin on DatabaseAccessor { + $OutletsTable get outlets => attachedDatabase.outlets; + $CustomersTable get customers => attachedDatabase.customers; + $CustomerAddressesTable get customerAddresses => + attachedDatabase.customerAddresses; + $UsersTable get users => attachedDatabase.users; + $OrdersTable get orders => attachedDatabase.orders; + $SyncQueuesTable get syncQueues => attachedDatabase.syncQueues; + CustomerDaoManager get managers => CustomerDaoManager(this); +} + +class CustomerDaoManager { + final _$CustomerDaoMixin _db; + CustomerDaoManager(this._db); + $$OutletsTableTableManager get outlets => + $$OutletsTableTableManager(_db.attachedDatabase, _db.outlets); + $$CustomersTableTableManager get customers => + $$CustomersTableTableManager(_db.attachedDatabase, _db.customers); + $$CustomerAddressesTableTableManager get customerAddresses => + $$CustomerAddressesTableTableManager( + _db.attachedDatabase, + _db.customerAddresses, + ); + $$UsersTableTableManager get users => + $$UsersTableTableManager(_db.attachedDatabase, _db.users); + $$OrdersTableTableManager get orders => + $$OrdersTableTableManager(_db.attachedDatabase, _db.orders); + $$SyncQueuesTableTableManager get syncQueues => + $$SyncQueuesTableTableManager(_db.attachedDatabase, _db.syncQueues); +} diff --git a/Mobile Operasional/lib/core/database/daos/order_dao.dart b/Mobile Operasional/lib/core/database/daos/order_dao.dart new file mode 100644 index 0000000..5fd0c53 --- /dev/null +++ b/Mobile Operasional/lib/core/database/daos/order_dao.dart @@ -0,0 +1,1249 @@ +import 'dart:convert'; +import 'dart:math'; + +import 'package:drift/drift.dart'; +import 'package:intl/intl.dart'; +import 'package:niogu_app/core/database/app_database.dart' + hide Product, OrderItem; +import 'package:niogu_app/core/database/tables/customer_addresses.dart'; +import 'package:niogu_app/core/database/tables/customers.dart'; +import 'package:niogu_app/core/database/tables/order_items.dart'; +import 'package:niogu_app/core/database/tables/orders.dart'; +import 'package:niogu_app/core/database/tables/outlet_inventories.dart'; +import 'package:niogu_app/core/database/tables/product_variants.dart'; +import 'package:niogu_app/core/database/tables/products.dart'; +import 'package:niogu_app/core/database/tables/raw_materials.dart'; +import 'package:niogu_app/core/database/tables/receipt_items.dart'; +import 'package:niogu_app/core/database/tables/stock_cards.dart'; +import 'package:niogu_app/core/database/tables/sync_queues.dart'; +import 'package:niogu_app/core/database/tables/transactions.dart'; +import 'package:niogu_app/core/database/tables/units.dart'; +import 'package:niogu_app/core/entities/custom_activity.dart'; +import 'package:niogu_app/core/entities/custom_sync_queue.dart'; +import 'package:niogu_app/core/enums/approval_process.dart'; +import 'package:niogu_app/core/enums/canceled_by.dart'; +import 'package:niogu_app/core/enums/table_type.dart'; +import 'package:niogu_app/core/utils/currency_format.dart'; +import 'package:niogu_app/core/enums/delivery_type.dart'; +import 'package:niogu_app/core/utils/extension_format.dart'; +import 'package:niogu_app/core/enums/item_type.dart'; +import 'package:niogu_app/core/utils/log_message.dart'; +import 'package:niogu_app/core/enums/message_activity_type.dart'; +import 'package:niogu_app/core/enums/order_source.dart'; +import 'package:niogu_app/core/enums/order_status.dart'; +import 'package:niogu_app/core/enums/stock_card_type.dart'; +import 'package:niogu_app/core/enums/stock_type.dart'; +import 'package:niogu_app/core/enums/sync_status.dart'; +import 'package:niogu_app/core/system/system_setting.dart'; +import 'package:niogu_app/core/enums/transaction_reference_type.dart'; +import 'package:niogu_app/core/enums/user_role.dart'; +import 'package:niogu_app/features/goods/products/domain/entities/product.dart'; +import 'package:niogu_app/features/goods/raw_materials/domain/entities/raw_material.dart'; +import 'package:niogu_app/features/online_store/domain/entities/online_store.dart'; +import 'package:niogu_app/features/pos/domain/entities/pos.dart'; +import 'package:niogu_app/features/report/transaction/domain/entities/transaction_report.dart'; +import 'package:uuid/uuid.dart'; + +part 'order_dao.g.dart'; + +@DriftAccessor( + tables: [ + Customers, + CustomerAddresses, + Products, + Units, + RawMaterials, + ProductVariants, + OutletInventories, + Orders, + OrderItems, + ReceiptItems, + StockCards, + Transactions, + SyncQueues, + ], +) +class OrderDao extends DatabaseAccessor with _$OrderDaoMixin { + OrderDao(AppDatabase db) : super(db); + + Stream> watchAllDisplayProductPos() async* { + final outletId = await SystemSetting.getCurrentOutletId(); + + final query = + selectOnly(products).join([ + innerJoin(units, units.localId.equalsExp(products.unitId)), + innerJoin( + productVariants, + productVariants.productId.equalsExp(products.localId), + ), + innerJoin( + outletInventories, + outletInventories.itemType.equals(ItemType.product_variant.type) & + outletInventories.itemId.equalsExp(productVariants.localId), + ), + ]) + ..addColumns([ + products.localId, + productVariants.localId, + outletInventories.localId, + products.localImagePath, + productVariants.localImagePath, + products.name, + productVariants.name, + productVariants.totalSold, + outletInventories.stockType, + outletInventories.stock, + units.name, + outletInventories.costPrice, + outletInventories.sellingPrice, + products.hasVariant, + ]) + ..orderBy([OrderingTerm.desc(productVariants.totalSold)]) + ..where(products.deletedAt.isNull()) + ..where(productVariants.deletedAt.isNull()) + ..where( + outletInventories.outletId.equals(outletId!) & + outletInventories.deletedAt.isNull(), + ); + + yield* query.watch().map((rows) { + return rows.map((row) { + final String productId = row.read(products.localId) ?? ''; + final String productVariantId = row.read(productVariants.localId) ?? ''; + final String outletInventoryId = + row.read(outletInventories.localId) ?? ''; + final dynamic rawImages = row.read(products.localImagePath); + + List productImages = []; + + if (rawImages is String) { + try { + productImages = (jsonDecode(rawImages) as List).cast(); + } catch (e) { + productImages = []; + } + } else if (rawImages is List) { + productImages = (rawImages).cast(); + } + + final bool hasVariant = row.read(products.hasVariant) ?? false; + final String? imagePath = hasVariant + ? row.read(productVariants.localImagePath) + : productImages.lastOrNull; + final String name = row.read(products.name) ?? ''; + final String variantName = row.read(productVariants.name) ?? ''; + final double currentSold = row.read(productVariants.totalSold) ?? 0.0; + final StockType stockType = StockType.values.byName( + (row.read(outletInventories.stockType) ?? ''), + ); + final double remainingStock = row.read(outletInventories.stock) ?? 0.0; + final String unitName = row.read(units.name) ?? ''; + final double costPrice = row.read(outletInventories.costPrice) ?? 0.0; + final double sellingPrice = + row.read(outletInventories.sellingPrice) ?? 0.0; + + return DisplayProductPos( + productId: productId, + productVariantId: productVariantId, + outletInventoryId: outletInventoryId, + imagePath: imagePath, + name: name, + variantName: variantName, + currentSold: currentSold, + stockType: stockType, + remainingStock: remainingStock, + unitName: unitName, + costPrice: costPrice, + sellingPrice: sellingPrice, + hasVariant: hasVariant, + ); + }).toList(); + }); + } + + Future _generateOrderNumber() async { + final now = DateTime.now(); + + final formatter = DateFormat('yyMMdd'); + + final dateCode = formatter.format(now); + + final tenantCode = await SystemSetting.getTenantCode(); + + final chars = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789'; + + final randomPart = List.generate( + 3, + (index) => chars[Random().nextInt(chars.length)], + ).join(); + + final prefix = 'INVC-$tenantCode-$dateCode-'; + + final lastOrder = + await (select(orders) + ..where((t) => t.orderNumber.like('$prefix%')) + ..orderBy([(t) => OrderingTerm.desc(t.orderNumber)]) + ..limit(1)) + .getSingleOrNull(); + + int nextSequence = 1; + + if (lastOrder != null) { + final parts = lastOrder.orderNumber.split('-'); + if (parts.isNotEmpty) { + final lastPart = parts.last; + if (lastPart.length >= 5) { + final lastSeqStr = lastPart.substring(lastPart.length - 5); + nextSequence = int.parse(lastSeqStr) + 1; + } + } + } + + // Format baru: INVC - [TenantCode] - [Tanggal] - [Random3] [Urutan5] + // Contoh: INVC-ZL6B25KQ-260328-XYZ00001 + final finalSequence = + '$randomPart${nextSequence.toString().padLeft(5, '0')}'; + + return '$prefix$finalSequence'; + } + + Future> _getProductUseMaterials( + String productVariantId, + double productQuantity, + ) async { + final outletId = await SystemSetting.getCurrentOutletId(); + + final query = + selectOnly(rawMaterials).join([ + innerJoin(units, units.localId.equalsExp(rawMaterials.unitId)), + innerJoin( + outletInventories, + outletInventories.itemType.equals(ItemType.raw_material.type) & + outletInventories.itemId.equalsExp(rawMaterials.localId), + ), + innerJoin( + receiptItems, + outletInventories.itemType.equals(ItemType.raw_material.type) & + receiptItems.rawMaterialId.equalsExp( + outletInventories.itemId, + ), + ), + ]) + ..addColumns([ + rawMaterials.name, + units.name, + outletInventories.localId, + outletInventories.itemId, + outletInventories.stock, + outletInventories.purchasePrice, + receiptItems.productVariantId, + receiptItems.rawMaterialId, + receiptItems.quantity, + ]) + ..where( + receiptItems.productVariantId.equals(productVariantId) & + receiptItems.quantity.isBiggerThanValue(0.0) & + receiptItems.deletedAt.isNull(), + ) + ..where(outletInventories.outletId.equals(outletId!)) + ..where(outletInventories.deletedAt.isNull()); + + final results = await query.get(); + + final Map groupedMap = { + for (final row in results) + row.read(outletInventories.localId)!: UpdateStockRawMaterial( + outletInventoryId: row.read(outletInventories.localId)!, + productVariantId: row.read(receiptItems.productVariantId) ?? '', + rawMaterialId: row.read(receiptItems.rawMaterialId) ?? '', + name: row.read(rawMaterials.name) ?? '', + unit: row.read(units.name) ?? '', + productQuantity: productQuantity, + stock: row.read(outletInventories.stock) ?? 0.0, + quantity: row.read(receiptItems.quantity) ?? 0.0, + purchasePrice: row.read(outletInventories.purchasePrice) ?? 0.0, + ), + }; + + return groupedMap.values.toList(); + } + + Future> getProductUseMaterials( + String productVariantId, + double productQuantity, + ) async { + return await _getProductUseMaterials(productVariantId, productQuantity); + } + + Future checkRawMaterialStockSufficient( + String productVariantId, + double productQuantity, + ) async { + final rawMaterials = await _getProductUseMaterials( + productVariantId, + productQuantity, + ); + + final Map runningStockTracker = {}; + + for (final rawMaterial in rawMaterials) { + final double currentStockBase = + runningStockTracker.containsKey(rawMaterial.outletInventoryId) + ? runningStockTracker[rawMaterial.outletInventoryId]! + : rawMaterial.stock; + + final double deduction = + rawMaterial.productQuantity * rawMaterial.quantity; + + final double stockAfterDeduction = currentStockBase - deduction; + + runningStockTracker[rawMaterial.outletInventoryId] = stockAfterDeduction; + + if (stockAfterDeduction <= 0) return false; + } + + return true; + } + + Future addSale({ + CustomersCompanion? customer, + CustomerAddressesCompanion? customerAddress, + required OrdersCompanion order, + required List orderItem, + required List totalSolds, + required List stockProducts, + required List cardProducts, + }) async { + try { + await transaction(() async { + final now = DateTime.now(); + + final orderNumber = await _generateOrderNumber(); + + await batch((batch) async { + if (customer != null) { + batch.insert(customers, customer); + + CustomSyncQueue.addToSyncQueue( + type: TableType.customers, + id: customer.localId.value, + action: SyncAction.create, + batch: batch, + syncQueues: syncQueues, + data: { + 'uuid': customer.localId.value, + 'outlet_id': customer.outletId.value, + 'name': customer.name.value, + 'phone_number': customer.phoneNumber.value, + 'source': customer.source.value, + 'created_at': now.toIso8601String(), + 'updated_at': now.toIso8601String(), + }, + ); + + if (customerAddress != null) { + batch.insert(customerAddresses, customerAddress); + } + } + + batch.insert(orders, order.copyWith(orderNumber: Value(orderNumber))); + + CustomSyncQueue.addToSyncQueue( + type: TableType.orders, + id: order.localId.value, + action: SyncAction.create, + batch: batch, + syncQueues: syncQueues, + data: { + 'uuid': order.localId.value, + 'order_number': orderNumber, + 'outlet_id': order.outletId.value, + 'outlet_name_snapshot': order.outletNameSnapshot.value, + 'customer_id': order.customerId.value, + 'customer_name_snapshot': order.customerNameSnapshot.value, + 'customer_phone_number_snapshot': + order.customerPhoneNumberSnapshot.value, + 'user_id': order.userId.value, + 'admin_name_snapshot': order.adminNameSnapshot.value, + 'source': order.source.value, + 'order_status': order.orderStatus.value, + 'total_order': order.totalOrder.value, + 'discount': order.discount.value, + 'tax': order.tax.value, + 'total_amount': order.totalAmount.value, + 'amount_paid': order.amountPaid.value, + 'change_amount': order.changeAmount.value, + 'notes': order.notes.value, + 'payment_proof_url': order.paymentProofPath.value, + 'customer_address_snapshot': order.customerAddressSnapshot.value, + 'created_at': now.toIso8601String(), + 'updated_at': now.toIso8601String(), + }, + ); + + final String category = order.source.value == 'pos' + ? 'Penjualan Kasir' + : 'Penjualan Toko Online'; + + final transaction = TransactionsCompanion( + localId: Value(const Uuid().v7().toString()), + outletId: order.outletId, + userId: order.userId, + referenceType: Value(TransactionReferenceType.orders.type), + referenceId: order.localId, + referenceNumber: Value(orderNumber), + stakeholder: order.customerNameSnapshot, + transactionType: Value(TransactionType.income.type), + category: Value(category), + amount: order.totalAmount, + note: order.notes, + ); + + batch.insert(transactions, transaction); + + CustomSyncQueue.addToSyncQueue( + type: TableType.transactions, + id: transaction.localId.value, + action: SyncAction.create, + batch: batch, + syncQueues: syncQueues, + data: { + 'uuid': transaction.localId.value, + 'outlet_id': transaction.outletId.value, + 'user_id': transaction.userId.value, + 'reference_type': transaction.referenceType.value, + 'reference_id': transaction.referenceId.value, + 'reference_number': transaction.referenceNumber.value, + 'stakeholder': transaction.stakeholder.value, + 'transaction_type': transaction.transactionType.value, + 'category': transaction.category.value, + 'amount': transaction.amount.value, + 'note': transaction.note.value, + 'created_at': now.toIso8601String(), + 'updated_at': now.toIso8601String(), + }, + ); + + for (final item in orderItem) { + batch.insert(orderItems, item); + + CustomSyncQueue.addToSyncQueue( + type: TableType.order_items, + id: item.localId.value, + action: SyncAction.create, + batch: batch, + syncQueues: syncQueues, + data: { + 'uuid': item.localId.value, + 'order_id': item.orderId.value, + 'product_variant_id': item.productVariantId.value, + 'quantity': item.quantity.value, + 'product_image_url_snapshot': item.productImageSnapshot.value, + 'product_name_snapshot': item.productNameSnapshot.value, + 'product_varian_name_snapshot': + item.productVariantNameSnapshot.value, + 'selling_price_snapshot': item.sellingPriceSnapshot.value, + 'subtotal': item.subtotal.value, + 'created_at': now.toIso8601String(), + 'updated_at': now.toIso8601String(), + }, + ); + } + + for (final item in totalSolds) { + batch.update( + productVariants, + ProductVariantsCompanion( + totalSold: Value(item.qtySold + item.currentSold), + updatedAt: Value(now), + ), + where: (tbl) => tbl.localId.equals(item.id), + ); + + CustomSyncQueue.addToSyncQueue( + type: TableType.product_variants, + id: item.id, + action: SyncAction.update, + batch: batch, + syncQueues: syncQueues, + data: { + 'uuid': item.id, + 'qty_sold': item.qtySold, + 'updated_at': now.toIso8601String(), + }, + ); + } + + final List productUseMaterials = []; + + for (final stockProduct in stockProducts) { + if (stockProduct.stockType == StockType.fixed) { + batch.update( + outletInventories, + OutletInventoriesCompanion( + stock: Value(stockProduct.stock - stockProduct.quantity), + syncStatus: Value(SyncStatus.dirty.status), + updatedAt: Value(now), + ), + where: (inv) { + return inv.localId.equals(stockProduct.outletInventoryId); + }, + ); + } + productUseMaterials.addAll( + await _getProductUseMaterials( + stockProduct.productVariantId, + stockProduct.quantity, + ), + ); + } + + final Map runningStockTracker = {}; + + final List cardRawMaterials = []; + + final userId = await SystemSetting.getCurrentUserId(); + + if (productUseMaterials.isNotEmpty) { + for (final productUseMaterial in productUseMaterials) { + final double currentStockBase = + runningStockTracker.containsKey( + productUseMaterial.outletInventoryId, + ) + ? runningStockTracker[productUseMaterial.outletInventoryId]! + : productUseMaterial.stock; + + final double deduction = + productUseMaterial.productQuantity * + productUseMaterial.quantity; + + final double stockAfterDeduction = currentStockBase - deduction; + + runningStockTracker[productUseMaterial.outletInventoryId] = + stockAfterDeduction; + + batch.update( + outletInventories, + OutletInventoriesCompanion( + stock: Value(stockAfterDeduction), + syncStatus: Value(SyncStatus.dirty.status), + updatedAt: Value(now), + ), + where: (inv) { + return inv.localId.equals( + productUseMaterial.outletInventoryId, + ); + }, + ); + + cardRawMaterials.add( + StockCardsCompanion( + localId: Value(Uuid().v7().toString()), + outletInventoryId: Value( + productUseMaterial.outletInventoryId, + ), + userId: Value(userId!), + type: Value(StockCardType.sale.type), + qtyChange: Value(deduction), + currentStock: Value(stockAfterDeduction), + stockType: Value(StockType.fixed.type), + costPrice: Value(productUseMaterial.purchasePrice), + referenceId: order.localId, + syncStatus: Value(SyncStatus.dirty.status), + ), + ); + } + } + + if (cardProducts.isNotEmpty) { + for (final card in cardProducts) { + batch.insert(stockCards, card); + CustomSyncQueue.addToSyncQueue( + type: TableType.stock_cards, + id: card.localId.value, + action: SyncAction.create, + batch: batch, + syncQueues: syncQueues, + data: { + 'uuid': card.localId.value, + 'outlet_inventory_id': card.outletInventoryId.value, + 'user_id': card.userId.value, + 'type': card.type.value, + 'qty_change': card.qtyChange.value, + 'current_stock': card.currentStock.value, + 'stock_type': card.stockType.value, + 'cost_price': card.costPrice.value, + 'reference_id': card.referenceId.value, + 'created_at': now.toIso8601String(), + }, + ); + } + } + + if (cardRawMaterials.isNotEmpty) { + for (final card in cardRawMaterials) { + batch.insert(stockCards, card); + CustomSyncQueue.addToSyncQueue( + type: TableType.stock_cards, + id: card.localId.value, + action: SyncAction.create, + batch: batch, + syncQueues: syncQueues, + data: { + 'uuid': card.localId.value, + 'outlet_inventory_id': card.outletInventoryId.value, + 'user_id': card.userId.value, + 'type': card.type.value, + 'qty_change': card.qtyChange.value, + 'current_stock': card.currentStock.value, + 'stock_type': card.stockType.value, + 'cost_price': card.costPrice.value, + 'reference_id': card.referenceId.value, + 'created_at': now.toIso8601String(), + }, + ); + } + } + }); + }); + } catch (e, st) { + LogMessage.log.e(e.toString(), error: e, stackTrace: st); + rethrow; + } + } + + Future getOrderDetail(String orderId) async { + final query = + selectOnly(orders).join([ + innerJoin(orderItems, orderItems.orderId.equalsExp(orders.localId)), + ]) + ..addColumns([ + orders.localId, + orders.orderNumber, + orders.createdAt, + orders.source, + orders.orderStatus, + orders.outletNameSnapshot, + orders.adminNameSnapshot, + orders.customerNameSnapshot, + orders.customerPhoneNumberSnapshot, + orders.customerAddressSnapshot, + orders.notes, + orderItems.productNameSnapshot, + orderItems.productVariantNameSnapshot, + orderItems.sellingPriceSnapshot, + orderItems.quantity, + orderItems.subtotal, + orders.totalOrder, + orders.discount, + orders.tax, + orders.totalAmount, + orders.amountPaid, + orders.changeAmount, + orders.paymentProofPath, + ]) + ..where(orders.localId.equals(orderId)); + final results = await query.get(); + + final Map groupedMap = {}; + + for (final row in results) { + final String orderId = row.read(orders.localId) ?? ''; + + final String orderNumber = (row.read(orders.orderNumber) ?? '') + .toUniqueNumberCleaner(); + + final DateTime orderDate = row.read(orders.createdAt) ?? DateTime.now(); + + final OrderSource orderSource = OrderSource.values.byName( + (row.read(orders.source) ?? ''), + ); + + final String status = row.read(orders.orderStatus) ?? ''; + + final OrderStatus orderStatus = OrderStatus.values.byName(status); + + final String outletName = row.read(orders.outletNameSnapshot) ?? ''; + + final String userName = row.read(orders.adminNameSnapshot) ?? ''; + + final String? customerName = row.read(orders.customerNameSnapshot); + + final String? customerPhone = row.read( + orders.customerPhoneNumberSnapshot, + ); + + final dynamic rawCustomerAddress = row.read( + orders.customerAddressSnapshot, + ); + + Map customerAddressSnapshot = {}; + + if (rawCustomerAddress is String) { + try { + customerAddressSnapshot = (jsonDecode(rawCustomerAddress) as Map) + .cast(); + } catch (e) { + customerAddressSnapshot = {}; + LogMessage.log.e(e.toString(), error: e); + } + } else if (rawCustomerAddress is Map) { + customerAddressSnapshot = (rawCustomerAddress).cast(); + } + + String? customerAddress; + + if (customerAddressSnapshot.isNotEmpty) { + customerAddress = customerAddressSnapshot["full_address"] as String?; + } + + final String? note = row.read(orders.notes); + + final double totalOrder = row.read(orders.totalOrder) ?? 0.0; + + final double discount = row.read(orders.discount) ?? 0.0; + + final double tax = row.read(orders.tax) ?? 0.0; + + final double totalAmount = row.read(orders.totalAmount) ?? 0.0; + + final double payAmount = row.read(orders.amountPaid) ?? 0.0; + + final double changeAmount = row.read(orders.changeAmount) ?? 0.0; + + final String? paymentProofPath = row.read(orders.paymentProofPath); + + groupedMap.putIfAbsent(orderId, () { + return OrderDetail( + orderId: orderId, + orderNumber: orderNumber, + orderDate: orderDate, + orderSource: orderSource, + orderStatus: orderStatus, + outletName: outletName, + userName: userName, + customerName: customerName, + customerPhone: customerPhone, + customerAddress: customerAddress, + note: note, + items: [], + totalOrder: totalOrder, + discount: discount, + tax: tax, + totalAmount: totalAmount, + payAmount: payAmount, + changeAmount: changeAmount, + paymentProofPath: paymentProofPath, + ); + }); + + final String name = row.read(orderItems.productNameSnapshot) ?? ''; + + final String? variantName = row.read( + orderItems.productVariantNameSnapshot, + ); + + final double sellingPrice = + row.read(orderItems.sellingPriceSnapshot) ?? 0.0; + + final double quantity = row.read(orderItems.quantity) ?? 0.0; + + final double subtotal = row.read(orderItems.subtotal) ?? 0.0; + + groupedMap[orderId]!.items.add( + OrderItem( + name: name, + variantName: variantName, + sellingPrice: sellingPrice, + quantity: quantity, + subtotal: subtotal, + ), + ); + } + + return groupedMap.values.single; + } + + Future cancelOrder(String orderId) async { + return await transaction(() async { + final now = DateTime.now(); + + final rowsAffected = + (await update(orders) + ..where((order) => order.localId.equals(orderId))) + .write( + OrdersCompanion( + orderStatus: Value(OrderStatus.canceled.status), + syncStatus: Value(SyncStatus.dirty.status), + updatedAt: Value(now), + ), + ); + + final payload = SyncPayload( + table: TableType.orders.type, + localId: orderId, + action: SyncAction.update.action, + data: { + 'uuid': orderId, + 'order_status': OrderStatus.canceled.status, + 'updated_at': now.toIso8601String(), + }, + ).toJson(); + + await into(syncQueues).insert( + SyncQueuesCompanion( + tableType: Value(TableType.orders.type), + tableLocalId: Value(orderId), + action: Value(SyncAction.update.action), + status: Value(SyncStatus.dirty.status), + payload: Value(payload), + ), + ); + + return rowsAffected; + }); + } + + Future updateAlreadyReadOrderOnline(String orderId) async { + return transaction(() async { + final now = DateTime.now(); + + final rowsAffected = + (await update(orders) + ..where((order) => order.localId.equals(orderId))) + .write( + OrdersCompanion( + alreadyRead: Value(false), + syncStatus: Value(SyncStatus.dirty.status), + updatedAt: Value(now), + ), + ); + + final payload = SyncPayload( + table: TableType.orders.type, + localId: orderId, + action: SyncAction.update.action, + data: { + 'uuid': orderId, + 'already_read': false, + 'updated_at': now.toIso8601String(), + }, + ).toJson(); + + await into(syncQueues).insert( + SyncQueuesCompanion( + tableType: Value(TableType.orders.type), + tableLocalId: Value(orderId), + action: Value(SyncAction.update.action), + status: Value(SyncStatus.dirty.status), + payload: Value(payload), + ), + ); + + return rowsAffected; + }); + } + + Stream watchOrderOnlineHasNotReview() async* { + final userRole = await SystemSetting.getCurrentUserRole(); + + final outletId = await SystemSetting.getCurrentOutletId(); + + final query = selectOnly(orders) + ..addColumns([ + orders.localId, + orders.orderNumber, + orders.customerNameSnapshot, + orders.totalAmount, + ]) + ..where( + orders.source.equals(OrderSource.online.source) & + orders.alreadyRead.equals(true) & + (orders.orderStatus.equals(OrderStatus.pending_payment.status) | + orders.orderStatus.equals( + OrderStatus.pending_confirmation.status, + )) & + (userRole == UserRole.admin + ? orders.outletId.equals(outletId!) + : const Constant(true)), + ) + ..limit(1); + + yield* query.watchSingle().map((row) { + final id = row.read(orders.localId); + + if (id != null) { + return MessageActivity( + type: MessageActivityType.new_online_order, + messages: [ + id, + row.read(orders.orderNumber)!, + row.read(orders.customerNameSnapshot)!, + CurrencyFormat.formatToIdr( + (row.read(orders.totalAmount) ?? 0.0), + 0, + ), + ], + ); + } + + return null; + }); + } + + Stream watchNumberOfNeedToBeProcessed() async* { + final userRole = await SystemSetting.getCurrentUserRole(); + + final outletId = await SystemSetting.getCurrentOutletId(); + + final query = selectOnly(orders) + ..addColumns([orders.localId.count()]) + ..groupBy([orders.source, orders.orderStatus]) + ..orderBy([OrderingTerm.desc(orders.createdAt)]) + ..where(orders.source.equals(OrderSource.online.source)) + ..where( + orders.orderStatus.equals(OrderStatus.pending_payment.status) | + orders.orderStatus.equals(OrderStatus.pending_confirmation.status), + ) + ..where( + userRole == UserRole.admin + ? orders.outletId.equals(outletId!) + : const Constant(true), + ); + + yield* query.watch().map((rows) { + return rows.fold(0, (sum, row) { + return sum + (row.read(orders.localId.count()) ?? 0); + }); + }); + } + + Stream> watchAllOnlineOrders({ + String? outletId, + DateTime? startDate, + DateTime? endDate, + }) async* { + final query = + selectOnly(orders).join([ + innerJoin(orderItems, orderItems.orderId.equalsExp(orders.localId)), + innerJoin( + productVariants, + productVariants.localId.equalsExp(orderItems.productVariantId), + ), + innerJoin( + products, + products.localId.equalsExp(productVariants.productId), + ), + ]) + ..addColumns([ + orders.localId, + productVariants.localImagePath, + orders.orderNumber, + orders.orderStatus, + orders.isCancellation, + orders.cancellationAccepted, + orders.approvalProccess, + orderItems.localId.count(), + orders.totalAmount, + orders.createdAt, + products.hasVariant, + products.localImagePath, + productVariants.localImagePath, + ]) + ..groupBy([orders.localId]) + ..orderBy([OrderingTerm.desc(orders.createdAt)]) + ..where(orders.source.equals(OrderSource.online.source)) + ..where( + outletId != null + ? orders.outletId.equals(outletId) + : const Constant(true), + ) + ..where( + (startDate != null && endDate != null) + ? orders.createdAt.isBiggerOrEqualValue(startDate) & + orders.createdAt.isSmallerThanValue( + endDate.add(const Duration(days: 1)), + ) + : const Constant(true), + ); + + yield* query.watch().map((rows) { + return rows.map((row) { + final dynamic rawImages = row.read(products.localImagePath); + + List productImages = []; + + if (rawImages is String) { + try { + productImages = (jsonDecode(rawImages) as List).cast(); + } catch (e) { + productImages = []; + } + } else if (rawImages is List) { + productImages = (rawImages).cast(); + } + + final bool hasVariant = row.read(products.hasVariant) ?? false; + + final String? displayImage = hasVariant + ? row.read(productVariants.localImagePath) + : productImages.lastOrNull; + + return OnlineOrder( + id: row.read(orders.localId)!, + displayImage: displayImage, + orderNumber: (row.read(orders.orderNumber) ?? '') + .toUniqueNumberCleaner(), + orderStatus: OrderStatus.values.byName( + (row.read(orders.orderStatus) ?? ''), + ), + isCancellation: row.read(orders.isCancellation) ?? false, + cancellationAccepted: row.read(orders.cancellationAccepted) ?? false, + process: ApprovalProcess.values.byName( + (row.read(orders.approvalProccess) ?? 'done'), + ), + itemCount: row.read(orderItems.localId.count()) ?? 0, + orderTotal: row.read(orders.totalAmount) ?? 0.0, + orderDate: row.read(orders.createdAt) ?? DateTime.now(), + ); + }).toList(); + }); + } + + Future getOnlineOrderDetailById(String orderId) async { + final query = + selectOnly(orders).join([ + innerJoin(orderItems, orderItems.orderId.equalsExp(orders.localId)), + ]) + ..addColumns([ + orders.localId, + orders.outletNameSnapshot, + orders.orderNumber, + orders.createdAt, + orders.notes, + orders.orderStatus, + orders.isCancellation, + orders.cancellationAccepted, + orders.approvalProccess, + orders.deliveryType, + orders.customerNameSnapshot, + orders.customerPhoneNumberSnapshot, + orders.customerAddressSnapshot, + orders.deliveryPrefrence, + orders.paymentMethod, + orders.paymentProofPath, + orders.shippingCost, + orderItems.quantity, + orderItems.productImageSnapshot, + orderItems.productNameSnapshot, + orderItems.productVariantNameSnapshot, + orderItems.sellingPriceSnapshot, + orderItems.subtotal, + orders.totalOrder, + orders.totalAmount, + ]) + ..where(orders.localId.equals(orderId)); + + final results = await query.get(); + + final Map groupedMap = {}; + + for (final row in results) { + final orderId = row.read(orders.localId)!; + + final outletName = row.read(orders.outletNameSnapshot) ?? ''; + + final orderNumber = (row.read(orders.orderNumber) ?? '') + .toUniqueNumberCleaner(); + + final orderDate = row.read(orders.createdAt) ?? DateTime.now(); + + final notes = row.read(orders.notes); + + final orderStatus = OrderStatus.values.byName( + (row.read(orders.orderStatus) ?? ''), + ); + + final isCancellation = row.read(orders.isCancellation) ?? false; + + final cancellationAccepted = + row.read(orders.cancellationAccepted) ?? false; + + final process = ApprovalProcess.values.byName( + (row.read(orders.approvalProccess) ?? 'done'), + ); + + final deliveryType = DeliveryType.values.byName( + (row.read(orders.deliveryType) ?? ''), + ); + + final dynamic rawCustomerAddress = row.read( + orders.customerAddressSnapshot, + ); + + Map customerAddressSnapshot = {}; + + if (rawCustomerAddress is String) { + try { + customerAddressSnapshot = (jsonDecode(rawCustomerAddress) as Map) + .cast(); + } catch (e) { + customerAddressSnapshot = {}; + LogMessage.log.e(e.toString(), error: e); + } + } else if (rawCustomerAddress is Map) { + customerAddressSnapshot = (rawCustomerAddress).cast(); + } + + String? customerAddress; + + if (customerAddressSnapshot.isNotEmpty) { + customerAddress = customerAddressSnapshot["full_address"] as String?; + } + + final deliveryInfo = DeliveryInfo( + name: row.read(orders.customerNameSnapshot) ?? '', + phoneNumber: row.read(orders.customerPhoneNumberSnapshot) ?? '', + fullAddress: customerAddress ?? '', + preference: row.read(orders.deliveryPrefrence) ?? '', + ); + + final String paymentMethod = row.read(orders.paymentMethod) ?? ''; + + final paymentProof = row.read(orders.paymentProofPath) ?? ''; + + final totalOrder = row.read(orders.totalOrder) ?? 0.0; + + final deliveryFee = row.read(orders.shippingCost) ?? 0.0; + + final totalAmount = row.read(orders.totalAmount) ?? 0.0; + + groupedMap.putIfAbsent( + orderId, + () => OnlineOrderDetail( + id: orderId, + outletName: outletName, + orderNumber: orderNumber, + orderDate: orderDate, + notes: notes, + orderStatus: orderStatus, + isCancellation: isCancellation, + cancellationAccepted: cancellationAccepted, + process: process, + deliveryType: deliveryType, + deliveryInfo: deliveryInfo, + paymentMethod: paymentMethod, + paymentProof: paymentProof, + items: [], + totalOrder: totalOrder, + deliveryFee: deliveryFee, + totalAmount: totalAmount, + ), + ); + + groupedMap[orderId]!.items.add( + OnlineOrderItem( + imagePath: row.read(orderItems.productImageSnapshot), + name: row.read(orderItems.productNameSnapshot) ?? '', + variantName: row.read(orderItems.productVariantNameSnapshot), + sellingPrice: row.read(orderItems.sellingPriceSnapshot) ?? 0.0, + quantity: row.read(orderItems.quantity) ?? 0.0, + subtotal: row.read(orderItems.subtotal) ?? 0.0, + ), + ); + } + + return groupedMap.values.single; + } + + Stream watchOrderStatusById(String orderId) async* { + final query = selectOnly(orders) + ..addColumns([ + orders.isCancellation, + orders.cancellationAccepted, + orders.approvalProccess, + orders.orderStatus, + orders.canceledBy, + ]) + ..where(orders.localId.equals(orderId)) + ..limit(1); + + yield* query.watchSingle().map((row) { + final orderStatus = OrderStatus.values.byName( + (row.read(orders.orderStatus) ?? ''), + ); + + final cBay = row.read(orders.canceledBy); + + CanceledBy? canceledBy; + + if (orderStatus == OrderStatus.canceled && + cBay != null && + cBay.isNotEmpty) { + canceledBy = CanceledBy.values.byName(cBay); + } + + return ViewOrderStatus( + isCancellation: row.read(orders.isCancellation) ?? false, + cancellationAccepted: row.read(orders.cancellationAccepted) ?? false, + process: ApprovalProcess.values.byName( + (row.read(orders.approvalProccess) ?? 'done'), + ), + orderStatus: orderStatus, + canceledBy: canceledBy, + ); + }); + } + + Future updateOrder({ + required String orderId, + required OrderStatus orderStatus, + required bool cancellationAccepted, + ApprovalProcess? process, + CanceledBy? canceledBy, + }) async { + return transaction(() async { + final now = DateTime.now(); + + final rowsAffected = + (await update(orders) + ..where((order) => order.localId.equals(orderId))) + .write( + OrdersCompanion( + orderStatus: Value(orderStatus.status), + cancellationAccepted: Value(cancellationAccepted), + approvalProccess: Value.absentIfNull(process?.process), + canceledBy: Value.absentIfNull(canceledBy?.type), + syncStatus: Value(SyncStatus.dirty.status), + updatedAt: Value(now), + ), + ); + + final payload = SyncPayload( + table: TableType.orders.type, + localId: orderId, + action: SyncAction.update.action, + data: { + 'uuid': orderId, + 'order_status': orderStatus.status, + 'cancellation_accepted': cancellationAccepted, + 'approval_proccess': process?.process, + 'canceledBy': canceledBy?.type, + 'updated_at': now.toIso8601String(), + }, + ).toJson(); + + await into(syncQueues).insert( + SyncQueuesCompanion( + tableType: Value(TableType.orders.type), + tableLocalId: Value(orderId), + action: Value(SyncAction.update.action), + status: Value(SyncStatus.dirty.status), + payload: Value(payload), + ), + ); + + return rowsAffected; + }); + } +} diff --git a/Mobile Operasional/lib/core/database/daos/order_dao.g.dart b/Mobile Operasional/lib/core/database/daos/order_dao.g.dart new file mode 100644 index 0000000..e62e999 --- /dev/null +++ b/Mobile Operasional/lib/core/database/daos/order_dao.g.dart @@ -0,0 +1,72 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'order_dao.dart'; + +// ignore_for_file: type=lint +mixin _$OrderDaoMixin on DatabaseAccessor { + $OutletsTable get outlets => attachedDatabase.outlets; + $CustomersTable get customers => attachedDatabase.customers; + $CustomerAddressesTable get customerAddresses => + attachedDatabase.customerAddresses; + $CategoriesTable get categories => attachedDatabase.categories; + $UnitsTable get units => attachedDatabase.units; + $ProductsTable get products => attachedDatabase.products; + $RawMaterialsTable get rawMaterials => attachedDatabase.rawMaterials; + $ProductVariantsTable get productVariants => attachedDatabase.productVariants; + $OutletInventoriesTable get outletInventories => + attachedDatabase.outletInventories; + $UsersTable get users => attachedDatabase.users; + $OrdersTable get orders => attachedDatabase.orders; + $OrderItemsTable get orderItems => attachedDatabase.orderItems; + $ReceiptItemsTable get receiptItems => attachedDatabase.receiptItems; + $StockCardsTable get stockCards => attachedDatabase.stockCards; + $TransactionsTable get transactions => attachedDatabase.transactions; + $SyncQueuesTable get syncQueues => attachedDatabase.syncQueues; + OrderDaoManager get managers => OrderDaoManager(this); +} + +class OrderDaoManager { + final _$OrderDaoMixin _db; + OrderDaoManager(this._db); + $$OutletsTableTableManager get outlets => + $$OutletsTableTableManager(_db.attachedDatabase, _db.outlets); + $$CustomersTableTableManager get customers => + $$CustomersTableTableManager(_db.attachedDatabase, _db.customers); + $$CustomerAddressesTableTableManager get customerAddresses => + $$CustomerAddressesTableTableManager( + _db.attachedDatabase, + _db.customerAddresses, + ); + $$CategoriesTableTableManager get categories => + $$CategoriesTableTableManager(_db.attachedDatabase, _db.categories); + $$UnitsTableTableManager get units => + $$UnitsTableTableManager(_db.attachedDatabase, _db.units); + $$ProductsTableTableManager get products => + $$ProductsTableTableManager(_db.attachedDatabase, _db.products); + $$RawMaterialsTableTableManager get rawMaterials => + $$RawMaterialsTableTableManager(_db.attachedDatabase, _db.rawMaterials); + $$ProductVariantsTableTableManager get productVariants => + $$ProductVariantsTableTableManager( + _db.attachedDatabase, + _db.productVariants, + ); + $$OutletInventoriesTableTableManager get outletInventories => + $$OutletInventoriesTableTableManager( + _db.attachedDatabase, + _db.outletInventories, + ); + $$UsersTableTableManager get users => + $$UsersTableTableManager(_db.attachedDatabase, _db.users); + $$OrdersTableTableManager get orders => + $$OrdersTableTableManager(_db.attachedDatabase, _db.orders); + $$OrderItemsTableTableManager get orderItems => + $$OrderItemsTableTableManager(_db.attachedDatabase, _db.orderItems); + $$ReceiptItemsTableTableManager get receiptItems => + $$ReceiptItemsTableTableManager(_db.attachedDatabase, _db.receiptItems); + $$StockCardsTableTableManager get stockCards => + $$StockCardsTableTableManager(_db.attachedDatabase, _db.stockCards); + $$TransactionsTableTableManager get transactions => + $$TransactionsTableTableManager(_db.attachedDatabase, _db.transactions); + $$SyncQueuesTableTableManager get syncQueues => + $$SyncQueuesTableTableManager(_db.attachedDatabase, _db.syncQueues); +} diff --git a/Mobile Operasional/lib/core/database/daos/order_item_dao.dart b/Mobile Operasional/lib/core/database/daos/order_item_dao.dart new file mode 100644 index 0000000..cb0f940 --- /dev/null +++ b/Mobile Operasional/lib/core/database/daos/order_item_dao.dart @@ -0,0 +1,11 @@ +import 'package:drift/drift.dart'; +import 'package:niogu_app/core/database/app_database.dart'; +import 'package:niogu_app/core/database/tables/order_items.dart'; + +part 'order_item_dao.g.dart'; + +@DriftAccessor(tables: [OrderItems]) +class OrderItemDao extends DatabaseAccessor with _$OrderItemDaoMixin { + + OrderItemDao(AppDatabase db) : super(db); +} \ No newline at end of file diff --git a/Mobile Operasional/lib/core/database/daos/order_item_dao.g.dart b/Mobile Operasional/lib/core/database/daos/order_item_dao.g.dart new file mode 100644 index 0000000..842bd1d --- /dev/null +++ b/Mobile Operasional/lib/core/database/daos/order_item_dao.g.dart @@ -0,0 +1,43 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'order_item_dao.dart'; + +// ignore_for_file: type=lint +mixin _$OrderItemDaoMixin on DatabaseAccessor { + $OutletsTable get outlets => attachedDatabase.outlets; + $CustomersTable get customers => attachedDatabase.customers; + $UsersTable get users => attachedDatabase.users; + $OrdersTable get orders => attachedDatabase.orders; + $CategoriesTable get categories => attachedDatabase.categories; + $UnitsTable get units => attachedDatabase.units; + $ProductsTable get products => attachedDatabase.products; + $ProductVariantsTable get productVariants => attachedDatabase.productVariants; + $OrderItemsTable get orderItems => attachedDatabase.orderItems; + OrderItemDaoManager get managers => OrderItemDaoManager(this); +} + +class OrderItemDaoManager { + final _$OrderItemDaoMixin _db; + OrderItemDaoManager(this._db); + $$OutletsTableTableManager get outlets => + $$OutletsTableTableManager(_db.attachedDatabase, _db.outlets); + $$CustomersTableTableManager get customers => + $$CustomersTableTableManager(_db.attachedDatabase, _db.customers); + $$UsersTableTableManager get users => + $$UsersTableTableManager(_db.attachedDatabase, _db.users); + $$OrdersTableTableManager get orders => + $$OrdersTableTableManager(_db.attachedDatabase, _db.orders); + $$CategoriesTableTableManager get categories => + $$CategoriesTableTableManager(_db.attachedDatabase, _db.categories); + $$UnitsTableTableManager get units => + $$UnitsTableTableManager(_db.attachedDatabase, _db.units); + $$ProductsTableTableManager get products => + $$ProductsTableTableManager(_db.attachedDatabase, _db.products); + $$ProductVariantsTableTableManager get productVariants => + $$ProductVariantsTableTableManager( + _db.attachedDatabase, + _db.productVariants, + ); + $$OrderItemsTableTableManager get orderItems => + $$OrderItemsTableTableManager(_db.attachedDatabase, _db.orderItems); +} diff --git a/Mobile Operasional/lib/core/database/daos/outlet_campaign_dao.dart b/Mobile Operasional/lib/core/database/daos/outlet_campaign_dao.dart new file mode 100644 index 0000000..37c6833 --- /dev/null +++ b/Mobile Operasional/lib/core/database/daos/outlet_campaign_dao.dart @@ -0,0 +1,10 @@ +import 'package:drift/drift.dart'; +import 'package:niogu_app/core/database/app_database.dart'; +import 'package:niogu_app/core/database/tables/outlet_campaigns.dart'; + +part 'outlet_campaign_dao.g.dart'; + +@DriftAccessor(tables: [OutletCampaigns]) +class OutletCampaignDao extends DatabaseAccessor with _$OutletCampaignDaoMixin { + OutletCampaignDao(AppDatabase db) : super(db); +} \ No newline at end of file diff --git a/Mobile Operasional/lib/core/database/daos/outlet_campaign_dao.g.dart b/Mobile Operasional/lib/core/database/daos/outlet_campaign_dao.g.dart new file mode 100644 index 0000000..27d74ef --- /dev/null +++ b/Mobile Operasional/lib/core/database/daos/outlet_campaign_dao.g.dart @@ -0,0 +1,25 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'outlet_campaign_dao.dart'; + +// ignore_for_file: type=lint +mixin _$OutletCampaignDaoMixin on DatabaseAccessor { + $CampaignsTable get campaigns => attachedDatabase.campaigns; + $OutletsTable get outlets => attachedDatabase.outlets; + $OutletCampaignsTable get outletCampaigns => attachedDatabase.outletCampaigns; + OutletCampaignDaoManager get managers => OutletCampaignDaoManager(this); +} + +class OutletCampaignDaoManager { + final _$OutletCampaignDaoMixin _db; + OutletCampaignDaoManager(this._db); + $$CampaignsTableTableManager get campaigns => + $$CampaignsTableTableManager(_db.attachedDatabase, _db.campaigns); + $$OutletsTableTableManager get outlets => + $$OutletsTableTableManager(_db.attachedDatabase, _db.outlets); + $$OutletCampaignsTableTableManager get outletCampaigns => + $$OutletCampaignsTableTableManager( + _db.attachedDatabase, + _db.outletCampaigns, + ); +} diff --git a/Mobile Operasional/lib/core/database/daos/outlet_dao.dart b/Mobile Operasional/lib/core/database/daos/outlet_dao.dart new file mode 100644 index 0000000..8e8abce --- /dev/null +++ b/Mobile Operasional/lib/core/database/daos/outlet_dao.dart @@ -0,0 +1,906 @@ +import 'package:drift/drift.dart'; +import 'package:niogu_app/core/database/app_database.dart' hide Outlet; +import 'package:niogu_app/core/database/tables/outlets.dart'; +import 'package:niogu_app/core/database/tables/sync_queues.dart'; +import 'package:niogu_app/core/database/tables/tenants.dart'; +import 'package:niogu_app/core/database/tables/transactions.dart'; +import 'package:niogu_app/core/database/tables/users.dart'; +import 'package:niogu_app/core/entities/custom_sync_queue.dart'; +import 'package:niogu_app/core/enums/table_type.dart'; +import 'package:niogu_app/core/utils/bcrypt.dart'; +import 'package:niogu_app/core/enums/sync_status.dart'; +import 'package:niogu_app/core/system/system_setting.dart'; +import 'package:niogu_app/core/enums/user_role.dart'; +import 'package:niogu_app/core/utils/log_message.dart'; +import 'package:niogu_app/features/outlets/domain/entities/outlet.dart'; +import 'package:niogu_app/features/stock_in/domain/entities/stock_in_transfer.dart'; + +part 'outlet_dao.g.dart'; + +@DriftAccessor(tables: [Users, Outlets, Transactions, Tenants, SyncQueues]) +class OutletDao extends DatabaseAccessor with _$OutletDaoMixin { + OutletDao(AppDatabase db) : super(db); + + Future getIsMainOutlet() async { + final outletId = await SystemSetting.getCurrentOutletId(); + + final query = selectOnly(outlets) + ..addColumns([outlets.isMainOutlet]) + ..where(outlets.localId.equals(outletId!)) + ..limit(1); + + final result = await query.getSingle(); + + return result.read(outlets.isMainOutlet) ?? false; + } + + Stream watchOutletCompleteness() async* { + final currentOutletId = await SystemSetting.getCurrentOutletId(); + final query = selectOnly(outlets) + ..addColumns([ + outlets.localBannerPath, + outlets.name, + outlets.phoneNumber, + outlets.email, + outlets.fullAddress, + ]) + ..where(outlets.localId.equals(currentOutletId!)) + ..limit(1); + + yield* query.watchSingle().map((row) { + double percent = 0.0; + + if (row.read(outlets.localBannerPath) != null) { + percent += 0.2; + } + + if (row.read(outlets.phoneNumber) != null) { + percent += 0.2; + } + + if (row.read(outlets.name) != null) { + percent += 0.2; + } + + if (row.read(outlets.email) != null) { + percent += 0.2; + } + + if (row.read(outlets.fullAddress) != null) { + percent += 0.2; + } + + return percent; + }); + } + + Stream> watchUserDeletedByOutlet() async* { + final query = selectOnly(users) + ..addColumns([users.outletId, users.localId.count()]) + ..groupBy([users.outletId]) + ..where(users.deletedAt.isNotNull()); + + yield* query.watch().map((rows) { + return rows.map((row) { + return UserDeletedByOutlet( + outletId: row.read(users.outletId)!, + amount: row.read(users.localId.count()) ?? 0, + ); + }).toList(); + }); + } + + Stream watchLimitAddOutlet() async* { + final query = selectOnly(tenants) + ..addColumns([tenants.limitOutlet]) + ..limit(1); + + yield* query.watchSingle().map((row) { + return row.read(tenants.limitOutlet)!; + }); + } + + Stream> watchAllDisplayOutlets() async* { + final query = + selectOnly(outlets).join([ + leftOuterJoin(users, users.outletId.equalsExp(outlets.localId)), + leftOuterJoin( + transactions, + transactions.outletId.equalsExp(outlets.localId), + ), + ]) + ..addColumns([ + outlets.localId, + outlets.localBannerPath, + outlets.name, + outlets.isMainOutlet, + outlets.isActive, + outlets.fullAddress, + users.localId.count(distinct: true), + transactions.localId.count(distinct: true), + ]) + ..groupBy([outlets.localId]) + ..orderBy([OrderingTerm.asc(outlets.createdAt)]) + ..where(outlets.deletedAt.isNull()); + + yield* query.watch().map((rows) { + return rows.map((row) { + final String id = row.read(outlets.localId)!; + + final String? bannerPath = row.read(outlets.localBannerPath); + + final String name = row.read(outlets.name) ?? ''; + + final bool isMainOutlet = row.read(outlets.isMainOutlet) ?? false; + + final bool isActive = row.read(outlets.isActive) ?? false; + + final String? fullAddress = row.read(outlets.fullAddress); + + int userStaffAmount = + (row.read(users.localId.count(distinct: true)) ?? 0); + + final int transactionAmount = + row.read(transactions.localId.count(distinct: true)) ?? 0; + + return DisplayOutlet( + id: id, + bannerPath: bannerPath, + name: name, + isMainOutlet: isMainOutlet, + isActive: isActive, + fullAddress: fullAddress, + userStaffAmount: userStaffAmount, + transactionAmount: transactionAmount, + ); + }).toList(); + }); + } + + Future getOutletById(String id) async { + final outletQuery = + selectOnly(outlets).join([ + leftOuterJoin( + transactions, + transactions.outletId.equalsExp(outlets.localId), + ), + ]) + ..addColumns([ + outlets.localId, + outlets.localBannerPath, + outlets.name, + outlets.phoneNumber, + outlets.email, + outlets.fullAddress, + outlets.latitude, + outlets.longitude, + outlets.isMainOutlet, + outlets.isActive, + transactions.localId.count(distinct: true), + ]) + ..groupBy([outlets.localId]) + ..where(outlets.localId.equals(id) & outlets.deletedAt.isNull()); + + final tenantQuery = selectOnly(tenants) + ..addColumns([tenants.businessName]) + ..limit(1); + + final tenantResult = await tenantQuery.getSingle(); + + final outletResult = await outletQuery.getSingle(); + + final tenantName = tenantResult.read(tenants.businessName) ?? ''; + + final String outletId = outletResult.read(outlets.localId)!; + + final String? bannerPath = outletResult.read(outlets.localBannerPath); + + final String name = outletResult.read(outlets.name) ?? ''; + + final String phoneNumber = outletResult.read(outlets.phoneNumber) ?? ''; + + final String? email = outletResult.read(outlets.email); + + final String? fullAddress = outletResult.read(outlets.fullAddress); + + final double? latitude = outletResult.read(outlets.latitude); + + final double? longitude = outletResult.read(outlets.longitude); + + final bool isMainOutlet = outletResult.read(outlets.isMainOutlet) ?? false; + + final bool isActive = outletResult.read(outlets.isActive) ?? false; + + final int transactionAmount = + outletResult.read(transactions.localId.count(distinct: true)) ?? 0; + + return OutletDetail( + id: outletId, + bannerPath: bannerPath, + tenantName: tenantName, + name: name, + phoneNumber: phoneNumber, + email: email, + fullAddress: fullAddress, + latitude: latitude, + longitude: longitude, + isMainOutlet: isMainOutlet, + isActive: isActive, + transactionAmount: transactionAmount, + ); + } + + Future getMainOutlet() async { + final query = selectOnly(outlets) + ..addColumns([outlets.localId, outlets.name]) + ..where(outlets.isMainOutlet); + + final result = await query.getSingle(); + + final String id = result.read(outlets.localId)!; + + final String name = result.read(outlets.name) ?? ''; + + return MainOutlet(id: id, name: name); + } + + Stream> watchAllStaffAdmins(String outletId) async* { + final query = + selectOnly(users).join([ + leftOuterJoin( + transactions, + transactions.userId.equalsExp(users.localId), + ), + ]) + ..addColumns([ + users.localId, + users.name, + users.phoneNumber, + users.email, + users.shiftName, + users.shiftStartTime, + users.shiftEndTime, + users.isActive, + transactions.localId.count(), + ]) + ..groupBy([users.localId]) + ..where( + users.outletId.equals(outletId) & + users.role.equals(UserRole.owner.role).not() & + users.deletedAt.isNull(), + ); + + yield* query.watch().map((rows) { + return rows.map((row) { + final String userId = row.read(users.localId)!; + + final String name = row.read(users.name) ?? ''; + + final String phoneNumber = row.read(users.phoneNumber) ?? ''; + + final String email = row.read(users.email) ?? ''; + + final int transactionAmount = + row.read(transactions.localId.count()) ?? 0; + + final String shiftName = row.read(users.shiftName) ?? ''; + + final String shiftStartTime = row.read(users.shiftStartTime) ?? ''; + + final String shiftEndTime = row.read(users.shiftEndTime) ?? ''; + + final bool isActive = row.read(users.isActive) ?? false; + + return StaffAdminDetail( + id: userId, + name: name, + phoneNumber: phoneNumber, + email: email, + isActive: isActive, + transactionAmount: transactionAmount, + shiftName: shiftName, + shiftStartTime: shiftStartTime, + shiftEndTime: shiftEndTime, + ); + }).toList(); + }); + } + + Future validatePhoneAndEmail({ + required String phoneNumber, + String? email, + String? excludeId, + }) async { + final query = selectOnly(outlets) + ..addColumns([outlets.phoneNumber, outlets.email]) + ..where( + outlets.phoneNumber.equals(phoneNumber) | + (email != null + ? outlets.email.equals(email) + : const Constant(false)), + ) + ..where( + excludeId != null + ? outlets.localId.isNotValue(excludeId) + : const Constant(true), + ) + ..where(outlets.deletedAt.isNull()) + ..limit(1); + + final result = await query.getSingleOrNull(); + + return result != null; + } + + Future validateAddUserStaff({ + required String firstPhoneNumber, + required String firstEmail, + String? secondPhoneNumber, + String? secondEmail, + }) async { + final query = selectOnly(users) + ..addColumns([users.name, users.phoneNumber, users.email]) + ..where( + (users.phoneNumber.equals(firstPhoneNumber) | + (secondPhoneNumber != null + ? users.phoneNumber.equals(secondPhoneNumber) + : const Constant(false))) | + (users.email.equals(firstEmail) | + (secondEmail != null + ? users.email.equals(secondEmail) + : const Constant(false))), + ) + ..where(users.deletedAt.isNull()) + ..limit(1); + + final result = await query.getSingleOrNull(); + + return result != null; + } + + Future saveStaffAdmin({ + required String outletId, + UpsertStaffAdmin? firstStaffAdmin, + UpsertStaffAdmin? secondStaffAdmin, + }) async { + try { + await transaction(() async { + await batch((batch) async { + final now = DateTime.now(); + + if (firstStaffAdmin != null) { + batch.insert( + users, + UsersCompanion( + localId: Value(firstStaffAdmin.id), + outletId: Value(outletId), + name: Value(firstStaffAdmin.name), + email: Value(firstStaffAdmin.email), + phoneNumber: Value(firstStaffAdmin.phoneNumber), + passwordHash: firstStaffAdmin.passwordHash != null + ? Value(Bcrypt.hashPassword(firstStaffAdmin.passwordHash!)) + : const Value.absent(), + role: Value(UserRole.admin.role), + shiftName: Value(firstStaffAdmin.shiftName), + shiftStartTime: Value(firstStaffAdmin.shiftStartTime), + shiftEndTime: Value(firstStaffAdmin.shiftEndTime), + isActive: Value(firstStaffAdmin.isActive), + ), + onConflict: DoUpdate( + (old) => UsersCompanion( + shiftName: Value(firstStaffAdmin.shiftName), + shiftStartTime: Value(firstStaffAdmin.shiftStartTime), + shiftEndTime: Value(firstStaffAdmin.shiftEndTime), + isActive: Value(firstStaffAdmin.isActive), + syncStatus: Value(SyncStatus.dirty.status), + updatedAt: Value(now), + ), + ), + ); + + final existing = + await (select(syncQueues)..where( + (queue) => + queue.tableType.equals(TableType.users.type) & + queue.tableLocalId.equals(firstStaffAdmin.id), + )) + .getSingleOrNull(); + + if (existing != null) { + CustomSyncQueue.updateSyncQueue( + type: TableType.users, + id: firstStaffAdmin.id, + action: SyncAction.update, + batch: batch, + syncQueues: syncQueues, + data: { + 'uuid': firstStaffAdmin.id, + 'shift_name': firstStaffAdmin.shiftName, + 'shift_start_time': firstStaffAdmin.shiftStartTime, + 'shift_end_time': firstStaffAdmin.shiftEndTime, + 'is_active': firstStaffAdmin.isActive, + 'updated_at': now.toIso8601String(), + }, + ); + } else { + final passwordHash = firstStaffAdmin.passwordHash; + CustomSyncQueue.addToSyncQueue( + type: TableType.users, + id: firstStaffAdmin.id, + action: SyncAction.create, + batch: batch, + syncQueues: syncQueues, + data: { + 'uuid': firstStaffAdmin.id, + 'outlet_id': outletId, + 'name': firstStaffAdmin.name, + 'email': firstStaffAdmin.email, + 'phone_number': firstStaffAdmin.phoneNumber, + ...{ + if (passwordHash != null) + 'password': Bcrypt.hashPassword(passwordHash), + }, + 'role': UserRole.admin.role, + 'shift_name': firstStaffAdmin.shiftName, + 'shift_start_time': firstStaffAdmin.shiftStartTime, + 'shift_end_time': firstStaffAdmin.shiftEndTime, + 'is_active': firstStaffAdmin.isActive, + 'created_at': now.toIso8601String(), + 'updated_at': now.toIso8601String(), + }, + ); + } + } + + if (secondStaffAdmin != null) { + batch.insert( + users, + UsersCompanion( + localId: Value(secondStaffAdmin.id), + outletId: Value(outletId), + name: Value(secondStaffAdmin.name), + email: Value(secondStaffAdmin.email), + phoneNumber: Value(secondStaffAdmin.phoneNumber), + passwordHash: secondStaffAdmin.passwordHash != null + ? Value(Bcrypt.hashPassword(secondStaffAdmin.passwordHash!)) + : const Value.absent(), + role: Value(UserRole.admin.role), + shiftName: Value(secondStaffAdmin.shiftName), + shiftStartTime: Value(secondStaffAdmin.shiftStartTime), + shiftEndTime: Value(secondStaffAdmin.shiftEndTime), + isActive: Value(secondStaffAdmin.isActive), + ), + onConflict: DoUpdate( + (old) => UsersCompanion( + shiftName: Value(secondStaffAdmin.shiftName), + shiftStartTime: Value(secondStaffAdmin.shiftStartTime), + shiftEndTime: Value(secondStaffAdmin.shiftEndTime), + isActive: Value(secondStaffAdmin.isActive), + syncStatus: Value(SyncStatus.dirty.status), + updatedAt: Value(now), + ), + ), + ); + + final existing = + await (select(syncQueues)..where( + (queue) => + queue.tableType.equals(TableType.users.type) & + queue.tableLocalId.equals(secondStaffAdmin.id), + )) + .getSingleOrNull(); + + if (existing != null) { + CustomSyncQueue.updateSyncQueue( + type: TableType.users, + id: secondStaffAdmin.id, + action: SyncAction.update, + batch: batch, + syncQueues: syncQueues, + data: { + 'uuid': secondStaffAdmin.id, + 'shift_name': secondStaffAdmin.shiftName, + 'shift_start_time': secondStaffAdmin.shiftStartTime, + 'shift_end_time': secondStaffAdmin.shiftEndTime, + 'is_active': secondStaffAdmin.isActive, + 'updated_at': now.toIso8601String(), + }, + ); + } else { + final passwordHash = secondStaffAdmin.passwordHash; + CustomSyncQueue.addToSyncQueue( + type: TableType.users, + id: secondStaffAdmin.id, + action: SyncAction.create, + batch: batch, + syncQueues: syncQueues, + data: { + 'uuid': secondStaffAdmin.id, + 'outlet_id': outletId, + 'name': secondStaffAdmin.name, + 'email': secondStaffAdmin.email, + 'phone_number': secondStaffAdmin.phoneNumber, + ...{ + if (passwordHash != null) + 'password': Bcrypt.hashPassword(passwordHash), + }, + 'role': UserRole.admin.role, + 'shift_name': secondStaffAdmin.shiftName, + 'shift_start_time': secondStaffAdmin.shiftStartTime, + 'shift_end_time': secondStaffAdmin.shiftEndTime, + 'is_active': secondStaffAdmin.isActive, + 'created_at': now.toIso8601String(), + 'updated_at': now.toIso8601String(), + }, + ); + } + } + }); + }); + } catch (e, st) { + LogMessage.log.e(e.toString(), error: e, stackTrace: st); + rethrow; + } + } + + Future deleteStaffAdmin(String userId) async { + return await transaction(() async { + final now = DateTime.now(); + + final rowsAffected = + (await update(users) + ..where((user) => user.localId.equals(userId))) + .write(UsersCompanion(deletedAt: Value(now))); + + final payload = SyncPayload( + table: TableType.users.type, + localId: userId, + action: SyncAction.delete.action, + data: {'uuid': userId, 'deleted_at': now.toIso8601String()}, + ).toJson(); + + await into(syncQueues).insert( + SyncQueuesCompanion( + tableType: Value(TableType.users.type), + tableLocalId: Value(userId), + action: Value(SyncAction.delete.action), + status: Value(SyncStatus.dirty.status), + payload: Value(payload), + ), + ); + + return rowsAffected; + }); + } + + Future saveOutlet(UpsertOutlet upsertOutlet) async { + try { + await transaction(() async { + await batch((batch) async { + final now = DateTime.now(); + + batch.insert( + outlets, + OutletsCompanion( + localId: Value(upsertOutlet.id), + name: Value(upsertOutlet.name), + email: Value(upsertOutlet.email), + phoneNumber: Value(upsertOutlet.phoneNumber), + fullAddress: Value(upsertOutlet.fullAddress), + latitude: Value(upsertOutlet.latitude), + longitude: Value(upsertOutlet.longitude), + localBannerPath: Value(upsertOutlet.bannerPath), + isMainOutlet: Value(upsertOutlet.isMainOutlet), + isActive: Value(upsertOutlet.isActive), + ), + onConflict: DoUpdate( + (old) => OutletsCompanion( + name: Value(upsertOutlet.name), + email: Value(upsertOutlet.email), + phoneNumber: Value(upsertOutlet.phoneNumber), + fullAddress: Value(upsertOutlet.fullAddress), + latitude: Value(upsertOutlet.latitude), + longitude: Value(upsertOutlet.longitude), + localBannerPath: Value(upsertOutlet.bannerPath), + isActive: Value(upsertOutlet.isActive), + syncStatus: Value(SyncStatus.dirty.status), + updatedAt: Value(now), + ), + ), + ); + + final existing = + await (select(syncQueues)..where( + (queue) => + queue.tableType.equals(TableType.outlets.type) & + queue.tableLocalId.equals(upsertOutlet.id), + )) + .getSingleOrNull(); + + if (existing != null) { + CustomSyncQueue.updateSyncQueue( + type: TableType.outlets, + id: upsertOutlet.id, + action: SyncAction.update, + batch: batch, + syncQueues: syncQueues, + data: { + 'uuid': upsertOutlet.id, + 'name': upsertOutlet.name, + 'email': upsertOutlet.email, + 'phone_number': upsertOutlet.phoneNumber, + 'full_address': upsertOutlet.fullAddress, + 'latitude': upsertOutlet.latitude, + 'longitude': upsertOutlet.longitude, + 'server_banner_url': upsertOutlet.bannerPath, + 'is_active': upsertOutlet.isActive, + 'updated_at': now.toIso8601String(), + }, + ); + } else { + CustomSyncQueue.addToSyncQueue( + type: TableType.outlets, + id: upsertOutlet.id, + action: SyncAction.create, + batch: batch, + syncQueues: syncQueues, + data: { + 'uuid': upsertOutlet.id, + 'name': upsertOutlet.name, + 'email': upsertOutlet.email, + 'phone_number': upsertOutlet.phoneNumber, + 'full_address': upsertOutlet.fullAddress, + 'latitude': upsertOutlet.latitude, + 'longitude': upsertOutlet.longitude, + 'server_banner_url': upsertOutlet.bannerPath, + 'is_main_outlet': upsertOutlet.isMainOutlet, + 'is_active': upsertOutlet.isActive, + 'created_at': now.toIso8601String(), + 'updated_at': now.toIso8601String(), + }, + ); + } + + if (upsertOutlet.isMainOutlet) { + batch.update( + outlets, + OutletsCompanion( + isMainOutlet: Value(upsertOutlet.isMainOutlet), + syncStatus: Value(SyncStatus.dirty.status), + updatedAt: Value(now), + ), + where: (tbl) => tbl.localId.equals(upsertOutlet.id), + ); + + CustomSyncQueue.addToSyncQueue( + type: TableType.outlets, + id: upsertOutlet.id, + action: SyncAction.update, + batch: batch, + syncQueues: syncQueues, + data: { + 'uuid': upsertOutlet.id, + 'is_main_outlet': upsertOutlet.isMainOutlet, + 'updated_at': now.toIso8601String(), + }, + ); + + batch.update( + outlets, + OutletsCompanion( + isMainOutlet: Value(!upsertOutlet.isMainOutlet), + syncStatus: Value(SyncStatus.dirty.status), + updatedAt: Value(now), + ), + where: (tbl) => tbl.localId.equals(upsertOutlet.id).not(), + ); + } + + if (upsertOutlet.firstStaffAdmin != null) { + final userComp = UsersCompanion( + localId: Value(upsertOutlet.firstStaffAdmin!.id), + outletId: Value(upsertOutlet.id), + name: Value(upsertOutlet.firstStaffAdmin!.name), + email: Value(upsertOutlet.firstStaffAdmin!.email), + phoneNumber: Value(upsertOutlet.firstStaffAdmin!.phoneNumber), + passwordHash: upsertOutlet.firstStaffAdmin!.passwordHash != null + ? Value( + Bcrypt.hashPassword( + upsertOutlet.firstStaffAdmin!.passwordHash!, + ), + ) + : const Value.absent(), + role: Value(UserRole.admin.role), + shiftName: Value(upsertOutlet.firstStaffAdmin!.shiftName), + shiftStartTime: Value( + upsertOutlet.firstStaffAdmin!.shiftStartTime, + ), + shiftEndTime: Value(upsertOutlet.firstStaffAdmin!.shiftEndTime), + isActive: Value(upsertOutlet.firstStaffAdmin!.isActive), + ); + + batch.insert(users, userComp); + + CustomSyncQueue.addToSyncQueue( + type: TableType.users, + id: userComp.localId.value, + action: SyncAction.create, + batch: batch, + syncQueues: syncQueues, + data: { + 'uuid': userComp.localId.value, + 'outlet_id': userComp.outletId.value, + 'name': userComp.name.value, + 'email': userComp.email.value, + 'phone_number': userComp.phoneNumber.value, + 'password': userComp.passwordHash.value, + 'role': userComp.role.value, + 'shift_name': userComp.shiftName.value, + 'shift_start_time': userComp.shiftStartTime.value, + 'shift_end_time': userComp.shiftEndTime.value, + 'is_active': userComp.isActive.value, + 'created_at': now.toIso8601String(), + 'updated_at': now.toIso8601String(), + }, + ); + } + + if (upsertOutlet.secondStaffAdmin != null) { + final userComp = UsersCompanion( + localId: Value(upsertOutlet.secondStaffAdmin!.id), + outletId: Value(upsertOutlet.id), + name: Value(upsertOutlet.secondStaffAdmin!.name), + email: Value(upsertOutlet.secondStaffAdmin!.email), + phoneNumber: Value(upsertOutlet.secondStaffAdmin!.phoneNumber), + passwordHash: upsertOutlet.secondStaffAdmin!.passwordHash != null + ? Value( + Bcrypt.hashPassword( + upsertOutlet.secondStaffAdmin!.passwordHash!, + ), + ) + : const Value.absent(), + role: Value(UserRole.admin.role), + shiftName: Value(upsertOutlet.secondStaffAdmin!.shiftName), + shiftStartTime: Value( + upsertOutlet.secondStaffAdmin!.shiftStartTime, + ), + shiftEndTime: Value(upsertOutlet.secondStaffAdmin!.shiftEndTime), + isActive: Value(upsertOutlet.firstStaffAdmin!.isActive), + ); + + batch.insert(users, userComp); + + CustomSyncQueue.addToSyncQueue( + type: TableType.users, + id: userComp.localId.value, + action: SyncAction.create, + batch: batch, + syncQueues: syncQueues, + data: { + 'uuid': userComp.localId.value, + 'outlet_id': userComp.outletId.value, + 'name': userComp.name.value, + 'email': userComp.email.value, + 'phone_number': userComp.phoneNumber.value, + 'password': userComp.passwordHash.value, + 'role': userComp.role.value, + 'shift_name': userComp.shiftName.value, + 'shift_start_time': userComp.shiftStartTime.value, + 'shift_end_time': userComp.shiftEndTime.value, + 'is_active': userComp.isActive.value, + 'created_at': now.toIso8601String(), + 'updated_at': now.toIso8601String(), + }, + ); + } + }); + }); + } catch (e, st) { + LogMessage.log.e(e.toString(), error: e, stackTrace: st); + rethrow; + } + } + + Future deleteOutlet(String outletId) async { + try { + await transaction(() async { + await batch((batch) { + final now = DateTime.now(); + + batch.update( + outlets, + OutletsCompanion(isActive: Value(false), deletedAt: Value(now)), + where: (tbl) => tbl.localId.equals(outletId), + ); + + CustomSyncQueue.updateSyncQueue( + type: TableType.outlets, + id: outletId, + action: SyncAction.delete, + batch: batch, + syncQueues: syncQueues, + data: { + 'uuid': outletId, + 'is_active': false, + 'deleted_at': now.toIso8601String(), + }, + ); + + batch.update( + users, + UsersCompanion(isActive: Value(false), deletedAt: Value(now)), + where: (tbl) => tbl.outletId.equals(outletId), + ); + + CustomSyncQueue.addToSyncQueue( + type: TableType.users, + id: outletId, + action: SyncAction.delete, + batch: batch, + syncQueues: syncQueues, + data: { + 'outlet_id': outletId, + 'role': UserRole.admin.role, + 'is_active': false, + 'deleted_at': now.toIso8601String(), + }, + ); + }); + }); + } catch (e, st) { + LogMessage.log.e(e.toString(), error: e, stackTrace: st); + rethrow; + } + } + + Stream> watchAllOutlets() async* { + final query = selectOnly(outlets) + ..addColumns([ + outlets.localId, + outlets.name, + outlets.localPhotoPath, + outlets.isMainOutlet, + outlets.isActive, + ]) + ..where(outlets.deletedAt.isNull()); + + yield* query.watch().map((rows) { + return rows.map((row) { + final String id = row.read(outlets.localId) ?? ''; + final String name = row.read(outlets.name) ?? ''; + final String photoPath = row.read(outlets.localPhotoPath) ?? ''; + final bool isMainOutlet = row.read(outlets.isMainOutlet) ?? false; + final bool isActive = row.read(outlets.isActive) ?? false; + + return Outlet( + id: id, + name: name, + photoPath: photoPath, + isMainOutlet: isMainOutlet, + isActive: isActive, + ); + }).toList(); + }); + } + + Stream> watchAllSourceOutlets(bool isShowAll) async* { + final currentOutletId = await SystemSetting.getCurrentOutletId(); + + final query = selectOnly(outlets) + ..addColumns([outlets.localId, outlets.name]) + ..where( + (isShowAll + ? const Constant(true) + : outlets.localId.isNotValue(currentOutletId!)) & + outlets.deletedAt.isNull(), + ); + + yield* query.watch().map((rows) { + return rows.map((row) { + final String id = row.read(outlets.localId) ?? ''; + final String name = row.read(outlets.name) ?? ''; + return SourceOutlet(id: id, name: name); + }).toList(); + }); + } +} diff --git a/Mobile Operasional/lib/core/database/daos/outlet_dao.g.dart b/Mobile Operasional/lib/core/database/daos/outlet_dao.g.dart new file mode 100644 index 0000000..d6c45cd --- /dev/null +++ b/Mobile Operasional/lib/core/database/daos/outlet_dao.g.dart @@ -0,0 +1,28 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'outlet_dao.dart'; + +// ignore_for_file: type=lint +mixin _$OutletDaoMixin on DatabaseAccessor { + $OutletsTable get outlets => attachedDatabase.outlets; + $UsersTable get users => attachedDatabase.users; + $TransactionsTable get transactions => attachedDatabase.transactions; + $TenantsTable get tenants => attachedDatabase.tenants; + $SyncQueuesTable get syncQueues => attachedDatabase.syncQueues; + OutletDaoManager get managers => OutletDaoManager(this); +} + +class OutletDaoManager { + final _$OutletDaoMixin _db; + OutletDaoManager(this._db); + $$OutletsTableTableManager get outlets => + $$OutletsTableTableManager(_db.attachedDatabase, _db.outlets); + $$UsersTableTableManager get users => + $$UsersTableTableManager(_db.attachedDatabase, _db.users); + $$TransactionsTableTableManager get transactions => + $$TransactionsTableTableManager(_db.attachedDatabase, _db.transactions); + $$TenantsTableTableManager get tenants => + $$TenantsTableTableManager(_db.attachedDatabase, _db.tenants); + $$SyncQueuesTableTableManager get syncQueues => + $$SyncQueuesTableTableManager(_db.attachedDatabase, _db.syncQueues); +} diff --git a/Mobile Operasional/lib/core/database/daos/outlet_inventory_dao.dart b/Mobile Operasional/lib/core/database/daos/outlet_inventory_dao.dart new file mode 100644 index 0000000..c787788 --- /dev/null +++ b/Mobile Operasional/lib/core/database/daos/outlet_inventory_dao.dart @@ -0,0 +1,798 @@ +import 'dart:convert'; + +import 'package:drift/drift.dart'; +import 'package:niogu_app/core/database/app_database.dart' hide StockCard; +import 'package:niogu_app/core/database/tables/outlet_inventories.dart'; +import 'package:niogu_app/core/database/tables/product_variants.dart'; +import 'package:niogu_app/core/database/tables/products.dart'; +import 'package:niogu_app/core/database/tables/raw_materials.dart'; +import 'package:niogu_app/core/database/tables/receipt_items.dart'; +import 'package:niogu_app/core/database/tables/stock_cards.dart'; +import 'package:niogu_app/core/database/tables/sync_queues.dart'; +import 'package:niogu_app/core/entities/custom_sync_queue.dart'; +import 'package:niogu_app/core/enums/item_type.dart'; +import 'package:niogu_app/core/enums/stock_type.dart'; +import 'package:niogu_app/core/enums/sync_status.dart'; +import 'package:niogu_app/core/enums/table_type.dart'; +import 'package:niogu_app/core/system/system_setting.dart'; +import 'package:niogu_app/core/enums/user_role.dart'; +import 'package:niogu_app/core/utils/log_message.dart'; +import 'package:niogu_app/features/goods/products/domain/entities/product.dart'; +import 'package:niogu_app/features/goods/raw_materials/domain/entities/raw_material.dart'; +import 'package:niogu_app/features/stock_in/domain/entities/stock_in.dart'; + +part 'outlet_inventory_dao.g.dart'; + +@DriftAccessor( + tables: [ + Products, + ProductVariants, + RawMaterials, + ReceiptItems, + OutletInventories, + StockCards, + SyncQueues, + ], +) +class OutletInventoryDao extends DatabaseAccessor + with _$OutletInventoryDaoMixin { + OutletInventoryDao(AppDatabase db) : super(db); + + Stream> watchAllItems(ItemType itemType) async* { + final outletId = await SystemSetting.getCurrentOutletId(); + + final List joins = []; + + final List columns = [ + outletInventories.localId, + outletInventories.itemId, + outletInventories.stock, + outletInventories.costPrice, + outletInventories.purchasePrice, + units.name, + outletInventories.sellingPrice, + ]; + + if (itemType == ItemType.product_variant) { + joins.addAll([ + innerJoin( + productVariants, + productVariants.localId.equalsExp(outletInventories.itemId) & + outletInventories.itemType.equals(ItemType.product_variant.type), + ), + innerJoin( + products, + products.localId.equalsExp(productVariants.productId), + ), + innerJoin(units, units.localId.equalsExp(products.unitId)), + ]); + + columns.addAll([ + products.localImagePath, + products.name, + products.hasVariant, + productVariants.localImagePath, + productVariants.name, + ]); + } else if (itemType == ItemType.raw_material) { + joins.addAll([ + innerJoin( + rawMaterials, + rawMaterials.localId.equalsExp(outletInventories.itemId) & + outletInventories.itemType.equals(ItemType.raw_material.type), + ), + innerJoin(units, units.localId.equalsExp(rawMaterials.unitId)), + ]); + columns.add(rawMaterials.name); + } + + final query = selectOnly(outletInventories).join(joins) + ..addColumns(columns) + ..where( + itemType == ItemType.product_variant + ? outletInventories.stockType.equals(StockType.fixed.type) + : const Constant(true), + ) + ..where( + outletInventories.outletId.equals(outletId!) & + outletInventories.deletedAt.isNull(), + ) + ..where( + itemType == ItemType.product_variant + ? productVariants.deletedAt.isNull() + : const Constant(true), + ) + ..where( + itemType == ItemType.product_variant + ? products.deletedAt.isNull() + : const Constant(true), + ) + ..where( + itemType == ItemType.raw_material + ? rawMaterials.deletedAt.isNull() + : const Constant(true), + ); + + yield* query.watch().map((rows) { + final Map groupedMap = {}; + + for (final row in rows) { + final outletInventoryId = row.read(outletInventories.localId)!; + + final itemId = row.read(outletInventories.itemId)!; + + bool hasVariant = false; + + List productImages = []; + + if (itemType == ItemType.product_variant) { + hasVariant = row.read(products.hasVariant) ?? false; + + final dynamic rawImages = row.read(products.localImagePath); + + if (hasVariant) { + final String imagePath = + row.read(productVariants.localImagePath) ?? ''; + productImages.add(imagePath); + } else { + if (rawImages is String) { + productImages = (jsonDecode(rawImages) as List).cast(); + } else if (rawImages is List) { + productImages = (rawImages).cast(); + } + } + } + + final String? localImagePath = + productImages.isNotEmpty && productImages.last.isNotEmpty + ? productImages.last + : null; + + final String name = itemType == ItemType.product_variant + ? row.read(products.name) ?? '' + : row.read(rawMaterials.name) ?? ''; + + final String? variantName = + itemType == ItemType.product_variant && hasVariant + ? row.read(productVariants.name) + : null; + + final double remainingStock = row.read(outletInventories.stock) ?? 0.0; + + final double costPrice = row.read(outletInventories.costPrice) ?? 0.0; + + final double purchasePrice = + row.read(outletInventories.purchasePrice) ?? 0.0; + + final String unit = row.read(units.name) ?? ''; + + final double sellingPrice = + row.read(outletInventories.sellingPrice) ?? 0.0; + + groupedMap[outletInventoryId] = DisplayItem( + id: outletInventoryId, + itemId: itemId, + localImagePath: localImagePath, + name: name, + variantName: variantName, + remainingStock: remainingStock, + costPrice: costPrice, + purchasePrice: purchasePrice, + unit: unit, + sellingPrice: sellingPrice, + ); + } + + return groupedMap.values.toList(); + }); + } + + Future saveOutletInventory({ + ProductsCompanion? product, + List? variants, + List? deleteVariants, + RawMaterialsCompanion? rawMaterial, + List? items, + List? deleteItems, + required List inventories, + required List cards, + bool modeChange = false, + String? modeBeforeChange, + String? modeAfterChange, + }) async { + try { + await transaction(() async { + final now = DateTime.now(); + + final currentUserRole = await SystemSetting.getCurrentUserRole(); + + final isDeleteByOwner = currentUserRole == UserRole.owner; + + await batch((batch) { + if (product != null && variants != null) { + batch.insert( + products, + product, + onConflict: DoUpdate((old) { + return product.copyWith( + syncStatus: Value(SyncStatus.dirty.status), + createdAt: const Value.absent(), + updatedAt: Value(now), + ); + }), + ); + + CustomSyncQueue.addToSyncQueue( + type: TableType.products, + id: product.localId.value, + action: SyncAction.update_or_create, + batch: batch, + syncQueues: syncQueues, + data: { + 'uuid': product.localId.value, + 'category_id': product.categoryId.value, + 'unit_id': product.unitId.value, + 'name': product.name.value, + 'server_image_url': product.localImagePath.value, + 'has_variant': product.hasVariant.value, + 'created_at': now.toIso8601String(), + 'updated_at': now.toIso8601String(), + }, + ); + + if (deleteVariants != null) { + for (var data in deleteVariants) { + batch.update( + productVariants, + ProductVariantsCompanion( + syncStatus: Value(SyncStatus.dirty.status), + deletedAt: Value(now), + ), + where: (variant) { + return variant.localId.equals(data.productVariantId); + }, + ); + + CustomSyncQueue.addToSyncQueue( + type: TableType.product_variants, + id: data.productVariantId, + action: SyncAction.delete, + batch: batch, + syncQueues: syncQueues, + data: { + 'uuid': data.productVariantId, + 'deleted_at': now.toIso8601String(), + }, + ); + + batch.update( + outletInventories, + OutletInventoriesCompanion( + isDeleteByOwner: Value(isDeleteByOwner), + syncStatus: Value(SyncStatus.dirty.status), + deletedAt: Value(now), + ), + where: (inventory) { + return inventory.itemType.equals( + ItemType.product_variant.type, + ) & + inventory.itemId.equals(data.productVariantId); + }, + ); + + CustomSyncQueue.addToSyncQueue( + type: TableType.outlet_inventories, + id: data.productVariantId, + action: SyncAction.delete, + batch: batch, + syncQueues: syncQueues, + data: { + 'item_id': data.productVariantId, + 'item_type': ItemType.product_variant.type, + 'is_delete_by_owner': isDeleteByOwner, + 'deleted_at': now.toIso8601String(), + }, + ); + + batch.update( + receiptItems, + ReceiptItemsCompanion( + syncStatus: Value(SyncStatus.dirty.status), + deletedAt: Value(now), + ), + where: (item) { + return item.productVariantId.equals(data.productVariantId); + }, + ); + + CustomSyncQueue.addToSyncQueue( + type: TableType.receipt_items, + id: data.productVariantId, + action: SyncAction.delete, + batch: batch, + syncQueues: syncQueues, + data: { + 'product_variant_id': data.productVariantId, + 'deleted_at': now.toIso8601String(), + }, + ); + } + } + + for (var variant in variants) { + batch.insert( + productVariants, + variant, + onConflict: DoUpdate((old) { + return variant.copyWith( + syncStatus: Value(SyncStatus.dirty.status), + createdAt: const Value.absent(), + updatedAt: Value(now), + ); + }), + ); + + CustomSyncQueue.addToSyncQueue( + type: TableType.product_variants, + id: variant.localId.value, + action: SyncAction.update_or_create, + batch: batch, + syncQueues: syncQueues, + data: { + 'uuid': variant.localId.value, + 'product_id': variant.productId.value, + 'name': variant.name.value, + 'server_image_url': variant.localImagePath.value, + 'is_product_variant': variant.isProductVariant.value, + 'created_at': now.toIso8601String(), + 'updated_at': now.toIso8601String(), + }, + ); + } + } else if (rawMaterial != null && items != null) { + batch.insert( + rawMaterials, + rawMaterial, + onConflict: DoUpdate((old) { + return rawMaterial.copyWith( + syncStatus: Value(SyncStatus.dirty.status), + createdAt: const Value.absent(), + updatedAt: Value(now), + ); + }), + ); + + CustomSyncQueue.addToSyncQueue( + type: TableType.raw_materials, + id: rawMaterial.localId.value, + action: SyncAction.update_or_create, + batch: batch, + syncQueues: syncQueues, + data: { + 'uuid': rawMaterial.localId.value, + 'unit_id': rawMaterial.unitId.value, + 'name': rawMaterial.name.value, + 'created_at': now.toIso8601String(), + 'updated_at': now.toIso8601String(), + }, + ); + + if (deleteItems != null) { + for (var data in deleteItems) { + batch.update( + receiptItems, + ReceiptItemsCompanion( + syncStatus: Value(SyncStatus.dirty.status), + deletedAt: Value(now), + ), + where: (item) { + return item.rawMaterialId.equals(data.rawMaterialId) & + item.productVariantId.equals(data.productUsageId); + }, + ); + + CustomSyncQueue.addToSyncQueue( + type: TableType.receipt_items, + id: data.rawMaterialId, + action: SyncAction.delete, + batch: batch, + syncQueues: syncQueues, + data: { + 'raw_material_id': data.rawMaterialId, + 'product_variant_id': data.productUsageId, + 'deleted_at': now.toIso8601String(), + }, + ); + } + } + + for (final item in items) { + batch.insert( + receiptItems, + item, + onConflict: DoUpdate((old) { + return item.copyWith( + quantity: item.quantity, + syncStatus: Value(SyncStatus.dirty.status), + createdAt: const Value.absent(), + updatedAt: Value(now), + ); + }), + ); + + CustomSyncQueue.addToSyncQueue( + type: TableType.receipt_items, + id: item.localId.value, + action: SyncAction.update_or_create, + batch: batch, + syncQueues: syncQueues, + data: { + 'uuid': item.localId.value, + 'product_variant_id': item.productVariantId.value, + 'raw_material_id': item.rawMaterialId.value, + 'quantity': item.quantity.value, + 'created_at': now.toIso8601String(), + 'updated_at': now.toIso8601String(), + }, + ); + } + } + + for (var inventory in inventories) { + batch.insert( + outletInventories, + inventory, + onConflict: DoUpdate((old) { + return inventory.copyWith( + syncStatus: Value(SyncStatus.dirty.status), + createdAt: const Value.absent(), + updatedAt: Value(DateTime.now()), + ); + }), + ); + + CustomSyncQueue.addToSyncQueue( + type: TableType.outlet_inventories, + id: inventory.localId.value, + action: SyncAction.update_or_create, + batch: batch, + syncQueues: syncQueues, + data: { + 'uuid': inventory.localId.value, + 'outlet_id': inventory.outletId.value, + 'item_type': inventory.itemType.value, + 'item_id': inventory.itemId.value, + 'stock_type': inventory.stockType.value, + 'stock': inventory.stock.value, + 'selling_price': inventory.sellingPrice.value, + 'cost_price': inventory.costPrice.value, + 'purchase_price': inventory.purchasePrice.value, + 'is_delete_by_owner': inventory.isDeleteByOwner.value, + 'created_at': now.toIso8601String(), + 'updated_at': now.toIso8601String(), + }, + ); + } + + if (cards.isNotEmpty) { + for (var card in cards) { + if (modeChange && + modeBeforeChange != null && + modeAfterChange != null) { + card = card.copyWith( + modeChange: Value(true), + modeBeforeChange: Value(modeBeforeChange), + modeAfterChange: Value(modeAfterChange), + ); + } + batch.insert(stockCards, card); + + CustomSyncQueue.addToSyncQueue( + type: TableType.stock_cards, + id: card.localId.value, + action: SyncAction.create, + batch: batch, + syncQueues: syncQueues, + data: { + 'uuid': card.localId.value, + 'outlet_inventory_id': card.outletInventoryId.value, + 'user_id': card.userId.value, + 'type': card.type.value, + 'qty_change': card.qtyChange.value, + 'current_stock': card.currentStock.value, + 'cost_price': card.costPrice.value, + 'stock_type': card.stockType.value, + 'mode_change': card.modeChange.value, + 'mode_before_change': card.modeBeforeChange.value, + 'mode_after_change': card.modeAfterChange.value, + 'created_at': now.toIso8601String(), + }, + ); + } + } + }); + }); + } catch (e, st) { + LogMessage.log.e(e.toString(), error: e, stackTrace: st); + rethrow; + } + } + + Future deleteOutletInventory({ + String? productId, + String? rawMaterialId, + String? outletId, + bool canDeleteAll = false, + }) async { + try { + await transaction(() async { + final now = DateTime.now(); + + final currentUserRole = await SystemSetting.getCurrentUserRole(); + + final isDeletedByOwner = currentUserRole == UserRole.owner; + + if (productId != null) { + final productVariantQuery = + selectOnly(productVariants).join([ + innerJoin( + outletInventories, + outletInventories.itemType.equals( + ItemType.product_variant.type, + ) & + outletInventories.itemId.equalsExp( + productVariants.localId, + ), + ), + ]) + ..addColumns([ + productVariants.localId, + outletInventories.localId, + productVariants.serverImageUrl, + ]) + ..where(productVariants.productId.equals(productId)) + ..where( + outletId != null && !canDeleteAll + ? outletInventories.outletId.equals(outletId) + : const Constant(true), + ) + ..where(productVariants.deletedAt.isNull()) + ..where(outletInventories.deletedAt.isNull()); + + final productVariantRows = await productVariantQuery.get(); + + final List deleteProducts = productVariantRows.map(( + row, + ) { + final String productVariantId = row.read(productVariants.localId)!; + + final String outletInventoryId = row.read( + outletInventories.localId, + )!; + + return DeleteProduct( + productVariantId: productVariantId, + outletInventoryId: outletInventoryId, + ); + }).toList(); + + await batch((batch) async { + for (var data in deleteProducts) { + batch.update( + outletInventories, + OutletInventoriesCompanion( + isDeleteByOwner: Value(isDeletedByOwner), + syncStatus: Value(SyncStatus.dirty.status), + deletedAt: Value(now), + ), + where: (inventory) { + return inventory.localId.equals(data.outletInventoryId) & + inventory.itemType.equals(ItemType.product_variant.type) & + inventory.itemId.equals(data.productVariantId); + }, + ); + + CustomSyncQueue.addToSyncQueue( + type: TableType.outlet_inventories, + id: data.outletInventoryId, + action: SyncAction.delete, + batch: batch, + syncQueues: syncQueues, + data: { + 'uuid': data.outletInventoryId, + 'item_type': ItemType.product_variant.type, + 'item_id': data.productVariantId, + 'is_delete_by_owner': isDeletedByOwner, + 'deleted_at': now.toIso8601String(), + }, + ); + + if (outletId == null && canDeleteAll) { + batch.update( + productVariants, + ProductVariantsCompanion( + syncStatus: Value(SyncStatus.dirty.status), + deletedAt: Value(now), + ), + where: (variant) { + return variant.localId.equals(data.productVariantId) & + variant.productId.equals(productId); + }, + ); + + CustomSyncQueue.addToSyncQueue( + type: TableType.product_variants, + id: data.productVariantId, + action: SyncAction.delete, + batch: batch, + syncQueues: syncQueues, + data: { + 'uuid': data.productVariantId, + 'product_id': productId, + 'deleted_at': now.toIso8601String(), + }, + ); + } + } + + if (outletId == null && canDeleteAll) { + batch.update( + products, + ProductsCompanion( + syncStatus: Value(SyncStatus.dirty.status), + deletedAt: Value(now), + ), + where: (product) { + return product.localId.equals(productId); + }, + ); + + CustomSyncQueue.addToSyncQueue( + type: TableType.products, + id: productId, + action: SyncAction.delete, + batch: batch, + syncQueues: syncQueues, + data: {'uuid': productId, 'deleted_at': now.toIso8601String()}, + ); + } + }); + } else if (rawMaterialId != null) { + final rawMaterialQuery = + selectOnly(rawMaterials).join([ + innerJoin( + outletInventories, + outletInventories.itemType.equals( + ItemType.raw_material.type, + ) & + outletInventories.itemId.equalsExp( + rawMaterials.localId, + ), + ), + innerJoin( + receiptItems, + receiptItems.rawMaterialId.equalsExp(rawMaterials.localId), + ), + ]) + ..addColumns([ + rawMaterials.localId, + outletInventories.localId, + receiptItems.localId, + ]) + ..where(rawMaterials.localId.equals(rawMaterialId)) + ..where( + outletId != null && !canDeleteAll + ? outletInventories.outletId.equals(outletId) + : const Constant(true), + ) + ..where(rawMaterials.deletedAt.isNull()) + ..where(outletInventories.deletedAt.isNull()) + ..where(receiptItems.deletedAt.isNull()); + + final rawMaterialRows = await rawMaterialQuery.get(); + + final List + deleteRawMaterials = rawMaterialRows.map((row) { + final String rawMaterialId = row.read(rawMaterials.localId) ?? ''; + final String outletInventoryId = + row.read(outletInventories.localId) ?? ''; + final String receiptItemId = row.read(receiptItems.localId) ?? ''; + + return DeleteRawMaterial( + rawMaterialId: rawMaterialId, + outletInventoryId: outletInventoryId, + receiptItemId: receiptItemId, + ); + }).toList(); + + await batch((batch) { + for (var data in deleteRawMaterials) { + batch.update( + outletInventories, + OutletInventoriesCompanion( + isDeleteByOwner: Value(isDeletedByOwner), + syncStatus: Value(SyncStatus.dirty.status), + deletedAt: Value(now), + ), + where: (inventory) { + return inventory.localId.equals(data.outletInventoryId) & + inventory.itemType.equals(ItemType.raw_material.type) & + inventory.itemId.equals(data.rawMaterialId); + }, + ); + + CustomSyncQueue.addToSyncQueue( + type: TableType.outlet_inventories, + id: data.outletInventoryId, + action: SyncAction.delete, + batch: batch, + syncQueues: syncQueues, + data: { + 'uuid': data.outletInventoryId, + 'item_type': ItemType.raw_material.type, + 'item_id': data.rawMaterialId, + 'is_delete_by_owner': isDeletedByOwner, + 'deleted_at': now.toIso8601String(), + }, + ); + + if (outletId == null && canDeleteAll) { + batch.update( + receiptItems, + ReceiptItemsCompanion( + syncStatus: Value(SyncStatus.dirty.status), + deletedAt: Value(now), + ), + where: (item) { + return item.localId.equals(data.receiptItemId) & + item.rawMaterialId.equals(data.rawMaterialId); + }, + ); + + CustomSyncQueue.addToSyncQueue( + type: TableType.receipt_items, + id: data.receiptItemId, + action: SyncAction.delete, + batch: batch, + syncQueues: syncQueues, + data: { + 'receipt_item_id': data.receiptItemId, + 'raw_material_id': data.rawMaterialId, + 'deleted_at': now.toIso8601String(), + }, + ); + } + } + + if (outletId == null && canDeleteAll) { + batch.update( + rawMaterials, + RawMaterialsCompanion( + syncStatus: Value(SyncStatus.dirty.status), + deletedAt: Value(now), + ), + where: (material) { + return material.localId.equals(rawMaterialId); + }, + ); + + CustomSyncQueue.addToSyncQueue( + type: TableType.raw_materials, + id: rawMaterialId, + action: SyncAction.delete, + batch: batch, + syncQueues: syncQueues, + data: { + 'uuid': rawMaterialId, + 'deleted_at': now.toIso8601String(), + }, + ); + } + }); + } + }); + } catch (e) { + rethrow; + } + } +} diff --git a/Mobile Operasional/lib/core/database/daos/outlet_inventory_dao.g.dart b/Mobile Operasional/lib/core/database/daos/outlet_inventory_dao.g.dart new file mode 100644 index 0000000..c587383 --- /dev/null +++ b/Mobile Operasional/lib/core/database/daos/outlet_inventory_dao.g.dart @@ -0,0 +1,53 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'outlet_inventory_dao.dart'; + +// ignore_for_file: type=lint +mixin _$OutletInventoryDaoMixin on DatabaseAccessor { + $CategoriesTable get categories => attachedDatabase.categories; + $UnitsTable get units => attachedDatabase.units; + $ProductsTable get products => attachedDatabase.products; + $ProductVariantsTable get productVariants => attachedDatabase.productVariants; + $RawMaterialsTable get rawMaterials => attachedDatabase.rawMaterials; + $ReceiptItemsTable get receiptItems => attachedDatabase.receiptItems; + $OutletsTable get outlets => attachedDatabase.outlets; + $OutletInventoriesTable get outletInventories => + attachedDatabase.outletInventories; + $UsersTable get users => attachedDatabase.users; + $StockCardsTable get stockCards => attachedDatabase.stockCards; + $SyncQueuesTable get syncQueues => attachedDatabase.syncQueues; + OutletInventoryDaoManager get managers => OutletInventoryDaoManager(this); +} + +class OutletInventoryDaoManager { + final _$OutletInventoryDaoMixin _db; + OutletInventoryDaoManager(this._db); + $$CategoriesTableTableManager get categories => + $$CategoriesTableTableManager(_db.attachedDatabase, _db.categories); + $$UnitsTableTableManager get units => + $$UnitsTableTableManager(_db.attachedDatabase, _db.units); + $$ProductsTableTableManager get products => + $$ProductsTableTableManager(_db.attachedDatabase, _db.products); + $$ProductVariantsTableTableManager get productVariants => + $$ProductVariantsTableTableManager( + _db.attachedDatabase, + _db.productVariants, + ); + $$RawMaterialsTableTableManager get rawMaterials => + $$RawMaterialsTableTableManager(_db.attachedDatabase, _db.rawMaterials); + $$ReceiptItemsTableTableManager get receiptItems => + $$ReceiptItemsTableTableManager(_db.attachedDatabase, _db.receiptItems); + $$OutletsTableTableManager get outlets => + $$OutletsTableTableManager(_db.attachedDatabase, _db.outlets); + $$OutletInventoriesTableTableManager get outletInventories => + $$OutletInventoriesTableTableManager( + _db.attachedDatabase, + _db.outletInventories, + ); + $$UsersTableTableManager get users => + $$UsersTableTableManager(_db.attachedDatabase, _db.users); + $$StockCardsTableTableManager get stockCards => + $$StockCardsTableTableManager(_db.attachedDatabase, _db.stockCards); + $$SyncQueuesTableTableManager get syncQueues => + $$SyncQueuesTableTableManager(_db.attachedDatabase, _db.syncQueues); +} diff --git a/Mobile Operasional/lib/core/database/daos/product_dao.dart b/Mobile Operasional/lib/core/database/daos/product_dao.dart new file mode 100644 index 0000000..8ca309e --- /dev/null +++ b/Mobile Operasional/lib/core/database/daos/product_dao.dart @@ -0,0 +1,485 @@ +import 'dart:convert'; + +import 'package:drift/drift.dart'; +import 'package:niogu_app/core/database/app_database.dart' hide Product, Outlet; +import 'package:niogu_app/core/database/tables/categories.dart'; +import 'package:niogu_app/core/database/tables/outlet_inventories.dart'; +import 'package:niogu_app/core/database/tables/outlets.dart'; +import 'package:niogu_app/core/database/tables/product_variants.dart'; +import 'package:niogu_app/core/database/tables/products.dart'; +import 'package:niogu_app/core/database/tables/units.dart'; +import 'package:niogu_app/core/utils/currency_format.dart'; +import 'package:niogu_app/core/utils/extension_format.dart'; +import 'package:niogu_app/core/enums/item_type.dart'; +import 'package:niogu_app/core/system/system_setting.dart'; +import 'package:niogu_app/core/enums/stock_type.dart'; +import 'package:niogu_app/features/outlets/domain/entities/outlet.dart'; +import 'package:niogu_app/features/goods/products/domain/entities/product.dart'; + +part 'product_dao.g.dart'; + +@DriftAccessor( + tables: [ + Products, + Categories, + Units, + ProductVariants, + OutletInventories, + Outlets, + ], +) +class ProductDao extends DatabaseAccessor with _$ProductDaoMixin { + ProductDao(AppDatabase db) : super(db); + + Stream> watchAllProducts() async* { + final outletId = await SystemSetting.getCurrentOutletId(); + + final masterQuery = selectOnly(productVariants) + ..addColumns([productVariants.productId, productVariants.localId.count()]) + ..groupBy([productVariants.productId]) + ..where(productVariants.deletedAt.isNull()); + + final masterSub = Subquery(masterQuery, 'master_sub'); + + final query = + selectOnly(products).join([ + innerJoin(units, units.localId.equalsExp(products.unitId)), + + innerJoin( + productVariants, + productVariants.productId.equalsExp(products.localId), + ), + innerJoin( + outletInventories, + outletInventories.itemType.equals(ItemType.product_variant.type) & + outletInventories.itemId.equalsExp(productVariants.localId), + ), + innerJoin( + masterSub, + masterSub + .ref(productVariants.productId) + .equalsExp(products.localId), + ), + ]) + ..addColumns([ + products.localId, + products.localImagePath, + products.name, + units.name, + products.hasVariant, + productVariants.localId.count(), + masterSub.ref(productVariants.localId.count()), + productVariants.isProductVariant, + outletInventories.stockType, + outletInventories.stock.sum(), + outletInventories.sellingPrice.min(), + ]) + ..groupBy([products.localId]) + ..orderBy([OrderingTerm.desc(products.createdAt)]) + ..where(products.deletedAt.isNull()) + ..where(productVariants.deletedAt.isNull()) + ..where( + outletInventories.outletId.equals(outletId!) & + outletInventories.deletedAt.isNull(), + ); + + yield* query.watch().map((rows) { + return rows.map((row) { + final String id = row.read(products.localId) ?? ''; + final String name = row.read(products.name) ?? ''; + final dynamic rawImages = row.read(products.localImagePath); + + List productImages = []; + + if (rawImages is String) { + try { + productImages = (jsonDecode(rawImages) as List).cast(); + } catch (e) { + productImages = []; + } + } else if (rawImages is List) { + productImages = (rawImages).cast(); + } + final bool hasVariant = row.read(products.hasVariant) ?? false; + + final bool isProductVariant = + row.read(productVariants.isProductVariant) ?? false; + + final int variantCount = hasVariant && isProductVariant + ? row.read(productVariants.localId.count()) ?? 0 + : 0; + + final int newVariantCount = hasVariant && isProductVariant + ? (row.read(masterSub.ref(productVariants.localId.count())) ?? 0) + : 0; + + final String stockType = row.read(outletInventories.stockType) ?? ''; + + final String remainingStock = + "${(row.read(outletInventories.stock.sum()) ?? 0.0).toStringWithoutTrailingZero()} ${row.read(units.name) ?? ''}"; + + final String displaySellingPrice = CurrencyFormat.formatToIdr( + (row.read(outletInventories.sellingPrice.min()) ?? 0.0), + 0, + ); + + return Product( + id: id, + name: name, + productImages: productImages, + hasVariant: hasVariant, + variantCount: variantCount, + newVariantCount: newVariantCount - variantCount, + stockType: stockType, + remainingStock: remainingStock, + displaySellingPrice: displaySellingPrice, + ); + }).toList(); + }); + } + + Future getProductById(String id, bool isEditing) async { + final productQuery = selectOnly(products) + ..addColumns([ + products.localId, + products.localImagePath, + products.name, + products.categoryId, + products.hasVariant, + products.unitId, + ]) + ..where(products.localId.equals(id) & products.deletedAt.isNull()) + ..limit(1); + + final outletId = await SystemSetting.getCurrentOutletId(); + + final productVariantQuery = + selectOnly(productVariants).join([ + innerJoin( + outletInventories, + outletInventories.itemType.equals(ItemType.product_variant.type) & + outletInventories.itemId.equalsExp(productVariants.localId), + ), + ]) + ..addColumns([ + productVariants.localId, + outletInventories.localId, + productVariants.localImagePath, + productVariants.serverImageUrl, + productVariants.name, + outletInventories.costPrice, + outletInventories.sellingPrice, + outletInventories.stockType, + outletInventories.stock, + ]) + ..where( + productVariants.productId.equals(id) & + productVariants.deletedAt.isNull(), + ) + ..where( + (isEditing + ? outletInventories.outletId.equals(outletId!) + : const Constant(true)) & + outletInventories.deletedAt.isNull(), + ); + + final productRow = await productQuery.getSingle(); + + final String localId = productRow.read(products.localId) ?? ''; + final String name = productRow.read(products.name) ?? ''; + final dynamic rawImages = productRow.read(products.localImagePath); + + List productImages = []; + + if (rawImages is String) { + try { + productImages = (jsonDecode(rawImages) as List).cast(); + } catch (e) { + productImages = []; + } + } else if (rawImages is List) { + productImages = (rawImages).cast(); + } + + final String categoryId = productRow.read(products.categoryId) ?? ''; + final bool hasVariant = productRow.read(products.hasVariant) ?? false; + final String unitId = productRow.read(products.unitId) ?? ''; + + final productVariantRows = await productVariantQuery.get(); + + List variants = productVariantRows.map((row) { + final String productVariantId = row.read(productVariants.localId) ?? ''; + final String outletInventoryId = + row.read(outletInventories.localId) ?? ''; + final String image = row.read(productVariants.localImagePath) ?? ''; + final String name = row.read(productVariants.name) ?? ''; + final double costPrice = row.read(outletInventories.costPrice) ?? 0.0; + final double sellingPrice = + row.read(outletInventories.sellingPrice) ?? 0.0; + final StockType stockType = StockType.values.byName( + (row.read(outletInventories.stockType) ?? ''), + ); + final double stock = row.read(outletInventories.stock) ?? 0.0; + + return ProductVariantDetail( + productVariantId: productVariantId, + outletInventoryId: outletInventoryId, + image: image, + name: name, + costPrice: costPrice, + sellingPrice: sellingPrice, + stockType: stockType, + stock: stock, + ); + }).toList(); + + return ProductDetail( + localId: localId, + productImages: productImages, + name: name, + categoryId: categoryId, + hasVariant: hasVariant, + variants: variants, + unitId: unitId, + ); + } + + Future> getNewVariants(String id) async { + final outletId = await SystemSetting.getCurrentOutletId(); + final query = + selectOnly(productVariants).join([ + innerJoin( + outletInventories, + outletInventories.itemType.equals(ItemType.product_variant.type) & + outletInventories.itemId.equalsExp(productVariants.localId), + ), + ]) + ..addColumns([ + productVariants.localId, + outletInventories.localId, + productVariants.localImagePath, + productVariants.name, + outletInventories.costPrice, + outletInventories.sellingPrice, + outletInventories.stockType, + outletInventories.stock, + ]) + ..where( + productVariants.productId.equals(id) & + productVariants.deletedAt.isNull(), + ) + ..where( + outletInventories.outletId.equals(outletId!).not() & + outletInventories.deletedAt.isNull(), + ); + + final results = await query.get(); + + return results.map((row) { + final String productVariantId = row.read(productVariants.localId) ?? ''; + final String image = row.read(productVariants.localImagePath) ?? ''; + final String name = row.read(productVariants.name) ?? ''; + final double costPrice = row.read(outletInventories.costPrice) ?? 0.0; + final double sellingPrice = + row.read(outletInventories.sellingPrice) ?? 0.0; + final double stock = row.read(outletInventories.stock) ?? 0.0; + + return ProductWithVariant( + productVariantId: productVariantId, + imagePath: image, + name: name, + stock: stock, + costPrice: costPrice, + sellingPrice: sellingPrice, + ); + }).toList(); + } + + Future getDeleteProductSnapshotById(String id) async { + final productQuery = selectOnly(products) + ..addColumns([products.localId, products.name, products.localImagePath]) + ..where(products.localId.equals(id) & products.deletedAt.isNull()) + ..limit(1); + + final productVariantQuery = + selectOnly(productVariants).join([ + innerJoin( + outletInventories, + outletInventories.itemType.equals(ItemType.product_variant.type) & + outletInventories.itemId.equalsExp(productVariants.localId), + ), + innerJoin( + outlets, + outlets.localId.equalsExp(outletInventories.outletId), + ), + ]) + ..addColumns([ + productVariants.totalSold, + outlets.localId, + outlets.name, + outlets.isMainOutlet, + ]) + ..where( + productVariants.productId.equals(id) & + productVariants.deletedAt.isNull(), + ) + ..where(outlets.deletedAt.isNull()) + ..where(outletInventories.deletedAt.isNull()); + + final productRow = await productQuery.getSingle(); + + final productVariantRows = await productVariantQuery.get(); + + final String productId = productRow.read(products.localId) ?? ''; + final String name = productRow.read(products.name) ?? ''; + final dynamic rawImages = productRow.read(products.localImagePath); + + List productImages = []; + + if (rawImages is String) { + try { + productImages = (jsonDecode(rawImages) as List).cast(); + } catch (e) { + productImages = []; + } + } else if (rawImages is List) { + productImages = (rawImages).cast(); + } + + final String? imagePath = productImages.isNotEmpty + ? productImages.last + : null; + + double totalSold = 0.0; + + final Map outletUses = {}; + + for (final row in productVariantRows) { + final String id = row.read(outlets.localId) ?? ''; + + final String name = row.read(outlets.name) ?? ''; + + final bool isMainOutlet = row.read(outlets.isMainOutlet) ?? false; + + outletUses.putIfAbsent( + id, + () => Outlet(id: id, name: name, isMainOutlet: isMainOutlet), + ); + + totalSold += row.read(productVariants.totalSold) ?? 0.0; + } + + final outletId = await SystemSetting.getCurrentOutletId(); + + final currentOutlet = outletUses.values + .where((outlet) => outlet.id == outletId) + .first; + + return DeleteProductSnapshot( + productId: productId, + imagePath: imagePath, + name: name, + totalSold: totalSold, + currentOutletName: currentOutlet.name, + isMainOutlet: currentOutlet.isMainOutlet, + totalOutletCount: outletUses.length, + ); + } + + Future checkAlreadyProductByOutlet(String id) async { + final productQuery = selectOnly(products) + ..addColumns([products.localId]) + ..where(products.localId.equals(id) & products.deletedAt.isNull()) + ..limit(1); + + final outletId = await SystemSetting.getCurrentOutletId(); + + final productVariantQuery = + selectOnly(productVariants).join([ + innerJoin( + outletInventories, + outletInventories.itemType.equals(ItemType.product_variant.type) & + outletInventories.itemId.equalsExp(productVariants.localId), + ), + ]) + ..addColumns([productVariants.localId, outletInventories.outletId]) + ..where( + productVariants.productId.equals(id) & + productVariants.deletedAt.isNull(), + ) + ..where( + outletInventories.outletId.equals(outletId!) & + outletInventories.deletedAt.isNull(), + ); + + final resultProduct = await productQuery.getSingleOrNull(); + + final resultProductVariants = await productVariantQuery.get(); + + return resultProduct != null && resultProductVariants.isNotEmpty; + } + + Stream> watchAllProductChoices() async* { + final query = + selectOnly(products).join([ + innerJoin( + productVariants, + productVariants.productId.equalsExp(products.localId), + ), + innerJoin( + outletInventories, + outletInventories.itemType.equals(ItemType.product_variant.type) & + outletInventories.itemId.equalsExp(productVariants.localId), + ), + ]) + ..addColumns([ + products.localId, + products.localImagePath, + products.name, + products.hasVariant, + ]) + ..where(products.deletedAt.isNull()) + ..where(productVariants.deletedAt.isNull()) + ..where(outletInventories.deletedAt.isNull()); + + yield* query.watch().map((rows) { + final Map groupedMap = {}; + + for (final row in rows) { + final productId = row.read(products.localId)!; + + final dynamic rawImages = row.read(products.localImagePath); + + List productImages = []; + + if (rawImages is String) { + try { + productImages = (jsonDecode(rawImages) as List).cast(); + } catch (e) { + productImages = []; + } + } else if (rawImages is List) { + productImages = (rawImages).cast(); + } + + String? imagePath; + + if (productImages.isNotEmpty) { + imagePath = productImages.last; + } + + groupedMap.putIfAbsent( + productId, + () => ProductChoice( + id: productId, + imagePath: imagePath, + name: row.read(products.name) ?? '', + hasVariant: row.read(products.hasVariant) ?? false, + ), + ); + } + + return groupedMap.values.toList(); + }); + } +} diff --git a/Mobile Operasional/lib/core/database/daos/product_dao.g.dart b/Mobile Operasional/lib/core/database/daos/product_dao.g.dart new file mode 100644 index 0000000..1acf829 --- /dev/null +++ b/Mobile Operasional/lib/core/database/daos/product_dao.g.dart @@ -0,0 +1,38 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'product_dao.dart'; + +// ignore_for_file: type=lint +mixin _$ProductDaoMixin on DatabaseAccessor { + $CategoriesTable get categories => attachedDatabase.categories; + $UnitsTable get units => attachedDatabase.units; + $ProductsTable get products => attachedDatabase.products; + $ProductVariantsTable get productVariants => attachedDatabase.productVariants; + $OutletsTable get outlets => attachedDatabase.outlets; + $OutletInventoriesTable get outletInventories => + attachedDatabase.outletInventories; + ProductDaoManager get managers => ProductDaoManager(this); +} + +class ProductDaoManager { + final _$ProductDaoMixin _db; + ProductDaoManager(this._db); + $$CategoriesTableTableManager get categories => + $$CategoriesTableTableManager(_db.attachedDatabase, _db.categories); + $$UnitsTableTableManager get units => + $$UnitsTableTableManager(_db.attachedDatabase, _db.units); + $$ProductsTableTableManager get products => + $$ProductsTableTableManager(_db.attachedDatabase, _db.products); + $$ProductVariantsTableTableManager get productVariants => + $$ProductVariantsTableTableManager( + _db.attachedDatabase, + _db.productVariants, + ); + $$OutletsTableTableManager get outlets => + $$OutletsTableTableManager(_db.attachedDatabase, _db.outlets); + $$OutletInventoriesTableTableManager get outletInventories => + $$OutletInventoriesTableTableManager( + _db.attachedDatabase, + _db.outletInventories, + ); +} diff --git a/Mobile Operasional/lib/core/database/daos/product_like_dao.dart b/Mobile Operasional/lib/core/database/daos/product_like_dao.dart new file mode 100644 index 0000000..2751ea3 --- /dev/null +++ b/Mobile Operasional/lib/core/database/daos/product_like_dao.dart @@ -0,0 +1,12 @@ +import 'package:drift/drift.dart'; +import 'package:niogu_app/core/database/app_database.dart'; +import 'package:niogu_app/core/database/tables/customers.dart'; +import 'package:niogu_app/core/database/tables/product_likes.dart'; +import 'package:niogu_app/core/database/tables/products.dart'; + +part 'product_like_dao.g.dart'; + +@DriftAccessor(tables: [Customers, Products, ProductLikes]) +class ProductLikeDao extends DatabaseAccessor with _$ProductLikeDaoMixin { + ProductLikeDao(AppDatabase db) : super(db); +} \ No newline at end of file diff --git a/Mobile Operasional/lib/core/database/daos/product_like_dao.g.dart b/Mobile Operasional/lib/core/database/daos/product_like_dao.g.dart new file mode 100644 index 0000000..ada599a --- /dev/null +++ b/Mobile Operasional/lib/core/database/daos/product_like_dao.g.dart @@ -0,0 +1,31 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'product_like_dao.dart'; + +// ignore_for_file: type=lint +mixin _$ProductLikeDaoMixin on DatabaseAccessor { + $OutletsTable get outlets => attachedDatabase.outlets; + $CustomersTable get customers => attachedDatabase.customers; + $CategoriesTable get categories => attachedDatabase.categories; + $UnitsTable get units => attachedDatabase.units; + $ProductsTable get products => attachedDatabase.products; + $ProductLikesTable get productLikes => attachedDatabase.productLikes; + ProductLikeDaoManager get managers => ProductLikeDaoManager(this); +} + +class ProductLikeDaoManager { + final _$ProductLikeDaoMixin _db; + ProductLikeDaoManager(this._db); + $$OutletsTableTableManager get outlets => + $$OutletsTableTableManager(_db.attachedDatabase, _db.outlets); + $$CustomersTableTableManager get customers => + $$CustomersTableTableManager(_db.attachedDatabase, _db.customers); + $$CategoriesTableTableManager get categories => + $$CategoriesTableTableManager(_db.attachedDatabase, _db.categories); + $$UnitsTableTableManager get units => + $$UnitsTableTableManager(_db.attachedDatabase, _db.units); + $$ProductsTableTableManager get products => + $$ProductsTableTableManager(_db.attachedDatabase, _db.products); + $$ProductLikesTableTableManager get productLikes => + $$ProductLikesTableTableManager(_db.attachedDatabase, _db.productLikes); +} diff --git a/Mobile Operasional/lib/core/database/daos/product_review_dao.dart b/Mobile Operasional/lib/core/database/daos/product_review_dao.dart new file mode 100644 index 0000000..3d46dbc --- /dev/null +++ b/Mobile Operasional/lib/core/database/daos/product_review_dao.dart @@ -0,0 +1,299 @@ +import 'dart:convert'; + +import 'package:drift/drift.dart'; +import 'package:niogu_app/core/database/app_database.dart'; +import 'package:niogu_app/core/database/tables/customers.dart'; +import 'package:niogu_app/core/database/tables/order_items.dart'; +import 'package:niogu_app/core/database/tables/orders.dart'; +import 'package:niogu_app/core/database/tables/product_likes.dart'; +import 'package:niogu_app/core/database/tables/product_reviews.dart'; +import 'package:niogu_app/core/database/tables/product_variants.dart'; +import 'package:niogu_app/core/database/tables/products.dart'; +import 'package:niogu_app/core/database/tables/units.dart'; +import 'package:niogu_app/core/enums/order_source.dart'; +import 'package:niogu_app/features/online_store/domain/entities/online_store.dart'; + +part 'product_review_dao.g.dart'; + +@DriftAccessor( + tables: [ + Customers, + Units, + Products, + ProductVariants, + ProductReviews, + ProductLikes, + Orders, + OrderItems, + ], +) +class ProductReviewDao extends DatabaseAccessor + with _$ProductReviewDaoMixin { + ProductReviewDao(AppDatabase db) : super(db); + + Stream> watchAllProductReviews() async* { + final ratingAvg = productVariants.averageRating.avg(); + final ratingQuery = selectOnly(productVariants) + ..addColumns([productVariants.productId, ratingAvg]) + ..groupBy([productVariants.productId]); + + final ratingSub = Subquery(ratingQuery, 'rating_sub'); + + final onlineSold = CaseWhenExpression( + cases: [ + CaseWhen( + orders.source.equals(OrderSource.online.source), + then: orderItems.quantity, + ), + ], + orElse: const Constant(0.0), + ).sum(); + + final posSold = CaseWhenExpression( + cases: [ + CaseWhen( + orders.source.equals(OrderSource.pos.source), + then: orderItems.quantity, + ), + ], + orElse: const Constant(0.0), + ).sum(); + + final salesQuery = + selectOnly(productVariants).join([ + leftOuterJoin( + orderItems, + orderItems.productVariantId.equalsExp(productVariants.localId), + ), + leftOuterJoin(orders, orders.localId.equalsExp(orderItems.orderId)), + ]) + ..addColumns([productVariants.productId, onlineSold, posSold]) + ..groupBy([productVariants.productId]); + + final salesSub = Subquery(salesQuery, 'sales_sub'); + + final query = + selectOnly(products).join([ + innerJoin(units, units.localId.equalsExp(products.unitId)), + + leftOuterJoin( + ratingSub, + ratingSub + .ref(productVariants.productId) + .equalsExp(products.localId), + ), + + leftOuterJoin( + salesSub, + salesSub.ref(productVariants.productId).equalsExp(products.localId), + ), + ])..addColumns([ + products.localId, + products.localImagePath, + products.name, + units.name, + ratingSub.ref(ratingAvg), + salesSub.ref(onlineSold), + salesSub.ref(posSold), + ]); + + yield* query.watch().map((rows) { + return rows.map((row) { + final rating = row.read(ratingSub.ref(ratingAvg)) ?? 0.0; + final online = row.read(salesSub.ref(onlineSold)) ?? 0.0; + final pos = row.read(salesSub.ref(posSold)) ?? 0.0; + + final dynamic rawImages = row.read(products.localImagePath); + + List productImages = []; + + if (rawImages is String) { + try { + productImages = (jsonDecode(rawImages) as List).cast(); + } catch (e) { + productImages = []; + } + } else if (rawImages is List) { + productImages = (rawImages).cast(); + } + + return DisplayProductReview( + id: row.read(products.localId)!, + imagePath: productImages.lastOrNull, + name: row.read(products.name) ?? '', + unit: row.read(units.name) ?? '', + averageRating: rating, + totalSoldInOnlineStore: online, + totalSoldInPos: pos, + ); + }).toList(); + }); + } + + Stream watchDetailProductReview( + String productId, + ) async* { + final fiveStar = CaseWhenExpression( + cases: [ + CaseWhen(productReviews.rating.equals(5), then: const Constant(1)), + ], + orElse: const Constant(0), + ).sum(); + + final fourStar = CaseWhenExpression( + cases: [ + CaseWhen(productReviews.rating.equals(4), then: const Constant(1)), + ], + orElse: const Constant(0), + ).sum(); + + final threeStar = CaseWhenExpression( + cases: [ + CaseWhen(productReviews.rating.equals(3), then: const Constant(1)), + ], + orElse: const Constant(0), + ).sum(); + + final twoStar = CaseWhenExpression( + cases: [ + CaseWhen(productReviews.rating.equals(2), then: const Constant(1)), + ], + orElse: const Constant(0), + ).sum(); + + final oneStar = CaseWhenExpression( + cases: [ + CaseWhen(productReviews.rating.equals(1), then: const Constant(1)), + ], + orElse: const Constant(0), + ).sum(); + + final avgRating = productVariants.averageRating.avg(); + final totalRating = productVariants.totalRating.sum(); + + final totalLike = productLikes.localId.count(); + + final likeQuery = selectOnly(productLikes) + ..addColumns([productLikes.productId, totalLike]) + ..where(productLikes.productId.equals(productId)) + ..where(productLikes.isLike.equals(true)); + + final likeSub = Subquery(likeQuery, 'like_sub'); + + final query = + select(products).join([ + innerJoin( + productVariants, + productVariants.productId.equalsExp(products.localId), + ), + leftOuterJoin( + productReviews, + productReviews.productVariantId.equalsExp( + productVariants.localId, + ), + ), + leftOuterJoin( + likeSub, + likeSub.ref(productLikes.productId).equalsExp(products.localId), + ), + ]) + ..addColumns([ + products.localImagePath, + avgRating, + totalRating, + fiveStar, + fourStar, + threeStar, + twoStar, + oneStar, + likeSub.ref(totalLike), + ]) + ..groupBy([products.localId]) + ..where(products.localId.equals(productId)); + + yield* query.watchSingle().map((row) { + final dynamic rawImages = row.read(products.localImagePath); + + List productImages = []; + + if (rawImages is String) { + try { + productImages = (jsonDecode(rawImages) as List).cast(); + } catch (e) { + productImages = []; + } + } else if (rawImages is List) { + productImages = (rawImages).cast(); + } + + final one = row.read(oneStar) ?? 0; + final two = row.read(twoStar) ?? 0; + final three = row.read(threeStar) ?? 0; + final four = row.read(fourStar) ?? 0; + final five = row.read(fiveStar) ?? 0; + + final total = one + two + three + four + five; + final t = total == 0 ? 1.0 : total.toDouble(); + + final distributions = [one / t, two / t, three / t, four / t, five / t]; + + return DetailProductReview( + imagePaths: productImages, + name: row.read(products.name) ?? '', + averageRating: row.read(avgRating) ?? 0.0, + totalRating: row.read(totalRating) ?? 0, + rating: RatingDistribution(distributions), + totalLike: row.read(likeSub.ref(totalLike)) ?? 0, + ); + }); + } + + Stream> watchAllProductComments( + String productId, + ) async* { + final query = + selectOnly(productReviews).join([ + innerJoin( + productVariants, + productVariants.localId.equalsExp( + productReviews.productVariantId, + ), + ), + ]) + ..addColumns([ + productReviews.customerNameSnapshot, + productReviews.rating, + productVariants.name, + productVariants.isProductVariant, + productReviews.comment, + productReviews.createdAt, + ]) + ..orderBy([OrderingTerm.desc(productReviews.createdAt)]) + ..where(productVariants.productId.equals(productId)); + + yield* query.watch().map((rows) { + return rows.map((row) { + final name = row.read(productReviews.customerNameSnapshot) ?? ''; + + final rating = row.read(productReviews.rating) ?? 0; + + final variantName = + (row.read(productVariants.isProductVariant) ?? false) + ? row.read(productVariants.name) + : null; + + final comment = row.read(productReviews.comment); + + final date = row.read(productReviews.createdAt) ?? DateTime.now(); + + return ProductComment( + name: name, + rating: rating, + variantName: variantName, + comment: comment, + date: date, + ); + }).toList(); + }); + } +} diff --git a/Mobile Operasional/lib/core/database/daos/product_review_dao.g.dart b/Mobile Operasional/lib/core/database/daos/product_review_dao.g.dart new file mode 100644 index 0000000..498694c --- /dev/null +++ b/Mobile Operasional/lib/core/database/daos/product_review_dao.g.dart @@ -0,0 +1,52 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'product_review_dao.dart'; + +// ignore_for_file: type=lint +mixin _$ProductReviewDaoMixin on DatabaseAccessor { + $OutletsTable get outlets => attachedDatabase.outlets; + $CustomersTable get customers => attachedDatabase.customers; + $UnitsTable get units => attachedDatabase.units; + $CategoriesTable get categories => attachedDatabase.categories; + $ProductsTable get products => attachedDatabase.products; + $ProductVariantsTable get productVariants => attachedDatabase.productVariants; + $ProductReviewsTable get productReviews => attachedDatabase.productReviews; + $ProductLikesTable get productLikes => attachedDatabase.productLikes; + $UsersTable get users => attachedDatabase.users; + $OrdersTable get orders => attachedDatabase.orders; + $OrderItemsTable get orderItems => attachedDatabase.orderItems; + ProductReviewDaoManager get managers => ProductReviewDaoManager(this); +} + +class ProductReviewDaoManager { + final _$ProductReviewDaoMixin _db; + ProductReviewDaoManager(this._db); + $$OutletsTableTableManager get outlets => + $$OutletsTableTableManager(_db.attachedDatabase, _db.outlets); + $$CustomersTableTableManager get customers => + $$CustomersTableTableManager(_db.attachedDatabase, _db.customers); + $$UnitsTableTableManager get units => + $$UnitsTableTableManager(_db.attachedDatabase, _db.units); + $$CategoriesTableTableManager get categories => + $$CategoriesTableTableManager(_db.attachedDatabase, _db.categories); + $$ProductsTableTableManager get products => + $$ProductsTableTableManager(_db.attachedDatabase, _db.products); + $$ProductVariantsTableTableManager get productVariants => + $$ProductVariantsTableTableManager( + _db.attachedDatabase, + _db.productVariants, + ); + $$ProductReviewsTableTableManager get productReviews => + $$ProductReviewsTableTableManager( + _db.attachedDatabase, + _db.productReviews, + ); + $$ProductLikesTableTableManager get productLikes => + $$ProductLikesTableTableManager(_db.attachedDatabase, _db.productLikes); + $$UsersTableTableManager get users => + $$UsersTableTableManager(_db.attachedDatabase, _db.users); + $$OrdersTableTableManager get orders => + $$OrdersTableTableManager(_db.attachedDatabase, _db.orders); + $$OrderItemsTableTableManager get orderItems => + $$OrderItemsTableTableManager(_db.attachedDatabase, _db.orderItems); +} diff --git a/Mobile Operasional/lib/core/database/daos/product_variant_dao.dart b/Mobile Operasional/lib/core/database/daos/product_variant_dao.dart new file mode 100644 index 0000000..fa48c2f --- /dev/null +++ b/Mobile Operasional/lib/core/database/daos/product_variant_dao.dart @@ -0,0 +1,13 @@ +import 'package:drift/drift.dart'; +import 'package:niogu_app/core/database/app_database.dart'; +import 'package:niogu_app/core/database/tables/outlet_inventories.dart'; +import 'package:niogu_app/core/database/tables/product_variants.dart'; + +part 'product_variant_dao.g.dart'; + +@DriftAccessor(tables: [ProductVariants, OutletInventories]) +class ProductVariantDao extends DatabaseAccessor + with _$ProductVariantDaoMixin { + ProductVariantDao(AppDatabase db) : super(db); + +} diff --git a/Mobile Operasional/lib/core/database/daos/product_variant_dao.g.dart b/Mobile Operasional/lib/core/database/daos/product_variant_dao.g.dart new file mode 100644 index 0000000..6255d27 --- /dev/null +++ b/Mobile Operasional/lib/core/database/daos/product_variant_dao.g.dart @@ -0,0 +1,38 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'product_variant_dao.dart'; + +// ignore_for_file: type=lint +mixin _$ProductVariantDaoMixin on DatabaseAccessor { + $CategoriesTable get categories => attachedDatabase.categories; + $UnitsTable get units => attachedDatabase.units; + $ProductsTable get products => attachedDatabase.products; + $ProductVariantsTable get productVariants => attachedDatabase.productVariants; + $OutletsTable get outlets => attachedDatabase.outlets; + $OutletInventoriesTable get outletInventories => + attachedDatabase.outletInventories; + ProductVariantDaoManager get managers => ProductVariantDaoManager(this); +} + +class ProductVariantDaoManager { + final _$ProductVariantDaoMixin _db; + ProductVariantDaoManager(this._db); + $$CategoriesTableTableManager get categories => + $$CategoriesTableTableManager(_db.attachedDatabase, _db.categories); + $$UnitsTableTableManager get units => + $$UnitsTableTableManager(_db.attachedDatabase, _db.units); + $$ProductsTableTableManager get products => + $$ProductsTableTableManager(_db.attachedDatabase, _db.products); + $$ProductVariantsTableTableManager get productVariants => + $$ProductVariantsTableTableManager( + _db.attachedDatabase, + _db.productVariants, + ); + $$OutletsTableTableManager get outlets => + $$OutletsTableTableManager(_db.attachedDatabase, _db.outlets); + $$OutletInventoriesTableTableManager get outletInventories => + $$OutletInventoriesTableTableManager( + _db.attachedDatabase, + _db.outletInventories, + ); +} diff --git a/Mobile Operasional/lib/core/database/daos/purchase_dao.dart b/Mobile Operasional/lib/core/database/daos/purchase_dao.dart new file mode 100644 index 0000000..a2b7735 --- /dev/null +++ b/Mobile Operasional/lib/core/database/daos/purchase_dao.dart @@ -0,0 +1,172 @@ +import 'package:drift/drift.dart'; +import 'package:niogu_app/core/database/app_database.dart' + hide Product, PurchaseItem; +import 'package:niogu_app/core/database/tables/purchase_items.dart'; +import 'package:niogu_app/core/database/tables/purchases.dart'; +import 'package:niogu_app/core/utils/extension_format.dart'; +import 'package:niogu_app/core/enums/payment_status.dart'; +import 'package:niogu_app/core/enums/purchase_status.dart'; +import 'package:niogu_app/core/enums/sync_status.dart'; +import 'package:niogu_app/features/report/transaction/domain/entities/transaction_report.dart'; + +part 'purchase_dao.g.dart'; + +@DriftAccessor( + tables: [ + Purchases, + PurchaseItems, + ], +) +class PurchaseDao extends DatabaseAccessor + with _$PurchaseDaoMixin { + PurchaseDao(AppDatabase db) : super(db); + + Future getPurchaseDetail(String purchaseId) async { + final query = + selectOnly(purchases).join([ + innerJoin( + purchaseItems, + purchaseItems.purchaseId.equalsExp(purchases.localId), + ) + ]) + ..addColumns([ + purchases.localId, + purchases.purchaseNumber, + purchases.createdAt, + purchases.paymentStatus, + purchases.purchaseStatus, + purchases.outletNameSnapshot, + purchases.supplierNameSnapshot, + purchases.supplierPhoneNumberSnapshot, + purchases.adminNameSnapshot, + purchases.notes, + purchaseItems.itemNameSnapshot, + purchaseItems.itemVariantNameSnapshot, + purchaseItems.purchasePriceSnaphot, + purchaseItems.quantity, + purchaseItems.subtotal, + purchases.totalPurchase, + purchases.discount, + purchases.tax, + purchases.totalAmount, + purchases.amountPaid, + purchases.changeAmount, + purchases.underPaymentAmount, + purchases.debtAmount, + purchases.paymentProofPath, + ]) + ..where(purchases.localId.equals(purchaseId)); + + final results = await query.get(); + + final Map groupedMap = {}; + + for (final row in results) { + final String purchaseId = row.read(purchases.localId) ?? ''; + + final String purchaseNumber = (row.read(purchases.purchaseNumber) ?? '') + .toUniqueNumberCleaner(); + + final DateTime purchaseDate = + row.read(purchases.createdAt) ?? DateTime.now(); + + final String status = row.read(purchases.paymentStatus) ?? ''; + + final PaymentStatus paymentStatus = PaymentStatus.values.byName(status); + + final String outletName = row.read(purchases.outletNameSnapshot) ?? ''; + + final String userName = row.read(purchases.adminNameSnapshot) ?? ''; + + final PurchaseStatus purchaseStatus = PurchaseStatus.values.byName( + (row.read(purchases.purchaseStatus)) ?? '', + ); + + final String? supplierName = row.read(purchases.supplierNameSnapshot); + + final String? supplierPhone = row.read(purchases.supplierPhoneNumberSnapshot); + + final String? note = row.read(purchases.notes); + + final double totalPurchase = row.read(purchases.totalPurchase) ?? 0.0; + + final double discount = row.read(purchases.discount) ?? 0.0; + + final double tax = row.read(purchases.tax) ?? 0.0; + + final double totalAmount = row.read(purchases.totalAmount) ?? 0.0; + + final double payAmount = row.read(purchases.amountPaid) ?? 0.0; + + final double changeAmount = row.read(purchases.changeAmount) ?? 0.0; + + final double underPaymentAmount = + row.read(purchases.underPaymentAmount) ?? 0.0; + + final double debtAmount = row.read(purchases.debtAmount) ?? 0.0; + + final String? paymentProofPath = row.read(purchases.paymentProofPath); + + groupedMap.putIfAbsent(purchaseId, () { + return PurchaseDetail( + purchaseId: purchaseId, + purchaseNumber: purchaseNumber, + purchaseDate: purchaseDate, + outletName: outletName, + userName: userName, + purchaseStatus: purchaseStatus, + supplierName: supplierName, + supplierPhone: supplierPhone, + note: note, + items: [], + totalPurchase: totalPurchase, + discount: discount, + tax: tax, + paymentStatus: paymentStatus, + totalAmount: totalAmount, + payAmount: payAmount, + changeAmount: changeAmount, + underPaymentAmount: underPaymentAmount, + debtAmount: debtAmount, + paymentProofPath: paymentProofPath, + ); + }); + + final String name = row.read(purchaseItems.itemNameSnapshot) ?? ''; + + final String? variantName = row.read(purchaseItems.itemVariantNameSnapshot); + + + final double purchasePrice = + row.read(purchaseItems.purchasePriceSnaphot) ?? 0.0; + + final double quantity = row.read(purchaseItems.quantity) ?? 0.0; + + final double subtotal = row.read(purchaseItems.subtotal) ?? 0.0; + + groupedMap[purchaseId]!.items.add( + PurchaseItem( + name: name, + variantName: variantName, + purchasePrice: purchasePrice, + quantity: quantity, + subtotal: subtotal, + ), + ); + } + + return groupedMap.values.single; + } + + Future cancelPurchase(String purchaseId) async { + return (await update(purchases) + ..where((purchase) => purchase.localId.equals(purchaseId))) + .write( + PurchasesCompanion( + purchaseStatus: Value(PurchaseStatus.cancelled.status), + syncStatus: Value(SyncStatus.dirty.status), + updatedAt: Value(DateTime.now()), + ), + ); + } +} diff --git a/Mobile Operasional/lib/core/database/daos/purchase_dao.g.dart b/Mobile Operasional/lib/core/database/daos/purchase_dao.g.dart new file mode 100644 index 0000000..1c9cf22 --- /dev/null +++ b/Mobile Operasional/lib/core/database/daos/purchase_dao.g.dart @@ -0,0 +1,28 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'purchase_dao.dart'; + +// ignore_for_file: type=lint +mixin _$PurchaseDaoMixin on DatabaseAccessor { + $OutletsTable get outlets => attachedDatabase.outlets; + $SuppliersTable get suppliers => attachedDatabase.suppliers; + $UsersTable get users => attachedDatabase.users; + $PurchasesTable get purchases => attachedDatabase.purchases; + $PurchaseItemsTable get purchaseItems => attachedDatabase.purchaseItems; + PurchaseDaoManager get managers => PurchaseDaoManager(this); +} + +class PurchaseDaoManager { + final _$PurchaseDaoMixin _db; + PurchaseDaoManager(this._db); + $$OutletsTableTableManager get outlets => + $$OutletsTableTableManager(_db.attachedDatabase, _db.outlets); + $$SuppliersTableTableManager get suppliers => + $$SuppliersTableTableManager(_db.attachedDatabase, _db.suppliers); + $$UsersTableTableManager get users => + $$UsersTableTableManager(_db.attachedDatabase, _db.users); + $$PurchasesTableTableManager get purchases => + $$PurchasesTableTableManager(_db.attachedDatabase, _db.purchases); + $$PurchaseItemsTableTableManager get purchaseItems => + $$PurchaseItemsTableTableManager(_db.attachedDatabase, _db.purchaseItems); +} diff --git a/Mobile Operasional/lib/core/database/daos/purchase_item_dao.dart b/Mobile Operasional/lib/core/database/daos/purchase_item_dao.dart new file mode 100644 index 0000000..53f36cf --- /dev/null +++ b/Mobile Operasional/lib/core/database/daos/purchase_item_dao.dart @@ -0,0 +1,11 @@ +import 'package:drift/drift.dart'; +import 'package:niogu_app/core/database/app_database.dart'; +import 'package:niogu_app/core/database/tables/purchase_items.dart'; + +part 'purchase_item_dao.g.dart'; + +@DriftAccessor(tables: [PurchaseItems]) +class PurchaseItemDao extends DatabaseAccessor with _$PurchaseItemDaoMixin { + + PurchaseItemDao(AppDatabase db) : super(db); +} \ No newline at end of file diff --git a/Mobile Operasional/lib/core/database/daos/purchase_item_dao.g.dart b/Mobile Operasional/lib/core/database/daos/purchase_item_dao.g.dart new file mode 100644 index 0000000..6d6026d --- /dev/null +++ b/Mobile Operasional/lib/core/database/daos/purchase_item_dao.g.dart @@ -0,0 +1,28 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'purchase_item_dao.dart'; + +// ignore_for_file: type=lint +mixin _$PurchaseItemDaoMixin on DatabaseAccessor { + $OutletsTable get outlets => attachedDatabase.outlets; + $SuppliersTable get suppliers => attachedDatabase.suppliers; + $UsersTable get users => attachedDatabase.users; + $PurchasesTable get purchases => attachedDatabase.purchases; + $PurchaseItemsTable get purchaseItems => attachedDatabase.purchaseItems; + PurchaseItemDaoManager get managers => PurchaseItemDaoManager(this); +} + +class PurchaseItemDaoManager { + final _$PurchaseItemDaoMixin _db; + PurchaseItemDaoManager(this._db); + $$OutletsTableTableManager get outlets => + $$OutletsTableTableManager(_db.attachedDatabase, _db.outlets); + $$SuppliersTableTableManager get suppliers => + $$SuppliersTableTableManager(_db.attachedDatabase, _db.suppliers); + $$UsersTableTableManager get users => + $$UsersTableTableManager(_db.attachedDatabase, _db.users); + $$PurchasesTableTableManager get purchases => + $$PurchasesTableTableManager(_db.attachedDatabase, _db.purchases); + $$PurchaseItemsTableTableManager get purchaseItems => + $$PurchaseItemsTableTableManager(_db.attachedDatabase, _db.purchaseItems); +} diff --git a/Mobile Operasional/lib/core/database/daos/raw_material_dao.dart b/Mobile Operasional/lib/core/database/daos/raw_material_dao.dart new file mode 100644 index 0000000..81ab88b --- /dev/null +++ b/Mobile Operasional/lib/core/database/daos/raw_material_dao.dart @@ -0,0 +1,422 @@ +import 'dart:convert'; + +import 'package:drift/drift.dart'; +import 'package:niogu_app/core/database/app_database.dart' + hide RawMaterial, Outlet; +import 'package:niogu_app/core/database/tables/outlet_inventories.dart'; +import 'package:niogu_app/core/database/tables/outlets.dart'; +import 'package:niogu_app/core/database/tables/product_variants.dart'; +import 'package:niogu_app/core/database/tables/products.dart'; +import 'package:niogu_app/core/database/tables/raw_materials.dart'; +import 'package:niogu_app/core/database/tables/receipt_items.dart'; +import 'package:niogu_app/core/database/tables/units.dart'; +import 'package:niogu_app/core/utils/currency_format.dart'; +import 'package:niogu_app/core/utils/extension_format.dart'; +import 'package:niogu_app/core/enums/item_type.dart'; +import 'package:niogu_app/core/system/system_setting.dart'; +import 'package:niogu_app/features/goods/raw_materials/domain/entities/raw_material.dart'; +import 'package:niogu_app/features/outlets/domain/entities/outlet.dart'; + +part 'raw_material_dao.g.dart'; + +@DriftAccessor( + tables: [ + RawMaterials, + Units, + Products, + ProductVariants, + ReceiptItems, + OutletInventories, + Outlets, + ], +) +class RawMaterialDao extends DatabaseAccessor + with _$RawMaterialDaoMixin { + RawMaterialDao(AppDatabase db) : super(db); + + Stream> watchAllMaterials() async* { + final outletId = await SystemSetting.getCurrentOutletId(); + final query = + selectOnly(rawMaterials).join([ + innerJoin(units, units.localId.equalsExp(rawMaterials.unitId)), + innerJoin( + receiptItems, + receiptItems.rawMaterialId.equalsExp(rawMaterials.localId), + ), + innerJoin( + outletInventories, + outletInventories.itemType.equals(ItemType.raw_material.type) & + outletInventories.itemId.equalsExp(rawMaterials.localId), + ), + ]) + ..addColumns([ + rawMaterials.localId, + rawMaterials.name, + units.name, + outletInventories.stock, + outletInventories.purchasePrice, + receiptItems.deletedAt, + ]) + ..orderBy([OrderingTerm.desc(rawMaterials.createdAt)]) + ..where(rawMaterials.deletedAt.isNull()) + ..where( + outletInventories.outletId.equals(outletId!) & + outletInventories.deletedAt.isNull(), + ); + + yield* query.watch().map((rows) { + final Map groupedMap = {}; + final Map countProductUsages = {}; + + for (final row in rows) { + final String id = row.read(rawMaterials.localId)!; + final String name = row.read(rawMaterials.name) ?? ''; + final String remainingStock = + "${(row.read(outletInventories.stock) ?? 0.0).toStringWithoutTrailingZero()} ${row.read(units.name) ?? ''}"; + final String displayPurchasePrice = CurrencyFormat.formatToIdr( + (row.read(outletInventories.purchasePrice) ?? 0.0), + 0, + ); + + final deletedAt = row.read(receiptItems.deletedAt); + + countProductUsages.update( + id, + (value) => value + (deletedAt == null ? 1 : 0), + ifAbsent: () => deletedAt == null ? 1 : 0, + ); + + final rawMaterial = RawMaterial( + id: id, + name: name, + remainingStock: remainingStock, + displayPurchasePrice: displayPurchasePrice, + countProductUsage: countProductUsages[id]!, + ); + + groupedMap.update( + id, + (value) => + rawMaterial.copyWith(countProductUsage: countProductUsages[id]), + ifAbsent: () => rawMaterial, + ); + } + + return groupedMap.values.toList(); + }); + } + + Future getRawMaterialById( + String id, + bool isEditing, + ) async { + final outletId = await SystemSetting.getCurrentOutletId(); + final materialQuery = + selectOnly(rawMaterials).join([ + innerJoin(units, units.localId.equalsExp(rawMaterials.unitId)), + innerJoin( + outletInventories, + outletInventories.itemType.equals("raw_material") & + outletInventories.itemId.equalsExp(rawMaterials.localId), + ), + ]) + ..addColumns([ + rawMaterials.localId, + rawMaterials.name, + rawMaterials.unitId, + outletInventories.localId, + outletInventories.purchasePrice, + outletInventories.stock, + ]) + ..where(rawMaterials.localId.equals(id)) + ..where(rawMaterials.deletedAt.isNull()) + ..where( + (isEditing + ? outletInventories.outletId.equals(outletId!) + : const Constant(true)) & + outletInventories.deletedAt.isNull(), + ) + ..limit(1); + + final materialRow = await materialQuery.getSingle(); + + final String materialId = materialRow.read(rawMaterials.localId) ?? ''; + final String name = materialRow.read(rawMaterials.name) ?? ''; + final String unitId = materialRow.read(rawMaterials.unitId) ?? ''; + final String outletInventoryId = + materialRow.read(outletInventories.localId) ?? ''; + final double purchasePrice = + materialRow.read(outletInventories.purchasePrice) ?? 0.0; + final double stock = materialRow.read(outletInventories.stock) ?? 0.0; + + final receiptItemQuery = + selectOnly(receiptItems).join([ + innerJoin( + productVariants, + productVariants.localId.equalsExp(receiptItems.productVariantId), + ), + ]) + ..addColumns([ + receiptItems.localId, + receiptItems.productVariantId, + receiptItems.quantity, + ]) + ..where( + receiptItems.rawMaterialId.equals(id) & + receiptItems.deletedAt.isNull(), + ); + + final receiptItemRows = await receiptItemQuery.get(); + + final List productUsages = receiptItemRows + .map((row) { + final String receiptItemId = row.read(receiptItems.localId) ?? ''; + final String productUsageId = + row.read(receiptItems.productVariantId) ?? ''; + final double quantity = row.read(receiptItems.quantity) ?? 0.0; + + return SelectedProductUsageMaterial( + receiptItemId: receiptItemId, + productUsageId: productUsageId, + quantity: quantity, + ); + }) + .toList(); + + return RawMaterialDetail( + id: materialId, + name: name, + purchasePrice: purchasePrice, + stock: stock, + unitId: unitId, + outletInventoryId: outletInventoryId, + productUsages: productUsages, + ); + } + + Stream> watchAllProductUsageMaterials( + bool isEditing, + ) async* { + final outletId = await SystemSetting.getCurrentOutletId(); + final query = + selectOnly(products).join([ + innerJoin( + productVariants, + productVariants.productId.equalsExp(products.localId), + ), + innerJoin( + outletInventories, + outletInventories.itemType.equals(ItemType.product_variant.type) & + outletInventories.itemId.equalsExp(productVariants.localId), + ), + ]) + ..addColumns([ + products.localId, + products.name, + products.localImagePath, + products.hasVariant, + products.deletedAt, + productVariants.localId, + productVariants.isProductVariant, + productVariants.name, + productVariants.deletedAt, + outletInventories.deletedAt, + ]) + ..orderBy([OrderingTerm.desc(products.createdAt)]) + ..where( + isEditing + ? outletInventories.outletId.equals(outletId!) + : outletInventories.outletId.equals(outletId!).not(), + ); + + yield* query.watch().map((rows) { + final Map groupedMap = {}; + + for (final row in rows) { + final productId = row.read(products.localId)!; + + final dynamic rawImages = row.read(products.localImagePath); + + List productImages = []; + + if (rawImages is String) { + try { + productImages = (jsonDecode(rawImages) as List).cast(); + } catch (e) { + productImages = []; + } + } else if (rawImages is List) { + productImages = (rawImages).cast(); + } + + final productDeletedAt = row.read(products.deletedAt); + + final variantDeletedAt = row.read(productVariants.deletedAt); + + final outletInventoryDeletedAt = row.read(outletInventories.deletedAt); + + final productHasBeenDelete = productDeletedAt != null; + + final variantHasBeenDelete = + (variantDeletedAt != null) || (outletInventoryDeletedAt != null); + + groupedMap.putIfAbsent( + productId, + () => ProductUsageMaterial( + id: productId, + name: row.read(products.name) ?? '', + imagePath: productImages.lastOrNull, + hasVariant: row.read(products.hasVariant) ?? false, + variants: [], + hasBeenDelete: productHasBeenDelete, + ), + ); + + groupedMap[productId]!.variants.add( + VariantUsageMaterial( + id: row.read(productVariants.localId)!, + name: row.read(productVariants.name) ?? '', + isProductVariant: + row.read(productVariants.isProductVariant) ?? false, + hasBeenDelete: variantHasBeenDelete, + ), + ); + } + + return groupedMap.values.toList(); + }); + } + + Future getRawMaterialSnapshotById( + String id, + ) async { + final receiptItemQuery = + selectOnly(receiptItems).join([ + innerJoin( + productVariants, + productVariants.localId.equalsExp(receiptItems.productVariantId), + ), + innerJoin( + rawMaterials, + rawMaterials.localId.equalsExp(receiptItems.rawMaterialId), + ), + ]) + ..addColumns([ + receiptItems.localId.count(), + receiptItems.rawMaterialId, + rawMaterials.name, + ]) + ..groupBy([receiptItems.rawMaterialId]) + ..where( + receiptItems.rawMaterialId.equals(id) & + receiptItems.deletedAt.isNull(), + ) + ..where(productVariants.deletedAt.isNull()) + ..where(rawMaterials.deletedAt.isNull()); + + final receiptItemRow = await receiptItemQuery.getSingle(); + + final String rawMaterialId = + receiptItemRow.read(receiptItems.rawMaterialId) ?? ''; + final String name = receiptItemRow.read(rawMaterials.name) ?? ''; + final int totalProductUsage = + receiptItemRow.read(receiptItems.localId.count()) ?? 0; + + final outletQuery = + selectOnly(outlets).join([ + innerJoin( + outletInventories, + outletInventories.outletId.equalsExp(outlets.localId), + ), + ]) + ..addColumns([outlets.localId, outlets.name, outlets.isMainOutlet]) + ..where( + outletInventories.itemType.equals("raw_material") & + outletInventories.itemId.equals(id), + ) + ..where(outlets.deletedAt.isNull()) + ..where(outletInventories.deletedAt.isNull()); + + final outletRows = await outletQuery.get(); + + final List outletList = outletRows.map((row) { + final String id = row.read(outlets.localId) ?? ''; + final String name = row.read(outlets.name) ?? ''; + final bool isMainOutlet = row.read(outlets.isMainOutlet) ?? false; + return Outlet( + id: id, + name: name, + // email: '', + // phoneNumber: '', + isMainOutlet: isMainOutlet, + ); + }).toList(); + + final outletId = await SystemSetting.getCurrentOutletId(); + + final outlet = outletList.where((outlet) => outlet.id == outletId).first; + + return DeleteRawMaterialSnapshot( + rawMaterialId: rawMaterialId, + name: name, + totalProductUsage: totalProductUsage, + currentOutletName: outlet.name, + isMainOutlet: outlet.isMainOutlet, + totalOutletCount: outletList.length, + ); + } + + Future checkAlreadyRawMaterialByOutlet(String id) async { + final outletId = await SystemSetting.getCurrentOutletId(); + + final query = + selectOnly(rawMaterials).join([ + innerJoin( + outletInventories, + outletInventories.itemType.equals(ItemType.raw_material.type) & + outletInventories.itemId.equalsExp(rawMaterials.localId), + ), + ]) + ..addColumns([rawMaterials.localId]) + ..where( + rawMaterials.localId.equals(id) & rawMaterials.deletedAt.isNull(), + ) + ..where( + outletInventories.outletId.equals(outletId!) & + outletInventories.deletedAt.isNull(), + ) + ..limit(1); + + final result = await query.getSingleOrNull(); + + return result != null; + } + + Stream> watchAllRawMaterialChoices() async* { + final query = + selectOnly(rawMaterials).join([ + innerJoin( + receiptItems, + receiptItems.rawMaterialId.equalsExp(rawMaterials.localId), + ), + ]) + ..addColumns([ + rawMaterials.localId, + rawMaterials.name, + receiptItems.localId.count(), + ]) + ..groupBy([rawMaterials.localId]) + ..where(rawMaterials.deletedAt.isNull()) + ..where(receiptItems.deletedAt.isNull()); + + yield* query.watch().map((rows) { + return rows.map((row) { + return RawMaterialChoice( + id: row.read(rawMaterials.localId)!, + name: row.read(rawMaterials.name) ?? '', + countProductUsage: row.read(receiptItems.localId.count()) ?? 0, + ); + }).toList(); + }); + } +} diff --git a/Mobile Operasional/lib/core/database/daos/raw_material_dao.g.dart b/Mobile Operasional/lib/core/database/daos/raw_material_dao.g.dart new file mode 100644 index 0000000..e8fcc8d --- /dev/null +++ b/Mobile Operasional/lib/core/database/daos/raw_material_dao.g.dart @@ -0,0 +1,44 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'raw_material_dao.dart'; + +// ignore_for_file: type=lint +mixin _$RawMaterialDaoMixin on DatabaseAccessor { + $UnitsTable get units => attachedDatabase.units; + $RawMaterialsTable get rawMaterials => attachedDatabase.rawMaterials; + $CategoriesTable get categories => attachedDatabase.categories; + $ProductsTable get products => attachedDatabase.products; + $ProductVariantsTable get productVariants => attachedDatabase.productVariants; + $ReceiptItemsTable get receiptItems => attachedDatabase.receiptItems; + $OutletsTable get outlets => attachedDatabase.outlets; + $OutletInventoriesTable get outletInventories => + attachedDatabase.outletInventories; + RawMaterialDaoManager get managers => RawMaterialDaoManager(this); +} + +class RawMaterialDaoManager { + final _$RawMaterialDaoMixin _db; + RawMaterialDaoManager(this._db); + $$UnitsTableTableManager get units => + $$UnitsTableTableManager(_db.attachedDatabase, _db.units); + $$RawMaterialsTableTableManager get rawMaterials => + $$RawMaterialsTableTableManager(_db.attachedDatabase, _db.rawMaterials); + $$CategoriesTableTableManager get categories => + $$CategoriesTableTableManager(_db.attachedDatabase, _db.categories); + $$ProductsTableTableManager get products => + $$ProductsTableTableManager(_db.attachedDatabase, _db.products); + $$ProductVariantsTableTableManager get productVariants => + $$ProductVariantsTableTableManager( + _db.attachedDatabase, + _db.productVariants, + ); + $$ReceiptItemsTableTableManager get receiptItems => + $$ReceiptItemsTableTableManager(_db.attachedDatabase, _db.receiptItems); + $$OutletsTableTableManager get outlets => + $$OutletsTableTableManager(_db.attachedDatabase, _db.outlets); + $$OutletInventoriesTableTableManager get outletInventories => + $$OutletInventoriesTableTableManager( + _db.attachedDatabase, + _db.outletInventories, + ); +} diff --git a/Mobile Operasional/lib/core/database/daos/receipt_item_dao.dart b/Mobile Operasional/lib/core/database/daos/receipt_item_dao.dart new file mode 100644 index 0000000..19c85f1 --- /dev/null +++ b/Mobile Operasional/lib/core/database/daos/receipt_item_dao.dart @@ -0,0 +1,10 @@ +import 'package:drift/drift.dart'; +import 'package:niogu_app/core/database/app_database.dart'; +import 'package:niogu_app/core/database/tables/receipt_items.dart'; + +part 'receipt_item_dao.g.dart'; + +@DriftAccessor(tables: [ReceiptItems]) +class ReceiptItemDao extends DatabaseAccessor with _$ReceiptItemDaoMixin { + ReceiptItemDao(AppDatabase db) : super(db); +} \ No newline at end of file diff --git a/Mobile Operasional/lib/core/database/daos/receipt_item_dao.g.dart b/Mobile Operasional/lib/core/database/daos/receipt_item_dao.g.dart new file mode 100644 index 0000000..20bcd65 --- /dev/null +++ b/Mobile Operasional/lib/core/database/daos/receipt_item_dao.g.dart @@ -0,0 +1,34 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'receipt_item_dao.dart'; + +// ignore_for_file: type=lint +mixin _$ReceiptItemDaoMixin on DatabaseAccessor { + $CategoriesTable get categories => attachedDatabase.categories; + $UnitsTable get units => attachedDatabase.units; + $ProductsTable get products => attachedDatabase.products; + $ProductVariantsTable get productVariants => attachedDatabase.productVariants; + $RawMaterialsTable get rawMaterials => attachedDatabase.rawMaterials; + $ReceiptItemsTable get receiptItems => attachedDatabase.receiptItems; + ReceiptItemDaoManager get managers => ReceiptItemDaoManager(this); +} + +class ReceiptItemDaoManager { + final _$ReceiptItemDaoMixin _db; + ReceiptItemDaoManager(this._db); + $$CategoriesTableTableManager get categories => + $$CategoriesTableTableManager(_db.attachedDatabase, _db.categories); + $$UnitsTableTableManager get units => + $$UnitsTableTableManager(_db.attachedDatabase, _db.units); + $$ProductsTableTableManager get products => + $$ProductsTableTableManager(_db.attachedDatabase, _db.products); + $$ProductVariantsTableTableManager get productVariants => + $$ProductVariantsTableTableManager( + _db.attachedDatabase, + _db.productVariants, + ); + $$RawMaterialsTableTableManager get rawMaterials => + $$RawMaterialsTableTableManager(_db.attachedDatabase, _db.rawMaterials); + $$ReceiptItemsTableTableManager get receiptItems => + $$ReceiptItemsTableTableManager(_db.attachedDatabase, _db.receiptItems); +} diff --git a/Mobile Operasional/lib/core/database/daos/stock_card_dao.dart b/Mobile Operasional/lib/core/database/daos/stock_card_dao.dart new file mode 100644 index 0000000..117898f --- /dev/null +++ b/Mobile Operasional/lib/core/database/daos/stock_card_dao.dart @@ -0,0 +1,980 @@ +import 'dart:convert'; +import 'dart:math'; + +import 'package:drift/drift.dart'; +import 'package:intl/intl.dart'; +import 'package:niogu_app/core/database/app_database.dart'; +import 'package:niogu_app/core/database/tables/orders.dart'; +import 'package:niogu_app/core/database/tables/outlet_inventories.dart'; +import 'package:niogu_app/core/database/tables/outlets.dart'; +import 'package:niogu_app/core/database/tables/product_variants.dart'; +import 'package:niogu_app/core/database/tables/products.dart'; +import 'package:niogu_app/core/database/tables/purchase_items.dart'; +import 'package:niogu_app/core/database/tables/purchases.dart'; +import 'package:niogu_app/core/database/tables/raw_materials.dart'; +import 'package:niogu_app/core/database/tables/stock_cards.dart'; +import 'package:niogu_app/core/database/tables/sync_queues.dart'; +import 'package:niogu_app/core/database/tables/transactions.dart'; +import 'package:niogu_app/core/database/tables/units.dart'; +import 'package:niogu_app/core/database/tables/users.dart'; +import 'package:niogu_app/core/entities/custom_sync_queue.dart'; +import 'package:niogu_app/core/enums/table_type.dart'; +import 'package:niogu_app/core/utils/extension_format.dart'; +import 'package:niogu_app/core/enums/item_type.dart'; +import 'package:niogu_app/core/enums/stock_card_type.dart'; +import 'package:niogu_app/core/enums/stock_type.dart'; +import 'package:niogu_app/core/enums/sync_status.dart'; +import 'package:niogu_app/core/system/system_setting.dart'; +import 'package:niogu_app/core/enums/transaction_reference_type.dart'; +import 'package:niogu_app/core/utils/log_message.dart'; +import 'package:niogu_app/features/report/stock_report/domain/entities/stok_report.dart'; +import 'package:niogu_app/features/stock_in/domain/entities/stock_in.dart'; +import 'package:uuid/uuid.dart'; + +part 'stock_card_dao.g.dart'; + +@DriftAccessor( + tables: [ + Outlets, + Users, + Units, + Products, + ProductVariants, + RawMaterials, + Orders, + Purchases, + PurchaseItems, + StockCards, + OutletInventories, + Transactions, + SyncQueues, + ], +) +class StockCardDao extends DatabaseAccessor + with _$StockCardDaoMixin { + StockCardDao(AppDatabase db) : super(db); + + Future _getModeBoundary(String id) async { + final lastModeChange = + await (select(stockCards) + ..where( + (t) => + t.outletInventoryId.equals(id) & t.modeChange.equals(true), + ) + ..orderBy([ + (t) => OrderingTerm.desc(t.createdAt), + (t) => OrderingTerm.desc(t.localId), + ]) + ..limit(1)) + .getSingleOrNull(); + + return lastModeChange?.createdAt; + } + + bool _isBetween(DateTime date, DateTime startDate, DateTime endDate) { + return date.isAfter(startDate) && + date.isBefore(endDate.add(const Duration(days: 1))); + } + + Stream> watchAllStockReports({ + String? outletId, + ItemType? itemType, + DateTime? startDate, + DateTime? endDate, + }) { + final bool hasFilter = startDate != null && endDate != null; + + final productUnits = alias(units, 'product_units'); + + final rawMaterialUnits = alias(units, 'raw_material_units'); + + final query = + selectOnly(outletInventories).join([ + leftOuterJoin( + stockCards, + outletInventories.localId.equalsExp(stockCards.outletInventoryId), + ), + leftOuterJoin( + productVariants, + outletInventories.itemType.equals(ItemType.product_variant.type) & + outletInventories.itemId.equalsExp(productVariants.localId), + ), + leftOuterJoin( + products, + productVariants.productId.equalsExp(products.localId), + ), + leftOuterJoin( + rawMaterials, + outletInventories.itemType.equals(ItemType.raw_material.type) & + outletInventories.itemId.equalsExp(rawMaterials.localId), + ), + leftOuterJoin( + productUnits, + products.unitId.equalsExp(productUnits.localId), + ), + leftOuterJoin( + rawMaterialUnits, + rawMaterials.unitId.equalsExp(rawMaterialUnits.localId), + ), + ]) + ..addColumns([ + outletInventories.localId, + outletInventories.itemType, + outletInventories.stockType, + outletInventories.stock, + products.localId, + products.name, + products.hasVariant, + products.localImagePath, + productUnits.name, + productVariants.name, + productVariants.localImagePath, + productVariants.isProductVariant, + rawMaterials.name, + rawMaterialUnits.name, + stockCards.type, + stockCards.qtyChange, + stockCards.stockType, + stockCards.createdAt, + outletInventories.isDeleteByOwner, + outletInventories.deletedAt, + ]) + ..orderBy([OrderingTerm.asc(outletInventories.deletedAt)]) + ..where( + outletId != null + ? outletInventories.outletId.equals(outletId) + : const Constant(true), + ) + ..where( + itemType != null + ? outletInventories.itemType.equals(itemType.type) + : const Constant(true), + ); + + return query.watch().asyncMap((rows) async { + final Map groupedMap = {}; + + final Map> movementTemps = {}; + + final inventoryIds = rows + .map((row) => row.read(outletInventories.localId)!) + .toList(); + + Map initialStockMap = {}; + + if (hasFilter && inventoryIds.isNotEmpty) { + for (final invId in inventoryIds) { + final lastCardBefore = + await (select(stockCards) + ..where( + (t) => + t.outletInventoryId.equals(invId) & + t.createdAt.isSmallerThanValue(startDate), + ) + ..orderBy([ + (t) => OrderingTerm.desc(t.createdAt), + (t) => OrderingTerm.desc(t.localId), + ]) + ..limit(1)) + .getSingleOrNull(); + + initialStockMap[invId] = lastCardBefore?.currentStock ?? 0.0; + } + } + + for (final row in rows) { + final invId = row.read(outletInventories.localId)!; + + final itemType = ItemType.values.byName( + (row.read(outletInventories.itemType) ?? ''), + ); + + final isDeleteByOwner = + row.read(outletInventories.isDeleteByOwner) ?? false; + + final bool hasBeenDelete = + row.read(outletInventories.deletedAt) != null; + + final String name = itemType == ItemType.product_variant + ? row.read(products.name) ?? '' + : row.read(rawMaterials.name) ?? ''; + + String? variantName; + + List productImages = []; + + if (itemType == ItemType.product_variant) { + final hasVariant = row.read(products.hasVariant) ?? false; + + final isProductVariant = + row.read(productVariants.isProductVariant) ?? false; + + final dynamic rawImages = row.read(products.localImagePath); + + if ((hasVariant && isProductVariant) || + (hasBeenDelete & !hasVariant && isProductVariant)) + variantName = row.read(productVariants.name); + + if (hasVariant && isProductVariant && !hasBeenDelete) { + final String imagePath = + row.read(productVariants.localImagePath) ?? ''; + productImages.add(imagePath); + } else if (!hasVariant && !isProductVariant && !hasBeenDelete) { + if (rawImages is String) { + productImages = (jsonDecode(rawImages) as List).cast(); + } else if (rawImages is List) { + productImages = (rawImages).cast(); + } + } + } + + String? localImagePath; + + if (productImages.isNotEmpty && productImages.last.isNotEmpty) { + localImagePath = productImages.last; + } + + final currentStockType = StockType.values.byName( + (row.read(outletInventories.stockType) ?? ''), + ); + + final type = row.read(stockCards.type) ?? ''; + + final StockCardType stockCardType = StockCardType.values.byName(type); + + final qtyChange = row.read(stockCards.qtyChange) ?? 0.0; + + final stockType = StockType.values.byName( + (row.read(stockCards.stockType) ?? ''), + ); + + final bool isUnlimited = currentStockType == StockType.unlimited; + + final masterStock = row.read(outletInventories.stock) ?? 0.0; + + final unit = itemType == ItemType.product_variant + ? (row.read(productUnits.name) ?? '') + : (row.read(rawMaterialUnits.name) ?? ''); + + final createdAt = row.read(stockCards.createdAt) ?? DateTime.now(); + + movementTemps.putIfAbsent(invId, () => []); + + movementTemps[invId]!.add( + MovementTemp( + type: stockCardType, + initialStock: masterStock, + qty: qtyChange, + date: createdAt, + stockType: stockType, + ), + ); + + groupedMap.putIfAbsent( + invId, + () => StockReportItem( + id: invId, + itemType: itemType, + name: name, + variantName: variantName, + imageProduct: localImagePath, + isUnlimited: isUnlimited, + initialStock: 0.0, + totalStockIn: 0.0, + totalStockOut: 0.0, + lastStock: 0.0, + unit: unit, + isDeleteByOwner: isDeleteByOwner, + hasBeenDelete: hasBeenDelete, + ), + ); + } + + final Map initialStockMaps = {}; + + final Map totalStockInMaps = {}; + + final Map totalStockOutMaps = {}; + + final Map lastStockMaps = {}; + + for (final entry in movementTemps.entries) { + final invId = entry.key; + + final items = entry.value; + + final boundary = await _getModeBoundary(invId); + + double initialStock = 0.0; + + double totalStockIn = 0.0; + + double totalStockOut = 0.0; + + double lastStock = 0.0; + + for (final item in items) { + final stockType = item.stockType; + + final qtyChange = item.qty; + + final createdAt = item.date; + + final type = item.type; + + final inRange = hasFilter && boundary != null + ? createdAt.isAfter(boundary) && + _isBetween(createdAt, startDate, endDate) + : hasFilter && boundary == null + ? _isBetween(createdAt, startDate, endDate) + : !hasFilter && boundary != null + ? createdAt.isAfter(boundary) + : true; + + final conditions = [StockCardType.sale, StockCardType.adjustment_out]; + + bool incomingCondition = inRange && !conditions.contains(type); + + bool outgoingCondition = inRange && conditions.contains(type); + + bool initialStockFilter = false; + if (hasFilter) { + initialStockFilter = + type == StockCardType.initial_stock && + createdAt.isBefore(startDate); + + incomingCondition = + incomingCondition && + stockType == StockType.fixed && + _isBetween(createdAt, startDate, endDate) && + !initialStockFilter; + + outgoingCondition = + outgoingCondition && _isBetween(createdAt, startDate, endDate); + } + if (incomingCondition) { + totalStockIn += qtyChange; + } + + if (outgoingCondition) { + totalStockOut += qtyChange; + } + + if (hasFilter) { + initialStock = initialStockMap[invId] ?? 0.0; + + lastStock = initialStock + totalStockIn; + + if (stockType == StockType.fixed) { + lastStock = lastStock - totalStockOut; + } + } else { + lastStock = item.initialStock; + } + } + + initialStockMaps[invId] = initialStock; + + totalStockInMaps[invId] = totalStockIn; + + totalStockOutMaps[invId] = totalStockOut; + + lastStockMaps[invId] = lastStock; + } + + for (final entry in groupedMap.entries) { + final id = entry.key; + + final value = entry.value; + + groupedMap[id] = value.copyWith( + initialStock: initialStockMaps[id], + totalStockIn: totalStockInMaps[id], + totalStockOut: totalStockOutMaps[id], + lastStock: lastStockMaps[id], + ); + } + + return groupedMap.values.toList(); + }); + } + + Future getStockReportById({ + required String id, + required ItemType itemType, + DateTime? startDate, + DateTime? endDate, + }) async { + final bool hasFilter = startDate != null && endDate != null; + + final List joins = [ + innerJoin( + outletInventories, + stockCards.outletInventoryId.equalsExp(outletInventories.localId), + ), + innerJoin(outlets, outlets.localId.equalsExp(outletInventories.outletId)), + innerJoin(users, users.localId.equalsExp(stockCards.userId)), + ]; + + final List columns = []; + + if (itemType == ItemType.product_variant) { + joins.addAll([ + innerJoin( + productVariants, + outletInventories.itemType.equals(ItemType.product_variant.type) & + outletInventories.itemId.equalsExp(productVariants.localId), + ), + innerJoin( + products, + productVariants.productId.equalsExp(products.localId), + ), + innerJoin(units, products.unitId.equalsExp(units.localId)), + ]); + columns.addAll([ + productVariants.name, + products.name, + products.hasVariant, + productVariants.isProductVariant, + outletInventories.deletedAt, + ]); + } else { + joins.addAll([ + innerJoin( + rawMaterials, + outletInventories.itemType.equals(ItemType.raw_material.type) & + outletInventories.itemId.equalsExp(rawMaterials.localId), + ), + innerJoin(units, rawMaterials.unitId.equalsExp(units.localId)), + ]); + columns.addAll([rawMaterials.name]); + } + + final boundary = await _getModeBoundary(id); + + final query = + selectOnly(stockCards).join([ + ...joins, + leftOuterJoin( + orders, + stockCards.type.equals(StockCardType.sale.type) & + stockCards.referenceId.equalsExp(orders.localId), + ), + leftOuterJoin( + purchases, + stockCards.type.equals(StockCardType.purchase.type) & + stockCards.referenceId.equalsExp(purchases.localId), + ), + ]) + ..addColumns([ + outletInventories.localId, + outletInventories.stockType, + outletInventories.stock, + outlets.name, + stockCards.type, + stockCards.qtyChange, + stockCards.currentStock, + stockCards.stockType, + stockCards.modeChange, + stockCards.modeBeforeChange, + stockCards.modeAfterChange, + stockCards.referenceId, + orders.orderNumber, + purchases.purchaseNumber, + stockCards.documentNumber, + stockCards.notes, + stockCards.createdAt, + users.name, + units.name, + ...columns, + ]) + ..orderBy([OrderingTerm.asc(stockCards.createdAt)]) + ..where(stockCards.outletInventoryId.equals(id)); + + final results = await query.get(); + + final Map groupedMap = {}; + + Map initialStockMap = {}; + + if (hasFilter && results.isNotEmpty) { + final invId = results.first.read(outletInventories.localId)!; + + final lastCardBefore = + await (select(stockCards) + ..where( + (t) => + t.outletInventoryId.equals(invId) & + t.createdAt.isSmallerThanValue(startDate), + ) + ..orderBy([ + (t) => OrderingTerm.desc(t.createdAt), + (t) => OrderingTerm.desc(t.localId), + ]) + ..limit(1)) + .getSingleOrNull(); + + initialStockMap[invId] = lastCardBefore?.currentStock ?? 0.0; + } + + for (final row in results) { + final invId = row.read(outletInventories.localId)!; + + String itemName = ''; + + String? variantName; + + if (itemType == ItemType.product_variant) { + itemName = (row.read(products.name) ?? ''); + + final hasVariant = row.read(products.hasVariant) ?? false; + + final isProductVariant = + row.read(productVariants.isProductVariant) ?? false; + + final bool hasBeenDelete = + row.read(outletInventories.deletedAt) != null; + + if ((hasVariant && isProductVariant) || + (hasBeenDelete & !hasVariant && isProductVariant)) + variantName = row.read(productVariants.name); + } else { + itemName = (row.read(rawMaterials.name) ?? ''); + } + + final isUnlimited = + (row.read(outletInventories.stockType) ?? '') == 'unlimited'; + + final initialStock = hasFilter ? (initialStockMap[invId] ?? 0.0) : 0.0; + + final lastStock = row.read(outletInventories.stock) ?? 0.0; + + final createdAt = row.read(stockCards.createdAt)!; + + groupedMap.putIfAbsent( + invId, + () => StockReportDetail( + itemName: itemName, + variantName: variantName, + isUnlimited: isUnlimited, + outletName: row.read(outlets.name) ?? '', + startDate: startDate, + endDate: endDate, + initialStock: initialStock, + totalStockIn: 0, + totalStockOut: 0, + lastStock: lastStock, + unit: row.read(units.name) ?? '', + createdAt: createdAt, + movements: [], + ), + ); + + final type = row.read(stockCards.type) ?? ''; + + final qtyChange = row.read(stockCards.qtyChange) ?? 0.0; + + String description = ''; + + double stockIn = 0; + + double stockOut = 0; + + bool isStockIn = false; + + final isMovementUnlimited = + (row.read(stockCards.stockType) ?? '') == 'unlimited'; + + switch (type) { + case 'initial_stock': + case 'production': + case 'purchase': + case 'transfer_in': + case 'adjustment_in': + stockIn = qtyChange; + isStockIn = true; + break; + + case 'sale': + case 'adjustment_out': + stockOut = qtyChange; + isStockIn = false; + break; + } + + final documentNumber = row.read(stockCards.documentNumber); + + switch (type) { + case 'initial_stock': + description = 'Pembuatan Stok Baru'; + break; + case 'sale': + final orderNumber = row.read(orders.orderNumber); + description = + 'Penjualan ${orderNumber != null ? '(${orderNumber.toUniqueNumberCleaner()})' : ''}'; + break; + case 'production': + description = 'Produksi'; + break; + case 'purchase': + final purchaseNumber = row.read(purchases.purchaseNumber); + description = + 'Pembelian ${purchaseNumber != null ? '(${purchaseNumber.toUniqueNumberCleaner()})' : ''}'; + break; + case 'transfer_in': + description = + 'Transfer Masuk ${documentNumber != null && documentNumber.isNotEmpty ? '(Ref : $documentNumber)' : ''}'; + break; + case 'adjustment_in': + description = 'Penyesuaian Masuk'; + break; + case 'adjustment_out': + description = 'Penyesuaian Keluar'; + break; + default: + description = ''; + } + + final notes = row.read(stockCards.notes); + + final stockCardType = StockCardType.values.byName(type); + + final referenceId = row.read(stockCards.referenceId); + + final modeChange = row.read(stockCards.modeChange) ?? false; + + String? beforeChange = row.read(stockCards.modeBeforeChange); + + if (beforeChange != null) { + beforeChange = beforeChange == 'unlimited' ? 'Selalu Ada' : 'Tetap'; + } + + String? afterChange = row.read(stockCards.modeAfterChange); + + if (afterChange != null) { + afterChange = afterChange == 'unlimited' ? 'Selalu Ada' : 'Tetap'; + } + + groupedMap[invId]!.movements.add( + StockMovement( + date: createdAt, + description: description, + notes: notes, + userName: row.read(users.name) ?? '', + type: stockCardType, + referenceId: referenceId, + isStockIn: isStockIn, + isUnlimited: isMovementUnlimited, + modeChange: modeChange, + beforeChange: beforeChange, + afterChange: afterChange, + stockIn: stockIn, + stockOut: stockOut, + remainingStock: row.read(stockCards.currentStock) ?? 0, + ), + ); + } + + for (final entry in groupedMap.entries) { + final invId = entry.key; + + final detail = entry.value; + + double totalIn = 0; + + double totalOut = 0; + + for (final m in detail.movements) { + final inRange = hasFilter && boundary != null + ? m.date.isAfter(boundary) && _isBetween(m.date, startDate, endDate) + : hasFilter && boundary == null + ? _isBetween(m.date, startDate, endDate) + : !hasFilter && boundary != null + ? m.date.isAfter(boundary) + : true; + + if (inRange) { + totalIn += m.stockIn; + totalOut += m.stockOut; + } + } + + final lastStock = hasFilter + ? detail.initialStock + totalIn - totalOut + : detail.lastStock; + + groupedMap[invId] = detail.copyWith( + totalStockIn: totalIn, + totalStockOut: totalOut, + lastStock: lastStock, + ); + } + + return groupedMap.values.single; + } + + Future _generatePurchaseNumber() async { + final now = DateTime.now(); + + final formatter = DateFormat('yyMMdd'); + + final dateCode = formatter.format(now); + + final tenantCode = await SystemSetting.getTenantCode(); + + final chars = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789'; + + final randomPart = List.generate( + 3, + (index) => chars[Random().nextInt(chars.length)], + ).join(); + + final prefix = 'PO-$tenantCode-$dateCode-'; + + final lastPurchase = + await (select(purchases) + ..where((t) => t.purchaseNumber.like('$prefix%')) + ..orderBy([(t) => OrderingTerm.desc(t.purchaseNumber)]) + ..limit(1)) + .getSingleOrNull(); + + int nextSequence = 1; + + if (lastPurchase != null) { + final parts = lastPurchase.purchaseNumber.split('-'); + if (parts.isNotEmpty) { + final lastPart = parts.last; + if (lastPart.length >= 5) { + final lastSeqStr = lastPart.substring(lastPart.length - 5); + nextSequence = int.parse(lastSeqStr) + 1; + } + } + } + + // Format baru: PO - [TenantCode] - [Tanggal] - [Random3] [Urutan5] + // Contoh: PO-ZL6B25KQ-260328-XYZ00001 + final finalSequence = + '$randomPart${nextSequence.toString().padLeft(5, '0')}'; + + return '$prefix$finalSequence'; + } + + Future updateStockItem({ + SuppliersCompanion? supplier, + PurchasesCompanion? purchase, + required List purchaseItem, + required List items, + }) async { + try { + await transaction(() async { + final now = DateTime.now(); + + final currentUserId = await SystemSetting.getCurrentUserId(); + + await batch((batch) async { + if (supplier != null) { + batch.insert(suppliers, supplier); + + CustomSyncQueue.addToSyncQueue( + type: TableType.suppliers, + id: supplier.localId.value, + action: SyncAction.create, + batch: batch, + syncQueues: syncQueues, + data: { + 'uuid': supplier.localId.value, + 'name': supplier.name.value, + 'phone_number': supplier.phoneNumber.value, + 'created_at': now.toIso8601String(), + 'updated_at': now.toIso8601String(), + }, + ); + } + + if (purchase != null) { + final purchaseNumber = await _generatePurchaseNumber(); + + batch.insert( + purchases, + purchase.copyWith(purchaseNumber: Value(purchaseNumber)), + ); + + CustomSyncQueue.addToSyncQueue( + type: TableType.purchases, + id: purchase.localId.value, + action: SyncAction.create, + batch: batch, + syncQueues: syncQueues, + data: { + 'uuid': purchase.localId.value, + 'outlet_id': purchase.outletId.value, + 'outlet_name_snapshot': purchase.outletNameSnapshot.value, + 'purchase_number': purchaseNumber, + 'supplier_id': purchase.supplierId.value, + 'supplier_name_snapshot': purchase.supplierNameSnapshot.value, + 'supplier_phone_number_snapshot': + purchase.supplierPhoneNumberSnapshot.value, + 'user_id': purchase.userId.value, + 'admin_name_snapshot': purchase.adminNameSnapshot.value, + 'purchase_status': purchase.purchaseStatus.value, + 'total_purchase': purchase.totalPurchase.value, + 'discount': purchase.discount.value, + 'tax': purchase.tax.value, + 'payment_status': purchase.paymentStatus.value, + 'total_amount': purchase.totalAmount.value, + 'amount_paid': purchase.amountPaid.value, + 'change_amount': purchase.changeAmount.value, + 'under_payment_amount': purchase.underPaymentAmount.value, + 'debt_amount': purchase.debtAmount.value, + 'payment_proof_url': purchase.paymentProofPath.value, + 'notes': purchase.notes.value, + 'created_at': now.toIso8601String(), + 'updated_at': now.toIso8601String(), + }, + ); + + final transaction = TransactionsCompanion( + localId: Value(const Uuid().v7().toString()), + outletId: purchase.outletId, + userId: Value(currentUserId!), + referenceType: Value(TransactionReferenceType.purchases.type), + referenceId: purchase.localId, + referenceNumber: Value(purchaseNumber), + stakeholder: purchase.supplierNameSnapshot, + transactionType: Value(TransactionType.expense.type), + category: Value("Pembelian Barang"), + amount: purchase.totalAmount, + note: purchase.notes, + ); + + batch.insert(transactions, transaction); + + CustomSyncQueue.addToSyncQueue( + type: TableType.transactions, + id: transaction.localId.value, + action: SyncAction.create, + batch: batch, + syncQueues: syncQueues, + data: { + 'uuid': transaction.localId.value, + 'outlet_id': transaction.outletId.value, + 'user_id': transaction.userId.value, + 'reference_type': transaction.referenceType.value, + 'reference_id': transaction.referenceId.value, + 'reference_number': transaction.referenceNumber.value, + 'stakeholder': transaction.stakeholder.value, + 'transaction_type': transaction.transactionType.value, + 'category': transaction.category.value, + 'amount': transaction.amount.value, + 'note': transaction.note.value, + 'created_at': now.toIso8601String(), + 'updated_at': now.toIso8601String(), + }, + ); + + for (final item in purchaseItem) { + batch.insert(purchaseItems, item); + + CustomSyncQueue.addToSyncQueue( + type: TableType.purchase_items, + id: item.localId.value, + action: SyncAction.create, + batch: batch, + syncQueues: syncQueues, + data: { + 'uuid': item.localId.value, + 'purchase_id': item.purchaseId.value, + 'item_type': item.itemType.value, + 'item_id': item.itemId.value, + 'quantity': item.quantity.value, + 'item_image_url_snapshot': item.itemImageSnapshot.value, + 'item_name_snapshot': item.itemNameSnapshot.value, + 'item_name_variant_snapshot': + item.itemVariantNameSnapshot.value, + 'purchase_price_snapshot': item.purchasePriceSnaphot.value, + 'subtotal': item.subtotal.value, + 'created_at': now.toIso8601String(), + 'updated_at': now.toIso8601String(), + }, + ); + } + } + + for (final item in items) { + batch.update( + outletInventories, + OutletInventoriesCompanion( + stock: Value(item.updateStock), + costPrice: Value(item.costPrice), + purchasePrice: Value(item.purchasePrice), + syncStatus: Value(SyncStatus.dirty.status), + updatedAt: Value(DateTime.now()), + ), + where: (inventory) { + return inventory.localId.equals(item.outletInventoryId); + }, + ); + + CustomSyncQueue.addToSyncQueue( + type: TableType.outlet_inventories, + id: item.outletInventoryId, + action: SyncAction.update, + batch: batch, + syncQueues: syncQueues, + data: { + 'uuid': item.outletInventoryId, + 'cost_price': item.costPrice, + 'purchase_price': item.purchasePrice, + 'updated_at': now.toIso8601String(), + }, + ); + + final card = StockCardsCompanion( + localId: Value(item.id), + outletInventoryId: Value(item.outletInventoryId), + userId: Value(currentUserId!), + type: Value(item.stockCardType.type), + receiveFromId: Value(item.receiveFromId), + qtyChange: Value(item.qtyChange.abs()), + currentStock: Value(item.currentStock), + stockType: Value(StockType.fixed.type), + costPrice: Value(item.costPrice), + referenceId: Value(item.referenceId), + documentNumber: Value(item.documentNumber), + notes: Value(item.notes), + syncStatus: Value(item.syncStatus.status), + ); + + batch.insert(stockCards, card); + + CustomSyncQueue.addToSyncQueue( + type: TableType.stock_cards, + id: card.localId.value, + action: SyncAction.create, + batch: batch, + syncQueues: syncQueues, + data: { + 'uuid': card.localId.value, + 'outlet_inventory_id': card.outletInventoryId.value, + 'user_id': card.userId.value, + 'type': card.type.value, + 'receive_from_id': card.receiveFromId.value, + 'qty_change': card.qtyChange.value, + 'current_stock': card.currentStock.value, + 'stock_type': card.stockType.value, + 'cost_price': card.costPrice.value, + 'reference_id': card.referenceId.value, + 'document_number': card.documentNumber.value, + 'notes': card.notes.value, + 'created_at': now.toIso8601String(), + }, + ); + } + }); + }); + } catch (e, st) { + LogMessage.log.e(e.toString(), error: e, stackTrace: st); + rethrow; + } + } +} diff --git a/Mobile Operasional/lib/core/database/daos/stock_card_dao.g.dart b/Mobile Operasional/lib/core/database/daos/stock_card_dao.g.dart new file mode 100644 index 0000000..34a7ad4 --- /dev/null +++ b/Mobile Operasional/lib/core/database/daos/stock_card_dao.g.dart @@ -0,0 +1,68 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'stock_card_dao.dart'; + +// ignore_for_file: type=lint +mixin _$StockCardDaoMixin on DatabaseAccessor { + $OutletsTable get outlets => attachedDatabase.outlets; + $UsersTable get users => attachedDatabase.users; + $UnitsTable get units => attachedDatabase.units; + $CategoriesTable get categories => attachedDatabase.categories; + $ProductsTable get products => attachedDatabase.products; + $ProductVariantsTable get productVariants => attachedDatabase.productVariants; + $RawMaterialsTable get rawMaterials => attachedDatabase.rawMaterials; + $CustomersTable get customers => attachedDatabase.customers; + $OrdersTable get orders => attachedDatabase.orders; + $SuppliersTable get suppliers => attachedDatabase.suppliers; + $PurchasesTable get purchases => attachedDatabase.purchases; + $PurchaseItemsTable get purchaseItems => attachedDatabase.purchaseItems; + $OutletInventoriesTable get outletInventories => + attachedDatabase.outletInventories; + $StockCardsTable get stockCards => attachedDatabase.stockCards; + $TransactionsTable get transactions => attachedDatabase.transactions; + $SyncQueuesTable get syncQueues => attachedDatabase.syncQueues; + StockCardDaoManager get managers => StockCardDaoManager(this); +} + +class StockCardDaoManager { + final _$StockCardDaoMixin _db; + StockCardDaoManager(this._db); + $$OutletsTableTableManager get outlets => + $$OutletsTableTableManager(_db.attachedDatabase, _db.outlets); + $$UsersTableTableManager get users => + $$UsersTableTableManager(_db.attachedDatabase, _db.users); + $$UnitsTableTableManager get units => + $$UnitsTableTableManager(_db.attachedDatabase, _db.units); + $$CategoriesTableTableManager get categories => + $$CategoriesTableTableManager(_db.attachedDatabase, _db.categories); + $$ProductsTableTableManager get products => + $$ProductsTableTableManager(_db.attachedDatabase, _db.products); + $$ProductVariantsTableTableManager get productVariants => + $$ProductVariantsTableTableManager( + _db.attachedDatabase, + _db.productVariants, + ); + $$RawMaterialsTableTableManager get rawMaterials => + $$RawMaterialsTableTableManager(_db.attachedDatabase, _db.rawMaterials); + $$CustomersTableTableManager get customers => + $$CustomersTableTableManager(_db.attachedDatabase, _db.customers); + $$OrdersTableTableManager get orders => + $$OrdersTableTableManager(_db.attachedDatabase, _db.orders); + $$SuppliersTableTableManager get suppliers => + $$SuppliersTableTableManager(_db.attachedDatabase, _db.suppliers); + $$PurchasesTableTableManager get purchases => + $$PurchasesTableTableManager(_db.attachedDatabase, _db.purchases); + $$PurchaseItemsTableTableManager get purchaseItems => + $$PurchaseItemsTableTableManager(_db.attachedDatabase, _db.purchaseItems); + $$OutletInventoriesTableTableManager get outletInventories => + $$OutletInventoriesTableTableManager( + _db.attachedDatabase, + _db.outletInventories, + ); + $$StockCardsTableTableManager get stockCards => + $$StockCardsTableTableManager(_db.attachedDatabase, _db.stockCards); + $$TransactionsTableTableManager get transactions => + $$TransactionsTableTableManager(_db.attachedDatabase, _db.transactions); + $$SyncQueuesTableTableManager get syncQueues => + $$SyncQueuesTableTableManager(_db.attachedDatabase, _db.syncQueues); +} diff --git a/Mobile Operasional/lib/core/database/daos/supplier_dao.dart b/Mobile Operasional/lib/core/database/daos/supplier_dao.dart new file mode 100644 index 0000000..4d6d3ef --- /dev/null +++ b/Mobile Operasional/lib/core/database/daos/supplier_dao.dart @@ -0,0 +1,212 @@ +import 'package:drift/drift.dart'; +import 'package:niogu_app/core/database/app_database.dart'; +import 'package:niogu_app/core/database/tables/purchases.dart'; +import 'package:niogu_app/core/database/tables/suppliers.dart'; +import 'package:niogu_app/core/database/tables/sync_queues.dart'; +import 'package:niogu_app/core/entities/custom_sync_queue.dart'; +import 'package:niogu_app/core/enums/table_type.dart'; +import 'package:niogu_app/core/utils/extension_format.dart'; +import 'package:niogu_app/core/enums/payment_status.dart'; +import 'package:niogu_app/core/enums/sync_status.dart'; +import 'package:niogu_app/features/stock_in/domain/entities/stock_in_purchase.dart'; +import 'package:niogu_app/features/supplier/domain/entities/supplier.dart'; + +part 'supplier_dao.g.dart'; + +@DriftAccessor(tables: [Suppliers, Purchases, SyncQueues]) +class SupplierDao extends DatabaseAccessor + with _$SupplierDaoMixin { + SupplierDao(AppDatabase db) : super(db); + + Stream> watchAllSuppliers() async* { + final query = selectOnly(suppliers) + ..addColumns([suppliers.localId, suppliers.name]) + ..where(suppliers.deletedAt.isNull()); + + yield* query.watch().map((rows) { + return rows.map((row) { + final String id = row.read(suppliers.localId)!; + + final String name = row.read(suppliers.name) ?? ''; + + return DisplaySuppliers(id: id, name: name); + }).toList(); + }); + } + + Future getSupplierById(String id) async { + final supplierQuery = selectOnly(suppliers) + ..addColumns([ + suppliers.localId, + suppliers.name, + suppliers.email, + suppliers.phoneNumber, + ]) + ..where(suppliers.localId.equals(id) & suppliers.deletedAt.isNull()) + ..limit(1); + + final purchaseQuery = selectOnly(purchases) + ..addColumns([ + purchases.localId, + purchases.purchaseNumber, + purchases.paymentStatus, + purchases.createdAt, + ]) + ..orderBy([OrderingTerm.desc(purchases.createdAt)]) + ..where(purchases.supplierId.equals(id)); + + final supplierRow = await supplierQuery.getSingle(); + + final String localId = supplierRow.read(suppliers.localId) ?? ''; + + final String name = supplierRow.read(suppliers.name) ?? ''; + + final String email = supplierRow.read(suppliers.email) ?? ''; + + final String phoneNumber = supplierRow.read(suppliers.phoneNumber) ?? ''; + + final supplierActivityRow = await purchaseQuery.get(); + + final List activities = supplierActivityRow.map((row) { + final String localId = row.read(purchases.localId) ?? ''; + + final String purchaseNumber = (row.read(purchases.purchaseNumber) ?? '') + .toUniqueNumberCleaner(); + + final String status = row.read(purchases.paymentStatus) ?? ''; + + final PaymentStatus paymentStatus = PaymentStatus.values.byName(status); + + final purchaseDate = row.read(purchases.createdAt) ?? DateTime.now(); + + return SupplierActivities( + id: localId, + purchaseNumber: purchaseNumber, + paymentStatus: paymentStatus, + purchaseDate: purchaseDate, + ); + }).toList(); + + return SupplierDetail( + id: localId, + name: name, + email: email, + phoneNumber: phoneNumber, + activities: activities, + ); + } + + Future saveSupplier(SuppliersCompanion supplier) async { + return transaction(() async { + final now = DateTime.now(); + + final rowsAffected = await into(suppliers).insert( + supplier, + onConflict: DoUpdate((old) { + return supplier.copyWith( + syncStatus: Value(SyncStatus.dirty.status), + createdAt: Value.absent(), + updatedAt: Value(now), + ); + }), + ); + + final payload = SyncPayload( + table: TableType.suppliers.type, + localId: supplier.localId.value, + action: SyncAction.update_or_create.action, + data: { + 'uuid': supplier.localId.value, + 'name': supplier.name.value, + 'email': supplier.email.value, + 'phone_number': supplier.phoneNumber.value, + 'created_at': now.toIso8601String(), + 'updated_at': now.toIso8601String(), + }, + ).toJson(); + + final syncQueueComp = SyncQueuesCompanion( + tableType: Value(TableType.suppliers.type), + tableLocalId: supplier.localId, + action: Value(SyncAction.update_or_create.action), + status: Value(SyncStatus.dirty.status), + payload: Value(payload), + ); + + final existing = + await (select(syncQueues)..where( + (t) => + t.tableType.equals(TableType.suppliers.type) & + t.tableLocalId.equals(supplier.localId.value), + )) + .getSingleOrNull(); + + if (existing != null) { + await (update( + syncQueues, + )..where((t) => t.id.equals(existing.id))).write(syncQueueComp); + } else { + await into(syncQueues).insert(syncQueueComp); + } + + return rowsAffected; + }); + } + + Future deleteSupplier(String id) async { + return transaction(() async { + final now = DateTime.now(); + + final rowsAffected = + (await update(suppliers) + ..where((supplier) => supplier.localId.equals(id))) + .write( + SuppliersCompanion( + syncStatus: Value(SyncStatus.dirty.status), + deletedAt: Value(DateTime.now()), + ), + ); + + final payload = SyncPayload( + table: TableType.suppliers.type, + localId: id, + action: SyncAction.delete.action, + data: {'uuid': id, 'deleted_at': now.toIso8601String()}, + ).toJson(); + + await into(syncQueues).insert( + SyncQueuesCompanion( + tableType: Value(TableType.suppliers.type), + tableLocalId: Value(id), + action: Value(SyncAction.delete.action), + status: Value(SyncStatus.dirty.status), + payload: Value(payload), + ), + ); + + return rowsAffected; + }); + } + + Stream> watchAllAlreadySuppliers() async* { + final query = selectOnly(suppliers) + ..addColumns([ + suppliers.localId, + suppliers.name, + suppliers.phoneNumber, + suppliers.address, + ]) + ..where(suppliers.deletedAt.isNull()); + + yield* query.watch().map((rows) { + return rows.map((row) { + return AlreadySupplier( + id: row.read(suppliers.localId)!, + name: row.read(suppliers.name) ?? '', + phoneNumber: row.read(suppliers.phoneNumber) ?? '', + address: row.read(suppliers.address) ?? '', + ); + }).toList(); + }); + } +} diff --git a/Mobile Operasional/lib/core/database/daos/supplier_dao.g.dart b/Mobile Operasional/lib/core/database/daos/supplier_dao.g.dart new file mode 100644 index 0000000..6a01750 --- /dev/null +++ b/Mobile Operasional/lib/core/database/daos/supplier_dao.g.dart @@ -0,0 +1,28 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'supplier_dao.dart'; + +// ignore_for_file: type=lint +mixin _$SupplierDaoMixin on DatabaseAccessor { + $SuppliersTable get suppliers => attachedDatabase.suppliers; + $OutletsTable get outlets => attachedDatabase.outlets; + $UsersTable get users => attachedDatabase.users; + $PurchasesTable get purchases => attachedDatabase.purchases; + $SyncQueuesTable get syncQueues => attachedDatabase.syncQueues; + SupplierDaoManager get managers => SupplierDaoManager(this); +} + +class SupplierDaoManager { + final _$SupplierDaoMixin _db; + SupplierDaoManager(this._db); + $$SuppliersTableTableManager get suppliers => + $$SuppliersTableTableManager(_db.attachedDatabase, _db.suppliers); + $$OutletsTableTableManager get outlets => + $$OutletsTableTableManager(_db.attachedDatabase, _db.outlets); + $$UsersTableTableManager get users => + $$UsersTableTableManager(_db.attachedDatabase, _db.users); + $$PurchasesTableTableManager get purchases => + $$PurchasesTableTableManager(_db.attachedDatabase, _db.purchases); + $$SyncQueuesTableTableManager get syncQueues => + $$SyncQueuesTableTableManager(_db.attachedDatabase, _db.syncQueues); +} diff --git a/Mobile Operasional/lib/core/database/daos/sync_queue_dao.dart b/Mobile Operasional/lib/core/database/daos/sync_queue_dao.dart new file mode 100644 index 0000000..ef7b051 --- /dev/null +++ b/Mobile Operasional/lib/core/database/daos/sync_queue_dao.dart @@ -0,0 +1,11 @@ +import 'package:drift/drift.dart'; +import 'package:niogu_app/core/database/app_database.dart'; +import 'package:niogu_app/core/database/tables/sync_queues.dart'; + +part 'sync_queue_dao.g.dart'; + +@DriftAccessor(tables: [SyncQueues]) +class SyncQueueDao extends DatabaseAccessor + with _$SyncQueueDaoMixin { + SyncQueueDao(AppDatabase db) : super(db); +} diff --git a/Mobile Operasional/lib/core/database/daos/sync_queue_dao.g.dart b/Mobile Operasional/lib/core/database/daos/sync_queue_dao.g.dart new file mode 100644 index 0000000..8fddf63 --- /dev/null +++ b/Mobile Operasional/lib/core/database/daos/sync_queue_dao.g.dart @@ -0,0 +1,16 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'sync_queue_dao.dart'; + +// ignore_for_file: type=lint +mixin _$SyncQueueDaoMixin on DatabaseAccessor { + $SyncQueuesTable get syncQueues => attachedDatabase.syncQueues; + SyncQueueDaoManager get managers => SyncQueueDaoManager(this); +} + +class SyncQueueDaoManager { + final _$SyncQueueDaoMixin _db; + SyncQueueDaoManager(this._db); + $$SyncQueuesTableTableManager get syncQueues => + $$SyncQueuesTableTableManager(_db.attachedDatabase, _db.syncQueues); +} diff --git a/Mobile Operasional/lib/core/database/daos/tenant_dao.dart b/Mobile Operasional/lib/core/database/daos/tenant_dao.dart new file mode 100644 index 0000000..014d4d1 --- /dev/null +++ b/Mobile Operasional/lib/core/database/daos/tenant_dao.dart @@ -0,0 +1,760 @@ +import 'dart:convert'; + +import 'package:drift/drift.dart'; +import 'package:niogu_app/core/database/app_database.dart'; +import 'package:niogu_app/core/database/tables/campaigns.dart'; +import 'package:niogu_app/core/database/tables/categories.dart'; +import 'package:niogu_app/core/database/tables/outlet_inventories.dart'; +import 'package:niogu_app/core/database/tables/outlets.dart'; +import 'package:niogu_app/core/database/tables/product_variants.dart'; +import 'package:niogu_app/core/database/tables/products.dart'; +import 'package:niogu_app/core/database/tables/sync_queues.dart'; +import 'package:niogu_app/core/database/tables/tenant_payment_methods.dart'; +import 'package:niogu_app/core/database/tables/tenants.dart'; +import 'package:niogu_app/core/database/tables/units.dart'; +import 'package:niogu_app/core/entities/custom_sync_queue.dart'; +import 'package:niogu_app/core/enums/delivery_fee_type.dart'; +import 'package:niogu_app/core/enums/item_type.dart'; +import 'package:niogu_app/core/enums/member_level.dart'; +import 'package:niogu_app/core/enums/review_display_policy.dart'; +import 'package:niogu_app/core/enums/review_display_type.dart'; +import 'package:niogu_app/core/enums/stock_type.dart'; +import 'package:niogu_app/core/enums/sync_status.dart'; +import 'package:niogu_app/core/enums/table_type.dart'; +import 'package:niogu_app/core/system/system_setting.dart'; +import 'package:niogu_app/core/utils/log_message.dart'; +import 'package:niogu_app/features/online_store/domain/entities/online_store.dart'; +import 'package:niogu_app/features/profile/domain/entities/profile.dart'; + +part 'tenant_dao.g.dart'; + +@DriftAccessor( + tables: [ + Units, + Categories, + Products, + ProductVariants, + OutletInventories, + Outlets, + Tenants, + TenantPaymentMethods, + Campaigns, + SyncQueues, + ], +) +class TenantDao extends DatabaseAccessor with _$TenantDaoMixin { + TenantDao(AppDatabase db) : super(db); + + Stream watchBusinessCompleteness() async* { + final query = selectOnly(tenants) + ..addColumns([ + tenants.localLogoPath, + tenants.businessEmail, + tenants.businessPhone, + tenants.businessType, + ]) + ..limit(1); + + yield* query.watchSingle().map((row) { + double percent = 0.2; + + final logo = row.read(tenants.localLogoPath); + + final email = row.read(tenants.businessEmail); + + final phone = row.read(tenants.businessPhone); + + final category = row.read(tenants.businessType); + + if (logo != null) { + percent += 0.2; + } + + if (email != null && email.isNotEmpty) { + percent += 0.2; + } + + if (phone != null && phone.isNotEmpty) { + percent += 0.2; + } + + if (category != null && category.isNotEmpty) { + percent += 0.2; + } + + return percent; + }); + } + + Stream watchMemberLevel() async* { + final query = selectOnly(tenants) + ..addColumns([tenants.subscriptionLevel]) + ..limit(1); + + yield* query.watchSingle().map((row) { + final level = row.read(tenants.subscriptionLevel)!; + + return MemberLevel.values.byName(level); + }); + } + + Future getBusinessInfo() async { + final query = selectOnly(tenants) + ..addColumns([ + tenants.localLogoPath, + tenants.businessCode, + tenants.businessName, + tenants.businessEmail, + tenants.businessPhone, + tenants.businessType, + ]) + ..limit(1); + + final result = await query.getSingle(); + + return BusinessInfo( + logoPath: result.read(tenants.localLogoPath), + businessCode: result.read(tenants.businessCode)!, + businessName: result.read(tenants.businessName) ?? '', + businessEmail: result.read(tenants.businessEmail) ?? '', + businessPhoneNumber: result.read(tenants.businessPhone) ?? '', + businessType: result.read(tenants.businessType) ?? '', + ); + } + + Future updateBusinessInfo(BusinessInfo business) async { + return transaction(() async { + final now = DateTime.now(); + + final currentTenantId = await SystemSetting.getTenantId(); + + final rowsAffected = (await update(tenants)).write( + TenantsCompanion( + localLogoPath: Value(business.logoPath), + businessName: Value(business.businessName), + businessEmail: Value(business.businessEmail), + businessPhone: Value(business.businessPhoneNumber), + businessType: Value(business.businessType), + syncStatus: Value(SyncStatus.dirty.status), + updatedAt: Value(now), + ), + ); + + final payload = SyncPayload( + table: TableType.tenants.type, + localId: currentTenantId!, + action: SyncAction.update.action, + data: { + 'uuid': currentTenantId, + 'server_logo_url': business.logoPath, + 'business_name': business.businessName, + 'business_email': business.businessEmail, + 'business_phone': business.businessPhoneNumber, + 'business_type': business.businessType, + 'updated_at': now.toIso8601String(), + }, + ).toJson(); + + final syncQueueComp = SyncQueuesCompanion( + tableType: Value(TableType.tenants.type), + tableLocalId: Value(currentTenantId), + action: Value(SyncAction.update.action), + status: Value(SyncStatus.dirty.status), + payload: Value(payload), + ); + + final existing = + await (select(syncQueues)..where( + (tbl) => + tbl.tableType.equals(TableType.tenants.type) & + tbl.tableLocalId.equals(currentTenantId), + )) + .getSingleOrNull(); + + if (existing != null) { + await (update( + syncQueues, + )..where((tbl) => tbl.id.equals(existing.id))).write(syncQueueComp); + } else { + await into(syncQueues).insert(syncQueueComp); + } + + return rowsAffected; + }); + } + + Future getConfigureOnlineStore() async { + final queryTenant = selectOnly(tenants) + ..addColumns([ + tenants.onlinStoreName, + tenants.onlineOpenTime, + tenants.onlineCloseTime, + tenants.isCloseService, + tenants.preparationTimeMinutes, + tenants.orderIntervalMinutes, + ]) + ..limit(1); + + final queryProduct = selectOnly(productVariants) + ..addColumns([productVariants.localId]) + ..where( + productVariants.isVisibleOnline & productVariants.deletedAt.isNull(), + ); + + final tenantRow = await queryTenant.getSingle(); + + final productRows = await queryProduct.get(); + + final List productIds = productRows + .map((row) => row.read(productVariants.localId) ?? '') + .toList(); + + return ConfigureOnlineStore( + onlineStoreName: tenantRow.read(tenants.onlinStoreName) ?? '', + onlineOpenTime: tenantRow.read(tenants.onlineOpenTime) ?? '', + onlineCloseTime: tenantRow.read(tenants.onlineCloseTime) ?? '', + isCloseService: tenantRow.read(tenants.isCloseService) ?? false, + preparationTimeMinutes: + tenantRow.read(tenants.preparationTimeMinutes) ?? 0, + orderIntervalMinutes: tenantRow.read(tenants.orderIntervalMinutes) ?? 0, + productIds: productIds, + ); + } + + Future configureOnlineStore(ConfigureOnlineStore configure) async { + await transaction(() async { + final now = DateTime.now(); + + final currentTenantId = await SystemSetting.getTenantId(); + + await batch((batch) async { + batch.update( + tenants, + TenantsCompanion( + onlinStoreName: Value(configure.onlineStoreName), + onlineOpenTime: Value(configure.onlineOpenTime), + onlineCloseTime: Value(configure.onlineCloseTime), + isCloseService: Value(configure.isCloseService), + preparationTimeMinutes: Value(configure.preparationTimeMinutes), + orderIntervalMinutes: Value(configure.orderIntervalMinutes), + syncStatus: Value(SyncStatus.dirty.status), + updatedAt: Value(now), + ), + ); + + final existing = + await (select(syncQueues)..where( + (tbl) => + tbl.tableType.equals(TableType.tenants.type) & + tbl.tableLocalId.equals(currentTenantId!), + )) + .getSingleOrNull(); + + if (existing != null) { + CustomSyncQueue.updateSyncQueue( + type: TableType.tenants, + id: currentTenantId!, + action: SyncAction.update, + batch: batch, + syncQueues: syncQueues, + data: { + 'uuid': currentTenantId, + 'online_store_name': configure.onlineStoreName, + 'online_open_time': configure.onlineOpenTime, + 'online_close_time': configure.onlineCloseTime, + 'is_close_service': configure.isCloseService, + 'preparation_time_minutes': configure.preparationTimeMinutes, + 'order_interval_minutes': configure.orderIntervalMinutes, + 'updated_at': now.toIso8601String(), + }, + ); + } else { + CustomSyncQueue.addToSyncQueue( + type: TableType.tenants, + id: currentTenantId!, + action: SyncAction.update, + batch: batch, + syncQueues: syncQueues, + data: { + 'uuid': currentTenantId, + 'online_store_name': configure.onlineStoreName, + 'online_open_time': configure.onlineOpenTime, + 'online_close_time': configure.onlineCloseTime, + 'is_close_service': configure.isCloseService, + 'preparation_time_minutes': configure.preparationTimeMinutes, + 'order_interval_minutes': configure.orderIntervalMinutes, + 'updated_at': now.toIso8601String(), + }, + ); + } + + batch.update( + productVariants, + ProductVariantsCompanion( + isVisibleOnline: Value(true), + syncStatus: Value(SyncStatus.dirty.status), + updatedAt: Value(now), + ), + where: (tbl) => tbl.localId.isIn(configure.productIds), + ); + + batch.update( + productVariants, + ProductVariantsCompanion( + isVisibleOnline: Value(false), + syncStatus: Value(SyncStatus.dirty.status), + updatedAt: Value(now), + ), + where: (tbl) => tbl.localId.isNotIn(configure.productIds), + ); + + CustomSyncQueue.addToSyncQueue( + type: TableType.product_variants, + id: 'BULK_VISIBILITY_${now.millisecondsSinceEpoch}', + action: SyncAction.bulk_update, + batch: batch, + syncQueues: syncQueues, + data: { + 'visible_ids': configure.productIds, + 'updated_at': now.toIso8601String(), + }, + ); + }); + }); + } + + Stream> watchAllOnlineStoreProducts() async* { + final query = + selectOnly(products).join([ + innerJoin(units, units.localId.equalsExp(products.unitId)), + innerJoin( + productVariants, + productVariants.productId.equalsExp(products.localId), + ), + innerJoin( + outletInventories, + outletInventories.itemType.equals(ItemType.product_variant.type) & + outletInventories.itemId.equalsExp(productVariants.localId), + ), + ]) + ..addColumns([ + products.localId, + products.name, + products.localImagePath, + products.hasVariant, + products.deletedAt, + productVariants.localId, + productVariants.name, + productVariants.isProductVariant, + productVariants.deletedAt, + outletInventories.stockType, + outletInventories.stock, + units.name, + outletInventories.sellingPrice, + outletInventories.deletedAt, + ]) + ..orderBy([OrderingTerm.desc(products.createdAt)]); + + yield* query.watch().map((rows) { + final Map groupedMap = {}; + + for (final row in rows) { + final productId = row.read(products.localId)!; + + final stockType = StockType.values.byName( + (row.read(outletInventories.stockType) ?? ''), + ); + + final remainingStock = row.read(outletInventories.stock) ?? 0.0; + + final sellingPrice = row.read(outletInventories.sellingPrice) ?? 0.0; + + final hasVariant = row.read(products.hasVariant) ?? false; + + final dynamic rawImages = row.read(products.localImagePath); + + List productImages = []; + + if (rawImages is String) { + try { + productImages = (jsonDecode(rawImages) as List).cast(); + } catch (e) { + productImages = []; + } + } else if (rawImages is List) { + productImages = (rawImages).cast(); + } + + final productDeletedAt = row.read(products.deletedAt); + + final variantDeletedAt = row.read(productVariants.deletedAt); + + final outletInventoryDeletedAt = row.read(outletInventories.deletedAt); + + final productHasBeenDelete = productDeletedAt != null; + + final variantHasBeenDelete = + (variantDeletedAt != null) || (outletInventoryDeletedAt != null); + + groupedMap.putIfAbsent( + productId, + () => OnlineStoreProduct( + id: productId, + name: row.read(products.name) ?? '', + stockType: stockType, + remainingStock: remainingStock, + unit: row.read(units.name) ?? '', + sellingPrice: sellingPrice, + imagePath: productImages.lastOrNull, + hasVariant: hasVariant, + variants: [], + hasBeenDelete: productHasBeenDelete, + ), + ); + + groupedMap[productId]!.variants.add( + VariantOnlineStoreProduct( + id: row.read(productVariants.localId)!, + name: row.read(productVariants.name) ?? '', + stockType: stockType, + remainingStock: remainingStock, + unit: row.read(units.name) ?? '', + sellingPrice: sellingPrice, + isProductVariant: + row.read(productVariants.isProductVariant) ?? false, + hasBeenDelete: variantHasBeenDelete, + ), + ); + } + + return groupedMap.values.toList(); + }); + } + + Future getShippingConfiuration() async { + final query = selectOnly(tenants) + ..addColumns([ + tenants.allowCod, + tenants.deliveryFeeType, + tenants.maxDeliveryRadiusKm, + tenants.deliveryFlatFee, + tenants.deliveryFeePerKm, + tenants.deliveryBaseFee, + ]) + ..limit(1); + + final result = await query.getSingle(); + + return ShippingConfiguration( + allowCOD: result.read(tenants.allowCod) ?? false, + deliveryFeeType: DeliveryFeeType.values.byName( + (result.read(tenants.deliveryFeeType) ?? ''), + ), + maxDeliveryRadiusKm: result.read(tenants.maxDeliveryRadiusKm) ?? 0.0, + deliveryFlatFee: result.read(tenants.deliveryFlatFee) ?? 0.0, + deliveryFeePerKm: result.read(tenants.deliveryFeePerKm) ?? 0.0, + deliveryBaseFee: result.read(tenants.deliveryBaseFee) ?? 0.0, + ); + } + + Future shippingConfiguration(ShippingConfiguration configure) async { + return transaction(() async { + final now = DateTime.now(); + + final currentTenantId = await SystemSetting.getTenantId(); + + final rowsAffected = (await update(tenants).write( + TenantsCompanion( + allowCod: Value(configure.allowCOD), + deliveryFeeType: Value(configure.deliveryFeeType.type), + maxDeliveryRadiusKm: Value(configure.maxDeliveryRadiusKm), + deliveryFlatFee: Value(configure.deliveryFlatFee), + deliveryFeePerKm: Value(configure.deliveryFeePerKm), + deliveryBaseFee: Value(configure.deliveryBaseFee), + syncStatus: Value(SyncStatus.dirty.status), + updatedAt: Value(now), + ), + )); + + final payload = SyncPayload( + table: TableType.tenants.type, + localId: currentTenantId!, + action: SyncAction.update.action, + data: { + 'uuid': currentTenantId, + 'allow_cod': configure.allowCOD, + 'delivery_fee_type': configure.deliveryFeeType.type, + 'max_delivery_radius_km': configure.maxDeliveryRadiusKm, + 'delivery_flat_fee': configure.deliveryFlatFee, + 'delivery_fee_per_km': configure.deliveryFeePerKm, + 'delivery_base_fee': configure.deliveryBaseFee, + 'updated_at': now.toIso8601String(), + }, + ).toJson(); + + final syncQueueComp = SyncQueuesCompanion( + tableType: Value(TableType.tenants.type), + tableLocalId: Value(currentTenantId), + action: Value(SyncAction.update.action), + status: Value(SyncStatus.dirty.status), + payload: Value(payload), + ); + + final existing = + await (select(syncQueues)..where( + (tbl) => + tbl.tableType.equals(TableType.tenants.type) & + tbl.tableLocalId.equals(currentTenantId), + )) + .getSingleOrNull(); + + if (existing != null) { + await (update( + syncQueues, + )..where((tbl) => tbl.id.equals(existing.id))).write(syncQueueComp); + } else { + await into(syncQueues).insert(syncQueueComp); + } + + return rowsAffected; + }); + } + + Future> getConfigurePaymentMethod() async { + final query = selectOnly(tenantPaymentMethods) + ..addColumns([ + tenantPaymentMethods.localId, + tenantPaymentMethods.sourceName, + tenantPaymentMethods.accountNumber, + tenantPaymentMethods.receipentName, + ]); + + final results = await query.get(); + + return results.map((row) { + return ConfigurePaymentMethod( + localId: row.read(tenantPaymentMethods.localId)!, + sourceName: row.read(tenantPaymentMethods.sourceName) ?? '', + accountNumber: row.read(tenantPaymentMethods.accountNumber) ?? '', + receipentName: row.read(tenantPaymentMethods.receipentName) ?? '', + ); + }).toList(); + } + + Future paymentMethodConfiguration( + List companions, + ) async { + try { + await transaction(() async { + final now = DateTime.now(); + + await batch((batch) { + for (final companion in companions) { + batch.insert( + tenantPaymentMethods, + companion, + onConflict: DoUpdate((old) { + return companion.copyWith( + sourceName: companion.sourceName, + accountNumber: companion.accountNumber, + receipentName: companion.receipentName, + syncStatus: Value(SyncStatus.dirty.status), + updatedAt: Value(now), + ); + }), + ); + + CustomSyncQueue.addToSyncQueue( + type: TableType.tenant_payment_methods, + id: companion.localId.value, + action: SyncAction.update_or_create, + batch: batch, + syncQueues: syncQueues, + data: { + 'uuid': companion.localId.value, + 'source_name': companion.sourceName.value, + 'account_number': companion.accountNumber.value, + 'receipent_name': companion.receipentName.value, + 'updated_at': now.toIso8601String(), + }, + ); + } + }); + }); + } catch (e, st) { + LogMessage.log.e(e.toString(), error: e, stackTrace: st); + rethrow; + } + } + + Future getRatingConfiguration() async { + final query = selectOnly(tenants) + ..addColumns([tenants.reviewDisplayType, tenants.reviewDisplayPolicy]) + ..limit(1); + + final result = await query.getSingle(); + + return RatingConfiguration( + type: ReviewDisplayType.values.byName( + result.read(tenants.reviewDisplayType)!, + ), + policy: ReviewDisplayPolicy.values.byName( + result.read(tenants.reviewDisplayPolicy)!, + ), + ); + } + + Future ratingConfiguration(RatingConfiguration configuration) async { + return await transaction(() async { + final now = DateTime.now(); + + final currentTenantId = await SystemSetting.getTenantId(); + + final rowsAffected = (await update(tenants)).write( + TenantsCompanion( + reviewDisplayType: Value(configuration.type.type), + reviewDisplayPolicy: Value(configuration.policy.policy), + syncStatus: Value(SyncStatus.dirty.status), + updatedAt: Value(now), + ), + ); + + final payload = SyncPayload( + table: TableType.tenants.type, + localId: currentTenantId!, + action: SyncAction.update.action, + data: { + 'uuid': currentTenantId, + 'review_display_type': configuration.type.type, + 'review_display_policy': configuration.policy.policy, + 'updated_at': now.toIso8601String(), + }, + ).toJson(); + + final syncQueueComp = SyncQueuesCompanion( + tableType: Value(TableType.tenants.type), + tableLocalId: Value(currentTenantId), + action: Value(SyncAction.update.action), + status: Value(SyncStatus.dirty.status), + payload: Value(payload), + ); + + final existing = + await (select(syncQueues)..where( + (tbl) => + tbl.tableType.equals(TableType.tenants.type) & + tbl.tableLocalId.equals(currentTenantId), + )) + .getSingleOrNull(); + + if (existing != null) { + await (update( + syncQueues, + )..where((tbl) => tbl.id.equals(existing.id))).write(syncQueueComp); + } else { + await into(syncQueues).insert(syncQueueComp); + } + + return rowsAffected; + }); + } + + Stream> watchAllProductCampaigns() async* { + final query = + selectOnly(products).join([ + innerJoin(units, units.localId.equalsExp(products.unitId)), + innerJoin( + productVariants, + productVariants.productId.equalsExp(products.localId), + ), + innerJoin( + outletInventories, + outletInventories.itemType.equals(ItemType.product_variant.type) & + outletInventories.itemId.equalsExp(productVariants.localId), + ), + ]) + ..addColumns([ + products.localId, + products.localImagePath, + products.name, + outletInventories.stockType, + outletInventories.stock.sum(), + units.name, + ]) + ..groupBy([products.localId]) + ..orderBy([OrderingTerm.desc(products.createdAt)]) + ..where(products.deletedAt.isNull()) + ..where(productVariants.deletedAt.isNull()) + ..where(outletInventories.deletedAt.isNull()); + + yield* query.watch().map((rows) { + return rows.map((row) { + final dynamic rawImages = row.read(products.localImagePath); + + List productImages = []; + + if (rawImages is String) { + productImages = (jsonDecode(rawImages) as List).cast(); + } else if (rawImages is List) { + productImages = (rawImages).cast(); + } + + return ProductCampaign( + id: row.read(products.localId)!, + imagePath: productImages.lastOrNull, + name: row.read(products.name) ?? '', + stockType: StockType.values.byName( + (row.read(outletInventories.stockType)!), + ), + remainingStock: row.read(outletInventories.stock.sum()) ?? 0.0, + unit: row.read(units.name) ?? '', + ); + }).toList(); + }); + } + + Stream> watchAllCategoryCampaigns() async* { + final query = + selectOnly(categories).join([ + leftOuterJoin( + products, + products.categoryId.equalsExp(categories.localId), + ), + ]) + ..addColumns([ + categories.localId, + categories.localImagePath, + categories.name, + products.localId.count(), + ]) + ..groupBy([categories.localId]) + ..orderBy([OrderingTerm.desc(products.localId.count())]); + + yield* query.watch().map((rows) { + return rows.map((row) { + return CategoryCampaign( + id: row.read(categories.localId)!, + imagePath: row.read(categories.localImagePath), + name: row.read(categories.name) ?? '', + productUseAmount: row.read(products.localId.count()) ?? 0, + ); + }).toList(); + }); + } + + Stream> watchAllTargetOutlets() async* { + final query = selectOnly(outlets) + ..addColumns([outlets.localId, outlets.name, outlets.fullAddress]) + ..where(outlets.deletedAt.isNull()); + + yield* query.watch().map((rows) { + return rows.map((row) { + return TargetOutlet( + id: row.read(outlets.localId)!, + name: row.read(outlets.name) ?? '', + address: row.read(outlets.fullAddress), + ); + }).toList(); + }); + } +} diff --git a/Mobile Operasional/lib/core/database/daos/tenant_dao.g.dart b/Mobile Operasional/lib/core/database/daos/tenant_dao.g.dart new file mode 100644 index 0000000..42264be --- /dev/null +++ b/Mobile Operasional/lib/core/database/daos/tenant_dao.g.dart @@ -0,0 +1,54 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'tenant_dao.dart'; + +// ignore_for_file: type=lint +mixin _$TenantDaoMixin on DatabaseAccessor { + $UnitsTable get units => attachedDatabase.units; + $CategoriesTable get categories => attachedDatabase.categories; + $ProductsTable get products => attachedDatabase.products; + $ProductVariantsTable get productVariants => attachedDatabase.productVariants; + $OutletsTable get outlets => attachedDatabase.outlets; + $OutletInventoriesTable get outletInventories => + attachedDatabase.outletInventories; + $TenantsTable get tenants => attachedDatabase.tenants; + $TenantPaymentMethodsTable get tenantPaymentMethods => + attachedDatabase.tenantPaymentMethods; + $CampaignsTable get campaigns => attachedDatabase.campaigns; + $SyncQueuesTable get syncQueues => attachedDatabase.syncQueues; + TenantDaoManager get managers => TenantDaoManager(this); +} + +class TenantDaoManager { + final _$TenantDaoMixin _db; + TenantDaoManager(this._db); + $$UnitsTableTableManager get units => + $$UnitsTableTableManager(_db.attachedDatabase, _db.units); + $$CategoriesTableTableManager get categories => + $$CategoriesTableTableManager(_db.attachedDatabase, _db.categories); + $$ProductsTableTableManager get products => + $$ProductsTableTableManager(_db.attachedDatabase, _db.products); + $$ProductVariantsTableTableManager get productVariants => + $$ProductVariantsTableTableManager( + _db.attachedDatabase, + _db.productVariants, + ); + $$OutletsTableTableManager get outlets => + $$OutletsTableTableManager(_db.attachedDatabase, _db.outlets); + $$OutletInventoriesTableTableManager get outletInventories => + $$OutletInventoriesTableTableManager( + _db.attachedDatabase, + _db.outletInventories, + ); + $$TenantsTableTableManager get tenants => + $$TenantsTableTableManager(_db.attachedDatabase, _db.tenants); + $$TenantPaymentMethodsTableTableManager get tenantPaymentMethods => + $$TenantPaymentMethodsTableTableManager( + _db.attachedDatabase, + _db.tenantPaymentMethods, + ); + $$CampaignsTableTableManager get campaigns => + $$CampaignsTableTableManager(_db.attachedDatabase, _db.campaigns); + $$SyncQueuesTableTableManager get syncQueues => + $$SyncQueuesTableTableManager(_db.attachedDatabase, _db.syncQueues); +} diff --git a/Mobile Operasional/lib/core/database/daos/tenant_payment_method_dao.dart b/Mobile Operasional/lib/core/database/daos/tenant_payment_method_dao.dart new file mode 100644 index 0000000..f275a2f --- /dev/null +++ b/Mobile Operasional/lib/core/database/daos/tenant_payment_method_dao.dart @@ -0,0 +1,10 @@ +import 'package:drift/drift.dart'; +import 'package:niogu_app/core/database/app_database.dart'; +import 'package:niogu_app/core/database/tables/tenant_payment_methods.dart'; + +part 'tenant_payment_method_dao.g.dart'; + +@DriftAccessor(tables: [TenantPaymentMethods]) +class TenantPaymentMethodDao extends DatabaseAccessor with _$TenantPaymentMethodDaoMixin { + TenantPaymentMethodDao(AppDatabase db) : super(db); +} \ No newline at end of file diff --git a/Mobile Operasional/lib/core/database/daos/tenant_payment_method_dao.g.dart b/Mobile Operasional/lib/core/database/daos/tenant_payment_method_dao.g.dart new file mode 100644 index 0000000..25de812 --- /dev/null +++ b/Mobile Operasional/lib/core/database/daos/tenant_payment_method_dao.g.dart @@ -0,0 +1,21 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'tenant_payment_method_dao.dart'; + +// ignore_for_file: type=lint +mixin _$TenantPaymentMethodDaoMixin on DatabaseAccessor { + $TenantPaymentMethodsTable get tenantPaymentMethods => + attachedDatabase.tenantPaymentMethods; + TenantPaymentMethodDaoManager get managers => + TenantPaymentMethodDaoManager(this); +} + +class TenantPaymentMethodDaoManager { + final _$TenantPaymentMethodDaoMixin _db; + TenantPaymentMethodDaoManager(this._db); + $$TenantPaymentMethodsTableTableManager get tenantPaymentMethods => + $$TenantPaymentMethodsTableTableManager( + _db.attachedDatabase, + _db.tenantPaymentMethods, + ); +} diff --git a/Mobile Operasional/lib/core/database/daos/transaction_dao.dart b/Mobile Operasional/lib/core/database/daos/transaction_dao.dart new file mode 100644 index 0000000..ad457f8 --- /dev/null +++ b/Mobile Operasional/lib/core/database/daos/transaction_dao.dart @@ -0,0 +1,93 @@ +import 'package:drift/drift.dart'; +import 'package:niogu_app/core/database/app_database.dart'; +import 'package:niogu_app/core/database/tables/customers.dart'; +import 'package:niogu_app/core/database/tables/orders.dart'; +import 'package:niogu_app/core/database/tables/purchases.dart'; +import 'package:niogu_app/core/database/tables/suppliers.dart'; +import 'package:niogu_app/core/database/tables/transactions.dart'; +import 'package:niogu_app/core/utils/extension_format.dart'; +import 'package:niogu_app/core/enums/transaction_reference_type.dart'; +import 'package:niogu_app/features/report/transaction/domain/entities/transaction_report.dart'; + +part 'transaction_dao.g.dart'; + +@DriftAccessor(tables: [Customers, Suppliers, Transactions, Orders, Purchases]) +class TransactionDao extends DatabaseAccessor + with _$TransactionDaoMixin { + TransactionDao(AppDatabase db) : super(db); + + Stream> watchAllTransactionItems({ + String? outletId, + TransactionType? type, + DateTime? startDate, + DateTime? endDate, + }) async* { + final query = selectOnly(transactions) + ..addColumns([ + transactions.referenceType, + transactions.referenceId, + transactions.referenceNumber, + transactions.stakeholder, + transactions.transactionType, + transactions.category, + transactions.amount, + transactions.createdAt, + ]) + ..orderBy([OrderingTerm.desc(transactions.createdAt)]) + ..where( + outletId != null + ? transactions.outletId.equals(outletId) + : const Constant(true), + ) + ..where( + type != null + ? transactions.transactionType.equals(type.type) + : const Constant(true), + ) + ..where( + (startDate != null && endDate != null) + ? transactions.createdAt.isBiggerOrEqualValue(startDate) & + transactions.createdAt.isSmallerThanValue( + endDate.add(const Duration(days: 1)), + ) + : const Constant(true), + ); + + yield* query.watch().map((rows) { + return rows.map((row) { + final String id = row.read(transactions.referenceId)!; + + final String transactionNumber = row.read( + transactions.referenceNumber, + )!; + + final String title = row.read(transactions.category) ?? ''; + + final bool isIncome = + (row.read(transactions.transactionType) ?? '') == 'income'; + + final String stakeholder = row.read(transactions.stakeholder) != null + ? "• ${row.read(transactions.stakeholder)}" + : ""; + + final String subtitle = + "${transactionNumber.toUniqueNumberCleaner()} $stakeholder"; + + final double amount = row.read(transactions.amount) ?? 0.0; + + final DateTime date = + row.read(transactions.createdAt) ?? DateTime.now(); + + return TransactionReportItem( + id: id, + transactionNumber: transactionNumber, + title: title, + subtitle: subtitle, + amount: amount, + date: date, + isIncome: isIncome, + ); + }).toList(); + }); + } +} diff --git a/Mobile Operasional/lib/core/database/daos/transaction_dao.g.dart b/Mobile Operasional/lib/core/database/daos/transaction_dao.g.dart new file mode 100644 index 0000000..951f09b --- /dev/null +++ b/Mobile Operasional/lib/core/database/daos/transaction_dao.g.dart @@ -0,0 +1,34 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'transaction_dao.dart'; + +// ignore_for_file: type=lint +mixin _$TransactionDaoMixin on DatabaseAccessor { + $OutletsTable get outlets => attachedDatabase.outlets; + $CustomersTable get customers => attachedDatabase.customers; + $SuppliersTable get suppliers => attachedDatabase.suppliers; + $UsersTable get users => attachedDatabase.users; + $TransactionsTable get transactions => attachedDatabase.transactions; + $OrdersTable get orders => attachedDatabase.orders; + $PurchasesTable get purchases => attachedDatabase.purchases; + TransactionDaoManager get managers => TransactionDaoManager(this); +} + +class TransactionDaoManager { + final _$TransactionDaoMixin _db; + TransactionDaoManager(this._db); + $$OutletsTableTableManager get outlets => + $$OutletsTableTableManager(_db.attachedDatabase, _db.outlets); + $$CustomersTableTableManager get customers => + $$CustomersTableTableManager(_db.attachedDatabase, _db.customers); + $$SuppliersTableTableManager get suppliers => + $$SuppliersTableTableManager(_db.attachedDatabase, _db.suppliers); + $$UsersTableTableManager get users => + $$UsersTableTableManager(_db.attachedDatabase, _db.users); + $$TransactionsTableTableManager get transactions => + $$TransactionsTableTableManager(_db.attachedDatabase, _db.transactions); + $$OrdersTableTableManager get orders => + $$OrdersTableTableManager(_db.attachedDatabase, _db.orders); + $$PurchasesTableTableManager get purchases => + $$PurchasesTableTableManager(_db.attachedDatabase, _db.purchases); +} diff --git a/Mobile Operasional/lib/core/database/daos/unit_dao.dart b/Mobile Operasional/lib/core/database/daos/unit_dao.dart new file mode 100644 index 0000000..6a5b773 --- /dev/null +++ b/Mobile Operasional/lib/core/database/daos/unit_dao.dart @@ -0,0 +1,138 @@ +import 'package:drift/drift.dart'; +import 'package:niogu_app/core/database/app_database.dart' hide Unit; +import 'package:niogu_app/core/database/tables/sync_queues.dart'; +import 'package:niogu_app/core/database/tables/units.dart'; +import 'package:niogu_app/core/entities/custom_sync_queue.dart'; +import 'package:niogu_app/core/enums/sync_status.dart'; +import 'package:niogu_app/core/enums/table_type.dart'; +import 'package:niogu_app/features/goods/units/domain/entities/unit.dart'; + +part 'unit_dao.g.dart'; + +@DriftAccessor(tables: [Units, SyncQueues]) +class UnitDao extends DatabaseAccessor with _$UnitDaoMixin { + UnitDao(AppDatabase db) : super(db); + + Stream> watchAllUnits() async* { + final query = selectOnly(units) + ..addColumns([units.localId, units.name]) + ..orderBy([OrderingTerm.desc(units.createdAt)]); + + yield* query.watch().map((rows) { + return rows.map((row) { + final String id = row.read(units.localId) ?? ''; + final String name = row.read(units.name) ?? ''; + return Unit(id: id, name: name); + }).toList(); + }); + } + + Future> getAllUnits() async { + final query = selectOnly(units) + ..addColumns([units.localId, units.name]) + ..orderBy([OrderingTerm.desc(units.createdAt)]); + + final results = await query.get(); + + return results.map((row) { + final String id = row.read(units.localId) ?? ''; + final String name = row.read(units.name) ?? ''; + return Unit(id: id, name: name); + }).toList(); + } + + Future getUnitById(String id) async { + final query = selectOnly(units) + ..addColumns([units.localId, units.name]) + ..where(units.localId.equals(id)) + ..limit(1); + + final result = await query.getSingle(); + + final String localId = result.read(units.localId) ?? ''; + final String name = result.read(units.name) ?? ''; + + return Unit(id: localId, name: name); + } + + Future insertUnit(UnitsCompanion unit) async { + return await transaction(() async { + final now = DateTime.now(); + + final id = await into(units).insert(unit); + + final payload = SyncPayload( + table: TableType.units.type, + localId: unit.localId.value, + action: SyncAction.create.action, + data: { + 'uuid': unit.localId.value, + 'name': unit.name.value, + 'created_at': now.toIso8601String(), + 'updated_at': now.toIso8601String(), + }, + ).toJson(); + + await into(syncQueues).insert( + SyncQueuesCompanion( + tableType: Value(TableType.units.type), + tableLocalId: unit.localId, + action: Value(SyncAction.create.action), + status: Value(SyncStatus.dirty.status), + payload: Value(payload), + ), + ); + + return id; + }); + } + + Future updateUnit(String id, String name) async { + return await transaction(() async { + final now = DateTime.now(); + + final rowsAffected = + (await update(units) + ..where((unit) => unit.localId.equals(id))) + .write( + UnitsCompanion( + name: Value(name), + syncStatus: Value(SyncStatus.dirty.status), + updatedAt: Value(now), + ), + ); + + final payload = SyncPayload( + table: TableType.units.type, + localId: id, + action: SyncAction.update.action, + data: {'uuid': id, 'name': name, 'updated_at': now.toIso8601String()}, + ).toJson(); + + final syncQueueComp = SyncQueuesCompanion( + tableType: Value(TableType.units.type), + tableLocalId: Value(id), + action: Value(SyncAction.update.action), + status: Value(SyncStatus.dirty.status), + payload: Value(payload), + ); + + final existing = + await (select(syncQueues)..where( + (t) => + t.tableType.equals(TableType.units.type) & + t.tableLocalId.equals(id), + )) + .getSingleOrNull(); + + if (existing != null) { + await (update( + syncQueues, + )..where((q) => q.id.equals(existing.id))).write(syncQueueComp); + } else { + await into(syncQueues).insert(syncQueueComp); + } + return rowsAffected; + }); + } +} diff --git a/Mobile Operasional/lib/core/database/daos/unit_dao.g.dart b/Mobile Operasional/lib/core/database/daos/unit_dao.g.dart new file mode 100644 index 0000000..31f9d67 --- /dev/null +++ b/Mobile Operasional/lib/core/database/daos/unit_dao.g.dart @@ -0,0 +1,19 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'unit_dao.dart'; + +// ignore_for_file: type=lint +mixin _$UnitDaoMixin on DatabaseAccessor { + $UnitsTable get units => attachedDatabase.units; + $SyncQueuesTable get syncQueues => attachedDatabase.syncQueues; + UnitDaoManager get managers => UnitDaoManager(this); +} + +class UnitDaoManager { + final _$UnitDaoMixin _db; + UnitDaoManager(this._db); + $$UnitsTableTableManager get units => + $$UnitsTableTableManager(_db.attachedDatabase, _db.units); + $$SyncQueuesTableTableManager get syncQueues => + $$SyncQueuesTableTableManager(_db.attachedDatabase, _db.syncQueues); +} diff --git a/Mobile Operasional/lib/core/database/daos/user_dao.dart b/Mobile Operasional/lib/core/database/daos/user_dao.dart new file mode 100644 index 0000000..ce9acfe --- /dev/null +++ b/Mobile Operasional/lib/core/database/daos/user_dao.dart @@ -0,0 +1,396 @@ +import 'package:drift/drift.dart'; +import 'package:flutter/material.dart'; +import 'package:niogu_app/core/database/app_database.dart'; +import 'package:niogu_app/core/database/tables/outlets.dart'; +import 'package:niogu_app/core/database/tables/sync_queues.dart'; +import 'package:niogu_app/core/database/tables/tenants.dart'; +import 'package:niogu_app/core/database/tables/users.dart'; +import 'package:niogu_app/core/entities/custom_activity.dart'; +import 'package:niogu_app/core/entities/custom_sync_queue.dart'; +import 'package:niogu_app/core/enums/table_type.dart'; +import 'package:niogu_app/core/models/outlet_model.dart'; +import 'package:niogu_app/core/models/staff_model.dart'; +import 'package:niogu_app/core/models/tenant_model.dart'; +import 'package:niogu_app/core/models/user_model.dart'; +import 'package:niogu_app/core/utils/bcrypt.dart'; +import 'package:niogu_app/core/utils/extension_format.dart'; +import 'package:niogu_app/core/enums/message_activity_type.dart'; +import 'package:niogu_app/core/enums/sync_status.dart'; +import 'package:niogu_app/core/system/system_setting.dart'; +import 'package:niogu_app/core/utils/log_message.dart'; +import 'package:niogu_app/core/utils/time_zone.dart'; +import 'package:niogu_app/core/enums/user_role.dart'; +import 'package:niogu_app/features/profile/domain/entities/profile.dart'; +import 'package:niogu_app/features/user/domain/entities/user.dart'; + +part 'user_dao.g.dart'; + +@DriftAccessor(tables: [Tenants, Outlets, Users, SyncQueues]) +class UserDao extends DatabaseAccessor with _$UserDaoMixin { + UserDao(AppDatabase db) : super(db); + + Future login({ + required TenantModel tenantModel, + required List outletModels, + required UserModel userModel, + required List staffModels, + String? localLogoPath, + required Map localBannerPaths, + required String password, + }) async { + try { + await transaction(() async { + await batch((batch) { + final tenantComp = tenantModel.toDriftCompanion( + localLogoPath: localLogoPath, + ); + + batch.insert( + tenants, + tenantComp, + onConflict: DoUpdate((_) => tenantComp), + ); + + for (final outletModel in outletModels) { + final localBannerPath = localBannerPaths[outletModel.serverId]; + + final outletComp = outletModel.toDriftCompanion( + localBannerPath: localBannerPath, + ); + + batch.insert( + outlets, + outletComp, + onConflict: DoUpdate((_) => outletComp), + ); + } + + final userComp = userModel.toDriftCompanion( + Bcrypt.hashPassword(password), + ); + + batch.insert(users, userComp, onConflict: DoUpdate((_) => userComp)); + + for (final staff in staffModels) { + final staffComp = staff.toDriftCompanion(); + batch.insert( + users, + staffComp, + onConflict: DoUpdate((_) => staffComp), + ); + } + }); + }); + } catch (e, st) { + LogMessage.log.e(e.toString(), error: e, stackTrace: st); + rethrow; + } + } + + Stream watchShiftInfo() async* { + final currentUserId = await SystemSetting.getCurrentUserId(); + + final query = selectOnly(users) + ..addColumns([users.shiftName, users.shiftStartTime, users.shiftEndTime]) + ..where(users.localId.equals(currentUserId!)) + ..limit(1); + + yield* query.watchSingle().map((row) { + return ShiftInfo( + shiftName: row.read(users.shiftName)!, + shiftStartTime: row.read(users.shiftStartTime)!, + shiftEndTime: row.read(users.shiftEndTime)!, + ); + }); + } + + Stream watchProfileCompleteness() async* { + final currentUserId = await SystemSetting.getCurrentUserId(); + + final query = selectOnly(users) + ..addColumns([users.name, users.placeOfBirth, users.dateOfBirth]) + ..where(users.localId.equals(currentUserId!)) + ..limit(1); + + yield* query.watchSingle().map((row) { + double percent = 0.6; + + final placeOfBirth = row.read(users.placeOfBirth); + + final dateOfBirth = row.read(users.dateOfBirth); + + if (placeOfBirth != null && placeOfBirth.isNotEmpty) { + percent += 0.2; + } + + if (dateOfBirth != null && dateOfBirth.isNotEmpty) { + percent += 0.2; + } + + return percent; + }); + } + + Future getProfileInfo() async { + final currentUserId = await SystemSetting.getCurrentUserId(); + + final query = selectOnly(users) + ..addColumns([ + users.name, + users.email, + users.phoneNumber, + users.placeOfBirth, + users.dateOfBirth, + ]) + ..where(users.localId.equals(currentUserId!)) + ..limit(1); + + final result = await query.getSingle(); + + return ProfileInfo( + name: result.read(users.name) ?? '', + email: result.read(users.email) ?? '', + phoneNumber: result.read(users.phoneNumber) ?? '', + placeOfBirth: result.read(users.placeOfBirth), + dateOfBirth: result.read(users.dateOfBirth), + ); + } + + Future updateProfile(UpsertProfile profile) async { + return await transaction(() async { + final now = DateTime.now(); + + final currentUserId = await SystemSetting.getCurrentUserId(); + + final rowsAffected = + (await update(users) + ..where((users) => users.localId.equals(currentUserId!))) + .write( + UsersCompanion( + name: Value(profile.name), + placeOfBirth: Value(profile.placeOfBirth), + dateOfBirth: Value(profile.dateOfBirth), + syncStatus: Value(SyncStatus.dirty.status), + updatedAt: Value(now), + ), + ); + + final payload = SyncPayload( + table: TableType.users.type, + localId: currentUserId!, + action: SyncAction.update.action, + data: { + 'uuid': currentUserId, + 'name': profile.name, + 'place_of_birth': profile.placeOfBirth, + 'date_of_birth': profile.dateOfBirth, + 'created_at': now.toIso8601String(), + 'updated_at': now.toIso8601String(), + }, + ).toJson(); + + final syncQueueComp = SyncQueuesCompanion( + tableType: Value(TableType.users.type), + tableLocalId: Value(currentUserId), + action: Value(SyncAction.update_or_create.action), + status: Value(SyncStatus.dirty.status), + payload: Value(payload), + ); + + final existing = + await (select(syncQueues)..where( + (q) => + q.tableType.equals(TableType.users.type) & + q.tableLocalId.equals(currentUserId), + )) + .getSingleOrNull(); + + if (existing != null) { + await (update( + syncQueues, + )..where((q) => q.id.equals(existing.id))).write(syncQueueComp); + } else { + await into(syncQueues).insert(syncQueueComp); + } + + return rowsAffected; + }); + } + + Future validatePassword(String inputPassword) async { + final currentUserId = await SystemSetting.getCurrentUserId(); + + final query = selectOnly(users) + ..addColumns([users.passwordHash]) + ..where(users.localId.equals(currentUserId!)) + ..limit(1); + + final result = await query.getSingle(); + + final passwordHash = result.read(users.passwordHash)!; + + return Bcrypt.verifyPassword(inputPassword, passwordHash); + } + + Future updatePassword(String newPassword) async { + return await transaction(() async { + final now = DateTime.now(); + + final currentUserId = await SystemSetting.getCurrentUserId(); + + final rowsAffected = + (await update(users) + ..where((user) => user.localId.equals(currentUserId!))) + .write( + UsersCompanion( + passwordHash: Value(Bcrypt.hashPassword(newPassword)), + syncStatus: Value(SyncStatus.dirty.status), + updatedAt: Value(now), + ), + ); + + final payload = SyncPayload( + table: TableType.users.type, + localId: currentUserId!, + action: SyncAction.update.action, + data: { + 'uuid': currentUserId, + 'password_hash': Bcrypt.hashPassword(newPassword), + 'created_at': now.toIso8601String(), + 'updated_at': now.toIso8601String(), + }, + ).toJson(); + + final syncQueueComp = SyncQueuesCompanion( + tableType: Value(TableType.users.type), + tableLocalId: Value(currentUserId), + action: Value(SyncAction.update_or_create.action), + status: Value(SyncStatus.dirty.status), + payload: Value(payload), + ); + + final existing = + await (select(syncQueues)..where( + (q) => + q.tableType.equals(TableType.users.type) & + q.tableLocalId.equals(currentUserId), + )) + .getSingleOrNull(); + + if (existing != null) { + await (update( + syncQueues, + )..where((q) => q.id.equals(existing.id))).write(syncQueueComp); + } else { + await into(syncQueues).insert(syncQueueComp); + } + + return rowsAffected; + }); + } + + TimeOfDay _parseTimeOfDay(String timeString) { + List parts = timeString.split('.'); + + int hour = int.parse(parts[0]); + int minute = int.parse(parts[1]); + + return TimeOfDay(hour: hour, minute: minute); + } + + Stream checkPermissionActivity() async* { + final userId = await SystemSetting.getCurrentUserId(); + + final userRole = await SystemSetting.getCurrentUserRole(); + + final outletId = await SystemSetting.getCurrentOutletId(); + + final query = + selectOnly( + outlets, + ).join([innerJoin(users, users.outletId.equalsExp(outlets.localId))]) + ..addColumns([ + outlets.name, + outlets.isActive, + users.isActive, + users.shiftName, + users.shiftStartTime, + users.shiftEndTime, + outlets.deletedAt, + users.deletedAt, + ]) + ..orderBy([OrderingTerm.asc(users.createdAt)]) + ..where(outlets.localId.equals(outletId!)) + ..where( + userRole == UserRole.admin + ? users.localId.equals(userId!) + : const Constant(true), + ) + ..limit(1); + yield* query.watch().map((rows) { + final currentTime = TimeOfDay.now(); + + for (final row in rows) { + final String name = row.read(outlets.name) ?? ''; + + final bool isOutletDeleted = row.read(outlets.deletedAt) != null; + + if (isOutletDeleted) + return MessageActivity( + type: MessageActivityType.lock_activity, + messages: ["Outlet ini ($name) telah di hapus"], + ); + + final bool isOutletActive = row.read(outlets.isActive) ?? false; + + if (!isOutletActive) + return MessageActivity( + type: MessageActivityType.lock_activity, + messages: ["Outlet ini ($name) telah di non aktifkan / tutup"], + ); + + if (userRole == UserRole.owner) return null; + + final bool isUserDeleted = row.read(users.deletedAt) != null; + + if (isUserDeleted) + return MessageActivity( + type: MessageActivityType.lock_activity, + messages: ["Anda telah diberhetikan sebagai admin"], + ); + + final bool isUserActive = row.read(outlets.isActive) ?? false; + + if (!isUserActive) + return MessageActivity( + type: MessageActivityType.lock_activity, + messages: ["Anda di non aktifkan sementara oleh pemilik"], + ); + + final String shiftName = row.read(users.shiftName) ?? ''; + + final TimeOfDay shiftStartTime = _parseTimeOfDay( + (row.read(users.shiftStartTime) ?? ''), + ); + + final TimeOfDay shiftEndTime = _parseTimeOfDay( + (row.read(users.shiftEndTime) ?? ''), + ); + + final hasShift = + currentTime.isAfterOrEqual(shiftStartTime) && + currentTime.isBefore(shiftEndTime); + + if (!hasShift) + return MessageActivity( + type: MessageActivityType.lock_activity, + messages: [ + "Saat ini belum shift kerja anda, shift kerja anda ($shiftName) di pukul ${(row.read(users.shiftStartTime))} ${TimeZone.getCurrentTimeZone()} - ${(row.read(users.shiftEndTime))} ${TimeZone.getCurrentTimeZone()}", + ], + ); + } + + return null; + }); + } +} diff --git a/Mobile Operasional/lib/core/database/daos/user_dao.g.dart b/Mobile Operasional/lib/core/database/daos/user_dao.g.dart new file mode 100644 index 0000000..d89d77a --- /dev/null +++ b/Mobile Operasional/lib/core/database/daos/user_dao.g.dart @@ -0,0 +1,25 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'user_dao.dart'; + +// ignore_for_file: type=lint +mixin _$UserDaoMixin on DatabaseAccessor { + $TenantsTable get tenants => attachedDatabase.tenants; + $OutletsTable get outlets => attachedDatabase.outlets; + $UsersTable get users => attachedDatabase.users; + $SyncQueuesTable get syncQueues => attachedDatabase.syncQueues; + UserDaoManager get managers => UserDaoManager(this); +} + +class UserDaoManager { + final _$UserDaoMixin _db; + UserDaoManager(this._db); + $$TenantsTableTableManager get tenants => + $$TenantsTableTableManager(_db.attachedDatabase, _db.tenants); + $$OutletsTableTableManager get outlets => + $$OutletsTableTableManager(_db.attachedDatabase, _db.outlets); + $$UsersTableTableManager get users => + $$UsersTableTableManager(_db.attachedDatabase, _db.users); + $$SyncQueuesTableTableManager get syncQueues => + $$SyncQueuesTableTableManager(_db.attachedDatabase, _db.syncQueues); +} diff --git a/Mobile Operasional/lib/core/database/database_holder.dart b/Mobile Operasional/lib/core/database/database_holder.dart new file mode 100644 index 0000000..a37e2bf --- /dev/null +++ b/Mobile Operasional/lib/core/database/database_holder.dart @@ -0,0 +1,19 @@ +import 'package:niogu_app/core/database/app_database.dart'; + +AppDatabase? _db; + +AppDatabase get database { + if (_db == null) { + throw Exception("Database belum diinisialisasi"); + } + return _db!; +} + +Future setDatabase(AppDatabase db) async { + _db = db; +} + +Future clearDatabase() async { + await _db?.close(); + _db = null; +} \ No newline at end of file diff --git a/Mobile Operasional/lib/core/database/tables/campaigns.dart b/Mobile Operasional/lib/core/database/tables/campaigns.dart new file mode 100644 index 0000000..e5a7d51 --- /dev/null +++ b/Mobile Operasional/lib/core/database/tables/campaigns.dart @@ -0,0 +1,23 @@ +import 'package:drift/drift.dart'; +import 'package:uuid/uuid.dart'; + +class Campaigns extends Table { + TextColumn get localId => text().clientDefault(() => const Uuid().v7().toString())(); + TextColumn get serverId => text().nullable()(); + TextColumn get title => text()(); + TextColumn get localBannerPath => text()(); + TextColumn get serverBannerUrl => text().nullable()(); + DateTimeColumn get startDate => dateTime().nullable()(); + DateTimeColumn get endDate => dateTime().nullable()(); + TextColumn get actionType => text().nullable()(); + TextColumn get actionReferenceId => text().nullable()(); + BoolColumn get isActive => boolean().withDefault(const Constant(true))(); + TextColumn get syncStatus => text().withDefault(const Constant('dirty'))(); + DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)(); + DateTimeColumn get updatedAt => dateTime().withDefault(currentDateAndTime)(); + DateTimeColumn get deletedAt => dateTime().nullable()(); + + @override + // TODO: implement primaryKey + Set>? get primaryKey => {localId}; +} \ No newline at end of file diff --git a/Mobile Operasional/lib/core/database/tables/categories.dart b/Mobile Operasional/lib/core/database/tables/categories.dart new file mode 100644 index 0000000..2725f28 --- /dev/null +++ b/Mobile Operasional/lib/core/database/tables/categories.dart @@ -0,0 +1,17 @@ +import 'package:drift/drift.dart'; +import 'package:uuid/uuid.dart'; + +class Categories extends Table { + TextColumn get localId => text().clientDefault(() => const Uuid().v7().toString())(); + TextColumn get serverId => text().nullable()(); + TextColumn get name => text().unique()(); + TextColumn get localImagePath => text().nullable()(); + TextColumn get serverImageUrl => text().nullable()(); + TextColumn get syncStatus => text().withDefault(const Constant('dirty'))(); + DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)(); + DateTimeColumn get updatedAt => dateTime().withDefault(currentDateAndTime)(); + + @override + // TODO: implement primaryKey + Set>? get primaryKey => {localId}; +} diff --git a/Mobile Operasional/lib/core/database/tables/customer_addresses.dart b/Mobile Operasional/lib/core/database/tables/customer_addresses.dart new file mode 100644 index 0000000..551699c --- /dev/null +++ b/Mobile Operasional/lib/core/database/tables/customer_addresses.dart @@ -0,0 +1,22 @@ +import 'package:drift/drift.dart'; +import 'package:niogu_app/core/database/tables/customers.dart'; +import 'package:uuid/uuid.dart'; + +class CustomerAddresses extends Table { + TextColumn get localId => text().clientDefault(() => const Uuid().v7().toString())(); + TextColumn get serverId => text().nullable()(); + TextColumn get customerId => text().references(Customers, #localId)(); + TextColumn get label => text().nullable()(); + TextColumn get fullAddress => text().nullable()(); + RealColumn get latitude => real().nullable()(); + RealColumn get longitude => real().nullable()(); + TextColumn get notes => text().nullable()(); + TextColumn get syncStatus => text().withDefault(const Constant('dirty'))(); + DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)(); + DateTimeColumn get updatedAt => dateTime().withDefault(currentDateAndTime)(); + DateTimeColumn get deletedAt => dateTime().nullable()(); + + @override + // TODO: implement primaryKey + Set>? get primaryKey => {localId}; +} \ No newline at end of file diff --git a/Mobile Operasional/lib/core/database/tables/customers.dart b/Mobile Operasional/lib/core/database/tables/customers.dart new file mode 100644 index 0000000..557a8aa --- /dev/null +++ b/Mobile Operasional/lib/core/database/tables/customers.dart @@ -0,0 +1,22 @@ +import 'package:drift/drift.dart'; +import 'package:niogu_app/core/database/tables/outlets.dart'; +import 'package:uuid/uuid.dart'; + +class Customers extends Table { + TextColumn get localId => text().clientDefault(() => const Uuid().v7().toString())(); + TextColumn get serverId => text().nullable()(); + TextColumn get outletId => text().references(Outlets, #localId)(); + TextColumn get name => text()(); + TextColumn get email => text().nullable()(); + TextColumn get phoneNumber => text().nullable()(); + TextColumn get passwordHash => text().nullable()(); + TextColumn get source => text()(); + TextColumn get syncStatus => text().withDefault(const Constant('dirty'))(); + DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)(); + DateTimeColumn get updatedAt => dateTime().withDefault(currentDateAndTime)(); + DateTimeColumn get deletedAt => dateTime().nullable()(); + + @override + // TODO: implement primaryKey + Set>? get primaryKey => {localId}; +} \ No newline at end of file diff --git a/Mobile Operasional/lib/core/database/tables/order_items.dart b/Mobile Operasional/lib/core/database/tables/order_items.dart new file mode 100644 index 0000000..e1b0887 --- /dev/null +++ b/Mobile Operasional/lib/core/database/tables/order_items.dart @@ -0,0 +1,25 @@ +import 'package:drift/drift.dart'; +import 'package:niogu_app/core/database/tables/orders.dart'; +import 'package:niogu_app/core/database/tables/product_variants.dart'; +import 'package:uuid/uuid.dart'; + +class OrderItems extends Table { + TextColumn get localId => text().clientDefault(() => const Uuid().v7().toString())(); + TextColumn get serverId => text().nullable()(); + TextColumn get orderId => text().references(Orders, #localId)(); + TextColumn get productVariantId => text().references(ProductVariants, #localId)(); + RealColumn get quantity => real().withDefault(const Constant(0))(); + TextColumn get productImageSnapshot => text().nullable()(); + TextColumn get productImageUrlSnapshot => text().nullable()(); + TextColumn get productNameSnapshot => text()(); + TextColumn get productVariantNameSnapshot => text().nullable()(); + RealColumn get sellingPriceSnapshot => real().withDefault(const Constant(0))(); + RealColumn get subtotal => real().withDefault(const Constant(0))(); + TextColumn get syncStatus => text().withDefault(const Constant('dirty'))(); + DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)(); + DateTimeColumn get updatedAt => dateTime().withDefault(currentDateAndTime)(); + + @override + // TODO: implement primaryKey + Set>? get primaryKey => {localId}; +} \ No newline at end of file diff --git a/Mobile Operasional/lib/core/database/tables/orders.dart b/Mobile Operasional/lib/core/database/tables/orders.dart new file mode 100644 index 0000000..5201e58 --- /dev/null +++ b/Mobile Operasional/lib/core/database/tables/orders.dart @@ -0,0 +1,48 @@ +import 'package:drift/drift.dart'; +import 'package:niogu_app/core/database/tables/customers.dart'; +import 'package:niogu_app/core/database/tables/outlets.dart'; +import 'package:niogu_app/core/database/tables/users.dart'; +import 'package:niogu_app/core/utils/string_map_converter.dart'; +import 'package:uuid/uuid.dart'; + +class Orders extends Table { + TextColumn get localId => text().clientDefault(() => const Uuid().v7().toString())(); + TextColumn get serverId => text().nullable()(); + TextColumn get orderNumber => text().unique()(); + TextColumn get outletId => text().references(Outlets, #localId)(); + TextColumn get outletNameSnapshot => text()(); + TextColumn get customerId => text().references(Customers, #localId).nullable()(); + TextColumn get customerNameSnapshot => text().nullable()(); + TextColumn get customerEmailSnapshot => text().nullable()(); + TextColumn get customerPhoneNumberSnapshot => text().nullable()(); + TextColumn get userId => text().references(Users, #localId)(); + TextColumn get adminNameSnapshot => text()(); + TextColumn get source => text()(); + BoolColumn get alreadyRead => boolean().nullable()(); + TextColumn get orderStatus => text()(); + BoolColumn get isCancellation => boolean().nullable()(); + BoolColumn get cancellationAccepted => boolean().nullable()(); + TextColumn get approvalProccess => text().nullable()(); + TextColumn get canceledBy => text().nullable()(); + TextColumn get deliveryType => text().nullable()(); + TextColumn get deliveryPrefrence => text().nullable()(); + RealColumn get shippingCost => real().withDefault(const Constant(0.0))(); + RealColumn get totalOrder => real().withDefault(const Constant(0.0))(); + RealColumn get discount => real().withDefault(const Constant(0.0))(); + RealColumn get tax => real().withDefault(const Constant(0.0))(); + RealColumn get totalAmount => real().withDefault(const Constant(0.0))(); + RealColumn get amountPaid => real().withDefault(const Constant(0.0))(); + RealColumn get changeAmount => real().withDefault(const Constant(0.0))(); + TextColumn get notes => text().nullable()(); + TextColumn get paymentProofPath => text().nullable()(); + TextColumn get paymentProofUrl => text().nullable()(); + TextColumn get paymentMethod => text().nullable()(); + TextColumn get customerAddressSnapshot => text().map(StringMapConverter()).nullable()(); + TextColumn get syncStatus => text().withDefault(const Constant('dirty'))(); + DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)(); + DateTimeColumn get updatedAt => dateTime().withDefault(currentDateAndTime)(); + + @override + // TODO: implement primaryKey + Set>? get primaryKey => {localId}; +} \ No newline at end of file diff --git a/Mobile Operasional/lib/core/database/tables/outlet_campaigns.dart b/Mobile Operasional/lib/core/database/tables/outlet_campaigns.dart new file mode 100644 index 0000000..469a2fb --- /dev/null +++ b/Mobile Operasional/lib/core/database/tables/outlet_campaigns.dart @@ -0,0 +1,19 @@ +import 'package:drift/drift.dart'; +import 'package:niogu_app/core/database/tables/campaigns.dart'; +import 'package:niogu_app/core/database/tables/outlets.dart'; +import 'package:uuid/uuid.dart'; + +class OutletCampaigns extends Table { + TextColumn get localId => text().clientDefault(() => const Uuid().v7().toString())(); + TextColumn get serverId => text().nullable()(); + TextColumn get campaignId => text().references(Campaigns, #localId)(); + TextColumn get outletId => text().references(Outlets, #localId).nullable()(); + TextColumn get syncStatus => text().withDefault(const Constant('dirty'))(); + DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)(); + DateTimeColumn get updatedAt => dateTime().withDefault(currentDateAndTime)(); + DateTimeColumn get deletedAt => dateTime().nullable()(); + + @override + // TODO: implement primaryKey + Set>? get primaryKey => {localId}; +} \ No newline at end of file diff --git a/Mobile Operasional/lib/core/database/tables/outlet_inventories.dart b/Mobile Operasional/lib/core/database/tables/outlet_inventories.dart new file mode 100644 index 0000000..7a9b8db --- /dev/null +++ b/Mobile Operasional/lib/core/database/tables/outlet_inventories.dart @@ -0,0 +1,25 @@ +import 'package:drift/drift.dart'; +import 'package:niogu_app/core/database/tables/outlets.dart'; +import 'package:uuid/uuid.dart'; + +class OutletInventories extends Table { + TextColumn get localId => text().clientDefault(() => const Uuid().v7().toString())(); + TextColumn get serverId => text().nullable()(); + TextColumn get outletId => text().references(Outlets, #localId)(); + TextColumn get itemType => text()(); + TextColumn get itemId => text()(); + TextColumn get stockType => text().withDefault(const Constant("fixed"))(); + RealColumn get stock => real().withDefault(const Constant(0.0))(); + RealColumn get sellingPrice => real().withDefault(const Constant(0.0))(); + RealColumn get costPrice => real().withDefault(const Constant(0.0))(); + RealColumn get purchasePrice => real().withDefault(const Constant(0.0))(); + BoolColumn get isDeleteByOwner => boolean().nullable()(); + TextColumn get syncStatus => text().withDefault(const Constant("dirty"))(); + DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)(); + DateTimeColumn get updatedAt => dateTime().withDefault(currentDateAndTime)(); + DateTimeColumn get deletedAt => dateTime().nullable()(); + + @override + // TODO: implement primaryKey + Set>? get primaryKey => {localId}; +} \ No newline at end of file diff --git a/Mobile Operasional/lib/core/database/tables/outlets.dart b/Mobile Operasional/lib/core/database/tables/outlets.dart new file mode 100644 index 0000000..4451043 --- /dev/null +++ b/Mobile Operasional/lib/core/database/tables/outlets.dart @@ -0,0 +1,26 @@ +import 'package:drift/drift.dart'; +import 'package:uuid/uuid.dart'; + +class Outlets extends Table { + TextColumn get localId => text().clientDefault(() => const Uuid().v7().toString())(); + TextColumn get serverId => text().nullable()(); + TextColumn get name => text()(); + TextColumn get email => text().nullable()(); + TextColumn get phoneNumber => text().nullable()(); + TextColumn get fullAddress => text().nullable()(); + RealColumn get latitude => real().nullable()(); + RealColumn get longitude => real().nullable()(); + TextColumn get localPhotoPath => text().nullable()(); + TextColumn get localBannerPath => text().nullable()(); + TextColumn get serverPhotoUrl => text().nullable()(); + TextColumn get serverBannerUrl => text().nullable()(); + BoolColumn get isMainOutlet => boolean().withDefault(const Constant(false))(); + BoolColumn get isActive => boolean().withDefault(const Constant(true))(); + TextColumn get syncStatus => text().withDefault(const Constant('dirty'))(); + DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)(); + DateTimeColumn get updatedAt => dateTime().withDefault(currentDateAndTime)(); + DateTimeColumn get deletedAt => dateTime().nullable()(); + + @override + Set>? get primaryKey => {localId}; +} diff --git a/Mobile Operasional/lib/core/database/tables/product_likes.dart b/Mobile Operasional/lib/core/database/tables/product_likes.dart new file mode 100644 index 0000000..36df054 --- /dev/null +++ b/Mobile Operasional/lib/core/database/tables/product_likes.dart @@ -0,0 +1,19 @@ +import 'package:drift/drift.dart'; +import 'package:niogu_app/core/database/tables/customers.dart'; +import 'package:niogu_app/core/database/tables/products.dart'; +import 'package:uuid/uuid.dart'; + +class ProductLikes extends Table { + TextColumn get localId => text().clientDefault(() => const Uuid().v7().toString())(); + TextColumn get serverId => text().nullable()(); + TextColumn get customerId => text().references(Customers, #localId).nullable()(); + TextColumn get productId => text().references(Products, #localId)(); + BoolColumn get isLike => boolean().withDefault(const Constant(false))(); + TextColumn get syncStatus => text().withDefault(const Constant('dirty'))(); + DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)(); + DateTimeColumn get updatedAt => dateTime().withDefault(currentDateAndTime)(); + + @override + // TODO: implement primaryKey + Set>? get primaryKey => {localId}; +} \ No newline at end of file diff --git a/Mobile Operasional/lib/core/database/tables/product_reviews.dart b/Mobile Operasional/lib/core/database/tables/product_reviews.dart new file mode 100644 index 0000000..7cc1b3d --- /dev/null +++ b/Mobile Operasional/lib/core/database/tables/product_reviews.dart @@ -0,0 +1,21 @@ +import 'package:drift/drift.dart'; +import 'package:niogu_app/core/database/tables/customers.dart'; +import 'package:niogu_app/core/database/tables/product_variants.dart'; +import 'package:uuid/uuid.dart'; + +class ProductReviews extends Table { + TextColumn get localId => text().clientDefault(() => const Uuid().v7().toString())(); + TextColumn get serverId => text().nullable()(); + TextColumn get customerId => text().references(Customers, #localId).nullable()(); + TextColumn get productVariantId => text().references(ProductVariants, #localId)(); + TextColumn get customerNameSnapshot => text().nullable()(); + IntColumn get rating => integer().withDefault(const Constant(0))(); + TextColumn get comment => text().nullable()(); + TextColumn get syncStatus => text().withDefault(const Constant('dirty'))(); + DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)(); + DateTimeColumn get updatedAt => dateTime().withDefault(currentDateAndTime)(); + + @override + // TODO: implement primaryKey + Set>? get primaryKey => {localId}; +} \ No newline at end of file diff --git a/Mobile Operasional/lib/core/database/tables/product_variants.dart b/Mobile Operasional/lib/core/database/tables/product_variants.dart new file mode 100644 index 0000000..c06a4a0 --- /dev/null +++ b/Mobile Operasional/lib/core/database/tables/product_variants.dart @@ -0,0 +1,25 @@ +import 'package:drift/drift.dart'; +import 'package:niogu_app/core/database/tables/products.dart'; +import 'package:uuid/uuid.dart'; + +class ProductVariants extends Table { + TextColumn get localId => text().clientDefault(() => const Uuid().v7().toString())(); + TextColumn get serverId => text().nullable()(); + TextColumn get productId => text().references(Products, #localId)(); + TextColumn get name => text()(); + TextColumn get localImagePath => text().nullable()(); + TextColumn get serverImageUrl => text().nullable()(); + BoolColumn get isVisibleOnline => + boolean().withDefault(const Constant(false))(); + IntColumn get totalRating => integer().withDefault(const Constant(0))(); + RealColumn get averageRating => real().withDefault(const Constant(0.0))(); + RealColumn get totalSold => real().withDefault(const Constant(0.0))(); + BoolColumn get isProductVariant => boolean().withDefault(const Constant(false))(); + TextColumn get syncStatus => text().withDefault(const Constant('dirty'))(); + DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)(); + DateTimeColumn get updatedAt => dateTime().withDefault(currentDateAndTime)(); + DateTimeColumn get deletedAt => dateTime().nullable()(); + + @override + Set>? get primaryKey => {localId}; +} diff --git a/Mobile Operasional/lib/core/database/tables/products.dart b/Mobile Operasional/lib/core/database/tables/products.dart new file mode 100644 index 0000000..ed01ba4 --- /dev/null +++ b/Mobile Operasional/lib/core/database/tables/products.dart @@ -0,0 +1,26 @@ +import 'package:drift/drift.dart'; +import 'package:niogu_app/core/database/tables/categories.dart'; +import 'package:niogu_app/core/database/tables/units.dart'; +import 'package:niogu_app/core/utils/string_list_converter.dart'; +import 'package:uuid/uuid.dart'; + +class Products extends Table { + TextColumn get localId => text().clientDefault(() => const Uuid().v7().toString())(); + TextColumn get serverId => text().nullable()(); + TextColumn get categoryId => text().references(Categories, #localId)(); + TextColumn get unitId => text().references(Units, #localId)(); + TextColumn get name => text()(); + TextColumn get localImagePath => + text().map(StringListConverter()).nullable()(); + TextColumn get serverImageUrl => + text().map(StringListConverter()).nullable()(); + BoolColumn get hasVariant => boolean().withDefault(const Constant(false))(); + TextColumn get syncStatus => text().withDefault(const Constant('dirty'))(); + DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)(); + DateTimeColumn get updatedAt => dateTime().withDefault(currentDateAndTime)(); + DateTimeColumn get deletedAt => dateTime().nullable()(); + + @override + // TODO: implement primaryKey + Set>? get primaryKey => {localId}; +} diff --git a/Mobile Operasional/lib/core/database/tables/purchase_items.dart b/Mobile Operasional/lib/core/database/tables/purchase_items.dart new file mode 100644 index 0000000..4e3891b --- /dev/null +++ b/Mobile Operasional/lib/core/database/tables/purchase_items.dart @@ -0,0 +1,25 @@ +import 'package:drift/drift.dart'; +import 'package:niogu_app/core/database/tables/purchases.dart'; +import 'package:uuid/uuid.dart'; + +class PurchaseItems extends Table { + TextColumn get localId => text().clientDefault(() => const Uuid().v7().toString())(); + TextColumn get serverId => text().nullable()(); + TextColumn get purchaseId => text().references(Purchases, #localId)(); + TextColumn get itemType => text()(); + TextColumn get itemId => text()(); + RealColumn get quantity => real().withDefault(const Constant(0.0))(); + TextColumn get itemImageSnapshot => text().nullable()(); + TextColumn get itemImageUrlSnapshot => text().nullable()(); + TextColumn get itemNameSnapshot => text()(); + TextColumn get itemVariantNameSnapshot => text().nullable()(); + RealColumn get purchasePriceSnaphot => real().withDefault(const Constant(0.0))(); + RealColumn get subtotal => real().withDefault(const Constant(0.0))(); + TextColumn get syncStatus => text().withDefault(const Constant('dirty'))(); + DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)(); + DateTimeColumn get updatedAt => dateTime().withDefault(currentDateAndTime)(); + + @override + // TODO: implement primaryKey + Set>? get primaryKey => {localId}; +} \ No newline at end of file diff --git a/Mobile Operasional/lib/core/database/tables/purchases.dart b/Mobile Operasional/lib/core/database/tables/purchases.dart new file mode 100644 index 0000000..6df2218 --- /dev/null +++ b/Mobile Operasional/lib/core/database/tables/purchases.dart @@ -0,0 +1,39 @@ +import 'package:drift/drift.dart'; +import 'package:niogu_app/core/database/tables/outlets.dart'; +import 'package:niogu_app/core/database/tables/suppliers.dart'; +import 'package:niogu_app/core/database/tables/users.dart'; +import 'package:uuid/uuid.dart'; + +class Purchases extends Table { + TextColumn get localId => text().clientDefault(() => const Uuid().v7().toString())(); + TextColumn get serverId => text().nullable()(); + TextColumn get purchaseNumber => text().unique()(); + TextColumn get outletId => text().references(Outlets, #localId)(); + TextColumn get outletNameSnapshot => text()(); + TextColumn get supplierId => text().references(Suppliers, #localId).nullable()(); + TextColumn get supplierNameSnapshot => text().nullable()(); + TextColumn get supplierEmailSnapshot => text().nullable()(); + TextColumn get supplierPhoneNumberSnapshot => text().nullable()(); + TextColumn get userId => text().references(Users, #localId)(); + TextColumn get adminNameSnapshot => text()(); + TextColumn get purchaseStatus => text()(); + RealColumn get totalPurchase => real().withDefault(const Constant(0.0))(); + RealColumn get discount => real().withDefault(const Constant(0.0))(); + RealColumn get tax => real().withDefault(const Constant(0.0))(); + TextColumn get paymentStatus => text()(); + RealColumn get totalAmount => real().withDefault(const Constant(0.0))(); + RealColumn get amountPaid => real().withDefault(const Constant(0.0))(); + RealColumn get changeAmount => real().withDefault(const Constant(0.0))(); + RealColumn get underPaymentAmount => real().withDefault(const Constant(0.0))(); + RealColumn get debtAmount => real().withDefault(const Constant(0.0))(); + TextColumn get paymentProofPath => text().nullable()(); + TextColumn get paymentProofUrl => text().nullable()(); + TextColumn get notes => text().nullable()(); + TextColumn get syncStatus => text().withDefault(const Constant('dirty'))(); + DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)(); + DateTimeColumn get updatedAt => dateTime().withDefault(currentDateAndTime)(); + + @override + // TODO: implement primaryKey + Set>? get primaryKey => {localId}; +} \ No newline at end of file diff --git a/Mobile Operasional/lib/core/database/tables/raw_materials.dart b/Mobile Operasional/lib/core/database/tables/raw_materials.dart new file mode 100644 index 0000000..01a4728 --- /dev/null +++ b/Mobile Operasional/lib/core/database/tables/raw_materials.dart @@ -0,0 +1,18 @@ +import 'package:drift/drift.dart'; +import 'package:niogu_app/core/database/tables/units.dart'; +import 'package:uuid/uuid.dart'; + +class RawMaterials extends Table { + TextColumn get localId => text().clientDefault(() => const Uuid().v7().toString())(); + TextColumn get serverId => text().nullable()(); + TextColumn get unitId => text().references(Units, #localId)(); + TextColumn get name => text()(); + TextColumn get syncStatus => text().withDefault(const Constant('dirty'))(); + DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)(); + DateTimeColumn get updatedAt => dateTime().withDefault(currentDateAndTime)(); + DateTimeColumn get deletedAt => dateTime().nullable()(); + + @override + // TODO: implement primaryKey + Set>? get primaryKey => {localId}; +} \ No newline at end of file diff --git a/Mobile Operasional/lib/core/database/tables/receipt_items.dart b/Mobile Operasional/lib/core/database/tables/receipt_items.dart new file mode 100644 index 0000000..e4cb344 --- /dev/null +++ b/Mobile Operasional/lib/core/database/tables/receipt_items.dart @@ -0,0 +1,20 @@ +import 'package:drift/drift.dart'; +import 'package:niogu_app/core/database/tables/product_variants.dart'; +import 'package:niogu_app/core/database/tables/raw_materials.dart'; +import 'package:uuid/uuid.dart'; + +class ReceiptItems extends Table { + TextColumn get localId => text().clientDefault(() => const Uuid().v7().toString())(); + TextColumn get serverId => text().nullable()(); + TextColumn get productVariantId => text().references(ProductVariants, #localId).nullable()(); + TextColumn get rawMaterialId => text().references(RawMaterials, #localId).nullable()(); + RealColumn get quantity => real().withDefault(const Constant(0.0))(); + TextColumn get syncStatus => text().withDefault(const Constant('dirty'))(); + DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)(); + DateTimeColumn get updatedAt => dateTime().withDefault(currentDateAndTime)(); + DateTimeColumn get deletedAt => dateTime().nullable()(); + + @override + // TODO: implement primaryKey + Set>? get primaryKey => {localId}; +} \ No newline at end of file diff --git a/Mobile Operasional/lib/core/database/tables/stock_cards.dart b/Mobile Operasional/lib/core/database/tables/stock_cards.dart new file mode 100644 index 0000000..0a9a9d0 --- /dev/null +++ b/Mobile Operasional/lib/core/database/tables/stock_cards.dart @@ -0,0 +1,32 @@ +import 'package:drift/drift.dart'; +import 'package:niogu_app/core/database/tables/outlet_inventories.dart'; +import 'package:niogu_app/core/database/tables/outlets.dart'; +import 'package:niogu_app/core/database/tables/users.dart'; +import 'package:uuid/uuid.dart'; + +class StockCards extends Table { + TextColumn get localId => text().clientDefault(() => const Uuid().v7().toString())(); + TextColumn get serverId => text().nullable()(); + TextColumn get outletInventoryId => + text().references(OutletInventories, #localId)(); + TextColumn get userId => text().references(Users, #localId)(); + TextColumn get type => text()(); + TextColumn get receiveFromId => + text().references(Outlets, #localId).nullable()(); + RealColumn get qtyChange => real().withDefault(const Constant(0.0))(); + RealColumn get currentStock => real().withDefault(const Constant(0.0))(); + TextColumn get stockType => text().withDefault(const Constant("fixed"))(); + BoolColumn get modeChange => boolean().withDefault(const Constant(false))(); + TextColumn get modeBeforeChange => text().nullable()(); + TextColumn get modeAfterChange => text().nullable()(); + RealColumn get costPrice => real().withDefault(const Constant(0.0))(); + TextColumn get referenceId => text().nullable()(); + TextColumn get documentNumber => text().nullable()(); + TextColumn get notes => text().nullable()(); + TextColumn get syncStatus => text().withDefault(const Constant("dirty"))(); + DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)(); + + @override + // TODO: implement primaryKey + Set>? get primaryKey => {localId}; +} diff --git a/Mobile Operasional/lib/core/database/tables/suppliers.dart b/Mobile Operasional/lib/core/database/tables/suppliers.dart new file mode 100644 index 0000000..e9043e8 --- /dev/null +++ b/Mobile Operasional/lib/core/database/tables/suppliers.dart @@ -0,0 +1,19 @@ +import 'package:drift/drift.dart'; +import 'package:uuid/uuid.dart'; + +class Suppliers extends Table { + TextColumn get localId => text().clientDefault(() => const Uuid().v7().toString())(); + TextColumn get serverId => text().nullable()(); + TextColumn get name => text()(); + TextColumn get email => text().nullable()(); + TextColumn get phoneNumber => text().nullable()(); + TextColumn get address => text().nullable()(); + TextColumn get syncStatus => text().withDefault(const Constant('dirty'))(); + DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)(); + DateTimeColumn get updatedAt => dateTime().withDefault(currentDateAndTime)(); + DateTimeColumn get deletedAt => dateTime().nullable()(); + + @override + // TODO: implement primaryKey + Set>? get primaryKey => {localId}; +} \ No newline at end of file diff --git a/Mobile Operasional/lib/core/database/tables/sync_queues.dart b/Mobile Operasional/lib/core/database/tables/sync_queues.dart new file mode 100644 index 0000000..1d23321 --- /dev/null +++ b/Mobile Operasional/lib/core/database/tables/sync_queues.dart @@ -0,0 +1,16 @@ +import 'package:drift/drift.dart'; +import 'package:niogu_app/core/utils/string_map_converter.dart'; + +class SyncQueues extends Table { + IntColumn get id => integer().autoIncrement()(); + TextColumn get tableType => text()(); + TextColumn get tableLocalId => text()(); + TextColumn get action => text()(); + TextColumn get status => text()(); + TextColumn get payload => text().map(StringMapConverter())(); + TextColumn get batchId => text().nullable()(); + TextColumn get lastError => text().nullable()(); + IntColumn get retryCount => integer().withDefault(const Constant(0))(); + DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)(); + DateTimeColumn get lastAttempt => dateTime().nullable()(); +} diff --git a/Mobile Operasional/lib/core/database/tables/tenant_payment_methods.dart b/Mobile Operasional/lib/core/database/tables/tenant_payment_methods.dart new file mode 100644 index 0000000..3cc1bf1 --- /dev/null +++ b/Mobile Operasional/lib/core/database/tables/tenant_payment_methods.dart @@ -0,0 +1,17 @@ +import 'package:drift/drift.dart'; +import 'package:uuid/uuid.dart'; + +class TenantPaymentMethods extends Table { + TextColumn get localId => text().clientDefault(() => const Uuid().v7().toString())(); + TextColumn get serverId => text().nullable()(); + TextColumn get sourceName => text().nullable()(); + TextColumn get accountNumber => text().nullable()(); + TextColumn get receipentName => text().nullable()(); + TextColumn get syncStatus => text().withDefault(const Constant('dirty'))(); + DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)(); + DateTimeColumn get updatedAt => dateTime().withDefault(currentDateAndTime)(); + + @override + // TODO: implement primaryKey + Set>? get primaryKey => {localId}; +} diff --git a/Mobile Operasional/lib/core/database/tables/tenants.dart b/Mobile Operasional/lib/core/database/tables/tenants.dart new file mode 100644 index 0000000..42c5ffa --- /dev/null +++ b/Mobile Operasional/lib/core/database/tables/tenants.dart @@ -0,0 +1,39 @@ +import 'package:drift/drift.dart'; +import 'package:uuid/uuid.dart'; + +class Tenants extends Table { + TextColumn get localId => text().clientDefault(() => const Uuid().v7().toString())(); + TextColumn get serverId => text().nullable()(); + TextColumn get businessCode => text().unique()(); + TextColumn get businessName => text()(); + TextColumn get businessEmail => text().unique().nullable()(); + TextColumn get businessPhone => text().unique().nullable()(); + TextColumn get businessType => text().nullable()() ; + TextColumn get localLogoPath => text().nullable()(); + TextColumn get serverLogoUrl => text().nullable()(); + TextColumn get subdomain => text().unique().nullable()(); + TextColumn get onlinStoreName => text().nullable()(); + TextColumn get onlineOpenTime => text().nullable()(); + TextColumn get onlineCloseTime => text().nullable()(); + BoolColumn get isCloseService => boolean().withDefault(const Constant(false))(); + IntColumn get preparationTimeMinutes => integer().withDefault(const Constant(0))(); + IntColumn get orderIntervalMinutes => integer().withDefault(const Constant(0))(); + BoolColumn get allowCod => boolean().withDefault(const Constant(false))(); + RealColumn get maxDeliveryRadiusKm => real().withDefault(const Constant(0.0))(); + TextColumn get deliveryFeeType => text().withDefault(const Constant('fixed'))(); + RealColumn get deliveryFlatFee => real().withDefault(const Constant(0.0))(); + RealColumn get deliveryFeePerKm => real().withDefault(const Constant(0.0))(); + RealColumn get deliveryBaseFee => real().withDefault(const Constant(0.0))(); + TextColumn get reviewDisplayType => text().withDefault(const Constant('rating_only'))(); + TextColumn get reviewDisplayPolicy => text().withDefault(const Constant('four_and_five_star_only'))(); + TextColumn get subscriptionLevel => text().withDefault(const Constant('standart'))(); + IntColumn get limitOutlet => integer().withDefault(const Constant(5))(); + DateTimeColumn get subscriptionExpiresAt => dateTime().nullable()(); + TextColumn get syncStatus => text().withDefault(const Constant('dirty'))(); + DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)(); + DateTimeColumn get updatedAt => dateTime().withDefault(currentDateAndTime)(); + + @override + // TODO: implement primaryKey + Set>? get primaryKey => {localId}; +} \ No newline at end of file diff --git a/Mobile Operasional/lib/core/database/tables/transactions.dart b/Mobile Operasional/lib/core/database/tables/transactions.dart new file mode 100644 index 0000000..b4b0971 --- /dev/null +++ b/Mobile Operasional/lib/core/database/tables/transactions.dart @@ -0,0 +1,26 @@ +import 'package:drift/drift.dart'; +import 'package:niogu_app/core/database/tables/outlets.dart'; +import 'package:niogu_app/core/database/tables/users.dart'; +import 'package:uuid/uuid.dart'; + +class Transactions extends Table { + TextColumn get localId => text().clientDefault(() => const Uuid().v7().toString())(); + TextColumn get serverId => text().nullable()(); + TextColumn get outletId => text().references(Outlets, #localId)(); + TextColumn get userId => text().references(Users, #localId)(); + TextColumn get referenceType => text()(); + TextColumn get referenceId => text()(); + TextColumn get referenceNumber => text()(); + TextColumn get stakeholder => text().nullable()(); + TextColumn get transactionType => text()(); + TextColumn get category => text()(); + RealColumn get amount => real().withDefault(const Constant(0.0))(); + TextColumn get note => text().nullable()(); + TextColumn get syncStatus => text().withDefault(const Constant('dirty'))(); + DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)(); + DateTimeColumn get updatedAt => dateTime().withDefault(currentDateAndTime)(); + + @override + // TODO: implement primaryKey + Set>? get primaryKey => {localId}; +} \ No newline at end of file diff --git a/Mobile Operasional/lib/core/database/tables/units.dart b/Mobile Operasional/lib/core/database/tables/units.dart new file mode 100644 index 0000000..197baaf --- /dev/null +++ b/Mobile Operasional/lib/core/database/tables/units.dart @@ -0,0 +1,15 @@ +import 'package:drift/drift.dart'; +import 'package:uuid/uuid.dart'; + +class Units extends Table{ + TextColumn get localId => text().clientDefault(() => const Uuid().v7().toString())(); + TextColumn get serverId => text().nullable()(); + TextColumn get name => text().unique()(); + TextColumn get syncStatus => text().withDefault(const Constant('dirty'))(); + DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)(); + DateTimeColumn get updatedAt => dateTime().withDefault(currentDateAndTime)(); + + @override + // TODO: implement primaryKey + Set>? get primaryKey => {localId}; +} \ No newline at end of file diff --git a/Mobile Operasional/lib/core/database/tables/users.dart b/Mobile Operasional/lib/core/database/tables/users.dart new file mode 100644 index 0000000..ac7c7a0 --- /dev/null +++ b/Mobile Operasional/lib/core/database/tables/users.dart @@ -0,0 +1,29 @@ +import 'package:drift/drift.dart'; +import 'package:niogu_app/core/database/tables/outlets.dart'; +import 'package:uuid/uuid.dart'; + +class Users extends Table { + TextColumn get localId => + text().clientDefault(() => const Uuid().v7().toString())(); + TextColumn get serverId => text().nullable()(); + TextColumn get outletId => text().references(Outlets, #localId)(); + TextColumn get name => text()(); + TextColumn get email => text().nullable()(); + TextColumn get phoneNumber => text().nullable()(); + TextColumn get passwordHash => text().nullable()(); + TextColumn get role => text()(); + TextColumn get placeOfBirth => text().nullable()(); + TextColumn get dateOfBirth => text().nullable()(); + TextColumn get shiftName => text().nullable()(); + TextColumn get shiftStartTime => text().nullable()(); + TextColumn get shiftEndTime => text().nullable()(); + BoolColumn get isActive => boolean().withDefault(const Constant(true))(); + TextColumn get syncStatus => text().withDefault(const Constant('dirty'))(); + DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)(); + DateTimeColumn get updatedAt => dateTime().withDefault(currentDateAndTime)(); + DateTimeColumn get deletedAt => dateTime().nullable()(); + + @override + // TODO: implement primaryKey + Set>? get primaryKey => {localId}; +} diff --git a/Mobile Operasional/lib/core/entities/custom_activity.dart b/Mobile Operasional/lib/core/entities/custom_activity.dart new file mode 100644 index 0000000..f64145b --- /dev/null +++ b/Mobile Operasional/lib/core/entities/custom_activity.dart @@ -0,0 +1,11 @@ +import 'package:niogu_app/core/enums/message_activity_type.dart'; + +class MessageActivity { + final MessageActivityType type; + final List messages; + + const MessageActivity({ + required this.type, + required this.messages + }); +} \ No newline at end of file diff --git a/Mobile Operasional/lib/core/entities/custom_sync_queue.dart b/Mobile Operasional/lib/core/entities/custom_sync_queue.dart new file mode 100644 index 0000000..16d8ca3 --- /dev/null +++ b/Mobile Operasional/lib/core/entities/custom_sync_queue.dart @@ -0,0 +1,81 @@ +import 'package:drift/drift.dart'; +import 'package:niogu_app/core/database/app_database.dart'; +import 'package:niogu_app/core/enums/sync_status.dart'; +import 'package:niogu_app/core/enums/table_type.dart'; + +class CustomSyncQueue { + static void addToSyncQueue({ + required TableType type, + required String id, + required SyncAction action, + required Batch batch, + required $SyncQueuesTable syncQueues, + required Map data, + }) { + final payload = SyncPayload( + table: type.type, + localId: id, + action: action.action, + data: data, + ).toJson(); + + batch.insert( + syncQueues, + SyncQueuesCompanion( + tableType: Value(type.type), + tableLocalId: Value(id), + action: Value(action.action), + status: Value(SyncStatus.dirty.status), + payload: Value(payload), + ), + ); + } + + static void updateSyncQueue({ + required TableType type, + required String id, + required SyncAction action, + required Batch batch, + required $SyncQueuesTable syncQueues, + required Map data, + }) { + final payload = SyncPayload( + table: type.type, + localId: id, + action: action.action, + data: data, + ).toJson(); + + batch.update( + syncQueues, + SyncQueuesCompanion( + tableType: Value(type.type), + tableLocalId: Value(id), + action: Value(action.action), + status: Value(SyncStatus.dirty.status), + payload: Value(payload), + ), + ); + } +} + +class SyncPayload { + final String table; + final String localId; + final String action; + final Map data; + + SyncPayload({ + required this.table, + required this.localId, + required this.action, + required this.data, + }); + + Map toJson() => { + 'table': table, + 'local_id': localId, + 'action': action, + 'data': data, + }; +} diff --git a/Mobile Operasional/lib/core/enums/approval_process.dart b/Mobile Operasional/lib/core/enums/approval_process.dart new file mode 100644 index 0000000..5cc942e --- /dev/null +++ b/Mobile Operasional/lib/core/enums/approval_process.dart @@ -0,0 +1,8 @@ +enum ApprovalProcess { + waiting('waiting'), + done('done'); + + final String process; + + const ApprovalProcess(this.process); +} \ No newline at end of file diff --git a/Mobile Operasional/lib/core/enums/campaign_type.dart b/Mobile Operasional/lib/core/enums/campaign_type.dart new file mode 100644 index 0000000..d77b03b --- /dev/null +++ b/Mobile Operasional/lib/core/enums/campaign_type.dart @@ -0,0 +1,8 @@ +enum CampaignType { + product('product'), + category('category'); + + final String type; + + const CampaignType(this.type); +} \ No newline at end of file diff --git a/Mobile Operasional/lib/core/enums/canceled_by.dart b/Mobile Operasional/lib/core/enums/canceled_by.dart new file mode 100644 index 0000000..a7a1286 --- /dev/null +++ b/Mobile Operasional/lib/core/enums/canceled_by.dart @@ -0,0 +1,8 @@ +enum CanceledBy { + user('user'), + customer('customer'); + + final String type; + + const CanceledBy(this.type); +} \ No newline at end of file diff --git a/Mobile Operasional/lib/core/enums/customer_source.dart b/Mobile Operasional/lib/core/enums/customer_source.dart new file mode 100644 index 0000000..8c3c1d4 --- /dev/null +++ b/Mobile Operasional/lib/core/enums/customer_source.dart @@ -0,0 +1,8 @@ +enum CustomerSource { + offline('offline'), + online("online"); + + final String source; + + const CustomerSource(this.source); +} diff --git a/Mobile Operasional/lib/core/enums/delivery_fee_type.dart b/Mobile Operasional/lib/core/enums/delivery_fee_type.dart new file mode 100644 index 0000000..cda873e --- /dev/null +++ b/Mobile Operasional/lib/core/enums/delivery_fee_type.dart @@ -0,0 +1,8 @@ +enum DeliveryFeeType { + fixed('fixed'), + per_km('per_km'); + + final String type; + + const DeliveryFeeType(this.type); +} \ No newline at end of file diff --git a/Mobile Operasional/lib/core/enums/delivery_type.dart b/Mobile Operasional/lib/core/enums/delivery_type.dart new file mode 100644 index 0000000..da3ac24 --- /dev/null +++ b/Mobile Operasional/lib/core/enums/delivery_type.dart @@ -0,0 +1,8 @@ +enum DeliveryType { + pick_up('pick_up'), + delivery('delivery'); + + final String type; + + const DeliveryType(this.type); +} \ No newline at end of file diff --git a/Mobile Operasional/lib/core/enums/item_type.dart b/Mobile Operasional/lib/core/enums/item_type.dart new file mode 100644 index 0000000..f0df519 --- /dev/null +++ b/Mobile Operasional/lib/core/enums/item_type.dart @@ -0,0 +1,7 @@ +enum ItemType { + product_variant("product_variant"), + raw_material("raw_material"); + + final String type; + const ItemType(this.type); +} \ No newline at end of file diff --git a/Mobile Operasional/lib/core/enums/member_level.dart b/Mobile Operasional/lib/core/enums/member_level.dart new file mode 100644 index 0000000..7e3d69f --- /dev/null +++ b/Mobile Operasional/lib/core/enums/member_level.dart @@ -0,0 +1,8 @@ +enum MemberLevel { + standart('standart'), + premium('premium'); + + final String level; + + const MemberLevel(this.level); +} \ No newline at end of file diff --git a/Mobile Operasional/lib/core/enums/message_activity_type.dart b/Mobile Operasional/lib/core/enums/message_activity_type.dart new file mode 100644 index 0000000..a117fc5 --- /dev/null +++ b/Mobile Operasional/lib/core/enums/message_activity_type.dart @@ -0,0 +1,8 @@ +enum MessageActivityType { + lock_activity('lock_activity'), + new_online_order('new_online_order'); + + final String type; + + const MessageActivityType(this.type); +} \ No newline at end of file diff --git a/Mobile Operasional/lib/core/enums/order_source.dart b/Mobile Operasional/lib/core/enums/order_source.dart new file mode 100644 index 0000000..6be3f8e --- /dev/null +++ b/Mobile Operasional/lib/core/enums/order_source.dart @@ -0,0 +1,8 @@ +enum OrderSource { + pos("pos"), + online("online"); + + final String source; + + const OrderSource(this.source); +} \ No newline at end of file diff --git a/Mobile Operasional/lib/core/enums/order_status.dart b/Mobile Operasional/lib/core/enums/order_status.dart new file mode 100644 index 0000000..8649db5 --- /dev/null +++ b/Mobile Operasional/lib/core/enums/order_status.dart @@ -0,0 +1,12 @@ +enum OrderStatus { + pending_payment("pending_payment"), + pending_confirmation("pending_confirmation"), + confirmed("confirmed"), + in_progress("in_progress"), + completed("completed"), + canceled("canceled"); + + final String status; + + const OrderStatus(this.status); +} \ No newline at end of file diff --git a/Mobile Operasional/lib/core/enums/payment_status.dart b/Mobile Operasional/lib/core/enums/payment_status.dart new file mode 100644 index 0000000..7bf2a9e --- /dev/null +++ b/Mobile Operasional/lib/core/enums/payment_status.dart @@ -0,0 +1,9 @@ +enum PaymentStatus { + paid('paid'), + partial('partial'), + debt('debt'); + + final String status; + + const PaymentStatus(this.status); +} \ No newline at end of file diff --git a/Mobile Operasional/lib/core/enums/purchase_status.dart b/Mobile Operasional/lib/core/enums/purchase_status.dart new file mode 100644 index 0000000..a8436a8 --- /dev/null +++ b/Mobile Operasional/lib/core/enums/purchase_status.dart @@ -0,0 +1,8 @@ +enum PurchaseStatus { + completed('completed'), + cancelled('cancelled'); + + final String status; + + const PurchaseStatus(this.status); +} \ No newline at end of file diff --git a/Mobile Operasional/lib/core/enums/report_type.dart b/Mobile Operasional/lib/core/enums/report_type.dart new file mode 100644 index 0000000..4c185f4 --- /dev/null +++ b/Mobile Operasional/lib/core/enums/report_type.dart @@ -0,0 +1,9 @@ +enum ReportType { + transaction('transaction'), + stock_card('stock_card'), + online_order('online_order'); + + final String type; + + const ReportType(this.type); +} \ No newline at end of file diff --git a/Mobile Operasional/lib/core/enums/review_display_policy.dart b/Mobile Operasional/lib/core/enums/review_display_policy.dart new file mode 100644 index 0000000..79d60f9 --- /dev/null +++ b/Mobile Operasional/lib/core/enums/review_display_policy.dart @@ -0,0 +1,8 @@ +enum ReviewDisplayPolicy { + all_rating('all_rating'), + four_and_five_star_only('four_and_five_star_only'); + + final String policy; + + const ReviewDisplayPolicy(this.policy); +} diff --git a/Mobile Operasional/lib/core/enums/review_display_type.dart b/Mobile Operasional/lib/core/enums/review_display_type.dart new file mode 100644 index 0000000..ec33a3b --- /dev/null +++ b/Mobile Operasional/lib/core/enums/review_display_type.dart @@ -0,0 +1,8 @@ +enum ReviewDisplayType { + rating_only('rating_only'), + rating_and_review('rating_and_review'); + + final String type; + + const ReviewDisplayType(this.type); +} \ No newline at end of file diff --git a/Mobile Operasional/lib/core/enums/stock_card_type.dart b/Mobile Operasional/lib/core/enums/stock_card_type.dart new file mode 100644 index 0000000..7e2a646 --- /dev/null +++ b/Mobile Operasional/lib/core/enums/stock_card_type.dart @@ -0,0 +1,13 @@ +enum StockCardType { + initial_stock("initial_stock"), + sale("sale"), + production("production"), + purchase("purchase"), + transfer_in("transfer_in"), + adjustment_in("adjustment_in"), + adjustment_out("adjustment_out"); + + final String type; + + const StockCardType(this.type); +} \ No newline at end of file diff --git a/Mobile Operasional/lib/core/enums/stock_type.dart b/Mobile Operasional/lib/core/enums/stock_type.dart new file mode 100644 index 0000000..3670338 --- /dev/null +++ b/Mobile Operasional/lib/core/enums/stock_type.dart @@ -0,0 +1,7 @@ +enum StockType { + fixed("fixed"), + unlimited("unlimited"); + + final String type; + const StockType(this.type); +} diff --git a/Mobile Operasional/lib/core/enums/sync_status.dart b/Mobile Operasional/lib/core/enums/sync_status.dart new file mode 100644 index 0000000..a9f98ce --- /dev/null +++ b/Mobile Operasional/lib/core/enums/sync_status.dart @@ -0,0 +1,21 @@ +enum SyncStatus { + dirty("dirty"), + syncing('syncing'), + synced('synced'), + failed('failed'); + + final String status; + const SyncStatus(this.status); +} + +enum SyncAction { + create('create'), + update('update'), + delete('delete'), + update_or_create('update_or_create'), + bulk_update('bulk_update'); + + final String action; + + const SyncAction(this.action); +} \ No newline at end of file diff --git a/Mobile Operasional/lib/core/enums/table_type.dart b/Mobile Operasional/lib/core/enums/table_type.dart new file mode 100644 index 0000000..636347e --- /dev/null +++ b/Mobile Operasional/lib/core/enums/table_type.dart @@ -0,0 +1,29 @@ +enum TableType { + campaigns('campaigns'), + categories('categories'), + customers('customers'), + order_items('order_items'), + orders('orders'), + outlet_campaigns('outlet_campaigns'), + outlet_inventories('outlet_inventories'), + outlets('outlets'), + product_likes('product_likes'), + product_reviews('product_reviews'), + product_variants('product_variants'), + products('products'), + purchase_items('purchase_items'), + purchases('purchases'), + raw_materials('raw_materials'), + receipt_items('receipt_items'), + stock_cards('stock_cards'), + suppliers('suppliers'), + tenant_payment_methods('tenant_payment_methods'), + tenants('tenants'), + transactions('transactions'), + units('units'), + users('users'); + + final String type; + + const TableType(this.type); +} \ No newline at end of file diff --git a/Mobile Operasional/lib/core/enums/transaction_reference_type.dart b/Mobile Operasional/lib/core/enums/transaction_reference_type.dart new file mode 100644 index 0000000..d300019 --- /dev/null +++ b/Mobile Operasional/lib/core/enums/transaction_reference_type.dart @@ -0,0 +1,17 @@ +enum TransactionReferenceType { + orders('orders'), + purchases('purchases'); + + final String type; + + const TransactionReferenceType(this.type); +} + +enum TransactionType { + income('income'), + expense('expense'); + + final String type; + + const TransactionType(this.type); +} \ No newline at end of file diff --git a/Mobile Operasional/lib/core/enums/user_role.dart b/Mobile Operasional/lib/core/enums/user_role.dart new file mode 100644 index 0000000..ddb48c6 --- /dev/null +++ b/Mobile Operasional/lib/core/enums/user_role.dart @@ -0,0 +1,8 @@ +enum UserRole { + owner('owner'), + admin('admin'); + + final String role; + + const UserRole(this.role); +} \ No newline at end of file diff --git a/Mobile Operasional/lib/core/errors/exceptions.dart b/Mobile Operasional/lib/core/errors/exceptions.dart new file mode 100644 index 0000000..a5ac659 --- /dev/null +++ b/Mobile Operasional/lib/core/errors/exceptions.dart @@ -0,0 +1,7 @@ +class ServerException implements Exception { + final String message; + ServerException(this.message); + + @override + String toString() => message; +} diff --git a/Mobile Operasional/lib/core/models/api_response.dart b/Mobile Operasional/lib/core/models/api_response.dart new file mode 100644 index 0000000..fcc2b32 --- /dev/null +++ b/Mobile Operasional/lib/core/models/api_response.dart @@ -0,0 +1,25 @@ +class ApiResponseModel { + final String status; + final String message; + final T? data; + final Map? error; + + ApiResponseModel({ + required this.status, + required this.message, + this.data, + this.error, + }); + + factory ApiResponseModel.fromJson( + Map json, + T Function(Object? json) fromJsonT, + ) { + return ApiResponseModel( + status: json['status'], + message: json['message'], + data: json['data'] != null ? fromJsonT(json['data']) : null, + error: json['error'], + ); + } +} diff --git a/Mobile Operasional/lib/core/models/outlet_model.dart b/Mobile Operasional/lib/core/models/outlet_model.dart new file mode 100644 index 0000000..e5dd57d --- /dev/null +++ b/Mobile Operasional/lib/core/models/outlet_model.dart @@ -0,0 +1,84 @@ +import 'package:json_annotation/json_annotation.dart'; +import 'package:drift/drift.dart' hide JsonKey; +import 'package:niogu_app/core/database/app_database.dart'; +import 'package:niogu_app/core/enums/sync_status.dart'; + +part 'outlet_model.g.dart'; + +@JsonSerializable() +class OutletModel { + @JsonKey(name: 'id') + final String serverId; + + final String name; + + final String? email; + + @JsonKey(name: 'phone_number') + final String? phoneNumber; + + @JsonKey(name: 'full_address') + final String? fullAddress; + + final double? latitude; + + final double? longitude; + + @JsonKey(name: 'server_photo_url') + final String? serverPhotoUrl; + + @JsonKey(name: 'server_banner_url') + final String? serverBannerUrl; + + @JsonKey(name: 'is_main_outlet') + final bool? isMainOutlet; + + @JsonKey(name: 'is_active') + final bool? isActive; + + @JsonKey(name: 'created_at') + final DateTime createdAt; + + @JsonKey(name: 'updated_at') + final DateTime updatedAt; + + const OutletModel({ + required this.serverId, + required this.name, + this.email, + this.phoneNumber, + this.fullAddress, + this.latitude, + this.longitude, + this.serverPhotoUrl, + this.serverBannerUrl, + this.isMainOutlet, + this.isActive, + required this.createdAt, + required this.updatedAt + }); + + factory OutletModel.fromJson(Map json) => _$OutletModelFromJson(json); + Map toJson() => _$OutletModelToJson(this); + + OutletsCompanion toDriftCompanion({String? localPhotoPath, String? localBannerPath}) { + return OutletsCompanion( + localId: Value(serverId), + serverId: Value(serverId), + name: Value(name), + email: Value(email), + phoneNumber: Value(phoneNumber), + fullAddress: Value(fullAddress), + latitude: Value(latitude), + longitude: Value(longitude), + serverPhotoUrl: Value(serverPhotoUrl), + serverBannerUrl: Value(serverBannerUrl), + localPhotoPath: Value(localPhotoPath), + localBannerPath: Value(localBannerPath), + isMainOutlet: Value(isMainOutlet ?? false), + syncStatus: Value(SyncStatus.synced.status), + createdAt: Value(createdAt), + updatedAt: Value(updatedAt) + ); + } +} \ No newline at end of file diff --git a/Mobile Operasional/lib/core/models/outlet_model.g.dart b/Mobile Operasional/lib/core/models/outlet_model.g.dart new file mode 100644 index 0000000..62e71fa --- /dev/null +++ b/Mobile Operasional/lib/core/models/outlet_model.g.dart @@ -0,0 +1,40 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'outlet_model.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +OutletModel _$OutletModelFromJson(Map json) => OutletModel( + serverId: json['id'] as String, + name: json['name'] as String, + email: json['email'] as String?, + phoneNumber: json['phone_number'] as String?, + fullAddress: json['full_address'] as String?, + latitude: (json['latitude'] as num?)?.toDouble(), + longitude: (json['longitude'] as num?)?.toDouble(), + serverPhotoUrl: json['server_photo_url'] as String?, + serverBannerUrl: json['server_banner_url'] as String?, + isMainOutlet: json['is_main_outlet'] as bool?, + isActive: json['is_active'] as bool?, + createdAt: DateTime.parse(json['created_at'] as String), + updatedAt: DateTime.parse(json['updated_at'] as String), +); + +Map _$OutletModelToJson(OutletModel instance) => + { + 'id': instance.serverId, + 'name': instance.name, + 'email': instance.email, + 'phone_number': instance.phoneNumber, + 'full_address': instance.fullAddress, + 'latitude': instance.latitude, + 'longitude': instance.longitude, + 'server_photo_url': instance.serverPhotoUrl, + 'server_banner_url': instance.serverBannerUrl, + 'is_main_outlet': instance.isMainOutlet, + 'is_active': instance.isActive, + 'created_at': instance.createdAt.toIso8601String(), + 'updated_at': instance.updatedAt.toIso8601String(), + }; diff --git a/Mobile Operasional/lib/core/models/staff_model.dart b/Mobile Operasional/lib/core/models/staff_model.dart new file mode 100644 index 0000000..d25d0d0 --- /dev/null +++ b/Mobile Operasional/lib/core/models/staff_model.dart @@ -0,0 +1,91 @@ +import 'package:drift/drift.dart' hide JsonKey; +import 'package:json_annotation/json_annotation.dart'; +import 'package:niogu_app/core/database/app_database.dart'; +import 'package:niogu_app/core/enums/sync_status.dart'; + +part 'staff_model.g.dart'; + +@JsonSerializable() +class StaffModel { + @JsonKey(name: 'id') + final String serverId; + + @JsonKey(name: 'outlet_id') + final String outletId; + + final String name; + + final String? email; + + @JsonKey(name: 'phone_number') + final String? phoneNumber; + + final String role; + + @JsonKey(name: 'place_of_birth') + final String? placeOfBirth; + + @JsonKey(name: 'date_of_birth') + final String? dateOfBirth; + + @JsonKey(name: 'shift_name') + final String? shiftName; + + @JsonKey(name: 'shift_start_time') + final String? shiftStartTime; + + @JsonKey(name: 'shift_end_time') + final String? shiftEndTime; + + @JsonKey(name: 'is_active') + final bool isActive; + + @JsonKey(name: 'created_at') + final DateTime createdAt; + + @JsonKey(name: 'updated_at') + final DateTime updatedAt; + + const StaffModel({ + required this.serverId, + required this.outletId, + required this.name, + this.email, + this.phoneNumber, + required this.role, + this.placeOfBirth, + this.dateOfBirth, + this.shiftName, + this.shiftStartTime, + this.shiftEndTime, + required this.isActive, + required this.createdAt, + required this.updatedAt, + }); + + factory StaffModel.fromJson(Map json) => + _$StaffModelFromJson(json); + + Map toJson() => _$StaffModelToJson(this); + + UsersCompanion toDriftCompanion() { + return UsersCompanion( + localId: Value(serverId), + serverId: Value(serverId), + outletId: Value(outletId), + name: Value(name), + email: Value(email), + phoneNumber: Value(phoneNumber), + role: Value(role), + placeOfBirth: Value(placeOfBirth), + dateOfBirth: Value(dateOfBirth), + shiftName: Value(shiftName), + shiftStartTime: Value(shiftStartTime), + shiftEndTime: Value(shiftEndTime), + isActive: Value(isActive), + syncStatus: Value(SyncStatus.synced.status), + createdAt: Value(createdAt), + updatedAt: Value(updatedAt), + ); + } +} diff --git a/Mobile Operasional/lib/core/models/staff_model.g.dart b/Mobile Operasional/lib/core/models/staff_model.g.dart new file mode 100644 index 0000000..36b9928 --- /dev/null +++ b/Mobile Operasional/lib/core/models/staff_model.g.dart @@ -0,0 +1,42 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'staff_model.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +StaffModel _$StaffModelFromJson(Map json) => StaffModel( + serverId: json['id'] as String, + outletId: json['outlet_id'] as String, + name: json['name'] as String, + email: json['email'] as String?, + phoneNumber: json['phone_number'] as String?, + role: json['role'] as String, + placeOfBirth: json['place_of_birth'] as String?, + dateOfBirth: json['date_of_birth'] as String?, + shiftName: json['shift_name'] as String?, + shiftStartTime: json['shift_start_time'] as String?, + shiftEndTime: json['shift_end_time'] as String?, + isActive: json['is_active'] as bool, + createdAt: DateTime.parse(json['created_at'] as String), + updatedAt: DateTime.parse(json['updated_at'] as String), +); + +Map _$StaffModelToJson(StaffModel instance) => + { + 'id': instance.serverId, + 'outlet_id': instance.outletId, + 'name': instance.name, + 'email': instance.email, + 'phone_number': instance.phoneNumber, + 'role': instance.role, + 'place_of_birth': instance.placeOfBirth, + 'date_of_birth': instance.dateOfBirth, + 'shift_name': instance.shiftName, + 'shift_start_time': instance.shiftStartTime, + 'shift_end_time': instance.shiftEndTime, + 'is_active': instance.isActive, + 'created_at': instance.createdAt.toIso8601String(), + 'updated_at': instance.updatedAt.toIso8601String(), + }; diff --git a/Mobile Operasional/lib/core/models/tenant_model.dart b/Mobile Operasional/lib/core/models/tenant_model.dart new file mode 100644 index 0000000..7e4277a --- /dev/null +++ b/Mobile Operasional/lib/core/models/tenant_model.dart @@ -0,0 +1,157 @@ +import 'package:drift/drift.dart' hide JsonKey; +import 'package:json_annotation/json_annotation.dart'; +import 'package:niogu_app/core/database/app_database.dart'; +import 'package:niogu_app/core/enums/sync_status.dart'; + +part 'tenant_model.g.dart'; + +@JsonSerializable() +class TenantModel { + @JsonKey(name: 'id') + final String serverId; + + @JsonKey(name: 'business_code') + final String businessCode; + + @JsonKey(name: 'business_name') + final String businessName; + + @JsonKey(name: 'business_email') + final String? businessEmail; + + @JsonKey(name: 'business_phone') + final String? businessPhone; + + @JsonKey(name: 'business_type') + final String? businessType; + + @JsonKey(name: 'server_logo_url') + final String? serverLogoUrl; + + final String? subdomain; + + @JsonKey(name: 'online_store_name') + final String? onlineStoreName; + + @JsonKey(name: 'online_open_time') + final String? onlineOpenTime; + + @JsonKey(name: 'online_close_time') + final String? onlineCloseTime; + + @JsonKey(name: 'is_close_service') + final bool? isCloseService; + + @JsonKey(name: 'preparation_time_minutes') + final int? preparationTimeMinutes; + + @JsonKey(name: 'order_interval_minutes') + final int? orderIntervalMinutes; + + @JsonKey(name: 'allow_cod') + final bool? allowCod; + + @JsonKey(name: 'max_delivery_radius_km') + final double? maxDeliveryRadiusKm; + + @JsonKey(name: 'delivery_fee_type') + final String? deliveryFeeType; + + @JsonKey(name: 'delivery_flat_fee') + final double? deliveryFlatFee; + + @JsonKey(name: 'delivery_fee_per_km') + final double? deliveryFeePerKm; + + @JsonKey(name: 'delivery_base_fee') + final double? deliveryBaseFee; + + @JsonKey(name: 'review_display_type') + final String? reviewDisplayType; + + @JsonKey(name: 'review_display_policy') + final String? reviewDisplayPolicy; + + @JsonKey(name: 'subscription_level') + final String? subscriptionLevel; + + @JsonKey(name: 'limit_outlet') + final int? limitOutlet; + + @JsonKey(name: 'subscription_expires_at') + final DateTime? subscriptionExpiresAt; + + @JsonKey(name: 'created_at') + final DateTime createdAt; + + @JsonKey(name: 'updated_at') + final DateTime updatedAt; + + const TenantModel({ + required this.serverId, + required this.businessCode, + required this.businessName, + this.businessEmail, + this.businessPhone, + this.businessType, + this.serverLogoUrl, + this.subdomain, + this.onlineStoreName, + this.onlineOpenTime, + this.onlineCloseTime, + this.isCloseService, + this.preparationTimeMinutes, + this.orderIntervalMinutes, + this.allowCod, + this.maxDeliveryRadiusKm, + this.deliveryFeeType, + this.deliveryFlatFee, + this.deliveryFeePerKm, + this.deliveryBaseFee, + this.reviewDisplayType, + this.reviewDisplayPolicy, + this.subscriptionLevel, + this.limitOutlet, + this.subscriptionExpiresAt, + required this.createdAt, + required this.updatedAt + }); + + factory TenantModel.fromJson(Map json) => _$TenantModelFromJson(json); + Map toJson() => _$TenantModelToJson(this); + + TenantsCompanion toDriftCompanion({String? localLogoPath}) { + return TenantsCompanion( + localId: Value(serverId), + serverId: Value(serverId), + businessCode: Value(businessCode), + businessName: Value(businessName), + businessEmail: Value(businessEmail), + businessPhone: Value(businessPhone), + businessType: Value(businessType), + serverLogoUrl: Value(serverLogoUrl), + localLogoPath: Value(localLogoPath), + subdomain: Value(subdomain), + onlinStoreName: Value(onlineStoreName), + onlineOpenTime: Value(onlineOpenTime), + onlineCloseTime: Value(onlineCloseTime), + isCloseService: Value(isCloseService ?? false), + preparationTimeMinutes: Value(preparationTimeMinutes ?? 0), + orderIntervalMinutes: Value(orderIntervalMinutes ?? 0), + allowCod: Value(allowCod ?? false), + maxDeliveryRadiusKm: Value(maxDeliveryRadiusKm ?? 0.0), + deliveryFeeType: Value(deliveryFeeType ?? 'fixed'), + deliveryFlatFee: Value(deliveryFlatFee ?? 0.0), + deliveryFeePerKm: Value(deliveryFeePerKm ?? 0.0), + deliveryBaseFee: Value(deliveryBaseFee ?? 0.0), + reviewDisplayType: Value(reviewDisplayType ?? 'rating_only'), + reviewDisplayPolicy: Value(reviewDisplayPolicy ?? 'four_and_five_star_only'), + subscriptionLevel: Value(subscriptionLevel ?? 'standart'), + limitOutlet: Value(limitOutlet ?? 5), + subscriptionExpiresAt: Value(subscriptionExpiresAt), + syncStatus: Value(SyncStatus.synced.status), + createdAt: Value(createdAt), + updatedAt: Value(updatedAt) + ); + } +} \ No newline at end of file diff --git a/Mobile Operasional/lib/core/models/tenant_model.g.dart b/Mobile Operasional/lib/core/models/tenant_model.g.dart new file mode 100644 index 0000000..b682257 --- /dev/null +++ b/Mobile Operasional/lib/core/models/tenant_model.g.dart @@ -0,0 +1,71 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'tenant_model.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +TenantModel _$TenantModelFromJson(Map json) => TenantModel( + serverId: json['id'] as String, + businessCode: json['business_code'] as String, + businessName: json['business_name'] as String, + businessEmail: json['business_email'] as String?, + businessPhone: json['business_phone'] as String?, + businessType: json['business_type'] as String?, + serverLogoUrl: json['server_logo_url'] as String?, + subdomain: json['subdomain'] as String?, + onlineStoreName: json['online_store_name'] as String?, + onlineOpenTime: json['online_open_time'] as String?, + onlineCloseTime: json['online_close_time'] as String?, + isCloseService: json['is_close_service'] as bool?, + preparationTimeMinutes: (json['preparation_time_minutes'] as num?)?.toInt(), + orderIntervalMinutes: (json['order_interval_minutes'] as num?)?.toInt(), + allowCod: json['allow_cod'] as bool?, + maxDeliveryRadiusKm: (json['max_delivery_radius_km'] as num?)?.toDouble(), + deliveryFeeType: json['delivery_fee_type'] as String?, + deliveryFlatFee: (json['delivery_flat_fee'] as num?)?.toDouble(), + deliveryFeePerKm: (json['delivery_fee_per_km'] as num?)?.toDouble(), + deliveryBaseFee: (json['delivery_base_fee'] as num?)?.toDouble(), + reviewDisplayType: json['review_display_type'] as String?, + reviewDisplayPolicy: json['review_display_policy'] as String?, + subscriptionLevel: json['subscription_level'] as String?, + limitOutlet: (json['limit_outlet'] as num?)?.toInt(), + subscriptionExpiresAt: json['subscription_expires_at'] == null + ? null + : DateTime.parse(json['subscription_expires_at'] as String), + createdAt: DateTime.parse(json['created_at'] as String), + updatedAt: DateTime.parse(json['updated_at'] as String), +); + +Map _$TenantModelToJson( + TenantModel instance, +) => { + 'id': instance.serverId, + 'business_code': instance.businessCode, + 'business_name': instance.businessName, + 'business_email': instance.businessEmail, + 'business_phone': instance.businessPhone, + 'business_type': instance.businessType, + 'server_logo_url': instance.serverLogoUrl, + 'subdomain': instance.subdomain, + 'online_store_name': instance.onlineStoreName, + 'online_open_time': instance.onlineOpenTime, + 'online_close_time': instance.onlineCloseTime, + 'is_close_service': instance.isCloseService, + 'preparation_time_minutes': instance.preparationTimeMinutes, + 'order_interval_minutes': instance.orderIntervalMinutes, + 'allow_cod': instance.allowCod, + 'max_delivery_radius_km': instance.maxDeliveryRadiusKm, + 'delivery_fee_type': instance.deliveryFeeType, + 'delivery_flat_fee': instance.deliveryFlatFee, + 'delivery_fee_per_km': instance.deliveryFeePerKm, + 'delivery_base_fee': instance.deliveryBaseFee, + 'review_display_type': instance.reviewDisplayType, + 'review_display_policy': instance.reviewDisplayPolicy, + 'subscription_level': instance.subscriptionLevel, + 'limit_outlet': instance.limitOutlet, + 'subscription_expires_at': instance.subscriptionExpiresAt?.toIso8601String(), + 'created_at': instance.createdAt.toIso8601String(), + 'updated_at': instance.updatedAt.toIso8601String(), +}; diff --git a/Mobile Operasional/lib/core/models/user_model.dart b/Mobile Operasional/lib/core/models/user_model.dart new file mode 100644 index 0000000..7c20ee7 --- /dev/null +++ b/Mobile Operasional/lib/core/models/user_model.dart @@ -0,0 +1,116 @@ +import 'package:drift/drift.dart' hide JsonKey; +import 'package:json_annotation/json_annotation.dart'; +import 'package:niogu_app/core/database/app_database.dart'; +import 'package:niogu_app/core/models/outlet_model.dart'; +import 'package:niogu_app/core/models/staff_model.dart'; +import 'package:niogu_app/core/models/tenant_model.dart'; +import 'package:niogu_app/core/enums/sync_status.dart'; + +part 'user_model.g.dart'; + +@JsonSerializable() +class UserModel { + @JsonKey(name: 'id') + final String serverId; + + final TenantModel tenant; + + @JsonKey(name: 'outlet_id') + final String outletId; + + @JsonKey(name: 'outlets') + final dynamic rawOutlets; + + @JsonKey(name: 'staff_list', defaultValue: []) + final List staffs; + + final String name; + + final String? email; + + @JsonKey(name: 'phone_number') + final String? phoneNumber; + + final String role; + + @JsonKey(name: 'place_of_birth') + final String? placeOfBirth; + + @JsonKey(name: 'date_of_birth') + final String? dateOfBirth; + + @JsonKey(name: 'shift_name') + final String? shiftName; + + @JsonKey(name: 'shift_start_time') + final String? shiftStartTime; + + @JsonKey(name: 'shift_end_time') + final String? shiftEndTime; + + @JsonKey(name: 'is_active') + final bool isActive; + + @JsonKey(name: 'created_at') + final DateTime createdAt; + + @JsonKey(name: 'updated_at') + final DateTime updatedAt; + + const UserModel({ + required this.serverId, + required this.tenant, + required this.outletId, + required this.rawOutlets, + required this.staffs, + required this.name, + this.email, + this.phoneNumber, + required this.role, + this.placeOfBirth, + this.dateOfBirth, + this.shiftName, + this.shiftStartTime, + this.shiftEndTime, + required this.isActive, + required this.createdAt, + required this.updatedAt, + }); + + List get outlets { + if (rawOutlets is List) { + return (rawOutlets as List) + .map((e) => OutletModel.fromJson(e as Map)) + .toList(); + } + // Jika berupa single object (Staff) + return [OutletModel.fromJson(rawOutlets as Map)]; + } + + factory UserModel.fromJson(Map json) => + _$UserModelFromJson(json); + + Map toJson() => _$UserModelToJson(this); + + UsersCompanion toDriftCompanion(String password) { + return UsersCompanion( + localId: Value(serverId), + serverId: Value(serverId), + outletId: Value(outletId), + name: Value(name), + email: Value(email), + phoneNumber: Value(phoneNumber), + passwordHash: Value(password), + role: Value(role), + placeOfBirth: Value(placeOfBirth), + dateOfBirth: Value(dateOfBirth), + shiftName: Value(shiftName), + shiftStartTime: Value(shiftStartTime), + shiftEndTime: Value(shiftEndTime), + isActive: Value(isActive), + syncStatus: Value(SyncStatus.synced.status), + createdAt: Value(createdAt), + updatedAt: Value(updatedAt), + ); + } +} diff --git a/Mobile Operasional/lib/core/models/user_model.g.dart b/Mobile Operasional/lib/core/models/user_model.g.dart new file mode 100644 index 0000000..8869d67 --- /dev/null +++ b/Mobile Operasional/lib/core/models/user_model.g.dart @@ -0,0 +1,51 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'user_model.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +UserModel _$UserModelFromJson(Map json) => UserModel( + serverId: json['id'] as String, + tenant: TenantModel.fromJson(json['tenant'] as Map), + outletId: json['outlet_id'] as String, + rawOutlets: json['outlets'], + staffs: + (json['staff_list'] as List?) + ?.map((e) => StaffModel.fromJson(e as Map)) + .toList() ?? + [], + name: json['name'] as String, + email: json['email'] as String?, + phoneNumber: json['phone_number'] as String?, + role: json['role'] as String, + placeOfBirth: json['place_of_birth'] as String?, + dateOfBirth: json['date_of_birth'] as String?, + shiftName: json['shift_name'] as String?, + shiftStartTime: json['shift_start_time'] as String?, + shiftEndTime: json['shift_end_time'] as String?, + isActive: json['is_active'] as bool, + createdAt: DateTime.parse(json['created_at'] as String), + updatedAt: DateTime.parse(json['updated_at'] as String), +); + +Map _$UserModelToJson(UserModel instance) => { + 'id': instance.serverId, + 'tenant': instance.tenant, + 'outlet_id': instance.outletId, + 'outlets': instance.rawOutlets, + 'staff_list': instance.staffs, + 'name': instance.name, + 'email': instance.email, + 'phone_number': instance.phoneNumber, + 'role': instance.role, + 'place_of_birth': instance.placeOfBirth, + 'date_of_birth': instance.dateOfBirth, + 'shift_name': instance.shiftName, + 'shift_start_time': instance.shiftStartTime, + 'shift_end_time': instance.shiftEndTime, + 'is_active': instance.isActive, + 'created_at': instance.createdAt.toIso8601String(), + 'updated_at': instance.updatedAt.toIso8601String(), +}; diff --git a/Mobile Operasional/lib/core/network/dio_client.dart b/Mobile Operasional/lib/core/network/dio_client.dart new file mode 100644 index 0000000..ad7d415 --- /dev/null +++ b/Mobile Operasional/lib/core/network/dio_client.dart @@ -0,0 +1,48 @@ +import 'package:dio/dio.dart'; +import 'package:flutter/foundation.dart'; +import 'package:niogu_app/core/constants/app_config.dart'; +import 'package:niogu_app/core/system/system_secure.dart'; +import 'package:niogu_app/core/utils/log_message.dart'; + +class DioClient { + late final Dio _dio; + + DioClient() { + _dio = Dio( + BaseOptions( + baseUrl: AppConfig.BASE_URL, + connectTimeout: const Duration(seconds: 15), + receiveTimeout: const Duration(seconds: 15), + responseType: ResponseType.json, + headers: { + 'Accept': 'application/json', + 'Content-Type': 'application/json', + }, + ), + ); + + _dio.interceptors.add( + LogInterceptor(requestBody: kDebugMode, responseBody: kDebugMode), + ); + + _dio.interceptors.add(InterceptorsWrapper( + onRequest: (options, handler) async { + final token = await SystemSecure.getAccessToken(); + + if (token != null) { + options.headers['Authorization'] = 'Bearer $token'; + } + + return handler.next(options); + }, + onError: (DioException e, handler) { + if (e.response?.statusCode == 401) { + LogMessage.log.e("Sesi berakhir, silakan login ulang."); + } + return handler.next(e); + }, + )); + } + + Dio get instance => _dio; +} diff --git a/Mobile Operasional/lib/core/providers/app_provider.dart b/Mobile Operasional/lib/core/providers/app_provider.dart new file mode 100644 index 0000000..a4e6bc2 --- /dev/null +++ b/Mobile Operasional/lib/core/providers/app_provider.dart @@ -0,0 +1,66 @@ +import 'package:dio/dio.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:niogu_app/core/database/app_database.dart'; +import 'package:niogu_app/core/entities/custom_activity.dart'; +import 'package:niogu_app/core/network/dio_client.dart'; +import 'package:niogu_app/core/database/database_holder.dart'; +import 'package:niogu_app/core/enums/user_role.dart'; +import 'package:niogu_app/core/services/sync_services.dart'; +import 'package:niogu_app/features/online_store/presentation/providers/online_store_provider.dart'; +import 'package:niogu_app/features/user/presentation/providers/user_provider.dart'; + +final dioProvider = Provider((ref) => DioClient().instance); + +final appDatabaseProvider = Provider((ref) => database); + +final syncServiceProvider = Provider((ref) { + final dio = ref.watch(dioProvider); + + final appDatabase = ref.watch(appDatabaseProvider); + + return SyncService(dio, appDatabase); +}); + +final outletIdByOwnerProvider = StateProvider((ref) => null); + +final currentOutletIdProvider = StateProvider((ref) => null); + +final currentOutletNameProvider = StateProvider((ref) => null); + +final currentUserIdProvider = StateProvider((ref) => null); + +final currentUserNameProvider = StateProvider((ref) => null); + +final currentUserEmailProvider = StateProvider((ref) => null); + +final currentUserRoleProvider = StateProvider((ref) => null); + +final currentStatusLoginProvider = StateProvider((ref) => false); + +final messageActivityStateProvider = StateProvider( + (ref) => null, +); + +final messageActivityProvider = Provider((ref) { + var messageActivity = ref.watch(messageActivityStateProvider); + + final isLoggedIn = ref.watch(currentStatusLoginProvider); + + if (isLoggedIn) { + final permissionActivityAsync = ref.watch(permissionActivityStreamProvider); + + final newOnlineOrderAsync = ref.watch( + onlineOrderHasNotReviewStreamProvider, + ); + + if (messageActivity == null) { + permissionActivityAsync.whenData((m) => messageActivity = m); + } + + if (messageActivity == null) { + newOnlineOrderAsync.whenData((m) => messageActivity = m); + } + } + + return messageActivity; +}); diff --git a/Mobile Operasional/lib/core/router/app_route.dart b/Mobile Operasional/lib/core/router/app_route.dart new file mode 100644 index 0000000..78edff4 --- /dev/null +++ b/Mobile Operasional/lib/core/router/app_route.dart @@ -0,0 +1,1494 @@ +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'package:niogu_app/features/auth/presentation/screens/admin_login_screen.dart'; +import 'package:niogu_app/features/auth/presentation/screens/forgot_password_screen.dart'; +import 'package:niogu_app/features/auth/presentation/screens/owner_login_screen.dart'; +import 'package:niogu_app/features/auth/presentation/screens/registration_screen.dart'; +import 'package:niogu_app/features/auth/presentation/screens/verification_otp_screen.dart'; +import 'package:niogu_app/features/customer/domain/entities/customer.dart'; +import 'package:niogu_app/features/customer/presentation/screens/add_customer_screen.dart'; +import 'package:niogu_app/features/customer/presentation/screens/customer_activities_screen.dart'; +import 'package:niogu_app/features/customer/presentation/screens/customer_addresses_screen.dart'; +import 'package:niogu_app/features/customer/presentation/screens/customer_screen.dart'; +import 'package:niogu_app/features/customer/presentation/screens/edit_customer_screen.dart'; +import 'package:niogu_app/features/customer/presentation/screens/map_customer_address_screen.dart'; +import 'package:niogu_app/features/goods/presentation/screens/good_choice_screen.dart'; +import 'package:niogu_app/features/goods/presentation/screens/good_screen.dart'; +import 'package:niogu_app/features/goods/raw_materials/presentation/screens/add_raw_material_screen.dart'; +import 'package:niogu_app/features/goods/raw_materials/presentation/screens/product_usage_raw_material_screen.dart'; +import 'package:niogu_app/features/goods/raw_materials/presentation/screens/edit_raw_material_screen.dart'; +import 'package:niogu_app/features/goods/products/presentation/screens/add_product_screen.dart'; +import 'package:niogu_app/features/goods/products/presentation/screens/add_product_variant_screen.dart'; +import 'package:niogu_app/features/goods/products/presentation/screens/edit_product_screen.dart'; +import 'package:niogu_app/features/goods/products/presentation/screens/edit_product_variant_screen.dart'; +import 'package:niogu_app/features/home/presentation/screens/home_screen.dart'; +import 'package:niogu_app/features/online_store/domain/entities/online_store.dart'; +import 'package:niogu_app/features/online_store/presentation/screens/add_campaign_screen.dart'; +import 'package:niogu_app/features/online_store/presentation/screens/campaign_screen.dart'; +import 'package:niogu_app/features/profile/domain/entities/profile.dart'; +import 'package:niogu_app/features/profile/presentation/screens/change_password_screen.dart'; +import 'package:niogu_app/features/online_store/presentation/screens/configure_online_store_screen.dart'; +import 'package:niogu_app/features/online_store/presentation/screens/detail_product_review_screen.dart'; +import 'package:niogu_app/features/online_store/presentation/screens/edit_campaign_screen.dart'; +import 'package:niogu_app/features/online_store/presentation/screens/full_product_review_screen.dart'; +import 'package:niogu_app/features/online_store/presentation/screens/link_campaign_screen.dart'; +import 'package:niogu_app/features/online_store/presentation/screens/online_store_order_detail_screen.dart'; +import 'package:niogu_app/features/online_store/presentation/screens/online_store_order_screen.dart'; +import 'package:niogu_app/features/online_store/presentation/screens/online_store_product_screen.dart'; +import 'package:niogu_app/features/online_store/presentation/screens/online_store_screen.dart'; +import 'package:niogu_app/features/online_store/presentation/screens/payment_method_screen.dart'; +import 'package:niogu_app/features/online_store/presentation/screens/product_review_screen.dart'; +import 'package:niogu_app/features/online_store/presentation/screens/rating_configuration_screen.dart'; +import 'package:niogu_app/features/online_store/presentation/screens/shipping_configuration_screen.dart'; +import 'package:niogu_app/features/online_store/presentation/screens/target_outlet_screen.dart'; +import 'package:niogu_app/features/outlets/domain/entities/outlet.dart'; +import 'package:niogu_app/features/outlets/presentation/screens/add_outlet_screen.dart'; +import 'package:niogu_app/features/outlets/presentation/screens/edit_outlet_screen.dart'; +import 'package:niogu_app/features/outlets/presentation/screens/edit_staff_admin_screen.dart'; +import 'package:niogu_app/features/outlets/presentation/screens/map_outlet_address_screen.dart'; +import 'package:niogu_app/features/outlets/presentation/screens/outlet_screen.dart'; +import 'package:niogu_app/features/pos/presentation/screens/already_customer_screen.dart'; +import 'package:niogu_app/features/pos/presentation/screens/pos_screen.dart'; +import 'package:niogu_app/features/profile/presentation/screens/business_info_screen.dart'; +import 'package:niogu_app/features/profile/presentation/screens/outlet_info_screen.dart'; +import 'package:niogu_app/features/profile/presentation/screens/profil_info_screen.dart'; +import 'package:niogu_app/features/profile/presentation/screens/profile_screen.dart'; +import 'package:niogu_app/features/report/presentation/screens/report_screen.dart'; +import 'package:niogu_app/features/report/stock_report/domain/entities/stok_report.dart'; +import 'package:niogu_app/features/report/stock_report/presentation/screens/stock_report_detail_screen.dart'; +import 'package:niogu_app/features/report/transaction/domain/entities/transaction_report.dart'; +import 'package:niogu_app/features/report/transaction/presentation/screens/transaction_report_order_detail_screen.dart'; +import 'package:niogu_app/features/report/transaction/presentation/screens/transaction_report_purchase_detail_screen.dart'; +import 'package:niogu_app/features/splash/screens/splash_screen.dart'; +import 'package:niogu_app/features/stock_check/presentation/screens/stock_check_screen.dart'; +import 'package:niogu_app/features/stock_in/presentation/screens/display_item_screen.dart'; +import 'package:niogu_app/features/stock_in/presentation/screens/stock_in_screen.dart'; +import 'package:niogu_app/features/stock_in/domain/entities/stock_in.dart'; +import 'package:niogu_app/features/stock_in/presentation/screens/already_supplier_screen.dart'; +import 'package:niogu_app/features/supplier/domain/entities/supplier.dart'; +import 'package:niogu_app/features/supplier/presentation/screens/add_supplier_screen.dart'; +import 'package:niogu_app/features/supplier/presentation/screens/edit_supplier_screen.dart'; +import 'package:niogu_app/features/supplier/presentation/screens/supplier_activites_screen.dart'; +import 'package:niogu_app/features/supplier/presentation/screens/supplier_screen.dart'; +import 'package:niogu_app/main_wrapper.dart'; + +final _rootNavigatorKey = GlobalKey(); + +class AppRoute { + static final String splashScreen = 'splash'; + static final String ownerLoginScreen = 'ownerLogin'; + static final String adminLoginScreen = 'adminLogin'; + static final String registrationScreen = 'registrationScreen'; + static final String forgotPasswordScreen = 'forgotPasswordScreen'; + static final String verificationOtpScreen = 'verificationOtp'; + static final String homeScreen = 'home'; + static final String stockCheckScreen = 'stockCheck'; + static final String stockInScreen = 'stockIn'; + static final String alreadySupplierScreen = 'alreadySupplier'; + static final String selectGoodScreen = 'selectGood'; + static final String customerScreen = 'customer'; + static final String addCustomerScreen = 'addCustomer'; + static final String editCustomerScreen = 'editCustomer'; + static final String mapCustomerAddressScreen = 'mapCustomerAddress'; + static final String customerAddressesScreen = 'customerAddresses'; + static final String customerActivitesScreen = 'customerActivites'; + static final String supplierScreen = 'supplier'; + static final String addSupplierScreen = 'addSupplier'; + static final String editSupplierScreen = 'editSupplier'; + static final String supplierActivityScreen = 'supplierActivities'; + static final String onlineStoreScreen = 'onlineStore'; + static final String configureStoreOnlineScreen = 'configureStoreOnline'; + static final String onlineStoreProductScreen = 'onlineStoreProduct'; + static final String onlineStoreOrderScreen = 'onlineStoreOrder'; + static final String onlineStoreOrderDetilScreen = 'onlineStoreOrderDetail'; + static final String shippingConfigurationScreen = 'shippingConfiguration'; + static final String paymentMethodScreen = 'paymentMethod'; + static final String productReviewScreen = 'productReview'; + static final String detailProductReviewScreen = 'detailProductReview'; + static final String fullProductReviewScreen = 'fullProductReview'; + static final String ratingConfigurationScreen = 'ratingConfiguration'; + static final String campaignScreen = 'campaigns'; + static final String addCampaignScreen = 'addCampaign'; + static final String editCampaignScreen = 'editCampaign'; + static final String linkCampaignScreen = 'linkCampaign'; + static final String targetOutletScreen = 'targetOutlet'; + static final String outletScreen = 'outlet'; + static final String addOutletScreen = 'addOutlet'; + static final String editOutletScreen = 'editOutlet'; + static final String editStaffAdminScreen = 'editStaffAdmin'; + static final String mapOutletAdressScreen = 'mapOutletAdress'; + static final String productScreen = 'products'; + static final String addProductScreen = "addProduct"; + static final String productChoiceScreen = 'productChoice'; + static final String editProductScreen = "editProduct"; + static final String addVariantProductScreen = "addVariantProduct"; + static final String editVariantProductScreen = "editVariantProduct"; + static final String addRawMaterialScreen = 'addRawMaterial'; + static final String rawMaterialChoiceScreen = 'rawMaterialChoice'; + static final String productUsageRawMaterialScreen = 'productUsageRawMaterial'; + static final String editRawMaterialScreen = 'editRawMaterial'; + static final String posScreen = 'pos'; + static final String alreadyCustomerScreen = 'alreadyCustomer'; + static final String transactionReportScreen = 'transactionReport'; + static final String transactionReportOrderDetailScreen = + 'transactionReportOrderDetail'; + static final String transactionReportPurchaseDetailScreen = + 'transactionReportPurchaseDetail'; + static final String stockReportDetailScreen = 'stockReportDetail'; + static final String profileScreen = 'profile'; + static final String profileInfoScreen = 'profileInfo'; + static final String businessInfoScreen = 'businessInfo'; + static final String outletInfoScreen = 'outletInfo'; + static final String changePasswordScreen = 'changePassword'; + + static final router = GoRouter( + navigatorKey: _rootNavigatorKey, + initialLocation: '/splash', + routes: [ + GoRoute( + name: splashScreen, + path: '/splash', + builder: (context, state) => const SplashScreen(), + ), + GoRoute( + parentNavigatorKey: _rootNavigatorKey, + name: ownerLoginScreen, + path: '/auth/owner/login', + pageBuilder: (context, state) { + return CustomTransitionPage( + key: state.pageKey, + child: const OwnerLoginScreen(), + + transitionsBuilder: + (context, animation, secondaryAnimation, child) => + FadeTransition( + opacity: CurveTween( + curve: Curves.easeIn, + ).animate(animation), + child: child, + ), + ); + }, + ), + GoRoute( + parentNavigatorKey: _rootNavigatorKey, + name: adminLoginScreen, + path: '/auth/admin/login', + pageBuilder: (context, state) { + return CustomTransitionPage( + key: state.pageKey, + child: const AdminLoginScreen(), + + transitionsBuilder: + (context, animation, secondaryAnimation, child) => + FadeTransition( + opacity: CurveTween( + curve: Curves.easeIn, + ).animate(animation), + child: child, + ), + ); + }, + ), + GoRoute( + parentNavigatorKey: _rootNavigatorKey, + name: registrationScreen, + path: '/auth/registration', + pageBuilder: (context, state) { + return CustomTransitionPage( + key: state.pageKey, + child: const RegistrationScreen(), + + transitionsBuilder: + (context, animation, secondaryAnimation, child) => + FadeTransition( + opacity: CurveTween( + curve: Curves.easeIn, + ).animate(animation), + child: child, + ), + ); + }, + ), + GoRoute( + parentNavigatorKey: _rootNavigatorKey, + name: forgotPasswordScreen, + path: '/auth/forgot-password', + pageBuilder: (context, state) { + return CustomTransitionPage( + key: state.pageKey, + child: const ForgotPasswordScreen(), + + transitionsBuilder: + (context, animation, secondaryAnimation, child) => + FadeTransition( + opacity: CurveTween( + curve: Curves.easeIn, + ).animate(animation), + child: child, + ), + ); + }, + ), + GoRoute( + parentNavigatorKey: _rootNavigatorKey, + name: verificationOtpScreen, + path: '/auth/verification-otp', + pageBuilder: (context, state) { + return CustomTransitionPage( + key: state.pageKey, + child: const VerificationOtpScreen(identifier: '081234567890'), + + transitionsBuilder: + (context, animation, secondaryAnimation, child) => + FadeTransition( + opacity: CurveTween( + curve: Curves.easeIn, + ).animate(animation), + child: child, + ), + ); + }, + ), + StatefulShellRoute.indexedStack( + builder: (context, state, navigationShell) { + return MainWrapper(navigationShell: navigationShell); + }, + branches: [ + StatefulShellBranch( + routes: [ + GoRoute( + name: homeScreen, + path: '/home', + builder: (context, state) => const HomeScreen(), + ), + ], + ), + StatefulShellBranch( + routes: [ + GoRoute( + name: productScreen, + path: '/products', + builder: (context, state) => const GoodScreen(), + ), + ], + ), + StatefulShellBranch( + routes: [ + GoRoute( + name: posScreen, + path: '/pos', + builder: (context, state) => const PosScreen(), + ), + ], + ), + StatefulShellBranch( + routes: [ + GoRoute( + name: transactionReportScreen, + path: '/transactionReport', + builder: (context, state) => const ReportScreen(), + ), + ], + ), + StatefulShellBranch( + routes: [ + GoRoute( + name: profileScreen, + path: '/profile', + builder: (context, state) => const ProfileScreen(), + ), + ], + ), + ], + ), + GoRoute( + parentNavigatorKey: _rootNavigatorKey, + name: stockCheckScreen, + path: '/stock-check', + pageBuilder: (context, state) { + return CustomTransitionPage( + key: state.pageKey, + child: const StockCheckScreen(), + + transitionsBuilder: + (context, animation, secondaryAnimation, child) => + FadeTransition( + opacity: CurveTween( + curve: Curves.easeIn, + ).animate(animation), + child: child, + ), + ); + }, + ), + GoRoute( + parentNavigatorKey: _rootNavigatorKey, + name: stockInScreen, + path: '/stock-in', + pageBuilder: (context, state) { + return CustomTransitionPage( + key: state.pageKey, + child: const StockInScreen(), + + transitionsBuilder: + (context, animation, secondaryAnimation, child) => + FadeTransition( + opacity: CurveTween( + curve: Curves.easeIn, + ).animate(animation), + child: child, + ), + ); + }, + ), + GoRoute( + parentNavigatorKey: _rootNavigatorKey, + name: alreadySupplierScreen, + path: '/already-supplier', + pageBuilder: (context, state) { + return CustomTransitionPage( + key: state.pageKey, + child: const AlreadySupplierScreen(), + + transitionsBuilder: + (context, animation, secondaryAnimation, child) => + FadeTransition( + opacity: CurveTween( + curve: Curves.easeIn, + ).animate(animation), + child: child, + ), + ); + }, + ), + GoRoute( + parentNavigatorKey: _rootNavigatorKey, + name: selectGoodScreen, + path: '/select-good', + pageBuilder: (context, state) { + final Map data = state.extra as Map; + + final type = data["type"] as SelectedItemType; + + final onPressed = data["on_pressed"] as VoidCallback? ?? null; + + return CustomTransitionPage( + key: state.pageKey, + child: DisplayItemScreen(type: type, onPressed: onPressed), + + transitionsBuilder: + (context, animation, secondaryAnimation, child) => + FadeTransition( + opacity: CurveTween( + curve: Curves.easeIn, + ).animate(animation), + child: child, + ), + ); + }, + ), + GoRoute( + parentNavigatorKey: _rootNavigatorKey, + name: customerScreen, + path: '/customers', + pageBuilder: (context, state) { + return CustomTransitionPage( + key: state.pageKey, + child: const CustomerScreen(), + + transitionsBuilder: + (context, animation, secondaryAnimation, child) => + FadeTransition( + opacity: CurveTween( + curve: Curves.easeIn, + ).animate(animation), + child: child, + ), + ); + }, + ), + GoRoute( + parentNavigatorKey: _rootNavigatorKey, + name: addCustomerScreen, + path: '/customers/add', + pageBuilder: (context, state) { + final Map? data = + state.extra as Map?; + + final String? initialName = data?["name"]; + + final String? initialPhone = data?["phone"]; + + return CustomTransitionPage( + key: state.pageKey, + child: AddCustomerScreen( + initialName: initialName, + initialPhone: initialPhone, + ), + transitionsBuilder: + (context, animation, secondaryAnimation, child) => + FadeTransition( + opacity: CurveTween( + curve: Curves.easeIn, + ).animate(animation), + child: child, + ), + ); + }, + ), + GoRoute( + parentNavigatorKey: _rootNavigatorKey, + name: editCustomerScreen, + path: '/customers/edit/:id', + pageBuilder: (context, state) { + final customerId = state.pathParameters["id"]; + return CustomTransitionPage( + key: state.pageKey, + child: EditCustomerScreen(customerId: customerId!), + + transitionsBuilder: + (context, animation, secondaryAnimation, child) => + FadeTransition( + opacity: CurveTween( + curve: Curves.easeIn, + ).animate(animation), + child: child, + ), + ); + }, + ), + GoRoute( + parentNavigatorKey: _rootNavigatorKey, + name: customerAddressesScreen, + path: '/customers/addresses', + pageBuilder: (context, state) { + final List addresses = + state.extra as List; + + return CustomTransitionPage( + key: state.pageKey, + child: CustomerAddressesScreen(addresses: addresses), + + transitionsBuilder: + (context, animation, secondaryAnimation, child) => + FadeTransition( + opacity: CurveTween( + curve: Curves.easeIn, + ).animate(animation), + child: child, + ), + ); + }, + ), + GoRoute( + parentNavigatorKey: _rootNavigatorKey, + name: mapCustomerAddressScreen, + path: '/customers/address/map', + pageBuilder: (context, state) { + final address = state.extra as CustomerAddress; + return CustomTransitionPage( + key: state.pageKey, + child: MapCustomerAddressScreen(address: address), + + transitionsBuilder: + (context, animation, secondaryAnimation, child) => + FadeTransition( + opacity: CurveTween( + curve: Curves.easeIn, + ).animate(animation), + child: child, + ), + ); + }, + ), + GoRoute( + parentNavigatorKey: _rootNavigatorKey, + name: customerActivitesScreen, + path: '/customers/activities', + pageBuilder: (context, state) { + final data = state.extra as Map; + + final bool isOnlineCustomer = data["is_online_customer"]; + + final List activites = data["activities"]; + + return CustomTransitionPage( + key: state.pageKey, + child: CustomerActivitiesScreen( + isOnlineCustomer: isOnlineCustomer, + activities: activites, + ), + + transitionsBuilder: + (context, animation, secondaryAnimation, child) => + FadeTransition( + opacity: CurveTween( + curve: Curves.easeIn, + ).animate(animation), + child: child, + ), + ); + }, + ), + GoRoute( + parentNavigatorKey: _rootNavigatorKey, + name: supplierScreen, + path: '/suppliers', + pageBuilder: (context, state) { + return CustomTransitionPage( + key: state.pageKey, + child: const SupplierScreen(), + + transitionsBuilder: + (context, animation, secondaryAnimation, child) => + FadeTransition( + opacity: CurveTween( + curve: Curves.easeIn, + ).animate(animation), + child: child, + ), + ); + }, + ), + GoRoute( + parentNavigatorKey: _rootNavigatorKey, + name: addSupplierScreen, + path: '/suppliers/add', + pageBuilder: (context, state) { + final Map? data = + state.extra as Map?; + + final String? initialName = data?["name"]; + + final String? initialPhone = data?["phone"]; + + return CustomTransitionPage( + key: state.pageKey, + child: AddSupplierScreen( + initialName: initialName, + initialPhone: initialPhone, + ), + transitionsBuilder: + (context, animation, secondaryAnimation, child) => + FadeTransition( + opacity: CurveTween( + curve: Curves.easeIn, + ).animate(animation), + child: child, + ), + ); + }, + ), + GoRoute( + parentNavigatorKey: _rootNavigatorKey, + name: editSupplierScreen, + path: '/suppliers/edit/:id', + pageBuilder: (context, state) { + final supplierId = state.pathParameters["id"]; + return CustomTransitionPage( + key: state.pageKey, + child: EditSupplierScreen(supplierId: supplierId!), + + transitionsBuilder: + (context, animation, secondaryAnimation, child) => + FadeTransition( + opacity: CurveTween( + curve: Curves.easeIn, + ).animate(animation), + child: child, + ), + ); + }, + ), + GoRoute( + parentNavigatorKey: _rootNavigatorKey, + name: supplierActivityScreen, + path: '/suppliers/activities', + pageBuilder: (context, state) { + final data = state.extra as Map; + + final List activites = data["activities"]; + + return CustomTransitionPage( + key: state.pageKey, + child: SupplierActivitesScreen(activities: activites), + + transitionsBuilder: + (context, animation, secondaryAnimation, child) => + FadeTransition( + opacity: CurveTween( + curve: Curves.easeIn, + ).animate(animation), + child: child, + ), + ); + }, + ), + GoRoute( + parentNavigatorKey: _rootNavigatorKey, + name: onlineStoreScreen, + path: '/online-store', + pageBuilder: (context, state) { + return CustomTransitionPage( + key: state.pageKey, + child: const OnlineStoreScreen(), + + transitionsBuilder: + (context, animation, secondaryAnimation, child) => + FadeTransition( + opacity: CurveTween( + curve: Curves.easeIn, + ).animate(animation), + child: child, + ), + ); + }, + ), + GoRoute( + parentNavigatorKey: _rootNavigatorKey, + name: configureStoreOnlineScreen, + path: '/online-store/configure', + pageBuilder: (context, state) { + final configure = state.extra as ConfigureOnlineStore; + + return CustomTransitionPage( + key: state.pageKey, + child: ConfigureOnlineStoreScreen(configure: configure), + + transitionsBuilder: + (context, animation, secondaryAnimation, child) => + FadeTransition( + opacity: CurveTween( + curve: Curves.easeIn, + ).animate(animation), + child: child, + ), + ); + }, + ), + GoRoute( + parentNavigatorKey: _rootNavigatorKey, + name: onlineStoreProductScreen, + path: '/online-store/products', + pageBuilder: (context, state) { + return CustomTransitionPage( + key: state.pageKey, + child: const OnlineStoreProductScreen(), + + transitionsBuilder: + (context, animation, secondaryAnimation, child) => + FadeTransition( + opacity: CurveTween( + curve: Curves.easeIn, + ).animate(animation), + child: child, + ), + ); + }, + ), + GoRoute( + parentNavigatorKey: _rootNavigatorKey, + name: onlineStoreOrderScreen, + path: '/online-store/orders', + pageBuilder: (context, state) { + return CustomTransitionPage( + key: state.pageKey, + child: const OnlineStoreOrdersScreen(), + + transitionsBuilder: + (context, animation, secondaryAnimation, child) => + FadeTransition( + opacity: CurveTween( + curve: Curves.easeIn, + ).animate(animation), + child: child, + ), + ); + }, + ), + GoRoute( + parentNavigatorKey: _rootNavigatorKey, + name: onlineStoreOrderDetilScreen, + path: '/online-store/orders/detail', + pageBuilder: (context, state) { + final detail = state.extra as OnlineOrderDetail; + + return CustomTransitionPage( + key: state.pageKey, + child: OnlineStoreOrderDetailScreen(detail: detail), + + transitionsBuilder: + (context, animation, secondaryAnimation, child) => + FadeTransition( + opacity: CurveTween( + curve: Curves.easeIn, + ).animate(animation), + child: child, + ), + ); + }, + ), + GoRoute( + parentNavigatorKey: _rootNavigatorKey, + name: shippingConfigurationScreen, + path: '/shipping-configuration', + pageBuilder: (context, state) { + final configuration = state.extra as ShippingConfiguration; + + return CustomTransitionPage( + key: state.pageKey, + child: ShippingConfigurationScreen(configuration: configuration), + + transitionsBuilder: + (context, animation, secondaryAnimation, child) => + FadeTransition( + opacity: CurveTween( + curve: Curves.easeIn, + ).animate(animation), + child: child, + ), + ); + }, + ), + GoRoute( + parentNavigatorKey: _rootNavigatorKey, + name: paymentMethodScreen, + path: '/payment-method', + pageBuilder: (context, state) { + final configures = state.extra as List; + + return CustomTransitionPage( + key: state.pageKey, + child: PaymentMethodsScreen(configures: configures), + + transitionsBuilder: + (context, animation, secondaryAnimation, child) => + FadeTransition( + opacity: CurveTween( + curve: Curves.easeIn, + ).animate(animation), + child: child, + ), + ); + }, + ), + GoRoute( + parentNavigatorKey: _rootNavigatorKey, + name: productReviewScreen, + path: '/product-reviews', + pageBuilder: (context, state) { + return CustomTransitionPage( + key: state.pageKey, + child: const ProductReviewScreen(), + + transitionsBuilder: + (context, animation, secondaryAnimation, child) => + FadeTransition( + opacity: CurveTween( + curve: Curves.easeIn, + ).animate(animation), + child: child, + ), + ); + }, + ), + GoRoute( + parentNavigatorKey: _rootNavigatorKey, + name: detailProductReviewScreen, + path: '/product-reviews/detail/:id', + pageBuilder: (context, state) { + final productId = state.pathParameters["id"]!; + return CustomTransitionPage( + key: state.pageKey, + child: DetailProductReviewScreen(productId: productId), + + transitionsBuilder: + (context, animation, secondaryAnimation, child) => + FadeTransition( + opacity: CurveTween( + curve: Curves.easeIn, + ).animate(animation), + child: child, + ), + ); + }, + ), + GoRoute( + parentNavigatorKey: _rootNavigatorKey, + name: fullProductReviewScreen, + path: '/product-reviews/detail/:id/full-reviews', + pageBuilder: (context, state) { + final productId = state.pathParameters["id"]!; + return CustomTransitionPage( + key: state.pageKey, + child: FullProductReviewScreen(productId: productId), + + transitionsBuilder: + (context, animation, secondaryAnimation, child) => + FadeTransition( + opacity: CurveTween( + curve: Curves.easeIn, + ).animate(animation), + child: child, + ), + ); + }, + ), + GoRoute( + parentNavigatorKey: _rootNavigatorKey, + name: ratingConfigurationScreen, + path: '/rating-configuration', + pageBuilder: (context, state) { + return CustomTransitionPage( + key: state.pageKey, + child: const RatingConfigurationScreen(), + + transitionsBuilder: + (context, animation, secondaryAnimation, child) => + FadeTransition( + opacity: CurveTween( + curve: Curves.easeIn, + ).animate(animation), + child: child, + ), + ); + }, + ), + GoRoute( + parentNavigatorKey: _rootNavigatorKey, + name: campaignScreen, + path: '/campaigns', + pageBuilder: (context, state) { + return CustomTransitionPage( + key: state.pageKey, + child: const CampaignScreen(), + + transitionsBuilder: + (context, animation, secondaryAnimation, child) => + FadeTransition( + opacity: CurveTween( + curve: Curves.easeIn, + ).animate(animation), + child: child, + ), + ); + }, + ), + GoRoute( + parentNavigatorKey: _rootNavigatorKey, + name: addCampaignScreen, + path: '/campaigns/add', + pageBuilder: (context, state) { + return CustomTransitionPage( + key: state.pageKey, + child: const AddCampaignScreen(), + + transitionsBuilder: + (context, animation, secondaryAnimation, child) => + FadeTransition( + opacity: CurveTween( + curve: Curves.easeIn, + ).animate(animation), + child: child, + ), + ); + }, + ), + GoRoute( + parentNavigatorKey: _rootNavigatorKey, + name: editCampaignScreen, + path: '/campaigns/edit/:id', + pageBuilder: (context, state) { + final campaignId = state.pathParameters["id"]!; + + return CustomTransitionPage( + key: state.pageKey, + child: EditCampaignScreen(campaignId: campaignId), + + transitionsBuilder: + (context, animation, secondaryAnimation, child) => + FadeTransition( + opacity: CurveTween( + curve: Curves.easeIn, + ).animate(animation), + child: child, + ), + ); + }, + ), + GoRoute( + parentNavigatorKey: _rootNavigatorKey, + name: linkCampaignScreen, + path: '/link-campaign', + pageBuilder: (context, state) { + return CustomTransitionPage( + key: state.pageKey, + child: const LinkCampaignScreen(), + + transitionsBuilder: + (context, animation, secondaryAnimation, child) => + FadeTransition( + opacity: CurveTween( + curve: Curves.easeIn, + ).animate(animation), + child: child, + ), + ); + }, + ), + GoRoute( + parentNavigatorKey: _rootNavigatorKey, + name: targetOutletScreen, + path: '/target-outlet', + pageBuilder: (context, state) { + return CustomTransitionPage( + key: state.pageKey, + child: const TargetOutletScreen(), + + transitionsBuilder: + (context, animation, secondaryAnimation, child) => + FadeTransition( + opacity: CurveTween( + curve: Curves.easeIn, + ).animate(animation), + child: child, + ), + ); + }, + ), + GoRoute( + parentNavigatorKey: _rootNavigatorKey, + name: outletScreen, + path: '/outlets', + pageBuilder: (context, state) { + return CustomTransitionPage( + key: state.pageKey, + child: const OutletScreen(), + + transitionsBuilder: + (context, animation, secondaryAnimation, child) => + FadeTransition( + opacity: CurveTween( + curve: Curves.easeIn, + ).animate(animation), + child: child, + ), + ); + }, + ), + GoRoute( + parentNavigatorKey: _rootNavigatorKey, + name: addOutletScreen, + path: '/outlets/add', + pageBuilder: (context, state) { + return CustomTransitionPage( + key: state.pageKey, + child: const AddOutletScreen(), + + transitionsBuilder: + (context, animation, secondaryAnimation, child) => + FadeTransition( + opacity: CurveTween( + curve: Curves.easeIn, + ).animate(animation), + child: child, + ), + ); + }, + ), + GoRoute( + parentNavigatorKey: _rootNavigatorKey, + name: editOutletScreen, + path: '/outlets/edit/:id', + pageBuilder: (context, state) { + final outletId = state.pathParameters["id"]; + return CustomTransitionPage( + key: state.pageKey, + child: EditOutletScreen(outletId: outletId!), + + transitionsBuilder: + (context, animation, secondaryAnimation, child) => + FadeTransition( + opacity: CurveTween( + curve: Curves.easeIn, + ).animate(animation), + child: child, + ), + ); + }, + ), + GoRoute( + parentNavigatorKey: _rootNavigatorKey, + name: editStaffAdminScreen, + path: '/outlets/staff-admin', + pageBuilder: (context, state) { + final Map data = state.extra as Map; + + final String outletId = data["outlet_id"]; + + final List admins = data["admins"]; + + return CustomTransitionPage( + key: state.pageKey, + child: EditStaffAdminScreen(outletId: outletId, admins: admins), + + transitionsBuilder: + (context, animation, secondaryAnimation, child) => + FadeTransition( + opacity: CurveTween( + curve: Curves.easeIn, + ).animate(animation), + child: child, + ), + ); + }, + ), + GoRoute( + parentNavigatorKey: _rootNavigatorKey, + name: mapOutletAdressScreen, + path: '/outlets/adress/map', + pageBuilder: (context, state) { + return CustomTransitionPage( + key: state.pageKey, + child: const MapOutletAddressScreen(), + + transitionsBuilder: + (context, animation, secondaryAnimation, child) => + FadeTransition( + opacity: CurveTween( + curve: Curves.easeIn, + ).animate(animation), + child: child, + ), + ); + }, + ), + GoRoute( + parentNavigatorKey: _rootNavigatorKey, + name: addProductScreen, + path: '/products/add', + pageBuilder: (context, state) { + return CustomTransitionPage( + key: state.pageKey, + child: const AddProductScreen(), + + transitionsBuilder: + (context, animation, secondaryAnimation, child) => + FadeTransition( + opacity: CurveTween( + curve: Curves.easeIn, + ).animate(animation), + child: child, + ), + ); + }, + ), + GoRoute( + parentNavigatorKey: _rootNavigatorKey, + name: productChoiceScreen, + path: '/products/choices', + pageBuilder: (context, state) { + return CustomTransitionPage( + key: state.pageKey, + child: const GoodChoiceScreen(), + + transitionsBuilder: + (context, animation, secondaryAnimation, child) => + FadeTransition( + opacity: CurveTween( + curve: Curves.easeIn, + ).animate(animation), + child: child, + ), + ); + }, + ), + GoRoute( + parentNavigatorKey: _rootNavigatorKey, + name: editProductScreen, + path: '/products/edit/:id', + pageBuilder: (context, state) { + final productId = state.pathParameters["id"]; + + final data = state.extra as Map; + + final isMainOutlet = data["is_main_outlet"] ?? false; + + final isEditing = data["is_editing"] ?? true; + + final hasNewVariant = data["has_new_variant"] ?? false; + + final newVariantCount = data["new_variant_count"] ?? 0; + + return CustomTransitionPage( + key: state.pageKey, + child: EditProductScreen( + productId: productId!, + isMainOutlet: isMainOutlet, + isEditing: isEditing, + hasNewVariant: hasNewVariant, + newVariantCount: newVariantCount, + ), + + transitionsBuilder: + (context, animation, secondaryAnimation, child) => + FadeTransition( + opacity: CurveTween( + curve: Curves.easeIn, + ).animate(animation), + child: child, + ), + ); + }, + ), + GoRoute( + parentNavigatorKey: _rootNavigatorKey, + name: addVariantProductScreen, + path: '/products/variant/add', + pageBuilder: (context, state) { + final data = state.extra as Map; + + final isMainOutlet = data["is_main_outlet"] ?? false; + + final isStockFixed = data["is_stock_fixed"] ?? false; + + return CustomTransitionPage( + key: state.pageKey, + child: AddProductVariantScreen( + isMainOutlet: isMainOutlet, + isStockFixed: isStockFixed, + ), + transitionsBuilder: + (context, animation, secondaryAnimation, child) => + FadeTransition( + opacity: CurveTween( + curve: Curves.easeIn, + ).animate(animation), + child: child, + ), + ); + }, + ), + GoRoute( + parentNavigatorKey: _rootNavigatorKey, + name: editVariantProductScreen, + path: '/products/variant/edit/:id', + pageBuilder: (context, state) { + final String? productId = state.pathParameters["id"]; + + final data = state.extra as Map; + + final bool isMainOutlet = data["is_main_outlet"]!; + + final bool isEditing = data["is_editing"]!; + + final bool hasNewVariant = data["has_new_variant"]!; + + final bool isStockFixed = data["is_stock_fixed"]!; + + return CustomTransitionPage( + key: state.pageKey, + child: EditProductVariantScreen( + productId: productId!, + isMainOutlet: isMainOutlet, + isEditing: isEditing, + hasNewVariant: hasNewVariant, + isStockFixed: isStockFixed, + ), + transitionsBuilder: + (context, animation, secondaryAnimation, child) => + FadeTransition( + opacity: CurveTween( + curve: Curves.easeIn, + ).animate(animation), + child: child, + ), + ); + }, + ), + GoRoute( + parentNavigatorKey: _rootNavigatorKey, + name: addRawMaterialScreen, + path: '/raw-materials/add', + pageBuilder: (context, state) { + return CustomTransitionPage( + key: state.pageKey, + child: const AddRawMaterialScreen(), + + transitionsBuilder: + (context, animation, secondaryAnimation, child) => + FadeTransition( + opacity: CurveTween( + curve: Curves.easeIn, + ).animate(animation), + child: child, + ), + ); + }, + ), + GoRoute( + parentNavigatorKey: _rootNavigatorKey, + name: rawMaterialChoiceScreen, + path: '/raw-materials/choices', + pageBuilder: (context, state) { + return CustomTransitionPage( + key: state.pageKey, + child: const GoodChoiceScreen(), + + transitionsBuilder: + (context, animation, secondaryAnimation, child) => + FadeTransition( + opacity: CurveTween( + curve: Curves.easeIn, + ).animate(animation), + child: child, + ), + ); + }, + ), + GoRoute( + parentNavigatorKey: _rootNavigatorKey, + name: editRawMaterialScreen, + path: '/raw-materials/edit/:id', + pageBuilder: (context, state) { + final rawMaterialId = state.pathParameters["id"]; + + final data = state.extra as Map; + + final isMainOutlet = data["is_main_outlet"] ?? false; + + final isEditing = data["is_editing"] ?? true; + + return CustomTransitionPage( + key: state.pageKey, + child: EditRawMaterialScreen( + rawMaterialId: rawMaterialId!, + isMainOutlet: isMainOutlet, + isEditing: isEditing, + ), + + transitionsBuilder: + (context, animation, secondaryAnimation, child) => + FadeTransition( + opacity: CurveTween( + curve: Curves.easeIn, + ).animate(animation), + child: child, + ), + ); + }, + ), + GoRoute( + parentNavigatorKey: _rootNavigatorKey, + name: productUsageRawMaterialScreen, + path: '/raw-materials/product/usage', + pageBuilder: (context, state) { + final Map data = state.extra as Map; + + final String? rawMaterialId = data["raw-material-id"]; + + final String unitName = data["unit-name"]; + + final bool isMainOutlet = data["is_main_outlet"]; + + final bool isEditing = data["is_editing"] ?? true; + + return CustomTransitionPage( + key: state.pageKey, + child: ProductUsageRawMaterialScreen( + rawMaterialId: rawMaterialId, + unitName: unitName, + isMainOutlet: isMainOutlet, + isEditing: isEditing, + ), + transitionsBuilder: + (context, animation, secondaryAnimation, child) => + FadeTransition( + opacity: CurveTween( + curve: Curves.easeIn, + ).animate(animation), + child: child, + ), + ); + }, + ), + GoRoute( + parentNavigatorKey: _rootNavigatorKey, + name: alreadyCustomerScreen, + path: '/already-customer', + pageBuilder: (context, state) { + return CustomTransitionPage( + key: state.pageKey, + child: const AlreadyCustomerScreen(), + + transitionsBuilder: + (context, animation, secondaryAnimation, child) => + FadeTransition( + opacity: CurveTween( + curve: Curves.easeIn, + ).animate(animation), + child: child, + ), + ); + }, + ), + GoRoute( + parentNavigatorKey: _rootNavigatorKey, + name: transactionReportOrderDetailScreen, + path: '/transaction-report-order-detail', + pageBuilder: (context, state) { + final OrderDetail orderDetail = state.extra as OrderDetail; + + return CustomTransitionPage( + key: state.pageKey, + child: TransactionReportOrderDetailScreen(orderDetail: orderDetail), + + transitionsBuilder: + (context, animation, secondaryAnimation, child) => + FadeTransition( + opacity: CurveTween( + curve: Curves.easeIn, + ).animate(animation), + child: child, + ), + ); + }, + ), + GoRoute( + parentNavigatorKey: _rootNavigatorKey, + name: transactionReportPurchaseDetailScreen, + path: '/transaction-report-purchase-detail', + pageBuilder: (context, state) { + final PurchaseDetail purchaseDetail = state.extra as PurchaseDetail; + + return CustomTransitionPage( + key: state.pageKey, + child: TransactionReportPurchaseDetailScreen( + purchaseDetail: purchaseDetail, + ), + + transitionsBuilder: + (context, animation, secondaryAnimation, child) => + FadeTransition( + opacity: CurveTween( + curve: Curves.easeIn, + ).animate(animation), + child: child, + ), + ); + }, + ), + GoRoute( + parentNavigatorKey: _rootNavigatorKey, + name: stockReportDetailScreen, + path: '/stok-report-detail-screen', + pageBuilder: (context, state) { + final StockReportDetail stockReportDetail = + state.extra as StockReportDetail; + + return CustomTransitionPage( + key: state.pageKey, + child: StockReportDetailScreen( + stockReportDetail: stockReportDetail, + ), + + transitionsBuilder: + (context, animation, secondaryAnimation, child) => + FadeTransition( + opacity: CurveTween( + curve: Curves.easeIn, + ).animate(animation), + child: child, + ), + ); + }, + ), + GoRoute( + parentNavigatorKey: _rootNavigatorKey, + name: profileInfoScreen, + path: '/profile-info', + pageBuilder: (context, state) { + final profileInfo = state.extra as ProfileInfo; + + return CustomTransitionPage( + key: state.pageKey, + child: ProfileInfoScreen(profileInfo: profileInfo), + + transitionsBuilder: + (context, animation, secondaryAnimation, child) => + FadeTransition( + opacity: CurveTween( + curve: Curves.easeIn, + ).animate(animation), + child: child, + ), + ); + }, + ), + GoRoute( + parentNavigatorKey: _rootNavigatorKey, + name: businessInfoScreen, + path: '/business-info', + pageBuilder: (context, state) { + final businessInfo = state.extra as BusinessInfo; + + return CustomTransitionPage( + key: state.pageKey, + child: BusinessInfoScreen(businessInfo: businessInfo), + + transitionsBuilder: + (context, animation, secondaryAnimation, child) => + FadeTransition( + opacity: CurveTween( + curve: Curves.easeIn, + ).animate(animation), + child: child, + ), + ); + }, + ), + GoRoute( + parentNavigatorKey: _rootNavigatorKey, + name: outletInfoScreen, + path: '/outlet-info', + pageBuilder: (context, state) { + final outletInfo = state.extra as OutletInfo; + return CustomTransitionPage( + key: state.pageKey, + child: OutletInfoScreen(outletInfo: outletInfo), + + transitionsBuilder: + (context, animation, secondaryAnimation, child) => + FadeTransition( + opacity: CurveTween( + curve: Curves.easeIn, + ).animate(animation), + child: child, + ), + ); + }, + ), + GoRoute( + parentNavigatorKey: _rootNavigatorKey, + name: changePasswordScreen, + path: '/change-password', + pageBuilder: (context, state) { + return CustomTransitionPage( + key: state.pageKey, + child: const ChangePasswordScreen(), + + transitionsBuilder: + (context, animation, secondaryAnimation, child) => + FadeTransition( + opacity: CurveTween( + curve: Curves.easeIn, + ).animate(animation), + child: child, + ), + ); + }, + ), + ], + ); +} diff --git a/Mobile Operasional/lib/core/services/sync_services.dart b/Mobile Operasional/lib/core/services/sync_services.dart new file mode 100644 index 0000000..f2e42de --- /dev/null +++ b/Mobile Operasional/lib/core/services/sync_services.dart @@ -0,0 +1,459 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; +import 'package:connectivity_plus/connectivity_plus.dart'; +import 'package:dio/dio.dart'; +import 'package:drift/drift.dart'; +import 'package:niogu_app/core/constants/app_url.dart'; +import 'package:niogu_app/core/database/app_database.dart'; +import 'package:niogu_app/core/enums/sync_status.dart'; +import 'package:niogu_app/core/enums/table_type.dart'; +import 'package:niogu_app/core/system/system_setting.dart'; +import 'package:niogu_app/core/utils/log_message.dart'; +import 'package:rxdart/rxdart.dart'; + +class SyncService { + final Dio _dio; + final AppDatabase _db; + StreamSubscription? _syncSubscription; + bool _isProcessing = false; + int _retryCount = 0; + + SyncService(this._dio, this._db); + + final Map _tablePriority = { + 'tenants': 1, + 'tenant_payment_methods': 2, + 'campaigns': 3, + 'outlets': 4, + 'outlet_campaigns': 5, + 'users': 6, + 'units': 7, + 'categories': 8, + 'products': 9, + 'product_variants': 10, + 'raw_materials': 11, + 'outlet_inventories': 12, + 'receipt_items': 13, + 'customers': 14, + 'customer_addresses': 15, + 'product_likes': 16, + 'product_reviews': 17, + 'suppliers': 18, + 'orders': 19, + 'purchases': 20, + 'transactions': 21, + 'order_items': 22, + 'purchase_items': 23, + 'stock_cards': 24, + }; + + final _imageFields = [ + 'server_logo_url', + 'server_photo_url', + 'server_banner_url', + 'server_image_url', + 'payment_proof_url', + 'product_image_url_snapshot', + 'item_image_url_snapshot', + ]; + + Future startWatching() async { + _syncSubscription = + (_db.select(_db.syncQueues)..orderBy([ + (t) => OrderingTerm(expression: t.id, mode: OrderingMode.asc), + ])) + .watch() + .debounceTime(const Duration(seconds: 3)) + .listen((queuedItems) async { + if (queuedItems.isNotEmpty && !_isProcessing) { + await processBatch(); + } + }); + } + + Future restartUpSync() async { + final connectivityResult = await Connectivity().checkConnectivity(); + + if (connectivityResult.any((con) => con == ConnectivityResult.none)) { + throw Exception(); + } + + await (_db.update( + _db.syncQueues, + )..where((t) => t.retryCount.isBiggerOrEqualValue(5))).write( + SyncQueuesCompanion( + status: Value(SyncStatus.dirty.status), + retryCount: Value(0), + ), + ); + } + + Future processBatch() async { + LogMessage.log.i("=== Process Batch ==="); + final isLoggedIn = await SystemSetting.isLoggedIn(); + + if (!isLoggedIn) return; + + final connectivityResult = await Connectivity().checkConnectivity(); + + if (connectivityResult.any((con) => con == ConnectivityResult.none)) return; + + if (_isProcessing) return; + + LogMessage.log.i("=== Start Process === "); + + _isProcessing = true; + + final String currentBatchId = DateTime.now().millisecondsSinceEpoch + .toString(); + + try { + final rawItems = + await (_db.select(_db.syncQueues) + ..where( + (t) => + (t.status.equals(SyncStatus.dirty.status) | + t.status.equals(SyncStatus.failed.status)) & + t.retryCount.isSmallerThanValue(5), + ) + ..orderBy([ + (t) => OrderingTerm(expression: t.id, mode: OrderingMode.asc), + ])) + .get(); + + if (rawItems.isEmpty) { + _isProcessing = false; + return; + } + + final sortedItems = List.from(rawItems); + sortedItems.sort((a, b) { + final int priorityA = _tablePriority[a.tableType] ?? 99; + final int priorityB = _tablePriority[b.tableType] ?? 99; + + if (priorityA == priorityB) { + return a.id.compareTo(b.id); + } + return priorityA.compareTo(priorityB); + }); + + final items = sortedItems.take(100).toList(); + + await _db.batch((batch) { + for (var item in items) { + batch.update( + _db.syncQueues, + SyncQueuesCompanion( + status: Value(SyncStatus.syncing.status), + batchId: Value(currentBatchId), + ), + where: (t) => t.id.equals(item.id), + ); + } + }); + + await Future.delayed(const Duration(seconds: 3)); + + await _handleImageUploads(items); + + await Future.delayed(const Duration(seconds: 3)); + + final refreshedItems = await (_db.select( + _db.syncQueues, + )..where((t) => t.id.isIn(items.map((e) => e.id)))).get(); + + await Future.delayed(const Duration(seconds: 3)); + + final batchPayload = { + 'batch_id': currentBatchId, + 'items': refreshedItems + .map((item) => jsonEncode(item.payload)) + .toList(), + }; + + LogMessage.log.i("=== Process To Sync Up, Try $_retryCount ==="); + + final response = await _dio.post(AppUrl.SYNC_UP, data: batchPayload); + + await Future.delayed(const Duration(seconds: 3)); + + final report = response.data['report']; + + await _db.transaction(() async { + for (var item in refreshedItems) { + final hasError = (report['errors'] as List).any( + (e) => e['local_id'] == item.tableLocalId, + ); + + if (!hasError) { + final payload = Map.from(item.payload); + + await _updateMasterSyncStatus( + item.tableType, + item.tableLocalId, + SyncStatus.synced.status, + data: payload['data'], + ); + + await (_db.delete( + _db.syncQueues, + )..where((t) => t.id.equals(item.id))).go(); + } + } + }); + + LogMessage.log.i("=== Process Sync Up Done ==="); + } catch (e, st) { + LogMessage.log.e( + "=== Process Sync Up Failed ===", + error: e, + stackTrace: st, + ); + await _handleBatchFailure(currentBatchId, e.toString()); + rethrow; + } finally { + _isProcessing = false; + _retryCount++; + } + } + + Future _handleImageUploads(List items) async { + for (final item in items) { + final payload = Map.from(item.payload); + final data = Map.from(payload['data']); + bool isChanged = false; + + for (final field in _imageFields) { + if (data.containsKey(field) && data[field] != null) { + final imageValue = data[field]; + + if (imageValue is List) { + final List serverPaths = []; + + for (final path in imageValue) { + final pathStr = path.toString(); + + if (_isLocalPath(pathStr)) { + final folderType = _getFolderTypeFromPath(pathStr); + + final uploaded = await _uploadSingleFile(pathStr, folderType); + + if (uploaded != null) { + serverPaths.add(uploaded); + isChanged = true; + } else { + serverPaths.add(pathStr); + } + } else { + serverPaths.add(pathStr); + } + } + data[field] = serverPaths; + } else if (imageValue is String && _isLocalPath(imageValue)) { + final folderType = _getFolderTypeFromPath(imageValue); + + final uploaded = await _uploadSingleFile(imageValue, folderType); + + if (uploaded != null) { + data[field] = uploaded; + isChanged = true; + } + } + } + } + + if (isChanged) { + payload['data'] = data; + await (_db.update(_db.syncQueues)..where((t) => t.id.equals(item.id))) + .write(SyncQueuesCompanion(payload: Value(payload))); + + LogMessage.log.i( + "Payload updated with server paths for item: ${item.tableLocalId}", + ); + } + } + } + + bool _isLocalPath(String path) { + return path.startsWith('/') || + path.contains('app_flutter') || + path.contains('cache'); + } + + String _getFolderTypeFromPath(String path) { + if (path.contains('product_category_images')) { + return 'product_category_images'; + } else if (path.contains('product_images')) { + return 'product_images'; + } else if (path.contains('product_variant_images')) { + return 'product_variant_images'; + } else if (path.contains('payment_proof_orders')) { + return 'payment_proof_orders'; + } else if (path.contains('payment_proof_purchases')) { + return 'payment_proof_purchases'; + } else if (path.contains('outlet_banner_images')) { + return 'outlet_banner_images'; + } else if(path.contains('campaign_images')) { + return 'campaign_images'; + } else if(path.contains('logo_images')) { + return 'logo_images'; + } + return 'others'; + } + + Future _uploadSingleFile(String localPath, String folderType) async { + try { + final file = File(localPath); + if (!await file.exists()) { + LogMessage.log.e("File fisik tidak ditemukan: $localPath"); + return null; + } + + final fileName = localPath.split('/').last; + + final formData = FormData.fromMap({ + "type": folderType, + "image": await MultipartFile.fromFile(localPath, filename: fileName), + }); + + final response = await _dio.post(AppUrl.UPLOAD_IMAGE, data: formData); + + if (response.statusCode == 200) { + return response.data['data']['server_path']; + } + return null; + } catch (e) { + LogMessage.log.e("Gagal upload ke $folderType: $e"); + return null; + } + } + + Future _updateMasterSyncStatus( + String tableName, + String localId, + String status, { + Map? data, + }) async { + String setClause = 'SET sync_status = ?, server_id = ?'; + + List variables = [Variable(status), Variable(localId)]; + + if (data != null) { + for (var field in _imageFields) { + if (data.containsKey(field) && data[field] != null) { + setClause += ', $field = ?'; + + final value = data[field] is List + ? jsonEncode(data[field]) + : data[field]; + + variables.add(Variable(value)); + } + } + } + + variables.add(Variable(localId)); + + await _db.customUpdate( + 'UPDATE $tableName $setClause WHERE local_id = ?', + variables: variables, + updates: {_getTableSet(TableType.values.byName(tableName))}, + ); + } + + ResultSetImplementation _getTableSet(TableType type) { + switch (type) { + case TableType.campaigns: + return _db.campaigns; + case TableType.categories: + return _db.categories; + case TableType.customers: + return _db.customers; + case TableType.order_items: + return _db.orderItems; + case TableType.orders: + return _db.orders; + case TableType.outlet_campaigns: + return _db.outletCampaigns; + case TableType.outlet_inventories: + return _db.outletInventories; + case TableType.outlets: + return _db.outlets; + case TableType.product_likes: + return _db.productLikes; + case TableType.product_reviews: + return _db.productReviews; + case TableType.product_variants: + return _db.productVariants; + case TableType.products: + return _db.products; + case TableType.purchase_items: + return _db.purchaseItems; + case TableType.purchases: + return _db.purchases; + case TableType.raw_materials: + return _db.rawMaterials; + case TableType.receipt_items: + return _db.receiptItems; + case TableType.stock_cards: + return _db.stockCards; + case TableType.suppliers: + return _db.suppliers; + case TableType.tenant_payment_methods: + return _db.tenantPaymentMethods; + case TableType.tenants: + return _db.tenants; + case TableType.transactions: + return _db.transactions; + case TableType.units: + return _db.units; + case TableType.users: + return _db.users; + } + } + + Future _handleBatchFailure(String batchId, String errorMessage) async { + await Future.delayed(const Duration(milliseconds: 500)); + + try { + await _db.transaction(() async { + final items = await (_db.select( + _db.syncQueues, + )..where((t) => t.batchId.equals(batchId))).get(); + + for (var item in items) { + final isPermanentFailure = item.retryCount >= 5; + + await (_db.update( + _db.syncQueues, + )..where((t) => t.id.equals(item.id))).write( + SyncQueuesCompanion( + status: Value( + isPermanentFailure + ? SyncStatus.failed.status + : SyncStatus.dirty.status, + ), + batchId: const Value(null), + retryCount: Value(item.retryCount + 1), + lastError: Value(errorMessage), + ), + ); + } + }); + + LogMessage.log.e( + "Batch $batchId failed. Items reverted to dirty for retry. with error $errorMessage", + ); + } catch (e, st) { + LogMessage.log.e( + "Critical error in _handleBatchFailure: ${e.toString()}", + error: e, + stackTrace: st, + ); + rethrow; + } + } + + void stopWatching() => _syncSubscription?.cancel(); +} diff --git a/Mobile Operasional/lib/core/services/working_service.dart b/Mobile Operasional/lib/core/services/working_service.dart new file mode 100644 index 0000000..e6d7a4c --- /dev/null +++ b/Mobile Operasional/lib/core/services/working_service.dart @@ -0,0 +1,24 @@ +import 'package:niogu_app/core/database/app_database.dart'; +import 'package:niogu_app/core/network/dio_client.dart'; +import 'package:niogu_app/core/services/sync_services.dart'; +import 'package:workmanager/workmanager.dart'; + +@pragma('vm:entry-point') +void callbackDispatcher() { + Workmanager().executeTask((taskName, inputData) async { + final db = AppDatabase(); + + final dio = DioClient().instance; + + final syncService = SyncService(dio, db); + + try { + await syncService.processBatch(); + return Future.value(true); + } catch (e) { + return Future.value(false); + } finally { + await db.close(); + } + }); +} diff --git a/Mobile Operasional/lib/core/system/system_secure.dart b/Mobile Operasional/lib/core/system/system_secure.dart new file mode 100644 index 0000000..b9e0213 --- /dev/null +++ b/Mobile Operasional/lib/core/system/system_secure.dart @@ -0,0 +1,36 @@ +import 'dart:convert'; +import 'dart:math'; + +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; +import 'package:niogu_app/core/constants/app_key.dart'; + +class SystemSecure { + static final _storage = const FlutterSecureStorage(); + + static Future generateDatabaseKey(String tenantCode) async { + final random = Random.secure(); + + final values = List.generate(32, (i) => random.nextInt(256)); + + await _storage.write( + key: "${AppKey.DATABASE_ENCRYPTION}_$tenantCode", + value: base64Url.encode(values), + ); + } + + static Future getDatabaseKey(String tenantCode) async { + return await _storage.read(key: "${AppKey.DATABASE_ENCRYPTION}_$tenantCode"); + } + + static Future saveAccessToken(String accessToken) async { + await _storage.write(key: AppKey.AUTH_TOKEN, value: accessToken); + } + + static Future getAccessToken() async { + return await _storage.read(key: AppKey.AUTH_TOKEN); + } + + static Future deleteAccessToken() async { + await _storage.delete(key: AppKey.AUTH_TOKEN); + } +} diff --git a/Mobile Operasional/lib/core/system/system_setting.dart b/Mobile Operasional/lib/core/system/system_setting.dart new file mode 100644 index 0000000..8b7d72e --- /dev/null +++ b/Mobile Operasional/lib/core/system/system_setting.dart @@ -0,0 +1,152 @@ +import 'package:niogu_app/core/constants/app_key.dart'; +import 'package:niogu_app/core/enums/user_role.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +class SystemSetting { + static Future isLoggedIn() async { + final prefs = await SharedPreferences.getInstance(); + + return await prefs.getBool(AppKey.USER_LOGIN) ?? false; + } + + static Future setIsLoggedIn() async { + final prefs = await SharedPreferences.getInstance(); + + await prefs.setBool(AppKey.USER_LOGIN, true); + } + + static Future getTenantId() async { + final prefs = await SharedPreferences.getInstance(); + + return await prefs.getString(AppKey.TENANT_ID); + } + + static Future getTenantCode() async { + final prefs = await SharedPreferences.getInstance(); + + return await prefs.getString(AppKey.TENANT_CODE); + } + + static Future getTenantName() async { + final prefs = await SharedPreferences.getInstance(); + + return await prefs.getString(AppKey.TENANT_NAME); + } + + static Future setTenant({ + String? tenantId, + String? tenantCode, + String? tenantName, + }) async { + final prefs = await SharedPreferences.getInstance(); + + if (tenantId != null) { + prefs.setString(AppKey.TENANT_ID, tenantId); + } + + if (tenantCode != null) { + prefs.setString(AppKey.TENANT_CODE, tenantCode); + } + + if (tenantName != null) { + prefs.setString(AppKey.TENANT_NAME, tenantName); + } + } + + static Future getOutletIdByOwner() async { + final prefs = await SharedPreferences.getInstance(); + + return await prefs.getString(AppKey.OUTLET_ID_BY_OWNER); + } + + static Future setOutletIdByOwner(String? outletId) async { + final prefs = await SharedPreferences.getInstance(); + + if (outletId != null) { + prefs.setString(AppKey.OUTLET_ID_BY_OWNER, outletId); + } + } + + static Future getCurrentOutletId() async { + final prefs = await SharedPreferences.getInstance(); + + return await prefs.getString(AppKey.OUTLET_ID); + } + + static Future getCurrentOutletName() async { + final prefs = await SharedPreferences.getInstance(); + + return await prefs.getString(AppKey.OUTLET_NAME); + } + + static Future switchOutlet({ + String? outletId, + String? outletName, + }) async { + final prefs = await SharedPreferences.getInstance(); + if (outletId != null) { + prefs.setString(AppKey.OUTLET_ID, outletId); + } + + if (outletName != null) { + prefs.setString(AppKey.OUTLET_NAME, outletName); + } + } + + static Future getCurrentUserId() async { + final prefs = await SharedPreferences.getInstance(); + + return await prefs.getString(AppKey.USER_ID); + } + + static Future getCurrentUserName() async { + final prefs = await SharedPreferences.getInstance(); + + return await prefs.getString(AppKey.USER_NAME); + } + + static Future getCurrentUserEmail() async { + final prefs = await SharedPreferences.getInstance(); + + return await prefs.getString(AppKey.USER_EMAIL); + } + + static Future getCurrentUserRole() async { + final prefs = await SharedPreferences.getInstance(); + + final userRole = await prefs.getString(AppKey.USER_ROLE); + + return UserRole.values.byName(userRole ?? 'owner'); + } + + static Future setUser({ + String? userId, + String? userName, + String? userEmail, + UserRole? userRole, + }) async { + final prefs = await SharedPreferences.getInstance(); + + if (userId != null) { + prefs.setString(AppKey.USER_ID, userId); + } + + if (userName != null) { + prefs.setString(AppKey.USER_NAME, userName); + } + + if (userEmail != null) { + prefs.setString(AppKey.USER_EMAIL, userEmail); + } + + if (userRole != null) { + prefs.setString(AppKey.USER_ROLE, userRole.role); + } + } + + static Future clear() async { + final prefs = await SharedPreferences.getInstance(); + + await prefs.clear(); + } +} diff --git a/Mobile Operasional/lib/core/utils/bcrypt.dart b/Mobile Operasional/lib/core/utils/bcrypt.dart new file mode 100644 index 0000000..8d0c998 --- /dev/null +++ b/Mobile Operasional/lib/core/utils/bcrypt.dart @@ -0,0 +1,11 @@ +import 'package:bcrypt/bcrypt.dart'; + +class Bcrypt { + static String hashPassword(String plainPassword) { + return BCrypt.hashpw(plainPassword, BCrypt.gensalt()); + } + + static bool verifyPassword(String input, String hash) { + return BCrypt.checkpw(input, hash); + } +} diff --git a/Mobile Operasional/lib/core/utils/captcha_noise_pointer.dart b/Mobile Operasional/lib/core/utils/captcha_noise_pointer.dart new file mode 100644 index 0000000..9d27cfd --- /dev/null +++ b/Mobile Operasional/lib/core/utils/captcha_noise_pointer.dart @@ -0,0 +1,33 @@ +import 'dart:math'; +import 'package:flutter/material.dart'; + +class CaptchaNoisePainter extends CustomPainter { + @override + void paint(Canvas canvas, Size size) { + final paint = Paint() + ..color = Colors.grey.withOpacity(0.3) + ..strokeWidth = 1.5 + ..strokeCap = StrokeCap.round; + + final random = Random(); + + for (int i = 0; i < 6; i++) { + canvas.drawLine( + Offset(random.nextDouble() * size.width, random.nextDouble() * size.height), + Offset(random.nextDouble() * size.width, random.nextDouble() * size.height), + paint, + ); + } + + for (int i = 0; i < 40; i++) { + canvas.drawCircle( + Offset(random.nextDouble() * size.width, random.nextDouble() * size.height), + 1, + paint, + ); + } + } + + @override + bool shouldRepaint(covariant CustomPainter oldDelegate) => true; +} \ No newline at end of file diff --git a/Mobile Operasional/lib/core/utils/currency_format.dart b/Mobile Operasional/lib/core/utils/currency_format.dart new file mode 100644 index 0000000..d804779 --- /dev/null +++ b/Mobile Operasional/lib/core/utils/currency_format.dart @@ -0,0 +1,12 @@ +import 'package:intl/intl.dart'; + +class CurrencyFormat { + static String formatToIdr(double amount, int decimalDigits) { + final NumberFormat currencyFormatter = NumberFormat.currency( + locale: 'id_ID', + symbol: 'Rp', + decimalDigits: decimalDigits, + ); + return currencyFormatter.format(amount); + } +} diff --git a/Mobile Operasional/lib/core/utils/date_input_formatter.dart b/Mobile Operasional/lib/core/utils/date_input_formatter.dart new file mode 100644 index 0000000..b074d0f --- /dev/null +++ b/Mobile Operasional/lib/core/utils/date_input_formatter.dart @@ -0,0 +1,29 @@ +import 'package:flutter/services.dart'; + +class DateInputFormatter extends TextInputFormatter { + @override + TextEditingValue formatEditUpdate( + TextEditingValue oldValue, TextEditingValue newValue) { + final text = newValue.text; + + // Mencegah input lebih dari 10 karakter (DD/MM/YYYY) + if (text.length > 10) return oldValue; + + if (newValue.selection.baseOffset < oldValue.selection.baseOffset) { + return newValue; + } + + var newText = text; + // Menambahkan "/" otomatis setelah posisi ke-2 (hari) dan ke-5 (bulan) + if (text.length == 2 || text.length == 5) { + if (!text.endsWith('/')) { + newText = '$text/'; + } + } + + return newValue.copyWith( + text: newText, + selection: TextSelection.collapsed(offset: newText.length), + ); + } +} \ No newline at end of file diff --git a/Mobile Operasional/lib/core/utils/extension_format.dart b/Mobile Operasional/lib/core/utils/extension_format.dart new file mode 100644 index 0000000..f965a53 --- /dev/null +++ b/Mobile Operasional/lib/core/utils/extension_format.dart @@ -0,0 +1,57 @@ +import 'package:flutter/material.dart'; + +extension DoubleFormatter on double { + String toStringWithoutTrailingZero() { + if (this % 1 == 0) { + return toStringAsFixed(0); + } else { + return toString(); + } + } + + String toRatingFormat({int maxDecimal = 2}) { + final rounded = double.parse(toStringAsFixed(maxDecimal)); + String result = rounded.toString(); + if (result.contains('.')) { + result = result.replaceFirst(RegExp(r'\.?0+$'), ''); + } + + return result; + } + + String get toCompact { + if (this < 1000) { + return toInt().toString(); + } else { + double result = this / 1000; + + String s = result.toStringAsFixed(2); + + s = s.replaceAll(RegExp(r"([.]*0+)(?!.*\d)"), ""); + + s = s.replaceAll('.', ','); + + return "${s}k"; + } + } +} + +extension StringFormatter on String { + String toUniqueNumberCleaner() { + String clean = this.replaceFirst(RegExp(r'-[A-Z0-9]{8}-'), '-'); + + clean = clean.replaceFirst(RegExp(r'-[A-Z0-9]{3}(?=\d{5})'), '-'); + + return clean; + } + + String capitalize() { + return "${this[0].toUpperCase()}${substring(1)}"; + } +} + +extension TimeOfDayExtension on TimeOfDay { + bool isAfterOrEqual(TimeOfDay other) { + return this.compareTo(other) >= 0; + } +} diff --git a/Mobile Operasional/lib/core/utils/image_service.dart b/Mobile Operasional/lib/core/utils/image_service.dart new file mode 100644 index 0000000..54678df --- /dev/null +++ b/Mobile Operasional/lib/core/utils/image_service.dart @@ -0,0 +1,43 @@ +import 'dart:io'; +import 'package:niogu_app/core/utils/log_message.dart'; +import 'package:path_provider/path_provider.dart'; +import 'package:path/path.dart' as p; + +class ImageService { + static Future saveImageToLocalDirectory( + File sourceFile, + String dir, + ) async { + try { + final directory = await getApplicationDocumentsDirectory(); + + final String path = directory.path; + final Directory imageDir = Directory('$path/$dir'); + if (!await imageDir.exists()) { + await imageDir.create(recursive: true); + } + + final String fileName = '${DateTime.now().millisecondsSinceEpoch}.jpg'; + + final String newPath = p.join(imageDir.path, fileName); + + final File newImage = await sourceFile.copy(newPath); + + return newImage.path; + } catch (e, st) { + LogMessage.log.e(e.toString(), error: e, stackTrace: st); + return null; + } + } + + static Future deleteLocalImage(String path) async { + try { + final file = File(path); + if (await file.exists()) { + await file.delete(); + } + } catch (e, st) { + LogMessage.log.e(e.toString(), error: e, stackTrace: st); + } + } +} diff --git a/Mobile Operasional/lib/core/utils/log_message.dart b/Mobile Operasional/lib/core/utils/log_message.dart new file mode 100644 index 0000000..d415342 --- /dev/null +++ b/Mobile Operasional/lib/core/utils/log_message.dart @@ -0,0 +1,15 @@ +import 'package:logger/logger.dart'; + +class LogMessage { + + static Logger _getLog() { + return Logger( + filter: DevelopmentFilter(), + level: Level.all, + output: ConsoleOutput(), + printer: PrettyPrinter(methodCount: 10), + ); + } + + static Logger get log => _getLog(); +} diff --git a/Mobile Operasional/lib/core/utils/login_required.dart b/Mobile Operasional/lib/core/utils/login_required.dart new file mode 100644 index 0000000..814c04e --- /dev/null +++ b/Mobile Operasional/lib/core/utils/login_required.dart @@ -0,0 +1,106 @@ +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'package:niogu_app/core/constants/app_color.dart'; +import 'package:niogu_app/core/constants/app_font_size.dart'; +import 'package:niogu_app/core/router/app_route.dart'; +import 'package:sizer/sizer.dart'; + +class LoginRequired { + static void showLoginRequired(BuildContext context) { + showDialog( + context: context, + builder: (context) { + final bool isTablet = 100.w >= 600; + + return Dialog( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(5.w), + ), + backgroundColor: Colors.white, + child: Padding( + padding: EdgeInsets.all(6.w), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + padding: EdgeInsets.all(4.w), + decoration: BoxDecoration( + color: Colors.blue[50], + shape: BoxShape.circle, + ), + child: Icon( + Icons.lock_person_rounded, + color: AppColor.primaryColor, + size: 12.w, + ), + ), + + SizedBox(height: 3.h), + + Text( + "Akses Terbatas", + style: TextStyle( + fontSize: (AppFontSize.medium - (isTablet ? 0 : 1.25)).sp, + fontWeight: FontWeight.bold, + color: AppColor.primaryColor, + ), + ), + SizedBox(height: 1.5.h), + Text( + "Masuk ke akun terlebih dahulu untuk mengakses fitur ini.", + textAlign: TextAlign.center, + style: TextStyle( + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + color: Colors.grey[600], + height: 1.5, + ), + ), + + SizedBox(height: 4.h), + + ElevatedButton( + onPressed: () { + context.pop(); + context.goNamed(AppRoute.ownerLoginScreen); + }, + style: ElevatedButton.styleFrom( + backgroundColor: AppColor.primaryColor, + minimumSize: Size(double.infinity, 6.5.h), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(3.w), + ), + elevation: 0, + ), + child: Text( + "Masuk Akun", + style: TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + fontSize: AppFontSize.medium.sp, + ), + ), + ), + + SizedBox(height: 1.5.h), + + TextButton( + onPressed: () => context.pop(), + child: Text( + "Nanti Saja", + style: TextStyle( + color: Colors.grey, + fontWeight: FontWeight.w500, + fontSize: AppFontSize.medium.sp, + ), + ), + ), + ], + ), + ), + ); + }, + ); + } +} diff --git a/Mobile Operasional/lib/core/utils/security_manager.dart b/Mobile Operasional/lib/core/utils/security_manager.dart new file mode 100644 index 0000000..e82e743 --- /dev/null +++ b/Mobile Operasional/lib/core/utils/security_manager.dart @@ -0,0 +1,24 @@ +import 'dart:convert'; +import 'dart:math'; + +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; +import 'package:niogu_app/core/constants/app_key.dart'; + +class SecurityKeyManager { + static const _storage = FlutterSecureStorage(); + + static Future getOrCreateDatabaseKey() async { + final existing = await _storage.read(key: AppKey.DATABASE_ENCRYPTION); + + if (existing == null) { + final random = Random.secure(); + + final values = List.generate(32, (i) => random.nextInt(256)); + + await _storage.write( + key: AppKey.DATABASE_ENCRYPTION, + value: base64Url.encode(values), + ); + } + } +} diff --git a/Mobile Operasional/lib/core/utils/string_list_converter.dart b/Mobile Operasional/lib/core/utils/string_list_converter.dart new file mode 100644 index 0000000..0519820 --- /dev/null +++ b/Mobile Operasional/lib/core/utils/string_list_converter.dart @@ -0,0 +1,20 @@ +import 'dart:convert'; + +import 'package:drift/drift.dart'; + +class StringListConverter extends TypeConverter, String>{ + @override + List fromSql(String fromDb) { + try { + return (jsonDecode(fromDb) as List).cast(); + } catch(e) { + return []; + } + } + + @override + String toSql(List value) { + return jsonEncode(value); + } + +} \ No newline at end of file diff --git a/Mobile Operasional/lib/core/utils/string_list_map_converter.dart b/Mobile Operasional/lib/core/utils/string_list_map_converter.dart new file mode 100644 index 0000000..6ecc0d6 --- /dev/null +++ b/Mobile Operasional/lib/core/utils/string_list_map_converter.dart @@ -0,0 +1,20 @@ +import 'dart:convert'; + +import 'package:drift/drift.dart'; + +class StringListMapConverter extends TypeConverter>, String> { + @override + List> fromSql(String fromDb) { + try { + return (jsonDecode(fromDb) as List).cast>(); + } catch(e) { + return []; + } + } + + @override + String toSql(List> value) { + return jsonEncode(value); + } + +} \ No newline at end of file diff --git a/Mobile Operasional/lib/core/utils/string_map_converter.dart b/Mobile Operasional/lib/core/utils/string_map_converter.dart new file mode 100644 index 0000000..a52da99 --- /dev/null +++ b/Mobile Operasional/lib/core/utils/string_map_converter.dart @@ -0,0 +1,20 @@ +import 'dart:convert'; + +import 'package:drift/drift.dart'; + +class StringMapConverter extends TypeConverter, String> { + @override + Map fromSql(String fromDb) { + try { + return (jsonDecode(fromDb) as Map).cast(); + } catch(e) { + return {}; + } + } + + @override + String toSql(Map value) { + return jsonEncode(value); + } + +} \ No newline at end of file diff --git a/Mobile Operasional/lib/core/utils/time_zone.dart b/Mobile Operasional/lib/core/utils/time_zone.dart new file mode 100644 index 0000000..b44ef3a --- /dev/null +++ b/Mobile Operasional/lib/core/utils/time_zone.dart @@ -0,0 +1,16 @@ +class TimeZone { + static String getCurrentTimeZone() { + final int offset = DateTime.now().timeZoneOffset.inHours; + + switch (offset) { + case 7: + return 'WIB'; + case 8: + return 'WITA'; + case 9: + return 'WIT'; + default: + return DateTime.now().timeZoneName; + } + } +} \ No newline at end of file diff --git a/Mobile Operasional/lib/core/widgets/custom_button.dart b/Mobile Operasional/lib/core/widgets/custom_button.dart new file mode 100644 index 0000000..c2d63bb --- /dev/null +++ b/Mobile Operasional/lib/core/widgets/custom_button.dart @@ -0,0 +1,37 @@ +import 'package:flutter/material.dart'; +import 'package:niogu_app/core/constants/app_color.dart'; +import 'package:niogu_app/core/constants/app_font_size.dart'; +import 'package:sizer/sizer.dart'; + +class CustomButton extends StatelessWidget { + final String title; + final VoidCallback? onPressed; + + const CustomButton({super.key, required this.title, this.onPressed}); + + @override + Widget build(BuildContext context) { + return SizedBox( + width: double.infinity, + height: 6.5.h, + child: ElevatedButton( + onPressed: onPressed, + style: ElevatedButton.styleFrom( + backgroundColor: AppColor.primaryColor, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(2.5.w), + ), + disabledBackgroundColor: Colors.grey.shade300 + ), + child: Text( + title, + style: TextStyle( + color: Colors.white, + fontSize: AppFontSize.medium.sp, + fontWeight: FontWeight.bold, + ), + ), + ), + ); + } +} diff --git a/Mobile Operasional/lib/core/widgets/custom_confirmation.dart b/Mobile Operasional/lib/core/widgets/custom_confirmation.dart new file mode 100644 index 0000000..fe4aa20 --- /dev/null +++ b/Mobile Operasional/lib/core/widgets/custom_confirmation.dart @@ -0,0 +1,149 @@ +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'package:niogu_app/core/constants/app_color.dart'; +import 'package:niogu_app/core/constants/app_font_size.dart'; +import 'package:sizer/sizer.dart'; + +class CustomConfirmation extends StatelessWidget { + final String title; + final String? body; + final Color color; + final IconData icon; + final VoidCallback? onConfirm; + final VoidCallback? onCancel; + final RichText? richText; + + const CustomConfirmation({ + super.key, + required this.title, + this.body, + required this.color, + required this.icon, + required this.onConfirm, + this.onCancel, + this.richText, + }); + + @override + Widget build(BuildContext context) { + final bool isTablet = 100.w >= 600; + final double maxWidth = 100.w >= 1280 + ? 1200 + : isTablet + ? 800 + : 400; + return Dialog( + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(4.5.w)), + backgroundColor: Colors.white, + insetPadding: EdgeInsets.symmetric(horizontal: 10.w), + elevation: 5, + child: Container( + padding: EdgeInsets.symmetric(horizontal: 5.w, vertical: 3.h), + width: double.infinity, + constraints: BoxConstraints(maxWidth: maxWidth), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(4.w), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + padding: EdgeInsets.all(4.w), + decoration: BoxDecoration( + color: color.withOpacity(0.1), + shape: BoxShape.circle, + ), + child: Icon(icon, color: color, size: 10.w), + ), + + SizedBox(height: 2.h), + + Text( + title, + style: TextStyle( + fontSize: AppFontSize.medium.sp, + fontWeight: FontWeight.bold, + color: Colors.black87, + ), + ), + + if (body != null) ...[ + SizedBox(height: 1.h), + Text( + body!, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + color: Colors.grey[800], + height: 1.5, + ), + ), + ], + + if (richText != null) richText!, + + SizedBox(height: 3.h), + + Row( + children: [ + Expanded( + child: ElevatedButton( + onPressed: onCancel != null + ? onCancel + : () => context.pop(), + style: ElevatedButton.styleFrom( + backgroundColor: Colors.grey.shade300, + padding: EdgeInsets.symmetric(vertical: 1.5.h), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(2.5.w), + ), + elevation: 0, + ), + child: Text( + onCancel != null ? "Tolak" : "Batal", + style: TextStyle( + color: Colors.grey[800], + fontSize: AppFontSize.medium.sp, + fontWeight: FontWeight.w600, + ), + ), + ), + ), + + SizedBox(width: 3.w), + + Expanded( + child: ElevatedButton( + onPressed: () { + onConfirm?.call(); + context.pop(); + }, + style: ElevatedButton.styleFrom( + backgroundColor: AppColor.primaryColor, + padding: EdgeInsets.symmetric(vertical: 1.5.h), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(2.5.w), + ), + elevation: 0, + ), + child: Text( + onCancel != null ? "Terima" : "Lanjutkan", + style: TextStyle( + color: Colors.white, + fontSize: AppFontSize.medium.sp, + fontWeight: FontWeight.w600, + ), + ), + ), + ), + ], + ), + ], + ), + ), + ); + } +} diff --git a/Mobile Operasional/lib/core/widgets/custom_empty_screen.dart b/Mobile Operasional/lib/core/widgets/custom_empty_screen.dart new file mode 100644 index 0000000..92164c4 --- /dev/null +++ b/Mobile Operasional/lib/core/widgets/custom_empty_screen.dart @@ -0,0 +1,85 @@ +import 'package:flutter/material.dart'; +import 'package:niogu_app/core/constants/app_color.dart'; +import 'package:niogu_app/core/constants/app_font_size.dart'; +import 'package:sizer/sizer.dart'; + +class CustomEmptyScreen extends StatelessWidget { + final String? title; + final String body; + final String? textButton; + final VoidCallback? onPressed; + + const CustomEmptyScreen({ + super.key, + this.title, + required this.body, + this.textButton, + this.onPressed, + }); + + @override + Widget build(BuildContext context) { + return SingleChildScrollView( + physics: const AlwaysScrollableScrollPhysics(), + padding: EdgeInsets.symmetric(vertical: 3.h), + child: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + if (title != null) ...[ + Text( + title!, + style: TextStyle( + fontSize: AppFontSize.large.sp, + fontWeight: FontWeight.bold, + color: Colors.black, + ), + ), + SizedBox(height: 1.h), + ], + Text( + body, + style: TextStyle( + fontSize: AppFontSize.small.sp, + color: Colors.grey, + ), + ), + SizedBox(height: 1.5.h), + + Icon( + Icons.search_off_rounded, + size: 15.h, + color: Colors.grey.shade300, + ), + + if (onPressed != null && textButton != null) ...[ + SizedBox(height: 1.5.h), + + SizedBox( + width: 70.w, + height: 6.h, + child: ElevatedButton( + onPressed: onPressed, + style: ElevatedButton.styleFrom( + backgroundColor: AppColor.primaryColor, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(2.05.w), + ), + ), + child: Text( + textButton!, + style: TextStyle( + color: Colors.white, + fontSize: AppFontSize.medium.sp, + fontWeight: FontWeight.bold, + ), + ), + ), + ), + ], + ], + ), + ), + ); + } +} diff --git a/Mobile Operasional/lib/core/widgets/custom_error_screen.dart b/Mobile Operasional/lib/core/widgets/custom_error_screen.dart new file mode 100644 index 0000000..1d7991d --- /dev/null +++ b/Mobile Operasional/lib/core/widgets/custom_error_screen.dart @@ -0,0 +1,64 @@ +import 'package:flutter/material.dart'; +import 'package:niogu_app/core/constants/app_color.dart'; +import 'package:niogu_app/core/constants/app_font_size.dart'; +import 'package:sizer/sizer.dart'; + +class CustomErrorScreen extends StatelessWidget { + final String message; + final VoidCallback onRefresh; + + const CustomErrorScreen({ + super.key, + required this.message, + required this.onRefresh, + }); + + @override + Widget build(BuildContext context) { + return Center( + child: SingleChildScrollView( + physics: const AlwaysScrollableScrollPhysics(), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + message, + style: TextStyle( + fontSize: AppFontSize.large.sp, + fontWeight: FontWeight.bold, + color: Colors.black, + ), + ), + SizedBox(height: 3.h), + + Icon(Icons.error_outline, size: 15.h, color: Colors.grey.shade300), + + SizedBox(height: 3.h), + + SizedBox( + width: 70.w, + height: 6.h, + child: ElevatedButton( + onPressed: onRefresh, + style: ElevatedButton.styleFrom( + backgroundColor: AppColor.primaryColor, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + ), + ), + child: Text( + "Refresh", + style: TextStyle( + color: Colors.white, + fontSize: AppFontSize.medium.sp, + fontWeight: FontWeight.bold, + ), + ), + ), + ), + ], + ), + ), + ); + } +} diff --git a/Mobile Operasional/lib/core/widgets/custom_form_input.dart b/Mobile Operasional/lib/core/widgets/custom_form_input.dart new file mode 100644 index 0000000..25cb607 --- /dev/null +++ b/Mobile Operasional/lib/core/widgets/custom_form_input.dart @@ -0,0 +1,95 @@ +import 'package:flutter/material.dart'; +import 'package:niogu_app/core/constants/app_font_size.dart'; +import 'package:sizer/sizer.dart'; + +class CustomFormInput extends StatelessWidget { + final String label; + final String? hint; + final IconData icon; + final TextEditingController controller; + final void Function(String)? onChanged; + final bool obscureText; + final TextInputType inputType; + final String? errorText; + final String? Function(String?)? validator; + final IconData? suffixIcon; + final VoidCallback? suffixPressed; + + const CustomFormInput({ + super.key, + required this.label, + this.hint, + required this.icon, + required this.controller, + this.onChanged, + this.obscureText = false, + this.inputType = TextInputType.text, + this.errorText, + this.validator, + this.suffixIcon, + this.suffixPressed, + }); + + @override + Widget build(BuildContext context) { + final bool isTablet = 100.w >= 600; + return TextFormField( + controller: controller, + onChanged: onChanged, + obscureText: obscureText, + keyboardType: inputType, + autovalidateMode: AutovalidateMode.onUserInteraction, + validator: validator, + style: TextStyle( + fontSize: isTablet ? AppFontSize.medium.sp : AppFontSize.small.sp, + ), + decoration: InputDecoration( + hintText: hint, + hintStyle: TextStyle( + fontSize: isTablet + ? (AppFontSize.medium - 1.25).sp + : (AppFontSize.small - 1.25).sp, + color: Colors.grey[500], + ), + labelText: label, + labelStyle: TextStyle(fontSize: (AppFontSize.medium - 1.25).sp), + prefixIcon: Padding( + padding: isTablet + ? EdgeInsets.symmetric(horizontal: 3.w) + : EdgeInsets.zero, + child: Icon(icon, color: Colors.grey, size: 5.w), + ), + errorText: errorText, + errorStyle: TextStyle( + color: Colors.red, + fontSize: isTablet ? AppFontSize.medium.sp : AppFontSize.small.sp, + ), + + errorBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(2.5.w), + borderSide: const BorderSide(color: Colors.redAccent), + ), + border: OutlineInputBorder(borderRadius: BorderRadius.circular(2.5.w)), + contentPadding: EdgeInsets.symmetric( + vertical: isTablet ? 3.6.h : 1.8.h, + ), + suffixIcon: suffixIcon == null + ? null + : Padding( + padding: EdgeInsets.symmetric( + horizontal: isTablet ? 5.w : 2.5.w, + ), + child: IconButton( + onPressed: suffixPressed, + icon: Icon( + suffixIcon, + size: 5.w, + color: Colors.grey.shade500, + ), + ), + ), + isDense: true, + ), + ); + } +} diff --git a/Mobile Operasional/lib/core/widgets/custom_not_login.dart b/Mobile Operasional/lib/core/widgets/custom_not_login.dart new file mode 100644 index 0000000..e35d68f --- /dev/null +++ b/Mobile Operasional/lib/core/widgets/custom_not_login.dart @@ -0,0 +1,119 @@ +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'package:niogu_app/core/constants/app_color.dart'; +import 'package:niogu_app/core/constants/app_font_size.dart'; +import 'package:niogu_app/core/router/app_route.dart'; +import 'package:sizer/sizer.dart'; + +class CustomNotLogin extends StatelessWidget { + const CustomNotLogin({super.key}); + + @override + Widget build(BuildContext context) { + final isTablet = 100.w >= 600; + + return Center( + child: Padding( + padding: EdgeInsets.symmetric(horizontal: 10.w), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Container( + padding: EdgeInsets.all(5.w), + decoration: BoxDecoration( + color: Colors.blue[50], + shape: BoxShape.circle, + ), + child: Icon( + Icons.lock_person_rounded, + color: AppColor.primaryColor, + size: 15.w, + ), + ), + + SizedBox(height: 4.h), + + Text( + "Akses Fitur Terbatas", + style: TextStyle( + fontSize: (AppFontSize.medium - (isTablet ? 0 : 1.25)).sp, + fontWeight: FontWeight.bold, + color: Colors.black87, + ), + ), + SizedBox(height: 1.5.h), + Text( + "Silakan masuk ke akun anda terlebih dahulu untuk mengakses fitur lengkap Niogu.", + textAlign: TextAlign.center, + style: TextStyle( + fontSize: isTablet + ? AppFontSize.small.sp + : AppFontSize.small.sp, + color: Colors.grey[600], + height: 1.5, + ), + ), + + SizedBox(height: 6.h), + + SizedBox( + width: double.infinity, + height: 7.h, + child: ElevatedButton.icon( + onPressed: () { + context.goNamed(AppRoute.ownerLoginScreen); + }, + icon: Icon(Icons.login_rounded, color: Colors.white, size: 5.w), + label: Text( + "Masuk Akun", + style: TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + fontSize: AppFontSize.medium.sp, + ), + ), + style: ElevatedButton.styleFrom( + backgroundColor: AppColor.primaryColor, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(3.w), + ), + elevation: 2, + ), + ), + ), + + SizedBox(height: 2.h), + + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + "Belum punya akun? ", + style: TextStyle( + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + ), + ), + GestureDetector( + onTap: () => context.goNamed(AppRoute.registrationScreen), + child: Text( + "Daftar Sekarang", + style: TextStyle( + color: AppColor.primaryColor, + fontWeight: FontWeight.bold, + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + ), + ), + ), + ], + ), + ], + ), + ), + ); + } +} diff --git a/Mobile Operasional/lib/core/widgets/custom_proof_image_uploader.dart b/Mobile Operasional/lib/core/widgets/custom_proof_image_uploader.dart new file mode 100644 index 0000000..34c35b6 --- /dev/null +++ b/Mobile Operasional/lib/core/widgets/custom_proof_image_uploader.dart @@ -0,0 +1,111 @@ +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:niogu_app/core/constants/app_font_size.dart'; +import 'package:sizer/sizer.dart'; + +class CustomProofImageUploader extends StatelessWidget { + final String? imagePath; + final String title; + final VoidCallback onTapImageAdd; + final VoidCallback onTapImageRemove; + const CustomProofImageUploader({ + super.key, + required this.imagePath, + required this.title, + required this.onTapImageAdd, + required this.onTapImageRemove, + }); + + @override + Widget build(BuildContext context) { + final File imageFile = File(imagePath ?? "image not found"); + + final bool imageFileExists = imageFile.existsSync(); + + return Stack( + clipBehavior: Clip.none, + children: [ + Container( + width: double.infinity, + height: 25.h, + decoration: BoxDecoration( + border: Border.all( + color: Colors.grey.shade300, + style: BorderStyle.solid, + ), + borderRadius: BorderRadius.circular(2.5.w), + color: Colors.white, + ), + child: imageFileExists + ? ClipRRect( + borderRadius: BorderRadius.circular(2.5.w), + child: Image.file( + imageFile, + fit: BoxFit.cover, + width: double.infinity, + height: double.infinity, + errorBuilder: (context, error, stackTrace) { + return Icon( + Icons.error, + color: Colors.grey[400], + size: 8.w, + ); + }, + ), + ) + : GestureDetector( + onTap: onTapImageAdd, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.camera_alt_outlined, + color: Colors.grey, + size: 8.w, + ), + SizedBox(height: 0.5.h), + Text( + title, + style: TextStyle( + color: Colors.grey[600], + fontSize: AppFontSize.medium.sp, + ), + ), + ], + ), + ), + ), + + if (imagePath != null) + Positioned( + top: 1.w, + right: 1.w, + child: Material( + color: Colors.transparent, + type: MaterialType.transparency, + child: InkWell( + onTap: onTapImageRemove, + child: Container( + padding: EdgeInsets.all(1.w), + decoration: BoxDecoration( + color: Colors.red[50], + shape: BoxShape.circle, + border: Border.all(color: Colors.red.withOpacity(0.2)), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.1), + blurRadius: 2, + offset: Offset(0, 1), + ), + ], + ), + child: Icon(Icons.remove, color: Colors.red, size: 4.w), + ), + ), + ), + ), + ], + ); + } +} diff --git a/Mobile Operasional/lib/core/widgets/custom_refresh_indicator.dart b/Mobile Operasional/lib/core/widgets/custom_refresh_indicator.dart new file mode 100644 index 0000000..6bd554e --- /dev/null +++ b/Mobile Operasional/lib/core/widgets/custom_refresh_indicator.dart @@ -0,0 +1,109 @@ +import 'package:flutter/material.dart'; +import 'package:sizer/sizer.dart'; +import 'package:niogu_app/core/constants/app_color.dart'; + +class CustomRefreshIndicator extends StatefulWidget { + final Widget child; + final Future Function() onRefresh; + + const CustomRefreshIndicator({ + super.key, + required this.child, + required this.onRefresh, + }); + + @override + State createState() => _CustomRefreshIndicatorState(); +} + +class _CustomRefreshIndicatorState extends State + with SingleTickerProviderStateMixin { + late ScrollController _scrollController; + late AnimationController _animationController; + bool _isRefreshing = false; + + @override + void initState() { + super.initState(); + _scrollController = ScrollController(); + _animationController = AnimationController( + vsync: this, + duration: const Duration(milliseconds: 1500), + ); + _scrollController.addListener(_handleScroll); + } + + @override + void dispose() { + _scrollController.dispose(); + _animationController.dispose(); + super.dispose(); + } + + void _handleScroll() { + if (_scrollController.offset <= -100 && !_isRefreshing) { + _triggerRefresh(); + } + } + + Future _triggerRefresh() async { + if (_isRefreshing) return; + setState(() { + _isRefreshing = true; + }); + + _animationController.repeat(); + + try { + await widget.onRefresh(); + } finally { + if (mounted) { + setState(() { + _isRefreshing = false; + }); + _animationController.stop(); + } + } + } + + @override + Widget build(BuildContext context) { + return Stack( + children: [ + SingleChildScrollView( + controller: _scrollController, + physics: const BouncingScrollPhysics(), + child: Column( + children: [ + if (_isRefreshing) + Padding( + padding: EdgeInsets.only(top: 2.h), + child: AnimatedBuilder( + animation: _animationController, + builder: (context, child) { + return Transform.rotate( + angle: _animationController.value * 6.28, + child: Container( + padding: EdgeInsets.all(3.w), + decoration: BoxDecoration( + shape: BoxShape.circle, + color: AppColor.primaryColor.withOpacity(0.1), + ), + child: Icon( + Icons.refresh_rounded, + color: AppColor.primaryColor, + size: 7.w, + ), + ), + ); + }, + ), + ), + widget.child, + ], + ), + ), + ], + ); + } +} diff --git a/Mobile Operasional/lib/core/widgets/custom_selectable_item.dart b/Mobile Operasional/lib/core/widgets/custom_selectable_item.dart new file mode 100644 index 0000000..f01325b --- /dev/null +++ b/Mobile Operasional/lib/core/widgets/custom_selectable_item.dart @@ -0,0 +1,122 @@ +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:niogu_app/core/constants/app_font_size.dart'; +import 'package:sizer/sizer.dart'; + +class CustomSelectableItem extends StatelessWidget { + final String id; + final String? imagePath; + final String title; + final String subtitle; + final bool isSelected; + final VoidCallback onTap; + const CustomSelectableItem({ + super.key, + required this.id, + this.imagePath, + required this.title, + required this.subtitle, + required this.isSelected, + required this.onTap, + }); + + @override + Widget build(BuildContext context) { + final bool isTablet = 100.w >= 600; + + final bool imagePathAvailable = imagePath != null && imagePath!.isNotEmpty; + + final File imageFile = File( + imagePathAvailable ? imagePath! : "image not found", + ); + + final bool imageFileExists = imageFile.existsSync(); + return Container( + margin: EdgeInsets.only(bottom: 1.5.h), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(2.5.w), + border: Border.all(color: Colors.grey.shade200, width: 1), + ), + child: InkWell( + onTap: onTap, + borderRadius: BorderRadius.circular(2.5.w), + child: Padding( + padding: EdgeInsets.all(4.w), + child: Row( + children: [ + Icon( + isSelected + ? Icons.radio_button_checked + : Icons.radio_button_off, + color: Colors.grey.shade700, + size: 5.5.w, + ), + SizedBox(width: 4.w), + Container( + width: 12.w, + height: 12.w, + decoration: BoxDecoration( + color: Colors.grey[100], + borderRadius: BorderRadius.circular(2.w), + ), + child: imageFileExists + ? ClipRRect( + borderRadius: BorderRadius.circular(2.5.w), + child: Image.file( + imageFile, + fit: BoxFit.cover, + width: double.infinity, + height: double.infinity, + errorBuilder: (context, error, stackTrace) { + return Icon( + Icons.error, + color: Colors.grey[400], + size: 8.w, + ); + }, + ), + ) + : Icon( + Icons.image_outlined, + size: 8.w, + color: Colors.grey[400], + ), + ), + SizedBox(width: 3.w), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + maxLines: 2, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + ), + ), + SizedBox(height: 0.75.h), + Text( + subtitle, + style: TextStyle( + color: Colors.grey.shade700, + fontSize: isTablet + ? (AppFontSize.medium - 1.25).sp + : (AppFontSize.small - 1.25).sp, + ), + ), + ], + ), + ), + ], + ), + ), + ), + ); + } +} diff --git a/Mobile Operasional/lib/core/widgets/custom_selector.dart b/Mobile Operasional/lib/core/widgets/custom_selector.dart new file mode 100644 index 0000000..998cc1a --- /dev/null +++ b/Mobile Operasional/lib/core/widgets/custom_selector.dart @@ -0,0 +1,58 @@ +import 'package:flutter/material.dart'; +import 'package:niogu_app/core/constants/app_font_size.dart'; +import 'package:sizer/sizer.dart'; + +class CustomSelector extends StatelessWidget { + final bool isTablet; + final String title; + final EdgeInsetsGeometry? padding; + final double? fontSize; + final VoidCallback onTap; + const CustomSelector({ + super.key, + required this.isTablet, + required this.title, + this.padding, + this.fontSize, + required this.onTap, + }); + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: onTap, + child: Container( + width: double.infinity, + padding: padding ?? EdgeInsets.all(4.w), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(3.w), + border: Border.all(color: Colors.grey.shade300), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + title, + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: + fontSize ?? + (isTablet + ? (AppFontSize.medium - 1.25).sp + : (AppFontSize.small - 1.25).sp), + color: Colors.grey.shade700, + ), + ), + Icon( + Icons.arrow_forward_ios, + size: 3.5.w, + color: Colors.grey.shade700, + ), + ], + ), + ), + ) + ; + } +} diff --git a/Mobile Operasional/lib/core/widgets/custom_snackbar.dart b/Mobile Operasional/lib/core/widgets/custom_snackbar.dart new file mode 100644 index 0000000..05094db --- /dev/null +++ b/Mobile Operasional/lib/core/widgets/custom_snackbar.dart @@ -0,0 +1,89 @@ +import 'package:flutter/material.dart'; +import 'package:niogu_app/core/constants/app_font_size.dart'; +import 'package:sizer/sizer.dart'; + +class CustomSnackbar { + static const Color _successColor = Color(0xFF4CAF50); + static const Color _errorColor = Color(0xFFE53935); + static const Color _warningColor = Color(0xFFFFC107); + + static void showSuccess(BuildContext context, String message) { + _show( + context, + Icons.check_circle_outline, + Colors.white, + message, + Colors.white, + _successColor, + ); + } + + static void showError(BuildContext context, String message) { + _show( + context, + Icons.error_outline, + Colors.white, + message, + Colors.white, + _errorColor, + ); + } + + static void showWarning(BuildContext context, String message) { + _show( + context, + Icons.warning_amber_outlined, + Colors.black87, + message, + Colors.black87, + _warningColor, + ); + } + + static void _show( + BuildContext context, + IconData icon, + Color iconColor, + String message, + Color messageColor, + Color backgroundColor, + ) { + final bool isTablet = 100.w >= 600; + final messenger = ScaffoldMessenger.of(context); + + messenger.removeCurrentSnackBar(); + + messenger.showSnackBar( + SnackBar( + content: Row( + children: [ + Icon(icon, color: iconColor, size: 6.w), + SizedBox(width: 3.w), + Expanded( + child: Text( + message, + style: TextStyle( + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + fontWeight: FontWeight.w600, + color: messageColor, + ), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + backgroundColor: backgroundColor, + behavior: SnackBarBehavior.floating, + elevation: 4, + margin: EdgeInsets.all(4.w), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(2.5.w), + ), + duration: const Duration(seconds: 3), + ), + ); + } +} diff --git a/Mobile Operasional/lib/core/widgets/custom_text_form_field.dart b/Mobile Operasional/lib/core/widgets/custom_text_form_field.dart new file mode 100644 index 0000000..a239f40 --- /dev/null +++ b/Mobile Operasional/lib/core/widgets/custom_text_form_field.dart @@ -0,0 +1,201 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:niogu_app/core/constants/app_color.dart'; +import 'package:niogu_app/core/constants/app_font_size.dart'; +import 'package:niogu_app/core/widgets/custom_snackbar.dart'; +import 'package:sizer/sizer.dart'; + +class CustomTextFormField extends StatelessWidget { + final String label; + final Color? labelColor; + final String? hint; + final String? addInformation; + final TextEditingController controller; + final TextInputType keyboardType; + final bool autoFocus; + final String? errorText; + final String? Function(String?)? validator; + final Function(String)? onChanged; + final bool isObscure; + final bool readOnly; + final List? inputFormatters; + final String? onTapReadOnlyMessage; + final Color? enabledBorderColor; + final Color? focusedBorderColor; + final IconData? prefixIcon; + final IconData? suffixIcon; + final VoidCallback? suffixPressed; + final String? suffixText; + + CustomTextFormField({ + super.key, + required this.label, + this.labelColor, + this.hint, + this.addInformation, + required this.controller, + this.keyboardType = TextInputType.text, + this.autoFocus = false, + this.errorText, + this.validator, + this.onChanged, + this.isObscure = false, + this.readOnly = false, + this.inputFormatters, + this.onTapReadOnlyMessage, + this.enabledBorderColor, + this.focusedBorderColor, + this.prefixIcon, + this.suffixIcon, + this.suffixPressed, + this.suffixText, + }); + + @override + Widget build(BuildContext context) { + final bool isTablet = 100.w >= 600; + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + label, + style: TextStyle( + fontSize: isTablet ? AppFontSize.medium.sp : AppFontSize.small.sp, + fontWeight: FontWeight.bold, + color: labelColor ?? Colors.black87, + ), + ), + if (addInformation != null) ...[ + SizedBox(height: 1.h), + Padding( + padding: EdgeInsets.only(bottom: 1.5.h), + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon( + Icons.info_outline, + size: 3.5.w, + color: AppColor.primaryColor, + ), + SizedBox(width: 0.75.w), + Text( + addInformation!, + style: TextStyle( + color: AppColor.primaryColor, + fontSize: isTablet + ? (AppFontSize.medium - 1.25).sp + : (AppFontSize.small - 1.25).sp, + ), + ), + ], + ), + ], + ), + ), + ], + SizedBox(height: 1.h), + TextFormField( + controller: controller, + keyboardType: keyboardType, + autofocus: autoFocus, + obscureText: isObscure, + onChanged: onChanged, + validator: validator, + readOnly: readOnly, + onTap: !readOnly + ? null + : () { + if (onTapReadOnlyMessage != null && + onTapReadOnlyMessage!.isNotEmpty) { + CustomSnackbar.showWarning(context, onTapReadOnlyMessage!); + } + }, + autovalidateMode: AutovalidateMode.onUserInteraction, + style: TextStyle( + fontSize: isTablet ? AppFontSize.medium.sp : AppFontSize.small.sp, + fontWeight: FontWeight.w500, + ), + inputFormatters: inputFormatters, + decoration: InputDecoration( + filled: readOnly, + fillColor: readOnly ? Colors.grey.shade100 : Colors.white, + hintText: hint, + hintStyle: TextStyle( + color: Colors.grey.shade500, + fontSize: isTablet ? AppFontSize.medium.sp : AppFontSize.small.sp, + ), + contentPadding: EdgeInsets.symmetric( + horizontal: 4.w, + vertical: 1.8.h, + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(2.5.w), + borderSide: BorderSide( + color: enabledBorderColor ?? Colors.grey.shade300, + ), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(2.5.w), + borderSide: BorderSide( + color: readOnly + ? Colors.grey.shade300 + : (focusedBorderColor ?? AppColor.primaryColor), + width: 1, + ), + ), + errorText: errorText, + errorStyle: TextStyle( + color: Colors.red, + fontSize: isTablet ? AppFontSize.medium.sp : AppFontSize.small.sp, + ), + errorBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(2.5.w), + borderSide: const BorderSide(color: Colors.redAccent), + ), + focusedErrorBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(2.5.w), + borderSide: const BorderSide(color: Colors.redAccent, width: 1.5), + ), + prefixIcon: prefixIcon == null + ? null + : Padding( + padding: EdgeInsets.symmetric( + horizontal: isTablet ? 5.w : 2.5.w, + ), + child: Icon( + prefixIcon, + size: 5.w, + color: Colors.grey.shade500, + ), + ), + suffixIcon: suffixIcon == null + ? null + : Padding( + padding: EdgeInsets.symmetric( + horizontal: isTablet ? 5.w : 2.5.w, + ), + child: IconButton( + onPressed: suffixPressed, + icon: Icon( + suffixIcon, + size: 5.w, + color: Colors.grey.shade500, + ), + ), + ), + suffixText: suffixText, + suffixStyle: TextStyle( + fontSize: isTablet + ? (AppFontSize.medium - 1.25).sp + : (AppFontSize.small - 1.25).sp, + color: Colors.grey, + ), + ), + ), + ], + ); + } +} diff --git a/Mobile Operasional/lib/core/widgets/custom_time_picker.dart b/Mobile Operasional/lib/core/widgets/custom_time_picker.dart new file mode 100644 index 0000000..0d3a033 --- /dev/null +++ b/Mobile Operasional/lib/core/widgets/custom_time_picker.dart @@ -0,0 +1,73 @@ +import 'package:flutter/material.dart'; +import 'package:niogu_app/core/constants/app_color.dart'; +import 'package:niogu_app/core/constants/app_font_size.dart'; +import 'package:niogu_app/core/utils/time_zone.dart'; +import 'package:sizer/sizer.dart'; + +class CustomTimePicker extends StatelessWidget { + final String label; + final TimeOfDay time; + final VoidCallback onTap; + final bool isEnabled; + const CustomTimePicker({ + super.key, + required this.label, + required this.time, + required this.onTap, + this.isEnabled = true, + }); + + @override + Widget build(BuildContext context) { + final bool isTablet = 100.w >= 600; + + final formattedTime = + '${time.hour.toString().padLeft(2, '0')}:${time.minute.toString().padLeft(2, '0')} ${TimeZone.getCurrentTimeZone()}'; + + return InkWell( + onTap: isEnabled ? onTap : null, + child: Container( + padding: EdgeInsets.symmetric(horizontal: 3.w, vertical: 1.8.h), + decoration: BoxDecoration( + color: Colors.grey[50], + borderRadius: BorderRadius.circular(2.5.w), + border: Border.all(color: Colors.grey.shade300), + ), + child: Row( + children: [ + Icon( + Icons.access_time, + color: isEnabled ? AppColor.primaryColor : Colors.grey.shade600, + size: 5.w, + ), + SizedBox(width: 2.w), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + label, + style: TextStyle( + fontSize: isTablet + ? (AppFontSize.medium - 1.25).sp + : (AppFontSize.small - 1.25).sp, + color: Colors.grey[600], + ), + ), + Text( + formattedTime, + style: TextStyle( + color: isEnabled ? Colors.black : Colors.grey.shade600, + fontSize: isTablet + ? (AppFontSize.medium - 1.25).sp + : (AppFontSize.small - 1.25).sp, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + ], + ), + ), + ); + } +} diff --git a/Mobile Operasional/lib/core/widgets/pop_up_notification.dart b/Mobile Operasional/lib/core/widgets/pop_up_notification.dart new file mode 100644 index 0000000..a08744c --- /dev/null +++ b/Mobile Operasional/lib/core/widgets/pop_up_notification.dart @@ -0,0 +1,306 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; +import 'package:niogu_app/core/components/modal_outlet_bottom.dart'; +import 'package:niogu_app/core/constants/app_color.dart'; +import 'package:niogu_app/core/constants/app_font_size.dart'; +import 'package:niogu_app/core/providers/app_provider.dart'; +import 'package:niogu_app/core/router/app_route.dart'; +import 'package:niogu_app/core/utils/log_message.dart'; +import 'package:niogu_app/core/enums/message_activity_type.dart'; +import 'package:niogu_app/features/online_store/presentation/providers/online_store_provider.dart'; +import 'package:sizer/sizer.dart'; + +class PopupNotification extends ConsumerWidget { + final bool isOwner; + final List messages; + final MessageActivityType type; + const PopupNotification({ + super.key, + required this.isOwner, + required this.messages, + this.type = MessageActivityType.lock_activity, + }); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final bool isTablet = 100.w >= 600; + return PopScope( + canPop: false, + child: Container( + color: Colors.black.withOpacity(0.5), + child: Center( + child: Material( + color: Colors.transparent, + child: Container( + width: 85.w, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(5.w), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.2), + blurRadius: 20, + offset: const Offset(0, 10), + ), + ], + ), + constraints: BoxConstraints(maxWidth: 85.w), + child: type == MessageActivityType.lock_activity + ? _buildLockActivityMessage(context, isTablet) + : _buildOnlineOrderMessage(context, ref, isTablet), + ), + ), + ), + ), + ); + } + + Widget _buildLockActivityMessage(BuildContext context, bool isTablet) { + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + width: double.infinity, + padding: EdgeInsets.symmetric(vertical: 3.h), + decoration: BoxDecoration( + color: AppColor.primaryColor, + borderRadius: BorderRadius.vertical(top: Radius.circular(5.w)), + ), + child: Column( + children: [ + Icon(Icons.lock_person_rounded, color: Colors.white, size: 12.w), + SizedBox(height: 1.5.h), + Text( + "Pemberhentian Aktivitas", + style: TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + fontSize: AppFontSize.medium.sp, + ), + ), + ], + ), + ), + + Padding( + padding: EdgeInsets.fromLTRB(6.w, 4.h, 6.w, 3.h), + child: Text( + messages[0], + textAlign: TextAlign.center, + style: TextStyle( + fontSize: (AppFontSize.medium - 1.25).sp, + color: Colors.grey[800], + height: 1.5, + ), + ), + ), + + Padding( + padding: EdgeInsets.symmetric(horizontal: 6.w, vertical: 3.h), + child: Column( + children: [ + ElevatedButton( + onPressed: () => SystemNavigator.pop(), + style: ElevatedButton.styleFrom( + backgroundColor: AppColor.primaryColor, + minimumSize: Size(double.infinity, 6.5.h), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(3.w), + ), + ), + child: Text( + "Tutup Aplikasi", + style: TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + fontSize: AppFontSize.medium.sp, + ), + ), + ), + + if (isOwner) ...[ + SizedBox(height: 2.h), + OutlinedButton.icon( + onPressed: () => showModalBottomSheet( + context: context, + useRootNavigator: true, + isScrollControlled: true, + constraints: const BoxConstraints( + maxWidth: double.infinity, + ), + builder: (context) => const ModalOutletBottom(), + ), + icon: Icon(Icons.swap_horiz, size: 7.w), + label: Text( + "Pindah ke Outlet Lain", + style: TextStyle(fontSize: AppFontSize.medium.sp), + ), + style: OutlinedButton.styleFrom( + foregroundColor: AppColor.primaryColor, + minimumSize: Size(double.infinity, 6.5.h), + side: BorderSide( + color: AppColor.primaryColor, + width: 0.35.w, + ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(3.w), + ), + ), + ), + ], + ], + ), + ), + ], + ); + } + + Future _updateAlreadyRead( + BuildContext context, + WidgetRef ref, + bool isProcessed, + ) async { + final router = GoRouter.of(context); + try { + await ref + .read(onlineStoreControllerProvider.notifier) + .updateAlreadyReadOrderOnline(messages[0]); + + if (!context.mounted) return; + + if (!isProcessed) { + ref.read(messageActivityStateProvider.notifier).state = null; + return; + } + + final onlineOrderDetail = await ref + .read(onlineStoreRepositoryProvider) + .fetchOnlineOrderDetailById(messages[0]); + + await Future.delayed(const Duration(milliseconds: 800), () { + router.pushNamed( + AppRoute.onlineStoreOrderDetilScreen, + extra: onlineOrderDetail, + ); + }); + } catch (e, st) { + LogMessage.log.e(e.toString(), error: e, stackTrace: st); + } + } + + Widget _buildOnlineOrderMessage( + BuildContext context, + WidgetRef ref, + bool isTablet, + ) { + final onlineStoreControllerState = ref.watch(onlineStoreControllerProvider); + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + width: double.infinity, + padding: EdgeInsets.symmetric(vertical: 3.h), + decoration: BoxDecoration( + color: AppColor.primaryColor, + borderRadius: BorderRadius.vertical(top: Radius.circular(5.w)), + ), + child: Column( + children: [ + Icon( + Icons.shopping_bag_outlined, + color: Colors.white, + size: 15.w, + ), + SizedBox(height: 2.h), + Text( + "Pesanan Baru Masuk!", + textAlign: TextAlign.center, + style: TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + fontSize: AppFontSize.medium.sp, + ), + ), + ], + ), + ), + + Padding( + padding: EdgeInsets.fromLTRB(6.w, 4.h, 6.w, 3.h), + child: Column( + children: [ + Text( + "Pesanan #${messages[1]} dari ${messages[2]} telah diterima.", + textAlign: TextAlign.center, + style: TextStyle( + fontSize: (AppFontSize.medium - 1.25).sp, + color: Colors.grey[700], + height: 1.5, + ), + ), + + SizedBox(height: 1.h), + + Text( + "Total Tagihan: ${messages[3]}", + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: (AppFontSize.medium - 1.25).sp, + color: Colors.black87, + ), + ), + ], + ), + ), + + Padding( + padding: EdgeInsets.symmetric(horizontal: 6.w, vertical: 3.h), + child: Column( + children: [ + ElevatedButton( + onPressed: onlineStoreControllerState.isLoading + ? null + : () async => await _updateAlreadyRead(context, ref, true), + style: ElevatedButton.styleFrom( + backgroundColor: AppColor.primaryColor, + minimumSize: Size(double.infinity, 6.5.h), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(3.w), + ), + ), + child: Text( + "Proses Pesanan", + style: TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + fontSize: AppFontSize.medium.sp, + ), + ), + ), + SizedBox(height: 2.h), + OutlinedButton.icon( + onPressed: onlineStoreControllerState.isLoading + ? null + : () async => await _updateAlreadyRead(context, ref, false), + label: Text( + "Nanti Saja", + style: TextStyle(fontSize: AppFontSize.medium.sp), + ), + style: OutlinedButton.styleFrom( + foregroundColor: AppColor.primaryColor, + minimumSize: Size(double.infinity, 6.5.h), + side: BorderSide(color: AppColor.primaryColor, width: 0.35.w), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(3.w), + ), + ), + ), + ], + ), + ), + ], + ); + } +} diff --git a/Mobile Operasional/lib/core/widgets/triangle_painter.dart b/Mobile Operasional/lib/core/widgets/triangle_painter.dart new file mode 100644 index 0000000..c2cc2eb --- /dev/null +++ b/Mobile Operasional/lib/core/widgets/triangle_painter.dart @@ -0,0 +1,22 @@ +import 'dart:ui'; + +import 'package:flutter/material.dart'; + +class TrianglePainter extends CustomPainter { + final Color color; + TrianglePainter(this.color); + + @override + void paint(Canvas canvas, Size size) { + final paint = Paint()..color = color; + final path = Path() + ..moveTo(0, 0) + ..lineTo(size.width, 0) + ..lineTo(size.width / 2, size.height) + ..close(); + canvas.drawPath(path, paint); + } + + @override + bool shouldRepaint(CustomPainter oldDelegate) => false; +} \ No newline at end of file diff --git a/Mobile Operasional/lib/features/auth/data/datasource/auth_remote_datasource_impl.dart b/Mobile Operasional/lib/features/auth/data/datasource/auth_remote_datasource_impl.dart new file mode 100644 index 0000000..9bd68d0 --- /dev/null +++ b/Mobile Operasional/lib/features/auth/data/datasource/auth_remote_datasource_impl.dart @@ -0,0 +1,49 @@ +import 'package:dio/dio.dart'; +import 'package:niogu_app/core/constants/app_url.dart'; +import 'package:niogu_app/core/models/api_response.dart'; +import 'package:niogu_app/features/auth/data/models/login_response_model.dart'; +import 'package:niogu_app/features/auth/data/models/register_request_model.dart'; + +abstract class IAuthRemoteDatasource { + Future> login({ + required String identifier, + required String password, + String? businessCode, + }); + + Future> register(RegisterRequestModel request); +} + +class AuthRemoteDatasourceImpl implements IAuthRemoteDatasource { + final Dio _dio; + + AuthRemoteDatasourceImpl(this._dio); + + @override + Future> login({ + required String identifier, + required String password, + String? businessCode, + }) async { + final response = await _dio.post( + AppUrl.LOGIN, + data: { + 'identifier': identifier, + 'password': password, + 'business_code': businessCode, + }, + ); + + return ApiResponseModel.fromJson( + response.data, + (json) => LoginResponseModel.fromJson(json as Map), + ); + } + + @override + Future> register(RegisterRequestModel request) async { + final response = await _dio.post(AppUrl.REGISTER, data: request.toJson()); + + return ApiResponseModel.fromJson(response.data, (json) => null); + } +} diff --git a/Mobile Operasional/lib/features/auth/data/models/login_response_model.dart b/Mobile Operasional/lib/features/auth/data/models/login_response_model.dart new file mode 100644 index 0000000..9b78420 --- /dev/null +++ b/Mobile Operasional/lib/features/auth/data/models/login_response_model.dart @@ -0,0 +1,18 @@ +import 'package:json_annotation/json_annotation.dart'; +import 'package:niogu_app/core/models/user_model.dart'; + +part 'login_response_model.g.dart'; + +@JsonSerializable() +class LoginResponseModel { + @JsonKey(name: 'access_token') + final String accessToken; + final UserModel user; + + LoginResponseModel({required this.accessToken, required this.user}); + + factory LoginResponseModel.fromJson(Map json) => + _$LoginResponseModelFromJson(json); + + Map toJson() => _$LoginResponseModelToJson(this); +} diff --git a/Mobile Operasional/lib/features/auth/data/models/login_response_model.g.dart b/Mobile Operasional/lib/features/auth/data/models/login_response_model.g.dart new file mode 100644 index 0000000..b7f784a --- /dev/null +++ b/Mobile Operasional/lib/features/auth/data/models/login_response_model.g.dart @@ -0,0 +1,19 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'login_response_model.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +LoginResponseModel _$LoginResponseModelFromJson(Map json) => + LoginResponseModel( + accessToken: json['access_token'] as String, + user: UserModel.fromJson(json['user'] as Map), + ); + +Map _$LoginResponseModelToJson(LoginResponseModel instance) => + { + 'access_token': instance.accessToken, + 'user': instance.user, + }; diff --git a/Mobile Operasional/lib/features/auth/data/models/register_request_model.dart b/Mobile Operasional/lib/features/auth/data/models/register_request_model.dart new file mode 100644 index 0000000..2b98591 --- /dev/null +++ b/Mobile Operasional/lib/features/auth/data/models/register_request_model.dart @@ -0,0 +1,33 @@ +import 'package:json_annotation/json_annotation.dart'; + +part 'register_request_model.g.dart'; + +@JsonSerializable() +class RegisterRequestModel { + @JsonKey(name: 'business_name') + final String businessName; + @JsonKey(name: 'business_code') + final String businessCode; + final String name; + @JsonKey(name: 'phone_number') + final String phoneNumber; + final String? email; + final String password; + @JsonKey(name: 'password_confirmation') + final String passwordConfirmation; + + RegisterRequestModel({ + required this.businessName, + required this.businessCode, + required this.name, + required this.phoneNumber, + this.email, + required this.password, + required this.passwordConfirmation, + }); + + factory RegisterRequestModel.fromJson(Map json) => + _$RegisterRequestModelFromJson(json); + + Map toJson() => _$RegisterRequestModelToJson(this); +} diff --git a/Mobile Operasional/lib/features/auth/data/models/register_request_model.g.dart b/Mobile Operasional/lib/features/auth/data/models/register_request_model.g.dart new file mode 100644 index 0000000..21fa0c8 --- /dev/null +++ b/Mobile Operasional/lib/features/auth/data/models/register_request_model.g.dart @@ -0,0 +1,31 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'register_request_model.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +RegisterRequestModel _$RegisterRequestModelFromJson( + Map json, +) => RegisterRequestModel( + businessName: json['business_name'] as String, + businessCode: json['business_code'] as String, + name: json['name'] as String, + phoneNumber: json['phone_number'] as String, + email: json['email'] as String?, + password: json['password'] as String, + passwordConfirmation: json['password_confirmation'] as String, +); + +Map _$RegisterRequestModelToJson( + RegisterRequestModel instance, +) => { + 'business_name': instance.businessName, + 'business_code': instance.businessCode, + 'name': instance.name, + 'phone_number': instance.phoneNumber, + 'email': instance.email, + 'password': instance.password, + 'password_confirmation': instance.passwordConfirmation, +}; diff --git a/Mobile Operasional/lib/features/auth/data/repositories/auth_repository_impl.dart b/Mobile Operasional/lib/features/auth/data/repositories/auth_repository_impl.dart new file mode 100644 index 0000000..a2463ec --- /dev/null +++ b/Mobile Operasional/lib/features/auth/data/repositories/auth_repository_impl.dart @@ -0,0 +1,95 @@ +import 'package:dio/dio.dart'; +import 'package:niogu_app/core/constants/app_generator.dart'; +import 'package:niogu_app/core/errors/exceptions.dart'; +import 'package:niogu_app/core/models/api_response.dart'; +import 'package:niogu_app/features/auth/data/datasource/auth_remote_datasource_impl.dart'; +import 'package:niogu_app/features/auth/data/models/login_response_model.dart'; +import 'package:niogu_app/features/auth/data/models/register_request_model.dart'; +import 'package:niogu_app/features/auth/domain/entities/user.dart'; +import 'package:niogu_app/features/auth/domain/repositories/i_auth_repository.dart'; + +class AuthRepositoryImpl implements IAuthRepository { + final IAuthRemoteDatasource _datasource; + + AuthRepositoryImpl(this._datasource); + + @override + Future> login(LoginUser user) async { + try { + return await _datasource.login( + identifier: user.identifier, + password: user.password, + businessCode: user.businessCode, + ); + } on DioException catch (e) { + String errorMessage = "Terjadi kesalahan koneksi"; + + final statusCode = e.response?.statusCode; + + final data = e.response?.data; + + if (statusCode != null && data != null) { + final error = data['error']; + + if (statusCode == 401) { + errorMessage = "Akun tidak ditemukan"; + } + + if (statusCode == 429 && error != null && error is Map) { + final seconds = error['seconds_remaining']; + if (seconds != null) { + final minutes = (seconds / 60).ceil(); + errorMessage = + "Terlalu banyak percobaan. Coba lagi dalam $minutes menit."; + } + } + } + + throw ServerException(errorMessage); + } + } + + @override + Future register(RegisterUser register) async { + try { + await _datasource.register( + RegisterRequestModel( + businessName: register.businessName, + businessCode: AppGenerator.generateBusinessCode(), + name: register.ownerName, + phoneNumber: register.phoneNumber, + email: register.email, + password: register.password, + passwordConfirmation: register.passwordConfirmation, + ), + ); + } on DioException catch (e) { + String errorMessage = "Terjadi kesalahan koneksi"; + + if (e.response != null) { + final data = e.response!.data; + + final statusCode = e.response!.statusCode; + + if (statusCode == 422) { + if (data['errors'] != null && data['errors'] is Map) { + final Map validationErrors = data['errors']; + + final firstKey = validationErrors.keys.first; + + final List messages = validationErrors[firstKey]; + + final message = messages.first.toString(); + + if (message == 'phone number already exists') { + errorMessage = "Nomor whatsapp telah terdaftar"; + } else if (message == 'email already exists') { + errorMessage = "Email telah terdaftar"; + } + } + } + } + throw ServerException(errorMessage); + } + } +} diff --git a/Mobile Operasional/lib/features/auth/domain/entities/user.dart b/Mobile Operasional/lib/features/auth/domain/entities/user.dart new file mode 100644 index 0000000..4d5cbdf --- /dev/null +++ b/Mobile Operasional/lib/features/auth/domain/entities/user.dart @@ -0,0 +1,29 @@ +class LoginUser { + final String identifier; + final String password; + final String? businessCode; + + const LoginUser({ + required this.identifier, + required this.password, + this.businessCode + }); +} + +class RegisterUser { + final String businessName; + final String ownerName; + final String phoneNumber; + final String? email; + final String password; + final String passwordConfirmation; + + const RegisterUser({ + required this.businessName, + required this.ownerName, + required this.phoneNumber, + this.email, + required this.password, + required this.passwordConfirmation + }); +} \ No newline at end of file diff --git a/Mobile Operasional/lib/features/auth/domain/repositories/i_auth_repository.dart b/Mobile Operasional/lib/features/auth/domain/repositories/i_auth_repository.dart new file mode 100644 index 0000000..7f5c573 --- /dev/null +++ b/Mobile Operasional/lib/features/auth/domain/repositories/i_auth_repository.dart @@ -0,0 +1,9 @@ +import 'package:niogu_app/core/models/api_response.dart'; +import 'package:niogu_app/features/auth/data/models/login_response_model.dart'; +import 'package:niogu_app/features/auth/domain/entities/user.dart'; + +abstract class IAuthRepository { + Future> login(LoginUser user); + + Future register(RegisterUser register); +} diff --git a/Mobile Operasional/lib/features/auth/presentation/controllers/auth_controller.dart b/Mobile Operasional/lib/features/auth/presentation/controllers/auth_controller.dart new file mode 100644 index 0000000..005d29a --- /dev/null +++ b/Mobile Operasional/lib/features/auth/presentation/controllers/auth_controller.dart @@ -0,0 +1,188 @@ +import 'dart:async'; +import 'dart:io'; +import 'package:dio/dio.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:niogu_app/core/database/app_database.dart'; +import 'package:niogu_app/core/models/outlet_model.dart'; +import 'package:niogu_app/core/providers/app_provider.dart'; +import 'package:niogu_app/core/database/database_holder.dart'; +import 'package:niogu_app/core/utils/image_service.dart'; +import 'package:niogu_app/core/system/system_secure.dart'; +import 'package:niogu_app/core/system/system_setting.dart'; +import 'package:niogu_app/core/enums/user_role.dart'; +import 'package:niogu_app/features/auth/domain/entities/user.dart'; +import 'package:niogu_app/features/auth/domain/repositories/i_auth_repository.dart'; +import 'package:niogu_app/features/auth/presentation/providers/auth_provider.dart'; +import 'package:path/path.dart' as p; +import 'package:path_provider/path_provider.dart'; + +class AuthController extends AutoDisposeAsyncNotifier { + late final IAuthRepository _repository; + + late final Dio _dio; + + @override + FutureOr build() { + _repository = ref.read(authRepositoryProvider); + + _dio = ref.read(dioProvider); + } + + Future _handleBusinessLogo(String? serverLogoUrl) async { + try { + if (serverLogoUrl != null && serverLogoUrl.isNotEmpty) { + final tempDir = await getTemporaryDirectory(); + final tempPath = p.join(tempDir.path, 'temp_logo.jpg'); + + await _dio.download(serverLogoUrl, tempPath); + + return await ImageService.saveImageToLocalDirectory( + File(tempPath), + 'logo_images', + ); + } + + return null; + } catch (e) { + rethrow; + } + } + + Future> _handleOutletBanners( + List outletModels, + ) async { + try { + final Map localBannerPaths = {}; + + for (final outlet in outletModels) { + final id = outlet.serverId; + + final banner = outlet.serverBannerUrl; + + if (banner != null && banner.isNotEmpty) { + final tempDir = await getTemporaryDirectory(); + final tempPath = p.join(tempDir.path, 'temp_logo.jpg'); + + await _dio.download(banner, tempPath); + + final localBannerPath = await ImageService.saveImageToLocalDirectory( + File(tempPath), + 'outlet_banner_images', + ); + + localBannerPaths.putIfAbsent(id, () => localBannerPath); + } + } + + return localBannerPaths; + } catch (e) { + rethrow; + } + } + + Future login(LoginUser user) async { + state = const AsyncValue.loading(); + + final result = await AsyncValue.guard(() async { + final response = await _repository.login(user); + + final data = response.data!; + + final tenantModel = data.user.tenant; + + final outletModels = data.user.outlets; + + final userModel = data.user; + + final staffModels = userModel.staffs; + + final localLogoPath = await _handleBusinessLogo( + tenantModel.serverLogoUrl, + ); + + final localBannerPaths = await _handleOutletBanners(outletModels); + + final accessToken = data.accessToken; + + await clearDatabase(); + + await SystemSetting.setTenant( + tenantId: tenantModel.serverId, + tenantCode: tenantModel.businessCode, + tenantName: tenantModel.businessName, + ); + + await Future.delayed(Duration.zero); + + final appDatabase = AppDatabase(); + + await setDatabase(appDatabase); + + await appDatabase.customSelect("SELECT 1").getSingle(); + + await appDatabase.userDao.login( + tenantModel: tenantModel, + outletModels: outletModels, + userModel: userModel, + staffModels: staffModels, + localLogoPath: localLogoPath, + localBannerPaths: localBannerPaths, + password: user.password, + ); + + ref.invalidate(appDatabaseProvider); + + final syncService = ref.read(syncServiceProvider); + + syncService.stopWatching(); + + await syncService.startWatching(); + + await SystemSecure.saveAccessToken(accessToken); + + await SystemSetting.setIsLoggedIn(); + + String? outletIdByOwner; + + final currentUserRole = UserRole.values.byName(userModel.role); + + if (currentUserRole == UserRole.owner) { + outletIdByOwner = userModel.outletId; + } + + await SystemSetting.setOutletIdByOwner(outletIdByOwner); + + final outletName = outletModels + .firstWhere((outlet) => outlet.serverId == userModel.outletId) + .name; + + await SystemSetting.switchOutlet( + outletId: userModel.outletId, + outletName: outletName, + ); + + await SystemSetting.setUser( + userId: userModel.serverId, + userName: userModel.name, + userEmail: userModel.email, + userRole: currentUserRole, + ); + }); + + state = result; + + if (result is AsyncError) throw result.error!; + } + + Future register(RegisterUser register) async { + state = const AsyncValue.loading(); + + final result = await AsyncValue.guard(() async { + await _repository.register(register); + }); + + state = result; + + if (result is AsyncError) throw result.error!; + } +} diff --git a/Mobile Operasional/lib/features/auth/presentation/providers/auth_provider.dart b/Mobile Operasional/lib/features/auth/presentation/providers/auth_provider.dart new file mode 100644 index 0000000..cde88ba --- /dev/null +++ b/Mobile Operasional/lib/features/auth/presentation/providers/auth_provider.dart @@ -0,0 +1,20 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:niogu_app/core/providers/app_provider.dart'; +import 'package:niogu_app/features/auth/data/datasource/auth_remote_datasource_impl.dart'; +import 'package:niogu_app/features/auth/data/repositories/auth_repository_impl.dart'; +import 'package:niogu_app/features/auth/domain/repositories/i_auth_repository.dart'; +import 'package:niogu_app/features/auth/presentation/controllers/auth_controller.dart'; + +final authRemoteDataSourceProvider = + Provider.autoDispose((ref) { + final dio = ref.watch(dioProvider); + return AuthRemoteDatasourceImpl(dio); + }); + +final authRepositoryProvider = Provider.autoDispose((ref) { + final datasource = ref.watch(authRemoteDataSourceProvider); + return AuthRepositoryImpl(datasource); +}); + +final authControllerProvider = + AsyncNotifierProvider.autoDispose(AuthController.new); diff --git a/Mobile Operasional/lib/features/auth/presentation/screens/admin_login_screen.dart b/Mobile Operasional/lib/features/auth/presentation/screens/admin_login_screen.dart new file mode 100644 index 0000000..38aa84c --- /dev/null +++ b/Mobile Operasional/lib/features/auth/presentation/screens/admin_login_screen.dart @@ -0,0 +1,444 @@ +import 'dart:math'; + +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; +import 'package:niogu_app/core/constants/app_asset.dart'; +import 'package:niogu_app/core/constants/app_color.dart'; +import 'package:niogu_app/core/constants/app_font_size.dart'; +import 'package:niogu_app/core/errors/exceptions.dart'; +import 'package:niogu_app/core/providers/app_provider.dart'; +import 'package:niogu_app/core/router/app_route.dart'; +import 'package:niogu_app/core/utils/captcha_noise_pointer.dart'; +import 'package:niogu_app/core/utils/log_message.dart'; +import 'package:niogu_app/core/system/system_setting.dart'; +import 'package:niogu_app/core/widgets/custom_snackbar.dart'; +import 'package:niogu_app/core/widgets/custom_text_form_field.dart'; +import 'package:niogu_app/features/auth/domain/entities/user.dart'; +import 'package:niogu_app/features/auth/presentation/providers/auth_provider.dart'; +import 'package:sizer/sizer.dart'; + +class AdminLoginScreen extends ConsumerStatefulWidget { + const AdminLoginScreen({super.key}); + + @override + ConsumerState createState() => _AdminLoginScreenState(); +} + +class _AdminLoginScreenState extends ConsumerState { + final _formKey = GlobalKey(); + + final _businessCodeController = TextEditingController(); + + final _identifierController = TextEditingController(); + + final _passwordController = TextEditingController(); + + final _captchaController = TextEditingController(); + + late String _currentCaptcha; + + bool _obscurePassword = true; + + @override + void initState() { + super.initState(); + _currentCaptcha = _generateRandomCaptcha(); + } + + @override + void dispose() { + // TODO: implement dispose + _businessCodeController.dispose(); + _identifierController.dispose(); + _passwordController.dispose(); + _captchaController.dispose(); + super.dispose(); + } + + String? _validation({String? value, required String message}) { + if (value == null || value.isEmpty) { + return message; + } + + return null; + } + + String _generateRandomCaptcha() { + const chars = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789'; + return List.generate( + 5, + (index) => chars[Random().nextInt(chars.length)], + ).join(); + } + + void _refreshCaptcha() { + setState(() { + _currentCaptcha = _generateRandomCaptcha(); + }); + } + + Future _setCurrentOutlet() async { + final currentOutletId = await SystemSetting.getCurrentOutletId(); + + final currentOutletName = await SystemSetting.getCurrentOutletName(); + + ref.read(currentOutletIdProvider.notifier).state = currentOutletId; + + ref.read(currentOutletNameProvider.notifier).state = currentOutletName; + } + + Future _setCurrentUser() async { + final currentUserId = await SystemSetting.getCurrentUserId(); + + final currentUserName = await SystemSetting.getCurrentUserName(); + + final currentUserEmail = await SystemSetting.getCurrentUserEmail(); + + final currentUserRole = await SystemSetting.getCurrentUserRole(); + + final outletIdByOwner = await SystemSetting.getOutletIdByOwner(); + + ref.read(currentUserIdProvider.notifier).state = currentUserId; + + ref.read(currentUserNameProvider.notifier).state = currentUserName; + + ref.read(currentUserEmailProvider.notifier).state = currentUserEmail; + + ref.read(currentUserRoleProvider.notifier).state = currentUserRole; + + ref.read(outletIdByOwnerProvider.notifier).state = outletIdByOwner; + + ref.read(currentStatusLoginProvider.notifier).state = true; + } + + Future _handleLogin() async { + if (!_formKey.currentState!.validate()) return; + + if (_captchaController.text.toUpperCase() != _currentCaptcha) { + CustomSnackbar.showError(context, "Kode captcha salah!"); + _refreshCaptcha(); + return; + } + + try { + final businessCode = _businessCodeController.text.trim(); + + final identifier = _identifierController.text.trim(); + + final password = _passwordController.text.trim(); + + await ref + .read(authControllerProvider.notifier) + .login( + LoginUser( + identifier: identifier, + password: password, + businessCode: businessCode, + ), + ); + + if (!mounted) return; + + await _setCurrentOutlet(); + + await _setCurrentUser(); + + CustomSnackbar.showSuccess(context, "Berhasil login"); + context.goNamed(AppRoute.homeScreen); + } on ServerException catch (e) { + if (!mounted) ; + + _refreshCaptcha(); + + CustomSnackbar.showError(context, e.message); + } catch (e, st) { + LogMessage.log.e(e.toString(), error: e, stackTrace: st); + CustomSnackbar.showError(context, "Ups, terjadi kesalahan"); + } + } + + @override + Widget build(BuildContext context) { + return LayoutBuilder( + builder: (context, constraints) { + final bool isTablet = 100.w >= 600; + + final authControllerState = ref.watch(authControllerProvider); + + final isLoading = authControllerState.isLoading; + + return SafeArea( + top: false, + bottom: true, + right: false, + left: false, + child: Scaffold( + backgroundColor: Colors.white, + body: SafeArea( + child: SingleChildScrollView( + padding: EdgeInsets.symmetric(horizontal: 8.w, vertical: 5.h), + child: Form( + key: _formKey, + child: Column( + children: [ + Image.asset(AppAsset.LOGO_UPDATE, height: 12.h), + + SizedBox(height: 2.5.h), + + Text( + "Masuk Akun Staf", + style: TextStyle( + fontSize: AppFontSize.large.sp, + fontWeight: FontWeight.bold, + color: AppColor.primaryColor, + ), + ), + SizedBox(height: 1.h), + Text( + "Kemajuan outlet perusahaan ada di tangan anda, berikan yang terbaik!", + textAlign: TextAlign.center, + style: TextStyle( + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + color: Colors.grey, + ), + ), + + SizedBox(height: 4.h), + + CustomTextFormField( + label: "ID Usaha/Perusahaan", + hint: "xxxxxxxx", + controller: _businessCodeController, + prefixIcon: Icons.business_outlined, + validator: (value) => _validation( + value: value, + message: "ID Perusahaan belum diisi", + ), + readOnly: isLoading, + ), + + SizedBox(height: 2.5.h), + + CustomTextFormField( + label: "Email atau No. WhatsApp", + hint: "contoh@email.com / 0812...", + controller: _identifierController, + prefixIcon: Icons.person_outline, + validator: (value) => _validation( + value: value, + message: "Kolom ini belum diisi", + ), + readOnly: isLoading, + ), + + SizedBox(height: 2.5.h), + + CustomTextFormField( + label: "Password", + hint: "••••••••", + controller: _passwordController, + prefixIcon: Icons.lock_outline, + isObscure: _obscurePassword, + suffixIcon: _obscurePassword + ? Icons.visibility_off + : Icons.visibility, + suffixPressed: () => setState( + () => _obscurePassword = !_obscurePassword, + ), + validator: (value) => _validation( + value: value, + message: "Password belum diisi", + ), + readOnly: isLoading, + ), + + SizedBox(height: 2.5.h), + Row( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Expanded( + flex: 2, + child: CustomTextFormField( + label: "Kode Captcha", + hint: "Input kode", + controller: _captchaController, + validator: (value) => _validation( + value: value, + message: "captcha belum diisi", + ), + readOnly: isLoading, + ), + ), + SizedBox(width: 4.w), + InkWell( + onTap: _refreshCaptcha, + borderRadius: BorderRadius.circular(2.w), + child: Container( + height: 7.h, + width: 30.w, + decoration: BoxDecoration( + color: Colors.grey[200], + borderRadius: BorderRadius.circular(2.w), + border: Border.all(color: Colors.grey[300]!), + ), + child: Stack( + alignment: Alignment.center, + children: [ + CustomPaint( + size: Size(32.w, 6.5.h), + painter: CaptchaNoisePainter(), + ), + Text( + _currentCaptcha, + style: TextStyle( + fontWeight: FontWeight.bold, + fontStyle: FontStyle.italic, + letterSpacing: 4, + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + color: Colors.black87, + ), + ), + ], + ), + ), + ), + ], + ), + + Align( + alignment: Alignment.centerRight, + child: TextButton( + onPressed: () => + context.pushNamed(AppRoute.forgotPasswordScreen), + child: Text( + "Lupa Password?", + style: TextStyle( + color: AppColor.primaryColor, + fontWeight: FontWeight.bold, + fontSize: isTablet + ? (AppFontSize.medium - 1.25).sp + : (AppFontSize.small - 1.25).sp, + ), + ), + ), + ), + + SizedBox(height: 2.5.h), + + ElevatedButton( + onPressed: authControllerState.isLoading + ? null + : _handleLogin, + style: ElevatedButton.styleFrom( + backgroundColor: AppColor.primaryColor, + minimumSize: Size(double.infinity, 7.h), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(3.w), + ), + elevation: 2, + ), + child: Text( + "Masuk Sekarang", + style: TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + fontSize: AppFontSize.medium.sp, + ), + ), + ), + + SizedBox(height: 4.h), + + _buildOwnerLink(isTablet), + + SizedBox(height: 2.5.h), + + Text( + "Powered By P.T. Niaga Nusantara", + style: TextStyle( + color: Colors.grey.shade600, + fontWeight: FontWeight.w500, + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + ), + ), + SizedBox(height: 0.75.h), + Text( + "Versi 1.0.0", + style: TextStyle( + color: Colors.grey, + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + ), + ), + ], + ), + ), + ), + ), + ), + ); + }, + ); + } + + Widget _buildOwnerLink(bool isTablet) { + return Material( + color: Colors.blue[50], + borderRadius: BorderRadius.circular(3.w), + child: InkWell( + onTap: () => context.pop(), + borderRadius: BorderRadius.circular(3.w), + child: Container( + padding: EdgeInsets.symmetric(horizontal: 4.w, vertical: 2.h), + decoration: BoxDecoration( + border: Border.all(color: Colors.blue[100]!), + borderRadius: BorderRadius.circular(3.w), + ), + child: Row( + children: [ + Icon(Icons.info_outline, color: Colors.blue[700], size: 5.w), + SizedBox(width: 3.w), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Apakah anda pemilik usaha?", + style: TextStyle( + color: Colors.blue[900], + fontSize: isTablet + ? (AppFontSize.medium - 1.25).sp + : (AppFontSize.small - 1.25).sp, + fontWeight: FontWeight.w500, + ), + ), + SizedBox(height: 0.5.h), + Text( + "Masuk sebagai pemilik di sini", + style: TextStyle( + color: AppColor.primaryColor, + fontSize: isTablet + ? (AppFontSize.medium - 0.5).sp + : (AppFontSize.small - 0.5).sp, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + ), + Icon(Icons.chevron_right, color: Colors.blue[300], size: 5.w), + ], + ), + ), + ), + ); + } +} diff --git a/Mobile Operasional/lib/features/auth/presentation/screens/forgot_password_screen.dart b/Mobile Operasional/lib/features/auth/presentation/screens/forgot_password_screen.dart new file mode 100644 index 0000000..7e6c998 --- /dev/null +++ b/Mobile Operasional/lib/features/auth/presentation/screens/forgot_password_screen.dart @@ -0,0 +1,133 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; +import 'package:niogu_app/core/constants/app_asset.dart'; +import 'package:niogu_app/core/constants/app_color.dart'; +import 'package:niogu_app/core/constants/app_font_size.dart'; +import 'package:niogu_app/core/router/app_route.dart'; +import 'package:niogu_app/core/widgets/custom_text_form_field.dart'; +import 'package:sizer/sizer.dart'; + +class ForgotPasswordScreen extends ConsumerStatefulWidget { + const ForgotPasswordScreen({super.key}); + + @override + ConsumerState createState() => + _ForgotPasswordScreenState(); +} + +class _ForgotPasswordScreenState extends ConsumerState { + final _formKey = GlobalKey(); + + final _identifierController = TextEditingController(); + + @override + Widget build(BuildContext context) { + return LayoutBuilder( + builder: (context, constraints) { + final bool isTablet = 100.w >= 600; + + return SafeArea( + top: false, + bottom: true, + right: false, + left: false, + child: Scaffold( + backgroundColor: Colors.white, + body: SafeArea( + child: SingleChildScrollView( + padding: EdgeInsets.symmetric(horizontal: 8.w, vertical: 5.h), + child: Form( + key: _formKey, + child: Column( + children: [ + Image.asset(AppAsset.LOGO_UPDATE, height: 12.h), + + SizedBox(height: 2.5.h), + + Text( + "Lupa Password", + style: TextStyle( + fontSize: AppFontSize.large.sp, + fontWeight: FontWeight.bold, + color: AppColor.primaryColor, + ), + ), + SizedBox(height: 1.h), + Text( + "Masukkan nomor whatsapp kamu yang terdaftar", + textAlign: TextAlign.center, + style: TextStyle( + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + color: Colors.grey, + ), + ), + + SizedBox(height: 4.h), + + CustomTextFormField( + label: "ID Usaha/Perusahaan", + hint: "xxxxxxxx", + controller: _identifierController, + prefixIcon: Icons.business_outlined, + ), + + SizedBox(height: 2.5.h), + + CustomTextFormField( + label: "No. WhatsApp", + hint: "Contoh:081234xxxxxx", + controller: _identifierController, + prefixIcon: Icons.phone_outlined, + ), + + SizedBox(height: 2.5.h), + + ElevatedButton( + onPressed: () => context.pushNamed(AppRoute.verificationOtpScreen), + style: ElevatedButton.styleFrom( + backgroundColor: AppColor.primaryColor, + minimumSize: Size(double.infinity, 7.h), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(3.w), + ), + elevation: 2, + ), + child: Text( + "Selanjutnya", + style: TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + fontSize: AppFontSize.medium.sp, + ), + ), + ), + + SizedBox(height: 4.h), + + GestureDetector( + onTap: () => context.pop(), + child: Text( + "Kembali Masuk", + style: TextStyle( + color: AppColor.primaryColor, + fontWeight: FontWeight.bold, + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + ), + ), + ), + ], + ), + ), + ), + ), + ), + ); + }, + ); + } +} diff --git a/Mobile Operasional/lib/features/auth/presentation/screens/owner_login_screen.dart b/Mobile Operasional/lib/features/auth/presentation/screens/owner_login_screen.dart new file mode 100644 index 0000000..e6df92f --- /dev/null +++ b/Mobile Operasional/lib/features/auth/presentation/screens/owner_login_screen.dart @@ -0,0 +1,446 @@ +import 'dart:math'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; +import 'package:niogu_app/core/constants/app_asset.dart'; +import 'package:niogu_app/core/constants/app_color.dart'; +import 'package:niogu_app/core/constants/app_font_size.dart'; +import 'package:niogu_app/core/errors/exceptions.dart'; +import 'package:niogu_app/core/providers/app_provider.dart'; +import 'package:niogu_app/core/router/app_route.dart'; +import 'package:niogu_app/core/utils/captcha_noise_pointer.dart'; +import 'package:niogu_app/core/utils/log_message.dart'; +import 'package:niogu_app/core/system/system_setting.dart'; +import 'package:niogu_app/core/widgets/custom_snackbar.dart'; +import 'package:niogu_app/core/widgets/custom_text_form_field.dart'; +import 'package:niogu_app/features/auth/domain/entities/user.dart'; +import 'package:niogu_app/features/auth/presentation/providers/auth_provider.dart'; +import 'package:sizer/sizer.dart'; + +class OwnerLoginScreen extends ConsumerStatefulWidget { + const OwnerLoginScreen({super.key}); + + @override + ConsumerState createState() => _OwnerLoginScreenState(); +} + +class _OwnerLoginScreenState extends ConsumerState { + final _formKey = GlobalKey(); + + final _identifierController = TextEditingController(); + + final _passwordController = TextEditingController(); + + final _captchaController = TextEditingController(); + + late String _currentCaptcha; + + bool _obscurePassword = true; + + @override + void initState() { + super.initState(); + _currentCaptcha = _generateRandomCaptcha(); + } + + @override + void dispose() { + // TODO: implement dispose + _identifierController.dispose(); + _passwordController.dispose(); + _captchaController.dispose(); + super.dispose(); + } + + String? _validation({String? value, required String message}) { + if (value == null || value.isEmpty) { + return message; + } + + return null; + } + + String _generateRandomCaptcha() { + const chars = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789'; + return List.generate( + 5, + (index) => chars[Random().nextInt(chars.length)], + ).join(); + } + + void _refreshCaptcha() { + setState(() { + _currentCaptcha = _generateRandomCaptcha(); + }); + } + + Future _setCurrentOutlet() async { + final currentOutletId = await SystemSetting.getCurrentOutletId(); + + final currentOutletName = await SystemSetting.getCurrentOutletName(); + + ref.read(currentOutletIdProvider.notifier).state = currentOutletId; + + ref.read(currentOutletNameProvider.notifier).state = currentOutletName; + } + + Future _setCurrentUser() async { + final currentUserId = await SystemSetting.getCurrentUserId(); + + final currentUserName = await SystemSetting.getCurrentUserName(); + + final currentUserEmail = await SystemSetting.getCurrentUserEmail(); + + final currentUserRole = await SystemSetting.getCurrentUserRole(); + + final outletIdByOwner = await SystemSetting.getOutletIdByOwner(); + + ref.read(currentUserIdProvider.notifier).state = currentUserId; + + ref.read(currentUserNameProvider.notifier).state = currentUserName; + + ref.read(currentUserEmailProvider.notifier).state = currentUserEmail; + + ref.read(currentUserRoleProvider.notifier).state = currentUserRole; + + ref.read(outletIdByOwnerProvider.notifier).state = outletIdByOwner; + + ref.read(currentStatusLoginProvider.notifier).state = true; + } + + Future _handleLogin() async { + if (!_formKey.currentState!.validate()) return; + + if (_captchaController.text.toUpperCase() != _currentCaptcha) { + CustomSnackbar.showError(context, "Kode captcha salah!"); + _refreshCaptcha(); + return; + } + + try { + final identifier = _identifierController.text.trim(); + + final password = _passwordController.text.trim(); + + await ref + .read(authControllerProvider.notifier) + .login(LoginUser(identifier: identifier, password: password)); + + if (!mounted) return; + + await _setCurrentOutlet(); + + await _setCurrentUser(); + + CustomSnackbar.showSuccess(context, "Berhasil login"); + context.goNamed(AppRoute.homeScreen); + } on ServerException catch (e) { + if (!mounted) ; + + _refreshCaptcha(); + + CustomSnackbar.showError(context, e.message); + } catch (e, st) { + LogMessage.log.e(e.toString(), error: e, stackTrace: st); + CustomSnackbar.showError(context, "Ups, terjadi kesalahan"); + } + } + + @override + Widget build(BuildContext context) { + return LayoutBuilder( + builder: (context, constraints) { + final bool isTablet = 100.w >= 600; + + final authControllerState = ref.watch(authControllerProvider); + + final isLoading = authControllerState.isLoading; + + return SafeArea( + top: false, + bottom: true, + right: false, + left: false, + child: Scaffold( + backgroundColor: Colors.white, + body: SafeArea( + child: SingleChildScrollView( + padding: EdgeInsets.symmetric(horizontal: 8.w, vertical: 5.h), + child: Form( + key: _formKey, + child: Column( + children: [ + Image.asset(AppAsset.LOGO_UPDATE, height: 12.h), + + SizedBox(height: 2.5.h), + + Text( + "Masuk Akun", + style: TextStyle( + fontSize: AppFontSize.large.sp, + fontWeight: FontWeight.bold, + color: AppColor.primaryColor, + ), + ), + SizedBox(height: 1.h), + Text( + "Kelola bisnis anda dengan mudah dan siap untuk berkembang", + textAlign: TextAlign.center, + style: TextStyle( + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + color: Colors.grey, + ), + ), + + SizedBox(height: 4.h), + + CustomTextFormField( + label: "Email atau No. WhatsApp", + hint: "contoh@email.com / 0812...", + controller: _identifierController, + prefixIcon: Icons.person_outline, + validator: (value) => _validation( + value: value, + message: "Kolom ini belum diisi", + ), + readOnly: isLoading, + ), + + SizedBox(height: 2.5.h), + + CustomTextFormField( + label: "Password", + hint: "••••••••", + controller: _passwordController, + prefixIcon: Icons.lock_outline, + isObscure: _obscurePassword, + suffixIcon: _obscurePassword + ? Icons.visibility_off + : Icons.visibility, + suffixPressed: () => setState( + () => _obscurePassword = !_obscurePassword, + ), + validator: (value) => _validation( + value: value, + message: "Password belum diisi", + ), + readOnly: isLoading, + ), + + SizedBox(height: 2.5.h), + Row( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Expanded( + flex: 2, + child: CustomTextFormField( + label: "Kode Captcha", + hint: "Input kode", + controller: _captchaController, + validator: (value) => _validation( + value: value, + message: "captcha belum diisi", + ), + readOnly: isLoading, + ), + ), + SizedBox(width: 4.w), + InkWell( + onTap: _refreshCaptcha, + borderRadius: BorderRadius.circular(2.w), + child: Container( + height: 7.h, + width: 30.w, + decoration: BoxDecoration( + color: Colors.grey[200], + borderRadius: BorderRadius.circular(2.w), + border: Border.all(color: Colors.grey[300]!), + ), + child: Stack( + alignment: Alignment.center, + children: [ + CustomPaint( + size: Size(32.w, 6.5.h), + painter: CaptchaNoisePainter(), + ), + Text( + _currentCaptcha, + style: TextStyle( + fontWeight: FontWeight.bold, + fontStyle: FontStyle.italic, + letterSpacing: 4, + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + color: Colors.black87, + ), + ), + ], + ), + ), + ), + ], + ), + + Align( + alignment: Alignment.centerRight, + child: TextButton( + onPressed: () => + context.pushNamed(AppRoute.forgotPasswordScreen), + child: Text( + "Lupa Password?", + style: TextStyle( + color: AppColor.primaryColor, + fontWeight: FontWeight.bold, + fontSize: isTablet + ? (AppFontSize.medium - 1.25).sp + : (AppFontSize.small - 1.25).sp, + ), + ), + ), + ), + + SizedBox(height: 2.5.h), + + ElevatedButton( + onPressed: isLoading ? null : _handleLogin, + style: ElevatedButton.styleFrom( + backgroundColor: AppColor.primaryColor, + minimumSize: Size(double.infinity, 7.h), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(3.w), + ), + disabledBackgroundColor: Colors.grey.shade300, + elevation: 2, + ), + child: Text( + "Masuk Sekarang", + style: TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + fontSize: AppFontSize.medium.sp, + ), + ), + ), + + SizedBox(height: 4.h), + + _buildAdminLink(isTablet), + + SizedBox(height: 2.5.h), + + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + "Belum punya akun? ", + style: TextStyle( + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + ), + ), + GestureDetector( + onTap: () => + context.pushNamed(AppRoute.registrationScreen), + child: Text( + "Daftar disini", + style: TextStyle( + color: AppColor.primaryColor, + fontWeight: FontWeight.bold, + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + ), + ), + ), + ], + ), + + SizedBox(height: 2.5.h), + Text( + "Powered By P.T. Niaga Nusantara", + style: TextStyle( + color: Colors.grey.shade600, + fontWeight: FontWeight.w500, + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + ), + ), + SizedBox(height: 0.75.h), + Text( + "Versi 1.0.0", + style: TextStyle( + color: Colors.grey, + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + ), + ), + ], + ), + ), + ), + ), + ), + ); + }, + ); + } + + Widget _buildAdminLink(bool isTablet) { + return Material( + color: Colors.blue[50], + borderRadius: BorderRadius.circular(3.w), + child: InkWell( + onTap: () => context.pushNamed(AppRoute.adminLoginScreen), + borderRadius: BorderRadius.circular(3.w), + child: Container( + padding: EdgeInsets.symmetric(horizontal: 4.w, vertical: 2.h), + decoration: BoxDecoration( + border: Border.all(color: Colors.blue[100]!), + borderRadius: BorderRadius.circular(3.w), + ), + child: Row( + children: [ + Icon(Icons.info_outline, color: Colors.blue[700], size: 5.w), + SizedBox(width: 3.w), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Apakah anda staf atau kasir?", + style: TextStyle( + color: Colors.blue[900], + fontSize: isTablet + ? (AppFontSize.medium - 1.25).sp + : (AppFontSize.small - 1.25).sp, + fontWeight: FontWeight.w500, + ), + ), + SizedBox(height: 0.5.h), + Text( + "Masuk sebagai admin di sini", + style: TextStyle( + color: AppColor.primaryColor, + fontSize: isTablet + ? (AppFontSize.medium - 0.5).sp + : (AppFontSize.small - 0.5).sp, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + ), + Icon(Icons.chevron_right, color: Colors.blue[300], size: 5.w), + ], + ), + ), + ), + ); + } +} diff --git a/Mobile Operasional/lib/features/auth/presentation/screens/registration_screen.dart b/Mobile Operasional/lib/features/auth/presentation/screens/registration_screen.dart new file mode 100644 index 0000000..45f22a9 --- /dev/null +++ b/Mobile Operasional/lib/features/auth/presentation/screens/registration_screen.dart @@ -0,0 +1,417 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; +import 'package:niogu_app/core/constants/app_asset.dart'; +import 'package:niogu_app/core/constants/app_color.dart'; +import 'package:niogu_app/core/constants/app_font_size.dart'; +import 'package:niogu_app/core/errors/exceptions.dart'; +import 'package:niogu_app/core/providers/app_provider.dart'; +import 'package:niogu_app/core/router/app_route.dart'; +import 'package:niogu_app/core/utils/log_message.dart'; +import 'package:niogu_app/core/system/system_setting.dart'; +import 'package:niogu_app/core/widgets/custom_snackbar.dart'; +import 'package:niogu_app/core/widgets/custom_text_form_field.dart'; +import 'package:niogu_app/features/auth/domain/entities/user.dart'; +import 'package:niogu_app/features/auth/presentation/providers/auth_provider.dart'; +import 'package:sizer/sizer.dart'; + +class RegistrationScreen extends ConsumerStatefulWidget { + const RegistrationScreen({super.key}); + + @override + ConsumerState createState() => _RegistrationScreenState(); +} + +class _RegistrationScreenState extends ConsumerState { + final _formKey = GlobalKey(); + + final _businessNameController = TextEditingController(); + + final _nameController = TextEditingController(); + + final _phoneNumberController = TextEditingController(); + + final _emailController = TextEditingController(); + + final _passwordController = TextEditingController(); + + final _passwordConfirmController = TextEditingController(); + + bool _obscurePassword = true; + + bool _obscurePasswordConfirm = true; + + @override + void initState() { + // TODO: implement initState + super.initState(); + } + + @override + void dispose() { + // TODO: implement dispose + _businessNameController.dispose(); + _nameController.dispose(); + _phoneNumberController.dispose(); + _emailController.dispose(); + _passwordController.dispose(); + _passwordConfirmController.dispose(); + super.dispose(); + } + + String? _validation({ + String? value, + String? message, + bool isEmail = false, + bool isPassword = false, + bool isPasswordConfirm = false, + }) { + if (value == null || value.isEmpty) { + return message; + } + + final _emailRegex = RegExp( + r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$', + ); + + if (isEmail && !_emailRegex.hasMatch(value)) { + return "Email tidak valid"; + } + + if (isPassword) { + if (value.length < 8) { + return "Password minimal harus 8 karakter"; + } + + final passwordRegExp = RegExp( + r'^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&#])[A-Za-z\d@$!%*?&#]{8,}$', + ); + + if (!passwordRegExp.hasMatch(value)) { + return "Gunakan huruf besar, kecil, angka, dan simbol"; + } + } + + if (isPasswordConfirm && value != _passwordController.text.trim()) { + return "Konfirmasi password tidak sesuai"; + } + + return null; + } + + Future _setCurrentOutlet() async { + final currentOutletId = await SystemSetting.getCurrentOutletId(); + + final currentOutletName = await SystemSetting.getCurrentOutletName(); + + ref.read(currentOutletIdProvider.notifier).state = currentOutletId; + + ref.read(currentOutletNameProvider.notifier).state = currentOutletName; + } + + Future _setCurrentUser() async { + final currentUserId = await SystemSetting.getCurrentUserId(); + + final currentUserName = await SystemSetting.getCurrentUserName(); + + final currentUserEmail = await SystemSetting.getCurrentUserEmail(); + + final currentUserRole = await SystemSetting.getCurrentUserRole(); + + final outletIdByOwner = await SystemSetting.getOutletIdByOwner(); + + ref.read(currentUserIdProvider.notifier).state = currentUserId; + + ref.read(currentUserNameProvider.notifier).state = currentUserName; + + ref.read(currentUserEmailProvider.notifier).state = currentUserEmail; + + ref.read(currentUserRoleProvider.notifier).state = currentUserRole; + + ref.read(outletIdByOwnerProvider.notifier).state = outletIdByOwner; + + ref.read(currentStatusLoginProvider.notifier).state = true; + } + + Future _register() async { + if (!_formKey.currentState!.validate()) return; + + try { + await ref + .read(authControllerProvider.notifier) + .register( + RegisterUser( + businessName: _businessNameController.text.trim(), + ownerName: _nameController.text.trim(), + phoneNumber: _phoneNumberController.text.trim(), + email: _emailController.text.trim(), + password: _passwordController.text.trim(), + passwordConfirmation: _passwordConfirmController.text.trim(), + ), + ); + + await ref + .read(authControllerProvider.notifier) + .login( + LoginUser( + identifier: _phoneNumberController.text.trim(), + password: _passwordController.text.trim(), + ), + ); + + if (!mounted) return; + + await _setCurrentOutlet(); + + await _setCurrentUser(); + + CustomSnackbar.showSuccess(context, "Berhasil login"); + context.goNamed(AppRoute.homeScreen); + } on ServerException catch (e) { + if (!mounted) ; + + CustomSnackbar.showError(context, e.message); + } catch (e, st) { + LogMessage.log.e(e.toString(), error: e, stackTrace: st); + CustomSnackbar.showError(context, "Ups, terjadi kesalahan"); + } + } + + @override + Widget build(BuildContext context) { + return LayoutBuilder( + builder: (context, constraints) { + final bool isTablet = 100.w >= 600; + + final authControllerState = ref.watch(authControllerProvider); + + final isLoading = authControllerState.isLoading; + + return SafeArea( + top: false, + bottom: true, + right: false, + left: false, + child: Scaffold( + backgroundColor: Colors.white, + body: SafeArea( + child: SingleChildScrollView( + padding: EdgeInsets.symmetric(horizontal: 8.w, vertical: 5.h), + child: Form( + key: _formKey, + child: Column( + children: [ + Image.asset(AppAsset.LOGO_UPDATE, height: 12.h), + + SizedBox(height: 2.5.h), + + Text( + "Daftar Akun", + style: TextStyle( + fontSize: AppFontSize.large.sp, + fontWeight: FontWeight.bold, + color: AppColor.primaryColor, + ), + ), + SizedBox(height: 1.h), + Text( + "Setiap hal besar selalu dimulai dengan hal kecil. perkuat pondasi bisnimu dan gunakan strategi terbaik", + textAlign: TextAlign.center, + style: TextStyle( + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + color: Colors.grey, + ), + ), + + SizedBox(height: 4.h), + + CustomTextFormField( + label: "Nama Usaha/Perusahaan", + hint: "Contoh: Ayam Gepok Pojok", + controller: _businessNameController, + prefixIcon: Icons.business_center_outlined, + validator: (value) => _validation( + value: value, + message: "Nama usaha belum diisi", + ), + readOnly: isLoading, + ), + + SizedBox(height: 2.5.h), + + CustomTextFormField( + label: "Nama Pemilik", + hint: "John Doe", + controller: _nameController, + prefixIcon: Icons.person_outline, + validator: (value) => _validation( + value: value, + message: "Nama pemilik belum diisi", + ), + readOnly: isLoading, + ), + + SizedBox(height: 2.5.h), + + CustomTextFormField( + label: "No. Whatsapp", + hint: "Contoh:081234xxxxxx", + keyboardType: TextInputType.phone, + controller: _phoneNumberController, + prefixIcon: Icons.phone_outlined, + validator: (value) => _validation( + value: value, + message: "No. Whatsapp belum diisi", + ), + readOnly: isLoading, + ), + + SizedBox(height: 2.5.h), + + CustomTextFormField( + label: "Email", + hint: "Opsional (Disarankan)", + keyboardType: TextInputType.emailAddress, + controller: _emailController, + prefixIcon: Icons.mail_outline_outlined, + validator: (value) => + _validation(value: value, isEmail: true), + readOnly: isLoading, + ), + + SizedBox(height: 2.5.h), + + CustomTextFormField( + label: "Password", + hint: "••••••••", + controller: _passwordController, + prefixIcon: Icons.lock_outline, + isObscure: _obscurePassword, + suffixIcon: _obscurePassword + ? Icons.visibility_off + : Icons.visibility, + suffixPressed: () { + setState(() { + _obscurePassword = !_obscurePassword; + }); + }, + validator: (value) => _validation( + value: value, + message: "Password belum diisi", + isPassword: true, + ), + readOnly: isLoading, + ), + + SizedBox(height: 2.5.h), + + CustomTextFormField( + label: "Konfirmasi Password", + hint: "••••••••", + controller: _passwordConfirmController, + prefixIcon: Icons.lock_outline, + isObscure: _obscurePasswordConfirm, + suffixIcon: _obscurePasswordConfirm + ? Icons.visibility_off + : Icons.visibility, + suffixPressed: () { + setState(() { + _obscurePasswordConfirm = !_obscurePasswordConfirm; + }); + }, + validator: (value) => _validation( + value: value, + message: "Konfirmasi password belum diisi", + isPasswordConfirm: true, + ), + readOnly: isLoading, + ), + + SizedBox(height: 2.5.h), + + ElevatedButton( + onPressed: isLoading ? null : _register, + style: ElevatedButton.styleFrom( + backgroundColor: AppColor.primaryColor, + minimumSize: Size(double.infinity, 7.h), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(3.w), + ), + elevation: 2, + disabledBackgroundColor: Colors.grey.shade300, + ), + child: Text( + "Daftar Sekarang", + style: TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + fontSize: AppFontSize.medium.sp, + ), + ), + ), + + SizedBox(height: 4.h), + + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + "Sudah punya akun? ", + style: TextStyle( + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + ), + ), + GestureDetector( + onTap: () => context.canPop() + ? context.pop() + : context.goNamed(AppRoute.ownerLoginScreen), + child: Text( + "Masuk", + style: TextStyle( + color: AppColor.primaryColor, + fontWeight: FontWeight.bold, + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + ), + ), + ), + ], + ), + + SizedBox(height: 2.5.h), + Text( + "Powered By P.T. Niaga Nusantara", + style: TextStyle( + color: Colors.grey.shade600, + fontWeight: FontWeight.w500, + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + ), + ), + SizedBox(height: 0.75.h), + Text( + "Versi 1.0.0", + style: TextStyle( + color: Colors.grey, + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + ), + ), + ], + ), + ), + ), + ), + ), + ); + }, + ); + } +} diff --git a/Mobile Operasional/lib/features/auth/presentation/screens/verification_otp_screen.dart b/Mobile Operasional/lib/features/auth/presentation/screens/verification_otp_screen.dart new file mode 100644 index 0000000..7277539 --- /dev/null +++ b/Mobile Operasional/lib/features/auth/presentation/screens/verification_otp_screen.dart @@ -0,0 +1,212 @@ +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'package:niogu_app/core/constants/app_asset.dart'; +import 'package:niogu_app/core/constants/app_color.dart'; +import 'package:niogu_app/core/constants/app_font_size.dart'; +import 'package:niogu_app/core/router/app_route.dart'; +import 'package:niogu_app/core/utils/log_message.dart'; +import 'package:pinput/pinput.dart'; +import 'package:sizer/sizer.dart'; + +class VerificationOtpScreen extends StatefulWidget { + final String identifier; // Email atau No WhatsApp + const VerificationOtpScreen({super.key, required this.identifier}); + + @override + State createState() => _VerificationOtpScreenState(); +} + +class _VerificationOtpScreenState extends State { + @override + Widget build(BuildContext context) { + return LayoutBuilder( + builder: (context, constraints) { + final bool isTablet = 100.w >= 600; + + final defaultPinTheme = PinTheme( + width: 13.w, + height: 15.w, + textStyle: TextStyle( + fontSize: AppFontSize.medium.sp, + fontWeight: FontWeight.bold, + color: Colors.black, + ), + decoration: BoxDecoration( + color: Colors.grey[50], + borderRadius: BorderRadius.circular(2.5.w), + border: Border.all(color: Colors.grey.shade300), + ), + ); + + final focusedPinTheme = defaultPinTheme.copyDecorationWith( + border: Border.all(color: AppColor.primaryColor, width: 1.5), + borderRadius: BorderRadius.circular(2.5.w), + ); + + final errorPinTheme = defaultPinTheme.copyDecorationWith( + border: Border.all(color: Colors.red, width: 1.5), + borderRadius: BorderRadius.circular(2.5.w), + ); + return SafeArea( + top: false, + bottom: true, + right: false, + left: false, + child: Scaffold( + backgroundColor: Colors.white, + appBar: AppBar( + backgroundColor: Colors.white, + elevation: 0, + leading: IconButton( + icon: const Icon(Icons.arrow_back, color: Colors.black), + onPressed: () => Navigator.pop(context), + ), + ), + body: SingleChildScrollView( + padding: EdgeInsets.symmetric(horizontal: 8.w), + child: Column( + children: [ + Image.asset(AppAsset.LOGO_UPDATE, height: 10.h), + + SizedBox(height: 4.h), + + Text( + "Verifikasi Kode OTP", + style: TextStyle( + fontSize: AppFontSize.medium.sp, + fontWeight: FontWeight.bold, + color: AppColor.primaryColor, + ), + ), + + SizedBox(height: 1.5.h), + + Text( + "Masukkan 6 digit kode yang dikirimkan ke", + style: TextStyle( + fontSize: isTablet + ? (AppFontSize.medium - 1).sp + : (AppFontSize.small - 1).sp, + color: Colors.black, + ), + ), + SizedBox(height: 0.5.h), + Text( + "08123456789", + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: isTablet + ? (AppFontSize.medium - 1).sp + : (AppFontSize.small - 1).sp, + ), + ), + + SizedBox(height: 1.h), + + GestureDetector( + onTap: () => context.pop(), + child: Text( + "Nomor Salah? Ubah di sini", + style: TextStyle( + color: AppColor.primaryColor, + fontWeight: FontWeight.bold, + fontSize: isTablet + ? (AppFontSize.medium - 1).sp + : (AppFontSize.small - 1).sp, + ), + ), + ), + + SizedBox(height: 6.h), + + Pinput( + length: 6, + defaultPinTheme: defaultPinTheme, + focusedPinTheme: focusedPinTheme, + separatorBuilder: (index) => SizedBox(width: 2.w), + hapticFeedbackType: HapticFeedbackType.lightImpact, + onCompleted: (pin) => LogMessage.log.i(pin), + cursor: Column( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Container( + margin: EdgeInsets.only(bottom: 2.w), + width: 5.w, + height: 1, + color: AppColor.primaryColor, + ), + ], + ), + errorText: "Kode otp salah", + errorTextStyle: TextStyle( + color: Colors.red, + fontSize: isTablet + ? (AppFontSize.medium - 1).sp + : (AppFontSize.small - 1).sp, + ), + errorPinTheme: errorPinTheme, + validator: (_) => null, + pinputAutovalidateMode: PinputAutovalidateMode.onSubmit, + ), + + SizedBox(height: 4.h), + + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + "Belum dapat kode? ", + style: TextStyle( + fontSize: isTablet + ? (AppFontSize.medium - 1).sp + : (AppFontSize.small - 1).sp, + color: Colors.grey, + ), + ), + TextButton( + onPressed: null, + child: Text( + "Minta ulang (00:30)", + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: isTablet + ? (AppFontSize.medium - 1).sp + : (AppFontSize.small - 1).sp, + color: AppColor.primaryColor, + ), + ), + ), + ], + ), + + SizedBox(height: 4.h), + + ElevatedButton( + onPressed: () => + context.pushNamed(AppRoute.changePasswordScreen), + style: ElevatedButton.styleFrom( + backgroundColor: AppColor.primaryColor, + minimumSize: Size(double.infinity, 7.h), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(3.w), + ), + elevation: 2, + ), + child: Text( + "Verifikasi Sekarang", + style: TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + fontSize: AppFontSize.medium.sp, + ), + ), + ), + ], + ), + ), + ), + ); + }, + ); + } +} diff --git a/Mobile Operasional/lib/features/customer/data/repositories/customer_repository_impl.dart b/Mobile Operasional/lib/features/customer/data/repositories/customer_repository_impl.dart new file mode 100644 index 0000000..ebfd6c3 --- /dev/null +++ b/Mobile Operasional/lib/features/customer/data/repositories/customer_repository_impl.dart @@ -0,0 +1,53 @@ +import 'package:drift/drift.dart'; +import 'package:niogu_app/core/database/app_database.dart'; +import 'package:niogu_app/core/enums/customer_source.dart'; +import 'package:niogu_app/core/system/system_setting.dart'; +import 'package:niogu_app/features/customer/domain/entities/customer.dart'; +import 'package:niogu_app/features/customer/domain/repositories/i_customer_repository.dart'; + +class CustomerRepositoryImpl implements ICustomerRepository { + final AppDatabase _appDatabase; + + const CustomerRepositoryImpl(this._appDatabase); + + @override + Stream> watchCustomers() { + return _appDatabase.customerDao.watchAllCustomers(); + } + + @override + Future fetchCustomerById(String id) async { + return await _appDatabase.customerDao.getCustomerById(id); + } + + @override + Stream> watchCustomerAddresses(String customerId) { + return _appDatabase.customerDao.watchAllCustomerAddresses(customerId); + } + + @override + Stream> watchCustomerActivities(String customerId) { + return _appDatabase.customerDao.watchAllCustomerActivities(customerId); + } + + @override + Future saveCustomer(UpsertCustomer customer) async { + final currentOutletId = await SystemSetting.getCurrentOutletId(); + + return await _appDatabase.customerDao.saveCustomer( + CustomersCompanion( + localId: Value(customer.id), + outletId: Value(currentOutletId!), + name: Value(customer.name), + email: Value(customer.email), + phoneNumber: Value(customer.phoneNumber), + source: Value(CustomerSource.offline.source), + ), + ); + } + + @override + Future deleteCustomer(String id) async { + return await _appDatabase.customerDao.deleteCustomer(id); + } +} diff --git a/Mobile Operasional/lib/features/customer/domain/entities/customer.dart b/Mobile Operasional/lib/features/customer/domain/entities/customer.dart new file mode 100644 index 0000000..e539dfe --- /dev/null +++ b/Mobile Operasional/lib/features/customer/domain/entities/customer.dart @@ -0,0 +1,75 @@ +import 'package:latlong2/latlong.dart'; +import 'package:niogu_app/core/enums/customer_source.dart'; +import 'package:niogu_app/core/enums/order_source.dart'; +import 'package:niogu_app/core/enums/order_status.dart'; +import 'package:uuid/uuid.dart'; + +enum CustomerEmpty { loading, empty_database, empty_search_result, has_data } + +class DisplayCustomers { + final String id; + final String name; + + const DisplayCustomers({ + required this.id, + required this.name + }); +} + +class UpsertCustomer { + final String id; + final String name; + final String? email; + final String? phoneNumber; + + UpsertCustomer({ + String? id, + required this.name, + required this.email, + required this.phoneNumber + }) : id = id ?? const Uuid().v7().toString(); +} + +class CustomerDetail { + final String id; + final String name; + final String email; + final String phoneNumber; + final CustomerSource customerSource; + + const CustomerDetail({ + required this.id, + required this.name, + required this.email, + required this.phoneNumber, + required this.customerSource, + }); +} + +class CustomerAddress { + final String label; + final String fullAddress; + final LatLng coordinate; + + const CustomerAddress({ + required this.label, + required this.fullAddress, + required this.coordinate, + }); +} + +class CustomerActivities { + final String id; + final String orderNumber; + final OrderSource orderSource; + final OrderStatus orderStatus; + final DateTime orderDate; + + const CustomerActivities({ + required this.id, + required this.orderNumber, + required this.orderSource, + required this.orderStatus, + required this.orderDate + }); +} \ No newline at end of file diff --git a/Mobile Operasional/lib/features/customer/domain/repositories/i_customer_repository.dart b/Mobile Operasional/lib/features/customer/domain/repositories/i_customer_repository.dart new file mode 100644 index 0000000..2c58736 --- /dev/null +++ b/Mobile Operasional/lib/features/customer/domain/repositories/i_customer_repository.dart @@ -0,0 +1,15 @@ +import 'package:niogu_app/features/customer/domain/entities/customer.dart'; + +abstract class ICustomerRepository { + Stream> watchCustomers(); + + Future fetchCustomerById(String id); + + Stream> watchCustomerAddresses(String customerId); + + Stream> watchCustomerActivities(String customerId); + + Future saveCustomer(UpsertCustomer customer); + + Future deleteCustomer(String id); +} diff --git a/Mobile Operasional/lib/features/customer/presentation/controllers/customer_controller.dart b/Mobile Operasional/lib/features/customer/presentation/controllers/customer_controller.dart new file mode 100644 index 0000000..8a1d109 --- /dev/null +++ b/Mobile Operasional/lib/features/customer/presentation/controllers/customer_controller.dart @@ -0,0 +1,38 @@ +import 'dart:async'; + +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:niogu_app/features/customer/domain/entities/customer.dart'; +import 'package:niogu_app/features/customer/domain/repositories/i_customer_repository.dart'; +import 'package:niogu_app/features/customer/presentation/providers/customer_provider.dart'; + +class CustomerController extends AutoDisposeAsyncNotifier { + late final ICustomerRepository _customerRepository; + @override + FutureOr build() { + _customerRepository = ref.read(customerRepositoryProvider); + } + + Future saveCustomer(UpsertCustomer customer) async { + state = const AsyncValue.loading(); + + final result = await AsyncValue.guard(() async { + await _customerRepository.saveCustomer(customer); + }); + + state = result; + + if (result is AsyncError) throw result.error!; + } + + Future deleteCustomer(String id) async { + state = const AsyncValue.loading(); + + final result = await AsyncValue.guard(() async { + await _customerRepository.deleteCustomer(id); + }); + + state = result; + + if (result is AsyncError) throw result.error!; + } +} diff --git a/Mobile Operasional/lib/features/customer/presentation/providers/customer_provider.dart b/Mobile Operasional/lib/features/customer/presentation/providers/customer_provider.dart new file mode 100644 index 0000000..8ad69b2 --- /dev/null +++ b/Mobile Operasional/lib/features/customer/presentation/providers/customer_provider.dart @@ -0,0 +1,85 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:niogu_app/core/providers/app_provider.dart'; +import 'package:niogu_app/features/customer/data/repositories/customer_repository_impl.dart'; +import 'package:niogu_app/features/customer/domain/entities/customer.dart'; +import 'package:niogu_app/features/customer/domain/repositories/i_customer_repository.dart'; +import 'package:niogu_app/features/customer/presentation/controllers/customer_controller.dart'; + +final customerRepositoryProvider = Provider((ref) { + final appDatabase = ref.watch(appDatabaseProvider); + return CustomerRepositoryImpl(appDatabase); +}); + +final customerStreamProvider = + StreamProvider.autoDispose>((ref) { + final customerRepository = ref.watch(customerRepositoryProvider); + return customerRepository.watchCustomers(); + }); + +final customerSearchProvider = StateProvider.autoDispose((ref) => ''); + +final filteredCustomerProvider = + Provider.autoDispose>>((ref) { + final customerStreamAsync = ref.watch(customerStreamProvider); + final customerSearchAsync = ref.watch(customerSearchProvider); + return customerStreamAsync.when( + data: (customers) { + if (customerSearchAsync.trim().isEmpty) { + return AsyncValue.data(customers); + } + + final filteredCustomers = customers.where((customer) { + return customer.name.toLowerCase().contains( + customerSearchAsync.toLowerCase(), + ); + }).toList(); + + return AsyncValue.data(filteredCustomers); + }, + error: (error, stackTrace) { + return AsyncValue.error(error, stackTrace); + }, + loading: () => const AsyncValue.loading(), + ); + }); + +final customerEmptyProvider = Provider.autoDispose((ref) { + final customerStreamAsync = ref.watch(customerStreamProvider); + final filteredCustomerAsync = ref.watch(filteredCustomerProvider); + final customerSearchAsync = ref.watch(customerSearchProvider); + + if (customerStreamAsync.isLoading) { + return CustomerEmpty.loading; + } + + final allCustomers = customerStreamAsync.value ?? []; + + if (allCustomers.isEmpty) { + return CustomerEmpty.empty_database; + } + + final filteredCustomers = filteredCustomerAsync.value ?? []; + + if (customerSearchAsync.isNotEmpty && filteredCustomers.isEmpty) { + return CustomerEmpty.empty_search_result; + } + + return CustomerEmpty.has_data; +}); + +final customerAddressesProvider = StreamProvider.family + .autoDispose, String>((ref, customerId) { + final customerRepository = ref.watch(customerRepositoryProvider); + return customerRepository.watchCustomerAddresses(customerId); + }); + +final customerActivitiesProvider = StreamProvider.family + .autoDispose, String>((ref, customerId) { + final customerRepository = ref.watch(customerRepositoryProvider); + return customerRepository.watchCustomerActivities(customerId); + }); + +final customerControllerProvider = + AsyncNotifierProvider.autoDispose( + CustomerController.new, + ); diff --git a/Mobile Operasional/lib/features/customer/presentation/screens/add_customer_screen.dart b/Mobile Operasional/lib/features/customer/presentation/screens/add_customer_screen.dart new file mode 100644 index 0000000..f8660a9 --- /dev/null +++ b/Mobile Operasional/lib/features/customer/presentation/screens/add_customer_screen.dart @@ -0,0 +1,214 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; +import 'package:niogu_app/core/components/top_back_bar_app.dart'; +import 'package:niogu_app/core/utils/log_message.dart'; +import 'package:niogu_app/core/widgets/custom_snackbar.dart'; +import 'package:niogu_app/core/widgets/custom_text_form_field.dart'; +import 'package:niogu_app/features/customer/domain/entities/customer.dart'; +import 'package:niogu_app/features/customer/presentation/providers/customer_provider.dart'; +import 'package:sizer/sizer.dart'; +import 'package:niogu_app/core/constants/app_color.dart'; +import 'package:niogu_app/core/constants/app_font_size.dart'; + +class AddCustomerScreen extends ConsumerStatefulWidget { + final String? initialName; + final String? initialPhone; + + const AddCustomerScreen({super.key, this.initialName, this.initialPhone}); + + @override + ConsumerState createState() => _AddCustomerScreenState(); +} + +class _AddCustomerScreenState extends ConsumerState { + final GlobalKey _formKey = GlobalKey(); + + final TextEditingController _nameController = TextEditingController(); + + final TextEditingController _emailController = TextEditingController(); + + final TextEditingController _phoneController = TextEditingController(); + + @override + void initState() { + super.initState(); + _nameController.text = widget.initialName ?? ''; + _phoneController.text = widget.initialPhone ?? ''; + } + + @override + void dispose() { + _nameController.dispose(); + _emailController.dispose(); + _phoneController.dispose(); + super.dispose(); + } + + Future _addCustomer() async { + if (!_formKey.currentState!.validate()) return; + + final String name = _nameController.text; + + final String? email = _emailController.text.isNotEmpty + ? _emailController.text + : null; + + final String? phoneNumber = _phoneController.text.isNotEmpty + ? _phoneController.text + : null; + + final customer = UpsertCustomer( + name: name, + email: email, + phoneNumber: phoneNumber, + ); + + try { + await ref + .read(customerControllerProvider.notifier) + .saveCustomer(customer); + + if (!mounted) return; + + CustomSnackbar.showSuccess(context, "Pelanggan berhasil disimpan"); + + context.pop(); + } catch (e, st) { + LogMessage.log.e(e.toString(), error: e, stackTrace: st); + CustomSnackbar.showError(context, "Ups, terjadi kesalahan"); + } + } + + @override + Widget build(BuildContext context) { + return LayoutBuilder( + builder: (context, constraints) { + final customerControllerState = ref.watch(customerControllerProvider); + + return SafeArea( + top: false, + bottom: true, + right: false, + left: false, + child: Scaffold( + backgroundColor: Colors.white, + appBar: TopBackBarApp( + title: "Tambah Pelanggan", + onTap: () => context.pop(), + ), + body: Column( + children: [ + Expanded( + child: SingleChildScrollView( + padding: EdgeInsets.symmetric( + horizontal: 5.w, + vertical: 2.h, + ), + child: Form( + key: _formKey, + child: Column( + children: [ + Center( + child: Container( + width: 25.w, + height: 25.w, + decoration: BoxDecoration( + color: Colors.grey[100], + shape: BoxShape.circle, + border: Border.all( + color: Colors.grey.shade200, + width: 2, + ), + ), + child: Icon( + Icons.person_rounded, + size: 12.w, + color: Colors.grey[400], + ), + ), + ), + + SizedBox(height: 4.h), + + CustomTextFormField( + label: "Nama Pelanggan", + controller: _nameController, + hint: "Masukkan nama", + validator: (value) { + if (value == null || value.isEmpty) { + return "Nama pelanggan belum diisi"; + } + + return null; + }, + prefixIcon: Icons.person_outline, + ), + + SizedBox(height: 2.h), + CustomTextFormField( + label: "Email", + controller: _emailController, + hint: "Masukkan email (opsional)", + keyboardType: TextInputType.emailAddress, + prefixIcon: Icons.email_outlined, + ), + SizedBox(height: 2.h), + CustomTextFormField( + label: "No. Handphone / WA", + controller: _phoneController, + hint: "Masukkan no handphone / wa (opsional)", + keyboardType: TextInputType.phone, + prefixIcon: Icons.phone_android_outlined, + ), + ], + ), + ), + ), + ), + + Container( + padding: EdgeInsets.all(5.w), + decoration: BoxDecoration( + color: Colors.white, + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.05), + blurRadius: 10, + offset: const Offset(0, -5), + ), + ], + ), + child: SizedBox( + width: double.infinity, + height: 6.5.h, + child: ElevatedButton( + onPressed: customerControllerState.isLoading + ? null + : _addCustomer, + style: ElevatedButton.styleFrom( + backgroundColor: AppColor.primaryColor, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(2.5.w), + ), + disabledBackgroundColor: Colors.grey.shade300, + ), + child: Text( + "Simpan Pelanggan", + style: TextStyle( + color: Colors.white, + fontSize: AppFontSize.medium.sp, + fontWeight: FontWeight.bold, + ), + ), + ), + ), + ), + ], + ), + ), + ); + }, + ); + } +} diff --git a/Mobile Operasional/lib/features/customer/presentation/screens/customer_activities_screen.dart b/Mobile Operasional/lib/features/customer/presentation/screens/customer_activities_screen.dart new file mode 100644 index 0000000..aad64ec --- /dev/null +++ b/Mobile Operasional/lib/features/customer/presentation/screens/customer_activities_screen.dart @@ -0,0 +1,50 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; +import 'package:niogu_app/core/components/top_back_bar_app.dart'; +import 'package:niogu_app/features/customer/domain/entities/customer.dart'; +import 'package:niogu_app/features/customer/presentation/widgets/customer_activity.dart'; +import 'package:sizer/sizer.dart'; + +class CustomerActivitiesScreen extends ConsumerWidget { + final bool isOnlineCustomer; + final List activities; + + const CustomerActivitiesScreen({ + super.key, + required this.isOnlineCustomer, + required this.activities, + }); + + @override + Widget build(BuildContext context, WidgetRef ref) { + return LayoutBuilder( + builder: (context, constraints) { + return SafeArea( + top: false, + bottom: true, + right: false, + left: false, + child: Scaffold( + backgroundColor: const Color(0xFFF9FAFB), + appBar: TopBackBarApp( + title: "Aktivitas Pelanggan", + onTap: () => context.pop(), + ), + body: ListView.builder( + padding: EdgeInsets.all(5.w), + itemCount: activities.length, + itemBuilder: (context, index) { + final activity = activities[index]; + return CustomerActivity( + isOnlineCustomer: isOnlineCustomer, + activity: activity, + ); + }, + ), + ), + ); + }, + ); + } +} diff --git a/Mobile Operasional/lib/features/customer/presentation/screens/customer_addresses_screen.dart b/Mobile Operasional/lib/features/customer/presentation/screens/customer_addresses_screen.dart new file mode 100644 index 0000000..bede979 --- /dev/null +++ b/Mobile Operasional/lib/features/customer/presentation/screens/customer_addresses_screen.dart @@ -0,0 +1,153 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; +import 'package:niogu_app/core/components/top_back_bar_app.dart'; +import 'package:niogu_app/core/constants/app_color.dart'; +import 'package:niogu_app/core/constants/app_font_size.dart'; +import 'package:niogu_app/core/router/app_route.dart'; +import 'package:niogu_app/features/customer/domain/entities/customer.dart'; +import 'package:sizer/sizer.dart'; + +class CustomerAddressesScreen extends ConsumerWidget { + final List addresses; + const CustomerAddressesScreen({super.key, required this.addresses}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + return LayoutBuilder( + builder: (context, constraints) { + final bool isTablet = 100.w >= 600; + return SafeArea( + top: false, + bottom: true, + right: false, + left: false, + child: Scaffold( + backgroundColor: const Color(0xFFF9FAFB), + appBar: TopBackBarApp( + title: "Alamat", + onTap: () => context.pop(), + ), + body: ListView.builder( + padding: EdgeInsets.all(4.w), + itemCount: addresses.length, + itemBuilder: (context, index) { + final address = addresses[index]; + return Container( + margin: EdgeInsets.only(bottom: 2.h), + padding: EdgeInsets.all(4.w), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(2.5.w), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.03), + blurRadius: 10, + offset: const Offset(0, 4), + ), + ], + border: Border.all(color: Colors.grey.shade100), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Container( + padding: EdgeInsets.all(2.w), + decoration: BoxDecoration( + color: AppColor.primaryColor.withOpacity(0.1), + shape: BoxShape.circle, + ), + child: Icon( + Icons.location_on, + color: AppColor.primaryColor, + size: 5.w, + ), + ), + SizedBox(width: 3.w), + Text( + address.label, + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: isTablet + ? (AppFontSize.medium - 1.25).sp + : (AppFontSize.small - 1.25).sp, + ), + ), + ], + ), + + SizedBox(height: 1.5.h), + + Text( + address.fullAddress, + style: TextStyle( + fontSize: isTablet + ? (AppFontSize.medium - 1.25).sp + : (AppFontSize.small - 1.25).sp, + color: Colors.grey[700], + height: 1.4, + ), + ), + + SizedBox(height: 1.5.h), + Divider(color: Colors.grey[100]), + SizedBox(height: 1.h), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + Icon( + Icons.straighten_rounded, + size: 5.w, + color: Colors.grey, + ), + SizedBox(width: 1.5.w), + Text( + "16.50 km", + style: TextStyle( + fontSize: isTablet + ? (AppFontSize.medium - 1.25).sp + : (AppFontSize.small - 1.25).sp, + color: Colors.black, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + TextButton.icon( + onPressed: () { + context.pushNamed( + AppRoute.mapCustomerAddressScreen, + extra: address, + ); + }, + icon: Icon( + Icons.map_outlined, + color: AppColor.primaryColor, + size: 5.w, + ), + label: Text( + "Lihat di Peta", + style: TextStyle( + color: AppColor.primaryColor, + fontSize: AppFontSize.small.sp, + fontWeight: FontWeight.bold, + ), + ), + ), + ], + ), + ], + ), + ); + }, + ), + ), + ); + }, + ); + } +} diff --git a/Mobile Operasional/lib/features/customer/presentation/screens/customer_screen.dart b/Mobile Operasional/lib/features/customer/presentation/screens/customer_screen.dart new file mode 100644 index 0000000..3bc9a28 --- /dev/null +++ b/Mobile Operasional/lib/features/customer/presentation/screens/customer_screen.dart @@ -0,0 +1,464 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter_contacts/flutter_contacts.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; +import 'package:niogu_app/core/widgets/custom_error_screen.dart'; +import 'package:niogu_app/core/router/app_route.dart'; +import 'package:niogu_app/core/widgets/custom_empty_screen.dart'; +import 'package:niogu_app/core/widgets/custom_snackbar.dart'; +import 'package:niogu_app/features/customer/domain/entities/customer.dart'; +import 'package:niogu_app/features/customer/presentation/providers/customer_provider.dart'; +import 'package:niogu_app/features/customer/presentation/widgets/customer_shimmer.dart'; +import 'package:sizer/sizer.dart'; +import 'package:niogu_app/core/constants/app_color.dart'; +import 'package:niogu_app/core/constants/app_font_size.dart'; + +class CustomerScreen extends ConsumerStatefulWidget { + const CustomerScreen({super.key}); + + @override + ConsumerState createState() => _CustomerScreenState(); +} + +class _CustomerScreenState extends ConsumerState { + final FocusNode _searchFocusNode = FocusNode(); + + Color _searchIconColor = Colors.grey; + + Timer? _debounce; + + final TextEditingController _searchController = TextEditingController(); + + @override + void initState() { + // TODO: implement initState + super.initState(); + + _searchFocusNode.addListener(() { + setState(() { + _searchIconColor = _searchFocusNode.hasFocus + ? Colors.black + : Colors.grey; + }); + }); + } + + @override + void dispose() { + _searchController.dispose(); + super.dispose(); + } + + void _onSearchChanged(String value) { + if (_debounce?.isActive ?? false) _debounce?.cancel(); + _debounce = Timer(const Duration(milliseconds: 800), () { + ref.read(customerSearchProvider.notifier).state = value; + }); + } + + Map> _groupedCustomers( + List customers, + ) { + customers.sort((a, b) => a.name.compareTo(b.name)); + + final Map> groupedCustomers = {}; + + for (final customer in customers) { + if (customer.name.isEmpty) continue; + + String firstLetter = customer.name[0].toUpperCase(); + + if (!RegExp(r'[A-Z]').hasMatch(firstLetter)) { + firstLetter = '#'; + } + + if (!groupedCustomers.containsKey(firstLetter)) { + groupedCustomers[firstLetter] = []; + } + groupedCustomers[firstLetter]!.add(customer); + } + + return groupedCustomers; + } + + Future _handleImportContact() async { + final permission = await FlutterContacts.requestPermission(); + if (permission) { + final contact = await FlutterContacts.openExternalPick(); + + if (contact != null) { + String name = contact.displayName; + String phone = ""; + + if (contact.phones.isNotEmpty) { + phone = contact.phones.first.number; + } + + if (!mounted) return; + + context.pushNamed( + AppRoute.addCustomerScreen, + extra: {'name': name, 'phone': phone}, + ); + } + } else { + CustomSnackbar.showWarning(context, "Akses Ditolak"); + } + } + + void _showAddOptions(BuildContext context) { + final bool isTablet = 100.w >= 600; + showModalBottomSheet( + context: context, + backgroundColor: Colors.white, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.vertical(top: Radius.circular(2.5.w)), + ), + constraints: const BoxConstraints(maxWidth: double.infinity), + builder: (context) { + return SafeArea( + child: Container( + width: 100.w, + padding: EdgeInsets.symmetric(vertical: 2.h), + clipBehavior: Clip.hardEdge, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.vertical(top: Radius.circular(6.w)), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: EdgeInsets.fromLTRB(5.w, 2.h, 5.w, 1.h), + child: Text( + "Tambah Pelanggan", + style: TextStyle( + fontSize: AppFontSize.medium.sp, + fontWeight: FontWeight.bold, + ), + ), + ), + SizedBox(height: 2.h), + ListTile( + leading: Padding( + padding: EdgeInsets.only(left: 5.w), + child: Icon( + Icons.edit_note_rounded, + color: Colors.blue, + size: 6.w, + ), + ), + title: Text( + "Input Manual", + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + ), + ), + subtitle: Text( + "Isi informasi pelanggan secara manual", + style: TextStyle( + fontSize: isTablet + ? (AppFontSize.medium - 1.25).sp + : (AppFontSize.small - 1.25).sp, + color: Colors.grey, + ), + ), + onTap: () { + context.pop(); + context.pushNamed(AppRoute.addCustomerScreen); + }, + ), + SizedBox(height: 2.h), + ListTile( + leading: Padding( + padding: EdgeInsets.only(left: 5.w), + child: Icon( + Icons.contacts_rounded, + color: Colors.green, + size: 6.w, + ), + ), + title: Text( + "Ambil dari Kontak HP", + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + ), + ), + subtitle: Text( + "Pilih langsung dari buku telepon", + style: TextStyle( + fontSize: isTablet + ? (AppFontSize.medium - 1.25).sp + : (AppFontSize.small - 1.25).sp, + color: Colors.grey, + ), + ), + onTap: () async { + context.pop(); + await _handleImportContact(); + }, + ), + SizedBox(height: 2.h), + ], + ), + ), + ); + }, + ); + } + + @override + Widget build(BuildContext context) { + return LayoutBuilder( + builder: (context, constraints) { + final bool isTablet = 100.w >= 600; + + const greyColor = Color(0xFFF5F5F5); + + final filteredCustomerState = ref.watch(filteredCustomerProvider); + + final customerEmptyState = ref.watch(customerEmptyProvider); + + return SafeArea( + top: false, + bottom: true, + right: false, + left: false, + child: Scaffold( + backgroundColor: const Color(0xFFF9FAFB), + appBar: AppBar( + backgroundColor: Colors.white, + surfaceTintColor: Colors.white, + elevation: 0, + toolbarHeight: isTablet ? 7.5.h : kToolbarHeight, + centerTitle: true, + leading: Center( + child: Material( + color: Colors.transparent, + type: MaterialType.canvas, + child: InkWell( + onTap: () => context.pop(), + borderRadius: BorderRadius.circular(2.5.w), + child: Container( + width: 10.w, + height: 10.w, + decoration: BoxDecoration( + color: Colors.white, + border: Border.all(color: Colors.grey.shade200), + borderRadius: BorderRadius.circular(2.5.w), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.05), + blurRadius: 5, + offset: const Offset(0, 2), + ), + ], + ), + child: Icon( + Icons.arrow_back_ios_new_rounded, + color: Colors.black87, + size: 5.w, + ), + ), + ), + ), + ), + leadingWidth: 18.w, + title: Text( + "Pelanggan", + style: TextStyle( + color: Colors.black87, + fontWeight: FontWeight.bold, + fontSize: AppFontSize.medium.sp, + ), + ), + actions: [ + IconButton( + onPressed: () => _showAddOptions(context), + icon: Icon( + Icons.add_circle, + color: AppColor.primaryColor, + size: 8.w, + ), + ), + SizedBox(width: 3.w), + ], + ), + + body: Padding( + padding: EdgeInsets.symmetric(horizontal: 5.w), + child: Column( + children: [ + Container( + height: 6.h, + padding: isTablet + ? EdgeInsets.symmetric(vertical: 1.h, horizontal: 2.w) + : EdgeInsets.zero, + decoration: BoxDecoration( + color: greyColor, + borderRadius: BorderRadius.circular(2.5.w), + ), + child: TextField( + focusNode: _searchFocusNode, + onChanged: _onSearchChanged, + textAlignVertical: TextAlignVertical.center, + style: TextStyle( + color: Colors.black87, + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + ), + decoration: InputDecoration( + hintText: "Cari nama pelanggan", + hintStyle: TextStyle( + color: _searchIconColor, + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + ), + prefixIcon: Icon( + Icons.search, + color: _searchIconColor, + size: 5.w, + ), + border: InputBorder.none, + contentPadding: EdgeInsets.zero, + isDense: true, + ), + ), + ), + + SizedBox(height: 3.h), + + Expanded( + child: filteredCustomerState.when( + data: (customers) { + final groupedData = _groupedCustomers(customers); + switch (customerEmptyState) { + case CustomerEmpty.loading: + return SizedBox(); + case CustomerEmpty.empty_database: + return CustomEmptyScreen( + title: "Tidak Ada Pelanggan", + body: "Kamu belum memiliki pelanggan", + ); + case CustomerEmpty.empty_search_result: + return CustomEmptyScreen( + body: "Pelanggan Tidak Ditemukan", + ); + case CustomerEmpty.has_data: + return ListView.builder( + physics: const AlwaysScrollableScrollPhysics(), + padding: EdgeInsets.only(bottom: 5.h), + itemCount: groupedData.keys.length, + itemBuilder: (context, index) { + String letter = groupedData.keys.elementAt( + index, + ); + List customers = + groupedData[letter]!; + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + width: double.infinity, + padding: EdgeInsets.symmetric( + horizontal: 5.w, + vertical: 1.h, + ), + color: Colors.grey[200], + child: Text( + letter, + style: TextStyle( + fontWeight: FontWeight.bold, + color: Colors.grey[700], + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + ), + ), + ), + + ...customers.map((customer) { + return Container( + padding: isTablet + ? EdgeInsets.symmetric( + vertical: 1.5.h, + ) + : EdgeInsets.zero, + color: Colors.white, + child: Column( + children: [ + ListTile( + onTap: () { + context.pushNamed( + AppRoute.editCustomerScreen, + pathParameters: { + "id": customer.id, + }, + ); + }, + contentPadding: + EdgeInsets.symmetric( + horizontal: 5.w, + vertical: 0.5.h, + ), + title: Text( + customer.name, + style: TextStyle( + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + fontWeight: FontWeight.w500, + color: Colors.black87, + ), + ), + trailing: Icon( + Icons.chevron_right_rounded, + size: 5.w, + color: Colors.grey[400], + ), + ), + Divider( + height: 1, + thickness: 1, + indent: 5.w, + color: Colors.grey[100], + ), + ], + ), + ); + }), + ], + ); + }, + ); + } + }, + error: (error, stackTrace) { + return CustomErrorScreen( + message: "Ups, terjadi kesalahan", + onRefresh: () {}, + ); + }, + loading: () => const CustomerShimmer(), + ), + ), + ], + ), + ), + ), + ); + }, + ); + } +} diff --git a/Mobile Operasional/lib/features/customer/presentation/screens/edit_customer_screen.dart b/Mobile Operasional/lib/features/customer/presentation/screens/edit_customer_screen.dart new file mode 100644 index 0000000..de23a78 --- /dev/null +++ b/Mobile Operasional/lib/features/customer/presentation/screens/edit_customer_screen.dart @@ -0,0 +1,953 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:geolocator/geolocator.dart'; +import 'package:go_router/go_router.dart'; +import 'package:niogu_app/core/components/top_back_bar_app.dart'; +import 'package:niogu_app/core/constants/app_color.dart'; +import 'package:niogu_app/core/constants/app_font_size.dart'; +import 'package:niogu_app/core/constants/app_temp.dart'; +import 'package:niogu_app/core/router/app_route.dart'; +import 'package:niogu_app/core/enums/customer_source.dart'; +import 'package:niogu_app/core/utils/log_message.dart'; +import 'package:niogu_app/core/widgets/custom_snackbar.dart'; +import 'package:niogu_app/core/widgets/custom_text_form_field.dart'; +import 'package:niogu_app/features/customer/domain/entities/customer.dart'; +import 'package:niogu_app/features/customer/presentation/providers/customer_provider.dart'; +import 'package:niogu_app/features/customer/presentation/widgets/customer_activity.dart'; +import 'package:niogu_app/features/customer/presentation/widgets/edit_customer_shimmer.dart'; +import 'package:sizer/sizer.dart'; + +class EditCustomerScreen extends ConsumerStatefulWidget { + final String customerId; + + const EditCustomerScreen({super.key, required this.customerId}); + + @override + ConsumerState createState() => _EditCustomerScreenState(); +} + +class _EditCustomerScreenState extends ConsumerState { + final GlobalKey _formKey = GlobalKey(); + + final TextEditingController _nameController = TextEditingController(); + + final TextEditingController _emailController = TextEditingController(); + + final TextEditingController _phoneController = TextEditingController(); + + bool _isOnlineCustomer = false; + + bool _isLoading = true; + + @override + void initState() { + super.initState(); + + WidgetsBinding.instance.addPostFrameCallback((_) { + Future.delayed(const Duration(milliseconds: 800), () { + _fetchCustomer(); + }); + }); + } + + @override + void dispose() { + // TODO: implement dispose + _nameController.dispose(); + _emailController.dispose(); + _phoneController.dispose(); + super.dispose(); + } + + Future _fetchCustomer() async { + try { + final customerRepository = ref.read(customerRepositoryProvider); + + final customer = await customerRepository.fetchCustomerById( + widget.customerId, + ); + + if (!mounted) return; + + _nameController.text = customer.name; + + _emailController.text = customer.email; + + _phoneController.text = customer.phoneNumber; + + _isOnlineCustomer = customer.customerSource == CustomerSource.online; + + setState(() { + _isLoading = false; + }); + } catch (e, st) { + if (!mounted) return; + + setState(() { + _isLoading = false; + }); + + LogMessage.log.e(e.toString(), error: e, stackTrace: st); + CustomSnackbar.showError(context, "Ups, terjadi kesalahan"); + context.pop(); + } + } + + String calculateDistance( + double startLat, + double startLng, + double endLat, + double endLng, + ) { + double distanceInMeters = Geolocator.distanceBetween( + startLat, + startLng, + endLat, + endLng, + ); + + if (distanceInMeters >= 1000) { + return "${(distanceInMeters / 1000).toStringAsFixed(2)} km"; + } else { + return "${distanceInMeters.toStringAsFixed(0)} m"; + } + } + + Future _updateCustomer() async { + if (!_formKey.currentState!.validate()) return; + try { + await ref + .read(customerControllerProvider.notifier) + .saveCustomer( + UpsertCustomer( + id: widget.customerId, + name: _nameController.text.trim(), + email: _emailController.text.trim(), + phoneNumber: _phoneController.text.trim(), + ), + ); + + if (!mounted) return; + + CustomSnackbar.showSuccess(context, "Pelanggan berhasil diubah"); + context.pop(); + } catch (e, st) { + LogMessage.log.e(e.toString(), error: e, stackTrace: st); + CustomSnackbar.showError(context, "Ups, terjadi kesalahan"); + context.pop(); + } + } + + Future _deleteCustomer() async { + try { + ref + .read(customerControllerProvider.notifier) + .deleteCustomer(widget.customerId); + + if (!mounted) return; + + CustomSnackbar.showSuccess(context, "Pelanggan berhasil dihapus"); + Navigator.pop(context); + context.pop(); + } catch (e, st) { + LogMessage.log.e(e.toString(), error: e, stackTrace: st); + CustomSnackbar.showError(context, "Ups, terjadi kesalahan"); + Navigator.pop(context); + context.pop(); + } + } + + void _showDeleteConfirmation(BuildContext context) { + final bool isTablet = 100.w >= 600; + final customerControllerState = ref.watch(customerControllerProvider); + showDialog( + context: context, + builder: (BuildContext context) { + return Dialog( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(4.5.w), + ), + elevation: 0, + backgroundColor: Colors.transparent, + child: Container( + padding: EdgeInsets.all(6.w), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(4.5.w), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + padding: EdgeInsets.all(4.w), + decoration: BoxDecoration( + color: Colors.red[50], + shape: BoxShape.circle, + ), + child: Icon( + Icons.warning_amber_rounded, + color: Colors.red, + size: 10.w, + ), + ), + SizedBox(height: 2.h), + + Text( + "Hapus Pelanggan?", + style: TextStyle( + fontSize: AppFontSize.medium.sp, + fontWeight: FontWeight.bold, + color: Colors.black87, + ), + ), + SizedBox(height: 1.h), + Text( + "Tindakan ini tidak dapat dibatalkan. Semua data profil pelanggan akan dihapus dari sistem.", + textAlign: TextAlign.center, + style: TextStyle( + fontSize: isTablet + ? (AppFontSize.medium - 1.25).sp + : (AppFontSize.small - 1.25).sp, + color: Colors.grey[600], + height: 1.5, + ), + ), + SizedBox(height: 3.h), + + Row( + children: [ + Expanded( + child: ElevatedButton( + onPressed: () => Navigator.pop(context), + style: ElevatedButton.styleFrom( + backgroundColor: Colors.grey.shade300, + padding: EdgeInsets.symmetric(vertical: 1.5.h), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(2.5.w), + ), + elevation: 0, + ), + child: Text( + "Batal", + style: TextStyle( + color: Colors.grey[800], + fontWeight: FontWeight.bold, + fontSize: AppFontSize.medium.sp, + ), + ), + ), + ), + SizedBox(width: 3.w), + Expanded( + child: ElevatedButton( + onPressed: customerControllerState.isLoading + ? null + : _deleteCustomer, + style: ElevatedButton.styleFrom( + padding: EdgeInsets.symmetric(vertical: 1.5.h), + backgroundColor: Colors.red, + elevation: 0, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(2.5.w), + ), + disabledBackgroundColor: Colors.grey.shade300, + ), + child: Text( + "Ya, Hapus", + style: TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + fontSize: AppFontSize.medium.sp, + ), + ), + ), + ), + ], + ), + ], + ), + ), + ); + }, + ); + } + + @override + Widget build(BuildContext context) { + return LayoutBuilder( + builder: (context, constraints) { + final bool isTablet = 100.w >= 600; + + final customerAddressesState = ref.watch( + customerAddressesProvider(widget.customerId), + ); + + final customerActivitiesState = ref.watch( + customerActivitiesProvider(widget.customerId), + ); + + final customerControllerState = ref.watch(customerControllerProvider); + + return SafeArea( + top: false, + bottom: true, + right: false, + left: false, + child: Scaffold( + backgroundColor: Colors.white, + appBar: TopBackBarApp( + title: "Detail Pelanggan", + onTap: () => context.pop(), + ), + body: _isLoading + ? const EditCustomerShimmer() + : SingleChildScrollView( + physics: const AlwaysScrollableScrollPhysics(), + child: Form( + key: _formKey, + child: Column( + children: [ + Container( + width: double.infinity, + color: Colors.white, + padding: EdgeInsets.symmetric(vertical: 3.h), + child: Column( + children: [ + Stack( + alignment: Alignment.bottomRight, + children: [ + CircleAvatar( + radius: 12.w, + backgroundColor: Colors.grey[200], + child: Icon( + Icons.person, + size: 15.w, + color: Colors.grey[400], + ), + ), + Container( + padding: EdgeInsets.all(1.5.w), + decoration: BoxDecoration( + color: _isOnlineCustomer + ? Colors.blue + : Colors.orange, + shape: BoxShape.circle, + border: Border.all( + color: Colors.white, + width: 2, + ), + ), + child: Icon( + _isOnlineCustomer + ? Icons.language + : Icons.storefront, + size: 5.w, + color: Colors.white, + ), + ), + ], + ), + SizedBox(height: 1.5.h), + Container( + padding: EdgeInsets.symmetric( + horizontal: 3.w, + vertical: 0.5.h, + ), + decoration: BoxDecoration( + color: _isOnlineCustomer + ? Colors.blue[50] + : Colors.orange[50], + borderRadius: BorderRadius.circular(4.5.w), + ), + child: Text( + _isOnlineCustomer + ? "Pelanggan Toko Online" + : "Pelanggan Offline", + style: TextStyle( + fontSize: isTablet + ? (AppFontSize.medium - 1.25).sp + : (AppFontSize.small - 1.25).sp, + fontWeight: FontWeight.bold, + color: _isOnlineCustomer + ? Colors.blue[700] + : Colors.orange[700], + ), + ), + ), + ], + ), + ), + + Container( + color: Colors.white, + padding: EdgeInsets.fromLTRB(5.w, 0, 5.w, 3.h), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + CustomTextFormField( + label: "Nama Pelanggan", + controller: _nameController, + hint: "Masukkan nama", + validator: (value) { + if (value == null || value.isEmpty) { + return "Nama pelanggan belum diisi"; + } + + return null; + }, + prefixIcon: Icons.person_outline, + readOnly: _isOnlineCustomer, + ), + + SizedBox(height: 2.h), + CustomTextFormField( + label: "Email", + controller: _emailController, + hint: "Masukkan email (opsional)", + keyboardType: TextInputType.emailAddress, + prefixIcon: Icons.email_outlined, + readOnly: _isOnlineCustomer, + ), + SizedBox(height: 2.h), + CustomTextFormField( + label: "No. Handphone / WA", + controller: _phoneController, + hint: "Masukkan no handphone / wa (opsional)", + keyboardType: TextInputType.phone, + prefixIcon: Icons.phone_android_outlined, + readOnly: _isOnlineCustomer, + ), + ], + ), + ), + + _buildSectionHeader("Alamat Pelanggan"), + + customerAddressesState.when( + data: (addresses) { + final int displayAddressCount = + addresses.length > 3 ? 3 : addresses.length; + + final bool hasMore = addresses.length > 3; + + return addresses.isEmpty + ? _buildEmptyOrErrorAddress( + Icons.location_off_outlined, + "Tidak ada alamat yang tersedia", + ) + : SizedBox( + height: 22.5.h, + child: ListView.builder( + padding: EdgeInsets.only( + left: 5.w, + right: 2.w, + ), + scrollDirection: Axis.horizontal, + itemCount: + displayAddressCount + + (hasMore ? 1 : 0), + itemBuilder: (context, index) { + if (index < displayAddressCount) { + final address = addresses[index]; + final String distance = + calculateDistance( + AppTemp.OUTLET_LATITUDE, + AppTemp.OUTLET_LONGITUDE, + address.coordinate.latitude, + address.coordinate.longitude, + ); + return Container( + width: 75.w, + margin: EdgeInsets.only( + right: 3.w, + ), + padding: EdgeInsets.all(3.w), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: + BorderRadius.circular( + 2.5.w, + ), + border: Border.all( + color: Colors.grey[200]!, + ), + ), + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Row( + children: [ + Container( + padding: EdgeInsets.all( + 2.w, + ), + decoration: + BoxDecoration( + color: AppColor + .primaryColor + .withOpacity( + 0.1, + ), + shape: BoxShape + .circle, + ), + child: Icon( + Icons.location_on, + color: AppColor + .primaryColor, + size: 5.w, + ), + ), + + SizedBox(width: 3.w), + Text( + address.label, + style: TextStyle( + fontSize: isTablet + ? (AppFontSize.medium - + 1.25) + .sp + : (AppFontSize.small - + 1.25) + .sp, + color: + Colors.grey[700], + ), + ), + ], + ), + SizedBox(height: 1.5.h), + + Text( + address.fullAddress, + overflow: + TextOverflow.ellipsis, + style: TextStyle( + fontSize: isTablet + ? (AppFontSize.medium - + 1.25) + .sp + : (AppFontSize.small - + 1.25) + .sp, + color: Colors.grey[700], + height: 1.4, + ), + ), + Divider( + color: Colors.grey[100], + ), + Row( + mainAxisAlignment: + MainAxisAlignment + .spaceBetween, + children: [ + Row( + children: [ + Icon( + Icons + .straighten_rounded, + size: 5.w, + color: Colors.grey, + ), + SizedBox(width: 3.w), + Text( + "$distance", + style: TextStyle( + fontSize: isTablet + ? (AppFontSize.medium - + 1.25) + .sp + : (AppFontSize.small - + 1.25) + .sp, + color: + Colors.black, + fontWeight: + FontWeight + .bold, + ), + ), + ], + ), + + TextButton.icon( + onPressed: () { + context.pushNamed( + AppRoute + .mapCustomerAddressScreen, + extra: address, + ); + }, + icon: Icon( + Icons.map_outlined, + color: AppColor + .primaryColor, + size: 5.w, + ), + label: Text( + "Lihat di Peta", + style: TextStyle( + color: AppColor + .primaryColor, + fontSize: + AppFontSize + .small + .sp, + fontWeight: + FontWeight.bold, + ), + ), + ), + ], + ), + ], + ), + ); + } else { + return GestureDetector( + onTap: () async { + FocusScope.of( + context, + ).unfocus(); + + await Future.delayed( + const Duration( + milliseconds: 400, + ), + ); + + context.pushNamed( + AppRoute + .customerAddressesScreen, + extra: addresses, + ); + }, + child: Container( + width: 35.w, + margin: EdgeInsets.only( + right: 5.w, + ), + decoration: BoxDecoration( + color: AppColor.primaryColor + .withOpacity(0.05), + borderRadius: + BorderRadius.circular( + 2.5.w, + ), + border: Border.all( + color: AppColor.primaryColor + .withOpacity(0.2), + ), + ), + child: Column( + mainAxisAlignment: + MainAxisAlignment.center, + children: [ + Icon( + Icons + .arrow_circle_right_outlined, + color: + AppColor.primaryColor, + size: 8.w, + ), + SizedBox(height: 0.5.h), + Text( + "${addresses.length - 3} Alamat\nLainnya", + textAlign: + TextAlign.center, + style: TextStyle( + color: AppColor + .primaryColor, + fontWeight: + FontWeight.bold, + fontSize: isTablet + ? (AppFontSize.medium - + 1.25) + .sp + : (AppFontSize.small - + 1.25) + .sp, + ), + ), + ], + ), + ), + ); + } + }, + ), + ); + }, + error: (error, stackTrace) => + _buildEmptyOrErrorAddress( + Icons.error_rounded, + "Ups, terjadi kesalahan", + ), + loading: () => const SizedBox(), + ), + + _buildSectionHeader("Aktivitas Pelanggan"), + + customerActivitiesState.when( + data: (activities) { + final int totalActivity = activities.length; + + final int displayActivityCount = totalActivity > 3 + ? 3 + : totalActivity; + + return activities.isEmpty + ? _buildEmptyOrErrorActivities( + Icons.history_rounded, + "Belum ada riwayat aktivitas pembelian", + ) + : Column( + children: [ + ListView.builder( + shrinkWrap: true, + physics: + const NeverScrollableScrollPhysics(), + padding: EdgeInsets.symmetric( + horizontal: 5.w, + ), + itemCount: displayActivityCount, + itemBuilder: (context, index) { + final activity = activities[index]; + + return CustomerActivity( + isOnlineCustomer: + _isOnlineCustomer, + activity: activity, + ); + }, + ), + + if (totalActivity > 3) + Padding( + padding: EdgeInsets.symmetric( + horizontal: 5.w, + ), + child: InkWell( + onTap: () { + context.pushNamed( + AppRoute + .customerActivitesScreen, + extra: { + "is_online_customer": + _isOnlineCustomer, + "activities": activities, + }, + ); + }, + borderRadius: + BorderRadius.circular(2.5.w), + child: Container( + padding: EdgeInsets.symmetric( + vertical: 1.5.h, + ), + decoration: BoxDecoration( + border: Border.all( + color: Colors.grey[300]!, + ), + borderRadius: + BorderRadius.circular( + 2.5.w, + ), + ), + child: Row( + mainAxisAlignment: + MainAxisAlignment.center, + children: [ + Text( + "Lihat ${totalActivity - 3} Aktivitas Lainnya", + style: TextStyle( + color: Colors.grey[600], + fontWeight: + FontWeight.bold, + fontSize: isTablet + ? (AppFontSize.medium - + 1.25) + .sp + : (AppFontSize.small - + 1.25) + .sp, + ), + ), + SizedBox(width: 2.w), + Icon( + Icons + .keyboard_arrow_down_rounded, + color: Colors.grey[600], + size: 5.w, + ), + ], + ), + ), + ), + ), + ], + ); + }, + error: (error, stackTrace) => + _buildEmptyOrErrorActivities( + Icons.error_rounded, + "Ups, terjadi kesalahan", + ), + loading: () => const SizedBox(), + ), + + if (!_isOnlineCustomer) ...[ + SizedBox(height: 5.h), + Padding( + padding: EdgeInsets.symmetric(horizontal: 5.w), + child: TextButton.icon( + onPressed: () => + _showDeleteConfirmation(context), + style: TextButton.styleFrom( + foregroundColor: Colors.red[700], + padding: EdgeInsets.symmetric( + vertical: 1.5.h, + horizontal: 4.w, + ), + ), + icon: Icon( + Icons.delete_outline_rounded, + size: 5.w, + ), + label: Text( + "Hapus Pelanggan Ini", + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: isTablet + ? (AppFontSize.medium - 1.25).sp + : (AppFontSize.small - 1.25).sp, + ), + ), + ), + ), + ], + SizedBox(height: 4.h), + ], + ), + ), + ), + + bottomNavigationBar: _isLoading + ? null + : Container( + padding: EdgeInsets.all(5.w), + decoration: BoxDecoration( + color: Colors.white, + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.05), + blurRadius: 10, + offset: const Offset(0, -5), + ), + ], + ), + child: ElevatedButton( + onPressed: + _isOnlineCustomer || customerControllerState.isLoading + ? null + : _updateCustomer, + style: ElevatedButton.styleFrom( + backgroundColor: AppColor.primaryColor, + minimumSize: Size(double.infinity, 6.5.h), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(2.5.w), + ), + disabledBackgroundColor: Colors.grey.shade300, + ), + child: Text( + "Perbarui Informasi Pelanggan", + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: AppFontSize.medium.sp, + color: Colors.white, + ), + ), + ), + ), + ), + ); + }, + ); + } + + Widget _buildSectionHeader(String title) { + return Padding( + padding: EdgeInsets.fromLTRB(5.w, 3.h, 5.w, 1.5.h), + child: Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Text( + title, + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: (AppFontSize.medium - 1.25).sp, + ), + ), + ], + ), + ); + } + + Widget _buildEmptyOrErrorAddress(IconData icon, String body) { + return Container( + height: 15.h, + width: double.infinity, + margin: EdgeInsets.symmetric(horizontal: 5.w), + decoration: BoxDecoration( + color: Colors.grey[50], + borderRadius: BorderRadius.circular(2.5.w), + border: Border.all( + color: Colors.grey.shade200, + style: BorderStyle.solid, + ), + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(icon, color: Colors.grey[400], size: 8.w), + SizedBox(height: 1.h), + Text( + body, + style: TextStyle( + fontSize: (AppFontSize.medium - 1.25).sp, + color: Colors.grey[500], + ), + ), + ], + ), + ); + } + + Widget _buildEmptyOrErrorActivities(IconData icon, String body) { + return Container( + width: double.infinity, + padding: EdgeInsets.symmetric(vertical: 4.h), + margin: EdgeInsets.symmetric(horizontal: 5.w), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(2.5.w), + border: Border.all(color: Colors.grey.shade100), + ), + child: Column( + children: [ + Icon(icon, color: Colors.grey[300], size: 10.w), + SizedBox(height: 1.5.h), + Text( + body, + style: TextStyle( + fontSize: (AppFontSize.medium - 1.25).sp, + color: Colors.grey[500], + ), + ), + ], + ), + ); + } +} diff --git a/Mobile Operasional/lib/features/customer/presentation/screens/map_customer_address_screen.dart b/Mobile Operasional/lib/features/customer/presentation/screens/map_customer_address_screen.dart new file mode 100644 index 0000000..3db1c73 --- /dev/null +++ b/Mobile Operasional/lib/features/customer/presentation/screens/map_customer_address_screen.dart @@ -0,0 +1,208 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_map/flutter_map.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; +import 'package:niogu_app/core/components/top_back_bar_app.dart'; +import 'package:niogu_app/core/constants/app_font_size.dart'; +import 'package:niogu_app/core/constants/app_color.dart'; +import 'package:niogu_app/core/widgets/triangle_painter.dart'; +import 'package:niogu_app/features/customer/domain/entities/customer.dart'; +import 'package:sizer/sizer.dart'; + +class MapCustomerAddressScreen extends ConsumerStatefulWidget { + final CustomerAddress address; + + const MapCustomerAddressScreen({super.key, required this.address}); + + @override + ConsumerState createState() => + _MapCustomerAddressScreenState(); +} + +class _MapCustomerAddressScreenState + extends ConsumerState { + final MapController _mapController = MapController(); + + @override + void initState() { + // TODO: implement initState + super.initState(); + } + + @override + void dispose() { + // TODO: implement dispose + _mapController.dispose(); + super.dispose(); + } + + void _goToLocation() { + _mapController.move(widget.address.coordinate, 16.0); + } + + void _zoomIn() { + _mapController.move( + _mapController.camera.center, + _mapController.camera.zoom + 1, + ); + } + + void _zoomOut() { + _mapController.move( + _mapController.camera.center, + _mapController.camera.zoom - 1, + ); + } + + @override + Widget build(BuildContext context) { + return LayoutBuilder( + builder: (context, constraints) { + return SafeArea( + top: false, + bottom: true, + right: false, + left: false, + child: Scaffold( + extendBodyBehindAppBar: true, + appBar: TopBackBarApp( + title: widget.address.label, + onTap: () => context.pop(), + ), + body: Stack( + children: [ + FlutterMap( + mapController: _mapController, + options: MapOptions( + initialCenter: widget.address.coordinate, + initialZoom: 16.0, + minZoom: 3.0, + maxZoom: 18.0, + ), + children: [ + TileLayer( + urlTemplate: + 'https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png', + subdomains: const ['a', 'b', 'c', 'd'], + userAgentPackageName: 'com.example.niogu_app', + ), + MarkerLayer( + markers: [ + Marker( + point: widget.address.coordinate, + width: 60.w, + height: 25.h, + alignment: Alignment.topCenter, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + padding: EdgeInsets.symmetric( + horizontal: 3.w, + vertical: 1.2.h, + ), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(2.w), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.1), + blurRadius: 8, + offset: const Offset(0, 4), + ), + ], + border: Border.all( + color: Colors.grey.shade200, + ), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + "Lokasi Pelanggan", + textAlign: TextAlign.center, + style: TextStyle( + fontSize: AppFontSize.small.sp, + fontWeight: FontWeight.bold, + color: AppColor.primaryColor, + ), + ), + SizedBox(height: 0.5.h), + Text( + widget.address.fullAddress, + textAlign: TextAlign.center, + maxLines: 3, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontSize: (AppFontSize.small - 1.25).sp, + color: Colors.grey[700], + fontWeight: FontWeight.w500, + ), + ), + ], + ), + ), + CustomPaint( + size: Size(5.w, 2.5.w), + painter: TrianglePainter(Colors.white), + ), + SizedBox(height: 0.5.h), + Icon( + Icons.location_on, + color: Colors.red, + size: 10.w, + ), + ], + ), + ), + ], + ), + ], + ), + + Positioned( + bottom: 4.h, + right: 5.w, + child: Column( + children: [ + _buildFloatingButton( + icon: Icons.my_location_rounded, + iconColor: AppColor.primaryColor, + onTap: _goToLocation, + ), + SizedBox(height: 2.h), + _buildFloatingButton(icon: Icons.add, onTap: _zoomIn), + SizedBox(height: 1.h), + _buildFloatingButton(icon: Icons.remove, onTap: _zoomOut), + ], + ), + ), + ], + ), + ), + ); + }, + ); + } + + Widget _buildFloatingButton({ + required IconData icon, + required VoidCallback onTap, + Color iconColor = Colors.black87, + }) { + return Material( + color: Colors.white, + elevation: 4, + borderRadius: BorderRadius.circular(2.w), + child: InkWell( + onTap: onTap, + borderRadius: BorderRadius.circular(2.w), + child: Container( + width: 12.w, + height: 12.w, + child: Icon(icon, color: iconColor, size: 6.w), + ), + ), + ); + } +} diff --git a/Mobile Operasional/lib/features/customer/presentation/widgets/customer_activity.dart b/Mobile Operasional/lib/features/customer/presentation/widgets/customer_activity.dart new file mode 100644 index 0000000..521e041 --- /dev/null +++ b/Mobile Operasional/lib/features/customer/presentation/widgets/customer_activity.dart @@ -0,0 +1,173 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; +import 'package:intl/intl.dart'; +import 'package:niogu_app/core/constants/app_color.dart'; +import 'package:niogu_app/core/constants/app_font_size.dart'; +import 'package:niogu_app/core/router/app_route.dart'; +import 'package:niogu_app/core/enums/order_source.dart'; +import 'package:niogu_app/core/enums/order_status.dart'; +import 'package:niogu_app/core/utils/time_zone.dart'; +import 'package:niogu_app/features/customer/domain/entities/customer.dart'; +import 'package:niogu_app/features/online_store/presentation/providers/online_store_provider.dart'; +import 'package:niogu_app/features/report/transaction/presentation/providers/transaction_report_provider.dart'; +import 'package:sizer/sizer.dart'; + +class CustomerActivity extends ConsumerWidget { + final bool isOnlineCustomer; + final CustomerActivities activity; + const CustomerActivity({ + super.key, + required this.isOnlineCustomer, + required this.activity, + }); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final bool isTablet = 100.w >= 600; + + final status = activity.orderStatus; + + final Color colorBadge = switch (status) { + OrderStatus.pending_payment => Colors.red.shade50, + OrderStatus.pending_confirmation => Colors.grey.shade50, + OrderStatus.confirmed => Colors.orange.shade50, + OrderStatus.in_progress => Colors.blue.shade50, + OrderStatus.completed => Colors.green.shade50, + OrderStatus.canceled => Colors.red.shade50, + }; + + final String orderStatus = switch (status) { + OrderStatus.pending_payment => "Verifikasi Pembayaran", + OrderStatus.pending_confirmation => "Tertunda", + OrderStatus.confirmed => "Dikonfirmasi", + OrderStatus.in_progress => "Pengiriman", + OrderStatus.completed => "Selesai", + OrderStatus.canceled => "Dibatalkan", + }; + + final Color colorStatus = switch (status) { + OrderStatus.pending_payment => Colors.red.shade700, + OrderStatus.pending_confirmation => Colors.grey.shade700, + OrderStatus.confirmed => Colors.orange.shade700, + OrderStatus.in_progress => Colors.blue.shade700, + OrderStatus.completed => Colors.green.shade700, + OrderStatus.canceled => Colors.red.shade700, + }; + + return GestureDetector( + onTap: () async { + if (activity.orderSource == OrderSource.pos) { + final orderDetail = await ref + .read(transactionReportRepositoryProvider) + .getOrderDetail(activity.id); + + FocusScope.of(context).unfocus(); + + await Future.delayed(const Duration(milliseconds: 800)); + + context.pushNamed( + AppRoute.transactionReportOrderDetailScreen, + extra: orderDetail, + ); + } else { + final onlineOrderDetail = await ref + .read(onlineStoreRepositoryProvider) + .fetchOnlineOrderDetailById(activity.id); + + FocusScope.of(context).unfocus(); + + await Future.delayed(const Duration(milliseconds: 800), () { + context.pushNamed( + AppRoute.onlineStoreOrderDetilScreen, + extra: onlineOrderDetail, + ); + }); + } + }, + child: Container( + margin: EdgeInsets.only(bottom: 1.5.h), + padding: EdgeInsets.all(4.w), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(2.5.w), + border: Border.all(color: Colors.grey[200]!), + ), + child: Row( + children: [ + Container( + padding: EdgeInsets.all(2.w), + decoration: BoxDecoration( + color: AppColor.primaryColor.withOpacity(0.1), + shape: BoxShape.circle, + ), + child: Icon( + Icons.shopping_bag_outlined, + color: AppColor.primaryColor, + size: 5.w, + ), + ), + SizedBox(width: 4.w), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + activity.orderNumber, + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: isTablet + ? (AppFontSize.medium - 1.25).sp + : (AppFontSize.small - 1.25).sp, + ), + ), + SizedBox(height: 0.75.h), + Text( + "Via ${isOnlineCustomer ? 'Toko Online' : 'Kasir Offline'}", + style: TextStyle( + fontSize: isTablet + ? (AppFontSize.medium - 1.25).sp + : (AppFontSize.small - 1.25).sp, + color: Colors.grey, + ), + ), + SizedBox(height: 0.75.h), + Container( + margin: EdgeInsets.only(top: 0.5.h), + padding: EdgeInsets.symmetric( + horizontal: 2.w, + vertical: 0.5.w, + ), + decoration: BoxDecoration( + color: colorBadge, + borderRadius: BorderRadius.circular(1.w), + ), + child: Text( + orderStatus, + style: TextStyle( + fontSize: isTablet + ? (AppFontSize.medium - 1.25).sp + : (AppFontSize.small - 1.25).sp, + fontWeight: FontWeight.bold, + color: colorStatus, + ), + ), + ), + ], + ), + ), + Text( + "${DateFormat("dd MMMM yyyy HH:mm").format(activity.orderDate)} ${TimeZone.getCurrentTimeZone()}", + style: TextStyle( + fontSize: isTablet + ? (AppFontSize.medium - 1.25).sp + : (AppFontSize.small - 1.25).sp, + color: Colors.grey, + ), + ), + ], + ), + ), + ); + } +} diff --git a/Mobile Operasional/lib/features/customer/presentation/widgets/customer_shimmer.dart b/Mobile Operasional/lib/features/customer/presentation/widgets/customer_shimmer.dart new file mode 100644 index 0000000..3459259 --- /dev/null +++ b/Mobile Operasional/lib/features/customer/presentation/widgets/customer_shimmer.dart @@ -0,0 +1,72 @@ +import 'package:flutter/material.dart'; +import 'package:shimmer/shimmer.dart'; +import 'package:sizer/sizer.dart'; + +class CustomerShimmer extends StatelessWidget { + const CustomerShimmer({super.key}); + + @override + Widget build(BuildContext context) { + return Shimmer.fromColors( + baseColor: Colors.grey.shade300, + highlightColor: Colors.grey.shade100, + enabled: true, + child: ListView.builder( + physics: const NeverScrollableScrollPhysics(), + itemCount: 3, + itemBuilder: (_, index) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + width: double.infinity, + height: 4.5.h, + color: Colors.white, + margin: EdgeInsets.only(bottom: 1.h), + ), + + Column( + children: List.generate( + 3, + (i) => Column( + children: [ + ListTile( + contentPadding: EdgeInsets.symmetric( + horizontal: 5.w, + vertical: 0.5.h, + ), + title: Container( + width: 40.w, + height: 1.5.h, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(1.w), + ), + ), + trailing: Container( + width: 5.w, + height: 5.w, + decoration: const BoxDecoration( + color: Colors.white, + shape: BoxShape.circle, + ), + ), + ), + Divider( + height: 1, + thickness: 1, + indent: 5.w, + color: Colors.white, + ), + ], + ), + ), + ), + SizedBox(height: 1.h), + ], + ); + }, + ), + ); + } +} diff --git a/Mobile Operasional/lib/features/customer/presentation/widgets/edit_customer_shimmer.dart b/Mobile Operasional/lib/features/customer/presentation/widgets/edit_customer_shimmer.dart new file mode 100644 index 0000000..f8e2ba6 --- /dev/null +++ b/Mobile Operasional/lib/features/customer/presentation/widgets/edit_customer_shimmer.dart @@ -0,0 +1,128 @@ +import 'package:flutter/material.dart'; +import 'package:shimmer/shimmer.dart'; +import 'package:sizer/sizer.dart'; + +class EditCustomerShimmer extends StatelessWidget { + const EditCustomerShimmer({super.key}); + + @override + Widget build(BuildContext context) { + return Shimmer.fromColors( + baseColor: Colors.grey.shade300, + highlightColor: Colors.grey.shade100, + child: Column( + children: [ + Expanded( + child: SingleChildScrollView( + physics: const NeverScrollableScrollPhysics(), + child: Column( + children: [ + Container( + width: double.infinity, + padding: EdgeInsets.symmetric(vertical: 3.h), + child: Column( + children: [ + Container( + width: 24.w, + height: 24.w, + decoration: const BoxDecoration( + color: Colors.white, + shape: BoxShape.circle, + ), + ), + SizedBox(height: 2.h), + Container( + width: 40.w, + height: 3.h, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(4.5.w), + ), + ), + ], + ), + ), + + Padding( + padding: EdgeInsets.symmetric(horizontal: 5.w), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: List.generate( + 3, + (index) => Padding( + padding: EdgeInsets.only(bottom: 2.h), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + width: 30.w, + height: 1.5.h, + color: Colors.white, + ), + SizedBox(height: 1.h), + Container( + width: double.infinity, + height: 7.h, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(2.5.w), + ), + ), + ], + ), + ), + ), + ), + ), + + _buildSectionLabelShimmer("Alamat Pelanggan"), + SizedBox( + height: 20.h, + child: ListView.builder( + scrollDirection: Axis.horizontal, + padding: EdgeInsets.only(left: 5.w), + itemCount: 2, + itemBuilder: (_, __) => Container( + width: 75.w, + margin: EdgeInsets.only(right: 3.w), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(2.5.w), + ), + ), + ), + ), + + _buildSectionLabelShimmer("Aktivitas Pelanggan"), + Column( + children: List.generate( + 2, + (index) => Container( + height: 12.h, + margin: EdgeInsets.fromLTRB(5.w, 0, 5.w, 1.5.h), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(2.5.w), + ), + ), + ), + ), + ], + ), + ), + ), + ], + ), + ); + } + + Widget _buildSectionLabelShimmer(String title) { + return Padding( + padding: EdgeInsets.fromLTRB(5.w, 3.h, 5.w, 1.5.h), + child: Align( + alignment: Alignment.centerLeft, + child: Container(width: 40.w, height: 2.h, color: Colors.white), + ), + ); + } +} diff --git a/Mobile Operasional/lib/features/goods/domain/entities/outlet_inventory.dart b/Mobile Operasional/lib/features/goods/domain/entities/outlet_inventory.dart new file mode 100644 index 0000000..ea1a140 --- /dev/null +++ b/Mobile Operasional/lib/features/goods/domain/entities/outlet_inventory.dart @@ -0,0 +1,30 @@ +import 'package:niogu_app/core/enums/item_type.dart'; +import 'package:niogu_app/core/enums/stock_type.dart'; +import 'package:niogu_app/core/enums/sync_status.dart'; +import 'package:uuid/uuid.dart'; + +class UpsertOutletInventory { + final String localId; + final String outletId; + final ItemType itemType; + final String itemId; + final StockType stockType; + final double stock; + final double sellingPrice; + final double costPrice; + final double purchasePrice; + final SyncStatus syncStatus; + + UpsertOutletInventory({ + String? localId, + required this.outletId, + required this.itemType, + required this.itemId, + required this.stockType, + required this.stock, + required this.costPrice, + required this.sellingPrice, + required this.purchasePrice, + this.syncStatus = SyncStatus.dirty, + }) : localId = localId ?? const Uuid().v7().toString(); +} \ No newline at end of file diff --git a/Mobile Operasional/lib/features/goods/domain/entities/stock_card.dart b/Mobile Operasional/lib/features/goods/domain/entities/stock_card.dart new file mode 100644 index 0000000..2616f92 --- /dev/null +++ b/Mobile Operasional/lib/features/goods/domain/entities/stock_card.dart @@ -0,0 +1,40 @@ +import 'package:niogu_app/core/enums/stock_card_type.dart'; +import 'package:niogu_app/core/enums/stock_type.dart'; +import 'package:niogu_app/core/enums/sync_status.dart'; +import 'package:uuid/uuid.dart'; + +class StockCard { + final String localId; + final String outletInventoryId; + final String userId; + final StockCardType stockCardType; + final double qtyChange; + final double currentStock; + final StockType stockType; + final bool modeChange; + final String? modeBeforeChange; + final String? modeAfterChange; + final double costPrice; + final String? referenceId; + final String? documentNumber; + final String? notes; + final SyncStatus syncStatus; + + StockCard({ + String? localId, + required this.outletInventoryId, + required this.userId, + required this.stockCardType, + required this.qtyChange, + required this.currentStock, + required this.costPrice, + required this.stockType, + this.modeChange = false, + this.modeBeforeChange, + this.modeAfterChange, + this.referenceId, + this.documentNumber, + this.notes, + this.syncStatus = SyncStatus.dirty + }) : localId = localId ?? Uuid().v7().toString(); +} \ No newline at end of file diff --git a/Mobile Operasional/lib/features/goods/presentation/screens/good_choice_screen.dart b/Mobile Operasional/lib/features/goods/presentation/screens/good_choice_screen.dart new file mode 100644 index 0000000..c3e2f2c --- /dev/null +++ b/Mobile Operasional/lib/features/goods/presentation/screens/good_choice_screen.dart @@ -0,0 +1,140 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; +import 'package:niogu_app/core/components/app_bar_with_tab.dart'; +import 'package:niogu_app/core/providers/app_provider.dart'; +import 'package:niogu_app/core/enums/user_role.dart'; +import 'package:niogu_app/core/widgets/pop_up_notification.dart'; +import 'package:niogu_app/features/goods/products/presentation/providers/product_provider.dart'; +import 'package:niogu_app/features/goods/products/presentation/screens/product_choice_screen.dart'; +import 'package:niogu_app/features/goods/raw_materials/presentation/providers/raw_material_provider.dart'; +import 'package:niogu_app/features/goods/raw_materials/presentation/screens/raw_material_choice_screen.dart'; +import 'package:sizer/sizer.dart'; + +class GoodChoiceScreen extends ConsumerStatefulWidget { + const GoodChoiceScreen({super.key}); + + @override + ConsumerState createState() => _GoodChoiceScreenState(); +} + +class _GoodChoiceScreenState extends ConsumerState + with SingleTickerProviderStateMixin { + late TabController _tabController; + + final FocusNode _searchFocusNode = FocusNode(); + + Color _searchIconColor = Colors.grey; + + Timer? _debounce; + + int _tabIndex = 0; + + @override + void initState() { + super.initState(); + + _tabController = TabController(length: 2, vsync: this); + + _tabController.addListener(_handleTabSelection); + + _searchFocusNode.addListener(() { + setState(() { + _searchIconColor = _searchFocusNode.hasFocus + ? Colors.black + : Colors.grey; + }); + }); + } + + @override + void dispose() { + _searchFocusNode.dispose(); + _tabController.removeListener(_handleTabSelection); + _tabController.dispose(); + _debounce?.cancel(); + super.dispose(); + } + + void _handleTabSelection() { + if (_tabController.index != _tabIndex) { + setState(() { + _tabIndex = _tabController.index; + }); + } + } + + void _onSearchChanged(String value) { + if (_debounce?.isActive ?? false) _debounce?.cancel(); + _debounce = Timer(const Duration(milliseconds: 800), () { + if (_tabIndex == 0) { + ref.read(productChoiceSearchProvider.notifier).state = value; + } else { + ref.read(rawMaterialChoiceSearchProvider.notifier).state = value; + } + }); + } + + @override + Widget build(BuildContext context) { + return LayoutBuilder( + builder: (context, constraints) { + final messageActivity = ref.watch(messageActivityProvider); + + final currentUserRole = ref.watch(currentUserRoleProvider); + + return SafeArea( + top: false, + bottom: true, + right: false, + left: false, + child: Stack( + children: [ + Scaffold( + backgroundColor: Colors.white, + resizeToAvoidBottomInset: false, + appBar: AppBarWithTab( + hintText: + "Cari nama ${_tabIndex == 0 ? 'produk' : 'bahan baku'}...", + searchIconColor: _searchIconColor, + canPop: true, + onPop: () => context.pop(), + showSwitchOutlet: currentUserRole == UserRole.owner, + searchFocusNode: _searchFocusNode, + tabController: _tabController, + onTap: (value) { + setState(() { + _tabIndex = value; + }); + }, + onSearchChanged: (value) => _onSearchChanged(value), + tabs: [ + Tab(text: "Produk", height: 10.h), + Tab(text: "Bahan Baku", height: 10.h), + ], + ), + + body: TabBarView( + controller: _tabController, + children: const [ + const ProductChoiceScreen(), + const RawMaterialChoiceScreen(), + ], + ), + ), + + if (messageActivity != null) + PopupNotification( + isOwner: currentUserRole == UserRole.owner, + messages: messageActivity.messages, + type: messageActivity.type, + ), + ], + ), + ); + }, + ); + } +} diff --git a/Mobile Operasional/lib/features/goods/presentation/screens/good_screen.dart b/Mobile Operasional/lib/features/goods/presentation/screens/good_screen.dart new file mode 100644 index 0000000..43d1b16 --- /dev/null +++ b/Mobile Operasional/lib/features/goods/presentation/screens/good_screen.dart @@ -0,0 +1,120 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:niogu_app/core/components/app_bar_with_tab.dart'; +import 'package:niogu_app/core/providers/app_provider.dart'; +import 'package:niogu_app/core/enums/user_role.dart'; +import 'package:niogu_app/features/goods/products/presentation/providers/product_provider.dart'; +import 'package:niogu_app/features/goods/products/presentation/screens/product_screen.dart'; +import 'package:niogu_app/features/goods/raw_materials/presentation/providers/raw_material_provider.dart'; +import 'package:niogu_app/features/goods/raw_materials/presentation/screens/raw_material_screen.dart'; +import 'package:sizer/sizer.dart'; + +class GoodScreen extends ConsumerStatefulWidget { + const GoodScreen({super.key}); + + @override + ConsumerState createState() => _GoodScreenState(); +} + +class _GoodScreenState extends ConsumerState + with SingleTickerProviderStateMixin { + late TabController _tabController; + + final FocusNode _searchFocusNode = FocusNode(); + + Color _searchIconColor = Colors.grey; + + Timer? _debounce; + + int _tabIndex = 0; + + @override + void initState() { + super.initState(); + + _tabController = TabController(length: 2, vsync: this); + + _tabController.addListener(_handleTabSelection); + + _searchFocusNode.addListener(() { + setState(() { + _searchIconColor = _searchFocusNode.hasFocus + ? Colors.black + : Colors.grey; + }); + }); + } + + @override + void dispose() { + _searchFocusNode.dispose(); + _tabController.removeListener(_handleTabSelection); + _tabController.dispose(); + _debounce?.cancel(); + super.dispose(); + } + + void _handleTabSelection() { + if (_tabController.index != _tabIndex) { + setState(() { + _tabIndex = _tabController.index; + }); + } + } + + void _onSearchChanged(String value) { + if (_debounce?.isActive ?? false) _debounce?.cancel(); + _debounce = Timer(const Duration(milliseconds: 800), () { + if (_tabIndex == 0) { + ref.read(productSearchProvider.notifier).state = value; + } else { + ref.read(rawMaterialSearchProvider.notifier).state = value; + } + }); + } + + @override + Widget build(BuildContext context) { + return LayoutBuilder( + builder: (context, constraints) { + final currentUserRole = ref.watch(currentUserRoleProvider); + + return SafeArea( + top: false, + bottom: true, + right: false, + left: false, + child: Scaffold( + backgroundColor: Colors.white, + resizeToAvoidBottomInset: false, + appBar: AppBarWithTab( + hintText: + "Cari nama ${_tabIndex == 0 ? 'produk' : 'bahan baku'}...", + searchIconColor: _searchIconColor, + showSwitchOutlet: currentUserRole == UserRole.owner, + searchFocusNode: _searchFocusNode, + tabController: _tabController, + onTap: (value) { + setState(() { + _tabIndex = value; + }); + }, + onSearchChanged: (value) => _onSearchChanged(value), + tabs: [ + Tab(text: "Produk", height: 10.h), + Tab(text: "Bahan Baku", height: 10.h), + ], + ), + + body: TabBarView( + controller: _tabController, + children: [const ProductScreen(), const RawMaterialScreen()], + ), + ), + ); + }, + ); + } +} diff --git a/Mobile Operasional/lib/features/goods/presentation/widgets/custom_item_detail.dart b/Mobile Operasional/lib/features/goods/presentation/widgets/custom_item_detail.dart new file mode 100644 index 0000000..10a5d4e --- /dev/null +++ b/Mobile Operasional/lib/features/goods/presentation/widgets/custom_item_detail.dart @@ -0,0 +1,50 @@ +import 'package:flutter/material.dart'; +import 'package:niogu_app/core/constants/app_font_size.dart'; +import 'package:sizer/sizer.dart'; + +class CustomItemDetail extends StatelessWidget { + final String label; + final String value; + final double width; + const CustomItemDetail({ + super.key, + required this.label, + required this.value, + this.width = 28, + }); + + @override + Widget build(BuildContext context) { + final bool isTablet = 100.w >= 600; + return Padding( + padding: const EdgeInsets.only(bottom: 2), + child: Row( + children: [ + SizedBox( + width: width.w, + child: Text( + label, + style: TextStyle( + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + color: Colors.black87, + ), + ), + ), + Expanded( + child: Text( + value, + style: TextStyle( + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + color: Colors.black87, + ), + ), + ), + ], + ), + ); + } +} diff --git a/Mobile Operasional/lib/features/goods/presentation/widgets/custom_search.dart b/Mobile Operasional/lib/features/goods/presentation/widgets/custom_search.dart new file mode 100644 index 0000000..b6ace69 --- /dev/null +++ b/Mobile Operasional/lib/features/goods/presentation/widgets/custom_search.dart @@ -0,0 +1,51 @@ +import 'package:flutter/material.dart'; +import 'package:niogu_app/core/constants/app_color.dart'; +import 'package:niogu_app/core/constants/app_font_size.dart'; +import 'package:sizer/sizer.dart'; + +class CustomSearch extends StatelessWidget { + final String hintText; + final FocusNode searchFocusNode; + final Color searchIconColor; + final Function(String) onChanged; + + const CustomSearch({ + super.key, + required this.hintText, + required this.searchFocusNode, + required this.searchIconColor, + required this.onChanged, + }); + + @override + Widget build(BuildContext context) { + final bool isTablet = 100.w >= 600; + return Padding( + padding: EdgeInsets.symmetric(horizontal: 4.w, vertical: 1.h), + child: TextField( + focusNode: searchFocusNode, + onChanged: onChanged, + style: TextStyle( + color: searchIconColor, + fontWeight: FontWeight.w500, + fontSize: isTablet ? AppFontSize.medium.sp : AppFontSize.small.sp, + ), + decoration: InputDecoration( + hintText: hintText, + hintStyle: TextStyle( + color: Colors.grey, + fontSize: isTablet ? AppFontSize.medium.sp : AppFontSize.small.sp, + ), + prefixIcon: Icon(Icons.search, color: searchIconColor, size: 6.w), + enabledBorder: const UnderlineInputBorder( + borderSide: BorderSide(color: Colors.grey), + ), + focusedBorder: UnderlineInputBorder( + borderSide: BorderSide(color: AppColor.primaryColor), + ), + contentPadding: EdgeInsets.symmetric(vertical: 4.h), + ), + ), + ); + } +} diff --git a/Mobile Operasional/lib/features/goods/presentation/widgets/dropdown_with_action.dart b/Mobile Operasional/lib/features/goods/presentation/widgets/dropdown_with_action.dart new file mode 100644 index 0000000..0624fb9 --- /dev/null +++ b/Mobile Operasional/lib/features/goods/presentation/widgets/dropdown_with_action.dart @@ -0,0 +1,133 @@ +import 'package:flutter/material.dart'; +import 'package:niogu_app/core/constants/app_color.dart'; +import 'package:niogu_app/core/constants/app_font_size.dart'; +import 'package:sizer/sizer.dart'; + +class DropdownWithAction extends StatelessWidget { + final String label; + final String? value; + final List items; + final Function(String?)? onChanged; + final VoidCallback? onAdd; + final VoidCallback? onEdit; + + const DropdownWithAction({ + super.key, + required this.label, + required this.value, + required this.items, + this.onChanged, + this.onAdd, + this.onEdit, + }); + + @override + Widget build(BuildContext context) { + final isTablet = 100.w >= 600; + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + label, + style: TextStyle( + fontSize: isTablet ? AppFontSize.medium.sp : AppFontSize.small.sp, + fontWeight: FontWeight.bold, + ), + ), + SizedBox(height: 1.h), + + Row( + children: [ + Expanded( + child: DropdownButtonFormField( + value: value, + isExpanded: true, + items: items + .map( + (item) => DropdownMenuItem( + value: item.id.toString(), + child: FittedBox( + child: Text( + item.name.toString(), + style: isTablet + ? null + : TextStyle(fontSize: AppFontSize.small.sp), + ), + ), + ), + ) + .toList(), + onChanged: onChanged, + icon: const Icon(Icons.keyboard_arrow_down_rounded), + decoration: InputDecoration( + filled: true, + fillColor: Colors.white, + contentPadding: EdgeInsets.symmetric( + horizontal: 4.w, + vertical: 1.8.h, + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(2.5.w), + borderSide: BorderSide(color: Colors.grey.shade300), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(2.5.w), + borderSide: const BorderSide( + color: AppColor.primaryColor, + width: 1.5, + ), + ), + ), + ), + ), + + SizedBox(width: 3.w), + + InkWell( + onTap: onAdd, + borderRadius: BorderRadius.circular(2.5.w), + child: Container( + width: 13.w, + height: 13.w, + decoration: BoxDecoration( + color: AppColor.primaryColor, + borderRadius: BorderRadius.circular(2.5.w), + boxShadow: [ + BoxShadow( + color: AppColor.primaryColor.withOpacity(0.3), + blurRadius: 8, + offset: const Offset(0, 4), + ), + ], + ), + child: Icon(Icons.add, color: Colors.white, size: 7.w), + ), + ), + SizedBox(width: 3.w), + + InkWell( + onTap: onEdit, + borderRadius: BorderRadius.circular(2.5.w), + child: Container( + width: 13.w, + height: 13.w, + decoration: BoxDecoration( + color: Colors.teal, + borderRadius: BorderRadius.circular(2.5.w), + boxShadow: [ + BoxShadow( + color: Colors.teal.withOpacity(0.3), + blurRadius: 8, + offset: const Offset(0, 4), + ), + ], + ), + child: Icon(Icons.edit, color: Colors.white, size: 7.w), + ), + ), + ], + ), + ], + ); + } +} diff --git a/Mobile Operasional/lib/features/goods/products/data/repositories/category_repository_impl.dart b/Mobile Operasional/lib/features/goods/products/data/repositories/category_repository_impl.dart new file mode 100644 index 0000000..76a548e --- /dev/null +++ b/Mobile Operasional/lib/features/goods/products/data/repositories/category_repository_impl.dart @@ -0,0 +1,41 @@ +import 'package:drift/drift.dart'; +import 'package:niogu_app/core/database/app_database.dart' hide Category; +import 'package:niogu_app/features/goods/products/domain/entities/category.dart'; +import 'package:niogu_app/features/goods/products/domain/repositories/i_category_repository.dart'; + +class CategoryRepositoryImpl implements ICategoryRepository { + final AppDatabase _appDatabase; + + CategoryRepositoryImpl(this._appDatabase); + + @override + Stream> watchCategories() { + return _appDatabase.categoryDao.watchAllCategories(); + } + + @override + Future> fetchCategories() async { + return await _appDatabase.categoryDao.getAllCategories(); + } + + @override + Future fetchCategoryById(String id) async { + return await _appDatabase.categoryDao.getCategoryById(id); + } + + @override + Future addCategory(Category category) async { + final categoryCompanion = CategoriesCompanion( + localId: Value(category.id), + name: Value(category.name), + localImagePath: Value(category.imagePath), + ); + + await _appDatabase.categoryDao.insertCategory(categoryCompanion); + } + + @override + Future updateCategory(String id, String name, String? imagePath) async { + await _appDatabase.categoryDao.updateCategory(id, name, imagePath); + } +} diff --git a/Mobile Operasional/lib/features/goods/products/data/repositories/product_repository_impl.dart b/Mobile Operasional/lib/features/goods/products/data/repositories/product_repository_impl.dart new file mode 100644 index 0000000..c9fb9c9 --- /dev/null +++ b/Mobile Operasional/lib/features/goods/products/data/repositories/product_repository_impl.dart @@ -0,0 +1,148 @@ +import 'package:drift/drift.dart'; +import 'package:niogu_app/core/database/app_database.dart' + hide Product, StockCard; +import 'package:niogu_app/features/goods/domain/entities/stock_card.dart'; +import 'package:niogu_app/features/goods/domain/entities/outlet_inventory.dart'; +import 'package:niogu_app/features/goods/products/domain/entities/product.dart'; +import 'package:niogu_app/features/goods/products/domain/repositories/i_product_repository.dart'; + +class ProductRepositoryImpl implements IProductRepository { + final AppDatabase _appDatabase; + + ProductRepositoryImpl(this._appDatabase); + + @override + Stream> watchProducts(String outletId) { + return _appDatabase.productDao.watchAllProducts(); + } + + @override + Future fetchProductById(String id, bool isEditing) async { + return await _appDatabase.productDao.getProductById(id, isEditing); + } + + @override + Future> fetchNewVariants(String id) async { + return await _appDatabase.productDao.getNewVariants(id); + } + + @override + Future fetchDeleteProductSnapshotById( + String id, + ) async { + return await _appDatabase.productDao.getDeleteProductSnapshotById(id); + } + + @override + Future saveProduct({ + required UpsertProduct product, + required List productVariants, + List? deleteVariants, + required List outletInventories, + required List stockCards, + bool modeChange = false, + String? modeBeforeChange, + String? modeAfterChange, + }) async { + final productCompanion = ProductsCompanion( + localId: Value(product.localId), + categoryId: Value(product.categoryId), + unitId: Value(product.unitId), + name: Value(product.name), + localImagePath: Value(product.localImagePath), + hasVariant: Value(product.hasVariant), + syncStatus: Value(product.syncStatus.status), + ); + + final List productVariantCompanions = []; + + for (final productVariant in productVariants) { + productVariantCompanions.add( + ProductVariantsCompanion( + localId: Value(productVariant.localId), + productId: Value(productVariant.productId), + name: Value(productVariant.name), + localImagePath: Value(productVariant.localImagePath), + isProductVariant: Value(productVariant.isProductVariant), + syncStatus: Value(productVariant.syncStatus.status), + ), + ); + } + + final List outletInventoryCompanions = []; + + for (final outletInventory in outletInventories) { + outletInventoryCompanions.add( + OutletInventoriesCompanion( + localId: Value(outletInventory.localId), + outletId: Value(outletInventory.outletId), + itemType: Value(outletInventory.itemType.type), + itemId: Value(outletInventory.itemId), + stockType: Value(outletInventory.stockType.type), + stock: Value(outletInventory.stock), + sellingPrice: Value(outletInventory.sellingPrice), + costPrice: Value(outletInventory.costPrice), + purchasePrice: Value(outletInventory.purchasePrice), + syncStatus: Value(outletInventory.syncStatus.status), + ), + ); + } + + final List cards = []; + + if (stockCards.isNotEmpty) { + for (final stockCard in stockCards) { + cards.add( + StockCardsCompanion( + localId: Value(stockCard.localId), + outletInventoryId: Value(stockCard.outletInventoryId), + userId: Value(stockCard.userId), + type: Value(stockCard.stockCardType.type), + qtyChange: Value(stockCard.qtyChange), + currentStock: Value(stockCard.currentStock), + costPrice: Value(stockCard.costPrice), + stockType: Value(stockCard.stockType.type), + modeChange: Value(stockCard.modeChange), + modeBeforeChange: Value(stockCard.modeBeforeChange), + modeAfterChange: Value(stockCard.modeAfterChange), + syncStatus: Value(stockCard.syncStatus.status), + ), + ); + } + } + + await _appDatabase.outletInventoryDao.saveOutletInventory( + product: productCompanion, + variants: productVariantCompanions, + deleteVariants: deleteVariants, + inventories: outletInventoryCompanions, + cards: cards, + modeChange: modeChange, + modeBeforeChange: modeBeforeChange, + modeAfterChange: modeAfterChange, + ); + } + + @override + Future deleteProduct( + String id, { + String? outletId, + required bool canDeleteAll, + }) async { + await _appDatabase.outletInventoryDao.deleteOutletInventory( + productId: id, + outletId: outletId, + canDeleteAll: canDeleteAll, + ); + } + + @override + Future checkAlreadyProductByOutlet(String id) async { + return await _appDatabase.productDao.checkAlreadyProductByOutlet(id); + } + + @override + Stream> watchProductChoices() { + return _appDatabase.productDao.watchAllProductChoices(); + } +} diff --git a/Mobile Operasional/lib/features/goods/products/domain/entities/category.dart b/Mobile Operasional/lib/features/goods/products/domain/entities/category.dart new file mode 100644 index 0000000..963761e --- /dev/null +++ b/Mobile Operasional/lib/features/goods/products/domain/entities/category.dart @@ -0,0 +1,10 @@ +import 'package:uuid/uuid.dart'; + +class Category { + final String id; + final String name; + final String? imagePath; + + Category({String? id, required this.name, this.imagePath}) + : id = id ?? const Uuid().v7().toString(); +} diff --git a/Mobile Operasional/lib/features/goods/products/domain/entities/product.dart b/Mobile Operasional/lib/features/goods/products/domain/entities/product.dart new file mode 100644 index 0000000..8fb3109 --- /dev/null +++ b/Mobile Operasional/lib/features/goods/products/domain/entities/product.dart @@ -0,0 +1,227 @@ +import 'package:niogu_app/core/enums/stock_type.dart'; +import 'package:niogu_app/core/enums/sync_status.dart'; +import 'package:uuid/uuid.dart'; + +enum ProductEmpty { loading, empty_database, empty_search_result, has_data } + +class Product { + final String id; + final String name; + final List? productImages; + final bool hasVariant; + final int variantCount; + final int newVariantCount; + final String stockType; + final String remainingStock; + final String displaySellingPrice; + + const Product({ + required this.id, + required this.name, + required this.productImages, + required this.hasVariant, + required this.variantCount, + required this.newVariantCount, + required this.stockType, + required this.remainingStock, + required this.displaySellingPrice, + }); +} + +class UpsertProduct { + final String localId; + final String categoryId; + final String unitId; + final String name; + final List? localImagePath; + final double averageRating; + final int ratingCount; + final bool hasVariant; + final SyncStatus syncStatus; + + UpsertProduct({ + String? localId, + required this.categoryId, + required this.unitId, + required this.name, + this.localImagePath, + this.averageRating = 0.0, + this.ratingCount = 0, + required this.hasVariant, + this.syncStatus = SyncStatus.dirty, + }) : localId = localId ?? const Uuid().v7().toString(); +} + +class UpsertProductVariant { + final String localId; + final String productId; + final String? outletInventoryId; + final String name; + final String? localImagePath; + final double? stock; + final double? costPrice; + final double? sellingPrice; + final bool isProductVariant; + final SyncStatus syncStatus; + + UpsertProductVariant({ + String? localId, + required this.productId, + this.outletInventoryId, + required this.name, + this.localImagePath, + this.stock, + this.costPrice, + this.sellingPrice, + this.isProductVariant = false, + this.syncStatus = SyncStatus.dirty, + }) : localId = localId ?? const Uuid().v7().toString(); +} + +class ProductWithVariant { + final String? productVariantId; + final String? outletInventoryId; + final String? imagePath; + final String name; + final double stock; + final double costPrice; + final double sellingPrice; + + const ProductWithVariant({ + this.productVariantId, + this.outletInventoryId, + this.imagePath, + required this.name, + this.stock = 0.0, + required this.costPrice, + required this.sellingPrice, + }); +} + +class ProductDetail { + final String localId; + final List? productImages; + final String name; + final String categoryId; + final bool hasVariant; + final List variants; + final String unitId; + + const ProductDetail({ + required this.localId, + this.productImages, + required this.name, + required this.categoryId, + required this.hasVariant, + required this.variants, + required this.unitId, + }); +} + +class ProductVariantDetail { + final String productVariantId; + final String outletInventoryId; + final String? image; + final String name; + final double costPrice; + final double sellingPrice; + final StockType stockType; + final double stock; + + const ProductVariantDetail({ + required this.productVariantId, + required this.outletInventoryId, + this.image, + required this.name, + required this.costPrice, + required this.sellingPrice, + required this.stockType, + required this.stock, + }); +} + +class DeleteProductVariant { + final String productId; + final String productVariantId; + final String outletInventoryId; + + const DeleteProductVariant({ + required this.productId, + required this.productVariantId, + required this.outletInventoryId, + }); +} + +class DeleteProductSnapshot { + final String productId; + final String? imagePath; + final String name; + final double totalSold; + final String currentOutletName; + final bool isMainOutlet; + final int totalOutletCount; + + DeleteProductSnapshot({ + required this.productId, + this.imagePath, + required this.name, + required this.totalSold, + required this.currentOutletName, + required this.isMainOutlet, + required this.totalOutletCount, + }); +} + +class DeleteProduct { + final String productVariantId; + final String outletInventoryId; + + DeleteProduct({ + required this.productVariantId, + required this.outletInventoryId, + }); +} + +class UpdateStockProduct { + final String outletInventoryId; + final String productVariantId; + final StockType stockType; + final double stock; + final double quantity; + + const UpdateStockProduct({ + required this.outletInventoryId, + required this.productVariantId, + required this.stockType, + required this.stock, + required this.quantity, + }); +} + +enum ProductChoiceEmpty { + loading, + empty_database, + empty_search_result, + has_data, +} + +class ProductChoice { + final String id; + final String? imagePath; + final String name; + final bool hasVariant; + + const ProductChoice({ + required this.id, + this.imagePath, + required this.name, + required this.hasVariant, + }); +} + +class NewVariant { + final String id; + final int amount; + + const NewVariant({required this.id, required this.amount}); +} diff --git a/Mobile Operasional/lib/features/goods/products/domain/repositories/i_category_repository.dart b/Mobile Operasional/lib/features/goods/products/domain/repositories/i_category_repository.dart new file mode 100644 index 0000000..e7b32cf --- /dev/null +++ b/Mobile Operasional/lib/features/goods/products/domain/repositories/i_category_repository.dart @@ -0,0 +1,13 @@ +import 'package:niogu_app/features/goods/products/domain/entities/category.dart'; + +abstract class ICategoryRepository { + Stream> watchCategories(); + + Future> fetchCategories(); + + Future fetchCategoryById(String id); + + Future addCategory(Category category); + + Future updateCategory(String id, String name, String? imagePath); +} diff --git a/Mobile Operasional/lib/features/goods/products/domain/repositories/i_product_repository.dart b/Mobile Operasional/lib/features/goods/products/domain/repositories/i_product_repository.dart new file mode 100644 index 0000000..a4d79ed --- /dev/null +++ b/Mobile Operasional/lib/features/goods/products/domain/repositories/i_product_repository.dart @@ -0,0 +1,34 @@ +import 'package:niogu_app/features/goods/domain/entities/stock_card.dart'; +import 'package:niogu_app/features/goods/domain/entities/outlet_inventory.dart'; +import 'package:niogu_app/features/goods/products/domain/entities/product.dart'; + +abstract class IProductRepository { + Stream> watchProducts(String outletId); + + Future fetchProductById(String id, bool isEditing); + + Future> fetchNewVariants(String id); + + Future fetchDeleteProductSnapshotById(String id); + + Future saveProduct({ + required UpsertProduct product, + required List productVariants, + List? deleteVariants, + required List outletInventories, + required List stockCards, + bool modeChange = false, + String? modeBeforeChange, + String? modeAfterChange, + }); + + Future deleteProduct( + String id, { + String? outletId, + required bool canDeleteAll, + }); + + Future checkAlreadyProductByOutlet(String id); + + Stream> watchProductChoices(); +} diff --git a/Mobile Operasional/lib/features/goods/products/presentation/controllers/category_controller.dart b/Mobile Operasional/lib/features/goods/products/presentation/controllers/category_controller.dart new file mode 100644 index 0000000..8234ba4 --- /dev/null +++ b/Mobile Operasional/lib/features/goods/products/presentation/controllers/category_controller.dart @@ -0,0 +1,39 @@ +import 'dart:async'; + +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:niogu_app/features/goods/products/domain/entities/category.dart'; +import 'package:niogu_app/features/goods/products/domain/repositories/i_category_repository.dart'; +import 'package:niogu_app/features/goods/products/presentation/providers/category_provider.dart'; + +class CategoryController extends AutoDisposeAsyncNotifier { + late final ICategoryRepository _categoryRepository; + + @override + FutureOr build() { + _categoryRepository = ref.read(categoryRepositoryProvider); + } + + Future addCategory(Category category) async { + state = const AsyncValue.loading(); + + final result = await AsyncValue.guard(() async { + await _categoryRepository.addCategory(category); + }); + + state = result; + + if (result is AsyncError) throw result.error!; + } + + Future updateCategory(String id, String name, String? imagePath) async { + state = const AsyncValue.loading(); + + final result = await AsyncValue.guard(() async { + await _categoryRepository.updateCategory(id, name, imagePath); + }); + + state = result; + + if (result is AsyncError) throw result.error!; + } +} diff --git a/Mobile Operasional/lib/features/goods/products/presentation/controllers/product_controller.dart b/Mobile Operasional/lib/features/goods/products/presentation/controllers/product_controller.dart new file mode 100644 index 0000000..035eec1 --- /dev/null +++ b/Mobile Operasional/lib/features/goods/products/presentation/controllers/product_controller.dart @@ -0,0 +1,66 @@ +import 'dart:async'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:niogu_app/features/goods/domain/entities/stock_card.dart'; +import 'package:niogu_app/features/goods/products/presentation/providers/product_provider.dart'; +import 'package:niogu_app/features/goods/domain/entities/outlet_inventory.dart'; +import 'package:niogu_app/features/goods/products/domain/entities/product.dart'; +import 'package:niogu_app/features/goods/products/domain/repositories/i_product_repository.dart'; + +class ProductController extends AutoDisposeAsyncNotifier { + late final IProductRepository _productRepository; + + @override + FutureOr build() { + _productRepository = ref.read(productRepositoryProvider); + } + + Future saveProduct({ + required UpsertProduct product, + required List productVariants, + List? deleteVariants, + required List outletInventories, + required List stockCards, + bool modeChange = false, + String? modeBeforeChange, + String? modeAfterChange, + }) async { + state = const AsyncValue.loading(); + + final result = await AsyncValue.guard(() async { + await _productRepository.saveProduct( + product: product, + productVariants: productVariants, + deleteVariants: deleteVariants, + outletInventories: outletInventories, + stockCards: stockCards, + modeChange: modeChange, + modeBeforeChange: modeBeforeChange, + modeAfterChange: modeAfterChange, + ); + }); + + state = result; + + if (result is AsyncError) throw result.error!; + } + + Future deleteProduct( + String id, { + String? outletId, + required bool canDeleteAll, + }) async { + state = const AsyncValue.loading(); + + final result = await AsyncValue.guard(() async { + await _productRepository.deleteProduct( + id, + outletId: outletId, + canDeleteAll: canDeleteAll, + ); + }); + + state = result; + + if (result is AsyncError) throw result.error!; + } +} diff --git a/Mobile Operasional/lib/features/goods/products/presentation/providers/category_provider.dart b/Mobile Operasional/lib/features/goods/products/presentation/providers/category_provider.dart new file mode 100644 index 0000000..5f0dac7 --- /dev/null +++ b/Mobile Operasional/lib/features/goods/products/presentation/providers/category_provider.dart @@ -0,0 +1,43 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:niogu_app/core/providers/app_provider.dart'; +import 'package:niogu_app/features/goods/products/data/repositories/category_repository_impl.dart'; +import 'package:niogu_app/features/goods/products/domain/entities/category.dart'; +import 'package:niogu_app/features/goods/products/domain/repositories/i_category_repository.dart'; +import 'package:niogu_app/features/goods/products/presentation/controllers/category_controller.dart'; + +final categoryRepositoryProvider = Provider((ref) { + final appDatabase = ref.watch(appDatabaseProvider); + return CategoryRepositoryImpl(appDatabase); +}); + +final categoryStreamProvider = StreamProvider.autoDispose>(( + ref, +) { + final categoryRepository = ref.watch(categoryRepositoryProvider); + return categoryRepository.watchCategories(); +}); + +final categorySearchProvider = StateProvider.autoDispose((ref) => ''); + +final validateCategoryProvider = Provider.autoDispose((ref) { + final categoryStreamAsync = ref.watch(categoryStreamProvider); + final categorySearchAsync = ref.watch(categorySearchProvider); + + return categoryStreamAsync.maybeWhen( + data: (categories) { + return categories + .where( + (category) => + category.name.trim().toLowerCase() == + categorySearchAsync.trim().toLowerCase(), + ) + .isEmpty; + }, + orElse: () => true, + ); +}); + +final categoryControllerProvider = + AsyncNotifierProvider.autoDispose( + CategoryController.new, + ); diff --git a/Mobile Operasional/lib/features/goods/products/presentation/providers/product_provider.dart b/Mobile Operasional/lib/features/goods/products/presentation/providers/product_provider.dart new file mode 100644 index 0000000..8c8e327 --- /dev/null +++ b/Mobile Operasional/lib/features/goods/products/presentation/providers/product_provider.dart @@ -0,0 +1,160 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:niogu_app/core/providers/app_provider.dart'; +import 'package:niogu_app/features/goods/products/data/repositories/product_repository_impl.dart'; +import 'package:niogu_app/features/goods/products/domain/entities/product.dart'; +import 'package:niogu_app/features/goods/products/domain/repositories/i_product_repository.dart'; +import 'package:niogu_app/features/goods/products/presentation/controllers/product_controller.dart'; + +final productRepositoryProvider = Provider((ref) { + final _appDatabase = ref.watch(appDatabaseProvider); + return ProductRepositoryImpl(_appDatabase); +}); + +final productStreamProvider = StreamProvider.autoDispose>((ref) { + final productRepository = ref.watch(productRepositoryProvider); + + final currentOutletId = ref.watch(currentOutletIdProvider); + + if(currentOutletId == null) { + return Stream>.value([]); + } + + return productRepository.watchProducts(currentOutletId); +}); + +final productSearchProvider = StateProvider.autoDispose((ref) => ''); + +final filteredProductProvider = Provider.autoDispose>>( + (ref) { + final productStreamAsync = ref.watch(productStreamProvider); + final productSearchAsync = ref.watch(productSearchProvider); + return productStreamAsync.when( + data: (products) { + if (productSearchAsync.trim().isEmpty) { + return AsyncValue.data(products); + } + + final filteredProducts = products.where((product) { + return product.name.toLowerCase().contains( + productSearchAsync.toLowerCase(), + ); + }).toList(); + + return AsyncValue.data(filteredProducts); + }, + error: (error, stackTrace) { + return AsyncValue.error(error, stackTrace); + }, + loading: () => const AsyncValue.loading(), + ); + }, +); + +final productEmptyProvider = Provider.autoDispose((ref) { + final productStreamAsync = ref.watch(productStreamProvider); + final filteredProductAsync = ref.watch(filteredProductProvider); + final productSearchAsync = ref.watch(productSearchProvider); + + if (productStreamAsync.isLoading) { + return ProductEmpty.loading; + } + + final allProducts = productStreamAsync.value ?? []; + + if (allProducts.isEmpty) { + return ProductEmpty.empty_database; + } + + final filteredProduct = filteredProductAsync.value ?? []; + + if (productSearchAsync.isNotEmpty && filteredProduct.isEmpty) { + return ProductEmpty.empty_search_result; + } + + return ProductEmpty.has_data; +}); + +final productControllerProvider = + AsyncNotifierProvider.autoDispose( + ProductController.new, + ); + +final productWithVariantProvider = + StateProvider.autoDispose>((ref) { + return []; + }); + +final deleteProductVariantProvider = StateProvider>(( + ref, +) { + return []; +}); + +final variantImageTempProvider = StateProvider>((ref) { + return []; +}); + +final productChoiceStreamProvider = + StreamProvider.autoDispose>((ref) { + final productRepository = ref.watch(productRepositoryProvider); + return productRepository.watchProductChoices(); + }); + +final productChoiceSearchProvider = StateProvider.autoDispose( + (ref) => '', +); + +final filteredProductChoiceProvider = + Provider.autoDispose>>((ref) { + final productStreamAsync = ref.watch(productChoiceStreamProvider); + final productSearchAsync = ref.watch(productChoiceSearchProvider); + return productStreamAsync.when( + data: (products) { + if (productSearchAsync.trim().isEmpty) { + return AsyncValue.data(products); + } + + final filteredProducts = products.where((product) { + return product.name.toLowerCase().contains( + productSearchAsync.toLowerCase(), + ); + }).toList(); + + return AsyncValue.data(filteredProducts); + }, + error: (error, stackTrace) { + return AsyncValue.error(error, stackTrace); + }, + loading: () => const AsyncValue.loading(), + ); + }); + +final productChoiceEmptyProvider = Provider.autoDispose(( + ref, +) { + final productStreamAsync = ref.watch(productChoiceStreamProvider); + final filteredProductAsync = ref.watch(filteredProductChoiceProvider); + final productSearchAsync = ref.watch(productChoiceSearchProvider); + + if (productStreamAsync.isLoading) { + return ProductChoiceEmpty.loading; + } + + final allProducts = productStreamAsync.value ?? []; + + if (allProducts.isEmpty) { + return ProductChoiceEmpty.empty_database; + } + + final filteredProduct = filteredProductAsync.value ?? []; + + if (productSearchAsync.isNotEmpty && filteredProduct.isEmpty) { + return ProductChoiceEmpty.empty_search_result; + } + + return ProductChoiceEmpty.has_data; +}); + +final selectedNewVariantProvider = StateProvider.autoDispose>( + (ref) => {}, +); diff --git a/Mobile Operasional/lib/features/goods/products/presentation/screens/add_product_category_screen.dart b/Mobile Operasional/lib/features/goods/products/presentation/screens/add_product_category_screen.dart new file mode 100644 index 0000000..0cc0d35 --- /dev/null +++ b/Mobile Operasional/lib/features/goods/products/presentation/screens/add_product_category_screen.dart @@ -0,0 +1,233 @@ +import 'dart:async'; +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; +import 'package:image_picker/image_picker.dart'; +import 'package:niogu_app/core/constants/app_font_size.dart'; +import 'package:niogu_app/core/utils/image_service.dart'; +import 'package:niogu_app/core/utils/log_message.dart'; +import 'package:niogu_app/core/widgets/custom_button.dart'; +import 'package:niogu_app/core/widgets/custom_snackbar.dart'; +import 'package:niogu_app/core/widgets/custom_text_form_field.dart'; +import 'package:niogu_app/features/goods/products/domain/entities/category.dart'; +import 'package:niogu_app/features/goods/products/presentation/providers/category_provider.dart'; +import 'package:niogu_app/features/goods/products/presentation/widgets/image_uploader_product_category.dart'; +import 'package:sizer/sizer.dart'; + +class AddProductCategoryScreen extends ConsumerStatefulWidget { + const AddProductCategoryScreen({super.key}); + + @override + ConsumerState createState() => + _AddProductCategoryScreenState(); +} + +class _AddProductCategoryScreenState + extends ConsumerState { + final GlobalKey _formKey = GlobalKey(); + + final TextEditingController _nameController = TextEditingController(); + + final ImagePicker _picker = ImagePicker(); + + bool _canPop = false; + + String? _categoryImage; + + String? _pathImageTemp; + + Timer? _debounce; + + @override + void initState() { + // TODO: implement initState + super.initState(); + } + + @override + void dispose() { + // TODO: implement dispose + _nameController.dispose(); + _debounce?.cancel(); + super.dispose(); + } + + Future _getImage() async { + try { + final XFile? pickedFile = await _picker.pickImage( + source: ImageSource.gallery, + imageQuality: 70, + ); + + if (pickedFile != null) { + File tempFile = File(pickedFile.path); + + String? savedPath = await ImageService.saveImageToLocalDirectory( + tempFile, + "product_category_images", + ); + + if (savedPath != null) { + _pathImageTemp = savedPath; + setState(() { + _categoryImage = savedPath; + }); + } + } + } catch (e, st) { + LogMessage.log.w(e.toString(), error: e, stackTrace: st); + CustomSnackbar.showWarning(context, "Akses ditolak"); + } + } + + Future _removeImage() async { + setState(() { + _categoryImage = null; + }); + } + + Future _cleanUpImages() async { + if (_pathImageTemp != null && _pathImageTemp!.isNotEmpty) { + await ImageService.deleteLocalImage(_pathImageTemp!); + } + } + + void _onChanged(String value) { + if (_debounce?.isActive ?? false) _debounce!.cancel(); + + _debounce = Timer(const Duration(milliseconds: 500), () async { + if (value.isEmpty) return; + + ref.read(categorySearchProvider.notifier).state = value; + }); + } + + Future _onCancel() async { + await _cleanUpImages(); + + if (mounted) { + setState(() { + _canPop = true; + }); + context.pop(); + } + } + + Future _addCategory(Category category) async { + try { + await ref.read(categoryControllerProvider.notifier).addCategory(category); + + if (!mounted) return; + + CustomSnackbar.showSuccess(context, "Kategori berhasil disimpan"); + context.pop(); + } catch (e, st) { + LogMessage.log.i(e.toString(), error: e, stackTrace: st); + CustomSnackbar.showError(context, "Ups, terjadi kesalahan"); + context.pop(); + } + } + + @override + Widget build(BuildContext context) { + final bool isTablet = 100.w >= 600; + + final double maxWidth = 100.w >= 1280 + ? 1200 + : isTablet + ? 800 + : 400; + final validateCategory = ref.watch(validateCategoryProvider); + + final categoryControllerState = ref.watch(categoryControllerProvider); + + return PopScope( + canPop: _canPop, + onPopInvokedWithResult: (didPop, result) async { + if (didPop) return; + + await _onCancel(); + }, + child: Dialog( + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(4.w)), + backgroundColor: Colors.white, + child: Form( + key: _formKey, + child: Container( + padding: EdgeInsets.all(5.w), + constraints: BoxConstraints(maxWidth: maxWidth), + child: SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + SizedBox(height: 1.h), + + Text( + "Foto Kategori (Opsional)", + style: TextStyle( + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + fontWeight: FontWeight.bold, + color: Colors.black87, + ), + ), + + SizedBox(height: 1.h), + + ImageUploaderProductCategory( + categoryImage: _categoryImage, + onTapCategoryImageAdd: _getImage, + onTapCategoryImageRemove: _removeImage, + ), + + SizedBox(height: 3.h), + + CustomTextFormField( + label: "Nama Kategori", + hint: "Contoh : Snack", + controller: _nameController, + validator: (value) { + if (value == null || value.trim().isEmpty) { + return "Nama kategori belum diisi"; + } + + return null; + }, + errorText: validateCategory + ? null + : "Kategori ini telah tersedia", + onChanged: (value) => _onChanged(value), + ), + + SizedBox(height: 3.h), + + CustomButton( + title: "Simpan", + onPressed: categoryControllerState.isLoading + ? null + : () async { + if (_formKey.currentState!.validate() && + ref.read(validateCategoryProvider)) { + final category = Category( + name: _nameController.text.trim(), + imagePath: _categoryImage, + ); + + await _addCategory(category); + } + }, + ), + + SizedBox(height: 1.h), + ], + ), + ), + ), + ), + ), + ); + } +} diff --git a/Mobile Operasional/lib/features/goods/products/presentation/screens/add_product_screen.dart b/Mobile Operasional/lib/features/goods/products/presentation/screens/add_product_screen.dart new file mode 100644 index 0000000..be3f7f4 --- /dev/null +++ b/Mobile Operasional/lib/features/goods/products/presentation/screens/add_product_screen.dart @@ -0,0 +1,848 @@ +import 'dart:io'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; +import 'package:image_picker/image_picker.dart'; +import 'package:niogu_app/core/components/top_back_bar_app.dart'; +import 'package:niogu_app/core/constants/app_color.dart'; +import 'package:niogu_app/core/constants/app_font_size.dart'; +import 'package:niogu_app/core/router/app_route.dart'; +import 'package:niogu_app/core/utils/image_service.dart'; +import 'package:niogu_app/core/enums/item_type.dart'; +import 'package:niogu_app/core/utils/log_message.dart'; +import 'package:niogu_app/core/enums/stock_card_type.dart'; +import 'package:niogu_app/core/enums/stock_type.dart'; +import 'package:niogu_app/core/system/system_setting.dart'; +import 'package:niogu_app/core/widgets/custom_selector.dart'; +import 'package:niogu_app/core/widgets/custom_snackbar.dart'; +import 'package:niogu_app/core/widgets/custom_text_form_field.dart'; +import 'package:niogu_app/features/goods/domain/entities/stock_card.dart'; +import 'package:niogu_app/features/goods/products/domain/entities/category.dart'; +import 'package:niogu_app/features/goods/products/domain/entities/product.dart'; +import 'package:niogu_app/features/goods/products/presentation/providers/category_provider.dart'; +import 'package:niogu_app/features/goods/products/presentation/providers/product_provider.dart'; +import 'package:niogu_app/features/goods/products/presentation/screens/add_product_category_screen.dart'; +import 'package:niogu_app/features/goods/products/presentation/screens/edit_product_category_screen.dart'; +import 'package:niogu_app/features/goods/products/presentation/widgets/add_photo_button.dart'; +import 'package:niogu_app/features/goods/presentation/widgets/dropdown_with_action.dart'; +import 'package:niogu_app/features/goods/products/presentation/widgets/image_preview_card.dart'; +import 'package:niogu_app/features/goods/products/presentation/widgets/selection_card.dart'; +import 'package:niogu_app/features/goods/units/domain/entities/unit.dart'; +import 'package:niogu_app/features/goods/units/presentation/providers/unit_provider.dart'; +import 'package:niogu_app/features/goods/units/presentation/screens/add_unit_screen.dart'; +import 'package:niogu_app/features/goods/units/presentation/screens/edit_unit_screen.dart'; +import 'package:niogu_app/features/goods/domain/entities/outlet_inventory.dart'; +import 'package:niogu_app/features/outlets/presentation/providers/outlet_provider.dart'; +import 'package:sizer/sizer.dart'; + +class AddProductScreen extends ConsumerStatefulWidget { + const AddProductScreen({super.key}); + + @override + ConsumerState createState() => _AddProductScreenState(); +} + +class _AddProductScreenState extends ConsumerState { + final GlobalKey _formKey = GlobalKey(); + final TextEditingController _nameController = TextEditingController(); + final TextEditingController _costPriceController = TextEditingController(); + final TextEditingController _sellingPriceController = TextEditingController(); + final TextEditingController _stockController = TextEditingController(); + + final ImagePicker _picker = ImagePicker(); + + List _productImages = []; + + bool _hasVariant = false; + + bool _isStockAlwaysAvailable = false; + + String? _selectedCategory; + + String? _selectedUnit; + + final List _pathImageRemoves = []; + + final List _pathImageTemps = []; + + @override + void initState() { + // TODO: implement initState + super.initState(); + + WidgetsBinding.instance.addPostFrameCallback((_) { + Future.microtask(() { + ref.invalidate(variantImageTempProvider); + }); + }); + } + + @override + void dispose() { + // TODO: implement dispose + _nameController.dispose(); + _costPriceController.dispose(); + _sellingPriceController.dispose(); + _stockController.dispose(); + super.dispose(); + } + + String? _validator({ + String? value, + required String message1, + String? message2, + bool isNumber = false, + bool isCostPrice = false, + bool isSellingPrice = false, + bool isStock = false, + String? errorMessage, + }) { + if (value == null || value.isEmpty) { + return message1; + } + + try { + if (isNumber && double.parse(value) <= 0) { + if (isCostPrice) return message2; + + if (isSellingPrice) return message2; + + if (isStock && !_isStockAlwaysAvailable) return message2; + } + } catch (e) { + return errorMessage; + } + + return null; + } + + Future _showPickerOptions() async { + final bool isTablet = 100.w >= 600; + showModalBottomSheet( + context: context, + backgroundColor: Colors.white, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.vertical(top: Radius.circular(2.5.w)), + ), + constraints: const BoxConstraints(maxWidth: double.infinity), + builder: (BuildContext context) { + return SafeArea( + child: Container( + width: 100.w, + padding: EdgeInsets.symmetric(vertical: 2.h, horizontal: 5.w), + clipBehavior: Clip.hardEdge, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.vertical(top: Radius.circular(6.w)), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + ListTile( + leading: Icon(Icons.photo_library, size: 6.w), + title: Text( + 'Galeri', + style: TextStyle( + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + ), + ), + onTap: () async { + await _getImage(ImageSource.gallery); + context.pop(); + }, + ), + + SizedBox(height: 2.h), + + ListTile( + leading: Icon(Icons.photo_camera, size: 6.w), + title: Text( + 'Kamera', + style: TextStyle( + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + ), + ), + onTap: () async { + await _getImage(ImageSource.camera); + context.pop(); + }, + ), + ], + ), + ), + ); + }, + ); + } + + Future _getImage(ImageSource ImageSource) async { + if (_productImages.length >= 3) { + CustomSnackbar.showError(context, "Maksimal 3 foto produk!"); + return; + } + + try { + final XFile? pickedFile = await _picker.pickImage( + source: ImageSource, + imageQuality: 70, + ); + + if (pickedFile != null) { + File tempFile = File(pickedFile.path); + + String? savedPath = await ImageService.saveImageToLocalDirectory( + tempFile, + "product_images", + ); + + if (savedPath != null) { + _pathImageTemps.add(savedPath); + setState(() { + _productImages.add(savedPath); + }); + } + } + } catch (e, st) { + LogMessage.log.w(e.toString(), error: e, stackTrace: st); + CustomSnackbar.showWarning(context, "Akses ditolak"); + } + } + + Future _removeImage(int index) async { + _pathImageRemoves.add(_productImages[index]); + setState(() { + _productImages.removeAt(index); + }); + } + + Future _cleanUpImages(List variantImageTemps) async { + for (var path in _pathImageTemps) { + if (path != null) { + await ImageService.deleteLocalImage(path); + } + } + + for (var path in variantImageTemps) { + if (path != null) { + await ImageService.deleteLocalImage(path); + } + } + } + + Future _saveProduct() async { + final productWithVariant = ref.read(productWithVariantProvider); + + if (!_formKey.currentState!.validate()) return; + + if (_selectedCategory == null) { + CustomSnackbar.showError(context, "Kategori produk belum dipilih"); + return; + } + + if (_selectedUnit == null) { + CustomSnackbar.showError(context, "Satuan produk belum dipilih"); + return; + } + + if (_hasVariant && productWithVariant.isEmpty) { + CustomSnackbar.showError(context, "Varian produk belum diatur"); + return; + } + + if (!_hasVariant) { + final double costPrice = double.parse(_costPriceController.text.trim()); + final double sellingPrice = double.parse( + _sellingPriceController.text.trim(), + ); + + if (costPrice >= sellingPrice) { + CustomSnackbar.showError( + context, + "Harga jual harus lebih besar dari modal", + ); + return; + } + } + + final product = UpsertProduct( + categoryId: _selectedCategory!, + unitId: _selectedUnit!.trim(), + name: _nameController.text.trim(), + localImagePath: _productImages, + hasVariant: _hasVariant, + ); + + final List productVariants = []; + + final List outletInventories = []; + + final outletId = await SystemSetting.getCurrentOutletId(); + + final List stockCards = []; + + final stockType = _isStockAlwaysAvailable + ? StockType.unlimited + : StockType.fixed; + + final userId = await SystemSetting.getCurrentUserId(); + + if (_hasVariant && productWithVariant.isNotEmpty) { + for (var variant in productWithVariant) { + productVariants.add( + UpsertProductVariant( + productId: product.localId, + name: variant.name, + localImagePath: variant.imagePath, + stock: variant.stock, + costPrice: variant.costPrice, + sellingPrice: variant.sellingPrice, + isProductVariant: true, + ), + ); + } + + for (var variant in productVariants) { + outletInventories.add( + UpsertOutletInventory( + outletId: outletId!, + itemType: ItemType.product_variant, + itemId: variant.localId, + stockType: _isStockAlwaysAvailable + ? StockType.unlimited + : StockType.fixed, + stock: _isStockAlwaysAvailable ? 0.0 : variant.stock!, + costPrice: variant.costPrice!, + sellingPrice: variant.sellingPrice!, + purchasePrice: 0.0, + ), + ); + } + + for (var inventory in outletInventories) { + stockCards.add( + StockCard( + outletInventoryId: inventory.localId, + userId: userId!, + stockCardType: StockCardType.initial_stock, + qtyChange: inventory.stock, + currentStock: inventory.stock, + costPrice: inventory.costPrice, + stockType: stockType, + ), + ); + } + } else { + productVariants.add( + UpsertProductVariant( + productId: product.localId, + name: product.name.trim(), + isProductVariant: false, + ), + ); + + final double stock = _isStockAlwaysAvailable + ? 0.0 + : double.parse(_stockController.text.trim()); + + final double costPrice = double.parse(_costPriceController.text.trim()); + + outletInventories.add( + UpsertOutletInventory( + outletId: outletId!, + itemType: ItemType.product_variant, + itemId: productVariants.single.localId, + stockType: stockType, + stock: stock, + costPrice: costPrice, + sellingPrice: double.parse(_sellingPriceController.text.trim()), + purchasePrice: 0.0, + ), + ); + + stockCards.add( + StockCard( + outletInventoryId: outletInventories.single.localId, + userId: userId!, + stockCardType: StockCardType.initial_stock, + qtyChange: stock, + currentStock: stock, + costPrice: costPrice, + stockType: stockType, + ), + ); + } + + try { + await ref + .read(productControllerProvider.notifier) + .saveProduct( + product: product, + productVariants: productVariants, + outletInventories: outletInventories, + stockCards: stockCards, + ); + + if (!mounted) return; + + for (var path in _pathImageRemoves) { + await ImageService.deleteLocalImage(path); + } + + ref.invalidate(variantImageTempProvider); + + CustomSnackbar.showSuccess(context, "Produk berhasil disimpan"); + + context.pop(); + } catch (e, st) { + LogMessage.log.e(e.toString(), error: e, stackTrace: st); + + CustomSnackbar.showError(context, "Ups, terjadi kesalahan"); + } + } + + @override + Widget build(BuildContext context) { + return LayoutBuilder( + builder: (context, constraints) { + final bool isTablet = 100.w >= 600; + + final productWithVariantState = ref.watch(productWithVariantProvider); + + final bool showStockField = !_hasVariant && !_isStockAlwaysAvailable; + + final List categories = []; + + final categoryState = ref.watch(categoryStreamProvider); + + categoryState.whenData((c) => categories.addAll(c)); + + final List units = []; + + final unitState = ref.watch(unitStreamProvider); + + unitState.whenData((u) => units.addAll(u)); + + final productControllerState = ref.watch(productControllerProvider); + + return PopScope( + canPop: false, + onPopInvokedWithResult: (didPop, result) async { + if (didPop) return; + + final variantImageTemps = ref.read(variantImageTempProvider); + + await _cleanUpImages(variantImageTemps); + + ref.invalidate(variantImageTempProvider); + + context.pop(); + }, + child: SafeArea( + top: false, + bottom: true, + right: false, + left: false, + child: Scaffold( + backgroundColor: const Color(0xFFF9FAFB), + appBar: TopBackBarApp( + title: "Tambah Produk", + onTap: () async { + final variantImageTemps = ref.read(variantImageTempProvider); + + await _cleanUpImages(variantImageTemps); + + ref.invalidate(variantImageTempProvider); + + context.canPop() + ? context.pop() + : context.goNamed(AppRoute.productScreen); + }, + ), + + body: SingleChildScrollView( + padding: EdgeInsets.symmetric(horizontal: 5.w, vertical: 3.h), + child: Form( + key: _formKey, + child: Column( + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Foto Produk", + style: TextStyle( + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + fontWeight: FontWeight.bold, + color: Colors.black87, + ), + ), + Text( + "${_productImages.length}/3", + style: TextStyle( + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + color: Colors.grey[600], + ), + ), + ], + ), + SizedBox(height: 1.5.h), + + SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Row( + children: [ + ..._productImages.asMap().entries.map((entry) { + final int index = entry.key; + final String path = entry.value; + + final File imageFile = File( + path.isNotEmpty ? path : "image not found", + ); + + final bool imageFileExists = imageFile + .existsSync(); + + return Padding( + padding: EdgeInsets.only(right: 3.w), + child: ImagePreviewCard( + imageFileExists: imageFileExists, + imageFile: imageFile, + onTap: () => _removeImage(index), + ), + ); + }).toList(), + + if (_productImages.length < 3) + AddPhotoButton(onTap: _showPickerOptions), + ], + ), + ), + ], + ), + + SizedBox(height: 3.h), + + CustomTextFormField( + label: "Nama Produk", + hint: "Contoh: Kopi Susu Gula Aren", + controller: _nameController, + validator: (value) => _validator( + value: value, + message1: "Nama produk belum diisi", + ), + ), + + SizedBox(height: 2.5.h), + + DropdownWithAction( + label: "Kategori Produk", + value: _selectedCategory, + items: categories, + onChanged: (val) => + setState(() => _selectedCategory = val), + onAdd: () async { + FocusScope.of(context).unfocus(); + + await Future.delayed( + const Duration(milliseconds: 400), + ); + + showDialog( + context: context, + builder: (_) => const AddProductCategoryScreen(), + ); + }, + onEdit: () async { + if (_selectedCategory == null) { + CustomSnackbar.showError( + context, + "Kategori belum dipilih", + ); + } else { + final categoryRepository = ref.read( + categoryRepositoryProvider, + ); + + final category = await categoryRepository + .fetchCategoryById(_selectedCategory!); + + if (category != null) { + showDialog( + context: context, + builder: (_) => EditProductCategoryScreen( + category: category, + ), + ); + } else { + CustomSnackbar.showError( + context, + "Kategori produk tidak tersedia", + ); + } + } + }, + ), + + SizedBox(height: 3.h), + + Text( + "Tipe Produk", + style: TextStyle( + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + fontWeight: FontWeight.bold, + ), + ), + + SizedBox(height: 1.5.h), + + Row( + children: [ + Expanded( + child: SelectionCard( + title: "Tanpa Varian", + isSelected: !_hasVariant, + onTap: () => setState(() => _hasVariant = false), + ), + ), + + SizedBox(width: 4.w), + + Expanded( + child: SelectionCard( + title: "Memiliki Varian", + isSelected: _hasVariant, + onTap: () => setState(() => _hasVariant = true), + ), + ), + ], + ), + + SizedBox(height: 3.h), + + if (!_hasVariant) ...[ + CustomTextFormField( + label: "Harga Modal", + hint: "0", + keyboardType: TextInputType.number, + controller: _costPriceController, + validator: (value) => _validator( + value: value, + message1: "Harga modal belum diisi", + message2: "Harga modal harus lebih dari 0", + isNumber: true, + isCostPrice: true, + errorMessage: "Harga modal tidak valid", + ), + ), + + SizedBox(height: 3.h), + + CustomTextFormField( + label: "Harga Jual", + hint: "0", + keyboardType: TextInputType.number, + controller: _sellingPriceController, + validator: (value) => _validator( + value: value, + message1: "Harga jual belum diisi", + message2: "Harga jual harus lebih dari 0", + isNumber: true, + isSellingPrice: true, + errorMessage: "Harga jual tidak valid", + ), + ), + ] else + CustomSelector( + isTablet: isTablet, + title: productWithVariantState.isEmpty + ? "Atur varian produk" + : "Lihat ${productWithVariantState.length} varian produk", + padding: EdgeInsets.symmetric( + horizontal: 4.w, + vertical: 2.h, + ), + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + onTap: () async { + FocusScope.of(context).unfocus(); + + final isMainOutlet = await ref + .read(outletRepositoryProvider) + .fetchIsMainOutlet(); + + await Future.delayed( + const Duration(milliseconds: 400), + ); + + context.pushNamed( + AppRoute.addVariantProductScreen, + // extra: !_isStockAlwaysAvailable, + extra: { + "is_main_outlet": isMainOutlet, + "is_stock_fixed": !_isStockAlwaysAvailable, + }, + ); + }, + ), + + SizedBox(height: 3.h), + + Text( + "Jenis Stok", + style: TextStyle( + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + fontWeight: FontWeight.bold, + ), + ), + SizedBox(height: 1.5.h), + + Row( + children: [ + Expanded( + child: SelectionCard( + title: "Tetap", + isSelected: !_isStockAlwaysAvailable, + onTap: () => setState( + () => _isStockAlwaysAvailable = false, + ), + ), + ), + + SizedBox(width: 4.w), + + Expanded( + child: SelectionCard( + title: "Selalu Ada", + isSelected: _isStockAlwaysAvailable, + onTap: () => setState( + () => _isStockAlwaysAvailable = true, + ), + ), + ), + ], + ), + + SizedBox(height: 3.h), + + if (showStockField) ...[ + CustomTextFormField( + label: "Stok Awal", + hint: "100", + keyboardType: TextInputType.number, + controller: _stockController, + validator: (value) => _validator( + value: value, + message1: "Stok belum diisi", + message2: "Stok harus lebih dari 0", + isNumber: true, + isStock: true, + errorMessage: "Stok tidak valid", + ), + ), + + SizedBox(height: 3.h), + ], + + DropdownWithAction( + label: "Satuan", + value: _selectedUnit, + items: units, + onChanged: (val) => setState(() => _selectedUnit = val), + onAdd: () async { + FocusScope.of(context).unfocus(); + + await Future.delayed( + const Duration(milliseconds: 400), + ); + + showDialog( + context: context, + builder: (_) => const AddUnitScreen(), + ); + }, + onEdit: () async { + if (_selectedUnit == null) { + CustomSnackbar.showError( + context, + "Satuan belum dipilih", + ); + } else { + final unitRepository = ref.read( + unitRepositoryProvider, + ); + + final unit = await unitRepository.fetchUnitById( + _selectedUnit!, + ); + + if (unit != null) { + showDialog( + context: context, + builder: (_) { + return EditUnitScreen(unit: unit); + }, + ); + } else { + CustomSnackbar.showError( + context, + "Satuan produk tidak tersedia", + ); + } + } + }, + ), + + SizedBox(height: 3.h), + + SizedBox( + width: double.infinity, + height: 6.5.h, + child: ElevatedButton( + onPressed: productControllerState.isLoading + ? null + : _saveProduct, + style: ElevatedButton.styleFrom( + backgroundColor: AppColor.primaryColor, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(2.0.w), + ), + disabledBackgroundColor: Colors.grey.shade300, + elevation: 4, + ), + child: Text( + "Simpan", + style: TextStyle( + color: Colors.white, + fontSize: AppFontSize.medium.sp, + fontWeight: FontWeight.bold, + ), + ), + ), + ), + ], + ), + ), + ), + ), + ), + ); + }, + ); + } +} diff --git a/Mobile Operasional/lib/features/goods/products/presentation/screens/add_product_variant_screen.dart b/Mobile Operasional/lib/features/goods/products/presentation/screens/add_product_variant_screen.dart new file mode 100644 index 0000000..dc02870 --- /dev/null +++ b/Mobile Operasional/lib/features/goods/products/presentation/screens/add_product_variant_screen.dart @@ -0,0 +1,504 @@ +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; +import 'package:image_picker/image_picker.dart'; +import 'package:niogu_app/core/components/top_back_bar_app.dart'; +import 'package:niogu_app/core/constants/app_color.dart'; +import 'package:niogu_app/core/constants/app_font_size.dart'; +import 'package:niogu_app/core/providers/app_provider.dart'; +import 'package:niogu_app/core/utils/extension_format.dart'; +import 'package:niogu_app/core/utils/image_service.dart'; +import 'package:niogu_app/core/utils/log_message.dart'; +import 'package:niogu_app/core/widgets/custom_snackbar.dart'; +import 'package:niogu_app/features/goods/products/domain/entities/product.dart'; +import 'package:niogu_app/features/goods/products/presentation/providers/product_provider.dart'; +import 'package:niogu_app/features/goods/products/presentation/widgets/product_variant_model.dart'; +import 'package:niogu_app/features/goods/products/presentation/widgets/manage_varian_shimmer.dart'; +import 'package:niogu_app/features/goods/products/presentation/widgets/variant_product_card.dart'; +import 'package:sizer/sizer.dart'; + +class AddProductVariantScreen extends ConsumerStatefulWidget { + final bool isMainOutlet; + final bool isStockFixed; + + const AddProductVariantScreen({super.key, required this.isMainOutlet, required this.isStockFixed}); + + @override + ConsumerState createState() => + _AddProductVariantScreenState(); +} + +class _AddProductVariantScreenState + extends ConsumerState { + List variants = [1, 2, 3]; + + final List> _formKeys = []; + + final List _variantModels = []; + + final ScrollController _scrollController = ScrollController(); + + final ImagePicker _picker = ImagePicker(); + + final List _pathImageTemps = []; + + bool _isLoading = true; + + @override + void initState() { + super.initState(); + + WidgetsBinding.instance.addPostFrameCallback((_) { + Future.delayed(const Duration(milliseconds: 800), () { + _fecthVariants(); + }); + }); + } + + @override + void dispose() { + for (var variant in _variantModels) { + variant.dispose(); + } + _scrollController.dispose(); + super.dispose(); + } + + void _fecthVariants() { + final data = ref.read(productWithVariantProvider); + if (data.isNotEmpty) { + for (int i = 0; i < data.length; i++) { + _formKeys.add(GlobalKey()); + _variantModels.add(ProductVariantModel()); + _variantModels[i].imagePath = data[i].imagePath; + _variantModels[i].nameController.text = data[i].name; + _variantModels[i].stockController.text = data[i].stock + .toStringWithoutTrailingZero(); + _variantModels[i].costPriceController.text = data[i].costPrice + .toStringWithoutTrailingZero(); + _variantModels[i].sellingPriceController.text = data[i].sellingPrice + .toStringWithoutTrailingZero(); + } + } else { + _formKeys.add(GlobalKey()); + _variantModels.add(ProductVariantModel()); + } + + setState(() { + _isLoading = false; + }); + } + + void _addVariant() { + setState(() { + _formKeys.add(GlobalKey()); + _variantModels.add(ProductVariantModel()); + }); + + WidgetsBinding.instance.addPostFrameCallback((_) { + if (_scrollController.hasClients) { + _scrollController.animateTo( + _scrollController.position.maxScrollExtent, + duration: const Duration(milliseconds: 400), + curve: Curves.easeOut, + ); + } + }); + } + + Future _removeVariant(int index) async { + setState(() { + _formKeys.removeAt(index); + _variantModels[index].dispose(); + _variantModels.removeAt(index); + }); + } + + Future _showPickerOptions(int index) async { + final bool isTablet = 100.w >= 600; + showModalBottomSheet( + context: context, + backgroundColor: Colors.white, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.vertical(top: Radius.circular(2.5.w)), + ), + constraints: const BoxConstraints(maxWidth: double.infinity), + builder: (BuildContext context) { + return SafeArea( + child: Container( + width: 100.w, + padding: EdgeInsets.symmetric(vertical: 2.h, horizontal: 5.w), + clipBehavior: Clip.hardEdge, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.vertical(top: Radius.circular(6.w)), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + ListTile( + leading: Icon(Icons.photo_library, size: 6.w), + title: Text( + 'Galeri', + style: TextStyle( + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + ), + ), + onTap: () async { + await _getImage(ImageSource.gallery, index); + context.pop(); + }, + ), + + SizedBox(height: 2.h), + + ListTile( + leading: Icon(Icons.photo_camera, size: 6.w), + title: Text( + 'Kamera', + style: TextStyle( + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + ), + ), + onTap: () async { + await _getImage(ImageSource.camera, index); + context.pop(); + }, + ), + ], + ), + ), + ); + }, + ); + } + + Future _getImage(ImageSource ImageSource, int index) async { + try { + final XFile? pickedFile = await _picker.pickImage( + source: ImageSource, + imageQuality: 70, + ); + + if (pickedFile != null) { + File tempFile = File(pickedFile.path); + + String? savedPath = await ImageService.saveImageToLocalDirectory( + tempFile, + "product_variant_images", + ); + + if (savedPath != null) { + _pathImageTemps.add(savedPath); + ref.read(variantImageTempProvider.notifier).state = _pathImageTemps; + setState(() { + _variantModels[index].imagePath = savedPath; + }); + } + } + } catch (e, st) { + LogMessage.log.w(e.toString(), error: e, stackTrace: st); + CustomSnackbar.showWarning(context, "Akses ditolak"); + } + } + + Future _removeImage(int index) async { + setState(() { + _variantModels[index].imagePath = null; + }); + } + + Future _cleanUpImages() async { + for (var path in _pathImageTemps) { + if (path != null) { + await ImageService.deleteLocalImage(path); + } + } + } + + void _saveVariant() { + final lastKey = _formKeys.last; + + if (!lastKey.currentState!.validate()) { + return; + } + + final List variants = []; + + try { + for (var _variantModel in _variantModels) { + final String? imagePath = _variantModel.imagePath; + + final String name = _variantModel.nameController.text.trim(); + + final double stock = widget.isStockFixed + ? double.parse(_variantModel.stockController.text.trim()) + : 0.0; + + final double costPrice = double.parse( + _variantModel.costPriceController.text.trim(), + ); + + final double sellingPrice = double.parse( + _variantModel.sellingPriceController.text.trim(), + ); + + if (costPrice >= sellingPrice) { + CustomSnackbar.showError( + context, + "Harga jual harus lebih besar dari modal", + ); + setState(() {}); + return; + } + + final productWithVariant = ProductWithVariant( + imagePath: imagePath, + name: name, + stock: stock, + costPrice: costPrice, + sellingPrice: sellingPrice, + ); + + variants.add(productWithVariant); + } + } catch (e) { + CustomSnackbar.showError(context, "Format angka tidak valid"); + return; + } + + ref.read(productWithVariantProvider.notifier).state = variants; + + CustomSnackbar.showSuccess( + context, + "${variants.length} varian produk berhasil dibuat", + ); + + context.pop(); + } + + @override + Widget build(BuildContext context) { + return LayoutBuilder( + builder: (context, constraints) { + final isTablet = 100.w >= 600; + + const backgroundColor = Color(0xFFF9FAFB); + + final currentUserRole = ref.watch(currentUserRoleProvider); + + return PopScope( + canPop: false, + onPopInvokedWithResult: (didPop, result) async { + if (didPop) return; + + await _cleanUpImages(); + + context.pop(); + }, + child: SafeArea( + top: false, + bottom: true, + right: false, + left: false, + child: Scaffold( + resizeToAvoidBottomInset: false, + backgroundColor: backgroundColor, + appBar: TopBackBarApp( + title: "Atur Varian Produk", + onTap: () async { + await _cleanUpImages(); + + context.pop(); + }, + ), + body: _isLoading + ? const ManageVariantShimmer() + : SingleChildScrollView( + controller: _scrollController, + padding: EdgeInsets.fromLTRB(5.w, 3.h, 5.w, 12.h), + child: Column( + children: [ + Container( + padding: EdgeInsets.symmetric( + horizontal: 2.w, + vertical: 1.h, + ), + margin: EdgeInsets.only(bottom: 2.h), + decoration: BoxDecoration( + color: Colors.blue[50], + borderRadius: BorderRadius.circular(4.w), + ), + child: Row( + children: [ + Icon( + Icons.info_outline, + color: AppColor.primaryColor, + size: 5.w, + ), + + SizedBox(width: 3.w), + + Expanded( + child: Text( + "Setiap varian dapat memiliki harga dan stok yang berbeda.", + style: TextStyle( + color: AppColor.primaryColor, + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + ), + ), + ), + ], + ), + ), + + ..._variantModels.asMap().entries.map((entry) { + final index = entry.key; + final variant = _variantModels[index]; + + return Padding( + padding: EdgeInsets.only(bottom: 3.h), + child: VariantProductCard( + userRole: currentUserRole!, + isMainOutlet: widget.isMainOutlet, + isEditing: false, + formKey: _formKeys[index], + length: _variantModels.length, + variant: variant, + index: index, + isStockFixed: widget.isStockFixed, + nameController: variant.nameController, + stockController: variant.stockController, + costPriceController: + variant.costPriceController, + sellingPriceController: + variant.sellingPriceController, + onTapRemoveVarian: () async => + await _removeVariant(index), + onTapVariantImageAdd: () async => + await _showPickerOptions(index), + onTapVariantImageRemove: () async => + await _removeImage(index), + ), + ); + }), + + GestureDetector( + onTap: () async { + FocusScope.of(context).unfocus(); + + await Future.delayed( + const Duration(milliseconds: 400), + ); + + final lastKey = _formKeys.last; + + if (!lastKey.currentState!.validate()) return; + + for (var variant in _variantModels) { + final double costPrice = double.parse( + variant.costPriceController.text.trim(), + ); + final double sellingPrice = double.parse( + variant.sellingPriceController.text + .trim(), + ); + + if (costPrice >= sellingPrice) { + CustomSnackbar.showError( + context, + "Harga jual harus lebih besar dari modal", + ); + return; + } + } + + _addVariant(); + }, + child: Container( + width: double.infinity, + padding: EdgeInsets.symmetric( + horizontal: 4.w, + vertical: 2.h, + ), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(3.w), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.add_circle_outline_rounded, + color: AppColor.primaryColor, + ), + + SizedBox(width: 3.w), + + Text( + "Tambah Varian Baru", + style: TextStyle( + color: AppColor.primaryColor, + fontSize: AppFontSize.medium.sp, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + ), + ), + ], + ), + ), + + bottomNavigationBar: _isLoading + ? const SizedBox() + : Container( + padding: EdgeInsets.all(5.w), + decoration: BoxDecoration( + color: Colors.white, + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.05), + blurRadius: 10, + offset: const Offset(0, -5), + ), + ], + ), + child: SizedBox( + height: 6.5.h, + child: ElevatedButton( + onPressed: _saveVariant, + style: ElevatedButton.styleFrom( + backgroundColor: AppColor.primaryColor, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(2.5.w), + ), + elevation: 0, + ), + child: Text( + "Simpan Perubahan", + style: TextStyle( + color: Colors.white, + fontSize: AppFontSize.medium.sp, + fontWeight: FontWeight.bold, + ), + ), + ), + ), + ), + ), + ), + ); + }, + ); + } +} diff --git a/Mobile Operasional/lib/features/goods/products/presentation/screens/delete_product_screen.dart b/Mobile Operasional/lib/features/goods/products/presentation/screens/delete_product_screen.dart new file mode 100644 index 0000000..f4246e1 --- /dev/null +++ b/Mobile Operasional/lib/features/goods/products/presentation/screens/delete_product_screen.dart @@ -0,0 +1,340 @@ +import 'dart:io'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; +import 'package:niogu_app/core/constants/app_font_size.dart'; +import 'package:niogu_app/core/utils/extension_format.dart'; +import 'package:niogu_app/core/utils/log_message.dart'; +import 'package:niogu_app/core/system/system_setting.dart'; +import 'package:niogu_app/core/widgets/custom_snackbar.dart'; +import 'package:niogu_app/features/goods/products/domain/entities/product.dart'; +import 'package:niogu_app/features/goods/products/presentation/providers/product_provider.dart'; +import 'package:sizer/sizer.dart'; + +class DeleteProductScreen extends ConsumerStatefulWidget { + final DeleteProductSnapshot productSnapshot; + const DeleteProductScreen({super.key, required this.productSnapshot}); + + @override + ConsumerState createState() => + _DeleteProductScreenState(); +} + +class _DeleteProductScreenState extends ConsumerState { + @override + void initState() { + // TODO: implement initState + super.initState(); + } + + @override + void dispose() { + // TODO: implement dispose + super.dispose(); + } + + Future _onDeleteCurrentProduct() async { + final outletId = await SystemSetting.getCurrentOutletId(); + + try { + await ref + .read(productControllerProvider.notifier) + .deleteProduct( + widget.productSnapshot.productId, + outletId: outletId!, + canDeleteAll: false, + ); + + if (!mounted) return; + + CustomSnackbar.showSuccess(context, 'Produk berhasil dihapus'); + context.pop(); + } catch (e, st) { + LogMessage.log.e(e.toString(), error: e, stackTrace: st); + CustomSnackbar.showError(context, "Ups, terjadi kesalahan"); + context.pop(); + } + } + + Future _onDeleteAllProduct() async { + try { + await ref + .read(productControllerProvider.notifier) + .deleteProduct(widget.productSnapshot.productId, canDeleteAll: true); + + if (!mounted) return; + + CustomSnackbar.showSuccess(context, 'Produk berhasil dihapus'); + context.pop(); + } catch (e, st) { + LogMessage.log.e(e.toString(), error: e, stackTrace: st); + CustomSnackbar.showError(context, "Ups, terjadi kesalahan"); + context.pop(); + } + } + + @override + Widget build(BuildContext context) { + final bool isTablet = 100.w >= 600; + final double maxWidth = 100.w >= 1280 + ? 1200 + : isTablet + ? 800 + : 400; + + final bool isMainOutlet = widget.productSnapshot.isMainOutlet; + + final int totalOutletCount = widget.productSnapshot.totalOutletCount; + + final bool isMultiLocation = isMainOutlet && totalOutletCount > 1; + + final bool imagePathAvailable = + widget.productSnapshot.imagePath != null && + widget.productSnapshot.imagePath!.isNotEmpty; + + final File imageFile = File( + imagePathAvailable + ? widget.productSnapshot.imagePath! + : "image not found", + ); + + final bool imageFileExists = imageFile.existsSync(); + + final productControllerState = ref.watch(productControllerProvider); + + return Dialog( + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(4.w)), + elevation: 0, + backgroundColor: Colors.transparent, + child: Container( + padding: EdgeInsets.all(5.w), + width: double.infinity, + constraints: BoxConstraints(maxWidth: maxWidth), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(4.w), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + padding: EdgeInsets.all(3.w), + decoration: BoxDecoration( + color: Colors.red[50], + shape: BoxShape.circle, + ), + child: Icon(Icons.delete_outline, color: Colors.red, size: 8.w), + ), + SizedBox(height: 2.h), + Text( + "Hapus Produk?", + style: TextStyle( + fontSize: AppFontSize.medium.sp, + fontWeight: FontWeight.bold, + color: Colors.black87, + ), + ), + SizedBox(height: 1.h), + Text( + "Tindakan ini tidak dapat dibatalkan.", + style: TextStyle( + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + color: Colors.grey[800], + ), + ), + SizedBox(height: 3.h), + + Container( + padding: EdgeInsets.symmetric(vertical: 3.w), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(2.5.w), + ), + child: Row( + children: [ + Container( + height: 15.w, + width: 15.w, + decoration: BoxDecoration( + color: Colors.grey[200], + border: imageFileExists + ? Border.all(color: Colors.grey) + : null, + borderRadius: BorderRadius.circular(2.5.w), + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(2.5.w), + child: imageFileExists + ? Image.file( + imageFile, + fit: BoxFit.cover, + width: double.infinity, + height: double.infinity, + errorBuilder: (context, error, stackTrace) { + return const Icon( + Icons.error, + color: Colors.grey, + ); + }, + ) + : Icon(Icons.image, color: Colors.grey, size: 8.w), + ), + ), + + SizedBox(width: 3.w), + + // Detail Text + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + widget.productSnapshot.name, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + fontWeight: FontWeight.bold, + color: Colors.black87, + ), + ), + SizedBox(height: 0.5.h), + // Badge Terjual + Container( + padding: EdgeInsets.symmetric( + horizontal: 2.w, + vertical: 0.5.h, + ), + decoration: BoxDecoration( + color: Colors.green[50], + borderRadius: BorderRadius.circular(1.w), + ), + child: Text( + "${widget.productSnapshot.totalSold.toCompact} Terjual", + style: TextStyle( + fontSize: isTablet + ? (AppFontSize.medium - 2).sp + : (AppFontSize.small - 2).sp, + color: Colors.green[700], + fontWeight: FontWeight.w600, + ), + ), + ), + SizedBox(height: 0.5.h), + + Row( + children: [ + Icon(Icons.store, size: 4.w, color: Colors.grey), + SizedBox(width: 1.w), + Expanded( + child: Text( + isMultiLocation + ? "${widget.productSnapshot.currentOutletName} (+${widget.productSnapshot.totalOutletCount - 1} lainnya)" + : widget.productSnapshot.currentOutletName, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontSize: isTablet + ? (AppFontSize.medium - 1).sp + : (AppFontSize.small - 1).sp, + color: Colors.grey[600], + ), + ), + ), + ], + ), + ], + ), + ), + ], + ), + ), + + SizedBox(height: 3.h), + + if (isMultiLocation) ...[ + _buildButton( + context, + label: "Hapus di Outlet Ini Saja", + color: const Color.fromARGB(255, 156, 10, 0), + onPressed: productControllerState.isLoading + ? null + : _onDeleteCurrentProduct, + ), + SizedBox(height: 1.5.h), + _buildButton( + context, + label: "Hapus di SEMUA Outlet", + color: Colors.red, + onPressed: productControllerState.isLoading + ? null + : _onDeleteAllProduct, + ), + ] else ...[ + _buildButton( + context, + label: "Ya, Hapus", + color: Colors.red, + onPressed: productControllerState.isLoading + ? null + : isMainOutlet && totalOutletCount == 1 + ? _onDeleteAllProduct + : _onDeleteCurrentProduct, + ), + ], + + SizedBox(height: 1.5.h), + + TextButton( + onPressed: productControllerState.isLoading + ? null + : () => context.pop(), + child: Text( + "Batal", + style: TextStyle( + color: Colors.grey[600], + fontSize: AppFontSize.medium.sp, + fontWeight: FontWeight.w600, + ), + ), + ), + ], + ), + ), + ); + } + + Widget _buildButton( + BuildContext context, { + required String label, + required Color color, + required VoidCallback? onPressed, + }) { + return SizedBox( + width: double.infinity, + height: 6.h, + child: ElevatedButton( + onPressed: onPressed, + style: ElevatedButton.styleFrom( + backgroundColor: color, + elevation: 0, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(2.5.w), + ), + disabledBackgroundColor: Colors.grey.shade300, + ), + child: Text( + label, + style: TextStyle( + color: Colors.white, + fontSize: AppFontSize.medium.sp, + fontWeight: FontWeight.bold, + ), + ), + ), + ); + } +} diff --git a/Mobile Operasional/lib/features/goods/products/presentation/screens/edit_product_category_screen.dart b/Mobile Operasional/lib/features/goods/products/presentation/screens/edit_product_category_screen.dart new file mode 100644 index 0000000..7510222 --- /dev/null +++ b/Mobile Operasional/lib/features/goods/products/presentation/screens/edit_product_category_screen.dart @@ -0,0 +1,235 @@ +import 'dart:async'; +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; +import 'package:image_picker/image_picker.dart'; +import 'package:niogu_app/core/utils/image_service.dart'; +import 'package:niogu_app/core/utils/log_message.dart'; +import 'package:niogu_app/core/widgets/custom_button.dart'; +import 'package:niogu_app/core/widgets/custom_snackbar.dart'; +import 'package:niogu_app/core/widgets/custom_text_form_field.dart'; +import 'package:niogu_app/features/goods/products/domain/entities/category.dart'; +import 'package:niogu_app/features/goods/products/presentation/providers/category_provider.dart'; +import 'package:niogu_app/features/goods/products/presentation/widgets/image_uploader_product_category.dart'; +import 'package:sizer/sizer.dart'; + +class EditProductCategoryScreen extends ConsumerStatefulWidget { + final Category category; + + const EditProductCategoryScreen({super.key, required this.category}); + + @override + ConsumerState createState() => + _EditProductCategoryScreenState(); +} + +class _EditProductCategoryScreenState + extends ConsumerState { + final GlobalKey _formKey = GlobalKey(); + + final TextEditingController _nameController = TextEditingController(); + + final ImagePicker _picker = ImagePicker(); + + bool _canPop = false; + + String? _categoryImage; + + String? _pathImageRemove; + + String? _pathImageTemp; + + Timer? _debounce; + + @override + void initState() { + // TODO: implement initState + super.initState(); + _categoryImage = widget.category.imagePath; + _nameController.text = widget.category.name; + } + + @override + void dispose() { + // TODO: implement dispose + _nameController.dispose(); + _debounce?.cancel(); + super.dispose(); + } + + Future _getImage() async { + try { + final XFile? pickedFile = await _picker.pickImage( + source: ImageSource.gallery, + imageQuality: 70, + ); + + if (pickedFile != null) { + File tempFile = File(pickedFile.path); + + String? savedPath = await ImageService.saveImageToLocalDirectory( + tempFile, + "product_category_images", + ); + + if (savedPath != null) { + _pathImageTemp = savedPath; + setState(() { + _categoryImage = savedPath; + }); + } + } + } catch (e, st) { + LogMessage.log.w(e.toString(), error: e, stackTrace: st); + CustomSnackbar.showWarning(context, "Akses ditolak"); + } + } + + Future _removeImage() async { + _pathImageRemove = _categoryImage; + setState(() { + _categoryImage = null; + }); + } + + Future _cleanUpImages() async { + if (_pathImageTemp != null && _pathImageTemp!.isNotEmpty) { + await ImageService.deleteLocalImage(_pathImageTemp!); + } + } + + void _onChanged(String value) { + if (_debounce?.isActive ?? false) _debounce!.cancel(); + + _debounce = Timer(const Duration(milliseconds: 500), () async { + if (value.isEmpty) return; + + ref.read(categorySearchProvider.notifier).state = value; + }); + } + + Future _onCancel() async { + await _cleanUpImages(); + + if (mounted) { + setState(() { + _canPop = true; + }); + context.pop(); + } + } + + Future _updateCategory( + String id, + String name, + String? imagePath, + ) async { + if (_pathImageRemove != null && _pathImageRemove!.isNotEmpty) { + await ImageService.deleteLocalImage(_pathImageRemove!); + } + + try { + await ref + .read(categoryControllerProvider.notifier) + .updateCategory(id, name, imagePath); + + if (!mounted) return; + + CustomSnackbar.showSuccess(context, "Kategori berhasil diubah"); + context.pop(); + } catch (e, st) { + LogMessage.log.i(e.toString(), error: e, stackTrace: st); + CustomSnackbar.showError(context, "Ups, terjadi kesalahan"); + context.pop(); + } + } + + @override + Widget build(BuildContext context) { + final bool isTablet = 100.w >= 600; + + final double maxWidth = 100.w >= 1280 + ? 1200 + : isTablet + ? 800 + : 400; + final validateCategory = ref.watch(validateCategoryProvider); + + final categoryControllerState = ref.watch(categoryControllerProvider); + + return PopScope( + canPop: _canPop, + onPopInvokedWithResult: (didPop, result) async { + if (didPop) return; + + await _onCancel(); + }, + child: Dialog( + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15)), + backgroundColor: Colors.white, + child: Form( + key: _formKey, + child: Container( + padding: EdgeInsets.all(5.w), + constraints: BoxConstraints(maxWidth: maxWidth), + child: SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + SizedBox(height: 1.h), + + ImageUploaderProductCategory( + categoryImage: _categoryImage, + onTapCategoryImageAdd: () async => await _getImage(), + onTapCategoryImageRemove: () async => await _removeImage(), + ), + + SizedBox(height: 3.h), + + CustomTextFormField( + label: "Nama Kategori", + hint: "Contoh : Snack", + controller: _nameController, + validator: (value) { + if (value == null || value.trim().isEmpty) { + return "Nama kategori belum diisi"; + } + + return null; + }, + errorText: validateCategory + ? null + : "Kategori ini telah tersedia", + onChanged: (value) => _onChanged(value), + ), + + SizedBox(height: 3.h), + + CustomButton( + title: "Simpan", + onPressed: categoryControllerState.isLoading + ? null + : () async { + if (_formKey.currentState!.validate() && + ref.read(validateCategoryProvider)) { + await _updateCategory( + widget.category.id, + _nameController.text.trim(), + _categoryImage, + ); + } + }, + ), + + SizedBox(height: 1.h), + ], + ), + ), + ), + ), + ), + ); + } +} diff --git a/Mobile Operasional/lib/features/goods/products/presentation/screens/edit_product_screen.dart b/Mobile Operasional/lib/features/goods/products/presentation/screens/edit_product_screen.dart new file mode 100644 index 0000000..3542c0e --- /dev/null +++ b/Mobile Operasional/lib/features/goods/products/presentation/screens/edit_product_screen.dart @@ -0,0 +1,1522 @@ +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; +import 'package:image_picker/image_picker.dart'; +import 'package:niogu_app/core/components/top_back_bar_app.dart'; +import 'package:niogu_app/core/constants/app_color.dart'; +import 'package:niogu_app/core/constants/app_font_size.dart'; +import 'package:niogu_app/core/providers/app_provider.dart'; +import 'package:niogu_app/core/router/app_route.dart'; +import 'package:niogu_app/core/utils/extension_format.dart'; +import 'package:niogu_app/core/utils/image_service.dart'; +import 'package:niogu_app/core/enums/item_type.dart'; +import 'package:niogu_app/core/utils/log_message.dart'; +import 'package:niogu_app/core/enums/stock_card_type.dart'; +import 'package:niogu_app/core/enums/stock_type.dart'; +import 'package:niogu_app/core/system/system_setting.dart'; +import 'package:niogu_app/core/enums/user_role.dart'; +import 'package:niogu_app/core/widgets/custom_confirmation.dart'; +import 'package:niogu_app/core/widgets/custom_selector.dart'; +import 'package:niogu_app/core/widgets/custom_snackbar.dart'; +import 'package:niogu_app/core/widgets/custom_text_form_field.dart'; +import 'package:niogu_app/core/widgets/pop_up_notification.dart'; +import 'package:niogu_app/features/goods/domain/entities/stock_card.dart'; +import 'package:niogu_app/features/goods/products/domain/entities/category.dart'; +import 'package:niogu_app/features/goods/products/domain/entities/product.dart'; +import 'package:niogu_app/features/goods/products/presentation/providers/category_provider.dart'; +import 'package:niogu_app/features/goods/products/presentation/providers/product_provider.dart'; +import 'package:niogu_app/features/goods/products/presentation/screens/add_product_category_screen.dart'; +import 'package:niogu_app/features/goods/products/presentation/screens/edit_product_category_screen.dart'; +import 'package:niogu_app/features/goods/products/presentation/widgets/edit_product_shimmer.dart'; +import 'package:niogu_app/features/goods/products/presentation/widgets/add_photo_button.dart'; +import 'package:niogu_app/features/goods/presentation/widgets/dropdown_with_action.dart'; +import 'package:niogu_app/features/goods/products/presentation/widgets/image_preview_card.dart'; +import 'package:niogu_app/features/goods/products/presentation/widgets/selection_card.dart'; +import 'package:niogu_app/features/goods/units/domain/entities/unit.dart'; +import 'package:niogu_app/features/goods/units/presentation/providers/unit_provider.dart'; +import 'package:niogu_app/features/goods/units/presentation/screens/add_unit_screen.dart'; +import 'package:niogu_app/features/goods/units/presentation/screens/edit_unit_screen.dart'; +import 'package:niogu_app/features/goods/domain/entities/outlet_inventory.dart'; +import 'package:niogu_app/features/outlets/presentation/providers/outlet_provider.dart'; +import 'package:sizer/sizer.dart'; + +class EditProductScreen extends ConsumerStatefulWidget { + final String productId; + final bool isMainOutlet; + final bool isEditing; + final bool hasNewVariant; + final int newVariantCount; + const EditProductScreen({ + super.key, + required this.productId, + this.isMainOutlet = true, + this.isEditing = true, + this.hasNewVariant = false, + this.newVariantCount = 0, + }); + + @override + ConsumerState createState() => _EditProductScreenState(); +} + +class _EditProductScreenState extends ConsumerState { + final GlobalKey _formKey = GlobalKey(); + + late final String _productId; + + late final bool _isMainOutlet; + + late final bool _isEditing; + + late final bool _hasNewVariant; + + late final int _newVariantCount; + + final TextEditingController _nameController = TextEditingController(); + + final TextEditingController _costPriceController = TextEditingController(); + + final TextEditingController _sellingPriceController = TextEditingController(); + + final TextEditingController _stockController = TextEditingController(); + + final ImagePicker _picker = ImagePicker(); + + List _productImages = []; + + bool _hasVariant = false; + + StockType _initialStockType = StockType.fixed; + + StockType _stockType = StockType.fixed; + + bool _isStockAlwaysAvailable = false; + + String? _selectedCategory; + + String? _selectedUnit; + + String? _productVariantId; + + String? _outletInventoryId; + + final List _pathImageRemoves = []; + + final List _pathImageTemps = []; + + final List _deleteVariants = []; + + bool _isLoading = true; + + List _outletInventoryIds = []; + + List _stockPrevs = []; + + @override + void initState() { + // TODO: implement initState + super.initState(); + + _productId = widget.productId; + + _isMainOutlet = widget.isMainOutlet; + + _isEditing = widget.isEditing; + + _hasNewVariant = widget.hasNewVariant; + + _newVariantCount = widget.newVariantCount; + + WidgetsBinding.instance.addPostFrameCallback((_) { + Future.microtask(() { + ref.invalidate(deleteProductVariantProvider); + ref.invalidate(variantImageTempProvider); + }); + + Future.delayed(const Duration(milliseconds: 800), () { + _fetchProduct(); + }); + }); + } + + @override + void dispose() { + // TODO: implement dispose + _nameController.dispose(); + _costPriceController.dispose(); + _sellingPriceController.dispose(); + _stockController.dispose(); + super.dispose(); + } + + Future _fetchProduct() async { + try { + final productRepository = ref.read(productRepositoryProvider); + + final product = await productRepository.fetchProductById( + _productId, + _isEditing, + ); + + if (!mounted) return; + + _productImages = product.productImages ?? []; + _nameController.text = product.name; + _selectedCategory = product.categoryId; + _hasVariant = product.hasVariant; + if (_hasVariant) { + final variants = product.variants.map((variant) { + return ProductWithVariant( + productVariantId: variant.productVariantId, + outletInventoryId: variant.outletInventoryId, + imagePath: variant.image, + name: variant.name, + stock: variant.stock, + costPrice: variant.costPrice, + sellingPrice: variant.sellingPrice, + ); + }).toList(); + + _outletInventoryIds = product.variants.map((variant) { + return variant.outletInventoryId; + }).toList(); + + _stockPrevs = product.variants.map((variant) { + return variant.stock; + }).toList(); + + ref.read(productWithVariantProvider.notifier).state = variants; + + _isStockAlwaysAvailable = + product.variants.last.stockType == StockType.unlimited; + } else { + _productVariantId = product.variants.single.productVariantId; + + _outletInventoryId = product.variants.single.outletInventoryId; + + _costPriceController.text = product.variants.single.costPrice + .toStringWithoutTrailingZero(); + + _sellingPriceController.text = product.variants.single.sellingPrice + .toStringWithoutTrailingZero(); + + _isStockAlwaysAvailable = + product.variants.single.stockType == StockType.unlimited; + + _stockController.text = product.variants.single.stock + .toStringWithoutTrailingZero(); + + _outletInventoryIds.add(product.variants.single.outletInventoryId); + + _stockPrevs.add(product.variants.single.stock); + } + + _initialStockType = _isStockAlwaysAvailable + ? StockType.unlimited + : StockType.fixed; + + _stockType = _initialStockType; + + _selectedUnit = product.unitId; + + setState(() { + _isLoading = false; + }); + } catch (e, st) { + if (!mounted) return; + + setState(() { + _isLoading = false; + }); + + LogMessage.log.e(e.toString(), error: e, stackTrace: st); + + CustomSnackbar.showError(context, "Ups, terjadi kesalahan"); + + context.pop(); + } + } + + List _images(UserRole userRole) { + if (userRole == UserRole.owner && _isMainOutlet) { + return [ + ..._productImages.asMap().entries.map((entry) { + final int index = entry.key; + final String path = entry.value; + + final File imageFile = File( + path.isNotEmpty ? path : "image not found", + ); + + final bool imageFileExists = imageFile.existsSync(); + + return Padding( + padding: EdgeInsets.only(right: 3.w), + child: ImagePreviewCard( + imageFileExists: imageFileExists, + imageFile: imageFile, + onTap: () => _removeImage(index), + ), + ); + }).toList(), + + if (_productImages.length < 3) + AddPhotoButton(onTap: _showPickerOptions), + ]; + } + + return [ + if (_productImages.isEmpty) + ...[0, 1, 2].map((_) { + return Container( + width: 30.w, + height: 30.w, + margin: EdgeInsets.only(right: 3.w), + decoration: BoxDecoration( + color: Colors.grey[200], + borderRadius: BorderRadius.circular(3.w), + ), + child: Icon( + Icons.image_outlined, + color: Colors.grey[400], + size: 8.w, + ), + ); + }).toList() + else ...[ + ..._productImages.map((imagePath) { + final bool imagePathAvailable = imagePath.isNotEmpty; + + final File imageFile = File( + imagePathAvailable ? imagePath : "image not found", + ); + + final bool imageFileExists = imageFile.existsSync(); + + return Container( + width: 30.w, + height: 30.w, + margin: EdgeInsets.only(right: 2.w), + decoration: BoxDecoration( + color: Colors.grey[200], + borderRadius: BorderRadius.circular(3.w), + ), + child: imageFileExists + ? ClipRRect( + borderRadius: BorderRadius.circular(2.5.w), + child: Image.file( + imageFile, + fit: BoxFit.cover, + width: double.infinity, + height: double.infinity, + errorBuilder: (context, error, stackTrace) { + return Icon( + Icons.error, + color: Colors.grey[400], + size: 8.w, + ); + }, + ), + ) + : Icon( + Icons.image_outlined, + color: Colors.grey[400], + size: 8.w, + ), + ); + }).toList(), + if (_productImages.length < 3) + ...(_productImages.length == 1 ? [0, 1] : [0]).map((_) { + return Container( + width: 30.w, + height: 30.w, + margin: EdgeInsets.only(right: 3.w), + decoration: BoxDecoration( + color: Colors.grey[200], + borderRadius: BorderRadius.circular(3.w), + ), + child: Icon( + Icons.image_outlined, + color: Colors.grey[400], + size: 8.w, + ), + ); + }).toList(), + ], + ]; + } + + String? _validator({ + String? value, + required String message1, + String? message2, + bool isNumber = false, + bool isCostPrice = false, + bool isSellingPrice = false, + String? errorMessage, + }) { + if (value == null || value.isEmpty) { + return message1; + } + + try { + if (isNumber && double.parse(value) <= 0) { + if (isCostPrice) return message2; + + if (isSellingPrice) return message2; + } + } catch (e) { + return errorMessage; + } + + return null; + } + + Future _showPickerOptions() async { + final bool isTablet = 100.w >= 600; + showModalBottomSheet( + context: context, + backgroundColor: Colors.white, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.vertical(top: Radius.circular(2.5.w)), + ), + constraints: const BoxConstraints(maxWidth: double.infinity), + builder: (BuildContext context) { + return SafeArea( + child: Container( + width: 100.w, + padding: EdgeInsets.symmetric(vertical: 2.h, horizontal: 5.w), + clipBehavior: Clip.hardEdge, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.vertical(top: Radius.circular(6.w)), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + ListTile( + leading: Icon(Icons.photo_library, size: 6.w), + title: Text( + 'Galeri', + style: TextStyle( + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + ), + ), + onTap: () async { + await _getImage(ImageSource.gallery); + context.pop(); + }, + ), + + SizedBox(height: 2.h), + + ListTile( + leading: Icon(Icons.photo_camera, size: 6.w), + title: Text( + 'Kamera', + style: TextStyle( + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + ), + ), + onTap: () async { + await _getImage(ImageSource.camera); + context.pop(); + }, + ), + ], + ), + ), + ); + }, + ); + } + + Future _getImage(ImageSource ImageSource) async { + if (_productImages.length >= 3) { + CustomSnackbar.showError(context, "Maksimal 3 foto produk!"); + return; + } + + try { + final XFile? pickedFile = await _picker.pickImage( + source: ImageSource, + imageQuality: 70, + ); + + if (pickedFile != null) { + File tempFile = File(pickedFile.path); + + String? savedPath = await ImageService.saveImageToLocalDirectory( + tempFile, + "product_images", + ); + + if (savedPath != null) { + _pathImageTemps.add(savedPath); + setState(() { + _productImages.add(savedPath); + }); + } + } + } catch (e, st) { + LogMessage.log.w(e.toString(), error: e, stackTrace: st); + CustomSnackbar.showWarning(context, "Akses ditolak"); + } + } + + Future _removeImage(int index) async { + _pathImageRemoves.add(_productImages[index]); + setState(() { + _productImages.removeAt(index); + }); + } + + Future _cleanUpImages(List variantImageTemps) async { + for (var path in _pathImageTemps) { + if (path != null) { + await ImageService.deleteLocalImage(path); + } + } + + for (var path in variantImageTemps) { + if (path != null) { + await ImageService.deleteLocalImage(path); + } + } + } + + double _totalStock(List productWithVariant) { + double totalStock = 0; + for (var variant in productWithVariant) { + if (variant.productVariantId != null && + variant.outletInventoryId != null) { + totalStock += variant.stock; + } + } + + return totalStock; + } + + void _handleProductHasNotVariant() { + final productWithVariant = ref.read(productWithVariantProvider); + + if (_hasVariant && productWithVariant.isNotEmpty) { + if (_totalStock(productWithVariant) > 0) { + showDialog( + context: context, + builder: (context) { + return CustomConfirmation( + title: "Ubah Jenis Produk ?", + body: + "Produk akan diubah menjadi 'Tidak Memiliki Varian'. Semua varian sebelumnya akan dihapus secara otomatis di seluruh outlet.", + color: Color(0xFFFFA000), + icon: Icons.warning_amber_rounded, + onConfirm: () { + for (var variant in productWithVariant) { + if (variant.productVariantId != null && + variant.outletInventoryId != null) { + _deleteVariants.add( + DeleteProductVariant( + productId: _productId, + productVariantId: variant.productVariantId!, + outletInventoryId: variant.outletInventoryId!, + ), + ); + } + } + + ref.read(deleteProductVariantProvider.notifier).state = [ + ..._deleteVariants, + ]; + + setState(() { + _hasVariant = false; + _stockController.text = "0"; + }); + + ref.read(productWithVariantProvider.notifier).state = []; + CustomSnackbar.showSuccess(context, "Jenis produk diubah"); + }, + ); + }, + ); + } else { + for (var variant in productWithVariant) { + if (variant.productVariantId != null && + variant.outletInventoryId != null) { + _deleteVariants.add( + DeleteProductVariant( + productId: _productId, + productVariantId: variant.productVariantId!, + outletInventoryId: variant.outletInventoryId!, + ), + ); + } + } + + ref.read(deleteProductVariantProvider.notifier).state = [ + ..._deleteVariants, + ]; + + setState(() { + _hasVariant = false; + _stockController.text = "0"; + }); + + ref.read(productWithVariantProvider.notifier).state = []; + CustomSnackbar.showSuccess(context, "Jenis produk diubah"); + } + } else { + ref.read(deleteProductVariantProvider.notifier).state = []; + setState(() => _hasVariant = false); + } + } + + void _handleProductHasVariant() { + if (!_hasVariant && + !_isStockAlwaysAvailable && + _stockController.text != "0") { + showDialog( + context: context, + builder: (context) { + return CustomConfirmation( + title: "Ubah Jenis Produk ?", + body: + "Produk akan diubah menjadi 'Memiliki Varian'. Sisa stok sebelumnya akan dihapus secara otomatis di seluruh outlet.", + color: Color(0xFFFFA000), + icon: Icons.warning_amber_rounded, + onConfirm: () { + _deleteVariants.add( + DeleteProductVariant( + productId: _productId, + productVariantId: _productVariantId!, + outletInventoryId: _outletInventoryId!, + ), + ); + + ref.read(deleteProductVariantProvider.notifier).state = [ + ..._deleteVariants, + ]; + + setState(() { + _hasVariant = true; + _stockController.text = "0"; + }); + CustomSnackbar.showSuccess(context, "Jenis produk diubah"); + }, + ); + }, + ); + } else if (!_hasVariant && _isStockAlwaysAvailable) { + if (_productVariantId != null && _outletInventoryId != null) { + _deleteVariants.add( + DeleteProductVariant( + productId: _productId, + productVariantId: _productVariantId!, + outletInventoryId: _outletInventoryId!, + ), + ); + + ref.read(deleteProductVariantProvider.notifier).state = [ + ..._deleteVariants, + ]; + } + setState(() { + _hasVariant = true; + }); + } else if (!_hasVariant && + !_isStockAlwaysAvailable && + _stockController.text == "0") { + if (_productVariantId != null && _outletInventoryId != null) { + _deleteVariants.add( + DeleteProductVariant( + productId: _productId, + productVariantId: _productVariantId!, + outletInventoryId: _outletInventoryId!, + ), + ); + + ref.read(deleteProductVariantProvider.notifier).state = [ + ..._deleteVariants, + ]; + } + + setState(() { + _hasVariant = true; + }); + } + } + + void _handleStockFixed() { + final productWithVariant = ref.read(productWithVariantProvider); + + if (_hasVariant && productWithVariant.isNotEmpty) { + final variants = productWithVariant.map((variant) { + final double stock = + variant.productVariantId == null && + variant.outletInventoryId == null + ? variant.stock + : 0.0; + + return ProductWithVariant( + productVariantId: variant.productVariantId, + outletInventoryId: variant.outletInventoryId, + imagePath: variant.imagePath, + name: variant.name, + stock: stock, + costPrice: variant.costPrice, + sellingPrice: variant.sellingPrice, + ); + }).toList(); + + ref.read(productWithVariantProvider.notifier).state = variants; + } + setState(() { + _isStockAlwaysAvailable = false; + _stockType = StockType.fixed; + }); + CustomSnackbar.showSuccess(context, "Jenis stok diubah"); + } + + void _handleStockAlwaysAvailbale() { + final productWithVariant = ref.read(productWithVariantProvider); + + if (!_isStockAlwaysAvailable && + _hasVariant && + productWithVariant.isNotEmpty) { + if (_totalStock(productWithVariant) > 0) { + showDialog( + context: context, + builder: (context) { + return CustomConfirmation( + title: "Ubah Jenis Stok ?", + body: + "Stok produk akan diubah menjadi 'Selalu Ada'. Sisa stok semua varian sebelumnya akan dihapus secara otomatis.", + color: Color(0xFFFFA000), + icon: Icons.warning_amber_rounded, + onConfirm: () { + final variants = productWithVariant.map((variant) { + final double stock = + variant.productVariantId == null && + variant.outletInventoryId == null + ? variant.stock + : 0.0; + + return ProductWithVariant( + productVariantId: variant.productVariantId, + outletInventoryId: variant.outletInventoryId, + imagePath: variant.imagePath, + + name: variant.name, + stock: stock, + costPrice: variant.costPrice, + sellingPrice: variant.sellingPrice, + ); + }).toList(); + + ref.read(productWithVariantProvider.notifier).state = variants; + + setState(() { + _isStockAlwaysAvailable = true; + _stockType = StockType.unlimited; + }); + CustomSnackbar.showSuccess(context, "Jenis stok diubah"); + }, + ); + }, + ); + } else { + setState(() { + _isStockAlwaysAvailable = true; + _stockType = StockType.unlimited; + }); + CustomSnackbar.showSuccess(context, "Jenis stok diubah"); + } + + return; + } + + final double stock = double.parse(_stockController.text.trim()); + + if (!_isStockAlwaysAvailable && + !_hasVariant && + stock > 0 && + productWithVariant.isEmpty) { + showDialog( + context: context, + builder: (context) { + return CustomConfirmation( + title: "Ubah Jenis Stok ?", + body: + "Stok produk akan diubah menjadi 'Selalu Ada'. Sisa stok sebelumnya akan dihapus secara otomatis.", + color: Color(0xFFFFA000), + icon: Icons.warning_amber_rounded, + onConfirm: () { + setState(() { + _isStockAlwaysAvailable = true; + _stockType = StockType.unlimited; + _stockController.text = "0"; + }); + CustomSnackbar.showSuccess(context, "Jenis stok diubah"); + }, + ); + }, + ); + return; + } + + if (!_isStockAlwaysAvailable) { + setState(() { + _isStockAlwaysAvailable = true; + _stockType = StockType.unlimited; + }); + CustomSnackbar.showSuccess(context, "Jenis stok diubah"); + } + } + + Future _saveProduct() async { + if (!_formKey.currentState!.validate()) return; + + if (_selectedCategory == null) { + CustomSnackbar.showError(context, "Kategori produk belum dipilih"); + return; + } + + if (_selectedUnit == null) { + CustomSnackbar.showError(context, "Satuan produk belum dipilih"); + return; + } + + final productWithVariant = ref.read(productWithVariantProvider); + + if (_hasVariant && productWithVariant.isEmpty) { + CustomSnackbar.showError(context, "Varian produk belum diatur"); + return; + } + + if (!_hasVariant) { + final double costPrice = double.parse(_costPriceController.text.trim()); + final double sellingPrice = double.parse( + _sellingPriceController.text.trim(), + ); + + if (costPrice >= sellingPrice) { + CustomSnackbar.showError( + context, + "Harga jual harus lebih besar dari modal", + ); + return; + } + } + + final product = UpsertProduct( + localId: _productId, + categoryId: _selectedCategory!, + unitId: _selectedUnit!.trim(), + name: _nameController.text, + localImagePath: _productImages, + hasVariant: _hasVariant, + ); + + final List productVariants = []; + + final List outletInventories = []; + + final outletId = await SystemSetting.getCurrentOutletId(); + + final userId = await SystemSetting.getCurrentUserId(); + + final List stockCards = []; + + final stockType = _isStockAlwaysAvailable + ? StockType.unlimited + : StockType.fixed; + + bool modeChange = false; + + String? modeBeforeChange; + + String? modeAfterChange; + + if (_initialStockType != _stockType) { + modeChange = true; + modeBeforeChange = _initialStockType.type; + modeAfterChange = _stockType.type; + } + + modeChange = + modeChange && modeBeforeChange != null && modeAfterChange != null; + + List? deleteVariants; + + if (ref.read(deleteProductVariantProvider).isNotEmpty) { + deleteVariants = ref.read(deleteProductVariantProvider); + } + + if (_hasVariant && productWithVariant.isNotEmpty) { + for (var variant in productWithVariant) { + productVariants.add( + UpsertProductVariant( + localId: variant.productVariantId, + productId: product.localId, + outletInventoryId: variant.outletInventoryId, + name: variant.name, + localImagePath: variant.imagePath, + stock: variant.stock, + costPrice: variant.costPrice, + sellingPrice: variant.sellingPrice, + isProductVariant: true, + ), + ); + } + + for (var variant in productVariants) { + outletInventories.add( + UpsertOutletInventory( + localId: !_isEditing ? null : variant.outletInventoryId, + outletId: outletId!, + itemType: ItemType.product_variant, + itemId: variant.localId, + stockType: stockType, + stock: _isStockAlwaysAvailable ? 0.0 : variant.stock!, + costPrice: variant.costPrice!, + sellingPrice: variant.sellingPrice!, + purchasePrice: 0.0, + ), + ); + } + + final dataIds = _outletInventoryIds.map((inventory) => inventory).toSet(); + + final dataStocks = _stockPrevs.map((stock) => stock).toList(); + + final results = outletInventories.where((inventory) { + return !dataIds.contains(inventory.localId) || + (dataIds.contains(inventory.localId) && + inventory.stock != + dataStocks[_outletInventoryIds.indexOf( + inventory.localId, + )]) || + modeChange; + }).toList(); + + if (results.isNotEmpty) { + for (var inventory in results) { + stockCards.add( + StockCard( + outletInventoryId: inventory.localId, + userId: userId!, + stockCardType: StockCardType.initial_stock, + qtyChange: inventory.stock, + currentStock: inventory.stock, + costPrice: inventory.costPrice, + stockType: stockType, + modeChange: modeChange, + modeBeforeChange: modeBeforeChange, + modeAfterChange: modeAfterChange, + ), + ); + } + } + } else { + productVariants.add( + UpsertProductVariant( + localId: _productVariantId, + productId: product.localId, + name: product.name.trim(), + isProductVariant: false, + ), + ); + + final double stock = _isStockAlwaysAvailable + ? 0.0 + : double.parse(_stockController.text.trim()); + + final double costPrice = double.parse(_costPriceController.text.trim()); + + outletInventories.add( + UpsertOutletInventory( + localId: !_isEditing ? null : _outletInventoryId, + outletId: outletId!, + itemType: ItemType.product_variant, + itemId: productVariants.single.localId, + stockType: stockType, + stock: stock, + costPrice: costPrice, + sellingPrice: double.parse(_sellingPriceController.text.trim()), + purchasePrice: 0.0, + ), + ); + + final results = outletInventories.where((inventory) { + return !_outletInventoryIds.contains(inventory.localId) || + (_outletInventoryIds.contains(inventory.localId) && + inventory.stock != _stockPrevs.single) || + modeChange || + !_isEditing; + }).toList(); + + if (results.isNotEmpty) { + stockCards.add( + StockCard( + outletInventoryId: results.single.localId, + userId: userId!, + stockCardType: StockCardType.initial_stock, + qtyChange: stock, + currentStock: stock, + costPrice: costPrice, + stockType: stockType, + modeChange: modeChange, + modeBeforeChange: modeBeforeChange, + modeAfterChange: modeAfterChange, + ), + ); + } + } + + try { + await ref + .read(productControllerProvider.notifier) + .saveProduct( + product: product, + productVariants: productVariants, + deleteVariants: deleteVariants, + outletInventories: outletInventories, + stockCards: stockCards, + modeChange: modeChange, + modeBeforeChange: modeBeforeChange, + modeAfterChange: modeAfterChange, + ); + + if (!mounted) return; + + for (var path in _pathImageRemoves) { + await ImageService.deleteLocalImage(path); + } + + ref.invalidate(deleteProductVariantProvider); + + ref.invalidate(variantImageTempProvider); + + CustomSnackbar.showSuccess( + context, + _isEditing ? "Produk diperbarui" : "Produk berhasil disimpan", + ); + + context.pop(); + } catch (e, st) { + LogMessage.log.e(e.toString(), error: e, stackTrace: st); + + CustomSnackbar.showError(context, "Ups, terjadi kesalahan"); + } + } + + @override + Widget build(BuildContext context) { + return LayoutBuilder( + builder: (context, constraints) { + final bool isTablet = 100.w >= 600; + + final messageActivity = ref.watch(messageActivityProvider); + + final currentUserRole = ref.watch(currentUserRoleProvider); + + final productWithVariantState = ref.watch(productWithVariantProvider); + + final selectedNewVariantState = ref.watch(selectedNewVariantProvider); + + final bool showStockField = !_hasVariant && !_isStockAlwaysAvailable; + + final List categories = []; + + final categoryState = ref.watch(categoryStreamProvider); + + categoryState.whenData((c) => categories.addAll(c)); + + final List units = []; + + final unitState = ref.watch(unitStreamProvider); + + unitState.whenData((u) => units.addAll(u)); + + final productControllerState = ref.watch(productControllerProvider); + + String selectorTitle = "Atur varian produk"; + + if (productWithVariantState.isNotEmpty) { + selectorTitle = + "Lihat ${productWithVariantState.length} varian produk"; + + if (_hasNewVariant && _newVariantCount > 0) { + final bool hasNewVariant = selectedNewVariantState.isNotEmpty; + + if (hasNewVariant) { + selectorTitle += " (termasuk varian baru)"; + } else { + selectorTitle += " (+ ${_newVariantCount} varian baru)"; + } + } + } + + return PopScope( + canPop: false, + onPopInvokedWithResult: (didPop, result) async { + if (didPop) return; + + final variantImageTemps = ref.read(variantImageTempProvider); + + await _cleanUpImages(variantImageTemps); + + ref.invalidate(deleteProductVariantProvider); + + ref.invalidate(variantImageTempProvider); + + context.pop(); + }, + child: SafeArea( + top: false, + bottom: true, + right: false, + left: false, + child: Stack( + children: [ + Scaffold( + backgroundColor: const Color(0xFFF9FAFB), + appBar: TopBackBarApp( + title: _isEditing ? "Edit Produk" : "Tambah Produk", + onTap: () async { + final variantImageTemps = ref.read( + variantImageTempProvider, + ); + + await _cleanUpImages(variantImageTemps); + + ref.invalidate(deleteProductVariantProvider); + + ref.invalidate(variantImageTempProvider); + + context.pop(); + }, + ), + + body: _isLoading + ? const EditProductShimmer() + : SingleChildScrollView( + padding: EdgeInsets.symmetric( + horizontal: 5.w, + vertical: 3.h, + ), + child: Form( + key: _formKey, + child: Column( + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Text( + "Foto Produk", + style: TextStyle( + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + fontWeight: FontWeight.bold, + color: Colors.black87, + ), + ), + Text( + "${_productImages.length}/3", + style: TextStyle( + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + color: Colors.grey[600], + ), + ), + ], + ), + SizedBox(height: 1.5.h), + + SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Row( + children: _images(currentUserRole!), + ), + ), + ], + ), + + SizedBox(height: 3.h), + + CustomTextFormField( + label: "Nama Produk", + hint: "Contoh: Kopi Susu Gula Aren", + controller: _nameController, + validator: (value) => _validator( + value: value, + message1: "Nama produk belum diisi", + ), + readOnly: + (currentUserRole == UserRole.owner && + !_isMainOutlet) || + currentUserRole == UserRole.admin, + ), + + SizedBox(height: 2.5.h), + + if (currentUserRole == UserRole.owner && + _isMainOutlet) + DropdownWithAction( + label: "Kategori Produk", + value: _selectedCategory, + items: categories, + onChanged: (val) => + setState(() => _selectedCategory = val), + onAdd: () async { + FocusScope.of(context).unfocus(); + + await Future.delayed( + const Duration(milliseconds: 400), + ); + + showDialog( + context: context, + builder: (_) => + const AddProductCategoryScreen(), + ); + }, + onEdit: () async { + if (_selectedCategory == null) { + CustomSnackbar.showError( + context, + "Kategori belum dipilih", + ); + } else { + final categoryRepository = ref.read( + categoryRepositoryProvider, + ); + + final category = + await categoryRepository + .fetchCategoryById( + _selectedCategory!, + ); + + if (category != null) { + showDialog( + context: context, + builder: (_) => + EditProductCategoryScreen( + category: category, + ), + ); + } else { + CustomSnackbar.showError( + context, + "Kategori produk tidak tersedia", + ); + } + } + }, + ) + else + CustomTextFormField( + label: "Kategori Produk", + controller: TextEditingController( + text: categories + .firstWhere( + (category) => + category.id == + _selectedCategory!, + ) + .name, + ), + readOnly: true, + ), + SizedBox(height: 3.h), + + if (currentUserRole == UserRole.owner && + _isMainOutlet) ...[ + Text( + "Tipe Produk", + style: TextStyle( + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + fontWeight: FontWeight.bold, + ), + ), + + SizedBox(height: 1.5.h), + + Row( + children: [ + Expanded( + child: SelectionCard( + title: "Tanpa Varian", + isSelected: !_hasVariant, + onTap: _handleProductHasNotVariant, + ), + ), + + SizedBox(width: 4.w), + + Expanded( + child: SelectionCard( + title: "Memiliki Varian", + isSelected: _hasVariant, + onTap: _handleProductHasVariant, + ), + ), + ], + ), + + SizedBox(height: 3.h), + ], + + if (!_hasVariant) ...[ + CustomTextFormField( + label: "Harga Modal", + hint: "0", + keyboardType: TextInputType.number, + controller: _costPriceController, + validator: (value) => _validator( + value: value, + message1: "Harga modal belum diisi", + message2: "Harga modal lebih dari 0", + isNumber: true, + isCostPrice: true, + errorMessage: "Harga modal tidak valid", + ), + ), + + SizedBox(height: 3.h), + + CustomTextFormField( + label: "Harga Jual", + hint: "0", + keyboardType: TextInputType.number, + controller: _sellingPriceController, + validator: (value) => _validator( + value: value, + message1: "Harga jual belum diisi", + message2: "Harga jual harus lebih dari 0", + isNumber: true, + isSellingPrice: true, + errorMessage: "Harga jual tidak valid", + ), + ), + ] else + CustomSelector( + isTablet: isTablet, + title: selectorTitle, + padding: EdgeInsets.symmetric( + horizontal: 4.w, + vertical: 2.h, + ), + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + onTap: () async { + FocusScope.of(context).unfocus(); + + final isMainOutlet = await ref + .read(outletRepositoryProvider) + .fetchIsMainOutlet(); + + await Future.delayed( + const Duration(milliseconds: 400), + ); + + context.pushNamed( + AppRoute.editVariantProductScreen, + pathParameters: {"id": _productId}, + extra: { + "is_main_outlet": isMainOutlet, + "is_editing": _isEditing, + "has_new_variant": _hasNewVariant, + "is_stock_fixed": + !_isStockAlwaysAvailable, + }, + ); + }, + ), + + if (currentUserRole == UserRole.owner && + _isMainOutlet) ...[ + SizedBox(height: 3.h), + + Text( + "Jenis Stok", + style: TextStyle( + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + fontWeight: FontWeight.bold, + ), + ), + SizedBox(height: 1.5.h), + + Row( + children: [ + Expanded( + child: SelectionCard( + title: "Tetap", + isSelected: !_isStockAlwaysAvailable, + onTap: _handleStockFixed, + ), + ), + + SizedBox(width: 4.w), + + Expanded( + child: SelectionCard( + title: "Selalu Ada", + isSelected: _isStockAlwaysAvailable, + onTap: _handleStockAlwaysAvailbale, + ), + ), + ], + ), + ], + + if (showStockField) ...[ + SizedBox(height: 3.h), + + CustomTextFormField( + label: _isEditing + ? "Sisa Stok" + : "Stok Awal", + hint: "100", + keyboardType: TextInputType.number, + controller: _stockController, + readOnly: _isEditing, + onTapReadOnlyMessage: + "Tambah stok di menu stok masuk", + ), + ], + + SizedBox(height: 3.h), + + if (currentUserRole == UserRole.owner && + _isMainOutlet) + DropdownWithAction( + label: "Satuan", + value: _selectedUnit, + items: units, + onChanged: (val) => + setState(() => _selectedUnit = val), + onAdd: () async { + FocusScope.of(context).unfocus(); + + await Future.delayed( + const Duration(milliseconds: 400), + ); + + showDialog( + context: context, + builder: (_) => const AddUnitScreen(), + ); + }, + onEdit: () async { + if (_selectedUnit == null) { + CustomSnackbar.showError( + context, + "Satuan belum dipilih", + ); + } else { + final unitRepository = ref.read( + unitRepositoryProvider, + ); + + final unit = await unitRepository + .fetchUnitById(_selectedUnit!); + + if (unit != null) { + showDialog( + context: context, + builder: (_) { + return EditUnitScreen(unit: unit); + }, + ); + } else { + CustomSnackbar.showError( + context, + "Satuan produk tidak tersedia", + ); + } + } + }, + ) + else + CustomTextFormField( + label: "Satuan", + controller: TextEditingController( + text: units + .firstWhere( + (unit) => unit.id == _selectedUnit!, + ) + .name, + ), + readOnly: true, + ), + + SizedBox(height: 3.h), + + SizedBox( + width: double.infinity, + height: 6.5.h, + child: ElevatedButton( + onPressed: productControllerState.isLoading + ? null + : _saveProduct, + style: ElevatedButton.styleFrom( + backgroundColor: AppColor.primaryColor, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + 2.0.w, + ), + ), + disabledBackgroundColor: + Colors.grey.shade300, + elevation: 4, + ), + child: Text( + "Simpan", + style: TextStyle( + color: Colors.white, + fontSize: AppFontSize.medium.sp, + fontWeight: FontWeight.bold, + ), + ), + ), + ), + ], + ), + ), + ), + ), + + if (messageActivity != null) + PopupNotification( + isOwner: currentUserRole == UserRole.owner, + messages: messageActivity.messages, + type: messageActivity.type, + ), + ], + ), + ), + ); + }, + ); + } +} diff --git a/Mobile Operasional/lib/features/goods/products/presentation/screens/edit_product_variant_screen.dart b/Mobile Operasional/lib/features/goods/products/presentation/screens/edit_product_variant_screen.dart new file mode 100644 index 0000000..aa18d28 --- /dev/null +++ b/Mobile Operasional/lib/features/goods/products/presentation/screens/edit_product_variant_screen.dart @@ -0,0 +1,641 @@ +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; +import 'package:image_picker/image_picker.dart'; +import 'package:niogu_app/core/components/top_back_bar_app.dart'; +import 'package:niogu_app/core/constants/app_color.dart'; +import 'package:niogu_app/core/constants/app_font_size.dart'; +import 'package:niogu_app/core/providers/app_provider.dart'; +import 'package:niogu_app/core/utils/extension_format.dart'; +import 'package:niogu_app/core/utils/image_service.dart'; +import 'package:niogu_app/core/utils/log_message.dart'; +import 'package:niogu_app/core/enums/user_role.dart'; +import 'package:niogu_app/core/widgets/custom_snackbar.dart'; +import 'package:niogu_app/core/widgets/pop_up_notification.dart'; +import 'package:niogu_app/features/goods/products/domain/entities/product.dart'; +import 'package:niogu_app/features/goods/products/presentation/providers/product_provider.dart'; +import 'package:niogu_app/features/goods/products/presentation/widgets/product_variant_model.dart'; +import 'package:niogu_app/features/goods/products/presentation/widgets/manage_varian_shimmer.dart'; +import 'package:niogu_app/features/goods/products/presentation/widgets/variant_product_card.dart'; +import 'package:sizer/sizer.dart'; + +class EditProductVariantScreen extends ConsumerStatefulWidget { + final String productId; + final bool isMainOutlet; + final bool isEditing; + final bool hasNewVariant; + final bool isStockFixed; + + const EditProductVariantScreen({ + super.key, + required this.productId, + this.isMainOutlet = true, + this.isEditing = true, + this.hasNewVariant = false, + required this.isStockFixed, + }); + + @override + ConsumerState createState() => + _EditProductVariantScreenState(); +} + +class _EditProductVariantScreenState + extends ConsumerState { + late final String _productId; + + late final bool _hasNewVariant; + + final List> _formKeys = []; + + Map _selectedNewVariants = {}; + + final List _variantModels = []; + + final ScrollController _scrollController = ScrollController(); + + final ImagePicker _picker = ImagePicker(); + + final List _pathImageTemps = []; + + final List _deleteVariantModels = []; + + bool _isLoading = true; + + @override + void initState() { + super.initState(); + _productId = widget.productId; + + _hasNewVariant = widget.hasNewVariant; + + WidgetsBinding.instance.addPostFrameCallback((_) { + Future.delayed(const Duration(milliseconds: 800), () { + _fecthVariants(); + }); + }); + } + + @override + void dispose() { + for (final variant in _variantModels) { + variant.dispose(); + } + + for (final variant in _deleteVariantModels) { + variant.dispose(); + } + _scrollController.dispose(); + super.dispose(); + } + + Future _fecthVariants() async { + final variants = [...ref.read(productWithVariantProvider)]; + + if (_hasNewVariant) { + final newVariants = await ref + .read(productRepositoryProvider) + .fetchNewVariants(_productId); + + final List nVariants = []; + + for (final variant in newVariants) { + if (!variants.any( + (v) => v.productVariantId == variant.productVariantId, + )) { + nVariants.add(variant); + } + } + + final selectedNewVariants = ref.read(selectedNewVariantProvider); + + if (selectedNewVariants.isNotEmpty) { + _selectedNewVariants = {...selectedNewVariants}; + } else { + _selectedNewVariants = { + for (final variant in nVariants) variant.productVariantId!: false, + }; + } + + variants.addAll(nVariants); + } + + if (variants.isNotEmpty) { + for (int i = 0; i < variants.length; i++) { + _formKeys.add(GlobalKey()); + + _variantModels.add(ProductVariantModel()); + + _variantModels[i].productVariantId = variants[i].productVariantId; + + _variantModels[i].outletInventoryId = variants[i].outletInventoryId; + + _variantModels[i].imagePath = variants[i].imagePath; + + _variantModels[i].nameController.text = variants[i].name; + + _variantModels[i].stockController.text = variants[i].stock + .toStringWithoutTrailingZero(); + + _variantModels[i].costPriceController.text = variants[i].costPrice + .toStringWithoutTrailingZero(); + + _variantModels[i].sellingPriceController.text = variants[i].sellingPrice + .toStringWithoutTrailingZero(); + } + } else { + _formKeys.add(GlobalKey()); + + _variantModels.add(ProductVariantModel()); + } + + setState(() { + _isLoading = false; + }); + } + + void _onSelectedNewVariant(ProductVariantModel variant) { + final id = variant.productVariantId; + + setState(() { + _selectedNewVariants[id!] = !_selectedNewVariants[id]!; + }); + } + + void _addVariant() { + setState(() { + _formKeys.add(GlobalKey()); + _variantModels.add(ProductVariantModel()); + }); + + WidgetsBinding.instance.addPostFrameCallback((_) { + if (_scrollController.hasClients) { + _scrollController.animateTo( + _scrollController.position.maxScrollExtent, + duration: const Duration(milliseconds: 300), + curve: Curves.easeOut, + ); + } + }); + } + + Future _removeVariant(int index) async { + _deleteVariantModels.add(_variantModels[index]); + setState(() { + _formKeys.removeAt(index); + _variantModels[index].dispose(); + _variantModels.removeAt(index); + }); + } + + Future _showPickerOptions(int index) async { + final bool isTablet = 100.w >= 600; + showModalBottomSheet( + context: context, + backgroundColor: Colors.white, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.vertical(top: Radius.circular(2.5.w)), + ), + constraints: const BoxConstraints(maxWidth: double.infinity), + builder: (BuildContext context) { + return SafeArea( + child: Container( + width: 100.w, + padding: EdgeInsets.symmetric(vertical: 2.h, horizontal: 5.w), + clipBehavior: Clip.hardEdge, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.vertical(top: Radius.circular(6.w)), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + ListTile( + leading: Icon(Icons.photo_library, size: 6.w), + title: Text( + 'Galeri', + style: TextStyle( + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + ), + ), + onTap: () async { + await _getImage(ImageSource.gallery, index); + context.pop(); + }, + ), + + SizedBox(height: 2.h), + + ListTile( + leading: Icon(Icons.photo_camera, size: 6.w), + title: Text( + 'Kamera', + style: TextStyle( + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + ), + ), + onTap: () async { + await _getImage(ImageSource.camera, index); + context.pop(); + }, + ), + ], + ), + ), + ); + }, + ); + } + + Future _getImage(ImageSource ImageSource, int index) async { + try { + final XFile? pickedFile = await _picker.pickImage( + source: ImageSource, + imageQuality: 70, + ); + + if (pickedFile != null) { + File tempFile = File(pickedFile.path); + + String? savedPath = await ImageService.saveImageToLocalDirectory( + tempFile, + "product_variant_images", + ); + + if (savedPath != null) { + _pathImageTemps.add(savedPath); + ref.read(variantImageTempProvider.notifier).state = _pathImageTemps; + setState(() { + _variantModels[index].imagePath = savedPath; + }); + } + } + } catch (e, st) { + LogMessage.log.w(e.toString(), error: e, stackTrace: st); + CustomSnackbar.showWarning(context, "Akses ditolak"); + } + } + + Future _removeImage(int index) async { + setState(() { + _variantModels[index].imagePath = null; + }); + } + + Future _cleanUpImages() async { + for (var path in _pathImageTemps) { + if (path != null) { + await ImageService.deleteLocalImage(path); + } + } + } + + void _saveVariant() { + final lastKey = _formKeys.last; + + if (!lastKey.currentState!.validate()) { + return; + } + + final List variants = []; + + try { + for (var variantModel in _variantModels) { + final String? productVariantId = variantModel.productVariantId; + + final String? outletInventoryId = variantModel.outletInventoryId; + + final String? imagePath = variantModel.imagePath; + + final String name = variantModel.nameController.text.trim(); + + final double stock = widget.isStockFixed + ? double.parse(variantModel.stockController.text.trim()) + : 0.0; + + final double costPrice = double.parse( + variantModel.costPriceController.text.trim(), + ); + + final double sellingPrice = double.parse( + variantModel.sellingPriceController.text.trim(), + ); + + if (costPrice >= sellingPrice) { + CustomSnackbar.showError( + context, + "Harga jual harus lebih besar dari modal", + ); + return; + } + + final isSelected = _selectedNewVariants[variantModel.productVariantId]; + + if (isSelected != null && !isSelected) continue; + + final productWithVariant = ProductWithVariant( + productVariantId: productVariantId, + outletInventoryId: outletInventoryId, + imagePath: imagePath, + name: name, + stock: stock, + costPrice: costPrice, + sellingPrice: sellingPrice, + ); + variants.add(productWithVariant); + } + } catch (e, st) { + LogMessage.log.e(e.toString(), error: e, stackTrace: st); + CustomSnackbar.showError(context, "Format angka tidak valid"); + return; + } + + final List deleteVariants = []; + + for (var variant in _deleteVariantModels) { + if (variant.productVariantId != null && + variant.outletInventoryId != null) { + deleteVariants.add( + DeleteProductVariant( + productId: _productId, + productVariantId: variant.productVariantId!, + outletInventoryId: variant.outletInventoryId!, + ), + ); + } + } + + if (deleteVariants.isNotEmpty) { + ref.read(deleteProductVariantProvider.notifier).state = deleteVariants; + } + + final Map selectedNewVariants = {}; + + for (final selected in _selectedNewVariants.entries) { + final key = selected.key; + + final value = selected.value; + + if (value) selectedNewVariants.putIfAbsent(key, () => value); + } + + ref.read(selectedNewVariantProvider.notifier).state = selectedNewVariants; + + ref.read(productWithVariantProvider.notifier).state = variants; + + CustomSnackbar.showSuccess(context, "Varian produk diperbarui"); + + context.pop(); + } + + @override + Widget build(BuildContext context) { + return LayoutBuilder( + builder: (context, constraints) { + final isTablet = 100.w >= 600; + + const backgroundColor = Color(0xFFF9FAFB); + + final messageActivity = ref.watch(messageActivityProvider); + + final currentUserRole = ref.watch(currentUserRoleProvider); + + return PopScope( + canPop: false, + onPopInvokedWithResult: (didPop, result) async { + if (didPop) return; + + await _cleanUpImages(); + + context.pop(); + }, + child: SafeArea( + top: false, + bottom: true, + right: false, + left: false, + child: Stack( + children: [ + Scaffold( + resizeToAvoidBottomInset: false, + backgroundColor: backgroundColor, + appBar: TopBackBarApp( + title: "Atur Varian Produk", + onTap: () async { + await _cleanUpImages(); + + context.pop(); + }, + ), + body: _isLoading + ? const ManageVariantShimmer() + : SingleChildScrollView( + controller: _scrollController, + padding: EdgeInsets.fromLTRB(5.w, 3.h, 5.w, 12.h), + child: Column( + children: [ + Container( + padding: EdgeInsets.symmetric( + horizontal: 2.w, + vertical: 1.h, + ), + margin: EdgeInsets.only(bottom: 2.h), + decoration: BoxDecoration( + color: Colors.blue[50], + borderRadius: BorderRadius.circular(4.w), + ), + child: Row( + children: [ + Icon( + Icons.info_outline, + color: AppColor.primaryColor, + size: 5.w, + ), + + SizedBox(width: 3.w), + + Expanded( + child: Text( + "Setiap varian dapat memiliki harga dan stok yang berbeda.", + style: TextStyle( + color: AppColor.primaryColor, + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + ), + ), + ), + ], + ), + ), + + ..._variantModels.asMap().entries.map((entry) { + final index = entry.key; + + final variant = _variantModels[index]; + + final id = variant.productVariantId; + + final isSelected = + _selectedNewVariants[id] ?? false; + + return Padding( + padding: EdgeInsets.only(bottom: 3.h), + child: VariantProductCard( + userRole: currentUserRole!, + isMainOutlet: widget.isMainOutlet, + isEditing: widget.isEditing, + isSelected: isSelected, + formKey: _formKeys[index], + length: _variantModels.length, + variant: variant, + index: index, + isStockFixed: widget.isStockFixed, + nameController: variant.nameController, + stockController: variant.stockController, + costPriceController: + variant.costPriceController, + sellingPriceController: + variant.sellingPriceController, + onTapSelected: () => + _onSelectedNewVariant(variant), + onTapRemoveVarian: () async => + await _removeVariant(index), + onTapVariantImageAdd: () async => + await _showPickerOptions(index), + onTapVariantImageRemove: () async => + await _removeImage(index), + ), + ); + }), + + if (currentUserRole == UserRole.owner && + widget.isMainOutlet) + GestureDetector( + onTap: () async { + FocusScope.of(context).unfocus(); + + await Future.delayed( + const Duration(milliseconds: 800), + ); + + final lastKey = _formKeys.last; + + if (!lastKey.currentState!.validate()) + return; + + for (var variant in _variantModels) { + final double costPrice = double.parse( + variant.costPriceController.text.trim(), + ); + final double sellingPrice = double.parse( + variant.sellingPriceController.text + .trim(), + ); + + if (costPrice >= sellingPrice) { + CustomSnackbar.showError( + context, + "Harga jual harus lebih besar dari modal", + ); + return; + } + } + + _addVariant(); + }, + child: Container( + width: double.infinity, + padding: EdgeInsets.symmetric( + horizontal: 4.w, + vertical: 2.h, + ), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(3.w), + ), + child: Row( + mainAxisAlignment: + MainAxisAlignment.center, + children: [ + Icon( + Icons.add_circle_outline_rounded, + color: AppColor.primaryColor, + ), + + SizedBox(width: 3.w), + + Text( + "Tambah Varian Baru", + style: TextStyle( + color: AppColor.primaryColor, + fontSize: AppFontSize.medium.sp, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + ), + ), + ], + ), + ), + + bottomNavigationBar: _isLoading + ? const SizedBox() + : Container( + padding: EdgeInsets.all(5.w), + decoration: BoxDecoration( + color: Colors.white, + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.05), + blurRadius: 10, + offset: const Offset(0, -5), + ), + ], + ), + child: SizedBox( + height: 6.5.h, + child: ElevatedButton( + onPressed: _saveVariant, + style: ElevatedButton.styleFrom( + backgroundColor: AppColor.primaryColor, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(2.5.w), + ), + elevation: 0, + ), + child: Text( + "Simpan Perubahan", + style: TextStyle( + color: Colors.white, + fontSize: AppFontSize.medium.sp, + fontWeight: FontWeight.bold, + ), + ), + ), + ), + ), + ), + + if (messageActivity != null) + PopupNotification( + isOwner: currentUserRole == UserRole.owner, + messages: messageActivity.messages, + type: messageActivity.type, + ), + ], + ), + ), + ); + }, + ); + } +} diff --git a/Mobile Operasional/lib/features/goods/products/presentation/screens/product_choice_screen.dart b/Mobile Operasional/lib/features/goods/products/presentation/screens/product_choice_screen.dart new file mode 100644 index 0000000..779a957 --- /dev/null +++ b/Mobile Operasional/lib/features/goods/products/presentation/screens/product_choice_screen.dart @@ -0,0 +1,143 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; +import 'package:niogu_app/core/constants/app_color.dart'; +import 'package:niogu_app/core/widgets/custom_error_screen.dart'; +import 'package:niogu_app/core/router/app_route.dart'; +import 'package:niogu_app/core/widgets/custom_empty_screen.dart'; +import 'package:niogu_app/core/widgets/custom_snackbar.dart'; +import 'package:niogu_app/features/goods/products/domain/entities/product.dart'; +import 'package:niogu_app/features/goods/products/presentation/providers/product_provider.dart'; +import 'package:niogu_app/core/widgets/custom_selectable_item.dart'; +import 'package:niogu_app/features/outlets/presentation/providers/outlet_provider.dart'; +import 'package:sizer/sizer.dart'; + +class ProductChoiceScreen extends ConsumerStatefulWidget { + const ProductChoiceScreen({super.key}); + + @override + ConsumerState createState() => + _ProductChoiceScreenState(); +} + +class _ProductChoiceScreenState extends ConsumerState + with AutomaticKeepAliveClientMixin { + @override + // TODO: implement wantKeepAlive + bool get wantKeepAlive => true; + + String? _selectedItem; + + @override + Widget build(BuildContext context) { + super.build(context); + + final filteredProductState = ref.watch(filteredProductChoiceProvider); + + final productEmptyState = ref.watch(productChoiceEmptyProvider); + + return filteredProductState.when( + data: (products) { + switch (productEmptyState) { + case ProductChoiceEmpty.loading: + return const SizedBox(); + case ProductChoiceEmpty.empty_database: + return CustomEmptyScreen( + title: "Tidak Ada Produk", + body: "Belum ada produk yang tersedia", + ); + case ProductChoiceEmpty.empty_search_result: + return const CustomEmptyScreen(body: "Produk Tidak Ditemukan"); + case ProductChoiceEmpty.has_data: + return Stack( + children: [ + ListView.builder( + padding: EdgeInsets.all(4.w), + itemCount: products.length, + itemBuilder: (context, index) { + final product = products[index]; + + final hasVariant = product.hasVariant; + + return CustomSelectableItem( + id: product.id, + title: product.name, + subtitle: hasVariant + ? "Produk memiliki varian" + : "Produk tanpa varian", + imagePath: product.imagePath, + isSelected: _selectedItem == product.id, + onTap: () { + setState(() { + _selectedItem = product.id; + }); + }, + ); + }, + ), + + Positioned( + bottom: 10.h, + right: 4.w, + width: 12.5.w, + height: 12.5.w, + child: FloatingActionButton( + heroTag: "btn_edit_product", + onPressed: _selectedItem == null + ? null + : () async { + FocusScope.of(context).unfocus(); + + final alreadyProduct = await ref + .read(productRepositoryProvider) + .checkAlreadyProductByOutlet(_selectedItem!); + + if (alreadyProduct) { + CustomSnackbar.showWarning( + context, + "Produk ini telah tersedia di outlet ini", + ); + return; + } + + final isMainOutlet = await ref + .read(outletRepositoryProvider) + .fetchIsMainOutlet(); + + await Future.delayed( + const Duration(milliseconds: 400), + ); + + context.pushNamed( + AppRoute.editProductScreen, + pathParameters: {"id": _selectedItem!}, + extra: { + "is_main_outlet": isMainOutlet, + "is_editing": false, + }, + ); + }, + backgroundColor: _selectedItem == null + ? Colors.grey.shade300 + : AppColor.primaryColor, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(2.5.w), + ), + elevation: 0, + child: Icon( + Icons.keyboard_arrow_right, + color: Colors.white, + size: 8.w, + ), + ), + ), + ], + ); + } + }, + error: (error, stackTrace) => + CustomErrorScreen(message: "Ups, terjadi kesalahan", onRefresh: () {}), + loading: () => const SizedBox(), + ); + } +} diff --git a/Mobile Operasional/lib/features/goods/products/presentation/screens/product_screen.dart b/Mobile Operasional/lib/features/goods/products/presentation/screens/product_screen.dart new file mode 100644 index 0000000..26ced8c --- /dev/null +++ b/Mobile Operasional/lib/features/goods/products/presentation/screens/product_screen.dart @@ -0,0 +1,380 @@ +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; +import 'package:niogu_app/core/constants/app_color.dart'; +import 'package:niogu_app/core/constants/app_font_size.dart'; +import 'package:niogu_app/core/widgets/custom_error_screen.dart'; +import 'package:niogu_app/core/providers/app_provider.dart'; +import 'package:niogu_app/core/router/app_route.dart'; +import 'package:niogu_app/core/enums/user_role.dart'; +import 'package:niogu_app/core/widgets/custom_empty_screen.dart'; +import 'package:niogu_app/core/widgets/custom_not_login.dart'; +import 'package:niogu_app/features/goods/presentation/widgets/custom_item_detail.dart'; +import 'package:niogu_app/features/goods/products/domain/entities/product.dart'; +import 'package:niogu_app/features/goods/products/presentation/providers/product_provider.dart'; +import 'package:niogu_app/features/goods/products/presentation/screens/delete_product_screen.dart'; +import 'package:niogu_app/features/goods/products/presentation/widgets/product_list_shimmer.dart'; +import 'package:niogu_app/features/outlets/presentation/providers/outlet_provider.dart'; +import 'package:sizer/sizer.dart'; + +class ProductScreen extends ConsumerStatefulWidget { + const ProductScreen({super.key}); + + @override + ConsumerState createState() => _ProductScreenState(); +} + +class _ProductScreenState extends ConsumerState + with AutomaticKeepAliveClientMixin { + @override + // TODO: implement wantKeepAlive + bool get wantKeepAlive => true; + + @override + void initState() { + super.initState(); + } + + @override + void dispose() { + super.dispose(); + } + + @override + Widget build(BuildContext context) { + super.build(context); + + final bool isTablet = 100.w >= 600; + + final isLoggedIn = ref.watch(currentStatusLoginProvider); + + if (!isLoggedIn) { + return const CustomNotLogin(); + } + + final productState = ref.watch(filteredProductProvider); + + final productEmptyState = ref.watch(productEmptyProvider); + + return productState.when( + data: (products) { + switch (productEmptyState) { + case ProductEmpty.loading: + return const SizedBox(); + case ProductEmpty.empty_database: + return CustomEmptyScreen( + title: "Tidak Ada Produk", + body: "Kamu belum memiliki produk", + textButton: "Buat Produk Pertamamu", + onPressed: () async { + final currentUserRole = ref.read(currentUserRoleProvider); + + final isMainOutlet = await ref + .read(outletRepositoryProvider) + .fetchIsMainOutlet(); + + if (currentUserRole == UserRole.owner && isMainOutlet) { + context.pushNamed(AppRoute.addProductScreen); + } else { + context.pushNamed(AppRoute.productChoiceScreen); + } + }, + ); + case ProductEmpty.empty_search_result: + return const CustomEmptyScreen(body: "Produk Tidak Ditemukan"); + case ProductEmpty.has_data: + return Stack( + children: [ + ListView.builder( + padding: EdgeInsets.all(4.w), + itemCount: products.length, + itemBuilder: (context, index) { + final product = products[index]; + + final bool imagePathAvailable = + product.productImages != null && + product.productImages!.isNotEmpty && + product.productImages!.last.isNotEmpty; + + final File imageFile = File( + imagePathAvailable + ? product.productImages!.last + : "image not found", + ); + + final bool imageFileExists = imageFile.existsSync(); + + return RepaintBoundary( + child: Card( + elevation: 0, + color: Colors.white, + shape: RoundedRectangleBorder( + side: BorderSide(color: Colors.grey.shade300), + borderRadius: BorderRadius.circular(2.5.w), + ), + margin: EdgeInsets.only(bottom: 2.h), + child: Padding( + padding: EdgeInsets.symmetric( + vertical: 2.h, + horizontal: 2.w, + ), + child: Material( + color: Colors.white, + clipBehavior: Clip.hardEdge, + child: InkWell( + onTap: () async { + FocusScope.of(context).unfocus(); + + final isMainOutlet = await ref + .read(outletRepositoryProvider) + .fetchIsMainOutlet(); + + await Future.delayed( + const Duration(milliseconds: 400), + ); + + context.pushNamed( + AppRoute.editProductScreen, + pathParameters: {"id": product.id}, + extra: { + "is_main_outlet": isMainOutlet, + "has_new_variant": + product.newVariantCount > 0, + "new_variant_count": + product.newVariantCount, + }, + ); + }, + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "${index + 1}.", + style: TextStyle( + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + fontWeight: FontWeight.bold, + ), + ), + SizedBox(width: 3.w), + + Container( + height: 17.w, + width: 17.w, + decoration: BoxDecoration( + border: Border.all( + color: Colors.grey.shade400, + ), + borderRadius: BorderRadius.circular( + 2.5.w, + ), + ), + child: imageFileExists + ? ClipRRect( + borderRadius: BorderRadius.circular( + 2.5.w, + ), + child: Image.file( + imageFile, + fit: BoxFit.cover, + width: double.infinity, + height: double.infinity, + errorBuilder: + (context, error, stackTrace) { + return Icon( + Icons.error, + color: Colors.grey[400], + size: 8.w, + ); + }, + ), + ) + : Icon( + Icons.image_outlined, + size: 8.w, + color: Colors.grey[400], + ), + ), + + SizedBox(width: 3.w), + + Expanded( + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text( + product.name, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + fontWeight: FontWeight.bold, + ), + ), + SizedBox(height: 0.5.h), + + if (product.hasVariant) + CustomItemDetail( + label: "Jumlah Varian", + value: ": ${product.variantCount}", + ), + + CustomItemDetail( + label: "Jenis Stok", + value: + ": ${product.stockType == 'fixed' ? 'Tetap' : 'Selalu Ada'}", + ), + + if (product.stockType == 'fixed') + CustomItemDetail( + label: "Sisa Stok", + value: + ": ${product.remainingStock}", + ), + + CustomItemDetail( + label: "Harga Jual", + value: + ": ${product.displaySellingPrice}", + ), + if (product.newVariantCount > 0) ...[ + SizedBox(height: 1.h), + Container( + padding: EdgeInsets.symmetric( + horizontal: 2.5.w, + vertical: 0.5.h, + ), + decoration: BoxDecoration( + color: AppColor.primaryColor + .withOpacity(0.1), + borderRadius: + BorderRadius.circular(1.5.w), + border: Border.all( + color: AppColor.primaryColor + .withOpacity(0.2), + width: 0.5, + ), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + Icons.auto_awesome_rounded, + size: 5.w, + color: AppColor.primaryColor, + ), + SizedBox(width: 1.5.w), + Text( + "${product.newVariantCount} Varian Baru Tersedia", + style: TextStyle( + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + fontWeight: FontWeight.bold, + color: + AppColor.primaryColor, + ), + ), + ], + ), + ), + ], + ], + ), + ), + + IconButton( + onPressed: () async { + final String id = product.id; + + final productRepository = ref.read( + productRepositoryProvider, + ); + + final productSnapshot = + await productRepository + .fetchDeleteProductSnapshotById( + id, + ); + + FocusScope.of(context).unfocus(); + + await Future.delayed( + const Duration(milliseconds: 400), + ); + + showDialog( + context: context, + builder: (context) => + DeleteProductScreen( + productSnapshot: productSnapshot, + ), + ); + }, + icon: Icon( + Icons.delete, + color: Colors.red, + size: 6.w, + ), + ), + ], + ), + ), + ), + ), + ), + ); + }, + ), + + Positioned( + bottom: 10.h, + right: 4.w, + width: 12.5.w, + height: 12.5.w, + child: FloatingActionButton( + heroTag: "btn_add_product", + onPressed: () async { + FocusScope.of(context).unfocus(); + + await Future.delayed(const Duration(milliseconds: 400)); + + final currentUserRole = ref.read(currentUserRoleProvider); + + final isMainOutlet = await ref + .read(outletRepositoryProvider) + .fetchIsMainOutlet(); + + if (currentUserRole == UserRole.owner && isMainOutlet) { + context.pushNamed(AppRoute.addProductScreen); + } else { + context.pushNamed(AppRoute.productChoiceScreen); + } + }, + elevation: 0, + backgroundColor: AppColor.primaryColor, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(2.5.w), + ), + child: Icon(Icons.add, color: Colors.white, size: 8.w), + ), + ), + ], + ); + } + }, + error: (error, stackTrace) { + return CustomErrorScreen( + message: "Ups, terjadi kesalahan", + onRefresh: () {}, + ); + }, + loading: () => const ProductListShimmer(), + ); + } +} diff --git a/Mobile Operasional/lib/features/goods/products/presentation/widgets/add_photo_button.dart b/Mobile Operasional/lib/features/goods/products/presentation/widgets/add_photo_button.dart new file mode 100644 index 0000000..5dc369e --- /dev/null +++ b/Mobile Operasional/lib/features/goods/products/presentation/widgets/add_photo_button.dart @@ -0,0 +1,69 @@ +import 'package:flutter/material.dart'; +import 'package:niogu_app/core/constants/app_color.dart'; +import 'package:niogu_app/core/constants/app_font_size.dart'; +import 'package:sizer/sizer.dart'; + +class AddPhotoButton extends StatelessWidget { + final VoidCallback onTap; + const AddPhotoButton({super.key, required this.onTap}); + + @override + Widget build(BuildContext context) { + final bool isTablet = 100.w >= 600; + + return InkWell( + onTap: onTap, + borderRadius: BorderRadius.circular(2.5.w), + child: Container( + width: 30.w, + height: 30.w, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(2.5.w), + border: Border.all( + color: Colors.grey.shade300, + width: 1, + style: BorderStyle.solid, + ), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.03), + blurRadius: 4, + offset: const Offset(0, 2), + ), + ], + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Container( + padding: EdgeInsets.all(2.w), + decoration: BoxDecoration( + color: AppColor.primaryColor.withOpacity( + 0.1, + ), // Navy transparan + shape: BoxShape.circle, + ), + child: Icon( + Icons.add_a_photo_rounded, + color: AppColor.primaryColor, + size: 7.w, + ), + ), + SizedBox(height: 0.8.h), + Text( + "Tambah\nFoto", + textAlign: TextAlign.center, + style: TextStyle( + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + color: Colors.grey[600], + ), + ), + ], + ), + ), + ); + } +} \ No newline at end of file diff --git a/Mobile Operasional/lib/features/goods/products/presentation/widgets/edit_product_shimmer.dart b/Mobile Operasional/lib/features/goods/products/presentation/widgets/edit_product_shimmer.dart new file mode 100644 index 0000000..c5e6f3e --- /dev/null +++ b/Mobile Operasional/lib/features/goods/products/presentation/widgets/edit_product_shimmer.dart @@ -0,0 +1,120 @@ +import 'package:flutter/material.dart'; +import 'package:shimmer/shimmer.dart'; +import 'package:sizer/sizer.dart'; + +class EditProductShimmer extends StatelessWidget { + const EditProductShimmer({super.key}); + + @override + Widget build(BuildContext context) { + final baseColor = Colors.grey[300]!; + final highlightColor = Colors.grey[100]!; + const backgroundColor = Color(0xFFF9FAFB); + + return Scaffold( + backgroundColor: backgroundColor, + body: Shimmer.fromColors( + baseColor: baseColor, + highlightColor: highlightColor, + child: SingleChildScrollView( + padding: EdgeInsets.fromLTRB(5.w, 3.h, 5.w, 5.h), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildLabelPlaceholder(width: 25.w), + SizedBox(height: 1.h), + Container( + width: 30.w, + height: 30.w, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(2.5.w), + ), + ), + + SizedBox(height: 4.h), + _buildInputPlaceholder(labelWidth: 30.w), + + SizedBox(height: 3.h), + _buildInputPlaceholder(labelWidth: 35.w), + + SizedBox(height: 3.h), + _buildLabelPlaceholder(width: 25.w), + SizedBox(height: 1.h), + Row( + children: [ + Expanded( + child: _buildBlock(height: 7.h, borderRadius: 2.5.w), + ), + SizedBox(width: 4.w), + Expanded( + child: _buildBlock(height: 7.h, borderRadius: 2.5.w), + ), + ], + ), + + SizedBox(height: 3.h), + _buildInputPlaceholder(labelWidth: 25.w), + + SizedBox(height: 3.h), + _buildInputPlaceholder(labelWidth: 25.w), + + SizedBox(height: 3.h), + _buildLabelPlaceholder(width: 25.w), + SizedBox(height: 1.h), + Row( + children: [ + Expanded( + child: _buildBlock(height: 7.h, borderRadius: 2.5.w), + ), + SizedBox(width: 4.w), + Expanded( + child: _buildBlock(height: 7.h, borderRadius: 2.5.w), + ), + ], + ), + + SizedBox(height: 3.h), + _buildInputPlaceholder(labelWidth: 25.w), + + SizedBox(height: 5.h), + ], + ), + ), + ), + ); + } + + Widget _buildLabelPlaceholder({required double width}) { + return Container( + width: width, + height: 1.5.h, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(1.w), + ), + ); + } + + Widget _buildInputPlaceholder({required double labelWidth}) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildLabelPlaceholder(width: labelWidth), + SizedBox(height: 1.h), + _buildBlock(height: 6.h, borderRadius: 2.5.w), + ], + ); + } + + Widget _buildBlock({required double height, required double borderRadius}) { + return Container( + width: double.infinity, + height: height, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(borderRadius), + ), + ); + } +} diff --git a/Mobile Operasional/lib/features/goods/products/presentation/widgets/image_preview_card.dart b/Mobile Operasional/lib/features/goods/products/presentation/widgets/image_preview_card.dart new file mode 100644 index 0000000..4c1a9c6 --- /dev/null +++ b/Mobile Operasional/lib/features/goods/products/presentation/widgets/image_preview_card.dart @@ -0,0 +1,81 @@ +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:sizer/sizer.dart'; + +class ImagePreviewCard extends StatelessWidget { + final bool imageFileExists; + final File imageFile; + final VoidCallback onTap; + + const ImagePreviewCard({ + super.key, + required this.imageFileExists, + required this.imageFile, + required this.onTap, + }); + + @override + Widget build(BuildContext context) { + return Stack( + clipBehavior: Clip.none, + children: [ + Container( + width: 30.w, + height: 30.w, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(2.5.w), + border: Border.all(color: Colors.grey.shade300), + color: Colors.grey[200], + ), + child: imageFileExists + ? ClipRRect( + borderRadius: BorderRadiusGeometry.circular(2.5.w), + child: Image.file( + imageFile, + fit: BoxFit.cover, + width: double.infinity, + height: double.infinity, + errorBuilder: (context, error, stackTrace) { + return Icon( + Icons.error, + color: Colors.grey[400], + size: 8.w, + ); + }, + ), + ) + : Icon(Icons.image, color: Colors.grey[400], size: 8.w), + ), + + Positioned( + top: 1.w, + right: 1.w, + child: Material( + color: Colors.transparent, + type: MaterialType.transparency, + child: InkWell( + onTap: onTap, + child: Container( + padding: EdgeInsets.all(1.w), + decoration: BoxDecoration( + color: Colors.red[50], + shape: BoxShape.circle, + border: Border.all(color: Colors.red.withOpacity(0.2)), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.1), + blurRadius: 2, + offset: Offset(0, 1), + ), + ], + ), + child: Icon(Icons.remove, color: Colors.red, size: 4.w), + ), + ), + ), + ), + ], + ); + } +} diff --git a/Mobile Operasional/lib/features/goods/products/presentation/widgets/image_uploader_product_category.dart b/Mobile Operasional/lib/features/goods/products/presentation/widgets/image_uploader_product_category.dart new file mode 100644 index 0000000..b7425ad --- /dev/null +++ b/Mobile Operasional/lib/features/goods/products/presentation/widgets/image_uploader_product_category.dart @@ -0,0 +1,126 @@ +import 'dart:io'; +import 'package:flutter/material.dart'; +import 'package:niogu_app/core/constants/app_color.dart'; +import 'package:niogu_app/core/constants/app_font_size.dart'; +import 'package:sizer/sizer.dart'; + +class ImageUploaderProductCategory extends StatelessWidget { + final String? categoryImage; + final VoidCallback onTapCategoryImageAdd; + final VoidCallback onTapCategoryImageRemove; + + const ImageUploaderProductCategory({ + super.key, + this.categoryImage, + required this.onTapCategoryImageAdd, + required this.onTapCategoryImageRemove, + }); + + @override + Widget build(BuildContext context) { + final bool isTablet = 100.w >= 600; + + final bool hasImage = categoryImage != null && categoryImage!.isNotEmpty; + + if (hasImage) { + final File imageFile = File(categoryImage ?? "image not found"); + + final bool imageFileExists = imageFile.existsSync(); + + return Stack( + clipBehavior: Clip.none, + children: [ + Container( + height: 15.h, + width: 15.h, + decoration: BoxDecoration( + border: Border.all(color: Colors.grey), + borderRadius: BorderRadius.circular(2.5.w), + color: Colors.grey[200], // Simulasi gambar + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(2.5.w), + child: imageFileExists + ? Image.file( + imageFile, + fit: BoxFit.cover, + width: double.infinity, + height: double.infinity, + errorBuilder: (context, error, stackTrace) { + return Icon(Icons.error, color: Colors.grey, size: 8.w); + }, + ) + : Icon(Icons.image, color: Colors.grey, size: 8.w), + ), + ), + + Positioned( + top: 1.w, + right: 1.w, + child: Material( + color: Colors.transparent, + type: MaterialType.transparency, + child: InkWell( + onTap: onTapCategoryImageRemove, + child: Container( + padding: EdgeInsets.all(1.w), + decoration: BoxDecoration( + color: Colors.red[50], + shape: BoxShape.circle, + border: Border.all(color: Colors.red.withOpacity(0.2)), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.1), + blurRadius: 2, + offset: Offset(0, 1), + ), + ], + ), + child: Icon(Icons.remove, color: Colors.red, size: 4.w), + ), + ), + ), + ), + ], + ); + } else { + return Material( + type: MaterialType.transparency, + color: Colors.white, + child: GestureDetector( + onTap: onTapCategoryImageAdd, + child: Container( + width: 15.h, + height: 15.h, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(2.5.w), + border: Border.all(color: Colors.grey.shade300), + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.add_a_photo_rounded, + color: AppColor.primaryColor, + size: 7.w, + ), + SizedBox(height: 0.5.h), + Text( + "Tambah\nFoto", + textAlign: TextAlign.center, + style: TextStyle( + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + color: Colors.grey[600], + ), + ), + ], + ), + ), + ), + ); + } + } +} diff --git a/Mobile Operasional/lib/features/goods/products/presentation/widgets/image_uploader_product_variant.dart b/Mobile Operasional/lib/features/goods/products/presentation/widgets/image_uploader_product_variant.dart new file mode 100644 index 0000000..cd05563 --- /dev/null +++ b/Mobile Operasional/lib/features/goods/products/presentation/widgets/image_uploader_product_variant.dart @@ -0,0 +1,130 @@ +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:niogu_app/core/constants/app_font_size.dart'; +import 'package:niogu_app/core/enums/user_role.dart'; +import 'package:niogu_app/features/goods/products/presentation/widgets/product_variant_model.dart'; +import 'package:sizer/sizer.dart'; + +class ImageUploaderProductVariant extends StatelessWidget { + final UserRole userRole; + final bool isMainOutlet; + final ProductVariantModel variant; + final VoidCallback onTapVariantImageAdd; + final VoidCallback onTapVariantImageRemove; + + const ImageUploaderProductVariant({ + super.key, + required this.userRole, + required this.isMainOutlet, + required this.variant, + required this.onTapVariantImageAdd, + required this.onTapVariantImageRemove, + }); + + @override + Widget build(BuildContext context) { + final bool hasImage = + variant.imagePath != null && variant.imagePath!.isNotEmpty; + + if (hasImage) { + final File imageFile = File(variant.imagePath ?? "image not found"); + final bool imageFileExists = imageFile.existsSync(); + + return Stack( + clipBehavior: Clip.none, + children: [ + Container( + width: 25.w, + height: 25.w, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(2.5.w), + border: Border.all(color: Colors.grey.shade300), + ), + child: imageFileExists + ? ClipRRect( + borderRadius: BorderRadius.circular(2.5.w), + child: Image.file( + imageFile, + fit: BoxFit.cover, + width: double.infinity, + height: double.infinity, + errorBuilder: (context, error, stackTrace) { + return Icon( + Icons.error, + color: Colors.grey[400], + size: 8.w, + ); + }, + ), + ) + : Icon(Icons.image, color: Colors.grey[400], size: 8.w), + ), + + if (userRole == UserRole.owner && isMainOutlet) + Positioned( + top: 1.w, + right: 1.w, + child: Material( + color: Colors.transparent, + type: MaterialType.transparency, + child: InkWell( + onTap: onTapVariantImageRemove, + child: Container( + padding: EdgeInsets.all(1.w), + decoration: BoxDecoration( + color: Colors.red[50], + shape: BoxShape.circle, + border: Border.all(color: Colors.red.withOpacity(0.2)), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.1), + blurRadius: 2, + offset: Offset(0, 1), + ), + ], + ), + child: Icon(Icons.remove, color: Colors.red, size: 4.w), + ), + ), + ), + ), + ], + ); + } else { + return GestureDetector( + onTap: onTapVariantImageAdd, + child: Container( + width: 25.w, + height: 25.w, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(2.5.w), + border: Border.all(color: Colors.grey.shade300), + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + userRole == UserRole.owner && isMainOutlet + ? Icons.camera_alt_outlined + : Icons.image_outlined, + color: Colors.grey, + size: 6.w, + ), + if (userRole == UserRole.owner && isMainOutlet) ...[ + SizedBox(height: 0.5.h), + Text( + "Foto", + style: TextStyle( + fontSize: AppFontSize.small.sp, + color: Colors.grey, + ), + ), + ], + ], + ), + ), + ); + } + } +} diff --git a/Mobile Operasional/lib/features/goods/products/presentation/widgets/manage_varian_shimmer.dart b/Mobile Operasional/lib/features/goods/products/presentation/widgets/manage_varian_shimmer.dart new file mode 100644 index 0000000..428f65e --- /dev/null +++ b/Mobile Operasional/lib/features/goods/products/presentation/widgets/manage_varian_shimmer.dart @@ -0,0 +1,162 @@ +import 'package:flutter/material.dart'; +import 'package:shimmer/shimmer.dart'; +import 'package:sizer/sizer.dart'; + +class ManageVariantShimmer extends StatelessWidget { + const ManageVariantShimmer({super.key}); + + @override + Widget build(BuildContext context) { + final baseColor = Colors.grey[300]!; + final highlightColor = Colors.grey[100]!; + const backgroundColor = Color(0xFFF9FAFB); + + return Scaffold( + backgroundColor: backgroundColor, + body: Shimmer.fromColors( + baseColor: baseColor, + highlightColor: highlightColor, + child: Column( + children: [ + Expanded( + child: SingleChildScrollView( + padding: EdgeInsets.fromLTRB(5.w, 3.h, 5.w, 0), + child: Column( + children: [ + Container( + width: double.infinity, + height: 5.h, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(2.w), + ), + ), + SizedBox(height: 3.h), + + _buildVariantCardShimmer(), + + SizedBox(height: 3.h), + + _buildVariantCardShimmer(), + + SizedBox(height: 3.h), + + Container( + width: double.infinity, + height: 7.h, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(2.5.w), + border: Border.all(color: Colors.white, width: 2), + ), + ), + ], + ), + ), + ), + ], + ), + ), + ); + } + + Widget _buildVariantCardShimmer() { + return Container( + padding: EdgeInsets.all(4.w), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(4.w), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Container( + // Badge Varian #1 + width: 25.w, + height: 3.h, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(4.5.w), + ), + ), + Container( + // Tombol Hapus (Bulat) + width: 8.w, + height: 8.w, + decoration: const BoxDecoration( + color: Colors.white, + shape: BoxShape.circle, + ), + ), + ], + ), + + SizedBox(height: 2.h), + + Row( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Container( + width: 20.w, + height: 20.w, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(2.5.w), + ), + ), + SizedBox(width: 3.w), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container(width: 20.w, height: 1.5.h, color: Colors.white), + SizedBox(height: 1.h), + Container( + height: 6.h, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(2.5.w), + ), + ), + ], + ), + ), + ], + ), + + SizedBox(height: 2.5.h), + + _buildInputShimmer(), + + SizedBox(height: 2.5.h), + + _buildInputShimmer(), + + SizedBox(height: 2.5.h), + + _buildInputShimmer(), + ], + ), + ); + } + + Widget _buildInputShimmer() { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container(width: 15.w, height: 1.5.h, color: Colors.white), + SizedBox(height: 1.h), + Container( + width: double.infinity, + height: 6.h, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(2.5.w), + ), + ), + ], + ); + } +} diff --git a/Mobile Operasional/lib/features/goods/products/presentation/widgets/product_list_shimmer.dart b/Mobile Operasional/lib/features/goods/products/presentation/widgets/product_list_shimmer.dart new file mode 100644 index 0000000..25171c0 --- /dev/null +++ b/Mobile Operasional/lib/features/goods/products/presentation/widgets/product_list_shimmer.dart @@ -0,0 +1,97 @@ +import 'package:flutter/material.dart'; +import 'package:shimmer/shimmer.dart'; +import 'package:sizer/sizer.dart'; + +class ProductListShimmer extends StatelessWidget { + const ProductListShimmer({super.key}); + + @override + Widget build(BuildContext context) { + final baseColor = Colors.grey[300]!; + final highlightColor = Colors.grey[100]!; + + return ListView.builder( + padding: EdgeInsets.fromLTRB(4.w, 2.h, 4.w, 12.h), + itemCount: 6, + itemBuilder: (context, index) { + return Container( + margin: EdgeInsets.only(bottom: 2.h), + padding: EdgeInsets.all(3.w), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(2.5.w), + border: Border.all(color: Colors.grey.shade200), + ), + child: Shimmer.fromColors( + baseColor: baseColor, + highlightColor: highlightColor, + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + width: 25.w, + height: 25.w, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(2.5.w), + border: Border.all(color: Colors.white), + ), + ), + + SizedBox(width: 4.w), + + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + width: 40.w, + height: 2.h, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(1.w), + ), + ), + SizedBox(height: 1.5.h), + + _buildLinePlaceholder(width: 25.w), + SizedBox(height: 0.8.h), + + _buildLinePlaceholder(width: 20.w), + SizedBox(height: 0.8.h), + + _buildLinePlaceholder(width: 30.w), + ], + ), + ), + + Padding( + padding: EdgeInsets.only(top: 1.h), + child: Container( + width: 5.w, + height: 5.w, + decoration: const BoxDecoration( + color: Colors.white, + shape: BoxShape.circle, + ), + ), + ), + ], + ), + ), + ); + }, + ); + } + + Widget _buildLinePlaceholder({required double width}) { + return Container( + width: width, + height: 1.2.h, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(0.5), + ), + ); + } +} \ No newline at end of file diff --git a/Mobile Operasional/lib/features/goods/products/presentation/widgets/product_variant_model.dart b/Mobile Operasional/lib/features/goods/products/presentation/widgets/product_variant_model.dart new file mode 100644 index 0000000..2792542 --- /dev/null +++ b/Mobile Operasional/lib/features/goods/products/presentation/widgets/product_variant_model.dart @@ -0,0 +1,17 @@ +import 'package:flutter/material.dart'; + +class ProductVariantModel { + String? productVariantId; + String? outletInventoryId; + String? imagePath; + TextEditingController nameController = TextEditingController(); + TextEditingController stockController = TextEditingController(); + TextEditingController costPriceController = TextEditingController(); + TextEditingController sellingPriceController = TextEditingController(); + + void dispose() { + nameController.dispose(); + stockController.dispose(); + sellingPriceController.dispose(); + } +} \ No newline at end of file diff --git a/Mobile Operasional/lib/features/goods/products/presentation/widgets/selection_card.dart b/Mobile Operasional/lib/features/goods/products/presentation/widgets/selection_card.dart new file mode 100644 index 0000000..85d3128 --- /dev/null +++ b/Mobile Operasional/lib/features/goods/products/presentation/widgets/selection_card.dart @@ -0,0 +1,67 @@ +import 'package:flutter/material.dart'; +import 'package:niogu_app/core/constants/app_color.dart'; +import 'package:niogu_app/core/constants/app_font_size.dart'; +import 'package:sizer/sizer.dart'; + +class SelectionCard extends StatelessWidget { + final bool isSelected; + final String title; + final VoidCallback onTap; + + const SelectionCard({ + super.key, + required this.isSelected, + required this.title, + required this.onTap, + }); + + @override + Widget build(BuildContext context) { + final isTablet = 100.w >= 600; + return InkWell( + onTap: onTap, + borderRadius: BorderRadius.circular(2.5.w), + child: AnimatedContainer( + duration: const Duration(milliseconds: 200), + padding: EdgeInsets.symmetric(horizontal: 4.w, vertical: 2.h), + decoration: BoxDecoration( + color: Colors.white, + border: Border.all(color: Colors.grey.shade300, width: 1), + borderRadius: BorderRadius.circular(2.5.w), + ), + child: Row( + children: [ + Container( + padding: const EdgeInsets.all(2), + decoration: BoxDecoration( + shape: BoxShape.circle, + border: Border.all(color: Colors.grey.shade400, width: 2), + ), + child: Container( + width: 2.5.w, + height: 2.5.w, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: isSelected + ? AppColor.primaryColor + : Colors.transparent, + ), + ), + ), + SizedBox(width: 3.w), + Text( + title, + style: TextStyle( + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + fontWeight: isSelected ? FontWeight.bold : FontWeight.normal, + color: isSelected ? AppColor.primaryColor : Colors.black87, + ), + ), + ], + ), + ), + ); + } +} diff --git a/Mobile Operasional/lib/features/goods/products/presentation/widgets/variant_product_card.dart b/Mobile Operasional/lib/features/goods/products/presentation/widgets/variant_product_card.dart new file mode 100644 index 0000000..dc10ccd --- /dev/null +++ b/Mobile Operasional/lib/features/goods/products/presentation/widgets/variant_product_card.dart @@ -0,0 +1,292 @@ +import 'package:flutter/material.dart'; +import 'package:niogu_app/core/constants/app_color.dart'; +import 'package:niogu_app/core/constants/app_font_size.dart'; +import 'package:niogu_app/core/enums/user_role.dart'; +import 'package:niogu_app/core/widgets/custom_text_form_field.dart'; +import 'package:niogu_app/features/goods/products/presentation/widgets/product_variant_model.dart'; +import 'package:niogu_app/features/goods/products/presentation/widgets/image_uploader_product_variant.dart'; +import 'package:niogu_app/features/goods/raw_materials/presentation/widgets/checkbox_product_card.dart'; +import 'package:sizer/sizer.dart'; + +class VariantProductCard extends StatelessWidget { + final UserRole userRole; + final bool isMainOutlet; + final bool isEditing; + final bool isSelected; + final GlobalKey formKey; + final int length; + final ProductVariantModel variant; + final int index; + final bool isStockFixed; + final TextEditingController nameController; + final TextEditingController stockController; + final TextEditingController costPriceController; + final TextEditingController sellingPriceController; + final VoidCallback? onTapSelected; + final VoidCallback onTapRemoveVarian; + final VoidCallback onTapVariantImageAdd; + final VoidCallback onTapVariantImageRemove; + + const VariantProductCard({ + super.key, + required this.userRole, + required this.isMainOutlet, + required this.isEditing, + this.isSelected = false, + required this.formKey, + required this.length, + required this.variant, + required this.index, + required this.isStockFixed, + required this.nameController, + required this.stockController, + required this.costPriceController, + required this.sellingPriceController, + this.onTapSelected, + required this.onTapRemoveVarian, + required this.onTapVariantImageAdd, + required this.onTapVariantImageRemove, + }); + + @override + Widget build(BuildContext context) { + final double costPrice = + double.tryParse(costPriceController.text.trim()) ?? 0.0; + + final double sellingPrice = + double.tryParse(sellingPriceController.text.trim()) ?? 0.0; + + final bool validatePrice = + double.tryParse(costPriceController.text.trim()) != null && + double.tryParse(sellingPriceController.text.trim()) != null && + costPrice > 0 && + sellingPrice > 0 && + costPrice >= sellingPrice; + + final enabledBorderColor = validatePrice + ? Colors.red + : Colors.grey.shade300; + + final focusedBorderColor = validatePrice + ? Colors.red + : AppColor.primaryColor; + + final bool readOnly = + isEditing && + variant.productVariantId != null && + variant.outletInventoryId != null; + + final String? message = readOnly ? "Tambah stok di menu stok masuk" : null; + + String title = "Variant #${index + 1}"; + + final isNewVariant = + ((userRole == UserRole.owner && !isMainOutlet) || + userRole == UserRole.admin) && + variant.outletInventoryId == null; + + if (isNewVariant) title = "Varian Baru"; + + return Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(4.w), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.03), + blurRadius: 10, + offset: const Offset(0, 4), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: EdgeInsets.fromLTRB(4.w, 2.h, 4.w, 0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Container( + padding: EdgeInsets.symmetric( + horizontal: 3.w, + vertical: 0.5.h, + ), + decoration: BoxDecoration( + color: AppColor.primaryColor.withOpacity(0.1), + borderRadius: BorderRadius.circular(4.05.w), + ), + child: Text( + title, + style: TextStyle( + color: AppColor.primaryColor, + fontWeight: FontWeight.bold, + fontSize: AppFontSize.small.sp, + ), + ), + ), + + if (length > 1 && userRole == UserRole.owner && isMainOutlet) + InkWell( + onTap: onTapRemoveVarian, + borderRadius: BorderRadius.circular(4.w), + child: Container( + padding: EdgeInsets.all(2.w), + decoration: BoxDecoration( + color: Colors.red[50], + borderRadius: BorderRadius.circular(4.w), + ), + child: Icon( + Icons.delete_outline_rounded, + color: Colors.red, + size: 5.w, + ), + ), + ) + else if (isNewVariant) + GestureDetector( + onTap: onTapSelected, + child: CheckboxProductCard(isSelected: isSelected), + ), + ], + ), + ), + + Divider(color: Colors.grey[100], height: 3.h), + + Form( + key: formKey, + child: Padding( + padding: EdgeInsets.fromLTRB(4.w, 0, 4.w, 3.h), + child: Column( + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ImageUploaderProductVariant( + userRole: userRole, + isMainOutlet: isMainOutlet, + variant: variant, + onTapVariantImageAdd: onTapVariantImageAdd, + onTapVariantImageRemove: onTapVariantImageRemove, + ), + + SizedBox(width: 3.w), + + Expanded( + child: CustomTextFormField( + label: "Nama Varian", + controller: nameController, + validator: (value) { + if (value == null || value.isEmpty) { + return "Nama varian belum diisi"; + } + + return null; + }, + readOnly: + (userRole == UserRole.owner && !isMainOutlet) || + userRole == UserRole.admin, + ), + ), + ], + ), + + SizedBox(height: 2.5.h), + + CustomTextFormField( + label: "Harga Modal", + hint: "0", + keyboardType: TextInputType.number, + controller: costPriceController, + validator: (value) { + if (value == null || value.isEmpty) { + return "Harga modal belum diisi"; + } + + try { + if (double.parse(value) <= 0) { + return "Harga modal harus lebih dari 0"; + } + } catch (e) { + return "Harga modal tidak valid"; + } + + return null; + }, + enabledBorderColor: enabledBorderColor, + focusedBorderColor: focusedBorderColor, + ), + + SizedBox(height: 2.5.h), + + CustomTextFormField( + label: "Harga Jual", + hint: "0", + keyboardType: TextInputType.number, + controller: sellingPriceController, + validator: (value) { + if (value == null || value.isEmpty) { + return "Harga jual belum diisi"; + } + + try { + if (double.parse(value) <= 0) { + return "Harga jual harus lebih dari 0"; + } + } catch (e) { + return "Harga jual tidak valid"; + } + + return null; + }, + enabledBorderColor: enabledBorderColor, + focusedBorderColor: focusedBorderColor, + ), + + SizedBox(height: 2.5.h), + + if (isStockFixed) + Row( + children: [ + Expanded( + child: CustomTextFormField( + label: "Stok", + hint: "0", + keyboardType: TextInputType.number, + controller: stockController, + validator: isStockFixed + ? (value) { + if (value == null || value.isEmpty) { + return "Stok belum diisi"; + } + + try { + if (!readOnly && + double.parse(value) <= 0) { + if (isStockFixed) { + return "Stok harus lebih dari 0"; + } + } + } catch (e) { + return "Stok tidak valid"; + } + + return null; + } + : null, + readOnly: readOnly, + onTapReadOnlyMessage: message, + ), + ), + ], + ), + ], + ), + ), + ), + ], + ), + ); + } +} diff --git a/Mobile Operasional/lib/features/goods/raw_materials/data/repositories/raw_material_repository_impl.dart b/Mobile Operasional/lib/features/goods/raw_materials/data/repositories/raw_material_repository_impl.dart new file mode 100644 index 0000000..d34c81d --- /dev/null +++ b/Mobile Operasional/lib/features/goods/raw_materials/data/repositories/raw_material_repository_impl.dart @@ -0,0 +1,144 @@ +import 'package:drift/drift.dart'; +import 'package:niogu_app/core/database/app_database.dart' + hide RawMaterial, StockCard; +import 'package:niogu_app/features/goods/domain/entities/stock_card.dart'; +import 'package:niogu_app/features/goods/raw_materials/domain/entities/raw_material.dart'; +import 'package:niogu_app/features/goods/raw_materials/domain/repositories/i_raw_material_repository.dart'; +import 'package:niogu_app/features/goods/domain/entities/outlet_inventory.dart'; + +class RawMaterialRepositoryImpl implements IRawMaterialRepository { + final AppDatabase _appDatabase; + + RawMaterialRepositoryImpl(this._appDatabase); + + @override + Stream> watchMaterials(String currentOutlet) { + return _appDatabase.rawMaterialDao.watchAllMaterials(); + } + + @override + Future fetchRawMaterialById( + String id, + bool isEditing, + ) async { + return await _appDatabase.rawMaterialDao.getRawMaterialById(id, isEditing); + } + + @override + Stream> watchProductUsageMaterials( + String currentOutlet, + bool isEditing, + ) { + return _appDatabase.rawMaterialDao.watchAllProductUsageMaterials(isEditing); + } + + @override + Future fetchDeleteRawMaterialSnapshotById( + String id, + ) async { + return await _appDatabase.rawMaterialDao.getRawMaterialSnapshotById(id); + } + + @override + Future checkAlreadyRawMaterialByOutlet(String id) async { + return await _appDatabase.rawMaterialDao.checkAlreadyRawMaterialByOutlet( + id, + ); + } + + @override + Stream> watchRawMaterialChoices() { + return _appDatabase.rawMaterialDao.watchAllRawMaterialChoices(); + } + + @override + Future saveRawMaterial({ + required UpsertRawMaterial rawMaterial, + required List receiptItems, + List? deleteItems, + required List outletInventories, + required List stockCards, + }) async { + final rawMaterialCompanions = RawMaterialsCompanion( + localId: Value(rawMaterial.localId), + unitId: Value(rawMaterial.unitId), + name: Value(rawMaterial.name), + ); + + final List receiptItemCompanions = []; + + for (var item in receiptItems) { + receiptItemCompanions.add( + ReceiptItemsCompanion( + localId: Value(item.localId), + productVariantId: Value(item.productUsageId), + rawMaterialId: Value(item.rawMaterialId), + quantity: Value(item.quantity), + ), + ); + } + + final List outletInventoryCompanions = []; + + for (var outletInventory in outletInventories) { + outletInventoryCompanions.add( + OutletInventoriesCompanion( + localId: Value(outletInventory.localId), + outletId: Value(outletInventory.outletId), + itemType: Value(outletInventory.itemType.type), + itemId: Value(outletInventory.itemId), + stockType: Value(outletInventory.stockType.type), + stock: Value(outletInventory.stock), + sellingPrice: Value(outletInventory.sellingPrice), + costPrice: Value(outletInventory.costPrice), + purchasePrice: Value(outletInventory.purchasePrice), + syncStatus: Value(outletInventory.syncStatus.status), + ), + ); + } + + final List cards = []; + + if (stockCards.isNotEmpty) { + for (var stockCard in stockCards) { + cards.add( + StockCardsCompanion( + localId: Value(stockCard.localId), + outletInventoryId: Value(stockCard.outletInventoryId), + userId: Value(stockCard.userId), + type: Value(stockCard.stockCardType.type), + qtyChange: Value(stockCard.qtyChange), + currentStock: Value(stockCard.currentStock), + costPrice: Value(stockCard.costPrice), + stockType: Value(stockCard.stockType.type), + modeChange: Value(stockCard.modeChange), + modeBeforeChange: Value(stockCard.modeBeforeChange), + modeAfterChange: Value(stockCard.modeAfterChange), + syncStatus: Value(stockCard.syncStatus.status), + ), + ); + } + } + + await _appDatabase.outletInventoryDao.saveOutletInventory( + rawMaterial: rawMaterialCompanions, + items: receiptItemCompanions, + deleteItems: deleteItems, + inventories: outletInventoryCompanions, + cards: cards, + ); + } + + @override + Future deleteRawMaterial( + String id, { + String? outletId, + bool canDeleteAll = false, + }) async { + await _appDatabase.outletInventoryDao.deleteOutletInventory( + rawMaterialId: id, + outletId: outletId, + canDeleteAll: canDeleteAll, + ); + } +} diff --git a/Mobile Operasional/lib/features/goods/raw_materials/domain/entities/raw_material.dart b/Mobile Operasional/lib/features/goods/raw_materials/domain/entities/raw_material.dart new file mode 100644 index 0000000..1bc7420 --- /dev/null +++ b/Mobile Operasional/lib/features/goods/raw_materials/domain/entities/raw_material.dart @@ -0,0 +1,236 @@ +import 'package:niogu_app/core/enums/sync_status.dart'; +import 'package:uuid/uuid.dart'; + +enum RawMaterialEmpty { loading, empty_database, empty_search_result, has_data } + +class RawMaterial { + final String id; + final String name; + final String remainingStock; + final String displayPurchasePrice; + final int countProductUsage; + + const RawMaterial({ + required this.id, + required this.name, + required this.remainingStock, + required this.displayPurchasePrice, + required this.countProductUsage, + }); + + RawMaterial copyWith({int? countProductUsage}) { + return RawMaterial( + id: id, + name: name, + remainingStock: remainingStock, + displayPurchasePrice: displayPurchasePrice, + countProductUsage: countProductUsage ?? this.countProductUsage, + ); + } +} + +class RawMaterialDetail { + final String id; + final String name; + final double purchasePrice; + final double stock; + final String unitId; + final String outletInventoryId; + final List productUsages; + + const RawMaterialDetail({ + required this.id, + required this.name, + required this.purchasePrice, + required this.stock, + required this.unitId, + required this.outletInventoryId, + required this.productUsages, + }); +} + +class UpsertRawMaterial { + final String localId; + final String unitId; + final String name; + final SyncStatus syncStatus; + + UpsertRawMaterial({ + String? localId, + required this.unitId, + required this.name, + this.syncStatus = SyncStatus.dirty, + }) : localId = localId ?? Uuid().v7().toString(); +} + +class UpsertReceiptItem { + final String localId; + final String productUsageId; + final String rawMaterialId; + final double quantity; + final SyncStatus syncStatus; + + UpsertReceiptItem({ + String? localId, + required this.productUsageId, + required this.rawMaterialId, + required this.quantity, + this.syncStatus = SyncStatus.dirty, + }) : localId = localId ?? Uuid().v7().toString(); +} + +enum ProductUsageRawMaterialEmpty { + loading, + empty_database, + empty_search_result, + has_data, +} + +class ProductUsageMaterial { + final String id; + final String name; + final String? imagePath; + final bool hasVariant; + final List variants; + final bool hasBeenDelete; + + const ProductUsageMaterial({ + required this.id, + required this.name, + this.imagePath, + this.hasVariant = false, + required this.variants, + required this.hasBeenDelete, + }); +} + +class VariantUsageMaterial { + final String id; + final String name; + final bool isProductVariant; + final bool hasBeenDelete; + + const VariantUsageMaterial({ + required this.id, + required this.name, + required this.isProductVariant, + required this.hasBeenDelete, + }); +} + +class SelectedProductUsageMaterial { + final String? receiptItemId; + final String productUsageId; + final double quantity; + + const SelectedProductUsageMaterial({ + this.receiptItemId, + required this.productUsageId, + required this.quantity, + }); + + SelectedProductUsageMaterial copyWith({double? quantity}) { + return SelectedProductUsageMaterial( + receiptItemId: receiptItemId, + productUsageId: productUsageId, + quantity: quantity ?? this.quantity, + ); + } +} + +class DeleteProductUsageMaterial { + final String rawMaterialId; + final String productUsageId; + + const DeleteProductUsageMaterial({ + required this.rawMaterialId, + required this.productUsageId, + }); +} + +class DeleteRawMaterialSnapshot { + final String rawMaterialId; + final String name; + final int totalProductUsage; + final String currentOutletName; + final bool isMainOutlet; + final int totalOutletCount; + + const DeleteRawMaterialSnapshot({ + required this.rawMaterialId, + required this.name, + required this.totalProductUsage, + required this.currentOutletName, + required this.isMainOutlet, + required this.totalOutletCount, + }); +} + +class DeleteRawMaterial { + final String rawMaterialId; + final String outletInventoryId; + final String receiptItemId; + + const DeleteRawMaterial({ + required this.rawMaterialId, + required this.outletInventoryId, + required this.receiptItemId, + }); +} + +class UpdateStockRawMaterial { + final String outletInventoryId; + final String productVariantId; + final String rawMaterialId; + final String name; + final String unit; + final double productQuantity; + final double quantity; + final double stock; + final double purchasePrice; + + const UpdateStockRawMaterial({ + this.outletInventoryId = '', + required this.productVariantId, + required this.rawMaterialId, + required this.name, + required this.unit, + required this.productQuantity, + required this.quantity, + this.stock = 0.0, + this.purchasePrice = 0.0, + }); + + UpdateStockRawMaterial copyWith({ + String? outletInventoryId, + double? stock, + double? purchasePrice, + }) { + return UpdateStockRawMaterial( + outletInventoryId: outletInventoryId ?? this.outletInventoryId, + productVariantId: productVariantId, + rawMaterialId: rawMaterialId, + name: name, + unit: unit, + productQuantity: productQuantity, + quantity: quantity, + stock: stock ?? this.stock, + purchasePrice: purchasePrice ?? this.purchasePrice, + ); + } +} + +enum RawMaterialChoiceEmpty { loading, empty_database, empty_search_result, has_data } + + +class RawMaterialChoice { + final String id; + final String name; + final int countProductUsage; + + const RawMaterialChoice({ + required this.id, + required this.name, + required this.countProductUsage + }); +} diff --git a/Mobile Operasional/lib/features/goods/raw_materials/domain/repositories/i_raw_material_repository.dart b/Mobile Operasional/lib/features/goods/raw_materials/domain/repositories/i_raw_material_repository.dart new file mode 100644 index 0000000..2453ac9 --- /dev/null +++ b/Mobile Operasional/lib/features/goods/raw_materials/domain/repositories/i_raw_material_repository.dart @@ -0,0 +1,33 @@ +import 'package:niogu_app/features/goods/domain/entities/stock_card.dart'; +import 'package:niogu_app/features/goods/raw_materials/domain/entities/raw_material.dart'; +import 'package:niogu_app/features/goods/domain/entities/outlet_inventory.dart'; + +abstract class IRawMaterialRepository { + Stream> watchMaterials(String currentOutlet); + + Future fetchRawMaterialById(String id, bool isEditing); + + Stream> watchProductUsageMaterials(String currentOutlet, bool isEditing); + + Future fetchDeleteRawMaterialSnapshotById( + String id, + ); + + Future checkAlreadyRawMaterialByOutlet(String id); + + Stream> watchRawMaterialChoices(); + + Future saveRawMaterial({ + required UpsertRawMaterial rawMaterial, + required List receiptItems, + List? deleteItems, + required List outletInventories, + required List stockCards, + }); + + Future deleteRawMaterial( + String id, { + String? outletId, + bool canDeleteAll = false, + }); +} diff --git a/Mobile Operasional/lib/features/goods/raw_materials/presentation/controllers/raw_material_controller.dart b/Mobile Operasional/lib/features/goods/raw_materials/presentation/controllers/raw_material_controller.dart new file mode 100644 index 0000000..ccd33bf --- /dev/null +++ b/Mobile Operasional/lib/features/goods/raw_materials/presentation/controllers/raw_material_controller.dart @@ -0,0 +1,101 @@ +import 'dart:async'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:niogu_app/features/goods/domain/entities/stock_card.dart'; +import 'package:niogu_app/features/goods/raw_materials/domain/entities/raw_material.dart'; +import 'package:niogu_app/features/goods/raw_materials/domain/repositories/i_raw_material_repository.dart'; +import 'package:niogu_app/features/goods/raw_materials/presentation/providers/raw_material_provider.dart'; +import 'package:niogu_app/features/goods/domain/entities/outlet_inventory.dart'; + +class RawMaterialController extends AutoDisposeAsyncNotifier { + late final IRawMaterialRepository _rawMaterialRepository; + @override + FutureOr build() { + _rawMaterialRepository = ref.read(rawMaterialRepositoryProvider); + } + + Future saveRawMaterial({ + required UpsertRawMaterial rawMaterial, + required List receiptItems, + List? deleteItems, + required List outletInventories, + required List stockCards, + }) async { + state = const AsyncValue.loading(); + + final result = await AsyncValue.guard(() async { + await _rawMaterialRepository.saveRawMaterial( + rawMaterial: rawMaterial, + receiptItems: receiptItems, + deleteItems: deleteItems, + outletInventories: outletInventories, + stockCards: stockCards, + ); + }); + + state = result; + + if (result is AsyncError) throw result.error!; + } + + Future deleteRawMaterial( + String id, { + String? outletId, + bool canDeleteAll = false, + }) async { + state = const AsyncValue.loading(); + + final result = await AsyncValue.guard(() async { + await _rawMaterialRepository.deleteRawMaterial( + id, + outletId: outletId, + canDeleteAll: canDeleteAll, + ); + }); + + state = result; + + if (result is AsyncError) throw result.error!; + } +} + +class SelectedProductUsageMaterialController + extends StateNotifier> { + SelectedProductUsageMaterialController() : super({}); + + double quantityOf(String productUsageId) => + state[productUsageId]?.quantity ?? 0.0; + + void hydrate(List oldData) { + state = {for (final data in oldData) data.productUsageId: data}; + } + + void restore(Map initialProductUsages) { + state = Map.from(initialProductUsages); + } + + void toggle(String productUsageId, String? receiptItemId) { + if (state.containsKey(productUsageId)) { + state = {...state}..remove(productUsageId); + } else { + state = { + ...state, + productUsageId: SelectedProductUsageMaterial( + receiptItemId: receiptItemId, + productUsageId: productUsageId, + quantity: 0.0, + ), + }; + } + } + + void setQuantity(String productUsageId, double quantity) { + if (!state.containsKey(productUsageId)) return; + + state = { + ...state, + productUsageId: state[productUsageId]!.copyWith(quantity: quantity), + }; + } + + void clear() => state = {}; +} diff --git a/Mobile Operasional/lib/features/goods/raw_materials/presentation/providers/raw_material_provider.dart b/Mobile Operasional/lib/features/goods/raw_materials/presentation/providers/raw_material_provider.dart new file mode 100644 index 0000000..436bcbb --- /dev/null +++ b/Mobile Operasional/lib/features/goods/raw_materials/presentation/providers/raw_material_provider.dart @@ -0,0 +1,234 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:niogu_app/core/providers/app_provider.dart'; +import 'package:niogu_app/features/goods/raw_materials/data/repositories/raw_material_repository_impl.dart'; +import 'package:niogu_app/features/goods/raw_materials/domain/entities/raw_material.dart'; +import 'package:niogu_app/features/goods/raw_materials/domain/repositories/i_raw_material_repository.dart'; +import 'package:niogu_app/features/goods/raw_materials/presentation/controllers/raw_material_controller.dart'; + +final rawMaterialRepositoryProvider = Provider((ref) { + final appDatabase = ref.watch(appDatabaseProvider); + return RawMaterialRepositoryImpl(appDatabase); +}); + +final rawMaterialStreamProvider = StreamProvider.autoDispose>( + (ref) { + final rawMaterialRepository = ref.watch(rawMaterialRepositoryProvider); + + final currentOutlet = ref.watch(currentOutletIdProvider); + + if(currentOutlet == null) { + return Stream>.value([]); + } + + return rawMaterialRepository.watchMaterials(currentOutlet); + }, +); + +final rawMaterialSearchProvider = StateProvider.autoDispose( + (ref) => '', +); + +final filteredRawMaterialProvider = + Provider.autoDispose>>((ref) { + final rawMaterialStreamAsync = ref.watch(rawMaterialStreamProvider); + final rawMaterialSearchAsync = ref.watch(rawMaterialSearchProvider); + return rawMaterialStreamAsync.when( + data: (rawMaterials) { + if (rawMaterialSearchAsync.trim().isEmpty) { + return AsyncValue.data(rawMaterials); + } + final filteredRawMaterials = rawMaterials.where((rawMaterial) { + return rawMaterial.name.toLowerCase().contains( + rawMaterialSearchAsync.toLowerCase(), + ); + }).toList(); + + return AsyncValue.data(filteredRawMaterials); + }, + error: (error, stackTrace) { + return AsyncValue.error(error, stackTrace); + }, + loading: () => const AsyncLoading(), + ); + }); + +final rawMaterialEmptyProvider = Provider.autoDispose((ref) { + final rawMaterialStreamAsync = ref.watch(rawMaterialStreamProvider); + final filteredRawMaterialAsync = ref.watch(filteredRawMaterialProvider); + final rawMaterialSearchAsync = ref.watch(rawMaterialSearchProvider); + + final allRawMaterials = rawMaterialStreamAsync.value ?? []; + + if (allRawMaterials.isEmpty) { + return RawMaterialEmpty.empty_database; + } + + final filteredRawMaterial = filteredRawMaterialAsync.value ?? []; + + if (rawMaterialSearchAsync.isNotEmpty && filteredRawMaterial.isEmpty) { + return RawMaterialEmpty.empty_search_result; + } + + return RawMaterialEmpty.has_data; +}); + +final productUsageMaterialStreamProvider = StreamProvider.family + .autoDispose, bool>((ref, isEditing) { + final rawMaterialRepository = ref.watch(rawMaterialRepositoryProvider); + + final currentOutlet = ref.watch(currentOutletIdProvider); + + if(currentOutlet == null) { + return Stream>.value([]); + } + + return rawMaterialRepository.watchProductUsageMaterials( + currentOutlet, + isEditing, + ); + }); + +final productUsageMaterialSearchProvider = StateProvider.autoDispose( + (ref) => '', +); + +final filteredProductUsageMaterialProvider = + Provider.family.autoDispose>, bool>((ref,isEditing) { + final productUsageMaterialStreamAsync = ref.watch( + productUsageMaterialStreamProvider(isEditing), + ); + final productUsageMaterialSearchAsync = ref.watch( + productUsageMaterialSearchProvider, + ); + + return productUsageMaterialStreamAsync.when( + data: (productUsageMaterials) { + if (productUsageMaterialSearchAsync.trim().isEmpty) { + return AsyncValue.data(productUsageMaterials); + } + + final filteredProductUsageMaterials = productUsageMaterials.where(( + productUsageMaterial, + ) { + return productUsageMaterial.name.toLowerCase().contains( + productUsageMaterialSearchAsync.toLowerCase(), + ); + }).toList(); + + return AsyncValue.data(filteredProductUsageMaterials); + }, + error: (error, stackTrace) { + return AsyncValue.error(error, stackTrace); + }, + loading: () => const AsyncValue.loading(), + ); + }); + +final productUsageMaterialEmptyProvider = + Provider.family.autoDispose((ref, isEditing) { + final productUsageMaterialStreamAsync = ref.watch( + productUsageMaterialStreamProvider(isEditing) + ); + final filteredProductUsageMaterialAsync = ref.watch( + filteredProductUsageMaterialProvider(isEditing) + ); + final productUsageMaterialSearchAsync = ref.watch( + productUsageMaterialSearchProvider, + ); + + final allProductUsageMaterials = + productUsageMaterialStreamAsync.value ?? []; + + if (allProductUsageMaterials.isEmpty) { + return ProductUsageRawMaterialEmpty.empty_database; + } + + final filteredProductUsageMaterials = + filteredProductUsageMaterialAsync.value ?? []; + + if (productUsageMaterialSearchAsync.isNotEmpty && + filteredProductUsageMaterials.isEmpty) { + return ProductUsageRawMaterialEmpty.empty_search_result; + } + + return ProductUsageRawMaterialEmpty.has_data; + }); + +final rawMaterialControllerProvider = + AsyncNotifierProvider.autoDispose( + RawMaterialController.new, + ); + +final selectedProductUsageMaterialProvider = + StateNotifierProvider.autoDispose< + SelectedProductUsageMaterialController, + Map + >((ref) { + return SelectedProductUsageMaterialController(); + }); + +final receiptItemIdProvider = StateProvider>((ref) => {}); + +final expandedProductProvider = StateProvider.family.autoDispose( + (ref, productId) => false, +); + +final deleteProductUsageMaterialProvider = + StateProvider>((ref) => []); + +final rawMaterialChoiceStreamProvider = + StreamProvider.autoDispose>((ref) { + final rawMaterialRepository = ref.watch(rawMaterialRepositoryProvider); + return rawMaterialRepository.watchRawMaterialChoices(); + }); + +final rawMaterialChoiceSearchProvider = StateProvider.autoDispose( + (ref) => '', +); + +final filteredRawMaterialChoiceProvider = + Provider.autoDispose>>((ref) { + final rawMaterialStreamAsync = ref.watch(rawMaterialChoiceStreamProvider); + final rawMaterialSearchAsync = ref.watch(rawMaterialChoiceSearchProvider); + return rawMaterialStreamAsync.when( + data: (rawMaterials) { + if (rawMaterialSearchAsync.trim().isEmpty) { + return AsyncValue.data(rawMaterials); + } + final filteredRawMaterials = rawMaterials.where((rawMaterial) { + return rawMaterial.name.toLowerCase().contains( + rawMaterialSearchAsync.toLowerCase(), + ); + }).toList(); + + return AsyncValue.data(filteredRawMaterials); + }, + error: (error, stackTrace) { + return AsyncValue.error(error, stackTrace); + }, + loading: () => const AsyncLoading(), + ); + }); + +final rawMaterialChoiceEmptyProvider = + Provider.autoDispose((ref) { + final rawMaterialStreamAsync = ref.watch(rawMaterialChoiceStreamProvider); + final filteredRawMaterialAsync = ref.watch( + filteredRawMaterialChoiceProvider, + ); + final rawMaterialSearchAsync = ref.watch(rawMaterialChoiceSearchProvider); + + final allRawMaterials = rawMaterialStreamAsync.value ?? []; + + if (allRawMaterials.isEmpty) { + return RawMaterialChoiceEmpty.empty_database; + } + + final filteredRawMaterial = filteredRawMaterialAsync.value ?? []; + + if (rawMaterialSearchAsync.isNotEmpty && filteredRawMaterial.isEmpty) { + return RawMaterialChoiceEmpty.empty_search_result; + } + + return RawMaterialChoiceEmpty.has_data; + }); diff --git a/Mobile Operasional/lib/features/goods/raw_materials/presentation/screens/add_raw_material_screen.dart b/Mobile Operasional/lib/features/goods/raw_materials/presentation/screens/add_raw_material_screen.dart new file mode 100644 index 0000000..32eb41d --- /dev/null +++ b/Mobile Operasional/lib/features/goods/raw_materials/presentation/screens/add_raw_material_screen.dart @@ -0,0 +1,426 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; +import 'package:niogu_app/core/components/top_back_bar_app.dart'; +import 'package:niogu_app/core/constants/app_color.dart'; +import 'package:niogu_app/core/constants/app_font_size.dart'; +import 'package:niogu_app/core/router/app_route.dart'; +import 'package:niogu_app/core/enums/item_type.dart'; +import 'package:niogu_app/core/utils/log_message.dart'; +import 'package:niogu_app/core/enums/stock_card_type.dart'; +import 'package:niogu_app/core/enums/stock_type.dart'; +import 'package:niogu_app/core/system/system_setting.dart'; +import 'package:niogu_app/core/widgets/custom_selector.dart'; +import 'package:niogu_app/core/widgets/custom_snackbar.dart'; +import 'package:niogu_app/core/widgets/custom_text_form_field.dart'; +import 'package:niogu_app/features/goods/domain/entities/stock_card.dart'; +import 'package:niogu_app/features/goods/presentation/widgets/dropdown_with_action.dart'; +import 'package:niogu_app/features/goods/raw_materials/domain/entities/raw_material.dart'; +import 'package:niogu_app/features/goods/raw_materials/presentation/providers/raw_material_provider.dart'; +import 'package:niogu_app/features/goods/units/domain/entities/unit.dart'; +import 'package:niogu_app/features/goods/units/presentation/providers/unit_provider.dart'; +import 'package:niogu_app/features/goods/units/presentation/screens/add_unit_screen.dart'; +import 'package:niogu_app/features/goods/units/presentation/screens/edit_unit_screen.dart'; +import 'package:niogu_app/features/goods/domain/entities/outlet_inventory.dart'; +import 'package:niogu_app/features/outlets/presentation/providers/outlet_provider.dart'; +import 'package:sizer/sizer.dart'; + +class AddRawMaterialScreen extends ConsumerStatefulWidget { + const AddRawMaterialScreen({super.key}); + + @override + ConsumerState createState() => + _AddRawMaterialScreenState(); +} + +class _AddRawMaterialScreenState extends ConsumerState { + final GlobalKey _formKey = GlobalKey(); + + final TextEditingController _nameController = TextEditingController(); + + final TextEditingController _purchasePriceController = + TextEditingController(); + + final TextEditingController _stockController = TextEditingController(); + + String? _selectedUnit; + + @override + void initState() { + // TODO: implement initState + super.initState(); + } + + @override + void dispose() { + // TODO: implement dispose + _nameController.dispose(); + _purchasePriceController.dispose(); + _stockController.dispose(); + super.dispose(); + } + + String? _validator({ + String? value, + required String message1, + String? message2, + bool isNumber = false, + bool isPurchasePrice = false, + bool isStock = false, + String? errorMessage, + }) { + if (value == null || value.isEmpty) { + return message1; + } + + try { + if (isNumber && double.parse(value) <= 0) { + if (isPurchasePrice) return message2; + + if (isStock) return message2; + } + } catch (e) { + return errorMessage; + } + + return null; + } + + Future _saveRawMaterial() async { + if (!_formKey.currentState!.validate()) { + return; + } + + if (_selectedUnit == null) { + CustomSnackbar.showError(context, "Satuan bahan baku belum dipilih"); + return; + } + + final String name = _nameController.text.trim(); + final String unitId = _selectedUnit!; + final rawMaterial = UpsertRawMaterial(unitId: unitId, name: name); + + final selectProductUsageMaterials = ref + .read(selectedProductUsageMaterialProvider) + .values + .toList(); + + final List receiptItems = selectProductUsageMaterials + .map((selectProductUsage) { + return UpsertReceiptItem( + productUsageId: selectProductUsage.productUsageId, + rawMaterialId: rawMaterial.localId, + quantity: selectProductUsage.quantity, + ); + }) + .toList(); + + final userId = await SystemSetting.getCurrentUserId(); + + final String? outletId = await SystemSetting.getCurrentOutletId(); + + try { + final double stock = double.parse(_stockController.text.trim()); + + final double purchasePrice = double.parse( + _purchasePriceController.text.trim(), + ); + + final List outletInventories = [ + UpsertOutletInventory( + outletId: outletId!, + itemType: ItemType.raw_material, + itemId: rawMaterial.localId, + stockType: StockType.fixed, + stock: stock, + costPrice: 0.0, + sellingPrice: 0.0, + purchasePrice: purchasePrice, + ), + ]; + + final List stockCards = [ + StockCard( + outletInventoryId: outletInventories.single.localId, + userId: userId!, + stockCardType: StockCardType.initial_stock, + qtyChange: stock, + currentStock: stock, + costPrice: purchasePrice, + stockType: StockType.fixed, + ), + ]; + + final selectProductUsage = ref.read(selectedProductUsageMaterialProvider); + + if (selectProductUsage.values.isEmpty) { + CustomSnackbar.showError(context, "Penggunaan bahan baku belum diatur"); + return; + } + + try { + await ref + .read(rawMaterialControllerProvider.notifier) + .saveRawMaterial( + rawMaterial: rawMaterial, + receiptItems: receiptItems, + outletInventories: outletInventories, + stockCards: stockCards, + ); + + if (!mounted) return; + + CustomSnackbar.showSuccess(context, "Bahan baku berhasil disimpan"); + + context.pop(); + } catch (e, st) { + LogMessage.log.e(e.toString(), error: e, stackTrace: st); + + CustomSnackbar.showError(context, "Ups, terjadi kesalahan"); + } + } catch (e) { + CustomSnackbar.showError(context, "Format angka tidak valid"); + } + } + + @override + Widget build(BuildContext context) { + return LayoutBuilder( + builder: (context, constraints) { + final bool isTablet = 100.w >= 600; + + final List units = []; + + final unitState = ref.watch(unitStreamProvider); + + unitState.whenData((u) => units.addAll(u)); + + final selectProductUsageMaterialState = ref.watch( + selectedProductUsageMaterialProvider, + ); + + final rawMaterialControllerState = ref.watch( + rawMaterialControllerProvider, + ); + + return PopScope( + canPop: false, + onPopInvokedWithResult: (didPop, result) async { + if (didPop) return; + + context.pop(); + }, + child: SafeArea( + top: false, + bottom: true, + right: false, + left: false, + child: Scaffold( + backgroundColor: const Color(0xFFF9FAFB), + appBar: TopBackBarApp( + title: "Tambah Bahan Baku", + onTap: () => context.pop(), + ), + + body: SingleChildScrollView( + padding: EdgeInsets.symmetric( + horizontal: 5.w, + vertical: 3.h, + ), + child: Form( + key: _formKey, + child: Column( + children: [ + CustomTextFormField( + label: "Nama Bahan Baku", + hint: "Contoh: Kopi Susu Gula Aren", + controller: _nameController, + validator: (value) => _validator( + value: value, + message1: "Nama produk belum diisi", + ), + ), + + SizedBox(height: 3.h), + + CustomTextFormField( + label: "Harga Beli", + hint: "0", + keyboardType: TextInputType.number, + controller: _purchasePriceController, + validator: (value) => _validator( + value: value, + message1: "Harga beli belum diisi", + message2: "Harga beli harus lebih dari 0", + isNumber: true, + isPurchasePrice: true, + errorMessage: "Harga beli tidak valid", + ), + ), + + SizedBox(height: 3.h), + + CustomTextFormField( + label: "Stok Awal", + hint: "100", + keyboardType: TextInputType.number, + controller: _stockController, + validator: (value) => _validator( + value: value, + message1: "Stok belum diisi", + message2: "Stok harus lebih dari 0", + isNumber: true, + isStock: true, + errorMessage: "Stok tidak valid", + ), + ), + + SizedBox(height: 3.h), + + DropdownWithAction( + label: "Satuan", + value: _selectedUnit, + items: units, + onChanged: (val) => + setState(() => _selectedUnit = val), + onAdd: () async { + FocusScope.of(context).unfocus(); + + await Future.delayed( + const Duration(milliseconds: 400), + ); + + showDialog( + context: context, + builder: (_) => const AddUnitScreen(), + ); + }, + onEdit: () async { + if (_selectedUnit == null) { + CustomSnackbar.showError( + context, + "Satuan belum dipilih", + ); + } else { + final unitRepository = ref.read( + unitRepositoryProvider, + ); + + final unit = await unitRepository.fetchUnitById( + _selectedUnit!, + ); + + if (unit != null) { + showDialog( + context: context, + builder: (_) { + return EditUnitScreen(unit: unit); + }, + ); + } else { + CustomSnackbar.showError( + context, + "Satuan produk tidak tersedia", + ); + } + } + }, + ), + + SizedBox(height: 3.h), + + CustomSelector( + isTablet: isTablet, + title: + selectProductUsageMaterialState.values.isEmpty + ? "Atur penggunaan bahan baku" + : "Lihat ${selectProductUsageMaterialState.values.length} penggunaan bahan baku", + padding: EdgeInsets.symmetric( + horizontal: 4.w, + vertical: 2.h, + ), + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + onTap: () async { + FocusScope.of(context).unfocus(); + + await Future.delayed( + const Duration(milliseconds: 400), + ); + + final isMainOutlet = await ref + .read(outletRepositoryProvider) + .fetchIsMainOutlet(); + + if (_selectedUnit == null) { + CustomSnackbar.showError( + context, + "Satuan belum dipilih", + ); + } else { + final unitRepository = ref.read( + unitRepositoryProvider, + ); + + final unit = await unitRepository.fetchUnitById( + _selectedUnit!, + ); + + if (unit != null) { + context.pushNamed( + AppRoute.productUsageRawMaterialScreen, + extra: { + "unit-name": unit.name, + "is_main_outlet": isMainOutlet, + "is_editing": true + }, + ); + } else { + CustomSnackbar.showError( + context, + "Satuan produk tidak tersedia", + ); + } + } + }, + ), + + SizedBox(height: 3.h), + + SizedBox( + width: double.infinity, + height: 6.5.h, + child: ElevatedButton( + onPressed: rawMaterialControllerState.isLoading + ? null + : _saveRawMaterial, + style: ElevatedButton.styleFrom( + backgroundColor: AppColor.primaryColor, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(2.5.w), + ), + disabledBackgroundColor: Colors.grey.shade300, + elevation: 4, + ), + child: Text( + "Simpan", + style: TextStyle( + color: Colors.white, + fontSize: AppFontSize.medium.sp, + fontWeight: FontWeight.bold, + ), + ), + ), + ), + ], + ), + ), + ), + ), + ), + ); + }, + ); + } +} diff --git a/Mobile Operasional/lib/features/goods/raw_materials/presentation/screens/delete_raw_material_screen.dart b/Mobile Operasional/lib/features/goods/raw_materials/presentation/screens/delete_raw_material_screen.dart new file mode 100644 index 0000000..6062614 --- /dev/null +++ b/Mobile Operasional/lib/features/goods/raw_materials/presentation/screens/delete_raw_material_screen.dart @@ -0,0 +1,300 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; +import 'package:niogu_app/core/constants/app_font_size.dart'; +import 'package:niogu_app/core/utils/log_message.dart'; +import 'package:niogu_app/core/system/system_setting.dart'; +import 'package:niogu_app/core/widgets/custom_snackbar.dart'; +import 'package:niogu_app/features/goods/raw_materials/domain/entities/raw_material.dart'; +import 'package:niogu_app/features/goods/raw_materials/presentation/providers/raw_material_provider.dart'; +import 'package:sizer/sizer.dart'; + +class DeleteRawMaterialScreen extends ConsumerStatefulWidget { + final DeleteRawMaterialSnapshot rawMaterialSnapshot; + const DeleteRawMaterialScreen({super.key, required this.rawMaterialSnapshot}); + + @override + ConsumerState createState() => + _DeleteRawMaterialScreenState(); +} + +class _DeleteRawMaterialScreenState + extends ConsumerState { + @override + void initState() { + // TODO: implement initState + super.initState(); + } + + @override + void dispose() { + // TODO: implement dispose + super.dispose(); + } + + Future _onDeleteCurrentRawMaterial() async { + final outletId = await SystemSetting.getCurrentOutletId(); + + try { + await ref + .read(rawMaterialControllerProvider.notifier) + .deleteRawMaterial( + widget.rawMaterialSnapshot.rawMaterialId, + outletId: outletId!, + canDeleteAll: false, + ); + + if (!mounted) return; + + CustomSnackbar.showSuccess(context, 'Bahan baku berhasil dihapus'); + context.pop(); + } catch (e, st) { + LogMessage.log.e(e.toString(), error: e, stackTrace: st); + CustomSnackbar.showError(context, "Ups, terjadi kesalahan"); + context.pop(); + } + } + + Future _onDeleteAllRawMaterial() async { + try { + await ref + .read(rawMaterialControllerProvider.notifier) + .deleteRawMaterial( + widget.rawMaterialSnapshot.rawMaterialId, + canDeleteAll: true, + ); + + if (!mounted) return; + + CustomSnackbar.showSuccess(context, 'Bahan baku berhasil dihapus'); + + context.pop(); + } catch (e, st) { + LogMessage.log.e(e.toString(), error: e, stackTrace: st); + CustomSnackbar.showError(context, "Ups, terjadi kesalahan"); + context.pop(); + } + } + + @override + Widget build(BuildContext context) { + final bool isTablet = 100.w >= 600; + + final double maxWidth = 100.w >= 1280 + ? 1200 + : isTablet + ? 800 + : 400; + + final bool isMainOutlet = widget.rawMaterialSnapshot.isMainOutlet; + + final int totalOutletCount = widget.rawMaterialSnapshot.totalOutletCount; + + final bool isMultiLocation = isMainOutlet && totalOutletCount > 1; + + final rawMaterialControllerState = ref.watch(rawMaterialControllerProvider); + + return Dialog( + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), + elevation: 0, + backgroundColor: Colors.transparent, + child: Container( + padding: EdgeInsets.all(5.w), + width: double.infinity, + constraints: BoxConstraints(maxWidth: maxWidth), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(4.w), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + padding: EdgeInsets.all(3.w), + decoration: BoxDecoration( + color: Colors.red[50], + shape: BoxShape.circle, + ), + child: Icon(Icons.delete_outline, color: Colors.red, size: 8.w), + ), + SizedBox(height: 2.h), + Text( + "Hapus Bahan Baku?", + style: TextStyle( + fontSize: AppFontSize.medium.sp, + fontWeight: FontWeight.bold, + color: Colors.black87, + ), + ), + SizedBox(height: 1.h), + Text( + "Tindakan ini tidak dapat dibatalkan.", + style: TextStyle( + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + color: Colors.grey[800], + ), + ), + SizedBox(height: 3.h), + + Container( + padding: EdgeInsets.symmetric(vertical: 3.w), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(2.5.w), + ), + child: Row( + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + widget.rawMaterialSnapshot.name, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + fontWeight: FontWeight.bold, + color: Colors.black87, + ), + ), + SizedBox(height: 0.5.h), + // Badge Terjual + Container( + padding: EdgeInsets.symmetric( + horizontal: 2.w, + vertical: 0.5.h, + ), + decoration: BoxDecoration( + color: Colors.green[50], + borderRadius: BorderRadius.circular(1.w), + ), + child: Text( + "${widget.rawMaterialSnapshot.totalProductUsage} Produk menggunakan bahan baku ini", + style: TextStyle( + fontSize: isTablet + ? (AppFontSize.medium - 2).sp + : (AppFontSize.small - 2).sp, + color: Colors.green[700], + fontWeight: FontWeight.w600, + ), + ), + ), + SizedBox(height: 0.5.h), + + Row( + children: [ + Icon(Icons.store, size: 4.w, color: Colors.grey), + SizedBox(width: 1.w), + Expanded( + child: Text( + isMultiLocation + ? "${widget.rawMaterialSnapshot.currentOutletName} (+${widget.rawMaterialSnapshot.totalOutletCount - 1} lainnya)" + : widget + .rawMaterialSnapshot + .currentOutletName, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontSize: isTablet + ? (AppFontSize.medium - 1).sp + : (AppFontSize.small - 1).sp, + color: Colors.grey[600], + ), + ), + ), + ], + ), + ], + ), + ), + ], + ), + ), + + SizedBox(height: 3.h), + + if (isMultiLocation) ...[ + _buildButton( + context, + label: "Hapus di Outlet Ini Saja", + color: const Color.fromARGB(255, 156, 10, 0), + onPressed: rawMaterialControllerState.isLoading + ? null + : _onDeleteCurrentRawMaterial, + ), + SizedBox(height: 1.5.h), + _buildButton( + context, + label: "Hapus di SEMUA Outlet", + color: Colors.red, + onPressed: rawMaterialControllerState.isLoading + ? null + : _onDeleteAllRawMaterial, + ), + ] else ...[ + _buildButton( + context, + label: "Hapus Bahan Baku", + color: Colors.red, + onPressed: rawMaterialControllerState.isLoading + ? null + : isMainOutlet && totalOutletCount == 1 + ? _onDeleteAllRawMaterial + : _onDeleteCurrentRawMaterial, + ), + ], + + SizedBox(height: 1.5.h), + + TextButton( + onPressed: () => context.pop(), + child: Text( + "Batal", + style: TextStyle( + color: Colors.grey[600], + fontSize: AppFontSize.medium.sp, + fontWeight: FontWeight.w600, + ), + ), + ), + ], + ), + ), + ); + } + + Widget _buildButton( + BuildContext context, { + required String label, + required Color color, + required VoidCallback? onPressed, + }) { + return SizedBox( + width: double.infinity, + height: 6.h, + child: ElevatedButton( + onPressed: onPressed, + style: ElevatedButton.styleFrom( + backgroundColor: color, + elevation: 0, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(2.5.w), + ), + disabledBackgroundColor: Colors.grey.shade300, + ), + child: Text( + label, + style: TextStyle( + color: Colors.white, + fontSize: AppFontSize.medium.sp, + fontWeight: FontWeight.bold, + ), + ), + ), + ); + } +} diff --git a/Mobile Operasional/lib/features/goods/raw_materials/presentation/screens/edit_raw_material_screen.dart b/Mobile Operasional/lib/features/goods/raw_materials/presentation/screens/edit_raw_material_screen.dart new file mode 100644 index 0000000..ffe1a33 --- /dev/null +++ b/Mobile Operasional/lib/features/goods/raw_materials/presentation/screens/edit_raw_material_screen.dart @@ -0,0 +1,587 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; +import 'package:niogu_app/core/components/top_back_bar_app.dart'; +import 'package:niogu_app/core/constants/app_color.dart'; +import 'package:niogu_app/core/constants/app_font_size.dart'; +import 'package:niogu_app/core/providers/app_provider.dart'; +import 'package:niogu_app/core/router/app_route.dart'; +import 'package:niogu_app/core/utils/extension_format.dart'; +import 'package:niogu_app/core/enums/item_type.dart'; +import 'package:niogu_app/core/utils/log_message.dart'; +import 'package:niogu_app/core/enums/stock_card_type.dart'; +import 'package:niogu_app/core/enums/stock_type.dart'; +import 'package:niogu_app/core/system/system_setting.dart'; +import 'package:niogu_app/core/enums/user_role.dart'; +import 'package:niogu_app/core/widgets/custom_selector.dart'; +import 'package:niogu_app/core/widgets/custom_snackbar.dart'; +import 'package:niogu_app/core/widgets/custom_text_form_field.dart'; +import 'package:niogu_app/core/widgets/pop_up_notification.dart'; +import 'package:niogu_app/features/goods/domain/entities/stock_card.dart'; +import 'package:niogu_app/features/goods/presentation/widgets/dropdown_with_action.dart'; +import 'package:niogu_app/features/goods/raw_materials/domain/entities/raw_material.dart'; +import 'package:niogu_app/features/goods/raw_materials/presentation/providers/raw_material_provider.dart'; +import 'package:niogu_app/features/goods/raw_materials/presentation/widgets/edit_raw_material_shimmer.dart'; +import 'package:niogu_app/features/goods/units/domain/entities/unit.dart'; +import 'package:niogu_app/features/goods/units/presentation/providers/unit_provider.dart'; +import 'package:niogu_app/features/goods/units/presentation/screens/add_unit_screen.dart'; +import 'package:niogu_app/features/goods/units/presentation/screens/edit_unit_screen.dart'; +import 'package:niogu_app/features/goods/domain/entities/outlet_inventory.dart'; +import 'package:niogu_app/features/outlets/presentation/providers/outlet_provider.dart'; +import 'package:sizer/sizer.dart'; + +class EditRawMaterialScreen extends ConsumerStatefulWidget { + final String rawMaterialId; + final bool isMainOutlet; + final bool isEditing; + const EditRawMaterialScreen({ + super.key, + required this.rawMaterialId, + this.isMainOutlet = false, + this.isEditing = true, + }); + + @override + ConsumerState createState() => + _EditRawMaterialScreenState(); +} + +class _EditRawMaterialScreenState extends ConsumerState { + late final String _rawMaterialId; + + late final bool _isMainOutlet; + + late final bool _isEditing; + + final GlobalKey _formKey = GlobalKey(); + + final TextEditingController _nameController = TextEditingController(); + + final TextEditingController _purchasePriceController = + TextEditingController(); + + final TextEditingController _stockController = TextEditingController(); + + String? _selectedUnit; + + String? _outletInventoryId; + + bool _isLoading = true; + + @override + void initState() { + // TODO: implement initState + super.initState(); + + _rawMaterialId = widget.rawMaterialId; + + _isMainOutlet = widget.isMainOutlet; + + _isEditing = widget.isEditing; + + WidgetsBinding.instance.addPostFrameCallback((_) { + Future.microtask(() { + ref.invalidate(receiptItemIdProvider); + + ref.invalidate(deleteProductUsageMaterialProvider); + }); + + Future.delayed(const Duration(milliseconds: 800), () { + _fetchRawMaterial(); + }); + }); + } + + @override + void dispose() { + // TODO: implement dispose + _nameController.dispose(); + _purchasePriceController.dispose(); + _stockController.dispose(); + super.dispose(); + } + + Future _fetchRawMaterial() async { + try { + final rawMaterialRepository = ref.read(rawMaterialRepositoryProvider); + + final rawMaterial = await rawMaterialRepository.fetchRawMaterialById( + _rawMaterialId, + _isEditing, + ); + + if (!mounted) return; + + _nameController.text = rawMaterial.name; + + _purchasePriceController.text = rawMaterial.purchasePrice + .toStringWithoutTrailingZero(); + + _stockController.text = rawMaterial.stock.toStringWithoutTrailingZero(); + + _selectedUnit = rawMaterial.unitId; + + _outletInventoryId = rawMaterial.outletInventoryId; + + ref + .read(selectedProductUsageMaterialProvider.notifier) + .hydrate(rawMaterial.productUsages); + + final Map receiptItemIds = { + for (final data in rawMaterial.productUsages) + data.productUsageId: data.receiptItemId, + }; + + ref.read(receiptItemIdProvider.notifier).state = receiptItemIds; + + setState(() { + _isLoading = false; + }); + } catch (e, st) { + if (!mounted) return; + + setState(() { + _isLoading = false; + }); + + LogMessage.log.e(e.toString(), error: e, stackTrace: st); + CustomSnackbar.showError(context, "Ups, terjadi kesalahan"); + context.pop(); + } + } + + String? _validator({ + String? value, + required String message1, + String? message2, + bool isNumber = false, + bool isPurchasePrice = false, + bool isStock = false, + String? errorMessage, + }) { + if (value == null || value.isEmpty) { + return message1; + } + + try { + if (isNumber && double.parse(value) <= 0) { + if (isPurchasePrice) return message2; + + if (isStock) return message2; + } + } catch (e) { + return errorMessage; + } + + return null; + } + + Future _saveRawMaterial() async { + if (!_formKey.currentState!.validate()) { + return; + } + + if (_selectedUnit == null) { + CustomSnackbar.showError(context, "Satuan bahan baku belum dipilih"); + return; + } + + final String name = _nameController.text.trim(); + final String unitId = _selectedUnit!; + final rawMaterial = UpsertRawMaterial( + localId: _rawMaterialId, + unitId: unitId, + name: name, + ); + + final selectProductUsageMaterials = ref + .read(selectedProductUsageMaterialProvider) + .values + .toList(); + + final List receiptItems = selectProductUsageMaterials + .map((selectProductUsage) { + return UpsertReceiptItem( + localId: selectProductUsage.receiptItemId, + productUsageId: selectProductUsage.productUsageId, + rawMaterialId: rawMaterial.localId, + quantity: selectProductUsage.quantity, + ); + }) + .toList(); + + final userId = await SystemSetting.getCurrentUserId(); + + final outletId = await SystemSetting.getCurrentOutletId(); + + try { + final double stock = double.parse(_stockController.text.trim()); + + final double purchasePrice = double.parse( + _purchasePriceController.text.trim(), + ); + + final List outletInventories = [ + UpsertOutletInventory( + localId: !_isEditing ? null : _outletInventoryId, + outletId: outletId!, + itemType: ItemType.raw_material, + itemId: rawMaterial.localId, + stockType: StockType.fixed, + stock: stock, + costPrice: 0.0, + sellingPrice: 0.0, + purchasePrice: purchasePrice, + ), + ]; + + final deleteItems = ref.read(deleteProductUsageMaterialProvider); + + final List stockCards = [ + StockCard( + outletInventoryId: outletInventories.single.localId, + userId: userId!, + stockCardType: StockCardType.initial_stock, + qtyChange: stock, + currentStock: stock, + costPrice: purchasePrice, + stockType: StockType.fixed, + ), + ]; + + final selectProductUsageMaterial = ref.read( + selectedProductUsageMaterialProvider, + ); + + if (selectProductUsageMaterial.values.isEmpty) { + CustomSnackbar.showError(context, "Penggunaan bahan baku belum diatur"); + return; + } + + try { + await ref + .read(rawMaterialControllerProvider.notifier) + .saveRawMaterial( + rawMaterial: rawMaterial, + receiptItems: receiptItems, + deleteItems: deleteItems, + outletInventories: outletInventories, + stockCards: _isEditing ? [] : stockCards, + ); + + if (!mounted) return; + + ref.invalidate(receiptItemIdProvider); + + ref.invalidate(deleteProductUsageMaterialProvider); + + CustomSnackbar.showSuccess( + context, + _isEditing ? "Bahan baku diperbarui" : "Bahan baku berhasil disimpan", + ); + + context.pop(); + } catch (e, st) { + LogMessage.log.e(e.toString(), error: e, stackTrace: st); + + CustomSnackbar.showError(context, "Ups, terjadi kesalahan"); + } + } catch (e) { + CustomSnackbar.showError(context, "Format angka tidak valid"); + } + } + + @override + Widget build(BuildContext context) { + return LayoutBuilder( + builder: (context, constraints) { + final bool isTablet = 100.w >= 600; + + final messageActivity = ref.watch(messageActivityProvider); + + final currentUserRole = ref.watch(currentUserRoleProvider); + + final List units = []; + + final unitState = ref.watch(unitStreamProvider); + + unitState.whenData((u) => units.addAll(u)); + + final selectProductUsageMaterialState = ref.watch( + selectedProductUsageMaterialProvider, + ); + + final rawMaterialControllerState = ref.watch( + rawMaterialControllerProvider, + ); + + return PopScope( + canPop: false, + onPopInvokedWithResult: (didPop, result) async { + if (didPop) return; + + ref.invalidate(receiptItemIdProvider); + + ref.invalidate(deleteProductUsageMaterialProvider); + + context.pop(); + }, + child: SafeArea( + top: false, + bottom: true, + right: false, + left: false, + child: Stack( + children: [ + Scaffold( + backgroundColor: const Color(0xFFF9FAFB), + appBar: TopBackBarApp( + title: "Edit Bahan Baku", + onTap: () { + ref.invalidate(receiptItemIdProvider); + + ref.invalidate(deleteProductUsageMaterialProvider); + + context.pop(); + }, + ), + + body: _isLoading + ? const EditRawMaterialShimmer() + : SingleChildScrollView( + padding: EdgeInsets.symmetric( + horizontal: 5.w, + vertical: 3.h, + ), + child: Form( + key: _formKey, + child: Column( + children: [ + CustomTextFormField( + label: "Nama Bahan Baku", + hint: "Contoh: Kopi Susu Gula Aren", + controller: _nameController, + validator: (value) => _validator( + value: value, + message1: "Nama produk belum diisi", + ), + readOnly: + (currentUserRole == UserRole.owner && + !_isMainOutlet) || + currentUserRole == UserRole.admin, + ), + + SizedBox(height: 3.h), + + CustomTextFormField( + label: "Harga Beli", + hint: "0", + keyboardType: TextInputType.number, + controller: _purchasePriceController, + validator: (value) => _validator( + value: value, + message1: "Harga beli belum diisi", + message2: "Harga beli harus lebih dari 0", + isNumber: true, + isPurchasePrice: true, + errorMessage: "Harga beli tidak valid ", + ), + ), + + SizedBox(height: 3.h), + + CustomTextFormField( + label: _isEditing ? "Sisa Stok" : "Stok Awal", + hint: "100", + keyboardType: TextInputType.number, + controller: _stockController, + validator: (value) => _validator( + value: value, + message1: "Stok belum diisi", + message2: "Stok harus lebih dari 0", + isNumber: true, + isStock: true, + errorMessage: "Stok tidak valid", + ), + readOnly: _isEditing, + onTapReadOnlyMessage: + "Tambah stok di menu stok masuk", + ), + + SizedBox(height: 3.h), + + if (currentUserRole == UserRole.owner && + _isMainOutlet) + DropdownWithAction( + label: "Satuan", + value: _selectedUnit, + items: units, + onChanged: (val) => + setState(() => _selectedUnit = val), + onAdd: () async { + FocusScope.of(context).unfocus(); + + await Future.delayed( + const Duration(milliseconds: 400), + ); + + showDialog( + context: context, + builder: (_) => const AddUnitScreen(), + ); + }, + onEdit: () async { + if (_selectedUnit == null) { + CustomSnackbar.showError( + context, + "Satuan belum dipilih", + ); + } else { + final unitRepository = ref.read( + unitRepositoryProvider, + ); + + final unit = await unitRepository + .fetchUnitById(_selectedUnit!); + + if (unit != null) { + showDialog( + context: context, + builder: (_) { + return EditUnitScreen(unit: unit); + }, + ); + } else { + CustomSnackbar.showError( + context, + "Satuan produk tidak tersedia", + ); + } + } + }, + ) + else + CustomTextFormField( + label: "Satuan", + controller: TextEditingController( + text: units + .firstWhere( + (unit) => unit.id == _selectedUnit!, + ) + .name, + ), + readOnly: true, + ), + SizedBox(height: 3.h), + + CustomSelector( + isTablet: isTablet, + title: + selectProductUsageMaterialState + .values + .isEmpty + ? "Atur penggunaan bahan baku" + : "Lihat ${selectProductUsageMaterialState.values.length} penggunaan bahan baku", + padding: EdgeInsets.symmetric( + horizontal: 4.w, + vertical: 2.h, + ), + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + onTap: () async { + FocusScope.of(context).unfocus(); + + await Future.delayed( + const Duration(milliseconds: 400), + ); + + final isMainOutlet = await ref + .read(outletRepositoryProvider) + .fetchIsMainOutlet(); + + if (_selectedUnit == null) { + CustomSnackbar.showError( + context, + "Satuan belum dipilih", + ); + } else { + final unitRepository = ref.read( + unitRepositoryProvider, + ); + + final unit = await unitRepository + .fetchUnitById(_selectedUnit!); + + if (unit != null) { + context.pushNamed( + AppRoute + .productUsageRawMaterialScreen, + extra: { + "raw-material-id": _rawMaterialId, + "unit-name": unit.name, + "is_main_outlet": isMainOutlet, + "is_editing": _isEditing, + }, + ); + } else { + CustomSnackbar.showError( + context, + "Satuan produk tidak tersedia", + ); + } + } + }, + ), + + SizedBox(height: 3.h), + + SizedBox( + width: double.infinity, + height: 6.5.h, + child: ElevatedButton( + onPressed: + rawMaterialControllerState.isLoading + ? null + : _saveRawMaterial, + style: ElevatedButton.styleFrom( + backgroundColor: AppColor.primaryColor, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + 2.5.w, + ), + ), + disabledBackgroundColor: + Colors.grey.shade300, + elevation: 4, + ), + child: Text( + "Simpan", + style: TextStyle( + color: Colors.white, + fontSize: AppFontSize.medium.sp, + fontWeight: FontWeight.bold, + ), + ), + ), + ), + ], + ), + ), + ), + ), + + if (messageActivity != null) + PopupNotification( + isOwner: currentUserRole == UserRole.owner, + messages: messageActivity.messages, + type: messageActivity.type, + ), + ], + ), + ), + ); + }, + ); + } +} diff --git a/Mobile Operasional/lib/features/goods/raw_materials/presentation/screens/product_usage_raw_material_screen.dart b/Mobile Operasional/lib/features/goods/raw_materials/presentation/screens/product_usage_raw_material_screen.dart new file mode 100644 index 0000000..6e730d5 --- /dev/null +++ b/Mobile Operasional/lib/features/goods/raw_materials/presentation/screens/product_usage_raw_material_screen.dart @@ -0,0 +1,853 @@ +import 'dart:async'; +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; +import 'package:niogu_app/core/components/top_back_bar_app.dart'; +import 'package:niogu_app/core/constants/app_color.dart'; +import 'package:niogu_app/core/constants/app_font_size.dart'; +import 'package:niogu_app/core/widgets/custom_error_screen.dart'; +import 'package:niogu_app/core/providers/app_provider.dart'; +import 'package:niogu_app/core/router/app_route.dart'; +import 'package:niogu_app/core/utils/extension_format.dart'; +import 'package:niogu_app/core/enums/user_role.dart'; +import 'package:niogu_app/core/widgets/custom_empty_screen.dart'; +import 'package:niogu_app/core/widgets/custom_snackbar.dart'; +import 'package:niogu_app/core/widgets/pop_up_notification.dart'; +import 'package:niogu_app/features/goods/raw_materials/domain/entities/raw_material.dart'; +import 'package:niogu_app/features/goods/raw_materials/presentation/providers/raw_material_provider.dart'; +import 'package:niogu_app/features/goods/raw_materials/presentation/widgets/product_usage_raw_material_shimmer.dart'; +import 'package:niogu_app/features/goods/raw_materials/presentation/widgets/checkbox_product_card.dart'; +import 'package:niogu_app/features/goods/raw_materials/presentation/widgets/quantity_usage.dart'; +import 'package:sizer/sizer.dart'; + +class ProductUsageRawMaterialScreen extends ConsumerStatefulWidget { + final String? rawMaterialId; + final String unitName; + final bool isMainOutlet; + final bool isEditing; + const ProductUsageRawMaterialScreen({ + super.key, + this.rawMaterialId, + required this.unitName, + this.isMainOutlet = false, + this.isEditing = true, + }); + + @override + ConsumerState createState() => + _ProductUsageRawMaterialScreenState(); +} + +class _ProductUsageRawMaterialScreenState + extends ConsumerState { + late final String? _rawMaterialId; + + late final String _unitName; + + late final bool _isMainOutlet; + + late final bool _isEditing; + + final FocusNode _searchFocusNode = FocusNode(); + + final GlobalKey _formKey = GlobalKey(); + + final Map _controllers = {}; + + Color _searchIconColor = Colors.grey; + + Timer? _debounce; + + Map _initialProductUsages = {}; + + final List _deleteProductUsageMaterials = []; + + @override + void initState() { + // TODO: implement initState + super.initState(); + + _rawMaterialId = widget.rawMaterialId; + + _unitName = widget.unitName; + + _isMainOutlet = widget.isMainOutlet; + + _isEditing = widget.isEditing; + + _searchFocusNode.addListener(() { + setState(() { + _searchIconColor = _searchFocusNode.hasFocus + ? Colors.black + : Colors.grey; + }); + }); + + WidgetsBinding.instance.addPostFrameCallback((_) { + _initialProductUsages = ref.read(selectedProductUsageMaterialProvider); + }); + } + + @override + void dispose() { + // TODO: implement dispose + _searchFocusNode.dispose(); + + for (final controller in _controllers.values) { + controller.dispose(); + } + + _debounce?.cancel(); + super.dispose(); + } + + TextEditingController _controllerOf(String productUsageId) { + return _controllers.putIfAbsent(productUsageId, () { + final quantity = ref + .read(selectedProductUsageMaterialProvider.notifier) + .quantityOf(productUsageId); + + return TextEditingController( + text: quantity > 0 ? quantity.toStringWithoutTrailingZero() : '', + ); + }); + } + + void _onSearchChanged(String value) { + if (_debounce?.isActive ?? false) _debounce?.cancel(); + _debounce = Timer(const Duration(milliseconds: 800), () async { + ref.read(productUsageMaterialSearchProvider.notifier).state = value; + }); + } + + void _deleteProductUsageMaterial(String productUsageId) { + final bool isSelected = ref + .read(selectedProductUsageMaterialProvider) + .containsKey(productUsageId); + + if (isSelected && _rawMaterialId != null) { + _deleteProductUsageMaterials.removeWhere((product) { + return product.productUsageId == productUsageId; + }); + } else if (!isSelected && _rawMaterialId != null) { + _deleteProductUsageMaterials.add( + DeleteProductUsageMaterial( + rawMaterialId: _rawMaterialId, + productUsageId: productUsageId, + ), + ); + } + } + + void _commitAllQuantity() { + _controllers.forEach((key, value) { + final String val = _controllers[key]?.text ?? ''; + final double quantity = double.tryParse(val) ?? 0.0; + ref + .read(selectedProductUsageMaterialProvider.notifier) + .setQuantity(key, quantity); + }); + } + + String? _validator({ + required String? value, + required String message, + required String errorMessage, + }) { + try { + if (value != null && value.isNotEmpty) { + final double val = double.parse(value); + + if (val <= 0) { + return message; + } + } + } catch (e) { + return errorMessage; + } + + return null; + } + + void _saveProductUsage() { + final selectProductUsageMaterial = ref.read( + selectedProductUsageMaterialProvider, + ); + + if (selectProductUsageMaterial.values.isEmpty) { + CustomSnackbar.showWarning( + context, + "Pilih minimal satu produk yang menggunakan bahan baku ini", + ); + return; + } + + if (!_formKey.currentState!.validate()) return; + + if (_deleteProductUsageMaterials.isNotEmpty) { + ref.read(deleteProductUsageMaterialProvider.notifier).state = + _deleteProductUsageMaterials; + } + + _commitAllQuantity(); + + CustomSnackbar.showSuccess( + context, + "${selectProductUsageMaterial.values.length} " + "penggunaan bahan baku berhasil dibuat", + ); + + context.pop(); + } + + @override + Widget build(BuildContext context) { + return LayoutBuilder( + builder: (context, constraints) { + final bool isTablet = 100.w >= 600; + + const backgroundColor = Color(0xFFF9FAFB); + + final messageActivity = ref.watch(messageActivityProvider); + + final currentUserRole = ref.watch(currentUserRoleProvider); + + final productUsageMaterialState = ref.watch( + filteredProductUsageMaterialProvider(_isEditing), + ); + + final productUsageMaterialEmptyState = ref.watch( + productUsageMaterialEmptyProvider(_isEditing), + ); + + final selectedProduct = ref.watch(selectedProductUsageMaterialProvider); + + return SafeArea( + top: false, + bottom: true, + right: false, + left: false, + child: Stack( + children: [ + Scaffold( + backgroundColor: backgroundColor, + appBar: TopBackBarApp( + title: "Penggunaan Bahan Baku", + onTap: () { + context.pop(); + + WidgetsBinding.instance.addPostFrameCallback((_) { + ref + .read(selectedProductUsageMaterialProvider.notifier) + .restore(_initialProductUsages); + }); + }, + ), + body: Column( + children: [ + Container( + height: 12.h, + padding: EdgeInsets.fromLTRB(5.w, 3.h, 5.w, 2.h), + color: backgroundColor, + child: Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(2.5.w), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.03), + blurRadius: 10, + offset: const Offset(0, 4), + ), + ], + ), + child: TextField( + focusNode: _searchFocusNode, + onChanged: (value) => _onSearchChanged(value), + style: TextStyle( + color: Colors.black87, + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + ), + decoration: InputDecoration( + hintText: "Cari produk...", + hintStyle: TextStyle( + color: _searchIconColor, + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + ), + prefixIcon: Icon( + Icons.search, + color: Colors.grey, + size: 5.w, + ), + border: InputBorder.none, + contentPadding: EdgeInsets.symmetric( + horizontal: 4.w, + vertical: 1.5.h, + ), + ), + ), + ), + ), + Expanded( + child: Form( + key: _formKey, + child: productUsageMaterialState.when( + data: (productUsageMaterials) { + switch (productUsageMaterialEmptyState) { + case ProductUsageRawMaterialEmpty.loading: + return const SizedBox(); + case ProductUsageRawMaterialEmpty.empty_database: + return CustomEmptyScreen( + title: "Tidak Ada Produk", + body: "Kamu belum memiliki produk", + textButton: "Buat Produk Pertamamu", + onPressed: () => context.pushNamed( + AppRoute.addProductScreen, + ), + ); + case ProductUsageRawMaterialEmpty + .empty_search_result: + return SingleChildScrollView( + physics: + const AlwaysScrollableScrollPhysics(), + padding: EdgeInsets.symmetric(vertical: 3.h), + child: Center( + child: Column( + mainAxisAlignment: + MainAxisAlignment.center, + children: [ + Text( + "Produk Tidak Ditemukan", + style: TextStyle( + fontSize: AppFontSize.large.sp, + fontWeight: FontWeight.bold, + color: Colors.black, + ), + ), + + SizedBox(height: 1.5.h), + + Icon( + Icons.search_off_rounded, + size: 15.h, + color: Colors.grey.shade300, + ), + ], + ), + ), + ); + + case ProductUsageRawMaterialEmpty.has_data: + return ListView.builder( + padding: EdgeInsets.fromLTRB( + 5.w, + 0, + 5.w, + 12.h, + ), + itemCount: productUsageMaterials.length, + itemBuilder: (context, index) { + var productUsageMaterial = + productUsageMaterials[index]; + + final activeProduct = productUsageMaterial + .variants + .where((variant) { + return !variant.hasBeenDelete; + }) + .toList(); + + final historyProduct = productUsageMaterial + .variants + .where((variant) { + return variant.hasBeenDelete; + }) + .toList(); + + final bool isTablet = 100.w >= 600; + + final bool imagePathAvailable = + productUsageMaterial.imagePath != + null && + productUsageMaterial + .imagePath! + .isNotEmpty; + + final File imageFile = File( + imagePathAvailable + ? productUsageMaterial.imagePath! + : 'image not found', + ); + + final bool imageFileExists = imageFile + .existsSync(); + + final isExpanded = ref.watch( + expandedProductProvider( + productUsageMaterial.id, + ), + ); + + return AnimatedContainer( + duration: const Duration( + milliseconds: 200, + ), + margin: EdgeInsets.only(bottom: 2.h), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular( + 4.w, + ), + border: Border.all( + color: Colors.grey.shade200, + width: 1, + ), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity( + 0.03, + ), + blurRadius: 8, + offset: const Offset(0, 4), + ), + ], + ), + child: Column( + children: _buildItems( + isTablet: isTablet, + userRole: currentUserRole!, + selectedProduct: selectedProduct, + productUsageMaterial: + productUsageMaterial, + activeProduct: activeProduct, + historyProduct: historyProduct, + imageFileExists: imageFileExists, + imageFile: imageFile, + isExpanded: isExpanded, + ), + ), + ); + }, + ); + } + }, + error: (error, stackTrace) { + return CustomErrorScreen( + message: "Ups, terjadi kesalahan", + onRefresh: () {}, + ); + }, + loading: () => const ProductUsageRawMaterialShimmer(), + ), + ), + ), + ], + ), + + bottomNavigationBar: !_isEditing + ? null + : productUsageMaterialState.maybeWhen( + data: (_) { + switch (productUsageMaterialEmptyState) { + case ProductUsageRawMaterialEmpty.loading: + return const SizedBox(); + case ProductUsageRawMaterialEmpty.empty_database: + return const SizedBox(); + case ProductUsageRawMaterialEmpty + .empty_search_result: + return _buildSaveButton(); + case ProductUsageRawMaterialEmpty.has_data: + return _buildSaveButton(); + } + }, + orElse: () => const SizedBox(), + ), + ), + + if (messageActivity != null) + PopupNotification( + isOwner: currentUserRole == UserRole.owner, + messages: messageActivity.messages, + type: messageActivity.type, + ), + ], + ), + ); + }, + ); + } + + List _buildItems({ + required bool isTablet, + required UserRole userRole, + required Map selectedProduct, + required ProductUsageMaterial productUsageMaterial, + required List activeProduct, + required List historyProduct, + required bool imageFileExists, + required File imageFile, + required bool isExpanded, + }) { + final readOnly = + (userRole == UserRole.owner && !_isMainOutlet) || + userRole == UserRole.admin; + return [ + InkWell( + onTap: () { + if (productUsageMaterial.hasVariant) { + ref + .read(expandedProductProvider(productUsageMaterial.id).notifier) + .state = !ref.read( + expandedProductProvider(productUsageMaterial.id), + ); + } else if (_isEditing) { + final productUsageId = activeProduct.single.id; + + final receiptItemIdState = ref.read(receiptItemIdProvider); + + final receiptItemId = receiptItemIdState[productUsageId]; + + ref + .read(selectedProductUsageMaterialProvider.notifier) + .toggle(productUsageId, receiptItemId); + + _deleteProductUsageMaterial(productUsageId); + } + }, + borderRadius: BorderRadius.vertical( + top: Radius.circular(4.w), + bottom: isExpanded || historyProduct.isNotEmpty + ? Radius.zero + : Radius.circular(4.w), + ), + child: Padding( + padding: EdgeInsets.all(3.w), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + width: 20.w, + height: 20.w, + decoration: BoxDecoration( + color: Colors.grey[100], + borderRadius: BorderRadius.circular(2.5.w), + border: Border.all(color: Colors.grey.shade300), + ), + child: imageFileExists + ? ClipRRect( + borderRadius: BorderRadius.circular(2.5.w), + child: Image.file( + imageFile, + fit: BoxFit.cover, + width: double.infinity, + height: double.infinity, + errorBuilder: (context, error, stackTrace) { + return Icon( + Icons.error, + color: Colors.grey, + size: 7.w, + ); + }, + ), + ) + : Icon( + Icons.image, + color: Colors.grey[400], + size: 7.w, + ), // Placeholder + ), + SizedBox(width: 4.w), + + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + productUsageMaterial.name, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + fontWeight: FontWeight.bold, + color: activeProduct.isEmpty + ? Colors.grey + : Colors.black87, + decoration: activeProduct.isEmpty + ? TextDecoration.lineThrough + : null, + ), + ), + + if (activeProduct.isNotEmpty) ...[ + SizedBox(height: 1.h), + + if (productUsageMaterial.hasVariant) + Text( + "${activeProduct.length} Varian", + style: TextStyle( + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + color: Colors.grey, + ), + ) + else ...[ + if (selectedProduct.containsKey( + activeProduct.single.id, + )) + QuantityUsage( + hasVariant: productUsageMaterial.hasVariant, + unitName: _unitName, + controller: _controllerOf(activeProduct.single.id), + validator: (value) => _validator( + value: value, + message: "Jumlah penggunaan harus lebih dari 0", + errorMessage: "Jumlah penggunaan tidak valid", + ), + readOnly: readOnly, + ), + ], + ], + ], + ), + ), + if (activeProduct.isNotEmpty) ...[ + if (productUsageMaterial.hasVariant) + Icon( + isExpanded + ? Icons.keyboard_arrow_up_rounded + : Icons.keyboard_arrow_down_rounded, + color: Colors.grey, + size: 7.w, + ) + else + CheckboxProductCard( + isSelected: selectedProduct.containsKey( + activeProduct.single.id, + ), + ), + ], + ], + ), + ), + ), + + if (activeProduct.isNotEmpty && + productUsageMaterial.hasVariant && + isExpanded) ...[ + Container(height: 1, color: Colors.grey[100]), + + Padding( + padding: EdgeInsets.symmetric(vertical: 1.h), + child: Column( + children: activeProduct.map((variant) { + return InkWell( + onTap: !_isEditing + ? null + : () { + final receiptItemIdState = ref.read( + receiptItemIdProvider, + ); + + final receiptItemId = receiptItemIdState[variant.id]; + ref + .read(selectedProductUsageMaterialProvider.notifier) + .toggle(variant.id, receiptItemId); + + _deleteProductUsageMaterial(variant.id); + }, + child: Padding( + padding: EdgeInsets.symmetric( + horizontal: 4.w, + vertical: 1.5.h, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + SizedBox(width: 4.w), + CheckboxProductCard( + isSelected: selectedProduct.containsKey(variant.id), + ), + + SizedBox(width: 3.w), + + Expanded( + child: Text( + variant.name, + style: TextStyle( + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + color: Colors.black, + ), + ), + ), + ], + ), + + if (selectedProduct.containsKey(variant.id)) + Padding( + padding: EdgeInsets.only(left: 14.w), + child: QuantityUsage( + hasVariant: productUsageMaterial.hasVariant, + unitName: widget.unitName, + controller: _controllerOf(variant.id), + validator: (value) => _validator( + value: value, + message: "Jumlah penggunaan harus lebih dari 0", + errorMessage: "Jumlah penggunaan tidak valid", + ), + readOnly: readOnly, + ), + ), + ], + ), + ), + ); + }).toList(), + ), + ), + ], + + if (historyProduct.isNotEmpty) ...[ + Divider(height: 1, color: Colors.grey.shade200), + + Theme( + data: Theme.of(context).copyWith(dividerColor: Colors.transparent), + child: ExpansionTile( + tilePadding: EdgeInsets.symmetric(horizontal: 4.w), + visualDensity: VisualDensity.compact, + title: Row( + children: [ + Icon(Icons.history, size: 4.5.w, color: Colors.grey[400]), + SizedBox(width: 2.w), + Text( + "Lihat riwayat / item terhapus", + style: TextStyle( + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + color: Colors.grey[500], + fontWeight: FontWeight.w500, + ), + ), + SizedBox(width: 2.w), + Container( + padding: EdgeInsets.symmetric( + horizontal: 1.5.w, + vertical: 0.5.w, + ), + decoration: BoxDecoration( + color: Colors.grey[200], + borderRadius: BorderRadius.circular(1.w), + ), + child: Text( + "${historyProduct.length}", + style: TextStyle( + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + color: Colors.grey[600], + ), + ), + ), + ], + ), + children: historyProduct.map((variant) { + return Container( + color: Colors.grey[50], + padding: EdgeInsets.symmetric(horizontal: 4.w, vertical: 1.5.h), + child: Row( + children: [ + Icon(Icons.block, color: Colors.grey[400], size: 5.w), + SizedBox(width: 3.w), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + variant.name, + style: TextStyle( + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + color: Colors.grey[500], + decoration: TextDecoration.lineThrough, // Coret + ), + ), + + Text( + variant.isProductVariant + ? "Varian Dihapus" + : "Tipe Produk Sebelumnya (Tanpa varian)", + style: TextStyle( + fontSize: isTablet + ? (AppFontSize.medium - 2).sp + : (AppFontSize.small - 2).sp, + color: Colors.red[300], + ), + ), + ], + ), + ), + ], + ), + ); + }).toList(), + ), + ), + ], + ]; + } + + Widget _buildSaveButton() { + final rawMaterialControllerState = ref.watch(rawMaterialControllerProvider); + return Container( + padding: EdgeInsets.all(5.w), + decoration: BoxDecoration( + color: Colors.white, + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.05), + blurRadius: 10, + offset: const Offset(0, -5), + ), + ], + ), + child: SizedBox( + height: 6.5.h, + child: ElevatedButton( + onPressed: rawMaterialControllerState.isLoading + ? null + : _saveProductUsage, + style: ElevatedButton.styleFrom( + backgroundColor: rawMaterialControllerState.isLoading + ? Colors.grey + : AppColor.primaryColor, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(2.5.w), + ), + elevation: 0, + ), + child: Text( + "Simpan", + style: TextStyle( + color: Colors.white, + fontSize: AppFontSize.medium.sp, + fontWeight: FontWeight.bold, + ), + ), + ), + ), + ); + } +} diff --git a/Mobile Operasional/lib/features/goods/raw_materials/presentation/screens/raw_material_choice_screen.dart b/Mobile Operasional/lib/features/goods/raw_materials/presentation/screens/raw_material_choice_screen.dart new file mode 100644 index 0000000..6a4aa19 --- /dev/null +++ b/Mobile Operasional/lib/features/goods/raw_materials/presentation/screens/raw_material_choice_screen.dart @@ -0,0 +1,143 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; +import 'package:niogu_app/core/constants/app_color.dart'; +import 'package:niogu_app/core/widgets/custom_error_screen.dart'; +import 'package:niogu_app/core/router/app_route.dart'; +import 'package:niogu_app/core/widgets/custom_empty_screen.dart'; +import 'package:niogu_app/core/widgets/custom_selectable_item.dart'; +import 'package:niogu_app/core/widgets/custom_snackbar.dart'; +import 'package:niogu_app/features/goods/raw_materials/domain/entities/raw_material.dart'; +import 'package:niogu_app/features/goods/raw_materials/presentation/providers/raw_material_provider.dart'; +import 'package:niogu_app/features/outlets/presentation/providers/outlet_provider.dart'; +import 'package:sizer/sizer.dart'; + +class RawMaterialChoiceScreen extends ConsumerStatefulWidget { + const RawMaterialChoiceScreen({super.key}); + + @override + ConsumerState createState() => + _RawMaterialChoiceScreenState(); +} + +class _RawMaterialChoiceScreenState + extends ConsumerState + with AutomaticKeepAliveClientMixin { + @override + // TODO: implement wantKeepAlive + bool get wantKeepAlive => true; + + String? _selectedItem; + + @override + Widget build(BuildContext context) { + super.build(context); + final filteredRawMaterialState = ref.watch( + filteredRawMaterialChoiceProvider, + ); + + final rawMaterialEmptyState = ref.watch(rawMaterialChoiceEmptyProvider); + + return filteredRawMaterialState.when( + data: (rawMaterials) { + switch (rawMaterialEmptyState) { + case RawMaterialChoiceEmpty.loading: + return const SizedBox(); + case RawMaterialChoiceEmpty.empty_database: + return CustomEmptyScreen( + title: "Tidak Ada Bahan Baku", + body: "Belum ada bahan baku yang tersedia", + ); + case RawMaterialChoiceEmpty.empty_search_result: + return const CustomEmptyScreen(body: "Bahan Baku Tidak Ditemukan"); + case RawMaterialChoiceEmpty.has_data: + return Stack( + children: [ + ListView.builder( + padding: EdgeInsets.all(4.w), + itemCount: rawMaterials.length, + itemBuilder: (context, index) { + final rawMaterial = rawMaterials[index]; + + return CustomSelectableItem( + id: rawMaterial.id, + title: rawMaterial.name, + subtitle: + "Jumlah Produk : ${rawMaterial.countProductUsage}", + isSelected: _selectedItem == rawMaterial.id, + onTap: () { + setState(() { + _selectedItem = rawMaterial.id; + }); + }, + ); + }, + ), + + Positioned( + bottom: 10.h, + right: 4.w, + width: 12.5.w, + height: 12.5.w, + child: FloatingActionButton( + heroTag: "btn_edit_raw_material", + onPressed: _selectedItem == null + ? null + : () async { + FocusScope.of(context).unfocus(); + + final alreadyProduct = await ref + .read(rawMaterialRepositoryProvider) + .checkAlreadyRawMaterialByOutlet( + _selectedItem!, + ); + + if (alreadyProduct) { + CustomSnackbar.showWarning( + context, + "Bahan baku ini telah tersedia di outlet ini", + ); + return; + } + + final isMainOutlet = await ref + .read(outletRepositoryProvider) + .fetchIsMainOutlet(); + + await Future.delayed( + const Duration(milliseconds: 400), + ); + + context.pushNamed( + AppRoute.editRawMaterialScreen, + pathParameters: {"id": _selectedItem!}, + extra: { + "is_main_outlet": isMainOutlet, + "is_editing": false, + }, + ); + }, + backgroundColor: _selectedItem == null + ? Colors.grey.shade300 + : AppColor.primaryColor, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(2.5.w), + ), + elevation: 0, + child: Icon( + Icons.keyboard_arrow_right, + color: Colors.white, + size: 8.w, + ), + ), + ), + ], + ); + } + }, + error: (error, stackTrace) => + CustomErrorScreen(message: "Ups, terjadi kesalahan", onRefresh: () {}), + loading: () => const SizedBox(), + ); + } +} diff --git a/Mobile Operasional/lib/features/goods/raw_materials/presentation/screens/raw_material_screen.dart b/Mobile Operasional/lib/features/goods/raw_materials/presentation/screens/raw_material_screen.dart new file mode 100644 index 0000000..981cad1 --- /dev/null +++ b/Mobile Operasional/lib/features/goods/raw_materials/presentation/screens/raw_material_screen.dart @@ -0,0 +1,302 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; +import 'package:niogu_app/core/constants/app_color.dart'; +import 'package:niogu_app/core/constants/app_font_size.dart'; +import 'package:niogu_app/core/widgets/custom_error_screen.dart'; +import 'package:niogu_app/core/providers/app_provider.dart'; +import 'package:niogu_app/core/router/app_route.dart'; +import 'package:niogu_app/core/utils/log_message.dart'; +import 'package:niogu_app/core/enums/user_role.dart'; +import 'package:niogu_app/core/widgets/custom_empty_screen.dart'; +import 'package:niogu_app/core/widgets/custom_not_login.dart'; +import 'package:niogu_app/features/goods/presentation/widgets/custom_item_detail.dart'; +import 'package:niogu_app/features/goods/raw_materials/domain/entities/raw_material.dart'; +import 'package:niogu_app/features/goods/raw_materials/presentation/providers/raw_material_provider.dart'; +import 'package:niogu_app/features/goods/raw_materials/presentation/screens/delete_raw_material_screen.dart'; +import 'package:niogu_app/features/goods/raw_materials/presentation/widgets/raw_material_list_shimmer.dart'; +import 'package:niogu_app/features/outlets/presentation/providers/outlet_provider.dart'; +import 'package:sizer/sizer.dart'; + +class RawMaterialScreen extends ConsumerStatefulWidget { + const RawMaterialScreen({super.key}); + + @override + ConsumerState createState() => _RawMaterialScreenState(); +} + +class _RawMaterialScreenState extends ConsumerState + with AutomaticKeepAliveClientMixin { + @override + // TODO: implement wantKeepAlive + bool get wantKeepAlive => true; + + @override + Widget build(BuildContext context) { + super.build(context); + + final bool isTablet = 100.w >= 600; + + final isLoggedIn = ref.watch(currentStatusLoginProvider); + + if (!isLoggedIn) { + return const CustomNotLogin(); + } + + final rawMaterialState = ref.watch(filteredRawMaterialProvider); + + final rawMaterialEmptyState = ref.watch(rawMaterialEmptyProvider); + + return rawMaterialState.when( + data: (rawMaterials) { + switch (rawMaterialEmptyState) { + case RawMaterialEmpty.loading: + return const SizedBox(); + + case RawMaterialEmpty.empty_database: + return CustomEmptyScreen( + title: "Tidak Ada Bahan Baku", + body: "Kamu belum memmiliki bahan baku", + textButton: "Tambah Bahan Baku", + onPressed: () async { + final currentUserRole = ref.read(currentUserRoleProvider); + + final isMainOutlet = await ref + .read(outletRepositoryProvider) + .fetchIsMainOutlet(); + + if (currentUserRole == UserRole.owner && isMainOutlet) { + context.pushNamed(AppRoute.addRawMaterialScreen); + } else { + context.pushNamed(AppRoute.rawMaterialChoiceScreen); + } + }, + ); + + case RawMaterialEmpty.empty_search_result: + return SingleChildScrollView( + physics: const AlwaysScrollableScrollPhysics(), + padding: EdgeInsets.symmetric(vertical: 3.h), + child: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + "Bahan Baku Tidak Ditemukan", + style: TextStyle( + fontSize: AppFontSize.large.sp, + fontWeight: FontWeight.bold, + color: Colors.black, + ), + ), + + SizedBox(height: 1.5.h), + + Icon( + Icons.search_off_rounded, + size: 15.h, + color: Colors.grey.shade300, + ), + ], + ), + ), + ); + + case RawMaterialEmpty.has_data: + return Stack( + children: [ + ListView.builder( + padding: EdgeInsets.all(4.w), + itemCount: rawMaterials.length, + itemBuilder: (context, index) { + final rawMaterial = rawMaterials[index]; + return Card( + elevation: 0, + color: Colors.white, + shape: RoundedRectangleBorder( + side: BorderSide(color: Colors.grey.shade200), + borderRadius: BorderRadius.circular(2.5.w), + ), + margin: EdgeInsets.only(bottom: 2.h), + child: Padding( + padding: EdgeInsets.symmetric( + vertical: 2.h, + horizontal: 2.w, + ), + child: Material( + color: Colors.white, + clipBehavior: Clip.hardEdge, + child: InkWell( + onTap: () async { + FocusScope.of(context).unfocus(); + + final isMainOutlet = await ref + .read(outletRepositoryProvider) + .fetchIsMainOutlet(); + + await Future.delayed( + const Duration(milliseconds: 400), + ); + + final currentUserRole = ref.read( + currentUserRoleProvider, + ); + + final isEditing = + (currentUserRole == UserRole.owner && + isMainOutlet) || + currentUserRole == UserRole.admin; + + context.pushNamed( + AppRoute.editRawMaterialScreen, + pathParameters: {"id": rawMaterial.id}, + extra: { + "is_main_outlet": isMainOutlet, + "is_editing": isEditing, + }, + ); + }, + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "${index + 1}.", + style: TextStyle( + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + fontWeight: FontWeight.bold, + ), + ), + SizedBox(width: 3.w), + + Expanded( + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text( + rawMaterial.name, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + fontWeight: FontWeight.bold, + ), + ), + SizedBox(height: 0.5.h), + + CustomItemDetail( + label: "Jumlah Produk", + value: + ": ${rawMaterial.countProductUsage}", + ), + + CustomItemDetail( + label: "Sisa Stok", + value: + ": ${rawMaterial.remainingStock}", + ), + CustomItemDetail( + label: "Harga Beli", + value: + ": ${rawMaterial.displayPurchasePrice}", + ), + ], + ), + ), + + IconButton( + onPressed: () async { + final String id = rawMaterial.id; + + final rawMaterialRepository = ref.read( + rawMaterialRepositoryProvider, + ); + + final rawMaterialSnapshot = + await rawMaterialRepository + .fetchDeleteRawMaterialSnapshotById( + id, + ); + + FocusScope.of(context).unfocus(); + + await Future.delayed( + const Duration(milliseconds: 800), + ); + + showDialog( + context: context, + builder: (context) => + DeleteRawMaterialScreen( + rawMaterialSnapshot: + rawMaterialSnapshot, + ), + ); + }, + icon: Icon( + Icons.delete, + color: Colors.red, + size: 6.w, + ), + ), + ], + ), + ), + ), + ), + ); + }, + ), + Positioned( + bottom: 10.h, + right: 4.w, + width: 12.5.w, + height: 12.5.w, + child: FloatingActionButton( + heroTag: "btn_add_raw_material", + onPressed: () async { + FocusScope.of(context).unfocus(); + + await Future.delayed(const Duration(milliseconds: 400)); + + final currentUserRole = ref.read(currentUserRoleProvider); + + final isMainOutlet = await ref + .read(outletRepositoryProvider) + .fetchIsMainOutlet(); + + if (currentUserRole == UserRole.owner && isMainOutlet) { + context.pushNamed(AppRoute.addRawMaterialScreen); + } else { + context.pushNamed(AppRoute.rawMaterialChoiceScreen); + } + }, + backgroundColor: AppColor.primaryColor, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(2.5.w), + ), + child: Icon(Icons.add, color: Colors.white, size: 8.w), + ), + ), + ], + ); + } + }, + error: (error, stackTrace) { + LogMessage.log.e( + error.toString(), + error: error, + stackTrace: stackTrace, + ); + return CustomErrorScreen( + message: "Ups, terjadi kesalahan", + onRefresh: () {}, + ); + }, + loading: () => const RawMaterialListShimmer(), + ); + } +} diff --git a/Mobile Operasional/lib/features/goods/raw_materials/presentation/widgets/checkbox_product_card.dart b/Mobile Operasional/lib/features/goods/raw_materials/presentation/widgets/checkbox_product_card.dart new file mode 100644 index 0000000..e6965fe --- /dev/null +++ b/Mobile Operasional/lib/features/goods/raw_materials/presentation/widgets/checkbox_product_card.dart @@ -0,0 +1,28 @@ +import 'package:flutter/material.dart'; +import 'package:niogu_app/core/constants/app_color.dart'; +import 'package:sizer/sizer.dart'; + +class CheckboxProductCard extends StatelessWidget { + final bool isSelected; + const CheckboxProductCard({super.key, required this.isSelected}); + + @override + Widget build(BuildContext context) { + return AnimatedContainer( + duration: const Duration(milliseconds: 200), + width: 7.w, + height: 7.w, + decoration: BoxDecoration( + color: isSelected ? AppColor.primaryColor : Colors.transparent, + shape: BoxShape.circle, + border: Border.all( + color: isSelected ? AppColor.primaryColor : Colors.grey.shade400, + width: 2, + ), + ), + child: isSelected + ? Icon(Icons.check, color: Colors.white, size: 5.w) + : null, + ); + } +} diff --git a/Mobile Operasional/lib/features/goods/raw_materials/presentation/widgets/edit_raw_material_shimmer.dart b/Mobile Operasional/lib/features/goods/raw_materials/presentation/widgets/edit_raw_material_shimmer.dart new file mode 100644 index 0000000..5e69c2c --- /dev/null +++ b/Mobile Operasional/lib/features/goods/raw_materials/presentation/widgets/edit_raw_material_shimmer.dart @@ -0,0 +1,94 @@ +import 'package:flutter/material.dart'; +import 'package:shimmer/shimmer.dart'; +import 'package:sizer/sizer.dart'; + +class EditRawMaterialShimmer extends StatelessWidget { + const EditRawMaterialShimmer({super.key}); + + @override + Widget build(BuildContext context) { + final baseColor = Colors.grey[300]!; + final highlightColor = Colors.grey[100]!; + const backgroundColor = Color(0xFFF9FAFB); + + return Scaffold( + backgroundColor: backgroundColor, + body: Shimmer.fromColors( + baseColor: baseColor, + highlightColor: highlightColor, + child: SingleChildScrollView( + padding: EdgeInsets.fromLTRB(5.w, 3.h, 5.w, 5.h), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildInputPlaceholder(labelWidth: 30.w), + + SizedBox(height: 3.h), + _buildInputPlaceholder(labelWidth: 35.w), + + SizedBox(height: 3.h), + _buildLabelPlaceholder(width: 25.w), + + SizedBox(height: 3.h), + _buildLabelPlaceholder(width: 25.w), + + SizedBox(height: 1.h), + Row( + children: [ + Expanded( + child: _buildBlock(height: 7.h, borderRadius: 2.5.w), + ), + SizedBox(width: 4.w), + Expanded( + child: _buildBlock(height: 7.h, borderRadius: 2.5.w), + ), + ], + ), + + SizedBox(height: 3.h), + _buildInputPlaceholder(labelWidth: 25.w), + + SizedBox(height: 3.h), + _buildInputPlaceholder(labelWidth: 25.w), + + SizedBox(height: 5.h), + ], + ), + ), + ), + ); + } + + Widget _buildLabelPlaceholder({required double width}) { + return Container( + width: width, + height: 1.5.h, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(4), + ), + ); + } + + Widget _buildInputPlaceholder({required double labelWidth}) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildLabelPlaceholder(width: labelWidth), + SizedBox(height: 1.h), + _buildBlock(height: 6.h, borderRadius: 2.5.w), + ], + ); + } + + Widget _buildBlock({required double height, required double borderRadius}) { + return Container( + width: double.infinity, + height: height, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(borderRadius), + ), + ); + } +} \ No newline at end of file diff --git a/Mobile Operasional/lib/features/goods/raw_materials/presentation/widgets/product_usage_raw_material_shimmer.dart b/Mobile Operasional/lib/features/goods/raw_materials/presentation/widgets/product_usage_raw_material_shimmer.dart new file mode 100644 index 0000000..74ec434 --- /dev/null +++ b/Mobile Operasional/lib/features/goods/raw_materials/presentation/widgets/product_usage_raw_material_shimmer.dart @@ -0,0 +1,115 @@ +import 'package:flutter/material.dart'; +import 'package:shimmer/shimmer.dart'; +import 'package:sizer/sizer.dart'; + +class ProductUsageRawMaterialShimmer extends StatelessWidget { + const ProductUsageRawMaterialShimmer({super.key}); + + @override + Widget build(BuildContext context) { + final baseColor = Colors.grey[300]!; + final highlightColor = Colors.grey[100]!; + const backgroundColor = Color(0xFFF9FAFB); + + return Scaffold( + backgroundColor: backgroundColor, + body: Shimmer.fromColors( + baseColor: baseColor, + highlightColor: highlightColor, + child: Column( + children: [ + Container( + margin: EdgeInsets.fromLTRB(5.w, 3.h, 5.w, 2.h), + height: 6.h, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(2.5.w), + ), + ), + + Expanded( + child: ListView.builder( + padding: EdgeInsets.fromLTRB(5.w, 0, 5.w, 5.h), + itemCount: 6, + itemBuilder: (context, index) { + return Container( + margin: EdgeInsets.only(bottom: 2.h), + padding: EdgeInsets.all(3.w), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(4.w), + ), + child: Row( + children: [ + Container( + width: 12.w, + height: 12.w, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(2.5.w), + ), + ), + + SizedBox(width: 4.w), + + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Judul Produk + Container( + width: 40.w, + height: 1.5.h, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(1.w), + ), + ), + SizedBox(height: 1.h), + Container( + width: 20.w, + height: 1.2.h, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(1.w), + ), + ), + ], + ), + ), + Container( + width: 6.w, + height: 6.w, + decoration: const BoxDecoration( + color: Colors.white, + shape: BoxShape.circle, + ), + ), + ], + ), + ); + }, + ), + ), + + Container( + padding: EdgeInsets.all(5.w), + decoration: const BoxDecoration( + color: Colors.white, + boxShadow: [BoxShadow(color: Colors.white, blurRadius: 10, offset: Offset(0, -5))] + ), + child: Container( + width: double.infinity, + height: 6.5.h, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(2.5.w), + ), + ), + ), + ], + ), + ), + ); + } +} \ No newline at end of file diff --git a/Mobile Operasional/lib/features/goods/raw_materials/presentation/widgets/quantity_usage.dart b/Mobile Operasional/lib/features/goods/raw_materials/presentation/widgets/quantity_usage.dart new file mode 100644 index 0000000..666ac77 --- /dev/null +++ b/Mobile Operasional/lib/features/goods/raw_materials/presentation/widgets/quantity_usage.dart @@ -0,0 +1,87 @@ +import 'package:flutter/material.dart'; +import 'package:niogu_app/core/constants/app_color.dart'; +import 'package:niogu_app/core/constants/app_font_size.dart'; +import 'package:sizer/sizer.dart'; + +class QuantityUsage extends StatelessWidget { + final bool hasVariant; + final String unitName; + final TextEditingController controller; + final String? Function(String?)? validator; + final bool readOnly; + const QuantityUsage({ + super.key, + required this.hasVariant, + required this.unitName, + required this.controller, + required this.validator, + this.readOnly = false, + }); + + @override + Widget build(BuildContext context) { + final bool isTablet = 100.w >= 600; + return Container( + margin: EdgeInsets.only(top: 1.h), + width: double.infinity, + child: TextFormField( + controller: controller, + validator: validator, + autovalidateMode: AutovalidateMode.onUserInteraction, + keyboardType: TextInputType.number, + style: TextStyle( + fontSize: isTablet ? AppFontSize.medium.sp : AppFontSize.small.sp, + fontWeight: FontWeight.w600, + ), + readOnly: readOnly, + decoration: InputDecoration( + hintText: "Opsional, misal 10 $unitName,(Disarankan)", + hintStyle: TextStyle( + color: Colors.grey[400], + fontSize: isTablet + ? AppFontSize.medium.sp - (hasVariant ? 0 : 2) + : AppFontSize.small.sp - (hasVariant ? 0 : 2), + fontWeight: FontWeight.normal, + ), + filled: readOnly, + fillColor: readOnly ? Colors.grey.shade100 : Colors.white, + contentPadding: EdgeInsets.symmetric( + horizontal: 3.w, + vertical: 1.2.h, + ), + errorStyle: TextStyle( + color: Colors.red, + fontSize: isTablet + ? (AppFontSize.medium - (hasVariant ? 0.5 : 2.5)).sp + : (AppFontSize.small - (hasVariant ? 0.5 : 2.5)).sp, + ), + errorBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(2.5.w), + borderSide: const BorderSide(color: Colors.redAccent), + ), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(2.5.w), + borderSide: BorderSide(color: Colors.grey.shade300), + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(2.5.w), + borderSide: BorderSide(color: Colors.grey.shade300), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(2.5.w), + borderSide: BorderSide( + color: readOnly ? Colors.grey.shade300 : AppColor.primaryColor, + ), + ), + isDense: true, + suffixText: unitName, + suffixStyle: TextStyle( + color: Colors.grey.shade600, + fontWeight: FontWeight.w600, + fontSize: isTablet ? AppFontSize.medium.sp : AppFontSize.small.sp, + ), + ), + ), + ); + } +} diff --git a/Mobile Operasional/lib/features/goods/raw_materials/presentation/widgets/raw_material_list_shimmer.dart b/Mobile Operasional/lib/features/goods/raw_materials/presentation/widgets/raw_material_list_shimmer.dart new file mode 100644 index 0000000..c48b5e1 --- /dev/null +++ b/Mobile Operasional/lib/features/goods/raw_materials/presentation/widgets/raw_material_list_shimmer.dart @@ -0,0 +1,85 @@ +import 'package:flutter/material.dart'; +import 'package:shimmer/shimmer.dart'; +import 'package:sizer/sizer.dart'; + +class RawMaterialListShimmer extends StatelessWidget { + const RawMaterialListShimmer({super.key}); + + @override + Widget build(BuildContext context) { + final baseColor = Colors.grey[300]!; + final highlightColor = Colors.grey[100]!; + + return ListView.builder( + padding: EdgeInsets.fromLTRB(4.w, 2.h, 4.w, 12.h), + itemCount: 6, + itemBuilder: (context, index) { + return Container( + margin: EdgeInsets.only(bottom: 2.h), + padding: EdgeInsets.all(3.w), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(2.5.w), + border: Border.all(color: Colors.grey.shade200), + ), + child: Shimmer.fromColors( + baseColor: baseColor, + highlightColor: highlightColor, + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + width: 40.w, + height: 2.h, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(1.w), + ), + ), + SizedBox(height: 1.5.h), + + _buildLinePlaceholder(width: 25.w), + SizedBox(height: 0.8.h), + + _buildLinePlaceholder(width: 20.w), + SizedBox(height: 0.8.h), + + _buildLinePlaceholder(width: 30.w), + ], + ), + ), + + Padding( + padding: EdgeInsets.only(top: 1.h), + child: Container( + width: 5.w, + height: 5.w, + decoration: const BoxDecoration( + color: Colors.white, + shape: BoxShape.circle, + ), + ), + ), + ], + ), + ), + ); + }, + ); + } + + Widget _buildLinePlaceholder({required double width}) { + return Container( + width: width, + height: 1.2.h, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(0.5), + ), + ); + } +} \ No newline at end of file diff --git a/Mobile Operasional/lib/features/goods/units/data/unit_repository_impl.dart b/Mobile Operasional/lib/features/goods/units/data/unit_repository_impl.dart new file mode 100644 index 0000000..d423044 --- /dev/null +++ b/Mobile Operasional/lib/features/goods/units/data/unit_repository_impl.dart @@ -0,0 +1,40 @@ +import 'package:drift/drift.dart'; +import 'package:niogu_app/core/database/app_database.dart' hide Unit; +import 'package:niogu_app/features/goods/units/domain/entities/unit.dart'; +import 'package:niogu_app/features/goods/units/domain/repositories/i_unit_repository.dart'; + +class UnitRepositoryImpl implements IUnitRepository { + final AppDatabase _appDatabase; + + UnitRepositoryImpl(this._appDatabase); + + @override + Stream> watchUnits() { + return _appDatabase.unitDao.watchAllUnits(); + } + + @override + Future> fetchUnits() async { + return await _appDatabase.unitDao.getAllUnits(); + } + + @override + Future fetchUnitById(String id) async { + return await _appDatabase.unitDao.getUnitById(id); + } + + @override + Future addUnit(Unit unit) async { + final unitCompanion = UnitsCompanion( + localId: Value(unit.id), + name: Value(unit.name), + ); + + await _appDatabase.unitDao.insertUnit(unitCompanion); + } + + @override + Future updateUnit(String id, String name) async { + await _appDatabase.unitDao.updateUnit(id, name); + } +} diff --git a/Mobile Operasional/lib/features/goods/units/domain/entities/unit.dart b/Mobile Operasional/lib/features/goods/units/domain/entities/unit.dart new file mode 100644 index 0000000..39f95a2 --- /dev/null +++ b/Mobile Operasional/lib/features/goods/units/domain/entities/unit.dart @@ -0,0 +1,9 @@ +import 'package:uuid/uuid.dart'; + +class Unit { + final String id; + final String name; + + Unit({String? id, required this.name}) + : id = id ?? const Uuid().v7().toString(); +} diff --git a/Mobile Operasional/lib/features/goods/units/domain/repositories/i_unit_repository.dart b/Mobile Operasional/lib/features/goods/units/domain/repositories/i_unit_repository.dart new file mode 100644 index 0000000..4d41166 --- /dev/null +++ b/Mobile Operasional/lib/features/goods/units/domain/repositories/i_unit_repository.dart @@ -0,0 +1,13 @@ +import 'package:niogu_app/features/goods/units/domain/entities/unit.dart'; + +abstract class IUnitRepository { + Stream> watchUnits(); + + Future> fetchUnits(); + + Future fetchUnitById(String id); + + Future addUnit(Unit unit); + + Future updateUnit(String id, String name); +} \ No newline at end of file diff --git a/Mobile Operasional/lib/features/goods/units/presentation/controller/unit_controller.dart b/Mobile Operasional/lib/features/goods/units/presentation/controller/unit_controller.dart new file mode 100644 index 0000000..9e58745 --- /dev/null +++ b/Mobile Operasional/lib/features/goods/units/presentation/controller/unit_controller.dart @@ -0,0 +1,32 @@ +import 'dart:async'; + +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:niogu_app/features/goods/units/domain/entities/unit.dart'; +import 'package:niogu_app/features/goods/units/domain/repositories/i_unit_repository.dart'; +import 'package:niogu_app/features/goods/units/presentation/providers/unit_provider.dart'; + +class UnitController extends AutoDisposeAsyncNotifier { + late IUnitRepository _unitRepository; + + @override + FutureOr build() { + _unitRepository = ref.read(unitRepositoryProvider); + } + + Future addUnit(Unit unit) async { + state = const AsyncValue.loading(); + + state = await AsyncValue.guard(() async { + await _unitRepository.addUnit(unit); + }); + } + + Future updateUnit(String id, String name) async { + state = const AsyncValue.loading(); + + state = await AsyncValue.guard(() async { + await _unitRepository.updateUnit(id, name); + }); + } + +} diff --git a/Mobile Operasional/lib/features/goods/units/presentation/providers/unit_provider.dart b/Mobile Operasional/lib/features/goods/units/presentation/providers/unit_provider.dart new file mode 100644 index 0000000..b8cef22 --- /dev/null +++ b/Mobile Operasional/lib/features/goods/units/presentation/providers/unit_provider.dart @@ -0,0 +1,37 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:niogu_app/core/providers/app_provider.dart'; +import 'package:niogu_app/features/goods/units/data/unit_repository_impl.dart'; +import 'package:niogu_app/features/goods/units/domain/entities/unit.dart'; +import 'package:niogu_app/features/goods/units/domain/repositories/i_unit_repository.dart'; +import 'package:niogu_app/features/goods/units/presentation/controller/unit_controller.dart'; + +final unitRepositoryProvider = Provider((ref) { + final appDatabase = ref.watch(appDatabaseProvider); + return UnitRepositoryImpl(appDatabase); +}); + +final unitStreamProvider = StreamProvider.autoDispose>((ref) { + final unitRepository = ref.watch(unitRepositoryProvider); + return unitRepository.watchUnits(); +}); + +final unitSearchProvider = StateProvider.autoDispose((ref) => ''); + +final validateUnitProvider = Provider.autoDispose((ref) { + final unitStreamAsync = ref.watch(unitStreamProvider); + final unitSearchAsync = ref.watch(unitSearchProvider); + + return unitStreamAsync.maybeWhen( + data: (units) { + return units.where((unit) { + return unit.name.trim().toLowerCase() == + unitSearchAsync.trim().toLowerCase(); + }).isEmpty; + }, + orElse: () => true, + ); +}); + +final unitControllerProvider = AsyncNotifierProvider.autoDispose( + UnitController.new, +); diff --git a/Mobile Operasional/lib/features/goods/units/presentation/screens/add_unit_screen.dart b/Mobile Operasional/lib/features/goods/units/presentation/screens/add_unit_screen.dart new file mode 100644 index 0000000..3a0276d --- /dev/null +++ b/Mobile Operasional/lib/features/goods/units/presentation/screens/add_unit_screen.dart @@ -0,0 +1,130 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; +import 'package:niogu_app/core/utils/log_message.dart'; +import 'package:niogu_app/core/widgets/custom_button.dart'; +import 'package:niogu_app/core/widgets/custom_snackbar.dart'; +import 'package:niogu_app/core/widgets/custom_text_form_field.dart'; +import 'package:niogu_app/features/goods/units/domain/entities/unit.dart'; +import 'package:niogu_app/features/goods/units/presentation/providers/unit_provider.dart'; +import 'package:sizer/sizer.dart'; + +class AddUnitScreen extends ConsumerStatefulWidget { + const AddUnitScreen({super.key}); + + @override + ConsumerState createState() => _AddUnitScreenState(); +} + +class _AddUnitScreenState extends ConsumerState { + final GlobalKey _formKey = GlobalKey(); + final TextEditingController _nameController = TextEditingController(); + + Timer? _debounce; + + @override + void initState() { + // TODO: implement initState + super.initState(); + } + + @override + void dispose() { + // TODO: implement dispose + _nameController.dispose(); + _debounce?.cancel(); + super.dispose(); + } + + void _onChanged(String value) { + if (_debounce?.isActive ?? false) _debounce!.cancel(); + + _debounce = Timer(const Duration(milliseconds: 500), () async { + if (value.isEmpty) return; + + ref.read(unitSearchProvider.notifier).state = value; + }); + } + + Future _addUnit(Unit unit) async { + try { + await ref.read(unitControllerProvider.notifier).addUnit(unit); + + if(!mounted) return; + + CustomSnackbar.showSuccess(context, "Satuan berhasil disimpan"); + context.pop(); + + } catch (e, st) { + LogMessage.log.i(e.toString(), error: e, stackTrace: st); + CustomSnackbar.showError(context, "Ups, terjadi kesalahan"); + context.pop(); + } + } + + @override + Widget build(BuildContext context) { + final bool isTablet = 100.w >= 600; + + final double maxWidth = 100.w >= 1280 + ? 1200 + : isTablet + ? 800 + : 400; + + final validateUnit = ref.watch(validateUnitProvider); + + final unitControllerState = ref.watch(unitControllerProvider); + + return Dialog( + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(4.w)), + backgroundColor: Colors.white, + child: Container( + padding: EdgeInsets.all(5.w), + constraints: BoxConstraints(maxWidth: maxWidth), + child: Form( + key: _formKey, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + SizedBox(height: 2.h), + + CustomTextFormField( + label: "Nama Satuan", + hint: "Contoh : Kg", + controller: _nameController, + validator: (value) { + if (value == null || value.trim().isEmpty) { + return "Nama satuan belum diisi"; + } + return null; + }, + errorText: validateUnit ? null : "Satuan ini telah tersedia", + onChanged: (value) => _onChanged(value), + ), + + SizedBox(height: 3.h), + + CustomButton( + title: "Simpan", + onPressed: unitControllerState.isLoading + ? null + : () async { + if (_formKey.currentState!.validate() && + ref.read(validateUnitProvider)) { + await _addUnit( + Unit(name: _nameController.text.trim()), + ); + } + }, + ), + SizedBox(height: 1.h), + ], + ), + ), + ), + ); + } +} diff --git a/Mobile Operasional/lib/features/goods/units/presentation/screens/edit_unit_screen.dart b/Mobile Operasional/lib/features/goods/units/presentation/screens/edit_unit_screen.dart new file mode 100644 index 0000000..24a828f --- /dev/null +++ b/Mobile Operasional/lib/features/goods/units/presentation/screens/edit_unit_screen.dart @@ -0,0 +1,132 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; +import 'package:niogu_app/core/utils/log_message.dart'; +import 'package:niogu_app/core/widgets/custom_button.dart'; +import 'package:niogu_app/core/widgets/custom_snackbar.dart'; +import 'package:niogu_app/core/widgets/custom_text_form_field.dart'; +import 'package:niogu_app/features/goods/units/domain/entities/unit.dart'; +import 'package:niogu_app/features/goods/units/presentation/providers/unit_provider.dart'; +import 'package:sizer/sizer.dart'; + +class EditUnitScreen extends ConsumerStatefulWidget { + final Unit unit; + const EditUnitScreen({super.key, required this.unit}); + + @override + ConsumerState createState() => _EditUnitScreenState(); +} + +class _EditUnitScreenState extends ConsumerState { + final GlobalKey _formKey = GlobalKey(); + final TextEditingController _nameController = TextEditingController(); + + Timer? _debounce; + + @override + void initState() { + // TODO: implement initState + super.initState(); + _nameController.text = widget.unit.name; + } + + @override + void dispose() { + // TODO: implement dispose + _nameController.dispose(); + _debounce?.cancel(); + super.dispose(); + } + + void _onChanged(String value) { + if (_debounce?.isActive ?? false) _debounce!.cancel(); + + _debounce = Timer(const Duration(milliseconds: 500), () async { + if (value.isEmpty) return; + + ref.read(unitSearchProvider.notifier).state = value; + }); + } + + Future _updateUnit(String id, String name) async { + try { + await ref.read(unitControllerProvider.notifier).updateUnit(id, name); + + if (!mounted) return; + + CustomSnackbar.showSuccess(context, "Satuan berhasil diubah"); + context.pop(); + } catch (e, st) { + LogMessage.log.i(e.toString(), error: e, stackTrace: st); + CustomSnackbar.showError(context, "Ups, terjadi kesalahan"); + context.pop(); + } + } + + @override + Widget build(BuildContext context) { + final bool isTablet = 100.w >= 600; + + final double maxWidth = 100.w >= 1280 + ? 1200 + : isTablet + ? 800 + : 400; + + final validateUnit = ref.watch(validateUnitProvider); + + final unitControllerState = ref.watch(unitControllerProvider); + + return Dialog( + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(4.w)), + backgroundColor: Colors.white, + child: Container( + padding: EdgeInsets.all(5.w), + constraints: BoxConstraints(maxWidth: maxWidth), + child: Form( + key: _formKey, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + SizedBox(height: 2.h), + + CustomTextFormField( + label: "Nama Satuan", + hint: "Contoh : Kg", + controller: _nameController, + validator: (value) { + if (value == null || value.trim().isEmpty) { + return "Nama satuan belum diisi"; + } + return null; + }, + errorText: validateUnit ? null : "Satuan ini telah tersedia", + onChanged: (value) => _onChanged(value), + ), + + SizedBox(height: 3.h), + + CustomButton( + title: "Simpan", + onPressed: unitControllerState.isLoading + ? null + : () async { + if (_formKey.currentState!.validate() && + ref.read(validateUnitProvider)) { + await _updateUnit( + widget.unit.id, + _nameController.text.trim(), + ); + } + }, + ), + SizedBox(height: 1.h), + ], + ), + ), + ), + ); + } +} diff --git a/Mobile Operasional/lib/features/home/presentation/screens/home_screen.dart b/Mobile Operasional/lib/features/home/presentation/screens/home_screen.dart new file mode 100644 index 0000000..6b809f1 --- /dev/null +++ b/Mobile Operasional/lib/features/home/presentation/screens/home_screen.dart @@ -0,0 +1,658 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; +import 'package:niogu_app/core/constants/app_color.dart'; +import 'package:niogu_app/core/constants/app_font_size.dart'; +import 'package:niogu_app/core/providers/app_provider.dart'; +import 'package:niogu_app/core/router/app_route.dart'; +import 'package:niogu_app/core/services/sync_services.dart'; +import 'package:niogu_app/core/utils/log_message.dart'; +import 'package:niogu_app/core/utils/login_required.dart'; +import 'package:niogu_app/core/enums/sync_status.dart'; +import 'package:niogu_app/core/components/modal_outlet_bottom.dart'; +import 'package:niogu_app/core/utils/time_zone.dart'; +import 'package:niogu_app/core/enums/user_role.dart'; +import 'package:niogu_app/core/widgets/custom_snackbar.dart'; +import 'package:niogu_app/features/home/presentation/widgets/menu_card.dart'; +import 'package:niogu_app/features/home/presentation/widgets/menu_item_card.dart'; +import 'package:niogu_app/features/home/presentation/widgets/section_title.dart'; +import 'package:niogu_app/features/home/presentation/widgets/sync_icon.dart'; +import 'package:niogu_app/features/online_store/presentation/providers/online_store_provider.dart'; +import 'package:niogu_app/features/user/domain/entities/user.dart'; +import 'package:niogu_app/features/user/presentation/providers/user_provider.dart'; +import 'package:sizer/sizer.dart'; + +class HomeScreen extends ConsumerStatefulWidget { + const HomeScreen({super.key}); + + @override + ConsumerState createState() => _HomeScreenState(); +} + +class _HomeScreenState extends ConsumerState { + SyncStatus _syncStatus = SyncStatus.synced; + + bool _isProcessing = false; + + final List> _ownerActivityMenus = [ + {'title': 'Cek Stok', 'icon': Icons.checklist, 'color': Colors.orange}, + { + 'title': 'Stok Masuk', + 'icon': Icons.arrow_circle_down, + 'color': Colors.green, + }, + {'title': 'Pelanggan', 'icon': Icons.person, 'color': Colors.blue}, + {'title': 'Pemasok', 'icon': Icons.car_rental, 'color': Colors.purple}, + ]; + + final List> _adminActivityMenus = [ + { + 'id': 'stock_check', + 'title': 'Cek Stok', + 'icon': Icons.checklist, + 'color': Colors.orange, + }, + { + 'id': 'stock_in', + 'title': 'Stok Masuk', + 'icon': Icons.arrow_circle_down, + 'color': Colors.green, + }, + { + 'id': 'online_order', + 'title': 'Pesanan Online', + 'icon': Icons.shopping_bag_outlined, + 'color': Colors.blue, + }, + ]; + + final List> _ownerAssetMenus = [ + { + "id": "online_store", + 'title': 'Toko\nOnline', + 'icon': Icons.language, + 'color': Colors.indigo, + 'firstBadge': 'Gratis', + }, + { + "id": "outlets", + 'title': 'Outlet\nUsaha', + 'icon': Icons.store, + 'color': Colors.teal, + 'firstBadge': 'Scale Up', + }, + ]; + + @override + void initState() { + // TODO: implement initState + super.initState(); + } + + @override + void dispose() { + // TODO: implement dispose + super.dispose(); + } + + Future _triggerFullSync() async { + if (_isProcessing) return; + + setState(() { + _isProcessing = true; + _syncStatus = SyncStatus.syncing; + }); + + try { + final dio = ref.read(dioProvider); + + final db = ref.read(appDatabaseProvider); + + final syncService = SyncService(dio, db); + + await syncService.restartUpSync(); + + await syncService.processBatch(); + + setState(() { + _isProcessing = false; + _syncStatus = SyncStatus.synced; + }); + + CustomSnackbar.showSuccess(context, "Sinkronisasi berhasil"); + } catch (e, st) { + LogMessage.log.e(e.toString(), error: e, stackTrace: st); + setState(() { + _isProcessing = false; + _syncStatus = SyncStatus.failed; + }); + CustomSnackbar.showError( + context, + "Pastikan teruhubung dengan koneksi internet", + ); + } + } + + @override + Widget build(BuildContext context) { + return LayoutBuilder( + builder: (context, constraints) { + final bool isTablet = 100.w >= 600; + + final isLoggedIn = ref.watch(currentStatusLoginProvider); + + final currentUserName = ref.watch(currentUserNameProvider); + + final currentUserRole = ref.watch(currentUserRoleProvider); + + final currentOutletName = ref.watch(currentOutletNameProvider); + + String? badge; + + if (isLoggedIn) { + final badgeCountState = ref.watch( + numberOfNeedToBeProcessedStreamProvider, + ); + + badgeCountState.whenData((count) { + if (count > 0) badge = count.toString(); + }); + } + + ShiftInfo shiftInfo = ShiftInfo( + shiftName: '---', + shiftStartTime: '---', + shiftEndTime: '---', + ); + + if (isLoggedIn) { + final shiftInfoState = ref.watch(shiftInfoStreamProvider); + + shiftInfoState.whenData((s) => shiftInfo = s); + } + + return SafeArea( + top: false, + bottom: true, + right: false, + left: false, + child: Scaffold( + backgroundColor: Colors.white, + body: CustomScrollView( + slivers: [ + SliverToBoxAdapter( + child: Stack( + clipBehavior: Clip.none, + children: [ + Container( + height: 28.h, + padding: EdgeInsets.symmetric( + horizontal: 5.w, + vertical: 6.h, + ), + decoration: BoxDecoration( + color: AppColor.primaryColor, + borderRadius: BorderRadius.only( + bottomLeft: Radius.circular(10.w), + bottomRight: Radius.circular(10.w), + ), + ), + child: Column( + children: [ + Row( + children: [ + if (isLoggedIn) ...[ + CircleAvatar( + radius: 6.w, + backgroundColor: Colors.white24, + child: Icon( + Icons.person_rounded, + size: 10.w, + color: Colors.white, + ), // Contoh foto user + ), + SizedBox(width: 3.w), + + Expanded( + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text( + "Halo, ${currentUserName!}", + style: TextStyle( + color: Colors.white, + fontWeight: FontWeight.w500, + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + ), + ), + + if (currentUserRole == + UserRole.admin) ...[ + SizedBox(height: 0.5.h), + Text( + "${shiftInfo.shiftName} : ${shiftInfo.shiftStartTime} ${TimeZone.getCurrentTimeZone()} - ${shiftInfo.shiftEndTime} ${TimeZone.getCurrentTimeZone()}", + style: TextStyle( + color: Colors.white, + fontWeight: FontWeight.w500, + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + ), + ), + ], + + SizedBox(height: 0.5.h), + + GestureDetector( + onTap: + currentUserRole == UserRole.admin + ? null + : () { + showModalBottomSheet( + context: context, + useRootNavigator: true, + isScrollControlled: true, + constraints: + const BoxConstraints( + maxWidth: + double.infinity, + ), + builder: (context) => + const ModalOutletBottom(), + ); + }, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Flexible( + child: Text( + currentOutletName!, + style: TextStyle( + color: Colors.white, + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + fontWeight: FontWeight.bold, + ), + maxLines: 1, + overflow: + TextOverflow.ellipsis, + ), + ), + if (currentUserRole == + UserRole.owner) ...[ + SizedBox(width: 1.w), + Icon( + Icons + .keyboard_arrow_down_rounded, + color: Colors.white, + size: 5.w, + ), + ], + ], + ), + ), + ], + ), + ), + + GestureDetector( + onTap: _triggerFullSync, + child: Container( + padding: EdgeInsets.all(2.w), + decoration: BoxDecoration( + color: Colors.white.withOpacity( + 0.15, + ), // Transparan halus + shape: BoxShape.circle, + ), + child: SyncIcon(syncStatus: _syncStatus), + ), + ), + ] else ...[ + OutlinedButton.icon( + onPressed: () => context.goNamed( + AppRoute.ownerLoginScreen, + ), + icon: Icon( + Icons.login_rounded, + color: Colors.white, + size: 4.5.w, + ), + label: Text( + "Masuk Akun", + style: TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + ), + ), + style: OutlinedButton.styleFrom( + side: const BorderSide( + color: Colors.white70, + width: 1.5, + ), + padding: EdgeInsets.symmetric( + horizontal: 4.w, + vertical: 1.h, + ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + 2.w, + ), + ), + backgroundColor: Colors.white.withOpacity( + 0.1, + ), + ), + ), + ], + + if (isTablet) SizedBox(width: 3.w), + + /** Notification + Stack( + children: [ + IconButton( + onPressed: () {}, + padding: EdgeInsets.zero, + icon: Icon( + Icons.notifications_none_rounded, + color: Colors.white, + size: 7.w, + ), + ), + Positioned( + right: isTablet + ? 10 + : 100.w > 360 + ? 12.5 + : 15, + top: isTablet + ? 10 + : 100.w > 360 + ? 12.5 + : 15, + child: Container( + padding: EdgeInsets.all(0.5.w), + decoration: const BoxDecoration( + color: Colors.red, + shape: BoxShape.circle, + ), + constraints: BoxConstraints( + minWidth: 2.5.w, + minHeight: 2.5.w, + ), // Dot merah + ), + ), + ], + ), + */ + ], + ), + ], + ), + ), + + Positioned( + top: 16.h, + left: 0, + right: 0, + child: SizedBox( + height: 18.h, + child: PageView.builder( + controller: PageController(viewportFraction: 0.9), + itemCount: 3, + itemBuilder: (context, index) { + return Container( + margin: EdgeInsets.symmetric(horizontal: 2.w), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(5.w), + gradient: LinearGradient( + colors: index == 0 + ? [ + Colors.blueAccent, + Colors.lightBlueAccent, + ] + : [ + Colors.orangeAccent, + Colors.deepOrangeAccent, + ], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + boxShadow: [ + BoxShadow( + color: AppColor.primaryColor.withOpacity( + 0.2, + ), + blurRadius: 10, + offset: const Offset(0, 5), + ), + ], + ), + child: Stack( + children: [ + Positioned( + right: -5.w, + bottom: -5.w, + child: Icon( + Icons.sell, + color: Colors.white.withOpacity(0.2), + size: 30.w, + ), + ), + Padding( + padding: EdgeInsets.all(5.w), + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + mainAxisAlignment: + MainAxisAlignment.center, + children: [ + Container( + padding: EdgeInsets.symmetric( + horizontal: 2.w, + vertical: 0.5.h, + ), + decoration: BoxDecoration( + color: Colors.white24, + borderRadius: + BorderRadius.circular(2.w), + ), + child: Text( + "Promo Baru", + style: TextStyle( + color: Colors.white, + fontWeight: FontWeight.w500, + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + ), + ), + ), + SizedBox(height: 1.h), + Text( + "Diskon Spesial Hari Ini!", + style: TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + ), + ), + ], + ), + ), + ], + ), + ); + }, + ), + ), + ), + ], + ), + ), + + SliverToBoxAdapter(child: SizedBox(height: 8.h)), + + /** + SliverPadding( + padding: EdgeInsets.symmetric(horizontal: 5.w), + sliver: SliverGrid( + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 2, + childAspectRatio: 1.6, + mainAxisSpacing: 2.h, + crossAxisSpacing: 3.w, + ), + delegate: SliverChildBuilderDelegate((context, index) { + final menu = _ownerActivityMenus[index]; + return MenuItemCard( + title: menu['title'], + icon: menu['icon'], + color: menu['color'], + badge: menu['badge'], + onTap: () { + if (index == 0) { + } else if (index == 1) { + context.pushNamed(AppRoute.stockInScreen); + } + }, + ); + }, childCount: _ownerActivityMenus.length), + ), + ), + */ + if (currentUserRole == UserRole.owner || !isLoggedIn) ...[ + SectionTitle(title: "Aktivitas Utama"), + + SliverPadding( + padding: EdgeInsets.symmetric(horizontal: 5.w), + sliver: SliverGrid( + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 4, + childAspectRatio: 0.8, + mainAxisSpacing: 2.h, + crossAxisSpacing: 2.w, + ), + delegate: SliverChildBuilderDelegate((context, index) { + final menu = _ownerActivityMenus[index]; + return MenuCard( + title: menu['title'], + icon: menu['icon'], + color: menu['color'], + onTap: !isLoggedIn + ? () => LoginRequired.showLoginRequired(context) + : () { + if (index == 0) { + context.pushNamed( + AppRoute.stockCheckScreen, + ); + } else if (index == 1) { + context.pushNamed(AppRoute.stockInScreen); + } else if (index == 2) { + context.pushNamed(AppRoute.customerScreen); + } else if (index == 3) { + context.pushNamed(AppRoute.supplierScreen); + } + }, + ); + }, childCount: _ownerActivityMenus.length), + ), + ), + + SliverToBoxAdapter(child: SizedBox(height: 3.h)), + + SectionTitle(title: "Aset & Manajemen"), + + SliverPadding( + padding: EdgeInsets.symmetric(horizontal: 5.w), + sliver: SliverGrid( + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 2, + childAspectRatio: 1.6, + mainAxisSpacing: 2.h, + crossAxisSpacing: 3.w, + ), + delegate: SliverChildBuilderDelegate((context, index) { + final menu = _ownerAssetMenus[index]; + + return MenuItemCard( + title: menu['title'], + icon: menu['icon'], + color: menu['color'], + firstBadge: menu['firstBadge'], + secondBadge: + menu['id'] == 'online_store' && badge != null + ? badge + : null, + onTap: !isLoggedIn + ? () => LoginRequired.showLoginRequired(context) + : () { + if (index == 0) { + context.pushNamed( + AppRoute.onlineStoreScreen, + ); + } else if (index == 1) { + context.pushNamed(AppRoute.outletScreen); + } + }, + ); + }, childCount: _ownerAssetMenus.length), + ), + ), + ] else if (currentUserRole == UserRole.admin) ...[ + SectionTitle(title: "Aktivitas Utama"), + + SliverPadding( + padding: EdgeInsets.symmetric(horizontal: 5.w), + sliver: SliverGrid( + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 3, + childAspectRatio: 0.8, + mainAxisSpacing: 2.h, + crossAxisSpacing: 2.w, + ), + delegate: SliverChildBuilderDelegate((context, index) { + final menu = _adminActivityMenus[index]; + return MenuCard( + title: menu['title'], + icon: menu['icon'], + color: menu['color'], + badge: menu['id'] == 'online_order' && badge != null + ? badge + : null, + onTap: () { + if (index == 0) { + context.pushNamed(AppRoute.stockCheckScreen); + } else if (index == 1) { + context.pushNamed(AppRoute.stockInScreen); + } else if (index == 2) { + context.pushNamed( + AppRoute.onlineStoreOrderScreen, + ); + } + }, + ); + }, childCount: _adminActivityMenus.length), + ), + ), + ], + + SliverToBoxAdapter(child: SizedBox(height: 12.h)), + ], + ), + ), + ); + }, + ); + } +} diff --git a/Mobile Operasional/lib/features/home/presentation/widgets/menu_card.dart b/Mobile Operasional/lib/features/home/presentation/widgets/menu_card.dart new file mode 100644 index 0000000..3dda058 --- /dev/null +++ b/Mobile Operasional/lib/features/home/presentation/widgets/menu_card.dart @@ -0,0 +1,94 @@ +import 'package:flutter/material.dart'; +import 'package:niogu_app/core/constants/app_font_size.dart'; +import 'package:sizer/sizer.dart'; + +class MenuCard extends StatelessWidget { + final String title; + final IconData icon; + final Color color; + final String? badge; + final VoidCallback? onTap; + + const MenuCard({ + super.key, + required this.title, + required this.icon, + required this.color, + this.badge, + this.onTap, + }); + + @override + Widget build(BuildContext context) { + final bool isTablet = 100.w >= 600; + return InkWell( + onTap: onTap, + child: Column( + children: [ + Expanded( + child: Container( + width: double.infinity, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(4.w), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.15), + blurRadius: 10, + offset: const Offset(0, 4), + ), + ], + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Container( + padding: EdgeInsets.all(2.5.w), + decoration: BoxDecoration( + color: color.withOpacity(0.1), + shape: BoxShape.circle, + ), + child: Icon(icon, color: color, size: 6.w), + ), + + if (badge != null) ...[ + SizedBox(width: 3.w), + + CircleAvatar( + radius: 2.5.w, + backgroundColor: Colors.red, + child: Text( + badge!, + style: TextStyle( + color: Colors.white, + fontSize: + ((isTablet + ? AppFontSize.medium + : AppFontSize.small) - + 1.25) + .sp, + fontWeight: FontWeight.bold, + ), + ), + ), + ], + ], + ), + ), + ), + + SizedBox(height: 1.h), + Text( + title, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: AppFontSize.small.sp, + color: Colors.grey[800], + fontWeight: FontWeight.w500, + ), + ), + ], + ), + ); + } +} diff --git a/Mobile Operasional/lib/features/home/presentation/widgets/menu_item_card.dart b/Mobile Operasional/lib/features/home/presentation/widgets/menu_item_card.dart new file mode 100644 index 0000000..532661d --- /dev/null +++ b/Mobile Operasional/lib/features/home/presentation/widgets/menu_item_card.dart @@ -0,0 +1,118 @@ +import 'package:flutter/material.dart'; +import 'package:niogu_app/core/constants/app_font_size.dart'; +import 'package:sizer/sizer.dart'; + +class MenuItemCard extends StatelessWidget { + final String title; + final IconData icon; + final Color color; + final String? firstBadge; + final String? secondBadge; + final VoidCallback? onTap; + + const MenuItemCard({ + super.key, + required this.title, + required this.icon, + required this.color, + this.firstBadge, + this.secondBadge, + this.onTap, + }); + + @override + Widget build(BuildContext context) { + final bool isTablet = 100.w >= 600; + return Container( + padding: EdgeInsets.all(3.w), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(4.w), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.15), + blurRadius: 10, + offset: const Offset(0, 4), + ), + ], + ), + child: InkWell( + onTap: onTap, + child: Row( + children: [ + Container( + padding: EdgeInsets.all(2.5.w), + decoration: BoxDecoration( + color: color.withOpacity(0.1), + borderRadius: BorderRadius.circular(2.75.w), + ), + child: Icon(icon, color: color, size: 6.w), + ), + SizedBox(width: 3.w), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + if (firstBadge != null) + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + margin: EdgeInsets.only(bottom: 0.5.h), + padding: EdgeInsets.symmetric( + horizontal: 1.5.w, + vertical: 0.2.h, + ), + decoration: BoxDecoration( + color: color, + borderRadius: BorderRadius.circular(1.w), + ), + child: Text( + firstBadge!, + style: TextStyle( + color: Colors.white, + fontSize: (AppFontSize.small - 2).sp, + fontWeight: FontWeight.w500, + ), + ), + ), + + if (secondBadge != null) + CircleAvatar( + radius: 2.5.w, + backgroundColor: Colors.red, + child: Text( + secondBadge!, + style: TextStyle( + color: Colors.white, + fontSize: + ((isTablet + ? AppFontSize.medium + : AppFontSize.small) - + 1.25) + .sp, + fontWeight: FontWeight.bold, + ), + ), + ), + ], + ), + Text( + title.replaceAll('\n', ' '), + style: TextStyle( + fontSize: AppFontSize.small.sp, + fontWeight: FontWeight.w500, + color: Colors.black, + ), + ), + ], + ), + ), + ], + ), + ), + ); + } +} diff --git a/Mobile Operasional/lib/features/home/presentation/widgets/section_title.dart b/Mobile Operasional/lib/features/home/presentation/widgets/section_title.dart new file mode 100644 index 0000000..ff5c7f4 --- /dev/null +++ b/Mobile Operasional/lib/features/home/presentation/widgets/section_title.dart @@ -0,0 +1,25 @@ +import 'package:flutter/material.dart'; +import 'package:niogu_app/core/constants/app_font_size.dart'; +import 'package:sizer/sizer.dart'; + +class SectionTitle extends StatelessWidget { + final String title; + const SectionTitle({super.key, required this.title}); + + @override + Widget build(BuildContext context) { + return SliverToBoxAdapter( + child: Padding( + padding: EdgeInsets.fromLTRB(5.w, 0, 5.w, 2.h), + child: Text( + title, + style: TextStyle( + fontSize: AppFontSize.medium.sp, + fontWeight: FontWeight.bold, + color: Colors.black, + ), + ), + ), + ); + } +} \ No newline at end of file diff --git a/Mobile Operasional/lib/features/home/presentation/widgets/sync_icon.dart b/Mobile Operasional/lib/features/home/presentation/widgets/sync_icon.dart new file mode 100644 index 0000000..a4d26a2 --- /dev/null +++ b/Mobile Operasional/lib/features/home/presentation/widgets/sync_icon.dart @@ -0,0 +1,42 @@ +import 'package:flutter/material.dart'; +import 'package:niogu_app/core/enums/sync_status.dart'; +import 'package:sizer/sizer.dart'; + +class SyncIcon extends StatelessWidget { + final SyncStatus syncStatus; + + const SyncIcon({super.key, required this.syncStatus}); + + @override + Widget build(BuildContext context) { + switch (syncStatus) { + case SyncStatus.dirty: + return Icon( + Icons.cloud_queue_outlined, + color: Colors.amberAccent, + size: 6.w, + ); + case SyncStatus.syncing: + return SizedBox( + width: 6.w, + height: 6.w, + child: const CircularProgressIndicator( + color: Colors.white, + strokeWidth: 2, + ), + ); + case SyncStatus.failed: + return Icon( + Icons.cloud_off_rounded, + color: Colors.redAccent, + size: 6.w, + ); + case SyncStatus.synced: + return Icon( + Icons.cloud_done_rounded, + color: Colors.greenAccent, + size: 6.w, + ); + } + } +} \ No newline at end of file diff --git a/Mobile Operasional/lib/features/online_store/data/repositories/online_store_repository_impl.dart b/Mobile Operasional/lib/features/online_store/data/repositories/online_store_repository_impl.dart new file mode 100644 index 0000000..f4c8678 --- /dev/null +++ b/Mobile Operasional/lib/features/online_store/data/repositories/online_store_repository_impl.dart @@ -0,0 +1,202 @@ +import 'package:drift/drift.dart'; +import 'package:niogu_app/core/database/app_database.dart'; +import 'package:niogu_app/core/entities/custom_activity.dart'; +import 'package:niogu_app/core/enums/approval_process.dart'; +import 'package:niogu_app/core/enums/canceled_by.dart'; +import 'package:niogu_app/core/enums/order_status.dart'; +import 'package:niogu_app/core/enums/sync_status.dart'; +import 'package:niogu_app/features/online_store/domain/entities/online_store.dart'; +import 'package:niogu_app/features/online_store/domain/repositories/i_online_store_repository.dart'; + +class OnlineStoreRepositoryImpl implements IOnlineStoreRepository { + final AppDatabase _appDatabase; + + OnlineStoreRepositoryImpl(this._appDatabase); + + @override + Future getConfigureOnlineStore() async { + return await _appDatabase.tenantDao.getConfigureOnlineStore(); + } + + @override + Future configureOnlineStore(ConfigureOnlineStore configure) async { + await _appDatabase.tenantDao.configureOnlineStore(configure); + } + + @override + Stream> watchOnlineStoreProducts() { + return _appDatabase.tenantDao.watchAllOnlineStoreProducts(); + } + + @override + Future updateAlreadyOrderOnline(String orderId) async { + return await _appDatabase.orderDao.updateAlreadyReadOrderOnline(orderId); + } + + @override + Stream watchOrderOnlineHasNotReview() { + return _appDatabase.orderDao.watchOrderOnlineHasNotReview(); + } + + @override + Stream watchNumberOfNeedToBeProcessed() { + return _appDatabase.orderDao.watchNumberOfNeedToBeProcessed(); + } + + @override + Stream> watchOnlineOrders({ + String? outletId, + DateTime? startDate, + DateTime? endDate, + }) { + return _appDatabase.orderDao.watchAllOnlineOrders( + outletId: outletId, + startDate: startDate, + endDate: endDate, + ); + } + + @override + Future fetchOnlineOrderDetailById(String orderId) async { + return await _appDatabase.orderDao.getOnlineOrderDetailById(orderId); + } + + @override + Stream watchOrderStatusById(String orderId) { + return _appDatabase.orderDao.watchOrderStatusById(orderId); + } + + @override + Future updateOrder({ + required String orderId, + required OrderStatus orderStatus, + required bool cancellationAccepted, + ApprovalProcess? process, + CanceledBy? canceledBy, + }) async { + return await _appDatabase.orderDao.updateOrder( + orderId: orderId, + orderStatus: orderStatus, + cancellationAccepted: cancellationAccepted, + process: process, + canceledBy: canceledBy, + ); + } + + @override + Future fetchShippingConfiguration() async { + return await _appDatabase.tenantDao.getShippingConfiuration(); + } + + @override + Future shippingConfiguration(ShippingConfiguration configure) async { + return await _appDatabase.tenantDao.shippingConfiguration(configure); + } + + @override + Future> fetchConfigurePaymentMethod() async { + return await _appDatabase.tenantDao.getConfigurePaymentMethod(); + } + + @override + Future paymentMethodConfiguration( + List configures, + ) async { + final List companions = []; + + for (final configure in configures) { + String? sourceName; + + String? accountNumber; + + String? receipentName; + + if (configure.sourceName.isNotEmpty) { + sourceName = configure.sourceName; + } + + if (configure.accountNumber.isNotEmpty) { + accountNumber = configure.accountNumber; + } + + if (configure.receipentName.isNotEmpty) { + receipentName = configure.receipentName; + } + + companions.add( + TenantPaymentMethodsCompanion( + localId: Value(configure.localId), + sourceName: Value(sourceName), + accountNumber: Value(accountNumber), + receipentName: Value(receipentName), + syncStatus: Value(SyncStatus.dirty.status), + ), + ); + } + + await _appDatabase.tenantDao.paymentMethodConfiguration(companions); + } + + @override + Stream> watchProductReviews() { + return _appDatabase.productReviewDao.watchAllProductReviews(); + } + + @override + Stream watchDetailProductReview(String productId) { + return _appDatabase.productReviewDao.watchDetailProductReview(productId); + } + + @override + Stream> watchProductComments(String productId) { + return _appDatabase.productReviewDao.watchAllProductComments(productId); + } + + @override + Future fetchRatingConfiguration() async { + return await _appDatabase.tenantDao.getRatingConfiguration(); + } + + @override + Future ratingConfiguration(RatingConfiguration configuration) async { + return await _appDatabase.tenantDao.ratingConfiguration(configuration); + } + + @override + Stream> watchProductCampaigns() { + return _appDatabase.tenantDao.watchAllProductCampaigns(); + } + + @override + Stream> watchCategoryCampaigns() { + return _appDatabase.tenantDao.watchAllCategoryCampaigns(); + } + + @override + Stream> watchCampaigns() { + return _appDatabase.campaignDao.watchAllCampaigns(); + } + + @override + Stream> watchTargetOutlets() { + return _appDatabase.tenantDao.watchAllTargetOutlets(); + } + + @override + Future fetchCampaignById(String id) async { + return await _appDatabase.campaignDao.getCampaignById(id); + } + + @override + Future saveCampaign({ + required UpsertCampaign campaign, + List? deleteTargetOutlets, + }) async { + return _appDatabase.campaignDao.saveCampaign(campaign, deleteTargetOutlets); + } + + @override + Future deleteCampaign(String campaignId) async { + return _appDatabase.campaignDao.deleteCampaign(campaignId); + } +} diff --git a/Mobile Operasional/lib/features/online_store/domain/entities/online_store.dart b/Mobile Operasional/lib/features/online_store/domain/entities/online_store.dart new file mode 100644 index 0000000..f9007c8 --- /dev/null +++ b/Mobile Operasional/lib/features/online_store/domain/entities/online_store.dart @@ -0,0 +1,484 @@ +import 'package:niogu_app/core/enums/approval_process.dart'; +import 'package:niogu_app/core/enums/campaign_type.dart'; +import 'package:niogu_app/core/enums/canceled_by.dart'; +import 'package:niogu_app/core/enums/delivery_fee_type.dart'; +import 'package:niogu_app/core/enums/delivery_type.dart'; +import 'package:niogu_app/core/enums/order_status.dart'; +import 'package:niogu_app/core/enums/review_display_policy.dart'; +import 'package:niogu_app/core/enums/review_display_type.dart'; +import 'package:niogu_app/core/enums/stock_type.dart'; +import 'package:uuid/uuid.dart'; + +class ConfigureOnlineStore { + final String onlineStoreName; + final String onlineOpenTime; + final String onlineCloseTime; + final bool isCloseService; + final int preparationTimeMinutes; + final int orderIntervalMinutes; + final List productIds; + + const ConfigureOnlineStore({ + required this.onlineStoreName, + required this.onlineOpenTime, + required this.onlineCloseTime, + required this.isCloseService, + required this.preparationTimeMinutes, + required this.orderIntervalMinutes, + required this.productIds, + }); +} + +enum OnlineStoreProductEmpty { + loading, + empty_database, + empty_search_result, + has_data, +} + +class OnlineStoreProduct { + final String id; + final String name; + final StockType stockType; + final double remainingStock; + final String unit; + final double sellingPrice; + final String? imagePath; + final bool hasVariant; + final List variants; + final bool hasBeenDelete; + + const OnlineStoreProduct({ + required this.id, + required this.name, + required this.stockType, + required this.remainingStock, + required this.unit, + required this.sellingPrice, + this.imagePath, + required this.hasVariant, + required this.variants, + required this.hasBeenDelete, + }); +} + +class VariantOnlineStoreProduct { + final String id; + final String name; + final StockType stockType; + final double remainingStock; + final String unit; + final double sellingPrice; + final bool isProductVariant; + final bool hasBeenDelete; + + const VariantOnlineStoreProduct({ + required this.id, + required this.name, + required this.stockType, + required this.remainingStock, + required this.unit, + required this.sellingPrice, + required this.isProductVariant, + required this.hasBeenDelete, + }); +} + +enum OnlineOrderEmpty { loading, empty_database, empty_search_result, has_data } + +class OnlineOrder { + final String id; + final String? displayImage; + final String orderNumber; + final OrderStatus orderStatus; + final bool isCancellation; + final bool cancellationAccepted; + final ApprovalProcess process; + final int itemCount; + final double orderTotal; + final DateTime orderDate; + + const OnlineOrder({ + required this.id, + this.displayImage, + required this.orderNumber, + required this.orderStatus, + required this.isCancellation, + required this.cancellationAccepted, + required this.process, + required this.itemCount, + required this.orderTotal, + required this.orderDate, + }); +} + +class OnlineOrderDetail { + final String id; + final String outletName; + final String orderNumber; + final DateTime orderDate; + final String? notes; + final OrderStatus orderStatus; + final bool isCancellation; + final bool cancellationAccepted; + final ApprovalProcess process; + final DeliveryType deliveryType; + final DeliveryInfo deliveryInfo; + final String paymentMethod; + final String paymentProof; + final List items; + final double totalOrder; + final double deliveryFee; + final double totalAmount; + + const OnlineOrderDetail({ + required this.id, + required this.outletName, + required this.orderNumber, + required this.orderDate, + this.notes, + required this.orderStatus, + required this.isCancellation, + required this.cancellationAccepted, + required this.process, + required this.deliveryType, + required this.deliveryInfo, + required this.paymentMethod, + required this.paymentProof, + required this.items, + required this.totalOrder, + required this.deliveryFee, + required this.totalAmount, + }); +} + +class DeliveryInfo { + final String name; + final String phoneNumber; + final String fullAddress; + final String preference; + + const DeliveryInfo({ + required this.name, + required this.phoneNumber, + required this.fullAddress, + required this.preference, + }); +} + +class OnlineOrderItem { + final String? imagePath; + final String name; + final String? variantName; + final double sellingPrice; + final double quantity; + final double subtotal; + + const OnlineOrderItem({ + this.imagePath, + required this.name, + this.variantName, + required this.sellingPrice, + required this.quantity, + required this.subtotal, + }); +} + +class ViewOrderStatus { + final bool isCancellation; + final bool cancellationAccepted; + final ApprovalProcess process; + final OrderStatus orderStatus; + final CanceledBy? canceledBy; + + const ViewOrderStatus({ + required this.isCancellation, + required this.cancellationAccepted, + required this.process, + required this.orderStatus, + required this.canceledBy, + }); +} + +class ShippingConfiguration { + final bool allowCOD; + final DeliveryFeeType deliveryFeeType; + final double maxDeliveryRadiusKm; + final double deliveryFlatFee; + final double deliveryFeePerKm; + final double deliveryBaseFee; + + const ShippingConfiguration({ + required this.allowCOD, + required this.deliveryFeeType, + required this.maxDeliveryRadiusKm, + required this.deliveryFlatFee, + required this.deliveryFeePerKm, + required this.deliveryBaseFee, + }); +} + +class ConfigurePaymentMethod { + final String localId; + final String sourceName; + final String accountNumber; + final String receipentName; + + ConfigurePaymentMethod({ + String? localId, + required this.sourceName, + required this.accountNumber, + required this.receipentName, + }) : localId = localId ?? Uuid().v7().toString(); +} + +class DisplayProductReview { + final String id; + final String? imagePath; + final String name; + final double averageRating; + final double totalSoldInOnlineStore; + final double totalSoldInPos; + final String unit; + + const DisplayProductReview({ + required this.id, + this.imagePath, + required this.name, + required this.averageRating, + required this.totalSoldInOnlineStore, + required this.totalSoldInPos, + required this.unit, + }); + + DisplayProductReview copyWith({ + double? averageRating, + double? totalSoldInOnlineStore, + double? totalSoldInPos, + }) { + return DisplayProductReview( + id: id, + imagePath: imagePath, + name: name, + averageRating: averageRating ?? this.averageRating, + totalSoldInOnlineStore: + totalSoldInOnlineStore ?? this.totalSoldInOnlineStore, + totalSoldInPos: totalSoldInPos ?? this.totalSoldInPos, + unit: unit, + ); + } +} + +class DetailProductReview { + final List imagePaths; + final String name; + final double averageRating; + final int totalRating; + final RatingDistribution rating; + final int totalLike; + + const DetailProductReview({ + required this.imagePaths, + required this.name, + required this.averageRating, + required this.totalRating, + required this.rating, + required this.totalLike, + }); +} + +class RatingDistribution { + final List values; + + const RatingDistribution(this.values); + + double get star1 => values[0]; + double get star2 => values[1]; + double get star3 => values[2]; + double get star4 => values[3]; + double get star5 => values[4]; +} + +class ProductComment { + final String name; + final int rating; + final String? variantName; + final String? comment; + final DateTime date; + + const ProductComment({ + required this.name, + required this.rating, + this.variantName, + this.comment, + required this.date, + }); +} + +class RatingConfiguration { + final ReviewDisplayType type; + final ReviewDisplayPolicy policy; + + const RatingConfiguration({required this.type, required this.policy}); +} + +enum ProductCampaignEmpty { + loading, + empty_database, + empty_search_result, + has_data, +} + +class ProductCampaign { + final String id; + final String? imagePath; + final String name; + final StockType stockType; + final double remainingStock; + final String unit; + + const ProductCampaign({ + required this.id, + this.imagePath, + required this.name, + required this.stockType, + required this.remainingStock, + required this.unit, + }); +} + +enum CategoryCampaignEmpty { + loading, + empty_database, + empty_search_result, + has_data, +} + +class CategoryCampaign { + final String id; + final String? imagePath; + final String name; + final int productUseAmount; + + const CategoryCampaign({ + required this.id, + this.imagePath, + required this.name, + required this.productUseAmount, + }); +} + +class SelectedItemCampaign { + final String id; + final String name; + final CampaignType type; + + const SelectedItemCampaign({ + required this.id, + required this.name, + required this.type, + }); +} + +class TargetOutlet { + final String id; + final String name; + final String? address; + + const TargetOutlet({required this.id, required this.name, this.address}); +} + +class SelectedTargetOutlet { + final String? id; + final String outletId; + final String name; + + const SelectedTargetOutlet({ + this.id, + required this.outletId, + required this.name, + }); + + SelectedTargetOutlet copyWith({String? id}) { + return SelectedTargetOutlet(id: id, outletId: outletId, name: name); + } +} + +class DisplayCampaign { + final String id; + final String imagePath; + final String name; + final DateTime? startDate; + final DateTime? endDate; + final bool isActive; + final int outletUseAmount; + + const DisplayCampaign({ + required this.id, + required this.imagePath, + required this.name, + this.startDate, + this.endDate, + required this.isActive, + required this.outletUseAmount, + }); +} + +class CampaignDetail { + final String id; + final String imagePath; + final String name; + final DateTime? startDate; + final DateTime? endDate; + final CampaignType? campaignType; + final String? linkId; + final String? linkName; + final List targetOutlets; + final bool isActive; + + const CampaignDetail({ + required this.id, + required this.imagePath, + required this.name, + this.startDate, + this.endDate, + this.campaignType, + this.linkId, + this.linkName, + required this.targetOutlets, + required this.isActive, + }); +} + +class UpsertCampaign { + final String id; + final String imagePath; + final String name; + final DateTime? startDate; + final DateTime? endDate; + final CampaignType? campaignType; + final String? linkId; + final List targetOutlets; + final bool isActive; + + UpsertCampaign({ + String? id, + required this.imagePath, + required this.name, + this.startDate, + this.endDate, + this.campaignType, + this.linkId, + required this.targetOutlets, + this.isActive = true, + }) : id = id ?? const Uuid().v7().toString(); +} + +class UpsertTargetOutlet { + final String id; + final String outletId; + + UpsertTargetOutlet({String? id, required this.outletId}) + : id = id ?? const Uuid().v7().toString(); + +} diff --git a/Mobile Operasional/lib/features/online_store/domain/repositories/i_online_store_repository.dart b/Mobile Operasional/lib/features/online_store/domain/repositories/i_online_store_repository.dart new file mode 100644 index 0000000..36dac4d --- /dev/null +++ b/Mobile Operasional/lib/features/online_store/domain/repositories/i_online_store_repository.dart @@ -0,0 +1,74 @@ +import 'package:niogu_app/core/entities/custom_activity.dart'; +import 'package:niogu_app/core/enums/approval_process.dart'; +import 'package:niogu_app/core/enums/canceled_by.dart'; +import 'package:niogu_app/core/enums/order_status.dart'; +import 'package:niogu_app/features/online_store/domain/entities/online_store.dart'; + +abstract class IOnlineStoreRepository { + Future getConfigureOnlineStore(); + + Future configureOnlineStore(ConfigureOnlineStore configure); + + Stream> watchOnlineStoreProducts(); + + Future updateAlreadyOrderOnline(String orderId); + + Stream watchOrderOnlineHasNotReview(); + + Stream watchNumberOfNeedToBeProcessed(); + + Stream> watchOnlineOrders({ + String? outletId, + DateTime? startDate, + DateTime? endDate, + }); + + Future fetchOnlineOrderDetailById(String orderId); + + Stream watchOrderStatusById(String orderId); + + Future updateOrder({ + required String orderId, + required OrderStatus orderStatus, + required bool cancellationAccepted, + ApprovalProcess? process, + CanceledBy? canceledBy, + }); + + Future fetchShippingConfiguration(); + + Future shippingConfiguration(ShippingConfiguration configure); + + Future> fetchConfigurePaymentMethod(); + + Future paymentMethodConfiguration( + List configures, + ); + + Stream> watchProductReviews(); + + Stream watchDetailProductReview(String productId); + + Stream> watchProductComments(String productId); + + Future fetchRatingConfiguration(); + + Future ratingConfiguration(RatingConfiguration configuration); + + Stream> watchProductCampaigns(); + + Stream> watchCategoryCampaigns(); + + Stream> watchCampaigns(); + + Stream> watchTargetOutlets(); + + Future fetchCampaignById(String id); + + Future saveCampaign({ + required UpsertCampaign campaign, + List? deleteTargetOutlets, + }); + + Future deleteCampaign(String campaignId); +} diff --git a/Mobile Operasional/lib/features/online_store/presentation/controllers/online_store_controller.dart b/Mobile Operasional/lib/features/online_store/presentation/controllers/online_store_controller.dart new file mode 100644 index 0000000..15908cf --- /dev/null +++ b/Mobile Operasional/lib/features/online_store/presentation/controllers/online_store_controller.dart @@ -0,0 +1,169 @@ +import 'dart:async'; + +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:niogu_app/core/enums/approval_process.dart'; +import 'package:niogu_app/core/enums/canceled_by.dart'; +import 'package:niogu_app/core/enums/order_status.dart'; +import 'package:niogu_app/features/online_store/domain/entities/online_store.dart'; +import 'package:niogu_app/features/online_store/domain/repositories/i_online_store_repository.dart'; +import 'package:niogu_app/features/online_store/presentation/providers/online_store_provider.dart'; + +class OnlineStoreController extends AutoDisposeAsyncNotifier { + late final IOnlineStoreRepository _onlineStoreRepository; + + @override + FutureOr build() { + _onlineStoreRepository = ref.read(onlineStoreRepositoryProvider); + } + + Future updateAlreadyReadOrderOnline(String orderId) async { + state = const AsyncValue.loading(); + + final result = await AsyncValue.guard(() async { + await _onlineStoreRepository.updateAlreadyOrderOnline(orderId); + }); + + state = result; + + if (result is AsyncError) throw result.error!; + } + + Future configureOnlineStore(ConfigureOnlineStore configure) async { + state = const AsyncValue.loading(); + + final result = await AsyncValue.guard(() async { + await _onlineStoreRepository.configureOnlineStore(configure); + }); + + state = result; + + if (result is AsyncError) throw result.error!; + } + + Future updateOrder({ + required String orderId, + required OrderStatus orderStatus, + bool cancellationAccepted = false, + ApprovalProcess? process, + CanceledBy? canceledBy, + }) async { + state = const AsyncValue.loading(); + + final result = await AsyncValue.guard(() async { + await _onlineStoreRepository.updateOrder( + orderId: orderId, + orderStatus: orderStatus, + cancellationAccepted: cancellationAccepted, + process: process, + canceledBy: canceledBy, + ); + }); + + state = result; + + if (result is AsyncError) throw result.error!; + } + + Future shippingConfiguration(ShippingConfiguration configure) async { + state = const AsyncValue.loading(); + + final result = await AsyncValue.guard(() async { + await _onlineStoreRepository.shippingConfiguration(configure); + }); + + state = result; + + if (result is AsyncError) throw result.error!; + } + + Future paymentMethodConfiguration( + List configures, + ) async { + state = const AsyncValue.loading(); + + final result = await AsyncValue.guard(() async { + await _onlineStoreRepository.paymentMethodConfiguration(configures); + }); + + state = result; + + if (result is AsyncError) throw result.error!; + } + + Future ratingConfiguration(RatingConfiguration configuration) async { + state = const AsyncValue.loading(); + + final result = await AsyncValue.guard(() async { + await _onlineStoreRepository.ratingConfiguration(configuration); + }); + + state = result; + + if (result is AsyncError) throw result.error!; + } + + Future saveCampaign({ + required UpsertCampaign campaign, + List? deleteTargetOutlets, + }) async { + state = const AsyncValue.loading(); + + final result = await AsyncValue.guard(() async { + await _onlineStoreRepository.saveCampaign( + campaign: campaign, + deleteTargetOutlets: deleteTargetOutlets, + ); + }); + + state = result; + + if (result is AsyncError) throw result.error!; + } + + Future deleteCampaign(String campaignId) async { + state = const AsyncValue.loading(); + + final result = await AsyncValue.guard(() async { + await _onlineStoreRepository.deleteCampaign(campaignId); + }); + + state = result; + + if (result is AsyncError) throw result.error!; + } +} + +class SelectedTargetOutletController + extends StateNotifier> { + SelectedTargetOutletController() : super({}); + + void hydrate(List oldData) { + state = {for (final data in oldData) data.outletId: data}; + } + + void toggleSelectAll(SelectedTargetOutlet outlet) { + if (state.containsKey(outlet.outletId)) { + state = {...state} + ..remove(outlet.outletId) + ..[outlet.outletId] = outlet; + } else { + state = {...state, outlet.outletId: outlet}; + } + } + + void toggle(SelectedTargetOutlet outlet) { + if (state.containsKey(outlet.outletId)) { + state = {...state}..remove(outlet.outletId); + } else { + state = {...state, outlet.outletId: outlet}; + } + } + + void clear() { + state = {...state}..clear(); + } + + void restore(Map inititalTargets) { + state = Map.from(inititalTargets); + } +} diff --git a/Mobile Operasional/lib/features/online_store/presentation/providers/online_store_provider.dart b/Mobile Operasional/lib/features/online_store/presentation/providers/online_store_provider.dart new file mode 100644 index 0000000..70e03b4 --- /dev/null +++ b/Mobile Operasional/lib/features/online_store/presentation/providers/online_store_provider.dart @@ -0,0 +1,366 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:niogu_app/core/entities/custom_activity.dart'; +import 'package:niogu_app/core/providers/app_provider.dart'; +import 'package:niogu_app/core/enums/report_type.dart'; +import 'package:niogu_app/features/goods/products/presentation/providers/category_provider.dart'; +import 'package:niogu_app/features/online_store/data/repositories/online_store_repository_impl.dart'; +import 'package:niogu_app/features/online_store/domain/entities/online_store.dart'; +import 'package:niogu_app/features/online_store/domain/repositories/i_online_store_repository.dart'; +import 'package:niogu_app/features/online_store/presentation/controllers/online_store_controller.dart'; +import 'package:niogu_app/features/report/presentation/providers/report_provider.dart'; + +final onlineStoreRepositoryProvider = + Provider.autoDispose((ref) { + final appDatabase = ref.watch(appDatabaseProvider); + return OnlineStoreRepositoryImpl(appDatabase); + }); + +final onlineStoreProductStreamProvider = + StreamProvider.autoDispose>((ref) { + final onlineStoreRepository = ref.watch(onlineStoreRepositoryProvider); + return onlineStoreRepository.watchOnlineStoreProducts(); + }); + +final onlineStoreProductSearchProvider = StateProvider.autoDispose( + (ref) => '', +); + +final filteredOnlineStoreProductProvider = + Provider.autoDispose>>((ref) { + final onlineStoreProductStreamAsync = ref.watch( + onlineStoreProductStreamProvider, + ); + + final onlineStoreProductSearchAsync = ref.watch( + onlineStoreProductSearchProvider, + ); + + return onlineStoreProductStreamAsync.when( + data: (products) { + if (onlineStoreProductSearchAsync.trim().isEmpty) { + return AsyncValue.data(products); + } + + final filteredOnlineStoreProducts = products.where((product) { + return product.name.toLowerCase().contains( + onlineStoreProductSearchAsync, + ); + }).toList(); + + return AsyncValue.data(filteredOnlineStoreProducts); + }, + error: (error, stackTrace) => AsyncValue.error(error, stackTrace), + loading: () => const AsyncValue.loading(), + ); + }); + +final onlineStoreProductEmptyProvider = + Provider.autoDispose((ref) { + final onlineStoreProductStreamAsync = ref.watch( + onlineStoreProductStreamProvider, + ); + + final filteredOnlineStoreProductAsync = ref.watch( + filteredOnlineStoreProductProvider, + ); + + final onlineStoreProductSearchAsync = ref.watch( + onlineStoreProductSearchProvider, + ); + + final allProductOnlineStores = onlineStoreProductStreamAsync.value ?? []; + + if (allProductOnlineStores.isEmpty) { + return OnlineStoreProductEmpty.empty_database; + } + + final filteredOnlineStoreProducts = + filteredOnlineStoreProductAsync.value ?? []; + + if (onlineStoreProductSearchAsync.isNotEmpty && + filteredOnlineStoreProducts.isEmpty) { + return OnlineStoreProductEmpty.empty_search_result; + } + + return OnlineStoreProductEmpty.has_data; + }); + +final onlineStoreControllerProvider = + AsyncNotifierProvider.autoDispose( + OnlineStoreController.new, + ); + +final selectedOnlineStoreProductProvider = + StateProvider.autoDispose>((ref) => {}); + +final expandedOnlineStoreProductProvider = StateProvider.family + .autoDispose((ref, productId) => false); + +final onlineOrderHasNotReviewStreamProvider = + StreamProvider.autoDispose((ref) { + final onlineStoreRepository = ref.watch(onlineStoreRepositoryProvider); + return onlineStoreRepository.watchOrderOnlineHasNotReview(); + }); + +final numberOfNeedToBeProcessedStreamProvider = StreamProvider.autoDispose( + (ref) { + final onlineStoreRepository = ref.watch(onlineStoreRepositoryProvider); + return onlineStoreRepository.watchNumberOfNeedToBeProcessed(); + }, +); + +final onlineOrderStreamProvider = StreamProvider.autoDispose>( + (ref) { + final onlineStoreRepository = ref.watch(onlineStoreRepositoryProvider); + + final outletId = ref.watch( + filteredByOutletProvider(ReportType.online_order), + ); + + final dateRange = ref.watch( + filteredByDateTimeProvider(ReportType.online_order), + ); + + final startDate = dateRange?.start; + + final endDate = dateRange?.end; + + return onlineStoreRepository.watchOnlineOrders( + outletId: outletId, + startDate: startDate, + endDate: endDate, + ); + }, +); + +final onlineOrderSearchProvider = StateProvider.autoDispose( + (ref) => '', +); + +final filteredOnlineOrderProvider = + Provider.autoDispose>>((ref) { + final onlineOrderStreamAsync = ref.watch(onlineOrderStreamProvider); + + final onlineOrderSearchAsync = ref.watch(onlineOrderSearchProvider); + + return onlineOrderStreamAsync.when( + data: (orders) { + if (onlineOrderSearchAsync.trim().isEmpty) { + return AsyncValue.data(orders); + } + + final filteredOrders = orders.where((order) { + return order.orderNumber.toLowerCase().contains( + onlineOrderSearchAsync.toLowerCase(), + ); + }).toList(); + + return AsyncValue.data(filteredOrders); + }, + error: (error, stackTrace) { + return AsyncValue.error(error, stackTrace); + }, + loading: () => const AsyncValue.loading(), + ); + }); + +final onlineOrderEmptyProvider = Provider.autoDispose((ref) { + final onlineOrderStreamAsync = ref.watch(onlineOrderStreamProvider); + + final filteredOnlineOrderAsync = ref.watch(filteredOnlineOrderProvider); + + final onlineOrderSearchAsync = ref.watch(onlineOrderSearchProvider); + + if (onlineOrderStreamAsync.isLoading) { + return OnlineOrderEmpty.loading; + } + + final allOrderOnline = onlineOrderStreamAsync.value ?? []; + + if (allOrderOnline.isEmpty) { + return OnlineOrderEmpty.empty_database; + } + + final filteredOnlineOrder = filteredOnlineOrderAsync.value ?? []; + + if (onlineOrderSearchAsync.isNotEmpty && filteredOnlineOrder.isEmpty) { + return OnlineOrderEmpty.empty_search_result; + } + + return OnlineOrderEmpty.has_data; +}); + +final orderStatusByIdProvider = StreamProvider.family + .autoDispose((ref, orderId) { + final onlineStoreRepository = ref.watch(onlineStoreRepositoryProvider); + + return onlineStoreRepository.watchOrderStatusById(orderId); + }); + +final displayProductReviewStreamProvider = + StreamProvider.autoDispose>((ref) { + final onlineStoreRepository = ref.watch(onlineStoreRepositoryProvider); + return onlineStoreRepository.watchProductReviews(); + }); + +final reviewDetailStreamProvider = StreamProvider.family + .autoDispose((ref, productId) { + final onlineStoreRepository = ref.watch(onlineStoreRepositoryProvider); + return onlineStoreRepository.watchDetailProductReview(productId); + }); + +final productCommentStreamProvider = StreamProvider.family + .autoDispose, String>((ref, productId) { + final onlineStoreRepository = ref.watch(onlineStoreRepositoryProvider); + return onlineStoreRepository.watchProductComments(productId); + }); + +final campaignStreamProvider = + StreamProvider.autoDispose>((ref) { + final onlineStoreRepository = ref.watch(onlineStoreRepositoryProvider); + return onlineStoreRepository.watchCampaigns(); + }); + +final productCampaignStreamProvider = + StreamProvider.autoDispose>((ref) { + final onlineStoreRepository = ref.watch(onlineStoreRepositoryProvider); + return onlineStoreRepository.watchProductCampaigns(); + }); + +final productCampaignSearchProvider = StateProvider.autoDispose( + (ref) => '', +); + +final filteredProductCampaignStreamProvider = + Provider.autoDispose>>((ref) { + final productStreamAsync = ref.watch(productCampaignStreamProvider); + + final productSearchAsync = ref.watch(productCampaignSearchProvider); + + return productStreamAsync.when( + data: (products) { + if (productSearchAsync.trim().isEmpty) { + return AsyncValue.data(products); + } + + final filteredProducts = products.where((product) { + return product.name.toLowerCase().contains( + productSearchAsync.toLowerCase(), + ); + }).toList(); + + return AsyncValue.data(filteredProducts); + }, + error: (error, stackTrace) => AsyncValue.error(error, stackTrace), + loading: () => const AsyncValue.loading(), + ); + }); + +final productCampaignEmptyProvider = Provider.autoDispose( + (ref) { + final productStreamAsync = ref.watch(productCampaignStreamProvider); + + final filteredProductAsync = ref.watch( + filteredProductCampaignStreamProvider, + ); + + final productSearchAsync = ref.watch(productCampaignSearchProvider); + + if (productStreamAsync.isLoading) { + return ProductCampaignEmpty.loading; + } + + final allProducts = productStreamAsync.value ?? []; + + if (allProducts.isEmpty) { + return ProductCampaignEmpty.empty_database; + } + + final filteredProducts = filteredProductAsync.value ?? []; + + if (productSearchAsync.isNotEmpty && filteredProducts.isEmpty) { + return ProductCampaignEmpty.empty_search_result; + } + + return ProductCampaignEmpty.has_data; + }, +); + +final categoryCampaignStreamProvider = + StreamProvider.autoDispose>((ref) { + final onlineStoreRepository = ref.watch(onlineStoreRepositoryProvider); + return onlineStoreRepository.watchCategoryCampaigns(); + }); + +final categoryCampaignSearchProvider = StateProvider.autoDispose((ref) => ''); + +final filteredCategoryCampaignStreamProvider = + Provider.autoDispose>>((ref) { + final categoryStreamAsync = ref.watch(categoryCampaignStreamProvider); + + final categorySearchAsync = ref.watch(categoryCampaignSearchProvider); + + return categoryStreamAsync.when( + data: (categories) { + if (categorySearchAsync.trim().isEmpty) { + return AsyncValue.data(categories); + } + + final filteredCategories = categories.where((category) { + return category.name.toLowerCase().contains( + categorySearchAsync.toLowerCase(), + ); + }).toList(); + + return AsyncValue.data(filteredCategories); + }, + error: (error, stackTrace) => AsyncValue.error(error, stackTrace), + loading: () => const AsyncValue.loading(), + ); + }); + +final categoryCampaignEmptyProvider = + Provider.autoDispose((ref) { + final categoryStreamAsync = ref.watch(categoryStreamProvider); + + final filteredCategoryStreamAsync = ref.watch( + filteredCategoryCampaignStreamProvider, + ); + + final categorySearchAsync = ref.watch(categorySearchProvider); + + if (categoryStreamAsync.isLoading) { + return CategoryCampaignEmpty.loading; + } + + final allCategories = categoryStreamAsync.value ?? []; + + if (allCategories.isEmpty) { + return CategoryCampaignEmpty.empty_database; + } + + final filteredCategories = filteredCategoryStreamAsync.value ?? []; + + if (categorySearchAsync.isNotEmpty && filteredCategories.isEmpty) { + return CategoryCampaignEmpty.empty_search_result; + } + + return CategoryCampaignEmpty.has_data; + }); + +final selectedCampaignItemProvider = + StateProvider.autoDispose((ref) => null); + +final targetOutletStreamProvider = + StreamProvider.autoDispose>((ref) { + final onlineStoreRepository = ref.watch(onlineStoreRepositoryProvider); + return onlineStoreRepository.watchTargetOutlets(); + }); + +final selectedTargetOutletControllerProvider = + StateNotifierProvider.autoDispose< + SelectedTargetOutletController, + Map + >((ref) { + return SelectedTargetOutletController(); + }); diff --git a/Mobile Operasional/lib/features/online_store/presentation/screens/add_campaign_screen.dart b/Mobile Operasional/lib/features/online_store/presentation/screens/add_campaign_screen.dart new file mode 100644 index 0000000..d70fc42 --- /dev/null +++ b/Mobile Operasional/lib/features/online_store/presentation/screens/add_campaign_screen.dart @@ -0,0 +1,686 @@ +import 'dart:io'; + +import 'package:calendar_date_picker2/calendar_date_picker2.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; +import 'package:image_picker/image_picker.dart'; +import 'package:intl/intl.dart'; +import 'package:niogu_app/core/components/top_back_bar_app.dart'; +import 'package:niogu_app/core/constants/app_color.dart'; +import 'package:niogu_app/core/constants/app_font_size.dart'; +import 'package:niogu_app/core/router/app_route.dart'; +import 'package:niogu_app/core/enums/campaign_type.dart'; +import 'package:niogu_app/core/utils/image_service.dart'; +import 'package:niogu_app/core/utils/log_message.dart'; +import 'package:niogu_app/core/widgets/custom_snackbar.dart'; +import 'package:niogu_app/core/widgets/custom_text_form_field.dart'; +import 'package:niogu_app/features/online_store/domain/entities/online_store.dart'; +import 'package:niogu_app/features/online_store/presentation/providers/online_store_provider.dart'; +import 'package:niogu_app/features/online_store/presentation/widgets/information_campaign.dart'; +import 'package:niogu_app/features/online_store/presentation/widgets/section_campaign_label.dart'; +import 'package:niogu_app/core/widgets/custom_selector.dart'; +import 'package:niogu_app/features/report/presentation/widgets/filtered_date_range.dart'; +import 'package:sizer/sizer.dart'; + +class AddCampaignScreen extends ConsumerStatefulWidget { + const AddCampaignScreen({super.key}); + + @override + ConsumerState createState() => _AddCampaignScreenState(); +} + +class _AddCampaignScreenState extends ConsumerState { + final GlobalKey _formKey = GlobalKey(); + + final TextEditingController _nameController = TextEditingController(); + + final ImagePicker _picker = ImagePicker(); + + final List _imagePathTemps = []; + + String? _imagePath; + + List _initialValue = [ + DateTime.now(), + DateTime.now().add(const Duration(days: 7)), + ]; + + DateTimeRange? _selectedDateRange; + + @override + void initState() { + // TODO: implement initState + super.initState(); + } + + @override + void dispose() { + // TODO: implement dispose + _nameController.dispose(); + super.dispose(); + } + + String _formatGroupDate(DateTime date) { + return DateFormat('dd MMMM yyyy').format(date); + } + + Future _showPickerOptions() async { + final bool isTablet = 100.w >= 600; + showModalBottomSheet( + context: context, + backgroundColor: Colors.white, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.vertical(top: Radius.circular(2.5.w)), + ), + constraints: const BoxConstraints(maxWidth: double.infinity), + builder: (BuildContext context) { + return SafeArea( + child: Container( + width: 100.w, + padding: EdgeInsets.symmetric(vertical: 2.h, horizontal: 5.w), + clipBehavior: Clip.hardEdge, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.vertical(top: Radius.circular(6.w)), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + ListTile( + leading: Icon(Icons.photo_library, size: 6.w), + title: Text( + 'Galeri', + style: TextStyle( + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + ), + ), + onTap: () async { + await _getImage(ImageSource.gallery); + context.pop(); + }, + ), + + SizedBox(height: 2.h), + + ListTile( + leading: Icon(Icons.photo_camera, size: 6.w), + title: Text( + 'Kamera', + style: TextStyle( + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + ), + ), + onTap: () async { + await _getImage(ImageSource.camera); + context.pop(); + }, + ), + ], + ), + ), + ); + }, + ); + } + + Future _getImage(ImageSource ImageSource) async { + try { + final XFile? pickedFile = await _picker.pickImage( + source: ImageSource, + imageQuality: 70, + ); + + if (pickedFile != null) { + File tempFile = File(pickedFile.path); + + String? imagePath = await ImageService.saveImageToLocalDirectory( + tempFile, + "campaign_images", + ); + + if (imagePath != null) { + _imagePathTemps.add(imagePath); + setState(() { + _imagePath = imagePath; + }); + } + } + } catch (e, st) { + LogMessage.log.e(e.toString(), error: e, stackTrace: st); + CustomSnackbar.showWarning(context, "Akses ditolak"); + } + } + + Future _cleanUpImages() async { + for (final image in _imagePathTemps) { + if (image != null) { + await ImageService.deleteLocalImage(image); + } + } + } + + Future _showDateRangePicker() async { + final bool isTablet = 100.w >= 600; + + List tempSelectedDates = List.from(_initialValue); + + showModalBottomSheet( + context: context, + useRootNavigator: true, + isScrollControlled: true, + backgroundColor: Colors.transparent, + constraints: BoxConstraints(maxWidth: double.infinity), + builder: (context) { + return StatefulBuilder( + builder: (context, setModalState) { + return Container( + height: 80.h, + width: double.infinity, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.vertical( + top: Radius.circular(4.5.w), + ), + ), + constraints: BoxConstraints( + maxHeight: 80.h, + maxWidth: double.infinity, + ), + child: Column( + children: [ + SizedBox(height: 1.5.h), + Container( + width: 12.w, + height: 0.6.h, + decoration: BoxDecoration( + color: Colors.grey[300], + borderRadius: BorderRadius.circular(2.5.w), + ), + ), + Padding( + padding: EdgeInsets.symmetric(vertical: 2.h), + child: Text( + "Pilih Rentang Tanggal", + style: TextStyle( + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + fontWeight: FontWeight.bold, + ), + ), + ), + Divider(height: 1, color: Colors.grey[200]), + + Expanded( + child: CalendarDatePicker2( + config: CalendarDatePicker2Config( + calendarType: CalendarDatePicker2Type.range, + selectedDayHighlightColor: AppColor.primaryColor, + weekdayLabels: [ + 'Min', + 'Sen', + 'Sel', + 'Rab', + 'Kam', + 'Jum', + 'Sab', + ], + weekdayLabelTextStyle: TextStyle( + color: Colors.grey[600], + fontWeight: FontWeight.bold, + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + ), + controlsTextStyle: TextStyle( + color: Colors.black, + fontWeight: FontWeight.bold, + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + ), + centerAlignModePicker: true, + firstDayOfWeek: 1, + ), + value: tempSelectedDates, + onValueChanged: (dates) { + setModalState(() { + tempSelectedDates = dates; + }); + }, + ), + ), + + Container( + padding: EdgeInsets.all(5.w), + decoration: BoxDecoration( + color: Colors.white, + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.05), + blurRadius: 10, + offset: const Offset(0, -5), + ), + ], + ), + child: Row( + children: [ + Expanded( + child: ElevatedButton( + onPressed: () { + setState(() { + _selectedDateRange = null; + }); + context.pop(); + }, + style: ElevatedButton.styleFrom( + padding: EdgeInsets.symmetric(vertical: 1.5.h), + backgroundColor: Colors.grey.shade300, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(2.5.w), + ), + ), + child: Text( + "Atur Ulang", + style: TextStyle( + color: Colors.black, + fontWeight: FontWeight.bold, + fontSize: AppFontSize.medium.sp, + ), + ), + ), + ), + SizedBox(width: 3.w), + Expanded( + child: ElevatedButton( + onPressed: () => context.pop(), + style: ElevatedButton.styleFrom( + padding: EdgeInsets.symmetric(vertical: 1.5.h), + backgroundColor: Colors.grey.shade500, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(2.5.w), + ), + ), + child: Text( + "Batal", + style: TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + fontSize: AppFontSize.medium.sp, + ), + ), + ), + ), + SizedBox(width: 3.w), + Expanded( + child: ElevatedButton( + onPressed: tempSelectedDates.length < 2 + ? null + : () { + _initialValue = tempSelectedDates; + final startDate = tempSelectedDates[0]!; + + final endDate = tempSelectedDates[1]!; + + final dateRange = DateTimeRange( + start: startDate, + end: endDate, + ); + + setState(() { + _selectedDateRange = dateRange; + }); + context.pop(); + }, + style: ElevatedButton.styleFrom( + padding: EdgeInsets.symmetric(vertical: 1.5.h), + backgroundColor: AppColor.primaryColor, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(2.5.w), + ), + disabledBackgroundColor: Colors.grey.shade300, + ), + child: Text( + "Pilih", + style: TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + fontSize: AppFontSize.medium.sp, + ), + ), + ), + ), + ], + ), + ), + ], + ), + ); + }, + ); + }, + ); + } + + Future _saveCampaign() async { + if (_imagePath == null) { + CustomSnackbar.showError(context, "Tambahkan gambar campaign"); + return; + } + + if (!_formKey.currentState!.validate()) return; + + final selectedTargetOutlets = ref.read( + selectedTargetOutletControllerProvider, + ); + + if (selectedTargetOutlets.isEmpty) { + CustomSnackbar.showError(context, "Tentukan tujuan campaign"); + return; + } + + final selectedCampaignItem = ref.read(selectedCampaignItemProvider); + + final targetOutlets = selectedTargetOutlets.values.map((outlet) { + return UpsertTargetOutlet(outletId: outlet.outletId); + }).toList(); + + try { + await ref + .read(onlineStoreControllerProvider.notifier) + .saveCampaign( + campaign: UpsertCampaign( + imagePath: _imagePath!, + name: _nameController.text.trim(), + startDate: _selectedDateRange?.start, + endDate: _selectedDateRange?.end, + campaignType: selectedCampaignItem?.type, + linkId: selectedCampaignItem?.id, + targetOutlets: targetOutlets, + ), + ); + + if (!mounted) return; + + CustomSnackbar.showSuccess(context, "Campaign baru berhasil dibuat"); + context.pop(); + } catch (e, st) { + LogMessage.log.e(e.toString(), error: e, stackTrace: st); + CustomSnackbar.showError(context, "Ups, terjadi kesalahan"); + } + } + + @override + Widget build(BuildContext context) { + return LayoutBuilder( + builder: (context, constraints) { + final bool isTablet = 100.w >= 600; + + final File imageFile = File(_imagePath ?? "image not found"); + + final bool imageFileExists = imageFile.existsSync(); + + final selectedItemCampaignState = ref.watch( + selectedCampaignItemProvider, + ); + + final type = selectedItemCampaignState?.type == CampaignType.product + ? "Produk" + : "Kategori"; + + final linkName = "${selectedItemCampaignState?.name} ($type)"; + + String? targetName; + + final selectedTargetOutletControllerState = ref.watch( + selectedTargetOutletControllerProvider, + ); + + if (selectedTargetOutletControllerState.isNotEmpty) { + if (selectedTargetOutletControllerState.values.length > 1) { + targetName = + selectedTargetOutletControllerState.values.first.name + + " +${selectedTargetOutletControllerState.values.length - 1} lainnya"; + } else { + targetName = selectedTargetOutletControllerState.values.first.name; + } + } + + final onlineStoreControllerState = ref.watch( + onlineStoreControllerProvider, + ); + + return PopScope( + canPop: false, + onPopInvokedWithResult: (didPop, result) async { + if (didPop) return; + + await _cleanUpImages(); + + context.pop(); + }, + child: SafeArea( + top: false, + bottom: true, + right: false, + left: false, + child: Scaffold( + backgroundColor: const Color(0xFFF9FAFB), + appBar: TopBackBarApp( + title: "Buat Campaign Baru", + onTap: () async { + await _cleanUpImages(); + + context.pop(); + }, + ), + body: SingleChildScrollView( + padding: EdgeInsets.all(5.w), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SectionCampaignLabel( + isTablet: isTablet, + label: "Banner Campaign (Landscape)", + ), + Stack( + clipBehavior: Clip.none, + children: [ + Container( + height: 20.h, + width: double.infinity, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(3.w), + border: Border.all( + color: Colors.grey.shade300, + style: BorderStyle.solid, + ), + ), + child: imageFileExists + ? ClipRRect( + borderRadius: BorderRadius.circular(2.5.w), + child: Image.file( + imageFile, + fit: BoxFit.cover, + width: double.infinity, + height: double.infinity, + errorBuilder: (context, error, stackTrace) { + return Icon( + Icons.error, + color: Colors.grey[400], + size: 10.w, + ); + }, + ), + ) + : GestureDetector( + onTap: _showPickerOptions, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.add_photo_alternate_outlined, + color: Colors.grey.shade700, + size: 10.w, + ), + + SizedBox(height: 0.75.h), + + Text( + "Ketuk untuk unggah banner", + style: TextStyle( + fontSize: isTablet + ? (AppFontSize.medium - 1.25).sp + : (AppFontSize.small - 1.25).sp, + color: Colors.grey.shade700, + ), + ), + ], + ), + ), + ), + + if (_imagePath != null) + Positioned( + top: 1.w, + right: 1.w, + child: Material( + color: Colors.transparent, + type: MaterialType.transparency, + child: InkWell( + onTap: () { + setState(() { + _imagePath = null; + }); + }, + child: Container( + padding: EdgeInsets.all(1.w), + decoration: BoxDecoration( + color: Colors.red[50], + shape: BoxShape.circle, + border: Border.all( + color: Colors.red.withOpacity(0.2), + ), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.1), + blurRadius: 2, + offset: Offset(0, 1), + ), + ], + ), + child: Icon( + Icons.remove, + color: Colors.red, + size: 4.w, + ), + ), + ), + ), + ), + ], + ), + SizedBox(height: 3.h), + + Form( + key: _formKey, + child: CustomTextFormField( + label: "Nama Campaign", + hint: "Contoh: Promo Gajian", + controller: _nameController, + validator: (value) { + if (value == null || value.isEmpty) { + return "Nama campaign belum diisi"; + } + + return null; + }, + ), + ), + SizedBox(height: 2.h), + InformationCampaign( + isTablet: isTablet, + information: + "Masa berlaku yang ditampikan di toko online (opsional)", + ), + FilteredDateRange( + value: _selectedDateRange == null + ? "Pilih Masa Durasi" + : "${_formatGroupDate(_selectedDateRange!.start)} - ${_formatGroupDate(_selectedDateRange!.end)}", + onTap: _showDateRangePicker, + ), + + SizedBox(height: 3.h), + + SectionCampaignLabel( + isTablet: isTablet, + label: "Tautkan Campaign", + ), + InformationCampaign( + isTablet: isTablet, + information: + "Tautkan ke kategori atau produk tertentu (opsional)", + ), + CustomSelector( + isTablet: isTablet, + title: selectedItemCampaignState == null + ? "Pilih Kategori Atau Produk" + : linkName, + onTap: () { + context.pushNamed(AppRoute.linkCampaignScreen); + }, + ), + SizedBox(height: 3.h), + + SectionCampaignLabel( + isTablet: isTablet, + label: "Tujuan Campaign", + ), + InformationCampaign( + isTablet: isTablet, + information: "Tentukan outlet yang menampilkan", + ), + CustomSelector( + isTablet: isTablet, + title: targetName == null ? "Pilih Outlet" : targetName, + onTap: () { + context.pushNamed(AppRoute.targetOutletScreen); + }, + ), + SizedBox(height: 2.h), + ElevatedButton( + onPressed: onlineStoreControllerState.isLoading + ? null + : _saveCampaign, + style: ElevatedButton.styleFrom( + backgroundColor: AppColor.primaryColor, + minimumSize: Size(double.infinity, 7.h), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(3.w), + ), + ), + child: Text( + "Simpan", + style: TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + fontSize: AppFontSize.medium.sp, + ), + ), + ), + ], + ), + ), + ), + ), + ); + }, + ); + } +} diff --git a/Mobile Operasional/lib/features/online_store/presentation/screens/campaign_screen.dart b/Mobile Operasional/lib/features/online_store/presentation/screens/campaign_screen.dart new file mode 100644 index 0000000..a592833 --- /dev/null +++ b/Mobile Operasional/lib/features/online_store/presentation/screens/campaign_screen.dart @@ -0,0 +1,255 @@ +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; +import 'package:intl/intl.dart'; +import 'package:niogu_app/core/components/top_back_bar_app.dart'; +import 'package:niogu_app/core/constants/app_color.dart'; +import 'package:niogu_app/core/constants/app_font_size.dart'; +import 'package:niogu_app/core/widgets/custom_error_screen.dart'; +import 'package:niogu_app/core/router/app_route.dart'; +import 'package:niogu_app/core/widgets/custom_empty_screen.dart'; +import 'package:niogu_app/features/online_store/domain/entities/online_store.dart'; +import 'package:niogu_app/features/online_store/presentation/providers/online_store_provider.dart'; +import 'package:sizer/sizer.dart'; + +class CampaignScreen extends ConsumerWidget { + const CampaignScreen({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + return LayoutBuilder( + builder: (context, constraints) { + final bool isTablet = 100.w >= 600; + + List campaigns = []; + + final campaignState = ref.watch(campaignStreamProvider); + + campaignState.whenData((c) => campaigns = c); + + return SafeArea( + top: false, + bottom: true, + right: false, + left: false, + child: Scaffold( + backgroundColor: const Color(0xFFF9FAFB), + appBar: TopBackBarApp( + title: "Daftar Campaign", + onTap: () => context.pop(), + ), + body: campaignState.when( + data: (campaigns) { + if (campaigns.isEmpty) { + return CustomEmptyScreen( + title: "Tidak Ada Campaign", + body: "Kamu belum membuat campaign", + textButton: "Buat Campaign Baru", + onPressed: () { + context.pushNamed(AppRoute.addCampaignScreen); + }, + ); + } + return ListView.builder( + padding: EdgeInsets.all(5.w), + itemCount: campaigns.length, + itemBuilder: (context, index) { + final campaign = campaigns[index]; + + final startDate = campaign.startDate; + + final endDate = campaign.endDate; + + final bool imagePathAvailable = + campaign.imagePath.isNotEmpty; + + final File imageFile = File( + imagePathAvailable + ? campaign.imagePath + : "image not found", + ); + + final bool imageFileExists = imageFile.existsSync(); + + return GestureDetector( + onTap: () { + context.pushNamed( + AppRoute.editCampaignScreen, + pathParameters: {"id": campaign.id}, + ); + }, + child: Container( + margin: EdgeInsets.only(bottom: 2.5.h), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(3.w), + border: Border.all(color: Colors.grey.shade200), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.02), + blurRadius: 10, + ), + ], + ), + child: Column( + children: [ + Container( + height: 18.h, + decoration: BoxDecoration( + color: Colors.grey[100], + borderRadius: BorderRadius.vertical( + top: Radius.circular(3.w), + ), + ), + child: Center( + child: imageFileExists + ? ClipRRect( + borderRadius: BorderRadius.circular( + 2.5.w, + ), + child: Image.file( + imageFile, + fit: BoxFit.cover, + width: double.infinity, + height: double.infinity, + errorBuilder: + (context, error, stackTrace) { + return Icon( + Icons.error, + color: Colors.grey[400], + size: 10.w, + ); + }, + ), + ) + : Icon( + Icons.image_outlined, + size: 10.w, + color: Colors.grey[400], + ), + ), + ), + Padding( + padding: EdgeInsets.all(4.w), + child: Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text( + campaign.name, + maxLines: 2, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + ), + ), + + SizedBox(height: 0.75.h), + + Text( + startDate != null && endDate != null + ? "${DateFormat('dd MMM').format(startDate)} - ${DateFormat('dd MMM yyyy').format(endDate)}" + : "Selalu di tampilkan", + style: TextStyle( + color: Colors.grey.shade700, + fontSize: isTablet + ? (AppFontSize.medium - 1.25).sp + : (AppFontSize.small - 1.25).sp, + ), + ), + + SizedBox(height: 0.75.h), + + Text( + "${campaign.outletUseAmount} Outlet menampilkan", + style: TextStyle( + color: Colors.grey.shade700, + fontSize: isTablet + ? (AppFontSize.medium - 1.25).sp + : (AppFontSize.small - 1.25).sp, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + Container( + padding: EdgeInsets.symmetric( + horizontal: 3.w, + vertical: 0.5.h, + ), + decoration: BoxDecoration( + color: campaign.isActive + ? Colors.green.withOpacity(0.1) + : Colors.grey.withOpacity(0.1), + borderRadius: BorderRadius.circular(5.w), + ), + child: Text( + campaign.isActive ? "Aktif" : "Berakhir", + style: TextStyle( + color: campaign.isActive + ? Colors.green + : Colors.grey, + fontWeight: FontWeight.bold, + fontSize: isTablet + ? (AppFontSize.medium - 1.25).sp + : (AppFontSize.small - 1.25).sp, + ), + ), + ), + ], + ), + ), + ], + ), + ), + ); + }, + ); + }, + error: (error, stackTrace) => CustomErrorScreen( + message: "Ups, terjadi kesalahan", + onRefresh: () {}, + ), + loading: () => const SizedBox(), + ), + bottomNavigationBar: campaigns.isEmpty || campaignState.isLoading + ? null + : Container( + padding: EdgeInsets.all(5.w), + child: ElevatedButton( + onPressed: () { + context.pushNamed(AppRoute.addCampaignScreen); + }, + style: ElevatedButton.styleFrom( + backgroundColor: AppColor.primaryColor, + minimumSize: Size(double.infinity, 6.5.h), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(3.w), + ), + ), + child: Text( + "Buat Campaign Baru", + style: TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + fontSize: AppFontSize.medium.sp, + ), + ), + ), + ), + ), + ); + }, + ); + } +} diff --git a/Mobile Operasional/lib/features/online_store/presentation/screens/category_campaign_screen.dart b/Mobile Operasional/lib/features/online_store/presentation/screens/category_campaign_screen.dart new file mode 100644 index 0000000..2fcc192 --- /dev/null +++ b/Mobile Operasional/lib/features/online_store/presentation/screens/category_campaign_screen.dart @@ -0,0 +1,81 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:niogu_app/core/widgets/custom_error_screen.dart'; +import 'package:niogu_app/core/enums/campaign_type.dart'; +import 'package:niogu_app/core/widgets/custom_empty_screen.dart'; +import 'package:niogu_app/features/online_store/domain/entities/online_store.dart'; +import 'package:niogu_app/features/online_store/presentation/providers/online_store_provider.dart'; +import 'package:niogu_app/core/widgets/custom_selectable_item.dart'; +import 'package:sizer/sizer.dart'; + +class CategoryCampaignScreen extends ConsumerStatefulWidget { + const CategoryCampaignScreen({super.key}); + + @override + ConsumerState createState() => + _CategoryCampaignScreenState(); +} + +class _CategoryCampaignScreenState extends ConsumerState + with AutomaticKeepAliveClientMixin { + @override + // TODO: implement wantKeepAlive + bool get wantKeepAlive => true; + + @override + Widget build(BuildContext context) { + super.build(context); + + final filteredCategoryState = ref.watch( + filteredCategoryCampaignStreamProvider, + ); + + final categoryEmptyState = ref.watch(categoryCampaignEmptyProvider); + + final selectedCampaignItem = ref.watch(selectedCampaignItemProvider); + + return filteredCategoryState.when( + data: (categories) { + switch (categoryEmptyState) { + case CategoryCampaignEmpty.loading: + return const SizedBox(); + case CategoryCampaignEmpty.empty_database: + return CustomEmptyScreen( + title: "Tidak Ada Kategori", + body: "Kamu belum memiliki kategori", + ); + case CategoryCampaignEmpty.empty_search_result: + return const CustomEmptyScreen(body: "Kategori Tidak Ditemukan"); + case CategoryCampaignEmpty.has_data: + return ListView.builder( + padding: EdgeInsets.all(4.w), + itemCount: categories.length, + itemBuilder: (context, index) { + final category = categories[index]; + + return CustomSelectableItem( + id: category.id, + title: category.name, + subtitle: "Total ${category.productUseAmount} produk", + imagePath: category.imagePath, + isSelected: selectedCampaignItem?.id == category.id, + onTap: () { + ref + .read(selectedCampaignItemProvider.notifier) + .state = SelectedItemCampaign( + id: category.id, + name: category.name, + type: CampaignType.category, + ); + }, + ); + }, + ); + } + }, + error: (error, stackTrace) => + CustomErrorScreen(message: "Ups, terjadi kesalahan", onRefresh: () {}), + loading: () => const SizedBox(), + ); + } +} diff --git a/Mobile Operasional/lib/features/online_store/presentation/screens/configure_online_store_screen.dart b/Mobile Operasional/lib/features/online_store/presentation/screens/configure_online_store_screen.dart new file mode 100644 index 0000000..d35163b --- /dev/null +++ b/Mobile Operasional/lib/features/online_store/presentation/screens/configure_online_store_screen.dart @@ -0,0 +1,382 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; +import 'package:niogu_app/core/components/top_back_bar_app.dart'; +import 'package:niogu_app/core/constants/app_color.dart'; +import 'package:niogu_app/core/constants/app_font_size.dart'; +import 'package:niogu_app/core/router/app_route.dart'; +import 'package:niogu_app/core/utils/log_message.dart'; +import 'package:niogu_app/core/widgets/custom_selector.dart'; +import 'package:niogu_app/core/widgets/custom_snackbar.dart'; +import 'package:niogu_app/core/widgets/custom_text_form_field.dart'; +import 'package:niogu_app/core/widgets/custom_time_picker.dart'; +import 'package:niogu_app/features/online_store/domain/entities/online_store.dart'; +import 'package:niogu_app/features/online_store/presentation/providers/online_store_provider.dart'; +import 'package:sizer/sizer.dart'; + +class ConfigureOnlineStoreScreen extends ConsumerStatefulWidget { + final ConfigureOnlineStore configure; + const ConfigureOnlineStoreScreen({super.key, required this.configure}); + + @override + ConsumerState createState() => + _ConfigureOnlineStoreScreenState(); +} + +class _ConfigureOnlineStoreScreenState + extends ConsumerState { + final TextEditingController _storeNameController = TextEditingController(); + + final TextEditingController _prepTimeController = TextEditingController(); + + final TextEditingController _gapTimeController = TextEditingController(); + + late TimeOfDay _onlineOpenTime; + + late TimeOfDay _onlineCloseTime; + + bool _isCloseService = false; + + @override + void initState() { + // TODO: implement initState + super.initState(); + + final configure = widget.configure; + + _storeNameController.text = configure.onlineStoreName; + + _onlineOpenTime = configure.onlineOpenTime.isEmpty + ? TimeOfDay(hour: 08, minute: 00) + : _parseTimeOfDay(configure.onlineOpenTime); + + _onlineCloseTime = configure.onlineCloseTime.isEmpty + ? TimeOfDay(hour: 20, minute: 00) + : _parseTimeOfDay(configure.onlineCloseTime); + + _isCloseService = configure.isCloseService; + + final Map selectedOnlineStoreProducts = { + for (final productId in configure.productIds) productId: productId, + }; + + if (configure.preparationTimeMinutes > 0) { + _prepTimeController.text = "${configure.preparationTimeMinutes}"; + } + + if (configure.orderIntervalMinutes > 0) { + _gapTimeController.text = "${configure.orderIntervalMinutes}"; + } + + WidgetsBinding.instance.addPostFrameCallback((_) { + ref.read(selectedOnlineStoreProductProvider.notifier).state = + selectedOnlineStoreProducts; + }); + } + + @override + void dispose() { + // TODO: implement dispose + _storeNameController.dispose(); + _prepTimeController.dispose(); + _gapTimeController.dispose(); + super.dispose(); + } + + TimeOfDay _parseTimeOfDay(String timeString) { + List parts = timeString.split('.'); + + int hour = int.parse(parts[0]); + int minute = int.parse(parts[1]); + + return TimeOfDay(hour: hour, minute: minute); + } + + Future _selectTime(bool isStart) async { + final TimeOfDay? picked = await showTimePicker( + context: context, + initialTime: isStart ? _onlineOpenTime : _onlineCloseTime, + builder: (BuildContext context, Widget? child) { + return MediaQuery( + data: MediaQuery.of(context).copyWith(alwaysUse24HourFormat: true), + child: child!, + ); + }, + ); + + if (picked != null) { + setState(() { + if (isStart) { + _onlineOpenTime = picked; + } else { + _onlineCloseTime = picked; + } + }); + } + } + + Future _saveConfigureOnlineStore() async { + final onlineStoreName = _storeNameController.text.trim(); + + final preparationTimeMinutes = + int.tryParse(_prepTimeController.text.trim()) ?? 0; + + final orderIntervalMinutes = + int.tryParse(_gapTimeController.text.trim()) ?? 0; + + final productIds = ref + .read(selectedOnlineStoreProductProvider) + .values + .toList(); + + final configure = ConfigureOnlineStore( + onlineStoreName: onlineStoreName, + onlineOpenTime: _onlineOpenTime.format(context), + onlineCloseTime: _onlineCloseTime.format(context), + isCloseService: _isCloseService, + preparationTimeMinutes: preparationTimeMinutes, + orderIntervalMinutes: orderIntervalMinutes, + productIds: productIds, + ); + + try { + await ref + .read(onlineStoreControllerProvider.notifier) + .configureOnlineStore(configure); + + if (!mounted) return; + + CustomSnackbar.showSuccess(context, "Toko online berhasil diperbarui"); + context.pop(); + } catch (e, st) { + LogMessage.log.e(e.toString(), error: e, stackTrace: st); + CustomSnackbar.showError(context, 'Ups, terjadi kesalahan'); + } + } + + @override + Widget build(BuildContext context) { + return LayoutBuilder( + builder: (context, constraints) { + final bool isTablet = 100.w >= 600; + + final selectedOnlineStoreProductState = ref.watch( + selectedOnlineStoreProductProvider, + ); + + final onlineStoreControllerState = ref.watch( + onlineStoreControllerProvider, + ); + + return SafeArea( + top: false, + bottom: true, + right: false, + left: false, + child: Scaffold( + backgroundColor: const Color(0xFFF9FAFB), + appBar: TopBackBarApp( + title: "Atur Toko Online", + onTap: () => context.pop(), + ), + body: SingleChildScrollView( + padding: EdgeInsets.all(5.w), + child: Column( + children: [ + CustomTextFormField( + label: "Nama Toko Online", + hint: "Contoh : Kopi Senja", + prefixIcon: Icons.store_outlined, + controller: _storeNameController, + validator: (value) { + if (value == null || value.isEmpty) { + return "Nama toko online belum diisi"; + } + + return null; + }, + ), + + SizedBox(height: 3.h), + + Align( + alignment: Alignment.centerLeft, + child: Padding( + padding: EdgeInsets.only(bottom: 1.5.h, left: 1.w), + child: Text( + "Jam Layanan Toko Online", + style: TextStyle( + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + fontWeight: FontWeight.bold, + color: Colors.black87, + ), + ), + ), + ), + + Container( + padding: EdgeInsets.all(4.w), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(3.w), + border: Border.all(color: Colors.grey.shade200), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Expanded( + child: CustomTimePicker( + label: "Jam Buka", + time: _onlineOpenTime, + onTap: () => _selectTime(true), + isEnabled: !_isCloseService, + ), + ), + Padding( + padding: EdgeInsets.symmetric(horizontal: 4.w), + child: Icon( + Icons.arrow_forward, + color: Colors.grey.shade400, + size: 5.w, + ), + ), + Expanded( + child: CustomTimePicker( + label: "Jam Tutup", + time: _onlineCloseTime, + onTap: () => _selectTime(false), + isEnabled: !_isCloseService, + ), + ), + ], + ), + + SizedBox(height: 0.75.h), + + Divider(color: Colors.grey.withOpacity(0.15)), + + Text( + "Tutup Layanan Toko Online Sekarang", + style: TextStyle( + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + fontWeight: FontWeight.bold, + color: Colors.black87, + ), + ), + + SizedBox(height: 0.75.h), + + SizedBox( + width: 10.w, + child: FittedBox( + fit: BoxFit.fill, + child: Switch( + value: _isCloseService, + onChanged: (_) { + setState(() { + _isCloseService = !_isCloseService; + }); + }, + activeColor: _isCloseService + ? AppColor.primaryColor + : Colors.grey, + ), + ), + ), + ], + ), + ), + + SizedBox(height: 3.h), + + CustomSelector( + isTablet: isTablet, + title: selectedOnlineStoreProductState.values.isEmpty + ? "Atur produk yang tampil di toko online" + : "Lihat ${selectedOnlineStoreProductState.values.length} produk toko online", + padding: EdgeInsets.symmetric( + horizontal: 4.w, + vertical: 2.h, + ), + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + onTap: () { + context.pushNamed(AppRoute.onlineStoreProductScreen); + }, + ), + + SizedBox(height: 2.h), + + CustomTextFormField( + label: "Waktu Mempersiapakan Pesanan (Menit)", + hint: "Contoh: 15", + suffixText: "Menit", + controller: _prepTimeController, + keyboardType: TextInputType.number, + validator: (value) { + if (value != null && value.isNotEmpty) { + if (int.tryParse(value) == null) { + return "Waktu persiapan tidak valid"; + } + } + return null; + }, + ), + + SizedBox(height: 2.h), + + CustomTextFormField( + label: "Jeda Waktu Pesanan Selanjutnya (Menit)", + hint: "Contoh: 15", + suffixText: "Menit", + controller: _gapTimeController, + keyboardType: TextInputType.number, + validator: (value) { + if (value != null && value.isNotEmpty) { + if (int.tryParse(value) == null) { + return "Jeda waktu tidak valid"; + } + } + return null; + }, + ), + + SizedBox(height: 5.h), + + ElevatedButton( + onPressed: onlineStoreControllerState.isLoading + ? null + : _saveConfigureOnlineStore, + style: ElevatedButton.styleFrom( + backgroundColor: AppColor.primaryColor, + minimumSize: Size(double.infinity, 7.h), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(3.w), + ), + disabledBackgroundColor: Colors.grey.shade300, + ), + child: Text( + "Simpan Perubahan", + style: TextStyle( + fontSize: AppFontSize.medium.sp, + fontWeight: FontWeight.bold, + color: Colors.white, + ), + ), + ), + ], + ), + ), + ), + ); + }, + ); + } +} diff --git a/Mobile Operasional/lib/features/online_store/presentation/screens/detail_product_review_screen.dart b/Mobile Operasional/lib/features/online_store/presentation/screens/detail_product_review_screen.dart new file mode 100644 index 0000000..67b2ce8 --- /dev/null +++ b/Mobile Operasional/lib/features/online_store/presentation/screens/detail_product_review_screen.dart @@ -0,0 +1,578 @@ +import 'dart:io'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; +import 'package:intl/intl.dart'; +import 'package:niogu_app/core/components/top_back_bar_app.dart'; +import 'package:niogu_app/core/constants/app_color.dart'; +import 'package:niogu_app/core/constants/app_font_size.dart'; +import 'package:niogu_app/core/widgets/custom_error_screen.dart'; +import 'package:niogu_app/core/router/app_route.dart'; +import 'package:niogu_app/core/utils/extension_format.dart'; +import 'package:niogu_app/features/online_store/presentation/providers/online_store_provider.dart'; +import 'package:sizer/sizer.dart'; + +class DetailProductReviewScreen extends ConsumerWidget { + final String productId; + const DetailProductReviewScreen({super.key, required this.productId}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + return LayoutBuilder( + builder: (context, constraints) { + final bool isTablet = 100.w >= 600; + + final reviewDetailState = ref.watch( + reviewDetailStreamProvider(productId), + ); + + final productCommentState = ref.watch( + productCommentStreamProvider(productId), + ); + return SafeArea( + top: false, + bottom: true, + right: false, + left: false, + child: Scaffold( + backgroundColor: const Color(0xFFF9FAFB), + appBar: TopBackBarApp( + title: "Rincian Ulasan Produk", + onTap: () => context.pop(), + ), + body: SingleChildScrollView( + padding: EdgeInsets.symmetric(vertical: 3.h), + child: reviewDetailState.when( + data: (review) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Column( + children: [ + SingleChildScrollView( + scrollDirection: Axis.horizontal, + padding: EdgeInsets.symmetric(horizontal: 3.5.w), + child: Row( + children: [ + if (review.imagePaths.isEmpty) + ...[0, 1, 2].map((_) { + return Container( + width: 30.w, + height: 30.w, + margin: EdgeInsets.only(right: 3.w), + decoration: BoxDecoration( + color: Colors.grey[200], + borderRadius: BorderRadius.circular( + 3.w, + ), + ), + child: Icon( + Icons.image_outlined, + color: Colors.grey[400], + size: 8.w, + ), + ); + }).toList() + else ...[ + ...review.imagePaths.map((imagePath) { + final bool imagePathAvailable = + imagePath.isNotEmpty; + + final File imageFile = File( + imagePathAvailable + ? imagePath + : "image not found", + ); + + final bool imageFileExists = imageFile + .existsSync(); + + return Container( + width: 30.w, + height: 30.w, + margin: EdgeInsets.only(right: 2.w), + decoration: BoxDecoration( + color: Colors.grey[200], + borderRadius: BorderRadius.circular( + 3.w, + ), + ), + child: imageFileExists + ? ClipRRect( + borderRadius: + BorderRadius.circular(2.5.w), + child: Image.file( + imageFile, + fit: BoxFit.cover, + width: double.infinity, + height: double.infinity, + errorBuilder: + ( + context, + error, + stackTrace, + ) { + return Icon( + Icons.error, + color: Colors.grey[400], + size: 8.w, + ); + }, + ), + ) + : Icon( + Icons.image_outlined, + color: Colors.grey[400], + size: 8.w, + ), + ); + }).toList(), + + if (review.imagePaths.length < 3) + ...(review.imagePaths.length == 1 + ? [0, 1] + : [0]) + .map((_) { + return Container( + width: 30.w, + height: 30.w, + margin: EdgeInsets.only(right: 3.w), + decoration: BoxDecoration( + color: Colors.grey[200], + borderRadius: + BorderRadius.circular(3.w), + ), + child: Icon( + Icons.image_outlined, + color: Colors.grey[400], + size: 8.w, + ), + ); + }) + .toList(), + ], + ], + ), + ), + + SizedBox(height: 2.h), + Padding( + padding: EdgeInsets.symmetric(horizontal: 5.w), + child: Center( + child: Text( + review.name, + textAlign: TextAlign.center, + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + ), + ), + ), + ), + ], + ), + + SizedBox(height: 4.h), + + Container( + margin: EdgeInsets.symmetric(horizontal: 5.w), + padding: EdgeInsets.all(5.w), + decoration: _cardDecoration(), + child: Row( + children: [ + Column( + children: [ + Row( + children: [ + Icon( + Icons.star_rounded, + color: Colors.orange, + size: 10.w, + ), + Text( + review.averageRating.toRatingFormat(), + style: TextStyle( + fontSize: (AppFontSize.large + 6).sp, + fontWeight: FontWeight.bold, + color: Colors.black87, + ), + ), + ], + ), + SizedBox(height: 0.5.h), + Text( + "Dari ${review.totalRating} rating", + style: TextStyle( + fontSize: isTablet + ? (AppFontSize.medium - 1.25).sp + : (AppFontSize.small - 1.25).sp, + color: Colors.grey.shade700, + ), + ), + ], + ), + SizedBox(width: 8.w), + Expanded( + child: Column( + children: [ + _buildRatingBar( + isTablet, + 5, + review.rating.star5, + ), + _buildRatingBar( + isTablet, + 4, + review.rating.star4, + ), + _buildRatingBar( + isTablet, + 3, + review.rating.star3, + ), + _buildRatingBar( + isTablet, + 2, + review.rating.star2, + ), + _buildRatingBar( + isTablet, + 1, + review.rating.star1, + ), + ], + ), + ), + ], + ), + ), + + SizedBox(height: 4.h), + + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: EdgeInsets.symmetric(horizontal: 5.w), + child: Text( + "Rincian Penilaian Produk", + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: (AppFontSize.medium - 1.25).sp, + ), + ), + ), + SizedBox(height: 2.h), + SizedBox( + height: 20.h, + child: productCommentState.when( + data: (comments) { + final totalCommentCount = comments.length > 6 + ? 6 + : comments.length; + if (comments.isEmpty) { + return Container( + height: 15.h, + width: double.infinity, + margin: EdgeInsets.symmetric( + horizontal: 5.w, + ), + decoration: BoxDecoration( + color: Colors.grey[50], + borderRadius: BorderRadius.circular( + 2.5.w, + ), + border: Border.all( + color: Colors.grey.shade200, + style: BorderStyle.solid, + ), + ), + child: Column( + mainAxisAlignment: + MainAxisAlignment.center, + children: [ + Icon( + Icons.reviews_outlined, + color: Colors.grey[400], + size: 8.w, + ), + SizedBox(height: 1.h), + Text( + "Belum terdapat ulasan produk", + style: TextStyle( + fontSize: + (AppFontSize.medium - 1.25).sp, + color: Colors.grey[500], + ), + ), + ], + ), + ); + } + + return ListView.builder( + scrollDirection: Axis.horizontal, + padding: EdgeInsets.symmetric( + horizontal: 4.w, + ), + itemCount: totalCommentCount, + itemBuilder: (context, index) { + final comment = comments[index]; + + if (index == 5) + return GestureDetector( + onTap: () { + context.pushNamed( + AppRoute.fullProductReviewScreen, + pathParameters: {"id": productId}, + ); + }, + child: Container( + width: 35.w, + margin: EdgeInsets.symmetric( + horizontal: 1.5.w, + vertical: 0.5.h, + ), + decoration: _cardDecoration() + .copyWith( + color: AppColor.primaryColor + .withOpacity(0.05), + ), + child: Column( + mainAxisAlignment: + MainAxisAlignment.center, + children: [ + Icon( + Icons + .arrow_circle_right_outlined, + color: AppColor.primaryColor, + size: 8.w, + ), + SizedBox(height: 1.h), + Text( + "Lihat Ulasan\nLengkap", + textAlign: TextAlign.center, + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: + (AppFontSize.medium - + 1.25) + .sp, + color: AppColor.primaryColor, + ), + ), + ], + ), + ), + ); + return Container( + width: 65.w, + margin: EdgeInsets.symmetric( + horizontal: 1.5.w, + vertical: 0.5.h, + ), + padding: EdgeInsets.all(4.w), + decoration: _cardDecoration(), + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Text( + comment.name, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: isTablet + ? (AppFontSize.medium - + 1.25) + .sp + : (AppFontSize.small - + 1.25) + .sp, + ), + ), + Row( + children: [ + Icon( + Icons.star_rounded, + color: Colors.orange, + size: 3.5.w, + ), + Text( + comment.rating.toString(), + style: TextStyle( + fontSize: isTablet + ? (AppFontSize.medium - + 1.25) + .sp + : (AppFontSize.small - + 1.25) + .sp, + ), + ), + ], + ), + ], + ), + Divider( + height: 1.h, + color: Colors.grey.shade300, + ), + Text( + "${comment.variantName != null && comment.variantName!.isNotEmpty ? comment.variantName : ''}", + style: TextStyle( + color: Colors.black87, + fontWeight: FontWeight.w600, + fontSize: isTablet + ? (AppFontSize.medium - 1.25) + .sp + : (AppFontSize.small - 1.25) + .sp, + ), + ), + if (comment.comment != null && + comment.comment!.isNotEmpty) + Text( + comment.comment!, + maxLines: 2, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontSize: isTablet + ? (AppFontSize.medium - + 1.25) + .sp + : (AppFontSize.small - 1.25) + .sp, + color: Colors.grey[700], + ), + ) + else + Text( + "Tidak ada ulasan", + style: TextStyle( + fontSize: isTablet + ? (AppFontSize.medium - + 1.25) + .sp + : (AppFontSize.small - 1.25) + .sp, + color: Colors.grey[600], + ), + ), + Text( + "${DateFormat('dd MMMM yyyy').format(comment.date)}", + style: TextStyle( + fontSize: isTablet + ? (AppFontSize.medium - 1.25) + .sp + : (AppFontSize.small - 1.25) + .sp, + color: Colors.grey[500], + ), + ), + ], + ), + ); + }, + ); + }, + error: (error, stackTrace) => CustomErrorScreen( + message: "Ups, terjadi kesalahan 1", + onRefresh: () {}, + ), + loading: () => const SizedBox(), + ), + ), + ], + ), + + SizedBox(height: 3.h), + + Padding( + padding: EdgeInsets.symmetric(horizontal: 5.w), + child: Row( + children: [ + Icon( + Icons.favorite_rounded, + color: Colors.red, + size: 5.w, + ), + SizedBox(width: 2.w), + Text( + "${review.totalLike} Pelanggan menyukai produk ini", + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + ), + ), + ], + ), + ), + ], + ); + }, + error: (error, stackTrace) { + return CustomErrorScreen( + message: "Ups, terjadi kesalahan", + onRefresh: () {}, + ); + }, + loading: () => const SizedBox(), + ), + ), + ), + ); + }, + ); + } + + BoxDecoration _cardDecoration() => BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(3.w), + border: Border.all(color: Colors.grey.shade200), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.02), + blurRadius: 10, + offset: const Offset(0, 4), + ), + ], + ); + + Widget _buildRatingBar(bool isTablet, int star, double percent) { + return Padding( + padding: EdgeInsets.symmetric(vertical: 0.2.h), + child: Row( + children: [ + Text( + "$star", + style: TextStyle( + fontSize: isTablet ? AppFontSize.medium.sp : AppFontSize.small.sp, + color: Colors.grey, + ), + ), + SizedBox(width: 2.w), + Expanded( + child: LinearProgressIndicator( + value: percent, + backgroundColor: Colors.grey[200], + color: Colors.orange, + minHeight: 0.6.h, + borderRadius: BorderRadius.circular(1.w), + ), + ), + ], + ), + ); + } +} diff --git a/Mobile Operasional/lib/features/online_store/presentation/screens/edit_campaign_screen.dart b/Mobile Operasional/lib/features/online_store/presentation/screens/edit_campaign_screen.dart new file mode 100644 index 0000000..266deaa --- /dev/null +++ b/Mobile Operasional/lib/features/online_store/presentation/screens/edit_campaign_screen.dart @@ -0,0 +1,914 @@ +import 'dart:io'; + +import 'package:calendar_date_picker2/calendar_date_picker2.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; +import 'package:image_picker/image_picker.dart'; +import 'package:intl/intl.dart'; +import 'package:niogu_app/core/components/top_back_bar_app.dart'; +import 'package:niogu_app/core/constants/app_color.dart'; +import 'package:niogu_app/core/constants/app_font_size.dart'; +import 'package:niogu_app/core/router/app_route.dart'; +import 'package:niogu_app/core/enums/campaign_type.dart'; +import 'package:niogu_app/core/utils/image_service.dart'; +import 'package:niogu_app/core/utils/log_message.dart'; +import 'package:niogu_app/core/widgets/custom_confirmation.dart'; +import 'package:niogu_app/core/widgets/custom_snackbar.dart'; +import 'package:niogu_app/core/widgets/custom_text_form_field.dart'; +import 'package:niogu_app/features/online_store/domain/entities/online_store.dart'; +import 'package:niogu_app/features/online_store/presentation/providers/online_store_provider.dart'; +import 'package:niogu_app/features/online_store/presentation/widgets/information_campaign.dart'; +import 'package:niogu_app/features/online_store/presentation/widgets/section_campaign_label.dart'; +import 'package:niogu_app/core/widgets/custom_selector.dart'; +import 'package:niogu_app/features/report/presentation/widgets/filtered_date_range.dart'; +import 'package:sizer/sizer.dart'; + +class EditCampaignScreen extends ConsumerStatefulWidget { + final String campaignId; + const EditCampaignScreen({super.key, required this.campaignId}); + + @override + ConsumerState createState() => _EditCampaignScreenState(); +} + +class _EditCampaignScreenState extends ConsumerState { + final GlobalKey _formKey = GlobalKey(); + + final TextEditingController _nameController = TextEditingController(); + + final ImagePicker _picker = ImagePicker(); + + final List _imagePathTemps = []; + + bool _isActive = false; + + bool _isLoading = true; + + String? _imagePath; + + List _initialValue = [ + DateTime.now(), + DateTime.now().add(const Duration(days: 7)), + ]; + + Map _initialTargets = {}; + + DateTimeRange? _selectedDateRange; + + @override + void initState() { + // TODO: implement initState + super.initState(); + + WidgetsBinding.instance.addPostFrameCallback((_) { + _fetchCampaign(); + }); + } + + @override + void dispose() { + // TODO: implement dispose + _nameController.dispose(); + super.dispose(); + } + + Future _fetchCampaign() async { + try { + final campaignDetail = await ref + .read(onlineStoreRepositoryProvider) + .fetchCampaignById(widget.campaignId); + + _imagePath = campaignDetail.imagePath; + + _nameController.text = campaignDetail.name; + + if (campaignDetail.startDate != null && campaignDetail.endDate != null) { + _selectedDateRange = DateTimeRange( + start: campaignDetail.startDate!, + end: campaignDetail.endDate!, + ); + } + + _isActive = campaignDetail.isActive; + + final linkId = campaignDetail.linkId; + + final linkName = campaignDetail.linkName; + + final campaignType = campaignDetail.campaignType; + + if (linkId != null && linkName != null && campaignType != null) { + ref + .read(selectedCampaignItemProvider.notifier) + .state = SelectedItemCampaign( + id: campaignDetail.linkId!, + name: campaignDetail.linkName!, + type: campaignDetail.campaignType!, + ); + } + + ref + .read(selectedTargetOutletControllerProvider.notifier) + .hydrate(campaignDetail.targetOutlets); + + _initialTargets = ref.read(selectedTargetOutletControllerProvider); + + setState(() { + _isLoading = false; + }); + } catch (e, st) { + setState(() { + _isLoading = false; + }); + LogMessage.log.e(e.toString(), error: e, stackTrace: st); + CustomSnackbar.showError(context, "Ups, terjadi kesalahan"); + context.pop(); + } + } + + String _formatGroupDate(DateTime date) { + return DateFormat('dd MMMM yyyy').format(date); + } + + Future _showPickerOptions() async { + final bool isTablet = 100.w >= 600; + showModalBottomSheet( + context: context, + backgroundColor: Colors.white, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.vertical(top: Radius.circular(2.5.w)), + ), + constraints: const BoxConstraints(maxWidth: double.infinity), + builder: (BuildContext context) { + return SafeArea( + child: Container( + width: 100.w, + padding: EdgeInsets.symmetric(vertical: 2.h, horizontal: 5.w), + clipBehavior: Clip.hardEdge, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.vertical(top: Radius.circular(6.w)), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + ListTile( + leading: Icon(Icons.photo_library, size: 6.w), + title: Text( + 'Galeri', + style: TextStyle( + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + ), + ), + onTap: () async { + await _getImage(ImageSource.gallery); + context.pop(); + }, + ), + + SizedBox(height: 2.h), + + ListTile( + leading: Icon(Icons.photo_camera, size: 6.w), + title: Text( + 'Kamera', + style: TextStyle( + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + ), + ), + onTap: () async { + await _getImage(ImageSource.camera); + context.pop(); + }, + ), + ], + ), + ), + ); + }, + ); + } + + Future _getImage(ImageSource ImageSource) async { + try { + final XFile? pickedFile = await _picker.pickImage( + source: ImageSource, + imageQuality: 70, + ); + + if (pickedFile != null) { + File tempFile = File(pickedFile.path); + + String? imagePath = await ImageService.saveImageToLocalDirectory( + tempFile, + "campaign_images", + ); + + if (imagePath != null) { + _imagePathTemps.add(imagePath); + setState(() { + _imagePath = imagePath; + }); + } + } + } catch (e, st) { + LogMessage.log.e(e.toString(), error: e, stackTrace: st); + CustomSnackbar.showWarning(context, "Akses ditolak"); + } + } + + Future _cleanUpImages() async { + for (final image in _imagePathTemps) { + if (image != null) { + await ImageService.deleteLocalImage(image); + } + } + } + + Future _showDateRangePicker() async { + final bool isTablet = 100.w >= 600; + + List tempSelectedDates = List.from(_initialValue); + + showModalBottomSheet( + context: context, + useRootNavigator: true, + isScrollControlled: true, + backgroundColor: Colors.transparent, + constraints: BoxConstraints(maxWidth: double.infinity), + builder: (context) { + return StatefulBuilder( + builder: (context, setModalState) { + return Container( + height: 80.h, + width: double.infinity, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.vertical( + top: Radius.circular(4.5.w), + ), + ), + constraints: BoxConstraints( + maxHeight: 80.h, + maxWidth: double.infinity, + ), + child: Column( + children: [ + SizedBox(height: 1.5.h), + Container( + width: 12.w, + height: 0.6.h, + decoration: BoxDecoration( + color: Colors.grey[300], + borderRadius: BorderRadius.circular(2.5.w), + ), + ), + Padding( + padding: EdgeInsets.symmetric(vertical: 2.h), + child: Text( + "Pilih Rentang Tanggal", + style: TextStyle( + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + fontWeight: FontWeight.bold, + ), + ), + ), + Divider(height: 1, color: Colors.grey[200]), + + Expanded( + child: CalendarDatePicker2( + config: CalendarDatePicker2Config( + calendarType: CalendarDatePicker2Type.range, + selectedDayHighlightColor: AppColor.primaryColor, + weekdayLabels: [ + 'Min', + 'Sen', + 'Sel', + 'Rab', + 'Kam', + 'Jum', + 'Sab', + ], + weekdayLabelTextStyle: TextStyle( + color: Colors.grey[600], + fontWeight: FontWeight.bold, + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + ), + controlsTextStyle: TextStyle( + color: Colors.black, + fontWeight: FontWeight.bold, + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + ), + centerAlignModePicker: true, + firstDayOfWeek: 1, + ), + value: tempSelectedDates, + onValueChanged: (dates) { + setModalState(() { + tempSelectedDates = dates; + }); + }, + ), + ), + + Container( + padding: EdgeInsets.all(5.w), + decoration: BoxDecoration( + color: Colors.white, + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.05), + blurRadius: 10, + offset: const Offset(0, -5), + ), + ], + ), + child: Row( + children: [ + Expanded( + child: ElevatedButton( + onPressed: () { + setState(() { + _selectedDateRange = null; + }); + context.pop(); + }, + style: ElevatedButton.styleFrom( + padding: EdgeInsets.symmetric(vertical: 1.5.h), + backgroundColor: Colors.grey.shade300, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(2.5.w), + ), + ), + child: Text( + "Atur Ulang", + style: TextStyle( + color: Colors.black, + fontWeight: FontWeight.bold, + fontSize: AppFontSize.medium.sp, + ), + ), + ), + ), + SizedBox(width: 3.w), + Expanded( + child: ElevatedButton( + onPressed: () => context.pop(), + style: ElevatedButton.styleFrom( + padding: EdgeInsets.symmetric(vertical: 1.5.h), + backgroundColor: Colors.grey.shade500, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(2.5.w), + ), + ), + child: Text( + "Batal", + style: TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + fontSize: AppFontSize.medium.sp, + ), + ), + ), + ), + SizedBox(width: 3.w), + Expanded( + child: ElevatedButton( + onPressed: tempSelectedDates.length < 2 + ? null + : () { + _initialValue = tempSelectedDates; + final startDate = tempSelectedDates[0]!; + + final endDate = tempSelectedDates[1]!; + + final dateRange = DateTimeRange( + start: startDate, + end: endDate, + ); + + setState(() { + _selectedDateRange = dateRange; + }); + context.pop(); + }, + style: ElevatedButton.styleFrom( + padding: EdgeInsets.symmetric(vertical: 1.5.h), + backgroundColor: AppColor.primaryColor, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(2.5.w), + ), + disabledBackgroundColor: Colors.grey.shade300, + ), + child: Text( + "Pilih", + style: TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + fontSize: AppFontSize.medium.sp, + ), + ), + ), + ), + ], + ), + ), + ], + ), + ); + }, + ); + }, + ); + } + + Future _deleteCampaign(String id) async { + try { + await ref.read(onlineStoreControllerProvider.notifier).deleteCampaign(id); + + if (!mounted) return; + + CustomSnackbar.showSuccess(context, "Campaign berhasil dihapus"); + context.pop(); + } catch (e, st) { + LogMessage.log.e(e.toString(), error: e, stackTrace: st); + CustomSnackbar.showError(context, "Ups, terjadi kesalahan"); + } + } + + Future _saveCampaign() async { + if (_imagePath == null) { + CustomSnackbar.showError(context, "Tambahkan gambar campaign"); + return; + } + + if (!_formKey.currentState!.validate()) return; + + final selectedTargetOutlets = ref.read( + selectedTargetOutletControllerProvider, + ); + + if (selectedTargetOutlets.isEmpty) { + CustomSnackbar.showError(context, "Tentukan tujuan campaign"); + return; + } + + final selectedCampaignItem = ref.read(selectedCampaignItemProvider); + + for (final target in _initialTargets.entries) { + final key = target.key; + + final value = target.value; + + if (selectedTargetOutlets.containsKey(key)) { + selectedTargetOutlets[key]!.copyWith(id: value.id); + } + } + + final List deleteTargetOutlets = []; + + for (final target in _initialTargets.entries) { + final key = target.key; + + final value = target.value; + + final selectedTargetId = selectedTargetOutlets[key]?.id; + + if (selectedTargetId != value.id) { + deleteTargetOutlets.add(value.id!); + } + } + + final targetOutlets = selectedTargetOutlets.values.map((target) { + return UpsertTargetOutlet(id: target.id, outletId: target.outletId); + }).toList(); + + try { + final campaign = UpsertCampaign( + id: widget.campaignId, + imagePath: _imagePath!, + name: _nameController.text.trim(), + startDate: _selectedDateRange?.start, + endDate: _selectedDateRange?.end, + campaignType: selectedCampaignItem?.type, + linkId: selectedCampaignItem?.id, + targetOutlets: targetOutlets, + ); + + await ref + .read(onlineStoreControllerProvider.notifier) + .saveCampaign( + campaign: campaign, + deleteTargetOutlets: deleteTargetOutlets, + ); + + if (!mounted) return; + + CustomSnackbar.showSuccess(context, "Campaign baru berhasil dibuat"); + context.pop(); + } catch (e, st) { + LogMessage.log.e(e.toString(), error: e, stackTrace: st); + CustomSnackbar.showError(context, "Ups, terjadi kesalahan"); + } + } + + @override + Widget build(BuildContext context) { + return LayoutBuilder( + builder: (context, constraints) { + final bool isTablet = 100.w >= 600; + + final File imageFile = File(_imagePath ?? "image not found"); + + final bool imageFileExists = imageFile.existsSync(); + + final selectedItemCampaignState = ref.watch( + selectedCampaignItemProvider, + ); + + final type = selectedItemCampaignState?.type == CampaignType.product + ? "Produk" + : "Kategori"; + + final linkName = "${selectedItemCampaignState?.name} ($type)"; + + String? targetName; + + final selectedTargetOutletControllerState = ref.watch( + selectedTargetOutletControllerProvider, + ); + + if (selectedTargetOutletControllerState.isNotEmpty) { + if (selectedTargetOutletControllerState.values.length > 1) { + targetName = + selectedTargetOutletControllerState.values.first.name + + " +${selectedTargetOutletControllerState.values.length - 1} lainnya"; + } else { + targetName = selectedTargetOutletControllerState.values.first.name; + } + } + + final onlineStoreControllerState = ref.watch( + onlineStoreControllerProvider, + ); + + return PopScope( + canPop: false, + onPopInvokedWithResult: (didPop, result) async { + if (didPop) return; + + await _cleanUpImages(); + + context.pop(); + + WidgetsBinding.instance.addPostFrameCallback((_) { + ref + .read(selectedTargetOutletControllerProvider.notifier) + .restore(_initialTargets); + }); + }, + child: SafeArea( + top: false, + bottom: true, + right: false, + left: false, + child: Scaffold( + backgroundColor: const Color(0xFFF9FAFB), + appBar: TopBackBarApp( + title: "Edit Campaign", + onTap: () async { + await _cleanUpImages(); + + context.pop(); + + WidgetsBinding.instance.addPostFrameCallback((_) { + ref + .read(selectedTargetOutletControllerProvider.notifier) + .restore(_initialTargets); + }); + }, + ), + body: _isLoading + ? const SizedBox() + : SingleChildScrollView( + padding: EdgeInsets.all(5.w), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SectionCampaignLabel( + isTablet: isTablet, + label: "Banner Campaign (Landscape)", + ), + Stack( + clipBehavior: Clip.none, + children: [ + Container( + height: 20.h, + width: double.infinity, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(3.w), + border: Border.all( + color: Colors.grey.shade300, + style: BorderStyle.solid, + ), + ), + child: imageFileExists + ? ClipRRect( + borderRadius: BorderRadius.circular( + 2.5.w, + ), + child: Image.file( + imageFile, + fit: BoxFit.cover, + width: double.infinity, + height: double.infinity, + errorBuilder: + (context, error, stackTrace) { + return Icon( + Icons.error, + color: Colors.grey[400], + size: 10.w, + ); + }, + ), + ) + : GestureDetector( + onTap: _showPickerOptions, + child: Column( + mainAxisAlignment: + MainAxisAlignment.center, + children: [ + Icon( + Icons + .add_photo_alternate_outlined, + color: Colors.grey.shade700, + size: 10.w, + ), + + SizedBox(height: 0.75.h), + + Text( + "Ketuk untuk unggah banner", + style: TextStyle( + fontSize: isTablet + ? (AppFontSize.medium - + 1.25) + .sp + : (AppFontSize.small - 1.25) + .sp, + color: Colors.grey.shade700, + ), + ), + ], + ), + ), + ), + + if (_imagePath != null) + Positioned( + top: 1.w, + right: 1.w, + child: Material( + color: Colors.transparent, + type: MaterialType.transparency, + child: InkWell( + onTap: () { + setState(() { + _imagePath = null; + }); + }, + child: Container( + padding: EdgeInsets.all(1.w), + decoration: BoxDecoration( + color: Colors.red[50], + shape: BoxShape.circle, + border: Border.all( + color: Colors.red.withOpacity(0.2), + ), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity( + 0.1, + ), + blurRadius: 2, + offset: Offset(0, 1), + ), + ], + ), + child: Icon( + Icons.remove, + color: Colors.red, + size: 4.w, + ), + ), + ), + ), + ), + ], + ), + + SizedBox(height: 3.h), + + Form( + key: _formKey, + child: CustomTextFormField( + label: "Nama Campaign", + hint: "Contoh: Promo Gajian", + controller: _nameController, + validator: (value) { + if (value == null || value.isEmpty) { + return "Nama campaign belum diisi"; + } + + return null; + }, + ), + ), + SizedBox(height: 2.h), + InformationCampaign( + isTablet: isTablet, + information: + "Masa berlaku yang ditampikan di toko online (opsional)", + ), + FilteredDateRange( + value: _selectedDateRange == null + ? "Pilih Masa Durasi" + : "${_formatGroupDate(_selectedDateRange!.start)} - ${_formatGroupDate(_selectedDateRange!.end)}", + onTap: _showDateRangePicker, + ), + + SizedBox(height: 2.h), + + Container( + padding: EdgeInsets.symmetric( + vertical: 2.w, + horizontal: 4.w, + ), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(3.w), + border: Border.all(color: Colors.grey.shade300), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Status Campaign", + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + color: Colors.grey.shade700, + ), + ), + SizedBox( + width: 10.w, + child: FittedBox( + fit: BoxFit.fill, + child: Switch( + value: _isActive, + onChanged: (_) { + setState(() { + _isActive = !_isActive; + }); + }, + activeColor: _isActive + ? AppColor.primaryColor + : Colors.grey, + ), + ), + ), + ], + ), + ), + + SizedBox(height: 3.h), + + SectionCampaignLabel( + isTablet: isTablet, + label: "Tautkan Campaign", + ), + InformationCampaign( + isTablet: isTablet, + information: + "Tautkan ke kategori atau produk tertentu (opsional)", + ), + CustomSelector( + isTablet: isTablet, + title: selectedItemCampaignState == null + ? "Pilih Kategori Atau Produk" + : linkName, + onTap: () { + context.pushNamed(AppRoute.linkCampaignScreen); + }, + ), + SizedBox(height: 3.h), + + SectionCampaignLabel( + isTablet: isTablet, + label: "Tujuan Campaign", + ), + InformationCampaign( + isTablet: isTablet, + information: "Tentukan outlet yang menampilkan", + ), + CustomSelector( + isTablet: isTablet, + title: targetName == null + ? "Pilih Outlet" + : targetName, + onTap: () { + context.pushNamed(AppRoute.targetOutletScreen); + }, + ), + + SizedBox(height: 2.h), + Center( + child: Padding( + padding: EdgeInsets.symmetric(horizontal: 5.w), + child: TextButton.icon( + onPressed: () { + showDialog( + context: context, + builder: (context) => CustomConfirmation( + title: "Hapus Campaign", + body: + "Yakin ingin menghapus campaign ini?.Tindakan ini tidak dapat dibatalkan", + color: Color(0xFFFFA000), + icon: Icons.warning_amber_rounded, + onConfirm: + onlineStoreControllerState.isLoading + ? null + : () async => await _deleteCampaign( + widget.campaignId, + ), + ), + ); + }, + style: TextButton.styleFrom( + foregroundColor: Colors.red[700], + padding: EdgeInsets.symmetric( + vertical: 1.5.h, + horizontal: 4.w, + ), + ), + icon: Icon( + Icons.delete_outline_rounded, + size: 5.w, + ), + label: Text( + "Hapus Campaign Ini", + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: isTablet + ? (AppFontSize.medium - 1.25).sp + : (AppFontSize.small - 1.25).sp, + ), + ), + ), + ), + ), + SizedBox(height: 2.h), + ElevatedButton( + onPressed: onlineStoreControllerState.isLoading + ? null + : _saveCampaign, + style: ElevatedButton.styleFrom( + backgroundColor: AppColor.primaryColor, + minimumSize: Size(double.infinity, 7.h), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(3.w), + ), + ), + child: Text( + "Simpan", + style: TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + fontSize: AppFontSize.medium.sp, + ), + ), + ), + ], + ), + ), + ), + ), + ); + }, + ); + } +} diff --git a/Mobile Operasional/lib/features/online_store/presentation/screens/full_product_review_screen.dart b/Mobile Operasional/lib/features/online_store/presentation/screens/full_product_review_screen.dart new file mode 100644 index 0000000..ca5b90d --- /dev/null +++ b/Mobile Operasional/lib/features/online_store/presentation/screens/full_product_review_screen.dart @@ -0,0 +1,199 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; +import 'package:intl/intl.dart'; +import 'package:niogu_app/core/components/top_back_bar_app.dart'; +import 'package:niogu_app/core/constants/app_color.dart'; +import 'package:niogu_app/core/constants/app_font_size.dart'; +import 'package:niogu_app/core/widgets/custom_error_screen.dart'; +import 'package:niogu_app/features/online_store/presentation/providers/online_store_provider.dart'; +import 'package:sizer/sizer.dart'; + +class FullProductReviewScreen extends ConsumerStatefulWidget { + final String productId; + const FullProductReviewScreen({super.key, required this.productId}); + + @override + ConsumerState createState() => + _FullProductReviewScreenState(); +} + +class _FullProductReviewScreenState + extends ConsumerState { + final Map _expandedStates = {}; + + @override + Widget build(BuildContext context) { + return LayoutBuilder( + builder: (context, constraints) { + final bool isTablet = 100.w >= 600; + + final productCommentState = ref.watch( + productCommentStreamProvider(widget.productId), + ); + + return SafeArea( + top: false, + bottom: true, + right: false, + left: false, + child: Scaffold( + backgroundColor: Colors.white, + appBar: TopBackBarApp( + title: "Ulasan Lengkap Produk", + onTap: () => context.pop(), + ), + body: productCommentState.when( + data: (comments) { + return ListView.separated( + padding: EdgeInsets.symmetric(horizontal: 5.w, vertical: 2.h), + itemCount: comments.length, + separatorBuilder: (context, index) => + Divider(height: 4.h, color: Colors.grey.shade100), + itemBuilder: (context, index) { + final comment = comments[index]; + final bool isExpanded = _expandedStates[index] ?? false; + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + comment.name, + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: isTablet + ? (AppFontSize.medium - 1.25).sp + : (AppFontSize.small - 1.25).sp, + ), + ), + + SizedBox(height: 0.75.h), + + Text( + "${DateFormat('dd MMMM yyyy').format(comment.date)}", + style: TextStyle( + color: Colors.grey.shade700, + fontSize: isTablet + ? (AppFontSize.medium - 1.25).sp + : (AppFontSize.small - 1.25).sp, + ), + ), + ], + ), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "${comment.variantName != null && comment.variantName!.isNotEmpty ? comment.variantName : ''}", + style: TextStyle( + fontWeight: FontWeight.w600, + fontSize: isTablet + ? (AppFontSize.medium - 1.25).sp + : (AppFontSize.small - 1.25).sp, + ), + ), + + SizedBox(height: 0.75.h), + + Row( + children: List.generate( + 5, + (starIndex) => Icon( + Icons.star_rounded, + color: starIndex < comment.rating + ? Colors.orange + : Colors.grey.shade300, + size: 4.w, + ), + ), + ), + ], + ), + ], + ), + + SizedBox(height: 1.5.h), + + if (comment.comment != null && + comment.comment!.isNotEmpty) + Text( + comment.comment!, + maxLines: isExpanded ? null : 2, + overflow: isExpanded + ? TextOverflow.visible + : TextOverflow.ellipsis, + style: TextStyle( + fontSize: isTablet + ? (AppFontSize.medium - 1.5).sp + : (AppFontSize.small - 1.5).sp, + color: Colors.black87, + height: 1.5, + ), + ) + else + Text( + "Tidak ada ulasan", + style: TextStyle( + fontSize: isTablet + ? (AppFontSize.medium - 1.5).sp + : (AppFontSize.small - 1.5).sp, + color: Colors.black87, + height: 1.5, + ), + ), + + if (comment.comment != null && + comment.comment!.isNotEmpty) + GestureDetector( + onTap: () => setState( + () => _expandedStates[index] = !isExpanded, + ), + child: Padding( + padding: EdgeInsets.symmetric(vertical: 1.h), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + isExpanded ? "Tutup" : "Lihat Selengkapnya", + style: TextStyle( + color: AppColor.primaryColor, + fontSize: isTablet + ? (AppFontSize.medium - 1.25).sp + : (AppFontSize.small - 1.25).sp, + fontWeight: FontWeight.bold, + ), + ), + Icon( + isExpanded + ? Icons.keyboard_arrow_up + : Icons.keyboard_arrow_down, + color: AppColor.primaryColor, + size: 4.w, + ), + ], + ), + ), + ), + ], + ); + }, + ); + }, + error: (error, stackTrace) => CustomErrorScreen( + message: "Ups, terjadi kesalahan", + onRefresh: () {}, + ), + loading: () => const SizedBox(), + ), + ), + ); + }, + ); + } +} diff --git a/Mobile Operasional/lib/features/online_store/presentation/screens/link_campaign_screen.dart b/Mobile Operasional/lib/features/online_store/presentation/screens/link_campaign_screen.dart new file mode 100644 index 0000000..8317de2 --- /dev/null +++ b/Mobile Operasional/lib/features/online_store/presentation/screens/link_campaign_screen.dart @@ -0,0 +1,158 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; +import 'package:niogu_app/core/components/app_bar_with_tab.dart'; +import 'package:niogu_app/core/constants/app_color.dart'; +import 'package:niogu_app/core/constants/app_font_size.dart'; +import 'package:niogu_app/features/online_store/presentation/providers/online_store_provider.dart'; +import 'package:niogu_app/features/online_store/presentation/screens/category_campaign_screen.dart'; +import 'package:niogu_app/features/online_store/presentation/screens/product_campaign_screen.dart'; +import 'package:sizer/sizer.dart'; + +class LinkCampaignScreen extends ConsumerStatefulWidget { + const LinkCampaignScreen({super.key}); + + @override + ConsumerState createState() => _LinkCampaignScreenState(); +} + +class _LinkCampaignScreenState extends ConsumerState + with SingleTickerProviderStateMixin { + late TabController _tabController; + + final FocusNode _searchFocusNode = FocusNode(); + + Color _searchIconColor = Colors.grey; + + Timer? _debounce; + + int _tabIndex = 0; + + @override + void initState() { + super.initState(); + _tabController = TabController(length: 2, vsync: this); + + _tabController.addListener(_handleTabSelection); + + _searchFocusNode.addListener(() { + setState(() { + _searchIconColor = _searchFocusNode.hasFocus + ? Colors.black + : Colors.grey; + }); + }); + } + + @override + void dispose() { + // TODO: implement dispose + _searchFocusNode.dispose(); + _tabController.removeListener(_handleTabSelection); + _tabController.dispose(); + _debounce?.cancel(); + super.dispose(); + } + + void _handleTabSelection() { + if (_tabController.index != _tabIndex) { + setState(() { + _tabIndex = _tabController.index; + }); + } + } + + void _onSearchChanged(String value) { + if (_debounce?.isActive ?? false) _debounce?.cancel(); + _debounce = Timer(const Duration(milliseconds: 800), () { + if (_tabIndex == 0) { + ref.read(productCampaignSearchProvider.notifier).state = value; + } else { + ref.read(categoryCampaignSearchProvider.notifier).state = value; + } + }); + } + + @override + Widget build(BuildContext context) { + return LayoutBuilder( + builder: (context, constraints) { + final selectedCampaignItemState = ref.watch( + selectedCampaignItemProvider, + ); + return SafeArea( + top: false, + bottom: true, + right: false, + left: false, + child: Scaffold( + backgroundColor: const Color(0xFFF9FAFB), + appBar: AppBarWithTab( + hintText: + "Cari nama ${_tabIndex == 0 ? 'produk' : 'kategori'}...", + searchIconColor: _searchIconColor, + canPop: true, + onPop: () => context.pop(), + showNotifIcon: false, + searchFocusNode: _searchFocusNode, + tabController: _tabController, + onTap: (value) { + setState(() { + _tabIndex = value; + }); + }, + onSearchChanged: (value) => _onSearchChanged(value), + tabs: [ + Tab(text: "Produk", height: 10.h), + Tab(text: "Kategori", height: 10.h), + ], + ), + body: TabBarView( + controller: _tabController, + children: const [ + const ProductCampaignScreen(), + const CategoryCampaignScreen(), + ], + ), + bottomNavigationBar: Container( + padding: EdgeInsets.all(5.w), + decoration: BoxDecoration( + color: Colors.white, + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.05), + blurRadius: 10, + offset: const Offset(0, -5), + ), + ], + ), + child: ElevatedButton( + onPressed: selectedCampaignItemState == null + ? null + : () => context.pop(), + style: ElevatedButton.styleFrom( + backgroundColor: AppColor.primaryColor, + minimumSize: Size(double.infinity, 6.5.h), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(3.w), + ), + disabledBackgroundColor: Colors.grey[300], + ), + child: Text( + "Tautkan Sekarang", + style: TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + fontSize: AppFontSize.medium.sp, + ), + ), + ), + ), + ), + ); + }, + ); + } +} diff --git a/Mobile Operasional/lib/features/online_store/presentation/screens/online_store_order_detail_screen.dart b/Mobile Operasional/lib/features/online_store/presentation/screens/online_store_order_detail_screen.dart new file mode 100644 index 0000000..a8c55bb --- /dev/null +++ b/Mobile Operasional/lib/features/online_store/presentation/screens/online_store_order_detail_screen.dart @@ -0,0 +1,368 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; +import 'package:intl/intl.dart'; +import 'package:niogu_app/core/components/top_back_bar_app.dart'; +import 'package:niogu_app/core/constants/app_color.dart'; +import 'package:niogu_app/core/constants/app_font_size.dart'; +import 'package:niogu_app/core/enums/approval_process.dart'; +import 'package:niogu_app/core/enums/canceled_by.dart'; +import 'package:niogu_app/core/enums/delivery_type.dart'; +import 'package:niogu_app/core/utils/log_message.dart'; +import 'package:niogu_app/core/enums/order_status.dart'; +import 'package:niogu_app/core/utils/time_zone.dart'; +import 'package:niogu_app/core/widgets/custom_confirmation.dart'; +import 'package:niogu_app/core/widgets/custom_snackbar.dart'; +import 'package:niogu_app/features/online_store/domain/entities/online_store.dart'; +import 'package:niogu_app/features/online_store/presentation/providers/online_store_provider.dart'; +import 'package:niogu_app/features/online_store/presentation/widgets/delivery_info_card.dart'; +import 'package:niogu_app/features/online_store/presentation/widgets/order_header_status.dart'; +import 'package:niogu_app/features/online_store/presentation/widgets/payment_info.dart'; +import 'package:niogu_app/features/online_store/presentation/widgets/product_details.dart'; +import 'package:niogu_app/features/online_store/presentation/widgets/status_step_trigger.dart'; +import 'package:niogu_app/features/online_store/presentation/widgets/summary_cost_info.dart'; +import 'package:sizer/sizer.dart'; + +class OnlineStoreOrderDetailScreen extends ConsumerWidget { + final OnlineOrderDetail detail; + const OnlineStoreOrderDetailScreen({super.key, required this.detail}); + + void _showConfirmUpdateStatus( + BuildContext context, + WidgetRef ref, + bool isLoading, + String newStatus, + Color statusColor, + String orderId, + OrderStatus orderStatus, + ) { + final bool isTablet = 100.w >= 600; + showDialog( + context: context, + builder: (context) => CustomConfirmation( + title: "Konfirmasi Perubahan", + color: statusColor, + icon: Icons.info_outline_rounded, + onConfirm: isLoading + ? null + : () async => + await _updateOrder(context, ref, orderId, orderStatus), + richText: RichText( + textAlign: TextAlign.center, + text: TextSpan( + style: TextStyle( + fontSize: isTablet ? AppFontSize.medium.sp : AppFontSize.small.sp, + color: Colors.grey.shade700, + height: 1.5, + ), + children: [ + const TextSpan( + text: + "Apakah anda yakin ingin mengubah status pesanan ini menjadi ", + ), + TextSpan( + text: "$newStatus?. ", + style: TextStyle( + fontSize: isTablet + ? (AppFontSize.medium).sp + : (AppFontSize.small).sp, + fontWeight: FontWeight.bold, + color: statusColor, + ), + ), + const TextSpan( + text: "Tindakan ini tidak dapat mengembalikan ke status awal", + ), + ], + ), + ), + ), + ); + } + + String _parseOrderStatus(OrderStatus orderStatus) { + switch (orderStatus) { + case OrderStatus.pending_payment: + return "Verifikasi Pembayaran"; + case OrderStatus.pending_confirmation: + return "Tertunda"; + case OrderStatus.confirmed: + return "Dikonfirmasi"; + case OrderStatus.in_progress: + return "Pengiriman"; + case OrderStatus.completed: + return "Selesai"; + case OrderStatus.canceled: + return "Dibatalkan"; + } + } + + Future _updateOrder( + BuildContext context, + WidgetRef ref, + String orderId, + OrderStatus newStatus, { + bool cancellationAccepted = false, + ApprovalProcess? process, + }) async { + try { + await ref + .read(onlineStoreControllerProvider.notifier) + .updateOrder( + orderId: detail.id, + orderStatus: newStatus, + cancellationAccepted: cancellationAccepted, + process: process, + canceledBy: CanceledBy.user, + ); + + if (!context.mounted) return; + + if (process != null) { + CustomSnackbar.showSuccess( + context, + "Respon pembatalan berhasil dilakukan", + ); + } else { + CustomSnackbar.showSuccess(context, "Status pesanan berhasil di ubah"); + } + } catch (e, st) { + LogMessage.log.e(e.toString(), error: e, stackTrace: st); + } + } + + @override + Widget build(BuildContext context, WidgetRef ref) { + return LayoutBuilder( + builder: (context, constraints) { + final bool isTablet = 100.w >= 600; + + final orderStatusState = ref.watch(orderStatusByIdProvider(detail.id)); + + bool isCancellation = detail.isCancellation; + + bool cancellationAccepted = detail.cancellationAccepted; + + ApprovalProcess process = detail.process; + + OrderStatus status = detail.orderStatus; + + CanceledBy? canceledBy; + + orderStatusState.whenData((o) { + isCancellation = o.isCancellation; + cancellationAccepted = o.cancellationAccepted; + process = o.process; + status = o.orderStatus; + canceledBy = o.canceledBy; + }); + + final onlineStoreControllerState = ref.watch( + onlineStoreControllerProvider, + ); + + final bool isLoading = onlineStoreControllerState.isLoading; + + final orderStatus = _parseOrderStatus(status); + + final Color colorStatus = switch (status) { + OrderStatus.pending_payment => Colors.red, + OrderStatus.pending_confirmation => Colors.grey, + OrderStatus.confirmed => Colors.blue, + OrderStatus.in_progress => Colors.orange, + OrderStatus.completed => Colors.green, + OrderStatus.canceled => Colors.red, + }; + + final isRequestCancellation = isCancellation && !cancellationAccepted; + + return SafeArea( + child: Scaffold( + backgroundColor: const Color(0xFFF9FAFB), + appBar: TopBackBarApp( + title: "Detail Pesanan", + onTap: () => context.pop(), + ), + body: SingleChildScrollView( + padding: EdgeInsets.all(5.w), + child: Column( + children: [ + OrderHeaderStatus( + outletName: detail.outletName, + orderNumber: detail.orderNumber, + orderStatus: orderStatus, + colorStatus: colorStatus, + orderDate: + "${DateFormat('dd MMMM yyyy HH:mm').format(detail.orderDate)} ${TimeZone.getCurrentTimeZone()}", + notes: detail.notes, + isCancellation: isCancellation, + cancellationAccepted: cancellationAccepted, + process: process, + status: status, + canceledBy: canceledBy, + ), + + SizedBox(height: 3.h), + + DeliveryInfoCard( + deliveryType: detail.deliveryType, + customerName: detail.deliveryInfo.name, + phoneNumber: detail.deliveryInfo.phoneNumber, + fullAddress: detail.deliveryInfo.fullAddress, + preference: detail.deliveryInfo.preference, + ), + + SizedBox(height: 3.h), + + PaymentInfo( + paymentMethod: detail.paymentMethod, + paymentProof: detail.paymentProof, + ), + + SizedBox(height: 3.h), + + ProductDetails(items: detail.items), + + SizedBox(height: 3.h), + + SummaryCostInfo( + totalOrder: detail.totalOrder, + deliveryFee: detail.deliveryFee, + totalAmount: detail.totalAmount, + ), + + SizedBox(height: 3.h), + + if (status != OrderStatus.pending_payment && + (!isCancellation || process == ApprovalProcess.done)) + StatusStepTrigger( + isDelivery: detail.deliveryType == DeliveryType.delivery, + currentStatus: orderStatus, + onPressed: (value, color, orderStatus) => + _showConfirmUpdateStatus( + context, + ref, + isLoading, + value, + color, + detail.id, + orderStatus, + ), + ), + + if (status == OrderStatus.pending_payment || + (isRequestCancellation && + process == ApprovalProcess.waiting)) + ElevatedButton( + onPressed: () { + showDialog( + context: context, + builder: (context) => CustomConfirmation( + title: isRequestCancellation + ? "Respon Pembatalan" + : "Konfirmasi Pembayaran", + color: Color(0xFFFFA000), + icon: Icons.warning_amber, + onConfirm: isLoading + ? null + : () async { + final orderStatus = isRequestCancellation + ? OrderStatus.canceled + : OrderStatus.confirmed; + + await _updateOrder( + context, + ref, + detail.id, + orderStatus, + cancellationAccepted: true, + process: isRequestCancellation + ? ApprovalProcess.done + : null, + ); + }, + onCancel: isRequestCancellation + ? isLoading + ? null + : () async { + await _updateOrder( + context, + ref, + detail.id, + status, + cancellationAccepted: false, + process: isRequestCancellation + ? ApprovalProcess.done + : null, + ); + + context.pop(); + } + : null, + richText: RichText( + textAlign: TextAlign.center, + text: TextSpan( + style: TextStyle( + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + color: Colors.grey.shade700, + height: 1.5, + ), + children: [ + TextSpan( + text: isRequestCancellation + ? "Terima permintaan pembatalan pesanan?. Tindakan akan mengubah pesanan menjadi " + : "Pastikan pembayaran telah diterima. Tindakan ini akan mengubah pesanan menjadi ", + ), + TextSpan( + text: isRequestCancellation + ? "Dibatalkan " + : "Dikonfirmasi ", + style: TextStyle( + fontSize: isTablet + ? (AppFontSize.medium).sp + : (AppFontSize.small).sp, + fontWeight: FontWeight.bold, + color: isRequestCancellation + ? Colors.red + : Colors.blue, + ), + ), + const TextSpan( + text: + "dan tidak dapat diubah ke status awal", + ), + ], + ), + ), + ), + ); + }, + style: ElevatedButton.styleFrom( + backgroundColor: AppColor.primaryColor, + minimumSize: Size(double.infinity, 6.5.h), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(3.w), + ), + ), + child: Text( + isCancellation && !cancellationAccepted + ? "Response Pembatalan" + : "Verifikasi Pembayaran", + style: TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + fontSize: AppFontSize.medium.sp, + ), + ), + ), + ], + ), + ), + ), + ); + }, + ); + } +} diff --git a/Mobile Operasional/lib/features/online_store/presentation/screens/online_store_order_screen.dart b/Mobile Operasional/lib/features/online_store/presentation/screens/online_store_order_screen.dart new file mode 100644 index 0000000..4cc565c --- /dev/null +++ b/Mobile Operasional/lib/features/online_store/presentation/screens/online_store_order_screen.dart @@ -0,0 +1,698 @@ +import 'dart:async'; + +import 'package:calendar_date_picker2/calendar_date_picker2.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; +import 'package:intl/intl.dart'; +import 'package:niogu_app/core/components/top_back_bar_app.dart'; +import 'package:niogu_app/core/constants/app_color.dart'; +import 'package:niogu_app/core/constants/app_font_size.dart'; +import 'package:niogu_app/core/widgets/custom_error_screen.dart'; +import 'package:niogu_app/core/providers/app_provider.dart'; +import 'package:niogu_app/core/router/app_route.dart'; +import 'package:niogu_app/core/enums/approval_process.dart'; +import 'package:niogu_app/core/enums/order_status.dart'; +import 'package:niogu_app/core/enums/report_type.dart'; +import 'package:niogu_app/core/enums/user_role.dart'; +import 'package:niogu_app/core/widgets/custom_empty_screen.dart'; +import 'package:niogu_app/core/widgets/pop_up_notification.dart'; +import 'package:niogu_app/features/online_store/domain/entities/online_store.dart'; +import 'package:niogu_app/features/online_store/presentation/providers/online_store_provider.dart'; +import 'package:niogu_app/features/online_store/presentation/widgets/online_store_order_shimmer.dart'; +import 'package:niogu_app/features/online_store/presentation/widgets/order_card.dart'; +import 'package:niogu_app/features/outlets/presentation/providers/outlet_provider.dart'; +import 'package:niogu_app/features/report/presentation/providers/report_provider.dart'; +import 'package:niogu_app/features/report/presentation/widgets/filtered_date_range.dart'; +import 'package:niogu_app/features/report/presentation/widgets/filtered_outlet.dart'; +import 'package:niogu_app/features/report/presentation/widgets/fixed_outlet.dart'; +import 'package:niogu_app/features/report/transaction/presentation/widgets/online_order_header.dart'; +import 'package:niogu_app/features/report/transaction/presentation/widgets/sliver_header_delegate.dart'; +import 'package:niogu_app/features/stock_in/domain/entities/stock_in_transfer.dart'; +import 'package:sizer/sizer.dart'; + +class OnlineStoreOrdersScreen extends ConsumerStatefulWidget { + const OnlineStoreOrdersScreen({super.key}); + + @override + ConsumerState createState() => + _OnlineStoreOrdersScreenState(); +} + +class _OnlineStoreOrdersScreenState + extends ConsumerState { + final FocusNode _searchFocusNode = FocusNode(); + + Color _searchIconColor = Colors.grey; + + String _selectedStatus = "Semua"; + + final List _statuses = [ + "Semua", + "Verifikasi Pembayaran", + "Tertunda", + "Dikonfirmasi", + "Pengiriman", + "Selesai", + "Pembatalan", + "Dibatalkan", + ]; + + Timer? _debounce; + + String? _selectedOutlet; + + DateTimeRange? _selectedDateRange; + + List _initialValue = [ + DateTime.now().subtract(const Duration(days: 7)), + DateTime.now(), + ]; + + @override + void initState() { + // TODO: implement initState + super.initState(); + + WidgetsBinding.instance.addPostFrameCallback((_) { + final currentUserRole = ref.read(currentUserRoleProvider); + + if (currentUserRole == UserRole.admin) { + final currentOutletId = ref.read(currentOutletIdProvider); + ref + .read( + filteredByOutletProvider(ReportType.online_order).notifier, + ) + .state = + currentOutletId; + } + }); + } + + @override + void dispose() { + // TODO: implement dispose + _debounce?.cancel(); + super.dispose(); + } + + void _onSearchChanged(String value) { + if (_debounce?.isActive ?? false) _debounce?.cancel(); + _debounce = Timer(const Duration(milliseconds: 800), () { + ref.read(onlineOrderSearchProvider.notifier).state = value; + }); + } + + bool _isSameDay(DateTime d1, DateTime d2) { + return d1.year == d2.year && d1.month == d2.month && d1.day == d2.day; + } + + String _formatGroupDate(DateTime date) { + return DateFormat('dd MMMM yyyy').format(date); + } + + Future _showDateRangePicker() async { + final bool isTablet = 100.w >= 600; + + List tempSelectedDates = List.from(_initialValue); + + showModalBottomSheet( + context: context, + useRootNavigator: true, + isScrollControlled: true, + backgroundColor: Colors.transparent, + constraints: BoxConstraints(maxWidth: double.infinity), + builder: (context) { + return StatefulBuilder( + builder: (context, setModalState) { + return Container( + height: 80.h, + width: double.infinity, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.vertical( + top: Radius.circular(4.5.w), + ), + ), + constraints: BoxConstraints( + maxHeight: 80.h, + maxWidth: double.infinity, + ), + child: Column( + children: [ + SizedBox(height: 1.5.h), + Container( + width: 12.w, + height: 0.6.h, + decoration: BoxDecoration( + color: Colors.grey[300], + borderRadius: BorderRadius.circular(2.5.w), + ), + ), + Padding( + padding: EdgeInsets.symmetric(vertical: 2.h), + child: Text( + "Pilih Rentang Tanggal", + style: TextStyle( + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + fontWeight: FontWeight.bold, + ), + ), + ), + Divider(height: 1, color: Colors.grey[200]), + + Expanded( + child: CalendarDatePicker2( + config: CalendarDatePicker2Config( + calendarType: CalendarDatePicker2Type.range, + selectedDayHighlightColor: AppColor.primaryColor, + weekdayLabels: [ + 'Min', + 'Sen', + 'Sel', + 'Rab', + 'Kam', + 'Jum', + 'Sab', + ], + weekdayLabelTextStyle: TextStyle( + color: Colors.grey[600], + fontWeight: FontWeight.bold, + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + ), + controlsTextStyle: TextStyle( + color: Colors.black, + fontWeight: FontWeight.bold, + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + ), + centerAlignModePicker: true, + firstDayOfWeek: 1, + ), + value: tempSelectedDates, + onValueChanged: (dates) { + setModalState(() { + tempSelectedDates = dates; + }); + }, + ), + ), + + Container( + padding: EdgeInsets.all(5.w), + decoration: BoxDecoration( + color: Colors.white, + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.05), + blurRadius: 10, + offset: const Offset(0, -5), + ), + ], + ), + child: Row( + children: [ + Expanded( + child: OutlinedButton( + onPressed: () { + _selectedDateRange = null; + ref + .read( + filteredByDateTimeProvider( + ReportType.online_order, + ).notifier, + ) + .state = + null; + context.pop(); + }, + style: OutlinedButton.styleFrom( + padding: EdgeInsets.symmetric(vertical: 1.5.h), + side: BorderSide(color: Colors.grey), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(2.5.w), + ), + ), + child: Text( + "Atur Ulang", + style: TextStyle( + color: Colors.black, + fontWeight: FontWeight.bold, + fontSize: AppFontSize.medium.sp, + ), + ), + ), + ), + SizedBox(width: 3.w), + Expanded( + child: OutlinedButton( + onPressed: () => context.pop(), + style: OutlinedButton.styleFrom( + padding: EdgeInsets.symmetric(vertical: 1.5.h), + side: BorderSide(color: AppColor.primaryColor), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(2.5.w), + ), + ), + child: Text( + "Batal", + style: TextStyle( + color: AppColor.primaryColor, + fontWeight: FontWeight.bold, + fontSize: AppFontSize.medium.sp, + ), + ), + ), + ), + SizedBox(width: 3.w), + Expanded( + child: ElevatedButton( + onPressed: tempSelectedDates.length < 2 + ? null + : () { + _initialValue = tempSelectedDates; + final startDate = tempSelectedDates[0]!; + + final endDate = tempSelectedDates[1]!; + + final dateRange = DateTimeRange( + start: startDate, + end: endDate, + ); + + _selectedDateRange = dateRange; + + ref + .read( + filteredByDateTimeProvider( + ReportType.online_order, + ).notifier, + ) + .state = + dateRange; + + context.pop(); + }, + style: ElevatedButton.styleFrom( + padding: EdgeInsets.symmetric(vertical: 1.5.h), + backgroundColor: AppColor.primaryColor, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(2.5.w), + ), + disabledBackgroundColor: Colors.grey.shade300, + ), + child: Text( + "Pilih", + style: TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + fontSize: AppFontSize.medium.sp, + ), + ), + ), + ), + ], + ), + ), + ], + ), + ); + }, + ); + }, + ); + } + + void _onSelectedOutletChange(String? value) { + _selectedOutlet = value != '0' ? value : null; + ref.read(filteredByOutletProvider(ReportType.online_order).notifier).state = + _selectedOutlet; + } + + String _parseOrderStatus(OrderStatus? orderStatus) { + switch (orderStatus) { + case null: + return "Semua"; + case OrderStatus.pending_payment: + return "Verifikasi Pembayaran"; + case OrderStatus.pending_confirmation: + return "Tertunda"; + case OrderStatus.confirmed: + return "Dikonfirmasi"; + case OrderStatus.in_progress: + return "Pengiriman"; + case OrderStatus.completed: + return "Selesai"; + case OrderStatus.canceled: + return "Dibatalkan"; + } + } + + List _filteredOrders(List orders) { + return orders.where((order) { + if (_selectedStatus == 'Semua') { + return true; + } + + if (_selectedStatus == 'Pembatalan') { + if ((order.orderStatus == OrderStatus.confirmed || + order.orderStatus == OrderStatus.in_progress) && + order.isCancellation && + !order.cancellationAccepted && + order.process == ApprovalProcess.waiting) { + return true; + } + } + + return _selectedStatus == _parseOrderStatus(order.orderStatus); + }).toList(); + } + + @override + Widget build(BuildContext context) { + return LayoutBuilder( + builder: (context, constraints) { + final bool isTablet = 100.w >= 600; + + const greyColor = Color(0xFFF5F5F5); + + final messageActivity = ref.watch(messageActivityProvider); + + final currentUserRole = ref.watch(currentUserRoleProvider); + + final sourceOutletState = ref.watch(sourceOutletStreamProvider(true)); + + final List sourceOutlets = [ + SourceOutlet(id: "0", name: "Semua Outlet"), + ]; + + if (currentUserRole == UserRole.owner) { + sourceOutletState.whenData((outlets) { + sourceOutlets.addAll(outlets); + }); + } + + final currentOutletName = ref.watch(currentOutletNameProvider); + + final filteredOnlineOrderState = ref.watch(filteredOnlineOrderProvider); + + final orderOnlineEmptyState = ref.watch(onlineOrderEmptyProvider); + + final List onlineOrders = []; + + filteredOnlineOrderState.whenData( + (orders) => onlineOrders.addAll(orders), + ); + + final Map orderCounts = { + "Semua": onlineOrders.length, + "Verifikasi Pembayaran": onlineOrders + .where( + (order) => order.orderStatus == OrderStatus.pending_payment, + ) + .length, + "Tertunda": onlineOrders + .where( + (order) => + order.orderStatus == OrderStatus.pending_confirmation, + ) + .length, + "Dikonfirmasi": onlineOrders + .where((order) => order.orderStatus == OrderStatus.confirmed) + .length, + "Pengiriman": onlineOrders + .where((order) => order.orderStatus == OrderStatus.in_progress) + .length, + "Selesai": onlineOrders + .where((order) => order.orderStatus == OrderStatus.completed) + .length, + "Pembatalan": onlineOrders + .where( + (order) => + (order.orderStatus == OrderStatus.confirmed || + order.orderStatus == OrderStatus.in_progress) && + order.isCancellation && + !order.cancellationAccepted && + order.process == ApprovalProcess.waiting, + ) + .length, + "Dibatalkan": onlineOrders + .where((order) => order.orderStatus == OrderStatus.canceled) + .length, + }; + + return SafeArea( + top: false, + bottom: true, + right: false, + left: false, + child: Stack( + children: [ + Scaffold( + backgroundColor: const Color(0xFFF9FAFB), + appBar: TopBackBarApp( + title: "Pesanan Dari Toko Online", + onTap: () => context.pop(), + ), + body: CustomScrollView( + physics: const ClampingScrollPhysics(), + slivers: [ + SliverAppBar( + pinned: false, + floating: true, + snap: true, + expandedHeight: 26.h, + backgroundColor: Colors.white, + elevation: 0, + automaticallyImplyLeading: false, + flexibleSpace: FlexibleSpaceBar( + background: Material( + type: MaterialType.canvas, + color: Colors.white, + elevation: 0, + child: Padding( + padding: EdgeInsets.fromLTRB(5.w, 2.h, 5.w, 1.h), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + height: 6.h, + decoration: BoxDecoration( + color: greyColor, + borderRadius: BorderRadius.circular(2.5.w), + ), + child: TextField( + focusNode: _searchFocusNode, + onChanged: (value) => + _onSearchChanged(value), + textAlignVertical: TextAlignVertical.center, + style: TextStyle( + color: Colors.black87, + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + ), + decoration: InputDecoration( + hintText: + "Cari berdasarkan nomor pesanan", + hintStyle: TextStyle( + color: _searchIconColor, + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + ), + prefixIcon: Icon( + Icons.search, + color: _searchIconColor, + size: 5.w, + ), + border: InputBorder.none, + contentPadding: EdgeInsets.zero, + isDense: true, + ), + ), + ), + + SizedBox(height: 1.5.h), + + if (currentUserRole == UserRole.owner) + FilteredOutlet( + value: sourceOutletState.isLoading + ? '0' + : (_selectedOutlet ?? '0'), + sourceOutlets: sourceOutlets, + onChanged: _onSelectedOutletChange, + ) + else + FixedOutlet(value: currentOutletName!), + + SizedBox(height: 1.5.h), + + FilteredDateRange( + value: _selectedDateRange != null + ? "${_formatGroupDate(_selectedDateRange!.start)} - ${_formatGroupDate(_selectedDateRange!.end)}" + : "Filter Tanggal", + onTap: _showDateRangePicker, + ), + ], + ), + ), + ), + ), + ), + + SliverPersistentHeader( + pinned: true, + delegate: SliverHeaderDelegate( + child: OrderOnlineHeader( + statuses: _statuses, + selectedStatus: _selectedStatus, + orderCounts: orderCounts, + onTap: (value) { + setState(() { + _selectedStatus = value; + }); + }, + ), + value: _selectedStatus, + ), + ), + + filteredOnlineOrderState.when( + data: (onlineOrders) { + switch (orderOnlineEmptyState) { + case OnlineOrderEmpty.loading: + return const SliverToBoxAdapter( + child: const SizedBox(), + ); + case OnlineOrderEmpty.empty_database: + return const SliverToBoxAdapter( + child: CustomEmptyScreen( + title: "Tidak Ada Pesanan", + body: "Kamu belum memiliki pesanan", + ), + ); + case OnlineOrderEmpty.empty_search_result: + return const SliverToBoxAdapter( + child: CustomEmptyScreen( + body: "Pesanan Tidak Ditemukan", + ), + ); + case OnlineOrderEmpty.has_data: + final orders = _filteredOrders(onlineOrders); + + if (orders.isEmpty) { + return const SliverToBoxAdapter( + child: CustomEmptyScreen( + body: "Pesanan Tidak Ditemukan", + ), + ); + } + + return SliverPadding( + padding: EdgeInsets.symmetric( + horizontal: 5.w, + vertical: 2.h, + ), + sliver: SliverList( + delegate: SliverChildBuilderDelegate(( + context, + index, + ) { + final order = orders[index]; + + bool showHeader = true; + if (index > 0) { + final prevOrder = orders[index - 1]; + if (_isSameDay( + prevOrder.orderDate, + order.orderDate, + )) { + showHeader = false; + } + } + return Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + if (showHeader) + Padding( + padding: EdgeInsets.symmetric( + horizontal: 5.w, + vertical: index > 0 ? 1.5.h : 0, + ), + child: Text( + _formatGroupDate(order.orderDate), + style: TextStyle( + color: Colors.grey[500], + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + fontWeight: FontWeight.bold, + ), + ), + ), + OrderCard( + order: order, + onTap: () async { + final onlineOrderDetail = await ref + .read( + onlineStoreRepositoryProvider, + ) + .fetchOnlineOrderDetailById( + order.id, + ); + + await Future.delayed( + const Duration(milliseconds: 800), + () { + context.pushNamed( + AppRoute + .onlineStoreOrderDetilScreen, + extra: onlineOrderDetail, + ); + }, + ); + }, + ), + ], + ); + }, childCount: orders.length), + ), + ); + } + }, + error: (error, stackTrace) { + return SliverToBoxAdapter( + child: CustomErrorScreen( + message: "Ups,terjadi kesalahan", + onRefresh: () {}, + ), + ); + }, + loading: () => const SliverToBoxAdapter( + child: const OnlineStoreOrderShimmer(), + ), + ), + ], + ), + ), + + if (messageActivity != null) + PopupNotification( + isOwner: currentUserRole == UserRole.owner, + messages: messageActivity.messages, + type: messageActivity.type, + ), + ], + ), + ); + }, + ); + } +} diff --git a/Mobile Operasional/lib/features/online_store/presentation/screens/online_store_product_screen.dart b/Mobile Operasional/lib/features/online_store/presentation/screens/online_store_product_screen.dart new file mode 100644 index 0000000..bdfa8f2 --- /dev/null +++ b/Mobile Operasional/lib/features/online_store/presentation/screens/online_store_product_screen.dart @@ -0,0 +1,915 @@ +import 'dart:async'; +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; +import 'package:niogu_app/core/components/top_back_bar_app.dart'; +import 'package:niogu_app/core/constants/app_color.dart'; +import 'package:niogu_app/core/constants/app_font_size.dart'; +import 'package:niogu_app/core/widgets/custom_error_screen.dart'; +import 'package:niogu_app/core/router/app_route.dart'; +import 'package:niogu_app/core/utils/currency_format.dart'; +import 'package:niogu_app/core/utils/extension_format.dart'; +import 'package:niogu_app/core/enums/stock_type.dart'; +import 'package:niogu_app/core/widgets/custom_empty_screen.dart'; +import 'package:niogu_app/core/widgets/custom_snackbar.dart'; +import 'package:niogu_app/features/goods/presentation/widgets/custom_item_detail.dart'; +import 'package:niogu_app/features/goods/raw_materials/presentation/widgets/checkbox_product_card.dart'; +import 'package:niogu_app/features/online_store/domain/entities/online_store.dart'; +import 'package:niogu_app/features/online_store/presentation/providers/online_store_provider.dart'; +import 'package:niogu_app/features/online_store/presentation/widgets/online_store_product_shimmer.dart'; +import 'package:sizer/sizer.dart'; + +class OnlineStoreProductScreen extends ConsumerStatefulWidget { + const OnlineStoreProductScreen({super.key}); + + @override + ConsumerState createState() => + _OnlineStoreProductScreenState(); +} + +class _OnlineStoreProductScreenState + extends ConsumerState { + final FocusNode _searchFocusNode = FocusNode(); + + final GlobalKey _formKey = GlobalKey(); + + final Map _controllers = {}; + + Color _searchIconColor = Colors.grey; + + Timer? _debounce; + + Map _initialOnlineStoreProducts = {}; + + @override + void initState() { + // TODO: implement initState + super.initState(); + + _searchFocusNode.addListener(() { + setState(() { + _searchIconColor = _searchFocusNode.hasFocus + ? Colors.black + : Colors.grey; + }); + }); + + WidgetsBinding.instance.addPostFrameCallback((_) { + _initialOnlineStoreProducts = ref.read( + selectedOnlineStoreProductProvider, + ); + }); + } + + @override + void dispose() { + // TODO: implement dispose + _searchFocusNode.dispose(); + + for (final controller in _controllers.values) { + controller.dispose(); + } + + _debounce?.cancel(); + super.dispose(); + } + + void _onSearchChanged(String value) { + if (_debounce?.isActive ?? false) _debounce?.cancel(); + _debounce = Timer(const Duration(milliseconds: 800), () async { + ref.read(onlineStoreProductSearchProvider.notifier).state = value; + }); + } + + void _saveOnlineStoreProduct() { + final selectedOnlineStoreProduct = ref.read( + selectedOnlineStoreProductProvider, + ); + + if (selectedOnlineStoreProduct.values.isEmpty) { + CustomSnackbar.showWarning(context, "Pilih minimal satu produk"); + + return; + } + + CustomSnackbar.showSuccess( + context, + "${selectedOnlineStoreProduct.values.length} " + "produk berhasil dibuat", + ); + + context.pop(); + } + + @override + Widget build(BuildContext context) { + return LayoutBuilder( + builder: (context, constraints) { + final bool isTablet = 100.w >= 600; + + const backgroundColor = Color(0xFFF9FAFB); + + final filteredOnlineStoreProductState = ref.watch( + filteredOnlineStoreProductProvider, + ); + + final onlineStoreProductEmptyState = ref.watch( + onlineStoreProductEmptyProvider, + ); + + return SafeArea( + top: false, + bottom: true, + right: false, + left: false, + child: Scaffold( + backgroundColor: backgroundColor, + appBar: TopBackBarApp( + title: "Produk Toko Online", + onTap: () { + context.pop(); + + WidgetsBinding.instance.addPostFrameCallback((_) { + ref + .read(selectedOnlineStoreProductProvider.notifier) + .state = + _initialOnlineStoreProducts; + }); + }, + ), + body: Column( + children: [ + Container( + height: 12.h, + padding: EdgeInsets.fromLTRB(5.w, 3.h, 5.w, 2.h), + color: backgroundColor, + child: Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(2.5.w), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.03), + blurRadius: 10, + offset: const Offset(0, 4), + ), + ], + ), + child: TextField( + focusNode: _searchFocusNode, + onChanged: (value) => _onSearchChanged(value), + style: TextStyle( + color: Colors.black87, + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + ), + decoration: InputDecoration( + hintText: "Cari produk...", + hintStyle: TextStyle( + color: _searchIconColor, + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + ), + prefixIcon: Icon( + Icons.search, + color: Colors.grey, + size: 5.w, + ), + border: InputBorder.none, + contentPadding: EdgeInsets.symmetric( + horizontal: 4.w, + vertical: 1.5.h, + ), + ), + ), + ), + ), + Expanded( + child: Form( + key: _formKey, + child: filteredOnlineStoreProductState.when( + data: (products) { + switch (onlineStoreProductEmptyState) { + case OnlineStoreProductEmpty.loading: + return const SizedBox(); + case OnlineStoreProductEmpty.empty_database: + return CustomEmptyScreen( + title: "Tidak Ada Produk", + body: "Kamu belum memiliki produk", + textButton: "Buat Produk Pertamamu", + onPressed: () => context.pushNamed( + AppRoute.addProductScreen, + ), + ); + case OnlineStoreProductEmpty.empty_search_result: + return SingleChildScrollView( + physics: + const AlwaysScrollableScrollPhysics(), + padding: EdgeInsets.symmetric(vertical: 3.h), + child: Center( + child: Column( + mainAxisAlignment: + MainAxisAlignment.center, + children: [ + Text( + "Produk Tidak Ditemukan", + style: TextStyle( + fontSize: AppFontSize.large.sp, + fontWeight: FontWeight.bold, + color: Colors.black, + ), + ), + + SizedBox(height: 1.5.h), + + Icon( + Icons.search_off_rounded, + size: 15.h, + color: Colors.grey.shade300, + ), + ], + ), + ), + ); + + case OnlineStoreProductEmpty.has_data: + return ListView.builder( + padding: EdgeInsets.fromLTRB( + 5.w, + 0, + 5.w, + 12.h, + ), + itemCount: products.length, + itemBuilder: (context, index) { + final product = products[index]; + + final activeProduct = product.variants + .where((variant) { + return !variant.hasBeenDelete; + }) + .toList(); + + final historyProduct = product.variants + .where((variant) { + return variant.hasBeenDelete; + }) + .toList(); + + final bool isTablet = 100.w >= 600; + + final bool imagePathAvailable = + product.imagePath != null && + product.imagePath!.isNotEmpty; + + final File imageFile = File( + imagePathAvailable + ? product.imagePath! + : 'image not found', + ); + + final bool imageFileExists = imageFile + .existsSync(); + + final selectedOnlineStoreProduct = ref + .watch( + selectedOnlineStoreProductProvider, + ); + + final isExpanded = ref.watch( + expandedOnlineStoreProductProvider( + product.id, + ), + ); + + return AnimatedContainer( + duration: const Duration( + milliseconds: 200, + ), + margin: EdgeInsets.only(bottom: 2.h), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular( + 4.w, + ), + border: Border.all( + color: Colors.grey.shade200, + width: 1, + ), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity( + 0.03, + ), + blurRadius: 8, + offset: const Offset(0, 4), + ), + ], + ), + child: Column( + children: [ + InkWell( + onTap: () { + if (product.hasVariant) { + ref + .read( + expandedOnlineStoreProductProvider( + product.id, + ).notifier, + ) + .state = !ref.read( + expandedOnlineStoreProductProvider( + product.id, + ), + ); + } else { + final productId = + activeProduct.single.id; + + final selectedProductState = ref + .read( + selectedOnlineStoreProductProvider, + ); + + final selectedProductNotifier = + ref.read( + selectedOnlineStoreProductProvider + .notifier, + ); + + if (selectedProductState + .containsKey(productId)) { + selectedProductNotifier + .state = { + ...selectedProductState, + }..remove(productId); + } else { + selectedProductNotifier + .state = { + ...selectedProductState, + productId: productId, + }; + } + } + }, + borderRadius: BorderRadius.vertical( + top: Radius.circular(4.w), + bottom: + isExpanded || + historyProduct.isNotEmpty + ? Radius.zero + : Radius.circular(4.w), + ), + child: Padding( + padding: EdgeInsets.all(3.w), + child: Row( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Container( + width: 20.w, + height: 20.w, + decoration: BoxDecoration( + color: Colors.grey[100], + borderRadius: + BorderRadius.circular( + 2.5.w, + ), + border: Border.all( + color: Colors + .grey + .shade300, + ), + ), + child: imageFileExists + ? ClipRRect( + borderRadius: + BorderRadius.circular( + 2.5.w, + ), + child: Image.file( + imageFile, + fit: BoxFit.cover, + width: double + .infinity, + height: double + .infinity, + errorBuilder: + ( + context, + error, + stackTrace, + ) { + return Icon( + Icons + .error, + color: Colors + .grey, + size: 7.w, + ); + }, + ), + ) + : Icon( + Icons.image, + color: Colors + .grey[400], + size: 7.w, + ), // Placeholder + ), + SizedBox(width: 4.w), + + Expanded( + child: Column( + crossAxisAlignment: + CrossAxisAlignment + .start, + children: [ + Text( + product.name, + overflow: TextOverflow + .ellipsis, + style: TextStyle( + fontSize: isTablet + ? AppFontSize + .medium + .sp + : AppFontSize + .small + .sp, + fontWeight: + FontWeight.bold, + color: + activeProduct + .isEmpty + ? Colors.grey + : Colors + .black87, + decoration: + activeProduct + .isEmpty + ? TextDecoration + .lineThrough + : null, + ), + ), + + if (activeProduct + .isNotEmpty) ...[ + SizedBox(height: 1.h), + + if (product + .hasVariant) + Text( + "${activeProduct.length} Varian", + style: TextStyle( + fontSize: + isTablet + ? AppFontSize + .medium + .sp + : AppFontSize + .small + .sp, + color: + Colors.grey, + ), + ) + else ...[ + CustomItemDetail( + label: + product.stockType == + StockType + .unlimited + ? "Stok Selalu Ada" + : "Sisa Stok", + value: + product.stockType == + StockType + .unlimited + ? "" + : ": ${product.remainingStock.toStringWithoutTrailingZero()} ${product.unit}", + width: + product.stockType == + StockType + .unlimited + ? 24 + : 22, + ), + + CustomItemDetail( + label: + "Harga Jual", + value: + ": ${CurrencyFormat.formatToIdr(product.sellingPrice, 0)}", + width: 22, + ), + ], + ], + ], + ), + ), + if (activeProduct + .isNotEmpty) ...[ + if (product.hasVariant) + Icon( + isExpanded + ? Icons + .keyboard_arrow_up_rounded + : Icons + .keyboard_arrow_down_rounded, + color: Colors.grey, + size: 7.w, + ) + else + CheckboxProductCard( + isSelected: + selectedOnlineStoreProduct + .containsKey( + activeProduct + .single + .id, + ), + ), + ], + ], + ), + ), + ), + + if (activeProduct.isNotEmpty && + product.hasVariant && + isExpanded) ...[ + Container( + height: 1, + color: Colors.grey[100], + ), + + Padding( + padding: EdgeInsets.symmetric( + vertical: 1.h, + ), + child: Column( + children: activeProduct.map(( + variant, + ) { + return InkWell( + onTap: () { + final selectedProductState = + ref.read( + selectedOnlineStoreProductProvider, + ); + + final selectedProductNotifier = + ref.read( + selectedOnlineStoreProductProvider + .notifier, + ); + + if (selectedProductState + .containsKey( + variant.id, + )) { + selectedProductNotifier + .state = { + ...selectedProductState, + }..remove(variant.id); + } else { + selectedProductNotifier + .state = { + ...selectedProductState, + variant.id: + variant.id, + }; + } + }, + child: Padding( + padding: + EdgeInsets.symmetric( + horizontal: 4.w, + vertical: 1.5.h, + ), + child: Column( + crossAxisAlignment: + CrossAxisAlignment + .start, + children: [ + Row( + crossAxisAlignment: + CrossAxisAlignment + .start, + children: [ + SizedBox( + width: 4.w, + ), + CheckboxProductCard( + isSelected: + selectedOnlineStoreProduct + .containsKey( + variant + .id, + ), + ), + + SizedBox( + width: 3.w, + ), + + Expanded( + child: Column( + mainAxisAlignment: + MainAxisAlignment + .start, + crossAxisAlignment: + CrossAxisAlignment + .start, + children: [ + Text( + variant + .name, + style: TextStyle( + fontWeight: + FontWeight.w700, + fontSize: + isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + color: Colors + .black, + ), + ), + + SizedBox( + height: + 0.5.h, + ), + + CustomItemDetail( + label: + variant.stockType == + StockType.unlimited + ? "Stok Selalu Ada" + : "Sisa Stok", + value: + variant.stockType == + StockType.unlimited + ? "" + : ": ${variant.remainingStock.toStringWithoutTrailingZero()} ${variant.unit}", + width: + variant.stockType == + StockType.unlimited + ? 24 + : 22, + ), + + CustomItemDetail( + label: + "Harga Jual", + value: + ": ${CurrencyFormat.formatToIdr(variant.sellingPrice, 0)}", + width: 22, + ), + ], + ), + ), + ], + ), + ], + ), + ), + ); + }).toList(), + ), + ), + ], + + if (historyProduct.isNotEmpty) ...[ + Divider( + height: 1, + color: Colors.grey.shade200, + ), + + Theme( + data: Theme.of(context).copyWith( + dividerColor: + Colors.transparent, + ), + child: ExpansionTile( + tilePadding: + EdgeInsets.symmetric( + horizontal: 4.w, + ), + visualDensity: + VisualDensity.compact, + title: Row( + children: [ + Icon( + Icons.history, + size: 4.5.w, + color: Colors.grey[400], + ), + SizedBox(width: 2.w), + Text( + "Lihat riwayat / item terhapus", + style: TextStyle( + fontSize: isTablet + ? AppFontSize + .medium + .sp + : AppFontSize + .small + .sp, + color: Colors.grey[500], + fontWeight: + FontWeight.w500, + ), + ), + SizedBox(width: 2.w), + Container( + padding: + EdgeInsets.symmetric( + horizontal: 1.5.w, + vertical: 0.5.w, + ), + decoration: BoxDecoration( + color: Colors.grey[200], + borderRadius: + BorderRadius.circular( + 1.w, + ), + ), + child: Text( + "${historyProduct.length}", + style: TextStyle( + fontSize: isTablet + ? AppFontSize + .medium + .sp + : AppFontSize + .small + .sp, + color: + Colors.grey[600], + ), + ), + ), + ], + ), + children: historyProduct.map(( + variant, + ) { + return Container( + color: Colors.grey[50], + padding: + EdgeInsets.symmetric( + horizontal: 4.w, + vertical: 1.5.h, + ), + child: Row( + children: [ + Icon( + Icons.block, + color: + Colors.grey[400], + size: 5.w, + ), + SizedBox(width: 3.w), + Expanded( + child: Column( + crossAxisAlignment: + CrossAxisAlignment + .start, + children: [ + Text( + variant.name, + style: TextStyle( + fontSize: + isTablet + ? AppFontSize + .medium + .sp + : AppFontSize + .small + .sp, + color: Colors + .grey[500], + decoration: + TextDecoration + .lineThrough, // Coret + ), + ), + + Text( + variant.isProductVariant + ? "Varian Dihapus" + : "Tipe Produk Sebelumnya (Tanpa varian)", + style: TextStyle( + fontSize: + isTablet + ? (AppFontSize.medium - + 2) + .sp + : (AppFontSize.small - + 2) + .sp, + color: Colors + .red[300], + ), + ), + ], + ), + ), + ], + ), + ); + }).toList(), + ), + ), + ], + ], + ), + ); + }, + ); + } + }, + error: (error, stackTrace) { + return CustomErrorScreen( + message: "Ups, terjadi kesalahan", + onRefresh: () {}, + ); + }, + loading: () => const OnlineStoreProductShimmer(), + ), + ), + ), + ], + ), + bottomNavigationBar: filteredOnlineStoreProductState.maybeWhen( + data: (_) { + switch (onlineStoreProductEmptyState) { + case OnlineStoreProductEmpty.loading: + return const SizedBox(); + case OnlineStoreProductEmpty.empty_database: + return const SizedBox(); + case OnlineStoreProductEmpty.empty_search_result: + return _buildSaveButton(); + case OnlineStoreProductEmpty.has_data: + return _buildSaveButton(); + } + }, + orElse: () => const SizedBox(), + ), + ), + + ); + }, + ); + } + + Widget _buildSaveButton() { + final onlineStoreControllerState = ref.watch(onlineStoreControllerProvider); + return Container( + padding: EdgeInsets.all(5.w), + decoration: BoxDecoration( + color: Colors.white, + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.05), + blurRadius: 10, + offset: const Offset(0, -5), + ), + ], + ), + child: SizedBox( + height: 6.5.h, + child: ElevatedButton( + onPressed: onlineStoreControllerState.isLoading + ? null + : _saveOnlineStoreProduct, + style: ElevatedButton.styleFrom( + backgroundColor: onlineStoreControllerState.isLoading + ? Colors.grey + : AppColor.primaryColor, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(2.5.w), + ), + elevation: 0, + ), + child: Text( + "Simpan", + style: TextStyle( + color: Colors.white, + fontSize: AppFontSize.medium.sp, + fontWeight: FontWeight.bold, + ), + ), + ), + ), + ); + } +} diff --git a/Mobile Operasional/lib/features/online_store/presentation/screens/online_store_screen.dart b/Mobile Operasional/lib/features/online_store/presentation/screens/online_store_screen.dart new file mode 100644 index 0000000..c86e668 --- /dev/null +++ b/Mobile Operasional/lib/features/online_store/presentation/screens/online_store_screen.dart @@ -0,0 +1,146 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; +import 'package:niogu_app/core/components/top_back_bar_app.dart'; +import 'package:niogu_app/core/router/app_route.dart'; +import 'package:niogu_app/features/online_store/presentation/providers/online_store_provider.dart'; +import 'package:niogu_app/features/online_store/presentation/widgets/domain_card.dart'; +import 'package:niogu_app/features/online_store/presentation/widgets/sub_menu_card.dart'; +import 'package:sizer/sizer.dart'; + +class OnlineStoreScreen extends ConsumerWidget { + const OnlineStoreScreen({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + return LayoutBuilder( + builder: (context, constraints) { + int badgeCount = 0; + + final badgeCountState = ref.watch( + numberOfNeedToBeProcessedStreamProvider, + ); + + badgeCountState.whenData((count) => badgeCount = count); + + return SafeArea( + top: false, + bottom: true, + right: false, + left: false, + child: Scaffold( + backgroundColor: const Color(0xFFF9FAFB), + appBar: TopBackBarApp( + title: "Toko Online", + onTap: () => Navigator.pop(context), + ), + body: SingleChildScrollView( + padding: EdgeInsets.all(5.w), + child: Column( + children: [ + DomainCard(domainName: "queenfruits.niogu.com"), + + SizedBox(height: 3.h), + + SubMenuCard( + title: "Atur Toko Online", + subtitle: "Kelola profil, deskripsi, dan tampilan toko.", + icon: Icons.settings_outlined, + onTap: () async { + final configure = await ref + .read(onlineStoreRepositoryProvider) + .getConfigureOnlineStore(); + + await Future.delayed( + const Duration(milliseconds: 800), + () { + context.pushNamed( + AppRoute.configureStoreOnlineScreen, + extra: configure, + ); + }, + ); + }, + ), + SubMenuCard( + title: "Pesanan Dari Toko Online", + subtitle: "Lihat dan proses pesanan yang masuk.", + icon: Icons.shopping_bag_outlined, + onTap: () { + context.pushNamed(AppRoute.onlineStoreOrderScreen); + }, + badgeCount: badgeCount > 0 ? badgeCount : null, + ), + SubMenuCard( + title: "Atur Biaya Pengiriman", + subtitle: "Konfigurasi tarif pengiriman.", + icon: Icons.local_shipping_outlined, + onTap: () async { + final configuration = await ref + .read(onlineStoreRepositoryProvider) + .fetchShippingConfiguration(); + + await Future.delayed( + const Duration(milliseconds: 800), + () { + context.pushNamed( + AppRoute.shippingConfigurationScreen, + extra: configuration, + ); + }, + ); + }, + ), + SubMenuCard( + title: "Atur Rekening/E-Wallet", + subtitle: "Kelola metode pembayaran pelanggan.", + icon: Icons.account_balance_wallet_outlined, + onTap: () async { + final configures = await ref + .read(onlineStoreRepositoryProvider) + .fetchConfigurePaymentMethod(); + + await Future.delayed( + const Duration(milliseconds: 800), + () { + context.pushNamed( + AppRoute.paymentMethodScreen, + extra: configures, + ); + }, + ); + }, + ), + SubMenuCard( + title: "Review Produk", + subtitle: "Pantau ulasan dan feedback pelanggan.", + icon: Icons.reviews_outlined, + onTap: () { + context.pushNamed(AppRoute.productReviewScreen); + }, + ), + SubMenuCard( + title: "Atur Rating Produk", + subtitle: "Kelola tampilan ulasan dan feedback pelanggan", + icon: Icons.star_outline, + onTap: () { + context.pushNamed(AppRoute.ratingConfigurationScreen); + }, + ), + SubMenuCard( + title: "Buat Campaign", + subtitle: "Tingkatkan penjualan dengan promo menarik.", + icon: Icons.campaign_outlined, + onTap: () { + context.pushNamed(AppRoute.campaignScreen); + }, + ), + ], + ), + ), + ), + ); + }, + ); + } +} diff --git a/Mobile Operasional/lib/features/online_store/presentation/screens/payment_method_screen.dart b/Mobile Operasional/lib/features/online_store/presentation/screens/payment_method_screen.dart new file mode 100644 index 0000000..4c95262 --- /dev/null +++ b/Mobile Operasional/lib/features/online_store/presentation/screens/payment_method_screen.dart @@ -0,0 +1,636 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; +import 'package:niogu_app/core/components/top_back_bar_app.dart'; +import 'package:niogu_app/core/constants/app_asset.dart'; +import 'package:niogu_app/core/constants/app_color.dart'; +import 'package:niogu_app/core/constants/app_font_size.dart'; +import 'package:niogu_app/core/utils/extension_format.dart'; +import 'package:niogu_app/core/utils/log_message.dart'; +import 'package:niogu_app/core/widgets/custom_snackbar.dart'; +import 'package:niogu_app/core/widgets/custom_text_form_field.dart'; +import 'package:niogu_app/features/online_store/domain/entities/online_store.dart'; +import 'package:niogu_app/features/online_store/presentation/providers/online_store_provider.dart'; +import 'package:sizer/sizer.dart'; + +class PaymentMethodsScreen extends ConsumerStatefulWidget { + final List configures; + const PaymentMethodsScreen({super.key, required this.configures}); + + @override + ConsumerState createState() => + _PaymentMethodsScreenState(); +} + +class _PaymentMethodsScreenState extends ConsumerState { + final Map> _controllers = { + "dana": { + "id": null, + "no": TextEditingController(), + "name": TextEditingController(), + }, + "gopay": { + "id": null, + "no": TextEditingController(), + "name": TextEditingController(), + }, + "shopeepay": { + "id": null, + "no": TextEditingController(), + "name": TextEditingController(), + }, + "bri": { + "id": null, + "no": TextEditingController(), + "name": TextEditingController(), + }, + "mandiri": { + "id": null, + "no": TextEditingController(), + "name": TextEditingController(), + }, + "bni": { + "id": null, + "no": TextEditingController(), + "name": TextEditingController(), + }, + "bca": { + "id": null, + "no": TextEditingController(), + "name": TextEditingController(), + }, + "custom": { + "id": null, + "source": TextEditingController(), + "no": TextEditingController(), + "name": TextEditingController(), + }, + }; + + bool _isButtonEnabled = false; + + String _expandedSection = ""; + + @override + void initState() { + super.initState(); + for (final group in _controllers.values) { + for (final controller in group.values) { + if (controller is TextEditingController) { + controller.addListener(_validateForm); + } + } + } + + _fetchConfiguration(); + } + + @override + void dispose() { + // TODO: implement dispose + for (final group in _controllers.values) { + for (final controller in group.values) { + if (controller is TextEditingController) { + controller.dispose(); + } + } + } + super.dispose(); + } + + void _fetchConfiguration() { + final configures = widget.configures; + + final List keys = [ + "dana", + "gopay", + "shopeepay", + "bri", + "mandiri", + "bni", + "bca", + ]; + + if (configures.isNotEmpty) { + for (final configure in configures) { + final sourceName = configure.sourceName; + + final accountNumber = configure.accountNumber; + + final receipentName = configure.receipentName; + + final hasData = + sourceName.isNotEmpty && + accountNumber.isNotEmpty && + receipentName.isNotEmpty; + + final String key = keys.contains(sourceName) ? sourceName : "custom"; + + _controllers[key]!["id"] = configure.localId; + + if (hasData) { + if (keys.contains(sourceName)) { + _controllers[sourceName]!["no"]!.text = accountNumber; + _controllers[sourceName]!["name"]!.text = receipentName; + } else { + _controllers["custom"]!["id"] = configure.localId; + _controllers["custom"]!["source"]!.text = sourceName.capitalize(); + _controllers["custom"]!["no"]!.text = accountNumber; + _controllers["custom"]!["name"]!.text = receipentName; + } + } + } + } + } + + void _validateForm() { + bool hasAtLeastOneComplete = false; + bool hasIncompleteEntry = false; + + _controllers.forEach((key, value) { + if (key == "custom") { + final bool s = value["source"]!.text.isNotEmpty; + final bool n = value["no"]!.text.isNotEmpty; + final bool p = value["name"]!.text.isNotEmpty; + + if ((s || n || p) && !(s && n && p)) { + hasIncompleteEntry = true; + } + + if (s && n && p) { + hasAtLeastOneComplete = true; + } + } else { + final bool n = value["no"]!.text.isNotEmpty; + final bool p = value["name"]!.text.isNotEmpty; + + if ((n || p) && !(n && p)) { + hasIncompleteEntry = true; + } + if (n && p) { + hasAtLeastOneComplete = true; + } + } + }); + final bool finalStatus = hasAtLeastOneComplete && !hasIncompleteEntry; + + if (_isButtonEnabled != finalStatus) { + setState(() { + _isButtonEnabled = finalStatus; + }); + } + } + + String? _getErrorText(String key, String value) { + final fields = _controllers[key]!; + + if (key == "custom") { + final bool s = fields["source"]!.text.isNotEmpty; + final bool n = fields["no"]!.text.isNotEmpty; + final bool p = fields["name"]!.text.isNotEmpty; + + if (fields[value] is TextEditingController) { + if ((s || n || p) && fields[value]!.text.isEmpty) { + return "Kolom ini belum diisi"; + } + } + } else { + final bool n = fields["no"]!.text.isNotEmpty; + final bool p = fields["name"]!.text.isNotEmpty; + + if (fields[value] is TextEditingController) { + if ((n || p) && !fields[value]!.text.isNotEmpty) { + return "Kolom ini belum diisi"; + } + } + } + return null; + } + + Future _configurePaymentMethod() async { + String? key; + + final List configures = []; + + for (final controller in _controllers.entries) { + final firstKey = controller.key; + + final values = controller.value; + + for (final value in values.entries) { + final secondKey = value.key; + + final String? errorText = _getErrorText(firstKey, secondKey); + + if (errorText != null) { + key = firstKey; + break; + } + } + configures.add( + ConfigurePaymentMethod( + localId: values["id"]?.toString(), + sourceName: firstKey == "custom" + ? values["source"]!.text.toLowerCase() + : firstKey, + accountNumber: values["no"]!.text, + receipentName: values["name"]!.text, + ), + ); + } + + if (key != null) { + setState(() { + if (key == 'dana' || key == 'gopay' || key == 'shopeepay') { + _expandedSection = "ewallet"; + } else if (key == 'bri' || + key == 'mandiri' || + key == 'bni' || + key == 'bca') { + _expandedSection = "bank"; + } else { + _expandedSection = "custom"; + } + }); + return; + } + + if (!_isButtonEnabled) { + CustomSnackbar.showError(context, "Tambah minimal 1 metode pembayaran"); + return; + } + + try { + await ref + .read(onlineStoreControllerProvider.notifier) + .paymentMethodConfiguration(configures); + + if (!mounted) return; + + CustomSnackbar.showSuccess( + context, + "Metode pembayaran berhasil diperbarui", + ); + context.pop(); + } catch (e, st) { + LogMessage.log.e(e.toString(), error: e, stackTrace: st); + CustomSnackbar.showError(context, "Ups, terjadi kesalahan"); + } + } + + @override + Widget build(BuildContext context) { + return LayoutBuilder( + builder: (context, constraints) { + final bool isTablet = 100.w >= 600; + + final onlineStoreControllerState = ref.watch( + onlineStoreControllerProvider, + ); + + return SafeArea( + top: false, + bottom: true, + right: false, + left: false, + child: Stack( + children: [ + Scaffold( + backgroundColor: const Color(0xFFF9FAFB), + appBar: TopBackBarApp( + title: "Atur Rekening/E-Wallet", + onTap: () => Navigator.pop(context), + ), + body: SingleChildScrollView( + padding: EdgeInsets.all(5.w), + child: Column( + children: [ + Container( + padding: EdgeInsets.all(4.w), + decoration: BoxDecoration( + color: Colors.amber.withOpacity(0.1), + borderRadius: BorderRadius.circular(3.w), + border: Border.all(color: Colors.amber.shade300), + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Icon( + Icons.info_outline_rounded, + color: Colors.amber.shade800, + size: 6.w, + ), + SizedBox(width: 3.w), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Informasi Verifikasi Manual", + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + color: Colors.amber.shade900, + ), + ), + SizedBox(height: 0.5.h), + Text( + "Pembayaran yang diterima melalui metode di bawah akan masuk langsung ke rekening/akun pribadi Anda. Harap lakukan pengecekan mutasi secara manual sebelum memproses pesanan.", + style: TextStyle( + fontSize: isTablet + ? (AppFontSize.medium - 1.25).sp + : (AppFontSize.small - 1.25).sp, + color: Colors.amber.shade900, + height: 1.4, + ), + ), + ], + ), + ), + ], + ), + ), + SizedBox(height: 3.h), + + _buildExpandableSection( + title: "E-Wallet", + icon: Icons.account_balance_wallet_outlined, + isExpanded: _expandedSection == "ewallet", + onTap: () => setState( + () => _expandedSection = _expandedSection == "ewallet" + ? "" + : "ewallet", + ), + children: [ + _buildPaymentItem( + _controllers['dana']!['no']!, + _controllers['dana']!['name']!, + _getErrorText('dana', 'no'), + _getErrorText('dana', 'name'), + isTablet, + "DANA", + AppAsset.DANA_LOGO, + isEwallet: true, + ), + _buildPaymentItem( + _controllers['gopay']!['no']!, + _controllers['gopay']!['name']!, + _getErrorText('gopay', 'no'), + _getErrorText('gopay', 'name'), + isTablet, + "GoPay", + AppAsset.GOPAY_LOGO, + isEwallet: true, + ), + _buildPaymentItem( + _controllers['shopeepay']!['no']!, + _controllers['shopeepay']!['name']!, + _getErrorText('shopeepay', 'no'), + _getErrorText('shopeepay', 'name'), + isTablet, + "ShopeePay", + AppAsset.SHOPEEPAY_LOGO, + isEwallet: true, + ), + ], + ), + + SizedBox(height: 2.h), + + _buildExpandableSection( + title: "Rekening Bank", + icon: Icons.account_balance_outlined, + isExpanded: _expandedSection == "bank", + onTap: () => setState( + () => _expandedSection = _expandedSection == "bank" + ? "" + : "bank", + ), + children: [ + _buildPaymentItem( + _controllers['bri']!['no']!, + _controllers['bri']!['name']!, + _getErrorText('bri', 'no'), + _getErrorText('bri', 'name'), + isTablet, + "Bank BRI", + AppAsset.BRI_LOGO, + ), + _buildPaymentItem( + _controllers['mandiri']!['no']!, + _controllers['mandiri']!['name']!, + _getErrorText('mandiri', 'no'), + _getErrorText('mandiri', 'name'), + isTablet, + "Bank Mandiri", + AppAsset.MANDIRI_LOGO, + ), + _buildPaymentItem( + _controllers['bni']!['no']!, + _controllers['bni']!['name']!, + _getErrorText('bni', 'no'), + _getErrorText('bni', 'name'), + isTablet, + "Bank BNI", + AppAsset.BNI_LOGO, + ), + _buildPaymentItem( + _controllers['bca']!['no']!, + _controllers['bca']!['name']!, + _getErrorText('bca', 'no'), + _getErrorText('bca', 'name'), + isTablet, + "Bank BCA", + AppAsset.BCA_LOGO, + ), + ], + ), + + SizedBox(height: 2.h), + + _buildExpandableSection( + title: "Pembayaran Lain", + icon: Icons.add_card_outlined, + isExpanded: _expandedSection == "custom", + onTap: () => setState( + () => _expandedSection = _expandedSection == "custom" + ? "" + : "custom", + ), + children: [ + _buildCustomForm( + _controllers['custom']!['source']!, + _controllers['custom']!['no']!, + _controllers['custom']!['name']!, + _getErrorText('custom', 'source'), + _getErrorText('custom', 'no'), + _getErrorText('custom', 'name'), + ), + ], + ), + + SizedBox(height: 5.h), + ElevatedButton( + onPressed: onlineStoreControllerState.isLoading + ? null + : _configurePaymentMethod, + style: ElevatedButton.styleFrom( + backgroundColor: AppColor.primaryColor, + minimumSize: Size(double.infinity, 7.h), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(3.w), + ), + disabledBackgroundColor: Colors.grey.shade300, + ), + child: Text( + "Simpan Metode Pembayaran", + style: TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + fontSize: AppFontSize.medium.sp, + ), + ), + ), + ], + ), + ), + ), + ], + ), + ); + }, + ); + } + + Widget _buildExpandableSection({ + required String title, + required IconData icon, + required bool isExpanded, + required VoidCallback onTap, + required List children, + }) { + return Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(3.w), + border: Border.all(color: Colors.grey.shade200), + ), + child: Column( + children: [ + ListTile( + onTap: onTap, + leading: Icon(icon, color: AppColor.primaryColor), + title: Text( + title, + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: AppFontSize.medium.sp, + ), + ), + trailing: Icon( + isExpanded ? Icons.keyboard_arrow_up : Icons.keyboard_arrow_down, + size: 5.w, + ), + ), + if (isExpanded) ...[ + const Divider(height: 1), + Padding( + padding: EdgeInsets.all(4.w), + child: Column(children: children), + ), + ], + ], + ), + ); + } + + Widget _buildPaymentItem( + TextEditingController accountController, + TextEditingController nameController, + String? errorAccountText, + String? errorNameText, + bool isTablet, + String name, + String assetPath, { + bool isEwallet = false, + }) { + return Padding( + padding: EdgeInsets.only(bottom: 2.h), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Image.asset(assetPath, width: 7.w, fit: BoxFit.contain), + SizedBox(width: 3.w), + Text( + name, + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: isTablet + ? (AppFontSize.medium - 1.25).sp + : (AppFontSize.small - 1.25).sp, + ), + ), + ], + ), + SizedBox(height: 1.5.h), + CustomTextFormField( + label: "Nomor ${isEwallet ? 'Akun' : 'Rekening'}", + hint: "Contoh: ${isEwallet ? '081234567890' : '0081236785'}", + controller: accountController, + prefixIcon: Icons.numbers, + errorText: errorAccountText, + ), + SizedBox(height: 1.h), + CustomTextFormField( + label: "Nama Penerima", + hint: "Isi penerima", + controller: nameController, + prefixIcon: Icons.person_outline, + errorText: errorNameText, + ), + ], + ), + ); + } + + Widget _buildCustomForm( + TextEditingController sourceController, + TextEditingController accountController, + TextEditingController nameController, + String? errorSourceText, + String? errorAccountText, + String? errorNameText, + ) { + return Column( + children: [ + CustomTextFormField( + label: "Sumber Pembayaran", + hint: "Contoh: Bank BSI/OVO", + controller: sourceController, + prefixIcon: Icons.source_outlined, + errorText: errorSourceText, + ), + SizedBox(height: 1.5.h), + CustomTextFormField( + label: "Nomor Rekening / Akun", + hint: "Contoh: 0081236785", + controller: accountController, + prefixIcon: Icons.numbers, + errorText: errorAccountText, + ), + SizedBox(height: 1.5.h), + CustomTextFormField( + label: "Nama Penerima", + hint: "Isi penerima", + controller: nameController, + prefixIcon: Icons.person_outline, + errorText: errorNameText, + ), + ], + ); + } +} diff --git a/Mobile Operasional/lib/features/online_store/presentation/screens/product_campaign_screen.dart b/Mobile Operasional/lib/features/online_store/presentation/screens/product_campaign_screen.dart new file mode 100644 index 0000000..9bd86e2 --- /dev/null +++ b/Mobile Operasional/lib/features/online_store/presentation/screens/product_campaign_screen.dart @@ -0,0 +1,93 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; +import 'package:niogu_app/core/widgets/custom_error_screen.dart'; +import 'package:niogu_app/core/router/app_route.dart'; +import 'package:niogu_app/core/enums/campaign_type.dart'; +import 'package:niogu_app/core/utils/extension_format.dart'; +import 'package:niogu_app/core/enums/stock_type.dart'; +import 'package:niogu_app/core/widgets/custom_empty_screen.dart'; +import 'package:niogu_app/features/online_store/domain/entities/online_store.dart'; +import 'package:niogu_app/features/online_store/presentation/providers/online_store_provider.dart'; +import 'package:niogu_app/core/widgets/custom_selectable_item.dart'; +import 'package:sizer/sizer.dart'; + +class ProductCampaignScreen extends ConsumerStatefulWidget { + const ProductCampaignScreen({super.key}); + + @override + ConsumerState createState() => + _ProductCampaignScreenState(); +} + +class _ProductCampaignScreenState extends ConsumerState + with AutomaticKeepAliveClientMixin { + @override + // TODO: implement wantKeepAlive + bool get wantKeepAlive => true; + + @override + Widget build(BuildContext context) { + super.build(context); + + final filteredProductState = ref.watch( + filteredProductCampaignStreamProvider, + ); + + final productEmptyState = ref.watch(productCampaignEmptyProvider); + + final selectedCampaignItem = ref.watch(selectedCampaignItemProvider); + + return filteredProductState.when( + data: (products) { + switch (productEmptyState) { + case ProductCampaignEmpty.loading: + return const SizedBox(); + case ProductCampaignEmpty.empty_database: + return CustomEmptyScreen( + title: "Tidak Ada Produk", + body: "Kamu belum memiliki produk", + textButton: "Buat Produk Pertamamu", + onPressed: () => context.pushNamed(AppRoute.addProductScreen), + ); + case ProductCampaignEmpty.empty_search_result: + return const CustomEmptyScreen(body: "Produk Tidak Ditemukan"); + case ProductCampaignEmpty.has_data: + return ListView.builder( + padding: EdgeInsets.all(4.w), + itemCount: products.length, + itemBuilder: (context, index) { + final product = products[index]; + + final stockType = product.stockType; + + final stock = stockType == StockType.unlimited + ? "Stok Selalu Ada" + : "Sisa Stok : ${product.remainingStock.toStringWithoutTrailingZero()}"; + + return CustomSelectableItem( + id: product.id, + title: product.name, + subtitle: stock, + imagePath: product.imagePath, + isSelected: selectedCampaignItem?.id == product.id, + onTap: () { + ref + .read(selectedCampaignItemProvider.notifier) + .state = SelectedItemCampaign( + id: product.id, + name: product.name, + type: CampaignType.product + ); + }, + ); + }, + ); + } + }, + error: (error, stackTrace) => + CustomErrorScreen(message: "Ups, terjadi kesalahan", onRefresh: () {}), + loading: () => const SizedBox(), + ); + } +} diff --git a/Mobile Operasional/lib/features/online_store/presentation/screens/product_review_screen.dart b/Mobile Operasional/lib/features/online_store/presentation/screens/product_review_screen.dart new file mode 100644 index 0000000..7a625f2 --- /dev/null +++ b/Mobile Operasional/lib/features/online_store/presentation/screens/product_review_screen.dart @@ -0,0 +1,263 @@ +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; +import 'package:niogu_app/core/components/top_back_bar_app.dart'; +import 'package:niogu_app/core/constants/app_font_size.dart'; +import 'package:niogu_app/core/widgets/custom_error_screen.dart'; +import 'package:niogu_app/core/router/app_route.dart'; +import 'package:niogu_app/core/utils/extension_format.dart'; +import 'package:niogu_app/core/widgets/custom_empty_screen.dart'; +import 'package:niogu_app/features/online_store/presentation/providers/online_store_provider.dart'; +import 'package:niogu_app/features/online_store/presentation/widgets/product_review_shimmer.dart'; +import 'package:sizer/sizer.dart'; + +class ProductReviewScreen extends ConsumerWidget { + const ProductReviewScreen({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + return LayoutBuilder( + builder: (context, constraints) { + final bool isTablet = 100.w >= 600; + + final reviewState = ref.watch(displayProductReviewStreamProvider); + + return SafeArea( + top: false, + bottom: true, + right: false, + left: false, + child: Scaffold( + backgroundColor: const Color(0xFFF9FAFB), + appBar: TopBackBarApp( + title: "Review Produk", + onTap: () => context.pop(), + ), + body: reviewState.when( + data: (reviews) { + if (reviews.isEmpty) { + return CustomEmptyScreen( + title: "Tidak Ada Produk", + body: "Kamu belum memiliki produk", + textButton: "Buat Produk Pertamamu", + onPressed: () => + context.pushNamed(AppRoute.addProductScreen), + ); + } + return ListView.builder( + padding: EdgeInsets.all(5.w), + itemCount: reviews.length, + itemBuilder: (context, index) { + final review = reviews[index]; + + final bool imagePathAvailable = + review.imagePath != null && + review.imagePath!.isNotEmpty; + + final File imageFile = File( + imagePathAvailable + ? review.imagePath! + : "image not found", + ); + + final bool imageFileExists = imageFile.existsSync(); + + final totalSoldInOnlineStore = + review.totalSoldInOnlineStore >= 1000 + ? review.totalSoldInOnlineStore.toCompact + : review.totalSoldInOnlineStore + .toStringWithoutTrailingZero(); + + final totalSoldInPos = review.totalSoldInPos >= 1000 + ? review.totalSoldInPos.toCompact + : review.totalSoldInPos.toStringWithoutTrailingZero(); + + return GestureDetector( + onTap: () { + context.pushNamed( + AppRoute.detailProductReviewScreen, + pathParameters: {"id": review.id}, + ); + }, + child: Container( + margin: EdgeInsets.only(bottom: 2.h), + padding: EdgeInsets.all(4.w), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(3.w), + border: Border.all(color: Colors.grey.shade200), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.02), + blurRadius: 10, + offset: const Offset(0, 4), + ), + ], + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + width: 15.w, + height: 15.w, + decoration: BoxDecoration( + color: Colors.grey[100], + borderRadius: BorderRadius.circular(2.w), + ), + child: imageFileExists + ? ClipRRect( + borderRadius: BorderRadius.circular( + 2.5.w, + ), + child: Image.file( + imageFile, + fit: BoxFit.cover, + width: double.infinity, + height: double.infinity, + errorBuilder: + (context, error, stackTrace) { + return Icon( + Icons.error, + color: Colors.grey[400], + size: 8.w, + ); + }, + ), + ) + : Icon( + Icons.image_outlined, + color: Colors.grey[400], + size: 8.w, + ), + ), + + SizedBox(width: 4.w), + + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Text( + review.name, + maxLines: 2, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + ), + ), + Row( + children: [ + Icon( + Icons.star_rounded, + color: Colors.orange, + size: 5.w, + ), + SizedBox(width: 1.w), + Text( + review.averageRating + .toRatingFormat(), + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + ), + ), + ], + ), + ], + ), + SizedBox(height: 1.5.h), + + Text( + "Penjualan", + style: TextStyle( + color: Colors.grey.shade700, + fontSize: isTablet + ? (AppFontSize.medium - 1.25).sp + : (AppFontSize.small - 1.25).sp, + ), + ), + + SizedBox(height: 1.h), + + _buildSaleInfo( + isTablet, + Icons.language_outlined, + "Toko Online", + "$totalSoldInOnlineStore ${review.unit}", + ), + SizedBox(height: 0.8.h), + _buildSaleInfo( + isTablet, + Icons.point_of_sale_outlined, + "Kasir", + "$totalSoldInPos ${review.unit}", + ), + ], + ), + ), + ], + ), + ), + ); + }, + ); + }, + error: (error, stackTrace) { + return CustomErrorScreen( + message: "Ups, terjadi kesalahan", + onRefresh: () {}, + ); + }, + loading: () => const ProductReviewShimmer(), + ), + ), + ); + }, + ); + } + + Widget _buildSaleInfo( + bool isTablet, + IconData icon, + String label, + String value, + ) { + return Row( + children: [ + Icon(icon, size: 4.5.w, color: Colors.grey[400]), + SizedBox(width: 2.w), + Text( + "$label:", + style: TextStyle( + color: Colors.grey[600], + fontSize: isTablet + ? (AppFontSize.medium - 1.25).sp + : (AppFontSize.small - 1.25).sp, + ), + ), + const Spacer(), + Text( + value, + style: TextStyle( + color: Colors.black87, + fontWeight: FontWeight.w600, + fontSize: isTablet + ? (AppFontSize.medium - 1.25).sp + : (AppFontSize.small - 1.25).sp, + ), + ), + ], + ); + } +} diff --git a/Mobile Operasional/lib/features/online_store/presentation/screens/rating_configuration_screen.dart b/Mobile Operasional/lib/features/online_store/presentation/screens/rating_configuration_screen.dart new file mode 100644 index 0000000..911e8f2 --- /dev/null +++ b/Mobile Operasional/lib/features/online_store/presentation/screens/rating_configuration_screen.dart @@ -0,0 +1,288 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; +import 'package:niogu_app/core/components/top_back_bar_app.dart'; +import 'package:niogu_app/core/constants/app_color.dart'; +import 'package:niogu_app/core/constants/app_font_size.dart'; +import 'package:niogu_app/core/utils/log_message.dart'; +import 'package:niogu_app/core/enums/review_display_policy.dart'; +import 'package:niogu_app/core/enums/review_display_type.dart'; +import 'package:niogu_app/core/widgets/custom_snackbar.dart'; +import 'package:niogu_app/features/online_store/domain/entities/online_store.dart'; +import 'package:niogu_app/features/online_store/presentation/providers/online_store_provider.dart'; +import 'package:sizer/sizer.dart'; + +class RatingConfigurationScreen extends ConsumerStatefulWidget { + const RatingConfigurationScreen({super.key}); + + @override + ConsumerState createState() => + _RatingConfigurationScreenState(); +} + +class _RatingConfigurationScreenState + extends ConsumerState { + ReviewDisplayType _type = ReviewDisplayType.rating_only; + + ReviewDisplayPolicy _policy = ReviewDisplayPolicy.four_and_five_star_only; + + @override + void initState() { + // TODO: implement initState + super.initState(); + + WidgetsBinding.instance.addPostFrameCallback((_) => _fetchConfiguration()); + } + + @override + void dispose() { + // TODO: implement dispose + super.dispose(); + } + + Future _fetchConfiguration() async { + try { + final ratingConfiguration = await ref + .read(onlineStoreRepositoryProvider) + .fetchRatingConfiguration(); + + _type = ratingConfiguration.type; + _policy = ratingConfiguration.policy; + setState(() {}); + } catch (e, st) { + LogMessage.log.e(e.toString(), error: e, stackTrace: st); + CustomSnackbar.showError(context, "Ups, terjadi kesalahan"); + context.pop(); + } + } + + Future _ratingConfiguration() async { + try { + await ref + .read(onlineStoreControllerProvider.notifier) + .ratingConfiguration( + RatingConfiguration(type: _type, policy: _policy), + ); + + if (!mounted) return; + CustomSnackbar.showSuccess( + context, + "Pengaturan rating produk berhasil diperbarui", + ); + context.pop(); + } catch (e, st) { + LogMessage.log.e(e.toString(), error: e, stackTrace: st); + CustomSnackbar.showError(context, "Ups, terjadi kesalahan"); + } + } + + @override + Widget build(BuildContext context) { + return LayoutBuilder( + builder: (context, constraints) { + final bool isTablet = 100.w >= 600; + + final onlineStoreControllerState = ref.watch( + onlineStoreControllerProvider, + ); + + return SafeArea( + top: false, + bottom: true, + right: false, + left: false, + child: Scaffold( + backgroundColor: const Color(0xFFF9FAFB), + appBar: TopBackBarApp( + title: "Atur Rating Produk", + onTap: () => context.pop(), + ), + body: SingleChildScrollView( + padding: EdgeInsets.all(5.w), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildSectionTitle(isTablet, "Jenis Tampilan Penilaian"), + _buildOptionCard( + isTablet: isTablet, + title: "Tampilkan Rating Saja", + subtitle: "Hanya menampilkan angka rata-rata bintang.", + value: ReviewDisplayType.rating_only, + groupValue: _type, + onChanged: (val) { + setState(() { + if (val is ReviewDisplayType) { + _type = val; + } + }); + }, + previewImage: "assets/images/preview_rating_only.png", + ), + _buildOptionCard( + isTablet: isTablet, + title: "Rating & Ulasan", + subtitle: + "Menampilkan bintang beserta teks ulasan pelanggan.", + value: ReviewDisplayType.rating_and_review, + groupValue: _type, + onChanged: (val) { + setState(() { + if (val is ReviewDisplayType) { + _type = val; + } + }); + }, + previewImage: "assets/images/preview_rating_comment.png", + ), + + SizedBox(height: 4.h), + + _buildSectionTitle( + isTablet, + "Kriteria Penilaian yang Muncul", + ), + _buildOptionCard( + isTablet: isTablet, + title: "Seluruh Rating", + subtitle: + "Menampilkan semua ulasan dari bintang 1 hingga 5.", + value: ReviewDisplayPolicy.all_rating, + groupValue: _policy, + onChanged: (val) { + setState(() { + if (val is ReviewDisplayPolicy) { + _policy = val; + } + }); + }, + previewImage: "assets/images/preview_all_stars.png", + ), + _buildOptionCard( + isTablet: isTablet, + title: "Bintang 4 & 5 Saja", + subtitle: + "Hanya ulasan positif yang tampil di toko online.", + value: ReviewDisplayPolicy.four_and_five_star_only, + groupValue: _policy, + onChanged: (val) { + setState(() { + if (val is ReviewDisplayPolicy) { + _policy = val; + } + }); + }, + previewImage: "assets/images/preview_good_stars.png", + ), + + SizedBox(height: 3.h), + ElevatedButton( + onPressed: onlineStoreControllerState.isLoading + ? null + : _ratingConfiguration, + style: ElevatedButton.styleFrom( + backgroundColor: AppColor.primaryColor, + minimumSize: Size(double.infinity, 7.h), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(3.w), + ), + disabledBackgroundColor: Colors.grey.shade300, + ), + child: Text( + "Simpan Konfigurasi", + style: TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + fontSize: AppFontSize.medium.sp, + ), + ), + ), + ], + ), + ), + ), + ); + }, + ); + } + + Widget _buildSectionTitle(bool isTablet, String title) { + return Padding( + padding: EdgeInsets.only(left: 1.w, bottom: 2.h), + child: Text( + title, + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: isTablet ? AppFontSize.medium.sp : AppFontSize.small.sp, + ), + ), + ); + } + + Widget _buildOptionCard({ + required bool isTablet, + required String title, + required String subtitle, + required dynamic value, + required dynamic groupValue, + required Function(dynamic) onChanged, + required String previewImage, + }) { + return GestureDetector( + onTap: () => onChanged(value), + child: Container( + margin: EdgeInsets.only(bottom: 2.h), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(3.w), + border: Border.all(color: Colors.grey.shade200, width: 1), + ), + child: Column( + children: [ + ListTile( + leading: Radio( + value: value, + groupValue: groupValue, + activeColor: AppColor.primaryColor, + onChanged: onChanged, + ), + title: Text( + title, + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + ), + ), + subtitle: Text( + subtitle, + style: TextStyle( + fontSize: isTablet + ? (AppFontSize.medium - 1.25).sp + : (AppFontSize.small - 1.25).sp, + color: Colors.grey.shade700, + ), + ), + ), + Container( + height: 15.h, + width: double.infinity, + margin: EdgeInsets.fromLTRB(4.w, 0, 4.w, 4.w), + decoration: BoxDecoration( + color: Colors.grey[100], + borderRadius: BorderRadius.circular(2.w), + ), + child: Center( + child: Icon( + Icons.image_outlined, + size: 10.w, + color: Colors.grey[400], + ), + ), + ), + ], + ), + ), + ); + } +} diff --git a/Mobile Operasional/lib/features/online_store/presentation/screens/shipping_configuration_screen.dart b/Mobile Operasional/lib/features/online_store/presentation/screens/shipping_configuration_screen.dart new file mode 100644 index 0000000..8a15abd --- /dev/null +++ b/Mobile Operasional/lib/features/online_store/presentation/screens/shipping_configuration_screen.dart @@ -0,0 +1,389 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; +import 'package:niogu_app/core/components/top_back_bar_app.dart'; +import 'package:niogu_app/core/constants/app_color.dart'; +import 'package:niogu_app/core/constants/app_font_size.dart'; +import 'package:niogu_app/core/enums/delivery_fee_type.dart'; +import 'package:niogu_app/core/utils/extension_format.dart'; +import 'package:niogu_app/core/utils/log_message.dart'; +import 'package:niogu_app/core/widgets/custom_snackbar.dart'; +import 'package:niogu_app/core/widgets/custom_text_form_field.dart'; +import 'package:niogu_app/features/online_store/domain/entities/online_store.dart'; +import 'package:niogu_app/features/online_store/presentation/providers/online_store_provider.dart'; +import 'package:sizer/sizer.dart'; + +class ShippingConfigurationScreen extends ConsumerStatefulWidget { + final ShippingConfiguration configuration; + const ShippingConfigurationScreen({super.key, required this.configuration}); + + @override + ConsumerState createState() => + _ShippingConfigurationScreenState(); +} + +class _ShippingConfigurationScreenState + extends ConsumerState { + final GlobalKey _formKey = GlobalKey(); + + final TextEditingController _maxRadiusDeliveryKmController = + TextEditingController(); + + final TextEditingController _deliveryFeeController = TextEditingController(); + + final TextEditingController _deliveryBaseFeeController = + TextEditingController(); + + bool _allowCOD = true; + + DeliveryFeeType _deliveryFeeType = DeliveryFeeType.fixed; + + @override + void initState() { + // TODO: implement initState + super.initState(); + + final configuration = widget.configuration; + + _allowCOD = configuration.allowCOD; + + if (configuration.maxDeliveryRadiusKm > 0) { + _maxRadiusDeliveryKmController.text = configuration.maxDeliveryRadiusKm + .toStringWithoutTrailingZero(); + } + + _deliveryFeeType = configuration.deliveryFeeType; + + final double deliveryFee = _deliveryFeeType == DeliveryFeeType.fixed + ? configuration.deliveryFlatFee + : configuration.deliveryFeePerKm; + + if (deliveryFee > 0) { + _deliveryFeeController.text = deliveryFee.toStringWithoutTrailingZero(); + } + + final double deliveryBaseFee = _deliveryFeeType == DeliveryFeeType.fixed + ? configuration.deliveryFlatFee + : configuration.deliveryBaseFee; + + if (deliveryBaseFee > 0) { + _deliveryBaseFeeController.text = deliveryBaseFee + .toStringWithoutTrailingZero(); + } + } + + @override + void dispose() { + // TODO: implement dispose + _maxRadiusDeliveryKmController.dispose(); + _deliveryFeeController.dispose(); + _deliveryBaseFeeController.dispose(); + super.dispose(); + } + + String? _validator({ + String? value, + required String message1, + required String message2, + required String message3, + bool isReadOnly = false, + }) { + if (isReadOnly) return null; + + if (value == null || value.isEmpty) { + return message1; + } + + if (double.tryParse(value) == null) { + return message2; + } + + if (double.parse(value) <= 0) { + return message3; + } + + return null; + } + + Future _shippingConfigureation() async { + if (!_formKey.currentState!.validate()) return; + + final maxDeliveryRadiusKm = double.parse( + _maxRadiusDeliveryKmController.text.trim(), + ); + + final deliveryFlatFee = _deliveryFeeType == DeliveryFeeType.fixed + ? double.parse(_deliveryFeeController.text.trim()) + : 0.0; + + final deliveryFeePerKm = _deliveryFeeType == DeliveryFeeType.per_km + ? double.parse(_deliveryFeeController.text.trim()) + : 0.0; + + final deliveryBaseFee = _deliveryFeeType == DeliveryFeeType.per_km + ? double.parse(_deliveryBaseFeeController.text.trim()) + : 0.0; + + final configure = ShippingConfiguration( + allowCOD: _allowCOD, + deliveryFeeType: _deliveryFeeType, + maxDeliveryRadiusKm: maxDeliveryRadiusKm, + deliveryFlatFee: deliveryFlatFee, + deliveryFeePerKm: deliveryFeePerKm, + deliveryBaseFee: deliveryBaseFee, + ); + + try { + await ref + .read(onlineStoreControllerProvider.notifier) + .shippingConfiguration(configure); + + if (!mounted) false; + + CustomSnackbar.showSuccess( + context, + "Biaya pengiriman berhasil diperbarui", + ); + context.pop(); + } catch (e, st) { + LogMessage.log.e(e.toString(), error: e, stackTrace: st); + CustomSnackbar.showError(context, "Ups, terjadi kesalahan"); + } + } + + @override + Widget build(BuildContext context) { + return LayoutBuilder( + builder: (context, constraints) { + final bool isTablet = 100.w >= 600; + + final onlineStoreControllerState = ref.watch( + onlineStoreControllerProvider, + ); + + return SafeArea( + child: Scaffold( + backgroundColor: const Color(0xFFF9FAFB), + appBar: TopBackBarApp( + title: "Atur Biaya Pengiriman", + onTap: () => context.pop(), + ), + body: SingleChildScrollView( + padding: EdgeInsets.all(5.w), + child: Form( + key: _formKey, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + padding: EdgeInsets.all(4.w), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(3.w), + border: Border.all(color: Colors.grey.shade200), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + Icon( + Icons.payments_outlined, + color: AppColor.primaryColor, + size: 6.w, + ), + SizedBox(width: 3.w), + Text( + "Izinkan Pesanan COD", + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + ), + ), + ], + ), + SizedBox( + width: 10.w, + child: FittedBox( + fit: BoxFit.fill, + child: Switch( + value: _allowCOD, + onChanged: (_) { + setState(() { + _allowCOD = !_allowCOD; + }); + }, + activeColor: _allowCOD + ? AppColor.primaryColor + : Colors.grey, + ), + ), + ), + ], + ), + ), + + SizedBox(height: 3.h), + + Padding( + padding: EdgeInsets.only(left: 1.w, bottom: 1.5.h), + child: Text( + "Metode & Biaya Pengiriman", + style: TextStyle( + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + fontWeight: FontWeight.bold, + color: Colors.black87, + ), + ), + ), + + Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(3.w), + border: Border.all(color: Colors.grey.shade200), + ), + child: Column( + children: [ + _buildMethodTile( + isTablet, + "Biaya Pengiriman Tetap", + DeliveryFeeType.fixed, + ), + const Divider(height: 1), + _buildMethodTile( + isTablet, + "Biaya Pengiriman Per Km", + DeliveryFeeType.per_km, + ), + ], + ), + ), + + SizedBox(height: 3.h), + CustomTextFormField( + label: "Radius Jarak Pengiriman (Km)", + hint: "Contoh: 10", + addInformation: + "Radius jarak pengiriman dari lokasi outlet dalam km", + controller: _maxRadiusDeliveryKmController, + suffixText: "/ Km", + keyboardType: TextInputType.number, + validator: (value) { + return _validator( + value: value, + message1: "Radius tidak belum diisi", + message2: "Radius tidak valid", + message3: "Radius harus lebih dari 0 km", + ); + }, + ), + SizedBox(height: 2.h), + CustomTextFormField( + label: _deliveryFeeType == DeliveryFeeType.fixed + ? "Biaya Pengiriman Tetap" + : "Biaya Pengiriman Per Km", + hint: "Contoh: 7.000", + controller: _deliveryFeeController, + suffixText: _deliveryFeeType == DeliveryFeeType.per_km + ? "/ Km" + : null, + keyboardType: TextInputType.number, + validator: (value) { + return _validator( + value: value, + message1: "Biaya pengiriman belum diisi", + message2: "Biaya pengiriman tidak valid", + message3: "Biaya pengiriman harus lebih dari 0", + ); + }, + ), + SizedBox(height: 2.h), + CustomTextFormField( + label: "Biaya Pengiriman Jarak < 1Km", + hint: "Contoh: 2.000", + controller: _deliveryFeeType == DeliveryFeeType.per_km + ? _deliveryBaseFeeController + : _deliveryFeeController, + keyboardType: TextInputType.number, + readOnly: _deliveryFeeType == DeliveryFeeType.fixed, + validator: (value) { + return _validator( + value: value, + message1: "Biaya pengiriman belum diisi", + message2: "Biaya pengiriman tidak valid", + message3: "Biaya pengiriman harus lebih dari 0", + isReadOnly: _deliveryFeeType == DeliveryFeeType.fixed, + ); + }, + ), + SizedBox(height: 5.h), + + ElevatedButton( + onPressed: onlineStoreControllerState.isLoading + ? null + : _shippingConfigureation, + style: ElevatedButton.styleFrom( + backgroundColor: AppColor.primaryColor, + minimumSize: Size(double.infinity, 7.h), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(3.w), + ), + disabledBackgroundColor: Colors.grey.shade300, + ), + child: Text( + "Simpan Pengaturan", + style: TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + fontSize: AppFontSize.medium.sp, + ), + ), + ), + ], + ), + ), + ), + ), + ); + }, + ); + } + + Widget _buildMethodTile( + bool isTablet, + String title, + DeliveryFeeType deliveryFeeType, + ) { + bool isSelected = _deliveryFeeType == deliveryFeeType; + return InkWell( + onTap: () => setState(() => _deliveryFeeType = deliveryFeeType), + child: Padding( + padding: EdgeInsets.all(4.w), + child: Row( + children: [ + Icon( + isSelected ? Icons.radio_button_checked : Icons.radio_button_off, + color: isSelected ? AppColor.primaryColor : Colors.grey, + size: 5.w, + ), + SizedBox(width: 3.w), + Text( + title, + style: TextStyle( + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + fontWeight: isSelected ? FontWeight.bold : FontWeight.normal, + ), + ), + ], + ), + ), + ); + } +} diff --git a/Mobile Operasional/lib/features/online_store/presentation/screens/target_outlet_screen.dart b/Mobile Operasional/lib/features/online_store/presentation/screens/target_outlet_screen.dart new file mode 100644 index 0000000..7d26d07 --- /dev/null +++ b/Mobile Operasional/lib/features/online_store/presentation/screens/target_outlet_screen.dart @@ -0,0 +1,244 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; +import 'package:niogu_app/core/components/top_back_bar_app.dart'; +import 'package:niogu_app/core/constants/app_color.dart'; +import 'package:niogu_app/core/constants/app_font_size.dart'; +import 'package:niogu_app/core/widgets/custom_error_screen.dart'; +import 'package:niogu_app/features/online_store/domain/entities/online_store.dart'; +import 'package:niogu_app/features/online_store/presentation/providers/online_store_provider.dart'; +import 'package:niogu_app/features/stock_in/presentation/widgets/checkbox_button.dart'; +import 'package:sizer/sizer.dart'; + +class TargetOutletScreen extends ConsumerWidget { + const TargetOutletScreen({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + return LayoutBuilder( + builder: (context, constraints) { + final bool isTablet = 100.w >= 600; + + final targetOutletState = ref.watch(targetOutletStreamProvider); + + final selectedOutlet = ref.watch( + selectedTargetOutletControllerProvider, + ); + + return SafeArea( + top: false, + bottom: true, + right: false, + left: false, + child: Scaffold( + appBar: TopBackBarApp( + title: "Tujuan Campaign", + onTap: () => context.pop(), + ), + body: targetOutletState.when( + data: (outlets) { + final bool isAllSelected = + selectedOutlet.isNotEmpty && + (outlets.length == selectedOutlet.values.length); + + return Column( + children: [ + InkWell( + onTap: () { + final selectedOutletState = ref.read( + selectedTargetOutletControllerProvider, + ); + + if (selectedOutletState.isNotEmpty && + (outlets.length == + selectedOutletState.values.length)) { + ref + .read( + selectedTargetOutletControllerProvider.notifier, + ) + .clear(); + return; + } + + for (final outlet in outlets) { + final selectedOutlet = SelectedTargetOutlet( + outletId: outlet.id, + name: outlet.name, + ); + + ref + .read( + selectedTargetOutletControllerProvider.notifier, + ) + .toggleSelectAll(selectedOutlet); + } + }, + child: Container( + padding: EdgeInsets.symmetric( + horizontal: 5.w, + vertical: 1.5.h, + ), + color: Colors.blue[50]!.withOpacity(0.5), + child: Row( + children: [ + CheckboxButton(isSelected: isAllSelected), + SizedBox(width: 4.w), + Text( + "Pilih Semua Outlet", + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + color: Colors.blue[800], + ), + ), + ], + ), + ), + ), + + Expanded( + child: ListView.builder( + padding: EdgeInsets.fromLTRB(4.w, 1.h, 4.w, 5.h), + itemCount: outlets.length, + itemBuilder: (context, index) { + final outlet = outlets[index]; + return Container( + margin: EdgeInsets.only(bottom: 1.5.h), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(2.5.w), + border: Border.all(color: Colors.grey.shade200), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.02), + blurRadius: 4, + offset: const Offset(0, 2), + ), + ], + ), + child: InkWell( + onTap: () { + final selectedOutlet = SelectedTargetOutlet( + outletId: outlet.id, + name: outlet.name, + ); + ref + .read( + selectedTargetOutletControllerProvider + .notifier, + ) + .toggle(selectedOutlet); + }, + borderRadius: BorderRadius.circular(2.5.w), + child: Padding( + padding: EdgeInsets.all(4.w), + child: Row( + children: [ + CheckboxButton( + isSelected: selectedOutlet.containsKey( + outlet.id, + ), + ), + SizedBox(width: 4.w), + Icon( + Icons.storefront_rounded, + color: Colors.grey[400], + size: 6.w, + ), + SizedBox(width: 3.w), + Expanded( + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text( + outlet.name, + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + color: Colors.black87, + ), + ), + + if (outlet.address != null && + outlet.address!.isNotEmpty) ...[ + SizedBox(height: 0.5.h), + Text( + outlet.address!, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + color: Colors.grey[600], + fontSize: isTablet + ? (AppFontSize.medium - + 1.25) + .sp + : (AppFontSize.small - 1.25) + .sp, + ), + ), + ], + ], + ), + ), + ], + ), + ), + ), + ); + }, + ), + ), + ], + ); + }, + error: (error, stackTrace) => CustomErrorScreen( + message: "Ups, terjadi kesalahan", + onRefresh: () {}, + ), + loading: () => const SizedBox(), + ), + bottomNavigationBar: Container( + padding: EdgeInsets.all(5.w), + decoration: BoxDecoration( + color: Colors.white, + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.05), + blurRadius: 10, + offset: const Offset(0, -5), + ), + ], + ), + child: ElevatedButton( + onPressed: selectedOutlet.values.isEmpty + ? null + : () => context.pop(), + style: ElevatedButton.styleFrom( + backgroundColor: AppColor.primaryColor, + minimumSize: Size(double.infinity, 6.5.h), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(3.w), + ), + disabledBackgroundColor: Colors.grey[300], + ), + child: Text( + "Pilih", + style: TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + fontSize: AppFontSize.medium.sp, + ), + ), + ), + ), + ), + ); + }, + ); + } +} diff --git a/Mobile Operasional/lib/features/online_store/presentation/widgets/delivery_info_card.dart b/Mobile Operasional/lib/features/online_store/presentation/widgets/delivery_info_card.dart new file mode 100644 index 0000000..b3e3804 --- /dev/null +++ b/Mobile Operasional/lib/features/online_store/presentation/widgets/delivery_info_card.dart @@ -0,0 +1,124 @@ +import 'package:flutter/material.dart'; +import 'package:niogu_app/core/constants/app_color.dart'; +import 'package:niogu_app/core/constants/app_font_size.dart'; +import 'package:niogu_app/core/enums/delivery_type.dart'; +import 'package:sizer/sizer.dart'; + +class DeliveryInfoCard extends StatelessWidget { + final DeliveryType deliveryType; + final String customerName; + final String phoneNumber; + final String fullAddress; + final String preference; + const DeliveryInfoCard({ + super.key, + required this.deliveryType, + required this.customerName, + required this.phoneNumber, + required this.fullAddress, + required this.preference, + }); + + @override + Widget build(BuildContext context) { + final bool isTablet = 100.w >= 600; + + final isDelivery = deliveryType == DeliveryType.delivery; + return Container( + padding: EdgeInsets.all(4.w), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(4.w), + border: Border.all(color: Colors.grey.shade200), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.02), + blurRadius: 10, + offset: const Offset(0, 4), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon( + deliveryType == DeliveryType.pick_up + ? Icons.store_outlined + : Icons.local_shipping_outlined, + size: 5.w, + color: AppColor.primaryColor, + ), + SizedBox(width: 2.w), + Text( + deliveryType == DeliveryType.pick_up + ? "Ambil Ditoko" + : "Informasi Pengiriman", + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: (AppFontSize.medium - (isTablet ? 0 : 1.25)).sp, + color: Colors.black87, + ), + ), + ], + ), + SizedBox(height: 2.h), + _buildInfoRow(isTablet, "Pembeli", customerName), + _buildInfoRow(isTablet, "Telepon", phoneNumber), + + if (isDelivery) ...[ + Divider(height: 3.h), + _buildInfoRow(isTablet, "Alamat", fullAddress), + ], + _buildInfoRow( + isTablet, + isDelivery ? "Preferensi" : "Diambil Pukul", + preference, + isBold: true, + ), + ], + ), + ); + } + + Widget _buildInfoRow( + bool isTablet, + String label, + String value, { + bool isBold = false, + }) { + return Padding( + padding: EdgeInsets.symmetric(vertical: 0.8.h), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + width: 25.w, + child: Text( + label, + style: TextStyle( + fontSize: isTablet + ? (AppFontSize.medium - 1.25).sp + : (AppFontSize.small - 1.25).sp, + color: Colors.grey[600], + ), + ), + ), + Expanded( + child: Text( + value, + style: TextStyle( + fontSize: isTablet + ? (AppFontSize.medium - 1.25).sp + : (AppFontSize.small - 1.25).sp, + fontWeight: isBold ? FontWeight.bold : FontWeight.normal, + color: Colors.black87, + ), + ), + ), + ], + ), + ); + } +} diff --git a/Mobile Operasional/lib/features/online_store/presentation/widgets/domain_card.dart b/Mobile Operasional/lib/features/online_store/presentation/widgets/domain_card.dart new file mode 100644 index 0000000..d0ac67d --- /dev/null +++ b/Mobile Operasional/lib/features/online_store/presentation/widgets/domain_card.dart @@ -0,0 +1,85 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:niogu_app/core/constants/app_color.dart'; +import 'package:niogu_app/core/constants/app_font_size.dart'; +import 'package:niogu_app/core/widgets/custom_snackbar.dart'; +import 'package:sizer/sizer.dart'; + +class DomainCard extends StatelessWidget { + final String domainName; + const DomainCard({super.key, required this.domainName}); + + @override + Widget build(BuildContext context) { + final bool isTablet = 100.w >= 600; + return Container( + padding: EdgeInsets.all(5.w), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(4.w), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.05), + blurRadius: 10, + offset: const Offset(0, 4), + ), + ], + ), + child: Row( + children: [ + CircleAvatar( + radius: 7.w, + backgroundColor: AppColor.primaryColor.withOpacity(0.1), + child: Icon( + Icons.language, + color: AppColor.primaryColor, + size: 8.w, + ), + ), + SizedBox(width: 4.w), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + domainName, + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + color: Colors.black87, + ), + ), + + SizedBox(height: 0.75.h), + + Text( + "Domain aktif", + style: TextStyle( + fontSize: isTablet + ? (AppFontSize.medium - 1.25).sp + : (AppFontSize.small - 1.25).sp, + color: Colors.green, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + ), + IconButton( + onPressed: () { + Clipboard.setData(ClipboardData(text: domainName)); + CustomSnackbar.showSuccess( + context, + "Domain berhasil disalin", + ); + }, + icon: Icon(Icons.copy_rounded, size: 5.w, color: Colors.grey), + ), + ], + ), + ); + + } +} \ No newline at end of file diff --git a/Mobile Operasional/lib/features/online_store/presentation/widgets/information_campaign.dart b/Mobile Operasional/lib/features/online_store/presentation/widgets/information_campaign.dart new file mode 100644 index 0000000..9368125 --- /dev/null +++ b/Mobile Operasional/lib/features/online_store/presentation/widgets/information_campaign.dart @@ -0,0 +1,42 @@ +import 'package:flutter/material.dart'; +import 'package:niogu_app/core/constants/app_color.dart'; +import 'package:niogu_app/core/constants/app_font_size.dart'; +import 'package:sizer/sizer.dart'; + +class InformationCampaign extends StatelessWidget { + final bool isTablet; + final String information; + const InformationCampaign({super.key, required this.isTablet, required this.information}); + + @override + Widget build(BuildContext context) { + return Padding( + padding: EdgeInsets.only(bottom: 1.5.h), + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon( + Icons.info_outline, + size: 3.5.w, + color: AppColor.primaryColor, + ), + SizedBox(width: 0.75.w), + Text( + information, + style: TextStyle( + color: AppColor.primaryColor, + fontSize: isTablet + ? (AppFontSize.medium - 1.25).sp + : (AppFontSize.small - 1.25).sp, + ), + ), + ], + ), + ], + ), + ); + } +} \ No newline at end of file diff --git a/Mobile Operasional/lib/features/online_store/presentation/widgets/online_store_order_shimmer.dart b/Mobile Operasional/lib/features/online_store/presentation/widgets/online_store_order_shimmer.dart new file mode 100644 index 0000000..cebb393 --- /dev/null +++ b/Mobile Operasional/lib/features/online_store/presentation/widgets/online_store_order_shimmer.dart @@ -0,0 +1,78 @@ +import 'package:flutter/material.dart'; +import 'package:shimmer/shimmer.dart'; +import 'package:sizer/sizer.dart'; + +class OnlineStoreOrderShimmer extends StatelessWidget { + const OnlineStoreOrderShimmer({super.key}); + + @override + Widget build(BuildContext context) { + return Shimmer.fromColors( + baseColor: Colors.grey[300]!, + highlightColor: Colors.grey[100]!, + child: ListView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + padding: EdgeInsets.symmetric(horizontal: 5.w, vertical: 2.h), + itemCount: 5, + itemBuilder: (context, index) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (index == 0) ...[ + Container( + width: 30.w, + height: 2.h, + margin: EdgeInsets.only(bottom: 2.h, left: 2.w), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(1.w), + ), + ), + ], + + Padding( + padding: EdgeInsets.symmetric(vertical: 1.5.h, horizontal: 2.w), + child: Row( + children: [ + Container( + width: 15.w, + height: 15.w, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(2.5.w), + ), + ), + SizedBox(width: 4.w), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + width: 25.w, + height: 1.8.h, + color: Colors.white, + ), + SizedBox(height: 1.h), + Container( + width: 40.w, + height: 1.5.h, + color: Colors.white, + ), + ], + ), + ), + // Ikon Panah Navigasi + Icon(Icons.chevron_right, color: Colors.white, size: 5.w), + ], + ), + ), + + Divider(height: 1, color: Colors.grey[200]), + ], + ); + }, + ), + ); + } +} \ No newline at end of file diff --git a/Mobile Operasional/lib/features/online_store/presentation/widgets/online_store_product_shimmer.dart b/Mobile Operasional/lib/features/online_store/presentation/widgets/online_store_product_shimmer.dart new file mode 100644 index 0000000..4b153ac --- /dev/null +++ b/Mobile Operasional/lib/features/online_store/presentation/widgets/online_store_product_shimmer.dart @@ -0,0 +1,116 @@ +import 'package:flutter/material.dart'; +import 'package:shimmer/shimmer.dart'; +import 'package:sizer/sizer.dart'; + +class OnlineStoreProductShimmer extends StatelessWidget { + const OnlineStoreProductShimmer({super.key}); + + @override + Widget build(BuildContext context) { + final baseColor = Colors.grey[300]!; + final highlightColor = Colors.grey[100]!; + const backgroundColor = Color(0xFFF9FAFB); + + return Scaffold( + backgroundColor: backgroundColor, + body: Shimmer.fromColors( + baseColor: baseColor, + highlightColor: highlightColor, + child: Column( + children: [ + Container( + margin: EdgeInsets.fromLTRB(5.w, 3.h, 5.w, 2.h), + height: 6.h, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(2.5.w), + ), + ), + + Expanded( + child: ListView.builder( + padding: EdgeInsets.fromLTRB(5.w, 0, 5.w, 5.h), + itemCount: 6, + itemBuilder: (context, index) { + return Container( + margin: EdgeInsets.only(bottom: 2.h), + padding: EdgeInsets.all(3.w), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(4.w), + ), + child: Row( + children: [ + Container( + width: 12.w, + height: 12.w, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(2.5.w), + ), + ), + + SizedBox(width: 4.w), + + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Judul Produk + Container( + width: 40.w, + height: 1.5.h, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(1.w), + ), + ), + SizedBox(height: 1.h), + Container( + width: 20.w, + height: 1.2.h, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(1.w), + ), + ), + ], + ), + ), + Container( + width: 6.w, + height: 6.w, + decoration: const BoxDecoration( + color: Colors.white, + shape: BoxShape.circle, + ), + ), + ], + ), + ); + }, + ), + ), + + Container( + padding: EdgeInsets.all(5.w), + decoration: const BoxDecoration( + color: Colors.white, + boxShadow: [BoxShadow(color: Colors.white, blurRadius: 10, offset: Offset(0, -5))] + ), + child: Container( + width: double.infinity, + height: 6.5.h, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(2.5.w), + ), + ), + ), + ], + ), + ), + ); + + } +} \ No newline at end of file diff --git a/Mobile Operasional/lib/features/online_store/presentation/widgets/order_card.dart b/Mobile Operasional/lib/features/online_store/presentation/widgets/order_card.dart new file mode 100644 index 0000000..e7a9df9 --- /dev/null +++ b/Mobile Operasional/lib/features/online_store/presentation/widgets/order_card.dart @@ -0,0 +1,118 @@ +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; +import 'package:niogu_app/core/constants/app_color.dart'; +import 'package:niogu_app/core/constants/app_font_size.dart'; +import 'package:niogu_app/core/utils/currency_format.dart'; +import 'package:niogu_app/core/utils/time_zone.dart'; +import 'package:niogu_app/features/online_store/domain/entities/online_store.dart'; +import 'package:niogu_app/features/online_store/presentation/widgets/product_thumbnail.dart'; +import 'package:sizer/sizer.dart'; + +class OrderCard extends StatelessWidget { + final OnlineOrder order; + final VoidCallback onTap; + const OrderCard({super.key, required this.order, required this.onTap}); + + @override + Widget build(BuildContext context) { + final bool isTablet = 100.w >= 600; + return GestureDetector( + onTap: onTap, + child: Container( + clipBehavior: Clip.antiAlias, + padding: EdgeInsets.all(4.w), + decoration: BoxDecoration( + color: Colors.white, + border: Border(bottom: BorderSide(color: Colors.grey.shade100)), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.02), + blurRadius: 5, + offset: const Offset(0, 2), + ), + ], + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ProductThumbnail(imagePath: order.displayImage), + SizedBox(width: 4.w), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + order.orderNumber, + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: isTablet + ? (AppFontSize.medium - 1.25).sp + : (AppFontSize.small - 1.25).sp, + ), + ), + Text( + "${DateFormat('HH:mm').format(order.orderDate)} ${TimeZone.getCurrentTimeZone()}", + style: TextStyle( + color: Colors.grey.shade700, + fontSize: isTablet + ? (AppFontSize.medium - 1.25).sp + : (AppFontSize.small - 1.25).sp, + ), + ), + ], + ), + SizedBox(height: 1.h), + _buildOrderInfoRow( + isTablet, + Icons.shopping_bag_outlined, + "${order.itemCount} Produk", + ), + SizedBox(height: 0.75.h), + _buildOrderInfoRow( + isTablet, + Icons.payments_outlined, + CurrencyFormat.formatToIdr(order.orderTotal, 0), + isPrice: true, + ), + ], + ), + ), + SizedBox(width: 2.w), + Icon(Icons.chevron_right, size: 4.5.w, color: Colors.grey.shade400), + ], + ), + ), + ); + } + + Widget _buildOrderInfoRow( + bool isTablet, + IconData icon, + String label, { + bool isPrice = false, + }) { + return Row( + children: [ + Icon( + icon, + size: 5.w, + color: isPrice ? AppColor.primaryColor : Colors.grey, + ), + SizedBox(width: 2.w), + Text( + label, + style: TextStyle( + fontSize: isTablet + ? (AppFontSize.medium - 1.25).sp + : (AppFontSize.small - 1.25).sp, + color: isPrice ? AppColor.primaryColor : Colors.grey[700], + fontWeight: isPrice ? FontWeight.bold : FontWeight.normal, + ), + ), + ], + ); + } +} diff --git a/Mobile Operasional/lib/features/online_store/presentation/widgets/order_header_status.dart b/Mobile Operasional/lib/features/online_store/presentation/widgets/order_header_status.dart new file mode 100644 index 0000000..47526ce --- /dev/null +++ b/Mobile Operasional/lib/features/online_store/presentation/widgets/order_header_status.dart @@ -0,0 +1,243 @@ +import 'package:flutter/material.dart'; +import 'package:niogu_app/core/constants/app_color.dart'; +import 'package:niogu_app/core/constants/app_font_size.dart'; +import 'package:niogu_app/core/enums/approval_process.dart'; +import 'package:niogu_app/core/enums/canceled_by.dart'; +import 'package:niogu_app/core/enums/order_status.dart'; +import 'package:sizer/sizer.dart'; + +class OrderHeaderStatus extends StatelessWidget { + final String outletName; + final String orderNumber; + final String orderStatus; + final Color colorStatus; + final String orderDate; + final String? notes; + final bool isCancellation; + final bool cancellationAccepted; + final ApprovalProcess process; + final OrderStatus status; + final CanceledBy? canceledBy; + const OrderHeaderStatus({ + super.key, + required this.outletName, + required this.orderNumber, + required this.orderStatus, + required this.colorStatus, + required this.orderDate, + this.notes, + this.isCancellation = false, + this.cancellationAccepted = false, + required this.process, + required this.status, + this.canceledBy, + }); + + @override + Widget build(BuildContext context) { + final bool isTablet = 100.w >= 600; + return Container( + width: double.infinity, + padding: EdgeInsets.all(4.w), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(3.w), + border: Border.all(color: Colors.grey.shade200), + ), + child: Column( + children: [ + Container( + width: 16.w, + height: 16.w, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: Colors.grey[100], + border: Border.all(color: Colors.grey.shade200), + ), + child: Icon( + Icons.store_rounded, + color: AppColor.primaryColor, + size: 8.w, + ), + ), + SizedBox(height: 1.5.h), + Text( + outletName, + style: TextStyle( + fontSize: AppFontSize.medium.sp, + fontWeight: FontWeight.bold, + color: Colors.black87, + ), + ), + + SizedBox(height: 0.5.h), + + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + "Powered by ", + style: TextStyle( + fontSize: (AppFontSize.small - 2).sp, + color: Colors.grey[500], + ), + ), + Text( + "Niogu", + style: TextStyle( + fontSize: (AppFontSize.small - 2).sp, + fontWeight: FontWeight.bold, + color: AppColor.primaryColor.withOpacity(0.7), + ), + ), + ], + ), + + SizedBox(height: 1.5.h), + + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + orderNumber, + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + ), + ), + Container( + padding: EdgeInsets.symmetric(horizontal: 3.w, vertical: 0.6.h), + decoration: BoxDecoration( + color: colorStatus.withOpacity(0.1), + borderRadius: BorderRadius.circular(5.w), + border: Border.all(color: colorStatus.withOpacity(0.2)), + ), + child: Text( + orderStatus, + style: TextStyle( + color: colorStatus, + fontWeight: FontWeight.bold, + fontSize: isTablet + ? (AppFontSize.medium - 1.25).sp + : (AppFontSize.small - 1.25).sp, + ), + ), + ), + ], + ), + Divider(height: 3.h), + Row( + children: [ + Icon( + Icons.calendar_today_outlined, + size: 4.w, + color: Colors.grey, + ), + SizedBox(width: 2.w), + Text( + orderDate, + style: TextStyle( + color: Colors.grey.shade700, + fontSize: isTablet + ? (AppFontSize.medium - 1.25).sp + : (AppFontSize.small - 1.25).sp, + ), + ), + ], + ), + + if (notes != null && notes!.isNotEmpty) ...[ + SizedBox(height: 2.h), + + Row( + children: [ + Icon(Icons.note_alt_outlined, size: 4.w, color: Colors.grey), + SizedBox(width: 2.w), + Text( + "Catatan", + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: isTablet + ? (AppFontSize.medium - 1.25).sp + : (AppFontSize.small - 1.25).sp, + ), + ), + ], + ), + SizedBox(height: 1.h), + Row( + children: [ + Text( + notes!, + style: TextStyle( + fontSize: isTablet + ? (AppFontSize.medium - 1.25).sp + : (AppFontSize.small - 1.25).sp, + color: Colors.grey[700], + fontStyle: FontStyle.italic, + ), + ), + ], + ), + ], + + if (isCancellation) ...[ + SizedBox(height: 2.h), + + Row( + children: [ + Icon( + process == ApprovalProcess.waiting + ? Icons.pending_outlined + : cancellationAccepted + ? Icons.check_outlined + : Icons.close_outlined, + size: 4.w, + color: Colors.grey, + ), + SizedBox(width: 2.w), + Text( + process == ApprovalProcess.waiting + ? "Menunggu respon persetujuan" + : cancellationAccepted + ? "Permintaan pembatalan pesanan diterima" + : "Permintaan pembatalan pesanan ditolak", + style: TextStyle( + color: Colors.black87, + fontWeight: FontWeight.bold, + fontSize: isTablet + ? (AppFontSize.medium - 1.25).sp + : (AppFontSize.small - 1.25).sp, + ), + ), + ], + ), + ], + + if (status == OrderStatus.canceled && canceledBy != null) ...[ + SizedBox(height: 2.h), + + Row( + children: [ + Icon(Icons.person_outlined, size: 4.w, color: Colors.grey), + SizedBox(width: 2.w), + Text( + "Dibatalkan oleh ${canceledBy == CanceledBy.user ? 'admin' : 'pelanggan'}", + style: TextStyle( + color: Colors.black87, + fontWeight: FontWeight.bold, + fontSize: isTablet + ? (AppFontSize.medium - 1.25).sp + : (AppFontSize.small - 1.25).sp, + ), + ), + ], + ), + ], + ], + ), + ); + } +} diff --git a/Mobile Operasional/lib/features/online_store/presentation/widgets/payment_info.dart b/Mobile Operasional/lib/features/online_store/presentation/widgets/payment_info.dart new file mode 100644 index 0000000..bde3f2a --- /dev/null +++ b/Mobile Operasional/lib/features/online_store/presentation/widgets/payment_info.dart @@ -0,0 +1,180 @@ +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:niogu_app/core/constants/app_color.dart'; +import 'package:niogu_app/core/constants/app_font_size.dart'; +import 'package:niogu_app/core/widgets/custom_snackbar.dart'; +import 'package:sizer/sizer.dart'; + +class PaymentInfo extends StatelessWidget { + final String paymentMethod; + final String paymentProof; + const PaymentInfo({ + super.key, + required this.paymentMethod, + required this.paymentProof, + }); + + @override + Widget build(BuildContext context) { + final bool isTablet = 100.w >= 600; + return Container( + padding: EdgeInsets.all(4.w), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(4.w), + border: Border.all(color: Colors.grey.shade200), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.02), + blurRadius: 10, + offset: const Offset(0, 4), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon( + Icons.account_balance_wallet_outlined, + size: 5.w, + color: AppColor.primaryColor, + ), + SizedBox(width: 2.w), + Text( + "Informasi Pembayaran", + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: (AppFontSize.medium - (isTablet ? 0 : 1.25)).sp, + color: Colors.black87, + ), + ), + ], + ), + SizedBox(height: 2.h), + + Material( + color: Colors.transparent, + child: InkWell( + onTap: paymentMethod == 'COD' + ? null + : () { + final File imageFile = File(paymentProof); + + final bool imageFileExists = imageFile.existsSync(); + + if (!imageFileExists) { + CustomSnackbar.showError( + context, + "Ups, terjadi kesalahan", + ); + return; + } + + showDialog( + context: context, + useRootNavigator: true, + builder: (context) => Dialog( + insetPadding: EdgeInsets.symmetric( + horizontal: 2.w, + vertical: 2.h, + ), + backgroundColor: Colors.transparent, + child: GestureDetector( + onTap: () => Navigator.pop(context), + child: Container( + width: MediaQuery.of(context).size.width, + constraints: BoxConstraints( + maxWidth: 85.w, + maxHeight: 85.h, + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(4.w), + child: InteractiveViewer( + child: Image.file( + File(paymentProof), + fit: BoxFit.contain, + width: double.infinity, + ), + ), + ), + ), + ), + ), + ); + }, + borderRadius: BorderRadius.circular(2.w), + child: Container( + padding: EdgeInsets.all(3.w), + decoration: BoxDecoration( + color: Colors.grey.shade50, + borderRadius: BorderRadius.circular(2.w), + border: Border.all(color: Colors.grey.shade200), + ), + child: Row( + children: [ + Container( + padding: EdgeInsets.all(2.w), + decoration: BoxDecoration( + color: Colors.white, + shape: BoxShape.circle, + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.05), + blurRadius: 5, + ), + ], + ), + child: Icon( + paymentMethod == 'COD' + ? Icons.payments_outlined + : Icons.receipt_long_outlined, + color: AppColor.primaryColor, + size: 5.w, + ), + ), + SizedBox(width: 3.w), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + paymentMethod, + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: isTablet + ? (AppFontSize.medium - 1.25).sp + : (AppFontSize.small - 1.25).sp, + ), + ), + if (paymentMethod != 'COD') + Text( + "Ketuk untuk lihat bukti pembayaran", + style: TextStyle( + fontSize: isTablet + ? (AppFontSize.medium - 1.25).sp + : (AppFontSize.small - 1.25).sp, + color: Colors.grey.shade600, + ), + ), + ], + ), + ), + if (paymentMethod != 'COD') + Icon( + Icons.arrow_forward_ios, + size: 3.5.w, + color: Colors.grey.shade400, + ), + ], + ), + ), + ), + ), + ], + ), + ); + } +} diff --git a/Mobile Operasional/lib/features/online_store/presentation/widgets/product_details.dart b/Mobile Operasional/lib/features/online_store/presentation/widgets/product_details.dart new file mode 100644 index 0000000..28ed5e4 --- /dev/null +++ b/Mobile Operasional/lib/features/online_store/presentation/widgets/product_details.dart @@ -0,0 +1,180 @@ +import 'package:flutter/material.dart'; +import 'package:niogu_app/core/constants/app_color.dart'; +import 'package:niogu_app/core/constants/app_font_size.dart'; +import 'package:niogu_app/core/utils/currency_format.dart'; +import 'package:niogu_app/core/utils/extension_format.dart'; +import 'package:niogu_app/features/online_store/domain/entities/online_store.dart'; +import 'package:niogu_app/features/online_store/presentation/widgets/product_thumbnail.dart'; +import 'package:sizer/sizer.dart'; + +class ProductDetails extends StatelessWidget { + final List items; + const ProductDetails({super.key, required this.items}); + + @override + Widget build(BuildContext context) { + final bool isTablet = 100.w >= 600; + return Container( + padding: EdgeInsets.all(4.w), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(4.w), + border: Border.all(color: Colors.grey.shade200), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.02), + blurRadius: 10, + offset: const Offset(0, 4), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon( + Icons.shopping_bag_outlined, + + size: 5.w, + color: AppColor.primaryColor, + ), + SizedBox(width: 2.w), + Text( + "Rincian Produk", + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: (AppFontSize.medium - (isTablet ? 0 : 1.25)).sp, + color: Colors.black87, + ), + ), + ], + ), + SizedBox(height: 2.h), + + ...items.map((item) { + return Padding( + padding: EdgeInsets.only(bottom: 2.h), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ProductThumbnail(imagePath: item.imagePath), + SizedBox(width: 3.w), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + item.name, + maxLines: 2, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: isTablet + ? (AppFontSize.medium - 1.25).sp + : (AppFontSize.small - 1.25).sp, + ), + ), + if (item.variantName != null && + item.variantName!.isNotEmpty) + Text( + item.variantName!, + style: TextStyle( + color: Colors.grey.shade700, + fontSize: isTablet + ? (AppFontSize.medium - 1.25).sp + : (AppFontSize.small - 1.25).sp, + ), + ), + + SizedBox(height: 1.h), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Harga Satuan", + style: TextStyle( + fontSize: isTablet + ? (AppFontSize.medium - 1.25).sp + : (AppFontSize.small - 1.25).sp, + color: Colors.grey.shade700, + ), + ), + Text( + CurrencyFormat.formatToIdr( + item.sellingPrice, + 0, + ), + style: TextStyle( + fontSize: isTablet + ? (AppFontSize.medium - 1.25).sp + : (AppFontSize.small - 1.25).sp, + color: Colors.black87, + ), + ), + ], + ), + Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text( + "Jumlah", + style: TextStyle( + fontSize: isTablet + ? (AppFontSize.medium - 1.25).sp + : (AppFontSize.small - 1.25).sp, + color: Colors.grey.shade700, + ), + ), + Text( + "x${item.quantity.toStringWithoutTrailingZero()}", + style: TextStyle( + fontSize: isTablet + ? (AppFontSize.medium - 1.25).sp + : (AppFontSize.small - 1.25).sp, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Text( + "Subtotal", + style: TextStyle( + fontSize: isTablet + ? (AppFontSize.medium - 1.25).sp + : (AppFontSize.small - 1.25).sp, + color: Colors.grey.shade700, + ), + ), + Text( + CurrencyFormat.formatToIdr(item.subtotal, 0), + style: TextStyle( + fontSize: isTablet + ? (AppFontSize.medium - 1.25).sp + : (AppFontSize.small - 1.25).sp, + fontWeight: FontWeight.bold, + color: AppColor.primaryColor, + ), + ), + ], + ), + ], + ), + ], + ), + ), + ], + ), + ); + }).toList(), + ], + ), + ); + } +} diff --git a/Mobile Operasional/lib/features/online_store/presentation/widgets/product_review_shimmer.dart b/Mobile Operasional/lib/features/online_store/presentation/widgets/product_review_shimmer.dart new file mode 100644 index 0000000..17526c4 --- /dev/null +++ b/Mobile Operasional/lib/features/online_store/presentation/widgets/product_review_shimmer.dart @@ -0,0 +1,62 @@ +import 'package:flutter/material.dart'; +import 'package:shimmer/shimmer.dart'; +import 'package:sizer/sizer.dart'; + +class ProductReviewShimmer extends StatelessWidget { + const ProductReviewShimmer({super.key}); + + @override + Widget build(BuildContext context) { + return Shimmer.fromColors( + baseColor: Colors.grey[300]!, + highlightColor: Colors.grey[100]!, + child: ListView.builder( + padding: EdgeInsets.all(5.w), + itemCount: 5, + itemBuilder: (context, index) { + return Container( + margin: EdgeInsets.only(bottom: 2.h), + padding: EdgeInsets.all(4.w), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(3.w), + border: Border.all(color: Colors.grey.shade200), + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + width: 20.w, + height: 20.w, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(2.w), + ), + ), + SizedBox(width: 4.w), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Container(width: 30.w, height: 2.h, color: Colors.white), + Container(width: 10.w, height: 2.h, color: Colors.white), + ], + ), + SizedBox(height: 2.h), + Container(width: 45.w, height: 1.5.h, color: Colors.white), + SizedBox(height: 1.h), + Container(width: 45.w, height: 1.5.h, color: Colors.white), + ], + ), + ), + ], + ), + ); + }, + ), + ); + } +} \ No newline at end of file diff --git a/Mobile Operasional/lib/features/online_store/presentation/widgets/product_thumbnail.dart b/Mobile Operasional/lib/features/online_store/presentation/widgets/product_thumbnail.dart new file mode 100644 index 0000000..c274e04 --- /dev/null +++ b/Mobile Operasional/lib/features/online_store/presentation/widgets/product_thumbnail.dart @@ -0,0 +1,50 @@ +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:sizer/sizer.dart'; + +class ProductThumbnail extends StatelessWidget { + final String? imagePath; + const ProductThumbnail({super.key, required this.imagePath}); + + @override + Widget build(BuildContext context) { + final bool imagePathAvailable = imagePath != null && imagePath!.isNotEmpty; + + final File imageFile = File( + imagePathAvailable ? imagePath! : "image not found", + ); + + final bool imageFileExists = imageFile.existsSync(); + + return RepaintBoundary( + child: Container( + width: 18.w, + height: 18.w, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(3.w), + border: Border.all(color: Colors.grey.shade200), + ), + child: imageFileExists + ? ClipRRect( + borderRadius: BorderRadius.circular(2.5.w), + child: Image.file( + imageFile, + fit: BoxFit.cover, + width: double.infinity, + height: double.infinity, + errorBuilder: (context, error, stackTrace) { + return Icon( + Icons.error, + color: Colors.grey[400], + size: 8.w, + ); + }, + ), + ) + : Icon(Icons.image_outlined, size: 8.w, color: Colors.grey[400]), + ), + ); + } +} diff --git a/Mobile Operasional/lib/features/online_store/presentation/widgets/section_campaign_label.dart b/Mobile Operasional/lib/features/online_store/presentation/widgets/section_campaign_label.dart new file mode 100644 index 0000000..0d31472 --- /dev/null +++ b/Mobile Operasional/lib/features/online_store/presentation/widgets/section_campaign_label.dart @@ -0,0 +1,23 @@ +import 'package:flutter/material.dart'; +import 'package:niogu_app/core/constants/app_font_size.dart'; +import 'package:sizer/sizer.dart'; + +class SectionCampaignLabel extends StatelessWidget { + final bool isTablet; + final String label; + const SectionCampaignLabel({super.key, required this.isTablet, required this.label}); + + @override + Widget build(BuildContext context) { + return Padding( + padding: EdgeInsets.only(bottom: 1.5.h, left: 1.w), + child: Text( + label, + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: isTablet ? AppFontSize.medium.sp : AppFontSize.small.sp, + ), + ), + ); + } +} \ No newline at end of file diff --git a/Mobile Operasional/lib/features/online_store/presentation/widgets/status_filter_item.dart b/Mobile Operasional/lib/features/online_store/presentation/widgets/status_filter_item.dart new file mode 100644 index 0000000..2740dc0 --- /dev/null +++ b/Mobile Operasional/lib/features/online_store/presentation/widgets/status_filter_item.dart @@ -0,0 +1,77 @@ +import 'package:flutter/material.dart'; +import 'package:niogu_app/core/constants/app_color.dart'; +import 'package:niogu_app/core/constants/app_font_size.dart'; +import 'package:sizer/sizer.dart'; + +class StatusFilterItem extends StatelessWidget { + final String label; + final bool isSelected; + final int count; + final VoidCallback onTap; + const StatusFilterItem({ + super.key, + required this.label, + required this.isSelected, + required this.count, + required this.onTap, + }); + + @override + Widget build(BuildContext context) { + final bool isTablet = 100.w >= 600; + return GestureDetector( + onTap: onTap, + child: AnimatedContainer( + duration: const Duration(milliseconds: 200), + margin: EdgeInsets.symmetric(horizontal: 1.5.w, vertical: 1.h), + padding: EdgeInsets.symmetric(horizontal: 4.w), + decoration: BoxDecoration( + color: isSelected ? AppColor.primaryColor : Colors.grey.shade100, + borderRadius: BorderRadius.circular(5.w), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + label, + style: TextStyle( + color: isSelected ? Colors.white : Colors.grey.shade700, + fontWeight: isSelected ? FontWeight.bold : FontWeight.w500, + fontSize: isTablet + ? (AppFontSize.medium - 1.25).sp + : (AppFontSize.small - 1.25).sp, + ), + ), + + // Angka jumlah pesanan yang terintegrasi + if (count > 0) ...[ + SizedBox(width: 2.w), + Container( + padding: EdgeInsets.symmetric( + horizontal: 1.8.w, + vertical: 0.2.h, + ), + decoration: BoxDecoration( + color: isSelected + ? Colors.white.withOpacity(0.2) + : Colors.grey.shade300, + borderRadius: BorderRadius.circular(2.5.w), + ), + child: Text( + count.toString(), + style: TextStyle( + color: isSelected ? Colors.white : Colors.grey.shade800, + fontSize: isTablet + ? (AppFontSize.medium - 1.25).sp + : (AppFontSize.small - 1.25).sp, + fontWeight: FontWeight.bold, + ), + ), + ), + ], + ], + ), + ), + ); + } +} diff --git a/Mobile Operasional/lib/features/online_store/presentation/widgets/status_step_trigger.dart b/Mobile Operasional/lib/features/online_store/presentation/widgets/status_step_trigger.dart new file mode 100644 index 0000000..64e0d90 --- /dev/null +++ b/Mobile Operasional/lib/features/online_store/presentation/widgets/status_step_trigger.dart @@ -0,0 +1,144 @@ +import 'package:flutter/material.dart'; +import 'package:niogu_app/core/constants/app_color.dart'; +import 'package:niogu_app/core/constants/app_font_size.dart'; +import 'package:niogu_app/core/enums/order_status.dart'; +import 'package:sizer/sizer.dart'; + +class StatusStepTrigger extends StatelessWidget { + final bool isDelivery; + final String currentStatus; + final void Function(String, Color, OrderStatus) onPressed; + const StatusStepTrigger({ + super.key, + required this.isDelivery, + required this.currentStatus, + required this.onPressed, + }); + + @override + Widget build(BuildContext context) { + final bool isTablet = 100.w >= 600; + + final bool canConfirm = + currentStatus == "Tertunda" || currentStatus == "Dikonfirmasi"; + + final bool canInProgress = + currentStatus == "Tertunda" || + currentStatus == "Dikonfirmasi" || + currentStatus == "Pengiriman"; + + final bool canCompleted = isDelivery + ? (currentStatus == "Pengiriman" || currentStatus == "Selesai") + : true; + + return Container( + padding: EdgeInsets.all(4.w), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(4.w), + border: Border.all(color: Colors.grey.shade200), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.02), + blurRadius: 10, + offset: const Offset(0, 4), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon( + Icons.edit_note_rounded, + + size: 5.w, + color: AppColor.primaryColor, + ), + SizedBox(width: 2.w), + Text( + "Perbarui Status Pesanan", + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: (AppFontSize.medium - (isTablet ? 0 : 1.25)).sp, + color: Colors.black87, + ), + ), + ], + ), + SizedBox(height: 2.h), + + SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Row( + children: [ + if (isDelivery) ...[ + _buildActionChip( + currentStatus == "Dikonfirmasi", + canConfirm, + "Dikonfirmasi", + canConfirm ? Colors.blue : Colors.grey.shade700, + OrderStatus.confirmed + ), + _buildActionChip( + currentStatus == "Pengiriman", + canInProgress, + "Pengiriman", + canInProgress ? Colors.orange : Colors.grey.shade700, + OrderStatus.in_progress + ), + ], + _buildActionChip( + currentStatus == "Selesai", + canCompleted, + "Selesai", + canCompleted ? Colors.green : Colors.grey.shade700, + OrderStatus.completed + ), + _buildActionChip( + currentStatus == "Dibatalkan", + true, + "Dibatalkan", + Colors.red, + OrderStatus.canceled + ), + ], + ), + ), + ], + ), + ); + } + + Widget _buildActionChip( + bool isActive, + bool canUpdate, + String value, + Color color, + OrderStatus orderStatus + ) { + return Padding( + padding: EdgeInsets.only(right: 2.w), + child: ActionChip( + onPressed: () => + !isActive && canUpdate ? onPressed.call(value, color, orderStatus) : null, + label: Text( + value, + style: TextStyle( + fontSize: AppFontSize.small.sp, + fontWeight: FontWeight.bold, + color: isActive ? Colors.white : color, + ), + ), + backgroundColor: isActive ? color : color.withOpacity(0.05), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(2.5.w), + side: BorderSide(color: color.withOpacity(0.2)), + ), + elevation: 0, + padding: EdgeInsets.symmetric(horizontal: 3.w, vertical: 1.h), + ), + ); + } +} diff --git a/Mobile Operasional/lib/features/online_store/presentation/widgets/sub_menu_card.dart b/Mobile Operasional/lib/features/online_store/presentation/widgets/sub_menu_card.dart new file mode 100644 index 0000000..464ac7b --- /dev/null +++ b/Mobile Operasional/lib/features/online_store/presentation/widgets/sub_menu_card.dart @@ -0,0 +1,96 @@ +import 'package:flutter/material.dart'; +import 'package:niogu_app/core/constants/app_color.dart'; +import 'package:niogu_app/core/constants/app_font_size.dart'; +import 'package:sizer/sizer.dart'; + +class SubMenuCard extends StatelessWidget { + final String title; + final String subtitle; + final IconData icon; + final VoidCallback onTap; + final int? badgeCount; + const SubMenuCard({ + super.key, + required this.title, + required this.subtitle, + required this.icon, + required this.onTap, + this.badgeCount, + }); + + @override + Widget build(BuildContext context) { + final bool isTablet = 100.w >= 600; + return Container( + margin: EdgeInsets.only(bottom: 2.h), + child: Material( + color: Colors.white, + borderRadius: BorderRadius.circular(3.w), + child: InkWell( + onTap: onTap, + borderRadius: BorderRadius.circular(3.w), + child: Container( + padding: EdgeInsets.all(4.w), + decoration: BoxDecoration( + border: Border.all(color: Colors.grey.shade200), + borderRadius: BorderRadius.circular(3.w), + ), + child: Row( + children: [ + Icon(icon, color: AppColor.primaryColor, size: 6.w), + SizedBox(width: 4.w), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: + (AppFontSize.medium - (isTablet ? 0 : 1.25)).sp, + color: Colors.black87, + ), + ), + Text( + subtitle, + style: TextStyle( + fontSize: + (AppFontSize.small - (isTablet ? 0 : 1.25)).sp, + color: Colors.grey.shade600, + ), + ), + ], + ), + ), + if (badgeCount != null) + CircleAvatar( + radius: 2.5.w, + backgroundColor: Colors.red, + child: Text( + badgeCount.toString(), + style: TextStyle( + color: Colors.white, + fontSize: + ((isTablet + ? AppFontSize.medium + : AppFontSize.small) - + 1.25) + .sp, + fontWeight: FontWeight.bold, + ), + ), + ), + Icon( + Icons.chevron_right, + color: Colors.grey.shade400, + size: 5.w, + ), + ], + ), + ), + ), + ), + ); + } +} diff --git a/Mobile Operasional/lib/features/online_store/presentation/widgets/summary_cost_info.dart b/Mobile Operasional/lib/features/online_store/presentation/widgets/summary_cost_info.dart new file mode 100644 index 0000000..dfb8259 --- /dev/null +++ b/Mobile Operasional/lib/features/online_store/presentation/widgets/summary_cost_info.dart @@ -0,0 +1,98 @@ +import 'package:flutter/material.dart'; +import 'package:niogu_app/core/constants/app_color.dart'; +import 'package:niogu_app/core/constants/app_font_size.dart'; +import 'package:niogu_app/core/utils/currency_format.dart'; +import 'package:sizer/sizer.dart'; + +class SummaryCostInfo extends StatelessWidget { + final double totalOrder; + final double deliveryFee; + final double totalAmount; + const SummaryCostInfo({ + super.key, + required this.totalOrder, + required this.deliveryFee, + required this.totalAmount, + }); + + @override + Widget build(BuildContext context) { + final bool isTablet = 100.w >= 600; + return Container( + padding: EdgeInsets.all(4.w), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(4.w), + border: Border.all(color: Colors.grey.shade200), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.02), + blurRadius: 10, + offset: const Offset(0, 4), + ), + ], + ), + child: Column( + children: [ + _buildCostRow( + isTablet, + "Total Harga (2 Produk)", + CurrencyFormat.formatToIdr(totalOrder, 0), + ), + _buildCostRow( + isTablet, + "Biaya Pengiriman", + CurrencyFormat.formatToIdr(deliveryFee, 0), + ), + Divider(height: 3.h), + _buildCostRow( + isTablet, + "Total Pesanan", + CurrencyFormat.formatToIdr(totalAmount, 0), + isTotal: true, + ), + SizedBox(height: 3.h), + ], + ), + ); + } + + Widget _buildCostRow( + bool isTablet, + String label, + String value, { + bool isTotal = false, + }) { + return Padding( + padding: EdgeInsets.symmetric(vertical: 0.5.h), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + label, + style: TextStyle( + fontSize: isTotal + ? AppFontSize.medium.sp + : isTablet + ? (AppFontSize.medium - 1.25).sp + : (AppFontSize.small - 1.25).sp, + fontWeight: isTotal ? FontWeight.bold : FontWeight.normal, + ), + ), + Text( + value, + style: TextStyle( + fontSize: isTotal + ? AppFontSize.medium.sp + : isTablet + ? (AppFontSize.medium - 1.25).sp + : (AppFontSize.small - 1.25).sp, + fontWeight: FontWeight.bold, + color: isTotal ? AppColor.primaryColor : Colors.black87, + ), + ), + ], + ), + ); + } +} diff --git a/Mobile Operasional/lib/features/outlets/data/repositories/outlet_repository_impl.dart b/Mobile Operasional/lib/features/outlets/data/repositories/outlet_repository_impl.dart new file mode 100644 index 0000000..3fc79c3 --- /dev/null +++ b/Mobile Operasional/lib/features/outlets/data/repositories/outlet_repository_impl.dart @@ -0,0 +1,114 @@ +import 'package:niogu_app/core/database/app_database.dart' hide Outlet; +import 'package:niogu_app/features/outlets/domain/entities/outlet.dart'; +import 'package:niogu_app/features/outlets/domain/repositories/i_outlet_repository.dart'; +import 'package:niogu_app/features/stock_in/domain/entities/stock_in_transfer.dart'; + +class OutletRepositoryImpl implements IOutletRepository { + final AppDatabase _appDatabase; + + OutletRepositoryImpl(this._appDatabase); + + @override + Future fetchIsMainOutlet() async { + return await _appDatabase.outletDao.getIsMainOutlet(); + } + + @override + Stream> watchUserDeletedByOutlet() { + return _appDatabase.outletDao.watchUserDeletedByOutlet(); + } + + @override + Stream watchLimitOutlet() { + return _appDatabase.outletDao.watchLimitAddOutlet(); + } + + @override + Stream> watchDisplayOutlets() { + return _appDatabase.outletDao.watchAllDisplayOutlets(); + } + + @override + Future getOutletById(String id) async { + return await _appDatabase.outletDao.getOutletById(id); + } + + @override + Future getMainOutlet() async { + return await _appDatabase.outletDao.getMainOutlet(); + } + + @override + Stream> watchStaffAdmins(String outletId) { + return _appDatabase.outletDao.watchAllStaffAdmins(outletId); + } + + @override + Future validatePhoneAndEmail({ + required String phoneNumber, + String? email, + String? excludeId, + }) async { + return await _appDatabase.outletDao.validatePhoneAndEmail( + phoneNumber: phoneNumber, + email: email, + excludeId: excludeId, + ); + } + + @override + Future validateAddUserStaff({ + required String firstPhoneNumber, + required String firstEmail, + String? secondPhoneNumber, + String? secondEmail, + }) async { + return await _appDatabase.outletDao.validateAddUserStaff( + firstPhoneNumber: firstPhoneNumber, + firstEmail: firstEmail, + secondPhoneNumber: secondPhoneNumber, + secondEmail: secondEmail, + ); + } + + @override + Future saveStaffAdmin({ + required String outletId, + UpsertStaffAdmin? firstStaffAdmin, + UpsertStaffAdmin? secondStaffAdmin, + }) async { + await _appDatabase.outletDao.saveStaffAdmin( + outletId: outletId, + firstStaffAdmin: firstStaffAdmin, + secondStaffAdmin: secondStaffAdmin, + ); + } + + @override + Future deleteOutlet(String outletId) async { + await _appDatabase.outletDao.deleteOutlet(outletId); + } + + @override + Future deleteStaffAdmin(String userId) async { + return await _appDatabase.outletDao.deleteStaffAdmin(userId); + } + + @override + Future saveOutlet(UpsertOutlet upsertOutlet) async { + await _appDatabase.outletDao.saveOutlet(upsertOutlet); + } + + @override + Stream> watchOutlets() { + return _appDatabase.outletDao.watchAllOutlets(); + } + + @override + Stream> watchSourceOutlets( + String currentOutletId, + bool isShowAll, + ) { + return _appDatabase.outletDao.watchAllSourceOutlets(isShowAll); + } +} diff --git a/Mobile Operasional/lib/features/outlets/domain/entities/outlet.dart b/Mobile Operasional/lib/features/outlets/domain/entities/outlet.dart new file mode 100644 index 0000000..1abcbab --- /dev/null +++ b/Mobile Operasional/lib/features/outlets/domain/entities/outlet.dart @@ -0,0 +1,194 @@ +import 'package:latlong2/latlong.dart'; +import 'package:uuid/uuid.dart'; + +class UserDeletedByOutlet { + final String outletId; + final int amount; + + const UserDeletedByOutlet({ + required this.outletId, + required this.amount + }); +} + +enum DisplayOutletEmpty { loading, empty_database, has_data } + +class DisplayOutlet { + final String id; + final String? bannerPath; + final String name; + final bool isMainOutlet; + final bool isActive; + final String? fullAddress; + final int userStaffAmount; + final int transactionAmount; + + const DisplayOutlet({ + required this.id, + this.bannerPath, + required this.name, + required this.isMainOutlet, + required this.isActive, + this.fullAddress, + required this.userStaffAmount, + required this.transactionAmount, + }); +} + +class OutletDetail { + final String id; + final String? bannerPath; + final String tenantName; + final String name; + final String phoneNumber; + final String? email; + final String? fullAddress; + final double? latitude; + final double? longitude; + final bool isMainOutlet; + final bool isActive; + final int transactionAmount; + + const OutletDetail({ + required this.id, + this.bannerPath, + required this.tenantName, + required this.name, + required this.phoneNumber, + this.email, + this.fullAddress, + this.latitude, + this.longitude, + required this.isMainOutlet, + required this.isActive, + required this.transactionAmount + }); +} + +class StaffAdminDetail { + final String id; + final String name; + final String phoneNumber; + final String email; + final bool isActive; + final int transactionAmount; + final String shiftName; + final String shiftStartTime; + final String shiftEndTime; + + const StaffAdminDetail({ + required this.id, + required this.name, + required this.phoneNumber, + required this.email, + required this.isActive, + required this.transactionAmount, + required this.shiftName, + required this.shiftStartTime, + required this.shiftEndTime, + }); +} + +class UpsertOutlet { + final String id; + final String? bannerPath; + final String name; + final String? phoneNumber; + final String? email; + final UpsertStaffAdmin? firstStaffAdmin; + final UpsertStaffAdmin? secondStaffAdmin; + final String? fullAddress; + final double? latitude; + final double? longitude; + final bool isMainOutlet; + final bool isActive; + + UpsertOutlet({ + String? id, + this.bannerPath, + required this.name, + this.phoneNumber, + this.email, + this.firstStaffAdmin, + this.secondStaffAdmin, + this.fullAddress, + this.latitude, + this.longitude, + this.isMainOutlet = false, + this.isActive = true + }) : id = id ?? const Uuid().v7().toString(); +} + +class UpsertStaffAdmin { + final String id; + final String name; + final String phoneNumber; + final String email; + final String? passwordHash; + final String shiftName; + final String shiftStartTime; + final String shiftEndTime; + final bool isActive; + + UpsertStaffAdmin({ + String? id, + required this.name, + required this.phoneNumber, + required this.email, + this.passwordHash, + required this.shiftName, + required this.shiftStartTime, + required this.shiftEndTime, + this.isActive = true, + }) : id = id ?? const Uuid().v7().toString(); + + UpsertStaffAdmin copyWith({bool? isActive}) { + return UpsertStaffAdmin( + id: id, + name: name, + phoneNumber: phoneNumber, + email: email, + passwordHash: passwordHash, + shiftName: shiftName, + shiftStartTime: shiftStartTime, + shiftEndTime: shiftEndTime, + isActive: isActive ?? this.isActive + ); + } +} + +class Outlet { + final String id; + final String name; + final String? photoPath; + final bool isMainOutlet; + final bool isActive; + + Outlet({ + required this.id, + required this.name, + this.photoPath, + this.isMainOutlet = false, + this.isActive = true, + }); +} + +class OutletAddressSelected { + final String fullAddress; + final LatLng currentLocation; + + const OutletAddressSelected({ + required this.fullAddress, + required this.currentLocation, + }); +} + +class MainOutlet { + final String id; + final String name; + + const MainOutlet({ + required this.id, + required this.name + }); +} diff --git a/Mobile Operasional/lib/features/outlets/domain/repositories/i_outlet_repository.dart b/Mobile Operasional/lib/features/outlets/domain/repositories/i_outlet_repository.dart new file mode 100644 index 0000000..8536cdd --- /dev/null +++ b/Mobile Operasional/lib/features/outlets/domain/repositories/i_outlet_repository.dart @@ -0,0 +1,50 @@ +import 'package:niogu_app/features/outlets/domain/entities/outlet.dart'; +import 'package:niogu_app/features/stock_in/domain/entities/stock_in_transfer.dart'; + +abstract class IOutletRepository { + Future fetchIsMainOutlet(); + + Stream> watchUserDeletedByOutlet(); + + Stream watchLimitOutlet(); + + Stream> watchDisplayOutlets(); + + Future getOutletById(String id); + + Future getMainOutlet(); + + Stream> watchStaffAdmins(String outletId); + + Future validatePhoneAndEmail({ + required String phoneNumber, + String? email, + String? excludeId, + }); + + Future validateAddUserStaff({ + required String firstPhoneNumber, + required String firstEmail, + String? secondPhoneNumber, + String? secondEmail, + }); + + Future saveStaffAdmin({ + required String outletId, + UpsertStaffAdmin? firstStaffAdmin, + UpsertStaffAdmin? secondStaffAdmin, + }); + + Future deleteStaffAdmin(String userId); + + Future saveOutlet(UpsertOutlet upsertOutlet); + + Future deleteOutlet(String outletId); + + Stream> watchOutlets(); + + Stream> watchSourceOutlets( + String currentOutletId, + bool isShowAll, + ); +} diff --git a/Mobile Operasional/lib/features/outlets/presentation/controllers/outlet_controller.dart b/Mobile Operasional/lib/features/outlets/presentation/controllers/outlet_controller.dart new file mode 100644 index 0000000..01b4107 --- /dev/null +++ b/Mobile Operasional/lib/features/outlets/presentation/controllers/outlet_controller.dart @@ -0,0 +1,71 @@ +import 'dart:async'; + +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:niogu_app/features/outlets/domain/entities/outlet.dart'; +import 'package:niogu_app/features/outlets/domain/repositories/i_outlet_repository.dart'; +import 'package:niogu_app/features/outlets/presentation/providers/outlet_provider.dart'; + +class OutletController extends AutoDisposeAsyncNotifier { + late final IOutletRepository _outletRepository; + + @override + FutureOr build() { + _outletRepository = ref.read(outletRepositoryProvider); + } + + Future saveStaffAdmin({ + required String outletId, + UpsertStaffAdmin? firstStaffAdmin, + UpsertStaffAdmin? secondStaffAdmin, + }) async { + state = const AsyncValue.loading(); + + final result = await AsyncValue.guard(() async { + await _outletRepository.saveStaffAdmin( + outletId: outletId, + firstStaffAdmin: firstStaffAdmin, + secondStaffAdmin: secondStaffAdmin, + ); + }); + + state = result; + + if (result is AsyncError) throw result.error!; + } + + Future deleteStaffAdmin(String userId) async { + state = const AsyncValue.loading(); + + final result = await AsyncValue.guard(() async { + await _outletRepository.deleteStaffAdmin(userId); + }); + + state = result; + + if (result is AsyncError) throw result.error!; + } + + Future saveOutlet(UpsertOutlet upsertOutlet) async { + state = const AsyncValue.loading(); + + final result = await AsyncValue.guard(() async { + await _outletRepository.saveOutlet(upsertOutlet); + }); + + state = result; + + if (result is AsyncError) throw result.error!; + } + + Future deleteOutlet(String outletId) async { + state = const AsyncValue.loading(); + + final result = await AsyncValue.guard(() async { + await _outletRepository.deleteOutlet(outletId); + }); + + state = result; + + if (result is AsyncError) throw result.error!; + } +} diff --git a/Mobile Operasional/lib/features/outlets/presentation/providers/outlet_provider.dart b/Mobile Operasional/lib/features/outlets/presentation/providers/outlet_provider.dart new file mode 100644 index 0000000..61b4a5e --- /dev/null +++ b/Mobile Operasional/lib/features/outlets/presentation/providers/outlet_provider.dart @@ -0,0 +1,90 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:niogu_app/core/providers/app_provider.dart'; +import 'package:niogu_app/features/outlets/data/repositories/outlet_repository_impl.dart'; +import 'package:niogu_app/features/outlets/domain/entities/outlet.dart'; +import 'package:niogu_app/features/outlets/domain/repositories/i_outlet_repository.dart'; +import 'package:niogu_app/features/outlets/presentation/controllers/outlet_controller.dart'; +import 'package:niogu_app/features/stock_in/domain/entities/stock_in_transfer.dart'; + +final outletRepositoryProvider = Provider((ref) { + final appDatabase = ref.watch(appDatabaseProvider); + return OutletRepositoryImpl(appDatabase); +}); + +final userDeletedByOutletStreamProvider = + StreamProvider.autoDispose>((ref) { + final outletRepository = ref.watch(outletRepositoryProvider); + + return outletRepository.watchUserDeletedByOutlet(); + }); + +final limitOutletStreamProvider = StreamProvider.autoDispose((ref) { + final outletRepository = ref.watch(outletRepositoryProvider); + return outletRepository.watchLimitOutlet(); +}); + +final displayOutletStreamProvider = + StreamProvider.autoDispose>((ref) { + final outletRepository = ref.watch(outletRepositoryProvider); + return outletRepository.watchDisplayOutlets(); + }); + +final displayOutletEmptyProvider = Provider.autoDispose(( + ref, +) { + final displayOutletStreamAsync = ref.watch(displayOutletStreamProvider); + + if (displayOutletStreamAsync.isLoading) { + return DisplayOutletEmpty.loading; + } + + final allOutlets = displayOutletStreamAsync.value ?? []; + + if (allOutlets.isEmpty) { + return DisplayOutletEmpty.empty_database; + } + + return DisplayOutletEmpty.has_data; +}); + +final staffAdminProvider = StreamProvider.family + .autoDispose, String>((ref, outletId) { + final outletRepository = ref.watch(outletRepositoryProvider); + return outletRepository.watchStaffAdmins(outletId); + }); + +final outletStreamProvider = StreamProvider.autoDispose>((ref) { + final isLoggedIn = ref.watch(currentStatusLoginProvider); + + final outletRepository = ref.watch(outletRepositoryProvider); + + if(!isLoggedIn) { + return Stream>.value([]); + } + + return outletRepository.watchOutlets(); +}); + +final mapOutletAddressProvider = StateProvider( + (ref) => null, +); + +final sourceOutletStreamProvider = StreamProvider.family + .autoDispose, bool>((ref, isShowAll) { + final isLoggedIn = ref.watch(currentStatusLoginProvider); + + final outletRepository = ref.watch(outletRepositoryProvider); + + final currentOutletId = ref.watch(currentOutletIdProvider); + + if(!isLoggedIn) { + return Stream>.value([]); + } + + return outletRepository.watchSourceOutlets(currentOutletId!, isShowAll); + }); + +final outletControllerProvider = + AsyncNotifierProvider.autoDispose( + OutletController.new, + ); diff --git a/Mobile Operasional/lib/features/outlets/presentation/screens/add_outlet_screen.dart b/Mobile Operasional/lib/features/outlets/presentation/screens/add_outlet_screen.dart new file mode 100644 index 0000000..d7902e9 --- /dev/null +++ b/Mobile Operasional/lib/features/outlets/presentation/screens/add_outlet_screen.dart @@ -0,0 +1,1005 @@ +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; +import 'package:image_picker/image_picker.dart'; +import 'package:niogu_app/core/components/top_back_bar_app.dart'; +import 'package:niogu_app/core/constants/app_color.dart'; +import 'package:niogu_app/core/constants/app_font_size.dart'; +import 'package:niogu_app/core/router/app_route.dart'; +import 'package:niogu_app/core/utils/image_service.dart'; +import 'package:niogu_app/core/utils/log_message.dart'; +import 'package:niogu_app/core/widgets/custom_snackbar.dart'; +import 'package:niogu_app/core/widgets/custom_text_form_field.dart'; +import 'package:niogu_app/features/outlets/domain/entities/outlet.dart'; +import 'package:niogu_app/features/outlets/presentation/providers/outlet_provider.dart'; +import 'package:niogu_app/features/outlets/presentation/widgets/staf_admin.dart'; +import 'package:sizer/sizer.dart'; + +class StaffAdminInformation { + final TextEditingController nameController; + final TextEditingController phoneNumberController; + final TextEditingController emailController; + final TextEditingController passwordController; + final TextEditingController passwordConfirmController; + final TextEditingController shiftNameController; + bool obsecurePassword; + bool obsecurePasswordConfirm; + TimeOfDay startTime; + TimeOfDay endTime; + + StaffAdminInformation({ + required this.nameController, + required this.phoneNumberController, + required this.emailController, + required this.passwordController, + required this.passwordConfirmController, + required this.shiftNameController, + this.obsecurePassword = true, + this.obsecurePasswordConfirm = true, + this.startTime = const TimeOfDay(hour: 08, minute: 00), + this.endTime = const TimeOfDay(hour: 16, minute: 00), + }); +} + +class AddOutletScreen extends ConsumerStatefulWidget { + const AddOutletScreen({super.key}); + + @override + ConsumerState createState() => _AddOutletScreenState(); +} + +class _AddOutletScreenState extends ConsumerState { + final GlobalKey _generalKey = GlobalKey(); + + final TextEditingController _nameController = TextEditingController(); + + final TextEditingController _phoneNumberController = TextEditingController(); + + final TextEditingController _emailController = TextEditingController(); + + bool _visibleFirstForm = false; + + final StaffAdminInformation _firstStaffAdmin = StaffAdminInformation( + nameController: TextEditingController(), + phoneNumberController: TextEditingController(), + emailController: TextEditingController(), + passwordController: TextEditingController(), + passwordConfirmController: TextEditingController(), + shiftNameController: TextEditingController(), + ); + + final GlobalKey _firstAdminKey = GlobalKey(); + + bool _visbleSecondForm = false; + + final StaffAdminInformation _secondStaffAdmin = StaffAdminInformation( + nameController: TextEditingController(), + phoneNumberController: TextEditingController(), + emailController: TextEditingController(), + passwordController: TextEditingController(), + passwordConfirmController: TextEditingController(), + shiftNameController: TextEditingController(), + ); + + final GlobalKey _secondAdminKey = GlobalKey(); + + final ImagePicker _picker = ImagePicker(); + + final _emailRegex = RegExp( + r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$', + ); + + final List _imagePathTemps = []; + + String? _imagePath; + + @override + void initState() { + // TODO: implement initState + super.initState(); + + WidgetsBinding.instance.addPostFrameCallback((_) { + ref.invalidate(mapOutletAddressProvider); + }); + } + + @override + void dispose() { + // TODO: implement dispose + _nameController.dispose(); + _phoneNumberController.dispose(); + _emailController.dispose(); + _firstStaffAdmin.nameController.dispose(); + _firstStaffAdmin.phoneNumberController.dispose(); + _firstStaffAdmin.emailController.dispose(); + _firstStaffAdmin.passwordController.dispose(); + _firstStaffAdmin.passwordConfirmController.dispose(); + _firstStaffAdmin.shiftNameController.dispose(); + _secondStaffAdmin.nameController.dispose(); + _secondStaffAdmin.phoneNumberController.dispose(); + _secondStaffAdmin.emailController.dispose(); + _secondStaffAdmin.passwordController.dispose(); + _secondStaffAdmin.passwordConfirmController.dispose(); + _secondStaffAdmin.shiftNameController.dispose(); + super.dispose(); + } + + Future _showPickerOptions() async { + final bool isTablet = 100.w >= 600; + showModalBottomSheet( + context: context, + backgroundColor: Colors.white, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.vertical(top: Radius.circular(2.5.w)), + ), + constraints: const BoxConstraints(maxWidth: double.infinity), + builder: (BuildContext context) { + return SafeArea( + child: Container( + width: 100.w, + padding: EdgeInsets.symmetric(vertical: 2.h, horizontal: 5.w), + clipBehavior: Clip.hardEdge, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.vertical(top: Radius.circular(6.w)), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + ListTile( + leading: Icon(Icons.photo_library, size: 6.w), + title: Text( + 'Galeri', + style: TextStyle( + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + ), + ), + onTap: () async { + await _getImage(ImageSource.gallery); + context.pop(); + }, + ), + + SizedBox(height: 2.h), + + ListTile( + leading: Icon(Icons.photo_camera, size: 6.w), + title: Text( + 'Kamera', + style: TextStyle( + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + ), + ), + onTap: () async { + await _getImage(ImageSource.camera); + context.pop(); + }, + ), + ], + ), + ), + ); + }, + ); + } + + Future _getImage(ImageSource ImageSource) async { + try { + final XFile? pickedFile = await _picker.pickImage( + source: ImageSource, + imageQuality: 70, + ); + + if (pickedFile != null) { + File tempFile = File(pickedFile.path); + + String? imagePath = await ImageService.saveImageToLocalDirectory( + tempFile, + "outlet_banner_images", + ); + + if (imagePath != null) { + _imagePathTemps.add(imagePath); + setState(() { + _imagePath = imagePath; + }); + } + } + } catch (e, st) { + LogMessage.log.e(e.toString(), error: e, stackTrace: st); + CustomSnackbar.showWarning(context, "Akses ditolak"); + } + } + + Future _cleanUpImages() async { + for (final image in _imagePathTemps) { + if (image != null) { + await ImageService.deleteLocalImage(image); + } + } + } + + Future _selectTime(bool isStart, StaffAdminInformation? admin) async { + final TimeOfDay? picked = await showTimePicker( + context: context, + initialTime: admin != null + ? (isStart ? admin.startTime : admin.endTime) + : TimeOfDay.now(), + builder: (BuildContext context, Widget? child) { + return MediaQuery( + data: MediaQuery.of(context).copyWith(alwaysUse24HourFormat: true), + child: child!, + ); + }, + ); + + if (picked != null) { + setState(() { + if (isStart) { + admin?.startTime = picked; + } else { + admin?.endTime = picked; + } + }); + } + } + + String? _validation( + String? value, + String message, { + StaffAdminInformation? admin, + bool isEmail = false, + bool isPassword = false, + bool isPasswordConfirm = false, + }) { + if (value == null || value.isEmpty) { + return message; + } + + if (isEmail && !_emailRegex.hasMatch(value)) { + return "Email tidak valid"; + } + + if (isPassword) { + if (value.length < 8) { + return "Password minimal harus 8 karakter"; + } + + final passwordRegExp = RegExp( + r'^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&#])[A-Za-z\d@$!%*?&#]{8,}$', + ); + + if (!passwordRegExp.hasMatch(value)) { + return "Gunakan huruf besar, kecil, angka, dan simbol"; + } + } + + if (isPasswordConfirm && value != admin?.passwordController.text.trim()) { + return "Konfirmasi password tidak sesuai"; + } + + return null; + } + + int _toMinutes(TimeOfDay time) { + return time.hour * 60 + time.minute; + } + + List> _normalizeShift(TimeOfDay start, TimeOfDay end) { + final s = _toMinutes(start); + final e = _toMinutes(end); + + if (e > s) { + return [ + [s, e], + ]; + } else { + return [ + [s, 1440], + [0, e], + ]; + } + } + + int _calculateDuration(TimeOfDay start, TimeOfDay end) { + final s = _toMinutes(start); + final e = _toMinutes(end); + + if (e > s) { + return e - s; + } else { + return (1440 - s) + e; + } + } + + String? _validateShift(TimeOfDay? start, TimeOfDay? end, int shift) { + if (start == null && end == null) { + return null; + } + + final duration = _calculateDuration(start!, end!); + + if (duration == 0) { + return "Shift $shift tidak boleh 24 jam penuh"; + } + + if (duration <= 0) { + return "Jam selesai shift $shift harus berbeda dari jam mulai shift $shift"; + } + + if (duration > 16 * 60) { + return "Shift $shift tidak boleh lebih dari 16 jam"; + } + + return null; + } + + bool _isShiftOverlap({ + required TimeOfDay start1, + required TimeOfDay end1, + required TimeOfDay start2, + required TimeOfDay end2, + }) { + final shift1 = _normalizeShift(start1, end1); + + final shift2 = _normalizeShift(start2, end2); + + for (final a in shift1) { + for (final b in shift2) { + final s1 = a[0]; + final e1 = a[1]; + final s2 = b[0]; + final e2 = b[1]; + + if (s1 < e2 && e1 > s2) { + return true; + } + } + } + + return false; + } + + Future _saveOutlet() async { + if (!_generalKey.currentState!.validate()) return; + + final phoneNumber = _phoneNumberController.text.trim(); + + String? email; + + if (_emailController.text.trim().isNotEmpty) { + email = _emailController.text.trim(); + } + + final validatePhoneAndEmail = await ref + .read(outletRepositoryProvider) + .validatePhoneAndEmail(phoneNumber: phoneNumber, email: email); + + if (validatePhoneAndEmail) { + CustomSnackbar.showError( + context, + 'No. hp atau email outlet telah tersedia', + ); + + return; + } + + final firstFormHasValue = + _firstStaffAdmin.nameController.text.isNotEmpty || + _firstStaffAdmin.phoneNumberController.text.isNotEmpty || + _firstStaffAdmin.emailController.text.isNotEmpty || + _firstStaffAdmin.passwordController.text.isNotEmpty || + _firstStaffAdmin.passwordConfirmController.text.isNotEmpty || + _firstStaffAdmin.shiftNameController.text.isNotEmpty; + + if (!_visibleFirstForm && !firstFormHasValue) { + CustomSnackbar.showError(context, "Informasi staf admin 1 wajib diisi"); + return; + } + + if (!_firstAdminKey.currentState!.validate()) return; + + final bool secondFormHasValue = + _secondStaffAdmin.nameController.text.isNotEmpty || + _secondStaffAdmin.phoneNumberController.text.isNotEmpty || + _secondStaffAdmin.emailController.text.isNotEmpty || + _secondStaffAdmin.passwordController.text.isNotEmpty || + _secondStaffAdmin.passwordConfirmController.text.isNotEmpty || + _secondStaffAdmin.shiftNameController.text.isNotEmpty; + + if (secondFormHasValue && !_secondAdminKey.currentState!.validate()) { + return; + } + + final firstPhoneNumber = _firstStaffAdmin.phoneNumberController.text; + + final firstEmail = _firstStaffAdmin.emailController.text; + + final secondPhoneNumber = _secondStaffAdmin.phoneNumberController.text; + + final secondEmail = _secondStaffAdmin.emailController.text; + + if (firstPhoneNumber == secondPhoneNumber) { + CustomSnackbar.showError(context, "No. hp staf 1 dan 2 tidak boleh sama"); + return; + } + + if (firstEmail == secondEmail) { + CustomSnackbar.showError(context, "Email staf 1 dan 2 tidak boleh sama"); + return; + } + + final validateFirstShift = _validateShift( + _firstStaffAdmin.startTime, + _firstStaffAdmin.endTime, + 1, + ); + + if (validateFirstShift != null) { + CustomSnackbar.showError(context, validateFirstShift); + return; + } + + final validateSecondShift = _validateShift( + _secondStaffAdmin.startTime, + _secondStaffAdmin.endTime, + 2, + ); + + if (validateSecondShift != null) { + CustomSnackbar.showError(context, validateSecondShift); + return; + } + + final firstStartShift = _firstStaffAdmin.startTime; + + final firstEndShift = _firstStaffAdmin.endTime; + + final secondStartShift = _secondStaffAdmin.startTime; + + final secondEndShift = _secondStaffAdmin.endTime; + + if (_isShiftOverlap( + start1: firstStartShift, + end1: firstEndShift, + start2: secondStartShift, + end2: secondEndShift, + ) && + secondFormHasValue) { + CustomSnackbar.showError( + context, + "Jam kerja shift 1 konflik dengan shift 2", + ); + return; + } + + final validateAddUserStaff = ref + .read(outletRepositoryProvider) + .validateAddUserStaff( + firstPhoneNumber: firstPhoneNumber, + firstEmail: firstEmail, + secondPhoneNumber: secondPhoneNumber, + secondEmail: secondEmail, + ); + + if (await validateAddUserStaff) { + CustomSnackbar.showError( + context, + "No. hp atau email staf telah tersedia", + ); + return; + } + + final firstStaffAdmin = UpsertStaffAdmin( + name: _firstStaffAdmin.nameController.text.trim(), + phoneNumber: _firstStaffAdmin.phoneNumberController.text.trim(), + email: _firstStaffAdmin.emailController.text.trim(), + passwordHash: _firstStaffAdmin.passwordController.text.trim(), + shiftName: _firstStaffAdmin.shiftNameController.text.trim(), + shiftStartTime: _firstStaffAdmin.startTime.format(context), + shiftEndTime: _firstStaffAdmin.endTime.format(context), + ); + + UpsertStaffAdmin? secondStaffAdmin; + + if (secondFormHasValue) { + secondStaffAdmin = UpsertStaffAdmin( + name: _secondStaffAdmin.nameController.text.trim(), + phoneNumber: _secondStaffAdmin.phoneNumberController.text.trim(), + email: _secondStaffAdmin.emailController.text.trim(), + passwordHash: _secondStaffAdmin.passwordController.text.trim(), + shiftName: _secondStaffAdmin.shiftNameController.text.trim(), + shiftStartTime: _secondStaffAdmin.startTime.format(context), + shiftEndTime: _secondStaffAdmin.endTime.format(context), + ); + } + + final selectedOutletAddress = ref.read(mapOutletAddressProvider); + + final upsertOutlet = UpsertOutlet( + bannerPath: _imagePath, + name: _nameController.text.trim(), + phoneNumber: _phoneNumberController.text.trim(), + email: email, + firstStaffAdmin: firstStaffAdmin, + secondStaffAdmin: secondStaffAdmin, + fullAddress: selectedOutletAddress?.fullAddress, + latitude: selectedOutletAddress?.currentLocation.latitude, + longitude: selectedOutletAddress?.currentLocation.longitude, + ); + + if (_imagePath == null && _imagePathTemps.isNotEmpty) { + await _cleanUpImages(); + } else if (_imagePath != null && + _imagePathTemps.isNotEmpty && + _imagePathTemps.length > 1) { + final imagePathTemps = _imagePathTemps + .where((i) => i != _imagePath) + .toList(); + + for (final image in imagePathTemps) { + if (image != null) { + await ImageService.deleteLocalImage(image); + } + } + } + + try { + await ref + .read(outletControllerProvider.notifier) + .saveOutlet(upsertOutlet); + + if (!mounted) return; + + ref.invalidate(mapOutletAddressProvider); + + CustomSnackbar.showSuccess(context, 'Outlet baru berhasil ditambahkan'); + context.pop(); + } catch (e, st) { + LogMessage.log.e(e.toString(), error: e, stackTrace: st); + CustomSnackbar.showError(context, 'Ups, terjadi kesalahan'); + } + } + + @override + Widget build(BuildContext context) { + return LayoutBuilder( + builder: (context, constraints) { + final bool isTablet = 100.w >= 600; + + final outletControllerState = ref.watch(outletControllerProvider); + + final mapOutletAddressState = ref.watch(mapOutletAddressProvider); + + final File imageFile = File(_imagePath ?? "image not found"); + + final bool imageFileExists = imageFile.existsSync(); + + return PopScope( + canPop: false, + onPopInvokedWithResult: (didPop, result) async { + if (didPop) return; + + await _cleanUpImages(); + + context.pop(); + + WidgetsBinding.instance.addPostFrameCallback((_) { + ref.invalidate(mapOutletAddressProvider); + }); + }, + child: SafeArea( + top: false, + bottom: true, + right: false, + left: false, + child: Scaffold( + backgroundColor: Colors.white, + appBar: TopBackBarApp( + title: "Tambah Outlet Baru", + onTap: () async { + await _cleanUpImages(); + + context.pop(); + + WidgetsBinding.instance.addPostFrameCallback((_) { + ref.invalidate(mapOutletAddressProvider); + }); + }, + ), + body: SingleChildScrollView( + padding: EdgeInsets.all(5.w), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: EdgeInsets.only(bottom: 1.5.h), + child: Text( + "Foto Outlet", + style: TextStyle( + fontSize: (AppFontSize.medium - 1.25).sp, + fontWeight: FontWeight.bold, + color: Colors.black87, + ), + ), + ), + Stack( + clipBehavior: Clip.none, + children: [ + Container( + height: 20.h, + width: double.infinity, + decoration: BoxDecoration( + color: Colors.grey.shade100, + borderRadius: BorderRadius.circular(3.w), + border: Border.all( + color: Colors.grey.shade300, + style: BorderStyle.none, + ), + ), + child: imageFileExists + ? ClipRRect( + borderRadius: BorderRadius.circular(2.5.w), + child: Image.file( + imageFile, + fit: BoxFit.cover, + width: double.infinity, + height: double.infinity, + errorBuilder: (context, error, stackTrace) { + return Icon( + Icons.error, + color: Colors.grey[400], + size: 10.w, + ); + }, + ), + ) + : GestureDetector( + onTap: _showPickerOptions, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.add_a_photo_outlined, + size: 10.w, + color: Colors.grey.shade700, + ), + SizedBox(height: 1.h), + Text( + "Unggah Foto Landscape", + style: TextStyle( + fontSize: isTablet + ? (AppFontSize.medium - 2).sp + : (AppFontSize.small - 2).sp, + color: Colors.grey.shade700, + ), + ), + ], + ), + ), + ), + + if (_imagePath != null) + Positioned( + top: 1.w, + right: 1.w, + child: Material( + color: Colors.transparent, + type: MaterialType.transparency, + child: InkWell( + onTap: () { + setState(() { + _imagePath = null; + }); + }, + child: Container( + padding: EdgeInsets.all(1.w), + decoration: BoxDecoration( + color: Colors.red[50], + shape: BoxShape.circle, + border: Border.all( + color: Colors.red.withOpacity(0.2), + ), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.1), + blurRadius: 2, + offset: Offset(0, 1), + ), + ], + ), + child: Icon( + Icons.remove, + color: Colors.red, + size: 4.w, + ), + ), + ), + ), + ), + ], + ), + SizedBox(height: 3.h), + + Form( + key: _generalKey, + child: Column( + children: [ + CustomTextFormField( + label: "Nama Outlet", + hint: "Contoh: Outlet Perum Gunung Batu", + prefixIcon: Icons.store_mall_directory_outlined, + controller: _nameController, + validator: (value) => + _validation(value, "Nama outlet belum diisi"), + ), + SizedBox(height: 3.h), + + CustomTextFormField( + label: "No. Handphone / WA Outlet", + hint: "081234567890", + prefixIcon: Icons.phone_android_outlined, + controller: _phoneNumberController, + keyboardType: TextInputType.phone, + validator: (value) => _validation( + value, + "No. handphone outlet belum diisi", + ), + ), + SizedBox(height: 3.h), + CustomTextFormField( + label: "Email", + hint: "(Opsional)", + prefixIcon: Icons.email_outlined, + keyboardType: TextInputType.emailAddress, + controller: _emailController, + validator: (value) { + if (value == null || value.isEmpty) { + return null; + } + + if (!_emailRegex.hasMatch(value)) { + return "Email outlet tidak valid"; + } + + return null; + }, + ), + ], + ), + ), + SizedBox(height: 3.h), + + Form( + key: _firstAdminKey, + child: StafAdmin( + stafAdminNumber: 1, + isFormVisible: _visibleFirstForm, + onTap: () { + setState(() { + _visibleFirstForm = !_visibleFirstForm; + }); + }, + stafAdmin: _firstStaffAdmin, + passwordSuffixPressed: () { + setState(() { + _firstStaffAdmin.obsecurePassword = + !_firstStaffAdmin.obsecurePassword; + }); + }, + passwordConfirmSuffixPressed: () { + setState(() { + _firstStaffAdmin.obsecurePasswordConfirm = + !_firstStaffAdmin.obsecurePasswordConfirm; + }); + }, + onStartTimeTap: () => + _selectTime(true, _firstStaffAdmin), + onEndTimeTap: () => + _selectTime(false, _firstStaffAdmin), + nameValidator: (value) => + _validation(value, "Nama staf belum diisi"), + phoneNumberValidator: (value) => _validation( + value, + "No. Handphone staf belum diisi", + ), + emailValidator: (value) => _validation( + value, + "Email staff belum diisi", + isEmail: true, + ), + passwordValidator: (value) => _validation( + value, + "Password belum diisi", + isPassword: true, + admin: _firstStaffAdmin, + ), + passwordConfirmValidator: (value) => _validation( + value, + "Konfirmasi password belum diisi", + isPasswordConfirm: true, + admin: _firstStaffAdmin, + ), + shiftNameValidator: (value) => + _validation(value, "Nama shift belum diisi"), + ), + ), + + SizedBox(height: 3.h), + + Form( + key: _secondAdminKey, + child: StafAdmin( + stafAdminNumber: 2, + isFormVisible: _visbleSecondForm, + onTap: () { + setState(() { + _visbleSecondForm = !_visbleSecondForm; + }); + }, + stafAdmin: _secondStaffAdmin, + passwordSuffixPressed: () { + setState(() { + _secondStaffAdmin.obsecurePassword = + !_secondStaffAdmin.obsecurePassword; + }); + }, + passwordConfirmSuffixPressed: () { + setState(() { + _secondStaffAdmin.obsecurePasswordConfirm = + !_secondStaffAdmin.obsecurePasswordConfirm; + }); + }, + onStartTimeTap: () => + _selectTime(true, _secondStaffAdmin), + onEndTimeTap: () => + _selectTime(false, _secondStaffAdmin), + nameValidator: (value) => + _validation(value, "Nama staf belum diisi"), + phoneNumberValidator: (value) => _validation( + value, + "No. Handphone staf belum diisi", + ), + emailValidator: (value) => _validation( + value, + "Email staff belum diisi", + isEmail: true, + ), + passwordValidator: (value) => _validation( + value, + "Password belum diisi", + isPassword: true, + admin: _secondStaffAdmin, + ), + passwordConfirmValidator: (value) => _validation( + value, + "Konfirmasi password belum diisi", + isPasswordConfirm: true, + admin: _secondStaffAdmin, + ), + shiftNameValidator: (value) => + _validation(value, "Nama shift belum diisi"), + ), + ), + + SizedBox(height: 3.h), + + Padding( + padding: EdgeInsets.only(bottom: 1.5.h), + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon( + Icons.info_outline, + size: 3.5.w, + color: AppColor.primaryColor, + ), + SizedBox(width: 0.75.w), + Text( + "Informasi alamat dapat dilengkapi nanti oleh staf admin", + style: TextStyle( + color: AppColor.primaryColor, + fontSize: isTablet + ? (AppFontSize.medium - 1.25).sp + : (AppFontSize.small - 1.25).sp, + ), + ), + ], + ), + ], + ), + ), + + Material( + color: Colors.transparent, + type: MaterialType.transparency, + child: InkWell( + onTap: () { + context.pushNamed(AppRoute.mapOutletAdressScreen); + }, + child: Container( + width: double.infinity, + padding: EdgeInsets.symmetric( + horizontal: 4.w, + vertical: 2.h, + ), + decoration: BoxDecoration( + border: Border.all(color: Colors.grey.shade300), + borderRadius: BorderRadius.circular(2.05.w), + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Icon( + Icons.map_outlined, + color: Colors.black, + size: 5.w, + ), + + SizedBox(width: 2.5.w), + + Expanded( + child: Text( + mapOutletAddressState != null + ? mapOutletAddressState.fullAddress + : "Atur alamat outlet", + style: TextStyle( + fontWeight: mapOutletAddressState != null + ? FontWeight.normal + : FontWeight.bold, + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + height: 1.4, + ), + ), + ), + ], + ), + ), + ), + ), + + SizedBox(height: 3.h), + + ElevatedButton( + onPressed: outletControllerState.isLoading + ? null + : _saveOutlet, + style: ElevatedButton.styleFrom( + backgroundColor: AppColor.primaryColor, + minimumSize: Size(double.infinity, 7.h), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(3.w), + ), + disabledBackgroundColor: Colors.grey.shade300, + ), + child: Text( + "Simpan", + style: TextStyle( + fontSize: AppFontSize.medium.sp, + fontWeight: FontWeight.bold, + color: Colors.white, + ), + ), + ), + SizedBox(height: 3.h), + ], + ), + ), + ), + ), + ); + }, + ); + } +} diff --git a/Mobile Operasional/lib/features/outlets/presentation/screens/delete_outlet_screen.dart b/Mobile Operasional/lib/features/outlets/presentation/screens/delete_outlet_screen.dart new file mode 100644 index 0000000..273c2e8 --- /dev/null +++ b/Mobile Operasional/lib/features/outlets/presentation/screens/delete_outlet_screen.dart @@ -0,0 +1,269 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; +import 'package:niogu_app/core/constants/app_font_size.dart'; +import 'package:niogu_app/core/providers/app_provider.dart'; +import 'package:niogu_app/core/utils/log_message.dart'; +import 'package:niogu_app/core/system/system_setting.dart'; +import 'package:niogu_app/core/widgets/custom_snackbar.dart'; +import 'package:niogu_app/features/outlets/presentation/providers/outlet_provider.dart'; +import 'package:sizer/sizer.dart'; + +class DeleteOutletScreen extends ConsumerStatefulWidget { + final String id; + final String name; + final int transactionAmount; + const DeleteOutletScreen({ + super.key, + required this.id, + required this.name, + required this.transactionAmount, + }); + + @override + ConsumerState createState() => _DeleteOutletScreenState(); +} + +class _DeleteOutletScreenState extends ConsumerState { + late String name; + + late int transactionAmount; + + @override + void initState() { + // TODO: implement initState + super.initState(); + + name = widget.name; + + transactionAmount = widget.transactionAmount; + } + + @override + void dispose() { + // TODO: implement dispose + super.dispose(); + } + + Future _onDeleteOutlet() async { + try { + await ref.read(outletControllerProvider.notifier).deleteOutlet(widget.id); + + if (!mounted) return; + + final currentOutletId = await ref.read(currentOutletIdProvider); + + if (currentOutletId == widget.id) { + final outlet = await ref.read(outletRepositoryProvider).getMainOutlet(); + + await SystemSetting.switchOutlet( + outletId: outlet.id, + outletName: outlet.name, + ); + + ref.read(currentOutletIdProvider.notifier).state = outlet.id; + + ref.read(currentOutletNameProvider.notifier).state = outlet.name; + } + + CustomSnackbar.showSuccess(context, 'Outlet berhasil dihapus'); + context.pop(); + context.pop(); + context.pop(); + } catch (e, st) { + LogMessage.log.e(e.toString(), error: e, stackTrace: st); + CustomSnackbar.showError(context, "Ups, terjadi kesalahan"); + context.pop(); + context.pop(); + context.pop(); + } + } + + @override + Widget build(BuildContext context) { + final bool isTablet = 100.w >= 600; + + final double maxWidth = 100.w >= 1280 + ? 1200 + : isTablet + ? 800 + : 400; + + final outletControllerState = ref.watch(outletControllerProvider); + + return Dialog( + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(4.w)), + elevation: 0, + backgroundColor: Colors.transparent, + child: Container( + padding: EdgeInsets.all(5.w), + width: double.infinity, + constraints: BoxConstraints(maxWidth: maxWidth), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(4.w), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + padding: EdgeInsets.all(3.w), + decoration: BoxDecoration( + color: Colors.red[50], + shape: BoxShape.circle, + ), + child: Icon(Icons.delete_outline, color: Colors.red, size: 8.w), + ), + SizedBox(height: 2.h), + Text( + "Hapus Outlet?", + style: TextStyle( + fontSize: AppFontSize.medium.sp, + fontWeight: FontWeight.bold, + color: Colors.black87, + ), + ), + SizedBox(height: 1.h), + Text( + "Tindakan ini tidak dapat dibatalkan.", + style: TextStyle( + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + color: Colors.grey[800], + ), + ), + SizedBox(height: 3.h), + + Container( + padding: EdgeInsets.symmetric(vertical: 3.w), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(2.5.w), + ), + child: Row( + children: [ + Container( + height: 15.w, + width: 15.w, + decoration: BoxDecoration( + color: Colors.grey.shade100, + borderRadius: BorderRadius.circular(2.5.w), + ), + child: Icon( + Icons.store_rounded, + color: Colors.grey.shade700, + size: 8.w, + ), + ), + + SizedBox(width: 3.w), + + // Detail Text + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + name, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + fontWeight: FontWeight.bold, + color: Colors.black87, + ), + ), + SizedBox(height: 0.5.h), + // Badge Terjual + Container( + padding: EdgeInsets.symmetric( + horizontal: 2.w, + vertical: 0.5.h, + ), + decoration: BoxDecoration( + color: Colors.green[50], + borderRadius: BorderRadius.circular(1.w), + ), + child: Text( + "$transactionAmount Transaksi", + style: TextStyle( + fontSize: isTablet + ? (AppFontSize.medium - 2).sp + : (AppFontSize.small - 2).sp, + color: Colors.green[700], + fontWeight: FontWeight.w600, + ), + ), + ), + ], + ), + ), + ], + ), + ), + + SizedBox(height: 3.h), + + _buildButton( + context, + label: "Hapus", + color: Colors.red, + onPressed: outletControllerState.isLoading + ? null + : _onDeleteOutlet, + ), + + SizedBox(height: 1.5.h), + + TextButton( + onPressed: outletControllerState.isLoading + ? null + : () => context.pop(), + child: Text( + "Batal", + style: TextStyle( + color: Colors.grey[600], + fontSize: AppFontSize.medium.sp, + fontWeight: FontWeight.w600, + ), + ), + ), + ], + ), + ), + ); + } + + Widget _buildButton( + BuildContext context, { + required String label, + required Color color, + required VoidCallback? onPressed, + }) { + return SizedBox( + width: double.infinity, + height: 6.h, + child: ElevatedButton( + onPressed: onPressed, + style: ElevatedButton.styleFrom( + backgroundColor: color, + elevation: 0, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(2.5.w), + ), + disabledBackgroundColor: Colors.grey.shade300, + ), + child: Text( + label, + style: TextStyle( + color: Colors.white, + fontSize: AppFontSize.medium.sp, + fontWeight: FontWeight.bold, + ), + ), + ), + ); + } +} diff --git a/Mobile Operasional/lib/features/outlets/presentation/screens/delete_staff_admin_screen.dart b/Mobile Operasional/lib/features/outlets/presentation/screens/delete_staff_admin_screen.dart new file mode 100644 index 0000000..7882ca2 --- /dev/null +++ b/Mobile Operasional/lib/features/outlets/presentation/screens/delete_staff_admin_screen.dart @@ -0,0 +1,255 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; +import 'package:niogu_app/core/constants/app_color.dart'; +import 'package:niogu_app/core/constants/app_font_size.dart'; +import 'package:niogu_app/core/utils/log_message.dart'; +import 'package:niogu_app/core/widgets/custom_snackbar.dart'; +import 'package:niogu_app/features/outlets/presentation/providers/outlet_provider.dart'; +import 'package:sizer/sizer.dart'; + +class DeleteStaffAdminScreen extends ConsumerStatefulWidget { + final String id; + final String name; + final int transactionAmount; + const DeleteStaffAdminScreen({ + super.key, + required this.id, + required this.name, + required this.transactionAmount, + }); + + @override + ConsumerState createState() => + _DeleteStaffAdminScreenState(); +} + +class _DeleteStaffAdminScreenState + extends ConsumerState { + late String name; + + late int transactionAmount; + + @override + void initState() { + // TODO: implement initState + super.initState(); + + name = widget.name; + + transactionAmount = widget.transactionAmount; + } + + @override + void dispose() { + // TODO: implement dispose + super.dispose(); + } + + Future _onDeleteStaff() async { + try { + await ref + .read(outletControllerProvider.notifier) + .deleteStaffAdmin(widget.id); + + if (!mounted) return; + + CustomSnackbar.showSuccess(context, 'Staf berhasil dihapus'); + context.pop(); + context.pop(); + } catch (e, st) { + LogMessage.log.e(e.toString(), error: e, stackTrace: st); + CustomSnackbar.showError(context, "Ups, terjadi kesalahan"); + context.pop(); + context.pop(); + } + } + + @override + Widget build(BuildContext context) { + final bool isTablet = 100.w >= 600; + + final double maxWidth = 100.w >= 1280 + ? 1200 + : isTablet + ? 800 + : 400; + + final outletControllerState = ref.watch(outletControllerProvider); + + return Dialog( + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(4.w)), + elevation: 0, + backgroundColor: Colors.transparent, + child: Container( + padding: EdgeInsets.all(5.w), + width: double.infinity, + constraints: BoxConstraints(maxWidth: maxWidth), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(4.w), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + padding: EdgeInsets.all(3.w), + decoration: BoxDecoration( + color: Colors.red[50], + shape: BoxShape.circle, + ), + child: Icon(Icons.delete_outline, color: Colors.red, size: 8.w), + ), + SizedBox(height: 2.h), + Text( + "Hapus Staf?", + style: TextStyle( + fontSize: AppFontSize.medium.sp, + fontWeight: FontWeight.bold, + color: Colors.black87, + ), + ), + SizedBox(height: 1.h), + Text( + "Tindakan ini tidak dapat dibatalkan.", + style: TextStyle( + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + color: Colors.grey[800], + ), + ), + SizedBox(height: 3.h), + + Container( + padding: EdgeInsets.symmetric(vertical: 3.w), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(2.5.w), + ), + child: Row( + children: [ + Container( + height: 10.w, + width: 10.w, + decoration: BoxDecoration( + color: Colors.blue[50], + borderRadius: BorderRadius.circular(2.5.w), + ), + child: Icon( + Icons.person_outline, + color: AppColor.primaryColor, + size: 5.w, + ), + ), + + SizedBox(width: 3.w), + + // Detail Text + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + name, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + fontWeight: FontWeight.bold, + color: Colors.black87, + ), + ), + SizedBox(height: 0.5.h), + // Badge Terjual + Container( + padding: EdgeInsets.symmetric( + horizontal: 2.w, + vertical: 0.5.h, + ), + decoration: BoxDecoration( + color: Colors.green[50], + borderRadius: BorderRadius.circular(1.w), + ), + child: Text( + "$transactionAmount Transaksi", + style: TextStyle( + fontSize: isTablet + ? (AppFontSize.medium - 2).sp + : (AppFontSize.small - 2).sp, + color: Colors.green[700], + fontWeight: FontWeight.w600, + ), + ), + ), + ], + ), + ), + ], + ), + ), + + SizedBox(height: 3.h), + + _buildButton( + context, + label: "Hapus", + color: Colors.red, + onPressed: outletControllerState.isLoading + ? null + : _onDeleteStaff, + ), + + SizedBox(height: 1.5.h), + + TextButton( + onPressed: outletControllerState.isLoading + ? null + : () => context.pop(), + child: Text( + "Batal", + style: TextStyle( + color: Colors.grey[600], + fontSize: AppFontSize.medium.sp, + fontWeight: FontWeight.w600, + ), + ), + ), + ], + ), + ), + ); + } + + Widget _buildButton( + BuildContext context, { + required String label, + required Color color, + required VoidCallback? onPressed, + }) { + return SizedBox( + width: double.infinity, + height: 6.h, + child: ElevatedButton( + onPressed: onPressed, + style: ElevatedButton.styleFrom( + backgroundColor: color, + elevation: 0, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(2.5.w), + ), + disabledBackgroundColor: Colors.grey.shade300, + ), + child: Text( + label, + style: TextStyle( + color: Colors.white, + fontSize: AppFontSize.medium.sp, + fontWeight: FontWeight.bold, + ), + ), + ), + ); + } +} diff --git a/Mobile Operasional/lib/features/outlets/presentation/screens/edit_outlet_screen.dart b/Mobile Operasional/lib/features/outlets/presentation/screens/edit_outlet_screen.dart new file mode 100644 index 0000000..4ffe93b --- /dev/null +++ b/Mobile Operasional/lib/features/outlets/presentation/screens/edit_outlet_screen.dart @@ -0,0 +1,875 @@ +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; +import 'package:image_picker/image_picker.dart'; +import 'package:latlong2/latlong.dart'; +import 'package:niogu_app/core/components/top_back_bar_app.dart'; +import 'package:niogu_app/core/constants/app_color.dart'; +import 'package:niogu_app/core/constants/app_font_size.dart'; +import 'package:niogu_app/core/providers/app_provider.dart'; +import 'package:niogu_app/core/router/app_route.dart'; +import 'package:niogu_app/core/utils/image_service.dart'; +import 'package:niogu_app/core/utils/log_message.dart'; +import 'package:niogu_app/core/widgets/custom_snackbar.dart'; +import 'package:niogu_app/core/widgets/custom_text_form_field.dart'; +import 'package:niogu_app/features/outlets/domain/entities/outlet.dart'; +import 'package:niogu_app/features/outlets/presentation/providers/outlet_provider.dart'; +import 'package:niogu_app/features/outlets/presentation/screens/delete_outlet_screen.dart'; +import 'package:niogu_app/features/outlets/presentation/widgets/edit_outlet_shimmer.dart'; +import 'package:sizer/sizer.dart'; + +class EditOutletScreen extends ConsumerStatefulWidget { + final String outletId; + const EditOutletScreen({super.key, required this.outletId}); + + @override + ConsumerState createState() => _EditOutletScreenState(); +} + +class _EditOutletScreenState extends ConsumerState { + final GlobalKey _generalKey = GlobalKey(); + + final TextEditingController _nameController = TextEditingController(); + + final TextEditingController _phoneNumberController = TextEditingController(); + + final TextEditingController _emailController = TextEditingController(); + + final ImagePicker _picker = ImagePicker(); + + final _emailRegex = RegExp( + r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$', + ); + + final List _imagePathTemps = []; + + String? _imagePath; + + bool _initializeIsMainOutlet = false; + + bool _isMainOutlet = false; + + bool _isActive = false; + + int _transactionAmount = 0; + + bool _isLoading = true; + + @override + void initState() { + // TODO: implement initState + super.initState(); + + WidgetsBinding.instance.addPostFrameCallback((_) { + Future.delayed(const Duration(milliseconds: 800), () { + ref.invalidate(mapOutletAddressProvider); + + _fetchOutlet(); + }); + }); + } + + @override + void dispose() { + // TODO: implement dispose + _nameController.dispose(); + _phoneNumberController.dispose(); + _emailController.dispose(); + super.dispose(); + } + + Future _fetchOutlet() async { + try { + final outlet = await ref + .read(outletRepositoryProvider) + .getOutletById(widget.outletId); + + if (!mounted) return; + + _imagePath = outlet.bannerPath; + + _nameController.text = outlet.name; + + _phoneNumberController.text = outlet.phoneNumber; + + if (outlet.email != null) { + _emailController.text = outlet.email!; + } + + if (outlet.fullAddress != null && + outlet.latitude != null && + outlet.longitude != null) { + ref + .read(mapOutletAddressProvider.notifier) + .state = OutletAddressSelected( + fullAddress: outlet.fullAddress!, + currentLocation: LatLng(outlet.latitude!, outlet.longitude!), + ); + } + + _initializeIsMainOutlet = outlet.isMainOutlet; + + _isMainOutlet = outlet.isMainOutlet; + + _isActive = outlet.isActive; + + _transactionAmount = outlet.transactionAmount; + + setState(() { + _isLoading = false; + }); + } catch (e, st) { + if (!mounted) return; + + setState(() { + _isLoading = false; + }); + + LogMessage.log.e(e.toString(), error: e, stackTrace: st); + CustomSnackbar.showError(context, "Ups, terjadi kesalahan"); + context.pop(); + } + } + + Future _showPickerOptions() async { + final bool isTablet = 100.w >= 600; + showModalBottomSheet( + context: context, + backgroundColor: Colors.white, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.vertical(top: Radius.circular(2.5.w)), + ), + constraints: const BoxConstraints(maxWidth: double.infinity), + builder: (BuildContext context) { + return SafeArea( + child: Container( + width: 100.w, + padding: EdgeInsets.symmetric(vertical: 2.h, horizontal: 5.w), + clipBehavior: Clip.hardEdge, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.vertical(top: Radius.circular(6.w)), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + ListTile( + leading: Icon(Icons.photo_library, size: 6.w), + title: Text( + 'Galeri', + style: TextStyle( + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + ), + ), + onTap: () async { + await _getImage(ImageSource.gallery); + context.pop(); + }, + ), + + SizedBox(height: 2.h), + + ListTile( + leading: Icon(Icons.photo_camera, size: 6.w), + title: Text( + 'Kamera', + style: TextStyle( + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + ), + ), + onTap: () async { + await _getImage(ImageSource.camera); + context.pop(); + }, + ), + ], + ), + ), + ); + }, + ); + } + + Future _getImage(ImageSource ImageSource) async { + try { + final XFile? pickedFile = await _picker.pickImage( + source: ImageSource, + imageQuality: 70, + ); + + if (pickedFile != null) { + File tempFile = File(pickedFile.path); + + String? imagePath = await ImageService.saveImageToLocalDirectory( + tempFile, + "outlet_banner_images", + ); + + if (imagePath != null) { + _imagePathTemps.add(imagePath); + setState(() { + _imagePath = imagePath; + }); + } + } + } catch (e, st) { + LogMessage.log.e(e.toString(), error: e, stackTrace: st); + CustomSnackbar.showWarning(context, "Akses ditolak"); + } + } + + Future _cleanUpImages() async { + for (final image in _imagePathTemps) { + if (image != null) { + await ImageService.deleteLocalImage(image); + } + } + } + + String? _validation(String? value, String message, {bool isEmail = false}) { + if (value == null || value.trim().isEmpty) { + return message; + } + + if (isEmail && !_emailRegex.hasMatch(value)) { + return "Email tidak valid"; + } + + return null; + } + + Future _saveOutlet() async { + if (!_generalKey.currentState!.validate()) return; + + final phoneNumber = _phoneNumberController.text.trim(); + + final email = _emailController.text.trim().isNotEmpty + ? _emailController.text.trim() + : null; + + final validatePhoneAndEmail = await ref + .read(outletRepositoryProvider) + .validatePhoneAndEmail( + phoneNumber: phoneNumber, + email: email, + excludeId: widget.outletId, + ); + + if (validatePhoneAndEmail) { + CustomSnackbar.showError( + context, + 'No. hp atau email outlet telah tersedia', + ); + + return; + } + + final selectedOutletAddress = ref.read(mapOutletAddressProvider); + + final upsertOutlet = UpsertOutlet( + id: widget.outletId, + bannerPath: _imagePath, + name: _nameController.text.trim(), + phoneNumber: _phoneNumberController.text.trim(), + email: _emailController.text.trim().isNotEmpty + ? _emailController.text.trim() + : null, + fullAddress: selectedOutletAddress?.fullAddress, + latitude: selectedOutletAddress?.currentLocation.latitude, + longitude: selectedOutletAddress?.currentLocation.longitude, + isMainOutlet: _isMainOutlet, + isActive: _isMainOutlet ? true : _isActive, + ); + + if (_imagePath == null && _imagePathTemps.isNotEmpty) { + await _cleanUpImages(); + } else if (_imagePath != null && + _imagePathTemps.isNotEmpty && + _imagePathTemps.length > 1) { + final imagePathTemps = _imagePathTemps + .where((i) => i != _imagePath) + .toList(); + + for (final image in imagePathTemps) { + if (image != null) { + await ImageService.deleteLocalImage(image); + } + } + } + + try { + await ref + .read(outletControllerProvider.notifier) + .saveOutlet(upsertOutlet); + + if (!mounted) return; + + ref.invalidate(mapOutletAddressProvider); + + final currentOutletId = ref.read(currentOutletIdProvider); + + if (currentOutletId == upsertOutlet.id) { + ref.read(currentOutletNameProvider.notifier).state = upsertOutlet.name; + } + + CustomSnackbar.showSuccess(context, 'Outlet berhasil diubah'); + context.pop(); + if (currentOutletId == upsertOutlet.id && !upsertOutlet.isActive) { + context.pop(); + } + } catch (e, st) { + LogMessage.log.e(e.toString(), error: e, stackTrace: st); + CustomSnackbar.showError(context, 'Ups, terjadi kesalahan'); + } + } + + @override + Widget build(BuildContext context) { + return LayoutBuilder( + builder: (context, constraints) { + final bool isTablet = 100.w >= 600; + + final outletControllerState = ref.watch(outletControllerProvider); + + final mapOutletAddressState = ref.watch(mapOutletAddressProvider); + + final adminState = ref.watch(staffAdminProvider(widget.outletId)); + + final List admins = []; + + adminState.whenData((a) => admins.addAll(a)); + + final File imageFile = File(_imagePath ?? "image not found"); + + final bool imageFileExists = imageFile.existsSync(); + + return PopScope( + canPop: false, + onPopInvokedWithResult: (didPop, result) async { + if (didPop) return; + + await _cleanUpImages(); + + context.pop(); + + WidgetsBinding.instance.addPostFrameCallback((_) { + ref.invalidate(mapOutletAddressProvider); + }); + }, + child: SafeArea( + top: false, + bottom: true, + right: false, + left: false, + child: Scaffold( + backgroundColor: Colors.white, + appBar: TopBackBarApp( + title: "Edit Outlet", + onTap: () async { + await _cleanUpImages(); + + context.pop(); + + WidgetsBinding.instance.addPostFrameCallback((_) { + ref.invalidate(mapOutletAddressProvider); + }); + }, + ), + body: _isLoading + ? Padding( + padding: EdgeInsets.all(5.w), + child: const EditOutletShimmer(), + ) + : SingleChildScrollView( + padding: EdgeInsets.all(5.w), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: EdgeInsets.only(bottom: 1.5.h), + child: Text( + "Foto Outlet", + style: TextStyle( + fontSize: (AppFontSize.medium - 1.25).sp, + fontWeight: FontWeight.bold, + color: Colors.black87, + ), + ), + ), + Stack( + clipBehavior: Clip.none, + children: [ + Container( + height: 20.h, + width: double.infinity, + decoration: BoxDecoration( + color: Colors.grey.shade100, + borderRadius: BorderRadius.circular(3.w), + border: Border.all( + color: Colors.grey.shade300, + style: BorderStyle.none, + ), + ), + child: imageFileExists + ? ClipRRect( + borderRadius: BorderRadius.circular( + 2.5.w, + ), + child: Image.file( + imageFile, + fit: BoxFit.cover, + width: double.infinity, + height: double.infinity, + errorBuilder: + (context, error, stackTrace) { + return Icon( + Icons.error, + color: Colors.grey[400], + size: 10.w, + ); + }, + ), + ) + : GestureDetector( + onTap: _showPickerOptions, + child: Column( + mainAxisAlignment: + MainAxisAlignment.center, + children: [ + Icon( + Icons.add_a_photo_outlined, + size: 10.w, + color: Colors.grey.shade700, + ), + SizedBox(height: 1.h), + Text( + "Unggah Foto Landscape", + style: TextStyle( + fontSize: isTablet + ? (AppFontSize.medium - 2) + .sp + : (AppFontSize.small - 2) + .sp, + color: Colors.grey.shade700, + ), + ), + ], + ), + ), + ), + + if (_imagePath != null) + Positioned( + top: 1.w, + right: 1.w, + child: Material( + color: Colors.transparent, + type: MaterialType.transparency, + child: InkWell( + onTap: () { + setState(() { + _imagePath = null; + }); + }, + child: Container( + padding: EdgeInsets.all(1.w), + decoration: BoxDecoration( + color: Colors.red[50], + shape: BoxShape.circle, + border: Border.all( + color: Colors.red.withOpacity(0.2), + ), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity( + 0.1, + ), + blurRadius: 2, + offset: Offset(0, 1), + ), + ], + ), + child: Icon( + Icons.remove, + color: Colors.red, + size: 4.w, + ), + ), + ), + ), + ), + ], + ), + SizedBox(height: 3.h), + + Form( + key: _generalKey, + child: Column( + children: [ + CustomTextFormField( + label: "Nama Outlet", + hint: "Contoh: Outlet Perum Gunung Batu", + prefixIcon: + Icons.store_mall_directory_outlined, + controller: _nameController, + validator: (value) => _validation( + value, + "Nama outlet belum diisi", + ), + ), + SizedBox(height: 3.h), + + CustomTextFormField( + label: "No. Handphone / WA Outlet", + hint: "081234567890", + prefixIcon: Icons.phone_android_outlined, + controller: _phoneNumberController, + keyboardType: TextInputType.phone, + validator: (value) => _validation( + value, + "No. handphone outlet belum diisi", + ), + ), + SizedBox(height: 3.h), + CustomTextFormField( + label: "Email Outlet", + hint: "(Opsional)", + prefixIcon: Icons.email_outlined, + keyboardType: TextInputType.emailAddress, + controller: _emailController, + validator: (value) { + if (value == null || value.isEmpty) { + return null; + } + + if (!_emailRegex.hasMatch(value)) { + return "Email outlet tidak valid"; + } + + return null; + }, + ), + ], + ), + ), + SizedBox(height: 3.h), + + Material( + color: Colors.transparent, + type: MaterialType.transparency, + child: InkWell( + onTap: () async { + final adminState = ref.read( + staffAdminProvider(widget.outletId), + ); + + final List admins = []; + + adminState.whenData((a) => admins.addAll(a)); + + await Future.delayed( + const Duration(milliseconds: 400), + ); + + context.pushNamed( + AppRoute.editStaffAdminScreen, + extra: { + "outlet_id": widget.outletId, + "admins": admins, + }, + ); + }, + child: Container( + width: double.infinity, + padding: EdgeInsets.symmetric( + horizontal: 4.w, + vertical: 2.h, + ), + decoration: BoxDecoration( + border: Border.all( + color: Colors.grey.shade300, + ), + borderRadius: BorderRadius.circular(2.05.w), + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Icon( + Icons.person_outline, + color: Colors.black, + size: 5.w, + ), + + SizedBox(width: 2.5.w), + + Expanded( + child: Text( + admins.isEmpty + ? "Staf admin belum biatur" + : "${admins.length} Staf admin", + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + height: 1.4, + ), + ), + ), + ], + ), + ), + ), + ), + SizedBox(height: 3.h), + Material( + color: Colors.transparent, + type: MaterialType.transparency, + child: InkWell( + onTap: () { + context.pushNamed( + AppRoute.mapOutletAdressScreen, + ); + }, + child: Container( + width: double.infinity, + padding: EdgeInsets.symmetric( + horizontal: 4.w, + vertical: 2.h, + ), + decoration: BoxDecoration( + border: Border.all( + color: Colors.grey.shade300, + ), + borderRadius: BorderRadius.circular(2.05.w), + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Icon( + Icons.map_outlined, + color: Colors.black, + size: 5.w, + ), + + SizedBox(width: 2.5.w), + + Expanded( + child: Text( + mapOutletAddressState != null + ? mapOutletAddressState.fullAddress + : "Atur alamat outlet", + style: TextStyle( + fontWeight: + mapOutletAddressState != null + ? FontWeight.normal + : FontWeight.bold, + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + height: 1.4, + ), + ), + ), + ], + ), + ), + ), + ), + + if (!_initializeIsMainOutlet) ...[ + SizedBox(height: 3.h), + Container( + width: double.infinity, + padding: EdgeInsets.symmetric( + horizontal: 4.w, + vertical: 2.h, + ), + decoration: BoxDecoration( + border: Border.all(color: Colors.grey.shade300), + borderRadius: BorderRadius.circular(2.05.w), + ), + child: Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Text( + "Jadikan Outlet Utama", + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + height: 1.4, + ), + ), + ), + SizedBox( + width: 10.w, + child: FittedBox( + fit: BoxFit.fill, + child: Switch( + value: _isMainOutlet, + onChanged: (_) { + setState(() { + _isMainOutlet = !_isMainOutlet; + if (!_isActive) { + _isActive = !_isActive; + } + }); + }, + activeColor: _isMainOutlet + ? AppColor.primaryColor + : Colors.grey, + ), + ), + ), + ], + ), + ), + ], + SizedBox(height: 3.h), + Container( + padding: EdgeInsets.all(4.w), + decoration: BoxDecoration( + color: Colors.red.withOpacity(0.05), + borderRadius: BorderRadius.circular(3.w), + border: Border.all( + color: Colors.red.withOpacity(0.1), + ), + ), + child: Column( + children: [ + ListTile( + contentPadding: EdgeInsets.zero, + leading: Icon( + Icons.power_settings_new, + size: 5.w, + color: _isActive + ? Colors.green + : Colors.grey, + ), + title: Text( + "Status Operasional Outlet", + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: AppFontSize.medium.sp, + ), + ), + subtitle: Text( + _isActive + ? "Aktif / Buka" + : "Non Aktif / Tutup", + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + ), + ), + trailing: + _initializeIsMainOutlet || _isMainOutlet + ? null + : SizedBox( + width: 10.w, + child: FittedBox( + fit: BoxFit.fill, + child: Switch( + value: _isActive, + onChanged: (_) { + setState(() { + _isActive = !_isActive; + }); + }, + activeColor: _isActive + ? Colors.green + : Colors.grey, + ), + ), + ), + ), + if (_initializeIsMainOutlet) ...[ + Divider(color: Colors.red.withOpacity(0.1)), + + Text( + "Outlet Utama", + style: TextStyle( + color: Colors.grey.shade700, + fontWeight: FontWeight.bold, + fontSize: (AppFontSize.medium - 1.25).sp, + ), + ), + ] else if (!_isMainOutlet) ...[ + Divider(color: Colors.red.withOpacity(0.1)), + TextButton.icon( + onPressed: () { + showDialog( + context: context, + builder: (context) => + DeleteOutletScreen( + id: widget.outletId, + name: _nameController.text, + transactionAmount: + _transactionAmount, + ), + ); + }, + icon: Icon( + Icons.delete_forever, + size: 5.w, + color: Colors.red, + ), + label: Text( + "Hapus Outlet Permanen", + style: TextStyle( + color: Colors.red, + fontWeight: FontWeight.bold, + fontSize: + (AppFontSize.medium - 1.25).sp, + ), + ), + ), + ], + ], + ), + ), + SizedBox(height: 3.h), + ElevatedButton( + onPressed: outletControllerState.isLoading + ? null + : _saveOutlet, + style: ElevatedButton.styleFrom( + backgroundColor: AppColor.primaryColor, + minimumSize: Size(double.infinity, 7.h), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(3.w), + ), + disabledBackgroundColor: Colors.grey.shade300, + ), + child: Text( + "Simpan", + style: TextStyle( + fontSize: AppFontSize.medium.sp, + fontWeight: FontWeight.bold, + color: Colors.white, + ), + ), + ), + SizedBox(height: 3.h), + ], + ), + ), + ), + ), + ); + }, + ); + } +} diff --git a/Mobile Operasional/lib/features/outlets/presentation/screens/edit_staff_admin_screen.dart b/Mobile Operasional/lib/features/outlets/presentation/screens/edit_staff_admin_screen.dart new file mode 100644 index 0000000..6c93a64 --- /dev/null +++ b/Mobile Operasional/lib/features/outlets/presentation/screens/edit_staff_admin_screen.dart @@ -0,0 +1,1056 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; +import 'package:niogu_app/core/components/top_back_bar_app.dart'; +import 'package:niogu_app/core/constants/app_color.dart'; +import 'package:niogu_app/core/constants/app_font_size.dart'; +import 'package:niogu_app/core/utils/log_message.dart'; +import 'package:niogu_app/core/widgets/custom_snackbar.dart'; +import 'package:niogu_app/core/widgets/custom_text_form_field.dart'; +import 'package:niogu_app/features/outlets/domain/entities/outlet.dart'; +import 'package:niogu_app/features/outlets/presentation/providers/outlet_provider.dart'; +import 'package:niogu_app/features/outlets/presentation/screens/add_outlet_screen.dart'; +import 'package:niogu_app/features/outlets/presentation/screens/delete_staff_admin_screen.dart'; +import 'package:niogu_app/features/outlets/presentation/widgets/staf_admin.dart'; +import 'package:niogu_app/core/widgets/custom_time_picker.dart'; +import 'package:sizer/sizer.dart'; + +class EditStaffAdminScreen extends ConsumerStatefulWidget { + final String outletId; + final List admins; + const EditStaffAdminScreen({ + super.key, + required this.outletId, + required this.admins, + }); + + @override + ConsumerState createState() => + _EditStafAdminScreenState(); +} + +class _EditStafAdminScreenState extends ConsumerState { + bool _visibleFirstForm = false; + + final StaffAdminInformation _firstStaffAdmin = StaffAdminInformation( + nameController: TextEditingController(), + phoneNumberController: TextEditingController(), + emailController: TextEditingController(), + passwordController: TextEditingController(), + passwordConfirmController: TextEditingController(), + shiftNameController: TextEditingController(), + ); + + final GlobalKey _firstAdminKey = GlobalKey(); + + bool _visbleSecondForm = false; + + final StaffAdminInformation _secondStaffAdmin = StaffAdminInformation( + nameController: TextEditingController(), + phoneNumberController: TextEditingController(), + emailController: TextEditingController(), + passwordController: TextEditingController(), + passwordConfirmController: TextEditingController(), + shiftNameController: TextEditingController(), + ); + + final GlobalKey _secondAdminKey = GlobalKey(); + + final _emailRegex = RegExp( + r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$', + ); + + final Map _statusStaffAdmin = {}; + + @override + void initState() { + // TODO: implement initState + super.initState(); + + for (var i = 0; i < widget.admins.length; i++) { + final admin = widget.admins[i]; + + _statusStaffAdmin.putIfAbsent(admin.id, () => admin.isActive); + + if (i == 0) { + _firstStaffAdmin.shiftNameController.text = admin.shiftName; + _firstStaffAdmin.startTime = _parseTimeOfDay(admin.shiftStartTime); + _firstStaffAdmin.endTime = _parseTimeOfDay(admin.shiftEndTime); + } else { + _secondStaffAdmin.shiftNameController.text = admin.shiftName; + _secondStaffAdmin.startTime = _parseTimeOfDay(admin.shiftStartTime); + _secondStaffAdmin.endTime = _parseTimeOfDay(admin.shiftEndTime); + } + } + } + + @override + void dispose() { + // TODO: implement dispose + _firstStaffAdmin.nameController.dispose(); + _firstStaffAdmin.phoneNumberController.dispose(); + _firstStaffAdmin.emailController.dispose(); + _firstStaffAdmin.passwordController.dispose(); + _firstStaffAdmin.passwordConfirmController.dispose(); + _firstStaffAdmin.shiftNameController.dispose(); + _secondStaffAdmin.nameController.dispose(); + _secondStaffAdmin.phoneNumberController.dispose(); + _secondStaffAdmin.emailController.dispose(); + _secondStaffAdmin.passwordController.dispose(); + _secondStaffAdmin.passwordConfirmController.dispose(); + _secondStaffAdmin.shiftNameController.dispose(); + super.dispose(); + } + + TimeOfDay _parseTimeOfDay(String timeString) { + List parts = timeString.split('.'); + + int hour = int.parse(parts[0]); + int minute = int.parse(parts[1]); + + return TimeOfDay(hour: hour, minute: minute); + } + + Future _selectTime(bool isStart, StaffAdminInformation? admin) async { + final TimeOfDay? picked = await showTimePicker( + context: context, + initialTime: admin != null + ? (isStart ? admin.startTime : admin.endTime) + : TimeOfDay.now(), + builder: (BuildContext context, Widget? child) { + return MediaQuery( + data: MediaQuery.of(context).copyWith(alwaysUse24HourFormat: true), + child: child!, + ); + }, + ); + + if (picked != null) { + setState(() { + if (isStart) { + admin?.startTime = picked; + } else { + admin?.endTime = picked; + } + }); + } + } + + int _toMinutes(TimeOfDay time) { + return time.hour * 60 + time.minute; + } + + List> _normalizeShift(TimeOfDay start, TimeOfDay end) { + final s = _toMinutes(start); + final e = _toMinutes(end); + + if (e > s) { + return [ + [s, e], + ]; + } else { + return [ + [s, 1440], + [0, e], + ]; + } + } + + int _calculateDuration(TimeOfDay start, TimeOfDay end) { + final s = _toMinutes(start); + final e = _toMinutes(end); + + if (e > s) { + return e - s; + } else { + return (1440 - s) + e; + } + } + + String? _validateShift(TimeOfDay? start, TimeOfDay? end, int shift) { + if (start == null && end == null) { + return null; + } + + final duration = _calculateDuration(start!, end!); + + if (duration == 0) { + return "Shift $shift tidak boleh 24 jam penuh"; + } + + if (duration <= 0) { + return "Jam selesai shift $shift harus berbeda dari jam mulai shift $shift"; + } + + if (duration > 16 * 60) { + return "Shift $shift tidak boleh lebih dari 16 jam"; + } + + return null; + } + + bool _isShiftOverlap({ + required TimeOfDay start1, + required TimeOfDay end1, + required TimeOfDay start2, + required TimeOfDay end2, + }) { + final shift1 = _normalizeShift(start1, end1); + + final shift2 = _normalizeShift(start2, end2); + + for (final a in shift1) { + for (final b in shift2) { + final s1 = a[0]; + final e1 = a[1]; + final s2 = b[0]; + final e2 = b[1]; + + if (s1 < e2 && e1 > s2) { + return true; + } + } + } + + return false; + } + + String? _validation( + String? value, + String message, { + StaffAdminInformation? admin, + bool isEmail = false, + bool isPassword = false, + bool isPasswordConfirm = false, + }) { + if (value == null || value.trim().isEmpty) { + return message; + } + + if (isEmail && !_emailRegex.hasMatch(value)) { + return "Email tidak valid"; + } + + if (isPassword) { + if (value.length < 8) { + return "Password minimal harus 8 karakter"; + } + + final passwordRegExp = RegExp( + r'^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&#])[A-Za-z\d@$!%*?&#]{8,}$', + ); + + if (!passwordRegExp.hasMatch(value)) { + return "Gunakan huruf besar, kecil, angka, dan simbol"; + } + } + + if (isPasswordConfirm && value != admin?.passwordController.text.trim()) { + return "Konfirmasi password tidak sesuai"; + } + + return null; + } + + Future _saveStaffAdmin() async { + UpsertStaffAdmin? firstStaffAdmin; + + UpsertStaffAdmin? secondStaffAdmin; + + final admins = widget.admins; + + if (admins.length == 0) { + final firstFormHasValue = + _firstStaffAdmin.nameController.text.isNotEmpty || + _firstStaffAdmin.phoneNumberController.text.isNotEmpty || + _firstStaffAdmin.emailController.text.isNotEmpty || + _firstStaffAdmin.passwordController.text.isNotEmpty || + _firstStaffAdmin.passwordConfirmController.text.isNotEmpty || + _firstStaffAdmin.shiftNameController.text.isNotEmpty; + + if (!_visibleFirstForm && !firstFormHasValue) { + CustomSnackbar.showError(context, "Informasi staf admin 1 wajib diisi"); + return; + } + + if (!_firstAdminKey.currentState!.validate()) return; + + final bool secondFormHasValue = + _secondStaffAdmin.nameController.text.isNotEmpty || + _secondStaffAdmin.phoneNumberController.text.isNotEmpty || + _secondStaffAdmin.emailController.text.isNotEmpty || + _secondStaffAdmin.passwordController.text.isNotEmpty || + _secondStaffAdmin.passwordConfirmController.text.isNotEmpty || + _secondStaffAdmin.shiftNameController.text.isNotEmpty; + + if (secondFormHasValue && !_secondAdminKey.currentState!.validate()) { + return; + } + + final firstPhoneNumber = _firstStaffAdmin.phoneNumberController.text; + + final firstEmail = _firstStaffAdmin.emailController.text; + + final secondPhoneNumber = _secondStaffAdmin.phoneNumberController.text; + + final secondEmail = _secondStaffAdmin.emailController.text; + + if (firstPhoneNumber == secondPhoneNumber) { + CustomSnackbar.showError( + context, + "No. hp staf 1 dan 2 tidak boleh sama", + ); + return; + } + + if (firstEmail == secondEmail) { + CustomSnackbar.showError( + context, + "Email staf 1 dan 2 tidak boleh sama", + ); + return; + } + + final validateFirstShift = _validateShift( + _firstStaffAdmin.startTime, + _firstStaffAdmin.endTime, + 1, + ); + + if (validateFirstShift != null) { + CustomSnackbar.showError(context, validateFirstShift); + return; + } + + final validateSecondShift = _validateShift( + _secondStaffAdmin.startTime, + _secondStaffAdmin.endTime, + 2, + ); + + if (validateSecondShift != null) { + CustomSnackbar.showError(context, validateSecondShift); + return; + } + + final firstStartShift = _firstStaffAdmin.startTime; + + final firstEndShift = _firstStaffAdmin.endTime; + + final secondStartShift = _secondStaffAdmin.startTime; + + final secondEndShift = _secondStaffAdmin.endTime; + + if (_isShiftOverlap( + start1: firstStartShift, + end1: firstEndShift, + start2: secondStartShift, + end2: secondEndShift, + ) && + secondFormHasValue) { + CustomSnackbar.showError( + context, + "Jam kerja shift 1 konflik dengan shift 2", + ); + return; + } + + final validateAddUserStaff = ref + .read(outletRepositoryProvider) + .validateAddUserStaff( + firstPhoneNumber: firstPhoneNumber, + firstEmail: firstEmail, + secondPhoneNumber: secondPhoneNumber, + secondEmail: secondEmail, + ); + + if (await validateAddUserStaff) { + CustomSnackbar.showError( + context, + "No. hp atau email staf telah tersedia", + ); + return; + } + + firstStaffAdmin = UpsertStaffAdmin( + name: _firstStaffAdmin.nameController.text.trim(), + phoneNumber: _firstStaffAdmin.phoneNumberController.text.trim(), + email: _firstStaffAdmin.emailController.text.trim(), + passwordHash: _firstStaffAdmin.passwordController.text.trim(), + shiftName: _firstStaffAdmin.shiftNameController.text.trim(), + shiftStartTime: _firstStaffAdmin.startTime.format(context), + shiftEndTime: _firstStaffAdmin.endTime.format(context), + ); + + if (secondFormHasValue) { + secondStaffAdmin = UpsertStaffAdmin( + name: _secondStaffAdmin.nameController.text.trim(), + phoneNumber: _secondStaffAdmin.phoneNumberController.text.trim(), + email: _secondStaffAdmin.emailController.text.trim(), + passwordHash: _secondStaffAdmin.passwordController.text.trim(), + shiftName: _secondStaffAdmin.shiftNameController.text.trim(), + shiftStartTime: _secondStaffAdmin.startTime.format(context), + shiftEndTime: _secondStaffAdmin.endTime.format(context), + ); + } + } else if (admins.length == 1) { + final bool secondFormHasValue = + _secondStaffAdmin.nameController.text.isNotEmpty || + _secondStaffAdmin.phoneNumberController.text.isNotEmpty || + _secondStaffAdmin.emailController.text.isNotEmpty || + _secondStaffAdmin.passwordController.text.isNotEmpty || + _secondStaffAdmin.passwordConfirmController.text.isNotEmpty || + _secondStaffAdmin.shiftNameController.text.isNotEmpty; + + if (secondFormHasValue && !_secondAdminKey.currentState!.validate()) { + return; + } + + final firstPhoneNumber = _secondStaffAdmin.phoneNumberController.text; + + final firstEmail = _secondStaffAdmin.emailController.text; + + final validateFirstShift = _validateShift( + _firstStaffAdmin.startTime, + _firstStaffAdmin.endTime, + 1, + ); + + if (validateFirstShift != null) { + CustomSnackbar.showError(context, validateFirstShift); + return; + } + + final validateSecondShift = _validateShift( + _secondStaffAdmin.startTime, + _secondStaffAdmin.endTime, + 2, + ); + + if (validateSecondShift != null) { + CustomSnackbar.showError(context, validateSecondShift); + return; + } + + final firstStartShift = _firstStaffAdmin.startTime; + + final firstEndShift = _firstStaffAdmin.endTime; + + final secondStartShift = _secondStaffAdmin.startTime; + + final secondEndShift = _secondStaffAdmin.endTime; + + if (_isShiftOverlap( + start1: firstStartShift, + end1: firstEndShift, + start2: secondStartShift, + end2: secondEndShift, + ) && + secondFormHasValue) { + CustomSnackbar.showError( + context, + "Jam kerja shift 1 konflik dengan shift 2", + ); + return; + } + + if (firstPhoneNumber.isNotEmpty || firstEmail.isNotEmpty) { + final validateAddUserStaff = ref + .read(outletRepositoryProvider) + .validateAddUserStaff( + firstPhoneNumber: firstPhoneNumber, + firstEmail: firstEmail, + ); + + if (await validateAddUserStaff) { + CustomSnackbar.showError( + context, + "No. hp atau email staf telah tersedia", + ); + return; + } + } + + firstStaffAdmin = UpsertStaffAdmin( + id: admins.single.id, + name: admins.single.name, + phoneNumber: admins.single.phoneNumber, + email: admins.single.email, + shiftName: _firstStaffAdmin.shiftNameController.text.trim(), + shiftStartTime: firstStartShift.format(context), + shiftEndTime: firstEndShift.format(context), + isActive: _statusStaffAdmin[admins.single.id] ?? true, + ); + + if (secondFormHasValue) { + secondStaffAdmin = UpsertStaffAdmin( + name: _secondStaffAdmin.nameController.text.trim(), + phoneNumber: _secondStaffAdmin.phoneNumberController.text.trim(), + email: _secondStaffAdmin.emailController.text.trim(), + passwordHash: _secondStaffAdmin.passwordController.text.trim(), + shiftName: _secondStaffAdmin.shiftNameController.text.trim(), + shiftStartTime: _secondStaffAdmin.startTime.format(context), + shiftEndTime: _secondStaffAdmin.endTime.format(context), + ); + } + } else { + final validateFirstShift = _validateShift( + _firstStaffAdmin.startTime, + _firstStaffAdmin.endTime, + 1, + ); + + if (validateFirstShift != null) { + CustomSnackbar.showError(context, validateFirstShift); + return; + } + + final validateSecondShift = _validateShift( + _secondStaffAdmin.startTime, + _secondStaffAdmin.endTime, + 2, + ); + + if (validateSecondShift != null) { + CustomSnackbar.showError(context, validateSecondShift); + return; + } + + final firstStartShift = _firstStaffAdmin.startTime; + + final firstEndShift = _firstStaffAdmin.endTime; + + final secondStartShift = _secondStaffAdmin.startTime; + + final secondEndShift = _secondStaffAdmin.endTime; + + if (_isShiftOverlap( + start1: firstStartShift, + end1: firstEndShift, + start2: secondStartShift, + end2: secondEndShift, + )) { + CustomSnackbar.showError( + context, + "Jam kerja shift 1 konflik dengan shift 2", + ); + return; + } + + firstStaffAdmin = UpsertStaffAdmin( + id: admins.first.id, + name: admins.first.name, + phoneNumber: admins.first.phoneNumber, + email: admins.first.email, + shiftName: _firstStaffAdmin.shiftNameController.text.trim(), + shiftStartTime: firstStartShift.format(context), + shiftEndTime: firstEndShift.format(context), + isActive: _statusStaffAdmin[admins.first.id] ?? true, + ); + + secondStaffAdmin = UpsertStaffAdmin( + id: admins.last.id, + name: admins.last.name, + phoneNumber: admins.last.phoneNumber, + email: admins.last.email, + shiftName: _secondStaffAdmin.shiftNameController.text.trim(), + shiftStartTime: secondStartShift.format(context), + shiftEndTime: secondEndShift.format(context), + isActive: _statusStaffAdmin[admins.last.id] ?? true, + ); + } + + try { + await ref + .read(outletControllerProvider.notifier) + .saveStaffAdmin( + outletId: widget.outletId, + firstStaffAdmin: firstStaffAdmin, + secondStaffAdmin: secondStaffAdmin, + ); + if (!mounted) return; + + CustomSnackbar.showSuccess(context, 'Staf admin berhasil diperbarui'); + context.pop(); + } catch (e, st) { + LogMessage.log.e(e.toString(), error: e, stackTrace: st); + CustomSnackbar.showError(context, 'Ups, terjadi kesalahan'); + } + } + + @override + Widget build(BuildContext context) { + return LayoutBuilder( + builder: (context, constraints) { + final admins = widget.admins; + + final outletControllerState = ref.watch(outletControllerProvider); + + final bool isLoading = outletControllerState.isLoading; + + return SafeArea( + top: false, + bottom: true, + right: false, + left: false, + child: Scaffold( + backgroundColor: Colors.white, + appBar: TopBackBarApp( + title: "Atur Staf Admin", + onTap: () => context.pop(), + ), + body: CustomScrollView( + slivers: [ + SliverPadding( + padding: EdgeInsets.all(4.w), + sliver: admins.length == 0 + ? SliverToBoxAdapter( + child: Column( + children: [ + Form( + key: _firstAdminKey, + child: StafAdmin( + stafAdminNumber: 1, + isFormVisible: _visibleFirstForm, + onTap: () { + setState(() { + _visibleFirstForm = !_visibleFirstForm; + }); + }, + stafAdmin: _firstStaffAdmin, + passwordSuffixPressed: () { + setState(() { + _firstStaffAdmin.obsecurePassword = + !_firstStaffAdmin.obsecurePassword; + }); + }, + passwordConfirmSuffixPressed: () { + setState(() { + _firstStaffAdmin.obsecurePasswordConfirm = + !_firstStaffAdmin + .obsecurePasswordConfirm; + }); + }, + onStartTimeTap: () => + _selectTime(true, _firstStaffAdmin), + onEndTimeTap: () => + _selectTime(false, _firstStaffAdmin), + nameValidator: (value) => _validation( + value, + "Nama staf belum diisi", + ), + phoneNumberValidator: (value) => _validation( + value, + "No. Handphone staf belum diisi", + ), + emailValidator: (value) => _validation( + value, + "Email staff belum diisi", + isEmail: true, + ), + passwordValidator: (value) => _validation( + value, + "Password belum diisi", + isPassword: true, + admin: _firstStaffAdmin, + ), + passwordConfirmValidator: (value) => + _validation( + value, + "Konfirmasi password belum diisi", + isPasswordConfirm: true, + admin: _firstStaffAdmin, + ), + shiftNameValidator: (value) => _validation( + value, + "Nama shift belum diisi", + ), + ), + ), + + SizedBox(height: 3.h), + + Form( + key: _secondAdminKey, + child: StafAdmin( + stafAdminNumber: 2, + isFormVisible: _visbleSecondForm, + onTap: () { + setState(() { + _visbleSecondForm = !_visbleSecondForm; + }); + }, + stafAdmin: _secondStaffAdmin, + passwordSuffixPressed: () { + setState(() { + _secondStaffAdmin.obsecurePassword = + !_secondStaffAdmin.obsecurePassword; + }); + }, + passwordConfirmSuffixPressed: () { + setState(() { + _secondStaffAdmin + .obsecurePasswordConfirm = + !_secondStaffAdmin + .obsecurePasswordConfirm; + }); + }, + onStartTimeTap: () => + _selectTime(true, _secondStaffAdmin), + onEndTimeTap: () => + _selectTime(false, _secondStaffAdmin), + nameValidator: (value) => _validation( + value, + "Nama staf belum diisi", + ), + phoneNumberValidator: (value) => _validation( + value, + "No. Handphone staf belum diisi", + ), + emailValidator: (value) => _validation( + value, + "Email staff belum diisi", + isEmail: true, + ), + passwordValidator: (value) => _validation( + value, + "Password belum diisi", + isPassword: true, + admin: _secondStaffAdmin, + ), + passwordConfirmValidator: (value) => + _validation( + value, + "Konfirmasi password belum diisi", + isPasswordConfirm: true, + admin: _secondStaffAdmin, + ), + shiftNameValidator: (value) => _validation( + value, + "Nama shift belum diisi", + ), + ), + ), + ], + ), + ) + : admins.length == 1 + ? SliverToBoxAdapter( + child: Column( + children: [ + _buildStaffAdmin( + admins[0], + _firstStaffAdmin, + isLoading, + ), + + SizedBox(height: 3.h), + + Form( + key: _secondAdminKey, + child: StafAdmin( + stafAdminNumber: 2, + isFormVisible: _visibleFirstForm, + onTap: () { + setState(() { + _visibleFirstForm = !_visibleFirstForm; + }); + }, + stafAdmin: _secondStaffAdmin, + passwordSuffixPressed: () { + setState(() { + _secondStaffAdmin.obsecurePassword = + !_secondStaffAdmin.obsecurePassword; + }); + }, + passwordConfirmSuffixPressed: () { + setState(() { + _secondStaffAdmin + .obsecurePasswordConfirm = + !_secondStaffAdmin + .obsecurePasswordConfirm; + }); + }, + onStartTimeTap: () => + _selectTime(true, _secondStaffAdmin), + onEndTimeTap: () => + _selectTime(false, _secondStaffAdmin), + nameValidator: (value) => _validation( + value, + "Nama staf belum diisi", + ), + phoneNumberValidator: (value) => _validation( + value, + "No. Handphone staf belum diisi", + ), + emailValidator: (value) => _validation( + value, + "Email staff belum diisi", + isEmail: true, + ), + passwordValidator: (value) => _validation( + value, + "Password belum diisi", + isPassword: true, + admin: _secondStaffAdmin, + ), + passwordConfirmValidator: (value) => + _validation( + value, + "Konfirmasi password belum diisi", + isPasswordConfirm: true, + admin: _secondStaffAdmin, + ), + shiftNameValidator: (value) => _validation( + value, + "Nama shift belum diisi", + ), + ), + ), + ], + ), + ) + : SliverList( + delegate: SliverChildBuilderDelegate(( + context, + index, + ) { + final adminDetail = admins[index]; + + final adminInfo = index == 0 + ? _firstStaffAdmin + : _secondStaffAdmin; + + return _buildStaffAdmin( + adminDetail, + adminInfo, + isLoading, + ); + }, childCount: admins.length), + ), + ), + SliverToBoxAdapter( + child: Padding( + padding: EdgeInsets.all(4.w), + child: ElevatedButton( + onPressed: isLoading ? null : _saveStaffAdmin, + style: ElevatedButton.styleFrom( + backgroundColor: AppColor.primaryColor, + minimumSize: Size(double.infinity, 7.h), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(3.w), + ), + disabledBackgroundColor: Colors.grey.shade300, + ), + child: Text( + "Simpan", + style: TextStyle( + fontSize: AppFontSize.medium.sp, + fontWeight: FontWeight.bold, + color: Colors.white, + ), + ), + ), + ), + ), + SliverToBoxAdapter(child: SizedBox(height: 3.h)), + ], + ), + ), + ); + }, + ); + } + + Widget _buildStaffAdmin( + StaffAdminDetail adminDetail, + StaffAdminInformation adminInfo, + bool isLoading, + ) { + final bool isTablet = 100.w >= 600; + return Container( + padding: EdgeInsets.all(4.w), + margin: EdgeInsets.only(bottom: 3.h), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(2.5.w), + border: Border.all(color: Colors.grey.shade200), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.02), + blurRadius: 5, + offset: const Offset(0, 2), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + height: 10.w, + width: 10.w, + padding: EdgeInsets.all(2.w), + decoration: BoxDecoration( + color: Colors.blue[50], + borderRadius: BorderRadius.circular(2.w), + ), + child: Icon( + Icons.person_outline, + color: AppColor.primaryColor, + size: 5.w, + ), + ), + SizedBox(width: 3.w), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + adminDetail.name, + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + ), + ), + SizedBox(height: 0.5.h), + Text( + adminDetail.phoneNumber, + style: TextStyle( + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + ), + ), + SizedBox(height: 0.5.h), + Text( + adminDetail.email, + style: TextStyle( + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + ), + ), + SizedBox(height: 0.5.h), + Text( + "${adminDetail.transactionAmount} Transaksi", + style: TextStyle( + color: Colors.grey[600], + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + ), + ), + ], + ), + ), + + Row( + children: [ + Text( + (_statusStaffAdmin[adminDetail.id] ?? false) + ? "Aktif" + : "Non Aktif", + style: TextStyle( + color: (_statusStaffAdmin[adminDetail.id] ?? false) + ? AppColor.primaryColor + : Colors.grey.shade700, + fontWeight: FontWeight.bold, + fontSize: isTablet + ? (AppFontSize.medium - 1.25).sp + : (AppFontSize.small - 1.25).sp, + ), + ), + + SizedBox(width: 0.75.w), + + SizedBox( + width: 10.w, + child: FittedBox( + fit: BoxFit.fill, + child: Switch( + value: _statusStaffAdmin[adminDetail.id]!, + onChanged: (_) { + setState(() { + _statusStaffAdmin[adminDetail.id] = + !_statusStaffAdmin[adminDetail.id]!; + }); + }, + activeColor: AppColor.primaryColor, + ), + ), + ), + ], + ), + ], + ), + Divider(height: 3.h, color: Colors.grey[100]), + + SizedBox(height: 2.h), + + CustomTextFormField( + label: "Pengaturan Shift Kerja", + hint: "Contoh: Shift Pagi atau Shift 1", + controller: adminInfo.shiftNameController, + ), + + SizedBox(height: 1.5.h), + Row( + children: [ + Expanded( + child: CustomTimePicker( + label: "Jam Mulai", + time: adminInfo.startTime, + onTap: () => _selectTime(true, adminInfo), + ), + ), + SizedBox(width: 3.w), + Expanded( + child: CustomTimePicker( + label: "Jam Selesai", + time: adminInfo.endTime, + onTap: () => _selectTime(false, adminInfo), + ), + ), + ], + ), + SizedBox(height: 2.h), + + Center( + child: TextButton.icon( + onPressed: isLoading + ? null + : () async { + FocusScope.of(context).unfocus(); + + await Future.delayed(const Duration(milliseconds: 400)); + + showDialog( + context: context, + builder: (context) => DeleteStaffAdminScreen( + id: adminDetail.id, + name: adminDetail.name, + transactionAmount: adminDetail.transactionAmount, + ), + ); + }, + icon: Icon(Icons.delete_outline, size: 5.w, color: Colors.red), + label: Text( + "Hapus", + style: TextStyle( + color: Colors.red, + fontWeight: FontWeight.bold, + fontSize: (AppFontSize.medium - 1.25).sp, + ), + ), + ), + ), + ], + ), + ); + } +} diff --git a/Mobile Operasional/lib/features/outlets/presentation/screens/map_outlet_address_screen.dart b/Mobile Operasional/lib/features/outlets/presentation/screens/map_outlet_address_screen.dart new file mode 100644 index 0000000..6e0565d --- /dev/null +++ b/Mobile Operasional/lib/features/outlets/presentation/screens/map_outlet_address_screen.dart @@ -0,0 +1,299 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_map/flutter_map.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:geocoding/geocoding.dart'; +import 'package:geolocator/geolocator.dart'; +import 'package:go_router/go_router.dart'; +import 'package:latlong2/latlong.dart'; +import 'package:niogu_app/core/components/top_back_bar_app.dart'; +import 'package:niogu_app/core/constants/app_color.dart'; +import 'package:niogu_app/core/constants/app_font_size.dart'; +import 'package:niogu_app/core/widgets/triangle_painter.dart'; +import 'package:niogu_app/features/outlets/domain/entities/outlet.dart'; +import 'package:niogu_app/features/outlets/presentation/providers/outlet_provider.dart'; +import 'package:sizer/sizer.dart'; + +class MapOutletAddressScreen extends ConsumerStatefulWidget { + const MapOutletAddressScreen({super.key}); + + @override + ConsumerState createState() => + _MapOutletAddressScreenState(); +} + +class _MapOutletAddressScreenState + extends ConsumerState { + final MapController _mapController = MapController(); + + final TextEditingController _addressController = TextEditingController(); + + LatLng _currentLocation = const LatLng(-6.2000, 106.8166); + + bool _isLoading = false; + + @override + void initState() { + // TODO: implement initState + super.initState(); + final mapOutletAddress = ref.read(mapOutletAddressProvider); + + if (mapOutletAddress != null) { + _addressController.text = mapOutletAddress.fullAddress; + + _currentLocation = mapOutletAddress.currentLocation; + } + } + + @override + void dispose() { + // TODO: implement dispose + _mapController.dispose(); + _addressController.dispose(); + super.dispose(); + } + + Future _getCurrentPosition() async { + setState(() => _isLoading = true); + + final bool serviceEnabled = await Geolocator.isLocationServiceEnabled(); + if (!serviceEnabled) return; + + LocationPermission permission = await Geolocator.checkPermission(); + if (permission == LocationPermission.denied) { + permission = await Geolocator.requestPermission(); + if (permission == LocationPermission.denied) return; + } + + Position position = await Geolocator.getCurrentPosition(); + setState(() { + _currentLocation = LatLng(position.latitude, position.longitude); + _mapController.move(_currentLocation, 15.0); + _isLoading = false; + }); + + try { + final List placemarks = await placemarkFromCoordinates( + position.latitude, + position.longitude, + ); + if (placemarks.isNotEmpty) { + final Placemark place = placemarks[0]; + _addressController.text = + "${place.street}, ${place.subLocality}, ${place.locality}, ${place.subAdministrativeArea}, ${place.administrativeArea}, ${place.postalCode}"; + } + } catch (e) { + debugPrint(e.toString()); + setState(() => _isLoading = true); + } + } + + void _selectedCurrentLocation() { + final fullAddress = _addressController.text; + + ref.read(mapOutletAddressProvider.notifier).state = OutletAddressSelected( + fullAddress: fullAddress, + currentLocation: _currentLocation, + ); + + context.pop(); + } + + @override + Widget build(BuildContext context) { + return LayoutBuilder( + builder: (context, constraints) { + return SafeArea( + top: false, + bottom: true, + right: false, + left: false, + child: Scaffold( + extendBodyBehindAppBar: true, + appBar: TopBackBarApp( + title: "Alamat Outlet", + onTap: () => context.pop(), + ), + body: Column( + children: [ + Expanded( + child: Stack( + children: [ + FlutterMap( + mapController: _mapController, + options: MapOptions( + initialCenter: _currentLocation, + initialZoom: 16.0, + minZoom: 3.0, + maxZoom: 18.0, + ), + children: [ + TileLayer( + urlTemplate: + 'https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png', + subdomains: const ['a', 'b', 'c', 'd'], + userAgentPackageName: 'com.example.niogu_app', + ), + MarkerLayer( + markers: [ + Marker( + point: _currentLocation, + width: 70.w, + height: 25.h, + alignment: Alignment.topCenter, + child: _addressController.text.isEmpty + ? const SizedBox() + : Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + padding: EdgeInsets.symmetric( + horizontal: 3.w, + vertical: 1.2.h, + ), + constraints: BoxConstraints( + maxWidth: 65.w, + ), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: + BorderRadius.circular(2.w), + boxShadow: [ + BoxShadow( + color: Colors.black + .withOpacity(0.1), + blurRadius: 8, + offset: const Offset(0, 4), + ), + ], + border: Border.all( + color: Colors.grey.shade200, + ), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Row( + mainAxisAlignment: + MainAxisAlignment + .spaceBetween, + children: [ + SizedBox(width: 5.w), + Text( + "Lokasi Outlet", + textAlign: + TextAlign.center, + style: TextStyle( + fontSize: AppFontSize + .small + .sp, + fontWeight: + FontWeight.bold, + color: AppColor + .primaryColor, + ), + ), + + InkWell( + onTap: () { + setState(() { + _addressController + .clear(); + }); + }, + child: Icon( + Icons.close, + color: Colors.grey, + size: 5.w, + ), + ), + ], + ), + SizedBox(height: 0.5.h), + Text( + _addressController.text, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: + (AppFontSize.small - + 1.25) + .sp, + color: Colors.grey[700], + fontWeight: FontWeight.w500, + height: 1.4, + ), + ), + ], + ), + ), + CustomPaint( + size: Size(5.w, 2.5.w), + painter: TrianglePainter( + Colors.white, + ), + ), + SizedBox(height: 0.5.h), + Icon( + Icons.location_on, + color: Colors.red, + size: 10.w, + ), + ], + ), + ), + ], + ), + ], + ), + + Positioned( + bottom: 3.h, + left: 5.w, + right: 5.w, + child: OutlinedButton.icon( + onPressed: _addressController.text.isNotEmpty + ? _selectedCurrentLocation + : _getCurrentPosition, + icon: _addressController.text.isNotEmpty + ? null + : Icon( + Icons.my_location, + size: 5.w, + color: AppColor.primaryColor, + ), + label: Text( + _addressController.text.isNotEmpty + ? "Pilih Lokasi Ini" + : "Gunakan Lokasi Saat Ini", + style: TextStyle( + fontSize: (AppFontSize.medium).sp, + fontWeight: FontWeight.bold, + ), + ), + style: OutlinedButton.styleFrom( + minimumSize: Size(double.infinity, 5.h), + foregroundColor: AppColor.primaryColor, + side: BorderSide(color: AppColor.primaryColor), + ), + ), + ), + + if (_isLoading) + Container( + color: Colors.white.withOpacity(0.3), + child: Center( + child: CircularProgressIndicator( + color: Colors.grey.shade700, + ), + ), + ), + ], + ), + ), + ], + ), + ), + ); + }, + ); + } +} diff --git a/Mobile Operasional/lib/features/outlets/presentation/screens/outlet_screen.dart b/Mobile Operasional/lib/features/outlets/presentation/screens/outlet_screen.dart new file mode 100644 index 0000000..c8cd0fa --- /dev/null +++ b/Mobile Operasional/lib/features/outlets/presentation/screens/outlet_screen.dart @@ -0,0 +1,503 @@ +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; +import 'package:niogu_app/core/components/top_back_bar_app.dart'; +import 'package:niogu_app/core/constants/app_color.dart'; +import 'package:niogu_app/core/constants/app_font_size.dart'; +import 'package:niogu_app/core/widgets/custom_error_screen.dart'; +import 'package:niogu_app/core/providers/app_provider.dart'; +import 'package:niogu_app/core/router/app_route.dart'; +import 'package:niogu_app/core/widgets/custom_empty_screen.dart'; +import 'package:niogu_app/features/outlets/domain/entities/outlet.dart'; +import 'package:niogu_app/features/outlets/presentation/providers/outlet_provider.dart'; +import 'package:niogu_app/features/outlets/presentation/widgets/outlet_shimmer.dart'; +import 'package:niogu_app/features/outlets/presentation/widgets/quota_indicator.dart'; +import 'package:sizer/sizer.dart'; + +class OutletScreen extends ConsumerWidget { + const OutletScreen({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + return LayoutBuilder( + builder: (context, constraints) { + final bool isTablet = 100.w >= 600; + + final outletState = ref.watch(displayOutletStreamProvider); + + int limitOutlet = 5; + + final limitOutletState = ref.watch(limitOutletStreamProvider); + + limitOutletState.whenData((limit) => limitOutlet = limit); + + bool isLimitReached = false; + + outletState.whenData((outlets) { + isLimitReached = outlets.length >= limitOutlet; + }); + + final userDeletedState = ref.watch(userDeletedByOutletStreamProvider); + + final List userDeleted = []; + + userDeletedState.whenData((u) => userDeleted.addAll(u)); + + final outletEmptyState = ref.watch(displayOutletEmptyProvider); + + final outletIdByOwnerState = ref.watch(outletIdByOwnerProvider); + + return SafeArea( + top: false, + bottom: true, + right: false, + left: false, + child: Scaffold( + backgroundColor: const Color(0xFFF9FAFB), + appBar: TopBackBarApp( + title: "Outlet Usaha", + onTap: () => context.pop(), + ), + body: outletState.when( + data: (outlets) { + switch (outletEmptyState) { + case DisplayOutletEmpty.loading: + return const SizedBox(); + case DisplayOutletEmpty.empty_database: + return CustomEmptyScreen( + title: "Tidak Ada Outlet", + body: "Kamu belum memiliki outlet", + ); + case DisplayOutletEmpty.has_data: + return Column( + children: [ + QuotaIndicator(current: outlets.length, max: limitOutlet), + + Expanded( + child: ListView.builder( + padding: EdgeInsets.symmetric(horizontal: 5.w), + itemCount: outlets.length, + itemBuilder: (context, index) { + final outlet = outlets[index]; + + final bool imagePathAvailable = + outlet.bannerPath != null && + outlet.bannerPath!.isNotEmpty; + + final File imageFile = File( + imagePathAvailable + ? outlet.bannerPath! + : "image not found", + ); + + int userStaffAmount = outlet.userStaffAmount; + + final bool isOutletOwner = + outletIdByOwnerState != null && + outlet.id == outletIdByOwnerState; + + if (userDeleted.isNotEmpty) { + for (final user in userDeleted) { + if (isOutletOwner && + user.outletId == outlet.id) { + userStaffAmount = + outlet.userStaffAmount - + user.amount - + 1; + } else if (user.outletId == outlet.id) { + userStaffAmount = + outlet.userStaffAmount - + user.amount; + } + } + } else { + if (isOutletOwner) { + userStaffAmount = + outlet.userStaffAmount - 1; + } + } + + final bool imageFileExists = imageFile + .existsSync(); + + return Material( + color: Colors.transparent, + type: MaterialType.transparency, + child: InkWell( + onTap: () { + context.pushNamed( + AppRoute.editOutletScreen, + pathParameters: {"id": outlet.id}, + ); + }, + child: Container( + margin: EdgeInsets.only(bottom: 3.h), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular( + 4.w, + ), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity( + 0.06, + ), + blurRadius: 15, + offset: const Offset(0, 8), + ), + ], + ), + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Stack( + children: [ + imageFileExists + ? Container( + height: 22.h, + width: double.infinity, + decoration: BoxDecoration( + borderRadius: + BorderRadius.vertical( + top: + Radius.circular( + 4.w, + ), + ), + image: + DecorationImage( + image: + FileImage( + imageFile, + ), + fit: BoxFit + .cover, + ), + ), + ) + : Container( + height: 22.h, + width: double.infinity, + decoration: BoxDecoration( + borderRadius: + BorderRadius.vertical( + top: + Radius.circular( + 4.w, + ), + ), + color: Colors + .grey + .shade200, + ), + child: Column( + mainAxisAlignment: + MainAxisAlignment + .center, + children: [ + Icon( + Icons + .store_outlined, + color: + Colors.grey, + size: 20.w, + ), + ], + ), + ), + if (imageFileExists) + Container( + height: 22.h, + width: double.infinity, + decoration: BoxDecoration( + borderRadius: + BorderRadius.vertical( + top: + Radius.circular( + 4.w, + ), + ), + gradient: LinearGradient( + begin: + Alignment.topCenter, + end: Alignment + .bottomCenter, + colors: [ + Colors.black + .withOpacity(0.4), + Colors.transparent, + ], + ), + ), + ), + ], + ), + + Padding( + padding: EdgeInsets.all(4.w), + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: + MainAxisAlignment + .spaceBetween, + children: [ + Text( + outlet.name, + style: TextStyle( + color: Colors.black87, + fontSize: isTablet + ? AppFontSize + .medium + .sp + : AppFontSize + .small + .sp, + fontWeight: + FontWeight.bold, + ), + ), + Row( + children: [ + Container( + width: 2.w, + height: 2.w, + decoration: BoxDecoration( + color: + outlet + .isActive + ? Colors.green + : Colors.red, + shape: BoxShape + .circle, + ), + ), + SizedBox( + width: 1.5.w, + ), + Text( + outlet.isActive + ? 'Aktif / Buka' + : 'Non Aktif / Tutup', + style: TextStyle( + fontSize: isTablet + ? (AppFontSize.medium - + 2) + .sp + : (AppFontSize.small - + 2) + .sp, + color: + outlet + .isActive + ? Colors.green + : Colors.red, + fontWeight: + FontWeight + .w600, + ), + ), + ], + ), + ], + ), + SizedBox(height: 1.h), + if (outlet.isMainOutlet) + Text( + "• Outlet Utama", + style: TextStyle( + color: Colors + .grey + .shade700, + fontSize: isTablet + ? (AppFontSize.medium - + 2) + .sp + : (AppFontSize.small - + 2) + .sp, + fontWeight: + FontWeight.bold, + ), + ), + + if (outlet.fullAddress != + null && + outlet + .fullAddress! + .isNotEmpty) ...[ + SizedBox(height: 1.5.h), + Row( + children: [ + Icon( + Icons + .location_on_outlined, + size: 5.w, + color: Colors.grey, + ), + SizedBox(width: 2.w), + Expanded( + child: Text( + outlet.fullAddress!, + style: TextStyle( + color: Colors + .grey + .shade700, + fontSize: isTablet + ? (AppFontSize.medium - + 1.25) + .sp + : (AppFontSize.small - + 1.25) + .sp, + ), + maxLines: 1, + overflow: + TextOverflow + .ellipsis, + ), + ), + ], + ), + ], + SizedBox(height: 1.h), + Divider( + color: Colors.grey[100], + ), + SizedBox(height: 1.h), + Row( + mainAxisAlignment: + MainAxisAlignment + .spaceBetween, + children: [ + _buildInfoMini( + Icons.people_outline, + "$userStaffAmount Staf", + isTablet, + ), + _buildInfoMini( + Icons + .shopping_bag_outlined, + "${outlet.transactionAmount} Transaksi", + isTablet, + ), + Icon( + Icons.arrow_forward_ios, + size: 3.5.w, + color: Colors.grey[400], + ), + ], + ), + ], + ), + ), + ], + ), + ), + ), + ); + }, + ), + ), + ], + ); + } + }, + error: (error, stackTrace) { + return CustomErrorScreen( + message: "Ups, terjadi kesalahan", + onRefresh: () {}, + ); + }, + loading: () => const OutletShimmer(), + ), + bottomNavigationBar: outletState.isLoading + ? null + : Container( + padding: EdgeInsets.all(5.w), + decoration: BoxDecoration( + color: Colors.white, + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.05), + blurRadius: 10, + offset: const Offset(0, -5), + ), + ], + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + ElevatedButton( + onPressed: isLimitReached + ? null + : () { + final outletState = ref.read( + outletStreamProvider, + ); + + bool isLimitReached = false; + + outletState.whenData((outlets) { + isLimitReached = outlets.length >= 5; + }); + + if (isLimitReached) { + } else { + context.pushNamed( + AppRoute.addOutletScreen, + ); + } + }, + style: ElevatedButton.styleFrom( + backgroundColor: AppColor.primaryColor, + minimumSize: Size(double.infinity, 6.5.h), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(3.w), + ), + disabledBackgroundColor: !isLimitReached + ? null + : Colors.grey.shade300, + ), + child: Text( + "Tambah Outlet Baru", + style: TextStyle( + fontSize: AppFontSize.medium.sp, + fontWeight: FontWeight.bold, + color: Colors.white, + ), + ), + ), + ], + ), + ), + ), + ); + }, + ); + } + + Widget _buildInfoMini(IconData icon, String label, bool isTablet) { + return Row( + children: [ + Icon(icon, size: 4.w, color: AppColor.primaryColor), + SizedBox(width: 1.5.w), + Text( + label, + style: TextStyle( + color: Colors.black54, + fontSize: isTablet + ? (AppFontSize.medium - 1.25).sp + : (AppFontSize.small - 1.25).sp, + fontWeight: FontWeight.w500, + ), + ), + ], + ); + } +} diff --git a/Mobile Operasional/lib/features/outlets/presentation/widgets/edit_outlet_shimmer.dart b/Mobile Operasional/lib/features/outlets/presentation/widgets/edit_outlet_shimmer.dart new file mode 100644 index 0000000..8a8cc56 --- /dev/null +++ b/Mobile Operasional/lib/features/outlets/presentation/widgets/edit_outlet_shimmer.dart @@ -0,0 +1,96 @@ +import 'package:flutter/material.dart'; +import 'package:shimmer/shimmer.dart'; +import 'package:sizer/sizer.dart'; + +class EditOutletShimmer extends StatelessWidget { + const EditOutletShimmer({super.key}); + + @override + Widget build(BuildContext context) { + return Shimmer.fromColors( + baseColor: Colors.grey.shade300, + highlightColor: Colors.grey.shade100, + child: SingleChildScrollView( + physics: const NeverScrollableScrollPhysics(), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container(width: 30.w, height: 2.h, color: Colors.white), + SizedBox(height: 1.5.h), + Container( + height: 20.h, + width: double.infinity, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(3.w), + ), + ), + SizedBox(height: 3.h), + _buildInputPlaceholder(), + SizedBox(height: 3.h), + _buildInputPlaceholder(), + SizedBox(height: 3.h), + _buildInputPlaceholder(), + SizedBox(height: 3.h), + + Container( + width: double.infinity, + height: 7.h, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(2.w), + ), + ), + SizedBox(height: 3.h), + Container( + width: double.infinity, + height: 7.h, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(2.w), + ), + ), + SizedBox(height: 3.h), + + Container( + width: double.infinity, + height: 18.h, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(3.w), + ), + ), + SizedBox(height: 3.h), + + Container( + width: double.infinity, + height: 7.h, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(3.w), + ), + ), + ], + ), + ), + ); + } + + Widget _buildInputPlaceholder() { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container(width: 25.w, height: 1.5.h, color: Colors.white), + SizedBox(height: 1.h), + Container( + width: double.infinity, + height: 6.h, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(2.5.w), + ), + ), + ], + ); + } +} \ No newline at end of file diff --git a/Mobile Operasional/lib/features/outlets/presentation/widgets/outlet_card.dart b/Mobile Operasional/lib/features/outlets/presentation/widgets/outlet_card.dart new file mode 100644 index 0000000..e69de29 diff --git a/Mobile Operasional/lib/features/outlets/presentation/widgets/outlet_shimmer.dart b/Mobile Operasional/lib/features/outlets/presentation/widgets/outlet_shimmer.dart new file mode 100644 index 0000000..e5f9bee --- /dev/null +++ b/Mobile Operasional/lib/features/outlets/presentation/widgets/outlet_shimmer.dart @@ -0,0 +1,101 @@ +import 'package:flutter/material.dart'; +import 'package:shimmer/shimmer.dart'; +import 'package:sizer/sizer.dart'; + +class OutletShimmer extends StatelessWidget { + const OutletShimmer({super.key}); + + @override + Widget build(BuildContext context) { + return Shimmer.fromColors( + baseColor: Colors.grey.shade300, + highlightColor: Colors.grey.shade100, + child: SingleChildScrollView( + physics: const NeverScrollableScrollPhysics(), + child: Column( + children: [ + Padding( + padding: EdgeInsets.fromLTRB(5.w, 2.h, 5.w, 3.h), + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Container(width: 30.w, height: 1.5.h, color: Colors.white), + Container(width: 15.w, height: 1.5.h, color: Colors.white), + ], + ), + SizedBox(height: 1.5.h), + Container( + width: double.infinity, + height: 1.h, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(1.w), + ), + ), + ], + ), + ), + + ListView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + padding: EdgeInsets.symmetric(horizontal: 5.w), + itemCount: 2, + itemBuilder: (context, index) { + return Container( + margin: EdgeInsets.only(bottom: 3.h), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(4.w), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + height: 22.h, + width: double.infinity, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.vertical(top: Radius.circular(4.w)), + ), + ), + Padding( + padding: EdgeInsets.all(4.w), + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Container(width: 40.w, height: 2.h, color: Colors.white), + Container(width: 20.w, height: 1.5.h, color: Colors.white), + ], + ), + SizedBox(height: 2.h), + Container(width: double.infinity, height: 1.5.h, color: Colors.white), + SizedBox(height: 2.h), + Divider(color: Colors.grey[200]), + SizedBox(height: 1.h), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Container(width: 20.w, height: 1.5.h, color: Colors.white), + Container(width: 25.w, height: 1.5.h, color: Colors.white), + Container(width: 5.w, height: 5.w, color: Colors.white), + ], + ), + ], + ), + ), + ], + ), + ); + }, + ), + ], + ), + ), + ); + } +} \ No newline at end of file diff --git a/Mobile Operasional/lib/features/outlets/presentation/widgets/quota_indicator.dart b/Mobile Operasional/lib/features/outlets/presentation/widgets/quota_indicator.dart new file mode 100644 index 0000000..27ce461 --- /dev/null +++ b/Mobile Operasional/lib/features/outlets/presentation/widgets/quota_indicator.dart @@ -0,0 +1,58 @@ +import 'package:flutter/material.dart'; +import 'package:niogu_app/core/constants/app_color.dart'; +import 'package:niogu_app/core/constants/app_font_size.dart'; +import 'package:sizer/sizer.dart'; + +class QuotaIndicator extends StatelessWidget { + final int current; + final int max; + const QuotaIndicator({super.key, required this.current, required this.max}); + + @override + Widget build(BuildContext context) { + final bool isTablet = 100.w >= 600; + return Padding( + padding: EdgeInsets.fromLTRB(5.w, 1.h, 5.w, 2.h), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Kuota", + style: TextStyle( + fontSize: isTablet + ? (AppFontSize.medium - 1.25).sp + : (AppFontSize.small - 1.25).sp, + fontWeight: FontWeight.bold, + color: Colors.grey[800], + ), + ), + Text( + "$current / $max Outlet", + style: TextStyle( + fontSize: isTablet + ? (AppFontSize.medium - 1.25).sp + : (AppFontSize.small - 1.25).sp, + fontWeight: FontWeight.bold, + color: AppColor.primaryColor, + ), + ), + ], + ), + SizedBox(height: 1.h), + ClipRRect( + borderRadius: BorderRadius.circular(1.w), + child: LinearProgressIndicator( + value: current / max, + minHeight: 1.h, + backgroundColor: Colors.grey[200], + valueColor: AlwaysStoppedAnimation(AppColor.primaryColor), + ), + ), + ], + ), + ); + } +} diff --git a/Mobile Operasional/lib/features/outlets/presentation/widgets/staf_admin.dart b/Mobile Operasional/lib/features/outlets/presentation/widgets/staf_admin.dart new file mode 100644 index 0000000..bddf70f --- /dev/null +++ b/Mobile Operasional/lib/features/outlets/presentation/widgets/staf_admin.dart @@ -0,0 +1,210 @@ +import 'package:flutter/material.dart'; +import 'package:niogu_app/core/constants/app_color.dart'; +import 'package:niogu_app/core/constants/app_font_size.dart'; +import 'package:niogu_app/core/widgets/custom_form_input.dart'; +import 'package:niogu_app/core/widgets/custom_text_form_field.dart'; +import 'package:niogu_app/features/outlets/presentation/screens/add_outlet_screen.dart'; +import 'package:niogu_app/core/widgets/custom_time_picker.dart'; +import 'package:sizer/sizer.dart'; + +class StafAdmin extends StatelessWidget { + final int stafAdminNumber; + final bool isFormVisible; + final StaffAdminInformation stafAdmin; + final VoidCallback passwordSuffixPressed; + final VoidCallback passwordConfirmSuffixPressed; + final VoidCallback onTap; + final VoidCallback onStartTimeTap; + final VoidCallback onEndTimeTap; + final String? Function(String?)? nameValidator; + final String? Function(String?)? phoneNumberValidator; + final String? Function(String?)? emailValidator; + final String? Function(String?)? passwordValidator; + final String? Function(String?)? passwordConfirmValidator; + final String? Function(String?)? shiftNameValidator; + const StafAdmin({ + super.key, + required this.stafAdminNumber, + required this.isFormVisible, + required this.stafAdmin, + required this.passwordSuffixPressed, + required this.passwordConfirmSuffixPressed, + required this.onTap, + required this.onStartTimeTap, + required this.onEndTimeTap, + this.nameValidator, + this.phoneNumberValidator, + this.emailValidator, + this.passwordValidator, + this.passwordConfirmValidator, + this.shiftNameValidator, + }); + + @override + Widget build(BuildContext context) { + final bool isTablet = 100.w >= 600; + return Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(2.5.w), + border: Border.all(color: Colors.grey.shade300), + ), + child: Column( + children: [ + InkWell( + onTap: onTap, + borderRadius: BorderRadius.vertical( + top: Radius.circular(2.5.w), + bottom: Radius.circular(isFormVisible ? 0 : 2.5.w), + ), + child: Padding( + padding: EdgeInsets.symmetric(horizontal: 4.w, vertical: 1.5.h), + child: Row( + children: [ + Container( + padding: EdgeInsets.all(1.5.w), + decoration: BoxDecoration( + color: isFormVisible + ? AppColor.primaryColor.withOpacity(0.1) + : Colors.grey.shade100, + shape: BoxShape.circle, + ), + child: Icon( + Icons.info_outlined, + color: isFormVisible + ? AppColor.primaryColor + : Colors.grey, + size: 5.w, + ), + ), + SizedBox(width: 3.w), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Informasi Staf Admin ($stafAdminNumber)", + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + color: Colors.black87, + ), + ), + SizedBox(height: 1.h), + Text( + stafAdminNumber > 1 ? "Opsional" : "Wajib", + style: TextStyle( + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + color: Colors.grey, + ), + ), + ], + ), + ), + Icon( + isFormVisible + ? Icons.keyboard_arrow_up_rounded + : Icons.keyboard_arrow_down_rounded, + color: Colors.grey, + size: 6.w, + ), + ], + ), + ), + ), + + if (isFormVisible) ...[ + Divider(height: 1, color: Colors.grey.shade200), + Padding( + padding: EdgeInsets.all(4.w), + child: Column( + children: [ + CustomFormInput( + label: "Nama", + icon: Icons.person_outlined, + controller: stafAdmin.nameController, + inputType: TextInputType.text, + validator: nameValidator, + ), + SizedBox(height: 1.5.h), + CustomFormInput( + label: "No. Handpone / WA", + icon: Icons.phone_android_outlined, + controller: stafAdmin.phoneNumberController, + inputType: TextInputType.phone, + validator: phoneNumberValidator, + ), + SizedBox(height: 1.5.h), + CustomFormInput( + label: "Email", + icon: Icons.email_outlined, + controller: stafAdmin.emailController, + inputType: TextInputType.emailAddress, + validator: emailValidator, + ), + SizedBox(height: 1.5.h), + CustomFormInput( + label: "Password", + icon: Icons.lock_outline, + controller: stafAdmin.passwordController, + inputType: TextInputType.text, + obscureText: stafAdmin.obsecurePassword, + suffixIcon: stafAdmin.obsecurePassword + ? Icons.visibility_off_outlined + : Icons.visibility_outlined, + suffixPressed: passwordSuffixPressed, + validator: passwordValidator, + ), + SizedBox(height: 1.5.h), + CustomFormInput( + label: "Konfirmasi Password", + icon: Icons.lock_reset_outlined, + controller: stafAdmin.passwordConfirmController, + inputType: TextInputType.text, + obscureText: stafAdmin.obsecurePasswordConfirm, + suffixIcon: stafAdmin.obsecurePasswordConfirm + ? Icons.visibility_off_outlined + : Icons.visibility_outlined, + suffixPressed: passwordConfirmSuffixPressed, + validator: passwordConfirmValidator, + ), + SizedBox(height: 2.h), + CustomTextFormField( + label: "Pengaturan Shift Kerja", + hint: "Contoh: Shift Pagi atau Shift 1", + controller: stafAdmin.shiftNameController, + validator: shiftNameValidator, + ), + SizedBox(height: 1.5.h), + Row( + children: [ + Expanded( + child: CustomTimePicker( + label: "Jam Mulai", + time: stafAdmin.startTime, + onTap: onStartTimeTap, + ), + ), + SizedBox(width: 3.w), + Expanded( + child: CustomTimePicker( + label: "Jam Selesai", + time: stafAdmin.endTime, + onTap: onEndTimeTap, + ), + ), + ], + ), + ], + ), + ), + ], + ], + ), + ); + } +} diff --git a/Mobile Operasional/lib/features/pos/data/repositories/pos_repository_impl.dart b/Mobile Operasional/lib/features/pos/data/repositories/pos_repository_impl.dart new file mode 100644 index 0000000..4b75c14 --- /dev/null +++ b/Mobile Operasional/lib/features/pos/data/repositories/pos_repository_impl.dart @@ -0,0 +1,193 @@ +import 'package:drift/drift.dart'; +import 'package:niogu_app/core/database/app_database.dart' hide Product; +import 'package:niogu_app/core/enums/customer_source.dart'; +import 'package:niogu_app/core/enums/stock_card_type.dart'; +import 'package:niogu_app/core/enums/stock_type.dart'; +import 'package:niogu_app/core/system/system_setting.dart'; +import 'package:niogu_app/features/goods/products/domain/entities/product.dart'; +import 'package:niogu_app/features/goods/raw_materials/domain/entities/raw_material.dart'; +import 'package:niogu_app/features/pos/domain/entities/pos.dart'; +import 'package:niogu_app/features/pos/domain/repositories/i_pos_repository.dart'; +import 'package:uuid/uuid.dart'; + +class PosRepositoryImpl implements IPosRepository { + final AppDatabase _appDatabase; + + PosRepositoryImpl(this._appDatabase); + + @override + Stream> watchDisplayProductPos(String outletId) { + return _appDatabase.orderDao.watchAllDisplayProductPos(); + } + + @override + Stream> watchAlreadyCustomers(String outletId) { + return _appDatabase.customerDao.watchAllAlreadyCustomers(); + } + + @override + Future checkRawMaterialStockSufficient( + String productVariantId, + double productQuantity, + ) async { + return await _appDatabase.orderDao.checkRawMaterialStockSufficient( + productVariantId, + productQuantity, + ); + } + + @override + Future> getProductUseMaterials( + String productVariantId, + double productQuantity, + ) async { + return await _appDatabase.orderDao.getProductUseMaterials( + productVariantId, + productQuantity, + ); + } + + @override + Future addSale( + CustomerInformation? customerInfo, + NewSale newSale, + List itemSales, + ) async { + final outletId = await SystemSetting.getCurrentOutletId(); + + final outletName = await SystemSetting.getCurrentOutletName(); + + final userId = await SystemSetting.getCurrentUserId(); + + final userName = await SystemSetting.getCurrentUserName(); + + CustomersCompanion? customer; + + // CustomerAddressesCompanion? customerAddress; + + if (customerInfo != null) { + customer = CustomersCompanion( + localId: Value(customerInfo.id), + outletId: Value(outletId!), + name: Value(customerInfo.name), + phoneNumber: Value(customerInfo.phoneNumber), + source: Value(CustomerSource.offline.source), + ); + + /** + if (customerInfo.address != null) { + customerAddress = CustomerAddressesCompanion( + localId: Value(const Uuid().v7().toString()), + customerId: Value(customerInfo.id), + fullAddress: Value(customerInfo.address), + ); + } + */ + } + + final order = OrdersCompanion( + localId: Value(newSale.localId), + outletId: Value(outletId!), + outletNameSnapshot: Value(outletName!), + customerId: Value(newSale.customerId), + customerNameSnapshot: Value(newSale.customerNameSnapshot), + customerPhoneNumberSnapshot: Value(newSale.customerPhoneNumberSnapshot), + userId: Value(userId!), + adminNameSnapshot: Value(userName!), + source: Value(newSale.orderSource.source), + + /** + * dev + source: Value(OrderSource.online.source), + alreadyRead: Value(true), + orderStatus: Value(OrderStatus.pending_confirmation.status), + deliveryType: Value(DeliveryType.delivery.type), + deliveryPrefrence: Value("Secepatnya"), + */ + orderStatus: Value(newSale.orderStatus.status), + totalOrder: Value(newSale.totalOrder), + discount: Value(newSale.otherInformation.discount), + tax: Value(newSale.otherInformation.tax), + totalAmount: Value(newSale.totalAmount), + amountPaid: Value(newSale.amountPaid), + changeAmount: Value(newSale.changeAmount), + notes: Value(newSale.otherInformation.note), + paymentProofPath: Value(newSale.paymentProofPath), + + // paymentMethod: Value("COD"), // dev + customerAddressSnapshot: Value(newSale.customerAddressSnapshot), + + /** + * dev + customerAddressSnapshot: Value({ + "full_address": AppTemp.ADDRESS.first.fullAddress, + "latitude": AppTemp.ADDRESS.first.coordinate.latitude, + "longitude": AppTemp.ADDRESS.first.coordinate.longitude, + }), + */ + syncStatus: Value(newSale.syncStatus.status), + ); + + final List orderItems = itemSales.map((item) { + return OrderItemsCompanion( + localId: Value(item.localId), + orderId: Value(item.orderId), + productVariantId: Value(item.productVariantId), + quantity: Value(item.quantity), + productImageSnapshot: Value(item.productImageSnapshot), + productNameSnapshot: Value(item.productNameSnapshot), + productVariantNameSnapshot: Value(item.productVariantNameSnapshot), + sellingPriceSnapshot: Value(item.sellingPriceSnapshot), + subtotal: Value(item.subtotal), + syncStatus: Value(item.syncStatus.status), + ); + }).toList(); + + final List totalSolds = itemSales.map((item) { + return UpdateTotalSold( + id: item.productVariantId, + qtySold: item.quantity, + currentSold: item.currentSold, + ); + }).toList(); + + final List stockProducts = itemSales.map((item) { + return UpdateStockProduct( + outletInventoryId: item.outletInventoryId, + productVariantId: item.productVariantId, + stockType: item.stockType, + stock: item.stock, + quantity: item.quantity, + ); + }).toList(); + + final List cardProducts = itemSales.map((item) { + return StockCardsCompanion( + localId: Value(Uuid().v7().toString()), + outletInventoryId: Value(item.outletInventoryId), + userId: Value(userId), + type: Value(StockCardType.sale.type), + qtyChange: Value(item.quantity), + currentStock: Value( + item.stockType == StockType.fixed + ? (item.stock - item.quantity) + : 0.0, + ), + stockType: Value(item.stockType.type), + costPrice: Value(item.costPrice), + referenceId: order.localId, + syncStatus: Value(item.syncStatus.status), + ); + }).toList(); + + await _appDatabase.orderDao.addSale( + customer: customer, + // customerAddress: customerAddress, + order: order, + orderItem: orderItems, + totalSolds: totalSolds, + stockProducts: stockProducts, + cardProducts: cardProducts, + ); + } +} diff --git a/Mobile Operasional/lib/features/pos/domain/entities/pos.dart b/Mobile Operasional/lib/features/pos/domain/entities/pos.dart new file mode 100644 index 0000000..9466c53 --- /dev/null +++ b/Mobile Operasional/lib/features/pos/domain/entities/pos.dart @@ -0,0 +1,222 @@ +import 'package:niogu_app/core/enums/order_source.dart'; +import 'package:niogu_app/core/enums/order_status.dart'; +import 'package:niogu_app/core/enums/stock_type.dart'; +import 'package:niogu_app/core/enums/sync_status.dart'; +import 'package:uuid/uuid.dart'; + +class DisplayProductPos { + final String productId; + final String productVariantId; + final String outletInventoryId; + final String? imagePath; + final String name; + final String variantName; + final double currentSold; + final StockType stockType; + final double remainingStock; + final String unitName; + final double costPrice; + final double sellingPrice; + final bool hasVariant; + + DisplayProductPos({ + required this.productId, + required this.productVariantId, + required this.outletInventoryId, + this.imagePath, + required this.name, + required this.variantName, + required this.currentSold, + required this.stockType, + required this.remainingStock, + required this.unitName, + required this.costPrice, + required this.sellingPrice, + required this.hasVariant, + }); +} + +class CartItems { + final String currentOutletId; + final String outletInventoryId; + final String id; + final String? imagePath; + final String name; + final String? variantName; + final double currentSold; + final double quantity; + final double costPrice; + final double sellingPrice; + final StockType stockType; + final double remainingStock; + final String unit; + + const CartItems({ + required this.currentOutletId, + required this.outletInventoryId, + required this.id, + this.imagePath, + required this.name, + this.variantName, + required this.currentSold, + this.quantity = 0.0, + required this.costPrice, + required this.sellingPrice, + required this.stockType, + required this.remainingStock, + required this.unit, + }); + + CartItems copyWith({double? quantity}) { + return CartItems( + currentOutletId: currentOutletId, + outletInventoryId: outletInventoryId, + id: id, + imagePath: imagePath, + name: name, + variantName: variantName, + currentSold: currentSold, + quantity: quantity ?? this.quantity, + costPrice: costPrice, + sellingPrice: sellingPrice, + stockType: stockType, + remainingStock: remainingStock, + unit: unit, + ); + } +} + +enum AlreadyCustomerEmpty { + loading, + empty_database, + empty_search_result, + has_data, +} + +class AlreadyCustomer { + final String id; + final String name; + final String phoneNumber; + final List addresses; + + const AlreadyCustomer({ + required this.id, + required this.name, + required this.phoneNumber, + required this.addresses, + }); +} + +class SelectedCustomer { + final String id; + final String outletId; + final String name; + final String phoneNumber; + final String address; + + const SelectedCustomer({ + required this.id, + required this.outletId, + required this.name, + required this.phoneNumber, + required this.address, + }); +} + +class OtherInformation { + final double discount; + final double tax; + final String? note; + + const OtherInformation({this.discount = 0.0, this.tax = 0.0, this.note}); +} + +class CustomerInformation { + final String id; + final String name; + final String? phoneNumber; + final String? address; + + CustomerInformation({ + String? id, + required this.name, + this.phoneNumber, + this.address, + }) : id = id ?? const Uuid().v7().toString(); +} + +class NewSale { + final String localId; + final String? customerId; + final String? customerNameSnapshot; + final String? customerPhoneNumberSnapshot; + final OrderSource orderSource; + final OrderStatus orderStatus; + final double totalOrder; + final OtherInformation otherInformation; + final double totalAmount; + final double amountPaid; + final double changeAmount; + final String? paymentProofPath; + final Map customerAddressSnapshot; + final SyncStatus syncStatus; + + NewSale({ + this.customerId, + required this.customerNameSnapshot, + required this.customerPhoneNumberSnapshot, + this.orderSource = OrderSource.pos, + this.orderStatus = OrderStatus.completed, + required this.totalOrder, + required this.otherInformation, + required this.totalAmount, + required this.amountPaid, + required this.changeAmount, + required this.paymentProofPath, + required this.customerAddressSnapshot, + this.syncStatus = SyncStatus.dirty, + }) : localId = const Uuid().v7().toString(); +} + +class ItemSale { + final String localId; + final String orderId; + final String outletInventoryId; + final String productVariantId; + final StockType stockType; + final double stock; + final double currentSold; + final double quantity; + final double costPrice; + final String? productImageSnapshot; + final String productNameSnapshot; + final String? productVariantNameSnapshot; + final double sellingPriceSnapshot; + final double subtotal; + final SyncStatus syncStatus; + + ItemSale({ + required this.orderId, + required this.outletInventoryId, + required this.productVariantId, + required this.stockType, + required this.stock, + required this.currentSold, + required this.quantity, + required this.costPrice, + this.productImageSnapshot, + required this.productNameSnapshot, + this.productVariantNameSnapshot, + required this.sellingPriceSnapshot, + required this.subtotal, + this.syncStatus = SyncStatus.dirty, + }) : localId = const Uuid().v7().toString(); +} + +class UpdateTotalSold { + final String id; + final double qtySold; + final double currentSold; + + const UpdateTotalSold({required this.id, required this.qtySold, required this.currentSold}); +} diff --git a/Mobile Operasional/lib/features/pos/domain/repositories/i_pos_repository.dart b/Mobile Operasional/lib/features/pos/domain/repositories/i_pos_repository.dart new file mode 100644 index 0000000..1da8f6e --- /dev/null +++ b/Mobile Operasional/lib/features/pos/domain/repositories/i_pos_repository.dart @@ -0,0 +1,24 @@ +import 'package:niogu_app/features/goods/raw_materials/domain/entities/raw_material.dart'; +import 'package:niogu_app/features/pos/domain/entities/pos.dart'; + +abstract class IPosRepository { + Stream> watchDisplayProductPos(String outletId); + + Stream> watchAlreadyCustomers(String outletId); + + Future checkRawMaterialStockSufficient( + String productVariantId, + double productQuantity, + ); + + Future> getProductUseMaterials( + String productVariantId, + double productQuantity, + ); + + Future addSale( + CustomerInformation? customer, + NewSale newSale, + List itemSales, + ); +} diff --git a/Mobile Operasional/lib/features/pos/presentation/controllers/pos_controller.dart b/Mobile Operasional/lib/features/pos/presentation/controllers/pos_controller.dart new file mode 100644 index 0000000..29f51d4 --- /dev/null +++ b/Mobile Operasional/lib/features/pos/presentation/controllers/pos_controller.dart @@ -0,0 +1,101 @@ +import 'dart:async'; + +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:niogu_app/core/system/system_setting.dart'; +import 'package:niogu_app/features/pos/domain/entities/pos.dart'; +import 'package:niogu_app/features/pos/domain/repositories/i_pos_repository.dart'; +import 'package:niogu_app/features/pos/presentation/providers/pos_provider.dart'; + +class PosController extends AutoDisposeAsyncNotifier { + late final IPosRepository _posRepository; + + @override + FutureOr build() { + _posRepository = ref.read(posRepositoryProvider); + } + + Future addSale( + CustomerInformation? customer, + NewSale newSale, + List itemSales, + ) async { + state = const AsyncValue.loading(); + + final result = await AsyncValue.guard(() async { + await _posRepository.addSale(customer, newSale, itemSales); + }); + + state = result; + + if (result is AsyncError) throw result.error!; + } +} + +class CartItemsController extends StateNotifier> { + CartItemsController() : super({}); + + bool isInCart(String productVariantId) => state.containsKey(productVariantId); + + double quantityOf(String productVariantId) => + state[productVariantId]?.quantity ?? 0.0; + + Future increment(DisplayProductPos product) async { + final cartItem = state[product.productVariantId]; + + if (cartItem == null) { + final currentOutletId = await SystemSetting.getCurrentOutletId(); + + state = { + ...state, + product.productVariantId: CartItems( + currentOutletId: currentOutletId!, + outletInventoryId: product.outletInventoryId, + id: product.productVariantId, + imagePath: product.imagePath, + name: product.name, + variantName: product.hasVariant ? product.variantName : null, + currentSold: product.currentSold, + quantity: 1, + costPrice: product.costPrice, + sellingPrice: product.sellingPrice, + stockType: product.stockType, + remainingStock: product.remainingStock, + unit: product.unitName, + ), + }; + } else { + state = { + ...state, + product.productVariantId: cartItem.copyWith( + quantity: cartItem.quantity + 1, + ), + }; + } + } + + void decrement(String productVariantId) { + final cartItem = state[productVariantId]; + + if (cartItem == null) return; + + if (cartItem.quantity <= 1) { + state = {...state}..remove(productVariantId); + } else { + state = { + ...state, + productVariantId: cartItem.copyWith(quantity: cartItem.quantity - 1), + }; + } + } + + void customQuantity(String productVariantId, double quantity) { + final cartItem = state[productVariantId]; + + if (cartItem == null) return; + + state = {...state, productVariantId: cartItem.copyWith(quantity: quantity)}; + } + + void delete(String productVariantId) => + state = {...state}..remove(productVariantId); +} diff --git a/Mobile Operasional/lib/features/pos/presentation/providers/pos_provider.dart b/Mobile Operasional/lib/features/pos/presentation/providers/pos_provider.dart new file mode 100644 index 0000000..f2a6611 --- /dev/null +++ b/Mobile Operasional/lib/features/pos/presentation/providers/pos_provider.dart @@ -0,0 +1,169 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:niogu_app/core/providers/app_provider.dart'; +import 'package:niogu_app/features/goods/products/domain/entities/product.dart'; +import 'package:niogu_app/features/pos/data/repositories/pos_repository_impl.dart'; +import 'package:niogu_app/features/pos/domain/entities/pos.dart'; +import 'package:niogu_app/features/pos/domain/repositories/i_pos_repository.dart'; +import 'package:niogu_app/features/pos/presentation/controllers/pos_controller.dart'; + +final posRepositoryProvider = Provider((ref) { + final appDatabase = ref.watch(appDatabaseProvider); + return PosRepositoryImpl(appDatabase); +}); + +final displayProductPosStreamProvider = + StreamProvider.autoDispose>((ref) { + final posRepository = ref.watch(posRepositoryProvider); + + final currentOutletId = ref.watch(currentOutletIdProvider); + + if(currentOutletId == null) { + return Stream>.value([]); + } + + return posRepository.watchDisplayProductPos(currentOutletId); + }); + +final displayProductPosBySearchProvider = StateProvider.autoDispose( + (ref) => '', +); + +final filteredDisplayProductPosProvider = + Provider.autoDispose>>((ref) { + final productStreamAsync = ref.watch(displayProductPosStreamProvider); + final productSearchAsync = ref.watch(displayProductPosBySearchProvider); + return productStreamAsync.when( + data: (products) { + if (productSearchAsync.trim().isEmpty) { + return AsyncValue.data(products); + } + + final filteredProduct = products.where((product) { + return product.name.toLowerCase().contains( + productSearchAsync.toLowerCase(), + ); + }).toList(); + + return AsyncValue.data(filteredProduct); + }, + error: (error, stackTrace) { + return AsyncValue.error(error, stackTrace); + }, + loading: () => const AsyncValue.loading(), + ); + }); + +final displayProductPosEmptyProvider = Provider.autoDispose(( + ref, +) { + final productStreamAsync = ref.watch(displayProductPosStreamProvider); + final filteredProductAsync = ref.watch(filteredDisplayProductPosProvider); + final productSearchAsync = ref.watch(displayProductPosBySearchProvider); + + if (productStreamAsync.isLoading) { + return ProductEmpty.loading; + } + + final allProducts = productStreamAsync.value ?? []; + + if (allProducts.isEmpty) { + return ProductEmpty.empty_database; + } + + final filteredProduct = filteredProductAsync.value ?? []; + + if (productSearchAsync.isNotEmpty && filteredProduct.isEmpty) { + return ProductEmpty.empty_search_result; + } + + return ProductEmpty.has_data; +}); + +final cartItemsControllerProvider = + StateNotifierProvider>((ref) { + return CartItemsController(); + }); + +final alreadyCustomerStreamProvider = + StreamProvider.autoDispose>((ref) { + final posRepository = ref.watch(posRepositoryProvider); + + final currentOutletId = ref.watch(currentOutletIdProvider); + + if(currentOutletId == null) { + return Stream>.value([]); + } + + return posRepository.watchAlreadyCustomers(currentOutletId); + }); + +final alreadyCustomerSearchProvider = StateProvider.autoDispose( + (ref) => '', +); + +final filteredAlreadyCustomerProvider = + Provider.autoDispose>>((ref) { + final alreadyCustomerStreamAsync = ref.watch( + alreadyCustomerStreamProvider, + ); + final alreadyCustomerSearchAsync = ref.watch( + alreadyCustomerSearchProvider, + ); + + return alreadyCustomerStreamAsync.when( + data: (customers) { + if (alreadyCustomerSearchAsync.trim().isEmpty) { + return AsyncValue.data(customers); + } + + final filteredCustomers = customers.where((customer) { + return customer.name.toLowerCase().contains( + alreadyCustomerSearchAsync.toLowerCase(), + ); + }).toList(); + + return AsyncValue.data(filteredCustomers); + }, + error: (error, stackTrace) => AsyncValue.error(error, stackTrace), + loading: () => const AsyncValue.loading(), + ); + }); + +final alreadyCustomerEmptyProvider = Provider.autoDispose( + (ref) { + final alreadyCustomerStreamAsync = ref.watch(alreadyCustomerStreamProvider); + final filteredAlreadyCustomerAsync = ref.watch( + filteredAlreadyCustomerProvider, + ); + final alreadyCustomerSearchAsync = ref.watch(alreadyCustomerSearchProvider); + + if (alreadyCustomerStreamAsync.isLoading) { + return AlreadyCustomerEmpty.loading; + } + + final allCustomers = alreadyCustomerStreamAsync.value ?? []; + + if (allCustomers.isEmpty) { + return AlreadyCustomerEmpty.empty_database; + } + + final filteredCustomer = filteredAlreadyCustomerAsync.value ?? []; + + if (alreadyCustomerSearchAsync.isNotEmpty && filteredCustomer.isEmpty) { + return AlreadyCustomerEmpty.empty_search_result; + } + + return AlreadyCustomerEmpty.has_data; + }, +); + +final expandedAlreadyCustomerProvider = StateProvider.autoDispose + .family((ref, customerId) => false); + +final selectedCustomerProvider = StateProvider.autoDispose( + (ref) => null, +); + +final posControllerProvider = AsyncNotifierProvider.autoDispose( + PosController.new, +); diff --git a/Mobile Operasional/lib/features/pos/presentation/screens/already_customer_screen.dart b/Mobile Operasional/lib/features/pos/presentation/screens/already_customer_screen.dart new file mode 100644 index 0000000..e0c5db2 --- /dev/null +++ b/Mobile Operasional/lib/features/pos/presentation/screens/already_customer_screen.dart @@ -0,0 +1,743 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter_contacts/flutter_contacts.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; +import 'package:niogu_app/core/widgets/custom_error_screen.dart'; +import 'package:niogu_app/core/providers/app_provider.dart'; +import 'package:niogu_app/core/router/app_route.dart'; +import 'package:niogu_app/core/enums/user_role.dart'; +import 'package:niogu_app/core/widgets/custom_empty_screen.dart'; +import 'package:niogu_app/core/widgets/custom_snackbar.dart'; +import 'package:niogu_app/core/widgets/pop_up_notification.dart'; +import 'package:niogu_app/features/pos/domain/entities/pos.dart'; +import 'package:niogu_app/features/pos/presentation/providers/pos_provider.dart'; +import 'package:niogu_app/features/pos/presentation/widgets/already_customer_shimmer.dart'; +import 'package:niogu_app/features/pos/presentation/widgets/checkbox_customer.dart'; +import 'package:sizer/sizer.dart'; +import 'package:niogu_app/core/constants/app_color.dart'; +import 'package:niogu_app/core/constants/app_font_size.dart'; + +class AlreadyCustomerScreen extends ConsumerStatefulWidget { + const AlreadyCustomerScreen({super.key}); + + @override + ConsumerState createState() => + _AlreadyCustomerScreenState(); +} + +class _AlreadyCustomerScreenState extends ConsumerState { + final FocusNode _searchFocusNode = FocusNode(); + + Color _searchIconColor = Colors.grey; + + Timer? _debounce; + + String? _selectedCustomerId; + + String? _selectedCustomerAddress; + + SelectedCustomer? _selectedCustomer; + + @override + void initState() { + // TODO: implement initState + super.initState(); + + _searchFocusNode.addListener(() { + setState(() { + _searchIconColor = _searchFocusNode.hasFocus + ? Colors.black + : Colors.grey; + }); + }); + } + + @override + void dispose() { + // TODO: implement dispose + _searchFocusNode.dispose(); + _debounce?.cancel(); + super.dispose(); + } + + void _onSearchChanged(String value) { + if (_debounce?.isActive ?? false) _debounce?.cancel(); + _debounce = Timer(const Duration(milliseconds: 800), () { + ref.read(alreadyCustomerSearchProvider.notifier).state = value; + }); + } + + Future _handleImportContact() async { + final permission = await FlutterContacts.requestPermission(); + if (permission) { + final contact = await FlutterContacts.openExternalPick(); + + if (contact != null) { + String name = contact.displayName; + String phone = ""; + + if (contact.phones.isNotEmpty) { + phone = contact.phones.first.number; + } + + if (!mounted) return; + + context.pushNamed( + AppRoute.addCustomerScreen, + extra: {'name': name, 'phone': phone}, + ); + } + } else { + CustomSnackbar.showWarning(context, "Akses Ditolak"); + } + } + + void _showAddOptions(BuildContext context) { + final bool isTablet = 100.w >= 600; + showModalBottomSheet( + context: context, + backgroundColor: Colors.white, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.vertical(top: Radius.circular(2.5.w)), + ), + constraints: const BoxConstraints(maxWidth: double.infinity), + builder: (context) { + return SafeArea( + child: Container( + width: 100.w, + padding: EdgeInsets.symmetric(vertical: 2.h), + clipBehavior: Clip.hardEdge, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.vertical(top: Radius.circular(6.w)), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: EdgeInsets.fromLTRB(5.w, 2.h, 5.w, 1.h), + child: Text( + "Tambah Pelanggan", + style: TextStyle( + fontSize: AppFontSize.medium.sp, + fontWeight: FontWeight.bold, + ), + ), + ), + SizedBox(height: 2.h), + ListTile( + leading: Padding( + padding: EdgeInsets.only(left: 5.w), + child: Icon( + Icons.edit_note_rounded, + color: Colors.blue, + size: 6.w, + ), + ), + title: Text( + "Input Manual", + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + ), + ), + subtitle: Text( + "Isi informasi pelanggan secara manual", + style: TextStyle( + fontSize: isTablet + ? (AppFontSize.medium - 1.25).sp + : (AppFontSize.small - 1.25).sp, + color: Colors.grey, + ), + ), + onTap: () { + context.pop(); + context.pushNamed(AppRoute.addCustomerScreen); + }, + ), + SizedBox(height: 2.h), + ListTile( + leading: Padding( + padding: EdgeInsets.only(left: 5.w), + child: Icon( + Icons.contacts_rounded, + color: Colors.green, + size: 6.w, + ), + ), + title: Text( + "Ambil dari Kontak HP", + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + ), + ), + subtitle: Text( + "Pilih langsung dari buku telepon", + style: TextStyle( + fontSize: isTablet + ? (AppFontSize.medium - 1.25).sp + : (AppFontSize.small - 1.25).sp, + color: Colors.grey, + ), + ), + onTap: () async { + context.pop(); + await _handleImportContact(); + }, + ), + SizedBox(height: 2.h), + ], + ), + ), + ); + }, + ); + } + + void _selectCustomer( + SelectedCustomer selectedCustomer, + String customerId, + String address, + ) { + setState(() { + if (_selectedCustomerId == customerId && + _selectedCustomerAddress == address) { + _selectedCustomerId = null; + _selectedCustomerAddress = null; + _selectedCustomer = null; + } else { + _selectedCustomerId = customerId; + _selectedCustomerAddress = address; + _selectedCustomer = selectedCustomer; + } + }); + } + + void _submitSelection() { + ref.read(selectedCustomerProvider.notifier).state = _selectedCustomer; + context.pop(); + } + + @override + Widget build(BuildContext context) { + return LayoutBuilder( + builder: (context, constraints) { + final bool isTablet = 100.w >= 600; + + const greyColor = Color(0xFFF5F5F5); + + final messageActivity = ref.watch(messageActivityProvider); + + final currentUserRole = ref.watch(currentUserRoleProvider); + + final filteredCustomerState = ref.watch( + filteredAlreadyCustomerProvider, + ); + + final customerEmptyState = ref.watch(alreadyCustomerEmptyProvider); + + return SafeArea( + top: false, + bottom: true, + right: false, + left: false, + child: Stack( + children: [ + Scaffold( + backgroundColor: Colors.grey[50], + appBar: AppBar( + backgroundColor: Colors.white, + surfaceTintColor: Colors.white, + elevation: 0, + toolbarHeight: 10.h, + titleSpacing: 0, + automaticallyImplyLeading: false, + title: Padding( + padding: EdgeInsets.symmetric(horizontal: 4.w), + child: Row( + children: [ + Center( + child: Material( + color: Colors.transparent, + type: MaterialType.canvas, + child: InkWell( + onTap: () => context.pop(), + borderRadius: BorderRadius.circular(2.5.w), + child: Container( + width: 10.w, + height: 10.w, + decoration: BoxDecoration( + color: Colors.white, + border: Border.all( + color: Colors.grey.shade200, + ), + borderRadius: BorderRadius.circular(2.5.w), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.05), + blurRadius: 5, + offset: const Offset(0, 2), + ), + ], + ), + child: Icon( + Icons.arrow_back_ios_new_rounded, + color: Colors.black87, + size: 5.w, + ), + ), + ), + ), + ), + + SizedBox(width: 3.w), + + Expanded( + child: Container( + height: 6.h, + padding: isTablet + ? EdgeInsets.symmetric( + vertical: 1.h, + horizontal: 2.w, + ) + : EdgeInsets.zero, + decoration: BoxDecoration( + color: greyColor, + borderRadius: BorderRadius.circular(2.5.w), + ), + child: TextField( + focusNode: _searchFocusNode, + onChanged: (value) => _onSearchChanged(value), + textAlignVertical: TextAlignVertical.center, + style: TextStyle( + color: Colors.black87, + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + ), + decoration: InputDecoration( + hintText: "Cari nama pelanggan...", + hintStyle: TextStyle( + color: _searchIconColor, + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + ), + prefixIcon: Icon( + Icons.search, + color: _searchIconColor, + size: 5.w, + ), + border: InputBorder.none, + contentPadding: EdgeInsets.zero, + isDense: true, + ), + ), + ), + ), + + SizedBox(width: 2.w), + + IconButton( + onPressed: () => _showAddOptions(context), + icon: Icon( + Icons.add_circle, + color: AppColor.primaryColor, + size: 8.w, + ), + ), + ], + ), + ), + bottom: PreferredSize( + preferredSize: Size.fromHeight(2), + child: Container(color: Colors.grey.shade100, height: 1), + ), + ), + body: filteredCustomerState.when( + data: (customers) { + switch (customerEmptyState) { + case AlreadyCustomerEmpty.loading: + return SizedBox(); + case AlreadyCustomerEmpty.empty_database: + return CustomEmptyScreen( + title: "Tidak Ada Pelanggan", + body: "Kamu belum memiliki pelanggan", + ); + + case AlreadyCustomerEmpty.empty_search_result: + return CustomEmptyScreen( + body: "Pelanggan Tidak Ditemukan", + ); + case AlreadyCustomerEmpty.has_data: + return ListView.builder( + padding: EdgeInsets.symmetric( + vertical: 2.h, + horizontal: 5.w, + ), + itemCount: customers.length, + itemBuilder: (context, index) { + final customer = customers[index]; + + final bool hasMultiAddress = + customer.addresses.length > 1; + + final bool isSingleSelected = + !hasMultiAddress && + _selectedCustomerId == customer.id; + + final bool isExpanded = ref.watch( + expandedAlreadyCustomerProvider(customer.id), + ); + + return AnimatedContainer( + duration: const Duration(milliseconds: 200), + margin: EdgeInsets.only(bottom: 2.h), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(4.w), + border: Border.all( + color: Colors.grey.shade200, + width: 1, + ), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.03), + blurRadius: 8, + offset: const Offset(0, 4), + ), + ], + ), + child: Column( + children: [ + InkWell( + onTap: () { + if (hasMultiAddress) { + ref + .read( + expandedAlreadyCustomerProvider( + customer.id, + ).notifier, + ) + .state = !ref.read( + expandedAlreadyCustomerProvider( + customer.id, + ), + ); + } else { + _selectCustomer( + SelectedCustomer( + id: customer.id, + outletId: ref.read( + currentOutletIdProvider, + )!, + name: customer.name, + phoneNumber: customer.phoneNumber, + address: customer.addresses.first, + ), + customer.id, + customer.addresses.first, + ); + } + }, + borderRadius: BorderRadius.vertical( + top: Radius.circular(4.w), + bottom: (hasMultiAddress && isExpanded) + ? Radius.zero + : Radius.circular(4.w), + ), + child: Padding( + padding: EdgeInsets.all(3.w), + child: Row( + children: [ + Container( + width: 12.w, + height: 12.w, + decoration: BoxDecoration( + color: Colors.grey[100], + shape: BoxShape.circle, + ), + alignment: Alignment.center, + child: Text( + customer.name[0].toUpperCase(), + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + color: Colors.grey, + ), + ), + ), + SizedBox(width: 4.w), + + Expanded( + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text( + customer.name, + style: TextStyle( + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + fontWeight: FontWeight.bold, + color: Colors.black87, + ), + ), + if (customer + .phoneNumber + .isNotEmpty) ...[ + SizedBox(height: 0.75.h), + Row( + children: [ + Icon( + Icons.phone_android, + size: 5.w, + color: Colors.grey, + ), + SizedBox(width: 1.w), + Text( + customer.phoneNumber, + style: TextStyle( + fontSize: isTablet + ? AppFontSize + .medium + .sp + : AppFontSize + .small + .sp, + color: + Colors.grey[600], + ), + ), + ], + ), + ], + + if (customer + .addresses + .first + .isNotEmpty && + !hasMultiAddress) ...[ + SizedBox(height: 0.75.h), + Row( + children: [ + Icon( + Icons + .location_on_outlined, + color: Colors.grey, + size: 5.w, + ), + + SizedBox(width: 1.w), + + Expanded( + child: Text( + customer + .addresses + .first, + overflow: TextOverflow + .ellipsis, + style: TextStyle( + fontSize: isTablet + ? AppFontSize + .medium + .sp + : AppFontSize + .small + .sp, + color: Colors + .grey[500], + ), + ), + ), + ], + ), + ], + ], + ), + ), + + if (customer + .addresses + .isNotEmpty) ...[ + if (hasMultiAddress) + Icon( + isExpanded + ? Icons + .keyboard_arrow_up_rounded + : Icons + .keyboard_arrow_down_rounded, + color: Colors.grey, + size: 7.w, + ) + else + CheckboxCustomer( + isSelected: isSingleSelected, + ), + ], + ], + ), + ), + ), + + if (hasMultiAddress && isExpanded) ...[ + Container( + height: 1, + color: Colors.grey[100], + ), + + Padding( + padding: EdgeInsets.symmetric( + vertical: 1.h, + ), + child: Column( + children: customer.addresses.map(( + address, + ) { + final bool isAddressSelected = + _selectedCustomerId == + customer.id && + _selectedCustomerAddress == + address; + + return InkWell( + onTap: () { + _selectCustomer( + SelectedCustomer( + id: customer.id, + outletId: ref.read( + currentOutletIdProvider, + )!, + name: customer.name, + phoneNumber: + customer.phoneNumber, + address: address, + ), + customer.id, + address, + ); + }, + child: Padding( + padding: EdgeInsets.symmetric( + horizontal: 4.w, + vertical: 1.5.h, + ), + child: Row( + children: [ + SizedBox(width: 4.w), + + CheckboxCustomer( + isSelected: + isAddressSelected, + ), + + SizedBox(width: 3.w), + + Expanded( + child: Text( + address, + overflow: + TextOverflow.ellipsis, + style: TextStyle( + fontSize: isTablet + ? AppFontSize + .medium + .sp + : AppFontSize + .small + .sp, + color: isAddressSelected + ? Colors.black + : Colors.grey[700], + fontWeight: + isAddressSelected + ? FontWeight.bold + : FontWeight.normal, + ), + ), + ), + ], + ), + ), + ); + }).toList(), + ), + ), + ], + ], + ), + ); + }, + ); + } + }, + error: (error, stackTrace) { + return CustomErrorScreen( + message: "Ups, terjadi kesalahan", + onRefresh: () {}, + ); + }, + loading: () => const AlreadyCustomerShimmer(), + ), + bottomNavigationBar: Container( + padding: EdgeInsets.all(5.w), + decoration: BoxDecoration( + color: Colors.white, + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.05), + blurRadius: 10, + offset: const Offset(0, -5), + ), + ], + ), + child: SizedBox( + height: 6.5.h, + child: ElevatedButton( + onPressed: (_selectedCustomerId != null) + ? _submitSelection + : null, + style: ElevatedButton.styleFrom( + backgroundColor: AppColor.primaryColor, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(2.5.w), + ), + elevation: 0, + disabledBackgroundColor: Colors.grey.shade300, + ), + child: Text( + "Pilih Pelanggan", + style: TextStyle( + color: Colors.white, + fontSize: AppFontSize.medium.sp, + fontWeight: FontWeight.bold, + ), + ), + ), + ), + ), + ), + + if (messageActivity != null) + PopupNotification( + isOwner: currentUserRole == UserRole.owner, + messages: messageActivity.messages, + type: messageActivity.type, + ), + ], + ), + ); + }, + ); + } +} diff --git a/Mobile Operasional/lib/features/pos/presentation/screens/pos_screen.dart b/Mobile Operasional/lib/features/pos/presentation/screens/pos_screen.dart new file mode 100644 index 0000000..fab078a --- /dev/null +++ b/Mobile Operasional/lib/features/pos/presentation/screens/pos_screen.dart @@ -0,0 +1,1183 @@ +import 'dart:async'; +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; +import 'package:image_picker/image_picker.dart'; +import 'package:niogu_app/core/constants/app_color.dart'; +import 'package:niogu_app/core/constants/app_font_size.dart'; +import 'package:niogu_app/core/utils/login_required.dart'; +import 'package:niogu_app/core/widgets/custom_error_screen.dart'; +import 'package:niogu_app/core/providers/app_provider.dart'; +import 'package:niogu_app/core/router/app_route.dart'; +import 'package:niogu_app/core/utils/currency_format.dart'; +import 'package:niogu_app/core/utils/extension_format.dart'; +import 'package:niogu_app/core/utils/image_service.dart'; +import 'package:niogu_app/core/utils/log_message.dart'; +import 'package:niogu_app/core/enums/stock_type.dart'; +import 'package:niogu_app/core/system/system_setting.dart'; +import 'package:niogu_app/core/widgets/custom_empty_screen.dart'; +import 'package:niogu_app/core/widgets/custom_not_login.dart'; +import 'package:niogu_app/core/widgets/custom_snackbar.dart'; +import 'package:niogu_app/features/goods/products/domain/entities/product.dart'; +import 'package:niogu_app/features/pos/domain/entities/pos.dart'; +import 'package:niogu_app/features/pos/presentation/providers/pos_provider.dart'; +import 'package:niogu_app/features/pos/presentation/widgets/cart_bottom_sheet.dart'; +import 'package:niogu_app/features/pos/presentation/widgets/decimal_quantity.dart'; +import 'package:niogu_app/features/pos/presentation/widgets/insufficient_stock_dialog.dart'; +import 'package:niogu_app/features/pos/presentation/widgets/pos_shimmer.dart'; +import 'package:niogu_app/features/pos/presentation/widgets/product_card.dart'; +import 'package:niogu_app/features/report/transaction/presentation/providers/transaction_report_provider.dart'; +import 'package:sizer/sizer.dart'; + +class PosScreen extends ConsumerStatefulWidget { + const PosScreen({super.key}); + + @override + ConsumerState createState() => _PosScreenState(); +} + +class _PosScreenState extends ConsumerState { + final FocusNode _searchFocusNode = FocusNode(); + + Color _searchIconColor = Colors.grey; + + final DraggableScrollableController _sheetController = + DraggableScrollableController(); + + final GlobalKey _formKey = GlobalKey(); + + final TextEditingController _discountController = TextEditingController(); + + final TextEditingController _taxController = TextEditingController(); + + final TextEditingController _noteController = TextEditingController(); + + final TextEditingController _nameController = TextEditingController(); + + final TextEditingController _phoneNumberController = TextEditingController(); + + final TextEditingController _addressController = TextEditingController(); + + final TextEditingController _payController = TextEditingController(); + + final List _imagePathTemps = []; + + final ImagePicker _picker = ImagePicker(); + + Timer? _debounce; + + bool _isOtherFormVisible = false; + + bool _isCustomerFormVisible = false; + + bool _validateChange = false; + + double _changeAmount = 0.0; + + String? _imagePath; + + @override + void initState() { + // TODO: implement initState + super.initState(); + _searchFocusNode.addListener(() { + setState(() { + _searchIconColor = _searchFocusNode.hasFocus + ? Colors.black + : Colors.grey; + }); + }); + } + + @override + void dispose() { + // TODO: implement dispose + _searchFocusNode.dispose(); + _discountController.dispose(); + _taxController.dispose(); + _noteController.dispose(); + _nameController.dispose(); + _phoneNumberController.dispose(); + _addressController.dispose(); + _payController.dispose(); + _debounce?.cancel(); + super.dispose(); + } + + void _expandSheet() { + FocusManager.instance.primaryFocus?.unfocus(); + + Future.delayed(const Duration(milliseconds: 100), () { + if (_sheetController.isAttached) { + _sheetController.animateTo( + 0.9, + duration: const Duration(milliseconds: 300), + curve: Curves.easeOut, + ); + } else { + LogMessage.log.i("Sheet controller is not attached"); + } + }); + } + + void _onSearchChanged(String value) { + if (_debounce?.isActive ?? false) _debounce?.cancel(); + _debounce = Timer(const Duration(milliseconds: 800), () { + ref.read(displayProductPosBySearchProvider.notifier).state = value; + }); + } + + int _calculateProductAmount(List cartItems) { + return cartItems.length; + } + + double _calculateTotalQty(List cartItems) { + return cartItems.fold(0.0, (sum, cartItem) { + return sum + cartItem.quantity; + }); + } + + double _calculateShoppingAmount(List cartItems) { + return cartItems.fold(0.0, (sum, cartItem) { + return sum + cartItem.quantity * cartItem.sellingPrice; + }); + } + + void _discountOnChanged(String value) { + final currentOutletId = ref.read(currentOutletIdProvider); + + final cartItemState = ref.read(cartItemsControllerProvider); + + final cartItems = cartItemState.values.where((cartItem) { + return cartItem.currentOutletId == currentOutletId; + }).toList(); + + final double payAmount = double.tryParse(_payController.text.trim()) ?? 0.0; + + if (_debounce?.isActive ?? false) _debounce?.cancel(); + _debounce = Timer(const Duration(milliseconds: 400), () { + setState(() { + _validateChange = payAmount < _calculateTotalBill(cartItems); + _discountController.text = value; + }); + }); + + if (_payController.text.trim().isNotEmpty) { + _calculateChange(_payController.text); + } + } + + void _taxOnChanged(String value) { + final currentOutletId = ref.read(currentOutletIdProvider); + + final cartItemState = ref.read(cartItemsControllerProvider); + + final cartItems = cartItemState.values.where((cartItem) { + return cartItem.currentOutletId == currentOutletId; + }).toList(); + + final double payAmount = double.tryParse(_payController.text.trim()) ?? 0.0; + + if (_debounce?.isActive ?? false) _debounce?.cancel(); + _debounce = Timer(const Duration(milliseconds: 400), () { + setState(() { + _validateChange = payAmount < _calculateTotalBill(cartItems); + _taxController.text = value; + }); + }); + + if (_payController.text.trim().isNotEmpty) { + _calculateChange(_payController.text); + } + } + + void _calculateChange(String value) { + final currentOutletId = ref.read(currentOutletIdProvider); + + final cartItemState = ref.read(cartItemsControllerProvider); + + final cartItems = cartItemState.values.where((cartItem) { + return cartItem.currentOutletId == currentOutletId; + }).toList(); + + final double payAmount = double.tryParse(value) ?? 0.0; + + final double totalBill = _calculateTotalBill(cartItems); + + _validateChange = payAmount < totalBill; + _payController.text = value; + if (payAmount >= totalBill && cartItems.isNotEmpty) { + _changeAmount = payAmount - totalBill; + } + } + + double _calculateTotalBill(List cartItems) { + final double discount = + double.tryParse(_discountController.text.trim()) ?? 0.0; + + final double tax = double.tryParse(_taxController.text.trim()) ?? 0.0; + + final double totalBiil = _calculateShoppingAmount(cartItems); + + return totalBiil - discount + tax; + } + + void _handleDecrement(DisplayProductPos product) { + final cartItemsNotifier = ref.read(cartItemsControllerProvider.notifier); + + cartItemsNotifier.decrement(product.productVariantId); + + if (_payController.text.trim().isNotEmpty) { + _calculateChange(_payController.text); + } + } + + Future _handleIncrement(DisplayProductPos product) async { + final cartItemsNotifier = ref.read(cartItemsControllerProvider.notifier); + + if (product.stockType == StockType.fixed) { + if (product.remainingStock == 0) { + CustomSnackbar.showError(context, "Stok habis"); + return; + } + + if (cartItemsNotifier.quantityOf(product.productVariantId) >= + product.remainingStock) { + CustomSnackbar.showError(context, "Stok tidak cukup"); + return; + } + } + + await cartItemsNotifier.increment(product); + + if (_payController.text.isNotEmpty) { + _calculateChange(_payController.text); + } + } + + void _handleDeleteItem(CartItems cartItem) { + final cartItemsNotifier = ref.read(cartItemsControllerProvider.notifier); + + cartItemsNotifier.delete(cartItem.id); + + if (_payController.text.trim().isNotEmpty) { + _calculateChange(_payController.text); + } + } + + void _clear() { + _discountController.clear(); + _taxController.clear(); + _noteController.clear(); + _nameController.clear(); + _phoneNumberController.clear(); + _addressController.clear(); + _payController.clear(); + } + + void _showInsufficientStockDialog({ + required String id, + required String name, + required double currentStock, + required double totalNeeded, + required double missingAmount, + required String unit, + required String triggerProductName, + required int otherProductsCount, + }) { + showDialog( + context: context, + builder: (context) { + return InsufficientStockDialog( + materialName: name, + currentStock: currentStock.toStringWithoutTrailingZero(), + totalNeeded: totalNeeded.toStringWithoutTrailingZero(), + missingAmount: missingAmount.toStringWithoutTrailingZero(), + unit: unit, + triggerProductName: triggerProductName, + otherProductsCount: otherProductsCount, + + onAddStockPressed: () { + context.pushNamed(AppRoute.stockInScreen); + }, + onEditUsagePressed: () { + context.pushNamed( + AppRoute.editRawMaterialScreen, + pathParameters: {"id": id}, + ); + }, + ); + }, + ); + } + + Future _showPickerOptions() async { + final bool isTablet = 100.w >= 600; + showModalBottomSheet( + context: context, + backgroundColor: Colors.white, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.vertical(top: Radius.circular(2.5.w)), + ), + constraints: const BoxConstraints(maxWidth: double.infinity), + builder: (BuildContext context) { + return SafeArea( + child: Container( + width: 100.w, + padding: EdgeInsets.symmetric(vertical: 2.h, horizontal: 5.w), + clipBehavior: Clip.hardEdge, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.vertical(top: Radius.circular(6.w)), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + ListTile( + leading: Icon(Icons.photo_library, size: 6.w), + title: Text( + 'Galeri', + style: TextStyle( + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + ), + ), + onTap: () async { + await _getImage(ImageSource.gallery); + context.pop(); + }, + ), + + SizedBox(height: 2.h), + + ListTile( + leading: Icon(Icons.photo_camera, size: 6.w), + title: Text( + 'Kamera', + style: TextStyle( + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + ), + ), + onTap: () async { + await _getImage(ImageSource.camera); + context.pop(); + }, + ), + ], + ), + ), + ); + }, + ); + } + + Future _getImage(ImageSource ImageSource) async { + try { + final XFile? pickedFile = await _picker.pickImage( + source: ImageSource, + imageQuality: 70, + ); + + if (pickedFile != null) { + File tempFile = File(pickedFile.path); + + String? imagePath = await ImageService.saveImageToLocalDirectory( + tempFile, + "payment_proof_orders", + ); + + if (imagePath != null) { + _imagePathTemps.add(imagePath); + setState(() { + _imagePath = imagePath; + }); + } + } + } catch (e, st) { + LogMessage.log.w(e.toString(), error: e, stackTrace: st); + CustomSnackbar.showWarning(context, "Akses ditolak"); + } + } + + Future _removeImage() async { + setState(() { + _imagePath = null; + }); + } + + Future _cleanUpImages() async { + for (final path in _imagePathTemps) { + if (path != null) { + await ImageService.deleteLocalImage(path); + } + } + } + + Future _addSale() async { + final cartItemState = ref.read(cartItemsControllerProvider); + + final currentOutletId = await SystemSetting.getCurrentOutletId(); + + final cartItems = cartItemState.values.where((cartItem) { + return cartItem.currentOutletId == currentOutletId; + }).toList(); + + if (cartItems.isEmpty) { + CustomSnackbar.showError(context, "Tambah minimal 1 produk"); + return; + } + + final Map> usedByProducts = {}; + + final Map requiredMaterials = {}; + + final Map materialNames = {}; + + final Map currentStocks = {}; + + final Map materialUnits = {}; + + for (final cartItem in cartItems) { + final rawMaterials = await ref + .read(posRepositoryProvider) + .getProductUseMaterials(cartItem.id, cartItem.quantity); + + for (final rawMaterial in rawMaterials) { + if (!usedByProducts.containsKey(rawMaterial.rawMaterialId)) { + usedByProducts[rawMaterial.rawMaterialId] = {}; + } + + usedByProducts[rawMaterial.rawMaterialId]!.add( + cartItem.variantName ?? cartItem.name, + ); + + final double needForThisItem = + rawMaterial.productQuantity * rawMaterial.quantity; + + if (requiredMaterials.containsKey(rawMaterial.rawMaterialId)) { + requiredMaterials[rawMaterial.rawMaterialId] = + requiredMaterials[rawMaterial.rawMaterialId]! + needForThisItem; + } else { + requiredMaterials[rawMaterial.rawMaterialId] = needForThisItem; + } + cartItem.variantName ?? cartItem.name; + materialNames[rawMaterial.rawMaterialId] = rawMaterial.name; + currentStocks[rawMaterial.rawMaterialId] = rawMaterial.stock; + materialUnits[rawMaterial.rawMaterialId] = rawMaterial.unit; + } + } + + for (final entry in requiredMaterials.entries) { + final String id = entry.key; + + final double totalNeeded = entry.value; + + final double currentStock = currentStocks[id] ?? 0.0; + + final String name = materialNames[id] ?? 'Bahan Baku'; + + final String unit = materialUnits[id] ?? ''; + + final products = usedByProducts[id]!.toList(); + + final String triggerProductName = products.first; + + final int otherProductsCount = products.length - 1; + + if (currentStock < totalNeeded) { + final double missingAmount = totalNeeded - currentStock; + _showInsufficientStockDialog( + id: id, + name: name, + currentStock: currentStock, + totalNeeded: totalNeeded, + missingAmount: missingAmount, + unit: unit, + triggerProductName: triggerProductName, + otherProductsCount: otherProductsCount, + ); + return; + } + } + + if (!_formKey.currentState!.validate()) return; + + try { + if (_discountController.text.isNotEmpty) { + final double discount = double.parse(_discountController.text.trim()); + + if (discount <= 0) { + CustomSnackbar.showError(context, "Diskon harus lebih dari 0"); + return; + } + } + } catch (e) { + CustomSnackbar.showError(context, "Nominal diskon tidak valid"); + return; + } + + try { + if (_taxController.text.isNotEmpty) { + final double tax = double.parse(_taxController.text.trim()); + + if (tax <= 0) { + CustomSnackbar.showError(context, "Pajak harus lebih dari 0"); + return; + } + } + } catch (e) { + CustomSnackbar.showError(context, "Nominal pajak tidak valid"); + return; + } + + try { + if (_payController.text.isNotEmpty && _validateChange) { + final double payAmount = double.parse(_payController.text.trim()); + + if (payAmount <= 0) { + CustomSnackbar.showError( + context, + "Nominal pembayaran harus lebih dari 0", + ); + return; + } + + final double totalBill = _calculateTotalBill(cartItems); + + if (totalBill > payAmount) { + CustomSnackbar.showError( + context, + "Kurang ${CurrencyFormat.formatToIdr((totalBill - payAmount), 0)}", + ); + return; + } + } + } catch (e) { + CustomSnackbar.showError(context, "Nominal pembayaran tidak valid"); + return; + } + + CustomerInformation? customer; + + final Map customerAddressSnapshot = {}; + + final bool customerInfoIsNotEmpty = + _nameController.text.isNotEmpty || + _phoneNumberController.text.isNotEmpty || + _addressController.text.isNotEmpty; + + if (customerInfoIsNotEmpty) { + if (_nameController.text.isEmpty) { + CustomSnackbar.showError(context, "Nama pelanggan belum diisi"); + return; + } + + final name = _nameController.text.trim(); + final phoneNumber = _phoneNumberController.text.trim(); + final address = _addressController.text.trim(); + + customer = CustomerInformation( + name: name, + phoneNumber: phoneNumber, + address: address, + ); + + if (address.isNotEmpty) { + customerAddressSnapshot.putIfAbsent("full_address", () => address); + } + } + + final selectedCustomerState = ref.read(selectedCustomerProvider); + + SelectedCustomer? selectedCustomer; + + if (selectedCustomerState?.outletId == currentOutletId) { + selectedCustomer = selectedCustomerState; + } + + if (selectedCustomer != null && selectedCustomer.address.isNotEmpty) { + customerAddressSnapshot.putIfAbsent( + "full_address", + () => selectedCustomer!.address, + ); + } + + String? customerId; + + String? customerName; + + String? customerPhoneNumber; + + if (selectedCustomer != null) { + customerId = selectedCustomer.id; + customerName = selectedCustomer.name; + customerPhoneNumber = selectedCustomer.phoneNumber; + } else if (customer != null) { + customerId = customer.id; + customerName = customer.name; + customerPhoneNumber = customer.phoneNumber; + } + + final newSale = NewSale( + customerId: customerId, + customerNameSnapshot: customerName, + customerPhoneNumberSnapshot: customerPhoneNumber, + totalOrder: _calculateShoppingAmount(cartItems), + otherInformation: OtherInformation( + discount: double.tryParse(_discountController.text.trim()) ?? 0.0, + tax: double.tryParse(_taxController.text.trim()) ?? 0.0, + note: _noteController.text.trim(), + ), + totalAmount: _calculateTotalBill(cartItems), + amountPaid: double.tryParse(_payController.text.trim()) ?? 0.0, + changeAmount: _changeAmount, + paymentProofPath: _imagePath, + customerAddressSnapshot: customerAddressSnapshot, + ); + + final itemSales = cartItems.map((item) { + return ItemSale( + orderId: newSale.localId, + outletInventoryId: item.outletInventoryId, + productVariantId: item.id, + stockType: item.stockType, + stock: item.remainingStock, + currentSold: item.currentSold, + quantity: item.quantity, + costPrice: item.costPrice, + productImageSnapshot: item.imagePath, + productNameSnapshot: item.name, + productVariantNameSnapshot: item.variantName, + sellingPriceSnapshot: item.sellingPrice, + subtotal: item.quantity * item.sellingPrice, + ); + }).toList(); + + try { + await ref + .read(posControllerProvider.notifier) + .addSale(customer, newSale, itemSales); + + if (!mounted) return; + + CustomSnackbar.showSuccess(context, "Penjualan baru berhasil dibuat"); + + _clear(); + + for (final path in _imagePathTemps) { + if (path != null && _imagePath != null && path != _imagePath) { + await ImageService.deleteLocalImage(path); + } + } + + setState(() { + _imagePath = null; + _imagePathTemps.clear(); + }); + + ref.invalidate(selectedCustomerProvider); + + ref.invalidate(cartItemsControllerProvider); + + final orderDetail = await ref + .read(transactionReportRepositoryProvider) + .getOrderDetail(newSale.localId); + + context.pushNamed( + AppRoute.transactionReportOrderDetailScreen, + extra: orderDetail, + ); + } catch (e, st) { + LogMessage.log.e(e.toString(), error: e, stackTrace: st); + CustomSnackbar.showError(context, "Ups, terjadi kesalahan"); + } + } + + @override + Widget build(BuildContext context) { + return LayoutBuilder( + builder: (context, constraints) { + final bool isTablet = 100.w >= 600; + + final bool isLandscape = + MediaQuery.of(context).orientation == Orientation.landscape; + + const greyColor = Color(0xFFF5F5F5); + + final isLoggedIn = ref.watch(currentStatusLoginProvider); + + final currentUserName = ref.watch(currentUserNameProvider); + + final currentOutletId = ref.watch(currentOutletIdProvider); + + final currentOutletName = ref.watch(currentOutletNameProvider); + + final currentUserRole = ref.watch(currentUserRoleProvider); + + final productState = isLoggedIn + ? ref.watch(filteredDisplayProductPosProvider) + : AsyncValue>.data([]); + + final productEmptyState = isLoggedIn + ? ref.watch(displayProductPosEmptyProvider) + : ProductEmpty.empty_database; + + final selectedCustomerState = ref.watch(selectedCustomerProvider); + + final selectedCustomer = + selectedCustomerState?.outletId == currentOutletId + ? selectedCustomerState + : null; + + final posControllerState = ref.watch(posControllerProvider); + + return PopScope( + canPop: false, + onPopInvokedWithResult: (didPop, result) async { + if (didPop) return; + + await _cleanUpImages(); + + context.goNamed(AppRoute.homeScreen); + + WidgetsBinding.instance.addPostFrameCallback((_) { + setState(() { + _imagePath = null; + }); + }); + }, + child: SafeArea( + top: false, + bottom: true, + right: false, + left: false, + child: Scaffold( + backgroundColor: Colors.grey[50], + resizeToAvoidBottomInset: false, + appBar: AppBar( + backgroundColor: Colors.white, + surfaceTintColor: Colors.white, + elevation: 0, + toolbarHeight: 10.h, + + titleSpacing: 0, + automaticallyImplyLeading: false, + title: Padding( + padding: EdgeInsets.symmetric(horizontal: 4.w), + child: Row( + children: [ + IconButton( + onPressed: () async { + await _cleanUpImages(); + + context.goNamed(AppRoute.homeScreen); + + WidgetsBinding.instance.addPostFrameCallback((_) { + setState(() { + _imagePath = null; + }); + }); + }, + icon: Icon( + Icons.close_rounded, + color: AppColor.primaryColor, + size: 7.w, + ), + padding: EdgeInsets.zero, + constraints: const BoxConstraints(), + ), + + SizedBox(width: 3.w), + + Expanded( + child: Container( + height: isTablet && isLandscape ? 12.h : 6.h, + padding: isTablet + ? EdgeInsets.symmetric( + vertical: isLandscape ? 2.h : 1.h, + horizontal: 2.w, + ) + : EdgeInsets.zero, + decoration: BoxDecoration( + color: greyColor, + borderRadius: BorderRadius.circular(2.5.w), + ), + child: TextField( + focusNode: _searchFocusNode, + onChanged: (value) => _onSearchChanged(value), + textAlignVertical: TextAlignVertical.center, + style: TextStyle( + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + ), + decoration: InputDecoration( + hintText: "Cari produk...", + hintStyle: TextStyle( + color: _searchIconColor, + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + ), + prefixIcon: Icon( + Icons.search, + color: _searchIconColor, + size: 5.w, + ), + border: InputBorder.none, + contentPadding: EdgeInsets.zero, + isDense: true, + ), + ), + ), + ), + + SizedBox(width: 3.w), + + /** + if (isTablet) ...[ + IconButton( + tooltip: isLandscape + ? "Ubah ke Potrait" + : "Ubah ke Landscape", + icon: Icon( + isLandscape + ? Icons.phone_iphone_rounded + : Icons.tablet_mac_rounded, + size: 5.w, + color: AppColor.primaryColor, + ), + onPressed: () { + if (isLandscape) { + SystemChrome.setPreferredOrientations([ + DeviceOrientation.portraitUp, + ]); + } else { + SystemChrome.setPreferredOrientations([ + DeviceOrientation.landscapeLeft, + DeviceOrientation.landscapeRight, + ]); + } + }, + ), + + SizedBox(width: 3.w), + ], + */ + + /** Notification + Stack( + children: [ + IconButton( + onPressed: () {}, + icon: Icon( + Icons.notifications_outlined, + color: AppColor.primaryColor, + size: 7.w, + ), + padding: EdgeInsets.zero, + ), + Positioned( + right: isTablet + ? 10 + : 100.w > 360 + ? 12.5 + : 15, + top: isTablet + ? 10 + : 100.w > 360 + ? 12.5 + : 15, + child: Container( + padding: EdgeInsets.all(0.5.w), + decoration: const BoxDecoration( + color: Colors.red, + shape: BoxShape.circle, + ), + constraints: BoxConstraints( + minWidth: 2.5.w, + minHeight: 2.5.w, + ), + ), + ), + ], + ), + */ + ], + ), + ), + ), + + body: productState.when( + data: (products) { + final cartItemsNotifier = ref.watch( + cartItemsControllerProvider.notifier, + ); + + final cartItemState = ref.watch(cartItemsControllerProvider); + + final cartItems = cartItemState.values.where((cartItem) { + return cartItem.currentOutletId == currentOutletId; + }).toList(); + + return Stack( + children: [ + !isLoggedIn + ? const CustomNotLogin() + : Padding( + padding: EdgeInsets.only( + left: 4.w, + right: 4.w, + top: 2.h, + bottom: 20.h, + ), + child: switch (productEmptyState) { + ProductEmpty.loading => const SizedBox(), + ProductEmpty.empty_database => + CustomEmptyScreen( + title: "Tidak Ada Produk", + body: "Kamu belum memiliki produk", + textButton: "Buat Produk Pertamamu", + onPressed: () => context.pushNamed( + AppRoute.addProductScreen, + ), + ), + ProductEmpty.empty_search_result => + const CustomEmptyScreen( + body: "Produk Tidak Ditemukan", + ), + ProductEmpty.has_data => GridView.builder( + itemCount: products.length, + gridDelegate: + SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 2, + childAspectRatio: 0.75, + crossAxisSpacing: 4.w, + mainAxisSpacing: 2.h, + ), + itemBuilder: (context, index) { + final product = products[index]; + + final bool isInCart = cartItemsNotifier + .isInCart(product.productVariantId); + + final double quantity = cartItemsNotifier + .quantityOf(product.productVariantId); + + return ProductCard( + product: product, + isInCart: isInCart, + quantity: quantity, + onPressed: () async => + await _handleIncrement(product), + onDecrementTap: () => + _handleDecrement(product), + onIncrementTap: () async => + await _handleIncrement(product), + ); + }, + ), + }, + ), + CartBottomSheet( + isLoggedIn: isLoggedIn, + sheetController: _sheetController, + formKey: _formKey, + currentUserName: currentUserName, + currentOutletName: currentOutletName, + currentUserRole: currentUserRole, + cartItems: cartItems, + shoppingAmount: _calculateShoppingAmount(cartItems), + productAmount: _calculateProductAmount(cartItems), + totalQty: _calculateTotalQty(cartItems), + discountController: _discountController, + taxController: _taxController, + noteController: _noteController, + nameController: _nameController, + phoneNumberController: _phoneNumberController, + addressController: _addressController, + validateChange: _validateChange, + totalBill: _calculateTotalBill(cartItems), + onViewOrderPressed: _expandSheet, + onOtherFormTap: () { + setState(() { + _isOtherFormVisible = !_isOtherFormVisible; + }); + }, + onCustomerFormTap: () { + setState(() { + _isCustomerFormVisible = !_isCustomerFormVisible; + }); + }, + isOtherFormVisible: _isOtherFormVisible, + isCustomerFormVisible: _isCustomerFormVisible, + selectedCustomer: selectedCustomer, + onTapCloseSelectedCustomer: selectedCustomer == null + ? null + : () { + ref + .read(selectedCustomerProvider.notifier) + .state = + null; + }, + payController: _payController, + calculateChange: (value) { + if (_debounce?.isActive ?? false) _debounce?.cancel(); + + _debounce = Timer( + const Duration(milliseconds: 400), + () { + final currentOutletId = ref.read( + currentOutletIdProvider, + ); + + final cartItemState = ref.read( + cartItemsControllerProvider, + ); + + final cartItems = cartItemState.values.where(( + cartItem, + ) { + return cartItem.currentOutletId == + currentOutletId; + }).toList(); + + final double payAmount = + double.tryParse(value) ?? 0.0; + + final double totalBill = _calculateTotalBill( + cartItems, + ); + + setState(() { + _validateChange = payAmount < totalBill; + _payController.text = value; + if (payAmount >= totalBill && + cartItems.isNotEmpty) { + _changeAmount = payAmount - totalBill; + } + }); + }, + ); + }, + changeAmount: _changeAmount, + imagePath: _imagePath, + onDecrementTap: (cartItem) { + final product = products.firstWhere( + (p) => p.productVariantId == cartItem.id, + ); + + _handleDecrement(product); + }, + onIncrementTap: (cartItem) async { + final product = products.firstWhere( + (p) => p.productVariantId == cartItem.id, + ); + + await _handleIncrement(product); + }, + onEditPressed: (cartItem) { + final String name = cartItem.name; + + final String variantName = + cartItem.variantName ?? '-'; + + final stockType = cartItem.stockType; + + final String remainingStock = cartItem.remainingStock + .toStringWithoutTrailingZero(); + final String unit = cartItem.unit; + + final String initialQuantity = cartItem.quantity + .toStringWithoutTrailingZero(); + + final String sellingPrice = + CurrencyFormat.formatToIdr( + cartItem.sellingPrice, + 0, + ); + + showDialog( + context: context, + builder: (_) => DecimalQuantity( + name: name, + variantName: variantName, + stockType: stockType, + remainingStock: remainingStock, + unit: unit, + initialQuantity: initialQuantity, + sellingPrice: sellingPrice, + onPressed: (value) { + try { + final double stock = double.parse( + remainingStock, + ); + + final double quantity = double.parse(value); + + if (stockType == StockType.fixed && + quantity > stock) { + CustomSnackbar.showError( + context, + "Stok tidak cukup", + ); + return; + } + + final cartItemsNotifier = ref.read( + cartItemsControllerProvider.notifier, + ); + + cartItemsNotifier.customQuantity( + cartItem.id, + quantity, + ); + + if (_payController.text.trim().isNotEmpty) { + _calculateChange(_payController.text); + } + } catch (e) { + CustomSnackbar.showError( + context, + "Stok tidak valid", + ); + } + }, + ), + ); + }, + onDeletePressed: _handleDeleteItem, + discountOnChanged: _discountOnChanged, + taxOnChanged: _taxOnChanged, + onTapImageAdd: !isLoggedIn + ? () => LoginRequired.showLoginRequired(context) + : _showPickerOptions, + onTapImageRemove: _removeImage, + onProccessPressed: + !isLoggedIn || posControllerState.isLoading + ? null + : _addSale, + ), + ], + ); + }, + error: (error, stackTrace) { + return CustomErrorScreen( + message: "Ups, terjadi kesalahan", + onRefresh: () {}, + ); + }, + loading: () => const PosShimmer(), + ), + ), + ), + ); + }, + ); + } +} diff --git a/Mobile Operasional/lib/features/pos/presentation/widgets/already_customer_shimmer.dart b/Mobile Operasional/lib/features/pos/presentation/widgets/already_customer_shimmer.dart new file mode 100644 index 0000000..192cb96 --- /dev/null +++ b/Mobile Operasional/lib/features/pos/presentation/widgets/already_customer_shimmer.dart @@ -0,0 +1,82 @@ +import 'package:flutter/material.dart'; +import 'package:shimmer/shimmer.dart'; +import 'package:sizer/sizer.dart'; + +class AlreadyCustomerShimmer extends StatelessWidget { + const AlreadyCustomerShimmer({super.key}); + + @override + Widget build(BuildContext context) { + final baseColor = Colors.grey[300]!; + final highlightColor = Colors.grey[100]!; + + return ListView.builder( + padding: EdgeInsets.fromLTRB(5.w, 0, 5.w, 12.h), + itemCount: 6, + itemBuilder: (context, index) { + return Container( + margin: EdgeInsets.only(bottom: 2.h), + padding: EdgeInsets.all(3.w), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(4.w), + border: Border.all(color: Colors.grey.shade200), + ), + child: Shimmer.fromColors( + baseColor: baseColor, + highlightColor: highlightColor, + child: Row( + children: [ + Container( + width: 12.w, + height: 12.w, + decoration: const BoxDecoration( + color: Colors.white, + shape: BoxShape.circle, + ), + ), + + SizedBox(width: 4.w), + + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + width: 40.w, + height: 1.5.h, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(1.w), + ), + ), + SizedBox(height: 1.h), + + Container( + width: 25.w, + height: 1.2.h, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(1.w), + ), + ), + ], + ), + ), + + Container( + width: 5.w, + height: 5.w, + decoration: const BoxDecoration( + color: Colors.white, + shape: BoxShape.circle, + ), + ), + ], + ), + ), + ); + }, + ); + } +} \ No newline at end of file diff --git a/Mobile Operasional/lib/features/pos/presentation/widgets/bill_summary.dart b/Mobile Operasional/lib/features/pos/presentation/widgets/bill_summary.dart new file mode 100644 index 0000000..04c741d --- /dev/null +++ b/Mobile Operasional/lib/features/pos/presentation/widgets/bill_summary.dart @@ -0,0 +1,42 @@ +import 'package:flutter/material.dart'; +import 'package:niogu_app/core/constants/app_color.dart'; +import 'package:niogu_app/core/constants/app_font_size.dart'; +import 'package:sizer/sizer.dart'; + +class BillSummary extends StatelessWidget { + final String label; + final String value; + + const BillSummary({super.key, required this.label, required this.value}); + + @override + Widget build(BuildContext context) { + return Container( + padding: EdgeInsets.all(3.w), + decoration: BoxDecoration( + color: AppColor.primaryColor.withOpacity(0.05), + borderRadius: BorderRadius.circular(2.w), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + label, + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: AppFontSize.medium.sp, + ), + ), + Text( + value, + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: AppFontSize.medium.sp, + color: AppColor.primaryColor, + ), + ), + ], + ), + ); + } +} diff --git a/Mobile Operasional/lib/features/pos/presentation/widgets/cart_bottom_sheet.dart b/Mobile Operasional/lib/features/pos/presentation/widgets/cart_bottom_sheet.dart new file mode 100644 index 0000000..1a4a3e6 --- /dev/null +++ b/Mobile Operasional/lib/features/pos/presentation/widgets/cart_bottom_sheet.dart @@ -0,0 +1,1105 @@ +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'package:niogu_app/core/constants/app_color.dart'; +import 'package:niogu_app/core/constants/app_font_size.dart'; +import 'package:niogu_app/core/router/app_route.dart'; +import 'package:niogu_app/core/utils/currency_format.dart'; +import 'package:niogu_app/core/utils/extension_format.dart'; +import 'package:niogu_app/core/utils/login_required.dart'; +import 'package:niogu_app/core/enums/user_role.dart'; +import 'package:niogu_app/core/widgets/custom_form_input.dart'; +import 'package:niogu_app/core/widgets/custom_proof_image_uploader.dart'; +import 'package:niogu_app/core/widgets/custom_text_form_field.dart'; +import 'package:niogu_app/features/pos/domain/entities/pos.dart'; +import 'package:niogu_app/features/pos/presentation/widgets/bill_summary.dart'; +import 'package:niogu_app/features/pos/presentation/widgets/cart_item_tile.dart'; +import 'package:niogu_app/features/pos/presentation/widgets/information_header.dart'; +import 'package:sizer/sizer.dart'; + +class CartBottomSheet extends StatelessWidget { + final bool isLoggedIn; + final DraggableScrollableController sheetController; + final GlobalKey formKey; + final String? currentUserName; + final String? currentOutletName; + final UserRole? currentUserRole; + final List cartItems; + final double shoppingAmount; + final int productAmount; + final double totalQty; + final TextEditingController discountController; + final TextEditingController taxController; + final TextEditingController noteController; + final TextEditingController nameController; + final TextEditingController phoneNumberController; + final TextEditingController addressController; + final bool validateChange; + final double totalBill; + final VoidCallback onViewOrderPressed; + final VoidCallback onOtherFormTap; + final VoidCallback onCustomerFormTap; + final bool isOtherFormVisible; + final bool isCustomerFormVisible; + final SelectedCustomer? selectedCustomer; + final VoidCallback? onTapCloseSelectedCustomer; + final TextEditingController payController; + final void Function(String)? calculateChange; + final double changeAmount; + final String? imagePath; + final void Function(CartItems) onDecrementTap; + final void Function(CartItems) onIncrementTap; + final void Function(CartItems) onEditPressed; + final void Function(CartItems) onDeletePressed; + final void Function(String)? discountOnChanged; + final void Function(String)? taxOnChanged; + final VoidCallback onTapImageAdd; + final VoidCallback onTapImageRemove; + final VoidCallback? onProccessPressed; + + const CartBottomSheet({ + super.key, + required this.isLoggedIn, + required this.sheetController, + required this.formKey, + this.currentUserName, + this.currentOutletName, + this.currentUserRole, + required this.cartItems, + required this.shoppingAmount, + required this.productAmount, + required this.totalQty, + required this.discountController, + required this.taxController, + required this.noteController, + required this.nameController, + required this.phoneNumberController, + required this.addressController, + required this.validateChange, + required this.totalBill, + required this.onViewOrderPressed, + required this.onOtherFormTap, + required this.onCustomerFormTap, + required this.isOtherFormVisible, + required this.isCustomerFormVisible, + required this.selectedCustomer, + this.onTapCloseSelectedCustomer, + required this.payController, + required this.calculateChange, + required this.changeAmount, + required this.imagePath, + required this.onDecrementTap, + required this.onIncrementTap, + required this.onEditPressed, + required this.onDeletePressed, + required this.discountOnChanged, + required this.taxOnChanged, + required this.onTapImageAdd, + required this.onTapImageRemove, + required this.onProccessPressed, + }); + + @override + Widget build(BuildContext context) { + final bool isTablet = 100.w >= 600; + + return DraggableScrollableSheet( + controller: sheetController, + initialChildSize: 0.24, + minChildSize: 0.24, + maxChildSize: 0.9, + builder: (context, scrollController) { + String? discountErrorText; + String? taxErrorText; + String? payErrorText; + + final double payAmount = + double.tryParse(payController.text.trim()) ?? 0.0; + + final bool isViewChangeAmount = + payAmount >= totalBill && cartItems.isNotEmpty; + + try { + if (discountController.text.isNotEmpty) { + final double discount = double.parse( + discountController.text.trim(), + ); + if (discount <= 0) { + discountErrorText = "Diskon harus lebih dari 0"; + } + } + } catch (e) { + discountErrorText = "Nominal diskon tidak valid"; + } + + try { + if (taxController.text.isNotEmpty) { + final double tax = double.parse(taxController.text.trim()); + if (tax <= 0) { + taxErrorText = "Pajak harus lebih dari 0"; + } + } + } catch (e) { + taxErrorText = "Nominal pajak tidak valid"; + } + + try { + if (payController.text.isNotEmpty && validateChange) { + final double payAmount = double.parse(payController.text.trim()); + if (payAmount <= 0) { + payErrorText = "Nominal pembayaran harus lebih dari 0"; + } + if (totalBill > payAmount) { + payErrorText = + "Kurang ${CurrencyFormat.formatToIdr((totalBill - payAmount), 0)}"; + } + } + } catch (e) { + payErrorText = "Nominal pembayaran tidak valid"; + } + + return Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.vertical(top: Radius.circular(5.w)), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.1), + blurRadius: 20, + offset: const Offset(0, -5), + ), + ], + ), + child: ClipRRect( + borderRadius: BorderRadiusGeometry.circular(2.5.w), + child: Material( + color: Colors.white, + type: MaterialType.canvas, + child: LayoutBuilder( + builder: (context, constraints) { + final List listChildren = [ + Text( + "Daftar Pesanan", + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + ), + ), + SizedBox(height: 2.h), + + if (cartItems.isEmpty) + Row( + children: [ + Icon( + Icons.add_box_outlined, + size: 5.w, + color: Colors.grey, + ), + SizedBox(width: 2.5.w), + Text( + "Keranjang Kosong, Tambah Produk", + style: TextStyle( + color: Colors.grey, + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + ), + ), + ], + ) + else + ...List.generate(cartItems.length, (index) { + return CartItemTile( + cartItem: cartItems[index], + onDecrementTap: onDecrementTap, + onIncrementTap: onIncrementTap, + onEditPressed: onEditPressed, + onDeletePressed: onDeletePressed, + ); + }), + Divider(height: 4.h), + + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + Expanded( + child: CustomTextFormField( + label: "Jumlah Produk", + controller: TextEditingController( + text: productAmount > 0 + ? productAmount.toString() + : '', + ), + readOnly: true, + ), + ), + SizedBox(width: 4.w), + Expanded( + child: CustomTextFormField( + label: "Total Kuantitas Produk", + controller: TextEditingController( + text: totalQty > 0 + ? totalQty.toStringWithoutTrailingZero() + : '', + ), + readOnly: true, + ), + ), + ], + ), + SizedBox(height: 3.h), + + CustomTextFormField( + label: "Subtotal", + controller: TextEditingController( + text: shoppingAmount > 0 + ? shoppingAmount.toStringWithoutTrailingZero() + : '', + ), + readOnly: true, + ), + + SizedBox(height: 3.h), + + Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(2.5.w), + border: Border.all(color: Colors.grey.shade300), + ), + child: Column( + children: [ + InkWell( + onTap: onOtherFormTap, + borderRadius: BorderRadius.vertical( + top: Radius.circular(2.5.w), + bottom: Radius.circular( + isOtherFormVisible ? 0 : 2.5.w, + ), + ), + child: Padding( + padding: EdgeInsets.symmetric( + horizontal: 4.w, + vertical: 1.5.h, + ), + child: Row( + children: [ + Container( + padding: EdgeInsets.all(1.5.w), + decoration: BoxDecoration( + color: isOtherFormVisible + ? AppColor.primaryColor.withOpacity( + 0.1, + ) + : Colors.grey[100], + shape: BoxShape.circle, + ), + child: Icon( + Icons.info_outlined, + color: isOtherFormVisible + ? AppColor.primaryColor + : Colors.grey, + size: 5.w, + ), + ), + SizedBox(width: 3.w), + Expanded( + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text( + "Informasi Lain", + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + color: Colors.black87, + ), + ), + SizedBox(height: 1.h), + Text( + "Opsional (Diskon, Pajak, Catatan)", + style: TextStyle( + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + color: Colors.grey, + ), + ), + ], + ), + ), + Icon( + isOtherFormVisible + ? Icons.keyboard_arrow_up_rounded + : Icons.keyboard_arrow_down_rounded, + color: Colors.grey, + size: 6.w, + ), + ], + ), + ), + ), + if (isOtherFormVisible) ...[ + Divider(height: 1, color: Colors.grey.shade200), + Padding( + padding: EdgeInsets.all(4.w), + child: Column( + children: [ + CustomFormInput( + label: "Diskon (Rp)", + icon: Icons.discount_outlined, + controller: discountController, + onChanged: discountOnChanged, + inputType: TextInputType.number, + validator: (value) { + if (value != null && value.isNotEmpty) { + try { + final double discount = double.parse( + value, + ); + if (discount <= 0) { + return "Diskon harus lebih dari 0"; + } + } catch (e) { + return "Nominal diskon tidak valid"; + } + } + return null; + }, + errorText: discountErrorText, + ), + SizedBox(height: 1.5.h), + CustomFormInput( + label: "Pajak (Rp)", + icon: Icons.money_outlined, + controller: taxController, + onChanged: taxOnChanged, + inputType: TextInputType.number, + validator: (value) { + if (value != null && value.isNotEmpty) { + try { + final double tax = double.parse( + value, + ); + if (tax <= 0) { + return "Pajak harus lebih dari 0"; + } + } catch (e) { + return "Nominal pajak tidak valid"; + } + } + return null; + }, + errorText: taxErrorText, + ), + SizedBox(height: 1.5.h), + CustomFormInput( + label: "Catatan", + icon: Icons.note_alt_outlined, + controller: noteController, + ), + ], + ), + ), + ], + ], + ), + ), + + SizedBox(height: 3.h), + + Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(2.5.w), + border: Border.all(color: Colors.grey.shade300), + ), + child: Column( + children: [ + InkWell( + onTap: onCustomerFormTap, + borderRadius: BorderRadius.vertical( + top: Radius.circular(2.5.w), + bottom: Radius.circular( + isCustomerFormVisible ? 0 : 2.5.w, + ), + ), + child: Padding( + padding: EdgeInsets.symmetric( + horizontal: 4.w, + vertical: 1.5.h, + ), + child: Row( + children: [ + Container( + padding: EdgeInsets.all(1.5.w), + decoration: BoxDecoration( + color: isCustomerFormVisible + ? AppColor.primaryColor.withOpacity( + 0.1, + ) + : Colors.grey[100], + shape: BoxShape.circle, + ), + child: Icon( + Icons.person_outline_rounded, + color: isCustomerFormVisible + ? AppColor.primaryColor + : Colors.grey, + size: 5.w, + ), + ), + SizedBox(width: 3.w), + Expanded( + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text( + "Informasi Pelanggan", + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + color: Colors.black87, + ), + ), + SizedBox(height: 1.h), + Text( + "Opsional (Nama, No. Hp / WA)", + style: TextStyle( + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + color: Colors.grey, + ), + ), + ], + ), + ), + Icon( + isCustomerFormVisible + ? Icons.keyboard_arrow_up_rounded + : Icons.keyboard_arrow_down_rounded, + color: Colors.grey, + size: 6.w, + ), + ], + ), + ), + ), + if (isCustomerFormVisible) ...[ + Divider(height: 1, color: Colors.grey.shade200), + Padding( + padding: EdgeInsets.all(4.w), + child: selectedCustomer != null + ? Container( + width: double.infinity, + padding: EdgeInsets.all(4.w), + decoration: BoxDecoration( + color: AppColor.primaryColor + .withOpacity(0.05), + borderRadius: BorderRadius.circular( + 2.5.w, + ), + border: Border.all( + color: AppColor.primaryColor + .withOpacity(0.3), + ), + ), + child: Row( + children: [ + Container( + padding: EdgeInsets.all(2.w), + decoration: BoxDecoration( + color: Colors.white, + shape: BoxShape.circle, + border: Border.all( + color: AppColor.primaryColor + .withOpacity(0.2), + ), + ), + child: Icon( + Icons.person, + color: AppColor.primaryColor, + size: 5.w, + ), + ), + SizedBox(width: 3.w), + Expanded( + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text( + selectedCustomer!.name, + style: TextStyle( + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + fontWeight: FontWeight.bold, + color: Colors.black87, + ), + ), + if (selectedCustomer! + .phoneNumber + .isNotEmpty) ...[ + SizedBox(height: 0.75.h), + Row( + children: [ + Icon( + Icons.phone_android, + size: 5.w, + color: Colors.grey[600], + ), + SizedBox(width: 1.w), + Text( + selectedCustomer! + .phoneNumber, + style: TextStyle( + fontSize: isTablet + ? AppFontSize + .medium + .sp + : AppFontSize + .small + .sp, + color: + Colors.grey[700], + ), + ), + ], + ), + ], + + if (selectedCustomer! + .address + .isNotEmpty) ...[ + SizedBox(height: 0.75.h), + Row( + crossAxisAlignment: + CrossAxisAlignment + .start, + children: [ + Icon( + Icons + .location_on_outlined, + size: 5.w, + color: Colors.grey[600], + ), + SizedBox(width: 1.w), + Expanded( + child: Text( + selectedCustomer! + .address, + style: TextStyle( + fontSize: isTablet + ? AppFontSize + .medium + .sp + : AppFontSize + .small + .sp, + color: Colors + .grey[700], + ), + ), + ), + ], + ), + ], + ], + ), + ), + InkWell( + onTap: onTapCloseSelectedCustomer, + child: Icon( + Icons.close, + color: Colors.grey, + size: 5.w, + ), + ), + ], + ), + ) + : Column( + children: [ + SizedBox( + width: double.infinity, + height: 5.5.h, + child: OutlinedButton.icon( + onPressed: () => !isLoggedIn + ? LoginRequired.showLoginRequired( + context, + ) + : context.pushNamed( + AppRoute + .alreadyCustomerScreen, + ), + style: OutlinedButton.styleFrom( + side: BorderSide( + color: AppColor.primaryColor, + ), + shape: RoundedRectangleBorder( + borderRadius: + BorderRadius.circular( + 2.5.w, + ), + ), + backgroundColor: Colors.blue[50], + ), + icon: Icon( + Icons.people_alt_rounded, + color: AppColor.primaryColor, + size: 5.w, + ), + label: Text( + "Pilih Pelanggan Tersedia", + style: TextStyle( + color: AppColor.primaryColor, + fontWeight: FontWeight.bold, + fontSize: AppFontSize.medium.sp, + ), + ), + ), + ), + SizedBox(height: 2.h), + Row( + children: [ + Expanded( + child: Divider( + color: Colors.grey.shade300, + ), + ), + Padding( + padding: EdgeInsets.symmetric( + horizontal: 3.w, + ), + child: Text( + "Atau Input Manual", + style: TextStyle( + fontSize: isTablet + ? (AppFontSize.medium - 2) + .sp + : (AppFontSize.small - 2) + .sp, + fontWeight: FontWeight.bold, + color: Colors.grey[400], + ), + ), + ), + Expanded( + child: Divider( + color: Colors.grey.shade300, + ), + ), + ], + ), + SizedBox(height: 2.h), + CustomFormInput( + label: "Nama Pelanggan", + icon: Icons.person, + controller: nameController, + ), + SizedBox(height: 1.5.h), + CustomFormInput( + label: "No. Hp / WA", + hint: "Opsional", + icon: Icons.phone_android, + controller: phoneNumberController, + inputType: TextInputType.phone, + ), + /** + SizedBox(height: 1.5.h), + CustomFormInput( + label: "Alamat", + hint: "Opsional", + icon: Icons.location_on_outlined, + controller: addressController, + ), + */ + ], + ), + ), + ], + ], + ), + ), + + SizedBox(height: 3.h), + + BillSummary( + label: "Total Tagihan", + value: totalBill > 0 + ? CurrencyFormat.formatToIdr(totalBill, 0) + : '', + ), + + SizedBox(height: 3.h), + + Text( + "Pembayaran", + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + ), + ), + SizedBox(height: 1.h), + + TextFormField( + controller: payController, + keyboardType: TextInputType.number, + style: TextStyle( + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + ), + autovalidateMode: AutovalidateMode.onUserInteraction, + validator: (value) { + try { + if (value == null || value.isEmpty) { + return "Masukkan nominal pembayaran"; + } + final double payAmount = double.parse(value); + if (payAmount <= 0) { + return "Nominal pembayaran harus lebih dari 0"; + } + } catch (e) { + return "Nominal pembayaran tidak valid"; + } + return null; + }, + onChanged: calculateChange, + decoration: InputDecoration( + labelText: "Bayar (Rp)", + labelStyle: TextStyle( + color: Colors.blue, + fontSize: AppFontSize.medium.sp, + ), + prefixIcon: Padding( + padding: isTablet + ? EdgeInsets.symmetric(horizontal: 3.w) + : EdgeInsets.zero, + child: Icon( + Icons.payments_outlined, + color: Colors.blue, + size: 5.w, + ), + ), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(2.5.w), + ), + errorText: payErrorText, + errorStyle: TextStyle( + color: Colors.red, + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + ), + errorBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(2.5.w), + borderSide: const BorderSide(color: Colors.redAccent), + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(2.5.w), + borderSide: const BorderSide(color: Colors.blue), + ), + contentPadding: EdgeInsets.symmetric( + vertical: isTablet ? 3.6.h : 1.8.h, + ), + ), + ), + + SizedBox(height: 2.h), + + TextField( + controller: TextEditingController( + text: isViewChangeAmount + ? CurrencyFormat.formatToIdr(changeAmount, 0) + : '', + ), + readOnly: true, + decoration: InputDecoration( + labelText: "Kembali", + labelStyle: TextStyle(fontSize: AppFontSize.medium.sp), + prefixIcon: Padding( + padding: isTablet + ? EdgeInsets.symmetric(horizontal: 3.w) + : EdgeInsets.zero, + child: Icon( + Icons.change_circle_outlined, + color: Colors.green, + size: 5.w, + ), + ), + filled: true, + fillColor: Colors.grey[100], + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(2.5.w), + borderSide: BorderSide.none, + ), + contentPadding: EdgeInsets.symmetric( + vertical: isTablet ? 3.6.h : 1.8.h, + ), + ), + style: TextStyle( + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + fontWeight: FontWeight.bold, + color: Colors.green[700], + ), + ), + + SizedBox(height: 3.h), + + CustomProofImageUploader( + imagePath: imagePath, + title: "Upload Bukti Pembayaran", + onTapImageAdd: onTapImageAdd, + onTapImageRemove: onTapImageRemove, + ), + + SizedBox(height: 3.h), + + SizedBox( + width: double.infinity, + height: 6.5.h, + child: ElevatedButton( + onPressed: onProccessPressed, + style: ElevatedButton.styleFrom( + backgroundColor: AppColor.primaryColor, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(2.5.w), + ), + disabledBackgroundColor: Colors.grey.shade300, + ), + child: Text( + "Proses Penjualan", + style: TextStyle( + color: Colors.white, + fontSize: AppFontSize.medium.sp, + fontWeight: FontWeight.bold, + ), + ), + ), + ), + + SizedBox( + height: MediaQuery.of(context).viewInsets.bottom + 2.h, + ), + ]; + + final bool isSquashed = constraints.maxHeight < 250; + + if (isSquashed) { + return SingleChildScrollView( + controller: scrollController, + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + InformationHeader( + + currentUserName: currentUserName, + currentOutletName: currentOutletName, + currentUserRole: currentUserRole, + ), + Center( + child: Container( + margin: EdgeInsets.only(top: 1.5.h), + width: 15.w, + height: 0.6.h, + decoration: BoxDecoration( + color: Colors.grey[300], + borderRadius: BorderRadius.circular(2.5.w), + ), + ), + ), + Padding( + padding: EdgeInsets.symmetric( + horizontal: 5.w, + vertical: 1.h, + ), + child: Row( + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Total Belanja", + style: TextStyle( + color: Colors.grey, + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + ), + ), + Text( + CurrencyFormat.formatToIdr( + shoppingAmount, + 0, + ), + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: AppFontSize.medium.sp, + color: Colors.black, + ), + ), + ], + ), + const Spacer(), + ElevatedButton.icon( + onPressed: onViewOrderPressed, + style: ElevatedButton.styleFrom( + backgroundColor: AppColor.primaryColor, + padding: EdgeInsets.symmetric( + horizontal: 6.w, + vertical: 1.h, + ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + 2.5.w, + ), + ), + ), + icon: Icon( + Icons.shopping_bag_outlined, + color: Colors.white, + size: 5.w, + ), + label: Text( + "Lihat Pesanan", + style: TextStyle( + color: Colors.white, + fontSize: AppFontSize.medium.sp, + fontWeight: FontWeight.bold, + ), + ), + ), + ], + ), + ), + const Divider(height: 1), + Form( + key: formKey, + child: Padding( + padding: EdgeInsets.fromLTRB(5.w, 2.h, 5.w, 5.h), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: listChildren, + ), + ), + ), + ], + ), + ); + } + + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + mainAxisSize: MainAxisSize.min, + children: [ + InformationHeader( + currentUserName: currentUserName, + currentOutletName: currentOutletName, + currentUserRole: currentUserRole, + ), + Center( + child: Container( + margin: EdgeInsets.only(top: 1.5.h), + width: 15.w, + height: 0.6.h, + decoration: BoxDecoration( + color: Colors.grey[300], + borderRadius: BorderRadius.circular(2.5.w), + ), + ), + ), + Padding( + padding: EdgeInsets.symmetric( + horizontal: 5.w, + vertical: 1.h, + ), + child: Row( + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Total Belanja", + style: TextStyle( + color: Colors.grey, + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + ), + ), + Text( + CurrencyFormat.formatToIdr(shoppingAmount, 0), + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: AppFontSize.medium.sp, + color: Colors.black, + ), + ), + ], + ), + const Spacer(), + ElevatedButton.icon( + onPressed: onViewOrderPressed, + style: ElevatedButton.styleFrom( + backgroundColor: AppColor.primaryColor, + padding: EdgeInsets.symmetric( + horizontal: 6.w, + vertical: 1.h, + ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(2.5.w), + ), + ), + icon: Icon( + Icons.shopping_bag_outlined, + color: Colors.white, + size: 5.w, + ), + label: Text( + "Lihat Pesanan", + style: TextStyle( + color: Colors.white, + fontSize: AppFontSize.medium.sp, + fontWeight: FontWeight.bold, + ), + ), + ), + ], + ), + ), + const Divider(height: 1), + Expanded( + child: Form( + key: formKey, + child: ListView( + controller: scrollController, + padding: EdgeInsets.fromLTRB(5.w, 2.h, 5.w, 5.h), + children: listChildren, + ), + ), + ), + ], + ); + }, + ), + ), + ), + ); + }, + ); + } +} diff --git a/Mobile Operasional/lib/features/pos/presentation/widgets/cart_item_tile.dart b/Mobile Operasional/lib/features/pos/presentation/widgets/cart_item_tile.dart new file mode 100644 index 0000000..a071392 --- /dev/null +++ b/Mobile Operasional/lib/features/pos/presentation/widgets/cart_item_tile.dart @@ -0,0 +1,205 @@ +import 'package:flutter/material.dart'; +import 'package:niogu_app/core/constants/app_color.dart'; +import 'package:niogu_app/core/constants/app_font_size.dart'; +import 'package:niogu_app/core/utils/currency_format.dart'; +import 'package:niogu_app/core/utils/extension_format.dart'; +import 'package:niogu_app/core/enums/stock_type.dart'; +import 'package:niogu_app/features/pos/domain/entities/pos.dart'; +import 'package:sizer/sizer.dart'; + +class CartItemTile extends StatelessWidget { + final CartItems cartItem; + final void Function(CartItems) onDecrementTap; + final void Function(CartItems) onIncrementTap; + final void Function(CartItems) onEditPressed; + final void Function(CartItems) onDeletePressed; + + const CartItemTile({ + super.key, + required this.cartItem, + required this.onDecrementTap, + required this.onIncrementTap, + required this.onEditPressed, + required this.onDeletePressed, + }); + + @override + Widget build(BuildContext context) { + final bool isTablet = 100.w >= 600; + + return Padding( + padding: EdgeInsets.only(bottom: 2.h), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + width: 10.w, + height: 10.w, + padding: EdgeInsets.symmetric(horizontal: 2.w, vertical: 0.5.h), + decoration: BoxDecoration( + color: Colors.grey[200], + borderRadius: BorderRadius.circular(1.w), + ), + child: Center( + child: Text( + "${cartItem.quantity.toStringWithoutTrailingZero()}", + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + ), + ), + ), + ), + SizedBox(width: 3.w), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + cartItem.name, + style: TextStyle( + overflow: TextOverflow.ellipsis, + fontWeight: FontWeight.w600, + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + height: 1.2, + ), + ), + + if (cartItem.variantName != null && + cartItem.variantName!.isNotEmpty) ...[ + SizedBox(height: 0.5.h), + Text( + cartItem.variantName!, + style: TextStyle( + overflow: TextOverflow.ellipsis, + color: Colors.grey, + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + height: 1.2, + ), + ), + ], + + SizedBox(height: 0.5.h), + + Text( + cartItem.stockType == StockType.unlimited + ? "Stok : Selalu Ada" + : "Sisa Stok : ${cartItem.remainingStock.toStringWithoutTrailingZero()} ${cartItem.unit}", + style: TextStyle( + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + ), + ), + Text( + CurrencyFormat.formatToIdr(cartItem.sellingPrice, 0), + style: TextStyle( + color: AppColor.primaryColor, + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + ), + Container( + decoration: BoxDecoration( + color: Colors.grey[50], + borderRadius: BorderRadius.circular(2.w), + border: Border.all(color: Colors.grey.shade200), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + InkWell( + onTap: cartItem.quantity > 1 + ? () => onDecrementTap(cartItem) + : null, + child: Padding( + padding: EdgeInsets.all(1.5.w), + child: Icon( + Icons.remove, + size: 4.w, + color: AppColor.primaryColor, + ), + ), + ), + + Container( + margin: EdgeInsets.only(top: 0.5.h), + width: 10.w, + padding: EdgeInsets.symmetric(horizontal: 2.w), + child: Center( + child: Text( + "${cartItem.quantity.toStringWithoutTrailingZero()}", + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + ), + ), + ), + ), + + InkWell( + onTap: () => onIncrementTap(cartItem), + child: Padding( + padding: EdgeInsets.all(1.5.w), + child: Icon( + Icons.add, + size: 4.w, + color: AppColor.primaryColor, + ), + ), + ), + ], + ), + ), + + SizedBox(width: 1.w), + + Container( + margin: EdgeInsets.only(top: 0.5.h), + child: IconButton( + onPressed: () => onEditPressed(cartItem), + icon: Icon( + Icons.edit_note_rounded, + color: Colors.grey, + size: 6.w, + ), + padding: EdgeInsets.zero, + constraints: const BoxConstraints(), + style: IconButton.styleFrom( + tapTargetSize: MaterialTapTargetSize.shrinkWrap, + ), + ), + ), + + SizedBox(width: 1.w), + + Container( + margin: EdgeInsets.only(top: 0.5.h), + child: IconButton( + onPressed: () => onDeletePressed(cartItem), + icon: Icon(Icons.delete_outline, color: Colors.red, size: 6.w), + padding: EdgeInsets.zero, + constraints: const BoxConstraints(), + style: IconButton.styleFrom( + tapTargetSize: MaterialTapTargetSize.shrinkWrap, + ), + ), + ), + ], + ), + ); + } +} diff --git a/Mobile Operasional/lib/features/pos/presentation/widgets/checkbox_customer.dart b/Mobile Operasional/lib/features/pos/presentation/widgets/checkbox_customer.dart new file mode 100644 index 0000000..cf4d379 --- /dev/null +++ b/Mobile Operasional/lib/features/pos/presentation/widgets/checkbox_customer.dart @@ -0,0 +1,29 @@ +import 'package:flutter/material.dart'; +import 'package:niogu_app/core/constants/app_color.dart'; +import 'package:sizer/sizer.dart'; + +class CheckboxCustomer extends StatelessWidget { + final bool isSelected; + + const CheckboxCustomer({super.key, required this.isSelected}); + + @override + Widget build(BuildContext context) { + return AnimatedContainer( + duration: const Duration(milliseconds: 200), + width: 6.w, + height: 6.w, + decoration: BoxDecoration( + color: isSelected ? AppColor.primaryColor : Colors.transparent, + shape: BoxShape.circle, + border: Border.all( + color: isSelected ? AppColor.primaryColor : Colors.grey.shade400, + width: 2, + ), + ), + child: isSelected + ? Icon(Icons.check, color: Colors.white, size: 4.w) + : null, + ); + } +} \ No newline at end of file diff --git a/Mobile Operasional/lib/features/pos/presentation/widgets/counter_button.dart b/Mobile Operasional/lib/features/pos/presentation/widgets/counter_button.dart new file mode 100644 index 0000000..dc86b95 --- /dev/null +++ b/Mobile Operasional/lib/features/pos/presentation/widgets/counter_button.dart @@ -0,0 +1,24 @@ +import 'package:flutter/material.dart'; +import 'package:niogu_app/core/constants/app_color.dart'; +import 'package:sizer/sizer.dart'; + +class CounterButton extends StatelessWidget { + final VoidCallback onTap; + final IconData icon; + const CounterButton({super.key, required this.onTap, required this.icon}); + + @override + Widget build(BuildContext context) { + return InkWell( + onTap: onTap, + child: Container( + padding: EdgeInsets.all(1.w), + decoration: BoxDecoration( + color: AppColor.primaryColor.withOpacity(0.1), + borderRadius: BorderRadius.circular(6), + ), + child: Icon(icon, size: 4.w, color: AppColor.primaryColor), + ), + ); + } +} \ No newline at end of file diff --git a/Mobile Operasional/lib/features/pos/presentation/widgets/decimal_quantity.dart b/Mobile Operasional/lib/features/pos/presentation/widgets/decimal_quantity.dart new file mode 100644 index 0000000..9289a36 --- /dev/null +++ b/Mobile Operasional/lib/features/pos/presentation/widgets/decimal_quantity.dart @@ -0,0 +1,201 @@ +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'package:niogu_app/core/constants/app_font_size.dart'; +import 'package:niogu_app/core/enums/stock_type.dart'; +import 'package:niogu_app/core/widgets/custom_button.dart'; +import 'package:niogu_app/core/widgets/custom_text_form_field.dart'; +import 'package:sizer/sizer.dart'; + +class DecimalQuantity extends StatefulWidget { + final String name; + final String variantName; + final StockType stockType; + final String remainingStock; + final String unit; + final String initialQuantity; + final String sellingPrice; + final Function(String) onPressed; + + const DecimalQuantity({ + super.key, + required this.name, + required this.variantName, + required this.stockType, + required this.remainingStock, + required this.unit, + required this.initialQuantity, + required this.sellingPrice, + required this.onPressed, + }); + + @override + State createState() => _DecimalQuantityState(); +} + +class _DecimalQuantityState extends State { + late GlobalKey _formKey; + late TextEditingController _controller; + + @override + void initState() { + super.initState(); + _formKey = GlobalKey(); + _controller = TextEditingController(text: widget.initialQuantity); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final bool isTablet = 100.w >= 600; + + final double maxWidth = 100.w >= 1280 + ? 1200 + : isTablet + ? 800 + : 400; + + return Dialog( + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(4.w)), + backgroundColor: Colors.white, + child: Container( + padding: EdgeInsets.all(5.w), + constraints: BoxConstraints(maxHeight: 85.h, maxWidth: maxWidth), + child: Form( + key: _formKey, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + "Masukkan Kuantitas", + style: TextStyle( + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + fontWeight: FontWeight.bold, + color: Colors.black87, + ), + ), + SizedBox(height: 2.h), + + Flexible( + child: SingleChildScrollView( + physics: const ClampingScrollPhysics(), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + CustomTextFormField( + label: "Nama Produk", + controller: TextEditingController(text: widget.name), + readOnly: true, + ), + + SizedBox(height: 1.5.h), + + CustomTextFormField( + label: "Varian Produk", + controller: TextEditingController( + text: widget.variantName, + ), + readOnly: true, + ), + + SizedBox(height: 1.5.h), + + Row( + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + CustomTextFormField( + label: "Sisa Stok", + controller: TextEditingController( + text: widget.stockType == StockType.fixed + ? "${widget.remainingStock} ${widget.unit}" + : "Selalu ada", + ), + readOnly: true, + ), + ], + ), + ), + SizedBox(width: 3.w), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + CustomTextFormField( + label: "Harga Jual", + controller: TextEditingController( + text: widget.sellingPrice, + ), + readOnly: true, + ), + ], + ), + ), + ], + ), + + SizedBox(height: 2.h), + + CustomTextFormField( + label: "Kuantitas Desimal", + hint: "Gunakan titik (.), misal 0.5", + keyboardType: const TextInputType.numberWithOptions( + decimal: true, + ), + autoFocus: true, + controller: _controller, + validator: (value) { + if (value == null || value.isEmpty) + return "Kuantitas belum diisi"; + if (double.tryParse(value) == null) + return "Kuantitas tidak valid"; + if (double.parse(value) <= 0) + return "Kuantitas harus lebih dari 0"; + + final double stock = double.parse( + widget.remainingStock, + ); + + final double quantity = double.parse(value.trim()); + + if (widget.stockType == StockType.fixed && + quantity > stock) + return "Stok tidak cukup"; + return null; + }, + ), + + SizedBox(height: 1.h), + ], + ), + ), + ), + + SizedBox(height: 2.h), + + CustomButton( + title: "Ubah", + onPressed: () { + if (!_formKey.currentState!.validate()) return; + + widget.onPressed(_controller.text.trim()); + + context.pop(); + }, + ), + SizedBox(height: 1.h), + ], + ), + ), + ), + ); + } +} diff --git a/Mobile Operasional/lib/features/pos/presentation/widgets/information_header.dart b/Mobile Operasional/lib/features/pos/presentation/widgets/information_header.dart new file mode 100644 index 0000000..fd009a0 --- /dev/null +++ b/Mobile Operasional/lib/features/pos/presentation/widgets/information_header.dart @@ -0,0 +1,135 @@ +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; +import 'package:niogu_app/core/constants/app_color.dart'; +import 'package:niogu_app/core/constants/app_font_size.dart'; +import 'package:niogu_app/core/components/modal_outlet_bottom.dart'; +import 'package:niogu_app/core/utils/time_zone.dart'; +import 'package:niogu_app/core/enums/user_role.dart'; +import 'package:sizer/sizer.dart'; + +class InformationHeader extends StatelessWidget { + final String? currentUserName; + final String? currentOutletName; + final UserRole? currentUserRole; + + const InformationHeader({ + super.key, + this.currentUserName, + this.currentOutletName, + this.currentUserRole, + }); + + @override + Widget build(BuildContext context) { + return Container( + padding: EdgeInsets.symmetric(horizontal: 4.w, vertical: 1.5.h), + decoration: BoxDecoration( + color: AppColor.primaryColor.withOpacity(0.05), + border: Border(bottom: BorderSide(color: Colors.grey.shade200)), + ), + child: Row( + children: [ + Container( + padding: EdgeInsets.all(2.w), + decoration: BoxDecoration( + color: Colors.white, + shape: BoxShape.circle, + border: Border.all(color: Colors.grey.shade200), + ), + child: Icon( + Icons.store_mall_directory_rounded, + color: AppColor.primaryColor, + size: 5.w, + ), + ), + SizedBox(width: 3.w), + + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + InkWell( + onTap: currentUserRole == UserRole.admin + ? null + : () { + showModalBottomSheet( + context: context, + useRootNavigator: true, + isScrollControlled: true, + constraints: const BoxConstraints( + maxWidth: double.infinity, + ), + builder: (context) => const ModalOutletBottom(), + ); + }, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + currentOutletName ?? "Tidak ada outlet", + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: AppFontSize.small.sp, + color: AppColor.primaryColor, + ), + ), + if (currentUserRole == UserRole.owner) + Icon( + Icons.keyboard_arrow_down_rounded, + color: AppColor.primaryColor, + size: 5.w, + ), + ], + ), + ), + + SizedBox(height: 0.2.h), + Row( + children: [ + Icon(Icons.person, color: Colors.grey, size: 3.w), + SizedBox(width: 1.w), + Text( + "Kasir: ${currentUserName ?? '-'}", + style: TextStyle( + fontSize: AppFontSize.small.sp, + color: Colors.grey[600], + fontWeight: FontWeight.w500, + ), + ), + ], + ), + ], + ), + ), + + Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Text( + DateFormat('dd MMMM yyyy').format(DateTime.now()), + style: TextStyle( + fontSize: AppFontSize.small.sp, + fontWeight: FontWeight.bold, + color: Colors.grey[700], + ), + ), + + StreamBuilder( + stream: Stream.periodic(const Duration(seconds: 1)), + builder: (context, snapshot) { + return Text( + "${DateFormat('HH:mm:ss').format(DateTime.now())} ${TimeZone.getCurrentTimeZone()}", + style: TextStyle( + fontSize: AppFontSize.small.sp, + color: Colors.grey[600], + ), + ); + }, + ), + ], + ), + ], + ), + ); + } +} diff --git a/Mobile Operasional/lib/features/pos/presentation/widgets/insufficient_stock_dialog.dart b/Mobile Operasional/lib/features/pos/presentation/widgets/insufficient_stock_dialog.dart new file mode 100644 index 0000000..c570da4 --- /dev/null +++ b/Mobile Operasional/lib/features/pos/presentation/widgets/insufficient_stock_dialog.dart @@ -0,0 +1,295 @@ +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'package:sizer/sizer.dart'; +import 'package:niogu_app/core/constants/app_color.dart'; +import 'package:niogu_app/core/constants/app_font_size.dart'; + +class InsufficientStockDialog extends StatelessWidget { + final String materialName; + final String currentStock; + final String totalNeeded; + final String missingAmount; + final String unit; + final String triggerProductName; + final int otherProductsCount; + + final VoidCallback onAddStockPressed; + final VoidCallback onEditUsagePressed; + + const InsufficientStockDialog({ + super.key, + required this.materialName, + required this.currentStock, + required this.totalNeeded, + required this.missingAmount, + required this.unit, + required this.triggerProductName, + this.otherProductsCount = 0, + required this.onAddStockPressed, + required this.onEditUsagePressed, + }); + + @override + Widget build(BuildContext context) { + const Color warningColor = Color(0xFFE65100); + final bool isTablet = 100.w >= 600; + final double maxWidth = 100.w >= 1280 + ? 1200 + : isTablet + ? 800 + : 400; + + return Dialog( + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(4.w)), + backgroundColor: Colors.white, + elevation: 0, + child: Container( + padding: EdgeInsets.all(5.w), + width: double.infinity, + constraints: BoxConstraints(maxWidth: maxWidth), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + padding: EdgeInsets.all(3.w), + decoration: BoxDecoration( + color: warningColor.withOpacity(0.1), + shape: BoxShape.circle, + ), + child: Icon( + Icons.warning_amber_rounded, + color: warningColor, + size: 8.w, + ), + ), + SizedBox(height: 2.h), + Text( + "Peringatan Stok Bahan Baku", + textAlign: TextAlign.center, + style: TextStyle( + fontSize: AppFontSize.medium.sp, + fontWeight: FontWeight.bold, + color: Colors.black87, + ), + ), + SizedBox(height: 1.h), + Text( + "Stok bahan baku tidak mencukupi untuk memproses pesanan ini.", + textAlign: TextAlign.center, + style: TextStyle( + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + color: Colors.grey[600], + ), + ), + + SizedBox(height: 3.h), + + Container( + padding: EdgeInsets.all(4.w), + decoration: BoxDecoration( + color: Colors.grey[50], + borderRadius: BorderRadius.circular(2.5.w), + border: Border.all(color: Colors.grey.shade200), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon( + Icons.inventory_2_outlined, + size: 4.w, + color: Colors.black54, + ), + SizedBox(width: 2.w), + Expanded( + child: Text( + materialName, + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + ), + ), + ), + ], + ), + Divider(height: 3.h, color: Colors.grey.shade300), + + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + _buildStatItem(isTablet, "Sisa", "$currentStock $unit"), + Container( + width: 1, + height: 4.h, + color: Colors.grey.shade300, + ), + _buildStatItem(isTablet, "Butuh", "$totalNeeded $unit"), + Container( + width: 1, + height: 4.h, + color: Colors.grey.shade300, + ), + _buildStatItem( + isTablet, + "Kurang", + "$missingAmount $unit", + isHighlight: true, // Merah + ), + ], + ), + ], + ), + ), + + SizedBox(height: 2.h), + + Container( + width: double.infinity, + padding: EdgeInsets.symmetric(horizontal: 3.w, vertical: 1.5.h), + decoration: BoxDecoration( + color: Colors.blue[50], + borderRadius: BorderRadius.circular(2.w), + border: Border.all(color: Colors.blue.withOpacity(0.2)), + ), + child: Row( + children: [ + Icon( + Icons.shopping_bag_outlined, + size: 4.5.w, + color: Colors.blue[700], + ), + SizedBox(width: 3.w), + Expanded( + child: RichText( + text: TextSpan( + style: TextStyle( + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + color: Colors.blue[900], + overflow: TextOverflow.ellipsis, + ), + children: [ + const TextSpan(text: "Digunakan oleh: "), + TextSpan( + text: triggerProductName, + style: const TextStyle(fontWeight: FontWeight.bold), + ), + if (otherProductsCount > 0) + TextSpan( + text: " +$otherProductsCount lainnya", + style: TextStyle( + color: Colors.blue[700], + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + ), + ), + ], + ), + ), + + SizedBox(height: 3.5.h), + + Column( + children: [ + SizedBox( + width: double.infinity, + height: 6.h, + child: ElevatedButton.icon( + onPressed: () { + context.pop(); + onAddStockPressed(); + }, + style: ElevatedButton.styleFrom( + backgroundColor: AppColor.primaryColor, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(2.5.w), + ), + elevation: 0, + ), + icon: Icon( + Icons.add_box_rounded, + color: Colors.white, + size: 5.w, + ), + label: Text( + "Tambah Bahan Baku", + style: TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + fontSize: AppFontSize.medium.sp, + ), + ), + ), + ), + + SizedBox(height: 1.5.h), + + SizedBox( + width: double.infinity, + height: 6.h, + child: OutlinedButton( + onPressed: () { + context.pop(); + onEditUsagePressed(); + }, + style: OutlinedButton.styleFrom( + side: BorderSide(color: AppColor.primaryColor), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(2.5.w), + ), + ), + child: Text( + "Ubah Jumlah Penggunaan", + style: TextStyle( + color: AppColor.primaryColor, + fontWeight: FontWeight.bold, + fontSize: AppFontSize.medium.sp, + ), + ), + ), + ), + ], + ), + ], + ), + ), + ); + } + + Widget _buildStatItem( + bool isTablet, + String label, + String value, { + bool isHighlight = false, + }) { + return Column( + children: [ + Text( + label, + style: TextStyle( + fontSize: isTablet ? AppFontSize.medium.sp : AppFontSize.small.sp, + color: Colors.grey[600], + ), + ), + SizedBox(height: 0.5.h), + Text( + value, + style: TextStyle( + fontSize: isTablet ? AppFontSize.medium.sp : AppFontSize.small.sp, + fontWeight: FontWeight.bold, + color: isHighlight ? Colors.red : Colors.black87, + ), + ), + ], + ); + } +} diff --git a/Mobile Operasional/lib/features/pos/presentation/widgets/pos_shimmer.dart b/Mobile Operasional/lib/features/pos/presentation/widgets/pos_shimmer.dart new file mode 100644 index 0000000..1e25304 --- /dev/null +++ b/Mobile Operasional/lib/features/pos/presentation/widgets/pos_shimmer.dart @@ -0,0 +1,80 @@ +import 'package:flutter/material.dart'; +import 'package:shimmer/shimmer.dart'; +import 'package:sizer/sizer.dart'; + +class PosShimmer extends StatelessWidget { + const PosShimmer({super.key}); + + @override + Widget build(BuildContext context) { + final bool isTablet = 100.w >= 600; + final int crossAxisCount = isTablet ? 4 : 2; + final double childAspectRatio = isTablet ? 0.75 : 0.65; + final baseColor = Colors.grey[300]!; + final highlightColor = Colors.grey[100]!; + + return Shimmer.fromColors( + baseColor: baseColor, + highlightColor: highlightColor, + child: GridView.builder( + padding: EdgeInsets.all(4.w), + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: crossAxisCount, + childAspectRatio: childAspectRatio, + crossAxisSpacing: 4.w, + mainAxisSpacing: 4.w, + ), + itemCount: 6, + itemBuilder: (context, index) { + return Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(2.5.w), + border: Border.all(color: Colors.white), + ), + padding: EdgeInsets.all(3.w), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + flex: 3, + child: Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(2.w), + ), + ), + ), + + SizedBox(height: 1.5.h), + + Container( + width: double.infinity, + height: 1.5.h, + color: Colors.white, + ), + SizedBox(height: 0.8.h), + + Container(width: 15.w, height: 1.2.h, color: Colors.white), + SizedBox(height: 0.8.h), + + Container(width: 20.w, height: 1.5.h, color: Colors.white), + + SizedBox(height: 1.5.h), + + Container( + width: double.infinity, + height: 4.h, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(2.w), + border: Border.all(color: Colors.white, width: 1.5), + ), + ), + ], + ), + ); + }, + ), + ); + } +} diff --git a/Mobile Operasional/lib/features/pos/presentation/widgets/product_card.dart b/Mobile Operasional/lib/features/pos/presentation/widgets/product_card.dart new file mode 100644 index 0000000..090e557 --- /dev/null +++ b/Mobile Operasional/lib/features/pos/presentation/widgets/product_card.dart @@ -0,0 +1,194 @@ +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:niogu_app/core/constants/app_color.dart'; +import 'package:niogu_app/core/constants/app_font_size.dart'; +import 'package:niogu_app/core/utils/currency_format.dart'; +import 'package:niogu_app/core/utils/extension_format.dart'; +import 'package:niogu_app/core/enums/stock_type.dart'; +import 'package:niogu_app/features/pos/domain/entities/pos.dart'; +import 'package:niogu_app/features/pos/presentation/widgets/counter_button.dart'; +import 'package:sizer/sizer.dart'; + +class ProductCard extends StatelessWidget { + final DisplayProductPos product; + final bool isInCart; + final double quantity; + final VoidCallback onPressed; + final VoidCallback onDecrementTap; + final VoidCallback onIncrementTap; + + const ProductCard({ + super.key, + required this.product, + required this.isInCart, + required this.quantity, + required this.onPressed, + required this.onDecrementTap, + required this.onIncrementTap, + }); + + @override + Widget build(BuildContext context) { + final bool isTablet = 100.w >= 600; + + final bool imagePathAvailable = + product.imagePath != null && + product.imagePath!.isNotEmpty && + product.imagePath!.isNotEmpty; + + final File imageFile = File( + imagePathAvailable ? product.imagePath! : "image not found", + ); + + final bool imageFileExists = imageFile.existsSync(); + + return Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(2.5.w), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.05), + blurRadius: 10, + offset: const Offset(0, 4), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: Container( + decoration: BoxDecoration( + color: imageFileExists ? Colors.transparent : Colors.grey[200], + borderRadius: BorderRadius.vertical( + top: Radius.circular(2.5.w), + ), + border: Border.all(color: Colors.grey.shade200, width: 1.5), + ), + child: imageFileExists + ? ClipRRect( + borderRadius: BorderRadius.circular(2.5.w), + child: Image.file( + imageFile, + fit: BoxFit.cover, + width: double.infinity, + height: double.infinity, + errorBuilder: (context, error, stackTrace) { + return Icon( + Icons.error, + size: 10.w, + color: Colors.grey, + ); + }, + ), + ) + : Center( + child: Icon(Icons.image, size: 10.w, color: Colors.grey), + ), + ), + ), + + Padding( + padding: EdgeInsets.all(3.w), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + product.name, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + fontWeight: FontWeight.bold, + ), + ), + + SizedBox(height: 0.5.h), + + Text( + product.hasVariant ? product.variantName : "", + style: TextStyle( + color: Colors.grey, + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + ), + ), + + SizedBox(height: 1.h), + + Text( + product.stockType == StockType.unlimited + ? "Stok : Selalu Ada" + : "Sisa Stok : ${product.remainingStock.toStringWithoutTrailingZero()} ${product.unitName}", + style: TextStyle( + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + ), + ), + SizedBox(height: 0.5.h), + + Text( + CurrencyFormat.formatToIdr(product.sellingPrice, 0), + style: TextStyle( + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + color: AppColor.primaryColor, + fontWeight: FontWeight.bold, + ), + ), + + SizedBox(height: 1.h), + + if (isInCart) + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + CounterButton(onTap: onDecrementTap, icon: Icons.remove), + Text( + "${quantity.toStringWithoutTrailingZero()}", + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small, + ), + ), + CounterButton(onTap: onIncrementTap, icon: Icons.add), + ], + ) + else + SizedBox( + width: double.infinity, + height: 4.h, + child: OutlinedButton( + onPressed: onPressed, + style: OutlinedButton.styleFrom( + side: BorderSide(color: AppColor.primaryColor), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(2.w), + ), + ), + child: Text( + "Tambah", + style: TextStyle( + fontSize: AppFontSize.medium.sp, + color: AppColor.primaryColor, + ), + ), + ), + ), + ], + ), + ), + ], + ), + ); + } +} diff --git a/Mobile Operasional/lib/features/profile/data/repositories/profile_repository_impl.dart b/Mobile Operasional/lib/features/profile/data/repositories/profile_repository_impl.dart new file mode 100644 index 0000000..a21d89d --- /dev/null +++ b/Mobile Operasional/lib/features/profile/data/repositories/profile_repository_impl.dart @@ -0,0 +1,60 @@ +import 'package:niogu_app/core/database/app_database.dart'; +import 'package:niogu_app/core/enums/member_level.dart'; +import 'package:niogu_app/features/profile/domain/entities/profile.dart'; +import 'package:niogu_app/features/profile/domain/repositories/i_profile_repository.dart'; + +class ProfileRepositoryImpl extends IProfileRepository { + final AppDatabase _appDatabase; + + ProfileRepositoryImpl(this._appDatabase); + + @override + Stream watchProfileCompleteness() { + return _appDatabase.userDao.watchProfileCompleteness(); + } + + @override + Stream watchBusinessCompleteness() { + return _appDatabase.tenantDao.watchBusinessCompleteness(); + } + + @override + Stream watchOutletCompleteness() { + return _appDatabase.outletDao.watchOutletCompleteness(); + } + + @override + Stream watchMemberLevel() { + return _appDatabase.tenantDao.watchMemberLevel(); + } + + @override + Future fetchProfileInfo() async { + return await _appDatabase.userDao.getProfileInfo(); + } + + @override + Future updateProfile(UpsertProfile profile) async { + return await _appDatabase.userDao.updateProfile(profile); + } + + @override + Future fetchBusinessInfo() async { + return await _appDatabase.tenantDao.getBusinessInfo(); + } + + @override + Future updateBusinessInfo(BusinessInfo business) async { + return await _appDatabase.tenantDao.updateBusinessInfo(business); + } + + @override + Future validatePassword(String inputPassword) async { + return await _appDatabase.userDao.validatePassword(inputPassword); + } + + @override + Future updatePassword(String newPassword) async { + return await _appDatabase.userDao.updatePassword(newPassword); + } +} diff --git a/Mobile Operasional/lib/features/profile/domain/entities/profile.dart b/Mobile Operasional/lib/features/profile/domain/entities/profile.dart new file mode 100644 index 0000000..6df5adf --- /dev/null +++ b/Mobile Operasional/lib/features/profile/domain/entities/profile.dart @@ -0,0 +1,63 @@ +class ProfileInfo { + final String name; + final String email; + final String phoneNumber; + final String? placeOfBirth; + final String? dateOfBirth; + + const ProfileInfo({ + required this.name, + required this.email, + required this.phoneNumber, + this.placeOfBirth, + this.dateOfBirth, + }); +} + +class UpsertProfile { + final String name; + final String? placeOfBirth; + final String? dateOfBirth; + + const UpsertProfile({ + required this.name, + this.placeOfBirth, + this.dateOfBirth + }); +} + +class BusinessInfo { + final String? logoPath; + final String businessCode; + final String businessName; + final String businessEmail; + final String businessPhoneNumber; + final String businessType; + + const BusinessInfo({ + this.logoPath, + required this.businessCode, + required this.businessName, + required this.businessEmail, + required this.businessPhoneNumber, + required this.businessType + }); +} + +class OutletInfo { + final String? imagePath; + final String tenantName; + final String name; + final String phoneNumber; + final String? email; + final String? fullAddress; + + const OutletInfo({ + this.imagePath, + required this.tenantName, + required this.name, + required this.phoneNumber, + this.email, + this.fullAddress, + }); +} diff --git a/Mobile Operasional/lib/features/profile/domain/repositories/i_profile_repository.dart b/Mobile Operasional/lib/features/profile/domain/repositories/i_profile_repository.dart new file mode 100644 index 0000000..e8b5eff --- /dev/null +++ b/Mobile Operasional/lib/features/profile/domain/repositories/i_profile_repository.dart @@ -0,0 +1,24 @@ +import 'package:niogu_app/core/enums/member_level.dart'; +import 'package:niogu_app/features/profile/domain/entities/profile.dart'; + +abstract class IProfileRepository { + Stream watchProfileCompleteness(); + + Stream watchBusinessCompleteness(); + + Stream watchOutletCompleteness(); + + Stream watchMemberLevel(); + + Future fetchProfileInfo(); + + Future updateProfile(UpsertProfile profile); + + Future fetchBusinessInfo(); + + Future updateBusinessInfo(BusinessInfo business); + + Future validatePassword(String inputPassword); + + Future updatePassword(String newPassword); +} \ No newline at end of file diff --git a/Mobile Operasional/lib/features/profile/presentation/controllers/profile_controller.dart b/Mobile Operasional/lib/features/profile/presentation/controllers/profile_controller.dart new file mode 100644 index 0000000..e255bc6 --- /dev/null +++ b/Mobile Operasional/lib/features/profile/presentation/controllers/profile_controller.dart @@ -0,0 +1,51 @@ +import 'dart:async'; + +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:niogu_app/features/profile/domain/entities/profile.dart'; +import 'package:niogu_app/features/profile/domain/repositories/i_profile_repository.dart'; +import 'package:niogu_app/features/profile/presentation/providers/profile_provider.dart'; + +class ProfileController extends AutoDisposeAsyncNotifier { + late final IProfileRepository _profileRepository; + + @override + FutureOr build() { + _profileRepository = ref.read(profileRepositoryProvider); + } + + Future updateProfile(UpsertProfile profile) async { + state = const AsyncValue.loading(); + + final result = await AsyncValue.guard(() async { + await _profileRepository.updateProfile(profile); + }); + + state = result; + + if (result is AsyncError) throw result.error!; + } + + Future updateBusinessInfo(BusinessInfo business) async { + state = const AsyncValue.loading(); + + final result = await AsyncValue.guard(() async { + await _profileRepository.updateBusinessInfo(business); + }); + + state = result; + + if (result is AsyncError) throw result.error!; + } + + Future updatePassword(String newPassword) async { + state = const AsyncValue.loading(); + + final result = await AsyncValue.guard(() async { + await _profileRepository.updatePassword(newPassword); + }); + + state = result; + + if (result is AsyncError) throw result.error!; + } +} \ No newline at end of file diff --git a/Mobile Operasional/lib/features/profile/presentation/providers/profile_provider.dart b/Mobile Operasional/lib/features/profile/presentation/providers/profile_provider.dart new file mode 100644 index 0000000..47f1730 --- /dev/null +++ b/Mobile Operasional/lib/features/profile/presentation/providers/profile_provider.dart @@ -0,0 +1,42 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:niogu_app/core/providers/app_provider.dart'; +import 'package:niogu_app/core/enums/member_level.dart'; +import 'package:niogu_app/features/profile/data/repositories/profile_repository_impl.dart'; +import 'package:niogu_app/features/profile/domain/repositories/i_profile_repository.dart'; +import 'package:niogu_app/features/profile/presentation/controllers/profile_controller.dart'; + +final profileRepositoryProvider = Provider.autoDispose(( + ref, +) { + final appDatabase = ref.watch(appDatabaseProvider); + return ProfileRepositoryImpl(appDatabase); +}); + +final profileCompletenessStreamProvider = StreamProvider.autoDispose(( + ref, +) { + final profileRepository = ref.watch(profileRepositoryProvider); + return profileRepository.watchProfileCompleteness(); +}); + +final businessCompletenessStreamProvider = StreamProvider.autoDispose(( + ref, +) { + final profileRepository = ref.watch(profileRepositoryProvider); + return profileRepository.watchBusinessCompleteness(); +}); + +final outletCompletenessStreamProvider = StreamProvider.autoDispose((ref) { + final profileRepository = ref.watch(profileRepositoryProvider); + return profileRepository.watchOutletCompleteness(); +}); + +final memberLevelStreamProvider = StreamProvider.autoDispose((ref) { + final profileRepository = ref.watch(profileRepositoryProvider); + return profileRepository.watchMemberLevel(); +}); + +final profileControllerProvider = + AsyncNotifierProvider.autoDispose( + ProfileController.new, + ); diff --git a/Mobile Operasional/lib/features/profile/presentation/screens/business_info_screen.dart b/Mobile Operasional/lib/features/profile/presentation/screens/business_info_screen.dart new file mode 100644 index 0000000..1bdc4cb --- /dev/null +++ b/Mobile Operasional/lib/features/profile/presentation/screens/business_info_screen.dart @@ -0,0 +1,444 @@ +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; +import 'package:image_picker/image_picker.dart'; +import 'package:niogu_app/core/components/top_back_bar_app.dart'; +import 'package:niogu_app/core/constants/app_color.dart'; +import 'package:niogu_app/core/constants/app_font_size.dart'; +import 'package:niogu_app/core/utils/image_service.dart'; +import 'package:niogu_app/core/utils/log_message.dart'; +import 'package:niogu_app/core/widgets/custom_snackbar.dart'; +import 'package:niogu_app/core/widgets/custom_text_form_field.dart'; +import 'package:niogu_app/features/profile/domain/entities/profile.dart'; +import 'package:niogu_app/features/profile/presentation/providers/profile_provider.dart'; +import 'package:sizer/sizer.dart'; + +class BusinessInfoScreen extends ConsumerStatefulWidget { + final BusinessInfo businessInfo; + const BusinessInfoScreen({super.key, required this.businessInfo}); + + @override + ConsumerState createState() => _BusinessInfoScreenState(); +} + +class _BusinessInfoScreenState extends ConsumerState { + final _formKey = GlobalKey(); + + late final String businessCode; + + final _nameController = TextEditingController(); + + final _emailController = TextEditingController(); + + final _phoneController = TextEditingController(); + + final _categoryController = TextEditingController(); + + final ImagePicker _picker = ImagePicker(); + + final List _logoPathTemps = []; + + String? _logoPath; + + @override + void initState() { + // TODO: implement initState + super.initState(); + + final businessInfo = widget.businessInfo; + + businessCode = businessInfo.businessCode; + + _logoPath = businessInfo.logoPath; + + _nameController.text = businessInfo.businessName; + + _emailController.text = businessInfo.businessEmail; + + _phoneController.text = businessInfo.businessPhoneNumber; + + _categoryController.text = businessInfo.businessType; + } + + @override + void dispose() { + // TODO: implement dispose + _nameController.dispose(); + _emailController.dispose(); + _phoneController.dispose(); + _categoryController.dispose(); + super.dispose(); + } + + Future _showPickerOptions() async { + final bool isTablet = 100.w >= 600; + showModalBottomSheet( + context: context, + backgroundColor: Colors.white, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.vertical(top: Radius.circular(2.5.w)), + ), + constraints: const BoxConstraints(maxWidth: double.infinity), + builder: (BuildContext context) { + return SafeArea( + child: Container( + width: 100.w, + padding: EdgeInsets.symmetric(vertical: 2.h, horizontal: 5.w), + clipBehavior: Clip.hardEdge, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.vertical(top: Radius.circular(6.w)), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + ListTile( + leading: Icon(Icons.photo_library, size: 6.w), + title: Text( + 'Galeri', + style: TextStyle( + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + ), + ), + onTap: () async { + await _getImage(ImageSource.gallery); + context.pop(); + }, + ), + + SizedBox(height: 2.h), + + ListTile( + leading: Icon(Icons.photo_camera, size: 6.w), + title: Text( + 'Kamera', + style: TextStyle( + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + ), + ), + onTap: () async { + await _getImage(ImageSource.camera); + context.pop(); + }, + ), + ], + ), + ), + ); + }, + ); + } + + Future _getImage(ImageSource ImageSource) async { + try { + final XFile? pickedFile = await _picker.pickImage( + source: ImageSource, + imageQuality: 70, + ); + + if (pickedFile != null) { + File tempFile = File(pickedFile.path); + + String? logoPath = await ImageService.saveImageToLocalDirectory( + tempFile, + "logo_images", + ); + + if (logoPath != null) { + _logoPathTemps.add(logoPath); + setState(() { + _logoPath = logoPath; + }); + } + } + } catch (e, st) { + LogMessage.log.e(e.toString(), error: e, stackTrace: st); + CustomSnackbar.showWarning(context, "Akses ditolak"); + } + } + + Future _cleanUpImages() async { + for (final image in _logoPathTemps) { + if (image != null) { + await ImageService.deleteLocalImage(image); + } + } + } + + Future _updateBusinessInfo() async { + if (!_formKey.currentState!.validate()) return; + + try { + await ref + .read(profileControllerProvider.notifier) + .updateBusinessInfo( + BusinessInfo( + logoPath: _logoPath, + businessCode: businessCode, + businessName: _nameController.text.trim(), + businessEmail: _emailController.text.trim(), + businessPhoneNumber: _phoneController.text.trim(), + businessType: _categoryController.text.trim(), + ), + ); + + if (!mounted) return; + + for (final path in _logoPathTemps) { + if (path != null && _logoPath != null && path != _logoPath) { + await ImageService.deleteLocalImage(path); + } + } + + CustomSnackbar.showSuccess(context, "Informasi usaha berhasil diubah"); + context.pop(); + } catch (e, st) { + LogMessage.log.e(e.toString(), error: e, stackTrace: st); + CustomSnackbar.showError(context, "Ups, terjadi kesalahan"); + } + } + + @override + Widget build(BuildContext context) { + return LayoutBuilder( + builder: (context, constraints) { + final bool isTablet = 100.w >= 600; + + final File imageFile = File(_logoPath ?? "image not found"); + + final bool imageFileExists = imageFile.existsSync(); + + final profileControllerState = ref.watch(profileControllerProvider); + + return PopScope( + canPop: false, + onPopInvokedWithResult: (didPop, result) async { + if (didPop) return; + + await _cleanUpImages(); + + context.pop(); + }, + child: SafeArea( + child: Scaffold( + backgroundColor: const Color(0xFFF9FAFB), + appBar: TopBackBarApp( + title: "Informasi Usaha", + onTap: () async { + await _cleanUpImages(); + + context.pop(); + }, + ), + body: SingleChildScrollView( + padding: EdgeInsets.all(5.w), + child: Form( + key: _formKey, + child: Column( + children: [ + Center( + child: Column( + children: [ + Stack( + clipBehavior: Clip.none, + children: [ + Container( + width: 25.w, + height: 25.w, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(4.w), + border: Border.all( + color: Colors.grey.shade200, + ), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.02), + blurRadius: 10, + ), + ], + ), + child: imageFileExists + ? ClipRRect( + borderRadius: BorderRadius.circular( + 2.5.w, + ), + child: Image.file( + imageFile, + fit: BoxFit.cover, + width: double.infinity, + height: double.infinity, + errorBuilder: + (context, error, stackTrace) { + return Icon( + Icons.error, + color: Colors.grey[400], + size: 10.w, + ); + }, + ), + ) + : IconButton( + onPressed: _showPickerOptions, + icon: Icon( + Icons.add_a_photo_outlined, + color: Colors.grey[400], + size: 8.w, + ), + ), + ), + + if (_logoPath != null) + Positioned( + top: 1.w, + right: 1.w, + child: Material( + color: Colors.transparent, + type: MaterialType.transparency, + child: InkWell( + onTap: () { + setState(() { + _logoPath = null; + }); + }, + child: Container( + padding: EdgeInsets.all(1.w), + decoration: BoxDecoration( + color: Colors.red[50], + shape: BoxShape.circle, + border: Border.all( + color: Colors.red.withOpacity( + 0.2, + ), + ), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity( + 0.1, + ), + blurRadius: 2, + offset: Offset(0, 1), + ), + ], + ), + child: Icon( + Icons.remove, + color: Colors.red, + size: 4.w, + ), + ), + ), + ), + ), + ], + ), + SizedBox(height: 1.5.h), + Text( + "Logo Usaha", + style: TextStyle( + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + color: Colors.grey, + ), + ), + ], + ), + ), + + SizedBox(height: 4.h), + + CustomTextFormField( + label: "ID Usaha", + controller: TextEditingController(text: businessCode), + readOnly: true, + prefixIcon: Icons.badge_outlined, + suffixIcon: Icons.copy_rounded, + suffixPressed: () { + Clipboard.setData(ClipboardData(text: businessCode)); + CustomSnackbar.showSuccess( + context, + "ID Usaha berhasil disalin", + ); + }, + ), + + SizedBox(height: 2.h), + + CustomTextFormField( + label: "Nama Usaha", + hint: "Masukkan nama bisnis Anda", + controller: _nameController, + prefixIcon: Icons.storefront_outlined, + validator: (value) { + if (value == null || value.isEmpty) { + return "Nama usaha belum diisi"; + } + + return null; + }, + ), + SizedBox(height: 2.h), + CustomTextFormField( + label: "Email Usaha", + hint: "bisnis@email.com", + controller: _emailController, + prefixIcon: Icons.email_outlined, + keyboardType: TextInputType.emailAddress, + ), + SizedBox(height: 2.h), + CustomTextFormField( + label: "No HP / WhatsApp Usaha", + hint: "0812xxxx", + controller: _phoneController, + prefixIcon: Icons.phone_android, + keyboardType: TextInputType.phone, + ), + SizedBox(height: 2.h), + CustomTextFormField( + label: "Kategori Usaha", + hint: "Contoh: F&B, Retail, Pertanian", + controller: _categoryController, + prefixIcon: Icons.category_outlined, + ), + + SizedBox(height: 5.h), + ElevatedButton( + onPressed: profileControllerState.isLoading + ? null + : _updateBusinessInfo, + style: ElevatedButton.styleFrom( + backgroundColor: AppColor.primaryColor, + minimumSize: Size(double.infinity, 7.h), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(3.w), + ), + disabledBackgroundColor: Colors.grey.shade300, + ), + child: Text( + "Simpan Perubahan", + style: TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + fontSize: AppFontSize.medium.sp, + ), + ), + ), + ], + ), + ), + ), + ), + ), + ); + }, + ); + } +} diff --git a/Mobile Operasional/lib/features/profile/presentation/screens/change_password_screen.dart b/Mobile Operasional/lib/features/profile/presentation/screens/change_password_screen.dart new file mode 100644 index 0000000..d8409a2 --- /dev/null +++ b/Mobile Operasional/lib/features/profile/presentation/screens/change_password_screen.dart @@ -0,0 +1,257 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; +import 'package:niogu_app/core/components/top_back_bar_app.dart'; +import 'package:niogu_app/core/constants/app_color.dart'; +import 'package:niogu_app/core/constants/app_font_size.dart'; +import 'package:niogu_app/core/utils/log_message.dart'; +import 'package:niogu_app/core/widgets/custom_snackbar.dart'; +import 'package:niogu_app/core/widgets/custom_text_form_field.dart'; +import 'package:niogu_app/features/profile/presentation/providers/profile_provider.dart'; +import 'package:sizer/sizer.dart'; + +class ChangePasswordScreen extends ConsumerStatefulWidget { + const ChangePasswordScreen({super.key}); + + @override + ConsumerState createState() => + _ChangePasswordScreenState(); +} + +class _ChangePasswordScreenState extends ConsumerState { + final _formKey = GlobalKey(); + + final _oldPasswordController = TextEditingController(); + + final _newPasswordController = TextEditingController(); + + final _newPasswordConfirmController = TextEditingController(); + + bool _obscureOld = true; + + bool _obscureNew = true; + + bool _obscureConfirm = true; + + @override + void initState() { + // TODO: implement initState + super.initState(); + } + + @override + void dispose() { + // TODO: implement dispose + _oldPasswordController.dispose(); + _newPasswordController.dispose(); + _newPasswordConfirmController.dispose(); + super.dispose(); + } + + String? _validation({ + String? value, + required String message, + bool isNewPassword = false, + bool isPasswordConfirm = false, + }) { + if (value == null || value.isEmpty) { + return message; + } + + if (isNewPassword) { + if (value.length < 8) { + return "Password minimal harus 8 karakter"; + } + + final passwordRegExp = RegExp( + r'^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&#])[A-Za-z\d@$!%*?&#]{8,}$', + ); + + if (!passwordRegExp.hasMatch(value)) { + return "Gunakan huruf besar, kecil, angka, dan simbol"; + } + } + + if (isPasswordConfirm && value != _newPasswordController.text.trim()) { + return "Konfirmasi password tidak sesuai"; + } + + return null; + } + + Future _updatePassword() async { + if (!_formKey.currentState!.validate()) return; + + final validatePassword = await ref + .read(profileRepositoryProvider) + .validatePassword(_oldPasswordController.text.trim()); + + if (!validatePassword) { + CustomSnackbar.showError(context, "Password lama tidak valid"); + return; + } + + try { + await ref + .read(profileControllerProvider.notifier) + .updatePassword(_newPasswordController.text.trim()); + + if (!mounted) return; + + CustomSnackbar.showSuccess(context, "Password berhasil diubah"); + context.pop(); + } catch (e, st) { + LogMessage.log.e(e.toString(), error: e, stackTrace: st); + CustomSnackbar.showError(context, 'Ups, terjadi kesalahan'); + } + } + + @override + Widget build(BuildContext context) { + final bool isTablet = 100.w >= 600; + + final profileControllerState = ref.watch(profileControllerProvider); + + return Scaffold( + backgroundColor: const Color(0xFFF9FAFB), + appBar: TopBackBarApp(title: "Ubah Password", onTap: () => context.pop()), + body: SingleChildScrollView( + padding: EdgeInsets.all(5.w), + child: Form( + key: _formKey, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + /** + Container( + padding: EdgeInsets.all(4.w), + decoration: BoxDecoration( + color: Colors.blue[50], + borderRadius: BorderRadius.circular(2.5.w), + border: Border.all(color: Colors.blue.shade100), + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Icon( + Icons.info_outline, + color: Colors.blue[700], + size: 5.w, + ), + SizedBox(width: 3.w), + Expanded( + child: Text( + "Demi keamanan akun, password hanya dapat diubah setiap 30 hari sekali. Gunakan password yang kuat dan mudah diingat", + style: TextStyle( + color: Colors.blue[900], + fontSize: isTablet + ? (AppFontSize.medium - 1.25).sp + : (AppFontSize.small - 1.25).sp, + ), + ), + ), + ], + ), + ), + + SizedBox(height: 3.h), + */ + CustomTextFormField( + label: "Password Lama", + hint: "••••••••", + controller: _oldPasswordController, + isObscure: _obscureOld, + suffixIcon: _obscureOld + ? Icons.visibility_off + : Icons.visibility, + suffixPressed: () => setState(() => _obscureOld = !_obscureOld), + validator: (value) => _validation( + value: value, + message: "Password lama belum diisi", + ), + ), + + SizedBox(height: 2.h), + + CustomTextFormField( + label: "Password Baru", + hint: "••••••••", + controller: _newPasswordController, + isObscure: _obscureNew, + suffixIcon: _obscureNew + ? Icons.visibility_off + : Icons.visibility, + suffixPressed: () => setState(() => _obscureNew = !_obscureNew), + validator: (value) => _validation( + value: value, + message: "Password baru belum diisi", + isNewPassword: true, + ), + ), + + SizedBox(height: 2.h), + + CustomTextFormField( + label: "Konfirmasi Password Baru", + hint: "••••••••", + controller: _newPasswordConfirmController, + isObscure: _obscureConfirm, + suffixIcon: _obscureConfirm + ? Icons.visibility_off + : Icons.visibility, + suffixPressed: () => + setState(() => _obscureConfirm = !_obscureConfirm), + validator: (value) => _validation( + value: value, + message: "Konfirmasi password belum diisi", + isPasswordConfirm: true, + ), + ), + + Align( + alignment: Alignment.centerRight, + child: TextButton( + onPressed: () {}, + child: Text( + "Lupa password?", + style: TextStyle( + color: AppColor.primaryColor, + fontWeight: FontWeight.bold, + fontSize: isTablet + ? (AppFontSize.medium - 1.25).sp + : (AppFontSize.small - 1.25).sp, + ), + ), + ), + ), + + SizedBox(height: 4.h), + + ElevatedButton( + onPressed: profileControllerState.isLoading + ? null + : _updatePassword, + style: ElevatedButton.styleFrom( + backgroundColor: AppColor.primaryColor, + minimumSize: Size(double.infinity, 7.h), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(3.w), + ), + disabledBackgroundColor: Colors.grey.shade300, + ), + child: Text( + "Perbarui Password", + style: TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + fontSize: AppFontSize.medium.sp, + ), + ), + ), + ], + ), + ), + ), + ); + } +} diff --git a/Mobile Operasional/lib/features/profile/presentation/screens/outlet_info_screen.dart b/Mobile Operasional/lib/features/profile/presentation/screens/outlet_info_screen.dart new file mode 100644 index 0000000..973e168 --- /dev/null +++ b/Mobile Operasional/lib/features/profile/presentation/screens/outlet_info_screen.dart @@ -0,0 +1,270 @@ +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; +import 'package:niogu_app/core/components/top_back_bar_app.dart'; +import 'package:niogu_app/core/constants/app_color.dart'; +import 'package:niogu_app/core/constants/app_font_size.dart'; +import 'package:niogu_app/core/providers/app_provider.dart'; +import 'package:niogu_app/core/router/app_route.dart'; +import 'package:niogu_app/core/utils/log_message.dart'; +import 'package:niogu_app/core/widgets/custom_snackbar.dart'; +import 'package:niogu_app/core/widgets/custom_text_form_field.dart'; +import 'package:niogu_app/features/outlets/domain/entities/outlet.dart'; +import 'package:niogu_app/features/outlets/presentation/providers/outlet_provider.dart'; +import 'package:niogu_app/features/profile/domain/entities/profile.dart'; +import 'package:sizer/sizer.dart'; + +class OutletInfoScreen extends ConsumerWidget { + final OutletInfo outletInfo; + const OutletInfoScreen({super.key, required this.outletInfo}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + return LayoutBuilder( + builder: (context, constraints) { + final bool isTablet = 100.w >= 600; + + final File imageFile = File(outletInfo.imagePath ?? "image not found"); + + final bool imageFileExists = imageFile.existsSync(); + + final outletControllerState = ref.watch(outletControllerProvider); + + final mapOutletAddressState = ref.watch(mapOutletAddressProvider); + + return SafeArea( + top: false, + bottom: true, + right: false, + left: false, + child: Scaffold( + backgroundColor: Colors.white, + appBar: TopBackBarApp( + title: "Informasi Outlet", + onTap: () => context.pop(), + ), + body: SingleChildScrollView( + padding: EdgeInsets.all(5.w), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: EdgeInsets.only(bottom: 1.5.h), + child: Text( + "Foto Outlet", + style: TextStyle( + fontSize: (AppFontSize.medium - 1.25).sp, + fontWeight: FontWeight.bold, + color: Colors.black87, + ), + ), + ), + + Container( + height: 20.h, + width: double.infinity, + decoration: BoxDecoration( + color: Colors.grey.shade100, + borderRadius: BorderRadius.circular(3.w), + border: Border.all( + color: Colors.grey.shade300, + style: BorderStyle.none, + ), + ), + child: imageFileExists + ? ClipRRect( + borderRadius: BorderRadius.circular(2.5.w), + child: Image.file( + imageFile, + fit: BoxFit.cover, + width: double.infinity, + height: double.infinity, + errorBuilder: (context, error, stackTrace) { + return Icon( + Icons.error, + color: Colors.grey[400], + size: 10.w, + ); + }, + ), + ) + : Icon( + Icons.store_outlined, + size: 10.w, + color: Colors.grey.shade700, + ), + ), + + SizedBox(height: 3.h), + + CustomTextFormField( + label: "Nama Usaha", + prefixIcon: Icons.store_mall_directory_outlined, + controller: TextEditingController( + text: outletInfo.tenantName, + ), + readOnly: true, + ), + SizedBox(height: 3.h), + + CustomTextFormField( + label: "Nama Outlet", + hint: '-', + prefixIcon: Icons.store_mall_directory_outlined, + controller: TextEditingController(text: outletInfo.name), + readOnly: true, + ), + SizedBox(height: 3.h), + + CustomTextFormField( + label: "No. Handphone / WA Outlet", + prefixIcon: Icons.phone_android_outlined, + controller: TextEditingController( + text: outletInfo.phoneNumber, + ), + keyboardType: TextInputType.phone, + readOnly: true, + ), + SizedBox(height: 3.h), + CustomTextFormField( + label: "Email", + hint: "-", + prefixIcon: Icons.email_outlined, + keyboardType: TextInputType.emailAddress, + controller: TextEditingController(text: outletInfo.email), + readOnly: true, + ), + SizedBox(height: 3.h), + + Material( + color: Colors.transparent, + type: MaterialType.transparency, + child: InkWell( + onTap: () { + context.pushNamed(AppRoute.mapOutletAdressScreen); + }, + child: Container( + width: double.infinity, + padding: EdgeInsets.symmetric( + horizontal: 4.w, + vertical: 2.h, + ), + decoration: BoxDecoration( + border: Border.all(color: Colors.grey.shade300), + borderRadius: BorderRadius.circular(2.05.w), + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Icon( + Icons.map_outlined, + color: Colors.black, + size: 5.w, + ), + + SizedBox(width: 2.5.w), + + Expanded( + child: Text( + mapOutletAddressState != null + ? mapOutletAddressState.fullAddress + : "Atur alamat outlet", + style: TextStyle( + fontWeight: mapOutletAddressState != null + ? FontWeight.normal + : FontWeight.bold, + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + height: 1.4, + ), + ), + ), + ], + ), + ), + ), + ), + + SizedBox(height: 3.h), + + ElevatedButton( + onPressed: outletControllerState.isLoading + ? null + : () async { + final selectedOutletAddress = ref.read( + mapOutletAddressProvider, + ); + + final currentOutletId = ref.read( + currentOutletIdProvider, + ); + + final upsertOutlet = UpsertOutlet( + id: currentOutletId, + bannerPath: outletInfo.imagePath, + name: outletInfo.name, + phoneNumber: outletInfo.phoneNumber, + email: outletInfo.email, + fullAddress: selectedOutletAddress?.fullAddress, + latitude: selectedOutletAddress + ?.currentLocation + .latitude, + longitude: selectedOutletAddress + ?.currentLocation + .longitude, + ); + + try { + await ref + .read(outletControllerProvider.notifier) + .saveOutlet(upsertOutlet); + + if (!context.mounted) return; + + CustomSnackbar.showSuccess( + context, + 'Outlet baru berhasil ditambahkan', + ); + context.pop(); + } catch (e, st) { + LogMessage.log.e( + e.toString(), + error: e, + stackTrace: st, + ); + CustomSnackbar.showError( + context, + 'Ups, terjadi kesalahan', + ); + } + }, + style: ElevatedButton.styleFrom( + backgroundColor: AppColor.primaryColor, + minimumSize: Size(double.infinity, 7.h), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(3.w), + ), + disabledBackgroundColor: Colors.grey.shade300, + ), + child: Text( + "Simpan", + style: TextStyle( + fontSize: AppFontSize.medium.sp, + fontWeight: FontWeight.bold, + color: Colors.white, + ), + ), + ), + SizedBox(height: 3.h), + ], + ), + ), + ), + ); + }, + ); + } +} diff --git a/Mobile Operasional/lib/features/profile/presentation/screens/profil_info_screen.dart b/Mobile Operasional/lib/features/profile/presentation/screens/profil_info_screen.dart new file mode 100644 index 0000000..7fde574 --- /dev/null +++ b/Mobile Operasional/lib/features/profile/presentation/screens/profil_info_screen.dart @@ -0,0 +1,254 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; +import 'package:niogu_app/core/components/top_back_bar_app.dart'; +import 'package:niogu_app/core/constants/app_color.dart'; +import 'package:niogu_app/core/constants/app_font_size.dart'; +import 'package:niogu_app/core/providers/app_provider.dart'; +import 'package:niogu_app/core/utils/date_input_formatter.dart'; +import 'package:niogu_app/core/utils/log_message.dart'; +import 'package:niogu_app/core/system/system_setting.dart'; +import 'package:niogu_app/core/widgets/custom_snackbar.dart'; +import 'package:niogu_app/core/widgets/custom_text_form_field.dart'; +import 'package:niogu_app/features/profile/domain/entities/profile.dart'; +import 'package:niogu_app/features/profile/presentation/providers/profile_provider.dart'; +import 'package:sizer/sizer.dart'; + +class ProfileInfoScreen extends ConsumerStatefulWidget { + final ProfileInfo profileInfo; + const ProfileInfoScreen({super.key, required this.profileInfo}); + + @override + ConsumerState createState() => _ProfileInfoScreenState(); +} + +class _ProfileInfoScreenState extends ConsumerState { + /** + String? _validateUsername(String? value) { + if (value == null || value.isEmpty) return "Username tidak boleh kosong"; + if (value.length < 5) return "Username minimal 5 karakter"; + + final RegExp usernameRegExp = RegExp( + r'^(?=[a-z0-9._]{5,20}$)(?!.*[_.]{2})[^_.].*[^_.]$', + ); + + if (!usernameRegExp.hasMatch(value)) { + return "Username hanya boleh huruf kecil, angka, _ dan ."; + } + return null; + } + */ + + final _formKey = GlobalKey(); + + late final String _email; + + late final String _phoneNumber; + + final TextEditingController _nameController = TextEditingController(); + + final TextEditingController _placeOfBirthController = TextEditingController(); + + final TextEditingController _dateOfBirthController = TextEditingController(); + + @override + void initState() { + // TODO: implement initState + super.initState(); + + final profileInfo = widget.profileInfo; + + _nameController.text = profileInfo.name; + + _email = profileInfo.email; + + _phoneNumber = profileInfo.phoneNumber; + + if (profileInfo.placeOfBirth != null && + profileInfo.placeOfBirth!.isNotEmpty) { + _placeOfBirthController.text = profileInfo.placeOfBirth!; + } + + if (profileInfo.dateOfBirth != null && + profileInfo.dateOfBirth!.isNotEmpty) { + _dateOfBirthController.text = profileInfo.dateOfBirth!; + } + } + + @override + void dispose() { + // TODO: implement dispose + _nameController.dispose(); + _placeOfBirthController.dispose(); + _dateOfBirthController.dispose(); + super.dispose(); + } + + Future _updateProfile() async { + if (!_formKey.currentState!.validate()) return; + + final profile = UpsertProfile( + name: _nameController.text.trim(), + placeOfBirth: _placeOfBirthController.text.trim(), + dateOfBirth: _dateOfBirthController.text.trim(), + ); + + try { + await ref.read(profileControllerProvider.notifier).updateProfile(profile); + + if (!mounted) return; + + final currentUserName = await SystemSetting.getCurrentUserName(); + + if (currentUserName != profile.name) { + await SystemSetting.setUser(userName: profile.name); + ref.read(currentUserNameProvider.notifier).state = profile.name; + } + + CustomSnackbar.showSuccess(context, "Informasi profil berhasil di ubah"); + context.pop(); + } catch (e, st) { + LogMessage.log.e(e.toString(), error: e, stackTrace: st); + CustomSnackbar.showError(context, "Ups, terjadi kesalahan"); + } + } + + @override + Widget build(BuildContext context) { + return LayoutBuilder( + builder: (context, constraints) { + final profileControllerState = ref.watch(profileControllerProvider); + return SafeArea( + top: false, + bottom: true, + right: false, + left: false, + child: Scaffold( + backgroundColor: const Color(0xFFF9FAFB), + appBar: TopBackBarApp( + title: "Informasi Profil", + onTap: () => Navigator.pop(context), + ), + body: SingleChildScrollView( + padding: EdgeInsets.all(5.w), + child: Form( + key: _formKey, + child: Column( + children: [ + CircleAvatar( + radius: 12.w, + backgroundColor: Colors.grey[200], + child: Icon( + Icons.person, + size: 12.w, + color: Colors.grey[400], + ), + ), + + SizedBox(height: 4.h), + + /** + CustomTextFormField( + label: "Username", + hint: "Contoh: ali_akbar", + controller: TextEditingController(), + prefixIcon: Icons.alternate_email, + validator: _validateUsername, + readOnly: true, + ), + + SizedBox(height: 2.h), + */ + CustomTextFormField( + label: "Nama", + hint: "Masukkan nama lengkap Anda", + controller: _nameController, + prefixIcon: Icons.person_outline, + validator: (value) { + if (value == null || value.isEmpty) { + return "Nama belum diisi"; + } + + return null; + }, + ), + SizedBox(height: 2.h), + CustomTextFormField( + label: "Email", + hint: "nama@email.com", + controller: TextEditingController(text: _email), + prefixIcon: Icons.email_outlined, + keyboardType: TextInputType.emailAddress, + readOnly: true, + ), + SizedBox(height: 2.h), + CustomTextFormField( + label: "No HP / WhatsApp", + hint: "0812xxxx", + controller: TextEditingController(text: _phoneNumber), + prefixIcon: Icons.phone_android, + keyboardType: TextInputType.phone, + readOnly: true, + ), + SizedBox(height: 2.h), + Row( + children: [ + Expanded( + child: CustomTextFormField( + label: "Tempat Lahir", + hint: "Jakarta", + controller: _placeOfBirthController, + ), + ), + SizedBox(width: 4.w), + Expanded( + child: CustomTextFormField( + label: "Tanggal Lahir", + hint: "31/12/1999", + controller: _dateOfBirthController, + keyboardType: TextInputType.number, + inputFormatters: [ + FilteringTextInputFormatter.allow( + RegExp(r'[0-9/]'), + ), + DateInputFormatter(), + ], + prefixIcon: Icons.calendar_today_outlined, + ), + ), + ], + ), + + SizedBox(height: 5.h), + ElevatedButton( + onPressed: profileControllerState.isLoading + ? null + : _updateProfile, + style: ElevatedButton.styleFrom( + backgroundColor: AppColor.primaryColor, + minimumSize: Size(double.infinity, 7.h), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(3.w), + ), + disabledBackgroundColor: Colors.grey.shade300, + ), + child: Text( + "Simpan Perubahan", + style: TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + fontSize: AppFontSize.medium.sp, + ), + ), + ), + ], + ), + ), + ), + ), + ); + }, + ); + } +} diff --git a/Mobile Operasional/lib/features/profile/presentation/screens/profile_screen.dart b/Mobile Operasional/lib/features/profile/presentation/screens/profile_screen.dart new file mode 100644 index 0000000..e593a5f --- /dev/null +++ b/Mobile Operasional/lib/features/profile/presentation/screens/profile_screen.dart @@ -0,0 +1,735 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; +import 'package:latlong2/latlong.dart'; +import 'package:niogu_app/core/constants/app_color.dart'; +import 'package:niogu_app/core/constants/app_font_size.dart'; +import 'package:niogu_app/core/providers/app_provider.dart'; +import 'package:niogu_app/core/router/app_route.dart'; +import 'package:niogu_app/core/utils/extension_format.dart'; +import 'package:niogu_app/core/enums/member_level.dart'; +import 'package:niogu_app/core/system/system_secure.dart'; +import 'package:niogu_app/core/system/system_setting.dart'; +import 'package:niogu_app/core/utils/time_zone.dart'; +import 'package:niogu_app/core/enums/user_role.dart'; +import 'package:niogu_app/core/widgets/custom_confirmation.dart'; +import 'package:niogu_app/features/outlets/domain/entities/outlet.dart'; +import 'package:niogu_app/features/outlets/presentation/providers/outlet_provider.dart'; +import 'package:niogu_app/features/profile/domain/entities/profile.dart'; +import 'package:niogu_app/features/profile/presentation/providers/profile_provider.dart'; +import 'package:niogu_app/features/user/domain/entities/user.dart'; +import 'package:niogu_app/features/user/presentation/providers/user_provider.dart'; +import 'package:sizer/sizer.dart'; + +class ProfileScreen extends ConsumerWidget { + const ProfileScreen({super.key}); + + void _setCurrentOutlet(WidgetRef ref) { + ref.read(currentOutletIdProvider.notifier).state = null; + + ref.read(currentOutletNameProvider.notifier).state = null; + } + + void _setCurrentUser(WidgetRef ref) { + ref.read(currentUserIdProvider.notifier).state = null; + + ref.read(currentUserNameProvider.notifier).state = null; + + ref.read(currentUserEmailProvider.notifier).state = null; + + ref.read(currentUserRoleProvider.notifier).state = null; + + ref.read(outletIdByOwnerProvider.notifier).state = null; + + ref.read(currentStatusLoginProvider.notifier).state = false; + } + + Future _logout(BuildContext context, WidgetRef ref) async { + showDialog( + context: context, + builder: (context) { + return CustomConfirmation( + title: "Keluar Akun", + body: "Anda yakin ingin keluar?", + color: Color(0xFFFFA000), + icon: Icons.logout_outlined, + onConfirm: () async { + await SystemSecure.deleteAccessToken(); + + await SystemSetting.clear(); + + _setCurrentOutlet(ref); + + _setCurrentUser(ref); + }, + ); + }, + ); + } + + @override + Widget build(BuildContext context, WidgetRef ref) { + return LayoutBuilder( + builder: (context, constraints) { + final bool isTablet = 100.w >= 600; + + final isLoggedIn = ref.watch(currentStatusLoginProvider); + + final currentUserName = ref.watch(currentUserNameProvider); + + final currentUserEmail = ref.watch(currentUserEmailProvider); + + final currentUserRole = ref.watch(currentUserRoleProvider); + + ShiftInfo shiftInfo = ShiftInfo( + shiftName: '---', + shiftStartTime: '---', + shiftEndTime: '---', + ); + + if (isLoggedIn) { + final shiftInfoState = ref.watch(shiftInfoStreamProvider); + + shiftInfoState.whenData((s) => shiftInfo = s); + } + + double profileCompleteness = 0.0; + + double businessCompleteness = 0.0; + + double outletCompleteness = 0.0; + + MemberLevel memberLevel = MemberLevel.standart; + + if (isLoggedIn) { + final profileCompletenessState = ref.watch( + profileCompletenessStreamProvider, + ); + + if (currentUserRole == UserRole.owner) { + final businessCompletenessState = ref.watch( + businessCompletenessStreamProvider, + ); + + businessCompletenessState.whenData((b) => businessCompleteness = b); + } else if (currentUserRole == UserRole.admin) { + final outletCompletenessState = ref.watch( + outletCompletenessStreamProvider, + ); + + outletCompletenessState.whenData((o) => outletCompleteness = o); + } + + profileCompletenessState.whenData((p) => profileCompleteness = p); + } + + final double profilePercentCompleteness = profileCompleteness * 100; + + final double businessPercentCompleteness = businessCompleteness * 100; + + final double outletPercentCompleteness = outletCompleteness * 100; + + if (isLoggedIn) { + final memberLevelState = ref.watch(memberLevelStreamProvider); + + memberLevelState.whenData((m) => memberLevel = m); + } + + return SafeArea( + top: false, + bottom: true, + right: false, + left: false, + child: Scaffold( + backgroundColor: const Color(0xFFF9FAFB), + body: SingleChildScrollView( + child: Column( + children: [ + Container( + width: double.infinity, + padding: EdgeInsets.fromLTRB(5.w, 8.h, 5.w, 4.h), + decoration: BoxDecoration( + color: AppColor.primaryColor, + borderRadius: BorderRadius.vertical( + bottom: Radius.circular(8.w), + ), + ), + child: Row( + children: [ + if (isLoggedIn) ...[ + CircleAvatar( + radius: 7.w, + backgroundColor: Colors.white24, + child: Icon( + Icons.person, + color: Colors.white, + size: 7.w, + ), + ), + SizedBox(width: 4.w), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + currentUserName!, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + ), + ), + if (currentUserRole == UserRole.owner && + currentUserEmail != null && + currentUserEmail.isNotEmpty) ...[ + SizedBox(height: 0.5.h), + Text( + currentUserEmail, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + color: Colors.white, + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + ), + ), + ] else if (currentUserRole == UserRole.admin) ...[ + SizedBox(height: 0.5.h), + Text( + "${shiftInfo.shiftName} : ${shiftInfo.shiftStartTime} ${TimeZone.getCurrentTimeZone()} - ${shiftInfo.shiftEndTime} ${TimeZone.getCurrentTimeZone()}", + style: TextStyle( + color: Colors.white, + fontWeight: FontWeight.w500, + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + ), + ), + ], + SizedBox(height: 0.5.h), + Container( + padding: EdgeInsets.symmetric( + horizontal: 2.w, + vertical: 0.3.h, + ), + decoration: BoxDecoration( + color: Colors.white24, + borderRadius: BorderRadius.circular(1.w), + ), + child: Text( + currentUserRole == UserRole.owner + ? "Owner Toko" + : "Staf Admin", + style: TextStyle( + color: Colors.white, + fontWeight: FontWeight.w600, + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + ), + ), + ), + ], + ), + ] else ...[ + OutlinedButton.icon( + onPressed: () => + context.goNamed(AppRoute.ownerLoginScreen), + icon: Icon( + Icons.login_rounded, + color: Colors.white, + size: 4.5.w, + ), + label: Text( + "Masuk Akun", + style: TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + ), + ), + style: OutlinedButton.styleFrom( + side: const BorderSide( + color: Colors.white70, + width: 1.5, + ), + padding: EdgeInsets.symmetric( + horizontal: 4.w, + vertical: 1.h, + ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(2.w), + ), + backgroundColor: Colors.white.withOpacity(0.1), + ), + ), + ], + ], + ), + ), + + !isLoggedIn + ? Column( + children: [ + _buildCompactLogin(context, isTablet), + ..._buildGeneralInformation( + context, + isTablet, + isLoggedIn, + ), + ], + ) + : Padding( + padding: EdgeInsets.symmetric(vertical: 3.h), + child: Column( + children: [ + SizedBox( + height: 14.h, + child: ListView( + scrollDirection: Axis.horizontal, + padding: EdgeInsets.symmetric( + horizontal: 5.w, + ), + children: [ + _buildProgressCard( + isTablet, + "Kelengkapan Profil", + profileCompleteness, + profileCompleteness == 1.0 + ? "Lengkap" + : "${profilePercentCompleteness.toStringAsFixed(0)}% Terisi", + ), + SizedBox(width: 4.w), + if (currentUserRole == UserRole.owner) + _buildProgressCard( + isTablet, + "Kelengkapan Usaha", + businessCompleteness, + businessPercentCompleteness == 1.0 + ? "Lengkap" + : "${businessPercentCompleteness.toStringAsFixed(0)}% Terisi", + ) + else if (currentUserRole == UserRole.admin) + _buildProgressCard( + isTablet, + "Kelengkapan Outlet", + outletCompleteness, + outletCompleteness == 1.0 + ? "Lengkap" + : "${outletPercentCompleteness.toStringAsFixed(0)}% Terisi", + ), + ], + ), + ), + + Padding( + padding: EdgeInsets.symmetric( + horizontal: 5.w, + vertical: 3.h, + ), + child: Column( + children: [ + if (currentUserRole == UserRole.owner) ...[ + Container( + padding: EdgeInsets.all(4.w), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular( + 3.w, + ), + border: Border.all( + color: Colors.grey.shade200, + ), + ), + child: Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + Icon( + Icons.verified_outlined, + color: Colors.blue, + size: 6.w, + ), + SizedBox(width: 3.w), + Text( + "Level Kemitraan: ${memberLevel.level.capitalize()}", + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + ), + ), + ], + ), + ], + ), + ), + + SizedBox(height: 3.h), + ], + + _buildProfileMenu( + isTablet: isTablet, + icon: Icons.person_outline, + title: "Informasi Profil", + onTap: () async { + final profileInfo = await ref + .read(profileRepositoryProvider) + .fetchProfileInfo(); + + await Future.delayed( + const Duration(milliseconds: 400), + () { + context.pushNamed( + AppRoute.profileInfoScreen, + extra: profileInfo, + ); + }, + ); + }, + ), + + if (currentUserRole == UserRole.owner) + _buildProfileMenu( + isTablet: isTablet, + icon: Icons.storefront_outlined, + title: "Informasi Usaha", + onTap: () async { + final businessInfo = await ref + .read(profileRepositoryProvider) + .fetchBusinessInfo(); + + await Future.delayed( + const Duration(milliseconds: 400), + () { + context.pushNamed( + AppRoute.businessInfoScreen, + extra: businessInfo, + ); + }, + ); + }, + ) + else if (currentUserRole == UserRole.admin) + _buildProfileMenu( + isTablet: isTablet, + icon: Icons.store_outlined, + title: "Informasi Outlet", + onTap: () async { + final currentOutletId = ref.read( + currentOutletIdProvider, + ); + + final outletDetail = await ref + .read(outletRepositoryProvider) + .getOutletById(currentOutletId!); + + final outletInfo = OutletInfo( + imagePath: outletDetail.bannerPath, + tenantName: outletDetail.tenantName, + name: outletDetail.name, + phoneNumber: + outletDetail.phoneNumber, + email: outletDetail.email, + fullAddress: + outletDetail.fullAddress, + ); + + if (outletDetail.fullAddress != + null && + outletDetail.latitude != null && + outletDetail.longitude != null) { + ref + .read( + mapOutletAddressProvider + .notifier, + ) + .state = OutletAddressSelected( + fullAddress: + outletDetail.fullAddress!, + currentLocation: LatLng( + outletDetail.latitude!, + outletDetail.longitude!, + ), + ); + } + + await Future.delayed( + const Duration(milliseconds: 400), + () { + context.pushNamed( + AppRoute.outletInfoScreen, + extra: outletInfo, + ); + }, + ); + }, + ), + + _buildProfileMenu( + isTablet: isTablet, + icon: Icons.headset_mic_outlined, + title: "Customer Service", + onTap: () {}, + ), + _buildProfileMenu( + isTablet: isTablet, + icon: Icons.print_outlined, + title: "Kertas Printer", + onTap: () {}, + ), + _buildProfileMenu( + isTablet: isTablet, + icon: Icons.lock_outlined, + title: "Ubah Password", + onTap: () { + context.pushNamed( + AppRoute.changePasswordScreen, + ); + }, + ), + _buildProfileMenu( + isTablet: isTablet, + icon: Icons.logout_outlined, + iconColor: Colors.red, + titleColor: Colors.red, + title: "Keluar Akun", + onTap: () async => + await _logout(context, ref), + ), + + ..._buildGeneralInformation( + context, + isTablet, + isLoggedIn, + ), + ], + ), + ), + ], + ), + ), + ], + ), + ), + ), + ); + }, + ); + } + + Widget _buildCompactLogin(BuildContext context, bool isTablet) { + return Container( + margin: EdgeInsets.symmetric(horizontal: 5.w, vertical: 3.h), + padding: EdgeInsets.all(5.w), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(4.w), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.05), + blurRadius: 10, + offset: const Offset(0, 4), + ), + ], + ), + child: Row( + children: [ + Container( + padding: EdgeInsets.all(3.w), + decoration: BoxDecoration( + color: Colors.blue[50], + shape: BoxShape.circle, + ), + child: Icon( + Icons.lock_outline_rounded, + color: AppColor.primaryColor, + size: 6.w, + ), + ), + SizedBox(width: 4.w), + // Teks Ajakan + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Akses Terbatas", + style: TextStyle( + fontSize: (AppFontSize.medium - (isTablet ? 0 : 1.25)).sp, + fontWeight: FontWeight.bold, + color: Colors.black87, + ), + ), + SizedBox(height: 0.5.h), + Text( + "Silakan login untuk melihat informasi profil lengkap Anda.", + style: TextStyle( + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + color: Colors.grey[600], + ), + ), + ], + ), + ), + ], + ), + ); + } + + Widget _buildProgressCard( + bool isTablet, + String title, + double progress, + String label, + ) { + return Container( + width: 65.w, + padding: EdgeInsets.all(4.w), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(3.w), + border: Border.all(color: Colors.grey.shade200), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + title, + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: isTablet ? AppFontSize.medium.sp : AppFontSize.small.sp, + ), + ), + SizedBox(height: 1.5.h), + LinearProgressIndicator( + value: progress, + backgroundColor: Colors.grey[200], + color: AppColor.primaryColor, + minHeight: 1.h, + borderRadius: BorderRadius.circular(2.w), + ), + SizedBox(height: 1.h), + Text( + label, + style: TextStyle( + color: Colors.grey.shade700, + fontSize: isTablet ? AppFontSize.medium.sp : AppFontSize.small.sp, + ), + ), + ], + ), + ); + } + + Widget _buildProfileMenu({ + required bool isTablet, + required IconData icon, + Color? iconColor, + Color? titleColor, + required String title, + required VoidCallback onTap, + }) { + return Container( + margin: EdgeInsets.only(bottom: 2.h), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(3.w), + border: Border.all(color: Colors.grey.shade200), + ), + child: ListTile( + onTap: onTap, + leading: Icon( + icon, + color: iconColor ?? AppColor.primaryColor, + size: 6.w, + ), + title: Text( + title, + style: TextStyle( + color: titleColor ?? Colors.black87, + fontSize: isTablet ? AppFontSize.medium.sp : AppFontSize.small.sp, + fontWeight: FontWeight.w500, + ), + ), + trailing: Icon(Icons.chevron_right, color: Colors.grey, size: 5.w), + ), + ); + } + + Widget _buildSocialIcon(IconData icon) { + return Container( + padding: EdgeInsets.all(2.5.w), + decoration: BoxDecoration( + shape: BoxShape.circle, + border: Border.all(color: Colors.grey.shade300), + ), + child: Icon(icon, color: Colors.grey[600], size: 5.w), + ); + } + + List _buildGeneralInformation( + BuildContext context, + bool isTablet, + bool isLoggedIn, + ) { + return [ + Column( + children: [ + Text( + "Ikuti kami di media sosial", + style: TextStyle( + fontSize: isTablet ? AppFontSize.medium.sp : AppFontSize.small.sp, + color: Colors.grey[600], + fontWeight: FontWeight.w500, + ), + ), + SizedBox(height: 2.h), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + _buildSocialIcon(Icons.camera_alt_outlined), + SizedBox(width: 5.w), + _buildSocialIcon(Icons.facebook_outlined), + SizedBox(width: 5.w), + _buildSocialIcon(Icons.language_outlined), + ], + ), + ], + ), + + SizedBox(height: 4.h), + Text( + "Powered By P.T. Niaga Nusantara", + style: TextStyle( + color: Colors.grey.shade600, + fontWeight: FontWeight.w500, + fontSize: isTablet ? AppFontSize.medium.sp : AppFontSize.small.sp, + ), + ), + if (isLoggedIn) ...[ + SizedBox(height: 0.75.h), + Text( + "Versi 1.0.0", + style: TextStyle( + color: Colors.grey, + fontSize: isTablet ? AppFontSize.medium.sp : AppFontSize.small.sp, + ), + ), + ], + SizedBox(height: 8.h), + ]; + } +} diff --git a/Mobile Operasional/lib/features/report/presentation/providers/report_provider.dart b/Mobile Operasional/lib/features/report/presentation/providers/report_provider.dart new file mode 100644 index 0000000..367130a --- /dev/null +++ b/Mobile Operasional/lib/features/report/presentation/providers/report_provider.dart @@ -0,0 +1,9 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:niogu_app/core/enums/report_type.dart'; + +final filteredByOutletProvider = StateProvider.family + .autoDispose((ref, type) => null); + +final filteredByDateTimeProvider = StateProvider.family + .autoDispose((ref, type) => null); diff --git a/Mobile Operasional/lib/features/report/presentation/screens/report_screen.dart b/Mobile Operasional/lib/features/report/presentation/screens/report_screen.dart new file mode 100644 index 0000000..65b3181 --- /dev/null +++ b/Mobile Operasional/lib/features/report/presentation/screens/report_screen.dart @@ -0,0 +1,120 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:niogu_app/core/components/app_bar_with_tab.dart'; +import 'package:niogu_app/features/report/stock_report/presentation/providers/stock_report_provider.dart'; +import 'package:niogu_app/features/report/stock_report/presentation/screens/stock_report_screen.dart'; +import 'package:niogu_app/features/report/transaction/presentation/providers/transaction_report_provider.dart'; +import 'package:niogu_app/features/report/transaction/presentation/screens/transaction_report_screen.dart'; +import 'package:sizer/sizer.dart'; + +class ReportScreen extends ConsumerStatefulWidget { + const ReportScreen({super.key}); + + @override + ConsumerState createState() => _ReportScreenState(); +} + +class _ReportScreenState extends ConsumerState + with SingleTickerProviderStateMixin { + late TabController _tabController; + + final FocusNode _searchFocusNode = FocusNode(); + + Color _searchIconColor = Colors.grey; + + Timer? _debounce; + + int _tabIndex = 0; + + @override + void initState() { + // TODO: implement initState + super.initState(); + + _tabController = TabController(length: 2, vsync: this); + + _tabController.addListener(_handleTabSelection); + + _searchFocusNode.addListener(() { + setState(() { + _searchIconColor = _searchFocusNode.hasFocus + ? Colors.black + : Colors.grey; + }); + }); + } + + @override + void dispose() { + // TODO: implement dispose + _searchFocusNode.dispose(); + _tabController.removeListener(_handleTabSelection); + _tabController.dispose(); + _debounce?.cancel(); + super.dispose(); + } + + void _handleTabSelection() { + if (_tabController.index != _tabIndex) { + setState(() { + _tabIndex = _tabController.index; + }); + } + } + + void _onSearchChanged(String value) { + if (_debounce?.isActive ?? false) _debounce?.cancel(); + _debounce = Timer(const Duration(milliseconds: 800), () { + if (_tabIndex == 0) { + ref.read(transactionReportSearchProvider.notifier).state = value; + } else { + ref.read(stockReportSearchProvider.notifier).state = value; + } + }); + } + + @override + Widget build(BuildContext context) { + return LayoutBuilder( + builder: (context, constraints) { + return SafeArea( + top: false, + bottom: true, + right: false, + left: false, + child: Scaffold( + backgroundColor: Colors.white, + resizeToAvoidBottomInset: false, + appBar: AppBarWithTab( + hintText: + "Cari ${_tabIndex == 0 ? 'nomor transaksi' : 'nama barang'}...", + searchIconColor: _searchIconColor, + showSwitchOutlet: false, + searchFocusNode: _searchFocusNode, + tabController: _tabController, + onTap: (value) { + setState(() { + _tabIndex = value; + }); + }, + onSearchChanged: (value) => _onSearchChanged(value), + tabs: [ + Tab(text: "Transaksi", height: 10.h), + Tab(text: "Arus Stok", height: 10.h), + ], + ), + body: TabBarView( + controller: _tabController, + children: const [ + const TransactionReportScreen(), + const StockReportScreen(), + ], + ), + ), + ); + }, + ); + } +} diff --git a/Mobile Operasional/lib/features/report/presentation/widgets/filtered_date_range.dart b/Mobile Operasional/lib/features/report/presentation/widgets/filtered_date_range.dart new file mode 100644 index 0000000..8b305c9 --- /dev/null +++ b/Mobile Operasional/lib/features/report/presentation/widgets/filtered_date_range.dart @@ -0,0 +1,53 @@ +import 'package:flutter/material.dart'; +import 'package:niogu_app/core/constants/app_font_size.dart'; +import 'package:sizer/sizer.dart'; + +class FilteredDateRange extends StatelessWidget { + final String value; + final VoidCallback onTap; + const FilteredDateRange({super.key, required this.value, required this.onTap}); + + @override + Widget build(BuildContext context) { + final bool isTablet = 100.w >= 600; + return InkWell( + onTap: onTap, + child: Container( + height: 5.5.h, + width: double.infinity, + constraints: BoxConstraints(maxWidth: double.infinity), + padding: EdgeInsets.symmetric(horizontal: 3.w), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(2.5.w), + border: Border.all(color: Colors.grey.shade300), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Icon( + Icons.calendar_month_outlined, + size: 4.5.w, + color: Colors.grey[600], + ), + SizedBox(width: 2.w), + Expanded( + child: Text( + value, + textAlign: TextAlign.start, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + color: Colors.grey[700], + fontWeight: FontWeight.w600, + ), + ), + ), + ], + ), + ), + ); + } +} diff --git a/Mobile Operasional/lib/features/report/presentation/widgets/filtered_outlet.dart b/Mobile Operasional/lib/features/report/presentation/widgets/filtered_outlet.dart new file mode 100644 index 0000000..9be75bb --- /dev/null +++ b/Mobile Operasional/lib/features/report/presentation/widgets/filtered_outlet.dart @@ -0,0 +1,57 @@ +import 'package:flutter/material.dart'; +import 'package:niogu_app/core/constants/app_font_size.dart'; +import 'package:niogu_app/features/stock_in/domain/entities/stock_in_transfer.dart'; +import 'package:sizer/sizer.dart'; + +class FilteredOutlet extends StatelessWidget { + final String value; + final List sourceOutlets; + final void Function(String?)? onChanged; + const FilteredOutlet({ + super.key, + required this.value, + required this.sourceOutlets, + required this.onChanged, + }); + + @override + Widget build(BuildContext context) { + final bool isTablet = 100.w >= 600; + return Container( + height: 5.5.h, + padding: EdgeInsets.symmetric(horizontal: 3.w), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(2.5.w), + border: Border.all(color: Colors.grey.shade300), + ), + child: DropdownButtonHideUnderline( + child: DropdownButton( + value: value, + icon: Icon(Icons.store_outlined, size: 5.w, color: Colors.grey[700]), + isExpanded: true, + style: TextStyle( + fontSize: isTablet ? AppFontSize.medium.sp : AppFontSize.small.sp, + color: Colors.black87, + fontWeight: FontWeight.w600, + ), + items: [ + ...sourceOutlets.map((outlet) { + return DropdownMenuItem( + value: outlet.id, + child: Text( + outlet.name, + overflow: TextOverflow.ellipsis, + style: isTablet + ? null + : TextStyle(fontSize: AppFontSize.small.sp), + ), + ); + }).toList(), + ], + onChanged: onChanged, + ), + ), + ); + } +} diff --git a/Mobile Operasional/lib/features/report/presentation/widgets/filtered_type.dart b/Mobile Operasional/lib/features/report/presentation/widgets/filtered_type.dart new file mode 100644 index 0000000..0d2828e --- /dev/null +++ b/Mobile Operasional/lib/features/report/presentation/widgets/filtered_type.dart @@ -0,0 +1,54 @@ +import 'package:flutter/material.dart'; +import 'package:niogu_app/core/constants/app_font_size.dart'; +import 'package:sizer/sizer.dart'; + +class FilteredType extends StatelessWidget { + final String value; + final List items; + final void Function(String?)? onChanged; + const FilteredType({ + super.key, + required this.value, + required this.items, + required this.onChanged, + }); + + @override + Widget build(BuildContext context) { + final bool isTablet = 100.w >= 600; + return Container( + height: 5.5.h, + padding: EdgeInsets.symmetric(horizontal: 3.w), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(2.5.w), + border: Border.all(color: Colors.grey.shade300), + ), + child: DropdownButtonHideUnderline( + child: DropdownButton( + value: value, + icon: Icon( + Icons.filter_alt_outlined, + size: 5.w, + color: Colors.grey[700], + ), + isExpanded: true, + style: TextStyle( + fontSize: isTablet ? AppFontSize.medium.sp : AppFontSize.small.sp, + color: Colors.black87, + fontWeight: FontWeight.w600, + ), + items: items + .map( + (e) => DropdownMenuItem( + value: e, + child: Text(e, overflow: TextOverflow.ellipsis), + ), + ) + .toList(), + onChanged: onChanged, + ), + ), + ); + } +} diff --git a/Mobile Operasional/lib/features/report/presentation/widgets/fixed_outlet.dart b/Mobile Operasional/lib/features/report/presentation/widgets/fixed_outlet.dart new file mode 100644 index 0000000..78415b4 --- /dev/null +++ b/Mobile Operasional/lib/features/report/presentation/widgets/fixed_outlet.dart @@ -0,0 +1,41 @@ +import 'package:flutter/material.dart'; +import 'package:niogu_app/core/constants/app_font_size.dart'; +import 'package:sizer/sizer.dart'; + +class FixedOutlet extends StatelessWidget { + final String value; + const FixedOutlet({super.key, required this.value}); + + @override + Widget build(BuildContext context) { + final bool isTablet = 100.w >= 600; + return Container( + height: 5.5.h, + padding: EdgeInsets.symmetric(horizontal: 3.w), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(2.5.w), + border: Border.all(color: Colors.grey.shade300), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Text( + value, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + fontWeight: FontWeight.w600, + ), + ), + ), + Icon(Icons.store_outlined, size: 5.w, color: Colors.grey.shade700), + ], + ), + ); + } +} diff --git a/Mobile Operasional/lib/features/report/presentation/widgets/report_header.dart b/Mobile Operasional/lib/features/report/presentation/widgets/report_header.dart new file mode 100644 index 0000000..ea430dc --- /dev/null +++ b/Mobile Operasional/lib/features/report/presentation/widgets/report_header.dart @@ -0,0 +1,49 @@ +import 'package:flutter/material.dart'; +import 'package:niogu_app/core/constants/app_color.dart'; +import 'package:niogu_app/core/constants/app_font_size.dart'; +import 'package:sizer/sizer.dart'; + +class ReportHeader extends StatelessWidget { + final String rightTitle; + final String leftTitle; + const ReportHeader({super.key, required this.rightTitle, required this.leftTitle}); + + @override + Widget build(BuildContext context) { + final bool isTablet = 100.w >= 600; + return Padding( + padding: EdgeInsets.fromLTRB(5.w, 1.h, 5.w, 1.h), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + rightTitle, + style: TextStyle( + fontSize: isTablet ? AppFontSize.medium.sp : AppFontSize.small.sp, + fontWeight: FontWeight.bold, + color: Colors.black87, + ), + ), + + TextButton.icon( + onPressed: () {}, + style: TextButton.styleFrom( + foregroundColor: AppColor.primaryColor, + padding: EdgeInsets.symmetric(horizontal: 4.w), + ), + icon: Icon(Icons.download, size: 5.w), + label: Text( + leftTitle, + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + ), + ), + ), + ], + ), + ); + } +} \ No newline at end of file diff --git a/Mobile Operasional/lib/features/report/stock_report/data/repositories/stock_report_repository_impl.dart b/Mobile Operasional/lib/features/report/stock_report/data/repositories/stock_report_repository_impl.dart new file mode 100644 index 0000000..0e9ba9d --- /dev/null +++ b/Mobile Operasional/lib/features/report/stock_report/data/repositories/stock_report_repository_impl.dart @@ -0,0 +1,40 @@ +import 'package:niogu_app/core/database/app_database.dart'; +import 'package:niogu_app/core/enums/item_type.dart'; +import 'package:niogu_app/features/report/stock_report/domain/entities/stok_report.dart'; +import 'package:niogu_app/features/report/stock_report/domain/repositories/i_stock_report_repository.dart'; + +class StockReportRepositoryImpl implements IStockReportRepository { + final AppDatabase _appDatabase; + + StockReportRepositoryImpl(this._appDatabase); + + @override + Stream> watchStockReports({ + String? outletId, + ItemType? itemType, + DateTime? startDate, + DateTime? endDate, + }) { + return _appDatabase.stockCardDao.watchAllStockReports( + outletId: outletId, + itemType: itemType, + startDate: startDate, + endDate: endDate + ); + } + + @override + Future getStockReportById({ + required String id, + required ItemType itemType, + DateTime? startDate, + DateTime? endDate, + }) async { + return await _appDatabase.stockCardDao.getStockReportById( + id: id, + itemType: itemType, + startDate: startDate, + endDate: endDate, + ); + } +} diff --git a/Mobile Operasional/lib/features/report/stock_report/domain/entities/stok_report.dart b/Mobile Operasional/lib/features/report/stock_report/domain/entities/stok_report.dart new file mode 100644 index 0000000..eeb3a28 --- /dev/null +++ b/Mobile Operasional/lib/features/report/stock_report/domain/entities/stok_report.dart @@ -0,0 +1,169 @@ +import 'package:niogu_app/core/enums/item_type.dart'; +import 'package:niogu_app/core/enums/stock_card_type.dart'; +import 'package:niogu_app/core/enums/stock_type.dart'; + +enum StockReportItemEmpty { + loading, + empty_database, + empty_search_result, + has_data, +} + +class MovementTemp { + final StockCardType type; + final double initialStock; + final double qty; + final DateTime date; + final StockType stockType; + + MovementTemp({ + required this.type, + required this.initialStock, + required this.qty, + required this.date, + required this.stockType, + }); +} + +class StockReportItem { + final String id; + final ItemType itemType; + final String name; + final String? imageProduct; + final String? variantName; + final bool isUnlimited; + final double initialStock; + final double totalStockIn; + final double totalStockOut; + final double lastStock; + final String unit; + final bool isDeleteByOwner; + final bool hasBeenDelete; + + const StockReportItem({ + required this.id, + required this.itemType, + required this.name, + this.variantName, + this.imageProduct, + required this.isUnlimited, + required this.initialStock, + required this.totalStockIn, + required this.totalStockOut, + required this.lastStock, + required this.unit, + required this.isDeleteByOwner, + required this.hasBeenDelete + }); + + StockReportItem copyWith({ + double? initialStock, + double? totalStockIn, + double? totalStockOut, + double? lastStock, + }) { + return StockReportItem( + id: id, + itemType: itemType, + name: name, + variantName: this.variantName, + imageProduct: imageProduct, + isUnlimited: isUnlimited, + initialStock: initialStock ?? this.initialStock, + totalStockIn: totalStockIn ?? this.totalStockIn, + totalStockOut: totalStockOut ?? this.totalStockOut, + lastStock: lastStock ?? this.lastStock, + unit: unit, + isDeleteByOwner: isDeleteByOwner, + hasBeenDelete: hasBeenDelete + ); + } +} + +class StockReportDetail { + final String itemName; + final String? variantName; + final bool isUnlimited; + final String outletName; + final DateTime? startDate; + final DateTime? endDate; + final double initialStock; + final double totalStockIn; + final double totalStockOut; + final double lastStock; + final String unit; + final List movements; + final DateTime createdAt; + + const StockReportDetail({ + required this.itemName, + required this.variantName, + required this.isUnlimited, + required this.outletName, + required this.startDate, + required this.endDate, + required this.initialStock, + required this.totalStockIn, + required this.totalStockOut, + required this.lastStock, + required this.unit, + required this.movements, + required this.createdAt, + }); + + StockReportDetail copyWith({ + double? totalStockIn, + double? totalStockOut, + double? lastStock, + }) { + return StockReportDetail( + itemName: itemName, + variantName: variantName, + isUnlimited: isUnlimited, + outletName: outletName, + startDate: startDate, + endDate: endDate, + initialStock: initialStock, + totalStockIn: totalStockIn ?? this.totalStockIn, + totalStockOut: totalStockOut ?? this.totalStockOut, + lastStock: lastStock ?? this.lastStock, + unit: unit, + movements: movements, + createdAt: createdAt, + ); + } +} + +class StockMovement { + final DateTime date; + final String description; + final String? notes; + final String userName; + final StockCardType type; + final String? referenceId; + final bool isStockIn; + final bool isUnlimited; + final bool modeChange; + final String? beforeChange; + final String? afterChange; + final double stockIn; + final double stockOut; + final double remainingStock; + + const StockMovement({ + required this.date, + required this.description, + this.notes, + required this.userName, + required this.type, + this.referenceId, + required this.isStockIn, + required this.isUnlimited, + required this.modeChange, + this.beforeChange, + this.afterChange, + required this.stockIn, + required this.stockOut, + required this.remainingStock, + }); +} diff --git a/Mobile Operasional/lib/features/report/stock_report/domain/repositories/i_stock_report_repository.dart b/Mobile Operasional/lib/features/report/stock_report/domain/repositories/i_stock_report_repository.dart new file mode 100644 index 0000000..6cb97ba --- /dev/null +++ b/Mobile Operasional/lib/features/report/stock_report/domain/repositories/i_stock_report_repository.dart @@ -0,0 +1,18 @@ +import 'package:niogu_app/core/enums/item_type.dart'; +import 'package:niogu_app/features/report/stock_report/domain/entities/stok_report.dart'; + +abstract class IStockReportRepository { + Stream> watchStockReports({ + String? outletId, + ItemType? itemType, + DateTime? startDate, + DateTime? endDate, + }); + + Future getStockReportById({ + required String id, + required ItemType itemType, + DateTime? startDate, + DateTime? endDate, + }); +} \ No newline at end of file diff --git a/Mobile Operasional/lib/features/report/stock_report/presentation/providers/stock_report_provider.dart b/Mobile Operasional/lib/features/report/stock_report/presentation/providers/stock_report_provider.dart new file mode 100644 index 0000000..7dacffe --- /dev/null +++ b/Mobile Operasional/lib/features/report/stock_report/presentation/providers/stock_report_provider.dart @@ -0,0 +1,103 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:niogu_app/core/providers/app_provider.dart'; +import 'package:niogu_app/core/enums/item_type.dart'; +import 'package:niogu_app/core/enums/report_type.dart'; +import 'package:niogu_app/features/report/presentation/providers/report_provider.dart'; +import 'package:niogu_app/features/report/stock_report/data/repositories/stock_report_repository_impl.dart'; +import 'package:niogu_app/features/report/stock_report/domain/entities/stok_report.dart'; +import 'package:niogu_app/features/report/stock_report/domain/repositories/i_stock_report_repository.dart'; + +final stockReportRepositoryProvider = + Provider.autoDispose((ref) { + final appDatabase = ref.watch(appDatabaseProvider); + return StockReportRepositoryImpl(appDatabase); + }); + +final filteredByItemTypeProvider = StateProvider.autoDispose( + (ref) => null, +); + +final stockReportStreamProvider = + StreamProvider.autoDispose>((ref) { + final isLoggedIn = ref.watch(currentStatusLoginProvider); + + final stockReportRepository = ref.watch(stockReportRepositoryProvider); + + final outletId = ref.watch( + filteredByOutletProvider(ReportType.stock_card), + ); + + final itemType = ref.watch(filteredByItemTypeProvider); + + final date = ref.watch(filteredByDateTimeProvider(ReportType.stock_card)); + + final startDate = date?.start; + + final endDate = date?.end; + + if (!isLoggedIn) { + return Stream>.value([]); + } + + return stockReportRepository.watchStockReports( + outletId: outletId, + itemType: itemType, + startDate: startDate, + endDate: endDate, + ); + }); + +final stockReportSearchProvider = StateProvider.autoDispose( + (ref) => '', +); + +final filteredStockReportProvider = + Provider.autoDispose>>((ref) { + final stockReportStreamAsync = ref.watch(stockReportStreamProvider); + final stockReportSearchAsync = ref.watch(stockReportSearchProvider); + return stockReportStreamAsync.when( + data: (stockReports) { + if (stockReportSearchAsync.trim().isEmpty) { + return AsyncValue.data(stockReports); + } + + final filteredStockReport = stockReports.where((stockReport) { + return stockReport.name.toLowerCase().contains( + stockReportSearchAsync.toLowerCase(), + ); + }).toList(); + + return AsyncValue.data(filteredStockReport); + }, + error: (error, stackTrace) { + return AsyncValue.error(error, stackTrace); + }, + loading: () => const AsyncValue.loading(), + ); + }); + +final stockReportEmptyProvider = Provider.autoDispose(( + ref, +) { + final stockReportStreamAsync = ref.watch(stockReportStreamProvider); + final filteredStockReportAsync = ref.watch(filteredStockReportProvider); + final stockReportSearchAsync = ref.watch(stockReportSearchProvider); + + if (stockReportStreamAsync.isLoading) { + return StockReportItemEmpty.loading; + } + + final allStockReprts = stockReportStreamAsync.value ?? []; + + if (allStockReprts.isEmpty) { + return StockReportItemEmpty.empty_database; + } + + final filteredStockReports = filteredStockReportAsync.value ?? []; + + if (stockReportSearchAsync.isNotEmpty && filteredStockReports.isEmpty) { + return StockReportItemEmpty.empty_search_result; + } + + return StockReportItemEmpty.has_data; +}); diff --git a/Mobile Operasional/lib/features/report/stock_report/presentation/screens/stock_report_detail_screen.dart b/Mobile Operasional/lib/features/report/stock_report/presentation/screens/stock_report_detail_screen.dart new file mode 100644 index 0000000..7104e9e --- /dev/null +++ b/Mobile Operasional/lib/features/report/stock_report/presentation/screens/stock_report_detail_screen.dart @@ -0,0 +1,236 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; +import 'package:niogu_app/core/components/top_back_bar_app.dart'; +import 'package:niogu_app/core/constants/app_color.dart'; +import 'package:niogu_app/core/constants/app_font_size.dart'; +import 'package:niogu_app/core/router/app_route.dart'; +import 'package:niogu_app/core/enums/stock_card_type.dart'; +import 'package:niogu_app/core/widgets/custom_empty_screen.dart'; +import 'package:niogu_app/features/report/stock_report/domain/entities/stok_report.dart'; +import 'package:niogu_app/features/report/stock_report/presentation/widgets/header_card.dart'; +import 'package:niogu_app/features/report/stock_report/presentation/widgets/movement_card.dart'; +import 'package:niogu_app/features/report/stock_report/presentation/widgets/sticky_footer.dart'; +import 'package:niogu_app/features/report/transaction/presentation/providers/transaction_report_provider.dart'; +import 'package:sizer/sizer.dart'; + +class StockReportDetailScreen extends ConsumerWidget { + final StockReportDetail stockReportDetail; + StockReportDetailScreen({super.key, required this.stockReportDetail}); + + bool _isBetween(DateTime value, DateTime start, DateTime end) { + final endInclusive = end.add(const Duration(days: 1)); + return !value.isBefore(start) && value.isBefore(endInclusive); + } + + @override + Widget build(BuildContext context, WidgetRef ref) { + return LayoutBuilder( + builder: (context, constraints) { + final movements = stockReportDetail.movements.where((m) { + final start = stockReportDetail.startDate; + final end = stockReportDetail.endDate; + + if (start == null || end == null) return true; + + return _isBetween(m.date, start, end); + }).toList(); + + final lastMovement = stockReportDetail.movements.where((movement) { + final bool filter = + movement.modeChange && + movement.beforeChange != null && + movement.afterChange != null; + + final start = stockReportDetail.startDate; + + final end = stockReportDetail.endDate; + + if (start == null || end == null) { + return filter; + } + + return filter && _isBetween(movement.date, start, end); + }); + + bool modeChange = lastMovement.lastOrNull?.modeChange ?? false; + + final beforeChange = lastMovement.lastOrNull?.beforeChange; + + final afterChange = lastMovement.lastOrNull?.afterChange; + + final dateChange = lastMovement.lastOrNull?.date; + + modeChange = + modeChange && + beforeChange != null && + afterChange != null && + dateChange != null; + + return SafeArea( + top: false, + bottom: true, + right: false, + left: false, + child: Scaffold( + backgroundColor: const Color(0xFFF9FAFB), + appBar: TopBackBarApp( + title: "Rincian Arus Stok", + onTap: () => context.pop(), + ), + body: Column( + children: [ + Expanded( + child: CustomScrollView( + physics: const BouncingScrollPhysics(), + slivers: [ + SliverToBoxAdapter( + child: HeaderCard( + modeChange: modeChange, + beforeChange: beforeChange, + afterChange: afterChange, + dateChange: dateChange, + stockReportDetail: stockReportDetail, + ), + ), + + movements.isEmpty + ? SliverToBoxAdapter( + child: CustomEmptyScreen( + body: "Tidak Ada Aktivitas Arus Stok", + ), + ) + : SliverPadding( + padding: EdgeInsets.symmetric( + horizontal: 5.w, + vertical: 2.h, + ), + sliver: SliverList( + delegate: SliverChildBuilderDelegate( + (context, index) => MovementCard( + movement: movements[index], + unit: stockReportDetail.unit, + onTap: () async { + final movement = movements[index]; + + if (movement.type == StockCardType.sale) { + final orderDetail = await ref + .read( + transactionReportRepositoryProvider, + ) + .getOrderDetail( + movement.referenceId!, + ); + + context.pushNamed( + AppRoute + .transactionReportOrderDetailScreen, + extra: orderDetail, + ); + } else if (movement.type == + StockCardType.purchase) { + final purchaseDetail = await ref + .read( + transactionReportRepositoryProvider, + ) + .getPurchaseDetail( + movement.referenceId!, + ); + + context.pushNamed( + AppRoute + .transactionReportPurchaseDetailScreen, + extra: purchaseDetail, + ); + } + }, + ), + childCount: movements.length, + ), + ), + ), + ], + ), + ), + + StickyFooter( + modeChange: modeChange, + stockReportDetail: stockReportDetail, + ), + ], + ), + bottomNavigationBar: Container( + padding: EdgeInsets.all(4.w), + decoration: BoxDecoration( + color: Colors.white, + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.05), + blurRadius: 10, + offset: const Offset(0, -5), + ), + ], + ), + child: Row( + children: [ + Expanded( + child: OutlinedButton.icon( + onPressed: () {}, + style: OutlinedButton.styleFrom( + padding: EdgeInsets.symmetric(vertical: 1.8.h), + side: BorderSide(color: AppColor.primaryColor), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(2.5.w), + ), + ), + icon: Icon( + Icons.share_outlined, + color: AppColor.primaryColor, + size: 5.w, + ), + label: Text( + "Bagikan", + style: TextStyle( + color: AppColor.primaryColor, + fontWeight: FontWeight.bold, + fontSize: AppFontSize.medium.sp, + ), + ), + ), + ), + SizedBox(width: 3.w), + Expanded( + child: ElevatedButton.icon( + onPressed: () {}, // Logic download PDF + style: ElevatedButton.styleFrom( + padding: EdgeInsets.symmetric(vertical: 1.8.h), + backgroundColor: AppColor.primaryColor, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(2.5.w), + ), + elevation: 0, + ), + icon: Icon( + Icons.picture_as_pdf_outlined, + color: Colors.white, + size: 5.w, + ), + label: Text( + "Unduh PDF", + style: TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + fontSize: AppFontSize.medium.sp, + ), + ), + ), + ), + ], + ), + ), + ), + ); + }, + ); + } +} diff --git a/Mobile Operasional/lib/features/report/stock_report/presentation/screens/stock_report_screen.dart b/Mobile Operasional/lib/features/report/stock_report/presentation/screens/stock_report_screen.dart new file mode 100644 index 0000000..d21b769 --- /dev/null +++ b/Mobile Operasional/lib/features/report/stock_report/presentation/screens/stock_report_screen.dart @@ -0,0 +1,507 @@ +import 'package:calendar_date_picker2/calendar_date_picker2.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; +import 'package:intl/intl.dart'; +import 'package:niogu_app/core/constants/app_color.dart'; +import 'package:niogu_app/core/constants/app_font_size.dart'; +import 'package:niogu_app/core/widgets/custom_error_screen.dart'; +import 'package:niogu_app/core/providers/app_provider.dart'; +import 'package:niogu_app/core/router/app_route.dart'; +import 'package:niogu_app/core/enums/item_type.dart'; +import 'package:niogu_app/core/enums/report_type.dart'; +import 'package:niogu_app/core/enums/user_role.dart'; +import 'package:niogu_app/core/widgets/custom_empty_screen.dart'; +import 'package:niogu_app/features/outlets/presentation/providers/outlet_provider.dart'; +import 'package:niogu_app/features/report/presentation/providers/report_provider.dart'; +import 'package:niogu_app/features/report/presentation/widgets/filtered_date_range.dart'; +import 'package:niogu_app/features/report/presentation/widgets/filtered_outlet.dart'; +import 'package:niogu_app/features/report/presentation/widgets/filtered_type.dart'; +import 'package:niogu_app/features/report/presentation/widgets/fixed_outlet.dart'; +import 'package:niogu_app/features/report/presentation/widgets/report_header.dart'; +import 'package:niogu_app/features/report/stock_report/domain/entities/stok_report.dart'; +import 'package:niogu_app/features/report/stock_report/presentation/providers/stock_report_provider.dart'; +import 'package:niogu_app/features/report/stock_report/presentation/widgets/stock_card_item.dart'; +import 'package:niogu_app/features/report/stock_report/presentation/widgets/stock_report_shimmer.dart'; +import 'package:niogu_app/features/report/transaction/presentation/widgets/sliver_header_delegate.dart'; +import 'package:niogu_app/features/stock_in/domain/entities/stock_in_transfer.dart'; +import 'package:sizer/sizer.dart'; + +class StockReportScreen extends ConsumerStatefulWidget { + const StockReportScreen({super.key}); + + @override + ConsumerState createState() => _StockReportScreenState(); +} + +class _StockReportScreenState extends ConsumerState + with AutomaticKeepAliveClientMixin { + String? _selectedOutlet; + + String _selectedType = "Semua Barang"; + + DateTimeRange? _selectedDateRange; + + List _initialValue = [ + DateTime.now().subtract(const Duration(days: 7)), + DateTime.now(), + ]; + + @override + // TODO: implement wantKeepAlive + bool get wantKeepAlive => true; + + @override + void initState() { + // TODO: implement initState + super.initState(); + + WidgetsBinding.instance.addPostFrameCallback((_) { + final currentOutletId = ref.read(currentOutletIdProvider); + ref.read(filteredByOutletProvider(ReportType.stock_card).notifier).state = + currentOutletId; + }); + } + + @override + void dispose() { + // TODO: implement dispose + super.dispose(); + } + + String _formatGroupDate(DateTime date) { + return DateFormat('dd MMMM yyyy').format(date); + } + + Future _showDateRangePicker() async { + final bool isTablet = 100.w >= 600; + + List tempSelectedDates = List.from(_initialValue); + + showModalBottomSheet( + context: context, + useRootNavigator: true, + isScrollControlled: true, + backgroundColor: Colors.transparent, + constraints: BoxConstraints(maxWidth: double.infinity), + builder: (context) { + return StatefulBuilder( + builder: (context, setModalState) { + return Container( + height: 80.h, + width: double.infinity, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.vertical( + top: Radius.circular(4.5.w), + ), + ), + constraints: BoxConstraints( + maxHeight: 80.h, + maxWidth: double.infinity, + ), + child: Column( + children: [ + SizedBox(height: 1.5.h), + Container( + width: 12.w, + height: 0.6.h, + decoration: BoxDecoration( + color: Colors.grey[300], + borderRadius: BorderRadius.circular(2.5.w), + ), + ), + Padding( + padding: EdgeInsets.symmetric(vertical: 2.h), + child: Text( + "Pilih Rentang Tanggal", + style: TextStyle( + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + fontWeight: FontWeight.bold, + ), + ), + ), + Divider(height: 1, color: Colors.grey[200]), + + Expanded( + child: CalendarDatePicker2( + config: CalendarDatePicker2Config( + calendarType: CalendarDatePicker2Type.range, + selectedDayHighlightColor: AppColor.primaryColor, + weekdayLabels: [ + 'Min', + 'Sen', + 'Sel', + 'Rab', + 'Kam', + 'Jum', + 'Sab', + ], + weekdayLabelTextStyle: TextStyle( + color: Colors.grey[600], + fontWeight: FontWeight.bold, + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + ), + controlsTextStyle: TextStyle( + color: Colors.black, + fontWeight: FontWeight.bold, + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + ), + centerAlignModePicker: true, + firstDayOfWeek: 1, + ), + value: tempSelectedDates, + onValueChanged: (dates) { + setModalState(() { + tempSelectedDates = dates; + }); + }, + ), + ), + + Container( + padding: EdgeInsets.all(5.w), + decoration: BoxDecoration( + color: Colors.white, + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.05), + blurRadius: 10, + offset: const Offset(0, -5), + ), + ], + ), + child: Row( + children: [ + Expanded( + child: OutlinedButton( + onPressed: () { + _selectedDateRange = null; + ref + .read( + filteredByDateTimeProvider( + ReportType.stock_card, + ).notifier, + ) + .state = + null; + context.pop(); + }, + style: OutlinedButton.styleFrom( + padding: EdgeInsets.symmetric(vertical: 1.5.h), + side: BorderSide(color: Colors.grey), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(2.5.w), + ), + ), + child: Text( + "Atur Ulang", + style: TextStyle( + color: Colors.black, + fontWeight: FontWeight.bold, + fontSize: AppFontSize.medium.sp, + ), + ), + ), + ), + SizedBox(width: 3.w), + Expanded( + child: OutlinedButton( + onPressed: () => context.pop(), + style: OutlinedButton.styleFrom( + padding: EdgeInsets.symmetric(vertical: 1.5.h), + side: BorderSide(color: AppColor.primaryColor), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(2.5.w), + ), + ), + child: Text( + "Batal", + style: TextStyle( + color: AppColor.primaryColor, + fontWeight: FontWeight.bold, + fontSize: AppFontSize.medium.sp, + ), + ), + ), + ), + SizedBox(width: 3.w), + Expanded( + child: ElevatedButton( + onPressed: tempSelectedDates.length < 2 + ? null + : () { + _initialValue = tempSelectedDates; + final startDate = tempSelectedDates[0]!; + + final endDate = tempSelectedDates[1]!; + + final dateRange = DateTimeRange( + start: startDate, + end: endDate, + ); + + _selectedDateRange = dateRange; + + ref + .read( + filteredByDateTimeProvider( + ReportType.stock_card, + ).notifier, + ) + .state = + dateRange; + + context.pop(); + }, + style: ElevatedButton.styleFrom( + padding: EdgeInsets.symmetric(vertical: 1.5.h), + backgroundColor: AppColor.primaryColor, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(2.5.w), + ), + disabledBackgroundColor: Colors.grey.shade300, + ), + child: Text( + "Pilih", + style: TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + fontSize: AppFontSize.medium.sp, + ), + ), + ), + ), + ], + ), + ), + ], + ), + ); + }, + ); + }, + ); + } + + void _onSelectedOutletChange(String? value) { + _selectedOutlet = value != '0' ? value : null; + ref.read(filteredByOutletProvider(ReportType.stock_card).notifier).state = + _selectedOutlet; + } + + @override + Widget build(BuildContext context) { + super.build(context); + + final isLoggedIn = ref.watch(currentStatusLoginProvider); + + final List sourceOutlets = []; + + final currentOutletId = ref.watch(currentOutletIdProvider); + + final currentUserRole = ref.watch(currentUserRoleProvider); + + final sourceOutletState = ref.watch(sourceOutletStreamProvider(true)); + + final currentOutletName = ref.watch(currentOutletNameProvider); + + if (currentUserRole == UserRole.owner) { + sourceOutletState.whenData((outlets) { + sourceOutlets.addAll(outlets); + }); + } + + final filteredStockReportState = ref.watch(filteredStockReportProvider); + + final stockReportEmptyState = isLoggedIn + ? ref.watch(stockReportEmptyProvider) + : StockReportItemEmpty.empty_database; + + return CustomScrollView( + physics: const ClampingScrollPhysics(), + slivers: [ + SliverAppBar( + pinned: false, + floating: true, + snap: true, + expandedHeight: 16.h, + backgroundColor: Colors.white, + elevation: 0, + flexibleSpace: FlexibleSpaceBar( + background: Material( + color: Colors.white, + elevation: 0, + child: Padding( + padding: EdgeInsets.fromLTRB(5.w, 2.h, 5.w, 1.h), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Row( + children: [ + Expanded( + flex: 3, + child: currentUserRole == UserRole.owner + ? FilteredOutlet( + value: sourceOutletState.isLoading + ? currentOutletId! + : (_selectedOutlet ?? currentOutletId!), + sourceOutlets: sourceOutlets, + onChanged: _onSelectedOutletChange, + ) + : FixedOutlet( + value: + currentOutletName ?? 'Tidak ada outlet', + ), + ), + + SizedBox(width: 3.w), + + Expanded( + flex: 3, + child: FilteredType( + value: _selectedType, + items: ["Semua Barang", "Produk", "Bahan Baku"], + onChanged: (value) { + _selectedType = value!; + final type = value == 'Semua Barang' + ? null + : value == 'Produk' + ? ItemType.product_variant + : ItemType.raw_material; + ref + .read(filteredByItemTypeProvider.notifier) + .state = + type; + }, + ), + ), + ], + ), + + SizedBox(height: 1.5.h), + + FilteredDateRange( + value: _selectedDateRange != null + ? "${_formatGroupDate(_selectedDateRange!.start)} - ${_formatGroupDate(_selectedDateRange!.end)}" + : "Filter Tanggal", + onTap: _showDateRangePicker, + ), + ], + ), + ), + ), + ), + ), + + filteredStockReportState.when( + data: (stockReports) { + switch (stockReportEmptyState) { + case StockReportItemEmpty.loading: + return const SliverToBoxAdapter(child: SizedBox()); + case StockReportItemEmpty.empty_database: + return const SliverToBoxAdapter( + child: CustomEmptyScreen( + title: "Tidak Ada Aktivitas Arus Stok", + body: "Kamu belum melakukan aktivitas arus stok", + ), + ); + case StockReportItemEmpty.empty_search_result: + return SliverMainAxisGroup( + slivers: [ + SliverPersistentHeader( + pinned: true, + delegate: SliverHeaderDelegate( + child: ReportHeader( + rightTitle: "Riwayat Arus Stok", + leftTitle: "Unduh Riwayat", + ), + ), + ), + const SliverToBoxAdapter( + child: CustomEmptyScreen(body: "Barang Tidak Ditemukan"), + ), + ], + ); + case StockReportItemEmpty.has_data: + return SliverMainAxisGroup( + slivers: [ + SliverPersistentHeader( + pinned: true, + delegate: SliverHeaderDelegate( + child: ReportHeader( + rightTitle: "Riwayat Arus Stok", + leftTitle: "Unduh Riwayat", + ), + ), + ), + SliverPadding( + padding: EdgeInsets.symmetric( + horizontal: 5.w, + vertical: 2.h, + ), + sliver: SliverList( + delegate: SliverChildBuilderDelegate((context, index) { + final item = stockReports[index]; + return StockCardItem( + item: item, + onTap: () async { + FocusScope.of(context).unfocus(); + + final dateRange = ref.read( + filteredByDateTimeProvider( + ReportType.stock_card, + ), + ); + + final startDate = dateRange?.start; + + final endDate = dateRange?.end; + + final stockReportDetail = await ref + .read(stockReportRepositoryProvider) + .getStockReportById( + id: item.id, + itemType: item.itemType, + startDate: startDate, + endDate: endDate, + ); + + await Future.delayed( + const Duration(milliseconds: 400), + ); + + context.pushNamed( + AppRoute.stockReportDetailScreen, + extra: stockReportDetail, + ); + }, + ); + }, childCount: stockReports.length), + ), + ), + ], + ); + } + }, + error: (error, stackTrace) { + return SliverToBoxAdapter( + child: CustomErrorScreen( + message: "Ups,terjadi kesalahan", + onRefresh: () {}, + ), + ); + }, + loading: () => + const SliverToBoxAdapter(child: const StockReportShimmer()), + ), + ], + ); + } +} diff --git a/Mobile Operasional/lib/features/report/stock_report/presentation/widgets/footer._item.dart b/Mobile Operasional/lib/features/report/stock_report/presentation/widgets/footer._item.dart new file mode 100644 index 0000000..e69de29 diff --git a/Mobile Operasional/lib/features/report/stock_report/presentation/widgets/header_card.dart b/Mobile Operasional/lib/features/report/stock_report/presentation/widgets/header_card.dart new file mode 100644 index 0000000..bb5f9db --- /dev/null +++ b/Mobile Operasional/lib/features/report/stock_report/presentation/widgets/header_card.dart @@ -0,0 +1,169 @@ +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; +import 'package:niogu_app/core/constants/app_color.dart'; +import 'package:niogu_app/core/constants/app_font_size.dart'; +import 'package:niogu_app/core/utils/extension_format.dart'; +import 'package:niogu_app/features/report/stock_report/domain/entities/stok_report.dart'; +import 'package:sizer/sizer.dart'; + +class HeaderCard extends StatelessWidget { + final bool modeChange; + final String? beforeChange; + final String? afterChange; + final DateTime? dateChange; + final StockReportDetail stockReportDetail; + const HeaderCard({ + super.key, + required this.modeChange, + required this.beforeChange, + required this.afterChange, + required this.dateChange, + required this.stockReportDetail, + }); + + @override + Widget build(BuildContext context) { + final bool isTablet = 100.w >= 600; + + return Container( + margin: EdgeInsets.all(5.w), + padding: EdgeInsets.all(5.w), + decoration: BoxDecoration( + color: AppColor.primaryColor, + borderRadius: BorderRadius.circular(4.w), + boxShadow: [ + BoxShadow( + color: AppColor.primaryColor.withOpacity(0.3), + blurRadius: 10, + offset: const Offset(0, 5), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + stockReportDetail.itemName, + overflow: TextOverflow.ellipsis, + style: TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + fontSize: isTablet ? AppFontSize.medium.sp : AppFontSize.small.sp, + ), + ), + if (stockReportDetail.variantName != null) ...[ + SizedBox(height: 0.75.h), + Text( + stockReportDetail.variantName!, + style: TextStyle( + color: Colors.white, + fontWeight: FontWeight.w400, + fontSize: isTablet + ? (AppFontSize.medium - 1.25).sp + : (AppFontSize.small - 1.25).sp, + ), + ), + ], + SizedBox(height: 1.5.h), + _buildHeaderRow( + Icons.storefront, + stockReportDetail.outletName, + isTablet, + ), + if (stockReportDetail.isUnlimited) ...[ + _buildHeaderRow( + Icons.check_box_outlined, + "Produk Selalu Ada", + isTablet, + ), + if (modeChange) ...[ + _buildHeaderRow( + Icons.update_outlined, + "Perubahan Stok Terbaru ($beforeChange ke $afterChange)", + isTablet, + ), + Padding( + padding: EdgeInsets.only(bottom: 0.8.h), + child: Row( + children: [ + Icon(Icons.date_range, color: Colors.white, size: 4.5.w), + SizedBox(width: 2.5.w), + Text( + "Tanggal Perubahan Terbaru: ${DateFormat('dd MMM yyyy').format(dateChange!)}", + style: TextStyle( + color: Colors.white, + fontWeight: FontWeight.w500, + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + ), + ), + ], + ), + ), + ], + ] else ...[ + if (stockReportDetail.startDate != null && + stockReportDetail.endDate != null) + _buildHeaderRow( + Icons.date_range, + "${DateFormat('dd MMM yyyy').format(stockReportDetail.startDate!)} - ${DateFormat('dd MMM yyyy').format(stockReportDetail.endDate!)}", + isTablet, + ), + _buildHeaderRow( + Icons.inventory_2_outlined, + "Stok Awal ${stockReportDetail.startDate != null ? 'Per ${DateFormat('dd MMM yyyy').format(stockReportDetail.startDate!)}' : ''}: ${stockReportDetail.isUnlimited ? 'Selalu Ada' : (stockReportDetail.initialStock.toStringWithoutTrailingZero() + ' ' + stockReportDetail.unit)}", + isTablet, + ), + if (modeChange) ...[ + _buildHeaderRow( + Icons.update_outlined, + "Perubahan Stok Terbaru ($beforeChange ke $afterChange)", + isTablet, + ), + Padding( + padding: EdgeInsets.only(bottom: 0.8.h), + child: Row( + children: [ + Icon(Icons.date_range, color: Colors.white, size: 4.5.w), + SizedBox(width: 2.5.w), + Text( + "Tanggal Perubahan Terbaru: ${DateFormat('dd MMM yyyy').format(dateChange!)}", + style: TextStyle( + color: Colors.white, + fontWeight: FontWeight.w500, + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + ), + ), + ], + ), + ), + ], + ], + ], + ), + ); + } + + Widget _buildHeaderRow(IconData icon, String text, bool isTablet) { + return Padding( + padding: EdgeInsets.only(bottom: 0.8.h), + child: Row( + children: [ + Icon(icon, color: Colors.white, size: 4.5.w), + SizedBox(width: 2.5.w), + Text( + text, + style: TextStyle( + color: Colors.white, + fontWeight: FontWeight.w500, + fontSize: isTablet ? AppFontSize.medium.sp : AppFontSize.small.sp, + ), + ), + ], + ), + ); + } +} diff --git a/Mobile Operasional/lib/features/report/stock_report/presentation/widgets/header_row.dart b/Mobile Operasional/lib/features/report/stock_report/presentation/widgets/header_row.dart new file mode 100644 index 0000000..e69de29 diff --git a/Mobile Operasional/lib/features/report/stock_report/presentation/widgets/movement_card.dart b/Mobile Operasional/lib/features/report/stock_report/presentation/widgets/movement_card.dart new file mode 100644 index 0000000..c6f6d2f --- /dev/null +++ b/Mobile Operasional/lib/features/report/stock_report/presentation/widgets/movement_card.dart @@ -0,0 +1,282 @@ +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; +import 'package:niogu_app/core/constants/app_color.dart'; +import 'package:niogu_app/core/constants/app_font_size.dart'; +import 'package:niogu_app/core/utils/extension_format.dart'; +import 'package:niogu_app/core/enums/stock_card_type.dart'; +import 'package:niogu_app/core/utils/time_zone.dart'; +import 'package:niogu_app/features/report/stock_report/domain/entities/stok_report.dart'; +import 'package:sizer/sizer.dart'; + +class MovementCard extends StatelessWidget { + final StockMovement movement; + final String unit; + final VoidCallback onTap; + const MovementCard({ + super.key, + required this.movement, + required this.unit, + required this.onTap, + }); + + @override + Widget build(BuildContext context) { + final bool isTablet = 100.w >= 600; + + bool modeChange = movement.modeChange; + + final String? beforeChange = movement.beforeChange; + + final String? afterChange = movement.afterChange; + + modeChange = modeChange && beforeChange != null && afterChange != null; + + final bool isUnlimited = movement.isUnlimited; + + final bool isStockIn = movement.isStockIn; + + final bool isInitialStock = movement.type == StockCardType.initial_stock; + + return GestureDetector( + onTap: onTap, + child: Container( + margin: EdgeInsets.only(bottom: 1.5.h), + padding: EdgeInsets.all(4.w), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(3.w), + border: Border.all(color: Colors.grey.shade200), + ), + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "${DateFormat('dd MMM yyyy HH:mm').format(movement.date)} ${TimeZone.getCurrentTimeZone()}", + style: TextStyle( + fontSize: isTablet + ? (AppFontSize.medium - 1.25).sp + : (AppFontSize.small - 1.25).sp, + color: Colors.grey.shade700, + ), + ), + Text( + movement.userName, + style: TextStyle( + color: Colors.grey[700], + fontWeight: FontWeight.bold, + fontSize: isTablet + ? (AppFontSize.medium - 1.25).sp + : (AppFontSize.small - 1.25).sp, + ), + ), + ], + ), + Divider(height: 3.h, color: Colors.grey[100]), + Row( + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (!isUnlimited || + (isUnlimited && isStockIn && modeChange) || + (isUnlimited && !isInitialStock)) ...[ + Text( + modeChange ? "Perubahan Stok" : movement.description, + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: isTablet + ? (AppFontSize.medium - 1.25).sp + : (AppFontSize.small - 1.25).sp, + ), + ), + ], + if (movement.notes != null && + movement.notes!.isNotEmpty) ...[ + SizedBox(height: 0.5.h), + Text( + movement.notes!, + style: TextStyle( + color: Colors.grey[700], + fontSize: isTablet + ? (AppFontSize.medium - 1.25).sp + : (AppFontSize.small - 1.25).sp, + ), + ), + ], + SizedBox(height: 0.5.h), + + if (isUnlimited && isInitialStock) ...[ + if (modeChange) ...[ + Row( + children: [ + Text( + beforeChange, + style: TextStyle( + color: Colors.grey.shade700, + fontWeight: FontWeight.bold, + fontSize: isTablet + ? (AppFontSize.medium - 1.25).sp + : (AppFontSize.small - 1.25).sp, + ), + ), + + SizedBox(width: 0.5.w), + + Icon( + Icons.arrow_forward_outlined, + color: Colors.grey.shade700, + size: 3.w, + ), + + SizedBox(width: 0.5.w), + + Text( + afterChange, + style: TextStyle( + color: Colors.grey.shade700, + fontWeight: FontWeight.bold, + fontSize: isTablet + ? (AppFontSize.medium - 1.25).sp + : (AppFontSize.small - 1.25).sp, + ), + ), + ], + ), + ] else + Text( + "Pembuatan Stok Baru", + style: TextStyle( + color: Colors.grey.shade700, + fontWeight: FontWeight.bold, + fontSize: isTablet + ? (AppFontSize.medium - 1.25).sp + : (AppFontSize.small - 1.25).sp, + ), + ), + ] else if ((isUnlimited && + !isInitialStock && + !isStockIn) || + (!isUnlimited && !isStockIn)) ...[ + Text( + "${movement.stockOut.toStringWithoutTrailingZero()} $unit", + style: TextStyle( + color: Colors.red[600], + fontWeight: FontWeight.bold, + fontSize: isTablet + ? (AppFontSize.medium - 1.25).sp + : (AppFontSize.small - 1.25).sp, + ), + ), + ] else ...[ + if (modeChange) ...[ + Row( + children: [ + Text( + "${movement.stockIn.toStringWithoutTrailingZero()} $unit", + style: TextStyle( + color: Colors.green.shade600, + fontWeight: FontWeight.bold, + fontSize: isTablet + ? (AppFontSize.medium - 1.25).sp + : (AppFontSize.small - 1.25).sp, + ), + ), + + SizedBox(width: 1.5.w), + + Text( + "• $beforeChange", + style: TextStyle( + color: Colors.grey.shade700, + fontWeight: FontWeight.bold, + fontSize: isTablet + ? (AppFontSize.medium - 1.25).sp + : (AppFontSize.small - 1.25).sp, + ), + ), + + SizedBox(width: 0.5.w), + + Icon( + Icons.arrow_forward_outlined, + color: Colors.grey.shade700, + size: 3.w, + ), + + SizedBox(width: 0.5.w), + + Text( + afterChange, + style: TextStyle( + color: Colors.grey.shade700, + fontWeight: FontWeight.bold, + fontSize: isTablet + ? (AppFontSize.medium - 1.25).sp + : (AppFontSize.small - 1.25).sp, + ), + ), + ], + ), + ] else + Text( + "${movement.stockIn.toStringWithoutTrailingZero()} $unit", + style: TextStyle( + color: Colors.green.shade600, + fontWeight: FontWeight.bold, + fontSize: isTablet + ? (AppFontSize.medium - 1.25).sp + : (AppFontSize.small - 1.25).sp, + ), + ), + ], + ], + ), + ), + Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + if (movement.isUnlimited) ...[ + Text( + "Produk Selalu Ada", + style: TextStyle( + color: AppColor.primaryColor, + fontWeight: FontWeight.bold, + fontSize: isTablet + ? (AppFontSize.medium - 1.25).sp + : (AppFontSize.small - 1.25).sp, + ), + ), + ] else ...[ + Text( + "Sisa Stok", + style: TextStyle( + color: Colors.grey, + fontSize: isTablet + ? (AppFontSize.medium - 1.25).sp + : (AppFontSize.small - 1.25).sp, + ), + ), + Text( + "${movement.remainingStock.toStringWithoutTrailingZero() + ' ' + unit}", + style: TextStyle( + color: AppColor.primaryColor, + fontWeight: FontWeight.bold, + fontSize: isTablet + ? (AppFontSize.medium - 1.25).sp + : (AppFontSize.small - 1.25).sp, + ), + ), + ], + ], + ), + ], + ), + ], + ), + ), + ); + } +} diff --git a/Mobile Operasional/lib/features/report/stock_report/presentation/widgets/sticky_footer.dart b/Mobile Operasional/lib/features/report/stock_report/presentation/widgets/sticky_footer.dart new file mode 100644 index 0000000..14d9205 --- /dev/null +++ b/Mobile Operasional/lib/features/report/stock_report/presentation/widgets/sticky_footer.dart @@ -0,0 +1,156 @@ +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; +import 'package:niogu_app/core/constants/app_color.dart'; +import 'package:niogu_app/core/constants/app_font_size.dart'; +import 'package:niogu_app/core/utils/extension_format.dart'; +import 'package:niogu_app/features/report/stock_report/domain/entities/stok_report.dart'; +import 'package:sizer/sizer.dart'; + +class StickyFooter extends StatelessWidget { + final bool modeChange; + final StockReportDetail stockReportDetail; + const StickyFooter({ + super.key, + required this.modeChange, + required this.stockReportDetail, + }); + + @override + Widget build(BuildContext context) { + final bool isTablet = 100.w >= 600; + return Container( + padding: EdgeInsets.fromLTRB(5.w, 1.5.h, 5.w, 2.h), + decoration: BoxDecoration( + color: Colors.white, + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.05), + blurRadius: 10, + offset: const Offset(0, -5), + ), + ], + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + if (stockReportDetail.isUnlimited) ...[ + if (modeChange) ...[ + SizedBox(height: 0.75.h), + Text( + "Total terjual dihitung sejak tanggal perubahan stok terbaru", + style: TextStyle( + fontSize: isTablet + ? (AppFontSize.medium - 1.25).sp + : (AppFontSize.small - 1.25).sp, + color: Colors.grey[700], + fontStyle: FontStyle.italic, + ), + ), + ], + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + "Total terjual ${stockReportDetail.endDate != null ? 'hingga ${DateFormat('dd MMMM yyyy').format(stockReportDetail.endDate!)}' : 'seluruh periode'}", + style: TextStyle( + color: Colors.black, + fontSize: isTablet + ? (AppFontSize.medium - 1.25).sp + : (AppFontSize.small - 1.25).sp, + fontWeight: FontWeight.bold, + ), + ), + SizedBox(width: 2.5.w), + Text( + "${stockReportDetail.totalStockOut.toStringWithoutTrailingZero()} ${stockReportDetail.unit}", + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: isTablet + ? (AppFontSize.medium - 1.25).sp + : (AppFontSize.small - 1.25).sp, + color: Colors.red[700]!, + ), + ), + ], + ), + ] else ...[ + if (modeChange) ...[ + Text( + "Akumulasi dihitung sejak tanggal perubahan stok terbaru", + style: TextStyle( + fontSize: isTablet + ? (AppFontSize.medium - 1.25).sp + : (AppFontSize.small - 1.25).sp, + color: Colors.grey[700], + fontStyle: FontStyle.italic, + ), + ), + SizedBox(height: 0.75.h), + ], + Text( + "Informasi akumulasi ${stockReportDetail.endDate != null ? 'per ${DateFormat('dd MMMM yyyy').format(stockReportDetail.endDate!)}' : 'seluruh periode'}", + style: TextStyle( + fontSize: isTablet + ? (AppFontSize.medium - 1.25).sp + : (AppFontSize.small - 1.25).sp, + color: Colors.black, + fontWeight: FontWeight.bold, + ), + ), + SizedBox(height: 1.5.h), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + _footerItem( + "Total Masuk", + "${(stockReportDetail.totalStockIn.toStringWithoutTrailingZero() + ' ' + stockReportDetail.unit)}", + Colors.green[700]!, + isTablet, + ), + _footerItem( + "Total Keluar", + "${stockReportDetail.totalStockOut.toStringWithoutTrailingZero()} ${stockReportDetail.unit}", + Colors.red[700]!, + isTablet, + ), + _footerItem( + "Stok Akhir", + "${(stockReportDetail.lastStock.toStringWithoutTrailingZero() + ' ' + stockReportDetail.unit)}", + AppColor.primaryColor, + isTablet, + ), + ], + ), + ], + ], + ), + ); + } + + Widget _footerItem(String label, String value, Color color, bool isTablet) { + return Column( + children: [ + Text( + label, + style: TextStyle( + fontSize: isTablet + ? (AppFontSize.medium - 1.25).sp + : (AppFontSize.small - 1.25).sp, + color: Colors.grey, + ), + ), + SizedBox(height: 0.5.h), + Text( + value, + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: isTablet + ? (AppFontSize.medium - 1.25).sp + : (AppFontSize.small - 1.25).sp, + color: color, + ), + ), + ], + ); + } +} diff --git a/Mobile Operasional/lib/features/report/stock_report/presentation/widgets/stock_card_item.dart b/Mobile Operasional/lib/features/report/stock_report/presentation/widgets/stock_card_item.dart new file mode 100644 index 0000000..4292842 --- /dev/null +++ b/Mobile Operasional/lib/features/report/stock_report/presentation/widgets/stock_card_item.dart @@ -0,0 +1,272 @@ +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:niogu_app/core/constants/app_color.dart'; +import 'package:niogu_app/core/constants/app_font_size.dart'; +import 'package:niogu_app/core/utils/extension_format.dart'; +import 'package:niogu_app/core/enums/item_type.dart'; +import 'package:niogu_app/features/report/stock_report/domain/entities/stok_report.dart'; +import 'package:sizer/sizer.dart'; + +class StockCardItem extends StatelessWidget { + final StockReportItem item; + final VoidCallback onTap; + const StockCardItem({super.key, required this.item, required this.onTap}); + + @override + Widget build(BuildContext context) { + final bool isTablet = 100.w >= 600; + + final bool imagePathAvailable = + item.imageProduct != null && + item.imageProduct!.isNotEmpty && + item.imageProduct!.isNotEmpty; + + final File imageFile = File( + imagePathAvailable ? item.imageProduct! : "image not found", + ); + + final bool imageFileExists = imageFile.existsSync(); + + return GestureDetector( + onTap: onTap, + child: Container( + clipBehavior: Clip.antiAlias, + margin: EdgeInsets.only(bottom: 2.h), + padding: EdgeInsets.all(4.w), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(2.5.w), + border: Border.all(color: Colors.grey.shade200), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.02), + blurRadius: 10, + offset: const Offset(0, 4), + ), + ], + ), + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Container( + width: 18.w, + height: 18.w, + decoration: BoxDecoration( + border: Border.all(color: Colors.grey.shade200, width: 1.5), + borderRadius: BorderRadius.circular(2.5.w), + ), + child: imageFileExists + ? ClipRRect( + borderRadius: BorderRadius.circular(2.5.w), + child: Image.file( + imageFile, + fit: BoxFit.cover, + width: double.infinity, + height: double.infinity, + errorBuilder: (context, error, stackTrace) { + return Icon( + Icons.error, + size: 8.w, + color: Colors.grey.shade400, + ); + }, + ), + ) + : Center( + child: Icon( + item.itemType == ItemType.product_variant + ? Icons.image + : Icons.inventory, + size: 8.w, + color: Colors.grey.shade400, + ), + ), + ), + SizedBox(width: 2.w), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + item.name, + style: TextStyle( + color: item.hasBeenDelete + ? Colors.grey.shade500 + : Colors.grey.shade700, + fontWeight: FontWeight.bold, + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + decoration: item.hasBeenDelete + ? TextDecoration.lineThrough + : TextDecoration.none, + ), + ), + + if (item.variantName != null && + item.variantName!.isNotEmpty) ...[ + SizedBox(height: 0.75.h), + Text( + item.variantName!, + style: TextStyle( + color: item.hasBeenDelete + ? Colors.grey.shade500 + : Colors.grey.shade700, + fontSize: isTablet + ? (AppFontSize.medium - 1.25).sp + : (AppFontSize.small - 1.25).sp, + decoration: item.hasBeenDelete + ? TextDecoration.lineThrough + : TextDecoration.none, + ), + ), + ], + + if (item.hasBeenDelete) ...[ + SizedBox(height: 0.75.h), + Text( + "Barang Dihapus ${item.isDeleteByOwner ? "Oleh Owner" : ""}", + style: TextStyle( + color: Colors.red.shade300, + fontSize: isTablet + ? (AppFontSize.medium - 1.25).sp + : (AppFontSize.small - 1.25).sp, + ), + ), + ], + ], + ), + ), + ], + ), + SizedBox(height: 2.h), + + Table( + children: [ + if (item.isUnlimited) ...[ + _buildTableRow( + "Terjual", + "${item.totalStockOut.toStringWithoutTrailingZero()} ${item.unit}", + Colors.red[600]!, + isTablet, + ), + ] else ...[ + _buildTableRow( + "Stok Awal", + "${item.initialStock.toStringWithoutTrailingZero()} ${item.unit}", + item.isUnlimited ? Colors.black : Colors.grey[700]!, + isTablet, + ), + _buildTableRow( + "Stok Masuk", + "${item.totalStockIn.toStringWithoutTrailingZero()} ${item.unit}", + item.isUnlimited ? Colors.black : Colors.green[600]!, + isTablet, + ), + _buildTableRow( + "Stok Keluar", + "${item.totalStockOut.toStringWithoutTrailingZero()} ${item.unit}", + Colors.red[600]!, + isTablet, + ), + ], + ], + ), + + Divider(height: 3.h, color: Colors.grey[100]), + + Row( + mainAxisAlignment: item.isUnlimited + ? MainAxisAlignment.center + : MainAxisAlignment.spaceBetween, + children: [ + if (item.isUnlimited) ...[ + Text( + "Produk Selalu Ada", + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: isTablet + ? (AppFontSize.medium - 1.25).sp + : (AppFontSize.small - 1.25).sp, + ), + ), + ] else ...[ + Text( + "Stok Akhir", + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: isTablet + ? (AppFontSize.medium - 1.25).sp + : (AppFontSize.small - 1.25).sp, + ), + ), + Container( + padding: EdgeInsets.symmetric( + horizontal: 3.w, + vertical: 0.5.h, + ), + decoration: BoxDecoration( + color: AppColor.primaryColor.withOpacity(0.1), + borderRadius: BorderRadius.circular(1.5.w), + ), + child: Text( + "${item.lastStock.toStringWithoutTrailingZero()} ${item.unit}", + style: TextStyle( + color: AppColor.primaryColor, + fontWeight: FontWeight.bold, + fontSize: isTablet + ? (AppFontSize.medium - 1.25).sp + : (AppFontSize.small - 1.25).sp, + ), + ), + ), + ], + ], + ), + ], + ), + ), + ); + } + + TableRow _buildTableRow( + String label, + String value, + Color valueColor, + bool isTablet, + ) { + return TableRow( + children: [ + Padding( + padding: EdgeInsets.symmetric(vertical: 0.8.h), + child: Text( + label, + style: TextStyle( + color: Colors.grey[600], + fontSize: isTablet + ? (AppFontSize.medium - 1.25).sp + : (AppFontSize.small - 1.25).sp, + ), + ), + ), + Padding( + padding: EdgeInsets.symmetric(vertical: 0.8.h), + child: Text( + value, + textAlign: TextAlign.right, + style: TextStyle( + color: valueColor, + fontWeight: FontWeight.w600, + fontSize: isTablet + ? (AppFontSize.medium - 1.25).sp + : (AppFontSize.small - 1.25).sp, + ), + ), + ), + ], + ); + } +} diff --git a/Mobile Operasional/lib/features/report/stock_report/presentation/widgets/stock_report_shimmer.dart b/Mobile Operasional/lib/features/report/stock_report/presentation/widgets/stock_report_shimmer.dart new file mode 100644 index 0000000..f02b066 --- /dev/null +++ b/Mobile Operasional/lib/features/report/stock_report/presentation/widgets/stock_report_shimmer.dart @@ -0,0 +1,71 @@ +import 'package:flutter/material.dart'; +import 'package:shimmer/shimmer.dart'; +import 'package:sizer/sizer.dart'; + +class StockReportShimmer extends StatelessWidget { + const StockReportShimmer({super.key}); + + @override + Widget build(BuildContext context) { + return Shimmer.fromColors( + baseColor: Colors.grey.shade300, + highlightColor: Colors.grey.shade100, + child: Column( + children: List.generate(3, (index) => _buildShimmerCard()), + ), + ); + } + + Widget _buildShimmerCard() { + return Container( + margin: EdgeInsets.symmetric(horizontal: 5.w, vertical: 2.h), + padding: EdgeInsets.all(4.w), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(3.w), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Container(width: 10.w, height: 10.w, color: Colors.white), + SizedBox(width: 3.w), + Container(width: 40.w, height: 2.h, color: Colors.white), + ], + ), + SizedBox(height: 3.h), + Column( + children: List.generate(3, (i) => Padding( + padding: EdgeInsets.only(bottom: 1.5.h), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Container(width: 25.w, height: 1.5.h, color: Colors.white), + Container(width: 15.w, height: 1.5.h, color: Colors.white), + ], + ), + )), + ), + + Divider(height: 3.h, color: Colors.white), + + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Container(width: 20.w, height: 2.h, color: Colors.white), + Container( + width: 25.w, + height: 4.h, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(2.w), + ), + ), + ], + ), + ], + ), + ); + } +} \ No newline at end of file diff --git a/Mobile Operasional/lib/features/report/transaction/data/repositories/transaction_report_repository_impl.dart b/Mobile Operasional/lib/features/report/transaction/data/repositories/transaction_report_repository_impl.dart new file mode 100644 index 0000000..97195ed --- /dev/null +++ b/Mobile Operasional/lib/features/report/transaction/data/repositories/transaction_report_repository_impl.dart @@ -0,0 +1,45 @@ +import 'package:niogu_app/core/database/app_database.dart'; +import 'package:niogu_app/core/enums/transaction_reference_type.dart'; +import 'package:niogu_app/features/report/transaction/domain/entities/transaction_report.dart'; +import 'package:niogu_app/features/report/transaction/domain/repositories/i_transaction_report_repository.dart'; + +class TransactionReportRepositoryImpl implements ITransactionReportRepository { + final AppDatabase _appDatabase; + + TransactionReportRepositoryImpl(this._appDatabase); + + @override + Stream> watchTransactionItems({ + String? outletId, + TransactionType? type, + DateTime? startDate, + DateTime? endDate, + }) { + return _appDatabase.transactionDao.watchAllTransactionItems( + outletId: outletId, + type: type, + startDate: startDate, + endDate: endDate, + ); + } + + @override + Future getOrderDetail(String orderId) async { + return await _appDatabase.orderDao.getOrderDetail(orderId); + } + + @override + Future getPurchaseDetail(String purchaseId) async { + return await _appDatabase.purchaseDao.getPurchaseDetail(purchaseId); + } + + @override + Future cancelOrder(String orderId) async { + return await _appDatabase.orderDao.cancelOrder(orderId); + } + + @override + Future cancelPurchase(String purchaseId) async { + return await _appDatabase.purchaseDao.cancelPurchase(purchaseId); + } +} diff --git a/Mobile Operasional/lib/features/report/transaction/domain/entities/transaction_report.dart b/Mobile Operasional/lib/features/report/transaction/domain/entities/transaction_report.dart new file mode 100644 index 0000000..a049d09 --- /dev/null +++ b/Mobile Operasional/lib/features/report/transaction/domain/entities/transaction_report.dart @@ -0,0 +1,157 @@ +import 'package:niogu_app/core/enums/order_source.dart'; +import 'package:niogu_app/core/enums/order_status.dart'; +import 'package:niogu_app/core/enums/payment_status.dart'; +import 'package:niogu_app/core/enums/purchase_status.dart'; + +enum TransactionReportItemEmpty { + loading, + empty_database, + empty_search_result, + has_data, +} + +class TransactionReportItem { + final String id; + final String transactionNumber; + final String title; + final String subtitle; + final double amount; + final DateTime date; + final bool isIncome; + + TransactionReportItem({ + required this.id, + required this.transactionNumber, + required this.title, + required this.subtitle, + required this.amount, + required this.date, + required this.isIncome, + }); +} + +class OrderDetail { + final String orderId; + final String orderNumber; + final DateTime orderDate; + final OrderSource orderSource; + final OrderStatus orderStatus; + final String outletName; + final String userName; + final String? localPhotoPath; + final String? customerName; + final String? customerPhone; + final String? customerAddress; + final String? note; + final List items; + final double totalOrder; + final double discount; + final double tax; + final double totalAmount; + final double payAmount; + final double changeAmount; + final String? paymentProofPath; + + OrderDetail({ + required this.orderId, + required this.orderNumber, + required this.orderDate, + required this.orderSource, + required this.orderStatus, + required this.outletName, + required this.userName, + this.localPhotoPath, + this.customerName, + this.customerPhone, + this.customerAddress, + this.note, + required this.items, + required this.totalOrder, + required this.discount, + required this.tax, + required this.totalAmount, + required this.payAmount, + required this.changeAmount, + required this.paymentProofPath + }); +} + +class OrderItem { + final String name; + final String? variantName; + final double sellingPrice; + final double quantity; + final double subtotal; + + const OrderItem({ + required this.name, + this.variantName, + required this.sellingPrice, + required this.quantity, + required this.subtotal, + }); +} + +class PurchaseDetail { + final String purchaseId; + final String purchaseNumber; + final DateTime purchaseDate; + final String outletName; + final String userName; + final PurchaseStatus purchaseStatus; + final String? localPhotoPath; + final String? supplierName; + final String? supplierPhone; + final String? note; + final List items; + final double totalPurchase; + final double discount; + final double tax; + final PaymentStatus paymentStatus; + final double totalAmount; + final double payAmount; + final double changeAmount; + final double underPaymentAmount; + final double debtAmount; + final String? paymentProofPath; + + const PurchaseDetail({ + required this.purchaseId, + required this.purchaseNumber, + required this.purchaseDate, + required this.userName, + required this.purchaseStatus, + required this.outletName, + this.localPhotoPath, + this.supplierName, + this.supplierPhone, + this.note, + required this.items, + required this.totalPurchase, + required this.discount, + required this.tax, + required this.paymentStatus, + required this.totalAmount, + required this.payAmount, + required this.changeAmount, + required this.underPaymentAmount, + required this.debtAmount, + this.paymentProofPath, + }); +} + +class PurchaseItem { + final String name; + final String? variantName; + final double purchasePrice; + final double quantity; + final double subtotal; + + const PurchaseItem({ + required this.name, + this.variantName, + required this.purchasePrice, + required this.quantity, + required this.subtotal, + }); +} diff --git a/Mobile Operasional/lib/features/report/transaction/domain/repositories/i_transaction_report_repository.dart b/Mobile Operasional/lib/features/report/transaction/domain/repositories/i_transaction_report_repository.dart new file mode 100644 index 0000000..418a1a4 --- /dev/null +++ b/Mobile Operasional/lib/features/report/transaction/domain/repositories/i_transaction_report_repository.dart @@ -0,0 +1,19 @@ +import 'package:niogu_app/core/enums/transaction_reference_type.dart'; +import 'package:niogu_app/features/report/transaction/domain/entities/transaction_report.dart'; + +abstract class ITransactionReportRepository { + Stream> watchTransactionItems({ + String? outletId, + TransactionType? type, + DateTime? startDate, + DateTime? endDate, + }); + + Future getOrderDetail(String orderId); + + Future getPurchaseDetail(String purchaseId); + + Future cancelOrder(String orderId); + + Future cancelPurchase(String purchaseId); +} \ No newline at end of file diff --git a/Mobile Operasional/lib/features/report/transaction/presentation/controllers/transaction_report_controller.dart b/Mobile Operasional/lib/features/report/transaction/presentation/controllers/transaction_report_controller.dart new file mode 100644 index 0000000..120b8c7 --- /dev/null +++ b/Mobile Operasional/lib/features/report/transaction/presentation/controllers/transaction_report_controller.dart @@ -0,0 +1,39 @@ +import 'dart:async'; + +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:niogu_app/features/report/transaction/domain/repositories/i_transaction_report_repository.dart'; +import 'package:niogu_app/features/report/transaction/presentation/providers/transaction_report_provider.dart'; + +class TransactionReportController extends AutoDisposeAsyncNotifier { + late final ITransactionReportRepository _transactionReportRepository; + @override + FutureOr build() { + _transactionReportRepository = ref.read( + transactionReportRepositoryProvider, + ); + } + + Future cancelOrder(String orderId) async { + state = const AsyncValue.loading(); + + final result = await AsyncValue.guard(() async { + await _transactionReportRepository.cancelOrder(orderId); + }); + + state = result; + + if (result is AsyncError) throw result.error!; + } + + Future cancelPurchase(String purchaseId) async { + state = const AsyncValue.loading(); + + final result = await AsyncValue.guard(() async { + await _transactionReportRepository.cancelPurchase(purchaseId); + }); + + state = result; + + if (result is AsyncError) throw result.error!; + } +} diff --git a/Mobile Operasional/lib/features/report/transaction/presentation/providers/transaction_report_provider.dart b/Mobile Operasional/lib/features/report/transaction/presentation/providers/transaction_report_provider.dart new file mode 100644 index 0000000..6c01f02 --- /dev/null +++ b/Mobile Operasional/lib/features/report/transaction/presentation/providers/transaction_report_provider.dart @@ -0,0 +1,126 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:niogu_app/core/providers/app_provider.dart'; +import 'package:niogu_app/core/enums/report_type.dart'; +import 'package:niogu_app/core/enums/transaction_reference_type.dart'; +import 'package:niogu_app/features/report/presentation/providers/report_provider.dart'; +import 'package:niogu_app/features/report/transaction/data/repositories/transaction_report_repository_impl.dart'; +import 'package:niogu_app/features/report/transaction/domain/entities/transaction_report.dart'; +import 'package:niogu_app/features/report/transaction/domain/repositories/i_transaction_report_repository.dart'; +import 'package:niogu_app/features/report/transaction/presentation/controllers/transaction_report_controller.dart'; + +final transactionReportRepositoryProvider = + Provider((ref) { + final appDatabase = ref.watch(appDatabaseProvider); + return TransactionReportRepositoryImpl(appDatabase); + }); + +final filteredByTransactionTypeProvider = + StateProvider.autoDispose((ref) => null); + +final transactionReportStreamProvider = + StreamProvider.autoDispose>((ref) { + final isLoggedIn = ref.watch(currentStatusLoginProvider); + + final transactionReportRepo = ref.watch( + transactionReportRepositoryProvider, + ); + + final outletId = ref.watch( + filteredByOutletProvider(ReportType.transaction), + ); + + final type = ref.watch(filteredByTransactionTypeProvider); + + final dateRange = ref.watch( + filteredByDateTimeProvider(ReportType.transaction), + ); + + final startDate = dateRange?.start; + + final endDate = dateRange?.end; + + if (!isLoggedIn) { + return Stream>.value([]); + } + + return transactionReportRepo.watchTransactionItems( + outletId: outletId, + type: type, + startDate: startDate, + endDate: endDate, + ); + }); + +final transactionReportSearchProvider = StateProvider.autoDispose( + (ref) => '', +); + +final filteredTransactionReportProvider = + Provider.autoDispose>>((ref) { + final transactionReportStreamAsync = ref.watch( + transactionReportStreamProvider, + ); + + final transactionReportSearchAsync = ref.watch( + transactionReportSearchProvider, + ); + + return transactionReportStreamAsync.when( + data: (transactions) { + if (transactionReportSearchAsync.trim().isEmpty) { + return AsyncValue.data(transactions); + } + + final filteredTransaction = transactions.where((transaction) { + return transaction.transactionNumber.toLowerCase().contains( + transactionReportSearchAsync.toLowerCase(), + ); + }).toList(); + + return AsyncValue.data(filteredTransaction); + }, + error: (error, stackTrace) { + return AsyncValue.error(error, stackTrace); + }, + loading: () => const AsyncValue.loading(), + ); + }); + +final transactionReportEmptyProvider = + Provider.autoDispose((ref) { + final transactionReportStreamAsync = ref.watch( + transactionReportStreamProvider, + ); + + final filteredTransactionReportAsync = ref.watch( + filteredTransactionReportProvider, + ); + + final transactionReportSearchAsync = ref.watch( + transactionReportSearchProvider, + ); + + if (transactionReportStreamAsync.isLoading) { + return TransactionReportItemEmpty.loading; + } + + final allTransactions = transactionReportStreamAsync.value ?? []; + + if (allTransactions.isEmpty) { + return TransactionReportItemEmpty.empty_database; + } + + final filteredTransaction = filteredTransactionReportAsync.value ?? []; + + if (transactionReportSearchAsync.isNotEmpty && + filteredTransaction.isEmpty) { + return TransactionReportItemEmpty.empty_search_result; + } + + return TransactionReportItemEmpty.has_data; + }); + +final transactionReportControllerProvider = + AsyncNotifierProvider.autoDispose( + TransactionReportController.new, + ); diff --git a/Mobile Operasional/lib/features/report/transaction/presentation/screens/transaction_report_order_detail_screen.dart b/Mobile Operasional/lib/features/report/transaction/presentation/screens/transaction_report_order_detail_screen.dart new file mode 100644 index 0000000..e058391 --- /dev/null +++ b/Mobile Operasional/lib/features/report/transaction/presentation/screens/transaction_report_order_detail_screen.dart @@ -0,0 +1,411 @@ +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:gal/gal.dart'; +import 'package:go_router/go_router.dart'; +import 'package:niogu_app/core/router/app_route.dart'; +import 'package:niogu_app/core/enums/order_source.dart'; +import 'package:niogu_app/core/enums/order_status.dart'; +import 'package:niogu_app/core/widgets/custom_confirmation.dart'; +import 'package:niogu_app/features/online_store/presentation/providers/online_store_provider.dart'; +import 'package:niogu_app/features/report/transaction/presentation/providers/transaction_report_provider.dart'; +import 'package:niogu_app/features/report/transaction/presentation/widgets/dashed_divider.dart'; +import 'package:niogu_app/features/report/transaction/presentation/widgets/payment_proof.dart'; +import 'package:permission_handler/permission_handler.dart'; +import 'package:niogu_app/core/components/top_back_bar_app.dart'; +import 'package:niogu_app/core/utils/log_message.dart'; +import 'package:niogu_app/core/widgets/custom_snackbar.dart'; +import 'package:niogu_app/features/report/transaction/domain/entities/transaction_report.dart'; +import 'package:niogu_app/features/report/transaction/presentation/widgets/brand_order_header.dart'; +import 'package:niogu_app/features/report/transaction/presentation/widgets/footer_order_info.dart'; +import 'package:niogu_app/features/report/transaction/presentation/widgets/general_order_info.dart'; +import 'package:niogu_app/features/report/transaction/presentation/widgets/order_item_list.dart'; +import 'package:niogu_app/features/report/transaction/presentation/widgets/payment_order_summary.dart'; +import 'package:path_provider/path_provider.dart'; +import 'package:screenshot/screenshot.dart'; +import 'package:share_plus/share_plus.dart'; +import 'package:sizer/sizer.dart'; +import 'package:niogu_app/core/constants/app_color.dart'; +import 'package:niogu_app/core/constants/app_font_size.dart'; + +class TransactionReportOrderDetailScreen extends ConsumerWidget { + final OrderDetail orderDetail; + + TransactionReportOrderDetailScreen({super.key, required this.orderDetail}); + + final ScreenshotController screenshotController = ScreenshotController(); + + Future _handleShareAction(BuildContext context) async { + final bool isTablet = 100.w >= 600; + showModalBottomSheet( + context: context, + backgroundColor: Colors.white, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.vertical(top: Radius.circular(2.5.w)), + ), + constraints: const BoxConstraints(maxWidth: double.infinity), + builder: (BuildContext context) { + return SafeArea( + child: Container( + width: 100.w, + padding: EdgeInsets.symmetric(vertical: 2.h, horizontal: 5.w), + clipBehavior: Clip.hardEdge, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.vertical(top: Radius.circular(6.w)), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + ListTile( + leading: Icon(Icons.download_rounded, size: 6.w), + title: Text( + 'Unduh', + style: TextStyle( + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + ), + ), + onTap: () async { + await _processImage(context, isDownload: true); + context.pop(); + }, + ), + + SizedBox(height: 2.h), + + ListTile( + leading: Icon(Icons.share_rounded, size: 6.w), + title: Text( + 'Bagikan', + style: TextStyle( + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + ), + ), + onTap: () async { + await _processImage(context, isDownload: false); + context.pop(); + }, + ), + ], + ), + ), + ); + }, + ); + } + + Future _processImage( + BuildContext context, { + required bool isDownload, + }) async { + try { + final image = await screenshotController.capture(); + if (image == null) return; + + if (isDownload) { + if (Platform.isAndroid) { + final request = await Permission.storage.request(); + + if (request.isDenied) { + CustomSnackbar.showWarning(context, "Akses Ditolak"); + return; + } + } + + await Gal.putImageBytes(image); + + if (!context.mounted) return; + CustomSnackbar.showSuccess( + context, + "Gambar berhasil disimpan ke galeri", + ); + } else { + final directory = await getTemporaryDirectory(); + + final imagePath = await File( + '${directory.path}/transaction.png', + ).create(); + + await imagePath.writeAsBytes(image); + + await Share.shareXFiles([XFile(imagePath.path)]); + } + } catch (e, st) { + if (!context.mounted) return; + LogMessage.log.e(e.toString(), error: e, stackTrace: st); + CustomSnackbar.showError(context, "Ups, terjadi kesalahan"); + } + } + + Future _cancelOrder( + BuildContext context, + WidgetRef ref, + String orderId, + ) async { + try { + await ref + .read(transactionReportControllerProvider.notifier) + .cancelOrder(orderId); + + if (!context.mounted) return; + + CustomSnackbar.showSuccess(context, "Transaksi berhasil dibatalkan"); + + context.pop(); + } catch (e, st) { + LogMessage.log.e(e.toString(), error: e, stackTrace: st); + + CustomSnackbar.showError(context, "Ups, terjadi kesalahan"); + + context.pop(); + } + } + + @override + Widget build(BuildContext context, WidgetRef ref) { + return LayoutBuilder( + builder: (context, constraints) { + final bool isTablet = 100.w >= 600; + + final transactionReportControllerState = ref.watch( + transactionReportControllerProvider, + ); + + return SafeArea( + top: false, + bottom: true, + right: false, + left: false, + child: Scaffold( + backgroundColor: const Color(0xFFF5F5F5), + appBar: TopBackBarApp( + title: "Detail Transaksi", + onTap: () => context.pop(), + ), + body: SingleChildScrollView( + padding: EdgeInsets.fromLTRB(4.w, 2.h, 4.w, 12.h), + child: Column( + children: [ + Screenshot( + controller: screenshotController, + child: Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(4.w), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.05), + blurRadius: 15, + offset: const Offset(0, 5), + ), + ], + ), + child: Column( + children: [ + BrandOrderHeader(orderDetail: orderDetail), + + const DashedDivider(), + + GeneralOrderInfo(orderDetail: orderDetail), + + const DashedDivider(), + + OrderItemList(orderDetail: orderDetail), + + const DashedDivider(), + + PaymentOrderSummary(orderDetail: orderDetail), + + if (orderDetail.paymentProofPath != null && + orderDetail.paymentProofPath!.isNotEmpty) ...[ + const DashedDivider(), + + PaymentProof( + imagePath: orderDetail.paymentProofPath!, + ), + ], + + if (orderDetail.note != null || + orderDetail.customerName != null) ...[ + const DashedDivider(), + FooterOrderInfo(orderDetail: orderDetail), + ], + + SizedBox(height: 3.h), + ], + ), + ), + ), + + SizedBox(height: 2.h), + + Padding( + padding: EdgeInsets.symmetric(horizontal: 5.w), + child: + orderDetail.orderSource == OrderSource.online || + (orderDetail.orderSource == OrderSource.pos && + orderDetail.orderStatus != OrderStatus.canceled) + ? TextButton.icon( + onPressed: () async { + if (orderDetail.orderSource == + OrderSource.online) { + final onlineOrderDetail = await ref + .read(onlineStoreRepositoryProvider) + .fetchOnlineOrderDetailById( + orderDetail.orderId, + ); + + await Future.delayed( + const Duration(milliseconds: 800), + () { + context.pushNamed( + AppRoute.onlineStoreOrderDetilScreen, + extra: onlineOrderDetail, + ); + }, + ); + } else { + showDialog( + context: context, + builder: (context) { + final orderId = orderDetail.orderId; + return CustomConfirmation( + title: "Batalkan Transaksi", + body: + "Batalkan transaksi ${orderDetail.orderNumber}", + color: Color(0xFFFFA000), + icon: Icons.warning_amber_rounded, + onConfirm: + transactionReportControllerState + .isLoading + ? null + : () async => await _cancelOrder( + context, + ref, + orderId, + ), + ); + }, + ); + } + }, + style: TextButton.styleFrom( + foregroundColor: + orderDetail.orderSource == OrderSource.online + ? AppColor.primaryColor + : Colors.red[700], + padding: EdgeInsets.symmetric( + vertical: 1.5.h, + horizontal: 4.w, + ), + ), + icon: orderDetail.orderSource == OrderSource.online + ? null + : Icon(Icons.close_rounded, size: 5.w), + label: Text( + orderDetail.orderSource == OrderSource.online + ? "Lihat Detail Pesanan Online" + : "Batalkan Transaksi Ini", + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + ), + ), + ) + : Text( + "Transaksi Dibatalkan", + style: TextStyle( + color: Colors.red.shade700, + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + fontWeight: FontWeight.w500, + ), + ), + ), + ], + ), + ), + + bottomNavigationBar: Container( + padding: EdgeInsets.all(4.w), + decoration: BoxDecoration( + color: Colors.white, + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.05), + blurRadius: 10, + offset: const Offset(0, -5), + ), + ], + ), + child: Row( + children: [ + Expanded( + child: OutlinedButton.icon( + onPressed: () => _handleShareAction(context), + style: OutlinedButton.styleFrom( + padding: EdgeInsets.symmetric(vertical: 1.8.h), + side: BorderSide(color: AppColor.primaryColor), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(2.5.w), + ), + ), + icon: Icon( + Icons.share_outlined, + color: AppColor.primaryColor, + size: 5.w, + ), + label: Text( + "Bagikan", + style: TextStyle( + color: AppColor.primaryColor, + fontWeight: FontWeight.bold, + fontSize: AppFontSize.medium.sp, + ), + ), + ), + ), + SizedBox(width: 3.w), + Expanded( + child: ElevatedButton.icon( + onPressed: () {}, + style: ElevatedButton.styleFrom( + padding: EdgeInsets.symmetric(vertical: 1.8.h), + backgroundColor: AppColor.primaryColor, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(2.5.w), + ), + elevation: 0, + ), + icon: Icon( + Icons.print_outlined, + color: Colors.white, + size: 5.w, + ), + label: Text( + "Cetak Struk", + style: TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + fontSize: AppFontSize.medium.sp, + ), + ), + ), + ), + ], + ), + ), + ), + ); + }, + ); + } +} diff --git a/Mobile Operasional/lib/features/report/transaction/presentation/screens/transaction_report_purchase_detail_screen.dart b/Mobile Operasional/lib/features/report/transaction/presentation/screens/transaction_report_purchase_detail_screen.dart new file mode 100644 index 0000000..a300af1 --- /dev/null +++ b/Mobile Operasional/lib/features/report/transaction/presentation/screens/transaction_report_purchase_detail_screen.dart @@ -0,0 +1,395 @@ +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:gal/gal.dart'; +import 'package:go_router/go_router.dart'; +import 'package:niogu_app/core/components/top_back_bar_app.dart'; +import 'package:niogu_app/core/constants/app_color.dart'; +import 'package:niogu_app/core/constants/app_font_size.dart'; +import 'package:niogu_app/core/utils/log_message.dart'; +import 'package:niogu_app/core/enums/purchase_status.dart'; +import 'package:niogu_app/core/widgets/custom_confirmation.dart'; +import 'package:niogu_app/core/widgets/custom_snackbar.dart'; +import 'package:niogu_app/features/report/transaction/domain/entities/transaction_report.dart'; +import 'package:niogu_app/features/report/transaction/presentation/providers/transaction_report_provider.dart'; +import 'package:niogu_app/features/report/transaction/presentation/widgets/brand_purchase_header.dart'; +import 'package:niogu_app/features/report/transaction/presentation/widgets/dashed_divider.dart'; +import 'package:niogu_app/features/report/transaction/presentation/widgets/footer_purchase_info.dart'; +import 'package:niogu_app/features/report/transaction/presentation/widgets/general_purchase_info.dart'; +import 'package:niogu_app/features/report/transaction/presentation/widgets/payment_proof.dart'; +import 'package:niogu_app/features/report/transaction/presentation/widgets/payment_purchase_summary.dart'; +import 'package:niogu_app/features/report/transaction/presentation/widgets/purchase_item_list.dart'; +import 'package:path_provider/path_provider.dart'; +import 'package:permission_handler/permission_handler.dart'; +import 'package:screenshot/screenshot.dart'; +import 'package:share_plus/share_plus.dart'; +import 'package:sizer/sizer.dart'; + +class TransactionReportPurchaseDetailScreen extends ConsumerWidget { + final PurchaseDetail purchaseDetail; + + TransactionReportPurchaseDetailScreen({ + super.key, + required this.purchaseDetail, + }); + + final ScreenshotController screenshotController = ScreenshotController(); + + Future _handleShareAction(BuildContext context) async { + final bool isTablet = 100.w >= 600; + showModalBottomSheet( + context: context, + backgroundColor: Colors.white, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.vertical(top: Radius.circular(2.5.w)), + ), + constraints: const BoxConstraints(maxWidth: double.infinity), + builder: (BuildContext context) { + return SafeArea( + child: Container( + width: 100.w, + padding: EdgeInsets.symmetric(vertical: 2.h, horizontal: 5.w), + clipBehavior: Clip.hardEdge, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.vertical(top: Radius.circular(6.w)), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + ListTile( + leading: Icon(Icons.download_rounded, size: 6.w), + title: Text( + 'Unduh', + style: TextStyle( + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + ), + ), + onTap: () async { + await _processImage(context, isDownload: true); + context.pop(); + }, + ), + + SizedBox(height: 2.h), + + ListTile( + leading: Icon(Icons.share_rounded, size: 6.w), + title: Text( + 'Bagikan', + style: TextStyle( + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + ), + ), + onTap: () async { + await _processImage(context, isDownload: false); + context.pop(); + }, + ), + ], + ), + ), + ); + }, + ); + } + + Future _processImage( + BuildContext context, { + required bool isDownload, + }) async { + try { + final image = await screenshotController.capture(); + if (image == null) return; + + if (isDownload) { + if (Platform.isAndroid) { + final request = await Permission.storage.request(); + + if (request.isDenied) { + CustomSnackbar.showWarning(context, "Akses Ditolak"); + return; + } + } + + await Gal.putImageBytes(image); + + if (!context.mounted) return; + CustomSnackbar.showSuccess( + context, + "Gambar berhasil disimpan ke galeri", + ); + } else { + final directory = await getTemporaryDirectory(); + + final imagePath = await File( + '${directory.path}/transaction.png', + ).create(); + + await imagePath.writeAsBytes(image); + + await Share.shareXFiles([XFile(imagePath.path)]); + } + } catch (e, st) { + if (!context.mounted) return; + LogMessage.log.e(e.toString(), error: e, stackTrace: st); + CustomSnackbar.showError(context, "Ups, terjadi kesalahan"); + } + } + + Future _cancelPurchase( + BuildContext context, + WidgetRef ref, + String purchaseId, + ) async { + try { + await ref + .read(transactionReportControllerProvider.notifier) + .cancelPurchase(purchaseId); + + if (!context.mounted) return; + + CustomSnackbar.showSuccess(context, "Transaksi berhasil dibatalkan"); + + context.pop(); + } catch (e, st) { + LogMessage.log.e(e.toString(), error: e, stackTrace: st); + + CustomSnackbar.showError(context, "Ups, terjadi kesalahan"); + + context.pop(); + } + } + + @override + Widget build(BuildContext context, WidgetRef ref) { + return LayoutBuilder( + builder: (context, constraints) { + final bool isTablet = 100.w >= 600; + + final transactionReportControllerState = ref.watch( + transactionReportControllerProvider, + ); + + return SafeArea( + top: false, + bottom: true, + right: false, + left: false, + child: Scaffold( + backgroundColor: const Color(0xFFF5F5F5), + appBar: TopBackBarApp( + title: "Detail Transaksi", + onTap: () => context.pop(), + ), + body: SingleChildScrollView( + padding: EdgeInsets.fromLTRB(4.w, 2.h, 4.w, 12.h), + child: Column( + children: [ + Screenshot( + controller: screenshotController, + child: Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(4.w), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.05), + blurRadius: 15, + offset: const Offset(0, 5), + ), + ], + ), + child: Column( + children: [ + BrandPurchaseHeader( + purchaseDetail: purchaseDetail, + ), + + const DashedDivider(), + + GeneralPurchaseInfo( + purchaseDetail: purchaseDetail, + ), + + const DashedDivider(), + + PurchaseItemList(purchaseDetail: purchaseDetail), + + const DashedDivider(), + + PaymentPurchaseSummary( + purchaseDetail: purchaseDetail, + ), + + if (purchaseDetail.paymentProofPath != null && + purchaseDetail + .paymentProofPath! + .isNotEmpty) ...[ + const DashedDivider(), + + PaymentProof( + imagePath: purchaseDetail.paymentProofPath!, + ), + ], + + if (purchaseDetail.note != null || + purchaseDetail.supplierName != null) ...[ + const DashedDivider(), + FooterPurchaseInfo( + purchaseDetail: purchaseDetail, + ), + ], + + SizedBox(height: 3.h), + ], + ), + ), + ), + + SizedBox(height: 2.h), + + Padding( + padding: EdgeInsets.symmetric(horizontal: 5.w), + child: + purchaseDetail.purchaseStatus == + PurchaseStatus.completed + ? TextButton.icon( + onPressed: () { + showDialog( + context: context, + builder: (context) { + final purchaseId = + purchaseDetail.purchaseId; + return CustomConfirmation( + title: "Batalkan Transaksi", + body: + "Batalkan transaksi ${purchaseDetail.purchaseNumber}", + color: Color(0xFFFFA000), + icon: Icons.warning_amber_rounded, + onConfirm: + transactionReportControllerState + .isLoading + ? null + : () async => await _cancelPurchase( + context, + ref, + purchaseId, + ), + ); + }, + ); + }, + style: TextButton.styleFrom( + foregroundColor: Colors.red[700], + padding: EdgeInsets.symmetric( + vertical: 1.5.h, + horizontal: 4.w, + ), + ), + icon: Icon(Icons.close_rounded, size: 5.w), + label: Text( + "Batalkan Transaksi Ini", + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + ), + ), + ) + : Text( + "Transaksi Dibatalkan", + style: TextStyle( + color: Colors.red.shade700, + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + fontWeight: FontWeight.w500, + ), + ), + ), + ], + ), + ), + + bottomNavigationBar: Container( + padding: EdgeInsets.all(4.w), + decoration: BoxDecoration( + color: Colors.white, + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.05), + blurRadius: 10, + offset: const Offset(0, -5), + ), + ], + ), + child: Row( + children: [ + Expanded( + child: OutlinedButton.icon( + onPressed: () => _handleShareAction(context), + style: OutlinedButton.styleFrom( + padding: EdgeInsets.symmetric(vertical: 1.8.h), + side: BorderSide(color: AppColor.primaryColor), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(2.5.w), + ), + ), + icon: Icon( + Icons.share_outlined, + color: AppColor.primaryColor, + size: 5.w, + ), + label: Text( + "Bagikan", + style: TextStyle( + color: AppColor.primaryColor, + fontWeight: FontWeight.bold, + fontSize: AppFontSize.medium.sp, + ), + ), + ), + ), + SizedBox(width: 3.w), + Expanded( + child: ElevatedButton.icon( + onPressed: () {}, + style: ElevatedButton.styleFrom( + padding: EdgeInsets.symmetric(vertical: 1.8.h), + backgroundColor: AppColor.primaryColor, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(2.5.w), + ), + elevation: 0, + ), + icon: Icon( + Icons.print_outlined, + color: Colors.white, + size: 5.w, + ), + label: Text( + "Cetak Struk", + style: TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + fontSize: AppFontSize.medium.sp, + ), + ), + ), + ), + ], + ), + ), + ), + ); + }, + ); + } +} diff --git a/Mobile Operasional/lib/features/report/transaction/presentation/screens/transaction_report_screen.dart b/Mobile Operasional/lib/features/report/transaction/presentation/screens/transaction_report_screen.dart new file mode 100644 index 0000000..6fdda83 --- /dev/null +++ b/Mobile Operasional/lib/features/report/transaction/presentation/screens/transaction_report_screen.dart @@ -0,0 +1,738 @@ +import 'package:calendar_date_picker2/calendar_date_picker2.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; +import 'package:intl/intl.dart'; +import 'package:niogu_app/core/widgets/custom_error_screen.dart'; +import 'package:niogu_app/core/providers/app_provider.dart'; +import 'package:niogu_app/core/router/app_route.dart'; +import 'package:niogu_app/core/utils/currency_format.dart'; +import 'package:niogu_app/core/enums/report_type.dart'; +import 'package:niogu_app/core/enums/transaction_reference_type.dart'; +import 'package:niogu_app/core/enums/user_role.dart'; +import 'package:niogu_app/core/widgets/custom_empty_screen.dart'; +import 'package:niogu_app/features/outlets/presentation/providers/outlet_provider.dart'; +import 'package:niogu_app/features/report/presentation/providers/report_provider.dart'; +import 'package:niogu_app/features/report/presentation/widgets/filtered_date_range.dart'; +import 'package:niogu_app/features/report/presentation/widgets/filtered_outlet.dart'; +import 'package:niogu_app/features/report/presentation/widgets/filtered_type.dart'; +import 'package:niogu_app/features/report/presentation/widgets/fixed_outlet.dart'; +import 'package:niogu_app/features/report/presentation/widgets/report_header.dart'; +import 'package:niogu_app/features/report/transaction/domain/entities/transaction_report.dart'; +import 'package:niogu_app/features/report/transaction/presentation/providers/transaction_report_provider.dart'; +import 'package:niogu_app/features/report/transaction/presentation/widgets/sliver_header_delegate.dart'; +import 'package:niogu_app/features/report/transaction/presentation/widgets/transaction_item.dart'; +import 'package:niogu_app/features/report/transaction/presentation/widgets/transaction_report_shimmer.dart'; +import 'package:niogu_app/features/stock_in/domain/entities/stock_in_transfer.dart'; +import 'package:sizer/sizer.dart'; +import 'package:niogu_app/core/constants/app_color.dart'; +import 'package:niogu_app/core/constants/app_font_size.dart'; + +class TransactionReportScreen extends ConsumerStatefulWidget { + const TransactionReportScreen({super.key}); + + @override + ConsumerState createState() => + _TransactionReportScreenState(); +} + +class _TransactionReportScreenState + extends ConsumerState + with AutomaticKeepAliveClientMixin { + String? _selectedOutlet; + + String _selectedType = "Semua Transaksi"; + + DateTimeRange? _selectedDateRange; + + List _initialValue = [ + DateTime.now().subtract(const Duration(days: 7)), + DateTime.now(), + ]; + + @override + // TODO: implement wantKeepAlive + bool get wantKeepAlive => true; + + @override + void initState() { + // TODO: implement initState + super.initState(); + + WidgetsBinding.instance.addPostFrameCallback((_) { + final currentUserRole = ref.read(currentUserRoleProvider); + + if (currentUserRole == UserRole.admin) { + final currentOutletId = ref.read(currentOutletIdProvider); + ref + .read(filteredByOutletProvider(ReportType.transaction).notifier) + .state = + currentOutletId; + } + }); + } + + @override + void dispose() { + // TODO: implement dispose + super.dispose(); + } + + bool _isSameDay(DateTime d1, DateTime d2) { + return d1.year == d2.year && d1.month == d2.month && d1.day == d2.day; + } + + String _formatGroupDate(DateTime date) { + return DateFormat('dd MMMM yyyy').format(date); + } + + Future _showDateRangePicker() async { + final bool isTablet = 100.w >= 600; + + List tempSelectedDates = List.from(_initialValue); + + showModalBottomSheet( + context: context, + useRootNavigator: true, + isScrollControlled: true, + backgroundColor: Colors.transparent, + constraints: BoxConstraints(maxWidth: double.infinity), + builder: (context) { + return StatefulBuilder( + builder: (context, setModalState) { + return Container( + height: 80.h, + width: double.infinity, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.vertical( + top: Radius.circular(4.5.w), + ), + ), + constraints: BoxConstraints( + maxHeight: 80.h, + maxWidth: double.infinity, + ), + child: Column( + children: [ + SizedBox(height: 1.5.h), + Container( + width: 12.w, + height: 0.6.h, + decoration: BoxDecoration( + color: Colors.grey[300], + borderRadius: BorderRadius.circular(2.5.w), + ), + ), + Padding( + padding: EdgeInsets.symmetric(vertical: 2.h), + child: Text( + "Pilih Rentang Tanggal", + style: TextStyle( + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + fontWeight: FontWeight.bold, + ), + ), + ), + Divider(height: 1, color: Colors.grey[200]), + + Expanded( + child: CalendarDatePicker2( + config: CalendarDatePicker2Config( + calendarType: CalendarDatePicker2Type.range, + selectedDayHighlightColor: AppColor.primaryColor, + weekdayLabels: [ + 'Min', + 'Sen', + 'Sel', + 'Rab', + 'Kam', + 'Jum', + 'Sab', + ], + weekdayLabelTextStyle: TextStyle( + color: Colors.grey[600], + fontWeight: FontWeight.bold, + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + ), + controlsTextStyle: TextStyle( + color: Colors.black, + fontWeight: FontWeight.bold, + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + ), + centerAlignModePicker: true, + firstDayOfWeek: 1, + ), + value: tempSelectedDates, + onValueChanged: (dates) { + setModalState(() { + tempSelectedDates = dates; + }); + }, + ), + ), + + Container( + padding: EdgeInsets.all(5.w), + decoration: BoxDecoration( + color: Colors.white, + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.05), + blurRadius: 10, + offset: const Offset(0, -5), + ), + ], + ), + child: Row( + children: [ + Expanded( + child: OutlinedButton( + onPressed: () { + _selectedDateRange = null; + ref + .read( + filteredByDateTimeProvider( + ReportType.transaction, + ).notifier, + ) + .state = + null; + context.pop(); + }, + style: OutlinedButton.styleFrom( + padding: EdgeInsets.symmetric(vertical: 1.5.h), + side: BorderSide(color: Colors.grey), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(2.5.w), + ), + ), + child: Text( + "Atur Ulang", + style: TextStyle( + color: Colors.black, + fontWeight: FontWeight.bold, + fontSize: AppFontSize.medium.sp, + ), + ), + ), + ), + SizedBox(width: 3.w), + Expanded( + child: OutlinedButton( + onPressed: () => context.pop(), + style: OutlinedButton.styleFrom( + padding: EdgeInsets.symmetric(vertical: 1.5.h), + side: BorderSide(color: AppColor.primaryColor), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(2.5.w), + ), + ), + child: Text( + "Batal", + style: TextStyle( + color: AppColor.primaryColor, + fontWeight: FontWeight.bold, + fontSize: AppFontSize.medium.sp, + ), + ), + ), + ), + SizedBox(width: 3.w), + Expanded( + child: ElevatedButton( + onPressed: tempSelectedDates.length < 2 + ? null + : () { + _initialValue = tempSelectedDates; + final startDate = tempSelectedDates[0]!; + + final endDate = tempSelectedDates[1]!; + + final dateRange = DateTimeRange( + start: startDate, + end: endDate, + ); + + _selectedDateRange = dateRange; + + ref + .read( + filteredByDateTimeProvider( + ReportType.transaction, + ).notifier, + ) + .state = + dateRange; + + context.pop(); + }, + style: ElevatedButton.styleFrom( + padding: EdgeInsets.symmetric(vertical: 1.5.h), + backgroundColor: AppColor.primaryColor, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(2.5.w), + ), + disabledBackgroundColor: Colors.grey.shade300, + ), + child: Text( + "Pilih", + style: TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + fontSize: AppFontSize.medium.sp, + ), + ), + ), + ), + ], + ), + ), + ], + ), + ); + }, + ); + }, + ); + } + + void _onSelectedOutletChange(String? value) { + _selectedOutlet = value != '0' ? value : null; + ref.read(filteredByOutletProvider(ReportType.transaction).notifier).state = + _selectedOutlet; + } + + @override + Widget build(BuildContext context) { + super.build(context); + + final bool isTablet = 100.w >= 600; + + final isLoggedIn = ref.watch(currentStatusLoginProvider); + + double totalIncome = 0.0; + + double totalExpense = 0.0; + + double netProfit = 0.0; + + final filteredTransactionState = ref.watch( + filteredTransactionReportProvider, + ); + + filteredTransactionState.whenData((transactions) { + for (final transaction in transactions) { + if (transaction.isIncome) { + totalIncome += transaction.amount; + } else { + totalExpense += transaction.amount; + } + } + + netProfit = totalIncome - totalExpense; + }); + + final transactionEmptyState = isLoggedIn + ? ref.watch(transactionReportEmptyProvider) + : TransactionReportItemEmpty.empty_database; + + final List sourceOutlets = [ + SourceOutlet(id: "0", name: "Semua Outlet"), + ]; + + final currentUserRole = ref.watch(currentUserRoleProvider); + + final sourceOutletState = ref.watch(sourceOutletStreamProvider(true)); + + final currentOutletName = ref.watch(currentOutletNameProvider); + + if (currentUserRole == UserRole.owner) { + sourceOutletState.whenData((outlets) { + sourceOutlets.addAll(outlets); + }); + } + + return CustomScrollView( + physics: const BouncingScrollPhysics(), + slivers: [ + SliverAppBar( + pinned: false, + floating: true, + snap: true, + stretch: true, + expandedHeight: 100.w > 800 + ? 48.h + : isTablet + ? 44.5.h + : 42.h, + backgroundColor: Colors.white, + elevation: 0, + flexibleSpace: FlexibleSpaceBar( + collapseMode: CollapseMode.parallax, + stretchModes: const [StretchMode.zoomBackground], + background: Material( + color: Colors.white, + elevation: 0, + child: Column( + children: [ + Padding( + padding: EdgeInsets.fromLTRB(5.w, 2.h, 5.w, 1.h), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Row( + children: [ + Expanded( + flex: 3, + child: currentUserRole == UserRole.owner + ? FilteredOutlet( + value: sourceOutletState.isLoading + ? '0' + : (_selectedOutlet ?? '0'), + sourceOutlets: sourceOutlets, + onChanged: _onSelectedOutletChange, + ) + : FixedOutlet( + value: + currentOutletName ?? + 'Tidak ada outlet', + ), + ), + + SizedBox(width: 3.w), + + Expanded( + flex: 3, + child: FilteredType( + value: _selectedType, + items: [ + "Semua Transaksi", + "Pendapatan", + "Pengeluaran", + ], + onChanged: (value) { + _selectedType = value!; + final type = value == 'Semua Transaksi' + ? null + : value == 'Pendapatan' + ? TransactionType.income + : TransactionType.expense; + ref + .read( + filteredByTransactionTypeProvider + .notifier, + ) + .state = + type; + }, + ), + ), + ], + ), + + SizedBox(height: 1.5.h), + + FilteredDateRange( + value: _selectedDateRange != null + ? "${_formatGroupDate(_selectedDateRange!.start)} - ${_formatGroupDate(_selectedDateRange!.end)}" + : "Filter Tanggal", + onTap: _showDateRangePicker, + ), + ], + ), + ), + + Container( + margin: EdgeInsets.symmetric( + horizontal: 5.w, + vertical: 1.h, + ), + padding: EdgeInsets.all(5.w), + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [ + AppColor.primaryColor, + AppColor.primaryColor.withOpacity(0.8), + ], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + borderRadius: BorderRadius.circular(4.w), + boxShadow: [ + BoxShadow( + color: AppColor.primaryColor.withOpacity(0.3), + blurRadius: 10, + offset: const Offset(0, 5), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Laba / Rugi", + style: TextStyle( + color: Colors.white70, + fontSize: AppFontSize.medium.sp, + ), + ), + SizedBox(height: 0.5.h), + Text( + CurrencyFormat.formatToIdr(netProfit, 0), + style: TextStyle( + color: Colors.white, + fontSize: AppFontSize.large.sp, + fontWeight: FontWeight.bold, + ), + ), + SizedBox(height: 2.5.h), + + Container( + padding: EdgeInsets.all(3.w), + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.15), + borderRadius: BorderRadius.circular(2.5.w), + ), + child: Row( + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon( + Icons.arrow_downward_rounded, + color: Colors.greenAccent, + size: 5.w, + ), + SizedBox(width: 1.w), + Text( + "Pendapatan", + style: TextStyle( + color: Colors.white70, + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + ), + ), + ], + ), + SizedBox(height: 0.5.h), + Text( + CurrencyFormat.formatToIdr( + totalIncome, + 0, + ), + style: TextStyle( + color: Colors.white, + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + ), + Container( + width: 1, + height: 4.h, + color: Colors.white24, + ), // Separator + SizedBox(width: 3.w), + // Expense + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon( + Icons.arrow_upward_rounded, + color: Colors.redAccent, + size: 5.w, + ), + SizedBox(width: 1.w), + Text( + "Pengeluaran", + style: TextStyle( + color: Colors.white70, + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + ), + ), + ], + ), + SizedBox(height: 0.5.h), + Text( + CurrencyFormat.formatToIdr( + totalExpense, + 0, + ), + style: TextStyle( + color: Colors.white, + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + ), + ], + ), + ), + ], + ), + ), + ], + ), + ), + ), + ), + + filteredTransactionState.when( + data: (transactions) { + switch (transactionEmptyState) { + case TransactionReportItemEmpty.loading: + return const SliverToBoxAdapter(child: const SizedBox()); + case TransactionReportItemEmpty.empty_database: + return const SliverToBoxAdapter( + child: CustomEmptyScreen( + title: "Tidak Ada Transaksi", + body: "Kamu belum melakukan transaksi", + ), + ); + case TransactionReportItemEmpty.empty_search_result: + return SliverMainAxisGroup( + slivers: [ + SliverPersistentHeader( + pinned: true, + delegate: SliverHeaderDelegate( + child: ReportHeader( + rightTitle: "Riwayat Transaksi", + leftTitle: "Unduh Laporan", + ), + ), + ), + const SliverToBoxAdapter( + child: CustomEmptyScreen( + body: "Transaksi Tidak Ditemukan", + ), + ), + ], + ); + case TransactionReportItemEmpty.has_data: + return SliverMainAxisGroup( + slivers: [ + SliverPersistentHeader( + pinned: true, + delegate: SliverHeaderDelegate( + child: ReportHeader( + rightTitle: "Riwayat Transaksi", + leftTitle: "Unduh Laporan", + ), + ), + ), + SliverList( + delegate: SliverChildBuilderDelegate((context, index) { + final item = transactions[index]; + bool showHeader = true; + + if (index > 0) { + final prevItem = transactions[index - 1]; + if (_isSameDay(prevItem.date, item.date)) + showHeader = false; + } + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (showHeader) + Padding( + padding: EdgeInsets.symmetric( + horizontal: 5.w, + vertical: 2.h, + ), + child: Text( + _formatGroupDate(item.date), + style: TextStyle( + color: Colors.grey[500], + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + fontWeight: FontWeight.bold, + ), + ), + ), + + TransactionItem( + item: item, + onTap: () async { + if (item.isIncome) { + final orderDetail = await ref + .read(transactionReportRepositoryProvider) + .getOrderDetail(item.id); + + FocusScope.of(context).unfocus(); + + await Future.delayed( + const Duration(milliseconds: 400), + ); + + context.pushNamed( + AppRoute.transactionReportOrderDetailScreen, + extra: orderDetail, + ); + } else { + final purchaseDetail = await ref + .read(transactionReportRepositoryProvider) + .getPurchaseDetail(item.id); + + FocusScope.of(context).unfocus(); + + await Future.delayed( + const Duration(milliseconds: 400), + ); + + context.pushNamed( + AppRoute + .transactionReportPurchaseDetailScreen, + extra: purchaseDetail, + ); + } + }, + ), + ], + ); + }, childCount: transactions.length), + ), + ], + ); + } + }, + error: (error, stackTrace) { + return SliverToBoxAdapter( + child: CustomErrorScreen( + message: "Ups,terjadi kesalahan", + onRefresh: () {}, + ), + ); + }, + loading: () => + const SliverToBoxAdapter(child: const TransactionReportShimmer()), + ), + SliverToBoxAdapter(child: SizedBox(height: 10.h)), + ], + ); + } +} diff --git a/Mobile Operasional/lib/features/report/transaction/presentation/widgets/brand_order_header.dart b/Mobile Operasional/lib/features/report/transaction/presentation/widgets/brand_order_header.dart new file mode 100644 index 0000000..092b528 --- /dev/null +++ b/Mobile Operasional/lib/features/report/transaction/presentation/widgets/brand_order_header.dart @@ -0,0 +1,81 @@ +import 'package:flutter/material.dart'; +import 'package:niogu_app/core/constants/app_color.dart'; +import 'package:niogu_app/core/constants/app_font_size.dart'; +import 'package:niogu_app/features/report/transaction/domain/entities/transaction_report.dart'; +import 'package:sizer/sizer.dart'; + +class BrandOrderHeader extends StatelessWidget { + final OrderDetail orderDetail; + + const BrandOrderHeader({super.key, required this.orderDetail}); + + @override + Widget build(BuildContext context) { + return Padding( + padding: EdgeInsets.symmetric(vertical: 3.h), + child: Column( + children: [ + Container( + width: 16.w, + height: 16.w, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: Colors.grey[100], + border: Border.all(color: Colors.grey.shade200), + ), + child: orderDetail.localPhotoPath != null + ? ClipOval( + child: Image.network( + orderDetail.localPhotoPath!, + fit: BoxFit.cover, + errorBuilder: (_, __, ___) => Icon( + Icons.store, + color: AppColor.primaryColor, + size: 8.w, + ), + ), + ) + : Icon( + Icons.store_rounded, + color: AppColor.primaryColor, + size: 8.w, + ), + ), + SizedBox(height: 1.5.h), + + // Nama Toko + Text( + orderDetail.outletName, + style: TextStyle( + fontSize: AppFontSize.medium.sp, + fontWeight: FontWeight.bold, + color: Colors.black87, + ), + ), + + SizedBox(height: 0.5.h), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + "Powered by ", + style: TextStyle( + fontSize: (AppFontSize.small - 2).sp, + color: Colors.grey[500], + ), + ), + Text( + "Niogu", + style: TextStyle( + fontSize: (AppFontSize.small - 2).sp, + fontWeight: FontWeight.bold, + color: AppColor.primaryColor.withOpacity(0.7), + ), + ), + ], + ), + ], + ), + ); + } +} \ No newline at end of file diff --git a/Mobile Operasional/lib/features/report/transaction/presentation/widgets/brand_purchase_header.dart b/Mobile Operasional/lib/features/report/transaction/presentation/widgets/brand_purchase_header.dart new file mode 100644 index 0000000..5be843e --- /dev/null +++ b/Mobile Operasional/lib/features/report/transaction/presentation/widgets/brand_purchase_header.dart @@ -0,0 +1,82 @@ +import 'package:flutter/material.dart'; +import 'package:niogu_app/core/constants/app_color.dart'; +import 'package:niogu_app/core/constants/app_font_size.dart'; +import 'package:niogu_app/features/report/transaction/domain/entities/transaction_report.dart'; +import 'package:sizer/sizer.dart'; + +class BrandPurchaseHeader extends StatelessWidget { + final PurchaseDetail purchaseDetail; + + const BrandPurchaseHeader({super.key, required this.purchaseDetail}); + + @override + Widget build(BuildContext context) { + return Padding( + padding: EdgeInsets.symmetric(vertical: 3.h), + child: Column( + children: [ + Container( + width: 16.w, + height: 16.w, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: Colors.grey[100], + border: Border.all(color: Colors.grey.shade200), + ), + child: purchaseDetail.localPhotoPath != null + ? ClipOval( + child: Image.network( + purchaseDetail.localPhotoPath!, + fit: BoxFit.cover, + errorBuilder: (_, __, ___) => Icon( + Icons.store, + color: AppColor.primaryColor, + size: 8.w, + ), + ), + ) + : Icon( + Icons.store_rounded, + color: AppColor.primaryColor, + size: 8.w, + ), + ), + SizedBox(height: 1.5.h), + + // Nama Toko + Text( + purchaseDetail.outletName, + style: TextStyle( + fontSize: AppFontSize.medium.sp, + fontWeight: FontWeight.bold, + color: Colors.black87, + ), + ), + + SizedBox(height: 0.5.h), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + "Powered by ", + style: TextStyle( + fontSize: (AppFontSize.small - 2).sp, + color: Colors.grey[500], + ), + ), + Text( + "Niogu", + style: TextStyle( + fontSize: (AppFontSize.small - 2).sp, + fontWeight: FontWeight.bold, + color: AppColor.primaryColor.withOpacity(0.7), + ), + ), + ], + ), + ], + ), + ); + + } +} \ No newline at end of file diff --git a/Mobile Operasional/lib/features/report/transaction/presentation/widgets/cancel_confirmation.dart b/Mobile Operasional/lib/features/report/transaction/presentation/widgets/cancel_confirmation.dart new file mode 100644 index 0000000..e1fd553 --- /dev/null +++ b/Mobile Operasional/lib/features/report/transaction/presentation/widgets/cancel_confirmation.dart @@ -0,0 +1,121 @@ +import 'package:flutter/material.dart'; +import 'package:niogu_app/core/constants/app_color.dart'; +import 'package:niogu_app/core/constants/app_font_size.dart'; +import 'package:niogu_app/features/report/transaction/presentation/widgets/cancel_option.dart'; +import 'package:sizer/sizer.dart'; + +class CancelConfirmation extends StatelessWidget { + final String title; + final String subtitle; + final VoidCallback? onCancelWithOutUpdateStock; + final VoidCallback? onCancelWithUpdateStock; + const CancelConfirmation({ + super.key, + required this.title, + required this.subtitle, + required this.onCancelWithOutUpdateStock, + required this.onCancelWithUpdateStock + }); + + @override + Widget build(BuildContext context) { + final bool isTablet = 100.w >= 600; + final double maxWidth = 100.w >= 1280 + ? 1200 + : isTablet + ? 800 + : 400; + return Dialog( + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(4.5.w)), + elevation: 0, + backgroundColor: Colors.transparent, + child: Container( + padding: EdgeInsets.all(6.w), + width: double.infinity, + constraints: BoxConstraints(maxWidth: maxWidth), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(4.5.w), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + padding: EdgeInsets.all(4.w), + decoration: BoxDecoration( + color: Colors.red[50], + shape: BoxShape.circle, + ), + child: Icon(Icons.cancel_outlined, color: Colors.red, size: 10.w), + ), + SizedBox(height: 2.h), + + Text( + title, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: AppFontSize.medium.sp, + fontWeight: FontWeight.bold, + color: Colors.black87, + ), + ), + SizedBox(height: 1.h), + Text( + subtitle, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: isTablet + ? (AppFontSize.medium - 1.25).sp + : (AppFontSize.small - 1.25).sp, + color: Colors.grey.shade800, + ), + ), + SizedBox(height: 3.h), + + CancelOption( + title: "Batalkan Tanpa Perbarui Stok", + subtitle: "Status batal, namun jumlah stok barang tetap.", + icon: Icons.inventory, + color: Colors.orange, + onTap: onCancelWithOutUpdateStock, + ), + + SizedBox(height: 1.5.h), + + CancelOption( + title: "Batalkan & Perbarui Stok", + subtitle: "Status batal dan stok barang akan dikembalikan.", + icon: Icons.settings_backup_restore_rounded, + color: AppColor.primaryColor, + onTap: onCancelWithUpdateStock, + ), + + SizedBox(height: 3.h), + + SizedBox( + width: double.infinity, + child: OutlinedButton( + onPressed: () => Navigator.pop(context), + style: OutlinedButton.styleFrom( + padding: EdgeInsets.symmetric(vertical: 1.5.h), + side: BorderSide(color: Colors.grey[300]!), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(2.5.w), + ), + ), + child: Text( + "Kembali", + style: TextStyle( + color: Colors.grey[700], + fontWeight: FontWeight.bold, + fontSize: AppFontSize.medium.sp, + ), + ), + ), + ), + ], + ), + ), + ); + } +} diff --git a/Mobile Operasional/lib/features/report/transaction/presentation/widgets/cancel_option.dart b/Mobile Operasional/lib/features/report/transaction/presentation/widgets/cancel_option.dart new file mode 100644 index 0000000..94c0b61 --- /dev/null +++ b/Mobile Operasional/lib/features/report/transaction/presentation/widgets/cancel_option.dart @@ -0,0 +1,70 @@ +import 'package:flutter/material.dart'; +import 'package:niogu_app/core/constants/app_font_size.dart'; +import 'package:sizer/sizer.dart'; + +class CancelOption extends StatelessWidget { + final String title; + final String subtitle; + final IconData icon; + final Color color; + final VoidCallback? onTap; + const CancelOption({ + super.key, + required this.title, + required this.subtitle, + required this.icon, + required this.color, + required this.onTap, + }); + + @override + Widget build(BuildContext context) { + return InkWell( + onTap: onTap, + borderRadius: BorderRadius.circular(3.w), + child: Container( + padding: EdgeInsets.all(3.w), + decoration: BoxDecoration( + border: Border.all(color: Colors.grey.shade200), + borderRadius: BorderRadius.circular(3.w), + ), + child: Row( + children: [ + Container( + padding: EdgeInsets.all(2.w), + decoration: BoxDecoration( + color: color.withOpacity(0.1), + shape: BoxShape.circle, + ), + child: Icon(icon, color: color, size: 6.w), + ), + SizedBox(width: 3.w), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: AppFontSize.small.sp, + ), + ), + Text( + subtitle, + style: TextStyle( + fontSize: (AppFontSize.small - 1.25).sp, + color: Colors.grey.shade800, + ), + ), + ], + ), + ), + Icon(Icons.chevron_right, color: Colors.grey, size: 5.w), + ], + ), + ), + ); + + } +} diff --git a/Mobile Operasional/lib/features/report/transaction/presentation/widgets/dashed_divider.dart b/Mobile Operasional/lib/features/report/transaction/presentation/widgets/dashed_divider.dart new file mode 100644 index 0000000..dd02637 --- /dev/null +++ b/Mobile Operasional/lib/features/report/transaction/presentation/widgets/dashed_divider.dart @@ -0,0 +1,20 @@ +import 'package:flutter/material.dart'; + +class DashedDivider extends StatelessWidget { + const DashedDivider({super.key}); + + @override + Widget build(BuildContext context) { + return Row( + children: List.generate( + 30, + (index) => Expanded( + child: Container( + color: index % 2 == 0 ? Colors.grey[300] : Colors.transparent, + height: 1, + ), + ), + ), + ); + } +} \ No newline at end of file diff --git a/Mobile Operasional/lib/features/report/transaction/presentation/widgets/footer_order_info.dart b/Mobile Operasional/lib/features/report/transaction/presentation/widgets/footer_order_info.dart new file mode 100644 index 0000000..903f672 --- /dev/null +++ b/Mobile Operasional/lib/features/report/transaction/presentation/widgets/footer_order_info.dart @@ -0,0 +1,117 @@ +import 'package:flutter/material.dart'; +import 'package:niogu_app/core/constants/app_font_size.dart'; +import 'package:niogu_app/features/report/transaction/domain/entities/transaction_report.dart'; +import 'package:sizer/sizer.dart'; + +class FooterOrderInfo extends StatelessWidget { + final OrderDetail orderDetail; + const FooterOrderInfo({super.key, required this.orderDetail}); + + @override + Widget build(BuildContext context) { + final bool isTablet = 100.w > 600; + return Padding( + padding: EdgeInsets.symmetric(horizontal: 5.w, vertical: 2.h), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (orderDetail.customerName != null) ...[ + Row( + children: [ + Icon(Icons.person_outline, size: 4.w, color: Colors.grey), + SizedBox(width: 2.w), + Text( + "Informasi Pelanggan", + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + ), + ), + ], + ), + SizedBox(height: 1.h), + Container( + width: double.infinity, + padding: EdgeInsets.all(3.w), + decoration: BoxDecoration( + color: Colors.grey[50], + borderRadius: BorderRadius.circular(2.w), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + orderDetail.customerName!, + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + ), + ), + if (orderDetail.customerPhone != null) ...[ + SizedBox(height: 1.h), + Text( + orderDetail.customerPhone!, + style: TextStyle( + color: Colors.grey[600], + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + ), + ), + ], + + if (orderDetail.customerAddress != null) ...[ + SizedBox(height: 1.h), + Text( + orderDetail.customerAddress!, + style: TextStyle( + color: Colors.grey[600], + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + ), + ), + ], + ], + ), + ), + SizedBox(height: 2.h), + ], + + if (orderDetail.note != null && orderDetail.note!.isNotEmpty) ...[ + Row( + children: [ + Icon(Icons.note_alt_outlined, size: 4.w, color: Colors.grey), + SizedBox(width: 2.w), + Text( + "Catatan", + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + ), + ), + ], + ), + SizedBox(height: 1.h), + Text( + orderDetail.note!, + style: TextStyle( + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + color: Colors.grey[700], + fontStyle: FontStyle.italic, + ), + ), + ], + ], + ), + ); + } +} \ No newline at end of file diff --git a/Mobile Operasional/lib/features/report/transaction/presentation/widgets/footer_purchase_info.dart b/Mobile Operasional/lib/features/report/transaction/presentation/widgets/footer_purchase_info.dart new file mode 100644 index 0000000..4b6eb35 --- /dev/null +++ b/Mobile Operasional/lib/features/report/transaction/presentation/widgets/footer_purchase_info.dart @@ -0,0 +1,105 @@ +import 'package:flutter/material.dart'; +import 'package:niogu_app/core/constants/app_font_size.dart'; +import 'package:niogu_app/features/report/transaction/domain/entities/transaction_report.dart'; +import 'package:sizer/sizer.dart'; + +class FooterPurchaseInfo extends StatelessWidget { + final PurchaseDetail purchaseDetail; + const FooterPurchaseInfo({super.key, required this.purchaseDetail}); + + @override + Widget build(BuildContext context) { + final bool isTablet = 100.w > 600; + return Padding( + padding: EdgeInsets.symmetric(horizontal: 5.w, vertical: 2.h), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (purchaseDetail.supplierName != null) ...[ + Row( + children: [ + Icon(Icons.person_outline, size: 4.w, color: Colors.grey), + SizedBox(width: 2.w), + Text( + "Informasi Pemasok", + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + ), + ), + ], + ), + SizedBox(height: 1.h), + Container( + width: double.infinity, + padding: EdgeInsets.all(3.w), + decoration: BoxDecoration( + color: Colors.grey[50], + borderRadius: BorderRadius.circular(2.w), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + purchaseDetail.supplierName!, + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + ), + ), + if (purchaseDetail.supplierPhone != null) ...[ + SizedBox(height: 1.h), + Text( + purchaseDetail.supplierPhone!, + style: TextStyle( + color: Colors.grey[600], + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + ), + ), + ], + ], + ), + ), + SizedBox(height: 2.h), + ], + + if (purchaseDetail.note != null && purchaseDetail.note!.isNotEmpty) ...[ + Row( + children: [ + Icon(Icons.note_alt_outlined, size: 4.w, color: Colors.grey), + SizedBox(width: 2.w), + Text( + "Catatan", + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + ), + ), + ], + ), + SizedBox(height: 1.h), + Text( + purchaseDetail.note!, + style: TextStyle( + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + color: Colors.grey[700], + fontStyle: FontStyle.italic, + ), + ), + ], + ], + ), + ); + + } +} \ No newline at end of file diff --git a/Mobile Operasional/lib/features/report/transaction/presentation/widgets/general_order_info.dart b/Mobile Operasional/lib/features/report/transaction/presentation/widgets/general_order_info.dart new file mode 100644 index 0000000..629e847 --- /dev/null +++ b/Mobile Operasional/lib/features/report/transaction/presentation/widgets/general_order_info.dart @@ -0,0 +1,44 @@ +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; +import 'package:niogu_app/core/enums/order_source.dart'; +import 'package:niogu_app/core/utils/time_zone.dart'; +import 'package:niogu_app/features/report/transaction/domain/entities/transaction_report.dart'; +import 'package:niogu_app/features/report/transaction/presentation/widgets/info_row.dart'; +import 'package:sizer/sizer.dart'; + +class GeneralOrderInfo extends StatelessWidget { + final OrderDetail orderDetail; + + const GeneralOrderInfo({super.key, required this.orderDetail}); + + @override + Widget build(BuildContext context) { + return Padding( + padding: EdgeInsets.symmetric(horizontal: 5.w, vertical: 2.h), + child: Column( + children: [ + InfoRow( + label: "No. Transaksi", + value: orderDetail.orderNumber, + isBoldValue: true, + ), + SizedBox(height: 1.h), + InfoRow( + label: "Waktu", + value: + "${DateFormat("dd MMMM yyyy HH:mm").format(orderDetail.orderDate)} ${TimeZone.getCurrentTimeZone()}", + ), + SizedBox(height: 1.h), + InfoRow( + label: "Sumber", + value: orderDetail.orderSource == OrderSource.pos + ? "Penjualan dari kasir" + : "Penjualan dari toko online", + ), + SizedBox(height: 1.h), + InfoRow(label: "Kasir", value: orderDetail.userName), + ], + ), + ); + } +} diff --git a/Mobile Operasional/lib/features/report/transaction/presentation/widgets/general_purchase_info.dart b/Mobile Operasional/lib/features/report/transaction/presentation/widgets/general_purchase_info.dart new file mode 100644 index 0000000..bac569a --- /dev/null +++ b/Mobile Operasional/lib/features/report/transaction/presentation/widgets/general_purchase_info.dart @@ -0,0 +1,45 @@ +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; +import 'package:niogu_app/core/enums/payment_status.dart'; +import 'package:niogu_app/core/utils/time_zone.dart'; +import 'package:niogu_app/features/report/transaction/domain/entities/transaction_report.dart'; +import 'package:niogu_app/features/report/transaction/presentation/widgets/info_row.dart'; +import 'package:sizer/sizer.dart'; + +class GeneralPurchaseInfo extends StatelessWidget { + final PurchaseDetail purchaseDetail; + const GeneralPurchaseInfo({super.key, required this.purchaseDetail}); + + @override + Widget build(BuildContext context) { + return Padding( + padding: EdgeInsets.symmetric(horizontal: 5.w, vertical: 2.h), + child: Column( + children: [ + InfoRow( + label: "No. Transaksi", + value: purchaseDetail.purchaseNumber, + isBoldValue: true, + ), + SizedBox(height: 1.h), + InfoRow( + label: "Waktu", + value: + "${DateFormat("dd MMMM yyyy HH:mm").format(purchaseDetail.purchaseDate)} ${TimeZone.getCurrentTimeZone()}", + ), + SizedBox(height: 1.h), + InfoRow( + label: "Status Pembayaran", + value: purchaseDetail.paymentStatus == PaymentStatus.paid + ? "Lunas" + : purchaseDetail.paymentStatus == PaymentStatus.partial + ? "Bayar Sebagian" + : "Hutang", + ), + SizedBox(height: 1.h), + InfoRow(label: "Pembeli", value: purchaseDetail.userName), + ], + ), + ); + } +} diff --git a/Mobile Operasional/lib/features/report/transaction/presentation/widgets/info_row.dart b/Mobile Operasional/lib/features/report/transaction/presentation/widgets/info_row.dart new file mode 100644 index 0000000..74007ec --- /dev/null +++ b/Mobile Operasional/lib/features/report/transaction/presentation/widgets/info_row.dart @@ -0,0 +1,40 @@ +import 'package:flutter/material.dart'; +import 'package:niogu_app/core/constants/app_font_size.dart'; +import 'package:sizer/sizer.dart'; + +class InfoRow extends StatelessWidget { + final String label; + final String value; + final bool isBoldValue; + const InfoRow({ + super.key, + required this.label, + required this.value, + this.isBoldValue = false, + }); + + @override + Widget build(BuildContext context) { + final bool isTablet = 100.w >= 600; + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + label, + style: TextStyle( + fontSize: isTablet ? AppFontSize.medium.sp : AppFontSize.small.sp, + color: Colors.grey[600], + ), + ), + Text( + value, + style: TextStyle( + fontSize: isTablet ? AppFontSize.medium.sp : AppFontSize.small.sp, + color: Colors.black87, + fontWeight: isBoldValue ? FontWeight.bold : FontWeight.normal, + ), + ), + ], + ); + } +} diff --git a/Mobile Operasional/lib/features/report/transaction/presentation/widgets/online_order_header.dart b/Mobile Operasional/lib/features/report/transaction/presentation/widgets/online_order_header.dart new file mode 100644 index 0000000..b70a099 --- /dev/null +++ b/Mobile Operasional/lib/features/report/transaction/presentation/widgets/online_order_header.dart @@ -0,0 +1,48 @@ +import 'package:flutter/material.dart'; +import 'package:niogu_app/features/online_store/presentation/widgets/status_filter_item.dart'; +import 'package:sizer/sizer.dart'; + +class OrderOnlineHeader extends StatelessWidget { + final List statuses; + final String selectedStatus; + final Map orderCounts; + final void Function(String) onTap; + const OrderOnlineHeader({ + super.key, + required this.statuses, + required this.selectedStatus, + required this.orderCounts, + required this.onTap, + }); + + @override + Widget build(BuildContext context) { + return Container( + width: double.infinity, + height: 7.h, + decoration: const BoxDecoration( + color: Colors.white, + boxShadow: [BoxShadow(color: Colors.white, spreadRadius: 2)], + ), + child: ListView.builder( + scrollDirection: Axis.horizontal, + padding: EdgeInsets.symmetric(horizontal: 4.w), + itemCount: statuses.length, + itemBuilder: (context, index) { + final status = statuses[index]; + + final bool isSelected = selectedStatus == status; + + final int count = orderCounts[status] ?? 0; + + return StatusFilterItem( + label: status, + isSelected: isSelected, + count: count, + onTap: () => onTap(status), + ); + }, + ), + ); + } +} diff --git a/Mobile Operasional/lib/features/report/transaction/presentation/widgets/order_item_list.dart b/Mobile Operasional/lib/features/report/transaction/presentation/widgets/order_item_list.dart new file mode 100644 index 0000000..99c503f --- /dev/null +++ b/Mobile Operasional/lib/features/report/transaction/presentation/widgets/order_item_list.dart @@ -0,0 +1,125 @@ +import 'package:flutter/material.dart'; +import 'package:niogu_app/core/constants/app_font_size.dart'; +import 'package:niogu_app/core/utils/currency_format.dart'; +import 'package:niogu_app/core/utils/extension_format.dart'; +import 'package:niogu_app/features/report/transaction/domain/entities/transaction_report.dart'; +import 'package:sizer/sizer.dart'; + +class OrderItemList extends StatelessWidget { + final OrderDetail orderDetail; + const OrderItemList({super.key, required this.orderDetail}); + + @override + Widget build(BuildContext context) { + final bool isTablet = 100.w > 600; + return Padding( + padding: EdgeInsets.symmetric(horizontal: 5.w, vertical: 2.h), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Detail Pesanan", + style: TextStyle( + fontSize: isTablet ? AppFontSize.medium.sp : AppFontSize.small.sp, + fontWeight: FontWeight.bold, + color: Colors.grey[800], + ), + ), + SizedBox(height: 1.5.h), + ...orderDetail.items.map((item) { + return Padding( + padding: EdgeInsets.only(bottom: 1.5.h), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + width: 10.w, + height: 10.w, + padding: EdgeInsets.symmetric( + horizontal: 2.w, + vertical: 0.5.h, + ), + decoration: BoxDecoration( + color: Colors.grey[100], + borderRadius: BorderRadius.circular(1.w), + border: Border.all(color: Colors.grey.shade300), + ), + child: Center( + child: Text( + "${item.quantity.toStringWithoutTrailingZero()}x", + style: TextStyle( + fontSize: isTablet + ? (AppFontSize.medium - 1.75).sp + : (AppFontSize.small - 1.75).sp, + fontWeight: FontWeight.bold, + color: Colors.black54, + ), + ), + ), + ), + + SizedBox(width: 3.w), + + // Detail Produk + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + item.name, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + fontWeight: FontWeight.bold, + color: Colors.black87, + ), + ), + if (item.variantName != null) ...[ + SizedBox(height: 0.75.h), + Text( + item.variantName!, + style: TextStyle( + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + color: Colors.grey[600], + ), + ), + ], + SizedBox(height: 0.75.h), + // Harga Satuan + Text( + "${CurrencyFormat.formatToIdr(item.sellingPrice, 0)}", + style: TextStyle( + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + color: Colors.grey[500], + ), + ), + ], + ), + ), + + // Subtotal Item + Text( + CurrencyFormat.formatToIdr(item.subtotal, 0), + style: TextStyle( + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + fontWeight: FontWeight.bold, + color: Colors.black87, + ), + ), + ], + ), + ); + }), + ], + ), + ); + } +} diff --git a/Mobile Operasional/lib/features/report/transaction/presentation/widgets/payment_order_summary.dart b/Mobile Operasional/lib/features/report/transaction/presentation/widgets/payment_order_summary.dart new file mode 100644 index 0000000..f0a486f --- /dev/null +++ b/Mobile Operasional/lib/features/report/transaction/presentation/widgets/payment_order_summary.dart @@ -0,0 +1,80 @@ +import 'package:flutter/material.dart'; +import 'package:niogu_app/core/constants/app_color.dart'; +import 'package:niogu_app/core/constants/app_font_size.dart'; +import 'package:niogu_app/core/utils/currency_format.dart'; +import 'package:niogu_app/features/report/transaction/domain/entities/transaction_report.dart'; +import 'package:niogu_app/features/report/transaction/presentation/widgets/summary_row.dart'; +import 'package:sizer/sizer.dart'; + +class PaymentOrderSummary extends StatelessWidget { + final OrderDetail orderDetail; + const PaymentOrderSummary({super.key, required this.orderDetail}); + + @override + Widget build(BuildContext context) { + final bool isTablet = 100.w > 600; + return Padding( + padding: EdgeInsets.symmetric(horizontal: 5.w, vertical: 2.h), + child: Column( + children: [ + SummaryRow( + label: "Subtotal", + value: CurrencyFormat.formatToIdr(orderDetail.totalOrder, 0), + ), + if (orderDetail.discount > 0) + SummaryRow( + label: "Diskon", + value: CurrencyFormat.formatToIdr(-orderDetail.discount, 0), + color: Colors.red, + ), + if (orderDetail.tax > 0) + SummaryRow( + label: "Pajak", + value: "+" + CurrencyFormat.formatToIdr(orderDetail.tax, 0), + color: Colors.green, + ), + + Divider(height: 3.h, color: Colors.grey.shade200), + + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "TOTAL", + style: TextStyle( + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + fontWeight: FontWeight.bold, + ), + ), + Text( + CurrencyFormat.formatToIdr(orderDetail.totalAmount, 0), + style: TextStyle( + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + fontWeight: FontWeight.bold, + color: AppColor.primaryColor, + ), + ), + ], + ), + + SizedBox(height: 2.h), + + SummaryRow( + label: "Bayar Tunai", + value: CurrencyFormat.formatToIdr(orderDetail.payAmount, 0), + ), + SizedBox(height: 0.5.h), + SummaryRow( + label: "Kembali", + value: CurrencyFormat.formatToIdr(orderDetail.changeAmount, 0), + isBold: true, + ), + ], + ), + ); + } +} diff --git a/Mobile Operasional/lib/features/report/transaction/presentation/widgets/payment_proof.dart b/Mobile Operasional/lib/features/report/transaction/presentation/widgets/payment_proof.dart new file mode 100644 index 0000000..e053455 --- /dev/null +++ b/Mobile Operasional/lib/features/report/transaction/presentation/widgets/payment_proof.dart @@ -0,0 +1,127 @@ +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:niogu_app/core/constants/app_font_size.dart'; +import 'package:niogu_app/core/widgets/custom_snackbar.dart'; +import 'package:sizer/sizer.dart'; + +class PaymentProof extends StatelessWidget { + final String imagePath; + const PaymentProof({super.key, required this.imagePath}); + + @override + Widget build(BuildContext context) { + final File imageFile = File(imagePath); + + final bool imageFileExists = imageFile.existsSync(); + + return Padding( + padding: EdgeInsets.all(4.w), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Bukti Pembayaran", + style: TextStyle( + fontSize: AppFontSize.small.sp, + fontWeight: FontWeight.bold, + color: Colors.grey[700], + ), + ), + SizedBox(height: 1.5.h), + GestureDetector( + onTap: () { + if (!imageFileExists) { + CustomSnackbar.showError(context, "Ups, terjadi kesalahan"); + return; + } + + showDialog( + context: context, + useRootNavigator: true, + builder: (context) => Dialog( + insetPadding: EdgeInsets.symmetric( + horizontal: 2.w, + vertical: 2.h, + ), + backgroundColor: Colors.transparent, + child: GestureDetector( + onTap: () => Navigator.pop(context), + child: Container( + width: MediaQuery.of(context).size.width, + constraints: BoxConstraints( + maxWidth: 85.w, + maxHeight: 85.h, + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(4.w), + child: InteractiveViewer( + child: Image.file( + File(imagePath), + fit: BoxFit.contain, + width: double.infinity, + ), + ), + ), + ), + ), + ), + ); + }, + child: imageFileExists + ? Container( + height: 20.h, + width: double.infinity, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(2.w), + image: DecorationImage( + image: FileImage(File(imagePath)), + fit: BoxFit.cover, + ), + ), + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(2.w), + color: Colors.black.withOpacity(0.2), + ), + child: const Center( + child: Icon( + Icons.fullscreen_rounded, + color: Colors.white, + size: 30, + ), + ), + ), + ) + : Container( + height: 20.h, + width: double.infinity, + decoration: BoxDecoration( + border: Border.all( + color: Colors.grey.shade300, + style: BorderStyle.solid, + ), + borderRadius: BorderRadius.circular(2.w), + color: Colors.white, + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.error, color: Colors.grey, size: 8.w), + SizedBox(height: 0.5.h), + Text( + "Ups, Terjadi Kesalahan", + style: TextStyle( + color: Colors.grey.shade600, + fontSize: AppFontSize.medium.sp, + ), + ), + ], + ), + ), + ), + ], + ), + ); + } +} diff --git a/Mobile Operasional/lib/features/report/transaction/presentation/widgets/payment_purchase_summary.dart b/Mobile Operasional/lib/features/report/transaction/presentation/widgets/payment_purchase_summary.dart new file mode 100644 index 0000000..d945a79 --- /dev/null +++ b/Mobile Operasional/lib/features/report/transaction/presentation/widgets/payment_purchase_summary.dart @@ -0,0 +1,105 @@ +import 'package:flutter/material.dart'; +import 'package:niogu_app/core/constants/app_color.dart'; +import 'package:niogu_app/core/constants/app_font_size.dart'; +import 'package:niogu_app/core/utils/currency_format.dart'; +import 'package:niogu_app/core/enums/payment_status.dart'; +import 'package:niogu_app/features/report/transaction/domain/entities/transaction_report.dart'; +import 'package:niogu_app/features/report/transaction/presentation/widgets/summary_row.dart'; +import 'package:sizer/sizer.dart'; + +class PaymentPurchaseSummary extends StatelessWidget { + final PurchaseDetail purchaseDetail; + const PaymentPurchaseSummary({super.key, required this.purchaseDetail}); + + @override + Widget build(BuildContext context) { + final bool isTablet = 100.w > 600; + + final paymentStatus = purchaseDetail.paymentStatus; + + return Padding( + padding: EdgeInsets.symmetric(horizontal: 5.w, vertical: 2.h), + child: Column( + children: [ + SummaryRow( + label: "Subtotal", + value: CurrencyFormat.formatToIdr(purchaseDetail.totalPurchase, 0), + ), + if (purchaseDetail.discount > 0) + SummaryRow( + label: "Diskon", + value: CurrencyFormat.formatToIdr(-purchaseDetail.discount, 0), + color: Colors.red, + ), + if (purchaseDetail.tax > 0) + SummaryRow( + label: "Pajak", + value: "+" + CurrencyFormat.formatToIdr(purchaseDetail.tax, 0), + color: Colors.green, + ), + + Divider(height: 3.h, color: Colors.grey.shade200), + + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "TOTAL", + style: TextStyle( + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + fontWeight: FontWeight.bold, + ), + ), + Text( + CurrencyFormat.formatToIdr(purchaseDetail.totalAmount, 0), + style: TextStyle( + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + fontWeight: FontWeight.bold, + color: AppColor.primaryColor, + ), + ), + ], + ), + + SizedBox(height: 2.h), + + if (paymentStatus == PaymentStatus.paid) ...[ + SummaryRow( + label: "Bayar Tunai", + value: CurrencyFormat.formatToIdr(purchaseDetail.payAmount, 0), + ), + SizedBox(height: 0.5.h), + SummaryRow( + label: "Kembali", + value: CurrencyFormat.formatToIdr(purchaseDetail.changeAmount, 0), + isBold: true, + ), + ] else if (paymentStatus == PaymentStatus.partial) ...[ + SummaryRow( + label: "Bayar Sebagian", + value: CurrencyFormat.formatToIdr(purchaseDetail.payAmount, 0), + ), + SizedBox(height: 0.5.h), + SummaryRow( + label: "Kurang Bayar", + value: CurrencyFormat.formatToIdr( + purchaseDetail.underPaymentAmount, + 0, + ), + isBold: true, + ), + ] else ...[ + SummaryRow( + label: "Hutang", + value: CurrencyFormat.formatToIdr(purchaseDetail.debtAmount, 0), + ), + ], + ], + ), + ); + } +} diff --git a/Mobile Operasional/lib/features/report/transaction/presentation/widgets/purchase_item_list.dart b/Mobile Operasional/lib/features/report/transaction/presentation/widgets/purchase_item_list.dart new file mode 100644 index 0000000..3d9047b --- /dev/null +++ b/Mobile Operasional/lib/features/report/transaction/presentation/widgets/purchase_item_list.dart @@ -0,0 +1,125 @@ +import 'package:flutter/material.dart'; +import 'package:niogu_app/core/constants/app_font_size.dart'; +import 'package:niogu_app/core/utils/currency_format.dart'; +import 'package:niogu_app/core/utils/extension_format.dart'; +import 'package:niogu_app/features/report/transaction/domain/entities/transaction_report.dart'; +import 'package:sizer/sizer.dart'; + +class PurchaseItemList extends StatelessWidget { + final PurchaseDetail purchaseDetail; + const PurchaseItemList({super.key, required this.purchaseDetail}); + + @override + Widget build(BuildContext context) { + final bool isTablet = 100.w > 600; + return Padding( + padding: EdgeInsets.symmetric(horizontal: 5.w, vertical: 2.h), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Detail Pembelian", + style: TextStyle( + fontSize: isTablet ? AppFontSize.medium.sp : AppFontSize.small.sp, + fontWeight: FontWeight.bold, + color: Colors.grey[800], + ), + ), + SizedBox(height: 1.5.h), + ...purchaseDetail.items.map((item) { + return Padding( + padding: EdgeInsets.only(bottom: 1.5.h), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + width: 10.w, + height: 10.w, + padding: EdgeInsets.symmetric( + horizontal: 2.w, + vertical: 0.5.h, + ), + decoration: BoxDecoration( + color: Colors.grey[100], + borderRadius: BorderRadius.circular(1.w), + border: Border.all(color: Colors.grey.shade300), + ), + child: Center( + child: Text( + "${item.quantity.toStringWithoutTrailingZero()}x", + style: TextStyle( + fontSize: isTablet + ? (AppFontSize.medium - 1.75).sp + : (AppFontSize.small - 1.75).sp, + fontWeight: FontWeight.bold, + color: Colors.black54, + ), + ), + ), + ), + + SizedBox(width: 3.w), + + // Detail Produk + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + item.name, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + fontWeight: FontWeight.bold, + color: Colors.black87, + ), + ), + if (item.variantName != null) ...[ + SizedBox(height: 0.75.h), + Text( + item.variantName!, + style: TextStyle( + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + color: Colors.grey[600], + ), + ), + ], + SizedBox(height: 0.75.h), + // Harga Satuan + Text( + "${CurrencyFormat.formatToIdr(item.purchasePrice, 0)}", + style: TextStyle( + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + color: Colors.grey[500], + ), + ), + ], + ), + ), + + // Subtotal Item + Text( + CurrencyFormat.formatToIdr(item.subtotal, 0), + style: TextStyle( + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + fontWeight: FontWeight.bold, + color: Colors.black87, + ), + ), + ], + ), + ); + }), + ], + ), + ); + } +} diff --git a/Mobile Operasional/lib/features/report/transaction/presentation/widgets/sliver_header_delegate.dart b/Mobile Operasional/lib/features/report/transaction/presentation/widgets/sliver_header_delegate.dart new file mode 100644 index 0000000..1c521ff --- /dev/null +++ b/Mobile Operasional/lib/features/report/transaction/presentation/widgets/sliver_header_delegate.dart @@ -0,0 +1,32 @@ +import 'package:flutter/material.dart'; +import 'package:sizer/sizer.dart'; + +class SliverHeaderDelegate extends SliverPersistentHeaderDelegate { + final Widget child; + final String? value; + + SliverHeaderDelegate({required this.child, this.value}); + + @override + double get minExtent => 7.h; + + @override + double get maxExtent => 7.h; + + @override + Widget build( + BuildContext context, + double shrinkOffset, + bool overlapsContent, + ) { + return Material( + color: Colors.white, + child: Container(alignment: Alignment.centerLeft, child: child), + ); + } + + @override + bool shouldRebuild(covariant SliverHeaderDelegate oldDelegate) { + return value != null ? (oldDelegate.value != value || oldDelegate.child != child) : false; + } +} diff --git a/Mobile Operasional/lib/features/report/transaction/presentation/widgets/summary_row.dart b/Mobile Operasional/lib/features/report/transaction/presentation/widgets/summary_row.dart new file mode 100644 index 0000000..a507def --- /dev/null +++ b/Mobile Operasional/lib/features/report/transaction/presentation/widgets/summary_row.dart @@ -0,0 +1,40 @@ +import 'package:flutter/material.dart'; +import 'package:niogu_app/core/constants/app_font_size.dart'; +import 'package:sizer/sizer.dart'; + +class SummaryRow extends StatelessWidget { + final String label; + final String value; + final Color? color; + final bool isBold; + const SummaryRow({super.key, required this.label, required this.value, this.color, this.isBold = false}); + + @override + Widget build(BuildContext context) { + final bool isTablet = 100.w > 600; + return Padding( + padding: EdgeInsets.only(bottom: 0.8.h), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + label, + style: TextStyle( + fontSize: isTablet ? AppFontSize.medium.sp : AppFontSize.small.sp, + color: Colors.grey[700], + fontWeight: isBold ? FontWeight.bold : FontWeight.normal, + ), + ), + Text( + value, + style: TextStyle( + fontSize: isTablet ? AppFontSize.medium.sp : AppFontSize.small.sp, + color: color ?? Colors.black87, + fontWeight: isBold ? FontWeight.bold : FontWeight.normal, + ), + ), + ], + ), + ); + } +} \ No newline at end of file diff --git a/Mobile Operasional/lib/features/report/transaction/presentation/widgets/transaction_item.dart b/Mobile Operasional/lib/features/report/transaction/presentation/widgets/transaction_item.dart new file mode 100644 index 0000000..4b3faf3 --- /dev/null +++ b/Mobile Operasional/lib/features/report/transaction/presentation/widgets/transaction_item.dart @@ -0,0 +1,114 @@ +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; +import 'package:niogu_app/core/constants/app_font_size.dart'; +import 'package:niogu_app/core/utils/currency_format.dart'; +import 'package:niogu_app/core/utils/time_zone.dart'; +import 'package:niogu_app/features/report/transaction/domain/entities/transaction_report.dart'; +import 'package:sizer/sizer.dart'; + +class TransactionItem extends StatelessWidget { + final TransactionReportItem item; + final VoidCallback onTap; + const TransactionItem({super.key, required this.item, required this.onTap}); + + @override + Widget build(BuildContext context) { + final bool isTablet = 100.w >= 600; + return GestureDetector( + onTap: onTap, + child: Container( + clipBehavior: Clip.antiAlias, + margin: EdgeInsets.symmetric(horizontal: 5.w, vertical: 0.8.h), + padding: EdgeInsets.all(3.w), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(2.5.w), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.02), + blurRadius: 5, + offset: const Offset(0, 2), + ), + ], + ), + child: Row( + children: [ + // Icon + Container( + padding: EdgeInsets.all(2.5.w), + decoration: BoxDecoration( + color: item.isIncome ? Colors.green[50] : Colors.red[50], + borderRadius: BorderRadius.circular(2.5.w), + ), + child: Icon( + item.isIncome + ? Icons.arrow_upward_rounded + : Icons.arrow_downward_rounded, + color: item.isIncome ? Colors.green[700] : Colors.red[700], + size: 5.w, + ), + ), + + SizedBox(width: 3.w), + + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + item.title, + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + color: Colors.black87, + ), + ), + SizedBox(height: 0.75.h), + Text( + item.subtitle, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + color: Colors.grey, + fontSize: isTablet + ? (AppFontSize.medium - 1.25).sp + : (AppFontSize.small - 1.25).sp, + ), + ), + ], + ), + ), + + Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Text( + "${item.isIncome ? '+' : '-'} ${CurrencyFormat.formatToIdr(item.amount, 0)}", + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + color: item.isIncome ? Colors.green[700] : Colors.red[700], + ), + ), + SizedBox(height: 0.75.h), + Text( + "${DateFormat('HH:mm').format(item.date)} ${TimeZone.getCurrentTimeZone()}", + style: TextStyle( + color: Colors.grey, + fontSize: isTablet + ? (AppFontSize.medium - 1.25).sp + : (AppFontSize.small - 1.25).sp, + ), + ), + ], + ), + ], + ), + ), + ); + } +} diff --git a/Mobile Operasional/lib/features/report/transaction/presentation/widgets/transaction_report_shimmer.dart b/Mobile Operasional/lib/features/report/transaction/presentation/widgets/transaction_report_shimmer.dart new file mode 100644 index 0000000..2d592e5 --- /dev/null +++ b/Mobile Operasional/lib/features/report/transaction/presentation/widgets/transaction_report_shimmer.dart @@ -0,0 +1,48 @@ +import 'package:flutter/material.dart'; +import 'package:shimmer/shimmer.dart'; +import 'package:sizer/sizer.dart'; + +class TransactionReportShimmer extends StatelessWidget { + const TransactionReportShimmer({super.key}); + + @override + Widget build(BuildContext context) { + return Shimmer.fromColors( + baseColor: Colors.grey.shade300, + highlightColor: Colors.grey.shade100, + child: Column( + children: List.generate(5, (index) => _buildShimmerItem()), + ), + ); + } + + Widget _buildShimmerItem() { + return Padding( + padding: EdgeInsets.symmetric(horizontal: 5.w, vertical: 1.h), + child: Row( + children: [ + Container( + width: 12.w, + height: 12.w, + decoration: const BoxDecoration( + color: Colors.white, + shape: BoxShape.circle, + ), + ), + SizedBox(width: 4.w), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container(width: 40.w, height: 1.5.h, color: Colors.white), + SizedBox(height: 1.h), + Container(width: 30.w, height: 1.2.h, color: Colors.white), + ], + ), + ), + Container(width: 20.w, height: 1.5.h, color: Colors.white), + ], + ), + ); + } +} \ No newline at end of file diff --git a/Mobile Operasional/lib/features/splash/screens/splash_screen.dart b/Mobile Operasional/lib/features/splash/screens/splash_screen.dart new file mode 100644 index 0000000..15b30a1 --- /dev/null +++ b/Mobile Operasional/lib/features/splash/screens/splash_screen.dart @@ -0,0 +1,236 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; +import 'package:niogu_app/core/constants/app_asset.dart'; +import 'package:niogu_app/core/constants/app_color.dart'; +import 'package:niogu_app/core/constants/app_font_size.dart'; +import 'package:niogu_app/core/database/app_database.dart'; +import 'package:niogu_app/core/database/database_holder.dart'; +import 'package:niogu_app/core/providers/app_provider.dart'; +import 'package:niogu_app/core/router/app_route.dart'; +import 'package:niogu_app/core/system/system_setting.dart'; +import 'package:sizer/sizer.dart'; + +class SplashScreen extends ConsumerStatefulWidget { + const SplashScreen({super.key}); + + @override + ConsumerState createState() => _SplashScreenState(); +} + +class _SplashScreenState extends ConsumerState + with SingleTickerProviderStateMixin { + late AnimationController _controller; + late Animation _fadeAnimation; + late Animation _slideAnimation; + + @override + void initState() { + // TODO: implement initState + super.initState(); + + _controller = AnimationController( + vsync: this, + duration: const Duration(seconds: 2), + ); + + _fadeAnimation = Tween( + begin: 0.0, + end: 1.0, + ).animate(CurvedAnimation(parent: _controller, curve: Curves.easeIn)); + + _slideAnimation = Tween( + begin: const Offset(0, 0.5), + end: Offset.zero, + ).animate(CurvedAnimation(parent: _controller, curve: Curves.easeOutQuart)); + + _controller.forward(); + + WidgetsBinding.instance.addPostFrameCallback((_) { + _initDatabase(); + + _setCurrentOutlet(); + + _setCurrentUser(); + + if (!mounted) return; + + Future.delayed(const Duration(seconds: 4), () { + context.goNamed(AppRoute.homeScreen); + }); + }); + } + + @override + void dispose() { + // TODO: implement dispose + _controller.dispose(); + super.dispose(); + } + + Future _initDatabase() async { + final tenantCode = await SystemSetting.getTenantCode(); + + if (tenantCode != null) { + final appDatabase = AppDatabase(); + + await setDatabase(appDatabase); + + await appDatabase.customSelect("SELECT 1").getSingle(); + + ref.invalidate(appDatabaseProvider); + + final syncService = ref.read(syncServiceProvider); + + syncService.stopWatching(); + + await syncService.startWatching(); + } + } + + Future _setCurrentOutlet() async { + final currentOutletId = await SystemSetting.getCurrentOutletId(); + + final currentOutletName = await SystemSetting.getCurrentOutletName(); + + ref.read(currentOutletIdProvider.notifier).state = currentOutletId; + + ref.read(currentOutletNameProvider.notifier).state = currentOutletName; + } + + Future _setCurrentUser() async { + final currentUserId = await SystemSetting.getCurrentUserId(); + + final currentUserName = await SystemSetting.getCurrentUserName(); + + final currentUserEmail = await SystemSetting.getCurrentUserEmail(); + + final currentUserRole = await SystemSetting.getCurrentUserRole(); + + final outletIdByOwner = await SystemSetting.getOutletIdByOwner(); + + final isLoggedIn = await SystemSetting.isLoggedIn(); + + ref.read(currentUserIdProvider.notifier).state = currentUserId; + + ref.read(currentUserNameProvider.notifier).state = currentUserName; + + ref.read(currentUserEmailProvider.notifier).state = currentUserEmail; + + ref.read(currentUserRoleProvider.notifier).state = currentUserRole; + + ref.read(outletIdByOwnerProvider.notifier).state = outletIdByOwner; + + ref.read(currentStatusLoginProvider.notifier).state = isLoggedIn; + } + + @override + Widget build(BuildContext context) { + return LayoutBuilder( + builder: (context, constraints) { + final bool isTablet = 100.w >= 600; + return SafeArea( + top: false, + bottom: true, + right: false, + left: false, + child: Scaffold( + backgroundColor: Colors.white, + body: Stack( + children: [ + Center( + child: FadeTransition( + opacity: _fadeAnimation, + child: SlideTransition( + position: _slideAnimation, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Image.asset( + AppAsset.LOGO_UPDATE, + height: 30.w, + width: 30.w, + fit: BoxFit.contain, + ), + + SizedBox(height: 3.h), + + Text( + "niogu", + style: TextStyle( + fontSize: AppFontSize.extraLarge.sp, + fontWeight: FontWeight.w900, + color: AppColor.primaryColor, + letterSpacing: -1.0, + height: 1.0, + ), + ), + + SizedBox(height: 3.h), + + Text( + "Cara Mudah Berniaga", + style: TextStyle( + fontSize: AppFontSize.large.sp, + color: Colors.grey[500], + letterSpacing: 2.0, + fontWeight: FontWeight.w500, + ), + ), + ], + ), + ), + ), + ), + + Positioned( + bottom: 5.h, + left: 0, + right: 0, + child: FadeTransition( + opacity: _fadeAnimation, + child: Column( + children: [ + Text( + "Powered By", + style: TextStyle( + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + color: Colors.grey[500], + ), + ), + SizedBox(height: 0.5.h), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.bolt_rounded, + size: isTablet ? 6.w : 4.w, + color: AppColor.primaryColor, + ), + SizedBox(width: 1.w), + Text( + "PT. Niaga Nusantara", + style: TextStyle( + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + fontWeight: FontWeight.bold, + color: Colors.black87, + ), + ), + ], + ), + ], + ), + ), + ), + ], + ), + ), + ); + }, + ); + } +} diff --git a/Mobile Operasional/lib/features/stock_check/data/repositories/stock_checked_repository_impl.dart b/Mobile Operasional/lib/features/stock_check/data/repositories/stock_checked_repository_impl.dart new file mode 100644 index 0000000..213e14e --- /dev/null +++ b/Mobile Operasional/lib/features/stock_check/data/repositories/stock_checked_repository_impl.dart @@ -0,0 +1,17 @@ +import 'package:niogu_app/core/database/app_database.dart'; +import 'package:niogu_app/features/stock_check/domain/repositories/i_stock_checked_repository.dart'; +import 'package:niogu_app/features/stock_in/domain/entities/stock_in.dart'; + +class StockCheckedRepositoryImpl implements IStockCheckedRepository { + final AppDatabase _appDatabase; + + StockCheckedRepositoryImpl(this._appDatabase); + + @override + Future updateStockItem(List items) async { + return await _appDatabase.stockCardDao.updateStockItem( + purchaseItem: [], + items: items, + ); + } +} diff --git a/Mobile Operasional/lib/features/stock_check/domain/entities/selected_checked_item.dart b/Mobile Operasional/lib/features/stock_check/domain/entities/selected_checked_item.dart new file mode 100644 index 0000000..7962996 --- /dev/null +++ b/Mobile Operasional/lib/features/stock_check/domain/entities/selected_checked_item.dart @@ -0,0 +1,49 @@ +import 'package:niogu_app/core/enums/item_type.dart'; + +enum DifferenceType { + suitable('suitable'), + less('less'), + more('more'); + + final String type; + + const DifferenceType(this.type); +} + +class SelectedCheckedItem { + final String id; + final ItemType itemType; + final String itemId; + final DifferenceType differenceType; + final double costPrice; + final double purchasePrice; + final double remainingStock; + final double quantity; + final String? note; + + const SelectedCheckedItem({ + required this.id, + required this.itemType, + required this.itemId, + required this.differenceType, + required this.costPrice, + required this.purchasePrice, + required this.remainingStock, + required this.quantity, + this.note + }); + + SelectedCheckedItem copyWith({double? quantity, String? note}) { + return SelectedCheckedItem( + id: id, + itemType: itemType, + itemId: itemId, + differenceType: differenceType, + costPrice: costPrice, + purchasePrice: purchasePrice, + remainingStock: remainingStock, + quantity: quantity ?? this.quantity, + note: note ?? this.note + ); + } +} diff --git a/Mobile Operasional/lib/features/stock_check/domain/repositories/i_stock_checked_repository.dart b/Mobile Operasional/lib/features/stock_check/domain/repositories/i_stock_checked_repository.dart new file mode 100644 index 0000000..3e56452 --- /dev/null +++ b/Mobile Operasional/lib/features/stock_check/domain/repositories/i_stock_checked_repository.dart @@ -0,0 +1,5 @@ +import 'package:niogu_app/features/stock_in/domain/entities/stock_in.dart'; + +abstract class IStockCheckedRepository { + Future updateStockItem(List items); +} \ No newline at end of file diff --git a/Mobile Operasional/lib/features/stock_check/presentation/controllers/stock_check_controller.dart b/Mobile Operasional/lib/features/stock_check/presentation/controllers/stock_check_controller.dart new file mode 100644 index 0000000..6bfc6fe --- /dev/null +++ b/Mobile Operasional/lib/features/stock_check/presentation/controllers/stock_check_controller.dart @@ -0,0 +1,27 @@ +import 'dart:async'; + +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:niogu_app/features/stock_check/domain/repositories/i_stock_checked_repository.dart'; +import 'package:niogu_app/features/stock_check/presentation/providers/stock_check_provider.dart'; +import 'package:niogu_app/features/stock_in/domain/entities/stock_in.dart'; + +class StockCheckController extends AutoDisposeAsyncNotifier { + late final IStockCheckedRepository _stockCheckedRepository; + + @override + FutureOr build() { + _stockCheckedRepository = ref.read(stockCheckedRepository); + } + + Future updateStockItem(List items) async { + state = const AsyncValue.loading(); + + final result = await AsyncValue.guard(() async { + await _stockCheckedRepository.updateStockItem(items); + }); + + state = result; + + if (result is AsyncError) throw result.error!; + } +} diff --git a/Mobile Operasional/lib/features/stock_check/presentation/providers/stock_check_provider.dart b/Mobile Operasional/lib/features/stock_check/presentation/providers/stock_check_provider.dart new file mode 100644 index 0000000..b4e1aea --- /dev/null +++ b/Mobile Operasional/lib/features/stock_check/presentation/providers/stock_check_provider.dart @@ -0,0 +1,21 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:niogu_app/core/providers/app_provider.dart'; +import 'package:niogu_app/features/stock_check/data/repositories/stock_checked_repository_impl.dart'; +import 'package:niogu_app/features/stock_check/domain/entities/selected_checked_item.dart'; +import 'package:niogu_app/features/stock_check/domain/repositories/i_stock_checked_repository.dart'; +import 'package:niogu_app/features/stock_check/presentation/controllers/stock_check_controller.dart'; + +final stockCheckedRepository = Provider.autoDispose(( + ref, +) { + final appDatabase = ref.watch(appDatabaseProvider); + return StockCheckedRepositoryImpl(appDatabase); +}); + +final selectedCheckedItemProvider = + StateProvider.autoDispose>((ref) => {}); + +final stockCheckedControllerProvider = + AsyncNotifierProvider.autoDispose( + StockCheckController.new, + ); diff --git a/Mobile Operasional/lib/features/stock_check/presentation/screens/product_stock_check_screen.dart b/Mobile Operasional/lib/features/stock_check/presentation/screens/product_stock_check_screen.dart new file mode 100644 index 0000000..ec0d37c --- /dev/null +++ b/Mobile Operasional/lib/features/stock_check/presentation/screens/product_stock_check_screen.dart @@ -0,0 +1,588 @@ +import 'dart:async'; +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; +import 'package:niogu_app/core/constants/app_color.dart'; +import 'package:niogu_app/core/constants/app_font_size.dart'; +import 'package:niogu_app/core/widgets/custom_error_screen.dart'; +import 'package:niogu_app/core/router/app_route.dart'; +import 'package:niogu_app/core/utils/extension_format.dart'; +import 'package:niogu_app/core/enums/item_type.dart'; +import 'package:niogu_app/core/widgets/custom_empty_screen.dart'; +import 'package:niogu_app/features/stock_check/domain/entities/selected_checked_item.dart'; +import 'package:niogu_app/features/stock_check/presentation/providers/stock_check_provider.dart'; +import 'package:niogu_app/features/stock_check/presentation/widgets/stock_check_shimmer.dart'; +import 'package:niogu_app/features/stock_in/domain/entities/stock_in.dart'; +import 'package:niogu_app/features/stock_in/presentation/provider/stock_in_provider.dart'; +import 'package:sizer/sizer.dart'; + +class ProductStockCheckScreen extends ConsumerStatefulWidget { + const ProductStockCheckScreen({super.key}); + + @override + ConsumerState createState() => + ProductStockCheckScreenState(); +} + +class ProductStockCheckScreenState + extends ConsumerState + with AutomaticKeepAliveClientMixin { + final Map _physicalControllers = {}; + + final Map _noteControllers = {}; + + final Map _statusColors = {}; + + final Map _statusTexts = {}; + + Timer? _debounce; + + @override + // TODO: implement wantKeepAlive + bool get wantKeepAlive => true; + + @override + void initState() { + // TODO: implement initState + super.initState(); + } + + @override + void dispose() { + // TODO: implement dispose + for (final controller in _physicalControllers.values) { + controller.dispose(); + } + + for (final controller in _noteControllers.values) { + controller.dispose(); + } + _debounce?.cancel(); + super.dispose(); + } + + TextEditingController _physicalController(String id) { + return _physicalControllers.putIfAbsent(id, () => TextEditingController()); + } + + TextEditingController _noteController(String id) { + return _noteControllers.putIfAbsent(id, () => TextEditingController()); + } + + void _calculateDifference(String value, DisplayItem product) { + if (_debounce?.isActive ?? false) _debounce?.cancel(); + + DifferenceType differenceType = DifferenceType.suitable; + + double quantity = 0.0; + + _debounce = Timer(const Duration(milliseconds: 400), () { + setState(() { + if (value.isEmpty || + (value.isNotEmpty && + (double.tryParse(value) == null || double.parse(value) < 0))) { + _statusColors[product.id] = Colors.grey; + _statusTexts[product.id] = "Belum Dicek"; + return; + } + final double physicalStock = double.parse(value); + + if (physicalStock == product.remainingStock) { + _statusColors[product.id] = Colors.green; + _statusTexts[product.id] = "Sesuai"; + quantity = product.remainingStock; + differenceType = DifferenceType.suitable; + } else if (physicalStock < product.remainingStock) { + _statusColors[product.id] = Colors.red; + _statusTexts[product.id] = + "Kurang ${(physicalStock - product.remainingStock).abs().toStringWithoutTrailingZero()} dari stok di sistem"; + quantity = (physicalStock - product.remainingStock); + differenceType = DifferenceType.less; + } else { + _statusColors[product.id] = Colors.blue; + _statusTexts[product.id] = + "Lebih ${(physicalStock - product.remainingStock).toStringWithoutTrailingZero()} dari stok di sistem"; + quantity = (physicalStock - product.remainingStock); + differenceType = DifferenceType.more; + } + }); + + final selectedItemState = ref.read(selectedCheckedItemProvider); + + final selectedItemNotifier = ref.read( + selectedCheckedItemProvider.notifier, + ); + + if (value.isNotEmpty && + double.tryParse(value.trim()) != null && + double.parse(value.trim()) >= 0) { + if (selectedItemState.containsKey(product.id)) { + final selectedItem = selectedItemState[product.id]; + + selectedItemNotifier.state = { + ...selectedItemState, + product.id: selectedItem!.copyWith(quantity: quantity), + }; + } else { + selectedItemNotifier.state = { + ...selectedItemState, + product.id: SelectedCheckedItem( + id: product.id, + itemType: ItemType.product_variant, + itemId: product.itemId, + differenceType: differenceType, + costPrice: product.costPrice, + purchasePrice: 0.0, + remainingStock: product.remainingStock, + quantity: quantity, + ), + }; + } + } else { + selectedItemNotifier.state = {...selectedItemState}..remove(product.id); + } + }); + } + + void clear() { + for (final controller in _physicalControllers.values) { + controller.clear(); + } + + for (final controller in _noteControllers.values) { + controller.clear(); + } + + _statusColors.clear(); + + _statusTexts.clear(); + } + + @override + Widget build(BuildContext context) { + super.build(context); + + final bool isTablet = 100.w >= 600; + + final productState = ref.watch( + filteredItemProvider(ItemType.product_variant), + ); + + final productEmptyState = ref.watch( + displayItemEmptyProvider(ItemType.product_variant), + ); + + return productState.when( + data: (products) { + switch (productEmptyState) { + case DisplayItemEmpty.loading: + return const SizedBox(); + + case DisplayItemEmpty.empty_database: + return CustomEmptyScreen( + title: "Tidak Ada Produk", + body: "Kamu belum memiliki produk", + textButton: "Buat Produk Pertamamu", + onPressed: () => context.pushNamed(AppRoute.addProductScreen), + ); + case DisplayItemEmpty.empty_search_result: + return SingleChildScrollView( + physics: const AlwaysScrollableScrollPhysics(), + padding: EdgeInsets.symmetric(vertical: 3.h), + child: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + "Produk Tidak Ditemukan", + style: TextStyle( + fontSize: AppFontSize.large.sp, + fontWeight: FontWeight.bold, + color: Colors.black, + ), + ), + + SizedBox(height: 1.5.h), + + Icon( + Icons.search_off_rounded, + size: 15.h, + color: Colors.grey.shade300, + ), + ], + ), + ), + ); + case DisplayItemEmpty.has_data: + return ListView.builder( + padding: EdgeInsets.all(4.w), + itemCount: products.length, + itemBuilder: (context, index) { + final product = products[index]; + + final String path = + product.localImagePath != null && + product.localImagePath!.isNotEmpty + ? product.localImagePath! + : 'image not found'; + + final File imageFile = File(path); + + final bool imageFileExists = imageFile.existsSync(); + + String? difference; + + String? symbol; + + Color? symbolColor; + + if (double.tryParse( + _physicalController(product.id).text.trim(), + ) != + null) { + final double physicalStock = double.parse( + _physicalController(product.id).text.trim(), + ); + + difference = (physicalStock - product.remainingStock) + .abs() + .toStringWithoutTrailingZero(); + if (physicalStock > product.remainingStock) { + symbol = "+"; + symbolColor = Colors.green; + } else if (physicalStock < product.remainingStock) { + symbol = "-"; + symbolColor = Colors.red; + } + } + + return Container( + margin: EdgeInsets.only(bottom: 1.5.h), + padding: EdgeInsets.all(4.w), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(2.5.w), + border: Border.all(color: Colors.grey.shade200), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.02), + blurRadius: 5, + offset: const Offset(0, 2), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + height: 15.w, + width: 15.w, + decoration: BoxDecoration( + border: Border.all( + color: Colors.grey.shade200, + width: 1.5, + ), + borderRadius: BorderRadius.circular(2.5.w), + ), + child: imageFileExists + ? ClipRRect( + borderRadius: BorderRadius.circular(2.5.w), + child: Image.file( + imageFile, + fit: BoxFit.cover, + width: double.infinity, + height: double.infinity, + errorBuilder: + (context, error, stackTrace) { + return const Icon( + Icons.error, + color: Colors.grey, + ); + }, + ), + ) + : Icon( + Icons.image_outlined, + size: 8.w, + color: Colors.grey, + ), + ), + SizedBox(width: 3.w), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + product.name, + maxLines: 2, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + ), + ), + if (product.variantName != null) ...[ + SizedBox(height: 0.5.h), + Text( + product.variantName!, + style: TextStyle( + color: Colors.grey[600], + fontSize: isTablet + ? (AppFontSize.medium - 1.25).sp + : (AppFontSize.small - 1.25).sp, + ), + ), + ], + SizedBox(height: 0.5.h), + Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Container( + padding: EdgeInsets.symmetric( + horizontal: 2.w, + vertical: 0.5.w, + ), + decoration: BoxDecoration( + color: Colors.grey[100], + borderRadius: BorderRadius.circular( + 1.w, + ), + ), + child: Text( + "Sistem: ${product.remainingStock.toStringWithoutTrailingZero()} ${product.unit}", + style: TextStyle( + fontSize: isTablet + ? (AppFontSize.medium - 1.25).sp + : (AppFontSize.small - 1.25).sp, + color: Colors.black87, + fontWeight: FontWeight.w600, + ), + ), + ), + + if (symbol != null && + symbolColor != null && + difference != null) + Text( + "$symbol $difference ${product.unit}", + style: TextStyle( + fontSize: isTablet + ? (AppFontSize.medium - 1.25).sp + : (AppFontSize.small - 1.25).sp, + color: symbolColor, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + ], + ), + ), + ], + ), + + Divider(height: 2.5.h, color: Colors.grey[100]), + + Row( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Expanded( + flex: 3, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Stok Fisik", + style: TextStyle( + fontSize: isTablet + ? (AppFontSize.medium - 1.25).sp + : (AppFontSize.small - 1.25).sp, + color: Colors.grey[600], + ), + ), + SizedBox(height: 0.8.h), + SizedBox( + height: 5.5.h, + child: TextFormField( + controller: _physicalController(product.id), + keyboardType: + const TextInputType.numberWithOptions( + decimal: true, + ), + onChanged: (value) => + _calculateDifference(value, product), + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: isTablet + ? (AppFontSize.medium - 1.25).sp + : (AppFontSize.small - 1.25).sp, + ), + decoration: InputDecoration( + contentPadding: EdgeInsets.symmetric( + horizontal: 3.w, + ), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular( + 2.w, + ), + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular( + 2.5.w, + ), + borderSide: BorderSide( + color: Colors.grey.shade300, + ), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular( + 2.w, + ), + borderSide: BorderSide( + color: AppColor.primaryColor, + width: 1.5, + ), + ), + suffixText: product.unit, + suffixStyle: TextStyle( + fontSize: isTablet + ? (AppFontSize.medium - 1.25).sp + : (AppFontSize.small - 1.25).sp, + color: Colors.grey, + ), + ), + ), + ), + ], + ), + ), + + SizedBox(width: 4.w), + + Expanded( + flex: 6, + child: Container( + height: 5.5.h, + padding: EdgeInsets.symmetric(horizontal: 2.w), + decoration: BoxDecoration( + color: + (_statusColors[product.id] ?? Colors.grey) + .withOpacity(0.1), + borderRadius: BorderRadius.circular(2.5.w), + border: Border.all( + color: + (_statusColors[product.id] ?? Colors.grey) + .withOpacity(0.3), + ), + ), + alignment: Alignment.centerLeft, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Selisih", + style: TextStyle( + fontSize: isTablet + ? (AppFontSize.medium - 1.25).sp + : (AppFontSize.small - 1.25).sp, + color: + (_statusColors[product.id] ?? + Colors.grey) + .withOpacity(0.8), + ), + ), + Text( + _statusTexts[product.id] ?? "Belum Dicek", + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontSize: isTablet + ? (AppFontSize.medium - 1.25).sp + : (AppFontSize.small - 1.25).sp, + fontWeight: FontWeight.bold, + color: + (_statusColors[product.id] ?? + Colors.grey), + ), + ), + ], + ), + ), + ), + ], + ), + + SizedBox(height: 1.5.h), + TextField( + controller: _noteController(product.id), + onChanged: (value) { + final selectedItemState = ref.read( + selectedCheckedItemProvider, + ); + + final selectedItemNotifier = ref.read( + selectedCheckedItemProvider.notifier, + ); + + if (selectedItemState.containsKey(product.id)) { + selectedItemNotifier.state = { + ...selectedItemState, + product.id: selectedItemState[product.id]! + .copyWith(note: value), + }; + } + }, + style: TextStyle( + fontSize: isTablet + ? (AppFontSize.medium - 1.25).sp + : (AppFontSize.small - 1.25).sp, + ), + decoration: InputDecoration( + hintText: "Catatan (Opsional) - cth: Barang rusak", + hintStyle: TextStyle( + fontSize: isTablet + ? (AppFontSize.medium - 1.25).sp + : (AppFontSize.small - 1.25).sp, + color: Colors.grey[400], + ), + isDense: true, + contentPadding: EdgeInsets.symmetric( + horizontal: 3.w, + vertical: 1.2.h, + ), + filled: true, + fillColor: Colors.grey[50], + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(2.5.w), + borderSide: BorderSide.none, + ), + prefixIcon: Icon( + Icons.edit_note, + size: 4.5.w, + color: Colors.grey[400], + ), + ), + ), + ], + ), + ); + }, + ); + } + }, + error: (error, stackTrace) { + return CustomErrorScreen(message: "Ups, terjadi kesalahan", onRefresh: () {}); + }, + loading: () => const StockCheckShimmer(), + ); + } +} diff --git a/Mobile Operasional/lib/features/stock_check/presentation/screens/raw_material_stock_check_screen.dart b/Mobile Operasional/lib/features/stock_check/presentation/screens/raw_material_stock_check_screen.dart new file mode 100644 index 0000000..de08af0 --- /dev/null +++ b/Mobile Operasional/lib/features/stock_check/presentation/screens/raw_material_stock_check_screen.dart @@ -0,0 +1,568 @@ +import 'dart:async'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; +import 'package:niogu_app/core/constants/app_color.dart'; +import 'package:niogu_app/core/constants/app_font_size.dart'; +import 'package:niogu_app/core/widgets/custom_error_screen.dart'; +import 'package:niogu_app/core/router/app_route.dart'; +import 'package:niogu_app/core/utils/extension_format.dart'; +import 'package:niogu_app/core/enums/item_type.dart'; +import 'package:niogu_app/core/widgets/custom_empty_screen.dart'; +import 'package:niogu_app/features/stock_check/domain/entities/selected_checked_item.dart'; +import 'package:niogu_app/features/stock_check/presentation/providers/stock_check_provider.dart'; +import 'package:niogu_app/features/stock_check/presentation/widgets/stock_check_shimmer.dart'; +import 'package:niogu_app/features/stock_in/domain/entities/stock_in.dart'; +import 'package:niogu_app/features/stock_in/presentation/provider/stock_in_provider.dart'; +import 'package:sizer/sizer.dart'; + +class RawMaterialStockCheckScreen extends ConsumerStatefulWidget { + const RawMaterialStockCheckScreen({super.key}); + + @override + ConsumerState createState() => + RawMaterialStockCheckScreenState(); +} + +class RawMaterialStockCheckScreenState + extends ConsumerState + with AutomaticKeepAliveClientMixin { + final Map _physicalControllers = {}; + + final Map _noteControllers = {}; + + final Map _statusColors = {}; + + final Map _statusTexts = {}; + + Timer? _debounce; + + @override + // TODO: implement wantKeepAlive + bool get wantKeepAlive => true; + + @override + void initState() { + // TODO: implement initState + super.initState(); + } + + @override + void dispose() { + // TODO: implement dispose + for (final controller in _physicalControllers.values) { + controller.dispose(); + } + + for (final controller in _noteControllers.values) { + controller.dispose(); + } + _debounce?.cancel(); + super.dispose(); + } + + TextEditingController _physicalController(String id) { + return _physicalControllers.putIfAbsent(id, () => TextEditingController()); + } + + TextEditingController _noteController(String id) { + return _noteControllers.putIfAbsent(id, () => TextEditingController()); + } + + void _calculateDifference(String value, DisplayItem rawMaterial) { + if (_debounce?.isActive ?? false) _debounce?.cancel(); + + DifferenceType differenceType = DifferenceType.suitable; + + double quantity = 0.0; + + _debounce = Timer(const Duration(milliseconds: 400), () { + setState(() { + if (value.isEmpty || + (value.isNotEmpty && + (double.tryParse(value) == null || double.parse(value) < 0))) { + _statusColors[rawMaterial.id] = Colors.grey; + _statusTexts[rawMaterial.id] = "Belum Dicek"; + return; + } + + final double physicalStock = double.parse(value); + + if (physicalStock == rawMaterial.remainingStock) { + _statusColors[rawMaterial.id] = Colors.green; + _statusTexts[rawMaterial.id] = "Sesuai"; + quantity = rawMaterial.remainingStock; + differenceType = DifferenceType.suitable; + } else if (physicalStock < rawMaterial.remainingStock) { + _statusColors[rawMaterial.id] = Colors.red; + _statusTexts[rawMaterial.id] = + "Kurang ${(physicalStock - rawMaterial.remainingStock).abs().toStringWithoutTrailingZero()} dari stok di sistem"; + quantity = (physicalStock - rawMaterial.remainingStock); + differenceType = DifferenceType.less; + } else { + _statusColors[rawMaterial.id] = Colors.blue; + _statusTexts[rawMaterial.id] = + "Lebih ${(physicalStock - rawMaterial.remainingStock).toStringWithoutTrailingZero()} dari stok di sistem"; + quantity = (physicalStock - rawMaterial.remainingStock); + differenceType = DifferenceType.more; + } + }); + + final selectedItemState = ref.read(selectedCheckedItemProvider); + + final selectedItemNotifier = ref.read( + selectedCheckedItemProvider.notifier, + ); + + if (value.isNotEmpty && + double.tryParse(value.trim()) != null && + double.parse(value.trim()) >= 0) { + if (selectedItemState.containsKey(rawMaterial.id)) { + final selectedItem = selectedItemState[rawMaterial.id]; + + selectedItemNotifier.state = { + ...selectedItemState, + rawMaterial.id: selectedItem!.copyWith(quantity: quantity), + }; + } else { + selectedItemNotifier.state = { + ...selectedItemState, + rawMaterial.id: SelectedCheckedItem( + id: rawMaterial.id, + itemType: ItemType.raw_material, + itemId: rawMaterial.itemId, + differenceType: differenceType, + costPrice: 0.0, + purchasePrice: rawMaterial.purchasePrice, + remainingStock: rawMaterial.remainingStock, + quantity: quantity, + ), + }; + } + } else { + selectedItemNotifier.state = {...selectedItemState} + ..remove(rawMaterial.id); + } + }); + } + + void clear() { + for (final controller in _physicalControllers.values) { + controller.clear(); + } + + for (final controller in _noteControllers.values) { + controller.clear(); + } + + _statusColors.clear(); + + _statusTexts.clear(); + } + + @override + Widget build(BuildContext context) { + super.build(context); + + final bool isTablet = 100.w >= 600; + + final rawMaterialState = ref.watch( + filteredItemProvider(ItemType.raw_material), + ); + + final rawMaterialEmptyState = ref.watch( + displayItemEmptyProvider(ItemType.raw_material), + ); + + return rawMaterialState.when( + data: (rawMaterials) { + switch (rawMaterialEmptyState) { + case DisplayItemEmpty.loading: + return const SizedBox(); + + case DisplayItemEmpty.empty_database: + return CustomEmptyScreen( + title: "Tidak Ada Produk", + body: "Kamu belum memiliki produk", + textButton: "Buat Produk Pertamamu", + onPressed: () => context.pushNamed(AppRoute.addRawMaterialScreen), + ); + case DisplayItemEmpty.empty_search_result: + return SingleChildScrollView( + physics: const AlwaysScrollableScrollPhysics(), + padding: EdgeInsets.symmetric(vertical: 3.h), + child: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + "Produk Tidak Ditemukan", + style: TextStyle( + fontSize: AppFontSize.large.sp, + fontWeight: FontWeight.bold, + color: Colors.black, + ), + ), + + SizedBox(height: 1.5.h), + + Icon( + Icons.search_off_rounded, + size: 15.h, + color: Colors.grey.shade300, + ), + ], + ), + ), + ); + case DisplayItemEmpty.has_data: + return ListView.builder( + padding: EdgeInsets.all(4.w), + itemCount: rawMaterials.length, + itemBuilder: (context, index) { + final rawMaterial = rawMaterials[index]; + + String? difference; + + String? symbol; + + Color? symbolColor; + + if (double.tryParse( + _physicalController(rawMaterial.id).text.trim(), + ) != + null) { + final double physicalStock = double.parse( + _physicalController(rawMaterial.id).text.trim(), + ); + + difference = (physicalStock - rawMaterial.remainingStock) + .abs() + .toStringWithoutTrailingZero(); + if (physicalStock > rawMaterial.remainingStock) { + symbol = "+"; + symbolColor = Colors.green; + } else if (physicalStock < rawMaterial.remainingStock) { + symbol = "-"; + symbolColor = Colors.red; + } + } + + return Container( + margin: EdgeInsets.only(bottom: 1.5.h), + padding: EdgeInsets.all(4.w), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(2.5.w), + border: Border.all(color: Colors.grey.shade200), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.02), + blurRadius: 5, + offset: const Offset(0, 2), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + height: 15.w, + width: 15.w, + decoration: BoxDecoration( + border: Border.all( + color: Colors.grey.shade200, + width: 1.5, + ), + borderRadius: BorderRadius.circular(2.5.w), + ), + child: Icon( + Icons.inventory_2_rounded, + size: 8.w, + color: Colors.grey, + ), + ), + SizedBox(width: 3.w), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + rawMaterial.name, + maxLines: 2, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + ), + ), + if (rawMaterial.variantName != null) ...[ + SizedBox(height: 0.5.h), + Text( + rawMaterial.variantName!, + style: TextStyle( + color: Colors.grey[600], + fontSize: isTablet + ? (AppFontSize.medium - 1.25).sp + : (AppFontSize.small - 1.25).sp, + ), + ), + ], + SizedBox(height: 0.5.h), + Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Container( + padding: EdgeInsets.symmetric( + horizontal: 2.w, + vertical: 0.5.w, + ), + decoration: BoxDecoration( + color: Colors.grey[100], + borderRadius: BorderRadius.circular( + 1.w, + ), + ), + child: Text( + "Sistem: ${rawMaterial.remainingStock.toStringWithoutTrailingZero()} ${rawMaterial.unit}", + style: TextStyle( + fontSize: isTablet + ? (AppFontSize.medium - 1.25).sp + : (AppFontSize.small - 1.25).sp, + color: Colors.black87, + fontWeight: FontWeight.w600, + ), + ), + ), + + if (symbol != null && + symbolColor != null && + difference != null) + Text( + "$symbol $difference ${rawMaterial.unit}", + style: TextStyle( + fontSize: isTablet + ? (AppFontSize.medium - 1.25).sp + : (AppFontSize.small - 1.25).sp, + color: symbolColor, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + ], + ), + ), + ], + ), + + Divider(height: 2.5.h, color: Colors.grey[100]), + + Row( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Expanded( + flex: 3, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Stok Fisik", + style: TextStyle( + fontSize: isTablet + ? (AppFontSize.medium - 1.25).sp + : (AppFontSize.small - 1.25).sp, + color: Colors.grey[600], + ), + ), + SizedBox(height: 0.8.h), + SizedBox( + height: 5.5.h, + child: TextFormField( + controller: _physicalController( + rawMaterial.id, + ), + keyboardType: + const TextInputType.numberWithOptions( + decimal: true, + ), + onChanged: (value) => _calculateDifference( + value, + rawMaterial, + ), + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: isTablet + ? (AppFontSize.medium - 1.25).sp + : (AppFontSize.small - 1.25).sp, + ), + decoration: InputDecoration( + contentPadding: EdgeInsets.symmetric( + horizontal: 3.w, + ), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular( + 2.w, + ), + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular( + 2.5.w, + ), + borderSide: BorderSide( + color: Colors.grey.shade300, + ), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular( + 2.w, + ), + borderSide: BorderSide( + color: AppColor.primaryColor, + width: 1.5, + ), + ), + suffixText: rawMaterial.unit, + suffixStyle: TextStyle( + fontSize: isTablet + ? (AppFontSize.medium - 1.25).sp + : (AppFontSize.small - 1.25).sp, + color: Colors.grey, + ), + ), + ), + ), + ], + ), + ), + + SizedBox(width: 4.w), + + Expanded( + flex: 6, + child: Container( + height: 5.5.h, + padding: EdgeInsets.symmetric(horizontal: 2.w), + decoration: BoxDecoration( + color: + (_statusColors[rawMaterial.id] ?? + Colors.grey) + .withOpacity(0.1), + borderRadius: BorderRadius.circular(2.5.w), + border: Border.all( + color: + (_statusColors[rawMaterial.id] ?? + Colors.grey) + .withOpacity(0.3), + ), + ), + alignment: Alignment.centerLeft, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Selisih", + style: TextStyle( + fontSize: isTablet + ? (AppFontSize.medium - 1.25).sp + : (AppFontSize.small - 1.25).sp, + color: + (_statusColors[rawMaterial.id] ?? + Colors.grey) + .withOpacity(0.8), + ), + ), + Text( + _statusTexts[rawMaterial.id] ?? + "Belum Dicek", + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontSize: isTablet + ? (AppFontSize.medium - 1.25).sp + : (AppFontSize.small - 1.25).sp, + fontWeight: FontWeight.bold, + color: + (_statusColors[rawMaterial.id] ?? + Colors.grey), + ), + ), + ], + ), + ), + ), + ], + ), + + SizedBox(height: 1.5.h), + TextField( + controller: _noteController(rawMaterial.id), + onChanged: (value) { + final selectedItemState = ref.read( + selectedCheckedItemProvider, + ); + + final selectedItemNotifier = ref.read( + selectedCheckedItemProvider.notifier, + ); + + if (selectedItemState.containsKey(rawMaterial.id)) { + selectedItemNotifier.state = { + ...selectedItemState, + rawMaterial.id: selectedItemState[rawMaterial.id]! + .copyWith(note: value), + }; + } + }, + style: TextStyle( + fontSize: isTablet + ? (AppFontSize.medium - 1.25).sp + : (AppFontSize.small - 1.25).sp, + ), + decoration: InputDecoration( + hintText: "Catatan (Opsional) - cth: Barang rusak", + hintStyle: TextStyle( + fontSize: isTablet + ? (AppFontSize.medium - 1.25).sp + : (AppFontSize.small - 1.25).sp, + color: Colors.grey[400], + ), + isDense: true, + contentPadding: EdgeInsets.symmetric( + horizontal: 3.w, + vertical: 1.2.h, + ), + filled: true, + fillColor: Colors.grey[50], + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(2.5.w), + borderSide: BorderSide.none, + ), + prefixIcon: Icon( + Icons.edit_note, + size: 4.5.w, + color: Colors.grey[400], + ), + ), + ), + ], + ), + ); + }, + ); + } + }, + error: (error, stackTrace) { + return CustomErrorScreen(message: "Ups, terjadi kesalahan", onRefresh: () {}); + }, + loading: () => const StockCheckShimmer(), + ); + } +} diff --git a/Mobile Operasional/lib/features/stock_check/presentation/screens/stock_check_screen.dart b/Mobile Operasional/lib/features/stock_check/presentation/screens/stock_check_screen.dart new file mode 100644 index 0000000..3341a13 --- /dev/null +++ b/Mobile Operasional/lib/features/stock_check/presentation/screens/stock_check_screen.dart @@ -0,0 +1,311 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; +import 'package:niogu_app/core/components/app_bar_with_tab.dart'; +import 'package:niogu_app/core/constants/app_font_size.dart'; +import 'package:niogu_app/core/enums/item_type.dart'; +import 'package:niogu_app/core/utils/log_message.dart'; +import 'package:niogu_app/core/enums/stock_card_type.dart'; +import 'package:niogu_app/core/widgets/custom_snackbar.dart'; +import 'package:niogu_app/features/stock_check/domain/entities/selected_checked_item.dart'; +import 'package:niogu_app/features/stock_check/presentation/providers/stock_check_provider.dart'; +import 'package:niogu_app/features/stock_check/presentation/screens/product_stock_check_screen.dart'; +import 'package:niogu_app/features/stock_check/presentation/screens/raw_material_stock_check_screen.dart'; +import 'package:niogu_app/features/stock_check/presentation/widgets/stock_adjustmen_confirmation.dart'; +import 'package:niogu_app/features/stock_in/domain/entities/stock_in.dart'; +import 'package:niogu_app/features/stock_in/presentation/provider/stock_in_provider.dart'; +import 'package:niogu_app/features/stock_in/presentation/widgets/summary_tag.dart'; +import 'package:sizer/sizer.dart'; +import 'package:niogu_app/core/constants/app_color.dart'; + +class StockCheckScreen extends ConsumerStatefulWidget { + const StockCheckScreen({super.key}); + + @override + ConsumerState createState() => _StockCheckScreenState(); +} + +class _StockCheckScreenState extends ConsumerState + with SingleTickerProviderStateMixin { + final GlobalKey _productKey = GlobalKey(); + + final GlobalKey _rawMaterialKey = + GlobalKey(); + + late TabController _tabController; + + final FocusNode _searchFocusNode = FocusNode(); + + Color _searchIconColor = Colors.grey; + + Timer? _debounce; + + int _tabIndex = 0; + + @override + void initState() { + super.initState(); + + _tabController = TabController(length: 2, vsync: this); + + _tabController.addListener(_handleTabSelection); + + _searchFocusNode.addListener(() { + setState(() { + _searchIconColor = _searchFocusNode.hasFocus + ? Colors.black + : Colors.grey; + }); + }); + } + + @override + void dispose() { + _searchFocusNode.dispose(); + _tabController.removeListener(_handleTabSelection); + _tabController.dispose(); + _debounce?.cancel(); + super.dispose(); + } + + void _handleTabSelection() { + if (_tabController.index != _tabIndex) { + setState(() { + _tabIndex = _tabController.index; + }); + } + } + + void _onSearchChanged(String value) { + if (_debounce?.isActive ?? false) _debounce?.cancel(); + _debounce = Timer(const Duration(milliseconds: 800), () { + ref.read(itemSearchProvider.notifier).state = value; + }); + } + + void _showConfirmDialog() { + final selectedItemState = ref.read(selectedCheckedItemProvider); + showDialog( + context: context, + builder: (_) => StockAdjustmenConfirmation( + totalItems: selectedItemState.length, + onCancel: () => context.pop(), + onConfirm: () async { + final selectedItems = selectedItemState.values.where((item) { + return item.differenceType != DifferenceType.suitable && + item.quantity != item.remainingStock; + }).toList(); + + final selectedProducts = selectedItemState.values.where((item) { + return item.itemType == ItemType.product_variant; + }).toList(); + + final selectedRawMaterials = selectedItemState.values.where((item) { + return item.itemType == ItemType.raw_material; + }).toList(); + + final items = selectedItems.map((item) { + final stockCardType = item.differenceType == DifferenceType.more + ? StockCardType.adjustment_in + : StockCardType.adjustment_out; + + final double updateStock = item.remainingStock + item.quantity; + + final double currentStock = updateStock; + + return UpdateStockItem( + outletInventoryId: item.id, + stockCardType: stockCardType, + remainingStock: item.remainingStock, + qtyChange: item.quantity, + updateStock: updateStock, + currentStock: currentStock, + costPrice: item.costPrice, + purchasePrice: item.purchasePrice, + notes: item.note, + ); + }).toList(); + + try { + await Future.delayed(const Duration(milliseconds: 400)); + + await ref + .read(stockCheckedControllerProvider.notifier) + .updateStockItem(items); + + if (!mounted) return; + + CustomSnackbar.showSuccess( + context, + "Stok barang berhasil diperbarui", + ); + + if (selectedProducts.isNotEmpty) { + _productKey.currentState?.clear(); + } + + if (selectedRawMaterials.isNotEmpty) { + _rawMaterialKey.currentState?.clear(); + } + + ref.invalidate(selectedCheckedItemProvider); + } catch (e, st) { + LogMessage.log.e(e.toString(), error: e, stackTrace: st); + CustomSnackbar.showError(context, "Ups, terjadi kesalahan"); + } finally { + if (mounted) { + context.pop(); + } + } + }, + ), + ); + } + + @override + Widget build(BuildContext context) { + return LayoutBuilder( + builder: (context, constraints) { + final selectedItemState = ref.watch(selectedCheckedItemProvider).values; + + final selectedProducts = selectedItemState.where((item) { + return item.itemType == ItemType.product_variant; + }).toList(); + + final int selectedProductCount = selectedProducts.length; + + final selectedRawMaterials = selectedItemState.where((item) { + return item.itemType == ItemType.raw_material; + }).toList(); + + final int selectedRawMaterialCount = selectedRawMaterials.length; + + final stockCheckedControllerState = ref.watch( + stockCheckedControllerProvider, + ); + + return PopScope( + canPop: false, + onPopInvokedWithResult: (didPop, result) { + if (didPop) return; + + context.pop(); + }, + child: SafeArea( + top: false, + bottom: true, + right: false, + left: false, + child: Scaffold( + backgroundColor: const Color(0xFFF9FAFB), + appBar: AppBarWithTab( + hintText: + "Cari nama ${_tabIndex == 0 ? 'produk' : 'bahan baku'}...", + searchIconColor: _searchIconColor, + canPop: true, + onPop: () => context.pop(), + showNotifIcon: false, + searchFocusNode: _searchFocusNode, + tabController: _tabController, + onTap: (value) { + setState(() { + _tabIndex = value; + }); + }, + onSearchChanged: (value) => _onSearchChanged(value), + tabs: [ + Tab(text: "Produk", height: 10.h), + Tab(text: "Bahan Baku", height: 10.h), + ], + ), + body: TabBarView( + controller: _tabController, + children: [ + ProductStockCheckScreen(key: _productKey), + RawMaterialStockCheckScreen(key: _rawMaterialKey), + ], + ), + bottomNavigationBar: Container( + padding: EdgeInsets.all(5.w), + decoration: BoxDecoration( + color: Colors.white, + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.05), + blurRadius: 10, + offset: const Offset(0, -5), + ), + ], + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Barang Dicek:", + style: TextStyle( + color: Colors.grey[600], + fontSize: AppFontSize.medium.sp, + ), + ), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SummaryTag( + text: "$selectedProductCount Produk", + color: Colors.blue, + ), + + SizedBox(width: 2.w), + SummaryTag( + text: "$selectedRawMaterialCount Bahan Baku", + color: Colors.orange, + ), + ], + ), + ], + ), + SizedBox(height: 1.5.h), + + SizedBox( + width: double.infinity, + height: 6.5.h, + child: ElevatedButton( + onPressed: stockCheckedControllerState.isLoading + ? null + : _tabIndex == 0 && selectedProducts.isEmpty + ? null + : _tabIndex == 1 && selectedRawMaterials.isEmpty + ? null + : _showConfirmDialog, + style: ElevatedButton.styleFrom( + backgroundColor: AppColor.primaryColor, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(2.5.w), + ), + disabledBackgroundColor: Colors.grey[300], + ), + child: Text( + "Simpan Penyesuaian Stok", + style: TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + fontSize: AppFontSize.medium.sp, + ), + ), + ), + ), + ], + ), + ), + ), + ), + ); + }, + ); + } +} diff --git a/Mobile Operasional/lib/features/stock_check/presentation/widgets/stock_adjustmen_confirmation.dart b/Mobile Operasional/lib/features/stock_check/presentation/widgets/stock_adjustmen_confirmation.dart new file mode 100644 index 0000000..315c91c --- /dev/null +++ b/Mobile Operasional/lib/features/stock_check/presentation/widgets/stock_adjustmen_confirmation.dart @@ -0,0 +1,155 @@ +import 'package:flutter/material.dart'; +import 'package:niogu_app/core/constants/app_color.dart'; +import 'package:niogu_app/core/constants/app_font_size.dart'; +import 'package:sizer/sizer.dart'; + +class StockAdjustmenConfirmation extends StatelessWidget { + final int totalItems; + final VoidCallback onCancel; + final VoidCallback onConfirm; + + const StockAdjustmenConfirmation({ + super.key, + required this.totalItems, + required this.onCancel, + required this.onConfirm, + }); + + @override + Widget build(BuildContext context) { + final bool isTablet = 100.w >= 600; + + final double maxWidth = 100.w >= 1280 + ? 1200 + : isTablet + ? 800 + : 400; + return Dialog( + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(4.w)), + backgroundColor: Colors.white, + elevation: 5, + child: Container( + padding: EdgeInsets.symmetric(horizontal: 5.w, vertical: 3.h), + width: double.infinity, + constraints: BoxConstraints(maxWidth: maxWidth), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(4.w), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + padding: EdgeInsets.all(4.w), + decoration: BoxDecoration( + color: AppColor.primaryColor.withOpacity(0.1), + shape: BoxShape.circle, + ), + child: Icon( + Icons.fact_check_rounded, + color: AppColor.primaryColor, + size: 10.w, + ), + ), + + SizedBox(height: 2.5.h), + + Text( + "Konfirmasi Penyesuaian", + textAlign: TextAlign.center, + style: TextStyle( + fontSize: AppFontSize.medium.sp, + fontWeight: FontWeight.bold, + color: Colors.black87, + ), + ), + + SizedBox(height: 1.5.h), + + RichText( + textAlign: TextAlign.center, + text: TextSpan( + style: TextStyle( + fontSize: 10.sp, + color: Colors.grey[600], + height: 1.5, + ), + children: [ + TextSpan( + text: "Sistem akan menyesuaikan stok sistem pada ", + style: TextStyle(fontSize: AppFontSize.medium.sp), + ), + TextSpan( + text: "$totalItems barang", + style: TextStyle( + color: Colors.black87, + fontSize: AppFontSize.medium.sp, + fontWeight: FontWeight.bold, + ), + ), + TextSpan( + text: + " sesuai dengan input fisik yang anda masukkan.\nLanjutkan?", + style: TextStyle(fontSize: AppFontSize.medium.sp), + ), + ], + ), + ), + + SizedBox(height: 3.5.h), + + Row( + children: [ + Expanded( + child: ElevatedButton( + onPressed: onCancel, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.grey.shade300, + padding: EdgeInsets.symmetric(vertical: 1.5.h), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(2.5.w), + ), + elevation: 0, + ), + child: Text( + "Batal", + style: TextStyle( + color: Colors.grey[800], + fontSize: AppFontSize.medium.sp, + fontWeight: FontWeight.w600, + ), + ), + ), + ), + + SizedBox(width: 3.w), + + Expanded( + child: ElevatedButton( + onPressed: onConfirm, + style: ElevatedButton.styleFrom( + padding: EdgeInsets.symmetric(vertical: 1.5.h), + backgroundColor: AppColor.primaryColor, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(2.5.w), + ), + elevation: 0, + ), + child: Text( + "Ya, Simpan", + style: TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + fontSize: AppFontSize.medium.sp, + ), + ), + ), + ), + ], + ), + ], + ), + ), + ); + } +} diff --git a/Mobile Operasional/lib/features/stock_check/presentation/widgets/stock_check_shimmer.dart b/Mobile Operasional/lib/features/stock_check/presentation/widgets/stock_check_shimmer.dart new file mode 100644 index 0000000..5e26499 --- /dev/null +++ b/Mobile Operasional/lib/features/stock_check/presentation/widgets/stock_check_shimmer.dart @@ -0,0 +1,133 @@ +import 'package:flutter/material.dart'; +import 'package:shimmer/shimmer.dart'; +import 'package:sizer/sizer.dart'; + +class StockCheckShimmer extends StatelessWidget { + const StockCheckShimmer({super.key}); + + @override + Widget build(BuildContext context) { + return Shimmer.fromColors( + baseColor: Colors.grey.shade300, + highlightColor: Colors.grey.shade100, + enabled: true, + child: ListView.builder( + padding: EdgeInsets.all(4.w), + itemCount: 6, + itemBuilder: (_, __) => Container( + margin: EdgeInsets.only(bottom: 1.5.h), + padding: EdgeInsets.all(4.w), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(2.5.w), + border: Border.all(color: Colors.grey.shade200), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Icon Placeholder + Container( + width: 12.w, + height: 12.w, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(2.w), + ), + ), + SizedBox(width: 3.w), + // Text Lines + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + width: 50.w, + height: 1.5.h, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(1.w), + ), + ), + SizedBox(height: 0.8.h), + // Varian + Container( + width: 20.w, + height: 1.2.h, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(1.w), + ), + ), + SizedBox(height: 0.8.h), + // Badge Stok Sistem + Container( + width: 30.w, + height: 2.h, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(1.w), + ), + ), + ], + ), + ), + ], + ), + + Divider(height: 2.5.h, color: Colors.grey[200]), + + Row( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Expanded( + flex: 4, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container(width: 15.w, height: 1.h, color: Colors.white), + SizedBox(height: 0.8.h), + Container( + height: 5.5.h, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(2.w), + ), + ), + ], + ), + ), + + SizedBox(width: 4.w), + + Expanded( + flex: 5, + child: Container( + height: 5.5.h, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(2.w), + ), + ), + ), + ], + ), + + SizedBox(height: 1.5.h), + Container( + height: 5.h, + width: double.infinity, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(2.w), + ), + ), + ], + ), + ), + ), + ); + } +} \ No newline at end of file diff --git a/Mobile Operasional/lib/features/stock_in/data/repositories/stock_in_purchase_repository_impl.dart b/Mobile Operasional/lib/features/stock_in/data/repositories/stock_in_purchase_repository_impl.dart new file mode 100644 index 0000000..284a150 --- /dev/null +++ b/Mobile Operasional/lib/features/stock_in/data/repositories/stock_in_purchase_repository_impl.dart @@ -0,0 +1,14 @@ +import 'package:niogu_app/core/database/app_database.dart'; +import 'package:niogu_app/features/stock_in/domain/entities/stock_in_purchase.dart'; +import 'package:niogu_app/features/stock_in/domain/repositories/i_stock_in_purchase_repository.dart'; + +class StockInPurchaseRepositoryImpl implements IStockInPurchaseRepository { + final AppDatabase _appDatabase; + + StockInPurchaseRepositoryImpl(this._appDatabase); + + @override + Stream> watchAllReadySuppliers() { + return _appDatabase.supplierDao.watchAllAlreadySuppliers(); + } +} \ No newline at end of file diff --git a/Mobile Operasional/lib/features/stock_in/data/repositories/stock_in_repository_impl.dart b/Mobile Operasional/lib/features/stock_in/data/repositories/stock_in_repository_impl.dart new file mode 100644 index 0000000..505e583 --- /dev/null +++ b/Mobile Operasional/lib/features/stock_in/data/repositories/stock_in_repository_impl.dart @@ -0,0 +1,104 @@ +import 'package:drift/drift.dart'; +import 'package:niogu_app/core/database/app_database.dart'; +import 'package:niogu_app/core/enums/item_type.dart'; +import 'package:niogu_app/core/enums/purchase_status.dart'; +import 'package:niogu_app/core/enums/sync_status.dart'; +import 'package:niogu_app/core/system/system_setting.dart'; +import 'package:niogu_app/features/stock_in/domain/entities/stock_in.dart'; +import 'package:niogu_app/features/stock_in/domain/repositories/i_stock_in_repository.dart'; + +class StockInRepositoryImpl implements IStockInRepository { + final AppDatabase _appDatabase; + + StockInRepositoryImpl(this._appDatabase); + + @override + Stream> watchItems(ItemType itemType, String outletId) { + return _appDatabase.outletInventoryDao.watchAllItems(itemType); + } + + @override + Future updateStockItem( + SupplierInformation? supplierInfo, + NewPurchase? newPurchase, + List? itemPurchases, + List items, + ) async { + SuppliersCompanion? supplier; + + if (supplierInfo != null) { + supplier = SuppliersCompanion( + localId: Value(supplierInfo.localId), + name: Value(supplierInfo.name), + phoneNumber: Value(supplierInfo.phoneNumber), + // address: Value(supplierInfo.address), + syncStatus: Value(SyncStatus.dirty.status), + ); + } + + PurchasesCompanion? purchase; + + final List purchaseItems = []; + + if (newPurchase != null && itemPurchases != null) { + final outletId = await SystemSetting.getCurrentOutletId(); + + final outletName = await SystemSetting.getCurrentOutletName(); + + final userId = await SystemSetting.getCurrentUserId(); + + final userName = await SystemSetting.getCurrentUserName(); + + purchase = PurchasesCompanion( + localId: Value(newPurchase.localId), + outletId: Value(outletId!), + outletNameSnapshot: Value(outletName!), + supplierId: Value(newPurchase.supplierId), + supplierNameSnapshot: Value(newPurchase.supplierNameSnapshot), + supplierPhoneNumberSnapshot: Value( + newPurchase.supplierPhoneNumberSnapshot, + ), + userId: Value(userId!), + adminNameSnapshot: Value(userName!), + purchaseStatus: Value(PurchaseStatus.completed.status), + totalPurchase: Value(newPurchase.totalPurchase), + discount: Value(newPurchase.otherInformation.discount), + tax: Value(newPurchase.otherInformation.tax), + paymentStatus: Value(newPurchase.paymentStatus.status), + totalAmount: Value(newPurchase.totalAmount), + amountPaid: Value(newPurchase.amountPaid), + changeAmount: Value(newPurchase.changeAmount), + underPaymentAmount: Value(newPurchase.underPaymentAmount), + debtAmount: Value(newPurchase.debtAmount), + paymentProofPath: Value(newPurchase.paymentProofPath), + notes: Value(newPurchase.otherInformation.note), + syncStatus: Value(newPurchase.syncStatus.status), + ); + + for (final item in itemPurchases) { + purchaseItems.add( + PurchaseItemsCompanion( + localId: Value(item.localId), + purchaseId: Value(item.purchaseId), + itemType: Value(item.itemType.type), + itemId: Value(item.itemId), + quantity: Value(item.quantity), + itemImageSnapshot: Value(item.imagePath), + itemNameSnapshot: Value(item.itemNameSnapshot), + itemVariantNameSnapshot: Value(item.itemVariantNameSnapshot), + purchasePriceSnaphot: Value(item.purchasePriceSnaphot), + subtotal: Value(item.subtotal), + syncStatus: Value(item.syncStatus.status), + ), + ); + } + } + + return _appDatabase.stockCardDao.updateStockItem( + supplier: supplier, + purchase: purchase, + purchaseItem: purchaseItems, + items: items, + ); + } +} diff --git a/Mobile Operasional/lib/features/stock_in/domain/entities/stock_in.dart b/Mobile Operasional/lib/features/stock_in/domain/entities/stock_in.dart new file mode 100644 index 0000000..2fd983c --- /dev/null +++ b/Mobile Operasional/lib/features/stock_in/domain/entities/stock_in.dart @@ -0,0 +1,200 @@ +import 'package:niogu_app/core/enums/item_type.dart'; +import 'package:niogu_app/core/enums/payment_status.dart'; +import 'package:niogu_app/core/enums/stock_card_type.dart'; +import 'package:niogu_app/core/enums/sync_status.dart'; +import 'package:uuid/uuid.dart'; + +enum DisplayItemEmpty { loading, empty_database, empty_search_result, has_data } + +class DisplayItem { + final String id; + final String itemId; + final String? localImagePath; + final String name; + final String? variantName; + final double costPrice; + final double purchasePrice; + final double remainingStock; + final String unit; + final double sellingPrice; + + const DisplayItem({ + required this.id, + required this.itemId, + this.localImagePath, + required this.name, + this.variantName, + required this.costPrice, + required this.purchasePrice, + required this.remainingStock, + required this.unit, + required this.sellingPrice, + }); +} + +enum SelectedItemType { production, transfer_in, purchase } + +class SelectedItem { + final String id; + final ItemType itemType; + final String itemId; + final String? localImagePath; + final String name; + final String? variantName; + final double costPrice; + final double purchasePrice; + final double remainingStock; + final String unit; + final double sellingPrice; + final double quantity; + final SelectedItemType type; + + const SelectedItem({ + required this.id, + required this.itemType, + required this.itemId, + this.localImagePath, + required this.name, + this.variantName, + required this.costPrice, + required this.purchasePrice, + required this.remainingStock, + required this.unit, + required this.sellingPrice, + this.quantity = 1.0, + required this.type, + }); + + SelectedItem copyWith({double? quantity}) { + return SelectedItem( + id: id, + itemType: itemType, + itemId: itemId, + localImagePath: localImagePath, + name: name, + variantName: variantName, + costPrice: costPrice, + remainingStock: remainingStock, + unit: unit, + sellingPrice: sellingPrice, + quantity: quantity ?? this.quantity, + purchasePrice: purchasePrice, + type: type, + ); + } +} + +class UpdateStockItem { + final String id; + final String outletInventoryId; + final StockCardType stockCardType; + final String? receiveFromId; + final double remainingStock; + final double qtyChange; + final double updateStock; + final double currentStock; + final double purchasePrice; + final double costPrice; + final String? referenceId; + final String? documentNumber; + final String? notes; + final SyncStatus syncStatus; + + UpdateStockItem({ + required this.outletInventoryId, + required this.stockCardType, + this.receiveFromId, + required this.remainingStock, + required this.qtyChange, + required this.updateStock, + required this.currentStock, + required this.costPrice, + required this.purchasePrice, + this.referenceId, + this.documentNumber, + this.notes, + this.syncStatus = SyncStatus.dirty, + }) : id = const Uuid().v7().toString(); +} + +class SupplierInformation { + final String localId; + final String name; + final String phoneNumber; + final String address; + + SupplierInformation({ + String? localId, + required this.name, + required this.phoneNumber, + required this.address, + }) : this.localId = localId ?? const Uuid().v7().toString(); +} + +class OtherInformation { + final double discount; + final double tax; + final String? note; + + const OtherInformation({this.discount = 0.0, this.tax = 0.0, this.note}); +} + +class NewPurchase { + final String localId; + final String? supplierId; + final String? supplierNameSnapshot; + final String? supplierPhoneNumberSnapshot; + final double totalPurchase; + final OtherInformation otherInformation; + final PaymentStatus paymentStatus; + final double totalAmount; + final double amountPaid; + final double changeAmount; + final double underPaymentAmount; + final double debtAmount; + final String? paymentProofPath; + final SyncStatus syncStatus; + + NewPurchase({ + this.supplierId, + this.supplierNameSnapshot, + this.supplierPhoneNumberSnapshot, + required this.totalPurchase, + required this.paymentStatus, + required this.totalAmount, + required this.otherInformation, + required this.amountPaid, + required this.changeAmount, + required this.underPaymentAmount, + required this.debtAmount, + this.paymentProofPath, + this.syncStatus = SyncStatus.dirty, + }) : this.localId = const Uuid().v7().toString(); +} + +class ItemPurchase { + final String localId; + final String purchaseId; + final ItemType itemType; + final String itemId; + final double quantity; + final String? imagePath; + final String itemNameSnapshot; + final String? itemVariantNameSnapshot; + final double purchasePriceSnaphot; + final double subtotal; + final SyncStatus syncStatus; + + ItemPurchase({ + required this.purchaseId, + required this.itemType, + required this.itemId, + required this.quantity, + required this.imagePath, + required this.itemNameSnapshot, + required this.itemVariantNameSnapshot, + required this.purchasePriceSnaphot, + required this.subtotal, + this.syncStatus = SyncStatus.dirty, + }) : localId = const Uuid().v7().toString(); +} diff --git a/Mobile Operasional/lib/features/stock_in/domain/entities/stock_in_purchase.dart b/Mobile Operasional/lib/features/stock_in/domain/entities/stock_in_purchase.dart new file mode 100644 index 0000000..c800ed9 --- /dev/null +++ b/Mobile Operasional/lib/features/stock_in/domain/entities/stock_in_purchase.dart @@ -0,0 +1,34 @@ +enum AlreadySupplierEmpty { + loading, + empty_database, + empty_search_result, + has_data +} + +class AlreadySupplier { + final String id; + final String name; + final String phoneNumber; + final String address; + + const AlreadySupplier({ + required this.id, + required this.name, + required this.phoneNumber, + required this.address + }); +} + +class SelectedSupplier { + final String id; + final String name; + final String phoneNumber; + final String address; + + const SelectedSupplier({ + required this.id, + required this.name, + required this.phoneNumber, + required this.address, + }); +} \ No newline at end of file diff --git a/Mobile Operasional/lib/features/stock_in/domain/entities/stock_in_transfer.dart b/Mobile Operasional/lib/features/stock_in/domain/entities/stock_in_transfer.dart new file mode 100644 index 0000000..94f9261 --- /dev/null +++ b/Mobile Operasional/lib/features/stock_in/domain/entities/stock_in_transfer.dart @@ -0,0 +1,9 @@ +class SourceOutlet { + final String id; + final String name; + + const SourceOutlet({ + required this.id, + required this.name + }); +} \ No newline at end of file diff --git a/Mobile Operasional/lib/features/stock_in/domain/repositories/i_stock_in_purchase_repository.dart b/Mobile Operasional/lib/features/stock_in/domain/repositories/i_stock_in_purchase_repository.dart new file mode 100644 index 0000000..186b5f0 --- /dev/null +++ b/Mobile Operasional/lib/features/stock_in/domain/repositories/i_stock_in_purchase_repository.dart @@ -0,0 +1,5 @@ +import 'package:niogu_app/features/stock_in/domain/entities/stock_in_purchase.dart'; + +abstract class IStockInPurchaseRepository { + Stream> watchAllReadySuppliers(); +} \ No newline at end of file diff --git a/Mobile Operasional/lib/features/stock_in/domain/repositories/i_stock_in_repository.dart b/Mobile Operasional/lib/features/stock_in/domain/repositories/i_stock_in_repository.dart new file mode 100644 index 0000000..d230cef --- /dev/null +++ b/Mobile Operasional/lib/features/stock_in/domain/repositories/i_stock_in_repository.dart @@ -0,0 +1,13 @@ +import 'package:niogu_app/core/enums/item_type.dart'; +import 'package:niogu_app/features/stock_in/domain/entities/stock_in.dart'; + +abstract class IStockInRepository { + Stream> watchItems(ItemType itemType, String outletId); + + Future updateStockItem( + SupplierInformation? supplierInfo, + NewPurchase? newPurchase, + List? itemPurchases, + List items, + ); +} diff --git a/Mobile Operasional/lib/features/stock_in/presentation/controllers/stock_in_controller.dart b/Mobile Operasional/lib/features/stock_in/presentation/controllers/stock_in_controller.dart new file mode 100644 index 0000000..3955d96 --- /dev/null +++ b/Mobile Operasional/lib/features/stock_in/presentation/controllers/stock_in_controller.dart @@ -0,0 +1,105 @@ +import 'dart:async'; + +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:niogu_app/core/enums/item_type.dart'; +import 'package:niogu_app/features/stock_in/domain/entities/stock_in.dart'; +import 'package:niogu_app/features/stock_in/domain/repositories/i_stock_in_repository.dart'; +import 'package:niogu_app/features/stock_in/presentation/provider/stock_in_provider.dart'; + +class StockInController extends AutoDisposeAsyncNotifier { + late final IStockInRepository _stockInRepository; + + @override + FutureOr build() { + _stockInRepository = ref.read(stockInRepositoryProvider); + } + + Future updateStockItem({ + SupplierInformation? supplierInfo, + NewPurchase? newPurchase, + List? itemPurchases, + required List items, + }) async { + state = const AsyncValue.loading(); + + final result = await AsyncValue.guard(() async { + await _stockInRepository.updateStockItem( + supplierInfo, + newPurchase, + itemPurchases, + items, + ); + }); + + state = result; + + if (result is AsyncError) throw result.error!; + } +} + +class SelectedItemController extends StateNotifier> { + final SelectedItemType _type; + + SelectedItemController(this._type) : super({}); + + void increment(String id) { + final item = state[id]; + + if (item == null) return; + + state = {...state, item.id: item.copyWith(quantity: item.quantity + 1)}; + } + + void decrement(String id) { + final item = state[id]; + + if (item == null) return; + + state = {...state, item.id: item.copyWith(quantity: item.quantity - 1)}; + } + + void toggleSelectAll(SelectedItem item) { + if (state.containsKey(item.id)) { + state = {...state} + ..remove(item.id) + ..[item.id] = item; + } else { + state = {...state, item.id: item}; + } + } + + void toggle(SelectedItem item) { + if (state.containsKey(item.id)) { + state = {...state}..remove(item.id); + } else { + state = {...state, item.id: item}; + } + } + + void customQuantity(String id, double quantity) { + final item = state[id]; + + if (item == null) return; + + state = {...state, id: item.copyWith(quantity: quantity)}; + } + + void delete(String id) { + final item = state[id]; + + if (item == null) return; + + state = {...state}..remove(id); + } + + void clear(ItemType itemType) { + state = {...state} + ..removeWhere((_, value) { + return value.itemType == itemType && value.type == _type; + }); + } + + void restore(Map initialItems) { + state = Map.from(initialItems); + } +} diff --git a/Mobile Operasional/lib/features/stock_in/presentation/provider/stock_in_provider.dart b/Mobile Operasional/lib/features/stock_in/presentation/provider/stock_in_provider.dart new file mode 100644 index 0000000..ec76394 --- /dev/null +++ b/Mobile Operasional/lib/features/stock_in/presentation/provider/stock_in_provider.dart @@ -0,0 +1,89 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:niogu_app/core/providers/app_provider.dart'; +import 'package:niogu_app/core/enums/item_type.dart'; +import 'package:niogu_app/features/stock_in/data/repositories/stock_in_repository_impl.dart'; +import 'package:niogu_app/features/stock_in/domain/entities/stock_in.dart'; +import 'package:niogu_app/features/stock_in/domain/repositories/i_stock_in_repository.dart'; +import 'package:niogu_app/features/stock_in/presentation/controllers/stock_in_controller.dart'; + +final stockInRepositoryProvider = Provider((ref) { + final appDatabase = ref.watch(appDatabaseProvider); + return StockInRepositoryImpl(appDatabase); +}); + +final displayItemStreamProvider = StreamProvider.family + .autoDispose, ItemType>((ref, itemType) { + final stockInRepository = ref.watch(stockInRepositoryProvider); + + final currentOutletId = ref.watch(currentOutletIdProvider); + + if(currentOutletId == null) { + return Stream>.value([]); + } + + return stockInRepository.watchItems(itemType, currentOutletId); + }); + +final itemSearchProvider = StateProvider.autoDispose((ref) => ''); + +final filteredItemProvider = Provider.family + .autoDispose>, ItemType>((ref, itemType) { + final itemStreamAsync = ref.watch(displayItemStreamProvider(itemType)); + final itemSearchAsync = ref.watch(itemSearchProvider); + return itemStreamAsync.when( + data: (items) { + if (itemSearchAsync.trim().isEmpty) { + return AsyncValue.data(items); + } + + final filteredItems = items.where((item) { + return item.name.toLowerCase().contains( + itemSearchAsync.toLowerCase(), + ); + }).toList(); + + return AsyncValue.data(filteredItems); + }, + error: (error, stackTrace) { + return AsyncValue.error(error, stackTrace); + }, + loading: () => const AsyncValue.loading(), + ); + }); + +final displayItemEmptyProvider = Provider.family + .autoDispose((ref, itemType) { + final itemStreamAsync = ref.watch(displayItemStreamProvider(itemType)); + final filteredItemAsync = ref.watch(filteredItemProvider(itemType)); + final itemSearchAsync = ref.watch(itemSearchProvider); + + if (itemStreamAsync.isLoading) { + return DisplayItemEmpty.loading; + } + + final allItems = itemStreamAsync.value ?? []; + + if (allItems.isEmpty) { + return DisplayItemEmpty.empty_database; + } + + final filteredItems = filteredItemAsync.value ?? []; + + if (itemSearchAsync.isNotEmpty && filteredItems.isEmpty) { + return DisplayItemEmpty.empty_search_result; + } + + return DisplayItemEmpty.has_data; + }); + +final selectedItemProvider = + StateNotifierProvider.family.autoDispose< + SelectedItemController, + Map, SelectedItemType + >((ref, type) { + return SelectedItemController(type); + }); + +final stockInControllerProvider = AsyncNotifierProvider.autoDispose( + StockInController.new, +); \ No newline at end of file diff --git a/Mobile Operasional/lib/features/stock_in/presentation/provider/stock_in_purchase_provider.dart b/Mobile Operasional/lib/features/stock_in/presentation/provider/stock_in_purchase_provider.dart new file mode 100644 index 0000000..78d7fbc --- /dev/null +++ b/Mobile Operasional/lib/features/stock_in/presentation/provider/stock_in_purchase_provider.dart @@ -0,0 +1,84 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:niogu_app/core/providers/app_provider.dart'; +import 'package:niogu_app/features/stock_in/data/repositories/stock_in_purchase_repository_impl.dart'; +import 'package:niogu_app/features/stock_in/domain/entities/stock_in_purchase.dart'; +import 'package:niogu_app/features/stock_in/domain/repositories/i_stock_in_purchase_repository.dart'; + +final proofPaymentPurchaseTempProvider = StateProvider>((ref) => []); + +final stockInPurchaseRepositoryProvider = Provider((ref) { + final appDatabase = ref.watch(appDatabaseProvider); + return StockInPurchaseRepositoryImpl(appDatabase); +}); + +final alreadySupplierStreamProvider = + StreamProvider.autoDispose>((ref) { + final stockInPurchaseRepostiory = ref.watch(stockInPurchaseRepositoryProvider); + return stockInPurchaseRepostiory.watchAllReadySuppliers(); + }); + +final alreadySupplierSearchProvider = StateProvider.autoDispose( + (ref) => '', +); + +final filteredAlreadySupplierProvider = + Provider.autoDispose>>((ref) { + final alreadySupplierStreamAsync = ref.watch( + alreadySupplierStreamProvider, + ); + final alreadySupplierSearchAsync = ref.watch( + alreadySupplierSearchProvider, + ); + + return alreadySupplierStreamAsync.when( + data: (suppliers) { + if (alreadySupplierSearchAsync.trim().isEmpty) { + return AsyncValue.data(suppliers); + } + + final filteredSuppliers = suppliers.where((supplier) { + return supplier.name.toLowerCase().contains( + alreadySupplierSearchAsync.toLowerCase(), + ); + }).toList(); + + return AsyncValue.data(filteredSuppliers); + }, + error: (error, stackTrace) => AsyncValue.error(error, stackTrace), + loading: () => const AsyncValue.loading(), + ); + }); + +final alreadySupplierEmptyProvider = Provider.autoDispose( + (ref) { + final alreadyCustomerStreamAsync = ref.watch(alreadySupplierStreamProvider); + final filteredAlreadyCustomerAsync = ref.watch( + filteredAlreadySupplierProvider, + ); + final alreadyCustomerSearchAsync = ref.watch(alreadySupplierSearchProvider); + + if (alreadyCustomerStreamAsync.isLoading) { + return AlreadySupplierEmpty.loading; + } + + final allCustomers = alreadyCustomerStreamAsync.value ?? []; + + if (allCustomers.isEmpty) { + return AlreadySupplierEmpty.empty_database; + } + + final filteredCustomer = filteredAlreadyCustomerAsync.value ?? []; + + if (alreadyCustomerSearchAsync.isNotEmpty && filteredCustomer.isEmpty) { + return AlreadySupplierEmpty.empty_search_result; + } + + return AlreadySupplierEmpty.has_data; + }, +); + +final selectedSupplierProvider = StateProvider.autoDispose( + (ref) => null, +); + +final totalPurchaseAmountProvider = StateProvider.autoDispose((ref) => 0.0); \ No newline at end of file diff --git a/Mobile Operasional/lib/features/stock_in/presentation/screens/already_supplier_screen.dart b/Mobile Operasional/lib/features/stock_in/presentation/screens/already_supplier_screen.dart new file mode 100644 index 0000000..c6afd8a --- /dev/null +++ b/Mobile Operasional/lib/features/stock_in/presentation/screens/already_supplier_screen.dart @@ -0,0 +1,626 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter_contacts/flutter_contacts.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; +import 'package:niogu_app/core/constants/app_color.dart'; +import 'package:niogu_app/core/constants/app_font_size.dart'; +import 'package:niogu_app/core/widgets/custom_error_screen.dart'; +import 'package:niogu_app/core/router/app_route.dart'; +import 'package:niogu_app/core/widgets/custom_snackbar.dart'; +import 'package:niogu_app/features/pos/presentation/widgets/checkbox_customer.dart'; +import 'package:niogu_app/features/stock_in/domain/entities/stock_in_purchase.dart'; +import 'package:niogu_app/features/stock_in/presentation/provider/stock_in_purchase_provider.dart'; +import 'package:niogu_app/features/stock_in/presentation/widgets/already_supplier_shimmer.dart'; +import 'package:sizer/sizer.dart'; + +class AlreadySupplierScreen extends ConsumerStatefulWidget { + const AlreadySupplierScreen({super.key}); + + @override + ConsumerState createState() => + _AlreadySupplierScreenState(); +} + +class _AlreadySupplierScreenState extends ConsumerState { + final FocusNode _searchFocusNode = FocusNode(); + + Color _searchIconColor = Colors.grey; + + Timer? _debounce; + + String? _selectedSupplierId; + + SelectedSupplier? _selectedSupplier; + + @override + void initState() { + // TODO: implement initState + super.initState(); + + _searchFocusNode.addListener(() { + setState(() { + _searchIconColor = _searchFocusNode.hasFocus + ? Colors.black + : Colors.grey; + }); + }); + } + + @override + void dispose() { + // TODO: implement dispose + _searchFocusNode.dispose(); + _debounce?.cancel(); + super.dispose(); + } + + void _onSearchChanged(String value) { + if (_debounce?.isActive ?? false) _debounce?.cancel(); + _debounce = Timer(const Duration(milliseconds: 800), () { + ref.read(alreadySupplierSearchProvider.notifier).state = value; + }); + } + + Future _handleImportContact() async { + final permission = await FlutterContacts.requestPermission(); + if (permission) { + final contact = await FlutterContacts.openExternalPick(); + + if (contact != null) { + String name = contact.displayName; + String phone = ""; + + if (contact.phones.isNotEmpty) { + phone = contact.phones.first.number; + } + + if (!mounted) return; + + context.pushNamed( + AppRoute.addSupplierScreen, + extra: {'name': name, 'phone': phone}, + ); + } + } else { + CustomSnackbar.showWarning(context, "Akses Ditolak"); + } + } + + void _showAddOptions(BuildContext context) { + final bool isTablet = 100.w >= 600; + showModalBottomSheet( + context: context, + backgroundColor: Colors.white, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.vertical(top: Radius.circular(2.5.w)), + ), + constraints: const BoxConstraints(maxWidth: double.infinity), + builder: (context) { + return SafeArea( + child: Container( + width: 100.w, + padding: EdgeInsets.symmetric(vertical: 2.h), + clipBehavior: Clip.hardEdge, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.vertical(top: Radius.circular(6.w)), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: EdgeInsets.fromLTRB(5.w, 2.h, 5.w, 1.h), + child: Text( + "Tambah Pemasok", + style: TextStyle( + fontSize: AppFontSize.medium.sp, + fontWeight: FontWeight.bold, + ), + ), + ), + SizedBox(height: 2.h), + ListTile( + leading: Padding( + padding: EdgeInsets.only(left: 5.w), + child: Icon( + Icons.edit_note_rounded, + color: Colors.blue, + size: 6.w, + ), + ), + title: Text( + "Input Manual", + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + ), + ), + subtitle: Text( + "Isi informasi pemasok secara manual", + style: TextStyle( + fontSize: isTablet + ? (AppFontSize.medium - 1.25).sp + : (AppFontSize.small - 1.25).sp, + color: Colors.grey, + ), + ), + onTap: () { + context.pop(); + context.pushNamed(AppRoute.addSupplierScreen); + }, + ), + SizedBox(height: 2.h), + ListTile( + leading: Padding( + padding: EdgeInsets.only(left: 5.w), + child: Icon( + Icons.contacts_rounded, + color: Colors.green, + size: 6.w, + ), + ), + title: Text( + "Ambil dari Kontak HP", + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + ), + ), + subtitle: Text( + "Pilih langsung dari buku telepon", + style: TextStyle( + fontSize: isTablet + ? (AppFontSize.medium - 1.25).sp + : (AppFontSize.small - 1.25).sp, + color: Colors.grey, + ), + ), + onTap: () async { + context.pop(); + await _handleImportContact(); + }, + ), + SizedBox(height: 2.h), + ], + ), + ), + ); + }, + ); + } + + void _selectSupplier(SelectedSupplier selectedSupplier, String supplierId) { + setState(() { + if (_selectedSupplierId == supplierId) { + _selectedSupplierId = null; + _selectedSupplier = null; + } else { + _selectedSupplierId = supplierId; + _selectedSupplier = selectedSupplier; + } + }); + } + + void _submitSelection() { + ref.read(selectedSupplierProvider.notifier).state = _selectedSupplier; + context.pop(); + } + + @override + Widget build(BuildContext context) { + return LayoutBuilder( + builder: (context, constraints) { + final bool isTablet = 100.w >= 600; + + const greyColor = Color(0xFFF5F5F5); + + final filteredSupplierState = ref.watch( + filteredAlreadySupplierProvider, + ); + + final supplierEmptyState = ref.watch(alreadySupplierEmptyProvider); + + return SafeArea( + top: false, + bottom: true, + right: false, + left: false, + child: Scaffold( + backgroundColor: Colors.grey[50], + appBar: AppBar( + backgroundColor: Colors.white, + surfaceTintColor: Colors.white, + elevation: 0, + toolbarHeight: 10.h, + titleSpacing: 0, + automaticallyImplyLeading: false, + title: Padding( + padding: EdgeInsets.symmetric(horizontal: 4.w), + child: Row( + children: [ + Center( + child: Material( + color: Colors.transparent, + type: MaterialType.canvas, + child: InkWell( + onTap: () => context.pop(), + borderRadius: BorderRadius.circular(2.5.w), + child: Container( + width: 10.w, + height: 10.w, + decoration: BoxDecoration( + color: Colors.white, + border: Border.all(color: Colors.grey.shade200), + borderRadius: BorderRadius.circular(2.5.w), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.05), + blurRadius: 5, + offset: const Offset(0, 2), + ), + ], + ), + child: Icon( + Icons.arrow_back_ios_new_rounded, + color: Colors.black87, + size: 5.w, + ), + ), + ), + ), + ), + + SizedBox(width: 3.w), + + Expanded( + child: Container( + height: 6.h, + padding: isTablet + ? EdgeInsets.symmetric( + vertical: 1.h, + horizontal: 2.w, + ) + : EdgeInsets.zero, + decoration: BoxDecoration( + color: greyColor, + borderRadius: BorderRadius.circular(2.5.w), + ), + child: TextField( + focusNode: _searchFocusNode, + onChanged: (value) => _onSearchChanged(value), + textAlignVertical: TextAlignVertical.center, + decoration: InputDecoration( + hintText: "Cari nama pemasok...", + hintStyle: TextStyle( + color: _searchIconColor, + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + ), + prefixIcon: Icon( + Icons.search, + color: _searchIconColor, + size: 5.w, + ), + border: InputBorder.none, + contentPadding: EdgeInsets.zero, + isDense: true, + ), + ), + ), + ), + + SizedBox(width: 2.w), + + IconButton( + onPressed: () => _showAddOptions(context), + icon: Icon( + Icons.add_circle, + color: AppColor.primaryColor, + size: 8.w, + ), + ), + ], + ), + ), + + bottom: PreferredSize( + preferredSize: Size.fromHeight(2), + child: Container(color: Colors.grey.shade100, height: 1), + ), + ), + body: filteredSupplierState.when( + data: (suppliers) { + switch (supplierEmptyState) { + case AlreadySupplierEmpty.loading: + return SizedBox(); + case AlreadySupplierEmpty.empty_database: + return SingleChildScrollView( + physics: const AlwaysScrollableScrollPhysics(), + padding: EdgeInsets.symmetric(vertical: 3.h), + child: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + "Tidak Ada Pemasok", + style: TextStyle( + fontSize: AppFontSize.large.sp, + fontWeight: FontWeight.bold, + color: Colors.black, + ), + ), + SizedBox(height: 1.h), + Text( + "Kamu belum memiliki pemasok", + style: TextStyle( + fontSize: AppFontSize.small.sp, + color: Colors.grey, + ), + ), + SizedBox(height: 1.5.h), + + Icon( + Icons.search_off_rounded, + size: 15.h, + color: Colors.grey.shade300, + ), + ], + ), + ), + ); + + case AlreadySupplierEmpty.empty_search_result: + return SingleChildScrollView( + physics: const AlwaysScrollableScrollPhysics(), + padding: EdgeInsets.symmetric(vertical: 3.h), + child: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + "Pemasok Tidak Ditemukan", + style: TextStyle( + fontSize: AppFontSize.large.sp, + fontWeight: FontWeight.bold, + color: Colors.black, + ), + ), + + SizedBox(height: 1.5.h), + + Icon( + Icons.search_off_rounded, + size: 15.h, + color: Colors.grey.shade300, + ), + ], + ), + ), + ); + case AlreadySupplierEmpty.has_data: + return ListView.builder( + padding: EdgeInsets.symmetric( + vertical: 2.h, + horizontal: 5.w, + ), + itemCount: suppliers.length, + itemBuilder: (context, index) { + final supplier = suppliers[index]; + + final bool isParentSelected = + _selectedSupplierId == supplier.id; + + return AnimatedContainer( + duration: const Duration(milliseconds: 200), + margin: EdgeInsets.only(bottom: 2.h), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(4.w), + border: Border.all( + color: Colors.grey.shade200, + width: 1, + ), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.03), + blurRadius: 8, + offset: const Offset(0, 4), + ), + ], + ), + child: Column( + children: [ + InkWell( + onTap: () { + _selectSupplier( + SelectedSupplier( + id: supplier.id, + name: supplier.name, + phoneNumber: supplier.phoneNumber, + address: supplier.address, + ), + supplier.id, + ); + }, + borderRadius: BorderRadius.vertical( + top: Radius.circular(4.w), + bottom: Radius.circular(4.w), + ), + child: Padding( + padding: EdgeInsets.all(3.w), + child: Row( + children: [ + Container( + width: 12.w, + height: 12.w, + decoration: BoxDecoration( + color: Colors.grey[100], + shape: BoxShape.circle, + ), + alignment: Alignment.center, + child: Text( + supplier.name[0].toUpperCase(), + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + color: Colors.grey, + ), + ), + ), + SizedBox(width: 4.w), + + Expanded( + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text( + supplier.name, + style: TextStyle( + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + fontWeight: FontWeight.bold, + color: Colors.black87, + ), + ), + if (supplier + .phoneNumber + .isNotEmpty) ...[ + SizedBox(height: 0.75.h), + Row( + children: [ + Icon( + Icons.phone_android, + size: 5.w, + color: Colors.grey, + ), + SizedBox(width: 1.w), + Text( + supplier.phoneNumber, + style: TextStyle( + fontSize: isTablet + ? AppFontSize + .medium + .sp + : AppFontSize + .small + .sp, + color: Colors.grey[600], + ), + ), + ], + ), + ], + + if (supplier + .address + .isNotEmpty) ...[ + SizedBox(height: 0.75.h), + Row( + children: [ + Icon( + Icons.location_on_outlined, + color: Colors.grey, + size: 5.w, + ), + + SizedBox(width: 1.w), + + Text( + supplier.address, + maxLines: 1, + overflow: + TextOverflow.ellipsis, + style: TextStyle( + fontSize: isTablet + ? AppFontSize + .medium + .sp + : AppFontSize + .small + .sp, + color: Colors.grey[500], + ), + ), + ], + ), + ], + ], + ), + ), + + CheckboxCustomer( + isSelected: isParentSelected, + ), + ], + ), + ), + ), + ], + ), + ); + }, + ); + } + }, + error: (error, stackTrace) { + return CustomErrorScreen( + message: "Ups, terjadi kesalahan", + onRefresh: () {}, + ); + }, + loading: () => const AlreadySupplierShimmer(), + ), + bottomNavigationBar: Container( + padding: EdgeInsets.all(5.w), + decoration: BoxDecoration( + color: Colors.white, + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.05), + blurRadius: 10, + offset: const Offset(0, -5), + ), + ], + ), + child: SizedBox( + height: 6.5.h, + child: ElevatedButton( + onPressed: (_selectedSupplierId != null) + ? _submitSelection + : null, + style: ElevatedButton.styleFrom( + backgroundColor: AppColor.primaryColor, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(2.5.w), + ), + elevation: 0, + disabledBackgroundColor: Colors.grey.shade300, + ), + child: Text( + "Pilih Pemasok", + style: TextStyle( + color: Colors.white, + fontSize: AppFontSize.medium.sp, + fontWeight: FontWeight.bold, + ), + ), + ), + ), + ), + ), + ); + }, + ); + } +} diff --git a/Mobile Operasional/lib/features/stock_in/presentation/screens/display_item_screen.dart b/Mobile Operasional/lib/features/stock_in/presentation/screens/display_item_screen.dart new file mode 100644 index 0000000..7405cc8 --- /dev/null +++ b/Mobile Operasional/lib/features/stock_in/presentation/screens/display_item_screen.dart @@ -0,0 +1,237 @@ +import 'dart:async'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; +import 'package:niogu_app/core/components/app_bar_with_tab.dart'; +import 'package:niogu_app/core/enums/item_type.dart'; +import 'package:niogu_app/features/stock_in/domain/entities/stock_in.dart'; +import 'package:niogu_app/features/stock_in/presentation/provider/stock_in_provider.dart'; +import 'package:niogu_app/features/stock_in/presentation/screens/display_product_screen.dart'; +import 'package:niogu_app/features/stock_in/presentation/screens/display_raw_material_screen.dart'; +import 'package:niogu_app/features/stock_in/presentation/widgets/summary_tag.dart'; +import 'package:sizer/sizer.dart'; +import 'package:niogu_app/core/constants/app_color.dart'; +import 'package:niogu_app/core/constants/app_font_size.dart'; + +class DisplayItemScreen extends ConsumerStatefulWidget { + final SelectedItemType type; + final VoidCallback? onPressed; + + const DisplayItemScreen({ + super.key, + required this.type, + required this.onPressed, + }); + + @override + ConsumerState createState() => _DisplayItemScreenState(); +} + +class _DisplayItemScreenState extends ConsumerState + with SingleTickerProviderStateMixin { + late TabController _tabController; + + final FocusNode _searchFocusNode = FocusNode(); + + Color _searchIconColor = Colors.grey; + + Timer? _debounce; + + int _tabIndex = 0; + + Map _initialItems = {}; + + @override + void initState() { + super.initState(); + + _tabController = TabController(length: 2, vsync: this); + + _tabController.addListener(_handleTabSelection); + + _searchFocusNode.addListener(() { + setState(() { + _searchIconColor = _searchFocusNode.hasFocus + ? Colors.black + : Colors.grey; + }); + }); + + WidgetsBinding.instance.addPostFrameCallback((_) { + _initialItems = ref.read(selectedItemProvider(widget.type)); + }); + } + + @override + void dispose() { + _searchFocusNode.dispose(); + _tabController.removeListener(_handleTabSelection); + _tabController.dispose(); + _debounce?.cancel(); + super.dispose(); + } + + void _handleTabSelection() { + if (_tabController.index != _tabIndex) { + setState(() { + _tabIndex = _tabController.index; + }); + } + } + + void _onSearchChanged(String value) { + if (_debounce?.isActive ?? false) _debounce?.cancel(); + _debounce = Timer(const Duration(milliseconds: 800), () { + ref.read(itemSearchProvider.notifier).state = value; + }); + } + + @override + Widget build(BuildContext context) { + return LayoutBuilder( + builder: (context, constraints) { + final selectedItemState = ref + .watch(selectedItemProvider(widget.type)) + .values + .where((item) { + return item.type == widget.type; + }) + .toList(); + + final selectedProducts = selectedItemState.where((item) { + return item.itemType == ItemType.product_variant; + }).toList(); + + final int selectedProductCount = selectedProducts.length; + + final selectedRawMaterials = selectedItemState.where((item) { + return item.itemType == ItemType.raw_material; + }).toList(); + + final int selectedRawMaterialCount = selectedRawMaterials.length; + + return PopScope( + canPop: false, + onPopInvokedWithResult: (didPop, result) { + if (didPop) return; + + context.pop(); + + WidgetsBinding.instance.addPostFrameCallback((_) { + ref + .read(selectedItemProvider(widget.type).notifier) + .restore(_initialItems); + }); + }, + child: SafeArea( + top: false, + bottom: true, + right: false, + left: false, + child: Scaffold( + backgroundColor: const Color(0xFFF9FAFB), + appBar: AppBarWithTab( + hintText: + "Cari nama ${_tabIndex == 0 ? 'produk' : 'bahan baku'}...", + searchIconColor: _searchIconColor, + canPop: true, + onPop: () => context.pop(), + showNotifIcon: false, + searchFocusNode: _searchFocusNode, + tabController: _tabController, + onTap: (value) { + setState(() { + _tabIndex = value; + }); + }, + onSearchChanged: (value) => _onSearchChanged(value), + tabs: [ + Tab(text: "Produk", height: 10.h), + Tab(text: "Bahan Baku", height: 10.h), + ], + ), + body: TabBarView( + controller: _tabController, + children: [ + DisplayProductScreen(type: widget.type), + DisplayRawMaterialScreen(type: widget.type), + ], + ), + + bottomNavigationBar: Container( + padding: EdgeInsets.all(5.w), + decoration: BoxDecoration( + color: Colors.white, + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.05), + blurRadius: 10, + offset: const Offset(0, -5), + ), + ], + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + if (selectedProductCount > 0 || + selectedRawMaterialCount > 0) + Padding( + padding: EdgeInsets.only(bottom: 1.5.h), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + if (selectedProductCount > 0) + SummaryTag( + text: "$selectedProductCount Produk", + color: Colors.blue, + ), + if (selectedProductCount > 0 && + selectedRawMaterialCount > 0) + SizedBox(width: 2.w), + if (selectedRawMaterialCount > 0) + SummaryTag( + text: "$selectedRawMaterialCount Bahan Baku", + color: Colors.orange, + ), + ], + ), + ), + + SizedBox( + width: double.infinity, + height: 6.h, + child: ElevatedButton( + onPressed: selectedItemState.isEmpty + ? null + : () { + context.pop(); + widget.onPressed?.call(); + }, + style: ElevatedButton.styleFrom( + backgroundColor: AppColor.primaryColor, + disabledBackgroundColor: Colors.grey[300], + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(2.5.w), + ), + elevation: 0, + ), + child: Text( + "Pilih Barang (${selectedProductCount + selectedRawMaterialCount})", + style: TextStyle( + color: Colors.white, + fontSize: AppFontSize.medium.sp, + fontWeight: FontWeight.bold, + ), + ), + ), + ), + ], + ), + ), + ), + ), + ); + }, + ); + } +} diff --git a/Mobile Operasional/lib/features/stock_in/presentation/screens/display_product_screen.dart b/Mobile Operasional/lib/features/stock_in/presentation/screens/display_product_screen.dart new file mode 100644 index 0000000..67c31ff --- /dev/null +++ b/Mobile Operasional/lib/features/stock_in/presentation/screens/display_product_screen.dart @@ -0,0 +1,351 @@ +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; +import 'package:niogu_app/core/constants/app_font_size.dart'; +import 'package:niogu_app/core/widgets/custom_error_screen.dart'; +import 'package:niogu_app/core/router/app_route.dart'; +import 'package:niogu_app/core/utils/extension_format.dart'; +import 'package:niogu_app/core/enums/item_type.dart'; +import 'package:niogu_app/core/widgets/custom_empty_screen.dart'; +import 'package:niogu_app/features/stock_in/presentation/widgets/select_item_shimmer.dart'; +import 'package:niogu_app/features/stock_in/domain/entities/stock_in.dart'; +import 'package:niogu_app/features/stock_in/presentation/provider/stock_in_provider.dart'; +import 'package:niogu_app/features/stock_in/presentation/widgets/checkbox_button.dart'; +import 'package:sizer/sizer.dart'; + +class DisplayProductScreen extends ConsumerStatefulWidget { + final SelectedItemType type; + + DisplayProductScreen({super.key, required this.type}); + + @override + ConsumerState createState() => + DisplayProductScreenState(); +} + +class DisplayProductScreenState extends ConsumerState + with AutomaticKeepAliveClientMixin { + @override + // TODO: implement wantKeepAlive + bool get wantKeepAlive => true; + + @override + void initState() { + // TODO: implement initState + super.initState(); + } + + @override + void dispose() { + // TODO: implement dispose + super.dispose(); + } + + void _toggleSelectAll(List products) { + final selectedItemState = ref.read(selectedItemProvider(widget.type)); + + final selectedItems = selectedItemState.values.where((item) { + return item.itemType == ItemType.product_variant && + item.type == widget.type; + }).toList(); + + if (selectedItems.isNotEmpty && (products.length == selectedItems.length)) { + ref + .read(selectedItemProvider(widget.type).notifier) + .clear(ItemType.product_variant); + return; + } + + for (final product in products) { + final selectedItem = SelectedItem( + id: product.id, + itemType: ItemType.product_variant, + itemId: product.itemId, + localImagePath: product.localImagePath, + name: product.name, + variantName: product.variantName, + costPrice: product.costPrice, + purchasePrice: 0.0, + remainingStock: product.remainingStock, + unit: product.unit, + sellingPrice: product.sellingPrice, + type: widget.type, + ); + + ref + .read(selectedItemProvider(widget.type).notifier) + .toggleSelectAll(selectedItem); + } + } + + @override + Widget build(BuildContext context) { + super.build(context); + + final bool isTablet = 100.w > 600; + + final productState = ref.watch( + filteredItemProvider(ItemType.product_variant), + ); + + final productEmptyState = ref.watch( + displayItemEmptyProvider(ItemType.product_variant), + ); + + final selectedItemState = ref.watch(selectedItemProvider(widget.type)); + + final selectedItems = selectedItemState.values.where((item) { + return item.itemType == ItemType.product_variant && + item.type == widget.type; + }).toList(); + + return productState.when( + data: (products) { + final bool isAllSelected = + selectedItems.isNotEmpty && + (products.length == selectedItems.length); + + switch (productEmptyState) { + case DisplayItemEmpty.loading: + return const SizedBox(); + + case DisplayItemEmpty.empty_database: + return CustomEmptyScreen( + title: "Tidak Ada Produk", + body: "Kamu belum memiliki produk", + textButton: "Buat Produk Pertamamu", + onPressed: () => context.pushNamed(AppRoute.addProductScreen), + ); + case DisplayItemEmpty.empty_search_result: + return SingleChildScrollView( + physics: const AlwaysScrollableScrollPhysics(), + padding: EdgeInsets.symmetric(vertical: 3.h), + child: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + "Produk Tidak Ditemukan", + style: TextStyle( + fontSize: AppFontSize.large.sp, + fontWeight: FontWeight.bold, + color: Colors.black, + ), + ), + + SizedBox(height: 1.5.h), + + Icon( + Icons.search_off_rounded, + size: 15.h, + color: Colors.grey.shade300, + ), + ], + ), + ), + ); + case DisplayItemEmpty.has_data: + return Column( + children: [ + InkWell( + onTap: () => _toggleSelectAll(products), + child: Container( + padding: EdgeInsets.symmetric( + horizontal: 5.w, + vertical: 1.5.h, + ), + color: Colors.blue[50]!.withOpacity(0.5), + child: Row( + children: [ + CheckboxButton(isSelected: isAllSelected), + SizedBox(width: 4.w), + Text( + "Pilih Semua Produk", + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + color: Colors.blue[800], + ), + ), + ], + ), + ), + ), + + Expanded( + child: ListView.builder( + padding: EdgeInsets.fromLTRB(4.w, 1.h, 4.w, 5.h), + itemCount: products.length, + itemBuilder: (context, index) { + final product = products[index]; + + final String path = + product.localImagePath != null && + product.localImagePath!.isNotEmpty + ? product.localImagePath! + : 'image not found'; + + final File imageFile = File(path); + + final bool imageFileExists = imageFile.existsSync(); + + return Container( + margin: EdgeInsets.only(bottom: 1.5.h), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(2.5.w), + border: Border.all( + color: Colors.grey.shade200, + width: 1, + ), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.02), + blurRadius: 4, + offset: const Offset(0, 2), + ), + ], + ), + child: InkWell( + onTap: () { + final selectedItem = SelectedItem( + id: product.id, + itemType: ItemType.product_variant, + itemId: product.itemId, + localImagePath: product.localImagePath, + name: product.name, + variantName: product.variantName, + costPrice: product.costPrice, + purchasePrice: 0.0, + remainingStock: product.remainingStock, + unit: product.unit, + sellingPrice: product.sellingPrice, + type: widget.type, + ); + + ref + .read( + selectedItemProvider(widget.type).notifier, + ) + .toggle(selectedItem); + }, + borderRadius: BorderRadius.circular(2.5.w), + child: Padding( + padding: EdgeInsets.all(3.w), + child: Row( + children: [ + CheckboxButton( + isSelected: + selectedItemState.containsKey( + product.id, + ) && + selectedItemState[product.id]?.type == + widget.type, + ), + + SizedBox(width: 4.w), + + Container( + height: 15.w, + width: 15.w, + decoration: BoxDecoration( + border: Border.all( + color: Colors.grey.shade200, + width: 1.5, + ), + borderRadius: BorderRadius.circular(2.5.w), + ), + child: imageFileExists + ? ClipRRect( + borderRadius: BorderRadius.circular( + 2.5.w, + ), + child: Image.file( + imageFile, + fit: BoxFit.cover, + width: double.infinity, + height: double.infinity, + errorBuilder: + (context, error, stackTrace) { + return const Icon( + Icons.error, + color: Colors.grey, + ); + }, + ), + ) + : Icon( + Icons.image_outlined, + size: 8.w, + color: Colors.grey, + ), + ), + + SizedBox(width: 3.w), + + Expanded( + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text( + product.name, + maxLines: 2, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + color: Colors.black87, + ), + ), + if (product.variantName != null && + product.variantName!.isNotEmpty) ...[ + SizedBox(height: 0.75.h), + Text( + product.variantName!, + style: TextStyle( + color: Colors.grey[600], + fontSize: isTablet + ? (AppFontSize.medium - 1.25).sp + : (AppFontSize.small - 1.25).sp, + ), + ), + ], + SizedBox(height: 0.75.h), + Text( + "Stok: ${product.remainingStock.toStringWithoutTrailingZero()} ${product.unit}", + style: TextStyle( + color: Colors.grey[600], + fontSize: isTablet + ? (AppFontSize.medium - 1.25).sp + : (AppFontSize.small - 1.25).sp, + ), + ), + ], + ), + ), + ], + ), + ), + ), + ); + }, + ), + ), + ], + ); + } + }, + error: (error, stackTrace) { + return CustomErrorScreen(message: "Ups, terjadi kesalahan", onRefresh: () {}); + }, + loading: () => const SelectItemShimmer(), + ); + } +} diff --git a/Mobile Operasional/lib/features/stock_in/presentation/screens/display_raw_material_screen.dart b/Mobile Operasional/lib/features/stock_in/presentation/screens/display_raw_material_screen.dart new file mode 100644 index 0000000..c085ce1 --- /dev/null +++ b/Mobile Operasional/lib/features/stock_in/presentation/screens/display_raw_material_screen.dart @@ -0,0 +1,304 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; +import 'package:niogu_app/core/constants/app_font_size.dart'; +import 'package:niogu_app/core/widgets/custom_error_screen.dart'; +import 'package:niogu_app/core/router/app_route.dart'; +import 'package:niogu_app/core/utils/extension_format.dart'; +import 'package:niogu_app/core/enums/item_type.dart'; +import 'package:niogu_app/core/widgets/custom_empty_screen.dart'; +import 'package:niogu_app/features/stock_in/presentation/widgets/select_item_shimmer.dart'; +import 'package:niogu_app/features/stock_in/domain/entities/stock_in.dart'; +import 'package:niogu_app/features/stock_in/presentation/provider/stock_in_provider.dart'; +import 'package:niogu_app/features/stock_in/presentation/widgets/checkbox_button.dart'; +import 'package:sizer/sizer.dart'; + +class DisplayRawMaterialScreen extends ConsumerStatefulWidget { + final SelectedItemType type; + + DisplayRawMaterialScreen({super.key, required this.type}); + + @override + ConsumerState createState() => + DisplayRawMaterialScreenState(); +} + +class DisplayRawMaterialScreenState + extends ConsumerState + with AutomaticKeepAliveClientMixin { + @override + // TODO: implement wantKeepAlive + bool get wantKeepAlive => true; + + @override + void initState() { + // TODO: implement initState + super.initState(); + } + + @override + void dispose() { + // TODO: implement dispose + super.dispose(); + } + + void _toggleSelectAll(List rawMaterials) { + final selectedItemState = ref.read(selectedItemProvider(widget.type)); + + final selectedItems = selectedItemState.values.where((item) { + return item.itemType == ItemType.raw_material && item.type == widget.type; + }).toList(); + + if (selectedItems.isNotEmpty && + (rawMaterials.length == selectedItems.length)) { + ref + .read(selectedItemProvider(widget.type).notifier) + .clear(ItemType.raw_material); + return; + } + + for (final rawMaterial in rawMaterials) { + final selectedItem = SelectedItem( + id: rawMaterial.id, + itemType: ItemType.raw_material, + itemId: rawMaterial.itemId, + localImagePath: rawMaterial.localImagePath, + name: rawMaterial.name, + variantName: rawMaterial.variantName, + costPrice: 0.0, + purchasePrice: rawMaterial.purchasePrice, + remainingStock: rawMaterial.remainingStock, + unit: rawMaterial.unit, + sellingPrice: rawMaterial.sellingPrice, + type: widget.type, + ); + + ref + .read(selectedItemProvider(widget.type).notifier) + .toggleSelectAll(selectedItem); + } + } + + @override + Widget build(BuildContext context) { + super.build(context); + + final bool isTablet = 100.w > 600; + + final rawMaterialState = ref.watch( + filteredItemProvider(ItemType.raw_material), + ); + + final rawMaterialEmptyState = ref.watch( + displayItemEmptyProvider(ItemType.raw_material), + ); + + final selectedItemState = ref.watch(selectedItemProvider(widget.type)); + + final selectedItems = selectedItemState.values.where((item) { + return item.itemType == ItemType.raw_material && item.type == widget.type; + }).toList(); + + return rawMaterialState.when( + data: (rawMaterials) { + final bool isAllSelected = + selectedItems.isNotEmpty && + (rawMaterials.length == selectedItems.length); + + switch (rawMaterialEmptyState) { + case DisplayItemEmpty.loading: + return const SizedBox(); + + case DisplayItemEmpty.empty_database: + return CustomEmptyScreen( + title: "Tidak Ada Bahan Baku", + body: "Kamu belum memmiliki bahan baku", + textButton: "Tambah Bahan Baku", + onPressed: () => context.pushNamed(AppRoute.addRawMaterialScreen), + ); + case DisplayItemEmpty.empty_search_result: + return SingleChildScrollView( + physics: const AlwaysScrollableScrollPhysics(), + padding: EdgeInsets.symmetric(vertical: 3.h), + child: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + "Bahan Baku Tidak Ditemukan", + style: TextStyle( + fontSize: AppFontSize.large.sp, + fontWeight: FontWeight.bold, + color: Colors.black, + ), + ), + + SizedBox(height: 1.5.h), + + Icon( + Icons.search_off_rounded, + size: 15.h, + color: Colors.grey.shade300, + ), + ], + ), + ), + ); + case DisplayItemEmpty.has_data: + return Column( + children: [ + InkWell( + onTap: () => _toggleSelectAll(rawMaterials), + child: Container( + padding: EdgeInsets.symmetric( + horizontal: 5.w, + vertical: 1.5.h, + ), + color: Colors.blue[50]!.withOpacity(0.5), + child: Row( + children: [ + CheckboxButton(isSelected: isAllSelected), + SizedBox(width: 4.w), + Text( + "Pilih Semua Bahan Baku", + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + color: Colors.blue[800], + ), + ), + ], + ), + ), + ), + + Expanded( + child: ListView.builder( + padding: EdgeInsets.fromLTRB(4.w, 1.h, 4.w, 5.h), + itemCount: rawMaterials.length, + itemBuilder: (context, index) { + final rawMaterial = rawMaterials[index]; + + return Container( + margin: EdgeInsets.only(bottom: 1.5.h), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(2.5.w), + border: Border.all( + color: Colors.grey.shade200, + width: 1, + ), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.02), + blurRadius: 4, + offset: const Offset(0, 2), + ), + ], + ), + child: InkWell( + onTap: () { + final selectedItem = SelectedItem( + id: rawMaterial.id, + itemType: ItemType.raw_material, + itemId: rawMaterial.itemId, + localImagePath: rawMaterial.localImagePath, + name: rawMaterial.name, + variantName: rawMaterial.variantName, + costPrice: 0.0, + purchasePrice: rawMaterial.purchasePrice, + remainingStock: rawMaterial.remainingStock, + unit: rawMaterial.unit, + sellingPrice: rawMaterial.sellingPrice, + type: widget.type, + ); + + ref + .read( + selectedItemProvider(widget.type).notifier, + ) + .toggle(selectedItem); + }, + borderRadius: BorderRadius.circular(2.5.w), + child: Padding( + padding: EdgeInsets.all(3.w), + child: Row( + children: [ + CheckboxButton( + isSelected: + selectedItemState.containsKey( + rawMaterial.id, + ) && + selectedItemState[rawMaterial.id]?.type == + widget.type, + ), + + SizedBox(width: 4.w), + + Container( + width: 15.w, + height: 15.w, + decoration: BoxDecoration( + color: Colors.grey[100], + borderRadius: BorderRadius.circular(2.w), + ), + child: Icon( + Icons.inventory_2_rounded, + color: Colors.grey[400], + size: 5.w, + ), + ), + + SizedBox(width: 3.w), + + Expanded( + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text( + rawMaterial.name, + maxLines: 2, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + color: Colors.black87, + ), + ), + SizedBox(height: 0.75.h), + Text( + "Stok: ${rawMaterial.remainingStock.toStringWithoutTrailingZero()} ${rawMaterial.unit}", + style: TextStyle( + color: Colors.grey[600], + fontSize: isTablet + ? (AppFontSize.medium - 1.25).sp + : (AppFontSize.small - 1.25).sp, + ), + ), + ], + ), + ), + ], + ), + ), + ), + ); + }, + ), + ), + ], + ); + } + }, + error: (error, stackTrace) { + return CustomErrorScreen(message: "Ups, terjadi kesalahan", onRefresh: () {}); + }, + loading: () => const SelectItemShimmer(), + ); + } +} diff --git a/Mobile Operasional/lib/features/stock_in/presentation/screens/stock_in_production_screen.dart b/Mobile Operasional/lib/features/stock_in/presentation/screens/stock_in_production_screen.dart new file mode 100644 index 0000000..c346e8b --- /dev/null +++ b/Mobile Operasional/lib/features/stock_in/presentation/screens/stock_in_production_screen.dart @@ -0,0 +1,414 @@ +import 'package:calendar_date_picker2/calendar_date_picker2.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; +import 'package:niogu_app/core/constants/app_color.dart'; +import 'package:niogu_app/core/constants/app_font_size.dart'; +import 'package:niogu_app/core/providers/app_provider.dart'; +import 'package:niogu_app/core/utils/extension_format.dart'; +import 'package:niogu_app/core/enums/item_type.dart'; +import 'package:niogu_app/core/utils/log_message.dart'; +import 'package:niogu_app/core/enums/stock_card_type.dart'; +import 'package:niogu_app/core/widgets/custom_snackbar.dart'; +import 'package:niogu_app/features/stock_in/presentation/widgets/body_form.dart'; +import 'package:niogu_app/features/stock_in/presentation/widgets/body_section.dart'; +import 'package:niogu_app/features/stock_in/presentation/widgets/empty_item.dart'; +import 'package:niogu_app/features/stock_in/presentation/widgets/item_quantity_amount.dart'; +import 'package:niogu_app/features/stock_in/presentation/widgets/note_section.dart'; +import 'package:niogu_app/features/stock_in/domain/entities/stock_in.dart'; +import 'package:niogu_app/features/stock_in/presentation/provider/stock_in_provider.dart'; +import 'package:niogu_app/features/stock_in/presentation/widgets/header_stock_in_production_section.dart'; +import 'package:sizer/sizer.dart'; + +class StockInProductionScreen extends ConsumerStatefulWidget { + const StockInProductionScreen({super.key}); + + @override + ConsumerState createState() => + StockInProductionScreenState(); +} + +class StockInProductionScreenState + extends ConsumerState + with AutomaticKeepAliveClientMixin { + DateTime _selectedDate = DateTime.now(); + + TextEditingController _noteController = TextEditingController(); + + @override + // TODO: implement wantKeepAlive + bool get wantKeepAlive => true; + + @override + void initState() { + // TODO: implement initState + super.initState(); + } + + @override + void dispose() { + _noteController.dispose(); + super.dispose(); + } + + Future _showProductionDatePicker(BuildContext context) async { + final bool isTablet = 100.w >= 600; + + List tempPickedDate = [_selectedDate]; + + final List? results = await showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: Colors.transparent, + constraints: const BoxConstraints(maxWidth: double.infinity), + builder: (context) { + return Container( + height: 80.h, + width: double.infinity, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.vertical(top: Radius.circular(4.5.w)), + ), + constraints: BoxConstraints( + maxHeight: 80.h, + maxWidth: double.infinity, + ), + child: Column( + children: [ + SizedBox(height: 1.5.h), + Container( + width: 12.w, + height: 0.6.h, + decoration: BoxDecoration( + color: Colors.grey[300], + borderRadius: BorderRadius.circular(2.5.w), + ), + ), + Padding( + padding: EdgeInsets.symmetric(vertical: 2.h), + child: Text( + "Pilih Tanggal Produksi", + style: TextStyle( + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + fontWeight: FontWeight.bold, + color: Colors.black87, + ), + ), + ), + Divider(height: 1, color: Colors.grey[200]), + + Expanded( + child: CalendarDatePicker2( + config: CalendarDatePicker2Config( + calendarType: CalendarDatePicker2Type.single, + selectedDayHighlightColor: AppColor.primaryColor, + weekdayLabels: [ + 'Min', + 'Sen', + 'Sel', + 'Rab', + 'Kam', + 'Jum', + 'Sab', + ], + weekdayLabelTextStyle: TextStyle( + color: Colors.grey[600], + fontWeight: FontWeight.bold, + ), + controlsTextStyle: TextStyle( + color: Colors.black, + fontWeight: FontWeight.bold, + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + ), + dayTextStyle: TextStyle( + color: Colors.black87, + fontWeight: FontWeight.w500, + ), + + centerAlignModePicker: true, + firstDayOfWeek: 1, // Senin + ), + value: tempPickedDate, + onValueChanged: (dates) { + tempPickedDate = dates; + }, + ), + ), + + Container( + padding: EdgeInsets.all(5.w), + decoration: BoxDecoration( + color: Colors.white, + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.05), + blurRadius: 10, + offset: const Offset(0, -5), + ), + ], + ), + child: Row( + children: [ + Expanded( + child: OutlinedButton( + onPressed: () => context.pop(), + style: OutlinedButton.styleFrom( + padding: EdgeInsets.symmetric(vertical: 1.5.h), + side: BorderSide(color: AppColor.primaryColor), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(2.5.w), + ), + ), + child: Text( + "Batal", + style: TextStyle( + color: AppColor.primaryColor, + fontWeight: FontWeight.bold, + fontSize: AppFontSize.medium.sp, + ), + ), + ), + ), + SizedBox(width: 3.w), + + Expanded( + child: ElevatedButton( + onPressed: () { + context.pop(tempPickedDate); + }, + style: ElevatedButton.styleFrom( + padding: EdgeInsets.symmetric(vertical: 1.5.h), + backgroundColor: AppColor.primaryColor, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(2.5.w), + ), + ), + child: Text( + "Pilih", + style: TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + fontSize: AppFontSize.medium.sp, + ), + ), + ), + ), + ], + ), + ), + ], + ), + ); + }, + ); + + if (results != null && results.isNotEmpty && results[0] != null) { + setState(() { + _selectedDate = results[0]!; + }); + } + } + + Future updateStockItem() async { + final selectedItemState = ref.read( + selectedItemProvider(SelectedItemType.production), + ); + + final selectedItems = selectedItemState.values.where((item) { + return item.type == SelectedItemType.production; + }).toList(); + + if (selectedItems.isEmpty) { + CustomSnackbar.showWarning(context, "Tambah minimal 1 barang"); + return; + } + + final items = selectedItems.map((item) { + return UpdateStockItem( + outletInventoryId: item.id, + stockCardType: StockCardType.production, + remainingStock: item.remainingStock, + qtyChange: item.quantity, + updateStock: item.remainingStock + item.quantity, + currentStock: item.remainingStock + item.quantity, + costPrice: item.costPrice, + purchasePrice: item.purchasePrice, + notes: _noteController.text.trim(), + ); + }).toList(); + + try { + await ref + .read(stockInControllerProvider.notifier) + .updateStockItem(items: items); + + if (!mounted) return; + + CustomSnackbar.showSuccess(context, "Stok barang berhasil diperbarui"); + + setState(() { + _selectedDate = DateTime.now(); + }); + + ref.invalidate(selectedItemProvider); + + _noteController.clear(); + } catch (e, st) { + LogMessage.log.e(e.toString(), error: e, stackTrace: st); + CustomSnackbar.showError(context, "Ups, terjadi kesalahan"); + } + } + + @override + Widget build(BuildContext context) { + super.build(context); + + final bool isTablet = 100.w >= 600; + + final currentUserName = ref.watch(currentUserNameProvider); + + final selectedItemState = ref.watch( + selectedItemProvider(SelectedItemType.production), + ); + + final selectedItems = selectedItemState.values.where((item) { + return item.type == SelectedItemType.production; + }).toList(); + + return Column( + children: [ + Expanded( + child: SingleChildScrollView( + padding: EdgeInsets.all(5.w), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + HeaderStockInProductionSection( + selectedDate: _selectedDate, + currentUserName: currentUserName!, + onTap: () => _showProductionDatePicker(context), + ), + + SizedBox(height: 3.h), + + BodySection( + label: "Hasil Produksi (${selectedItems.length})", + type: SelectedItemType.production, + ), + + SizedBox(height: 1.5.h), + + if (selectedItems.isEmpty) + const EmptyItem( + icon: Icons.post_add_rounded, + title: "Belum ada barang produksi", + subtitle: "Tekan 'Tambah Barang' untuk memulai", + ) + else + BodyForm( + selectedItems: selectedItems, + label: "Jumlah Produksi:", + onDecrementTap: (item) { + ref + .read( + selectedItemProvider( + SelectedItemType.production, + ).notifier, + ) + .decrement(item.id); + }, + onIncrementTap: (item) { + ref + .read( + selectedItemProvider( + SelectedItemType.production, + ).notifier, + ) + .increment(item.id); + }, + onEditPressed: (item) { + showDialog( + context: context, + builder: (_) { + final ItemType itemType = item.itemType; + + final String name = item.name; + + final String? variantName = item.variantName; + + final String remainingStock = item.remainingStock + .toStringWithoutTrailingZero(); + + final String unit = item.unit; + + final String initialQuantity = item.quantity + .toStringWithoutTrailingZero(); + + return ItemQuantityAmount( + itemType: itemType, + variantName: variantName, + name: name, + remainingStock: remainingStock, + unit: unit, + initialQuantity: initialQuantity, + label: "Jumlah Produksi", + onPressed: (value) { + try { + final double quantity = double.parse(value); + + ref + .read( + selectedItemProvider( + SelectedItemType.production, + ).notifier, + ) + .customQuantity(item.id, quantity); + } catch (e) { + CustomSnackbar.showError( + context, + "Stok tidak valid", + ); + } + }, + ); + }, + ); + }, + onDeletePressed: (item) { + ref + .read( + selectedItemProvider( + SelectedItemType.production, + ).notifier, + ) + .delete(item.id); + }, + ), + + SizedBox(height: 3.h), + + Text( + "Catatan (Opsional)", + style: TextStyle( + fontSize: (AppFontSize.medium - 1.25).sp, + fontWeight: FontWeight.bold, + ), + ), + SizedBox(height: 1.h), + NoteSection( + isTablet: isTablet, + controller: _noteController, + hint: "Contoh: Batch Produksi Pagi #001", + ), + ], + ), + ), + ), + ], + ); + } +} diff --git a/Mobile Operasional/lib/features/stock_in/presentation/screens/stock_in_purchase_screen.dart b/Mobile Operasional/lib/features/stock_in/presentation/screens/stock_in_purchase_screen.dart new file mode 100644 index 0000000..550358d --- /dev/null +++ b/Mobile Operasional/lib/features/stock_in/presentation/screens/stock_in_purchase_screen.dart @@ -0,0 +1,1129 @@ +import 'dart:async'; +import 'dart:io'; + +import 'package:calendar_date_picker2/calendar_date_picker2.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; +import 'package:image_picker/image_picker.dart'; +import 'package:niogu_app/core/router/app_route.dart'; +import 'package:niogu_app/core/utils/image_service.dart'; +import 'package:niogu_app/core/enums/item_type.dart'; +import 'package:niogu_app/core/utils/log_message.dart'; +import 'package:niogu_app/core/enums/payment_status.dart'; +import 'package:niogu_app/core/enums/stock_card_type.dart'; +import 'package:niogu_app/core/widgets/custom_snackbar.dart'; +import 'package:niogu_app/features/report/transaction/presentation/providers/transaction_report_provider.dart'; +import 'package:niogu_app/features/stock_in/presentation/widgets/empty_item.dart'; +import 'package:niogu_app/features/stock_in/presentation/widgets/item_quantity_amount.dart'; +import 'package:niogu_app/features/stock_in/domain/entities/stock_in.dart'; +import 'package:niogu_app/features/stock_in/presentation/provider/stock_in_provider.dart'; +import 'package:niogu_app/features/stock_in/presentation/provider/stock_in_purchase_provider.dart'; +import 'package:niogu_app/features/stock_in/presentation/widgets/financial_section.dart'; +import 'package:niogu_app/features/stock_in/presentation/widgets/header_stock_in_purchase_section.dart'; +import 'package:niogu_app/core/widgets/custom_proof_image_uploader.dart'; +import 'package:niogu_app/features/stock_in/presentation/widgets/purchase_item_card.dart'; +import 'package:sizer/sizer.dart'; +import 'package:niogu_app/core/constants/app_color.dart'; +import 'package:niogu_app/core/constants/app_font_size.dart'; +import 'package:niogu_app/core/utils/currency_format.dart'; +import 'package:niogu_app/core/utils/extension_format.dart'; + +class StockInPurchaseScreen extends ConsumerStatefulWidget { + const StockInPurchaseScreen({super.key}); + + @override + ConsumerState createState() => + StockInPurchaseScreenState(); +} + +class StockInPurchaseScreenState extends ConsumerState + with AutomaticKeepAliveClientMixin { + final GlobalKey _formKey = GlobalKey(); + + final TextEditingController _nameController = TextEditingController(); + + final TextEditingController _phoneNumberController = TextEditingController(); + + final TextEditingController _addressController = TextEditingController(); + + final TextEditingController _discountController = TextEditingController(); + + final TextEditingController _taxController = TextEditingController(); + + final TextEditingController _noteController = TextEditingController(); + + final TextEditingController _payController = TextEditingController(); + + final ImagePicker _picker = ImagePicker(); + + final List _imagePathTemps = []; + + DateTime _selectedDate = DateTime.now(); + + bool _isSupplierFormVisible = false; + + bool _isOtherFormVisible = false; + + PaymentStatus _paymentStatus = PaymentStatus.paid; + + String _selectedPaymentStatus = "Lunas"; + + bool _validateChange = false; + + double _changeAmount = 0.0; + + String? _imagePath; + + Timer? _debounce; + + @override + // TODO: implement wantKeepAlive + bool get wantKeepAlive => true; + + Map _controllers = {}; + + @override + void initState() { + // TODO: implement initState + super.initState(); + } + + @override + void dispose() { + _nameController.dispose(); + _phoneNumberController.dispose(); + _addressController.dispose(); + _discountController.dispose(); + _taxController.dispose(); + _payController.dispose(); + _noteController.dispose(); + _debounce?.cancel(); + super.dispose(); + } + + TextEditingController _controllerOf(SelectedItem item) { + return _controllers.putIfAbsent(item.id, () { + final double purchasePrice = item.itemType == ItemType.product_variant + ? item.costPrice + : item.purchasePrice; + + return TextEditingController( + text: purchasePrice > 0 + ? purchasePrice.toStringWithoutTrailingZero() + : '', + ); + }); + } + + double _calculateTotalPurchase(List items) { + return items.fold(0.0, (sum, item) { + final double purchasePrice = + double.tryParse(_controllerOf(item).text.trim()) ?? 0.0; + return sum + item.quantity * purchasePrice; + }); + } + + void _onPurchasePriceChange(String value) { + final itemState = ref.read(selectedItemProvider(SelectedItemType.purchase)); + + final items = itemState.values.where((item) { + return item.type == SelectedItemType.purchase; + }).toList(); + + final double payAmount = double.tryParse(_payController.text.trim()) ?? 0.0; + + final double totalAmount = _calculateTotalAmount(items); + + _updateGlobalTotal(totalAmount); + + if (_debounce?.isActive ?? false) _debounce?.cancel(); + _debounce = Timer(const Duration(milliseconds: 400), () { + setState(() { + _validateChange = payAmount < totalAmount; + if (payAmount >= totalAmount && items.isNotEmpty) { + _changeAmount = payAmount - totalAmount; + } + }); + }); + } + + void _onCalculateChanged(String value) { + final itemState = ref.read(selectedItemProvider(SelectedItemType.purchase)); + + final items = itemState.values.where((item) { + return item.type == SelectedItemType.purchase; + }).toList(); + + final double payAmount = double.tryParse(value) ?? 0.0; + + final double totalAmount = _calculateTotalAmount(items); + + _updateGlobalTotal(totalAmount); + + if (_debounce?.isActive ?? false) _debounce?.cancel(); + _debounce = Timer(const Duration(milliseconds: 400), () { + setState(() { + if (_paymentStatus == PaymentStatus.paid) { + _validateChange = payAmount < totalAmount; + _payController.text = value; + + if (payAmount >= totalAmount && items.isNotEmpty) { + _changeAmount = payAmount - totalAmount; + } + } else if (_paymentStatus == PaymentStatus.partial) { + _validateChange = payAmount >= totalAmount; + _payController.text = value; + + if (totalAmount > payAmount && items.isNotEmpty) { + _changeAmount = totalAmount - payAmount; + } + } + }); + }); + } + + void _calculateChange(String value) { + final itemState = ref.read(selectedItemProvider(SelectedItemType.purchase)); + + final items = itemState.values.where((item) { + return item.type == SelectedItemType.purchase; + }).toList(); + + final double payAmount = double.tryParse(value) ?? 0.0; + + final double totalAmount = _calculateTotalAmount(items); + + _updateGlobalTotal(totalAmount); + + if (_paymentStatus == PaymentStatus.paid) { + _validateChange = payAmount < totalAmount; + _payController.text = value; + + if (payAmount >= totalAmount && items.isNotEmpty) { + _changeAmount = payAmount - totalAmount; + } + } else if (_paymentStatus == PaymentStatus.partial) { + _validateChange = payAmount >= totalAmount; + _payController.text = value; + + if (totalAmount > payAmount && items.isNotEmpty) { + _changeAmount = totalAmount - payAmount; + } + } + } + + void _discountOnChanged(String value) { + final itemState = ref.read(selectedItemProvider(SelectedItemType.purchase)); + + final items = itemState.values.where((item) { + return item.type == SelectedItemType.purchase; + }).toList(); + + final double payAmount = double.tryParse(_payController.text.trim()) ?? 0.0; + + final double totalAmount = _calculateTotalAmount(items); + + _updateGlobalTotal(totalAmount); + + if (_debounce?.isActive ?? false) _debounce?.cancel(); + _debounce = Timer(const Duration(milliseconds: 400), () { + setState(() { + _validateChange = payAmount < totalAmount; + _discountController.text = value; + }); + }); + + if (_payController.text.trim().isNotEmpty) { + _calculateChange(_payController.text); + } + } + + void _taxOnChanged(String value) { + final itemState = ref.read(selectedItemProvider(SelectedItemType.purchase)); + + final items = itemState.values.where((item) { + return item.type == SelectedItemType.purchase; + }).toList(); + + final double payAmount = double.tryParse(_payController.text.trim()) ?? 0.0; + + final double totalAmount = _calculateTotalAmount(items); + + if (_debounce?.isActive ?? false) _debounce?.cancel(); + _debounce = Timer(const Duration(milliseconds: 400), () { + setState(() { + _validateChange = payAmount < totalAmount; + _taxController.text = value; + }); + }); + + if (_payController.text.trim().isNotEmpty) { + _calculateChange(_payController.text); + } + } + + void _onChangePaymentStatus(String? value) { + final itemState = ref.read(selectedItemProvider(SelectedItemType.purchase)); + + final items = itemState.values.where((item) { + return item.type == SelectedItemType.purchase; + }).toList(); + + final double payAmount = double.tryParse(_payController.text.trim()) ?? 0.0; + + final double totalAmount = _calculateTotalAmount(items); + + _updateGlobalTotal(totalAmount); + + setState(() { + _selectedPaymentStatus = value!; + if (value == 'Lunas') { + _paymentStatus = PaymentStatus.paid; + _validateChange = payAmount < totalAmount; + if (payAmount >= totalAmount && items.isNotEmpty) { + _changeAmount = payAmount - totalAmount; + } + } else if (value == 'Bayar Sebagian') { + _paymentStatus = PaymentStatus.partial; + _validateChange = payAmount >= totalAmount; + if (totalAmount > payAmount && items.isNotEmpty) { + _changeAmount = totalAmount - payAmount; + } + } else if (value == 'Hutang') { + _paymentStatus = PaymentStatus.debt; + } + }); + } + + double _calculateTotalAmount(List items) { + final double discount = + double.tryParse(_discountController.text.trim()) ?? 0.0; + + final double tax = double.tryParse(_taxController.text.trim()) ?? 0.0; + + final double totalPurchase = _calculateTotalPurchase(items); + + return totalPurchase - discount + tax; + } + + Future _showPurchaseDatePicker(BuildContext context) async { + final bool isTablet = 100.w >= 600; + + List tempPickedDate = [_selectedDate]; + + final List? results = await showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: Colors.transparent, + constraints: const BoxConstraints(maxWidth: double.infinity), + builder: (context) { + return Container( + height: 80.h, + width: double.infinity, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.vertical(top: Radius.circular(4.5.w)), + ), + constraints: BoxConstraints( + maxHeight: 80.h, + maxWidth: double.infinity, + ), + child: Column( + children: [ + SizedBox(height: 1.5.h), + Container( + width: 12.w, + height: 0.6.h, + decoration: BoxDecoration( + color: Colors.grey[300], + borderRadius: BorderRadius.circular(2.5.w), + ), + ), + Padding( + padding: EdgeInsets.symmetric(vertical: 2.h), + child: Text( + "Pilih Tanggal Diterima", + style: TextStyle( + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + fontWeight: FontWeight.bold, + color: Colors.black87, + ), + ), + ), + Divider(height: 1, color: Colors.grey[200]), + + Expanded( + child: CalendarDatePicker2( + config: CalendarDatePicker2Config( + calendarType: CalendarDatePicker2Type.single, + selectedDayHighlightColor: AppColor.primaryColor, + weekdayLabels: [ + 'Min', + 'Sen', + 'Sel', + 'Rab', + 'Kam', + 'Jum', + 'Sab', + ], + weekdayLabelTextStyle: TextStyle( + color: Colors.grey[600], + fontWeight: FontWeight.bold, + ), + controlsTextStyle: TextStyle( + color: Colors.black, + fontWeight: FontWeight.bold, + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + ), + dayTextStyle: TextStyle( + color: Colors.black87, + fontWeight: FontWeight.w500, + ), + + centerAlignModePicker: true, + firstDayOfWeek: 1, // Senin + ), + value: tempPickedDate, + onValueChanged: (dates) { + tempPickedDate = dates; + }, + ), + ), + + Container( + padding: EdgeInsets.all(5.w), + decoration: BoxDecoration( + color: Colors.white, + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.05), + blurRadius: 10, + offset: const Offset(0, -5), + ), + ], + ), + child: Row( + children: [ + Expanded( + child: OutlinedButton( + onPressed: () => context.pop(), + style: OutlinedButton.styleFrom( + padding: EdgeInsets.symmetric(vertical: 1.5.h), + side: BorderSide(color: AppColor.primaryColor), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(2.5.w), + ), + ), + child: Text( + "Batal", + style: TextStyle( + color: AppColor.primaryColor, + fontWeight: FontWeight.bold, + fontSize: AppFontSize.medium.sp, + ), + ), + ), + ), + SizedBox(width: 3.w), + + Expanded( + child: ElevatedButton( + onPressed: () { + context.pop(tempPickedDate); + }, + style: ElevatedButton.styleFrom( + padding: EdgeInsets.symmetric(vertical: 1.5.h), + backgroundColor: AppColor.primaryColor, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(2.5.w), + ), + ), + child: Text( + "Pilih", + style: TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + fontSize: AppFontSize.medium.sp, + ), + ), + ), + ), + ], + ), + ), + ], + ), + ); + }, + ); + + if (results != null && results.isNotEmpty && results[0] != null) { + setState(() { + _selectedDate = results[0]!; + }); + } + } + + Future _showPickerOptions() async { + final bool isTablet = 100.w >= 600; + showModalBottomSheet( + context: context, + backgroundColor: Colors.white, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.vertical(top: Radius.circular(2.5.w)), + ), + constraints: const BoxConstraints(maxWidth: double.infinity), + builder: (BuildContext context) { + return SafeArea( + child: Container( + width: 100.w, + padding: EdgeInsets.symmetric(vertical: 2.h, horizontal: 5.w), + clipBehavior: Clip.hardEdge, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.vertical(top: Radius.circular(6.w)), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + ListTile( + leading: Icon(Icons.photo_library, size: 6.w), + title: Text( + 'Galeri', + style: TextStyle( + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + ), + ), + onTap: () async { + await _getImage(ImageSource.gallery); + context.pop(); + }, + ), + + SizedBox(height: 2.h), + + ListTile( + leading: Icon(Icons.photo_camera, size: 6.w), + title: Text( + 'Kamera', + style: TextStyle( + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + ), + ), + onTap: () async { + await _getImage(ImageSource.camera); + context.pop(); + }, + ), + ], + ), + ), + ); + }, + ); + } + + Future _getImage(ImageSource ImageSource) async { + try { + final XFile? pickedFile = await _picker.pickImage( + source: ImageSource, + imageQuality: 70, + ); + + if (pickedFile != null) { + File tempFile = File(pickedFile.path); + + String? imagePath = await ImageService.saveImageToLocalDirectory( + tempFile, + "payment_proof_purchases", + ); + + if (imagePath != null) { + _imagePathTemps.add(imagePath); + ref.read(proofPaymentPurchaseTempProvider.notifier).state = + _imagePathTemps; + setState(() { + _imagePath = imagePath; + }); + } + } + } catch (e, st) { + LogMessage.log.w(e.toString(), error: e, stackTrace: st); + CustomSnackbar.showWarning(context, "Akses ditolak"); + } + } + + Future _removeImage() async { + setState(() { + _imagePath = null; + }); + } + + void _updateGlobalTotal(double totalAmount) { + ref.read(totalPurchaseAmountProvider.notifier).state = totalAmount; + } + + void _updateGlobalTotalByCounter() { + final selectedItemState = ref.watch( + selectedItemProvider(SelectedItemType.purchase), + ); + + final selectedItems = selectedItemState.values.where((item) { + return item.type == SelectedItemType.purchase; + }).toList(); + + final double totalAmount = _calculateTotalAmount(selectedItems); + + _updateGlobalTotal(totalAmount); + } + + void _clear() { + _nameController.clear(); + _phoneNumberController.clear(); + _addressController.clear(); + _discountController.clear(); + _taxController.clear(); + _noteController.clear(); + _payController.clear(); + } + + Future updateStockItem() async { + final selectedItemState = ref.read( + selectedItemProvider(SelectedItemType.purchase), + ); + + final selectedItems = selectedItemState.values.where((item) { + return item.type == SelectedItemType.purchase; + }).toList(); + + if (selectedItems.isEmpty) { + CustomSnackbar.showWarning(context, "Tambah minimal 1 barang"); + return; + } + + for (final item in selectedItems) { + final controller = _controllerOf(item); + + if (double.tryParse(controller.text.trim()) == null) { + CustomSnackbar.showError(context, "Nominal harga beli tidak valid"); + return; + } + + final double purchasePrice = double.parse(controller.text.trim()); + + if (purchasePrice <= 0) { + CustomSnackbar.showError(context, "Harga beli harus lebih dari 0"); + return; + } + + if (item.itemType == ItemType.product_variant && + purchasePrice >= item.sellingPrice) { + CustomSnackbar.showError( + context, + "Harga jual harus lebih besar dari harga beli", + ); + return; + } + } + + if (_paymentStatus != PaymentStatus.debt && + !_formKey.currentState!.validate()) + return; + + SupplierInformation? supplierInfo; + + final bool supplierInfoIsNotEmpty = + _nameController.text.isNotEmpty || + _phoneNumberController.text.isNotEmpty || + _addressController.text.isNotEmpty; + + if (supplierInfoIsNotEmpty) { + if (_nameController.text.isEmpty) { + CustomSnackbar.showError(context, "Nama pemasok belum diisi"); + return; + } + + final name = _nameController.text.trim(); + final phoneNumber = _phoneNumberController.text.trim(); + final address = _addressController.text.trim(); + + supplierInfo = SupplierInformation( + name: name, + phoneNumber: phoneNumber, + address: address, + ); + } + + try { + if (_discountController.text.isNotEmpty) { + final double discount = double.parse(_discountController.text.trim()); + + if (discount <= 0) { + CustomSnackbar.showError(context, "Diskon harus lebih dari 0"); + return; + } + } + } catch (e) { + CustomSnackbar.showError(context, "Nominal diskon tidak valid"); + return; + } + + try { + if (_taxController.text.isNotEmpty) { + final double tax = double.parse(_taxController.text.trim()); + + if (tax <= 0) { + CustomSnackbar.showError(context, "Pajak harus lebih dari 0"); + return; + } + } + } catch (e) { + CustomSnackbar.showError(context, "Nominal pajak tidak valid"); + return; + } + + try { + if (_payController.text.isNotEmpty && _validateChange) { + final double payAmount = double.parse(_payController.text.trim()); + + if (payAmount <= 0) { + CustomSnackbar.showError( + context, + "Nominal pembayaran harus lebih dari 0", + ); + return; + } + + final double totalAmount = _calculateTotalAmount(selectedItems); + + if (_paymentStatus == PaymentStatus.paid && totalAmount > payAmount) { + CustomSnackbar.showError( + context, + "Kurang ${CurrencyFormat.formatToIdr((totalAmount - payAmount), 0)}", + ); + return; + } + + if (_paymentStatus == PaymentStatus.partial && + payAmount >= totalAmount) { + CustomSnackbar.showWarning( + context, + "Jumlah pembayaran telah melunasi pembelian, ubah status pembelian", + ); + return; + } + } + } catch (e) { + CustomSnackbar.showError(context, "Nominal pembayaran tidak valid"); + return; + } + + final selectedSupplierState = ref.read(selectedSupplierProvider); + + final String? supplierId = selectedSupplierState != null + ? selectedSupplierState.id + : supplierInfo != null + ? supplierInfo.localId + : null; + + final double changeAmount = _paymentStatus == PaymentStatus.paid + ? _changeAmount + : 0.0; + + final double underPaymentAmount = _paymentStatus == PaymentStatus.partial + ? _changeAmount + : 0.0; + + final double debtAmount = _paymentStatus == PaymentStatus.debt + ? _calculateTotalAmount(selectedItems) + : 0.0; + + final newPurchase = NewPurchase( + supplierId: supplierId, + totalPurchase: _calculateTotalPurchase(selectedItems), + paymentStatus: _paymentStatus, + totalAmount: _calculateTotalAmount(selectedItems), + otherInformation: OtherInformation( + discount: double.tryParse(_discountController.text.trim()) ?? 0.0, + tax: double.tryParse(_taxController.text.trim()) ?? 0.0, + note: _noteController.text.trim(), + ), + amountPaid: double.tryParse(_payController.text.trim()) ?? 0.0, + changeAmount: changeAmount, + underPaymentAmount: underPaymentAmount, + debtAmount: debtAmount, + paymentProofPath: _imagePath, + ); + + final List itemPurchases = selectedItems.map((item) { + final controller = _controllerOf(item); + + final double purchasePrice = + double.tryParse(controller.text.trim()) ?? 0.0; + + return ItemPurchase( + purchaseId: newPurchase.localId, + itemType: item.itemType, + itemId: item.itemId, + quantity: item.quantity, + imagePath: item.localImagePath, + itemNameSnapshot: item.name, + itemVariantNameSnapshot: item.variantName, + purchasePriceSnaphot: purchasePrice, + subtotal: item.quantity * purchasePrice, + ); + }).toList(); + + final items = selectedItems.map((item) { + final controller = _controllerOf(item); + + final price = double.tryParse(controller.text.trim()) ?? 0.0; + + return UpdateStockItem( + outletInventoryId: item.id, + stockCardType: StockCardType.purchase, + remainingStock: item.remainingStock, + qtyChange: item.quantity, + updateStock: item.remainingStock + item.quantity, + currentStock: item.remainingStock + item.quantity, + costPrice: item.itemType == ItemType.product_variant ? price : 0.0, + purchasePrice: item.itemType == ItemType.raw_material ? price : 0.0, + referenceId: newPurchase.localId, + ); + }).toList(); + + try { + await ref + .read(stockInControllerProvider.notifier) + .updateStockItem( + supplierInfo: supplierInfo, + newPurchase: newPurchase, + itemPurchases: itemPurchases, + items: items, + ); + + if (!mounted) return; + + CustomSnackbar.showSuccess(context, "Stok barang berhasil diperbarui"); + + ref.invalidate(selectedSupplierProvider); + + for (final path in _imagePathTemps) { + if (path != null && _imagePath != null && path != _imagePath) { + await ImageService.deleteLocalImage(path); + } + } + + setState(() { + _selectedDate = DateTime.now(); + _imagePath = null; + _imagePathTemps.clear(); + }); + + ref.invalidate(selectedItemProvider); + + ref.invalidate(proofPaymentPurchaseTempProvider); + + _clear(); + + final purchaseDetail = await ref + .read(transactionReportRepositoryProvider) + .getPurchaseDetail(newPurchase.localId); + + FocusScope.of(context).unfocus(); + + await Future.delayed(const Duration(milliseconds: 400)); + + context.pushNamed( + AppRoute.transactionReportPurchaseDetailScreen, + extra: purchaseDetail, + ); + } catch (e, st) { + LogMessage.log.e(e.toString(), error: e, stackTrace: st); + CustomSnackbar.showError(context, "Ups, terjadi kesalahan"); + } + } + + @override + Widget build(BuildContext context) { + super.build(context); + + final selectedItemState = ref.watch( + selectedItemProvider(SelectedItemType.purchase), + ); + + final selectedItems = selectedItemState.values.where((item) { + return item.type == SelectedItemType.purchase; + }).toList(); + + final selectedSupplierState = ref.watch(selectedSupplierProvider); + + return Column( + children: [ + Expanded( + child: SingleChildScrollView( + padding: EdgeInsets.all(5.w), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + HeaderStockInPurchaseSection( + selectedDate: _selectedDate, + isSupplierFormVisible: _isSupplierFormVisible, + selectedSupplier: selectedSupplierState, + nameController: _nameController, + phoneNumberController: _phoneNumberController, + addressController: _addressController, + onTap: () => _showPurchaseDatePicker(context), + onSupplierFormTap: () { + setState(() { + _isSupplierFormVisible = !_isSupplierFormVisible; + }); + }, + onTapCloseSelectedSupplier: selectedSupplierState == null + ? null + : () { + ref.read(selectedSupplierProvider.notifier).state = + null; + }, + ), + + SizedBox(height: 3.h), + + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Barang Dibeli (${selectedItems.length})", + style: TextStyle( + fontSize: (AppFontSize.medium - 1.25).sp, + fontWeight: FontWeight.bold, + color: Colors.black87, + ), + ), + TextButton.icon( + onPressed: () => context.pushNamed( + AppRoute.selectGoodScreen, + extra: { + "type": SelectedItemType.purchase, + "on_pressed": () { + final itemState = ref.read( + selectedItemProvider(SelectedItemType.purchase), + ); + + final items = itemState.values.where((item) { + return item.type == SelectedItemType.purchase; + }).toList(); + + final double payAmount = + double.tryParse(_payController.text.trim()) ?? + 0.0; + + final double totalAmount = _calculateTotalAmount( + items, + ); + + _updateGlobalTotal(totalAmount); + + if (_payController.text.isNotEmpty) { + setState(() { + _validateChange = payAmount < totalAmount; + if (payAmount >= totalAmount && + items.isNotEmpty) { + _changeAmount = payAmount - totalAmount; + } + }); + } + }, + }, + ), + icon: Icon( + Icons.add_shopping_cart_rounded, + size: 5.w, + color: AppColor.primaryColor, + ), + label: Text( + "Tambah Barang", + style: TextStyle( + color: AppColor.primaryColor, + fontSize: (AppFontSize.medium - 1.25).sp, + fontWeight: FontWeight.bold, + ), + ), + ), + ], + ), + + if (selectedItems.isEmpty) + const EmptyItem( + icon: Icons.hourglass_empty_outlined, + title: "Belum ada barang pembelian", + subtitle: "Tekan 'Tambah Barang' untuk memulai", + ) + else + ListView.separated( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: selectedItems.length, + separatorBuilder: (c, i) => SizedBox(height: 2.h), + itemBuilder: (context, index) { + final item = selectedItems[index]; + return PurchaseItemCard( + item: item, + purchasePriceController: _controllerOf(item), + onChanged: _onPurchasePriceChange, + onDecrementTap: () { + ref + .read( + selectedItemProvider( + SelectedItemType.purchase, + ).notifier, + ) + .decrement(item.id); + + _updateGlobalTotalByCounter(); + + if (_payController.text.trim().isNotEmpty) { + _calculateChange(_payController.text); + } + }, + onIncrementTap: () { + ref + .read( + selectedItemProvider( + SelectedItemType.purchase, + ).notifier, + ) + .increment(item.id); + + _updateGlobalTotalByCounter(); + + if (_payController.text.trim().isNotEmpty) { + _calculateChange(_payController.text); + } + }, + onEditPressed: () { + showDialog( + context: context, + builder: (_) { + final ItemType itemType = item.itemType; + + final String name = item.name; + + final String? variantName = item.variantName; + + final String remainingStock = item.remainingStock + .toStringWithoutTrailingZero(); + + final String unit = item.unit; + + final String initialQuantity = item.quantity + .toStringWithoutTrailingZero(); + + return ItemQuantityAmount( + itemType: itemType, + variantName: variantName, + name: name, + remainingStock: remainingStock, + unit: unit, + initialQuantity: initialQuantity, + label: "Jumlah Diterima", + onPressed: (value) { + try { + final double quantity = double.parse(value); + + ref + .read( + selectedItemProvider( + SelectedItemType.purchase, + ).notifier, + ) + .customQuantity(item.id, quantity); + + _updateGlobalTotalByCounter(); + + if (_payController.text.trim().isNotEmpty) { + _calculateChange(_payController.text); + } + } catch (e) { + CustomSnackbar.showError( + context, + "Stok tidak valid", + ); + } + }, + ); + }, + ); + }, + onDeletePressed: () { + ref + .read( + selectedItemProvider( + SelectedItemType.purchase, + ).notifier, + ) + .delete(item.id); + + _updateGlobalTotalByCounter(); + + if (_payController.text.trim().isNotEmpty) { + _calculateChange(_payController.text); + } + }, + ); + }, + ), + + SizedBox(height: 3.h), + + FinancialSection( + formKey: _formKey, + totalPurchase: _calculateTotalPurchase(selectedItems), + isOtherFormVisible: _isOtherFormVisible, + items: selectedItems, + validateChange: _validateChange, + totalAmount: _calculateTotalAmount(selectedItems), + selectedPaymentStatus: _selectedPaymentStatus, + onOtherFormTap: () { + setState(() { + _isOtherFormVisible = !_isOtherFormVisible; + }); + }, + discountController: _discountController, + taxController: _taxController, + noteController: _noteController, + payController: _payController, + onCalculateChange: _onCalculateChanged, + changeAmount: _changeAmount, + discountOnChanged: _discountOnChanged, + taxOnChanged: _taxOnChanged, + onChanged: _onChangePaymentStatus, + ), + + SizedBox(height: 2.h), + + if (_paymentStatus == PaymentStatus.paid || + _paymentStatus == PaymentStatus.partial) + CustomProofImageUploader( + imagePath: _imagePath, + title: "Upload Foto Struk / Nota", + onTapImageAdd: _showPickerOptions, + onTapImageRemove: _removeImage, + ), + ], + ), + ), + ), + ], + ); + } +} diff --git a/Mobile Operasional/lib/features/stock_in/presentation/screens/stock_in_screen.dart b/Mobile Operasional/lib/features/stock_in/presentation/screens/stock_in_screen.dart new file mode 100644 index 0000000..7056ef1 --- /dev/null +++ b/Mobile Operasional/lib/features/stock_in/presentation/screens/stock_in_screen.dart @@ -0,0 +1,347 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; +import 'package:niogu_app/core/providers/app_provider.dart'; +import 'package:niogu_app/core/utils/currency_format.dart'; +import 'package:niogu_app/core/utils/image_service.dart'; +import 'package:niogu_app/core/enums/user_role.dart'; +import 'package:niogu_app/core/widgets/pop_up_notification.dart'; +import 'package:niogu_app/features/stock_in/domain/entities/stock_in.dart'; +import 'package:niogu_app/features/stock_in/presentation/provider/stock_in_provider.dart'; +import 'package:niogu_app/features/stock_in/presentation/screens/stock_in_production_screen.dart'; +import 'package:niogu_app/features/stock_in/presentation/provider/stock_in_purchase_provider.dart'; +import 'package:niogu_app/features/stock_in/presentation/screens/stock_in_purchase_screen.dart'; +import 'package:niogu_app/features/stock_in/presentation/screens/stock_in_transfer_screen.dart'; +import 'package:sizer/sizer.dart'; +import 'package:niogu_app/core/constants/app_color.dart'; +import 'package:niogu_app/core/constants/app_font_size.dart'; + +class StockInScreen extends ConsumerStatefulWidget { + const StockInScreen({super.key}); + + @override + ConsumerState createState() => _StockInScreenState(); +} + +class _StockInScreenState extends ConsumerState + with SingleTickerProviderStateMixin { + final GlobalKey _productionKey = GlobalKey(); + final GlobalKey _transferKey = GlobalKey(); + final GlobalKey _purchaseKey = GlobalKey(); + + late TabController _tabController; + + int _tabIndex = 0; + + @override + void initState() { + super.initState(); + _tabController = TabController(length: 3, vsync: this); + + _tabController.addListener(_handleTabSelection); + + WidgetsBinding.instance.addPostFrameCallback((_) { + Future.microtask(() { + ref.invalidate(proofPaymentPurchaseTempProvider); + }); + }); + } + + @override + void dispose() { + _tabController.removeListener(_handleTabSelection); + _tabController.dispose(); + super.dispose(); + } + + void _handleTabSelection() { + if (_tabController.index != _tabIndex) { + setState(() { + _tabIndex = _tabController.index; + }); + } + } + + Future _cleanUpImages() async { + final paths = ref.read(proofPaymentPurchaseTempProvider); + + for (final path in paths) { + if (path != null) { + await ImageService.deleteLocalImage(path); + } + } + + ref.invalidate(proofPaymentPurchaseTempProvider); + } + + Future _updateStockItem() async { + if (_tabIndex == 0) { + await _productionKey.currentState?.updateStockItem(); + } else if (_tabIndex == 1) { + await _transferKey.currentState?.updateStockItem(); + } else if (_tabIndex == 2) { + await _purchaseKey.currentState?.updateStockItem(); + } + } + + @override + Widget build(BuildContext context) { + return LayoutBuilder( + builder: (context, constraints) { + final messageActivity = ref.watch(messageActivityProvider); + + final currentUserRole = ref.watch(currentUserRoleProvider); + + final selectedItemProductionState = ref.watch( + selectedItemProvider(SelectedItemType.production), + ); + + final selectedItemTransferState = ref.watch( + selectedItemProvider(SelectedItemType.transfer_in), + ); + + final selectedItemPurchaseState = ref.watch( + selectedItemProvider(SelectedItemType.purchase), + ); + + final selectedItemProductions = selectedItemProductionState.values + .where((item) { + return item.type == SelectedItemType.production; + }) + .toList(); + + final selectedItemTransfer = selectedItemTransferState.values.where(( + item, + ) { + return item.type == SelectedItemType.transfer_in; + }).toList(); + + final selectedItemPurchase = selectedItemPurchaseState.values.where(( + item, + ) { + return item.type == SelectedItemType.purchase; + }).toList(); + + final double totalPurchaseAmount = ref.watch( + totalPurchaseAmountProvider, + ); + + final stockInControllerState = ref.watch(stockInControllerProvider); + + return PopScope( + canPop: false, + onPopInvokedWithResult: (didPop, result) async { + if (didPop) return; + + await _cleanUpImages(); + + context.pop(); + }, + child: SafeArea( + top: false, + bottom: true, + right: false, + left: false, + child: Stack( + children: [ + Scaffold( + backgroundColor: const Color(0xFFF9FAFB), + appBar: AppBar( + backgroundColor: Colors.white, + surfaceTintColor: Colors.white, + elevation: 0, + toolbarHeight: 10.h, + centerTitle: true, + leading: Center( + child: Material( + color: Colors.transparent, + type: MaterialType.canvas, + child: InkWell( + onTap: () async { + await _cleanUpImages(); + + context.pop(); + }, + borderRadius: BorderRadius.circular(2.5.w), + child: Container( + width: 10.w, + height: 10.w, + decoration: BoxDecoration( + color: Colors.white, + border: Border.all(color: Colors.grey.shade200), + borderRadius: BorderRadius.circular(2.5.w), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.05), + blurRadius: 5, + offset: const Offset(0, 2), + ), + ], + ), + child: Icon( + Icons.arrow_back_ios_new_rounded, + color: Colors.black87, + size: 5.w, + ), + ), + ), + ), + ), + leadingWidth: 18.w, + title: Text( + "Stok Masuk", + style: TextStyle( + color: Colors.black87, + fontWeight: FontWeight.bold, + fontSize: AppFontSize.medium.sp, + ), + ), + /** + actions: [ + IconButton( + onPressed: () {}, + icon: Icon( + Icons.help_outline_rounded, + color: Colors.grey[400], + size: 4.w, + ), + ), + SizedBox(width: 3.w), + ], + */ + bottom: PreferredSize( + preferredSize: Size.fromHeight(8.h), + child: Container( + height: 6.h, + margin: EdgeInsets.fromLTRB(4.w, 0, 4.w, 1.5.h), + decoration: BoxDecoration( + color: Colors.grey[100], + borderRadius: BorderRadius.circular(2.5.w), + ), + child: TabBar( + onTap: (value) { + setState(() { + _tabIndex = value; + }); + }, + controller: _tabController, + indicator: BoxDecoration( + color: AppColor.primaryColor, + borderRadius: BorderRadius.circular(2.05.w), + boxShadow: [ + BoxShadow( + color: AppColor.primaryColor.withOpacity(0.3), + blurRadius: 4, + offset: const Offset(0, 2), + ), + ], + ), + indicatorSize: TabBarIndicatorSize.tab, + dividerColor: Colors.transparent, + labelColor: Colors.white, + unselectedLabelColor: Colors.grey[600], + labelStyle: TextStyle( + fontWeight: FontWeight.bold, + fontSize: (AppFontSize.medium - 1.5).sp, + ), + padding: EdgeInsets.all(1.w), + tabs: [ + Tab(text: "Produksi", height: 10.h), + Tab(text: "Transfer", height: 10.h), + Tab(text: "Pembelian", height: 10.h), + ], + ), + ), + ), + ), + body: TabBarView( + controller: _tabController, + children: [ + StockInProductionScreen(key: _productionKey), + + StockInTransferScreen(key: _transferKey), + + StockInPurchaseScreen(key: _purchaseKey), + ], + ), + bottomNavigationBar: Container( + padding: EdgeInsets.all(5.w), + decoration: BoxDecoration( + color: Colors.white, + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.05), + blurRadius: 10, + offset: const Offset(0, -5), + ), + ], + ), + child: SizedBox( + width: double.infinity, + height: 6.5.h, + child: ElevatedButton( + onPressed: stockInControllerState.isLoading + ? null + : _tabIndex == 0 && selectedItemProductions.isEmpty + ? null + : _tabIndex == 1 && selectedItemTransfer.isEmpty + ? null + : _tabIndex == 2 && selectedItemPurchase.isEmpty + ? null + : _updateStockItem, + style: ElevatedButton.styleFrom( + backgroundColor: AppColor.primaryColor, + disabledBackgroundColor: Colors.grey[300], + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(2.5.w), + ), + ), + child: _tabIndex != 2 + ? Text( + "Simpan & Tambah Stok", + style: TextStyle( + color: Colors.white, + fontSize: AppFontSize.medium.sp, + fontWeight: FontWeight.bold, + ), + ) + : Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Text( + "Total: ${CurrencyFormat.formatToIdr(totalPurchaseAmount, 0)}", + style: TextStyle( + color: Colors.white, + fontSize: (AppFontSize.medium - 1.25).sp, + ), + ), + Text( + "Proses Pembelian", + style: TextStyle( + color: Colors.white, + fontSize: (AppFontSize.medium - 1.25).sp, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + ), + ), + ), + ), + + if (messageActivity != null) + PopupNotification( + isOwner: currentUserRole == UserRole.owner, + messages: messageActivity.messages, + type: messageActivity.type, + ), + ], + ), + ), + ); + }, + ); + } +} diff --git a/Mobile Operasional/lib/features/stock_in/presentation/screens/stock_in_transfer_screen.dart b/Mobile Operasional/lib/features/stock_in/presentation/screens/stock_in_transfer_screen.dart new file mode 100644 index 0000000..28d0a00 --- /dev/null +++ b/Mobile Operasional/lib/features/stock_in/presentation/screens/stock_in_transfer_screen.dart @@ -0,0 +1,449 @@ +import 'package:calendar_date_picker2/calendar_date_picker2.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; +import 'package:niogu_app/core/constants/app_color.dart'; +import 'package:niogu_app/core/utils/extension_format.dart'; +import 'package:niogu_app/core/enums/item_type.dart'; +import 'package:niogu_app/core/utils/log_message.dart'; +import 'package:niogu_app/core/enums/stock_card_type.dart'; +import 'package:niogu_app/core/widgets/custom_snackbar.dart'; +import 'package:niogu_app/features/outlets/presentation/providers/outlet_provider.dart'; +import 'package:niogu_app/features/stock_in/presentation/widgets/body_form.dart'; +import 'package:niogu_app/features/stock_in/presentation/widgets/body_section.dart'; +import 'package:niogu_app/features/stock_in/presentation/widgets/empty_item.dart'; +import 'package:niogu_app/features/stock_in/presentation/widgets/item_quantity_amount.dart'; +import 'package:niogu_app/features/stock_in/presentation/widgets/note_section.dart'; +import 'package:niogu_app/features/stock_in/domain/entities/stock_in.dart'; +import 'package:niogu_app/features/stock_in/presentation/provider/stock_in_provider.dart'; +import 'package:niogu_app/features/stock_in/presentation/widgets/header_stock_in_transfer_section.dart'; +import 'package:sizer/sizer.dart'; +import 'package:niogu_app/core/constants/app_font_size.dart'; + +class StockInTransferScreen extends ConsumerStatefulWidget { + const StockInTransferScreen({super.key}); + + @override + ConsumerState createState() => + StockInTransferScreenState(); +} + +class StockInTransferScreenState extends ConsumerState + with AutomaticKeepAliveClientMixin { + DateTime _selectedDate = DateTime.now(); + + String? _selectedSourceOutlet; + + final TextEditingController _refNumberController = TextEditingController(); + + final TextEditingController _noteController = TextEditingController(); + + @override + // TODO: implement wantKeepAlive + bool get wantKeepAlive => true; + + @override + void initState() { + super.initState(); + } + + @override + void dispose() { + _refNumberController.dispose(); + _noteController.dispose(); + super.dispose(); + } + + Future _showTransferInDatePicker(BuildContext context) async { + final bool isTablet = 100.w >= 600; + + List tempPickedDate = [_selectedDate]; + + final List? results = await showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: Colors.transparent, + constraints: const BoxConstraints(maxWidth: double.infinity), + builder: (context) { + return Container( + height: 80.h, + width: double.infinity, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.vertical(top: Radius.circular(4.5.w)), + ), + constraints: BoxConstraints( + maxHeight: 80.h, + maxWidth: double.infinity, + ), + child: Column( + children: [ + SizedBox(height: 1.5.h), + Container( + width: 12.w, + height: 0.6.h, + decoration: BoxDecoration( + color: Colors.grey[300], + borderRadius: BorderRadius.circular(2.5.w), + ), + ), + Padding( + padding: EdgeInsets.symmetric(vertical: 2.h), + child: Text( + "Pilih Tanggal Diterima", + style: TextStyle( + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + fontWeight: FontWeight.bold, + color: Colors.black87, + ), + ), + ), + Divider(height: 1, color: Colors.grey[200]), + + Expanded( + child: CalendarDatePicker2( + config: CalendarDatePicker2Config( + calendarType: CalendarDatePicker2Type.single, + selectedDayHighlightColor: AppColor.primaryColor, + weekdayLabels: [ + 'Min', + 'Sen', + 'Sel', + 'Rab', + 'Kam', + 'Jum', + 'Sab', + ], + weekdayLabelTextStyle: TextStyle( + color: Colors.grey[600], + fontWeight: FontWeight.bold, + ), + controlsTextStyle: TextStyle( + color: Colors.black, + fontWeight: FontWeight.bold, + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + ), + dayTextStyle: TextStyle( + color: Colors.black87, + fontWeight: FontWeight.w500, + ), + + centerAlignModePicker: true, + firstDayOfWeek: 1, // Senin + ), + value: tempPickedDate, + onValueChanged: (dates) { + tempPickedDate = dates; + }, + ), + ), + + Container( + padding: EdgeInsets.all(5.w), + decoration: BoxDecoration( + color: Colors.white, + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.05), + blurRadius: 10, + offset: const Offset(0, -5), + ), + ], + ), + child: Row( + children: [ + Expanded( + child: OutlinedButton( + onPressed: () => context.pop(), + style: OutlinedButton.styleFrom( + padding: EdgeInsets.symmetric(vertical: 1.5.h), + side: BorderSide(color: AppColor.primaryColor), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(2.5.w), + ), + ), + child: Text( + "Batal", + style: TextStyle( + color: AppColor.primaryColor, + fontWeight: FontWeight.bold, + fontSize: AppFontSize.medium.sp, + ), + ), + ), + ), + SizedBox(width: 3.w), + + Expanded( + child: ElevatedButton( + onPressed: () { + context.pop(tempPickedDate); + }, + style: ElevatedButton.styleFrom( + padding: EdgeInsets.symmetric(vertical: 1.5.h), + backgroundColor: AppColor.primaryColor, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(2.5.w), + ), + ), + child: Text( + "Pilih", + style: TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + fontSize: AppFontSize.medium.sp, + ), + ), + ), + ), + ], + ), + ), + ], + ), + ); + }, + ); + + if (results != null && results.isNotEmpty && results[0] != null) { + setState(() { + _selectedDate = results[0]!; + }); + } + } + + Future updateStockItem() async { + if (_selectedSourceOutlet == null) { + CustomSnackbar.showWarning(context, "Sumber asal belum dipilih"); + return; + } + + final selectedItemState = ref.read( + selectedItemProvider(SelectedItemType.transfer_in), + ); + + final selectedItems = selectedItemState.values.where((item) { + return item.type == SelectedItemType.transfer_in; + }).toList(); + + if (selectedItems.isEmpty) { + CustomSnackbar.showWarning(context, "Tambah minimal 1 barang"); + return; + } + + final items = selectedItems.map((item) { + return UpdateStockItem( + outletInventoryId: item.id, + stockCardType: StockCardType.transfer_in, + receiveFromId: _selectedSourceOutlet, + remainingStock: item.remainingStock, + qtyChange: item.quantity, + updateStock: item.remainingStock + item.quantity, + currentStock: item.remainingStock + item.quantity, + costPrice: item.costPrice, + purchasePrice: item.purchasePrice, + documentNumber: _refNumberController.text.trim(), + notes: _noteController.text.trim(), + ); + }).toList(); + + try { + await ref + .read(stockInControllerProvider.notifier) + .updateStockItem(items: items); + + if (!mounted) return; + + CustomSnackbar.showSuccess(context, "Stok barang berhasil diperbarui"); + + setState(() { + _selectedDate = DateTime.now(); + _selectedSourceOutlet = null; + }); + + _refNumberController.clear(); + + _noteController.clear(); + + ref.invalidate(selectedItemProvider); + } catch (e, st) { + LogMessage.log.e(e.toString(), error: e, stackTrace: st); + CustomSnackbar.showError(context, "Ups, terjadi kesalahan"); + } + } + + @override + Widget build(BuildContext context) { + super.build(context); + + final bool isTablet = 100.w > 600; + + final sourceOutletState = ref.watch(sourceOutletStreamProvider(false)); + + final selectedItemState = ref.watch( + selectedItemProvider(SelectedItemType.transfer_in), + ); + + final selectedItems = selectedItemState.values.where((item) { + return item.type == SelectedItemType.transfer_in; + }).toList(); + + return Column( + children: [ + Expanded( + child: SingleChildScrollView( + padding: EdgeInsets.all(5.w), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + sourceOutletState.maybeWhen( + data: (sourceOutlets) { + return HeaderStockInTransferSection( + selectedDate: _selectedDate, + onTap: () => _showTransferInDatePicker(context), + selectedSourceOutlet: _selectedSourceOutlet, + sourceOutlets: sourceOutlets, + onChanged: (value) { + setState(() { + _selectedSourceOutlet = value; + }); + }, + refNumberController: _refNumberController, + ); + }, + orElse: () { + return HeaderStockInTransferSection( + selectedDate: _selectedDate, + onTap: () => _showTransferInDatePicker(context), + selectedSourceOutlet: _selectedSourceOutlet, + sourceOutlets: [], + onChanged: (_) {}, + refNumberController: _refNumberController, + ); + }, + ), + + SizedBox(height: 3.h), + + BodySection( + label: "Barang Diterima (${selectedItems.length})", + type: SelectedItemType.transfer_in, + ), + + SizedBox(height: 1.5.h), + + if (selectedItems.isEmpty) + const EmptyItem( + icon: Icons.assignment_return_outlined, + title: "Belum ada barang transfer", + subtitle: "Tekan 'Tambah Barang' untuk memulai", + ) + else + BodyForm( + selectedItems: selectedItems, + label: "Jumlah Diterima:", + onDecrementTap: (item) { + ref + .read( + selectedItemProvider( + SelectedItemType.transfer_in, + ).notifier, + ) + .decrement(item.id); + }, + onIncrementTap: (item) { + ref + .read( + selectedItemProvider( + SelectedItemType.transfer_in, + ).notifier, + ) + .increment(item.id); + }, + onEditPressed: (item) { + showDialog( + context: context, + builder: (_) { + final ItemType itemType = item.itemType; + + final String name = item.name; + + final String? variantName = item.variantName; + + final String remainingStock = item.remainingStock + .toStringWithoutTrailingZero(); + + final String unit = item.unit; + + final String initialQuantity = item.quantity + .toStringWithoutTrailingZero(); + + return ItemQuantityAmount( + itemType: itemType, + variantName: variantName, + name: name, + remainingStock: remainingStock, + unit: unit, + initialQuantity: initialQuantity, + label: "Jumlah Transfer", + onPressed: (value) { + try { + final double quantity = double.parse(value); + + ref + .read( + selectedItemProvider( + SelectedItemType.transfer_in, + ).notifier, + ) + .customQuantity(item.id, quantity); + } catch (e) { + CustomSnackbar.showError( + context, + "Stok tidak valid", + ); + } + }, + ); + }, + ); + }, + onDeletePressed: (item) { + ref + .read( + selectedItemProvider( + SelectedItemType.transfer_in, + ).notifier, + ) + .delete(item.id); + }, + ), + + SizedBox(height: 3.h), + + Text( + "Catatan Penerimaan", + style: TextStyle( + fontSize: (AppFontSize.medium - 1.25).sp, + fontWeight: FontWeight.bold, + ), + ), + + SizedBox(height: 1.h), + NoteSection( + isTablet: isTablet, + controller: _noteController, + hint: "Contoh : Kardus sedikit penyok tapi barang aman", + ), + ], + ), + ), + ), + ], + ); + } +} diff --git a/Mobile Operasional/lib/features/stock_in/presentation/widgets/already_supplier_shimmer.dart b/Mobile Operasional/lib/features/stock_in/presentation/widgets/already_supplier_shimmer.dart new file mode 100644 index 0000000..ec34e02 --- /dev/null +++ b/Mobile Operasional/lib/features/stock_in/presentation/widgets/already_supplier_shimmer.dart @@ -0,0 +1,82 @@ +import 'package:flutter/material.dart'; +import 'package:shimmer/shimmer.dart'; +import 'package:sizer/sizer.dart'; + +class AlreadySupplierShimmer extends StatelessWidget { + const AlreadySupplierShimmer({super.key}); + + @override + Widget build(BuildContext context) { + final baseColor = Colors.grey[300]!; + final highlightColor = Colors.grey[100]!; + + return ListView.builder( + padding: EdgeInsets.fromLTRB(5.w, 0, 5.w, 12.h), + itemCount: 6, + itemBuilder: (context, index) { + return Container( + margin: EdgeInsets.only(bottom: 2.h), + padding: EdgeInsets.all(3.w), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(4.w), + border: Border.all(color: Colors.grey.shade200), + ), + child: Shimmer.fromColors( + baseColor: baseColor, + highlightColor: highlightColor, + child: Row( + children: [ + Container( + width: 12.w, + height: 12.w, + decoration: const BoxDecoration( + color: Colors.white, + shape: BoxShape.circle, + ), + ), + + SizedBox(width: 4.w), + + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + width: 40.w, + height: 1.5.h, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(1.w), + ), + ), + SizedBox(height: 1.h), + + Container( + width: 25.w, + height: 1.2.h, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(1.w), + ), + ), + ], + ), + ), + + Container( + width: 5.w, + height: 5.w, + decoration: const BoxDecoration( + color: Colors.white, + shape: BoxShape.circle, + ), + ), + ], + ), + ), + ); + }, + ); + } +} \ No newline at end of file diff --git a/Mobile Operasional/lib/features/stock_in/presentation/widgets/body_form.dart b/Mobile Operasional/lib/features/stock_in/presentation/widgets/body_form.dart new file mode 100644 index 0000000..480c49f --- /dev/null +++ b/Mobile Operasional/lib/features/stock_in/presentation/widgets/body_form.dart @@ -0,0 +1,44 @@ +import 'package:flutter/material.dart'; +import 'package:niogu_app/features/stock_in/domain/entities/stock_in.dart'; +import 'package:niogu_app/features/stock_in/presentation/widgets/item_input_card.dart'; +import 'package:sizer/sizer.dart'; + +class BodyForm extends StatelessWidget { + final List selectedItems; + final String label; + final void Function(SelectedItem) onDecrementTap; + final void Function(SelectedItem) onIncrementTap; + final void Function(SelectedItem) onEditPressed; + final void Function(SelectedItem) onDeletePressed; + + const BodyForm({ + super.key, + required this.selectedItems, + required this.label, + required this.onDecrementTap, + required this.onIncrementTap, + required this.onEditPressed, + required this.onDeletePressed, + }); + + @override + Widget build(BuildContext context) { + return ListView.separated( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: selectedItems.length, + separatorBuilder: (c, i) => SizedBox(height: 2.h), + itemBuilder: (context, index) { + final item = selectedItems[index]; + return ItemInputCard( + item: item, + label: label, + onDecrementTap: () => item.quantity > 1 ? onDecrementTap(item) : null, + onIncrementTap: () => onIncrementTap(item), + onEditPressed: () => onEditPressed(item), + onDeletePressed: () => onDeletePressed(item), + ); + }, + ); + } +} diff --git a/Mobile Operasional/lib/features/stock_in/presentation/widgets/body_section.dart b/Mobile Operasional/lib/features/stock_in/presentation/widgets/body_section.dart new file mode 100644 index 0000000..f74f830 --- /dev/null +++ b/Mobile Operasional/lib/features/stock_in/presentation/widgets/body_section.dart @@ -0,0 +1,49 @@ +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'package:niogu_app/core/constants/app_color.dart'; +import 'package:niogu_app/core/constants/app_font_size.dart'; +import 'package:niogu_app/core/router/app_route.dart'; +import 'package:niogu_app/features/stock_in/domain/entities/stock_in.dart'; +import 'package:sizer/sizer.dart'; + +class BodySection extends StatelessWidget { + final String label; + final SelectedItemType type; + const BodySection({super.key, required this.label, required this.type}); + + @override + Widget build(BuildContext context) { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + label, + style: TextStyle( + fontSize: (AppFontSize.medium - 1.25).sp, + fontWeight: FontWeight.bold, + color: Colors.black87, + ), + ), + TextButton.icon( + onPressed: () => context.pushNamed( + AppRoute.selectGoodScreen, + extra: {"type": type}, + ), + icon: Icon( + Icons.add_circle_outline, + size: 5.w, + color: AppColor.primaryColor, + ), + label: Text( + "Tambah Barang", + style: TextStyle( + color: AppColor.primaryColor, + fontSize: (AppFontSize.medium - 1.25).sp, + fontWeight: FontWeight.bold, + ), + ), + ), + ], + ); + } +} diff --git a/Mobile Operasional/lib/features/stock_in/presentation/widgets/checkbox_button.dart b/Mobile Operasional/lib/features/stock_in/presentation/widgets/checkbox_button.dart new file mode 100644 index 0000000..f91a986 --- /dev/null +++ b/Mobile Operasional/lib/features/stock_in/presentation/widgets/checkbox_button.dart @@ -0,0 +1,28 @@ +import 'package:flutter/material.dart'; +import 'package:niogu_app/core/constants/app_color.dart'; +import 'package:sizer/sizer.dart'; + +class CheckboxButton extends StatelessWidget { + final bool isSelected; + const CheckboxButton({super.key, required this.isSelected}); + + @override + Widget build(BuildContext context) { + return AnimatedContainer( + duration: const Duration(milliseconds: 200), + width: 6.w, + height: 6.w, + decoration: BoxDecoration( + color: isSelected ? AppColor.primaryColor : Colors.white, + shape: BoxShape.circle, + border: Border.all( + color: isSelected ? AppColor.primaryColor : Colors.grey.shade400, + width: 2, + ), + ), + child: isSelected + ? Icon(Icons.check, color: Colors.white, size: 4.w) + : null, + ); + } +} \ No newline at end of file diff --git a/Mobile Operasional/lib/features/stock_in/presentation/widgets/counter_button.dart b/Mobile Operasional/lib/features/stock_in/presentation/widgets/counter_button.dart new file mode 100644 index 0000000..be998a7 --- /dev/null +++ b/Mobile Operasional/lib/features/stock_in/presentation/widgets/counter_button.dart @@ -0,0 +1,20 @@ +import 'package:flutter/material.dart'; +import 'package:niogu_app/core/constants/app_color.dart'; +import 'package:sizer/sizer.dart'; + +class CounterButton extends StatelessWidget { + final IconData icon; + final VoidCallback? onTap; + const CounterButton({super.key, required this.icon, required this.onTap}); + + @override + Widget build(BuildContext context) { + return InkWell( + onTap: onTap, + child: Padding( + padding: EdgeInsets.all(1.5.w), + child: Icon(icon, size: 4.w, color: AppColor.primaryColor), + ), + ); + } +} diff --git a/Mobile Operasional/lib/features/stock_in/presentation/widgets/empty_item.dart b/Mobile Operasional/lib/features/stock_in/presentation/widgets/empty_item.dart new file mode 100644 index 0000000..fbb8bc1 --- /dev/null +++ b/Mobile Operasional/lib/features/stock_in/presentation/widgets/empty_item.dart @@ -0,0 +1,56 @@ +import 'package:flutter/material.dart'; +import 'package:niogu_app/core/constants/app_font_size.dart'; +import 'package:sizer/sizer.dart'; + +class EmptyItem extends StatelessWidget { + final IconData icon; + final String title; + final String subtitle; + const EmptyItem({ + super.key, + required this.icon, + required this.title, + required this.subtitle, + }); + + @override + Widget build(BuildContext context) { + final bool isTablet = 100.w >= 600; + return Container( + width: double.infinity, + padding: EdgeInsets.symmetric(vertical: 5.h), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(2.5.w), + border: Border.all( + color: Colors.grey.shade200, + style: BorderStyle.solid, + ), + ), + child: Column( + children: [ + Icon(icon, size: 12.w, color: Colors.grey[300]), + SizedBox(height: 2.h), + Text( + title, + style: TextStyle( + color: Colors.grey[500], + fontSize: AppFontSize.medium.sp, + fontWeight: FontWeight.bold, + ), + ), + SizedBox(height: 1.h), + Text( + subtitle, + style: TextStyle( + color: Colors.grey[500], + fontSize: isTablet + ? (AppFontSize.medium - 1.25).sp + : (AppFontSize.small - 1.25).sp, + ), + ), + ], + ), + ); + } +} diff --git a/Mobile Operasional/lib/features/stock_in/presentation/widgets/financial_section.dart b/Mobile Operasional/lib/features/stock_in/presentation/widgets/financial_section.dart new file mode 100644 index 0000000..a5cd93a --- /dev/null +++ b/Mobile Operasional/lib/features/stock_in/presentation/widgets/financial_section.dart @@ -0,0 +1,534 @@ +import 'package:flutter/material.dart'; +import 'package:niogu_app/core/constants/app_color.dart'; +import 'package:niogu_app/core/constants/app_font_size.dart'; +import 'package:niogu_app/core/utils/currency_format.dart'; +import 'package:niogu_app/core/widgets/custom_form_input.dart'; +import 'package:niogu_app/features/stock_in/domain/entities/stock_in.dart'; +import 'package:sizer/sizer.dart'; + +class FinancialSection extends StatelessWidget { + final GlobalKey formKey; + final double totalPurchase; + final bool isOtherFormVisible; + final List items; + final bool validateChange; + final double totalAmount; + final String selectedPaymentStatus; + final VoidCallback onOtherFormTap; + final TextEditingController discountController; + final TextEditingController taxController; + final TextEditingController noteController; + final TextEditingController payController; + final void Function(String)? onCalculateChange; + final double changeAmount; + final void Function(String)? discountOnChanged; + final void Function(String)? taxOnChanged; + final void Function(String?) onChanged; + const FinancialSection({ + super.key, + required this.formKey, + required this.totalPurchase, + required this.isOtherFormVisible, + required this.items, + required this.validateChange, + required this.totalAmount, + required this.selectedPaymentStatus, + required this.onOtherFormTap, + required this.discountController, + required this.taxController, + required this.noteController, + required this.payController, + required this.onCalculateChange, + required this.changeAmount, + required this.discountOnChanged, + required this.taxOnChanged, + required this.onChanged, + }); + + @override + Widget build(BuildContext context) { + final bool isTablet = 100.w >= 600; + String? discountErrorText; + + String? taxErrorText; + + String? payErrorText; + + final double payAmount = double.tryParse(payController.text.trim()) ?? 0.0; + + bool isViewChangeAmount = payAmount >= totalAmount && items.isNotEmpty; + + if (selectedPaymentStatus == 'Bayar Sebagian') { + isViewChangeAmount = totalAmount > payAmount && items.isNotEmpty; + } + + try { + if (discountController.text.isNotEmpty) { + final double discount = double.parse(discountController.text.trim()); + + if (discount <= 0) { + discountErrorText = "Diskon harus lebih dari 0"; + } + } + } catch (e) { + discountErrorText = "Nominal diskon tidak valid"; + } + + try { + if (taxController.text.isNotEmpty) { + final double tax = double.parse(taxController.text.trim()); + if (tax <= 0) { + taxErrorText = "Pajak harus lebih dari 0"; + } + } + } catch (e) { + taxErrorText = "Nominal pajak tidak valid"; + } + + try { + if (payController.text.isNotEmpty && validateChange) { + final double payAmount = double.parse(payController.text.trim()); + if (payAmount <= 0) { + payErrorText = "Nominal pembayaran harus lebih dari 0"; + } + if (selectedPaymentStatus == 'Lunas' && totalAmount > payAmount) { + payErrorText = + "Kurang ${CurrencyFormat.formatToIdr((totalAmount - payAmount), 0)}"; + } + } + } catch (e) { + payErrorText = "Nominal pembayaran tidak valid"; + } + + return Container( + padding: EdgeInsets.all(4.w), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(2.5.w), + border: Border.all(color: Colors.grey.shade200), + ), + child: Form( + key: formKey, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Subtotal", + style: TextStyle( + color: Colors.grey[700], + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + ), + ), + Text( + CurrencyFormat.formatToIdr(totalPurchase, 0), + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + ), + ), + ], + ), + Divider(height: 3.h), + + Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(2.5.w), + border: Border.all(color: Colors.grey.shade300), + ), + child: Column( + children: [ + InkWell( + onTap: onOtherFormTap, + borderRadius: BorderRadius.vertical( + top: Radius.circular(2.5.w), + bottom: Radius.circular(isOtherFormVisible ? 0 : 2.5.w), + ), + child: Padding( + padding: EdgeInsets.symmetric( + horizontal: 4.w, + vertical: 1.5.h, + ), + child: Row( + children: [ + Container( + padding: EdgeInsets.all(1.5.w), + decoration: BoxDecoration( + color: isOtherFormVisible + ? AppColor.primaryColor.withOpacity(0.1) + : Colors.grey[100], + shape: BoxShape.circle, + ), + child: Icon( + Icons.info_outlined, + color: isOtherFormVisible + ? AppColor.primaryColor + : Colors.grey, + size: 5.w, + ), + ), + SizedBox(width: 3.w), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Informasi Lain", + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + color: Colors.black87, + ), + ), + SizedBox(height: 1.h), + Text( + "Opsional (Diskon, Pajak, Catatan)", + style: TextStyle( + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + color: Colors.grey, + ), + ), + ], + ), + ), + Icon( + isOtherFormVisible + ? Icons.keyboard_arrow_up_rounded + : Icons.keyboard_arrow_down_rounded, + color: Colors.grey, + size: 6.w, + ), + ], + ), + ), + ), + if (isOtherFormVisible) ...[ + Divider(height: 1, color: Colors.grey.shade200), + Padding( + padding: EdgeInsets.all(4.w), + child: Column( + children: [ + CustomFormInput( + label: "Diskon (Rp)", + icon: Icons.discount_outlined, + controller: discountController, + onChanged: discountOnChanged, + inputType: TextInputType.number, + validator: (value) { + if (value != null && value.isNotEmpty) { + try { + final double discount = double.parse(value); + if (discount <= 0) { + return "Diskon harus lebih dari 0"; + } + } catch (e) { + return "Nominal diskon tidak valid"; + } + } + return null; + }, + errorText: discountErrorText, + ), + SizedBox(height: 1.5.h), + CustomFormInput( + label: "Pajak (Rp)", + icon: Icons.money_outlined, + controller: taxController, + onChanged: taxOnChanged, + inputType: TextInputType.number, + validator: (value) { + if (value != null && value.isNotEmpty) { + try { + final double tax = double.parse(value); + if (tax <= 0) { + return "Pajak harus lebih dari 0"; + } + } catch (e) { + return "Nominal pajak tidak valid"; + } + } + return null; + }, + errorText: taxErrorText, + ), + SizedBox(height: 1.5.h), + CustomFormInput( + label: "Catatan", + icon: Icons.note_alt_outlined, + controller: noteController, + ), + ], + ), + ), + ], + ], + ), + ), + SizedBox(height: 2.h), + + Container( + padding: EdgeInsets.all(3.w), + decoration: BoxDecoration( + color: AppColor.primaryColor.withOpacity(0.05), + borderRadius: BorderRadius.circular(2.w), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Total Pembelian", + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + ), + ), + Text( + CurrencyFormat.formatToIdr(totalAmount, 0), + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + color: AppColor.primaryColor, + ), + ), + ], + ), + ), + SizedBox(height: 2.h), + + Text( + "Status Pembelian", + style: TextStyle( + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + fontWeight: FontWeight.bold, + ), + ), + + SizedBox(height: 1.h), + + DropdownButtonFormField( + value: selectedPaymentStatus, + isExpanded: true, + items: ["Lunas", "Bayar Sebagian", "Hutang"] + .map( + (value) => DropdownMenuItem( + value: value, + child: Text( + value, + style: isTablet + ? null + : TextStyle(fontSize: AppFontSize.small.sp), + ), + ), + ) + .toList(), + onChanged: onChanged, + icon: const Icon(Icons.keyboard_arrow_down_rounded), + decoration: InputDecoration( + filled: true, + fillColor: Colors.white, + contentPadding: EdgeInsets.symmetric( + horizontal: 4.w, + vertical: 1.8.h, + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(2.5.w), + borderSide: BorderSide(color: Colors.grey.shade300), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(2.5.w), + borderSide: const BorderSide( + color: AppColor.primaryColor, + width: 1.5, + ), + ), + ), + ), + + SizedBox(height: 2.h), + + if (selectedPaymentStatus == 'Lunas' || + selectedPaymentStatus == 'Bayar Sebagian') ...[ + Text( + "Pembayaran", + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + ), + ), + SizedBox(height: 1.h), + + TextFormField( + controller: payController, + keyboardType: TextInputType.number, + style: TextStyle( + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + ), + autovalidateMode: AutovalidateMode.onUserInteraction, + onChanged: onCalculateChange, + validator: (value) { + try { + if (value == null || value.isEmpty) { + return "Masukkan nominal pembayaran"; + } + final double payAmount = double.parse(value); + if (payAmount <= 0) { + return "Nominal pembayaran harus lebih dari 0"; + } + } catch (e) { + return "Nominal pembayaran tidak valid"; + } + return null; + }, + decoration: InputDecoration( + labelText: "Bayar (Rp)", + labelStyle: TextStyle( + color: Colors.blue, + fontSize: AppFontSize.medium.sp, + ), + prefixIcon: Padding( + padding: isTablet + ? EdgeInsets.symmetric(horizontal: 3.w) + : EdgeInsets.zero, + child: Icon( + Icons.payments_outlined, + color: Colors.blue, + size: 5.w, + ), + ), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(2.5.w), + ), + errorText: payErrorText, + errorStyle: TextStyle( + color: Colors.red, + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + ), + errorBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(2.5.w), + borderSide: const BorderSide(color: Colors.redAccent), + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(2.5.w), + borderSide: const BorderSide(color: Colors.blue), + ), + contentPadding: EdgeInsets.symmetric( + vertical: isTablet ? 3.6.h : 1.8.h, + ), + ), + ), + + SizedBox(height: 2.h), + + TextField( + controller: TextEditingController( + text: isViewChangeAmount + ? CurrencyFormat.formatToIdr(changeAmount, 0) + : '', + ), + readOnly: true, + decoration: InputDecoration( + labelText: selectedPaymentStatus == 'Lunas' + ? "Kembali" + : 'Kurang Bayar', + labelStyle: TextStyle(fontSize: AppFontSize.medium.sp), + prefixIcon: Padding( + padding: isTablet + ? EdgeInsets.symmetric(horizontal: 3.w) + : EdgeInsets.zero, + child: Icon( + Icons.change_circle_outlined, + color: selectedPaymentStatus == 'Lunas' + ? Colors.green + : Colors.red, + size: 5.w, + ), + ), + filled: true, + fillColor: Colors.grey[100], + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(2.5.w), + borderSide: BorderSide.none, + ), + contentPadding: EdgeInsets.symmetric( + vertical: isTablet ? 3.6.h : 1.8.h, + ), + ), + style: TextStyle( + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + fontWeight: FontWeight.bold, + color: selectedPaymentStatus == 'Lunas' + ? Colors.green + : Colors.red, + ), + ), + ] else ...[ + TextField( + controller: TextEditingController( + text: items.isNotEmpty + ? CurrencyFormat.formatToIdr(totalAmount, 0) + : '', + ), + readOnly: true, + decoration: InputDecoration( + labelText: 'Hutang', + labelStyle: TextStyle(fontSize: AppFontSize.medium.sp), + prefixIcon: Padding( + padding: isTablet + ? EdgeInsets.symmetric(horizontal: 3.w) + : EdgeInsets.zero, + child: Icon( + Icons.change_circle_outlined, + color: Colors.red, + size: 5.w, + ), + ), + filled: true, + fillColor: Colors.grey[100], + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(2.5.w), + borderSide: BorderSide.none, + ), + contentPadding: EdgeInsets.symmetric( + vertical: isTablet ? 3.6.h : 1.8.h, + ), + ), + style: TextStyle( + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + fontWeight: FontWeight.bold, + color: Colors.red, + ), + ), + ], + ], + ), + ), + ); + } +} diff --git a/Mobile Operasional/lib/features/stock_in/presentation/widgets/header_stock_in_production_section.dart b/Mobile Operasional/lib/features/stock_in/presentation/widgets/header_stock_in_production_section.dart new file mode 100644 index 0000000..17269b8 --- /dev/null +++ b/Mobile Operasional/lib/features/stock_in/presentation/widgets/header_stock_in_production_section.dart @@ -0,0 +1,134 @@ +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; +import 'package:niogu_app/core/constants/app_color.dart'; +import 'package:niogu_app/core/constants/app_font_size.dart'; +import 'package:sizer/sizer.dart'; + +class HeaderStockInProductionSection extends StatelessWidget { + final DateTime selectedDate; + final String currentUserName; + final VoidCallback onTap; + const HeaderStockInProductionSection({ + super.key, + required this.selectedDate, + required this.currentUserName, + required this.onTap, + }); + + @override + Widget build(BuildContext context) { + final bool isTablet = 100.w >= 600; + return Container( + padding: EdgeInsets.all(4.w), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(2.5.w), + border: Border.all(color: Colors.grey.shade200), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.02), + blurRadius: 5, + offset: const Offset(0, 2), + ), + ], + ), + child: Column( + children: [ + InkWell( + onTap: onTap, + child: Row( + children: [ + Container( + padding: EdgeInsets.all(2.w), + decoration: BoxDecoration( + color: Colors.blue[50], + borderRadius: BorderRadius.circular(2.w), + ), + child: Icon( + Icons.calendar_today_rounded, + color: AppColor.primaryColor, + size: 5.w, + ), + ), + SizedBox(width: 3.w), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Tanggal Produksi", + style: TextStyle( + color: Colors.grey[600], + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + ), + ), + SizedBox(height: 0.5.h), + Text( + DateFormat('EEEE, dd MMMM yyyy').format(selectedDate), + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + ), + ), + ], + ), + ), + Icon(Icons.edit_outlined, size: 5.w, color: Colors.grey[400]), + ], + ), + ), + /** + Divider(height: 3.h, color: Colors.grey[100]), + Row( + children: [ + Container( + padding: EdgeInsets.all(2.w), + decoration: BoxDecoration( + color: Colors.orange[50], + borderRadius: BorderRadius.circular(2.w), + ), + child: Icon( + Icons.person_outline_rounded, + color: Colors.orange, + size: 5.w, + ), + ), + SizedBox(width: 3.w), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Penanggung Jawab", + style: TextStyle( + color: Colors.grey[600], + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + ), + ), + SizedBox(height: 0.5.h), + Text( + currentUserName, + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + ), + ), + ], + ), + ), + ], + ), + */ + ], + ), + ); + } +} diff --git a/Mobile Operasional/lib/features/stock_in/presentation/widgets/header_stock_in_purchase_section.dart b/Mobile Operasional/lib/features/stock_in/presentation/widgets/header_stock_in_purchase_section.dart new file mode 100644 index 0000000..010da51 --- /dev/null +++ b/Mobile Operasional/lib/features/stock_in/presentation/widgets/header_stock_in_purchase_section.dart @@ -0,0 +1,392 @@ +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'package:intl/intl.dart'; +import 'package:niogu_app/core/constants/app_color.dart'; +import 'package:niogu_app/core/constants/app_font_size.dart'; +import 'package:niogu_app/core/router/app_route.dart'; +import 'package:niogu_app/core/widgets/custom_form_input.dart'; +import 'package:niogu_app/features/stock_in/domain/entities/stock_in_purchase.dart'; +import 'package:sizer/sizer.dart'; + +class HeaderStockInPurchaseSection extends StatelessWidget { + final DateTime selectedDate; + final bool isSupplierFormVisible; + final SelectedSupplier? selectedSupplier; + final TextEditingController nameController; + final TextEditingController phoneNumberController; + final TextEditingController addressController; + final VoidCallback onTap; + final VoidCallback onSupplierFormTap; + final VoidCallback? onTapCloseSelectedSupplier; + const HeaderStockInPurchaseSection({ + super.key, + required this.selectedDate, + required this.isSupplierFormVisible, + required this.selectedSupplier, + required this.nameController, + required this.phoneNumberController, + required this.addressController, + required this.onTap, + required this.onSupplierFormTap, + required this.onTapCloseSelectedSupplier, + }); + + @override + Widget build(BuildContext context) { + final bool isTablet = 100.w >= 600; + return Container( + padding: EdgeInsets.all(4.w), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(2.5.w), + border: Border.all(color: Colors.grey.shade200), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.02), + blurRadius: 5, + offset: const Offset(0, 2), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + InkWell( + onTap: onTap, + child: Row( + children: [ + Container( + padding: EdgeInsets.all(2.w), + decoration: BoxDecoration( + color: Colors.blue[50], + borderRadius: BorderRadius.circular(2.w), + ), + child: Icon( + Icons.calendar_today_rounded, + color: AppColor.primaryColor, + size: 5.w, + ), + ), + SizedBox(width: 3.w), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Tanggal Pembelian", + style: TextStyle( + color: Colors.grey[600], + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + ), + ), + SizedBox(height: 0.5.h), + Text( + DateFormat('EEEE, dd MMMM yyyy').format(selectedDate), + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + ), + ), + ], + ), + ), + Icon(Icons.edit_outlined, size: 5.w, color: Colors.grey[400]), + ], + ), + ), + Divider(height: 3.h, color: Colors.grey[100]), + Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(2.5.w), + border: Border.all(color: Colors.grey.shade300), + ), + child: Column( + children: [ + InkWell( + onTap: onSupplierFormTap, + borderRadius: BorderRadius.vertical( + top: Radius.circular(2.5.w), + bottom: Radius.circular(isSupplierFormVisible ? 0 : 2.5.w), + ), + child: Padding( + padding: EdgeInsets.symmetric( + horizontal: 4.w, + vertical: 1.5.h, + ), + child: Row( + children: [ + Container( + padding: EdgeInsets.all(1.5.w), + decoration: BoxDecoration( + color: isSupplierFormVisible + ? AppColor.primaryColor.withOpacity(0.1) + : Colors.grey[100], + shape: BoxShape.circle, + ), + child: Icon( + Icons.person_outline_rounded, + color: isSupplierFormVisible + ? AppColor.primaryColor + : Colors.grey, + size: 5.w, + ), + ), + SizedBox(width: 3.w), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Informasi Pemasok", + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + color: Colors.black87, + ), + ), + SizedBox(height: 1.h), + Text( + "Opsional (Nama, No. Hp / WA)", + style: TextStyle( + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + color: Colors.grey, + ), + ), + ], + ), + ), + Icon( + isSupplierFormVisible + ? Icons.keyboard_arrow_up_rounded + : Icons.keyboard_arrow_down_rounded, + color: Colors.grey, + size: 6.w, + ), + ], + ), + ), + ), + if (isSupplierFormVisible) ...[ + Divider(height: 1, color: Colors.grey.shade200), + Padding( + padding: EdgeInsets.all(4.w), + child: selectedSupplier != null + ? Container( + width: double.infinity, + padding: EdgeInsets.all(4.w), + decoration: BoxDecoration( + color: AppColor.primaryColor.withOpacity(0.05), + borderRadius: BorderRadius.circular(2.5.w), + border: Border.all( + color: AppColor.primaryColor.withOpacity(0.3), + ), + ), + child: Row( + children: [ + Container( + padding: EdgeInsets.all(2.w), + decoration: BoxDecoration( + color: Colors.white, + shape: BoxShape.circle, + border: Border.all( + color: AppColor.primaryColor.withOpacity( + 0.2, + ), + ), + ), + child: Icon( + Icons.person, + color: AppColor.primaryColor, + size: 5.w, + ), + ), + SizedBox(width: 3.w), + Expanded( + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text( + selectedSupplier!.name, + style: TextStyle( + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + fontWeight: FontWeight.bold, + color: Colors.black87, + ), + ), + if (selectedSupplier! + .phoneNumber + .isNotEmpty) ...[ + SizedBox(height: 0.75.h), + Row( + children: [ + Icon( + Icons.phone_android, + size: 5.w, + color: Colors.grey[600], + ), + SizedBox(width: 1.w), + Text( + selectedSupplier!.phoneNumber, + style: TextStyle( + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + color: Colors.grey[700], + ), + ), + ], + ), + ], + + if (selectedSupplier! + .address + .isNotEmpty) ...[ + SizedBox(height: 0.75.h), + Row( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Icon( + Icons.location_on_outlined, + size: 5.w, + color: Colors.grey[600], + ), + SizedBox(width: 1.w), + Expanded( + child: Text( + selectedSupplier!.address, + style: TextStyle( + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + color: Colors.grey[700], + ), + ), + ), + ], + ), + ], + ], + ), + ), + InkWell( + onTap: onTapCloseSelectedSupplier, + child: Icon( + Icons.close, + color: Colors.grey, + size: 5.w, + ), + ), + ], + ), + ) + : Column( + children: [ + SizedBox( + width: double.infinity, + height: 5.5.h, + child: OutlinedButton.icon( + onPressed: () { + context.pushNamed( + AppRoute.alreadySupplierScreen, + ); + }, + style: OutlinedButton.styleFrom( + side: BorderSide( + color: AppColor.primaryColor, + ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + 2.5.w, + ), + ), + backgroundColor: Colors.blue[50], + ), + icon: Icon( + Icons.people_alt_rounded, + color: AppColor.primaryColor, + size: 5.w, + ), + label: Text( + "Pilih Pemasok Tersedia", + style: TextStyle( + color: AppColor.primaryColor, + fontWeight: FontWeight.bold, + fontSize: AppFontSize.medium.sp, + ), + ), + ), + ), + SizedBox(height: 2.h), + Row( + children: [ + Expanded( + child: Divider(color: Colors.grey.shade300), + ), + Padding( + padding: EdgeInsets.symmetric( + horizontal: 3.w, + ), + child: Text( + "Atau Input Manual", + style: TextStyle( + fontSize: isTablet + ? (AppFontSize.medium - 2).sp + : (AppFontSize.small - 2).sp, + fontWeight: FontWeight.bold, + color: Colors.grey[400], + ), + ), + ), + Expanded( + child: Divider(color: Colors.grey.shade300), + ), + ], + ), + SizedBox(height: 2.h), + CustomFormInput( + label: "Nama Pemasok", + icon: Icons.person, + controller: nameController, + ), + SizedBox(height: 1.5.h), + CustomFormInput( + label: "No. Hp / WA", + hint: "Opsional", + icon: Icons.phone_android, + controller: phoneNumberController, + inputType: TextInputType.phone, + ), + /** + SizedBox(height: 1.5.h), + CustomFormInput( + label: "Alamat", + hint: "Opsional", + icon: Icons.location_on_outlined, + controller: addressController, + ), + */ + ], + ), + ), + ], + ], + ), + ), + ], + ), + ); + } +} diff --git a/Mobile Operasional/lib/features/stock_in/presentation/widgets/header_stock_in_transfer_section.dart b/Mobile Operasional/lib/features/stock_in/presentation/widgets/header_stock_in_transfer_section.dart new file mode 100644 index 0000000..de65330 --- /dev/null +++ b/Mobile Operasional/lib/features/stock_in/presentation/widgets/header_stock_in_transfer_section.dart @@ -0,0 +1,151 @@ +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; +import 'package:niogu_app/core/constants/app_color.dart'; +import 'package:niogu_app/core/constants/app_font_size.dart'; +import 'package:niogu_app/core/widgets/custom_text_form_field.dart'; +import 'package:niogu_app/features/stock_in/domain/entities/stock_in_transfer.dart'; +import 'package:sizer/sizer.dart'; + +class HeaderStockInTransferSection extends StatelessWidget { + final DateTime selectedDate; + final VoidCallback onTap; + final String? selectedSourceOutlet; + final List sourceOutlets; + final void Function(String?) onChanged; + final TextEditingController refNumberController; + + const HeaderStockInTransferSection({ + super.key, + required this.selectedDate, + required this.onTap, + required this.selectedSourceOutlet, + required this.sourceOutlets, + required this.onChanged, + required this.refNumberController, + }); + + @override + Widget build(BuildContext context) { + final bool isTablet = 100.w >= 600; + return Container( + padding: EdgeInsets.all(4.w), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(2.5.w), + border: Border.all(color: Colors.grey.shade200), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.02), + blurRadius: 5, + offset: const Offset(0, 2), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + InkWell( + onTap: onTap, + child: Row( + children: [ + Container( + padding: EdgeInsets.all(2.w), + decoration: BoxDecoration( + color: Colors.blue[50], + borderRadius: BorderRadius.circular(2.w), + ), + child: Icon( + Icons.calendar_today_rounded, + color: AppColor.primaryColor, + size: 5.w, + ), + ), + SizedBox(width: 3.w), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Tanggal Terima", + style: TextStyle( + color: Colors.grey[600], + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + ), + ), + SizedBox(height: 0.5.h), + Text( + DateFormat('EEEE, dd MMMM yyyy').format(selectedDate), + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + ), + ), + ], + ), + ), + Icon(Icons.edit_outlined, size: 5.w, color: Colors.grey[400]), + ], + ), + ), + Divider(height: 3.h, color: Colors.grey[100]), + Text( + "Terima Dari (Asal)", + style: TextStyle( + fontSize: isTablet ? AppFontSize.medium.sp : AppFontSize.small.sp, + fontWeight: FontWeight.bold, + color: Colors.grey[700], + ), + ), + SizedBox(height: 1.h), + DropdownButtonFormField( + value: selectedSourceOutlet, + decoration: InputDecoration( + isDense: true, + contentPadding: EdgeInsets.symmetric( + horizontal: 4.w, + vertical: 1.5.h, + ), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(2.5.w), + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(2.5.w), + borderSide: BorderSide(color: Colors.grey.shade300), + ), + ), + icon: Icon( + Icons.keyboard_arrow_down_rounded, + color: Colors.grey[600], + ), + items: sourceOutlets.map((outlet) { + return DropdownMenuItem( + value: outlet.id, + child: Text( + outlet.name, + style: isTablet + ? null + : TextStyle(fontSize: AppFontSize.small.sp), + ), + ); + }).toList(), + onChanged: onChanged, + ), + + SizedBox(height: 2.h), + + CustomTextFormField( + label: "No. Surat Jalan / Referensi", + labelColor: Colors.grey[700], + hint: "(Opsional)", + controller: refNumberController, + prefixIcon: Icons.receipt_long_outlined, + ), + ], + ), + ); + } +} diff --git a/Mobile Operasional/lib/features/stock_in/presentation/widgets/item_input_card.dart b/Mobile Operasional/lib/features/stock_in/presentation/widgets/item_input_card.dart new file mode 100644 index 0000000..088caa3 --- /dev/null +++ b/Mobile Operasional/lib/features/stock_in/presentation/widgets/item_input_card.dart @@ -0,0 +1,246 @@ +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:niogu_app/core/constants/app_color.dart'; +import 'package:niogu_app/core/constants/app_font_size.dart'; +import 'package:niogu_app/core/utils/extension_format.dart'; +import 'package:niogu_app/core/enums/item_type.dart'; +import 'package:niogu_app/features/stock_in/presentation/widgets/counter_button.dart'; +import 'package:niogu_app/features/stock_in/domain/entities/stock_in.dart'; +import 'package:sizer/sizer.dart'; + +class ItemInputCard extends StatelessWidget { + final SelectedItem item; + final String label; + final VoidCallback? onIncrementTap; + final VoidCallback? onDecrementTap; + final VoidCallback onEditPressed; + final VoidCallback onDeletePressed; + const ItemInputCard({ + super.key, + required this.item, + required this.label, + required this.onIncrementTap, + required this.onDecrementTap, + required this.onEditPressed, + required this.onDeletePressed, + }); + + @override + Widget build(BuildContext context) { + final bool isTablet = 100.w > 600; + + final String path = + item.localImagePath != null && item.localImagePath!.isNotEmpty + ? item.localImagePath! + : 'image not found'; + + final File imageFile = File(path); + + final bool imageFileExists = imageFile.existsSync(); + + return Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(2.5.w), + border: Border.all(color: Colors.grey.shade200), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.02), + blurRadius: 5, + offset: const Offset(0, 2), + ), + ], + ), + child: Column( + children: [ + Padding( + padding: EdgeInsets.all(3.w), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + height: 15.w, + width: 15.w, + decoration: BoxDecoration( + border: Border.all(color: Colors.grey.shade200, width: 1.5), + borderRadius: BorderRadius.circular(2.5.w), + ), + child: imageFileExists + ? ClipRRect( + borderRadius: BorderRadius.circular(2.5.w), + child: Image.file( + imageFile, + fit: BoxFit.cover, + width: double.infinity, + height: double.infinity, + errorBuilder: (context, error, stackTrace) { + return const Icon( + Icons.error, + color: Colors.grey, + ); + }, + ), + ) + : Icon( + item.itemType == ItemType.product_variant + ? Icons.image_outlined + : Icons.inventory_2_rounded, + size: 8.w, + color: item.itemType == ItemType.product_variant + ? Colors.grey + : Colors.grey[400], + ), + ), + SizedBox(width: 3.w), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + item.name, + maxLines: 2, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + ), + ), + if (item.variantName != null && + item.variantName!.isNotEmpty) ...[ + SizedBox(height: 0.5.h), + Text( + "(${item.variantName})", + style: TextStyle( + color: Colors.grey[600], + fontWeight: FontWeight.bold, + fontSize: isTablet + ? (AppFontSize.medium - 1.25).sp + : (AppFontSize.small - 1.25).sp, + ), + ), + ], + SizedBox(height: 0.5.h), + Text( + "Sisa Stok: ${item.remainingStock.toStringWithoutTrailingZero()} ${item.unit}", + style: TextStyle( + color: Colors.grey[600], + fontSize: isTablet + ? (AppFontSize.medium - 1.25).sp + : (AppFontSize.small - 1.25).sp, + ), + ), + ], + ), + ), + Container( + margin: EdgeInsets.only(top: 0.5.h), + child: IconButton( + onPressed: onEditPressed, + icon: Icon( + Icons.edit_note_rounded, + color: Colors.grey, + size: 5.w, + ), + padding: EdgeInsets.zero, + constraints: const BoxConstraints(), + style: IconButton.styleFrom( + tapTargetSize: MaterialTapTargetSize.shrinkWrap, + ), + ), + ), + + SizedBox(width: 2.w), + + Container( + margin: EdgeInsets.only(top: 0.5.h), + child: IconButton( + onPressed: onDeletePressed, + icon: Icon( + Icons.delete_outline, + color: Colors.red, + size: 5.w, + ), + padding: EdgeInsets.zero, + constraints: const BoxConstraints(), + style: IconButton.styleFrom( + tapTargetSize: MaterialTapTargetSize.shrinkWrap, + ), + ), + ), + ], + ), + ), + Divider(height: 1, color: Colors.grey[100]), + Padding( + padding: EdgeInsets.symmetric(horizontal: 3.w, vertical: 2.h), + child: Row( + children: [ + Text( + label, + style: TextStyle( + fontSize: isTablet + ? (AppFontSize.medium - 1.25).sp + : (AppFontSize.small - 1.25).sp, + fontWeight: FontWeight.w500, + ), + ), + SizedBox(width: 3.w), + Expanded( + child: Container( + height: 5.h, + decoration: BoxDecoration( + color: Colors.grey[50], + borderRadius: BorderRadius.circular(2.w), + border: Border.all(color: Colors.grey.shade300), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + CounterButton( + icon: Icons.remove, + onTap: onDecrementTap, + ), + Expanded( + child: Container( + margin: EdgeInsets.only(top: 0.5.h), + padding: EdgeInsets.symmetric(horizontal: 2.w), + child: Center( + child: Text( + item.quantity.toStringWithoutTrailingZero(), + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: isTablet + ? (AppFontSize.medium - 1.25).sp + : (AppFontSize.small - 1.25).sp, + color: AppColor.primaryColor, + ), + ), + ), + ), + ), + CounterButton(icon: Icons.add, onTap: onIncrementTap), + ], + ), + ), + ), + SizedBox(width: 2.w), + Text( + item.unit, + style: TextStyle( + color: Colors.grey[600], + fontSize: isTablet + ? (AppFontSize.medium - 1.25).sp + : (AppFontSize.small - 1.25).sp, + ), + ), + ], + ), + ), + ], + ), + ); + } +} diff --git a/Mobile Operasional/lib/features/stock_in/presentation/widgets/item_quantity_amount.dart b/Mobile Operasional/lib/features/stock_in/presentation/widgets/item_quantity_amount.dart new file mode 100644 index 0000000..b191037 --- /dev/null +++ b/Mobile Operasional/lib/features/stock_in/presentation/widgets/item_quantity_amount.dart @@ -0,0 +1,167 @@ +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'package:niogu_app/core/constants/app_font_size.dart'; +import 'package:niogu_app/core/enums/item_type.dart'; +import 'package:niogu_app/core/widgets/custom_button.dart'; +import 'package:niogu_app/core/widgets/custom_text_form_field.dart'; +import 'package:sizer/sizer.dart'; + +class ItemQuantityAmount extends StatefulWidget { + final dynamic itemType; + final String name; + final String? variantName; + final String remainingStock; + final String unit; + final String initialQuantity; + final String label; + final Function(String) onPressed; + + const ItemQuantityAmount({ + super.key, + required this.itemType, + required this.name, + this.variantName, + required this.remainingStock, + required this.unit, + required this.initialQuantity, + required this.label, + required this.onPressed, + }); + + @override + State createState() => _ItemQuantityAmountState(); +} + +class _ItemQuantityAmountState extends State { + late GlobalKey _formKey; + late TextEditingController _controller; + + @override + void initState() { + super.initState(); + _formKey = GlobalKey(); + _controller = TextEditingController(text: widget.initialQuantity); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final bool isTablet = 100.w >= 600; + + final double maxWidth = 100.w >= 1280 + ? 1200 + : isTablet + ? 800 + : 400; + + return Dialog( + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(4.w)), + backgroundColor: Colors.white, + child: Container( + padding: EdgeInsets.all(5.w), + constraints: BoxConstraints(maxHeight: 85.h, maxWidth: maxWidth), + child: Form( + key: _formKey, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + "Masukkan Kuantitas", + style: TextStyle( + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + fontWeight: FontWeight.bold, + color: Colors.black87, + ), + ), + SizedBox(height: 2.h), + + Flexible( + child: SingleChildScrollView( + physics: const ClampingScrollPhysics(), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + CustomTextFormField( + label: "Nama Produk", + controller: TextEditingController(text: widget.name), + readOnly: true, + ), + + if (widget.itemType == ItemType.product_variant && + widget.variantName != null && + widget.variantName!.isNotEmpty) ...[ + SizedBox(height: 1.5.h), + + CustomTextFormField( + label: "Varian Produk", + controller: TextEditingController( + text: widget.variantName, + ), + readOnly: true, + ), + ], + + SizedBox(height: 1.5.h), + + CustomTextFormField( + label: "Sisa Stok", + controller: TextEditingController( + text: "${widget.remainingStock} ${widget.unit}", + ), + readOnly: true, + ), + + SizedBox(height: 2.h), + + CustomTextFormField( + label: widget.label, + hint: "Gunakan titik (.), misal 0.5", + keyboardType: const TextInputType.numberWithOptions( + decimal: true, + ), + autoFocus: true, + controller: _controller, + validator: (value) { + if (value == null || value.isEmpty) + return "${widget.label} belum diisi"; + if (double.tryParse(value) == null) + return "${widget.label} tidak valid"; + if (double.parse(value) <= 0) + return "${widget.label} harus lebih dari 0"; + return null; + }, + ), + + SizedBox(height: 1.h), + ], + ), + ), + ), + + SizedBox(height: 2.h), + + CustomButton( + title: "Ubah", + onPressed: () { + if (!_formKey.currentState!.validate()) return; + + widget.onPressed(_controller.text.trim()); + + context.pop(); + }, + ), + SizedBox(height: 1.h), + ], + ), + ), + ), + ); + } +} diff --git a/Mobile Operasional/lib/features/stock_in/presentation/widgets/note_section.dart b/Mobile Operasional/lib/features/stock_in/presentation/widgets/note_section.dart new file mode 100644 index 0000000..dab0f36 --- /dev/null +++ b/Mobile Operasional/lib/features/stock_in/presentation/widgets/note_section.dart @@ -0,0 +1,47 @@ +import 'package:flutter/material.dart'; +import 'package:niogu_app/core/constants/app_font_size.dart'; +import 'package:sizer/sizer.dart'; + +class NoteSection extends StatelessWidget { + final bool isTablet; + final TextEditingController controller; + final String hint; + const NoteSection({ + super.key, + required this.isTablet, + required this.controller, + required this.hint, + }); + + @override + Widget build(BuildContext context) { + return TextField( + maxLines: isTablet ? 6 : 3, + controller: controller, + style: TextStyle( + fontSize: isTablet + ? (AppFontSize.medium - 1.25).sp + : (AppFontSize.small - 1.25).sp, + ), + decoration: InputDecoration( + hintText: hint, + hintStyle: TextStyle( + color: Colors.grey[400], + fontSize: isTablet + ? (AppFontSize.medium - 1.25).sp + : (AppFontSize.small - 1.25).sp, + ), + filled: true, + fillColor: Colors.white, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(2.5.w), + borderSide: BorderSide(color: Colors.grey.shade200), + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(2.5.w), + borderSide: BorderSide(color: Colors.grey.shade200), + ), + ), + ); + } +} diff --git a/Mobile Operasional/lib/features/stock_in/presentation/widgets/purchase_item_card.dart b/Mobile Operasional/lib/features/stock_in/presentation/widgets/purchase_item_card.dart new file mode 100644 index 0000000..6029c83 --- /dev/null +++ b/Mobile Operasional/lib/features/stock_in/presentation/widgets/purchase_item_card.dart @@ -0,0 +1,386 @@ +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:niogu_app/core/constants/app_color.dart'; +import 'package:niogu_app/core/constants/app_font_size.dart'; +import 'package:niogu_app/core/utils/currency_format.dart'; +import 'package:niogu_app/core/utils/extension_format.dart'; +import 'package:niogu_app/core/enums/item_type.dart'; +import 'package:niogu_app/features/stock_in/presentation/widgets/counter_button.dart'; +import 'package:niogu_app/features/stock_in/domain/entities/stock_in.dart'; +import 'package:sizer/sizer.dart'; + +class PurchaseItemCard extends StatelessWidget { + final SelectedItem item; + final TextEditingController purchasePriceController; + final void Function(String)? onChanged; + final VoidCallback? onDecrementTap; + final VoidCallback? onIncrementTap; + final VoidCallback onEditPressed; + final VoidCallback onDeletePressed; + const PurchaseItemCard({ + super.key, + required this.item, + required this.purchasePriceController, + required this.onChanged, + required this.onDecrementTap, + required this.onIncrementTap, + required this.onEditPressed, + required this.onDeletePressed, + }); + + @override + Widget build(BuildContext context) { + final bool isTablet = 100.w >= 600; + + final double sellingPrice = item.sellingPrice; + + final double purchasePrice = + double.tryParse(purchasePriceController.text.trim()) ?? 0.0; + + final validatePurchasePrice = switch (item.itemType) { + ItemType.product_variant => + double.tryParse(purchasePriceController.text.trim()) != null && + purchasePrice > 0 && + sellingPrice > purchasePrice, + ItemType.raw_material => + double.tryParse(purchasePriceController.text.trim()) != null && + purchasePrice > 0, + }; + + final String path = + item.localImagePath != null && item.localImagePath!.isNotEmpty + ? item.localImagePath! + : 'image not found'; + + final File imageFile = File(path); + + final bool imageFileExists = imageFile.existsSync(); + + return Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(2.5.w), + border: Border.all(color: Colors.grey.shade200), + boxShadow: [ + BoxShadow(color: Colors.black.withOpacity(0.02), blurRadius: 5), + ], + ), + child: Column( + children: [ + Padding( + padding: EdgeInsets.all(3.w), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + width: 18.w, + height: 18.w, + decoration: BoxDecoration( + border: Border.all(color: Colors.grey.shade200, width: 1.5), + borderRadius: BorderRadius.circular(2.5.w), + ), + child: imageFileExists + ? ClipRRect( + borderRadius: BorderRadius.circular(2.5.w), + child: Image.file( + imageFile, + fit: BoxFit.cover, + width: double.infinity, + height: double.infinity, + errorBuilder: (context, error, stackTrace) { + return const Icon( + Icons.error, + color: Colors.grey, + ); + }, + ), + ) + : Icon( + item.itemType == ItemType.product_variant + ? Icons.image_outlined + : Icons.inventory_2_rounded, + size: 8.w, + color: item.itemType == ItemType.product_variant + ? Colors.grey + : Colors.grey[400], + ), + ), + SizedBox(width: 3.w), + Expanded( + flex: 3, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + item.name, + maxLines: 2, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: isTablet + ? (AppFontSize.medium - 1.25).sp + : (AppFontSize.small - 1.25).sp, + ), + ), + if (item.variantName != null && + item.variantName!.isNotEmpty) ...[ + SizedBox(height: 0.5.h), + Text( + "(${item.variantName})", + style: TextStyle( + color: Colors.grey[600], + fontWeight: FontWeight.bold, + fontSize: isTablet + ? (AppFontSize.medium - 1.25).sp + : (AppFontSize.small - 1.25).sp, + ), + ), + ], + SizedBox(height: 0.5.h), + Text( + "Sisa Stok: ${item.remainingStock.toStringWithoutTrailingZero()} ${item.unit}", + style: TextStyle( + color: Colors.grey[600], + fontSize: isTablet + ? (AppFontSize.medium - 1.25).sp + : (AppFontSize.small - 1.25).sp, + ), + ), + + if (item.itemType == ItemType.product_variant) ...[ + SizedBox(height: 0.5.h), + Text( + "Harga Jual: ${CurrencyFormat.formatToIdr(item.sellingPrice, 0)}", + style: TextStyle( + color: Colors.grey[600], + fontSize: isTablet + ? (AppFontSize.medium - 1.25).sp + : (AppFontSize.small - 1.25).sp, + ), + ), + ], + ], + ), + ), + Container( + margin: EdgeInsets.only(top: 0.5.h), + child: IconButton( + onPressed: onEditPressed, + icon: Icon( + Icons.edit_note_rounded, + color: Colors.grey, + size: 5.w, + ), + padding: EdgeInsets.zero, + constraints: const BoxConstraints(), + style: IconButton.styleFrom( + tapTargetSize: MaterialTapTargetSize.shrinkWrap, + ), + ), + ), + + SizedBox(width: 2.w), + + Container( + margin: EdgeInsets.only(top: 0.5.h), + child: IconButton( + onPressed: onDeletePressed, + icon: Icon( + Icons.delete_outline, + color: Colors.red, + size: 5.w, + ), + padding: EdgeInsets.zero, + constraints: const BoxConstraints(), + style: IconButton.styleFrom( + tapTargetSize: MaterialTapTargetSize.shrinkWrap, + ), + ), + ), + ], + ), + ), + Divider(height: 1, color: Colors.grey[100]), + Padding( + padding: EdgeInsets.all(3.w), + child: Row( + children: [ + Expanded( + flex: 3, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Jumlah Pembelian", + style: TextStyle( + fontSize: isTablet + ? (AppFontSize.medium - 1.25).sp + : (AppFontSize.small - 1.25).sp, + color: Colors.grey, + ), + ), + SizedBox(height: 0.5.h), + Container( + height: 5.h, + decoration: BoxDecoration( + color: Colors.grey[50], + borderRadius: BorderRadius.circular(2.w), + border: Border.all(color: Colors.grey.shade300), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + CounterButton( + icon: Icons.remove, + onTap: item.quantity > 1 ? onDecrementTap : null, + ), + Row( + children: [ + Text( + item.quantity.toStringWithoutTrailingZero(), + style: TextStyle( + fontSize: isTablet + ? (AppFontSize.medium - 1.25).sp + : (AppFontSize.small - 1.25).sp, + fontWeight: FontWeight.bold, + ), + ), + SizedBox(width: 1.w), + Text( + item.unit, + style: TextStyle( + fontSize: isTablet + ? (AppFontSize.medium - 1.25).sp + : (AppFontSize.small - 1.25).sp, + color: Colors.grey, + ), + ), + ], + ), + CounterButton( + icon: Icons.add, + onTap: onIncrementTap, + ), + ], + ), + ), + ], + ), + ), + + SizedBox(width: 3.w), + + Expanded( + flex: 3, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Harga Beli / Satuan", + style: TextStyle( + fontSize: isTablet + ? (AppFontSize.medium - 1.25).sp + : (AppFontSize.small - 1.25).sp, + color: Colors.grey, + ), + ), + SizedBox(height: 0.5.h), + + SizedBox( + height: 5.h, + child: TextFormField( + keyboardType: TextInputType.number, + controller: purchasePriceController, + onChanged: onChanged, + style: TextStyle( + fontSize: isTablet + ? (AppFontSize.medium - 1.25).sp + : (AppFontSize.small - 1.25).sp, + fontWeight: FontWeight.bold, + color: Colors.black87, + ), + decoration: InputDecoration( + prefixText: "Rp ", + prefixStyle: TextStyle( + color: Colors.grey[600], + fontSize: isTablet + ? (AppFontSize.medium - 1.25).sp + : (AppFontSize.small - 1.25).sp, + ), + contentPadding: EdgeInsets.symmetric( + horizontal: 3.w, + vertical: 0, + ), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(2.w), + borderSide: BorderSide( + color: Colors.grey.shade300, + ), + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(2.w), + borderSide: BorderSide( + color: validatePurchasePrice + ? Colors.grey.shade300 + : Colors.red, + ), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(2.w), + borderSide: BorderSide( + color: validatePurchasePrice + ? AppColor.primaryColor + : Colors.red, + ), + ), + filled: true, + fillColor: Colors.white, + ), + ), + ), + ], + ), + ), + ], + ), + ), + Container( + width: double.infinity, + padding: EdgeInsets.symmetric(horizontal: 3.w, vertical: 1.h), + decoration: BoxDecoration( + color: Colors.grey[50], + borderRadius: BorderRadius.vertical( + bottom: Radius.circular(2.5.w), + ), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Subtotal", + style: TextStyle( + fontSize: isTablet + ? (AppFontSize.medium - 1.25).sp + : (AppFontSize.small - 1.25).sp, + color: Colors.grey[700], + ), + ), + Text( + CurrencyFormat.formatToIdr(purchasePrice * item.quantity, 0), + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: isTablet + ? (AppFontSize.medium - 1.25).sp + : (AppFontSize.small - 1.25).sp, + color: AppColor.primaryColor, + ), + ), + ], + ), + ), + ], + ), + ); + } +} diff --git a/Mobile Operasional/lib/features/stock_in/presentation/widgets/save_button.dart b/Mobile Operasional/lib/features/stock_in/presentation/widgets/save_button.dart new file mode 100644 index 0000000..b856d7d --- /dev/null +++ b/Mobile Operasional/lib/features/stock_in/presentation/widgets/save_button.dart @@ -0,0 +1,48 @@ +import 'package:flutter/material.dart'; +import 'package:niogu_app/core/constants/app_color.dart'; +import 'package:niogu_app/core/constants/app_font_size.dart'; +import 'package:sizer/sizer.dart'; + +class SaveButton extends StatelessWidget { + final VoidCallback? onPressed; + const SaveButton({super.key, required this.onPressed}); + + @override + Widget build(BuildContext context) { + return Container( + padding: EdgeInsets.all(5.w), + decoration: BoxDecoration( + color: Colors.white, + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.05), + blurRadius: 10, + offset: const Offset(0, -5), + ), + ], + ), + child: SizedBox( + width: double.infinity, + height: 6.5.h, + child: ElevatedButton( + onPressed: onPressed, + style: ElevatedButton.styleFrom( + backgroundColor: AppColor.primaryColor, + disabledBackgroundColor: Colors.grey[300], + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(2.5.w), + ), + ), + child: Text( + "Simpan & Tambah Stok", + style: TextStyle( + color: Colors.white, + fontSize: AppFontSize.medium.sp, + fontWeight: FontWeight.bold, + ), + ), + ), + ), + ); + } +} diff --git a/Mobile Operasional/lib/features/stock_in/presentation/widgets/select_item_shimmer.dart b/Mobile Operasional/lib/features/stock_in/presentation/widgets/select_item_shimmer.dart new file mode 100644 index 0000000..60824d5 --- /dev/null +++ b/Mobile Operasional/lib/features/stock_in/presentation/widgets/select_item_shimmer.dart @@ -0,0 +1,79 @@ +import 'package:flutter/material.dart'; +import 'package:shimmer/shimmer.dart'; +import 'package:sizer/sizer.dart'; + +class SelectItemShimmer extends StatelessWidget { + const SelectItemShimmer({super.key}); + + @override + Widget build(BuildContext context) { + return Shimmer.fromColors( + baseColor: Colors.grey.shade300, + highlightColor: Colors.grey.shade100, + enabled: true, + child: ListView.builder( + padding: EdgeInsets.fromLTRB(4.w, 1.h, 4.w, 5.h), + itemCount: 8, + itemBuilder: (_, __) => Container( + margin: EdgeInsets.only(bottom: 1.5.h), + padding: EdgeInsets.all(3.w), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(2.5.w), + border: Border.all(color: Colors.grey.shade200), + ), + child: Row( + children: [ + Container( + width: 6.w, + height: 6.w, + decoration: const BoxDecoration( + color: Colors.white, + shape: BoxShape.circle, + ), + ), + + SizedBox(width: 4.w), + + Container( + width: 10.w, + height: 10.w, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(2.w), + ), + ), + + SizedBox(width: 3.w), + + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + width: 50.w, + height: 1.5.h, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(1.w), + ), + ), + SizedBox(height: 0.8.h), + Container( + width: 25.w, + height: 1.2.h, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(1.w), + ), + ), + ], + ), + ), + ], + ), + ), + ), + ); + } +} \ No newline at end of file diff --git a/Mobile Operasional/lib/features/stock_in/presentation/widgets/summary_tag.dart b/Mobile Operasional/lib/features/stock_in/presentation/widgets/summary_tag.dart new file mode 100644 index 0000000..bf88bbf --- /dev/null +++ b/Mobile Operasional/lib/features/stock_in/presentation/widgets/summary_tag.dart @@ -0,0 +1,33 @@ +import 'package:flutter/material.dart'; +import 'package:niogu_app/core/constants/app_font_size.dart'; +import 'package:sizer/sizer.dart'; + +class SummaryTag extends StatelessWidget { + final String text; + final MaterialColor color; + + const SummaryTag({super.key, required this.text, required this.color}); + + @override + Widget build(BuildContext context) { + final bool isTablet = 100.w > 600; + return Container( + padding: EdgeInsets.symmetric(horizontal: 3.w, vertical: 0.5.h), + decoration: BoxDecoration( + color: color.shade50, + borderRadius: BorderRadius.circular(4.5.w), + border: Border.all(color: color.shade200), + ), + child: Text( + text, + style: TextStyle( + color: color.shade700, + fontSize: isTablet + ? (AppFontSize.medium - 1.25).sp + : (AppFontSize.small - 1.25).sp, + fontWeight: FontWeight.bold, + ), + ), + ); + } +} \ No newline at end of file diff --git a/Mobile Operasional/lib/features/supplier/data/repositories/supplier_repository_impl.dart b/Mobile Operasional/lib/features/supplier/data/repositories/supplier_repository_impl.dart new file mode 100644 index 0000000..0664847 --- /dev/null +++ b/Mobile Operasional/lib/features/supplier/data/repositories/supplier_repository_impl.dart @@ -0,0 +1,37 @@ +import 'package:drift/drift.dart'; +import 'package:niogu_app/core/database/app_database.dart'; +import 'package:niogu_app/features/supplier/domain/entities/supplier.dart'; +import 'package:niogu_app/features/supplier/domain/repositories/i_supplier_repository.dart'; + +class SupplierRepositoryImpl implements ISupplierRepository { + final AppDatabase _appDatabase; + + SupplierRepositoryImpl(this._appDatabase); + + @override + Stream> watchSuppliers() { + return _appDatabase.supplierDao.watchAllSuppliers(); + } + + @override + Future fetchSupplierById(String id) async { + return await _appDatabase.supplierDao.getSupplierById(id); + } + + @override + Future saveSupplier(UpsertSupplier supplier) async { + return await _appDatabase.supplierDao.saveSupplier( + SuppliersCompanion( + localId: Value(supplier.id), + name: Value(supplier.name), + email: Value(supplier.email), + phoneNumber: Value(supplier.phoneNumber), + ), + ); + } + + @override + Future deleteSupplier(String id) async { + return await _appDatabase.supplierDao.deleteSupplier(id); + } +} diff --git a/Mobile Operasional/lib/features/supplier/domain/entities/supplier.dart b/Mobile Operasional/lib/features/supplier/domain/entities/supplier.dart new file mode 100644 index 0000000..a133843 --- /dev/null +++ b/Mobile Operasional/lib/features/supplier/domain/entities/supplier.dart @@ -0,0 +1,55 @@ +import 'package:niogu_app/core/enums/payment_status.dart'; +import 'package:uuid/uuid.dart'; + +enum SupplierEmpty { loading, empty_database, empty_search_result, has_data } + +class DisplaySuppliers { + final String id; + final String name; + + const DisplaySuppliers({required this.id, required this.name}); +} + +class UpsertSupplier { + final String id; + final String name; + final String? email; + final String? phoneNumber; + + UpsertSupplier({ + String? id, + required this.name, + required this.email, + required this.phoneNumber, + }) : id = id ?? const Uuid().v7().toString(); +} + +class SupplierDetail { + final String id; + final String name; + final String email; + final String phoneNumber; + final List activities; + + const SupplierDetail({ + required this.id, + required this.name, + required this.email, + required this.phoneNumber, + required this.activities, + }); +} + +class SupplierActivities { + final String id; + final String purchaseNumber; + final PaymentStatus paymentStatus; + final DateTime purchaseDate; + + const SupplierActivities({ + required this.id, + required this.purchaseNumber, + required this.paymentStatus, + required this.purchaseDate, + }); +} diff --git a/Mobile Operasional/lib/features/supplier/domain/repositories/i_supplier_repository.dart b/Mobile Operasional/lib/features/supplier/domain/repositories/i_supplier_repository.dart new file mode 100644 index 0000000..0fa7bc2 --- /dev/null +++ b/Mobile Operasional/lib/features/supplier/domain/repositories/i_supplier_repository.dart @@ -0,0 +1,11 @@ +import 'package:niogu_app/features/supplier/domain/entities/supplier.dart'; + +abstract class ISupplierRepository { + Stream> watchSuppliers(); + + Future fetchSupplierById(String id); + + Future saveSupplier(UpsertSupplier supplier); + + Future deleteSupplier(String id); +} \ No newline at end of file diff --git a/Mobile Operasional/lib/features/supplier/presentation/controllers/supplier_controller.dart b/Mobile Operasional/lib/features/supplier/presentation/controllers/supplier_controller.dart new file mode 100644 index 0000000..5591c85 --- /dev/null +++ b/Mobile Operasional/lib/features/supplier/presentation/controllers/supplier_controller.dart @@ -0,0 +1,39 @@ +import 'dart:async'; + +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:niogu_app/features/supplier/domain/entities/supplier.dart'; +import 'package:niogu_app/features/supplier/domain/repositories/i_supplier_repository.dart'; +import 'package:niogu_app/features/supplier/presentation/providers/supplier_provider.dart'; + +class SupplierController extends AutoDisposeAsyncNotifier { + late final ISupplierRepository _supplierRepository; + @override + FutureOr build() { + _supplierRepository = ref.read(supplierRepositoryProvider); + } + + Future saveSupplier(UpsertSupplier supplier) async { + state = const AsyncValue.loading(); + + final result = await AsyncValue.guard(() async { + await _supplierRepository.saveSupplier(supplier); + }); + + state = result; + + if (result is AsyncError) throw result.error!; + } + + Future deleteSupplier(String id) async { + state = const AsyncValue.loading(); + + final result = await AsyncValue.guard(() async { + await _supplierRepository.deleteSupplier(id); + }); + + state = result; + + if (result is AsyncError) throw result.error!; + } + +} \ No newline at end of file diff --git a/Mobile Operasional/lib/features/supplier/presentation/providers/supplier_provider.dart b/Mobile Operasional/lib/features/supplier/presentation/providers/supplier_provider.dart new file mode 100644 index 0000000..f144be3 --- /dev/null +++ b/Mobile Operasional/lib/features/supplier/presentation/providers/supplier_provider.dart @@ -0,0 +1,77 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:niogu_app/core/providers/app_provider.dart'; +import 'package:niogu_app/features/supplier/data/repositories/supplier_repository_impl.dart'; +import 'package:niogu_app/features/supplier/domain/entities/supplier.dart'; +import 'package:niogu_app/features/supplier/domain/repositories/i_supplier_repository.dart'; +import 'package:niogu_app/features/supplier/presentation/controllers/supplier_controller.dart'; + +final supplierRepositoryProvider = Provider.autoDispose(( + ref, +) { + final appDatabase = ref.watch(appDatabaseProvider); + + return SupplierRepositoryImpl(appDatabase); +}); + +final supplierStreamProvider = + StreamProvider.autoDispose>((ref) { + final supplierRepository = ref.watch(supplierRepositoryProvider); + + return supplierRepository.watchSuppliers(); + }); + +final supplierSearchProvider = StateProvider.autoDispose((ref) => ''); + +final filteredSupplierProvider = + Provider.autoDispose>>((ref) { + final supplierStreamAsync = ref.watch(supplierStreamProvider); + final supplierSearchAsync = ref.watch(supplierSearchProvider); + return supplierStreamAsync.when( + data: (suppliers) { + if (supplierSearchAsync.trim().isEmpty) { + return AsyncValue.data(suppliers); + } + + final filteredSuppliers = suppliers.where((supplier) { + return supplier.name.toLowerCase().contains( + supplierSearchAsync.toLowerCase(), + ); + }).toList(); + + return AsyncValue.data(filteredSuppliers); + }, + error: (error, stackTrace) { + return AsyncValue.error(error, stackTrace); + }, + loading: () => const AsyncValue.loading(), + ); + }); + +final supplierEmptyProvider = Provider.autoDispose((ref) { + final supplierStreamAsync = ref.watch(supplierStreamProvider); + final filteredSupplierAsync = ref.watch(filteredSupplierProvider); + final supplierSearchAsync = ref.watch(supplierSearchProvider); + + if (supplierStreamAsync.isLoading) { + return SupplierEmpty.loading; + } + + final allSuppliers = supplierStreamAsync.value ?? []; + + if (allSuppliers.isEmpty) { + return SupplierEmpty.empty_database; + } + + final filteredSuppliers = filteredSupplierAsync.value ?? []; + + if (supplierSearchAsync.isNotEmpty && filteredSuppliers.isEmpty) { + return SupplierEmpty.empty_search_result; + } + + return SupplierEmpty.has_data; +}); + +final supplierControllerProvider = + AsyncNotifierProvider.autoDispose( + SupplierController.new, + ); diff --git a/Mobile Operasional/lib/features/supplier/presentation/screens/add_supplier_screen.dart b/Mobile Operasional/lib/features/supplier/presentation/screens/add_supplier_screen.dart new file mode 100644 index 0000000..45fed11 --- /dev/null +++ b/Mobile Operasional/lib/features/supplier/presentation/screens/add_supplier_screen.dart @@ -0,0 +1,210 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; +import 'package:niogu_app/core/components/top_back_bar_app.dart'; +import 'package:niogu_app/core/constants/app_color.dart'; +import 'package:niogu_app/core/constants/app_font_size.dart'; +import 'package:niogu_app/core/utils/log_message.dart'; +import 'package:niogu_app/core/widgets/custom_snackbar.dart'; +import 'package:niogu_app/core/widgets/custom_text_form_field.dart'; +import 'package:niogu_app/features/supplier/domain/entities/supplier.dart'; +import 'package:niogu_app/features/supplier/presentation/providers/supplier_provider.dart'; +import 'package:sizer/sizer.dart'; + +class AddSupplierScreen extends ConsumerStatefulWidget { + final String? initialName; + final String? initialPhone; + const AddSupplierScreen({super.key, this.initialName, this.initialPhone}); + + @override + ConsumerState createState() => _AddSupplierScreenState(); +} + +class _AddSupplierScreenState extends ConsumerState { + final GlobalKey _formKey = GlobalKey(); + + final TextEditingController _nameController = TextEditingController(); + + final TextEditingController _emailController = TextEditingController(); + + final TextEditingController _phoneController = TextEditingController(); + + @override + void initState() { + super.initState(); + _nameController.text = widget.initialName ?? ''; + _phoneController.text = widget.initialPhone ?? ''; + } + + @override + void dispose() { + _nameController.dispose(); + _emailController.dispose(); + _phoneController.dispose(); + super.dispose(); + } + + Future _addSupplier() async { + final String name = _nameController.text; + + final String? email = _emailController.text.isNotEmpty + ? _emailController.text + : null; + + final String? phoneNumber = _phoneController.text.isNotEmpty + ? _phoneController.text + : null; + + final supplier = UpsertSupplier( + name: name, + email: email, + phoneNumber: phoneNumber, + ); + + try { + await ref + .read(supplierControllerProvider.notifier) + .saveSupplier(supplier); + + if (!mounted) return; + + CustomSnackbar.showSuccess(context, "Pemasok berhasil disimpan"); + context.pop(); + } catch (e, st) { + LogMessage.log.e(e.toString(), error: e, stackTrace: st); + CustomSnackbar.showError(context, "Ups, terjadi kesalahan"); + } + } + + @override + Widget build(BuildContext context) { + return LayoutBuilder( + builder: (context, constraints) { + final supplierControllerState = ref.watch(supplierControllerProvider); + + return SafeArea( + top: false, + bottom: true, + right: false, + left: false, + child: Scaffold( + backgroundColor: Colors.white, + appBar: TopBackBarApp( + title: "Tambah Pemasok", + onTap: () => context.pop(), + ), + body: Column( + children: [ + Expanded( + child: SingleChildScrollView( + padding: EdgeInsets.symmetric( + horizontal: 5.w, + vertical: 2.h, + ), + child: Form( + key: _formKey, + child: Column( + children: [ + Center( + child: Container( + width: 25.w, + height: 25.w, + decoration: BoxDecoration( + color: Colors.grey[100], + shape: BoxShape.circle, + border: Border.all( + color: Colors.grey.shade200, + width: 2, + ), + ), + child: Icon( + Icons.person_rounded, + size: 12.w, + color: Colors.grey[400], + ), + ), + ), + + SizedBox(height: 4.h), + + CustomTextFormField( + label: "Nama Pemasok", + controller: _nameController, + hint: "Masukkan nama", + validator: (value) { + if (value == null || value.isEmpty) { + return "Nama pemasok belum diisi"; + } + + return null; + }, + prefixIcon: Icons.person_outline, + ), + + SizedBox(height: 2.h), + CustomTextFormField( + label: "Email", + controller: _emailController, + hint: "Masukkan email (opsional)", + keyboardType: TextInputType.emailAddress, + prefixIcon: Icons.email_outlined, + ), + SizedBox(height: 2.h), + CustomTextFormField( + label: "No. Handphone / WA", + controller: _phoneController, + hint: "Masukkan no handphone / wa (opsional)", + keyboardType: TextInputType.phone, + prefixIcon: Icons.phone_android_outlined, + ), + ], + ), + ), + ), + ), + + Container( + padding: EdgeInsets.all(5.w), + decoration: BoxDecoration( + color: Colors.white, + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.05), + blurRadius: 10, + offset: const Offset(0, -5), + ), + ], + ), + child: SizedBox( + width: double.infinity, + height: 6.5.h, + child: ElevatedButton( + onPressed: supplierControllerState.isLoading + ? null + : _addSupplier, + style: ElevatedButton.styleFrom( + backgroundColor: AppColor.primaryColor, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(2.5.w), + ), + disabledBackgroundColor: Colors.grey.shade300, + ), + child: Text( + "Simpan Pemasok", + style: TextStyle( + color: Colors.white, + fontSize: AppFontSize.medium.sp, + fontWeight: FontWeight.bold, + ), + ), + ), + ), + ), + ], + ), + ), + ); + }, + ); + } +} diff --git a/Mobile Operasional/lib/features/supplier/presentation/screens/edit_supplier_screen.dart b/Mobile Operasional/lib/features/supplier/presentation/screens/edit_supplier_screen.dart new file mode 100644 index 0000000..bd3eee8 --- /dev/null +++ b/Mobile Operasional/lib/features/supplier/presentation/screens/edit_supplier_screen.dart @@ -0,0 +1,565 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; +import 'package:niogu_app/core/components/top_back_bar_app.dart'; +import 'package:niogu_app/core/constants/app_color.dart'; +import 'package:niogu_app/core/constants/app_font_size.dart'; +import 'package:niogu_app/core/router/app_route.dart'; +import 'package:niogu_app/core/utils/log_message.dart'; +import 'package:niogu_app/core/widgets/custom_snackbar.dart'; +import 'package:niogu_app/core/widgets/custom_text_form_field.dart'; +import 'package:niogu_app/features/supplier/domain/entities/supplier.dart'; +import 'package:niogu_app/features/supplier/presentation/providers/supplier_provider.dart'; +import 'package:niogu_app/features/supplier/presentation/widgets/edit_supplier_shimmer.dart'; +import 'package:niogu_app/features/supplier/presentation/widgets/supplier_activity.dart'; +import 'package:sizer/sizer.dart'; + +class EditSupplierScreen extends ConsumerStatefulWidget { + final String supplierId; + const EditSupplierScreen({super.key, required this.supplierId}); + + @override + ConsumerState createState() => _EditSupplierScreenState(); +} + +class _EditSupplierScreenState extends ConsumerState { + final GlobalKey _formKey = GlobalKey(); + + final TextEditingController _nameController = TextEditingController(); + + final TextEditingController _emailController = TextEditingController(); + + final TextEditingController _phoneController = TextEditingController(); + + List _activities = []; + + bool _isLoading = true; + + @override + void initState() { + super.initState(); + + WidgetsBinding.instance.addPostFrameCallback((_) { + Future.delayed(const Duration(milliseconds: 800), () { + _fetchSupplier(); + }); + }); + } + + Future _fetchSupplier() async { + try { + final supplierRepository = ref.read(supplierRepositoryProvider); + + final supplier = await supplierRepository.fetchSupplierById( + widget.supplierId, + ); + + _nameController.text = supplier.name; + + _emailController.text = supplier.email; + + _phoneController.text = supplier.phoneNumber; + + _activities = supplier.activities; + + setState(() { + _isLoading = false; + }); + } catch (e, st) { + if (!mounted) return; + + setState(() { + _isLoading = false; + }); + + LogMessage.log.e(e.toString(), error: e, stackTrace: st); + CustomSnackbar.showError(context, "Ups, terjadi kesalahan"); + context.pop(); + } + } + + Future _updateSupplier() async { + if (!_formKey.currentState!.validate()) return; + try { + await ref + .read(supplierControllerProvider.notifier) + .saveSupplier( + UpsertSupplier( + id: widget.supplierId, + name: _nameController.text.trim(), + email: _emailController.text.trim(), + phoneNumber: _phoneController.text.trim(), + ), + ); + + if (!mounted) return; + + CustomSnackbar.showSuccess(context, "Pemasok berhasil diubah"); + context.pop(); + } catch (e, st) { + LogMessage.log.e(e.toString(), error: e, stackTrace: st); + CustomSnackbar.showError(context, "Ups, terjadi kesalahan"); + context.pop(); + } + } + + Future _deleteSupplier() async { + try { + ref + .read(supplierControllerProvider.notifier) + .deleteSupplier(widget.supplierId); + + if (!mounted) return; + + CustomSnackbar.showSuccess(context, "Pemasok berhasil dihapus"); + Navigator.pop(context); + context.pop(); + } catch (e, st) { + LogMessage.log.e(e.toString(), error: e, stackTrace: st); + CustomSnackbar.showError(context, "Ups, terjadi kesalahan"); + Navigator.pop(context); + context.pop(); + } + } + + @override + void dispose() { + // TODO: implement dispose + _nameController.dispose(); + _emailController.dispose(); + _phoneController.dispose(); + super.dispose(); + } + + void _showDeleteConfirmation(BuildContext context) { + final bool isTablet = 100.w >= 600; + final supplierControllerState = ref.watch(supplierControllerProvider); + showDialog( + context: context, + builder: (BuildContext context) { + return Dialog( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(4.5.w), + ), + elevation: 0, + backgroundColor: Colors.transparent, + child: Container( + padding: EdgeInsets.all(6.w), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(4.5.w), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + padding: EdgeInsets.all(4.w), + decoration: BoxDecoration( + color: Colors.red[50], + shape: BoxShape.circle, + ), + child: Icon( + Icons.warning_amber_rounded, + color: Colors.red, + size: 10.w, + ), + ), + SizedBox(height: 2.h), + + Text( + "Hapus Pemasok?", + style: TextStyle( + fontSize: AppFontSize.medium.sp, + fontWeight: FontWeight.bold, + color: Colors.black87, + ), + ), + SizedBox(height: 1.h), + Text( + "Tindakan ini tidak dapat dibatalkan. Semua data profil pemasok akan dihapus dari sistem.", + textAlign: TextAlign.center, + style: TextStyle( + fontSize: isTablet + ? (AppFontSize.medium - 1.25).sp + : (AppFontSize.small - 1.25).sp, + color: Colors.grey[600], + height: 1.5, + ), + ), + SizedBox(height: 3.h), + + Row( + children: [ + Expanded( + child: ElevatedButton( + onPressed: () => Navigator.pop(context), + style: ElevatedButton.styleFrom( + backgroundColor: Colors.grey.shade300, + padding: EdgeInsets.symmetric(vertical: 1.5.h), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(2.5.w), + ), + elevation: 0, + ), + child: Text( + "Batal", + style: TextStyle( + color: Colors.grey[800], + fontWeight: FontWeight.bold, + fontSize: AppFontSize.medium.sp, + ), + ), + ), + ), + SizedBox(width: 3.w), + Expanded( + child: ElevatedButton( + onPressed: supplierControllerState.isLoading + ? null + : _deleteSupplier, + style: ElevatedButton.styleFrom( + padding: EdgeInsets.symmetric(vertical: 1.5.h), + backgroundColor: Colors.red, + elevation: 0, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(2.5.w), + ), + disabledBackgroundColor: Colors.grey.shade300, + ), + child: Text( + "Ya, Hapus", + style: TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + fontSize: AppFontSize.medium.sp, + ), + ), + ), + ), + ], + ), + ], + ), + ), + ); + }, + ); + } + + @override + Widget build(BuildContext context) { + return LayoutBuilder( + builder: (context, constraints) { + final bool isTablet = 100.w >= 600; + + final int totalActivity = _activities.length; + + final int displayActivityCount = totalActivity > 3 ? 3 : totalActivity; + + final supplierControllerState = ref.watch(supplierControllerProvider); + + return SafeArea( + top: false, + bottom: true, + right: false, + left: false, + child: Scaffold( + backgroundColor: Colors.white, + appBar: TopBackBarApp( + title: "Detail Pemasok", + onTap: () => context.pop(), + ), + body: _isLoading + ? const EditSupplierShimmer() + : RefreshIndicator( + onRefresh: () async { + await Future.delayed(const Duration(milliseconds: 800)); + + await _fetchSupplier(); + }, + color: AppColor.primaryColor, + backgroundColor: Colors.white, + strokeWidth: 2.5, + triggerMode: RefreshIndicatorTriggerMode.onEdge, + child: SingleChildScrollView( + child: Form( + key: _formKey, + child: Column( + children: [ + Center( + child: Container( + width: 25.w, + height: 25.w, + decoration: BoxDecoration( + color: Colors.grey[100], + shape: BoxShape.circle, + border: Border.all( + color: Colors.grey.shade200, + width: 2, + ), + ), + child: Icon( + Icons.person_rounded, + size: 12.w, + color: Colors.grey[400], + ), + ), + ), + + Container( + color: Colors.white, + padding: EdgeInsets.fromLTRB(5.w, 0, 5.w, 3.h), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + CustomTextFormField( + label: "Nama Pemasok", + controller: _nameController, + hint: "Masukkan nama", + validator: (value) { + if (value == null || value.isEmpty) { + return "Nama pemasok belum diisi"; + } + + return null; + }, + prefixIcon: Icons.person_outline, + ), + + SizedBox(height: 2.h), + CustomTextFormField( + label: "Email", + controller: _emailController, + hint: "Masukkan email (opsional)", + keyboardType: TextInputType.emailAddress, + prefixIcon: Icons.email_outlined, + ), + SizedBox(height: 2.h), + CustomTextFormField( + label: "No. Handphone / WA", + controller: _phoneController, + hint: + "Masukkan no handphone / wa (opsional)", + keyboardType: TextInputType.phone, + prefixIcon: Icons.phone_android_outlined, + ), + ], + ), + ), + + _buildSectionHeader("Aktivitas Dengan Pemasok"), + + _activities.isEmpty + ? Container( + width: double.infinity, + padding: EdgeInsets.symmetric( + vertical: 4.h, + ), + margin: EdgeInsets.symmetric( + horizontal: 5.w, + ), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular( + 2.5.w, + ), + border: Border.all( + color: Colors.grey.shade100, + ), + ), + child: Column( + children: [ + Icon( + Icons.history_rounded, + color: Colors.grey[300], + size: 10.w, + ), + SizedBox(height: 1.5.h), + Text( + "Belum ada riwayat aktivitas dengan pemasok", + style: TextStyle( + fontSize: + (AppFontSize.medium - 1.25).sp, + color: Colors.grey[500], + ), + ), + ], + ), + ) + : Column( + children: [ + ListView.builder( + shrinkWrap: true, + physics: + const NeverScrollableScrollPhysics(), + padding: EdgeInsets.symmetric( + horizontal: 5.w, + ), + itemCount: displayActivityCount, + itemBuilder: (context, index) { + final activity = _activities[index]; + + return SupplierActivity( + activity: activity, + ); + }, + ), + + if (totalActivity > 3) + Padding( + padding: EdgeInsets.symmetric( + horizontal: 5.w, + ), + child: InkWell( + onTap: () { + context.pushNamed( + AppRoute.supplierActivityScreen, + extra: { + "activities": _activities, + }, + ); + }, + borderRadius: BorderRadius.circular( + 2.5.w, + ), + child: Container( + padding: EdgeInsets.symmetric( + vertical: 1.5.h, + ), + decoration: BoxDecoration( + border: Border.all( + color: Colors.grey[300]!, + ), + borderRadius: + BorderRadius.circular( + 2.5.w, + ), + ), + child: Row( + mainAxisAlignment: + MainAxisAlignment.center, + children: [ + Text( + "Lihat ${totalActivity - 3} Aktivitas Lainnya", + style: TextStyle( + color: Colors.grey[600], + fontWeight: + FontWeight.bold, + fontSize: isTablet + ? (AppFontSize.medium - + 1.25) + .sp + : (AppFontSize.small - + 1.25) + .sp, + ), + ), + SizedBox(width: 2.w), + Icon( + Icons + .keyboard_arrow_down_rounded, + color: Colors.grey[600], + size: 5.w, + ), + ], + ), + ), + ), + ), + ], + ), + + SizedBox(height: 5.h), + Padding( + padding: EdgeInsets.symmetric(horizontal: 5.w), + child: TextButton.icon( + onPressed: () => + _showDeleteConfirmation(context), + style: TextButton.styleFrom( + foregroundColor: Colors.red[700], + padding: EdgeInsets.symmetric( + vertical: 1.5.h, + horizontal: 4.w, + ), + ), + icon: Icon( + Icons.delete_outline_rounded, + size: 5.w, + ), + label: Text( + "Hapus Pelanggan Ini", + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: isTablet + ? (AppFontSize.medium - 1.25).sp + : (AppFontSize.small - 1.25).sp, + ), + ), + ), + ), + SizedBox(height: 4.h), + ], + ), + ), + ), + ), + bottomNavigationBar: _isLoading + ? null + : Container( + padding: EdgeInsets.all(5.w), + decoration: BoxDecoration( + color: Colors.white, + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.05), + blurRadius: 10, + offset: const Offset(0, -5), + ), + ], + ), + child: ElevatedButton( + onPressed: supplierControllerState.isLoading + ? null + : _updateSupplier, + style: ElevatedButton.styleFrom( + backgroundColor: AppColor.primaryColor, + minimumSize: Size(double.infinity, 6.5.h), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(2.5.w), + ), + disabledBackgroundColor: Colors.grey.shade300, + ), + child: Text( + "Perbarui Informasi Pemasok", + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: AppFontSize.medium.sp, + color: Colors.white, + ), + ), + ), + ), + ), + ); + }, + ); + } + + Widget _buildSectionHeader(String title) { + return Padding( + padding: EdgeInsets.fromLTRB(5.w, 3.h, 5.w, 1.5.h), + child: Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Text( + title, + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: (AppFontSize.medium - 1.25).sp, + ), + ), + ], + ), + ); + } +} diff --git a/Mobile Operasional/lib/features/supplier/presentation/screens/supplier_activites_screen.dart b/Mobile Operasional/lib/features/supplier/presentation/screens/supplier_activites_screen.dart new file mode 100644 index 0000000..2ac0429 --- /dev/null +++ b/Mobile Operasional/lib/features/supplier/presentation/screens/supplier_activites_screen.dart @@ -0,0 +1,42 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; +import 'package:niogu_app/core/components/top_back_bar_app.dart'; +import 'package:niogu_app/features/supplier/domain/entities/supplier.dart'; +import 'package:niogu_app/features/supplier/presentation/widgets/supplier_activity.dart'; +import 'package:sizer/sizer.dart'; + +class SupplierActivitesScreen extends ConsumerWidget { + final List activities; + + const SupplierActivitesScreen({super.key, required this.activities}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + return LayoutBuilder( + builder: (context, constraints) { + return SafeArea( + top: false, + bottom: true, + right: false, + left: false, + child: Scaffold( + backgroundColor: const Color(0xFFF9FAFB), + appBar: TopBackBarApp( + title: "Aktivitas Dengan Pemasok", + onTap: () => context.pop(), + ), + body: ListView.builder( + padding: EdgeInsets.all(5.w), + itemCount: activities.length, + itemBuilder: (context, index) { + final activity = activities[index]; + return SupplierActivity(activity: activity); + }, + ), + ), + ); + }, + ); + } +} diff --git a/Mobile Operasional/lib/features/supplier/presentation/screens/supplier_screen.dart b/Mobile Operasional/lib/features/supplier/presentation/screens/supplier_screen.dart new file mode 100644 index 0000000..7df98d2 --- /dev/null +++ b/Mobile Operasional/lib/features/supplier/presentation/screens/supplier_screen.dart @@ -0,0 +1,464 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter_contacts/flutter_contacts.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; +import 'package:niogu_app/core/constants/app_color.dart'; +import 'package:niogu_app/core/constants/app_font_size.dart'; +import 'package:niogu_app/core/widgets/custom_error_screen.dart'; +import 'package:niogu_app/core/router/app_route.dart'; +import 'package:niogu_app/core/widgets/custom_empty_screen.dart'; +import 'package:niogu_app/core/widgets/custom_snackbar.dart'; +import 'package:niogu_app/features/supplier/domain/entities/supplier.dart'; +import 'package:niogu_app/features/supplier/presentation/providers/supplier_provider.dart'; +import 'package:niogu_app/features/supplier/presentation/widgets/supplier_shimmer.dart'; +import 'package:sizer/sizer.dart'; + +class SupplierScreen extends ConsumerStatefulWidget { + const SupplierScreen({super.key}); + + @override + ConsumerState createState() => _SupplierScreenState(); +} + +class _SupplierScreenState extends ConsumerState { + final FocusNode _searchFocusNode = FocusNode(); + + Color _searchIconColor = Colors.grey; + + Timer? _debounce; + + final TextEditingController _searchController = TextEditingController(); + + @override + void initState() { + // TODO: implement initState + super.initState(); + + _searchFocusNode.addListener(() { + setState(() { + _searchIconColor = _searchFocusNode.hasFocus + ? Colors.black + : Colors.grey; + }); + }); + } + + @override + void dispose() { + _searchController.dispose(); + super.dispose(); + } + + void _onSearchChanged(String value) { + if (_debounce?.isActive ?? false) _debounce?.cancel(); + _debounce = Timer(const Duration(milliseconds: 800), () { + ref.read(supplierSearchProvider.notifier).state = value; + }); + } + + Map> _groupedSuppliers( + List suppliers, + ) { + suppliers.sort((a, b) => a.name.compareTo(b.name)); + + final Map> groupedSuppliers = {}; + + for (final supplier in suppliers) { + if (supplier.name.isEmpty) continue; + + String firstLetter = supplier.name[0].toUpperCase(); + + if (!RegExp(r'[A-Z]').hasMatch(firstLetter)) { + firstLetter = '#'; + } + + if (!groupedSuppliers.containsKey(firstLetter)) { + groupedSuppliers[firstLetter] = []; + } + + groupedSuppliers[firstLetter]!.add(supplier); + } + + return groupedSuppliers; + } + + Future _handleImportContact() async { + final permission = await FlutterContacts.requestPermission(); + if (permission) { + final contact = await FlutterContacts.openExternalPick(); + + if (contact != null) { + String name = contact.displayName; + String phone = ""; + + if (contact.phones.isNotEmpty) { + phone = contact.phones.first.number; + } + + if (!mounted) return; + + context.pushNamed( + AppRoute.addSupplierScreen, + extra: {'name': name, 'phone': phone}, + ); + } + } else { + CustomSnackbar.showWarning(context, "Akses Ditolak"); + } + } + + void _showAddOptions(BuildContext context) { + final bool isTablet = 100.w >= 600; + showModalBottomSheet( + context: context, + backgroundColor: Colors.white, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.vertical(top: Radius.circular(2.5.w)), + ), + constraints: const BoxConstraints(maxWidth: double.infinity), + builder: (context) { + return SafeArea( + child: Container( + width: 100.w, + padding: EdgeInsets.symmetric(vertical: 2.h), + clipBehavior: Clip.hardEdge, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.vertical(top: Radius.circular(6.w)), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: EdgeInsets.fromLTRB(5.w, 2.h, 5.w, 1.h), + child: Text( + "Tambah Pemasok", + style: TextStyle( + fontSize: AppFontSize.medium.sp, + fontWeight: FontWeight.bold, + ), + ), + ), + SizedBox(height: 2.h), + ListTile( + leading: Padding( + padding: EdgeInsets.only(left: 5.w), + child: Icon( + Icons.edit_note_rounded, + color: Colors.blue, + size: 6.w, + ), + ), + title: Text( + "Input Manual", + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + ), + ), + subtitle: Text( + "Isi informasi pemasok secara manual", + style: TextStyle( + fontSize: isTablet + ? (AppFontSize.medium - 1.25).sp + : (AppFontSize.small - 1.25).sp, + color: Colors.grey, + ), + ), + onTap: () { + context.pop(); + context.pushNamed(AppRoute.addSupplierScreen); + }, + ), + SizedBox(height: 2.h), + ListTile( + leading: Padding( + padding: EdgeInsets.only(left: 5.w), + child: Icon( + Icons.contacts_rounded, + color: Colors.green, + size: 6.w, + ), + ), + title: Text( + "Ambil dari Kontak HP", + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + ), + ), + subtitle: Text( + "Pilih langsung dari buku telepon", + style: TextStyle( + fontSize: isTablet + ? (AppFontSize.medium - 1.25).sp + : (AppFontSize.small - 1.25).sp, + color: Colors.grey, + ), + ), + onTap: () async { + context.pop(); + await _handleImportContact(); + }, + ), + SizedBox(height: 2.h), + ], + ), + ), + ); + }, + ); + } + + @override + Widget build(BuildContext context) { + return LayoutBuilder( + builder: (context, constraints) { + final bool isTablet = 100.w >= 600; + + const greyColor = Color(0xFFF5F5F5); + + final filteredSupplierState = ref.watch(filteredSupplierProvider); + + final supplierEmptyState = ref.watch(supplierEmptyProvider); + + return SafeArea( + top: false, + bottom: true, + right: false, + left: false, + child: Scaffold( + backgroundColor: const Color(0xFFF9FAFB), + appBar: AppBar( + backgroundColor: Colors.white, + surfaceTintColor: Colors.white, + elevation: 0, + toolbarHeight: isTablet ? 7.5.h : kToolbarHeight, + centerTitle: true, + leading: Center( + child: Material( + color: Colors.transparent, + type: MaterialType.canvas, + child: InkWell( + onTap: () => context.pop(), + borderRadius: BorderRadius.circular(2.5.w), + child: Container( + width: 10.w, + height: 10.w, + decoration: BoxDecoration( + color: Colors.white, + border: Border.all(color: Colors.grey.shade200), + borderRadius: BorderRadius.circular(2.5.w), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.05), + blurRadius: 5, + offset: const Offset(0, 2), + ), + ], + ), + child: Icon( + Icons.arrow_back_ios_new_rounded, + color: Colors.black87, + size: 5.w, + ), + ), + ), + ), + ), + leadingWidth: 18.w, + title: Text( + "Pemasok", + style: TextStyle( + color: Colors.black87, + fontWeight: FontWeight.bold, + fontSize: AppFontSize.medium.sp, + ), + ), + actions: [ + IconButton( + onPressed: () => _showAddOptions(context), + icon: Icon( + Icons.add_circle, + color: AppColor.primaryColor, + size: 8.w, + ), + ), + SizedBox(width: 3.w), + ], + ), + body: Padding( + padding: EdgeInsets.symmetric(horizontal: 5.w), + child: Column( + children: [ + Container( + height: 6.h, + padding: isTablet + ? EdgeInsets.symmetric(vertical: 1.h, horizontal: 2.w) + : EdgeInsets.zero, + decoration: BoxDecoration( + color: greyColor, + borderRadius: BorderRadius.circular(2.5.w), + ), + child: TextField( + focusNode: _searchFocusNode, + onChanged: _onSearchChanged, + textAlignVertical: TextAlignVertical.center, + style: TextStyle( + color: Colors.black87, + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + ), + decoration: InputDecoration( + hintText: "Cari nama pemasok", + hintStyle: TextStyle( + color: _searchIconColor, + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + ), + prefixIcon: Icon( + Icons.search, + color: _searchIconColor, + size: 5.w, + ), + border: InputBorder.none, + contentPadding: EdgeInsets.zero, + isDense: true, + ), + ), + ), + + SizedBox(height: 3.h), + + Expanded( + child: filteredSupplierState.when( + data: (suppliers) { + final groupedData = _groupedSuppliers(suppliers); + switch (supplierEmptyState) { + case SupplierEmpty.loading: + return SizedBox(); + case SupplierEmpty.empty_database: + return CustomEmptyScreen( + title: "Tidak Ada Pemasok", + body: "Kamu belum memiliki pemasok", + ); + case SupplierEmpty.empty_search_result: + return CustomEmptyScreen( + body: "Pemasok Tidak Ditemukan", + ); + case SupplierEmpty.has_data: + return ListView.builder( + physics: const AlwaysScrollableScrollPhysics(), + padding: EdgeInsets.only(bottom: 5.h), + itemCount: groupedData.keys.length, + itemBuilder: (context, index) { + String letter = groupedData.keys.elementAt( + index, + ); + List suppliers = + groupedData[letter]!; + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + width: double.infinity, + padding: EdgeInsets.symmetric( + horizontal: 5.w, + vertical: 1.h, + ), + color: Colors.grey[200], + child: Text( + letter, + style: TextStyle( + fontWeight: FontWeight.bold, + color: Colors.grey[700], + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + ), + ), + ), + + ...suppliers.map((supplier) { + return Container( + padding: isTablet + ? EdgeInsets.symmetric( + vertical: 1.5.h, + ) + : EdgeInsets.zero, + color: Colors.white, + child: Column( + children: [ + ListTile( + onTap: () { + context.pushNamed( + AppRoute.editSupplierScreen, + pathParameters: { + "id": supplier.id, + }, + ); + }, + contentPadding: + EdgeInsets.symmetric( + horizontal: 5.w, + vertical: 0.5.h, + ), + title: Text( + supplier.name, + style: TextStyle( + fontSize: isTablet + ? AppFontSize.medium.sp + : AppFontSize.small.sp, + fontWeight: FontWeight.w500, + color: Colors.black87, + ), + ), + trailing: Icon( + Icons.chevron_right_rounded, + size: 5.w, + color: Colors.grey[400], + ), + ), + Divider( + height: 1, + thickness: 1, + indent: 5.w, + color: Colors.grey[100], + ), + ], + ), + ); + }), + ], + ); + }, + ); + } + }, + error: (error, stackTrace) { + return CustomErrorScreen( + message: "Ups, terjadi kesalahan", + onRefresh: () {}, + ); + }, + loading: () => const SupplierShimmer(), + ), + ), + ], + ), + ), + ), + ); + }, + ); + } +} diff --git a/Mobile Operasional/lib/features/supplier/presentation/widgets/edit_supplier_shimmer.dart b/Mobile Operasional/lib/features/supplier/presentation/widgets/edit_supplier_shimmer.dart new file mode 100644 index 0000000..9990314 --- /dev/null +++ b/Mobile Operasional/lib/features/supplier/presentation/widgets/edit_supplier_shimmer.dart @@ -0,0 +1,101 @@ +import 'package:flutter/material.dart'; +import 'package:shimmer/shimmer.dart'; +import 'package:sizer/sizer.dart'; + +class EditSupplierShimmer extends StatelessWidget { + const EditSupplierShimmer({super.key}); + + @override + Widget build(BuildContext context) { + return Shimmer.fromColors( + baseColor: Colors.grey.shade300, + highlightColor: Colors.grey.shade100, + child: Column( + children: [ + Expanded( + child: SingleChildScrollView( + physics: const NeverScrollableScrollPhysics(), + child: Column( + children: [ + Container( + width: double.infinity, + padding: EdgeInsets.symmetric(vertical: 3.h), + child: Column( + children: [ + Container( + width: 24.w, + height: 24.w, + decoration: const BoxDecoration( + color: Colors.white, + shape: BoxShape.circle, + ), + ), + ], + ), + ), + + Padding( + padding: EdgeInsets.symmetric(horizontal: 5.w), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: List.generate( + 3, + (index) => Padding( + padding: EdgeInsets.only(bottom: 2.h), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + width: 30.w, + height: 1.5.h, + color: Colors.white, + ), + SizedBox(height: 1.h), + Container( + width: double.infinity, + height: 7.h, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(2.5.w), + ), + ), + ], + ), + ), + ), + ), + ), + + _buildSectionLabelShimmer("Aktivitas Dengan Pemasok"), + Column( + children: List.generate( + 2, + (index) => Container( + height: 12.h, + margin: EdgeInsets.fromLTRB(5.w, 0, 5.w, 1.5.h), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(2.5.w), + ), + ), + ), + ), + ], + ), + ), + ), + ], + ), + ); + } + + Widget _buildSectionLabelShimmer(String title) { + return Padding( + padding: EdgeInsets.fromLTRB(5.w, 3.h, 5.w, 1.5.h), + child: Align( + alignment: Alignment.centerLeft, + child: Container(width: 40.w, height: 2.h, color: Colors.white), + ), + ); + } +} diff --git a/Mobile Operasional/lib/features/supplier/presentation/widgets/supplier_activity.dart b/Mobile Operasional/lib/features/supplier/presentation/widgets/supplier_activity.dart new file mode 100644 index 0000000..1d3c89b --- /dev/null +++ b/Mobile Operasional/lib/features/supplier/presentation/widgets/supplier_activity.dart @@ -0,0 +1,132 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; +import 'package:intl/intl.dart'; +import 'package:niogu_app/core/constants/app_color.dart'; +import 'package:niogu_app/core/constants/app_font_size.dart'; +import 'package:niogu_app/core/router/app_route.dart'; +import 'package:niogu_app/core/enums/payment_status.dart'; +import 'package:niogu_app/core/utils/time_zone.dart'; +import 'package:niogu_app/features/report/transaction/presentation/providers/transaction_report_provider.dart'; +import 'package:niogu_app/features/supplier/domain/entities/supplier.dart'; +import 'package:sizer/sizer.dart'; + +class SupplierActivity extends ConsumerWidget { + final SupplierActivities activity; + const SupplierActivity({super.key, required this.activity}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final bool isTablet = 100.w >= 600; + + final status = activity.paymentStatus; + + final Color colorBadge = switch (status) { + PaymentStatus.paid => Colors.green.shade50, + PaymentStatus.partial => Colors.blue.shade50, + PaymentStatus.debt => Colors.red.shade50, + }; + + final String paymentStatus = switch (status) { + PaymentStatus.paid => "Lunas", + PaymentStatus.partial => "Bayar Sebagian", + PaymentStatus.debt => "Hutang", + }; + + final Color colorPaymentStatus = switch (status) { + PaymentStatus.paid => Colors.green.shade700, + PaymentStatus.partial => Colors.blue.shade700, + PaymentStatus.debt => Colors.red.shade700, + }; + + return GestureDetector( + onTap: () async { + final purchaseDetail = await ref + .read(transactionReportRepositoryProvider) + .getPurchaseDetail(activity.id); + + FocusScope.of(context).unfocus(); + + await Future.delayed(const Duration(milliseconds: 400)); + + context.pushNamed( + AppRoute.transactionReportPurchaseDetailScreen, + extra: purchaseDetail, + ); + }, + child: Container( + margin: EdgeInsets.only(bottom: 1.5.h), + padding: EdgeInsets.all(4.w), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(2.5.w), + border: Border.all(color: Colors.grey[200]!), + ), + child: Row( + children: [ + Container( + padding: EdgeInsets.all(2.w), + decoration: BoxDecoration( + color: AppColor.primaryColor.withOpacity(0.1), + shape: BoxShape.circle, + ), + child: Icon( + Icons.shopping_bag_outlined, + color: AppColor.primaryColor, + size: 5.w, + ), + ), + SizedBox(width: 4.w), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + activity.purchaseNumber, + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: isTablet + ? (AppFontSize.medium - 1.25).sp + : (AppFontSize.small - 1.25).sp, + ), + ), + SizedBox(height: 0.75.h), + Container( + margin: EdgeInsets.only(top: 0.5.h), + padding: EdgeInsets.symmetric( + horizontal: 2.w, + vertical: 0.5.w, + ), + decoration: BoxDecoration( + color: colorBadge, + borderRadius: BorderRadius.circular(1.w), + ), + child: Text( + paymentStatus, + style: TextStyle( + fontSize: isTablet + ? (AppFontSize.medium - 1.25).sp + : (AppFontSize.small - 1.25).sp, + fontWeight: FontWeight.bold, + color: colorPaymentStatus, + ), + ), + ), + ], + ), + ), + Text( + "${DateFormat("dd MMMM yyyy HH:mm").format(activity.purchaseDate)} ${TimeZone.getCurrentTimeZone()}", + style: TextStyle( + fontSize: isTablet + ? (AppFontSize.medium - 1.25).sp + : (AppFontSize.small - 1.25).sp, + color: Colors.grey, + ), + ), + ], + ), + ), + ); + } +} diff --git a/Mobile Operasional/lib/features/supplier/presentation/widgets/supplier_shimmer.dart b/Mobile Operasional/lib/features/supplier/presentation/widgets/supplier_shimmer.dart new file mode 100644 index 0000000..544d060 --- /dev/null +++ b/Mobile Operasional/lib/features/supplier/presentation/widgets/supplier_shimmer.dart @@ -0,0 +1,72 @@ +import 'package:flutter/material.dart'; +import 'package:shimmer/shimmer.dart'; +import 'package:sizer/sizer.dart'; + +class SupplierShimmer extends StatelessWidget { + const SupplierShimmer({super.key}); + + @override + Widget build(BuildContext context) { + return Shimmer.fromColors( + baseColor: Colors.grey.shade300, + highlightColor: Colors.grey.shade100, + enabled: true, + child: ListView.builder( + physics: const NeverScrollableScrollPhysics(), + itemCount: 3, + itemBuilder: (_, index) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + width: double.infinity, + height: 4.5.h, + color: Colors.white, + margin: EdgeInsets.only(bottom: 1.h), + ), + + Column( + children: List.generate( + 3, + (i) => Column( + children: [ + ListTile( + contentPadding: EdgeInsets.symmetric( + horizontal: 5.w, + vertical: 0.5.h, + ), + title: Container( + width: 40.w, + height: 1.5.h, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(1.w), + ), + ), + trailing: Container( + width: 5.w, + height: 5.w, + decoration: const BoxDecoration( + color: Colors.white, + shape: BoxShape.circle, + ), + ), + ), + Divider( + height: 1, + thickness: 1, + indent: 5.w, + color: Colors.white, + ), + ], + ), + ), + ), + SizedBox(height: 1.h), + ], + ); + }, + ), + ); + } +} diff --git a/Mobile Operasional/lib/features/user/data/repositories/user_repository_impl.dart b/Mobile Operasional/lib/features/user/data/repositories/user_repository_impl.dart new file mode 100644 index 0000000..eac33ad --- /dev/null +++ b/Mobile Operasional/lib/features/user/data/repositories/user_repository_impl.dart @@ -0,0 +1,20 @@ +import 'package:niogu_app/core/database/app_database.dart'; +import 'package:niogu_app/core/entities/custom_activity.dart'; +import 'package:niogu_app/features/user/domain/entities/user.dart'; +import 'package:niogu_app/features/user/domain/repositories/i_user_repository.dart'; + +class UserRepositoryImpl implements IUserRepository { + final AppDatabase _appDatabase; + + UserRepositoryImpl(this._appDatabase); + + @override + Stream watchShiftInfo() { + return _appDatabase.userDao.watchShiftInfo(); + } + + @override + Stream checkPermissionActivity(String outletId) { + return _appDatabase.userDao.checkPermissionActivity(); + } +} diff --git a/Mobile Operasional/lib/features/user/domain/entities/user.dart b/Mobile Operasional/lib/features/user/domain/entities/user.dart new file mode 100644 index 0000000..c143ede --- /dev/null +++ b/Mobile Operasional/lib/features/user/domain/entities/user.dart @@ -0,0 +1,11 @@ +class ShiftInfo { + final String shiftName; + final String shiftStartTime; + final String shiftEndTime; + + const ShiftInfo({ + required this.shiftName, + required this.shiftStartTime, + required this.shiftEndTime + }); +} \ No newline at end of file diff --git a/Mobile Operasional/lib/features/user/domain/repositories/i_user_repository.dart b/Mobile Operasional/lib/features/user/domain/repositories/i_user_repository.dart new file mode 100644 index 0000000..7fa9926 --- /dev/null +++ b/Mobile Operasional/lib/features/user/domain/repositories/i_user_repository.dart @@ -0,0 +1,8 @@ +import 'package:niogu_app/core/entities/custom_activity.dart'; +import 'package:niogu_app/features/user/domain/entities/user.dart'; + +abstract class IUserRepository { + Stream watchShiftInfo(); + + Stream checkPermissionActivity(String outletId); +} diff --git a/Mobile Operasional/lib/features/user/presentation/providers/user_provider.dart b/Mobile Operasional/lib/features/user/presentation/providers/user_provider.dart new file mode 100644 index 0000000..5ebe4b3 --- /dev/null +++ b/Mobile Operasional/lib/features/user/presentation/providers/user_provider.dart @@ -0,0 +1,29 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:niogu_app/core/entities/custom_activity.dart'; +import 'package:niogu_app/core/providers/app_provider.dart'; +import 'package:niogu_app/features/user/data/repositories/user_repository_impl.dart'; +import 'package:niogu_app/features/user/domain/entities/user.dart'; +import 'package:niogu_app/features/user/domain/repositories/i_user_repository.dart'; + +final userRepositoryProvider = Provider.autoDispose((ref) { + final appDatabase = ref.watch(appDatabaseProvider); + return UserRepositoryImpl(appDatabase); +}); + +final shiftInfoStreamProvider = StreamProvider.autoDispose((ref) { + final userRepository = ref.watch(userRepositoryProvider); + + return userRepository.watchShiftInfo(); +}); + +final permissionActivityStreamProvider = StreamProvider((ref) { + final userRepository = ref.watch(userRepositoryProvider); + + final currentOutletId = ref.watch(currentOutletIdProvider); + + if(currentOutletId == null) { + return Stream.value(null); + } + + return userRepository.checkPermissionActivity(currentOutletId); +}); diff --git a/Mobile Operasional/lib/main_app.dart b/Mobile Operasional/lib/main_app.dart new file mode 100644 index 0000000..c63943b --- /dev/null +++ b/Mobile Operasional/lib/main_app.dart @@ -0,0 +1,80 @@ +import 'package:device_preview/device_preview.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_localizations/flutter_localizations.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:intl/date_symbol_data_local.dart'; +import 'package:intl/intl.dart'; +import 'package:niogu_app/core/router/app_route.dart'; +import 'package:niogu_app/core/services/working_service.dart'; +import 'package:sizer/sizer.dart'; +import 'package:workmanager/workmanager.dart'; + +Future main(List args) async { + WidgetsFlutterBinding.ensureInitialized(); + + await Workmanager().initialize(callbackDispatcher); + + await Workmanager().registerPeriodicTask( + "niogu-bg-sync", + "syncTask", + frequency: const Duration(minutes: 15), + constraints: Constraints(networkType: NetworkType.connected), + ); + + await initializeDateFormatting('id_ID', null); + + Intl.defaultLocale = "id_ID"; + + await SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]); + + runApp(const MainApp()); +} + +class MainApp extends StatelessWidget { + const MainApp({super.key}); + + @override + Widget build(BuildContext context) { + return ProviderScope( + child: DevicePreview( + enabled: false, + builder: (context) { + return Sizer( + builder: (context, orientation, deviceType) { + return MaterialApp.router( + title: "Niogu", + localizationsDelegates: const [ + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + ], + supportedLocales: const [ + const Locale('id', 'ID'), + const Locale('en', 'US'), + ], + locale: const Locale('id', 'ID'), + debugShowCheckedModeBanner: false, + theme: ThemeData(fontFamily: "Poppins", useMaterial3: true), + builder: (context, child) { + final mediaQueryData = MediaQuery.of(context); + + final scale = mediaQueryData.textScaler.clamp( + minScaleFactor: 1.0, + maxScaleFactor: 1.1, + ); + + return MediaQuery( + data: mediaQueryData.copyWith(textScaler: scale), + child: child!, + ); + }, + routerConfig: AppRoute.router, + ); + }, + ); + }, + ), + ); + } +} diff --git a/Mobile Operasional/lib/main_wrapper.dart b/Mobile Operasional/lib/main_wrapper.dart new file mode 100644 index 0000000..5ef0517 --- /dev/null +++ b/Mobile Operasional/lib/main_wrapper.dart @@ -0,0 +1,62 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; +import 'package:niogu_app/core/components/bottom_bar_app.dart'; +import 'package:niogu_app/core/providers/app_provider.dart'; +import 'package:niogu_app/core/enums/user_role.dart'; +import 'package:niogu_app/core/widgets/pop_up_notification.dart'; + +class MainWrapper extends ConsumerWidget { + final StatefulNavigationShell navigationShell; + + const MainWrapper({super.key, required this.navigationShell}); + + static final GlobalKey scaffoldKey = + GlobalKey(); + + @override + Widget build(BuildContext context, WidgetRef ref) { + return LayoutBuilder( + builder: (context, constraints) { + final messageActivity = ref.watch(messageActivityProvider); + + final currentUserRole = ref.watch(currentUserRoleProvider); + + return SafeArea( + top: false, + bottom: true, + right: false, + left: false, + child: Stack( + children: [ + Scaffold( + key: scaffoldKey, + extendBody: true, + resizeToAvoidBottomInset: false, + body: navigationShell, + bottomNavigationBar: navigationShell.currentIndex == 2 + ? null + : BottomBarApp( + currentIndex: navigationShell.currentIndex, + onTap: (index) { + navigationShell.goBranch( + index, + initialLocation: + index == navigationShell.currentIndex, + ); + }, + ), + ), + if (messageActivity != null) + PopupNotification( + isOwner: currentUserRole == UserRole.owner, + messages: messageActivity.messages, + type: messageActivity.type, + ), + ], + ), + ); + }, + ); + } +} diff --git a/Mobile Operasional/linux/.gitignore b/Mobile Operasional/linux/.gitignore new file mode 100644 index 0000000..d3896c9 --- /dev/null +++ b/Mobile Operasional/linux/.gitignore @@ -0,0 +1 @@ +flutter/ephemeral diff --git a/Mobile Operasional/linux/CMakeLists.txt b/Mobile Operasional/linux/CMakeLists.txt new file mode 100644 index 0000000..d9c81a2 --- /dev/null +++ b/Mobile Operasional/linux/CMakeLists.txt @@ -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 "niogoo_app") +# The unique GTK application identifier for this application. See: +# https://wiki.gnome.org/HowDoI/ChooseApplicationID +set(APPLICATION_ID "com.example.niogoo_app") + +# 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 "$<$>:-O3>") + target_compile_definitions(${TARGET} PRIVATE "$<$>: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() diff --git a/Mobile Operasional/linux/flutter/CMakeLists.txt b/Mobile Operasional/linux/flutter/CMakeLists.txt new file mode 100644 index 0000000..d5bd016 --- /dev/null +++ b/Mobile Operasional/linux/flutter/CMakeLists.txt @@ -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} +) diff --git a/Mobile Operasional/linux/flutter/generated_plugin_registrant.cc b/Mobile Operasional/linux/flutter/generated_plugin_registrant.cc new file mode 100644 index 0000000..5a91aeb --- /dev/null +++ b/Mobile Operasional/linux/flutter/generated_plugin_registrant.cc @@ -0,0 +1,27 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#include "generated_plugin_registrant.h" + +#include +#include +#include +#include + +void fl_register_plugins(FlPluginRegistry* registry) { + g_autoptr(FlPluginRegistrar) file_selector_linux_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "FileSelectorPlugin"); + file_selector_plugin_register_with_registrar(file_selector_linux_registrar); + g_autoptr(FlPluginRegistrar) flutter_secure_storage_linux_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterSecureStorageLinuxPlugin"); + flutter_secure_storage_linux_plugin_register_with_registrar(flutter_secure_storage_linux_registrar); + g_autoptr(FlPluginRegistrar) sqlite3_flutter_libs_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "Sqlite3FlutterLibsPlugin"); + sqlite3_flutter_libs_plugin_register_with_registrar(sqlite3_flutter_libs_registrar); + g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin"); + url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar); +} diff --git a/Mobile Operasional/linux/flutter/generated_plugin_registrant.h b/Mobile Operasional/linux/flutter/generated_plugin_registrant.h new file mode 100644 index 0000000..e0f0a47 --- /dev/null +++ b/Mobile Operasional/linux/flutter/generated_plugin_registrant.h @@ -0,0 +1,15 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#ifndef GENERATED_PLUGIN_REGISTRANT_ +#define GENERATED_PLUGIN_REGISTRANT_ + +#include + +// Registers Flutter plugins. +void fl_register_plugins(FlPluginRegistry* registry); + +#endif // GENERATED_PLUGIN_REGISTRANT_ diff --git a/Mobile Operasional/linux/flutter/generated_plugins.cmake b/Mobile Operasional/linux/flutter/generated_plugins.cmake new file mode 100644 index 0000000..0b808f1 --- /dev/null +++ b/Mobile Operasional/linux/flutter/generated_plugins.cmake @@ -0,0 +1,27 @@ +# +# Generated file, do not edit. +# + +list(APPEND FLUTTER_PLUGIN_LIST + file_selector_linux + flutter_secure_storage_linux + sqlite3_flutter_libs + url_launcher_linux +) + +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 $) + 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) diff --git a/Mobile Operasional/linux/runner/CMakeLists.txt b/Mobile Operasional/linux/runner/CMakeLists.txt new file mode 100644 index 0000000..e97dabc --- /dev/null +++ b/Mobile Operasional/linux/runner/CMakeLists.txt @@ -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}") diff --git a/Mobile Operasional/linux/runner/main.cc b/Mobile Operasional/linux/runner/main.cc new file mode 100644 index 0000000..e7c5c54 --- /dev/null +++ b/Mobile Operasional/linux/runner/main.cc @@ -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); +} diff --git a/Mobile Operasional/linux/runner/my_application.cc b/Mobile Operasional/linux/runner/my_application.cc new file mode 100644 index 0000000..0ab91e4 --- /dev/null +++ b/Mobile Operasional/linux/runner/my_application.cc @@ -0,0 +1,148 @@ +#include "my_application.h" + +#include +#ifdef GDK_WINDOWING_X11 +#include +#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) + +// Called when first Flutter frame received. +static void first_frame_cb(MyApplication* self, FlView* view) { + gtk_widget_show(gtk_widget_get_toplevel(GTK_WIDGET(view))); +} + +// 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, "niogoo_app"); + 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, "niogoo_app"); + } + + gtk_window_set_default_size(window, 1280, 720); + + 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); + GdkRGBA background_color; + // Background defaults to black, override it here if necessary, e.g. #00000000 + // for transparent. + gdk_rgba_parse(&background_color, "#000000"); + fl_view_set_background_color(view, &background_color); + gtk_widget_show(GTK_WIDGET(view)); + gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view)); + + // Show the window when Flutter renders. + // Requires the view to be realized so we can start rendering. + g_signal_connect_swapped(view, "first-frame", G_CALLBACK(first_frame_cb), + self); + gtk_widget_realize(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)); +} diff --git a/Mobile Operasional/linux/runner/my_application.h b/Mobile Operasional/linux/runner/my_application.h new file mode 100644 index 0000000..db16367 --- /dev/null +++ b/Mobile Operasional/linux/runner/my_application.h @@ -0,0 +1,21 @@ +#ifndef FLUTTER_MY_APPLICATION_H_ +#define FLUTTER_MY_APPLICATION_H_ + +#include + +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_ diff --git a/Mobile Operasional/macos/.gitignore b/Mobile Operasional/macos/.gitignore new file mode 100644 index 0000000..746adbb --- /dev/null +++ b/Mobile Operasional/macos/.gitignore @@ -0,0 +1,7 @@ +# Flutter-related +**/Flutter/ephemeral/ +**/Pods/ + +# Xcode-related +**/dgph +**/xcuserdata/ diff --git a/Mobile Operasional/macos/Flutter/Flutter-Debug.xcconfig b/Mobile Operasional/macos/Flutter/Flutter-Debug.xcconfig new file mode 100644 index 0000000..c2efd0b --- /dev/null +++ b/Mobile Operasional/macos/Flutter/Flutter-Debug.xcconfig @@ -0,0 +1 @@ +#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/Mobile Operasional/macos/Flutter/Flutter-Release.xcconfig b/Mobile Operasional/macos/Flutter/Flutter-Release.xcconfig new file mode 100644 index 0000000..c2efd0b --- /dev/null +++ b/Mobile Operasional/macos/Flutter/Flutter-Release.xcconfig @@ -0,0 +1 @@ +#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/Mobile Operasional/macos/Flutter/GeneratedPluginRegistrant.swift b/Mobile Operasional/macos/Flutter/GeneratedPluginRegistrant.swift new file mode 100644 index 0000000..0edd1d9 --- /dev/null +++ b/Mobile Operasional/macos/Flutter/GeneratedPluginRegistrant.swift @@ -0,0 +1,30 @@ +// +// Generated file. Do not edit. +// + +import FlutterMacOS +import Foundation + +import connectivity_plus +import file_selector_macos +import flutter_secure_storage_darwin +import gal +import geolocator_apple +import package_info_plus +import path_provider_foundation +import share_plus +import shared_preferences_foundation +import sqlite3_flutter_libs + +func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { + ConnectivityPlusPlugin.register(with: registry.registrar(forPlugin: "ConnectivityPlusPlugin")) + FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin")) + FlutterSecureStorageDarwinPlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStorageDarwinPlugin")) + GalPlugin.register(with: registry.registrar(forPlugin: "GalPlugin")) + GeolocatorPlugin.register(with: registry.registrar(forPlugin: "GeolocatorPlugin")) + FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin")) + PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) + SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin")) + SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) + Sqlite3FlutterLibsPlugin.register(with: registry.registrar(forPlugin: "Sqlite3FlutterLibsPlugin")) +} diff --git a/Mobile Operasional/macos/Runner.xcodeproj/project.pbxproj b/Mobile Operasional/macos/Runner.xcodeproj/project.pbxproj new file mode 100644 index 0000000..6bd129d --- /dev/null +++ b/Mobile Operasional/macos/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,705 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 54; + objects = { + +/* Begin PBXAggregateTarget section */ + 33CC111A2044C6BA0003C045 /* Flutter Assemble */ = { + isa = PBXAggregateTarget; + buildConfigurationList = 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */; + buildPhases = ( + 33CC111E2044C6BF0003C045 /* ShellScript */, + ); + dependencies = ( + ); + name = "Flutter Assemble"; + productName = FLX; + }; +/* End PBXAggregateTarget section */ + +/* Begin PBXBuildFile section */ + 331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C80D7294CF71000263BE5 /* RunnerTests.swift */; }; + 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */; }; + 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC10F02044A3C60003C045 /* AppDelegate.swift */; }; + 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; }; + 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; }; + 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 331C80D9294CF71000263BE5 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 33CC10E52044A3C60003C045 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 33CC10EC2044A3C60003C045; + remoteInfo = Runner; + }; + 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 33CC10E52044A3C60003C045 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 33CC111A2044C6BA0003C045; + remoteInfo = FLX; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 33CC110E2044A8840003C045 /* Bundle Framework */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Bundle Framework"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 331C80D5294CF71000263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 331C80D7294CF71000263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; + 333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = ""; }; + 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = ""; }; + 33CC10ED2044A3C60003C045 /* niogoo_app.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "niogoo_app.app"; sourceTree = BUILT_PRODUCTS_DIR; }; + 33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = ""; }; + 33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; }; + 33CC10F72044A3C60003C045 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Info.plist; path = Runner/Info.plist; sourceTree = ""; }; + 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainFlutterWindow.swift; sourceTree = ""; }; + 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Debug.xcconfig"; sourceTree = ""; }; + 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Release.xcconfig"; sourceTree = ""; }; + 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = "Flutter-Generated.xcconfig"; path = "ephemeral/Flutter-Generated.xcconfig"; sourceTree = ""; }; + 33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = ""; }; + 33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = ""; }; + 33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 331C80D2294CF70F00263BE5 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 33CC10EA2044A3C60003C045 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 331C80D6294CF71000263BE5 /* RunnerTests */ = { + isa = PBXGroup; + children = ( + 331C80D7294CF71000263BE5 /* RunnerTests.swift */, + ); + path = RunnerTests; + sourceTree = ""; + }; + 33BA886A226E78AF003329D5 /* Configs */ = { + isa = PBXGroup; + children = ( + 33E5194F232828860026EE4D /* AppInfo.xcconfig */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 333000ED22D3DE5D00554162 /* Warnings.xcconfig */, + ); + path = Configs; + sourceTree = ""; + }; + 33CC10E42044A3C60003C045 = { + isa = PBXGroup; + children = ( + 33FAB671232836740065AC1E /* Runner */, + 33CEB47122A05771004F2AC0 /* Flutter */, + 331C80D6294CF71000263BE5 /* RunnerTests */, + 33CC10EE2044A3C60003C045 /* Products */, + D73912EC22F37F3D000D13A0 /* Frameworks */, + ); + sourceTree = ""; + }; + 33CC10EE2044A3C60003C045 /* Products */ = { + isa = PBXGroup; + children = ( + 33CC10ED2044A3C60003C045 /* niogoo_app.app */, + 331C80D5294CF71000263BE5 /* RunnerTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 33CC11242044D66E0003C045 /* Resources */ = { + isa = PBXGroup; + children = ( + 33CC10F22044A3C60003C045 /* Assets.xcassets */, + 33CC10F42044A3C60003C045 /* MainMenu.xib */, + 33CC10F72044A3C60003C045 /* Info.plist */, + ); + name = Resources; + path = ..; + sourceTree = ""; + }; + 33CEB47122A05771004F2AC0 /* Flutter */ = { + isa = PBXGroup; + children = ( + 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */, + 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */, + 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */, + 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */, + ); + path = Flutter; + sourceTree = ""; + }; + 33FAB671232836740065AC1E /* Runner */ = { + isa = PBXGroup; + children = ( + 33CC10F02044A3C60003C045 /* AppDelegate.swift */, + 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */, + 33E51913231747F40026EE4D /* DebugProfile.entitlements */, + 33E51914231749380026EE4D /* Release.entitlements */, + 33CC11242044D66E0003C045 /* Resources */, + 33BA886A226E78AF003329D5 /* Configs */, + ); + path = Runner; + sourceTree = ""; + }; + D73912EC22F37F3D000D13A0 /* Frameworks */ = { + isa = PBXGroup; + children = ( + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 331C80D4294CF70F00263BE5 /* RunnerTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; + buildPhases = ( + 331C80D1294CF70F00263BE5 /* Sources */, + 331C80D2294CF70F00263BE5 /* Frameworks */, + 331C80D3294CF70F00263BE5 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 331C80DA294CF71000263BE5 /* PBXTargetDependency */, + ); + name = RunnerTests; + productName = RunnerTests; + productReference = 331C80D5294CF71000263BE5 /* RunnerTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 33CC10EC2044A3C60003C045 /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + 33CC10E92044A3C60003C045 /* Sources */, + 33CC10EA2044A3C60003C045 /* Frameworks */, + 33CC10EB2044A3C60003C045 /* Resources */, + 33CC110E2044A8840003C045 /* Bundle Framework */, + 3399D490228B24CF009A79C7 /* ShellScript */, + ); + buildRules = ( + ); + dependencies = ( + 33CC11202044C79F0003C045 /* PBXTargetDependency */, + ); + name = Runner; + productName = Runner; + productReference = 33CC10ED2044A3C60003C045 /* niogoo_app.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 33CC10E52044A3C60003C045 /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = YES; + LastSwiftUpdateCheck = 0920; + LastUpgradeCheck = 1510; + ORGANIZATIONNAME = ""; + TargetAttributes = { + 331C80D4294CF70F00263BE5 = { + CreatedOnToolsVersion = 14.0; + TestTargetID = 33CC10EC2044A3C60003C045; + }; + 33CC10EC2044A3C60003C045 = { + CreatedOnToolsVersion = 9.2; + LastSwiftMigration = 1100; + ProvisioningStyle = Automatic; + SystemCapabilities = { + com.apple.Sandbox = { + enabled = 1; + }; + }; + }; + 33CC111A2044C6BA0003C045 = { + CreatedOnToolsVersion = 9.2; + ProvisioningStyle = Manual; + }; + }; + }; + buildConfigurationList = 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 33CC10E42044A3C60003C045; + productRefGroup = 33CC10EE2044A3C60003C045 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 33CC10EC2044A3C60003C045 /* Runner */, + 331C80D4294CF70F00263BE5 /* RunnerTests */, + 33CC111A2044C6BA0003C045 /* Flutter Assemble */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 331C80D3294CF70F00263BE5 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 33CC10EB2044A3C60003C045 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */, + 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 3399D490228B24CF009A79C7 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "echo \"$PRODUCT_NAME.app\" > \"$PROJECT_DIR\"/Flutter/ephemeral/.app_filename && \"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh embed\n"; + }; + 33CC111E2044C6BF0003C045 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + Flutter/ephemeral/FlutterInputs.xcfilelist, + ); + inputPaths = ( + Flutter/ephemeral/tripwire, + ); + outputFileListPaths = ( + Flutter/ephemeral/FlutterOutputs.xcfilelist, + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire"; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 331C80D1294CF70F00263BE5 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 33CC10E92044A3C60003C045 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */, + 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */, + 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 331C80DA294CF71000263BE5 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 33CC10EC2044A3C60003C045 /* Runner */; + targetProxy = 331C80D9294CF71000263BE5 /* PBXContainerItemProxy */; + }; + 33CC11202044C79F0003C045 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 33CC111A2044C6BA0003C045 /* Flutter Assemble */; + targetProxy = 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 33CC10F42044A3C60003C045 /* MainMenu.xib */ = { + isa = PBXVariantGroup; + children = ( + 33CC10F52044A3C60003C045 /* Base */, + ); + name = MainMenu.xib; + path = Runner; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 331C80DB294CF71000263BE5 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.niogooApp.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/niogoo_app.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/niogoo_app"; + }; + name = Debug; + }; + 331C80DC294CF71000263BE5 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.niogooApp.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/niogoo_app.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/niogoo_app"; + }; + name = Release; + }; + 331C80DD294CF71000263BE5 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.niogooApp.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/niogoo_app.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/niogoo_app"; + }; + name = Profile; + }; + 338D0CE9231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + 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_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + 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_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = YES; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.15; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + }; + name = Profile; + }; + 338D0CEA231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_VERSION = 5.0; + }; + name = Profile; + }; + 338D0CEB231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Manual; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Profile; + }; + 33CC10F92044A3C60003C045 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + 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_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + 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_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = YES; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu11; + 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_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.15; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = macosx; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 33CC10FA2044A3C60003C045 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + 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_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + 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_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = YES; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.15; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + }; + name = Release; + }; + 33CC10FC2044A3C60003C045 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + }; + name = Debug; + }; + 33CC10FD2044A3C60003C045 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_VERSION = 5.0; + }; + name = Release; + }; + 33CC111C2044C6BA0003C045 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Manual; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Debug; + }; + 33CC111D2044C6BA0003C045 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 331C80DB294CF71000263BE5 /* Debug */, + 331C80DC294CF71000263BE5 /* Release */, + 331C80DD294CF71000263BE5 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC10F92044A3C60003C045 /* Debug */, + 33CC10FA2044A3C60003C045 /* Release */, + 338D0CE9231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC10FC2044A3C60003C045 /* Debug */, + 33CC10FD2044A3C60003C045 /* Release */, + 338D0CEA231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC111C2044C6BA0003C045 /* Debug */, + 33CC111D2044C6BA0003C045 /* Release */, + 338D0CEB231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 33CC10E52044A3C60003C045 /* Project object */; +} diff --git a/Mobile Operasional/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/Mobile Operasional/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/Mobile Operasional/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/Mobile Operasional/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/Mobile Operasional/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 0000000..aa22aed --- /dev/null +++ b/Mobile Operasional/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,99 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Mobile Operasional/macos/Runner.xcworkspace/contents.xcworkspacedata b/Mobile Operasional/macos/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..1d526a1 --- /dev/null +++ b/Mobile Operasional/macos/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/Mobile Operasional/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/Mobile Operasional/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/Mobile Operasional/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/Mobile Operasional/macos/Runner/AppDelegate.swift b/Mobile Operasional/macos/Runner/AppDelegate.swift new file mode 100644 index 0000000..b3c1761 --- /dev/null +++ b/Mobile Operasional/macos/Runner/AppDelegate.swift @@ -0,0 +1,13 @@ +import Cocoa +import FlutterMacOS + +@main +class AppDelegate: FlutterAppDelegate { + override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { + return true + } + + override func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool { + return true + } +} diff --git a/Mobile Operasional/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/Mobile Operasional/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..a2ec33f --- /dev/null +++ b/Mobile Operasional/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,68 @@ +{ + "images" : [ + { + "size" : "16x16", + "idiom" : "mac", + "filename" : "app_icon_16.png", + "scale" : "1x" + }, + { + "size" : "16x16", + "idiom" : "mac", + "filename" : "app_icon_32.png", + "scale" : "2x" + }, + { + "size" : "32x32", + "idiom" : "mac", + "filename" : "app_icon_32.png", + "scale" : "1x" + }, + { + "size" : "32x32", + "idiom" : "mac", + "filename" : "app_icon_64.png", + "scale" : "2x" + }, + { + "size" : "128x128", + "idiom" : "mac", + "filename" : "app_icon_128.png", + "scale" : "1x" + }, + { + "size" : "128x128", + "idiom" : "mac", + "filename" : "app_icon_256.png", + "scale" : "2x" + }, + { + "size" : "256x256", + "idiom" : "mac", + "filename" : "app_icon_256.png", + "scale" : "1x" + }, + { + "size" : "256x256", + "idiom" : "mac", + "filename" : "app_icon_512.png", + "scale" : "2x" + }, + { + "size" : "512x512", + "idiom" : "mac", + "filename" : "app_icon_512.png", + "scale" : "1x" + }, + { + "size" : "512x512", + "idiom" : "mac", + "filename" : "app_icon_1024.png", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/Mobile Operasional/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png b/Mobile Operasional/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png new file mode 100644 index 0000000..82b6f9d Binary files /dev/null and b/Mobile Operasional/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png differ diff --git a/Mobile Operasional/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png b/Mobile Operasional/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png new file mode 100644 index 0000000..13b35eb Binary files /dev/null and b/Mobile Operasional/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png differ diff --git a/Mobile Operasional/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png b/Mobile Operasional/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png new file mode 100644 index 0000000..0a3f5fa Binary files /dev/null and b/Mobile Operasional/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png differ diff --git a/Mobile Operasional/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png b/Mobile Operasional/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png new file mode 100644 index 0000000..bdb5722 Binary files /dev/null and b/Mobile Operasional/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png differ diff --git a/Mobile Operasional/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png b/Mobile Operasional/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png new file mode 100644 index 0000000..f083318 Binary files /dev/null and b/Mobile Operasional/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png differ diff --git a/Mobile Operasional/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png b/Mobile Operasional/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png new file mode 100644 index 0000000..326c0e7 Binary files /dev/null and b/Mobile Operasional/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png differ diff --git a/Mobile Operasional/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png b/Mobile Operasional/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png new file mode 100644 index 0000000..2f1632c Binary files /dev/null and b/Mobile Operasional/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png differ diff --git a/Mobile Operasional/macos/Runner/Base.lproj/MainMenu.xib b/Mobile Operasional/macos/Runner/Base.lproj/MainMenu.xib new file mode 100644 index 0000000..80e867a --- /dev/null +++ b/Mobile Operasional/macos/Runner/Base.lproj/MainMenu.xib @@ -0,0 +1,343 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Mobile Operasional/macos/Runner/Configs/AppInfo.xcconfig b/Mobile Operasional/macos/Runner/Configs/AppInfo.xcconfig new file mode 100644 index 0000000..5379d8b --- /dev/null +++ b/Mobile Operasional/macos/Runner/Configs/AppInfo.xcconfig @@ -0,0 +1,14 @@ +// Application-level settings for the Runner target. +// +// This may be replaced with something auto-generated from metadata (e.g., pubspec.yaml) in the +// future. If not, the values below would default to using the project name when this becomes a +// 'flutter create' template. + +// The application's name. By default this is also the title of the Flutter window. +PRODUCT_NAME = niogoo_app + +// The application's bundle identifier +PRODUCT_BUNDLE_IDENTIFIER = com.example.niogooApp + +// The copyright displayed in application information +PRODUCT_COPYRIGHT = Copyright © 2025 com.example. All rights reserved. diff --git a/Mobile Operasional/macos/Runner/Configs/Debug.xcconfig b/Mobile Operasional/macos/Runner/Configs/Debug.xcconfig new file mode 100644 index 0000000..36b0fd9 --- /dev/null +++ b/Mobile Operasional/macos/Runner/Configs/Debug.xcconfig @@ -0,0 +1,2 @@ +#include "../../Flutter/Flutter-Debug.xcconfig" +#include "Warnings.xcconfig" diff --git a/Mobile Operasional/macos/Runner/Configs/Release.xcconfig b/Mobile Operasional/macos/Runner/Configs/Release.xcconfig new file mode 100644 index 0000000..dff4f49 --- /dev/null +++ b/Mobile Operasional/macos/Runner/Configs/Release.xcconfig @@ -0,0 +1,2 @@ +#include "../../Flutter/Flutter-Release.xcconfig" +#include "Warnings.xcconfig" diff --git a/Mobile Operasional/macos/Runner/Configs/Warnings.xcconfig b/Mobile Operasional/macos/Runner/Configs/Warnings.xcconfig new file mode 100644 index 0000000..42bcbf4 --- /dev/null +++ b/Mobile Operasional/macos/Runner/Configs/Warnings.xcconfig @@ -0,0 +1,13 @@ +WARNING_CFLAGS = -Wall -Wconditional-uninitialized -Wnullable-to-nonnull-conversion -Wmissing-method-return-type -Woverlength-strings +GCC_WARN_UNDECLARED_SELECTOR = YES +CLANG_UNDEFINED_BEHAVIOR_SANITIZER_NULLABILITY = YES +CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE +CLANG_WARN__DUPLICATE_METHOD_MATCH = YES +CLANG_WARN_PRAGMA_PACK = YES +CLANG_WARN_STRICT_PROTOTYPES = YES +CLANG_WARN_COMMA = YES +GCC_WARN_STRICT_SELECTOR_MATCH = YES +CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES +CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES +GCC_WARN_SHADOW = YES +CLANG_WARN_UNREACHABLE_CODE = YES diff --git a/Mobile Operasional/macos/Runner/DebugProfile.entitlements b/Mobile Operasional/macos/Runner/DebugProfile.entitlements new file mode 100644 index 0000000..dddb8a3 --- /dev/null +++ b/Mobile Operasional/macos/Runner/DebugProfile.entitlements @@ -0,0 +1,12 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.cs.allow-jit + + com.apple.security.network.server + + + diff --git a/Mobile Operasional/macos/Runner/Info.plist b/Mobile Operasional/macos/Runner/Info.plist new file mode 100644 index 0000000..4789daa --- /dev/null +++ b/Mobile Operasional/macos/Runner/Info.plist @@ -0,0 +1,32 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIconFile + + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSMinimumSystemVersion + $(MACOSX_DEPLOYMENT_TARGET) + NSHumanReadableCopyright + $(PRODUCT_COPYRIGHT) + NSMainNibFile + MainMenu + NSPrincipalClass + NSApplication + + diff --git a/Mobile Operasional/macos/Runner/MainFlutterWindow.swift b/Mobile Operasional/macos/Runner/MainFlutterWindow.swift new file mode 100644 index 0000000..3cc05eb --- /dev/null +++ b/Mobile Operasional/macos/Runner/MainFlutterWindow.swift @@ -0,0 +1,15 @@ +import Cocoa +import FlutterMacOS + +class MainFlutterWindow: NSWindow { + override func awakeFromNib() { + let flutterViewController = FlutterViewController() + let windowFrame = self.frame + self.contentViewController = flutterViewController + self.setFrame(windowFrame, display: true) + + RegisterGeneratedPlugins(registry: flutterViewController) + + super.awakeFromNib() + } +} diff --git a/Mobile Operasional/macos/Runner/Release.entitlements b/Mobile Operasional/macos/Runner/Release.entitlements new file mode 100644 index 0000000..852fa1a --- /dev/null +++ b/Mobile Operasional/macos/Runner/Release.entitlements @@ -0,0 +1,8 @@ + + + + + com.apple.security.app-sandbox + + + diff --git a/Mobile Operasional/macos/RunnerTests/RunnerTests.swift b/Mobile Operasional/macos/RunnerTests/RunnerTests.swift new file mode 100644 index 0000000..61f3bd1 --- /dev/null +++ b/Mobile Operasional/macos/RunnerTests/RunnerTests.swift @@ -0,0 +1,12 @@ +import Cocoa +import FlutterMacOS +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. + } + +} diff --git a/Mobile Operasional/pubspec.lock b/Mobile Operasional/pubspec.lock new file mode 100644 index 0000000..198fb78 --- /dev/null +++ b/Mobile Operasional/pubspec.lock @@ -0,0 +1,1503 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + _fe_analyzer_shared: + dependency: transitive + description: + name: _fe_analyzer_shared + sha256: "8d7ff3948166b8ec5da0fbb5962000926b8e02f2ed9b3e51d1738905fbd4c98d" + url: "https://pub.dev" + source: hosted + version: "93.0.0" + analyzer: + dependency: transitive + description: + name: analyzer + sha256: de7148ed2fcec579b19f122c1800933dfa028f6d9fd38a152b04b1516cec120b + url: "https://pub.dev" + source: hosted + version: "10.0.1" + args: + dependency: transitive + description: + name: args + sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04 + url: "https://pub.dev" + source: hosted + version: "2.7.0" + async: + dependency: transitive + description: + name: async + sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" + url: "https://pub.dev" + source: hosted + version: "2.13.0" + bcrypt: + dependency: "direct main" + description: + name: bcrypt + sha256: "6073a700cbbc59f1d4ab27cd532755e3de5e676c4941f535f351374df849270b" + url: "https://pub.dev" + source: hosted + version: "1.2.0" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + build: + dependency: transitive + description: + name: build + sha256: "275bf6bb2a00a9852c28d4e0b410da1d833a734d57d39d44f94bfc895a484ec3" + url: "https://pub.dev" + source: hosted + version: "4.0.4" + build_config: + dependency: transitive + description: + name: build_config + sha256: "4070d2a59f8eec34c97c86ceb44403834899075f66e8a9d59706f8e7834f6f71" + url: "https://pub.dev" + source: hosted + version: "1.3.0" + build_daemon: + dependency: transitive + description: + name: build_daemon + sha256: bf05f6e12cfea92d3c09308d7bcdab1906cd8a179b023269eed00c071004b957 + url: "https://pub.dev" + source: hosted + version: "4.1.1" + build_runner: + dependency: "direct dev" + description: + name: build_runner + sha256: "7981eb922842c77033026eb4341d5af651562008cdb116bdfa31fc46516b6462" + url: "https://pub.dev" + source: hosted + version: "2.12.2" + built_collection: + dependency: transitive + description: + name: built_collection + sha256: "376e3dd27b51ea877c28d525560790aee2e6fbb5f20e2f85d5081027d94e2100" + url: "https://pub.dev" + source: hosted + version: "5.1.1" + built_value: + dependency: transitive + description: + name: built_value + sha256: "6ae8a6435a8c6520c7077b107e77f1fb4ba7009633259a4d49a8afd8e7efc5e9" + url: "https://pub.dev" + source: hosted + version: "8.12.4" + calendar_date_picker2: + dependency: "direct main" + description: + name: calendar_date_picker2 + sha256: "9c9b5586fb512bf1181d7f3a6273bffa9e65a4e16689902e112771e7d71d063b" + url: "https://pub.dev" + source: hosted + version: "2.0.1" + characters: + dependency: transitive + description: + name: characters + sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 + url: "https://pub.dev" + source: hosted + version: "1.4.0" + charcode: + dependency: transitive + description: + name: charcode + sha256: fb0f1107cac15a5ea6ef0a6ef71a807b9e4267c713bb93e00e92d737cc8dbd8a + url: "https://pub.dev" + source: hosted + version: "1.4.0" + checked_yaml: + dependency: transitive + description: + name: checked_yaml + sha256: "959525d3162f249993882720d52b7e0c833978df229be20702b33d48d91de70f" + url: "https://pub.dev" + source: hosted + version: "2.0.4" + cli_util: + dependency: transitive + description: + name: cli_util + sha256: ff6785f7e9e3c38ac98b2fb035701789de90154024a75b6cb926445e83197d1c + url: "https://pub.dev" + source: hosted + version: "0.4.2" + clock: + dependency: transitive + description: + name: clock + sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b + url: "https://pub.dev" + source: hosted + version: "1.1.2" + code_assets: + dependency: transitive + description: + name: code_assets + sha256: "83ccdaa064c980b5596c35dd64a8d3ecc68620174ab9b90b6343b753aa721687" + url: "https://pub.dev" + source: hosted + version: "1.0.0" + code_builder: + dependency: transitive + description: + name: code_builder + sha256: "6a6cab2ba4680d6423f34a9b972a4c9a94ebe1b62ecec4e1a1f2cba91fd1319d" + url: "https://pub.dev" + source: hosted + version: "4.11.1" + collection: + dependency: transitive + description: + name: collection + sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" + url: "https://pub.dev" + source: hosted + version: "1.19.1" + connectivity_plus: + dependency: "direct main" + description: + name: connectivity_plus + sha256: "33bae12a398f841c6cda09d1064212957265869104c478e5ad51e2fb26c3973c" + url: "https://pub.dev" + source: hosted + version: "7.0.0" + connectivity_plus_platform_interface: + dependency: transitive + description: + name: connectivity_plus_platform_interface + sha256: "42657c1715d48b167930d5f34d00222ac100475f73d10162ddf43e714932f204" + url: "https://pub.dev" + source: hosted + version: "2.0.1" + convert: + dependency: transitive + description: + name: convert + sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68 + url: "https://pub.dev" + source: hosted + version: "3.1.2" + cross_file: + dependency: transitive + description: + name: cross_file + sha256: "28bb3ae56f117b5aec029d702a90f57d285cd975c3c5c281eaca38dbc47c5937" + url: "https://pub.dev" + source: hosted + version: "0.3.5+2" + crypto: + dependency: transitive + description: + name: crypto + sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf + url: "https://pub.dev" + source: hosted + version: "3.0.7" + cupertino_icons: + dependency: "direct main" + description: + name: cupertino_icons + sha256: ba631d1c7f7bef6b729a622b7b752645a2d076dba9976925b8f25725a30e1ee6 + url: "https://pub.dev" + source: hosted + version: "1.0.8" + dart_earcut: + dependency: transitive + description: + name: dart_earcut + sha256: e485001bfc05dcbc437d7bfb666316182e3522d4c3f9668048e004d0eb2ce43b + url: "https://pub.dev" + source: hosted + version: "1.2.0" + dart_polylabel2: + dependency: transitive + description: + name: dart_polylabel2 + sha256: "7eeab15ce72894e4bdba6a8765712231fc81be0bd95247de4ad9966abc57adc6" + url: "https://pub.dev" + source: hosted + version: "1.0.0" + dart_style: + dependency: transitive + description: + name: dart_style + sha256: "29f7ecc274a86d32920b1d9cfc7502fa87220da41ec60b55f329559d5732e2b2" + url: "https://pub.dev" + source: hosted + version: "3.1.7" + dbus: + dependency: transitive + description: + name: dbus + sha256: d0c98dcd4f5169878b6cf8f6e0a52403a9dff371a3e2f019697accbf6f44a270 + url: "https://pub.dev" + source: hosted + version: "0.7.12" + device_frame: + dependency: transitive + description: + name: device_frame + sha256: "7b2ebb2a09d6cc0f086b51bd1412d7be83e0170056a7290349169be41164c86a" + url: "https://pub.dev" + source: hosted + version: "1.4.0" + device_preview: + dependency: "direct main" + description: + name: device_preview + sha256: "88aa1cc73ee9a8ec771b309dcbc4000cc66b5d8456b825980997640ab1195bf5" + url: "https://pub.dev" + source: hosted + version: "1.3.1" + dio: + dependency: "direct main" + description: + name: dio + sha256: aff32c08f92787a557dd5c0145ac91536481831a01b4648136373cddb0e64f8c + url: "https://pub.dev" + source: hosted + version: "5.9.2" + dio_web_adapter: + dependency: transitive + description: + name: dio_web_adapter + sha256: "2f9e64323a7c3c7ef69567d5c800424a11f8337b8b228bad02524c9fb3c1f340" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + drift: + dependency: "direct main" + description: + name: drift + sha256: "61f876c0291b194980bafd203f48e85d5fb04e4a7334367d1a89f44004dbcb83" + url: "https://pub.dev" + source: hosted + version: "2.32.0" + drift_dev: + dependency: "direct dev" + description: + name: drift_dev + sha256: d687e955cc4b1706ad49b3860fcc1045c09bbf1d84c3c7383615f7f9c3320aa2 + url: "https://pub.dev" + source: hosted + version: "2.32.0" + fake_async: + dependency: transitive + description: + name: fake_async + sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" + url: "https://pub.dev" + source: hosted + version: "1.3.3" + ffi: + dependency: transitive + description: + name: ffi + sha256: "6d7fd89431262d8f3125e81b50d3847a091d846eafcd4fdb88dd06f36d705a45" + url: "https://pub.dev" + source: hosted + version: "2.2.0" + file: + dependency: transitive + description: + name: file + sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 + url: "https://pub.dev" + source: hosted + version: "7.0.1" + file_selector_linux: + dependency: transitive + description: + name: file_selector_linux + sha256: "2567f398e06ac72dcf2e98a0c95df2a9edd03c2c2e0cacd4780f20cdf56263a0" + url: "https://pub.dev" + source: hosted + version: "0.9.4" + file_selector_macos: + dependency: transitive + description: + name: file_selector_macos + sha256: "5e0bbe9c312416f1787a68259ea1505b52f258c587f12920422671807c4d618a" + url: "https://pub.dev" + source: hosted + version: "0.9.5" + file_selector_platform_interface: + dependency: transitive + description: + name: file_selector_platform_interface + sha256: "35e0bd61ebcdb91a3505813b055b09b79dfdc7d0aee9c09a7ba59ae4bb13dc85" + url: "https://pub.dev" + source: hosted + version: "2.7.0" + file_selector_windows: + dependency: transitive + description: + name: file_selector_windows + sha256: "62197474ae75893a62df75939c777763d39c2bc5f73ce5b88497208bc269abfd" + url: "https://pub.dev" + source: hosted + version: "0.9.3+5" + fixnum: + dependency: transitive + description: + name: fixnum + sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be + url: "https://pub.dev" + source: hosted + version: "1.1.1" + flutter: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_contacts: + dependency: "direct main" + description: + name: flutter_contacts + sha256: "388d32cd33f16640ee169570128c933b45f3259bddbfae7a100bb49e5ffea9ae" + url: "https://pub.dev" + source: hosted + version: "1.1.9+2" + flutter_localizations: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_map: + dependency: "direct main" + description: + name: flutter_map + sha256: "391e7dc95cc3f5190748210a69d4cfeb5d8f84dcdfa9c3235d0a9d7742ccb3f8" + url: "https://pub.dev" + source: hosted + version: "8.2.2" + flutter_plugin_android_lifecycle: + dependency: transitive + description: + name: flutter_plugin_android_lifecycle + sha256: ee8068e0e1cd16c4a82714119918efdeed33b3ba7772c54b5d094ab53f9b7fd1 + url: "https://pub.dev" + source: hosted + version: "2.0.33" + flutter_riverpod: + dependency: "direct main" + description: + name: flutter_riverpod + sha256: "9532ee6db4a943a1ed8383072a2e3eeda041db5657cdf6d2acecf3c21ecbe7e1" + url: "https://pub.dev" + source: hosted + version: "2.6.1" + flutter_secure_storage: + dependency: "direct main" + description: + name: flutter_secure_storage + sha256: da922f2aab2d733db7e011a6bcc4a825b844892d4edd6df83ff156b09a9b2e40 + url: "https://pub.dev" + source: hosted + version: "10.0.0" + flutter_secure_storage_darwin: + dependency: transitive + description: + name: flutter_secure_storage_darwin + sha256: "8878c25136a79def1668c75985e8e193d9d7d095453ec28730da0315dc69aee3" + url: "https://pub.dev" + source: hosted + version: "0.2.0" + flutter_secure_storage_linux: + dependency: transitive + description: + name: flutter_secure_storage_linux + sha256: "2b5c76dce569ab752d55a1cee6a2242bcc11fdba927078fb88c503f150767cda" + url: "https://pub.dev" + source: hosted + version: "3.0.0" + flutter_secure_storage_platform_interface: + dependency: transitive + description: + name: flutter_secure_storage_platform_interface + sha256: "8ceea1223bee3c6ac1a22dabd8feefc550e4729b3675de4b5900f55afcb435d6" + url: "https://pub.dev" + source: hosted + version: "2.0.1" + flutter_secure_storage_web: + dependency: transitive + description: + name: flutter_secure_storage_web + sha256: "6a1137df62b84b54261dca582c1c09ea72f4f9a4b2fcee21b025964132d5d0c3" + url: "https://pub.dev" + source: hosted + version: "2.1.0" + flutter_secure_storage_windows: + dependency: transitive + description: + name: flutter_secure_storage_windows + sha256: "3b7c8e068875dfd46719ff57c90d8c459c87f2302ed6b00ff006b3c9fcad1613" + url: "https://pub.dev" + source: hosted + version: "4.1.0" + flutter_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" + flutter_web_plugins: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + freezed_annotation: + dependency: transitive + description: + name: freezed_annotation + sha256: "7294967ff0a6d98638e7acb774aac3af2550777accd8149c90af5b014e6d44d8" + url: "https://pub.dev" + source: hosted + version: "3.1.0" + gal: + dependency: "direct main" + description: + name: gal + sha256: "969598f986789127fd407a750413249e1352116d4c2be66e81837ffeeaafdfee" + url: "https://pub.dev" + source: hosted + version: "2.3.2" + geoclue: + dependency: transitive + description: + name: geoclue + sha256: c2a998c77474fc57aa00c6baa2928e58f4b267649057a1c76738656e9dbd2a7f + url: "https://pub.dev" + source: hosted + version: "0.1.1" + geocoding: + dependency: "direct main" + description: + name: geocoding + sha256: "606be036287842d779d7ec4e2f6c9435fc29bbbd3c6da6589710f981d8852895" + url: "https://pub.dev" + source: hosted + version: "4.0.0" + geocoding_android: + dependency: transitive + description: + name: geocoding_android + sha256: ba810da90d6633cbb82bbab630e5b4a3b7d23503263c00ae7f1ef0316dcae5b9 + url: "https://pub.dev" + source: hosted + version: "4.0.1" + geocoding_ios: + dependency: transitive + description: + name: geocoding_ios + sha256: "18ab1c8369e2b0dcb3a8ccc907319334f35ee8cf4cfef4d9c8e23b13c65cb825" + url: "https://pub.dev" + source: hosted + version: "3.1.0" + geocoding_platform_interface: + dependency: transitive + description: + name: geocoding_platform_interface + sha256: "8c2c8226e5c276594c2e18bfe88b19110ed770aeb7c1ab50ede570be8b92229b" + url: "https://pub.dev" + source: hosted + version: "3.2.0" + geolocator: + dependency: "direct main" + description: + name: geolocator + sha256: "79939537046c9025be47ec645f35c8090ecadb6fe98eba146a0d25e8c1357516" + url: "https://pub.dev" + source: hosted + version: "14.0.2" + geolocator_android: + dependency: transitive + description: + name: geolocator_android + sha256: "179c3cb66dfa674fc9ccbf2be872a02658724d1c067634e2c427cf6df7df901a" + url: "https://pub.dev" + source: hosted + version: "5.0.2" + geolocator_apple: + dependency: transitive + description: + name: geolocator_apple + sha256: dbdd8789d5aaf14cf69f74d4925ad1336b4433a6efdf2fce91e8955dc921bf22 + url: "https://pub.dev" + source: hosted + version: "2.3.13" + geolocator_linux: + dependency: transitive + description: + name: geolocator_linux + sha256: d64112a205931926f4363bb6bd48f14cb38e7326833041d170615586cd143797 + url: "https://pub.dev" + source: hosted + version: "0.2.4" + geolocator_platform_interface: + dependency: transitive + description: + name: geolocator_platform_interface + sha256: "30cb64f0b9adcc0fb36f628b4ebf4f731a2961a0ebd849f4b56200205056fe67" + url: "https://pub.dev" + source: hosted + version: "4.2.6" + geolocator_web: + dependency: transitive + description: + name: geolocator_web + sha256: b1ae9bdfd90f861fde8fd4f209c37b953d65e92823cb73c7dee1fa021b06f172 + url: "https://pub.dev" + source: hosted + version: "4.1.3" + geolocator_windows: + dependency: transitive + description: + name: geolocator_windows + sha256: "175435404d20278ffd220de83c2ca293b73db95eafbdc8131fe8609be1421eb6" + url: "https://pub.dev" + source: hosted + version: "0.2.5" + glob: + dependency: transitive + description: + name: glob + sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de + url: "https://pub.dev" + source: hosted + version: "2.1.3" + go_router: + dependency: "direct main" + description: + name: go_router + sha256: d8f590a69729f719177ea68eb1e598295e8dbc41bbc247fed78b2c8a25660d7c + url: "https://pub.dev" + source: hosted + version: "16.3.0" + graphs: + dependency: transitive + description: + name: graphs + sha256: "741bbf84165310a68ff28fe9e727332eef1407342fca52759cb21ad8177bb8d0" + url: "https://pub.dev" + source: hosted + version: "2.3.2" + gsettings: + dependency: transitive + description: + name: gsettings + sha256: "1b0ce661f5436d2db1e51f3c4295a49849f03d304003a7ba177d01e3a858249c" + url: "https://pub.dev" + source: hosted + version: "0.2.8" + hooks: + dependency: transitive + description: + name: hooks + sha256: e79ed1e8e1929bc6ecb6ec85f0cb519c887aa5b423705ded0d0f2d9226def388 + url: "https://pub.dev" + source: hosted + version: "1.0.2" + http: + dependency: transitive + description: + name: http + sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412" + url: "https://pub.dev" + source: hosted + version: "1.6.0" + http_multi_server: + dependency: transitive + description: + name: http_multi_server + sha256: aa6199f908078bb1c5efb8d8638d4ae191aac11b311132c3ef48ce352fb52ef8 + url: "https://pub.dev" + source: hosted + version: "3.2.2" + http_parser: + dependency: transitive + description: + name: http_parser + sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" + url: "https://pub.dev" + source: hosted + version: "4.1.2" + image_picker: + dependency: "direct main" + description: + name: image_picker + sha256: "784210112be18ea55f69d7076e2c656a4e24949fa9e76429fe53af0c0f4fa320" + url: "https://pub.dev" + source: hosted + version: "1.2.1" + image_picker_android: + dependency: transitive + description: + name: image_picker_android + sha256: eda9b91b7e266d9041084a42d605a74937d996b87083395c5e47835916a86156 + url: "https://pub.dev" + source: hosted + version: "0.8.13+14" + image_picker_for_web: + dependency: transitive + description: + name: image_picker_for_web + sha256: "66257a3191ab360d23a55c8241c91a6e329d31e94efa7be9cf7a212e65850214" + url: "https://pub.dev" + source: hosted + version: "3.1.1" + image_picker_ios: + dependency: transitive + description: + name: image_picker_ios + sha256: b9c4a438a9ff4f60808c9cf0039b93a42bb6c2211ef6ebb647394b2b3fa84588 + url: "https://pub.dev" + source: hosted + version: "0.8.13+6" + image_picker_linux: + dependency: transitive + description: + name: image_picker_linux + sha256: "1f81c5f2046b9ab724f85523e4af65be1d47b038160a8c8deed909762c308ed4" + url: "https://pub.dev" + source: hosted + version: "0.2.2" + image_picker_macos: + dependency: transitive + description: + name: image_picker_macos + sha256: "86f0f15a309de7e1a552c12df9ce5b59fe927e71385329355aec4776c6a8ec91" + url: "https://pub.dev" + source: hosted + version: "0.2.2+1" + image_picker_platform_interface: + dependency: transitive + description: + name: image_picker_platform_interface + sha256: "567e056716333a1647c64bb6bd873cff7622233a5c3f694be28a583d4715690c" + url: "https://pub.dev" + source: hosted + version: "2.11.1" + image_picker_windows: + dependency: transitive + description: + name: image_picker_windows + sha256: d248c86554a72b5495a31c56f060cf73a41c7ff541689327b1a7dbccc33adfae + url: "https://pub.dev" + source: hosted + version: "0.2.2" + internet_connection_checker: + dependency: "direct main" + description: + name: internet_connection_checker + sha256: "1c683e63e89c9ac66a40748b1b20889fd9804980da732bf2b58d6d5456c8e876" + url: "https://pub.dev" + source: hosted + version: "1.0.0+1" + intl: + dependency: "direct main" + description: + name: intl + sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5" + url: "https://pub.dev" + source: hosted + version: "0.20.2" + io: + dependency: transitive + description: + name: io + sha256: dfd5a80599cf0165756e3181807ed3e77daf6dd4137caaad72d0b7931597650b + url: "https://pub.dev" + source: hosted + version: "1.0.5" + json_annotation: + dependency: "direct main" + description: + name: json_annotation + sha256: cb09e7dac6210041fad964ed7fbee004f14258b4eca4040f72d1234062ace4c8 + url: "https://pub.dev" + source: hosted + version: "4.11.0" + json_serializable: + dependency: "direct dev" + description: + name: json_serializable + sha256: fbcf404b03520e6e795f6b9b39badb2b788407dfc0a50cf39158a6ae1ca78925 + url: "https://pub.dev" + source: hosted + version: "6.13.1" + latlong2: + dependency: "direct main" + description: + name: latlong2 + sha256: "98227922caf49e6056f91b6c56945ea1c7b166f28ffcd5fb8e72fc0b453cc8fe" + url: "https://pub.dev" + source: hosted + version: "0.9.1" + leak_tracker: + dependency: transitive + description: + name: leak_tracker + sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de" + url: "https://pub.dev" + source: hosted + version: "11.0.2" + leak_tracker_flutter_testing: + dependency: transitive + description: + name: leak_tracker_flutter_testing + sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1" + url: "https://pub.dev" + source: hosted + version: "3.0.10" + leak_tracker_testing: + dependency: transitive + description: + name: leak_tracker_testing + sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1" + url: "https://pub.dev" + source: hosted + version: "3.0.2" + lists: + dependency: transitive + description: + name: lists + sha256: "4ca5c19ae4350de036a7e996cdd1ee39c93ac0a2b840f4915459b7d0a7d4ab27" + url: "https://pub.dev" + source: hosted + version: "1.0.1" + logger: + dependency: "direct main" + description: + name: logger + sha256: "25aee487596a6257655a1e091ec2ae66bc30e7af663592cc3a27e6591e05035c" + url: "https://pub.dev" + source: hosted + version: "2.7.0" + logging: + dependency: transitive + description: + name: logging + sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61 + url: "https://pub.dev" + source: hosted + version: "1.3.0" + matcher: + dependency: transitive + description: + name: matcher + sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 + url: "https://pub.dev" + source: hosted + version: "0.12.17" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec + url: "https://pub.dev" + source: hosted + version: "0.11.1" + meta: + dependency: transitive + description: + name: meta + sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" + url: "https://pub.dev" + source: hosted + version: "1.17.0" + mgrs_dart: + dependency: transitive + description: + name: mgrs_dart + sha256: fb89ae62f05fa0bb90f70c31fc870bcbcfd516c843fb554452ab3396f78586f7 + url: "https://pub.dev" + source: hosted + version: "2.0.0" + mime: + dependency: transitive + description: + name: mime + sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6" + url: "https://pub.dev" + source: hosted + version: "2.0.0" + mocktail: + dependency: "direct dev" + description: + name: mocktail + sha256: "890df3f9688106f25755f26b1c60589a92b3ab91a22b8b224947ad041bf172d8" + url: "https://pub.dev" + source: hosted + version: "1.0.4" + native_toolchain_c: + dependency: transitive + description: + name: native_toolchain_c + sha256: "92b2ca62c8bd2b8d2f267cdfccf9bfbdb7322f778f8f91b3ce5b5cda23a3899f" + url: "https://pub.dev" + source: hosted + version: "0.17.5" + nested: + dependency: transitive + description: + name: nested + sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20" + url: "https://pub.dev" + source: hosted + version: "1.0.0" + nm: + dependency: transitive + description: + name: nm + sha256: "2c9aae4127bdc8993206464fcc063611e0e36e72018696cd9631023a31b24254" + url: "https://pub.dev" + source: hosted + version: "0.5.0" + package_config: + dependency: transitive + description: + name: package_config + sha256: f096c55ebb7deb7e384101542bfba8c52696c1b56fca2eb62827989ef2353bbc + url: "https://pub.dev" + source: hosted + version: "2.2.0" + package_info_plus: + dependency: transitive + description: + name: package_info_plus + sha256: f69da0d3189a4b4ceaeb1a3defb0f329b3b352517f52bed4290f83d4f06bc08d + url: "https://pub.dev" + source: hosted + version: "9.0.0" + package_info_plus_platform_interface: + dependency: transitive + description: + name: package_info_plus_platform_interface + sha256: "202a487f08836a592a6bd4f901ac69b3a8f146af552bbd14407b6b41e1c3f086" + url: "https://pub.dev" + source: hosted + version: "3.2.1" + path: + dependency: "direct main" + description: + name: path + sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" + url: "https://pub.dev" + source: hosted + version: "1.9.1" + path_provider: + dependency: "direct main" + description: + name: path_provider + sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd" + url: "https://pub.dev" + source: hosted + version: "2.1.5" + path_provider_android: + dependency: transitive + description: + name: path_provider_android + sha256: f2c65e21139ce2c3dad46922be8272bb5963516045659e71bb16e151c93b580e + url: "https://pub.dev" + source: hosted + version: "2.2.22" + path_provider_foundation: + dependency: transitive + description: + name: path_provider_foundation + sha256: "6d13aece7b3f5c5a9731eaf553ff9dcbc2eff41087fd2df587fd0fed9a3eb0c4" + url: "https://pub.dev" + source: hosted + version: "2.5.1" + path_provider_linux: + dependency: transitive + description: + name: path_provider_linux + sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 + url: "https://pub.dev" + source: hosted + version: "2.2.1" + path_provider_platform_interface: + dependency: transitive + description: + name: path_provider_platform_interface + sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + path_provider_windows: + dependency: transitive + description: + name: path_provider_windows + sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 + url: "https://pub.dev" + source: hosted + version: "2.3.0" + permission_handler: + dependency: "direct main" + description: + name: permission_handler + sha256: bc917da36261b00137bbc8896bf1482169cd76f866282368948f032c8c1caae1 + url: "https://pub.dev" + source: hosted + version: "12.0.1" + permission_handler_android: + dependency: transitive + description: + name: permission_handler_android + sha256: "1e3bc410ca1bf84662104b100eb126e066cb55791b7451307f9708d4007350e6" + url: "https://pub.dev" + source: hosted + version: "13.0.1" + permission_handler_apple: + dependency: transitive + description: + name: permission_handler_apple + sha256: f000131e755c54cf4d84a5d8bd6e4149e262cc31c5a8b1d698de1ac85fa41023 + url: "https://pub.dev" + source: hosted + version: "9.4.7" + permission_handler_html: + dependency: transitive + description: + name: permission_handler_html + sha256: "38f000e83355abb3392140f6bc3030660cfaef189e1f87824facb76300b4ff24" + url: "https://pub.dev" + source: hosted + version: "0.1.3+5" + permission_handler_platform_interface: + dependency: transitive + description: + name: permission_handler_platform_interface + sha256: eb99b295153abce5d683cac8c02e22faab63e50679b937fa1bf67d58bb282878 + url: "https://pub.dev" + source: hosted + version: "4.3.0" + permission_handler_windows: + dependency: transitive + description: + name: permission_handler_windows + sha256: "1a790728016f79a41216d88672dbc5df30e686e811ad4e698bfc51f76ad91f1e" + url: "https://pub.dev" + source: hosted + version: "0.2.1" + petitparser: + dependency: transitive + description: + name: petitparser + sha256: "91bd59303e9f769f108f8df05e371341b15d59e995e6806aefab827b58336675" + url: "https://pub.dev" + source: hosted + version: "7.0.2" + pinput: + dependency: "direct main" + description: + name: pinput + sha256: "4c3f1b84768b47a56a1abdaca551bd7cef4ac673b882209039ecdf803a5d6e68" + url: "https://pub.dev" + source: hosted + version: "6.0.2" + platform: + dependency: transitive + description: + name: platform + sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" + url: "https://pub.dev" + source: hosted + version: "3.1.6" + plugin_platform_interface: + dependency: transitive + description: + name: plugin_platform_interface + sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" + url: "https://pub.dev" + source: hosted + version: "2.1.8" + pool: + dependency: transitive + description: + name: pool + sha256: "978783255c543aa3586a1b3c21f6e9d720eb315376a915872c61ef8b5c20177d" + url: "https://pub.dev" + source: hosted + version: "1.5.2" + proj4dart: + dependency: transitive + description: + name: proj4dart + sha256: c8a659ac9b6864aa47c171e78d41bbe6f5e1d7bd790a5814249e6b68bc44324e + url: "https://pub.dev" + source: hosted + version: "2.1.0" + provider: + dependency: transitive + description: + name: provider + sha256: "4e82183fa20e5ca25703ead7e05de9e4cceed1fbd1eadc1ac3cb6f565a09f272" + url: "https://pub.dev" + source: hosted + version: "6.1.5+1" + pub_semver: + dependency: transitive + description: + name: pub_semver + sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585" + url: "https://pub.dev" + source: hosted + version: "2.2.0" + pubspec_parse: + dependency: transitive + description: + name: pubspec_parse + sha256: "0560ba233314abbed0a48a2956f7f022cce7c3e1e73df540277da7544cad4082" + url: "https://pub.dev" + source: hosted + version: "1.5.0" + recase: + dependency: transitive + description: + name: recase + sha256: e4eb4ec2dcdee52dcf99cb4ceabaffc631d7424ee55e56f280bc039737f89213 + url: "https://pub.dev" + source: hosted + version: "4.1.0" + riverpod: + dependency: transitive + description: + name: riverpod + sha256: "59062512288d3056b2321804332a13ffdd1bf16df70dcc8e506e411280a72959" + url: "https://pub.dev" + source: hosted + version: "2.6.1" + riverpod_annotation: + dependency: "direct main" + description: + name: riverpod_annotation + sha256: e14b0bf45b71326654e2705d462f21b958f987087be850afd60578fcd502d1b8 + url: "https://pub.dev" + source: hosted + version: "2.6.1" + rxdart: + dependency: "direct main" + description: + name: rxdart + sha256: "5c3004a4a8dbb94bd4bf5412a4def4acdaa12e12f269737a5751369e12d1a962" + url: "https://pub.dev" + source: hosted + version: "0.28.0" + screenshot: + dependency: "direct main" + description: + name: screenshot + sha256: "63817697a7835e6ce82add4228e15d233b74d42975c143ad8cfe07009fab866b" + url: "https://pub.dev" + source: hosted + version: "3.0.0" + share_plus: + dependency: "direct main" + description: + name: share_plus + sha256: "14c8860d4de93d3a7e53af51bff479598c4e999605290756bbbe45cf65b37840" + url: "https://pub.dev" + source: hosted + version: "12.0.1" + share_plus_platform_interface: + dependency: transitive + description: + name: share_plus_platform_interface + sha256: "88023e53a13429bd65d8e85e11a9b484f49d4c190abbd96c7932b74d6927cc9a" + url: "https://pub.dev" + source: hosted + version: "6.1.0" + shared_preferences: + dependency: "direct main" + description: + name: shared_preferences + sha256: "2939ae520c9024cb197fc20dee269cd8cdbf564c8b5746374ec6cacdc5169e64" + url: "https://pub.dev" + source: hosted + version: "2.5.4" + shared_preferences_android: + dependency: transitive + description: + name: shared_preferences_android + sha256: "8374d6200ab33ac99031a852eba4c8eb2170c4bf20778b3e2c9eccb45384fb41" + url: "https://pub.dev" + source: hosted + version: "2.4.21" + shared_preferences_foundation: + dependency: transitive + description: + name: shared_preferences_foundation + sha256: "4e7eaffc2b17ba398759f1151415869a34771ba11ebbccd1b0145472a619a64f" + url: "https://pub.dev" + source: hosted + version: "2.5.6" + shared_preferences_linux: + dependency: transitive + description: + name: shared_preferences_linux + sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + shared_preferences_platform_interface: + dependency: transitive + description: + name: shared_preferences_platform_interface + sha256: "57cbf196c486bc2cf1f02b85784932c6094376284b3ad5779d1b1c6c6a816b80" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + shared_preferences_web: + dependency: transitive + description: + name: shared_preferences_web + sha256: c49bd060261c9a3f0ff445892695d6212ff603ef3115edbb448509d407600019 + url: "https://pub.dev" + source: hosted + version: "2.4.3" + shared_preferences_windows: + dependency: transitive + description: + name: shared_preferences_windows + sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + shelf: + dependency: transitive + description: + name: shelf + sha256: e7dd780a7ffb623c57850b33f43309312fc863fb6aa3d276a754bb299839ef12 + url: "https://pub.dev" + source: hosted + version: "1.4.2" + shelf_web_socket: + dependency: transitive + description: + name: shelf_web_socket + sha256: "3632775c8e90d6c9712f883e633716432a27758216dfb61bd86a8321c0580925" + url: "https://pub.dev" + source: hosted + version: "3.0.0" + shimmer: + dependency: "direct main" + description: + name: shimmer + sha256: "5f88c883a22e9f9f299e5ba0e4f7e6054857224976a5d9f839d4ebdc94a14ac9" + url: "https://pub.dev" + source: hosted + version: "3.0.0" + sizer: + dependency: "direct main" + description: + name: sizer + sha256: "9963c89e4d30d7c2108de3eafc0a7e6a4a8009799376ea6be5ef0a9ad87cfbad" + url: "https://pub.dev" + source: hosted + version: "3.1.3" + sky_engine: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + source_gen: + dependency: transitive + description: + name: source_gen + sha256: adc962c96fffb2de1728ef396a995aaedcafbe635abdca13d2a987ce17e57751 + url: "https://pub.dev" + source: hosted + version: "4.2.1" + source_helper: + dependency: transitive + description: + name: source_helper + sha256: "1d3b229b2934034fb2e691fbb3d53e0f75a4af7b1407f88425ed8f209bcb1b8f" + url: "https://pub.dev" + source: hosted + version: "1.3.11" + source_span: + dependency: transitive + description: + name: source_span + sha256: "56a02f1f4cd1a2d96303c0144c93bd6d909eea6bee6bf5a0e0b685edbd4c47ab" + url: "https://pub.dev" + source: hosted + version: "1.10.2" + sqlite3: + dependency: transitive + description: + name: sqlite3 + sha256: caa693ad15a587a2b4fde093b728131a1827903872171089dedb16f7665d3a91 + url: "https://pub.dev" + source: hosted + version: "3.2.0" + sqlite3_flutter_libs: + dependency: "direct main" + description: + name: sqlite3_flutter_libs + sha256: eeb9e3a45207649076b808f8a5a74d68770d0b7f26ccef6d5f43106eee5375ad + url: "https://pub.dev" + source: hosted + version: "0.5.42" + sqlparser: + dependency: transitive + description: + name: sqlparser + sha256: faebfaa581dde5b6b55c499f41532c4883943162ebc12d7138c70cfcead733dc + url: "https://pub.dev" + source: hosted + version: "0.44.2" + stack_trace: + dependency: transitive + description: + name: stack_trace + sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" + url: "https://pub.dev" + source: hosted + version: "1.12.1" + state_notifier: + dependency: transitive + description: + name: state_notifier + sha256: b8677376aa54f2d7c58280d5a007f9e8774f1968d1fb1c096adcb4792fba29bb + url: "https://pub.dev" + source: hosted + version: "1.0.0" + stream_channel: + dependency: transitive + description: + name: stream_channel + sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + stream_transform: + dependency: transitive + description: + name: stream_transform + sha256: ad47125e588cfd37a9a7f86c7d6356dde8dfe89d071d293f80ca9e9273a33871 + url: "https://pub.dev" + source: hosted + version: "2.1.1" + string_scanner: + dependency: transitive + description: + name: string_scanner + sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" + url: "https://pub.dev" + source: hosted + version: "1.4.1" + term_glyph: + dependency: transitive + description: + name: term_glyph + sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" + url: "https://pub.dev" + source: hosted + version: "1.2.2" + test_api: + dependency: transitive + description: + name: test_api + sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55 + url: "https://pub.dev" + source: hosted + version: "0.7.7" + typed_data: + dependency: transitive + description: + name: typed_data + sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 + url: "https://pub.dev" + source: hosted + version: "1.4.0" + unicode: + dependency: transitive + description: + name: unicode + sha256: "0f69e46593d65245774d4f17125c6084d2c20b4e473a983f6e21b7d7762218f1" + url: "https://pub.dev" + source: hosted + version: "0.3.1" + url_launcher_linux: + dependency: transitive + description: + name: url_launcher_linux + sha256: d5e14138b3bc193a0f63c10a53c94b91d399df0512b1f29b94a043db7482384a + url: "https://pub.dev" + source: hosted + version: "3.2.2" + url_launcher_platform_interface: + dependency: transitive + description: + name: url_launcher_platform_interface + sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029" + url: "https://pub.dev" + source: hosted + version: "2.3.2" + url_launcher_web: + dependency: transitive + description: + name: url_launcher_web + sha256: d0412fcf4c6b31ecfdb7762359b7206ffba3bbffd396c6d9f9c4616ece476c1f + url: "https://pub.dev" + source: hosted + version: "2.4.2" + url_launcher_windows: + dependency: transitive + description: + name: url_launcher_windows + sha256: "712c70ab1b99744ff066053cbe3e80c73332b38d46e5e945c98689b2e66fc15f" + url: "https://pub.dev" + source: hosted + version: "3.1.5" + uuid: + dependency: "direct main" + description: + name: uuid + sha256: "1fef9e8e11e2991bb773070d4656b7bd5d850967a2456cfc83cf47925ba79489" + url: "https://pub.dev" + source: hosted + version: "4.5.3" + vector_math: + dependency: transitive + description: + name: vector_math + sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b + url: "https://pub.dev" + source: hosted + version: "2.2.0" + vm_service: + dependency: transitive + description: + name: vm_service + sha256: "45caa6c5917fa127b5dbcfbd1fa60b14e583afdc08bfc96dda38886ca252eb60" + url: "https://pub.dev" + source: hosted + version: "15.0.2" + watcher: + dependency: transitive + description: + name: watcher + sha256: "1398c9f081a753f9226febe8900fce8f7d0a67163334e1c94a2438339d79d635" + url: "https://pub.dev" + source: hosted + version: "1.2.1" + web: + dependency: transitive + description: + name: web + sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" + url: "https://pub.dev" + source: hosted + version: "1.1.1" + web_socket: + dependency: transitive + description: + name: web_socket + sha256: "34d64019aa8e36bf9842ac014bb5d2f5586ca73df5e4d9bf5c936975cae6982c" + url: "https://pub.dev" + source: hosted + version: "1.0.1" + web_socket_channel: + dependency: transitive + description: + name: web_socket_channel + sha256: d645757fb0f4773d602444000a8131ff5d48c9e47adfe9772652dd1a4f2d45c8 + url: "https://pub.dev" + source: hosted + version: "3.0.3" + win32: + dependency: transitive + description: + name: win32 + sha256: d7cb55e04cd34096cd3a79b3330245f54cb96a370a1c27adb3c84b917de8b08e + url: "https://pub.dev" + source: hosted + version: "5.15.0" + wkt_parser: + dependency: transitive + description: + name: wkt_parser + sha256: "8a555fc60de3116c00aad67891bcab20f81a958e4219cc106e3c037aa3937f13" + url: "https://pub.dev" + source: hosted + version: "2.0.0" + workmanager: + dependency: "direct main" + description: + name: workmanager + sha256: "065673b2a465865183093806925419d311a9a5e0995aa74ccf8920fd695e2d10" + url: "https://pub.dev" + source: hosted + version: "0.9.0+3" + workmanager_android: + dependency: transitive + description: + name: workmanager_android + sha256: "9ae744db4ef891f5fcd2fb8671fccc712f4f96489a487a1411e0c8675e5e8cb7" + url: "https://pub.dev" + source: hosted + version: "0.9.0+2" + workmanager_apple: + dependency: transitive + description: + name: workmanager_apple + sha256: "1cc12ae3cbf5535e72f7ba4fde0c12dd11b757caf493a28e22d684052701f2ca" + url: "https://pub.dev" + source: hosted + version: "0.9.1+2" + workmanager_platform_interface: + dependency: transitive + description: + name: workmanager_platform_interface + sha256: f40422f10b970c67abb84230b44da22b075147637532ac501729256fcea10a47 + url: "https://pub.dev" + source: hosted + version: "0.9.1+1" + xdg_directories: + dependency: transitive + description: + name: xdg_directories + sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15" + url: "https://pub.dev" + source: hosted + version: "1.1.0" + xml: + dependency: transitive + description: + name: xml + sha256: "971043b3a0d3da28727e40ed3e0b5d18b742fa5a68665cca88e74b7876d5e025" + url: "https://pub.dev" + source: hosted + version: "6.6.1" + yaml: + dependency: transitive + description: + name: yaml + sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce + url: "https://pub.dev" + source: hosted + version: "3.1.3" +sdks: + dart: ">=3.10.1 <4.0.0" + flutter: ">=3.38.0" diff --git a/Mobile Operasional/pubspec.yaml b/Mobile Operasional/pubspec.yaml new file mode 100644 index 0000000..f4867d6 --- /dev/null +++ b/Mobile Operasional/pubspec.yaml @@ -0,0 +1,116 @@ +name: niogu_app +description: "A new Flutter project." +# The following line prevents the package from being accidentally published to +# pub.dev using `flutter pub publish`. This is preferred for private packages. +publish_to: 'none' # Remove this line if you wish to publish to pub.dev + +# The following defines the version and build number for your application. +# A version number is three numbers separated by dots, like 1.2.43 +# followed by an optional build number separated by a +. +# Both the version and the builder number may be overridden in flutter +# build by specifying --build-name and --build-number, respectively. +# In Android, build-name is used as versionName while build-number used as versionCode. +# Read more about Android versioning at https://developer.android.com/studio/publish/versioning +# In iOS, build-name is used as CFBundleShortVersionString while build-number is used as CFBundleVersion. +# Read more about iOS versioning at +# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html +# In Windows, build-name is used as the major, minor, and patch parts +# of the product and file versions while build-number is used as the build suffix. +version: 1.0.0+1 + +environment: + sdk: ^3.10.1 + +# Dependencies specify other packages that your package needs in order to work. +# To automatically upgrade your package dependencies to the latest versions +# consider running `flutter pub upgrade --major-versions`. Alternatively, +# dependencies can be manually updated by changing the version numbers below to +# the latest version available on pub.dev. To see which dependencies have newer +# versions available, run `flutter pub outdated`. +dependencies: + flutter: + sdk: flutter + + # The following adds the Cupertino Icons font to your application. + # Use with the CupertinoIcons class for iOS style icons. + cupertino_icons: ^1.0.8 + path: ^1.9.0 + drift: ^2.29.0 + sizer: ^3.1.3 + uuid: ^4.5.2 + intl: ^0.20.2 + logger: ^2.6.2 + shimmer: ^3.0.0 + go_router: ^16.1.0 + image_picker: ^1.2.1 + path_provider: ^2.1.5 + device_preview: ^1.2.0 + flutter_riverpod: ^2.6.1 + riverpod_annotation: ^2.6.1 + sqlite3_flutter_libs: ^0.5.40 + permission_handler: ^12.0.1 + shared_preferences: ^2.5.4 + calendar_date_picker2: ^2.0.1 + flutter_localizations: + sdk: flutter + flutter_contacts: ^1.1.9+2 + flutter_map: ^8.2.2 + latlong2: ^0.9.1 + geolocator: ^14.0.2 + screenshot: ^3.0.0 + share_plus: ^12.0.1 + gal: ^2.3.2 + geocoding: ^4.0.0 + bcrypt: ^1.2.0 + pinput: ^6.0.2 + dio: ^5.9.2 + json_annotation: ^4.11.0 + flutter_secure_storage: ^10.0.0 + connectivity_plus: ^7.0.0 + workmanager: ^0.9.0+3 + rxdart: ^0.28.0 + internet_connection_checker: ^1.0.0+1 + +dev_dependencies: + flutter_test: + sdk: flutter + +# For information on the generic Dart part of this file, see the +# following page: https://dart.dev/tools/pub/pubspec + drift_dev: ^2.29.0 + build_runner: ^2.4.15 + mocktail: ^1.0.4 + json_serializable: ^6.13.1 + +# The following section is specific to Flutter packages. +flutter: + + # The following line ensures that the Material Icons font is + # included with your application, so that you can use the icons in + # the material Icons class. + uses-material-design: true + + # To add assets to your application, add an assets section, like this: + assets: + - assets/images/ + + # An image asset can refer to one or more resolution-specific "variants", see + # https://flutter.dev/to/resolution-aware-images + + # For details regarding adding assets from package dependencies, see + # https://flutter.dev/to/asset-from-package + + # To add custom fonts to your application, add a fonts section here, + # in this "flutter" section. Each entry in this list should have a + # "family" key with the font family name, and a "fonts" key with a + # list giving the asset and other descriptors for the font. For + # example: + fonts: + - family: Poppins + fonts: + - asset: assets/fonts/Poppins-Regular.ttf + - asset: assets/fonts/Poppins-Bold.ttf + weight: 700 + # + # For details regarding fonts from package dependencies, + # see https://flutter.dev/to/font-from-package diff --git a/Mobile Operasional/web/favicon.png b/Mobile Operasional/web/favicon.png new file mode 100644 index 0000000..8aaa46a Binary files /dev/null and b/Mobile Operasional/web/favicon.png differ diff --git a/Mobile Operasional/web/icons/Icon-192.png b/Mobile Operasional/web/icons/Icon-192.png new file mode 100644 index 0000000..b749bfe Binary files /dev/null and b/Mobile Operasional/web/icons/Icon-192.png differ diff --git a/Mobile Operasional/web/icons/Icon-512.png b/Mobile Operasional/web/icons/Icon-512.png new file mode 100644 index 0000000..88cfd48 Binary files /dev/null and b/Mobile Operasional/web/icons/Icon-512.png differ diff --git a/Mobile Operasional/web/icons/Icon-maskable-192.png b/Mobile Operasional/web/icons/Icon-maskable-192.png new file mode 100644 index 0000000..eb9b4d7 Binary files /dev/null and b/Mobile Operasional/web/icons/Icon-maskable-192.png differ diff --git a/Mobile Operasional/web/icons/Icon-maskable-512.png b/Mobile Operasional/web/icons/Icon-maskable-512.png new file mode 100644 index 0000000..d69c566 Binary files /dev/null and b/Mobile Operasional/web/icons/Icon-maskable-512.png differ diff --git a/Mobile Operasional/web/index.html b/Mobile Operasional/web/index.html new file mode 100644 index 0000000..182aa52 --- /dev/null +++ b/Mobile Operasional/web/index.html @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + niogu_app + + + + + + diff --git a/Mobile Operasional/web/manifest.json b/Mobile Operasional/web/manifest.json new file mode 100644 index 0000000..0d0003e --- /dev/null +++ b/Mobile Operasional/web/manifest.json @@ -0,0 +1,35 @@ +{ + "name": "niogoo_app", + "short_name": "niogoo_app", + "start_url": ".", + "display": "standalone", + "background_color": "#0175C2", + "theme_color": "#0175C2", + "description": "A new Flutter project.", + "orientation": "portrait-primary", + "prefer_related_applications": false, + "icons": [ + { + "src": "icons/Icon-192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "icons/Icon-512.png", + "sizes": "512x512", + "type": "image/png" + }, + { + "src": "icons/Icon-maskable-192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "maskable" + }, + { + "src": "icons/Icon-maskable-512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "maskable" + } + ] +} diff --git a/Mobile Operasional/windows/.gitignore b/Mobile Operasional/windows/.gitignore new file mode 100644 index 0000000..d492d0d --- /dev/null +++ b/Mobile Operasional/windows/.gitignore @@ -0,0 +1,17 @@ +flutter/ephemeral/ + +# Visual Studio user-specific files. +*.suo +*.user +*.userosscache +*.sln.docstates + +# Visual Studio build-related files. +x64/ +x86/ + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ diff --git a/Mobile Operasional/windows/CMakeLists.txt b/Mobile Operasional/windows/CMakeLists.txt new file mode 100644 index 0000000..7220bc1 --- /dev/null +++ b/Mobile Operasional/windows/CMakeLists.txt @@ -0,0 +1,108 @@ +# Project-level configuration. +cmake_minimum_required(VERSION 3.14) +project(niogoo_app 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 "niogoo_app") + +# Explicitly opt in to modern CMake behaviors to avoid warnings with recent +# versions of CMake. +cmake_policy(VERSION 3.14...3.25) + +# Define build configuration option. +get_property(IS_MULTICONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG) +if(IS_MULTICONFIG) + set(CMAKE_CONFIGURATION_TYPES "Debug;Profile;Release" + CACHE STRING "" FORCE) +else() + 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() +endif() +# Define settings for the Profile build mode. +set(CMAKE_EXE_LINKER_FLAGS_PROFILE "${CMAKE_EXE_LINKER_FLAGS_RELEASE}") +set(CMAKE_SHARED_LINKER_FLAGS_PROFILE "${CMAKE_SHARED_LINKER_FLAGS_RELEASE}") +set(CMAKE_C_FLAGS_PROFILE "${CMAKE_C_FLAGS_RELEASE}") +set(CMAKE_CXX_FLAGS_PROFILE "${CMAKE_CXX_FLAGS_RELEASE}") + +# Use Unicode for all projects. +add_definitions(-DUNICODE -D_UNICODE) + +# 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_17) + target_compile_options(${TARGET} PRIVATE /W4 /WX /wd"4100") + target_compile_options(${TARGET} PRIVATE /EHsc) + target_compile_definitions(${TARGET} PRIVATE "_HAS_EXCEPTIONS=0") + target_compile_definitions(${TARGET} PRIVATE "$<$:_DEBUG>") +endfunction() + +# Flutter library and tool build rules. +set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") +add_subdirectory(${FLUTTER_MANAGED_DIR}) + +# Application build; see runner/CMakeLists.txt. +add_subdirectory("runner") + + +# Generated plugin build rules, which manage building the plugins and adding +# them to the application. +include(flutter/generated_plugins.cmake) + + +# === Installation === +# Support files are copied into place next to the executable, so that it can +# run in place. This is done instead of making a separate bundle (as on Linux) +# so that building and running from within Visual Studio will work. +set(BUILD_BUNDLE_DIR "$") +# Make the "install" step default, as it's required to run. +set(CMAKE_VS_INCLUDE_INSTALL_TO_DEFAULT_BUILD 1) +if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) + set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) +endif() + +set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") +set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}") + +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) + +if(PLUGIN_BUNDLED_LIBRARIES) + install(FILES "${PLUGIN_BUNDLED_LIBRARIES}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endif() + +# Copy the native assets provided by the build.dart from all packages. +set(NATIVE_ASSETS_DIR "${PROJECT_BUILD_DIR}native_assets/windows/") +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. +install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + CONFIGURATIONS Profile;Release + COMPONENT Runtime) diff --git a/Mobile Operasional/windows/flutter/CMakeLists.txt b/Mobile Operasional/windows/flutter/CMakeLists.txt new file mode 100644 index 0000000..903f489 --- /dev/null +++ b/Mobile Operasional/windows/flutter/CMakeLists.txt @@ -0,0 +1,109 @@ +# This file controls Flutter-level build steps. It should not be edited. +cmake_minimum_required(VERSION 3.14) + +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. +set(WRAPPER_ROOT "${EPHEMERAL_DIR}/cpp_client_wrapper") + +# Set fallback configurations for older versions of the flutter tool. +if (NOT DEFINED FLUTTER_TARGET_PLATFORM) + set(FLUTTER_TARGET_PLATFORM "windows-x64") +endif() + +# === Flutter Library === +set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/flutter_windows.dll") + +# 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/windows/app.so" PARENT_SCOPE) + +list(APPEND FLUTTER_LIBRARY_HEADERS + "flutter_export.h" + "flutter_windows.h" + "flutter_messenger.h" + "flutter_plugin_registrar.h" + "flutter_texture_registrar.h" +) +list(TRANSFORM FLUTTER_LIBRARY_HEADERS PREPEND "${EPHEMERAL_DIR}/") +add_library(flutter INTERFACE) +target_include_directories(flutter INTERFACE + "${EPHEMERAL_DIR}" +) +target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}.lib") +add_dependencies(flutter flutter_assemble) + +# === Wrapper === +list(APPEND CPP_WRAPPER_SOURCES_CORE + "core_implementations.cc" + "standard_codec.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_CORE PREPEND "${WRAPPER_ROOT}/") +list(APPEND CPP_WRAPPER_SOURCES_PLUGIN + "plugin_registrar.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_PLUGIN PREPEND "${WRAPPER_ROOT}/") +list(APPEND CPP_WRAPPER_SOURCES_APP + "flutter_engine.cc" + "flutter_view_controller.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_APP PREPEND "${WRAPPER_ROOT}/") + +# Wrapper sources needed for a plugin. +add_library(flutter_wrapper_plugin STATIC + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_PLUGIN} +) +apply_standard_settings(flutter_wrapper_plugin) +set_target_properties(flutter_wrapper_plugin PROPERTIES + POSITION_INDEPENDENT_CODE ON) +set_target_properties(flutter_wrapper_plugin PROPERTIES + CXX_VISIBILITY_PRESET hidden) +target_link_libraries(flutter_wrapper_plugin PUBLIC flutter) +target_include_directories(flutter_wrapper_plugin PUBLIC + "${WRAPPER_ROOT}/include" +) +add_dependencies(flutter_wrapper_plugin flutter_assemble) + +# Wrapper sources needed for the runner. +add_library(flutter_wrapper_app STATIC + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_APP} +) +apply_standard_settings(flutter_wrapper_app) +target_link_libraries(flutter_wrapper_app PUBLIC flutter) +target_include_directories(flutter_wrapper_app PUBLIC + "${WRAPPER_ROOT}/include" +) +add_dependencies(flutter_wrapper_app 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. +set(PHONY_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/_phony_") +set_source_files_properties("${PHONY_OUTPUT}" PROPERTIES SYMBOLIC TRUE) +add_custom_command( + OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} + ${CPP_WRAPPER_SOURCES_CORE} ${CPP_WRAPPER_SOURCES_PLUGIN} + ${CPP_WRAPPER_SOURCES_APP} + ${PHONY_OUTPUT} + COMMAND ${CMAKE_COMMAND} -E env + ${FLUTTER_TOOL_ENVIRONMENT} + "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat" + ${FLUTTER_TARGET_PLATFORM} $ + VERBATIM +) +add_custom_target(flutter_assemble DEPENDS + "${FLUTTER_LIBRARY}" + ${FLUTTER_LIBRARY_HEADERS} + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_PLUGIN} + ${CPP_WRAPPER_SOURCES_APP} +) diff --git a/Mobile Operasional/windows/flutter/generated_plugin_registrant.cc b/Mobile Operasional/windows/flutter/generated_plugin_registrant.cc new file mode 100644 index 0000000..a9b640f --- /dev/null +++ b/Mobile Operasional/windows/flutter/generated_plugin_registrant.cc @@ -0,0 +1,38 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#include "generated_plugin_registrant.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +void RegisterPlugins(flutter::PluginRegistry* registry) { + ConnectivityPlusWindowsPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("ConnectivityPlusWindowsPlugin")); + FileSelectorWindowsRegisterWithRegistrar( + registry->GetRegistrarForPlugin("FileSelectorWindows")); + FlutterSecureStorageWindowsPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("FlutterSecureStorageWindowsPlugin")); + GalPluginCApiRegisterWithRegistrar( + registry->GetRegistrarForPlugin("GalPluginCApi")); + GeolocatorWindowsRegisterWithRegistrar( + registry->GetRegistrarForPlugin("GeolocatorWindows")); + PermissionHandlerWindowsPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("PermissionHandlerWindowsPlugin")); + SharePlusWindowsPluginCApiRegisterWithRegistrar( + registry->GetRegistrarForPlugin("SharePlusWindowsPluginCApi")); + Sqlite3FlutterLibsPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("Sqlite3FlutterLibsPlugin")); + UrlLauncherWindowsRegisterWithRegistrar( + registry->GetRegistrarForPlugin("UrlLauncherWindows")); +} diff --git a/Mobile Operasional/windows/flutter/generated_plugin_registrant.h b/Mobile Operasional/windows/flutter/generated_plugin_registrant.h new file mode 100644 index 0000000..dc139d8 --- /dev/null +++ b/Mobile Operasional/windows/flutter/generated_plugin_registrant.h @@ -0,0 +1,15 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#ifndef GENERATED_PLUGIN_REGISTRANT_ +#define GENERATED_PLUGIN_REGISTRANT_ + +#include + +// Registers Flutter plugins. +void RegisterPlugins(flutter::PluginRegistry* registry); + +#endif // GENERATED_PLUGIN_REGISTRANT_ diff --git a/Mobile Operasional/windows/flutter/generated_plugins.cmake b/Mobile Operasional/windows/flutter/generated_plugins.cmake new file mode 100644 index 0000000..875c271 --- /dev/null +++ b/Mobile Operasional/windows/flutter/generated_plugins.cmake @@ -0,0 +1,32 @@ +# +# Generated file, do not edit. +# + +list(APPEND FLUTTER_PLUGIN_LIST + connectivity_plus + file_selector_windows + flutter_secure_storage_windows + gal + geolocator_windows + permission_handler_windows + share_plus + sqlite3_flutter_libs + url_launcher_windows +) + +list(APPEND FLUTTER_FFI_PLUGIN_LIST +) + +set(PLUGIN_BUNDLED_LIBRARIES) + +foreach(plugin ${FLUTTER_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/windows plugins/${plugin}) + target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) + list(APPEND PLUGIN_BUNDLED_LIBRARIES $) + 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}/windows plugins/${ffi_plugin}) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) +endforeach(ffi_plugin) diff --git a/Mobile Operasional/windows/runner/CMakeLists.txt b/Mobile Operasional/windows/runner/CMakeLists.txt new file mode 100644 index 0000000..394917c --- /dev/null +++ b/Mobile Operasional/windows/runner/CMakeLists.txt @@ -0,0 +1,40 @@ +cmake_minimum_required(VERSION 3.14) +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} WIN32 + "flutter_window.cpp" + "main.cpp" + "utils.cpp" + "win32_window.cpp" + "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" + "Runner.rc" + "runner.exe.manifest" +) + +# 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 build version. +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION=\"${FLUTTER_VERSION}\"") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MAJOR=${FLUTTER_VERSION_MAJOR}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MINOR=${FLUTTER_VERSION_MINOR}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_PATCH=${FLUTTER_VERSION_PATCH}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_BUILD=${FLUTTER_VERSION_BUILD}") + +# Disable Windows macros that collide with C++ standard library functions. +target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX") + +# Add dependency libraries and include directories. Add any application-specific +# dependencies here. +target_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app) +target_link_libraries(${BINARY_NAME} PRIVATE "dwmapi.lib") +target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}") + +# Run the Flutter tool portions of the build. This must not be removed. +add_dependencies(${BINARY_NAME} flutter_assemble) diff --git a/Mobile Operasional/windows/runner/Runner.rc b/Mobile Operasional/windows/runner/Runner.rc new file mode 100644 index 0000000..f064744 --- /dev/null +++ b/Mobile Operasional/windows/runner/Runner.rc @@ -0,0 +1,121 @@ +// Microsoft Visual C++ generated resource script. +// +#pragma code_page(65001) +#include "resource.h" + +#define APSTUDIO_READONLY_SYMBOLS +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 2 resource. +// +#include "winres.h" + +///////////////////////////////////////////////////////////////////////////// +#undef APSTUDIO_READONLY_SYMBOLS + +///////////////////////////////////////////////////////////////////////////// +// English (United States) resources + +#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) +LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US + +#ifdef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// TEXTINCLUDE +// + +1 TEXTINCLUDE +BEGIN + "resource.h\0" +END + +2 TEXTINCLUDE +BEGIN + "#include ""winres.h""\r\n" + "\0" +END + +3 TEXTINCLUDE +BEGIN + "\r\n" + "\0" +END + +#endif // APSTUDIO_INVOKED + + +///////////////////////////////////////////////////////////////////////////// +// +// Icon +// + +// Icon with lowest ID value placed first to ensure application icon +// remains consistent on all systems. +IDI_APP_ICON ICON "resources\\app_icon.ico" + + +///////////////////////////////////////////////////////////////////////////// +// +// Version +// + +#if defined(FLUTTER_VERSION_MAJOR) && defined(FLUTTER_VERSION_MINOR) && defined(FLUTTER_VERSION_PATCH) && defined(FLUTTER_VERSION_BUILD) +#define VERSION_AS_NUMBER FLUTTER_VERSION_MAJOR,FLUTTER_VERSION_MINOR,FLUTTER_VERSION_PATCH,FLUTTER_VERSION_BUILD +#else +#define VERSION_AS_NUMBER 1,0,0,0 +#endif + +#if defined(FLUTTER_VERSION) +#define VERSION_AS_STRING FLUTTER_VERSION +#else +#define VERSION_AS_STRING "1.0.0" +#endif + +VS_VERSION_INFO VERSIONINFO + FILEVERSION VERSION_AS_NUMBER + PRODUCTVERSION VERSION_AS_NUMBER + FILEFLAGSMASK VS_FFI_FILEFLAGSMASK +#ifdef _DEBUG + FILEFLAGS VS_FF_DEBUG +#else + FILEFLAGS 0x0L +#endif + FILEOS VOS__WINDOWS32 + FILETYPE VFT_APP + FILESUBTYPE 0x0L +BEGIN + BLOCK "StringFileInfo" + BEGIN + BLOCK "040904e4" + BEGIN + VALUE "CompanyName", "com.example" "\0" + VALUE "FileDescription", "niogoo_app" "\0" + VALUE "FileVersion", VERSION_AS_STRING "\0" + VALUE "InternalName", "niogoo_app" "\0" + VALUE "LegalCopyright", "Copyright (C) 2025 com.example. All rights reserved." "\0" + VALUE "OriginalFilename", "niogoo_app.exe" "\0" + VALUE "ProductName", "niogoo_app" "\0" + VALUE "ProductVersion", VERSION_AS_STRING "\0" + END + END + BLOCK "VarFileInfo" + BEGIN + VALUE "Translation", 0x409, 1252 + END +END + +#endif // English (United States) resources +///////////////////////////////////////////////////////////////////////////// + + + +#ifndef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 3 resource. +// + + +///////////////////////////////////////////////////////////////////////////// +#endif // not APSTUDIO_INVOKED diff --git a/Mobile Operasional/windows/runner/flutter_window.cpp b/Mobile Operasional/windows/runner/flutter_window.cpp new file mode 100644 index 0000000..955ee30 --- /dev/null +++ b/Mobile Operasional/windows/runner/flutter_window.cpp @@ -0,0 +1,71 @@ +#include "flutter_window.h" + +#include + +#include "flutter/generated_plugin_registrant.h" + +FlutterWindow::FlutterWindow(const flutter::DartProject& project) + : project_(project) {} + +FlutterWindow::~FlutterWindow() {} + +bool FlutterWindow::OnCreate() { + if (!Win32Window::OnCreate()) { + return false; + } + + RECT frame = GetClientArea(); + + // The size here must match the window dimensions to avoid unnecessary surface + // creation / destruction in the startup path. + flutter_controller_ = std::make_unique( + frame.right - frame.left, frame.bottom - frame.top, project_); + // Ensure that basic setup of the controller was successful. + if (!flutter_controller_->engine() || !flutter_controller_->view()) { + return false; + } + RegisterPlugins(flutter_controller_->engine()); + SetChildContent(flutter_controller_->view()->GetNativeWindow()); + + flutter_controller_->engine()->SetNextFrameCallback([&]() { + this->Show(); + }); + + // Flutter can complete the first frame before the "show window" callback is + // registered. The following call ensures a frame is pending to ensure the + // window is shown. It is a no-op if the first frame hasn't completed yet. + flutter_controller_->ForceRedraw(); + + return true; +} + +void FlutterWindow::OnDestroy() { + if (flutter_controller_) { + flutter_controller_ = nullptr; + } + + Win32Window::OnDestroy(); +} + +LRESULT +FlutterWindow::MessageHandler(HWND hwnd, UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + // Give Flutter, including plugins, an opportunity to handle window messages. + if (flutter_controller_) { + std::optional result = + flutter_controller_->HandleTopLevelWindowProc(hwnd, message, wparam, + lparam); + if (result) { + return *result; + } + } + + switch (message) { + case WM_FONTCHANGE: + flutter_controller_->engine()->ReloadSystemFonts(); + break; + } + + return Win32Window::MessageHandler(hwnd, message, wparam, lparam); +} diff --git a/Mobile Operasional/windows/runner/flutter_window.h b/Mobile Operasional/windows/runner/flutter_window.h new file mode 100644 index 0000000..6da0652 --- /dev/null +++ b/Mobile Operasional/windows/runner/flutter_window.h @@ -0,0 +1,33 @@ +#ifndef RUNNER_FLUTTER_WINDOW_H_ +#define RUNNER_FLUTTER_WINDOW_H_ + +#include +#include + +#include + +#include "win32_window.h" + +// A window that does nothing but host a Flutter view. +class FlutterWindow : public Win32Window { + public: + // Creates a new FlutterWindow hosting a Flutter view running |project|. + explicit FlutterWindow(const flutter::DartProject& project); + virtual ~FlutterWindow(); + + protected: + // Win32Window: + bool OnCreate() override; + void OnDestroy() override; + LRESULT MessageHandler(HWND window, UINT const message, WPARAM const wparam, + LPARAM const lparam) noexcept override; + + private: + // The project to run. + flutter::DartProject project_; + + // The Flutter instance hosted by this window. + std::unique_ptr flutter_controller_; +}; + +#endif // RUNNER_FLUTTER_WINDOW_H_ diff --git a/Mobile Operasional/windows/runner/main.cpp b/Mobile Operasional/windows/runner/main.cpp new file mode 100644 index 0000000..e73e043 --- /dev/null +++ b/Mobile Operasional/windows/runner/main.cpp @@ -0,0 +1,43 @@ +#include +#include +#include + +#include "flutter_window.h" +#include "utils.h" + +int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, + _In_ wchar_t *command_line, _In_ int show_command) { + // Attach to console when present (e.g., 'flutter run') or create a + // new console when running with a debugger. + if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) { + CreateAndAttachConsole(); + } + + // Initialize COM, so that it is available for use in the library and/or + // plugins. + ::CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED); + + flutter::DartProject project(L"data"); + + std::vector command_line_arguments = + GetCommandLineArguments(); + + project.set_dart_entrypoint_arguments(std::move(command_line_arguments)); + + FlutterWindow window(project); + Win32Window::Point origin(10, 10); + Win32Window::Size size(1280, 720); + if (!window.Create(L"niogoo_app", origin, size)) { + return EXIT_FAILURE; + } + window.SetQuitOnClose(true); + + ::MSG msg; + while (::GetMessage(&msg, nullptr, 0, 0)) { + ::TranslateMessage(&msg); + ::DispatchMessage(&msg); + } + + ::CoUninitialize(); + return EXIT_SUCCESS; +} diff --git a/Mobile Operasional/windows/runner/resource.h b/Mobile Operasional/windows/runner/resource.h new file mode 100644 index 0000000..66a65d1 --- /dev/null +++ b/Mobile Operasional/windows/runner/resource.h @@ -0,0 +1,16 @@ +//{{NO_DEPENDENCIES}} +// Microsoft Visual C++ generated include file. +// Used by Runner.rc +// +#define IDI_APP_ICON 101 + +// Next default values for new objects +// +#ifdef APSTUDIO_INVOKED +#ifndef APSTUDIO_READONLY_SYMBOLS +#define _APS_NEXT_RESOURCE_VALUE 102 +#define _APS_NEXT_COMMAND_VALUE 40001 +#define _APS_NEXT_CONTROL_VALUE 1001 +#define _APS_NEXT_SYMED_VALUE 101 +#endif +#endif diff --git a/Mobile Operasional/windows/runner/resources/app_icon.ico b/Mobile Operasional/windows/runner/resources/app_icon.ico new file mode 100644 index 0000000..c04e20c Binary files /dev/null and b/Mobile Operasional/windows/runner/resources/app_icon.ico differ diff --git a/Mobile Operasional/windows/runner/runner.exe.manifest b/Mobile Operasional/windows/runner/runner.exe.manifest new file mode 100644 index 0000000..153653e --- /dev/null +++ b/Mobile Operasional/windows/runner/runner.exe.manifest @@ -0,0 +1,14 @@ + + + + + PerMonitorV2 + + + + + + + + + diff --git a/Mobile Operasional/windows/runner/utils.cpp b/Mobile Operasional/windows/runner/utils.cpp new file mode 100644 index 0000000..3a0b465 --- /dev/null +++ b/Mobile Operasional/windows/runner/utils.cpp @@ -0,0 +1,65 @@ +#include "utils.h" + +#include +#include +#include +#include + +#include + +void CreateAndAttachConsole() { + if (::AllocConsole()) { + FILE *unused; + if (freopen_s(&unused, "CONOUT$", "w", stdout)) { + _dup2(_fileno(stdout), 1); + } + if (freopen_s(&unused, "CONOUT$", "w", stderr)) { + _dup2(_fileno(stdout), 2); + } + std::ios::sync_with_stdio(); + FlutterDesktopResyncOutputStreams(); + } +} + +std::vector GetCommandLineArguments() { + // Convert the UTF-16 command line arguments to UTF-8 for the Engine to use. + int argc; + wchar_t** argv = ::CommandLineToArgvW(::GetCommandLineW(), &argc); + if (argv == nullptr) { + return std::vector(); + } + + std::vector command_line_arguments; + + // Skip the first argument as it's the binary name. + for (int i = 1; i < argc; i++) { + command_line_arguments.push_back(Utf8FromUtf16(argv[i])); + } + + ::LocalFree(argv); + + return command_line_arguments; +} + +std::string Utf8FromUtf16(const wchar_t* utf16_string) { + if (utf16_string == nullptr) { + return std::string(); + } + unsigned int target_length = ::WideCharToMultiByte( + CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, + -1, nullptr, 0, nullptr, nullptr) + -1; // remove the trailing null character + int input_length = (int)wcslen(utf16_string); + std::string utf8_string; + if (target_length == 0 || target_length > utf8_string.max_size()) { + return utf8_string; + } + utf8_string.resize(target_length); + int converted_length = ::WideCharToMultiByte( + CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, + input_length, utf8_string.data(), target_length, nullptr, nullptr); + if (converted_length == 0) { + return std::string(); + } + return utf8_string; +} diff --git a/Mobile Operasional/windows/runner/utils.h b/Mobile Operasional/windows/runner/utils.h new file mode 100644 index 0000000..3879d54 --- /dev/null +++ b/Mobile Operasional/windows/runner/utils.h @@ -0,0 +1,19 @@ +#ifndef RUNNER_UTILS_H_ +#define RUNNER_UTILS_H_ + +#include +#include + +// Creates a console for the process, and redirects stdout and stderr to +// it for both the runner and the Flutter library. +void CreateAndAttachConsole(); + +// Takes a null-terminated wchar_t* encoded in UTF-16 and returns a std::string +// encoded in UTF-8. Returns an empty std::string on failure. +std::string Utf8FromUtf16(const wchar_t* utf16_string); + +// Gets the command line arguments passed in as a std::vector, +// encoded in UTF-8. Returns an empty std::vector on failure. +std::vector GetCommandLineArguments(); + +#endif // RUNNER_UTILS_H_ diff --git a/Mobile Operasional/windows/runner/win32_window.cpp b/Mobile Operasional/windows/runner/win32_window.cpp new file mode 100644 index 0000000..60608d0 --- /dev/null +++ b/Mobile Operasional/windows/runner/win32_window.cpp @@ -0,0 +1,288 @@ +#include "win32_window.h" + +#include +#include + +#include "resource.h" + +namespace { + +/// Window attribute that enables dark mode window decorations. +/// +/// Redefined in case the developer's machine has a Windows SDK older than +/// version 10.0.22000.0. +/// See: https://docs.microsoft.com/windows/win32/api/dwmapi/ne-dwmapi-dwmwindowattribute +#ifndef DWMWA_USE_IMMERSIVE_DARK_MODE +#define DWMWA_USE_IMMERSIVE_DARK_MODE 20 +#endif + +constexpr const wchar_t kWindowClassName[] = L"FLUTTER_RUNNER_WIN32_WINDOW"; + +/// Registry key for app theme preference. +/// +/// A value of 0 indicates apps should use dark mode. A non-zero or missing +/// value indicates apps should use light mode. +constexpr const wchar_t kGetPreferredBrightnessRegKey[] = + L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize"; +constexpr const wchar_t kGetPreferredBrightnessRegValue[] = L"AppsUseLightTheme"; + +// The number of Win32Window objects that currently exist. +static int g_active_window_count = 0; + +using EnableNonClientDpiScaling = BOOL __stdcall(HWND hwnd); + +// Scale helper to convert logical scaler values to physical using passed in +// scale factor +int Scale(int source, double scale_factor) { + return static_cast(source * scale_factor); +} + +// Dynamically loads the |EnableNonClientDpiScaling| from the User32 module. +// This API is only needed for PerMonitor V1 awareness mode. +void EnableFullDpiSupportIfAvailable(HWND hwnd) { + HMODULE user32_module = LoadLibraryA("User32.dll"); + if (!user32_module) { + return; + } + auto enable_non_client_dpi_scaling = + reinterpret_cast( + GetProcAddress(user32_module, "EnableNonClientDpiScaling")); + if (enable_non_client_dpi_scaling != nullptr) { + enable_non_client_dpi_scaling(hwnd); + } + FreeLibrary(user32_module); +} + +} // namespace + +// Manages the Win32Window's window class registration. +class WindowClassRegistrar { + public: + ~WindowClassRegistrar() = default; + + // Returns the singleton registrar instance. + static WindowClassRegistrar* GetInstance() { + if (!instance_) { + instance_ = new WindowClassRegistrar(); + } + return instance_; + } + + // Returns the name of the window class, registering the class if it hasn't + // previously been registered. + const wchar_t* GetWindowClass(); + + // Unregisters the window class. Should only be called if there are no + // instances of the window. + void UnregisterWindowClass(); + + private: + WindowClassRegistrar() = default; + + static WindowClassRegistrar* instance_; + + bool class_registered_ = false; +}; + +WindowClassRegistrar* WindowClassRegistrar::instance_ = nullptr; + +const wchar_t* WindowClassRegistrar::GetWindowClass() { + if (!class_registered_) { + WNDCLASS window_class{}; + window_class.hCursor = LoadCursor(nullptr, IDC_ARROW); + window_class.lpszClassName = kWindowClassName; + window_class.style = CS_HREDRAW | CS_VREDRAW; + window_class.cbClsExtra = 0; + window_class.cbWndExtra = 0; + window_class.hInstance = GetModuleHandle(nullptr); + window_class.hIcon = + LoadIcon(window_class.hInstance, MAKEINTRESOURCE(IDI_APP_ICON)); + window_class.hbrBackground = 0; + window_class.lpszMenuName = nullptr; + window_class.lpfnWndProc = Win32Window::WndProc; + RegisterClass(&window_class); + class_registered_ = true; + } + return kWindowClassName; +} + +void WindowClassRegistrar::UnregisterWindowClass() { + UnregisterClass(kWindowClassName, nullptr); + class_registered_ = false; +} + +Win32Window::Win32Window() { + ++g_active_window_count; +} + +Win32Window::~Win32Window() { + --g_active_window_count; + Destroy(); +} + +bool Win32Window::Create(const std::wstring& title, + const Point& origin, + const Size& size) { + Destroy(); + + const wchar_t* window_class = + WindowClassRegistrar::GetInstance()->GetWindowClass(); + + const POINT target_point = {static_cast(origin.x), + static_cast(origin.y)}; + HMONITOR monitor = MonitorFromPoint(target_point, MONITOR_DEFAULTTONEAREST); + UINT dpi = FlutterDesktopGetDpiForMonitor(monitor); + double scale_factor = dpi / 96.0; + + HWND window = CreateWindow( + window_class, title.c_str(), WS_OVERLAPPEDWINDOW, + Scale(origin.x, scale_factor), Scale(origin.y, scale_factor), + Scale(size.width, scale_factor), Scale(size.height, scale_factor), + nullptr, nullptr, GetModuleHandle(nullptr), this); + + if (!window) { + return false; + } + + UpdateTheme(window); + + return OnCreate(); +} + +bool Win32Window::Show() { + return ShowWindow(window_handle_, SW_SHOWNORMAL); +} + +// static +LRESULT CALLBACK Win32Window::WndProc(HWND const window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + if (message == WM_NCCREATE) { + auto window_struct = reinterpret_cast(lparam); + SetWindowLongPtr(window, GWLP_USERDATA, + reinterpret_cast(window_struct->lpCreateParams)); + + auto that = static_cast(window_struct->lpCreateParams); + EnableFullDpiSupportIfAvailable(window); + that->window_handle_ = window; + } else if (Win32Window* that = GetThisFromHandle(window)) { + return that->MessageHandler(window, message, wparam, lparam); + } + + return DefWindowProc(window, message, wparam, lparam); +} + +LRESULT +Win32Window::MessageHandler(HWND hwnd, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + switch (message) { + case WM_DESTROY: + window_handle_ = nullptr; + Destroy(); + if (quit_on_close_) { + PostQuitMessage(0); + } + return 0; + + case WM_DPICHANGED: { + auto newRectSize = reinterpret_cast(lparam); + LONG newWidth = newRectSize->right - newRectSize->left; + LONG newHeight = newRectSize->bottom - newRectSize->top; + + SetWindowPos(hwnd, nullptr, newRectSize->left, newRectSize->top, newWidth, + newHeight, SWP_NOZORDER | SWP_NOACTIVATE); + + return 0; + } + case WM_SIZE: { + RECT rect = GetClientArea(); + if (child_content_ != nullptr) { + // Size and position the child window. + MoveWindow(child_content_, rect.left, rect.top, rect.right - rect.left, + rect.bottom - rect.top, TRUE); + } + return 0; + } + + case WM_ACTIVATE: + if (child_content_ != nullptr) { + SetFocus(child_content_); + } + return 0; + + case WM_DWMCOLORIZATIONCOLORCHANGED: + UpdateTheme(hwnd); + return 0; + } + + return DefWindowProc(window_handle_, message, wparam, lparam); +} + +void Win32Window::Destroy() { + OnDestroy(); + + if (window_handle_) { + DestroyWindow(window_handle_); + window_handle_ = nullptr; + } + if (g_active_window_count == 0) { + WindowClassRegistrar::GetInstance()->UnregisterWindowClass(); + } +} + +Win32Window* Win32Window::GetThisFromHandle(HWND const window) noexcept { + return reinterpret_cast( + GetWindowLongPtr(window, GWLP_USERDATA)); +} + +void Win32Window::SetChildContent(HWND content) { + child_content_ = content; + SetParent(content, window_handle_); + RECT frame = GetClientArea(); + + MoveWindow(content, frame.left, frame.top, frame.right - frame.left, + frame.bottom - frame.top, true); + + SetFocus(child_content_); +} + +RECT Win32Window::GetClientArea() { + RECT frame; + GetClientRect(window_handle_, &frame); + return frame; +} + +HWND Win32Window::GetHandle() { + return window_handle_; +} + +void Win32Window::SetQuitOnClose(bool quit_on_close) { + quit_on_close_ = quit_on_close; +} + +bool Win32Window::OnCreate() { + // No-op; provided for subclasses. + return true; +} + +void Win32Window::OnDestroy() { + // No-op; provided for subclasses. +} + +void Win32Window::UpdateTheme(HWND const window) { + DWORD light_mode; + DWORD light_mode_size = sizeof(light_mode); + LSTATUS result = RegGetValue(HKEY_CURRENT_USER, kGetPreferredBrightnessRegKey, + kGetPreferredBrightnessRegValue, + RRF_RT_REG_DWORD, nullptr, &light_mode, + &light_mode_size); + + if (result == ERROR_SUCCESS) { + BOOL enable_dark_mode = light_mode == 0; + DwmSetWindowAttribute(window, DWMWA_USE_IMMERSIVE_DARK_MODE, + &enable_dark_mode, sizeof(enable_dark_mode)); + } +} diff --git a/Mobile Operasional/windows/runner/win32_window.h b/Mobile Operasional/windows/runner/win32_window.h new file mode 100644 index 0000000..e901dde --- /dev/null +++ b/Mobile Operasional/windows/runner/win32_window.h @@ -0,0 +1,102 @@ +#ifndef RUNNER_WIN32_WINDOW_H_ +#define RUNNER_WIN32_WINDOW_H_ + +#include + +#include +#include +#include + +// A class abstraction for a high DPI-aware Win32 Window. Intended to be +// inherited from by classes that wish to specialize with custom +// rendering and input handling +class Win32Window { + public: + struct Point { + unsigned int x; + unsigned int y; + Point(unsigned int x, unsigned int y) : x(x), y(y) {} + }; + + struct Size { + unsigned int width; + unsigned int height; + Size(unsigned int width, unsigned int height) + : width(width), height(height) {} + }; + + Win32Window(); + virtual ~Win32Window(); + + // Creates a win32 window with |title| that is positioned and sized using + // |origin| and |size|. New windows are created on the default monitor. Window + // sizes are specified to the OS in physical pixels, hence to ensure a + // consistent size this function will scale the inputted width and height as + // as appropriate for the default monitor. The window is invisible until + // |Show| is called. Returns true if the window was created successfully. + bool Create(const std::wstring& title, const Point& origin, const Size& size); + + // Show the current window. Returns true if the window was successfully shown. + bool Show(); + + // Release OS resources associated with window. + void Destroy(); + + // Inserts |content| into the window tree. + void SetChildContent(HWND content); + + // Returns the backing Window handle to enable clients to set icon and other + // window properties. Returns nullptr if the window has been destroyed. + HWND GetHandle(); + + // If true, closing this window will quit the application. + void SetQuitOnClose(bool quit_on_close); + + // Return a RECT representing the bounds of the current client area. + RECT GetClientArea(); + + protected: + // Processes and route salient window messages for mouse handling, + // size change and DPI. Delegates handling of these to member overloads that + // inheriting classes can handle. + virtual LRESULT MessageHandler(HWND window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept; + + // Called when CreateAndShow is called, allowing subclass window-related + // setup. Subclasses should return false if setup fails. + virtual bool OnCreate(); + + // Called when Destroy is called. + virtual void OnDestroy(); + + private: + friend class WindowClassRegistrar; + + // OS callback called by message pump. Handles the WM_NCCREATE message which + // is passed when the non-client area is being created and enables automatic + // non-client DPI scaling so that the non-client area automatically + // responds to changes in DPI. All other messages are handled by + // MessageHandler. + static LRESULT CALLBACK WndProc(HWND const window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept; + + // Retrieves a class instance pointer for |window| + static Win32Window* GetThisFromHandle(HWND const window) noexcept; + + // Update the window frame's theme to match the system theme. + static void UpdateTheme(HWND const window); + + bool quit_on_close_ = false; + + // window handle for top level window. + HWND window_handle_ = nullptr; + + // window handle for hosted content. + HWND child_content_ = nullptr; +}; + +#endif // RUNNER_WIN32_WINDOW_H_