From 372c2bc14da012d058f1672160263aa92b99cded Mon Sep 17 00:00:00 2001 From: Narayanan Madaswamy Date: Sat, 29 Aug 2026 21:52:11 +0530 Subject: [PATCH] Web approach - basic look and feel done --- .../com/kifi/api/config/SecurityConfig.java | 22 + .../vendor/PurchaseOrderController.java | 11 +- .../service/vendor/PurchaseOrderService.java | 165 ++- .../src/test/java/com/kifi/api/DBTest.java | 38 +- kifi-app/.dockerignore | 16 + kifi-app/Dockerfile | 37 + kifi-app/build_n_push.sh | 29 + kifi-app/docker-compose.yml | 15 + kifi-app/lib/core/network/dio_client.dart | 17 +- kifi-app/lib/core/utils/snackbar_service.dart | 2 +- .../lib/core/widgets/desktop_sidebar.dart | 454 ++++++++ .../lib/core/widgets/responsive_layout.dart | 74 ++ .../core/widgets/smart_search_dropdown.dart | 296 +++--- .../auth/presentation/auth_screen.dart | 2 +- .../presentation/forgot_password_screen.dart | 2 +- .../auth/presentation/otp_screen.dart | 2 +- .../auth/presentation/profile_screen.dart | 2 +- .../presentation/reset_password_screen.dart | 2 +- .../presentation/setup_wizard_screen.dart | 2 +- .../budget/presentation/budget_screen.dart | 2 +- .../presentation/hub/business_hub_screen.dart | 2 +- .../presentation/reports/reports_screen.dart | 2 +- .../settings/business_settings_screen.dart | 2 +- .../presentation/accounts_screen.dart | 12 +- .../presentation/dashboard_screen.dart | 982 ++++++++++-------- .../presentation/maturity_dialog.dart | 2 +- .../widgets/budget_status_card.dart | 2 +- .../presentation/widgets/statistics_tab.dart | 2 +- .../widgets/swipeable_account_card.dart | 2 +- .../widgets/upcoming_dues_widget.dart | 2 +- .../features/inventory/domain/product.dart | 9 +- .../presentation/add_product_screen.dart | 103 +- .../category_management_screen.dart | 2 +- .../presentation/daily_rates_screen.dart | 2 +- .../presentation/product_list_screen.dart | 26 +- .../quick_adjust_stock_screen.dart | 2 +- .../presentation/stock_ledger_tab.dart | 2 +- .../providers/products_provider.dart | 140 +-- .../inventory/providers/uoms_provider.dart | 102 ++ .../presentation/onboarding_screen.dart | 2 +- .../presentation/project_board_screen.dart | 2 +- .../presentation/project_hub_screen.dart | 2 +- .../presentation/projects_screen.dart | 2 +- .../widgets/add_project_sheet.dart | 2 +- .../presentation/widgets/add_task_sheet.dart | 2 +- .../presentation/widgets/task_card.dart | 2 +- .../widgets/task_details_sheet.dart | 2 +- .../presentation/add_customer_sheet.dart | 5 +- .../presentation/customers_list_screen.dart | 2 +- .../presentation/invoice_builder_screen.dart | 5 +- .../presentation/invoice_details_screen.dart | 2 +- .../presentation/invoices_list_screen.dart | 2 +- .../widgets/quick_add_customer_sheet.dart | 2 +- .../widgets/receive_payment_sheet.dart | 2 +- .../sales/providers/customers_provider.dart | 21 +- .../presentation/add_transaction_screen.dart | 32 +- .../presentation/all_transactions_screen.dart | 383 +++---- .../widgets/transaction_filter_sheet.dart | 2 +- .../vendor/presentation/add_vendor_sheet.dart | 5 +- .../vendor/presentation/pay_vendor_sheet.dart | 2 +- .../purchase_order_builder_screen.dart | 11 +- .../purchase_order_details_screen.dart | 2 +- .../purchase_orders_list_screen.dart | 2 +- .../presentation/vendors_list_screen.dart | 2 +- .../widgets/quick_add_vendor_sheet.dart | 2 +- .../providers/purchase_orders_provider.dart | 2 + kifi-app/lib/main.dart | 3 +- kifi-app/nginx.conf | 55 + kifi-app/pubspec.lock | 8 +- kifi-app/pubspec.yaml | 2 +- kifi-app/web/index.html | 95 +- 71 files changed, 2209 insertions(+), 1044 deletions(-) create mode 100644 kifi-app/.dockerignore create mode 100644 kifi-app/Dockerfile create mode 100755 kifi-app/build_n_push.sh create mode 100644 kifi-app/docker-compose.yml create mode 100644 kifi-app/lib/core/widgets/desktop_sidebar.dart create mode 100644 kifi-app/lib/core/widgets/responsive_layout.dart create mode 100644 kifi-app/lib/features/inventory/providers/uoms_provider.dart create mode 100644 kifi-app/nginx.conf diff --git a/kifi-api/src/main/java/com/kifi/api/config/SecurityConfig.java b/kifi-api/src/main/java/com/kifi/api/config/SecurityConfig.java index ffafd1d..23c7c55 100644 --- a/kifi-api/src/main/java/com/kifi/api/config/SecurityConfig.java +++ b/kifi-api/src/main/java/com/kifi/api/config/SecurityConfig.java @@ -14,6 +14,12 @@ import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.server.SecurityWebFilterChain; import reactor.core.publisher.Mono; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.reactive.CorsConfigurationSource; +import org.springframework.web.cors.reactive.UrlBasedCorsConfigurationSource; +import java.util.Arrays; +import java.util.List; + @Configuration @EnableWebFluxSecurity @RequiredArgsConstructor @@ -27,9 +33,25 @@ public class SecurityConfig { return new BCryptPasswordEncoder(); } + @Bean + public CorsConfigurationSource corsConfigurationSource() { + CorsConfiguration configuration = new CorsConfiguration(); + configuration.setAllowedOriginPatterns(List.of("*")); + configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH", "HEAD")); + configuration.setAllowedHeaders(List.of("*")); + configuration.setExposedHeaders(List.of("*")); + configuration.setAllowCredentials(true); + configuration.setMaxAge(3600L); + + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + source.registerCorsConfiguration("/**", configuration); + return source; + } + @Bean public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) { return http + .cors(corsSpec -> corsSpec.configurationSource(corsConfigurationSource())) .exceptionHandling(exceptionHandlingSpec -> exceptionHandlingSpec .authenticationEntryPoint((swe, e) -> Mono.fromRunnable(() -> swe.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED)) diff --git a/kifi-api/src/main/java/com/kifi/api/controller/vendor/PurchaseOrderController.java b/kifi-api/src/main/java/com/kifi/api/controller/vendor/PurchaseOrderController.java index 38260e6..f7a37b4 100644 --- a/kifi-api/src/main/java/com/kifi/api/controller/vendor/PurchaseOrderController.java +++ b/kifi-api/src/main/java/com/kifi/api/controller/vendor/PurchaseOrderController.java @@ -1,6 +1,7 @@ package com.kifi.api.controller.vendor; import com.kifi.api.entity.vendor.PurchaseOrder; +import com.kifi.api.entity.vendor.PurchaseOrderItem; import com.kifi.api.entity.vendor.PurchasePayment; import com.kifi.api.service.vendor.PurchaseOrderService; import lombok.RequiredArgsConstructor; @@ -67,11 +68,15 @@ public class PurchaseOrderController { } @PutMapping("/{id}/receive") - public Mono> markAsReceived(@PathVariable Long id, Authentication authentication, @RequestBody PurchaseOrder po) { + public Mono> markAsReceived(@PathVariable Long id, Authentication authentication, @RequestBody(required = false) PurchaseOrder po) { Long userId = Long.valueOf(authentication.getDetails().toString()); - return purchaseOrderService.markAsReceived(userId, id, po.getItems()) + List items = (po != null) ? po.getItems() : null; + return purchaseOrderService.markAsReceived(userId, id, items) .map(ResponseEntity::ok) - .onErrorResume(e -> Mono.just(ResponseEntity.badRequest().build())); + .onErrorResume(e -> { + e.printStackTrace(); + return Mono.just(ResponseEntity.internalServerError().build()); + }); } @PostMapping("/{id}/payments") diff --git a/kifi-api/src/main/java/com/kifi/api/service/vendor/PurchaseOrderService.java b/kifi-api/src/main/java/com/kifi/api/service/vendor/PurchaseOrderService.java index 19de482..afe0cfe 100644 --- a/kifi-api/src/main/java/com/kifi/api/service/vendor/PurchaseOrderService.java +++ b/kifi-api/src/main/java/com/kifi/api/service/vendor/PurchaseOrderService.java @@ -12,7 +12,10 @@ import com.kifi.api.entity.inventory.InventoryItem; import com.kifi.api.service.accounting.LedgerService; import com.kifi.api.service.TransactionService; import com.kifi.api.repository.vendor.VendorRepository; -import com.kifi.api.entity.Transaction; +import com.kifi.api.entity.inventory.InventoryLocation; +import com.kifi.api.entity.inventory.InventoryBalance; +import com.kifi.api.repository.inventory.InventoryLocationRepository; +import com.kifi.api.repository.inventory.InventoryBalanceRepository; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import reactor.core.publisher.Flux; @@ -35,7 +38,8 @@ public class PurchaseOrderService { private final LedgerService ledgerService; private final TransactionService transactionService; private final VendorRepository vendorRepository; - private final com.kifi.api.repository.inventory.InventoryBalanceRepository inventoryBalanceRepository; + private final InventoryBalanceRepository inventoryBalanceRepository; + private final InventoryLocationRepository inventoryLocationRepository; public Flux getPurchaseOrders(Long userId) { return purchaseOrderRepository.findByUserId(userId) @@ -134,6 +138,7 @@ public class PurchaseOrderService { @Transactional public Mono markAsReceived(Long userId, Long id, List receivedItems) { return purchaseOrderRepository.findByUserIdAndId(userId, id) + .switchIfEmpty(Mono.error(new RuntimeException("PO not found"))) .flatMap(po -> { if ("RECEIVED".equals(po.getStatus())) { return Mono.error(new RuntimeException("PO is already marked as received")); @@ -142,75 +147,121 @@ public class PurchaseOrderService { po.setStatus("RECEIVED"); po.setUpdatedAt(LocalDateTime.now()); + List itemsToSave = (receivedItems != null && !receivedItems.isEmpty()) ? receivedItems : po.getItems(); + return purchaseOrderRepository.save(po) - .flatMap(savedPo -> purchaseOrderItemRepository.deleteByPoId(id) - .thenMany(Flux.fromIterable(receivedItems) - .map(item -> { - item.setId(null); - item.setPoId(id); - return item; - }) - .flatMap(purchaseOrderItemRepository::save)) - .collectList() - .flatMap(savedItems -> { - // Update inventory for received items - return Flux.fromIterable(savedItems) - .flatMap(item -> { - if (item.getProductId() != null) { - InventoryItem invItem = InventoryItem.builder() - .userId(userId) - .productId(item.getProductId()) - .vendorId(savedPo.getVendorId()) - .purchaseRef(savedPo.getPoNumber()) - .purchaseCost(item.getUnitPrice()) - .makingCharges(item.getMakingCharge()) - .sku(item.getSku()) - .huid(item.getHuid()) - .grossWeight(item.getWeight()) - .netWeight(item.getWeight()) - .build(); - return inventoryItemService.createItem(userId, invItem) - .then(inventoryBalanceRepository.findByProductIdAndLocationId(item.getProductId(), 2L) // Default location 2L - .flatMap(balance -> { - balance.setQuantity(balance.getQuantity().add(item.getWeight() != null ? item.getWeight() : BigDecimal.ONE)); - balance.setLastUpdated(LocalDateTime.now()); - return inventoryBalanceRepository.save(balance); - }) - .switchIfEmpty(Mono.defer(() -> { - com.kifi.api.entity.inventory.InventoryBalance newBalance = new com.kifi.api.entity.inventory.InventoryBalance(); - newBalance.setProductId(item.getProductId()); - newBalance.setLocationId(2L); - newBalance.setQuantity(item.getWeight() != null ? item.getWeight() : BigDecimal.ONE); - newBalance.setLastUpdated(LocalDateTime.now()); - return inventoryBalanceRepository.save(newBalance); - }))) - .then(Mono.just(item)); - } - return Mono.just(item); - }) - .then(Mono.defer(() -> vendorRepository.findById(savedPo.getVendorId()))) + .flatMap(savedPo -> { + Mono> itemsProcess; + if (itemsToSave != null && !itemsToSave.isEmpty()) { + itemsProcess = purchaseOrderItemRepository.deleteByPoId(id) + .thenMany(Flux.fromIterable(itemsToSave) + .map(item -> { + item.setId(null); + item.setPoId(id); + return item; + }) + .flatMap(purchaseOrderItemRepository::save)) + .collectList(); + } else { + itemsProcess = purchaseOrderItemRepository.findByPoId(id).collectList(); + } + + return itemsProcess.flatMap(savedItems -> { + // 1. Resolve or create user's default inventory location + Mono locationIdMono = inventoryLocationRepository.findByUserId(userId) + .next() + .map(InventoryLocation::getId) + .switchIfEmpty(Mono.defer(() -> inventoryLocationRepository.save(InventoryLocation.builder() + .userId(userId) + .name("Main Store") + .isPrimary(true) + .createdAt(LocalDateTime.now()) + .build()) + .map(InventoryLocation::getId))); + + // 2. Update stock & inventory items for all received items + Mono stockUpdates = locationIdMono.flatMap(locId -> + Flux.fromIterable(savedItems) + .flatMap(item -> { + if (item.getProductId() != null) { + InventoryItem invItem = InventoryItem.builder() + .userId(userId) + .productId(item.getProductId()) + .vendorId(savedPo.getVendorId()) + .purchaseRef(savedPo.getPoNumber()) + .purchaseCost(item.getUnitPrice()) + .makingCharges(item.getMakingCharge()) + .sku(item.getSku()) + .huid(item.getHuid()) + .grossWeight(item.getWeight()) + .netWeight(item.getWeight()) + .build(); + + BigDecimal itemQty = (item.getWeight() != null && item.getWeight().compareTo(BigDecimal.ZERO) > 0) + ? item.getWeight() + : (item.getQuantity() != null ? item.getQuantity() : BigDecimal.ONE); + + Mono balanceUpdate = inventoryBalanceRepository.findByProductIdAndLocationId(item.getProductId(), locId) + .flatMap(balance -> { + balance.setQuantity(balance.getQuantity().add(itemQty)); + balance.setLastUpdated(LocalDateTime.now()); + return inventoryBalanceRepository.save(balance); + }) + .switchIfEmpty(Mono.defer(() -> { + InventoryBalance newBalance = new InventoryBalance(); + newBalance.setProductId(item.getProductId()); + newBalance.setLocationId(locId); + newBalance.setQuantity(itemQty); + newBalance.setLastUpdated(LocalDateTime.now()); + return inventoryBalanceRepository.save(newBalance); + })); + + return inventoryItemService.createItem(userId, invItem) + .then(balanceUpdate) + .then(); + } + return Mono.empty(); + }) + .then() + ); + + // 3. Double-entry accounting ledger transaction + Mono accountingUpdate = Mono.defer(() -> { + if (savedPo.getVendorId() == null || savedPo.getTotalAmount() == null || savedPo.getTotalAmount().compareTo(BigDecimal.ZERO) <= 0) { + return Mono.empty(); + } + return vendorRepository.findById(savedPo.getVendorId()) .flatMap(vendor -> Mono.zip( ledgerService.getVendorLedger(userId, vendor.getId(), vendor.getName()), ledgerService.getInventoryAssetLedger(userId) )) .flatMap(ledgers -> { - Transaction tx = Transaction.builder() + com.kifi.api.entity.Transaction tx = com.kifi.api.entity.Transaction.builder() .userId(userId) - .fromWalletId(ledgers.getT1().getId()) // Vendor Payable (Liability increases via fromWallet subtraction) - .toWalletId(ledgers.getT2().getId()) // Inventory Asset (Asset increases via toWallet addition) + .fromWalletId(ledgers.getT1().getId()) // Vendor Payable (Liability increases) + .toWalletId(ledgers.getT2().getId()) // Inventory Asset (Asset increases) .type("PURCHASE_RECEIPT") .amount(savedPo.getTotalAmount()) .date(java.time.LocalDate.now()) .description("Receipt of PO #" + savedPo.getPoNumber()) .build(); - return transactionService.addTransaction(userId, tx); + return transactionService.addTransaction(userId, tx).then(); }) - .thenReturn(savedPo) - .map(poResult -> { - poResult.setItems(savedItems); - return poResult; + .onErrorResume(err -> { + System.err.println("Accounting ledger update skipped or failed: " + err.getMessage()); + return Mono.empty(); }); - })); + }); + + return stockUpdates + .then(accountingUpdate) + .thenReturn(savedPo) + .map(poResult -> { + poResult.setItems(savedItems); + return poResult; + }); + }); + }); }); } diff --git a/kifi-api/src/test/java/com/kifi/api/DBTest.java b/kifi-api/src/test/java/com/kifi/api/DBTest.java index eff3f31..c8f49c8 100644 --- a/kifi-api/src/test/java/com/kifi/api/DBTest.java +++ b/kifi-api/src/test/java/com/kifi/api/DBTest.java @@ -2,12 +2,46 @@ package com.kifi.api; import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.beans.factory.annotation.Autowired; +import java.time.LocalDateTime; + +import com.kifi.api.repository.UserRepository; +import com.kifi.api.repository.business.BusinessProfileRepository; +import com.kifi.api.entity.User; +import com.kifi.api.entity.business.BusinessProfile; @SpringBootTest public class DBTest { + @Autowired + private UserRepository userRepo; + @Autowired + private BusinessProfileRepository repo; @Test - public void contextLoads() { - System.out.println("Spring Boot test context loaded successfully."); + public void test() { + User user = new User(); + user.setEmail("dbtest@kifi.app"); + user.setPassword("hashedpwd"); + user.setName("DB Test"); + user.setSetupStatus("NOT_STARTED"); + user.setProfileType("BUSINESS"); + + User savedUser = userRepo.save(user).block(); + if (savedUser != null && savedUser.getId() != null) { + BusinessProfile bp = BusinessProfile.builder().userId(savedUser.getId()).build(); + bp.setBusinessName("Test"); + bp.setAddress(""); + bp.setEmailId(""); + bp.setPanNumber("ENC123"); + bp.setGstin("ENC123"); + bp.setNatureOfBusiness("JEWELLERY"); + bp.setMsmeNumber("ENC123"); + bp.setCreatedAt(LocalDateTime.now()); + bp.setUpdatedAt(LocalDateTime.now()); + repo.save(bp).block(); + + // Cleanup + userRepo.deleteById(savedUser.getId()).block(); + } } } diff --git a/kifi-app/.dockerignore b/kifi-app/.dockerignore new file mode 100644 index 0000000..cbaef0a --- /dev/null +++ b/kifi-app/.dockerignore @@ -0,0 +1,16 @@ +.git +.gitignore +.idea +.vscode +.dart_tool +build +android +ios +linux +macos +windows +test +*.md +*.iml +.DS_Store +update_add_product.py diff --git a/kifi-app/Dockerfile b/kifi-app/Dockerfile new file mode 100644 index 0000000..7be24f9 --- /dev/null +++ b/kifi-app/Dockerfile @@ -0,0 +1,37 @@ +# Stage 1: Build Flutter Web Application +FROM ghcr.io/cirruslabs/flutter:stable AS builder + +WORKDIR /app + +# Build arguments for dynamic API URL and subpath base href +ARG BASE_HREF=/kifi/ +ARG API_BASE_URL=https://app.technobeesolutions.in/api/kifi-v2 +ENV API_BASE_URL=${API_BASE_URL} + +# Copy dependency definitions first to leverage Docker layer caching +COPY pubspec.yaml pubspec.lock ./ +RUN flutter pub get + +# Copy source code and assets +COPY assets ./assets +COPY lib ./lib +COPY web ./web + +# Build Flutter Web release bundle with specified base href +RUN flutter build web --release --no-tree-shake-icons --base-href ${BASE_HREF} --dart-define=API_BASE_URL=${API_BASE_URL} + +# Stage 2: Serve with Nginx +FROM nginx:alpine + +# Remove default nginx html files +RUN rm -rf /usr/share/nginx/html/* + +# Copy custom nginx configuration +COPY nginx.conf /etc/nginx/conf.d/default.conf + +# Copy built web artifacts from builder stage +COPY --from=builder /app/build/web /usr/share/nginx/html + +EXPOSE 80 + +CMD ["nginx", "-g", "daemon off;"] diff --git a/kifi-app/build_n_push.sh b/kifi-app/build_n_push.sh new file mode 100755 index 0000000..b9de43f --- /dev/null +++ b/kifi-app/build_n_push.sh @@ -0,0 +1,29 @@ +#!/bin/bash + +# Configuration +REGISTRY="hub.technobeesolutions.in" +USERNAME="technobee_admin" +PASSWORD='M@tr!x#149@dm!N' +IMAGE_NAME="kifi-app" +TAG="latest" +TAG_ONE="kifi-v2" +FULL_IMAGE_NAME="$REGISTRY/$IMAGE_NAME:$TAG" +FULL_IMAGE_NAME_ONE="$REGISTRY/$IMAGE_NAME:$TAG_ONE" +BASE_HREF="/kifi/" +API_URL="https://app.technobeesolutions.in/api/kifi-v2" + +# Stop on any error +set -e + +echo "Logging into Docker registry: $REGISTRY..." +echo "$PASSWORD" | docker login "$REGISTRY" -u "$USERNAME" --password-stdin + +echo "Building and pushing Docker image for linux/amd64: $FULL_IMAGE_NAME and $FULL_IMAGE_NAME_ONE..." +docker buildx build --no-cache --platform linux/amd64 \ + --build-arg BASE_HREF="$BASE_HREF" \ + --build-arg API_BASE_URL="$API_URL" \ + -t "$FULL_IMAGE_NAME" \ + -t "$FULL_IMAGE_NAME_ONE" \ + --push . + +echo "Done! Image built and pushed successfully." diff --git a/kifi-app/docker-compose.yml b/kifi-app/docker-compose.yml new file mode 100644 index 0000000..c6bcfbb --- /dev/null +++ b/kifi-app/docker-compose.yml @@ -0,0 +1,15 @@ +services: + kifi-app: + image: hub.technobeesolutions.in/kifi-app:latest + container_name: kifi-app + ports: + - "${PORT:-8058}:80" + restart: unless-stopped + networks: + commons-network: + ipv4_address: 172.19.0.225 + +networks: + commons-network: + external: true + name: commons_commons-network diff --git a/kifi-app/lib/core/network/dio_client.dart b/kifi-app/lib/core/network/dio_client.dart index 2f7e576..c4432f4 100644 --- a/kifi-app/lib/core/network/dio_client.dart +++ b/kifi-app/lib/core/network/dio_client.dart @@ -1,9 +1,22 @@ +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:dio/dio.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import '../../main.dart'; import '../../features/auth/presentation/auth_screen.dart'; +const String _envApiBaseUrl = String.fromEnvironment('API_BASE_URL', defaultValue: ''); + +String _getEffectiveBaseUrl() { + if (_envApiBaseUrl.isNotEmpty) { + return _envApiBaseUrl; + } + if (kIsWeb) { + return 'https://app.technobeesolutions.in/api/kifi-v2'; + } + return 'https://app.technobeesolutions.in/api/kifi-v2'; +} + class DioClient { static final DioClient _instance = DioClient._internal(); final Dio dio; @@ -26,9 +39,7 @@ class DioClient { DioClient._internal() : dio = Dio(BaseOptions( - baseUrl: 'https://app.technobeesolutions.in/api/kifi-v2', - //baseUrl: 'http://192.168.0.104:8080/api/kifi-v2', // Local Mac IP for physical device testing - //baseUrl: 'http://192.168.1.5:8080/api/kifi-v2', // Current Local Mac IP (192.168.1.5) + baseUrl: _getEffectiveBaseUrl(), connectTimeout: const Duration(seconds: 10), receiveTimeout: const Duration(seconds: 10), )), diff --git a/kifi-app/lib/core/utils/snackbar_service.dart b/kifi-app/lib/core/utils/snackbar_service.dart index 359ecb9..d69de78 100644 --- a/kifi-app/lib/core/utils/snackbar_service.dart +++ b/kifi-app/lib/core/utils/snackbar_service.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:lucide_icons/lucide_icons.dart'; +import 'package:lucide_icons_flutter/lucide_icons.dart'; class SnackBarService { static void showSuccess(BuildContext context, String message) { diff --git a/kifi-app/lib/core/widgets/desktop_sidebar.dart b/kifi-app/lib/core/widgets/desktop_sidebar.dart new file mode 100644 index 0000000..db8ca1d --- /dev/null +++ b/kifi-app/lib/core/widgets/desktop_sidebar.dart @@ -0,0 +1,454 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:lucide_icons_flutter/lucide_icons.dart'; +import '../../features/inventory/providers/commodity_rates_provider.dart'; +import '../../features/business/providers/business_provider.dart'; +import '../../features/business/providers/business_mode_provider.dart'; +import '../../features/transactions/presentation/add_transaction_screen.dart'; +import '../../features/sales/presentation/invoice_builder_screen.dart'; +import '../../features/vendor/presentation/purchase_order_builder_screen.dart'; +import '../../features/inventory/presentation/add_product_screen.dart'; +import '../../features/auth/presentation/profile_screen.dart'; + +class DesktopSidebar extends ConsumerWidget { + final int selectedIndex; + final ValueChanged onDestinationSelected; + + const DesktopSidebar({ + super.key, + required this.selectedIndex, + required this.onDestinationSelected, + }); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final theme = Theme.of(context); + final isDark = theme.brightness == Brightness.dark; + final isBusinessMode = ref.watch(businessModeProvider); + final businessProfile = ref.watch(businessProfileProvider).asData?.value; + final ratesAsync = ref.watch(commodityRatesProvider); + + return Container( + width: 260, + decoration: BoxDecoration( + color: isDark ? const Color(0xFF131720) : Colors.white, + border: Border( + right: BorderSide( + color: isDark ? Colors.white10 : Colors.black12, + width: 1, + ), + ), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // 1. Brand Logo Header + Padding( + padding: const EdgeInsets.fromLTRB(20, 24, 20, 16), + child: Row( + children: [ + Container( + width: 40, + height: 40, + decoration: BoxDecoration( + gradient: const LinearGradient( + colors: [Color(0xFFD4AF37), Color(0xFFAA771C)], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + borderRadius: BorderRadius.circular(10), + boxShadow: [ + BoxShadow( + color: const Color(0xFFD4AF37).withValues(alpha: 0.3), + blurRadius: 10, + offset: const Offset(0, 4), + ), + ], + ), + child: const Center( + child: Icon(LucideIcons.gem, color: Colors.white, size: 22), + ), + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'KIFI', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.w900, + letterSpacing: 1.2, + color: isDark ? Colors.white : const Color(0xFF1E293B), + ), + ), + Text( + businessProfile?.businessName ?? 'Financial Ledger', + style: TextStyle( + fontSize: 11, + fontWeight: FontWeight.w500, + color: isDark ? Colors.white54 : Colors.black45, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ], + ), + ), + ], + ), + ), + + // 2. Quick Action Button + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 6), + child: PopupMenuButton( + onSelected: (action) { + switch (action) { + case 'transaction': + Navigator.push( + context, + MaterialPageRoute(builder: (_) => const AddTransactionScreen()), + ); + break; + case 'invoice': + Navigator.push( + context, + MaterialPageRoute(builder: (_) => const InvoiceBuilderScreen()), + ); + break; + case 'po': + Navigator.push( + context, + MaterialPageRoute(builder: (_) => const PurchaseOrderBuilderScreen()), + ); + break; + case 'product': + Navigator.push( + context, + MaterialPageRoute(builder: (_) => const AddProductScreen()), + ); + break; + } + }, + itemBuilder: (context) => [ + const PopupMenuItem( + value: 'transaction', + child: Row( + children: [ + Icon(LucideIcons.arrowDownUp, size: 18, color: Colors.blueAccent), + SizedBox(width: 12), + Text('New Transaction'), + ], + ), + ), + if (isBusinessMode) ...[ + const PopupMenuItem( + value: 'invoice', + child: Row( + children: [ + Icon(LucideIcons.fileText, size: 18, color: Colors.green), + SizedBox(width: 12), + Text('New Sales Invoice'), + ], + ), + ), + const PopupMenuItem( + value: 'po', + child: Row( + children: [ + Icon(LucideIcons.shoppingBag, size: 18, color: Colors.purpleAccent), + SizedBox(width: 12), + Text('New Purchase Order'), + ], + ), + ), + const PopupMenuItem( + value: 'product', + child: Row( + children: [ + Icon(LucideIcons.packagePlus, size: 18, color: Color(0xFFD4AF37)), + SizedBox(width: 12), + Text('Add Inventory Product'), + ], + ), + ), + ], + ], + child: Container( + width: double.infinity, + padding: const EdgeInsets.symmetric(vertical: 11, horizontal: 14), + decoration: BoxDecoration( + gradient: LinearGradient( + colors: isDark + ? [const Color(0xFF2563EB), const Color(0xFF1D4ED8)] + : [const Color(0xFF3B82F6), const Color(0xFF2563EB)], + ), + borderRadius: BorderRadius.circular(10), + boxShadow: [ + BoxShadow( + color: const Color(0xFF2563EB).withValues(alpha: 0.3), + blurRadius: 8, + offset: const Offset(0, 3), + ), + ], + ), + child: const Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(LucideIcons.plus, color: Colors.white, size: 18), + SizedBox(width: 8), + Text( + 'Create New', + style: TextStyle( + color: Colors.white, + fontWeight: FontWeight.w600, + fontSize: 13, + ), + ), + Spacer(), + Icon(LucideIcons.chevronDown, color: Colors.white70, size: 16), + ], + ), + ), + ), + ), + + const SizedBox(height: 8), + + // 3. Navigation List + Expanded( + child: ListView( + padding: const EdgeInsets.symmetric(horizontal: 12), + children: [ + _buildNavItem( + context, + index: 0, + icon: LucideIcons.layoutDashboard, + label: 'Dashboard', + isSelected: selectedIndex == 0, + ), + _buildNavItem( + context, + index: 1, + icon: LucideIcons.arrowDownUp, + label: 'Transactions', + isSelected: selectedIndex == 1, + ), + _buildNavItem( + context, + index: 2, + icon: LucideIcons.wallet, + label: 'Wallets & Accounts', + isSelected: selectedIndex == 2, + ), + _buildNavItem( + context, + index: 3, + icon: LucideIcons.target, + label: 'Budgets & Goals', + isSelected: selectedIndex == 3, + ), + + if (isBusinessMode) ...[ + const Padding( + padding: EdgeInsets.fromLTRB(12, 20, 12, 8), + child: Text( + 'BUSINESS & ERP', + style: TextStyle( + fontSize: 10, + fontWeight: FontWeight.w700, + letterSpacing: 1.1, + color: Colors.grey, + ), + ), + ), + _buildNavItem( + context, + index: 4, + icon: LucideIcons.briefcase, + label: 'Business Hub', + isSelected: selectedIndex == 4, + ), + ], + ], + ), + ), + + // 4. Live Metal Rate Widget in Sidebar + ratesAsync.when( + data: (rates) { + if (rates.isEmpty) return const SizedBox.shrink(); + final goldRate = rates.firstWhere( + (r) => r.commodityCode == 'GOLD', + orElse: () => rates.first, + ); + return Container( + margin: const EdgeInsets.symmetric(horizontal: 14, vertical: 8), + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: isDark ? const Color(0xFF1E2330) : const Color(0xFFF8FAFC), + borderRadius: BorderRadius.circular(10), + border: Border.all( + color: const Color(0xFFD4AF37).withValues(alpha: 0.2), + ), + ), + child: Row( + children: [ + const Icon(LucideIcons.coins, color: Color(0xFFD4AF37), size: 18), + const SizedBox(width: 10), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Gold (24K/10g)', + style: TextStyle( + fontSize: 10, + fontWeight: FontWeight.w500, + color: isDark ? Colors.white60 : Colors.black54, + ), + ), + Text( + '₹${(goldRate.rate * 10).toStringAsFixed(0)}', + style: const TextStyle( + fontSize: 13, + fontWeight: FontWeight.bold, + color: Color(0xFFD4AF37), + ), + ), + ], + ), + ), + const Icon(LucideIcons.trendingUp, color: Colors.green, size: 16), + ], + ), + ); + }, + loading: () => const SizedBox.shrink(), + error: (_, __) => const SizedBox.shrink(), + ), + + // 5. Sidebar Footer (Profile & Settings) + Container( + padding: const EdgeInsets.fromLTRB(14, 12, 14, 16), + decoration: BoxDecoration( + border: Border( + top: BorderSide( + color: isDark ? Colors.white10 : Colors.black12, + ), + ), + ), + child: Row( + children: [ + Expanded( + child: InkWell( + onTap: () { + Navigator.push( + context, + MaterialPageRoute(builder: (_) => const ProfileScreen()), + ); + }, + borderRadius: BorderRadius.circular(8), + child: Padding( + padding: const EdgeInsets.all(4), + child: Row( + children: [ + CircleAvatar( + radius: 16, + backgroundColor: isDark ? const Color(0xFF2563EB) : const Color(0xFF3B82F6), + child: const Icon(LucideIcons.user, size: 16, color: Colors.white), + ), + const SizedBox(width: 10), + Expanded( + child: Text( + 'Profile & Settings', + style: TextStyle( + fontSize: 13, + fontWeight: FontWeight.w600, + color: isDark ? Colors.white : Colors.black87, + ), + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + ), + ), + ), + IconButton( + tooltip: 'Settings', + icon: const Icon(LucideIcons.settings, size: 18), + onPressed: () { + Navigator.push( + context, + MaterialPageRoute(builder: (_) => const ProfileScreen()), + ); + }, + ), + ], + ), + ), + ], + ), + ); + } + + Widget _buildNavItem( + BuildContext context, { + required int index, + required IconData icon, + required String label, + required bool isSelected, + }) { + final theme = Theme.of(context); + final isDark = theme.brightness == Brightness.dark; + + return Padding( + padding: const EdgeInsets.symmetric(vertical: 2), + child: InkWell( + onTap: () => onDestinationSelected(index), + borderRadius: BorderRadius.circular(10), + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), + decoration: BoxDecoration( + color: isSelected + ? (isDark + ? const Color(0xFF2563EB).withValues(alpha: 0.18) + : const Color(0xFF2563EB).withValues(alpha: 0.1)) + : Colors.transparent, + borderRadius: BorderRadius.circular(10), + border: isSelected + ? Border.all( + color: const Color(0xFF2563EB).withValues(alpha: 0.3), + ) + : null, + ), + child: Row( + children: [ + Icon( + icon, + size: 19, + color: isSelected + ? const Color(0xFF3B82F6) + : (isDark ? Colors.white60 : Colors.black54), + ), + const SizedBox(width: 12), + Text( + label, + style: TextStyle( + fontSize: 13, + fontWeight: isSelected ? FontWeight.w700 : FontWeight.w500, + color: isSelected + ? (isDark ? Colors.white : const Color(0xFF1D4ED8)) + : (isDark ? Colors.white70 : Colors.black87), + ), + ), + ], + ), + ), + ), + ); + } +} diff --git a/kifi-app/lib/core/widgets/responsive_layout.dart b/kifi-app/lib/core/widgets/responsive_layout.dart new file mode 100644 index 0000000..d8a5be1 --- /dev/null +++ b/kifi-app/lib/core/widgets/responsive_layout.dart @@ -0,0 +1,74 @@ +import 'package:flutter/material.dart'; + +class ResponsiveBreakpoints { + static const double mobile = 768.0; + static const double desktop = 1024.0; + static const double wideDesktop = 1440.0; +} + +class ResponsiveLayout { + static bool isMobile(BuildContext context) => + MediaQuery.sizeOf(context).width < ResponsiveBreakpoints.mobile; + + static bool isTablet(BuildContext context) { + final width = MediaQuery.sizeOf(context).width; + return width >= ResponsiveBreakpoints.mobile && width < ResponsiveBreakpoints.desktop; + } + + static bool isDesktop(BuildContext context) => + MediaQuery.sizeOf(context).width >= ResponsiveBreakpoints.mobile; + + static bool isWideDesktop(BuildContext context) => + MediaQuery.sizeOf(context).width >= ResponsiveBreakpoints.wideDesktop; +} + +class MaxContentWidth extends StatelessWidget { + final Widget child; + final double maxWidth; + final EdgeInsetsGeometry padding; + + const MaxContentWidth({ + super.key, + required this.child, + this.maxWidth = 1200.0, + this.padding = EdgeInsets.zero, + }); + + @override + Widget build(BuildContext context) { + return Center( + child: ConstrainedBox( + constraints: BoxConstraints(maxWidth: maxWidth), + child: Padding( + padding: padding, + child: child, + ), + ), + ); + } +} + +class ResponsiveBuilder extends StatelessWidget { + final Widget Function(BuildContext context) mobile; + final Widget Function(BuildContext context)? tablet; + final Widget Function(BuildContext context) desktop; + + const ResponsiveBuilder({ + super.key, + required this.mobile, + this.tablet, + required this.desktop, + }); + + @override + Widget build(BuildContext context) { + final width = MediaQuery.sizeOf(context).width; + if (width >= ResponsiveBreakpoints.desktop) { + return desktop(context); + } else if (width >= ResponsiveBreakpoints.mobile) { + return (tablet ?? desktop)(context); + } else { + return mobile(context); + } + } +} diff --git a/kifi-app/lib/core/widgets/smart_search_dropdown.dart b/kifi-app/lib/core/widgets/smart_search_dropdown.dart index 383994f..58c7821 100644 --- a/kifi-app/lib/core/widgets/smart_search_dropdown.dart +++ b/kifi-app/lib/core/widgets/smart_search_dropdown.dart @@ -33,11 +33,10 @@ class SmartSearchDropdown extends StatefulWidget { } class _SmartSearchDropdownState extends State> { + final OverlayPortalController _overlayController = OverlayPortalController(); final LayerLink _layerLink = LayerLink(); final FocusNode _focusNode = FocusNode(); final TextEditingController _controller = TextEditingController(); - OverlayEntry? _overlayEntry; - bool _showAll = false; List _filteredItems = []; @override @@ -49,15 +48,10 @@ class _SmartSearchDropdownState extends State> { } _focusNode.addListener(() { if (_focusNode.hasFocus) { - _showOverlay(); - } else { - _removeOverlay(); - // Reset text to selected value if focus lost without selection - if (widget.value != null) { - _controller.text = widget.itemAsString(widget.value as T); - } else { - _controller.text = ''; - } + setState(() { + _filteredItems = widget.items; + }); + _overlayController.show(); } }); } @@ -81,7 +75,6 @@ class _SmartSearchDropdownState extends State> { void dispose() { _focusNode.dispose(); _controller.dispose(); - _removeOverlay(); super.dispose(); } @@ -98,147 +91,164 @@ class _SmartSearchDropdownState extends State> { }).toList(); } }); - _overlayEntry?.markNeedsBuild(); } - void _showOverlay() { - _removeOverlay(); - _showAll = true; - _filteredItems = widget.items; - _overlayEntry = _createOverlayEntry(); - Overlay.of(context).insert(_overlayEntry!); - } - - void _removeOverlay() { - _overlayEntry?.remove(); - _overlayEntry = null; - } - - OverlayEntry _createOverlayEntry() { - RenderBox renderBox = context.findRenderObject() as RenderBox; - var size = renderBox.size; - - return OverlayEntry( - builder: (context) => Positioned( - width: size.width, - child: CompositedTransformFollower( - link: _layerLink, - showWhenUnlinked: false, - offset: Offset(0.0, size.height + 5.0), - child: Material( - elevation: 4.0, - borderRadius: BorderRadius.circular(8.0), - color: Theme.of(context).cardColor, - child: StatefulBuilder( - builder: (context, setOverlayState) { - final displayItems = _showAll ? _filteredItems : (_filteredItems.isNotEmpty ? [_filteredItems.first] : []); - - return Container( - constraints: const BoxConstraints(maxHeight: 250), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - if (displayItems.isEmpty) - Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - children: [ - const Text('No matches found'), - if (widget.emptyActionText != null && widget.onEmptyActionPressed != null) - Padding( - padding: const EdgeInsets.only(top: 8.0), - child: TextButton( - onPressed: () { - _focusNode.unfocus(); - widget.onEmptyActionPressed!(); - }, - child: Text(widget.emptyActionText!), - ), - ), - ], - ), - ) - else - Flexible( - child: ListView.builder( - shrinkWrap: true, - padding: EdgeInsets.zero, - itemCount: displayItems.length, - itemBuilder: (context, index) { - final item = displayItems[index]; - return InkWell( - onTap: () { - widget.onChanged(item); - _controller.text = widget.itemAsString(item); - _focusNode.unfocus(); - }, - child: widget.itemBuilder != null - ? widget.itemBuilder!(context, item) - : Padding( - padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 12.0), - child: Text(widget.itemAsString(item)), - ), - ); - }, - ), - ), - if (!_showAll && _controller.text.isEmpty && widget.items.length > 1) - InkWell( - onTap: () { - setOverlayState(() { - _showAll = true; - }); - }, - child: Container( - width: double.infinity, - padding: const EdgeInsets.all(12.0), - decoration: BoxDecoration( - border: Border(top: BorderSide(color: Colors.grey.withOpacity(0.2))), - ), - child: const Text( - 'Show all', - textAlign: TextAlign.center, - style: TextStyle(color: Colors.blue, fontWeight: FontWeight.bold), - ), - ), - ), - ], - ), - ); - } - ), - ), - ), - ), - ); + void _hideOverlay() { + _overlayController.hide(); + if (widget.value != null) { + _controller.text = widget.itemAsString(widget.value as T); + } else { + _controller.text = ''; + } } @override Widget build(BuildContext context) { return CompositedTransformTarget( link: _layerLink, - child: TextFormField( - controller: _controller, - focusNode: _focusNode, - decoration: InputDecoration( - labelText: widget.labelText, - labelStyle: TextStyle(color: Theme.of(context).brightness == Brightness.dark ? Colors.white70 : Colors.grey[600]), - hintText: widget.hintText, - suffixIcon: const Icon(Icons.arrow_drop_down, color: Colors.grey), - filled: Theme.of(context).inputDecorationTheme.filled ?? true, - fillColor: widget.fillColor ?? Theme.of(context).inputDecorationTheme.fillColor ?? (Theme.of(context).brightness == Brightness.dark ? Colors.black26 : Colors.grey[100]), - border: Theme.of(context).inputDecorationTheme.border ?? OutlineInputBorder(borderRadius: BorderRadius.circular(16), borderSide: BorderSide.none), - enabledBorder: Theme.of(context).inputDecorationTheme.enabledBorder ?? OutlineInputBorder(borderRadius: BorderRadius.circular(16), borderSide: BorderSide.none), - focusedBorder: Theme.of(context).inputDecorationTheme.focusedBorder ?? OutlineInputBorder( - borderRadius: BorderRadius.circular(16), - borderSide: BorderSide(color: Theme.of(context).colorScheme.primary, width: 2) + child: TapRegion( + groupId: _layerLink, + child: OverlayPortal( + controller: _overlayController, + overlayChildBuilder: (BuildContext overlayContext) { + final renderBox = context.findRenderObject() as RenderBox?; + final width = (renderBox != null && renderBox.hasSize) ? renderBox.size.width : 300.0; + + return CompositedTransformFollower( + link: _layerLink, + showWhenUnlinked: false, + targetAnchor: Alignment.bottomLeft, + followerAnchor: Alignment.topLeft, + offset: const Offset(0.0, 6.0), + child: Align( + alignment: Alignment.topLeft, + child: SizedBox( + width: width, + child: TapRegion( + groupId: _layerLink, + onTapOutside: (event) { + _focusNode.unfocus(); + _hideOverlay(); + }, + child: Material( + elevation: 8.0, + shadowColor: Colors.black26, + borderRadius: BorderRadius.circular(16.0), + color: Theme.of(context).cardColor, + clipBehavior: Clip.antiAlias, + child: Container( + constraints: const BoxConstraints(maxHeight: 280), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(16.0), + border: Border.all( + color: Theme.of(context).dividerColor.withValues(alpha: 0.15), + ), + ), + child: _filteredItems.isEmpty + ? Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Text('No matches found', style: TextStyle(color: Colors.grey)), + if (widget.emptyActionText != null && widget.onEmptyActionPressed != null) + Padding( + padding: const EdgeInsets.only(top: 8.0), + child: TextButton( + onPressed: () { + _focusNode.unfocus(); + _overlayController.hide(); + widget.onEmptyActionPressed!(); + }, + child: Text(widget.emptyActionText!), + ), + ), + ], + ), + ) + : ListView.separated( + shrinkWrap: true, + padding: const EdgeInsets.symmetric(vertical: 4), + itemCount: _filteredItems.length, + separatorBuilder: (context, i) => Divider( + height: 1, + color: Theme.of(context).dividerColor.withValues(alpha: 0.1), + ), + itemBuilder: (context, index) { + final item = _filteredItems[index]; + final isSelected = widget.value == item; + + return InkWell( + onTap: () { + widget.onChanged(item); + _controller.text = widget.itemAsString(item); + _focusNode.unfocus(); + _overlayController.hide(); + }, + child: Container( + color: isSelected + ? Theme.of(context).colorScheme.primary.withValues(alpha: 0.1) + : null, + child: widget.itemBuilder != null + ? widget.itemBuilder!(context, item) + : Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 14.0), + child: Text( + widget.itemAsString(item), + style: TextStyle( + fontWeight: isSelected ? FontWeight.bold : FontWeight.normal, + color: isSelected ? Theme.of(context).colorScheme.primary : null, + ), + ), + ), + ), + ); + }, + ), + ), + ), + ), + ), + ), + ); + }, + child: TextFormField( + controller: _controller, + focusNode: _focusNode, + decoration: InputDecoration( + labelText: widget.labelText, + hintText: widget.hintText, + suffixIcon: IconButton( + icon: Icon( + _overlayController.isShowing ? Icons.arrow_drop_up : Icons.arrow_drop_down, + color: Colors.grey, + ), + onPressed: () { + if (_overlayController.isShowing) { + _focusNode.unfocus(); + _overlayController.hide(); + } else { + _focusNode.requestFocus(); + _filterItems(_controller.text); + _overlayController.show(); + } + }, + ), + ), + onTap: () { + if (!_overlayController.isShowing) { + _filterItems(_controller.text); + _overlayController.show(); + } + }, + onChanged: (val) { + if (!_overlayController.isShowing) { + _overlayController.show(); + } + _filterItems(val); + }, ), - contentPadding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16), ), - onChanged: (val) { - _showAll = true; // when user types, we want to show all matching results - _filterItems(val); - }, ), ); } diff --git a/kifi-app/lib/features/auth/presentation/auth_screen.dart b/kifi-app/lib/features/auth/presentation/auth_screen.dart index f7364bd..3507ad6 100644 --- a/kifi-app/lib/features/auth/presentation/auth_screen.dart +++ b/kifi-app/lib/features/auth/presentation/auth_screen.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:lucide_icons/lucide_icons.dart'; +import 'package:lucide_icons_flutter/lucide_icons.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../providers/auth_provider.dart'; diff --git a/kifi-app/lib/features/auth/presentation/forgot_password_screen.dart b/kifi-app/lib/features/auth/presentation/forgot_password_screen.dart index 21f2f22..d1c198a 100644 --- a/kifi-app/lib/features/auth/presentation/forgot_password_screen.dart +++ b/kifi-app/lib/features/auth/presentation/forgot_password_screen.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:lucide_icons/lucide_icons.dart'; +import 'package:lucide_icons_flutter/lucide_icons.dart'; import '../providers/auth_provider.dart'; import 'reset_password_screen.dart'; diff --git a/kifi-app/lib/features/auth/presentation/otp_screen.dart b/kifi-app/lib/features/auth/presentation/otp_screen.dart index 4c7a3ef..4617236 100644 --- a/kifi-app/lib/features/auth/presentation/otp_screen.dart +++ b/kifi-app/lib/features/auth/presentation/otp_screen.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:lucide_icons/lucide_icons.dart'; +import 'package:lucide_icons_flutter/lucide_icons.dart'; import '../providers/auth_provider.dart'; import '../../dashboard/presentation/dashboard_screen.dart'; import 'setup_wizard_screen.dart'; diff --git a/kifi-app/lib/features/auth/presentation/profile_screen.dart b/kifi-app/lib/features/auth/presentation/profile_screen.dart index 8b9e980..19533cd 100644 --- a/kifi-app/lib/features/auth/presentation/profile_screen.dart +++ b/kifi-app/lib/features/auth/presentation/profile_screen.dart @@ -3,7 +3,7 @@ import 'dart:ui'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; -import 'package:lucide_icons/lucide_icons.dart'; +import 'package:lucide_icons_flutter/lucide_icons.dart'; import 'package:path_provider/path_provider.dart'; import 'package:share_plus/share_plus.dart'; import 'package:cross_file/cross_file.dart'; diff --git a/kifi-app/lib/features/auth/presentation/reset_password_screen.dart b/kifi-app/lib/features/auth/presentation/reset_password_screen.dart index 433f955..c9372f5 100644 --- a/kifi-app/lib/features/auth/presentation/reset_password_screen.dart +++ b/kifi-app/lib/features/auth/presentation/reset_password_screen.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:lucide_icons/lucide_icons.dart'; +import 'package:lucide_icons_flutter/lucide_icons.dart'; import '../providers/auth_provider.dart'; import 'auth_screen.dart'; diff --git a/kifi-app/lib/features/auth/presentation/setup_wizard_screen.dart b/kifi-app/lib/features/auth/presentation/setup_wizard_screen.dart index f26f012..3613a89 100644 --- a/kifi-app/lib/features/auth/presentation/setup_wizard_screen.dart +++ b/kifi-app/lib/features/auth/presentation/setup_wizard_screen.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:lucide_icons/lucide_icons.dart'; +import 'package:lucide_icons_flutter/lucide_icons.dart'; import 'package:intl_phone_field/intl_phone_field.dart'; import '../../../../core/network/dio_client.dart'; import '../../dashboard/presentation/dashboard_screen.dart'; diff --git a/kifi-app/lib/features/budget/presentation/budget_screen.dart b/kifi-app/lib/features/budget/presentation/budget_screen.dart index d2b343d..097e790 100644 --- a/kifi-app/lib/features/budget/presentation/budget_screen.dart +++ b/kifi-app/lib/features/budget/presentation/budget_screen.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:lucide_icons/lucide_icons.dart'; +import 'package:lucide_icons_flutter/lucide_icons.dart'; import '../../transactions/providers/providers.dart'; import '../../transactions/data/models.dart'; import '../../../core/widgets/shimmer_loading.dart'; diff --git a/kifi-app/lib/features/business/presentation/hub/business_hub_screen.dart b/kifi-app/lib/features/business/presentation/hub/business_hub_screen.dart index 3e51d70..239dc64 100644 --- a/kifi-app/lib/features/business/presentation/hub/business_hub_screen.dart +++ b/kifi-app/lib/features/business/presentation/hub/business_hub_screen.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:lucide_icons/lucide_icons.dart'; +import 'package:lucide_icons_flutter/lucide_icons.dart'; import '../../../../core/theme/nature_colors.dart'; import '../../../inventory/presentation/product_list_screen.dart'; import '../../../inventory/presentation/daily_rates_screen.dart'; diff --git a/kifi-app/lib/features/business/presentation/reports/reports_screen.dart b/kifi-app/lib/features/business/presentation/reports/reports_screen.dart index b20529f..491f4da 100644 --- a/kifi-app/lib/features/business/presentation/reports/reports_screen.dart +++ b/kifi-app/lib/features/business/presentation/reports/reports_screen.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:lucide_icons/lucide_icons.dart'; +import 'package:lucide_icons_flutter/lucide_icons.dart'; import 'package:intl/intl.dart'; import '../../../inventory/providers/inventory_valuation_provider.dart'; diff --git a/kifi-app/lib/features/business/presentation/settings/business_settings_screen.dart b/kifi-app/lib/features/business/presentation/settings/business_settings_screen.dart index bed463e..09085c0 100644 --- a/kifi-app/lib/features/business/presentation/settings/business_settings_screen.dart +++ b/kifi-app/lib/features/business/presentation/settings/business_settings_screen.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:lucide_icons/lucide_icons.dart'; +import 'package:lucide_icons_flutter/lucide_icons.dart'; import '../../providers/business_provider.dart'; class BusinessSettingsScreen extends ConsumerStatefulWidget { diff --git a/kifi-app/lib/features/dashboard/presentation/accounts_screen.dart b/kifi-app/lib/features/dashboard/presentation/accounts_screen.dart index d0cf159..770ed16 100644 --- a/kifi-app/lib/features/dashboard/presentation/accounts_screen.dart +++ b/kifi-app/lib/features/dashboard/presentation/accounts_screen.dart @@ -1,11 +1,12 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:lucide_icons/lucide_icons.dart'; +import 'package:lucide_icons_flutter/lucide_icons.dart'; import 'package:intl/intl.dart'; import '../../transactions/providers/providers.dart'; import '../../transactions/data/models.dart'; import '../../transactions/data/repository.dart'; import 'wallet_ledger_screen.dart'; +import '../../../core/widgets/responsive_layout.dart'; class AccountsScreen extends ConsumerStatefulWidget { final String? initialFilterNature; @@ -449,9 +450,11 @@ class _AccountsScreenState extends ConsumerState { final walletsState = ref.watch(walletProvider); return Scaffold( - body: SafeArea( - bottom: false, - child: Column( + body: MaxContentWidth( + maxWidth: 1200, + child: SafeArea( + bottom: false, + child: Column( children: [ Padding( padding: const EdgeInsets.fromLTRB(16, 16, 16, 12), @@ -939,6 +942,7 @@ class _AccountsScreenState extends ConsumerState { ], ), ), + ), ); } } diff --git a/kifi-app/lib/features/dashboard/presentation/dashboard_screen.dart b/kifi-app/lib/features/dashboard/presentation/dashboard_screen.dart index 77b7c7d..39d4cd0 100644 --- a/kifi-app/lib/features/dashboard/presentation/dashboard_screen.dart +++ b/kifi-app/lib/features/dashboard/presentation/dashboard_screen.dart @@ -1,37 +1,30 @@ import 'dart:async'; import 'package:flutter/material.dart'; -import '../../projects/presentation/project_hub_screen.dart'; -import '../../projects/presentation/projects_screen.dart'; -import '../../projects/providers/project_mode_provider.dart'; - import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:lucide_icons/lucide_icons.dart'; +import 'package:lucide_icons_flutter/lucide_icons.dart'; import 'package:intl/intl.dart'; import '../../transactions/presentation/add_transaction_screen.dart'; import '../../transactions/presentation/all_transactions_screen.dart'; import '../../transactions/providers/providers.dart'; import '../../auth/presentation/profile_screen.dart'; import '../../transactions/data/models.dart'; - import '../../budget/presentation/budget_screen.dart'; - import '../providers/insight_provider.dart'; import '../../sales/providers/invoices_provider.dart'; import '../../sales/providers/customers_provider.dart'; -import '../../sales/domain/invoice.dart'; import '../../sales/presentation/customers_list_screen.dart'; import 'widgets/swipeable_account_card.dart'; import 'widgets/budget_status_card.dart'; - import 'widgets/upcoming_dues_widget.dart'; import 'widgets/statistics_tab.dart'; import '../../../core/widgets/shimmer_loading.dart'; import '../../../core/theme/nature_colors.dart'; -import 'wallet_ledger_screen.dart'; import 'maturity_dialog.dart'; import 'accounts_screen.dart'; import '../../business/providers/business_mode_provider.dart'; import '../../business/presentation/hub/business_hub_screen.dart'; +import '../../../core/widgets/responsive_layout.dart'; +import '../../../core/widgets/desktop_sidebar.dart'; class DashboardScreen extends ConsumerStatefulWidget { const DashboardScreen({super.key}); @@ -81,7 +74,7 @@ class _DashboardScreenState extends ConsumerState { } return DateTimeRange(start: start, end: DateTime.now()); } - + final now = DateTime.now(); DateTime start; DateTime end = now; @@ -121,14 +114,14 @@ class _DashboardScreenState extends ConsumerState { List _filterTransactions(List all) { if (_selectedFilter == 'All Time' || (_selectedFilter == 'Custom' && _customDateRange == null)) return all; - + final range = _getDateRange(all); final start = range.start; final end = range.end; return all.where((t) { final d = t.date; - return d.isAfter(start.subtract(const Duration(seconds: 1))) && - d.isBefore(end.add(const Duration(days: 1))); + return d.isAfter(start.subtract(const Duration(seconds: 1))) && + d.isBefore(end.add(const Duration(days: 1))); }).toList(); } @@ -154,130 +147,32 @@ class _DashboardScreenState extends ConsumerState { final walletsState = ref.watch(walletProvider); final invoicesState = ref.watch(invoicesProvider); final customersState = ref.watch(customersProvider); - final isProjectMode = ref.watch(projectModeProvider); final isBusinessMode = ref.watch(businessModeProvider); final allTransactions = transState.value ?? []; - + final range = _getDateRange(allTransactions); final startDate = range.start; final endDate = range.end; - String title = 'Dashboard'; - int logicalIndex = _currentIndex; - if (logicalIndex == 0) title = 'Dashboard'; - else { - if (isProjectMode) { - if (logicalIndex == 1) title = 'Projects'; - logicalIndex--; - } - if (isBusinessMode) { - if (logicalIndex == 1) title = 'Business Hub'; - logicalIndex--; - } - if (logicalIndex == 1) title = 'Statistics'; - else if (logicalIndex == 2) title = 'My Accounts'; - else if (logicalIndex == 3) title = 'Budgets'; - } + final isDesktop = ResponsiveLayout.isDesktop(context); return Scaffold( - appBar: AppBar( - title: Text(title), - backgroundColor: Colors.transparent, - elevation: 0, - actions: [ - Consumer( - builder: (context, ref, child) { - final invitationsState = ref.watch(invitationProvider); - final pendingInvites = invitationsState.value?.where((inv) => inv.status == 'PENDING').toList() ?? []; - - return PopupMenuButton( - icon: Stack( - children: [ - const Icon(LucideIcons.bell), - if (pendingInvites.isNotEmpty) - Positioned( - right: 0, - top: 0, - child: Container( - padding: const EdgeInsets.all(2), - decoration: BoxDecoration( - color: Colors.red, - borderRadius: BorderRadius.circular(6), - ), - constraints: const BoxConstraints(minWidth: 12, minHeight: 12), - child: Text( - '${pendingInvites.length}', - style: const TextStyle(color: Colors.white, fontSize: 8, fontWeight: FontWeight.bold), - textAlign: TextAlign.center, - ), - ), - ) - ], + appBar: isDesktop + ? null + : AppBar( + title: const Text('Dashboard'), + backgroundColor: Colors.transparent, + elevation: 0, + actions: [ + _buildNotificationBell(context), + IconButton( + icon: const Icon(LucideIcons.user), + onPressed: () { + Navigator.push(context, MaterialPageRoute(builder: (_) => const ProfileScreen())); + }, ), - offset: const Offset(0, 50), - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), - itemBuilder: (context) { - if (pendingInvites.isEmpty) { - return [ - const PopupMenuItem( - enabled: false, - child: Text('No new notifications'), - ) - ]; - } - return pendingInvites.map((inv) => PopupMenuItem( - enabled: false, - child: Container( - width: 250, - padding: const EdgeInsets.symmetric(vertical: 8), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text('You have been invited to a wallet by User ${inv.inviterId}', style: const TextStyle(fontSize: 14)), - const SizedBox(height: 12), - Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - TextButton( - onPressed: () { - Navigator.pop(context); - ref.read(invitationProvider.notifier).rejectInvitation(inv.id); - }, - style: TextButton.styleFrom(foregroundColor: Colors.red), - child: const Text('Reject'), - ), - const SizedBox(width: 8), - ElevatedButton( - onPressed: () { - Navigator.pop(context); - ref.read(invitationProvider.notifier).acceptInvitation(inv.id); - }, - style: ElevatedButton.styleFrom( - backgroundColor: const Color(0xFF6C63FF), - foregroundColor: Colors.white, - elevation: 0, - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), - ), - child: const Text('Accept'), - ), - ], - ) - ], - ), - ), - )).toList(); - }, - ); - } - ), - IconButton( - icon: const Icon(LucideIcons.user), - onPressed: () { - Navigator.push(context, MaterialPageRoute(builder: (_) => const ProfileScreen())); - }, - ), - ], - ), + ], + ), body: transState.when( loading: () => ListView.builder( padding: const EdgeInsets.all(16), @@ -288,27 +183,7 @@ class _DashboardScreenState extends ConsumerState { data: (allTransactions) { final transactions = _filterTransactions(allTransactions); - final totalIncome = transactions.where((t) => t.type == 'INCOME').fold(0.0, (s, t) => s + t.amount); - final totalExpense = transactions.where((t) => t.type == 'EXPENSE').fold(0.0, (s, t) => s + t.amount); - final totalInvestment = transactions.where((t) => t.type == 'INVESTMENT').fold(0.0, (s, t) => s + t.amount); - - double totalPayablesPeriod = 0.0; - double totalReceivablesPeriod = 0.0; - final safeWallets = walletsState.hasValue ? walletsState.value! : []; - for (var t in transactions) { - if (t.toWalletId != null) { - final w = safeWallets.where((w) => w.id == t.toWalletId); - if (w.isNotEmpty) { - final nature = w.first.nature; - if (nature == 'PAYABLES' || nature == 'LOAN') { - totalPayablesPeriod += t.amount; - } else if (nature == 'RECEIVABLES' || nature == 'LENDING') { - totalReceivablesPeriod += t.amount; - } - } - } - } double cashBalance = 0; double expenseBalance = 0; @@ -316,7 +191,7 @@ class _DashboardScreenState extends ConsumerState { double investmentsBalance = 0; double payablesBalance = 0; double receivablesBalance = 0; - + if (walletsState.hasValue) { for (var w in walletsState.value!) { final nature = w.nature ?? 'CASH'; @@ -335,50 +210,38 @@ class _DashboardScreenState extends ConsumerState { } } } - + // Process invoices to get total pending - double totalPendingInvoices = 0; Map pendingByCustomer = {}; - - if (invoicesState.hasValue && customersState.hasValue) { + if (invoicesState.hasValue) { final invoices = invoicesState.value!; - for (var inv in invoices) { if (inv.status != 'PAID' && inv.status != 'CANCELLED' && inv.customerId != null) { double paidAmount = inv.amountPaid ?? 0; double pendingAmount = inv.totalAmount - paidAmount; if (pendingAmount > 0) { pendingByCustomer[inv.customerId!] = (pendingByCustomer[inv.customerId!] ?? 0) + pendingAmount; - totalPendingInvoices += pendingAmount; } } } } - - receivablesBalance += totalPendingInvoices; - - // Build Payables Carousel Items + List payablesItems = []; if (walletsState.hasValue) { - final payableWallets = walletsState.value!.where((w) => w.nature == 'PAYABLES' || w.nature == 'LOAN').toList(); - for (var w in payableWallets) { - if (w.balance > 0) { + final wallets = walletsState.value!; + for (var w in wallets) { + if ((w.nature == 'PAYABLES' || w.nature == 'LOAN') && w.balance > 0) { payablesItems.add(CarouselItemData( - title: 'To: ${w.name}', + title: w.name, amount: w.balance, color: NatureColors.getColor('PAYABLES'), - icon: LucideIcons.userMinus, - onTap: () { - Navigator.push(context, MaterialPageRoute(builder: (_) => WalletLedgerScreen(wallet: w))); - } + icon: LucideIcons.alertCircle, )); } } } - // Build Receivables Carousel Items List receivablesItems = []; - if (invoicesState.hasValue && customersState.hasValue) { final customers = customersState.value!; pendingByCustomer.forEach((custId, amount) { @@ -390,310 +253,533 @@ class _DashboardScreenState extends ConsumerState { color: NatureColors.getColor('RECEIVABLES'), icon: LucideIcons.userPlus, onTap: () { - // Navigate to customer details or invoices in the future - } + Navigator.push(context, MaterialPageRoute(builder: (_) => const CustomersListScreen())); + }, )); } }); } - final List screens = []; - - screens.add(RefreshIndicator( - onRefresh: _onRefresh, - child: SingleChildScrollView( - physics: const AlwaysScrollableScrollPhysics(), - padding: const EdgeInsets.all(24.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (insight != null) ...[ - Container( - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: insight.type == 'WARNING' ? Colors.orange.shade50 : (insight.type == 'SUCCESS' ? Colors.green.shade50 : Colors.blue.shade50), - borderRadius: BorderRadius.circular(16), - border: Border.all(color: insight.type == 'WARNING' ? Colors.orange : (insight.type == 'SUCCESS' ? Colors.green : Colors.blue), width: 1), - ), - child: Row( + // Desktop multi-column dashboard body + Widget desktopDashboardView = RefreshIndicator( + onRefresh: _onRefresh, + child: SingleChildScrollView( + physics: const AlwaysScrollableScrollPhysics(), + padding: const EdgeInsets.symmetric(horizontal: 28.0, vertical: 24.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Top Row Header + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Financial Overview', + style: Theme.of(context).textTheme.headlineMedium?.copyWith( + fontWeight: FontWeight.w800, + letterSpacing: -0.5, + ), + ), + const SizedBox(height: 4), + Text( + DateFormat('EEEE, MMMM d, yyyy').format(DateTime.now()), + style: TextStyle( + fontSize: 13, + color: Theme.of(context).brightness == Brightness.dark + ? Colors.white60 + : Colors.black54, + ), + ), + ], + ), + Row( + children: [ + _buildNotificationBell(context), + const SizedBox(width: 8), + ElevatedButton.icon( + onPressed: () { + Navigator.push( + context, + MaterialPageRoute(builder: (_) => const AddTransactionScreen()), + ); + }, + icon: const Icon(LucideIcons.plus, size: 16), + label: const Text('Add Transaction'), + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFF2563EB), + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 12), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), + ), + ), + ], + ), + ], + ), + const SizedBox(height: 24), + + if (insight != null) ...[ + _buildInsightCard(insight), + const SizedBox(height: 24), + ], + + // 3-Column Summary Cards Grid on Desktop + GridView.count( + crossAxisCount: 3, + crossAxisSpacing: 16, + mainAxisSpacing: 16, + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + childAspectRatio: 2.2, + children: [ + _buildSummaryCard(context, 'Balance', cashBalance, NatureColors.getColor('BALANCE'), LucideIcons.wallet, onTap: () => Navigator.push(context, MaterialPageRoute(builder: (_) => const AccountsScreen(initialFilterNature: 'CASH')))), + _buildSummaryCard(context, 'Expense', expenseBalance, NatureColors.getColor('EXPENSE'), LucideIcons.arrowUpRight, onTap: () => Navigator.push(context, MaterialPageRoute(builder: (_) => const AccountsScreen(initialFilterNature: 'EXPENSE')))), + _buildSummaryCard(context, 'Savings', savingsBalance, NatureColors.getColor('SAVINGS'), LucideIcons.piggyBank, onTap: () => Navigator.push(context, MaterialPageRoute(builder: (_) => const AccountsScreen(initialFilterNature: 'SAVINGS')))), + _buildSummaryCard(context, 'Investments', investmentsBalance, NatureColors.getColor('INVESTMENTS'), LucideIcons.trendingUp, onTap: () => Navigator.push(context, MaterialPageRoute(builder: (_) => const AccountsScreen(initialFilterNature: 'INVESTMENTS')))), + _buildSummaryCard(context, 'Total Payables', payablesBalance, NatureColors.getColor('PAYABLES'), LucideIcons.alertCircle, onTap: () => Navigator.push(context, MaterialPageRoute(builder: (_) => const AccountsScreen(initialFilterNature: 'PAYABLES')))), + _buildSummaryCard(context, 'Total Receivables', receivablesBalance, NatureColors.getColor('RECEIVABLES'), LucideIcons.arrowDownLeft, onTap: () { + Navigator.push(context, MaterialPageRoute(builder: (_) => const CustomersListScreen())); + }), + ], + ), + const SizedBox(height: 28), + + // 2-Column Split Body Layout on Desktop + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Left Column (60%): Budget status & Recent Transactions + Expanded( + flex: 6, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - Icon( - insight.type == 'WARNING' ? LucideIcons.alertTriangle : (insight.type == 'SUCCESS' ? LucideIcons.checkCircle : LucideIcons.info), - color: insight.type == 'WARNING' ? Colors.orange : (insight.type == 'SUCCESS' ? Colors.green : Colors.blue), - ), - const SizedBox(width: 12), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text(insight.title, style: const TextStyle(fontWeight: FontWeight.bold)), - const SizedBox(height: 4), - Text(insight.message, style: const TextStyle(fontSize: 12)), - ], - ), + const BudgetStatusCard(), + const SizedBox(height: 24), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text('Recent Transactions', style: Theme.of(context).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.bold)), + TextButton( + onPressed: () { + Navigator.push(context, MaterialPageRoute(builder: (_) => const AllTransactionsScreen())); + }, + child: const Text('See All'), + ), + ], ), + const SizedBox(height: 8), + _buildRecentTransactionsList(allTransactions, safeWallets), ], ), ), - const SizedBox(height: 24), - ], - const BudgetStatusCard(), - const SizedBox(height: 24), - // Summary Cards Grid - GridView.count( - crossAxisCount: 2, - crossAxisSpacing: 16, - mainAxisSpacing: 16, - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - childAspectRatio: 1.5, - children: [ - _buildSummaryCard(context, 'Balance', cashBalance, NatureColors.getColor('BALANCE'), LucideIcons.wallet, onTap: () => Navigator.push(context, MaterialPageRoute(builder: (_) => const AccountsScreen(initialFilterNature: 'CASH')))), - _buildSummaryCard(context, 'Expense', expenseBalance, NatureColors.getColor('EXPENSE'), LucideIcons.arrowUpRight, onTap: () => Navigator.push(context, MaterialPageRoute(builder: (_) => const AccountsScreen(initialFilterNature: 'EXPENSE')))), - _buildSummaryCard(context, 'Savings', savingsBalance, NatureColors.getColor('SAVINGS'), LucideIcons.piggyBank, onTap: () => Navigator.push(context, MaterialPageRoute(builder: (_) => const AccountsScreen(initialFilterNature: 'SAVINGS')))), - _buildSummaryCard(context, 'Investments', investmentsBalance, NatureColors.getColor('INVESTMENTS'), LucideIcons.trendingUp, onTap: () => Navigator.push(context, MaterialPageRoute(builder: (_) => const AccountsScreen(initialFilterNature: 'INVESTMENTS')))), - _buildSummaryCard(context, 'Total Payables', payablesBalance, NatureColors.getColor('PAYABLES'), LucideIcons.alertCircle, onTap: () => Navigator.push(context, MaterialPageRoute(builder: (_) => const AccountsScreen(initialFilterNature: 'PAYABLES')))), - _buildSummaryCard(context, 'Total Receivables', receivablesBalance, NatureColors.getColor('RECEIVABLES'), LucideIcons.arrowDownLeft, onTap: () { - Navigator.push(context, MaterialPageRoute(builder: (_) => const CustomersListScreen())); - }), - ], - ), - - const SizedBox(height: 24), - const UpcomingDuesWidget(), + const SizedBox(width: 24), - if (payablesItems.isNotEmpty) ...[ - const SizedBox(height: 24), - Text('Upcoming Payables', style: Theme.of(context).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.bold)), - const SizedBox(height: 12), - SwipeableAccountCard(items: payablesItems), - ], - - if (receivablesItems.isNotEmpty) ...[ - const SizedBox(height: 24), - Text('Upcoming Receivables', style: Theme.of(context).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.bold)), - const SizedBox(height: 12), - SwipeableAccountCard(items: receivablesItems), - ], - - const SizedBox(height: 32), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text('Recent Transactions', style: Theme.of(context).textTheme.bodyLarge?.copyWith(fontWeight: FontWeight.bold)), - TextButton( - onPressed: () { - Navigator.push(context, MaterialPageRoute(builder: (_) => const AllTransactionsScreen())); - }, - child: const Text('See All'), + // Right Column (40%): Upcoming Dues & Payables / Receivables + Expanded( + flex: 4, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const UpcomingDuesWidget(), + if (payablesItems.isNotEmpty) ...[ + const SizedBox(height: 24), + Text('Upcoming Payables', style: Theme.of(context).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.bold)), + const SizedBox(height: 12), + SwipeableAccountCard(items: payablesItems), + ], + if (receivablesItems.isNotEmpty) ...[ + const SizedBox(height: 24), + Text('Upcoming Receivables', style: Theme.of(context).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.bold)), + const SizedBox(height: 12), + SwipeableAccountCard(items: receivablesItems), + ], + ], ), - ], - ), - const SizedBox(height: 8), - allTransactions.isEmpty - ? const Center(child: Padding(padding: EdgeInsets.all(16), child: Text('No transactions yet!'))) - : ListView.builder( - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - itemCount: allTransactions.length > 5 ? 5 : allTransactions.length, - itemBuilder: (context, index) { - final t = allTransactions[index]; - final isIncome = t.type == 'INCOME' || (t.type == 'TRANSFER' && t.fromWalletId == null); - final isInvestment = t.type == 'INVESTMENT'; - final isExpense = t.type == 'EXPENSE' || (t.type == 'TRANSFER' && t.toWalletId == null); - final isTransfer = t.type == 'TRANSFER' && t.fromWalletId != null && t.toWalletId != null; - - Color typeColor = Colors.grey; - IconData typeIcon = LucideIcons.arrowRightLeft; - - if (isIncome) { - typeColor = NatureColors.getColor('INCOME'); - typeIcon = LucideIcons.arrowDownCircle; - } else if (isExpense) { - typeColor = NatureColors.getColor('EXPENSE'); - typeIcon = LucideIcons.arrowUpCircle; - } else if (isInvestment) { - typeColor = NatureColors.getColor('INVESTMENTS'); - typeIcon = LucideIcons.trendingUp; - } else if (isTransfer) { - typeColor = NatureColors.getColor('TRANSFER'); - if (walletsState.hasValue) { - final toW = walletsState.value!.where((w) => w.id == t.toWalletId).firstOrNull; - final fromW = walletsState.value!.where((w) => w.id == t.fromWalletId).firstOrNull; - - if (toW != null && (toW.nature == 'PAYABLES' || toW.nature == 'LOAN')) { - typeColor = NatureColors.getColor('PAYABLES'); - typeIcon = LucideIcons.alertCircle; - } else if (toW != null && (toW.nature == 'RECEIVABLES' || toW.nature == 'LENDING')) { - typeColor = NatureColors.getColor('RECEIVABLES'); - typeIcon = LucideIcons.arrowDownLeft; - } else if (fromW != null && (fromW.nature == 'PAYABLES' || fromW.nature == 'LOAN')) { - typeColor = NatureColors.getColor('PAYABLES'); - typeIcon = LucideIcons.alertCircle; - } else if (fromW != null && (fromW.nature == 'RECEIVABLES' || fromW.nature == 'LENDING')) { - typeColor = NatureColors.getColor('RECEIVABLES'); - typeIcon = LucideIcons.arrowDownLeft; - } - } - } - - String categoryName = 'Unknown'; - if (categoriesState.hasValue) { - final match = categoriesState.value!.where((c) => c.id == t.categoryId); - if (match.isNotEmpty) categoryName = match.first.name; - } - - String accountName = 'Unknown'; - if (walletsState.hasValue) { - if (isExpense && t.toWalletId != null) { - final match = walletsState.value!.where((w) => w.id == t.toWalletId); - if (match.isNotEmpty) accountName = match.first.name; - } else if (isIncome && (t.toWalletId != null)) { - final match = walletsState.value!.where((w) => w.id == t.toWalletId); - if (match.isNotEmpty) accountName = match.first.name; - } else if (isTransfer && t.toWalletId != null) { - final match = walletsState.value!.where((w) => w.id == t.toWalletId); - if (match.isNotEmpty) accountName = 'To ${match.first.name}'; - } - } - - String fromName = ''; - if (t.fromWalletId != null && walletsState.hasValue) { - final match = walletsState.value!.where((w) => w.id == t.fromWalletId); - if (match.isNotEmpty) fromName = match.first.name; - } - - String toName = ''; - if (t.toWalletId != null && walletsState.hasValue) { - final match = walletsState.value!.where((w) => w.id == t.toWalletId); - if (match.isNotEmpty) toName = match.first.name; - } - - String subtitleText = ''; - if (categoryName == 'Unknown' && fromName.isNotEmpty && toName.isNotEmpty) { - subtitleText = '$fromName → $toName • ${DateFormat('MMM dd, yyyy').format(t.date)}'; - } else if (categoryName == 'Unknown' && toName.isNotEmpty) { - subtitleText = 'To $toName • ${DateFormat('MMM dd, yyyy').format(t.date)}'; - } else if (categoryName == 'Unknown' && fromName.isNotEmpty) { - subtitleText = 'From $fromName • ${DateFormat('MMM dd, yyyy').format(t.date)}'; - } else { - subtitleText = '$categoryName • ${DateFormat('MMM dd, yyyy').format(t.date)}${fromName.isNotEmpty ? ' • 💼 $fromName' : ''}'; - } - - return Card( - margin: const EdgeInsets.only(bottom: 12), - child: ListTile( - onTap: () { - Navigator.push(context, MaterialPageRoute(builder: (_) => AddTransactionScreen(transaction: t))); - }, - leading: CircleAvatar( - backgroundColor: typeColor.withOpacity(0.1), - child: Icon(typeIcon, color: typeColor), - ), - title: Text((t.description != null && t.description!.isNotEmpty) ? t.description! : accountName, style: const TextStyle(fontWeight: FontWeight.bold)), - subtitle: Text(subtitleText), - trailing: Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - Text( - '${isIncome ? '+' : (isTransfer ? '' : '-')}Rs. ${t.amount.toStringAsFixed(0)}', - style: TextStyle(fontWeight: FontWeight.bold, color: typeColor), - ), - if (t.investmentStatus == 'OPEN' && (typeColor == NatureColors.getColor('INVESTMENTS') || typeColor == NatureColors.getColor('RECEIVABLES'))) - GestureDetector( - onTap: () { - showDialog(context: context, builder: (_) => MaturityDialog(transaction: t)); - }, - child: const Padding( - padding: EdgeInsets.only(top: 4.0), - child: Text('Close', style: TextStyle(color: Colors.blue, fontWeight: FontWeight.bold, fontSize: 12)), - ), - ), - ], - ), - ), - ); - }, - ) - ], - ), + ), + ], + ), + ], ), - )); + ), + ); + // Mobile single-column scrollable dashboard body + Widget mobileDashboardView = RefreshIndicator( + onRefresh: _onRefresh, + child: SingleChildScrollView( + physics: const AlwaysScrollableScrollPhysics(), + padding: const EdgeInsets.all(20.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (insight != null) ...[ + _buildInsightCard(insight), + const SizedBox(height: 24), + ], + const BudgetStatusCard(), + const SizedBox(height: 24), + GridView.count( + crossAxisCount: 2, + crossAxisSpacing: 16, + mainAxisSpacing: 16, + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + childAspectRatio: 1.5, + children: [ + _buildSummaryCard(context, 'Balance', cashBalance, NatureColors.getColor('BALANCE'), LucideIcons.wallet, onTap: () => Navigator.push(context, MaterialPageRoute(builder: (_) => const AccountsScreen(initialFilterNature: 'CASH')))), + _buildSummaryCard(context, 'Expense', expenseBalance, NatureColors.getColor('EXPENSE'), LucideIcons.arrowUpRight, onTap: () => Navigator.push(context, MaterialPageRoute(builder: (_) => const AccountsScreen(initialFilterNature: 'EXPENSE')))), + _buildSummaryCard(context, 'Savings', savingsBalance, NatureColors.getColor('SAVINGS'), LucideIcons.piggyBank, onTap: () => Navigator.push(context, MaterialPageRoute(builder: (_) => const AccountsScreen(initialFilterNature: 'SAVINGS')))), + _buildSummaryCard(context, 'Investments', investmentsBalance, NatureColors.getColor('INVESTMENTS'), LucideIcons.trendingUp, onTap: () => Navigator.push(context, MaterialPageRoute(builder: (_) => const AccountsScreen(initialFilterNature: 'INVESTMENTS')))), + _buildSummaryCard(context, 'Total Payables', payablesBalance, NatureColors.getColor('PAYABLES'), LucideIcons.alertCircle, onTap: () => Navigator.push(context, MaterialPageRoute(builder: (_) => const AccountsScreen(initialFilterNature: 'PAYABLES')))), + _buildSummaryCard(context, 'Total Receivables', receivablesBalance, NatureColors.getColor('RECEIVABLES'), LucideIcons.arrowDownLeft, onTap: () { + Navigator.push(context, MaterialPageRoute(builder: (_) => const CustomersListScreen())); + }), + ], + ), + const SizedBox(height: 24), + const UpcomingDuesWidget(), + if (payablesItems.isNotEmpty) ...[ + const SizedBox(height: 24), + Text('Upcoming Payables', style: Theme.of(context).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.bold)), + const SizedBox(height: 12), + SwipeableAccountCard(items: payablesItems), + ], + if (receivablesItems.isNotEmpty) ...[ + const SizedBox(height: 24), + Text('Upcoming Receivables', style: Theme.of(context).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.bold)), + const SizedBox(height: 12), + SwipeableAccountCard(items: receivablesItems), + ], + const SizedBox(height: 32), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text('Recent Transactions', style: Theme.of(context).textTheme.bodyLarge?.copyWith(fontWeight: FontWeight.bold)), + TextButton( + onPressed: () { + Navigator.push(context, MaterialPageRoute(builder: (_) => const AllTransactionsScreen())); + }, + child: const Text('See All'), + ), + ], + ), + const SizedBox(height: 8), + _buildRecentTransactionsList(allTransactions, safeWallets), + ], + ), + ), + ); - if (isBusinessMode) { - screens.add(const BusinessHubScreen()); + if (isDesktop) { + final List desktopScreens = [ + desktopDashboardView, + const AllTransactionsScreen(), + const AccountsScreen(), + const BudgetScreen(), + if (isBusinessMode) const BusinessHubScreen(), + ]; + + int safeDesktopIndex = _currentIndex; + if (safeDesktopIndex >= desktopScreens.length) { + safeDesktopIndex = 0; + } + + return Row( + children: [ + DesktopSidebar( + selectedIndex: safeDesktopIndex, + onDestinationSelected: (idx) { + setState(() => _currentIndex = idx); + }, + ), + Expanded( + child: MaxContentWidth( + maxWidth: 1400, + child: IndexedStack( + index: safeDesktopIndex, + children: desktopScreens, + ), + ), + ), + ], + ); } - screens.add(StatisticsTab( - transactions: transactions, - wallets: safeWallets, - categories: categoriesState.value ?? [], - selectedFilter: _selectedFilter, - startDate: startDate, - endDate: endDate, - filters: _filters, - onFilterChanged: (val) { - setState(() => _selectedFilter = val); - }, - onPickCustomDateRange: _pickCustomDateRange, - onRefresh: _onRefresh, - )); - screens.add(const AccountsScreen()); - screens.add(const BudgetScreen()); + // Mobile View + final List mobileScreens = [ + mobileDashboardView, + if (isBusinessMode) const BusinessHubScreen(), + StatisticsTab( + transactions: transactions, + wallets: safeWallets, + categories: categoriesState.value ?? [], + selectedFilter: _selectedFilter, + startDate: startDate, + endDate: endDate, + filters: _filters, + onFilterChanged: (val) { + setState(() => _selectedFilter = val); + }, + onPickCustomDateRange: _pickCustomDateRange, + onRefresh: _onRefresh, + ), + const AccountsScreen(), + const BudgetScreen(), + ]; - int safeIndex = _currentIndex; - if (safeIndex >= screens.length) safeIndex = screens.length - 1; + int safeMobileIndex = _currentIndex; + if (safeMobileIndex >= mobileScreens.length) { + safeMobileIndex = 0; + } return IndexedStack( - index: safeIndex, - children: screens, + index: safeMobileIndex, + children: mobileScreens, ); }, ), - bottomNavigationBar: Builder( - builder: (context) { - final isBusinessMode = ref.watch(businessModeProvider); - final List navItems = []; - navItems.add(const BottomNavigationBarItem(icon: Icon(LucideIcons.home), label: 'Home')); - if (isBusinessMode) navItems.add(const BottomNavigationBarItem(icon: Icon(LucideIcons.briefcase), label: 'Business')); - navItems.add(const BottomNavigationBarItem(icon: Icon(LucideIcons.pieChart), label: 'Stats')); - navItems.add(const BottomNavigationBarItem(icon: Icon(LucideIcons.plusCircle), label: 'Add')); - navItems.add(const BottomNavigationBarItem(icon: Icon(LucideIcons.wallet), label: 'Wallets')); - navItems.add(const BottomNavigationBarItem(icon: Icon(LucideIcons.target), label: 'Budgets')); + bottomNavigationBar: isDesktop + ? null + : Builder( + builder: (context) { + final isBusinessMode = ref.watch(businessModeProvider); + final List navItems = []; + navItems.add(const BottomNavigationBarItem(icon: Icon(LucideIcons.home), label: 'Home')); + if (isBusinessMode) navItems.add(const BottomNavigationBarItem(icon: Icon(LucideIcons.briefcase), label: 'Business')); + navItems.add(const BottomNavigationBarItem(icon: Icon(LucideIcons.pieChart), label: 'Stats')); + navItems.add(const BottomNavigationBarItem(icon: Icon(LucideIcons.plusCircle), label: 'Add')); + navItems.add(const BottomNavigationBarItem(icon: Icon(LucideIcons.wallet), label: 'Wallets')); + navItems.add(const BottomNavigationBarItem(icon: Icon(LucideIcons.target), label: 'Budgets')); - int safeIndex = _currentIndex; - // Adjust _currentIndex for the display of bottom nav bar because of 'Add' button - int addIdx = 1; - if (isBusinessMode) addIdx++; - addIdx++; // For Stats + int addIdx = 1; + if (isBusinessMode) addIdx++; + addIdx++; // For Stats - int displayIndex = _currentIndex; - if (_currentIndex >= addIdx) displayIndex++; + int displayIndex = _currentIndex; + if (_currentIndex >= addIdx) displayIndex++; + if (displayIndex >= navItems.length) displayIndex = navItems.length - 1; - if (displayIndex >= navItems.length) displayIndex = navItems.length - 1; + return BottomNavigationBar( + currentIndex: displayIndex, + type: BottomNavigationBarType.fixed, + onTap: (index) { + if (index == addIdx) { + Navigator.push(context, MaterialPageRoute(builder: (_) => const AddTransactionScreen())); + } else { + setState(() { + _currentIndex = index > addIdx ? index - 1 : index; + }); + } + }, + selectedItemColor: Theme.of(context).colorScheme.primary, + unselectedItemColor: Colors.grey, + showSelectedLabels: true, + showUnselectedLabels: true, + items: navItems, + ); + }, + ), + ); + } - return BottomNavigationBar( - currentIndex: displayIndex, - type: BottomNavigationBarType.fixed, - onTap: (index) { - if (index == addIdx) { - Navigator.push(context, MaterialPageRoute(builder: (_) => const AddTransactionScreen())); - } else { - setState(() { - _currentIndex = index > addIdx ? index - 1 : index; - }); - } - }, - selectedItemColor: Theme.of(context).colorScheme.primary, - unselectedItemColor: Colors.grey, - showSelectedLabels: true, - showUnselectedLabels: true, - items: navItems, - ); + Widget _buildInsightCard(dynamic insight) { + return Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: insight.type == 'WARNING' ? Colors.orange.shade50 : (insight.type == 'SUCCESS' ? Colors.green.shade50 : Colors.blue.shade50), + borderRadius: BorderRadius.circular(16), + border: Border.all(color: insight.type == 'WARNING' ? Colors.orange : (insight.type == 'SUCCESS' ? Colors.green : Colors.blue), width: 1), + ), + child: Row( + children: [ + Icon( + insight.type == 'WARNING' ? LucideIcons.alertTriangle : (insight.type == 'SUCCESS' ? LucideIcons.checkCircle : LucideIcons.info), + color: insight.type == 'WARNING' ? Colors.orange : (insight.type == 'SUCCESS' ? Colors.green : Colors.blue), + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(insight.title, style: const TextStyle(fontWeight: FontWeight.bold)), + const SizedBox(height: 4), + Text(insight.message, style: const TextStyle(fontSize: 12)), + ], + ), + ), + ], + ), + ); + } + + Widget _buildRecentTransactionsList(List allTransactions, List safeWallets) { + if (allTransactions.isEmpty) { + return const Center(child: Padding(padding: EdgeInsets.all(16), child: Text('No transactions yet!'))); + } + + return ListView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: allTransactions.length > 6 ? 6 : allTransactions.length, + itemBuilder: (context, index) { + final t = allTransactions[index]; + final isIncome = t.type == 'INCOME' || (t.type == 'TRANSFER' && t.fromWalletId == null); + final isInvestment = t.type == 'INVESTMENT'; + final isExpense = t.type == 'EXPENSE' || (t.type == 'TRANSFER' && t.toWalletId == null); + final isTransfer = t.type == 'TRANSFER' && t.fromWalletId != null && t.toWalletId != null; + + Color typeColor = Colors.grey; + IconData typeIcon = LucideIcons.arrowRightLeft; + + if (isIncome) { + typeColor = NatureColors.getColor('INCOME'); + typeIcon = LucideIcons.arrowDownCircle; + } else if (isExpense) { + typeColor = NatureColors.getColor('EXPENSE'); + typeIcon = LucideIcons.arrowUpCircle; + } else if (isInvestment) { + typeColor = NatureColors.getColor('INVESTMENTS'); + typeIcon = LucideIcons.trendingUp; + } else if (isTransfer) { + typeColor = NatureColors.getColor('TRANSFER'); + final w = safeWallets.where((w) => w.id == t.toWalletId); + if (w.isNotEmpty) { + typeColor = NatureColors.getColor(w.first.nature ?? 'TRANSFER'); + } } - ), ); + + String accountName = 'Unknown Wallet'; + final targetWalletId = t.toWalletId ?? t.fromWalletId; + if (targetWalletId != null) { + final w = safeWallets.where((w) => w.id == targetWalletId); + if (w.isNotEmpty) accountName = w.first.name; + } + + String subtitleText = DateFormat('MMM dd, yyyy').format(t.date); + if (t.description != null && t.description!.isNotEmpty) { + subtitleText = '$accountName • $subtitleText'; + } + + return Card( + margin: const EdgeInsets.only(bottom: 8), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + child: ListTile( + leading: CircleAvatar( + backgroundColor: typeColor.withValues(alpha: 0.1), + child: Icon(typeIcon, color: typeColor), + ), + title: Text( + (t.description != null && t.description!.isNotEmpty) ? t.description! : accountName, + style: const TextStyle(fontWeight: FontWeight.bold), + ), + subtitle: Text(subtitleText), + trailing: Text( + '${isIncome ? '+' : (isTransfer ? '' : '-')}Rs. ${t.amount.toStringAsFixed(0)}', + style: TextStyle(fontWeight: FontWeight.bold, color: typeColor), + ), + onTap: () { + Navigator.push( + context, + MaterialPageRoute(builder: (_) => AddTransactionScreen(transaction: t)), + ); + }, + ), + ); + }, + ); + } + + Widget _buildNotificationBell(BuildContext context) { + return Consumer( + builder: (context, ref, child) { + final invitationsState = ref.watch(invitationProvider); + final pendingInvites = invitationsState.value?.where((inv) => inv.status == 'PENDING').toList() ?? []; + + return PopupMenuButton( + icon: Stack( + children: [ + const Icon(LucideIcons.bell), + if (pendingInvites.isNotEmpty) + Positioned( + right: 0, + top: 0, + child: Container( + padding: const EdgeInsets.all(2), + decoration: BoxDecoration( + color: Colors.red, + borderRadius: BorderRadius.circular(6), + ), + constraints: const BoxConstraints(minWidth: 12, minHeight: 12), + child: Text( + '${pendingInvites.length}', + style: const TextStyle(color: Colors.white, fontSize: 8, fontWeight: FontWeight.bold), + textAlign: TextAlign.center, + ), + ), + ) + ], + ), + offset: const Offset(0, 50), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), + itemBuilder: (context) { + if (pendingInvites.isEmpty) { + return [ + const PopupMenuItem( + enabled: false, + child: Text('No new notifications'), + ) + ]; + } + return pendingInvites + .map((inv) => PopupMenuItem( + enabled: false, + child: Container( + width: 250, + padding: const EdgeInsets.symmetric(vertical: 8), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('You have been invited to a wallet by User ${inv.inviterId}', style: const TextStyle(fontSize: 14)), + const SizedBox(height: 12), + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + TextButton( + onPressed: () { + Navigator.pop(context); + ref.read(invitationProvider.notifier).rejectInvitation(inv.id); + }, + child: const Text('Decline', style: TextStyle(color: Colors.red)), + ), + const SizedBox(width: 8), + ElevatedButton( + onPressed: () { + Navigator.pop(context); + ref.read(invitationProvider.notifier).acceptInvitation(inv.id); + }, + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFF6C63FF), + foregroundColor: Colors.white, + elevation: 0, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + ), + child: const Text('Accept'), + ), + ], + ) + ], + ), + ), + )) + .toList(); + }, + ); + }, + ); } Widget _buildSummaryCard(BuildContext context, String title, double amount, Color color, IconData icon, {VoidCallback? onTap}) { @@ -702,9 +788,9 @@ class _DashboardScreenState extends ConsumerState { child: Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( - color: color.withOpacity(0.1), + color: color.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(16), - border: Border.all(color: color.withOpacity(0.2)), + border: Border.all(color: color.withValues(alpha: 0.2)), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, diff --git a/kifi-app/lib/features/dashboard/presentation/maturity_dialog.dart b/kifi-app/lib/features/dashboard/presentation/maturity_dialog.dart index 9e71c10..f730c95 100644 --- a/kifi-app/lib/features/dashboard/presentation/maturity_dialog.dart +++ b/kifi-app/lib/features/dashboard/presentation/maturity_dialog.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:lucide_icons/lucide_icons.dart'; +import 'package:lucide_icons_flutter/lucide_icons.dart'; import 'package:intl/intl.dart'; import '../../transactions/data/models.dart'; import '../../transactions/providers/providers.dart'; diff --git a/kifi-app/lib/features/dashboard/presentation/widgets/budget_status_card.dart b/kifi-app/lib/features/dashboard/presentation/widgets/budget_status_card.dart index b9a564e..467bf4c 100644 --- a/kifi-app/lib/features/dashboard/presentation/widgets/budget_status_card.dart +++ b/kifi-app/lib/features/dashboard/presentation/widgets/budget_status_card.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:lucide_icons/lucide_icons.dart'; +import 'package:lucide_icons_flutter/lucide_icons.dart'; import '../../../transactions/providers/providers.dart'; class BudgetStatusCard extends ConsumerWidget { diff --git a/kifi-app/lib/features/dashboard/presentation/widgets/statistics_tab.dart b/kifi-app/lib/features/dashboard/presentation/widgets/statistics_tab.dart index 2900b59..5af5f3e 100644 --- a/kifi-app/lib/features/dashboard/presentation/widgets/statistics_tab.dart +++ b/kifi-app/lib/features/dashboard/presentation/widgets/statistics_tab.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:lucide_icons/lucide_icons.dart'; +import 'package:lucide_icons_flutter/lucide_icons.dart'; import '../../../transactions/data/models.dart'; import '../../../../core/theme/nature_colors.dart'; import 'day_wise_spending_chart.dart'; diff --git a/kifi-app/lib/features/dashboard/presentation/widgets/swipeable_account_card.dart b/kifi-app/lib/features/dashboard/presentation/widgets/swipeable_account_card.dart index a75a07e..a3d356d 100644 --- a/kifi-app/lib/features/dashboard/presentation/widgets/swipeable_account_card.dart +++ b/kifi-app/lib/features/dashboard/presentation/widgets/swipeable_account_card.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:lucide_icons/lucide_icons.dart'; +import 'package:lucide_icons_flutter/lucide_icons.dart'; class CarouselItemData { final String title; diff --git a/kifi-app/lib/features/dashboard/presentation/widgets/upcoming_dues_widget.dart b/kifi-app/lib/features/dashboard/presentation/widgets/upcoming_dues_widget.dart index fd5999c..786b3a0 100644 --- a/kifi-app/lib/features/dashboard/presentation/widgets/upcoming_dues_widget.dart +++ b/kifi-app/lib/features/dashboard/presentation/widgets/upcoming_dues_widget.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:lucide_icons/lucide_icons.dart'; +import 'package:lucide_icons_flutter/lucide_icons.dart'; import 'package:intl/intl.dart'; import '../../../transactions/providers/providers.dart'; import '../../../transactions/data/models.dart'; diff --git a/kifi-app/lib/features/inventory/domain/product.dart b/kifi-app/lib/features/inventory/domain/product.dart index adf938b..4757866 100644 --- a/kifi-app/lib/features/inventory/domain/product.dart +++ b/kifi-app/lib/features/inventory/domain/product.dart @@ -85,7 +85,14 @@ class Product { 0.0, isActive: json['isActive'] ?? json['is_active'] ?? true, imageIds: json['images'] != null - ? (json['images'] as List).map((i) => i['id'] as int).toList() + ? (json['images'] as List).map((i) { + if (i is Map) { + return (i['id'] as num).toInt(); + } else if (i is num) { + return i.toInt(); + } + return 0; + }).where((id) => id > 0).toList() : [], currentStock: (json['currentStock'] ?? json['current_stock'] as num?)?.toDouble() ?? diff --git a/kifi-app/lib/features/inventory/presentation/add_product_screen.dart b/kifi-app/lib/features/inventory/presentation/add_product_screen.dart index 8a92803..60b0437 100644 --- a/kifi-app/lib/features/inventory/presentation/add_product_screen.dart +++ b/kifi-app/lib/features/inventory/presentation/add_product_screen.dart @@ -1,14 +1,17 @@ import 'dart:io'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:lucide_icons/lucide_icons.dart'; +import 'package:lucide_icons_flutter/lucide_icons.dart'; import 'package:image_picker/image_picker.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart' as import_storage; import '../../../core/network/dio_client.dart'; import '../../../../core/widgets/smart_search_dropdown.dart'; +import '../../../core/widgets/responsive_layout.dart'; import '../domain/product.dart'; import '../providers/products_provider.dart'; import '../providers/product_categories_provider.dart'; +import '../providers/uoms_provider.dart'; import '../../business/providers/business_provider.dart'; import '../../transactions/presentation/widgets/attachment_gallery_screen.dart'; @@ -113,7 +116,11 @@ class _AddProductScreenState extends ConsumerState { } } for (final file in _images) { - allImages.add(FileImage(File(file.path))); + if (kIsWeb) { + allImages.add(NetworkImage(file.path)); + } else { + allImages.add(FileImage(File(file.path))); + } } Navigator.push( @@ -169,6 +176,7 @@ class _AddProductScreenState extends ConsumerState { @override Widget build(BuildContext context) { final categoriesState = ref.watch(productCategoriesProvider); + final uomsState = ref.watch(uomsProvider); return Scaffold( backgroundColor: Theme.of(context).colorScheme.surface, @@ -177,9 +185,12 @@ class _AddProductScreenState extends ConsumerState { backgroundColor: Colors.transparent, elevation: 0, ), - body: GestureDetector( - onTap: () => FocusScope.of(context).unfocus(), - child: Column( + body: MaxContentWidth( + maxWidth: 900, + child: GestureDetector( + behavior: HitTestBehavior.translucent, + onTap: () => FocusScope.of(context).unfocus(), + child: Column( children: [ Expanded( child: Form( @@ -209,16 +220,21 @@ class _AddProductScreenState extends ConsumerState { const Text('Category & Unit', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600)), const SizedBox(height: 12), categoriesState.when( - loading: () => const CircularProgressIndicator(), - error: (err, stack) => Text('Error loading categories: $err'), + loading: () => const Padding( + padding: EdgeInsets.symmetric(vertical: 8), + child: LinearProgressIndicator(), + ), + error: (err, stack) => Text('Error loading categories: $err', style: const TextStyle(color: Colors.red)), data: (categories) { - final leafCategories = categories.where((c) => !categories.any((child) => child.parentCategoryId == c.id)).toList(); - return leafCategories.isEmpty + final activeCategories = categories.where((c) => c.isActive).toList(); + final uoms = uomsState.value ?? []; + + return activeCategories.isEmpty ? const Text('No categories found. Manage categories in Business Hub.', style: TextStyle(color: Colors.orange)) : SmartSearchDropdown( hintText: 'Select Category*', value: _selectedCategory, - items: leafCategories, + items: activeCategories, itemAsString: (c) => c.name, itemBuilder: (context, item) { List path = []; @@ -232,12 +248,13 @@ class _AddProductScreenState extends ConsumerState { } } return Padding( - padding: const EdgeInsets.all(12.0), + padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 12.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(item.name, style: const TextStyle(fontWeight: FontWeight.bold)), - Text(path.join(' -> '), style: TextStyle(fontSize: 12, color: Colors.grey.shade600)), + if (path.length > 1) + Text(path.join(' -> '), style: TextStyle(fontSize: 12, color: Colors.grey.shade600)), ], ), ); @@ -258,6 +275,15 @@ class _AddProductScreenState extends ConsumerState { if (val.makingChargeType != null && val.makingChargeType!.isNotEmpty) { _makingChargesType = val.makingChargeType!; } + if (_uomId == null && uoms.isNotEmpty) { + final match = uoms.where((u) => + u.abbreviation?.toLowerCase() == val.baseUnit.toLowerCase() || + u.name.toLowerCase() == val.baseUnit.toLowerCase() + ).firstOrNull; + if (match != null) { + _uomId = match.id; + } + } } }); }, @@ -265,15 +291,28 @@ class _AddProductScreenState extends ConsumerState { }, ), const SizedBox(height: 16), - DropdownButtonFormField( - decoration: const InputDecoration(labelText: 'Unit of Measure (Optional)'), - value: _uomId, - items: const [ - DropdownMenuItem(value: 1, child: Text('Grams (g)')), - DropdownMenuItem(value: 2, child: Text('Kilograms (kg)')), - DropdownMenuItem(value: 3, child: Text('Pieces (pcs)')), - ], - onChanged: (val) => setState(() => _uomId = val), + uomsState.when( + loading: () => const Padding( + padding: EdgeInsets.symmetric(vertical: 8), + child: LinearProgressIndicator(), + ), + error: (err, stack) => Text('Error loading units: $err', style: const TextStyle(color: Colors.red)), + data: (uoms) { + final effectiveValue = uoms.any((u) => u.id == _uomId) ? _uomId : null; + return DropdownButtonFormField( + decoration: const InputDecoration( + labelText: 'Unit of Measure (Optional)', + ), + isExpanded: true, + hint: const Text('None (Optional)'), + value: effectiveValue, + items: uoms.map((u) => DropdownMenuItem( + value: u.id, + child: Text(u.displayName), + )).toList(), + onChanged: (val) => setState(() => _uomId = val), + ); + }, ), const SizedBox(height: 24), @@ -314,6 +353,7 @@ class _AddProductScreenState extends ConsumerState { Expanded( child: DropdownButtonFormField( value: _makingChargesType, + isExpanded: true, decoration: const InputDecoration(labelText: 'Charge Type'), items: const [ DropdownMenuItem(value: 'PER_GRAM', child: Text('Per Gram')), @@ -364,7 +404,7 @@ class _AddProductScreenState extends ConsumerState { final localIndex = index - _existingImageIds.length; return _buildImageThumbnail( isNetwork: false, - file: File(_images[localIndex].path), + xFile: _images[localIndex], onTap: () => _previewImage(index), onDelete: () => _removeImage(localIndex), ); @@ -379,8 +419,8 @@ class _AddProductScreenState extends ConsumerState { width: double.infinity, padding: const EdgeInsets.symmetric(vertical: 24), decoration: BoxDecoration( - color: Theme.of(context).colorScheme.primary.withOpacity(0.05), - border: Border.all(color: Theme.of(context).colorScheme.primary.withOpacity(0.3)), + color: Theme.of(context).colorScheme.primary.withValues(alpha: 0.05), + border: Border.all(color: Theme.of(context).colorScheme.primary.withValues(alpha: 0.3)), borderRadius: BorderRadius.circular(16), ), child: Column( @@ -403,6 +443,7 @@ class _AddProductScreenState extends ConsumerState { ], ), ), + ), ); } @@ -428,7 +469,13 @@ class _AddProductScreenState extends ConsumerState { ); } - Widget _buildImageThumbnail({required bool isNetwork, String? url, File? file, required VoidCallback onDelete, required VoidCallback onTap}) { + Widget _buildImageThumbnail({ + required bool isNetwork, + String? url, + XFile? xFile, + required VoidCallback onDelete, + required VoidCallback onTap, + }) { return Stack( fit: StackFit.expand, children: [ @@ -437,7 +484,7 @@ class _AddProductScreenState extends ConsumerState { child: Container( decoration: BoxDecoration( borderRadius: BorderRadius.circular(16), - border: Border.all(color: Colors.grey.withOpacity(0.2)), + border: Border.all(color: Colors.grey.withValues(alpha: 0.2)), ), child: ClipRRect( borderRadius: BorderRadius.circular(16), @@ -452,7 +499,9 @@ class _AddProductScreenState extends ConsumerState { headers: {'Authorization': 'Bearer $_token'}, errorBuilder: (ctx, err, stack) => const Icon(LucideIcons.imageOff, color: Colors.grey), )) - : Image.file(file!, fit: BoxFit.cover), + : (kIsWeb + ? Image.network(xFile!.path, fit: BoxFit.cover) + : Image.file(File(xFile!.path), fit: BoxFit.cover)), ), ), ), diff --git a/kifi-app/lib/features/inventory/presentation/category_management_screen.dart b/kifi-app/lib/features/inventory/presentation/category_management_screen.dart index ccd32ef..c19d20c 100644 --- a/kifi-app/lib/features/inventory/presentation/category_management_screen.dart +++ b/kifi-app/lib/features/inventory/presentation/category_management_screen.dart @@ -4,7 +4,7 @@ import '../providers/product_categories_provider.dart'; import '../../../core/theme/nature_colors.dart'; import '../../business/providers/business_mode_provider.dart'; import '../../../core/theme/app_theme.dart'; -import 'package:lucide_icons/lucide_icons.dart'; +import 'package:lucide_icons_flutter/lucide_icons.dart'; import 'dart:ui'; import '../../../core/utils/purity_utils.dart'; diff --git a/kifi-app/lib/features/inventory/presentation/daily_rates_screen.dart b/kifi-app/lib/features/inventory/presentation/daily_rates_screen.dart index 486d014..5269514 100644 --- a/kifi-app/lib/features/inventory/presentation/daily_rates_screen.dart +++ b/kifi-app/lib/features/inventory/presentation/daily_rates_screen.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:lucide_icons/lucide_icons.dart'; +import 'package:lucide_icons_flutter/lucide_icons.dart'; import 'package:intl/intl.dart'; import '../providers/product_categories_provider.dart'; import '../providers/commodity_rates_provider.dart'; diff --git a/kifi-app/lib/features/inventory/presentation/product_list_screen.dart b/kifi-app/lib/features/inventory/presentation/product_list_screen.dart index 28407f5..881e3fd 100644 --- a/kifi-app/lib/features/inventory/presentation/product_list_screen.dart +++ b/kifi-app/lib/features/inventory/presentation/product_list_screen.dart @@ -1,12 +1,13 @@ import 'package:flutter/material.dart'; import 'dart:async'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:lucide_icons/lucide_icons.dart'; +import 'package:lucide_icons_flutter/lucide_icons.dart'; import '../providers/products_provider.dart'; import '../providers/product_categories_provider.dart'; import 'add_product_screen.dart'; import 'product_detail_screen.dart'; import '../../../core/network/dio_client.dart'; +import '../../../core/widgets/responsive_layout.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; class ProductListScreen extends ConsumerStatefulWidget { @@ -66,7 +67,9 @@ class _ProductListScreenState extends ConsumerState { foregroundColor: Colors.black, centerTitle: true, ), - body: Column( + body: MaxContentWidth( + maxWidth: 1000, + child: Column( children: [ Container( color: Colors.white, @@ -138,13 +141,10 @@ class _ProductListScreenState extends ConsumerState { } final p = products[index]; - final catName = - categoriesState.value - ?.firstWhere( - (c) => c.id == p.categoryId, - orElse: () => categoriesState.value!.first, - ) - .name ?? + final catName = categoriesState.value + ?.where((c) => c.id == p.categoryId) + .firstOrNull + ?.name ?? 'No Category'; return Container( @@ -267,12 +267,16 @@ class _ProductListScreenState extends ConsumerState { ), ], ), + ), floatingActionButton: FloatingActionButton( - onPressed: () { - Navigator.push( + onPressed: () async { + await Navigator.push( context, MaterialPageRoute(builder: (_) => const AddProductScreen()), ); + if (mounted) { + ref.read(productsProvider.notifier).refresh(); + } }, backgroundColor: Theme.of(context).colorScheme.primary, child: const Icon(LucideIcons.plus, color: Colors.white), diff --git a/kifi-app/lib/features/inventory/presentation/quick_adjust_stock_screen.dart b/kifi-app/lib/features/inventory/presentation/quick_adjust_stock_screen.dart index e99ccef..eda6b72 100644 --- a/kifi-app/lib/features/inventory/presentation/quick_adjust_stock_screen.dart +++ b/kifi-app/lib/features/inventory/presentation/quick_adjust_stock_screen.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'dart:async'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:lucide_icons/lucide_icons.dart'; +import 'package:lucide_icons_flutter/lucide_icons.dart'; import 'package:kifi_app/features/inventory/providers/products_provider.dart'; import 'package:kifi_app/features/inventory/presentation/widgets/adjust_stock_sheet.dart'; import 'package:kifi_app/core/network/dio_client.dart'; diff --git a/kifi-app/lib/features/inventory/presentation/stock_ledger_tab.dart b/kifi-app/lib/features/inventory/presentation/stock_ledger_tab.dart index f44bc4b..56e8cfe 100644 --- a/kifi-app/lib/features/inventory/presentation/stock_ledger_tab.dart +++ b/kifi-app/lib/features/inventory/presentation/stock_ledger_tab.dart @@ -7,7 +7,7 @@ import 'package:kifi_app/features/inventory/providers/product_categories_provide import 'package:kifi_app/features/inventory/providers/commodity_rates_provider.dart'; import 'package:kifi_app/core/network/dio_client.dart'; import 'package:intl/intl.dart'; -import 'package:lucide_icons/lucide_icons.dart'; +import 'package:lucide_icons_flutter/lucide_icons.dart'; import 'package:kifi_app/features/vendor/presentation/purchase_order_details_screen.dart'; import 'package:kifi_app/features/vendor/providers/purchase_orders_provider.dart'; import 'package:kifi_app/core/utils/purity_utils.dart'; diff --git a/kifi-app/lib/features/inventory/providers/products_provider.dart b/kifi-app/lib/features/inventory/providers/products_provider.dart index 5bc80eb..23e0a7f 100644 --- a/kifi-app/lib/features/inventory/providers/products_provider.dart +++ b/kifi-app/lib/features/inventory/providers/products_provider.dart @@ -10,6 +10,7 @@ import '../domain/inventory_item.dart'; import 'package:dio/dio.dart'; import 'package:image_picker/image_picker.dart'; import 'package:flutter_image_compress/flutter_image_compress.dart'; +import 'package:flutter/foundation.dart'; class ProductsNotifier extends AsyncNotifier> { int _currentPage = 0; @@ -25,71 +26,74 @@ class ProductsNotifier extends AsyncNotifier> { return _fetchProducts(page: _currentPage, size: _pageSize); } - Future> _fetchProducts({ - required int page, - required int size, - }) async { - final response = await DioClient().dio.get( - '/inventory/products', - queryParameters: { + Future> _fetchProducts({int page = 0, int size = 50, String? search}) async { + try { + final queryParams = { 'page': page, 'size': size, - if (_currentSearchQuery.isNotEmpty) 'search': _currentSearchQuery, - }, - ); - if (response.statusCode == 200) { - final List data = response.data; - if (data.isNotEmpty) { - try { - final file = File( - '/Users/maddy/Projects/Kifi/kifi-app/debug_product.json', - ); - await file.writeAsString(jsonEncode(data.first)); - } catch (_) {} + }; + if (search != null && search.isNotEmpty) { + queryParams['search'] = search; } - final products = data.map((e) => Product.fromJson(e)).toList(); - if (products.length < size) { - _hasMore = false; + + final response = await DioClient().dio.get( + '/inventory/products', + queryParameters: queryParams, + ); + + if (response.data is List) { + final List list = response.data; + _hasMore = list.length >= size; + return list.map((json) => Product.fromJson(json as Map)).toList(); + } else if (response.data is Map && response.data['content'] != null) { + final List list = response.data['content']; + _hasMore = !(response.data['last'] ?? true); + return list.map((json) => Product.fromJson(json as Map)).toList(); } - return products; - } - return []; - } - - Future fetchNextPage() async { - if (!_hasMore || _isLoadingMore || state.isLoading) return; - - _isLoadingMore = true; - try { - final currentList = state.value ?? []; - final nextPage = _currentPage + 1; - final newProducts = await _fetchProducts(page: nextPage, size: _pageSize); - - _currentPage = nextPage; - state = AsyncValue.data([...currentList, ...newProducts]); - } catch (e, stack) { - // Don't override state with error, just keep the current list, but maybe show a toast. - } finally { - _isLoadingMore = false; + return []; + } catch (e) { + return []; } } Future refresh() async { - state = const AsyncValue.loading(); _currentPage = 0; _hasMore = true; - try { - state = AsyncValue.data( - await _fetchProducts(page: _currentPage, size: _pageSize), - ); - } catch (e, stack) { - state = AsyncValue.error(e, stack); - } + state = const AsyncValue.loading(); + state = await AsyncValue.guard(() => _fetchProducts(page: 0, size: _pageSize, search: _currentSearchQuery)); } Future search(String query) async { _currentSearchQuery = query; - await refresh(); + _currentPage = 0; + _hasMore = true; + state = const AsyncValue.loading(); + state = await AsyncValue.guard(() => _fetchProducts(page: 0, size: _pageSize, search: query)); + } + + Future fetchNextPage() async { + await loadMore(); + } + + Future loadMore() async { + if (!_hasMore || _isLoadingMore || state.isLoading) return; + + _isLoadingMore = true; + try { + final nextPage = _currentPage + 1; + final newItems = await _fetchProducts(page: nextPage, size: _pageSize, search: _currentSearchQuery); + + if (newItems.isNotEmpty) { + _currentPage = nextPage; + state = AsyncValue.data([...state.value ?? [], ...newItems]); + } else { + _hasMore = false; + } + } catch (e) { + // Handle silently or notify + } finally { + _isLoadingMore = false; + } } Future createProduct(Product product, {List? images}) async { @@ -103,15 +107,20 @@ class ProductsNotifier extends AsyncNotifier> { final productId = response.data['id']; for (var image in images) { final bytes = await image.readAsBytes(); - final compressedBytes = await FlutterImageCompress.compressWithList( - bytes, - minWidth: 800, - quality: 80, - ); + Uint8List uploadBytes = bytes; + if (!kIsWeb) { + try { + uploadBytes = await FlutterImageCompress.compressWithList( + bytes, + minWidth: 800, + quality: 80, + ); + } catch (_) {} + } final formData = FormData.fromMap({ 'file': MultipartFile.fromBytes( - compressedBytes, + uploadBytes, filename: image.name, ), }); @@ -143,15 +152,20 @@ class ProductsNotifier extends AsyncNotifier> { if (newImages != null && newImages.isNotEmpty) { for (var image in newImages) { final bytes = await image.readAsBytes(); - final compressedBytes = await FlutterImageCompress.compressWithList( - bytes, - minWidth: 800, - quality: 80, - ); + Uint8List uploadBytes = bytes; + if (!kIsWeb) { + try { + uploadBytes = await FlutterImageCompress.compressWithList( + bytes, + minWidth: 800, + quality: 80, + ); + } catch (_) {} + } final formData = FormData.fromMap({ 'file': MultipartFile.fromBytes( - compressedBytes, + uploadBytes, filename: image.name, ), }); diff --git a/kifi-app/lib/features/inventory/providers/uoms_provider.dart b/kifi-app/lib/features/inventory/providers/uoms_provider.dart new file mode 100644 index 0000000..c6ad4ba --- /dev/null +++ b/kifi-app/lib/features/inventory/providers/uoms_provider.dart @@ -0,0 +1,102 @@ +import 'dart:async'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import '../../../core/network/dio_client.dart'; + +class UnitOfMeasure { + final int? id; + final int? userId; + final String name; + final String? abbreviation; + + UnitOfMeasure({ + this.id, + this.userId, + required this.name, + this.abbreviation, + }); + + factory UnitOfMeasure.fromJson(Map json) { + return UnitOfMeasure( + id: json['id'], + userId: json['userId'] ?? json['user_id'], + name: json['name'] ?? '', + abbreviation: json['abbreviation'], + ); + } + + Map toJson() { + return { + 'id': id, + 'userId': userId, + 'name': name, + 'abbreviation': abbreviation, + }; + } + + String get displayName => abbreviation != null && abbreviation!.isNotEmpty ? '$name ($abbreviation)' : name; +} + +class UomsNotifier extends AsyncNotifier> { + @override + FutureOr> build() async { + return _fetchUoms(); + } + + Future> _fetchUoms() async { + try { + final response = await DioClient().dio.get('/inventory/uom'); + if (response.statusCode == 200) { + final List data = response.data; + final list = data.map((e) => UnitOfMeasure.fromJson(e)).toList(); + if (list.isNotEmpty) { + return list; + } + } + } catch (_) {} + + // If empty in database, seed standard units of measure + try { + final defaults = [ + {'name': 'Grams', 'abbreviation': 'g'}, + {'name': 'Kilograms', 'abbreviation': 'kg'}, + {'name': 'Pieces', 'abbreviation': 'pcs'}, + {'name': 'Carats', 'abbreviation': 'ct'}, + {'name': 'Milligrams', 'abbreviation': 'mg'}, + ]; + for (final def in defaults) { + await DioClient().dio.post('/inventory/uom', data: def); + } + final response = await DioClient().dio.get('/inventory/uom'); + if (response.statusCode == 200) { + final List data = response.data; + return data.map((e) => UnitOfMeasure.fromJson(e)).toList(); + } + } catch (_) {} + + return []; + } + + Future refresh() async { + state = const AsyncValue.loading(); + state = await AsyncValue.guard(() => _fetchUoms()); + } + + Future createUom(String name, String abbreviation) async { + try { + final response = await DioClient().dio.post( + '/inventory/uom', + data: {'name': name, 'abbreviation': abbreviation}, + ); + if (response.statusCode == 200 || response.statusCode == 201) { + final newUom = UnitOfMeasure.fromJson(response.data); + await refresh(); + return newUom; + } + } catch (_) {} + return null; + } +} + +final uomsProvider = AsyncNotifierProvider>(() { + return UomsNotifier(); +}); diff --git a/kifi-app/lib/features/onboarding/presentation/onboarding_screen.dart b/kifi-app/lib/features/onboarding/presentation/onboarding_screen.dart index bfbf6af..1d75168 100644 --- a/kifi-app/lib/features/onboarding/presentation/onboarding_screen.dart +++ b/kifi-app/lib/features/onboarding/presentation/onboarding_screen.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:shared_preferences/shared_preferences.dart'; -import 'package:lucide_icons/lucide_icons.dart'; +import 'package:lucide_icons_flutter/lucide_icons.dart'; import '../../dashboard/presentation/dashboard_screen.dart'; class OnboardingScreen extends StatefulWidget { diff --git a/kifi-app/lib/features/projects/presentation/project_board_screen.dart b/kifi-app/lib/features/projects/presentation/project_board_screen.dart index caf0f81..82751f4 100644 --- a/kifi-app/lib/features/projects/presentation/project_board_screen.dart +++ b/kifi-app/lib/features/projects/presentation/project_board_screen.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:lucide_icons/lucide_icons.dart'; +import 'package:lucide_icons_flutter/lucide_icons.dart'; import '../domain/project.dart'; import '../providers/project_provider.dart'; import 'widgets/add_task_sheet.dart'; diff --git a/kifi-app/lib/features/projects/presentation/project_hub_screen.dart b/kifi-app/lib/features/projects/presentation/project_hub_screen.dart index 0a496ae..788269f 100644 --- a/kifi-app/lib/features/projects/presentation/project_hub_screen.dart +++ b/kifi-app/lib/features/projects/presentation/project_hub_screen.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:lucide_icons/lucide_icons.dart'; +import 'package:lucide_icons_flutter/lucide_icons.dart'; import '../../../../core/theme/nature_colors.dart'; import 'projects_screen.dart'; import '../../sales/presentation/invoices_list_screen.dart'; diff --git a/kifi-app/lib/features/projects/presentation/projects_screen.dart b/kifi-app/lib/features/projects/presentation/projects_screen.dart index d7ca35d..091a64e 100644 --- a/kifi-app/lib/features/projects/presentation/projects_screen.dart +++ b/kifi-app/lib/features/projects/presentation/projects_screen.dart @@ -1,4 +1,4 @@ -import 'package:lucide_icons/lucide_icons.dart'; +import 'package:lucide_icons_flutter/lucide_icons.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../providers/project_provider.dart'; diff --git a/kifi-app/lib/features/projects/presentation/widgets/add_project_sheet.dart b/kifi-app/lib/features/projects/presentation/widgets/add_project_sheet.dart index fe922d9..887e85a 100644 --- a/kifi-app/lib/features/projects/presentation/widgets/add_project_sheet.dart +++ b/kifi-app/lib/features/projects/presentation/widgets/add_project_sheet.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import '../../domain/project.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:lucide_icons/lucide_icons.dart'; +import 'package:lucide_icons_flutter/lucide_icons.dart'; import '../../../sales/presentation/add_customer_sheet.dart'; import '../../providers/project_provider.dart'; import '../../../sales/providers/customers_provider.dart'; diff --git a/kifi-app/lib/features/projects/presentation/widgets/add_task_sheet.dart b/kifi-app/lib/features/projects/presentation/widgets/add_task_sheet.dart index fa1648c..cd1c7cb 100644 --- a/kifi-app/lib/features/projects/presentation/widgets/add_task_sheet.dart +++ b/kifi-app/lib/features/projects/presentation/widgets/add_task_sheet.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import '../../../../core/widgets/premium_text_field.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:lucide_icons/lucide_icons.dart'; +import 'package:lucide_icons_flutter/lucide_icons.dart'; import '../../domain/project_task.dart'; import '../../providers/project_provider.dart'; import '../../providers/user_provider.dart'; diff --git a/kifi-app/lib/features/projects/presentation/widgets/task_card.dart b/kifi-app/lib/features/projects/presentation/widgets/task_card.dart index ad2a934..bbd32e1 100644 --- a/kifi-app/lib/features/projects/presentation/widgets/task_card.dart +++ b/kifi-app/lib/features/projects/presentation/widgets/task_card.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:lucide_icons/lucide_icons.dart'; +import 'package:lucide_icons_flutter/lucide_icons.dart'; import '../../domain/project_task.dart'; import '../../providers/project_provider.dart'; import 'task_details_sheet.dart'; diff --git a/kifi-app/lib/features/projects/presentation/widgets/task_details_sheet.dart b/kifi-app/lib/features/projects/presentation/widgets/task_details_sheet.dart index c0994f3..adb1821 100644 --- a/kifi-app/lib/features/projects/presentation/widgets/task_details_sheet.dart +++ b/kifi-app/lib/features/projects/presentation/widgets/task_details_sheet.dart @@ -2,7 +2,7 @@ import 'dart:convert'; import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:lucide_icons/lucide_icons.dart'; +import 'package:lucide_icons_flutter/lucide_icons.dart'; import 'package:image_picker/image_picker.dart'; import 'package:file_picker/file_picker.dart'; import 'package:path_provider/path_provider.dart'; diff --git a/kifi-app/lib/features/sales/presentation/add_customer_sheet.dart b/kifi-app/lib/features/sales/presentation/add_customer_sheet.dart index 9c92ca0..9aa302b 100644 --- a/kifi-app/lib/features/sales/presentation/add_customer_sheet.dart +++ b/kifi-app/lib/features/sales/presentation/add_customer_sheet.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:lucide_icons/lucide_icons.dart'; +import 'package:lucide_icons_flutter/lucide_icons.dart'; import '../providers/customers_provider.dart'; import '../domain/customer.dart'; import 'dart:async'; @@ -222,7 +223,7 @@ class _AddCustomerSheetState extends ConsumerState { radius: 50, backgroundColor: Colors.grey[200], backgroundImage: _photo != null - ? FileImage(File(_photo!.path)) as ImageProvider + ? (kIsWeb ? NetworkImage(_photo!.path) : FileImage(File(_photo!.path))) as ImageProvider : (widget.customer?.photoUrl != null && _token != null ? NetworkImage( diff --git a/kifi-app/lib/features/sales/presentation/customers_list_screen.dart b/kifi-app/lib/features/sales/presentation/customers_list_screen.dart index 47a790e..20323cf 100644 --- a/kifi-app/lib/features/sales/presentation/customers_list_screen.dart +++ b/kifi-app/lib/features/sales/presentation/customers_list_screen.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:lucide_icons/lucide_icons.dart'; +import 'package:lucide_icons_flutter/lucide_icons.dart'; import '../providers/customers_provider.dart'; import 'dart:async'; import '../../../core/widgets/shimmer_loading.dart'; diff --git a/kifi-app/lib/features/sales/presentation/invoice_builder_screen.dart b/kifi-app/lib/features/sales/presentation/invoice_builder_screen.dart index 87d89f2..7a4c29f 100644 --- a/kifi-app/lib/features/sales/presentation/invoice_builder_screen.dart +++ b/kifi-app/lib/features/sales/presentation/invoice_builder_screen.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:lucide_icons/lucide_icons.dart'; +import 'package:lucide_icons_flutter/lucide_icons.dart'; import 'package:image_picker/image_picker.dart'; import 'dart:io'; import 'package:dio/dio.dart'; @@ -710,7 +711,7 @@ class _InvoiceBuilderScreenState extends ConsumerState { context, MaterialPageRoute( builder: (_) => AttachmentGalleryScreen( - images: [FileImage(File(_invoiceFile!.path))], + images: [kIsWeb ? NetworkImage(_invoiceFile!.path) : FileImage(File(_invoiceFile!.path))], initialIndex: 0, ), ), diff --git a/kifi-app/lib/features/sales/presentation/invoice_details_screen.dart b/kifi-app/lib/features/sales/presentation/invoice_details_screen.dart index 4e052fc..4e7c716 100644 --- a/kifi-app/lib/features/sales/presentation/invoice_details_screen.dart +++ b/kifi-app/lib/features/sales/presentation/invoice_details_screen.dart @@ -2,7 +2,7 @@ import 'dart:io'; import 'dart:typed_data'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:lucide_icons/lucide_icons.dart'; +import 'package:lucide_icons_flutter/lucide_icons.dart'; import 'package:intl/intl.dart'; import 'package:screenshot/screenshot.dart'; import 'package:path_provider/path_provider.dart'; diff --git a/kifi-app/lib/features/sales/presentation/invoices_list_screen.dart b/kifi-app/lib/features/sales/presentation/invoices_list_screen.dart index 324d6f4..7b48aa2 100644 --- a/kifi-app/lib/features/sales/presentation/invoices_list_screen.dart +++ b/kifi-app/lib/features/sales/presentation/invoices_list_screen.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:lucide_icons/lucide_icons.dart'; +import 'package:lucide_icons_flutter/lucide_icons.dart'; import 'package:intl/intl.dart'; import '../providers/invoices_provider.dart'; import '../domain/invoice.dart'; diff --git a/kifi-app/lib/features/sales/presentation/widgets/quick_add_customer_sheet.dart b/kifi-app/lib/features/sales/presentation/widgets/quick_add_customer_sheet.dart index 9b63a30..0f37559 100644 --- a/kifi-app/lib/features/sales/presentation/widgets/quick_add_customer_sheet.dart +++ b/kifi-app/lib/features/sales/presentation/widgets/quick_add_customer_sheet.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:lucide_icons/lucide_icons.dart'; +import 'package:lucide_icons_flutter/lucide_icons.dart'; import '../../../../core/widgets/premium_text_field.dart'; import '../../../../core/widgets/smart_search_dropdown.dart'; import '../../providers/customers_provider.dart'; diff --git a/kifi-app/lib/features/sales/presentation/widgets/receive_payment_sheet.dart b/kifi-app/lib/features/sales/presentation/widgets/receive_payment_sheet.dart index 64bd521..c06ac42 100644 --- a/kifi-app/lib/features/sales/presentation/widgets/receive_payment_sheet.dart +++ b/kifi-app/lib/features/sales/presentation/widgets/receive_payment_sheet.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:lucide_icons/lucide_icons.dart'; +import 'package:lucide_icons_flutter/lucide_icons.dart'; import '../../../../core/widgets/premium_text_field.dart'; import '../../../transactions/providers/providers.dart'; import '../../providers/invoices_provider.dart'; diff --git a/kifi-app/lib/features/sales/providers/customers_provider.dart b/kifi-app/lib/features/sales/providers/customers_provider.dart index 8b03ff6..1f79c13 100644 --- a/kifi-app/lib/features/sales/providers/customers_provider.dart +++ b/kifi-app/lib/features/sales/providers/customers_provider.dart @@ -1,5 +1,7 @@ import 'dart:async'; import 'dart:io'; +import 'dart:typed_data'; +import 'package:flutter/foundation.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:dio/dio.dart'; import 'package:image_picker/image_picker.dart'; @@ -90,15 +92,20 @@ class CustomersNotifier extends AsyncNotifier> { Future _uploadPhoto(int customerId, XFile photo) async { final bytes = await photo.readAsBytes(); - final compressedBytes = await FlutterImageCompress.compressWithList( - bytes, - minWidth: 413, - minHeight: 531, - quality: 85, - ); + Uint8List uploadBytes = bytes; + if (!kIsWeb) { + try { + uploadBytes = await FlutterImageCompress.compressWithList( + bytes, + minWidth: 413, + minHeight: 531, + quality: 85, + ); + } catch (_) {} + } final formData = FormData.fromMap({ - 'file': MultipartFile.fromBytes(compressedBytes, filename: photo.name), + 'file': MultipartFile.fromBytes(uploadBytes, filename: photo.name), }); await DioClient().dio.post('/customers/$customerId/photo', data: formData); diff --git a/kifi-app/lib/features/transactions/presentation/add_transaction_screen.dart b/kifi-app/lib/features/transactions/presentation/add_transaction_screen.dart index 6e998ba..0c1fd46 100644 --- a/kifi-app/lib/features/transactions/presentation/add_transaction_screen.dart +++ b/kifi-app/lib/features/transactions/presentation/add_transaction_screen.dart @@ -1,12 +1,16 @@ import 'dart:convert'; +import 'dart:typed_data'; +import 'package:flutter/foundation.dart' show kIsWeb; import 'package:flutter/material.dart'; import 'widgets/attachment_gallery_screen.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:lucide_icons/lucide_icons.dart'; +import 'package:lucide_icons_flutter/lucide_icons.dart'; import 'package:intl/intl.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart' as flutter_secure_storage; import 'package:image_picker/image_picker.dart'; import 'package:flutter_image_compress/flutter_image_compress.dart'; +import '../../../core/network/dio_client.dart'; +import '../../../core/widgets/responsive_layout.dart'; import '../providers/providers.dart'; import '../data/models.dart'; import '../../../core/utils/snackbar_service.dart'; @@ -133,11 +137,16 @@ class _AddTransactionScreenState extends ConsumerState { final XFile? image = await _picker.pickImage(source: source); if (image != null) { final bytes = await image.readAsBytes(); - final compressedBytes = await FlutterImageCompress.compressWithList( - bytes, - minWidth: 600, - quality: 80, - ); + Uint8List compressedBytes = bytes; + if (!kIsWeb) { + try { + compressedBytes = await FlutterImageCompress.compressWithList( + bytes, + minWidth: 600, + quality: 80, + ); + } catch (_) {} + } final base64String = base64Encode(compressedBytes); setState(() { base64Attachments.add({ @@ -751,8 +760,10 @@ class _AddTransactionScreenState extends ConsumerState { ) ], ), - body: SafeArea( - child: SingleChildScrollView( + body: MaxContentWidth( + maxWidth: 800, + child: SafeArea( + child: SingleChildScrollView( padding: const EdgeInsets.all(24.0), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, @@ -988,7 +999,7 @@ class _AddTransactionScreenState extends ConsumerState { borderRadius: BorderRadius.circular(8), image: DecorationImage( image: _jwtToken != null - ? NetworkImage('https://app.technobeesolutions.in/api/kifi-v2/transactions/${widget.transaction!.id}/attachments/${att.id}/content', headers: {'Authorization': 'Bearer $_jwtToken'}) + ? NetworkImage('${DioClient().dio.options.baseUrl}/transactions/${widget.transaction!.id}/attachments/${att.id}/content', headers: {'Authorization': 'Bearer $_jwtToken'}) : const AssetImage('assets/images/placeholder.png') as ImageProvider, fit: BoxFit.cover, ), @@ -1096,6 +1107,7 @@ class _AddTransactionScreenState extends ConsumerState { ), ), ), + ), ); } @@ -1105,7 +1117,7 @@ class _AddTransactionScreenState extends ConsumerState { // Add existing attachments for (final att in existingAttachments) { if (_jwtToken != null) { - allImages.add(NetworkImage('https://app.technobeesolutions.in/api/kifi-v2/transactions/${widget.transaction!.id}/attachments/${att.id}/content', headers: {'Authorization': 'Bearer $_jwtToken'})); + allImages.add(NetworkImage('${DioClient().dio.options.baseUrl}/transactions/${widget.transaction!.id}/attachments/${att.id}/content', headers: {'Authorization': 'Bearer $_jwtToken'})); } else { allImages.add(const AssetImage('assets/images/placeholder.png')); } diff --git a/kifi-app/lib/features/transactions/presentation/all_transactions_screen.dart b/kifi-app/lib/features/transactions/presentation/all_transactions_screen.dart index 73b011f..14ee450 100644 --- a/kifi-app/lib/features/transactions/presentation/all_transactions_screen.dart +++ b/kifi-app/lib/features/transactions/presentation/all_transactions_screen.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:lucide_icons/lucide_icons.dart'; +import 'package:lucide_icons_flutter/lucide_icons.dart'; import 'package:intl/intl.dart'; import '../providers/providers.dart'; import '../../dashboard/presentation/maturity_dialog.dart'; @@ -10,6 +10,7 @@ import '../../../core/widgets/empty_state.dart'; import 'add_transaction_screen.dart'; import '../presentation/widgets/transaction_filter_sheet.dart'; import '../../../core/theme/nature_colors.dart'; +import '../../../core/widgets/responsive_layout.dart'; class AllTransactionsScreen extends ConsumerStatefulWidget { const AllTransactionsScreen({super.key}); @@ -40,7 +41,6 @@ class _AllTransactionsScreenState extends ConsumerState { } void _onSearchChanged(String value) { - // Basic debounce for text search Future.delayed(const Duration(milliseconds: 500), () { if (_searchController.text == value) { ref.read(paginatedTransactionProvider.notifier).updateFilters(search: value); @@ -88,13 +88,15 @@ class _AllTransactionsScreenState extends ConsumerState { decoration: InputDecoration( hintText: 'Search transactions...', prefixIcon: const Icon(LucideIcons.search), - suffixIcon: _searchController.text.isNotEmpty ? IconButton( - icon: const Icon(LucideIcons.x), - onPressed: () { - _searchController.clear(); - _onSearchChanged(''); - }, - ) : null, + suffixIcon: _searchController.text.isNotEmpty + ? IconButton( + icon: const Icon(LucideIcons.x), + onPressed: () { + _searchController.clear(); + _onSearchChanged(''); + }, + ) + : null, filled: true, fillColor: Colors.grey.shade100, border: OutlineInputBorder( @@ -102,8 +104,8 @@ class _AllTransactionsScreenState extends ConsumerState { borderSide: BorderSide.none, ), focusedBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(16), - borderSide: const BorderSide(color: Color(0xFF6C63FF), width: 2) + borderRadius: BorderRadius.circular(16), + borderSide: const BorderSide(color: Color(0xFF6C63FF), width: 2), ), contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), ), @@ -111,197 +113,196 @@ class _AllTransactionsScreenState extends ConsumerState { ), ), ), - body: Builder( - builder: (context) { - if (transactions.isEmpty && paginatedState.isLoading) { - return ListView.builder( - padding: const EdgeInsets.all(16), - itemCount: 8, - itemBuilder: (context, index) => const ShimmerCard(), - ); - } + body: MaxContentWidth( + maxWidth: 1200, + child: Builder( + builder: (context) { + if (transactions.isEmpty && paginatedState.isLoading) { + return ListView.builder( + padding: const EdgeInsets.all(16), + itemCount: 8, + itemBuilder: (context, index) => const ShimmerCard(), + ); + } - if (transactions.isEmpty) { - return const EmptyStateWidget( - icon: LucideIcons.fileText, - title: 'No Transactions', - message: 'No transactions found matching your criteria.', - ); - } + if (transactions.isEmpty) { + return const EmptyStateWidget( + icon: LucideIcons.fileText, + title: 'No Transactions', + message: 'No transactions found matching your criteria.', + ); + } - return RefreshIndicator( - onRefresh: () async { - ref.read(paginatedTransactionProvider.notifier).clearFilters(); - _searchController.clear(); - }, - child: ListView.builder( - controller: _scrollController, - physics: const AlwaysScrollableScrollPhysics(), - padding: const EdgeInsets.all(16.0), - itemCount: transactions.length + (paginatedState.hasMore ? 1 : 0), - itemBuilder: (context, index) { - if (index == transactions.length) { - return const Padding( - padding: EdgeInsets.all(16.0), - child: Center(child: CircularProgressIndicator()), - ); - } + return RefreshIndicator( + onRefresh: () async { + ref.read(paginatedTransactionProvider.notifier).clearFilters(); + _searchController.clear(); + }, + child: ListView.builder( + controller: _scrollController, + physics: const AlwaysScrollableScrollPhysics(), + padding: const EdgeInsets.all(16.0), + itemCount: transactions.length + (paginatedState.hasMore ? 1 : 0), + itemBuilder: (context, index) { + if (index == transactions.length) { + return const Padding( + padding: EdgeInsets.all(16.0), + child: Center(child: CircularProgressIndicator()), + ); + } - final t = transactions[index]; - final isIncome = t.type == 'INCOME' || (t.type == 'TRANSFER' && t.fromWalletId == null); - final isInvestment = t.type == 'INVESTMENT'; - final isExpense = t.type == 'EXPENSE' || (t.type == 'TRANSFER' && t.toWalletId == null); - final isTransfer = t.type == 'TRANSFER' && t.fromWalletId != null && t.toWalletId != null; - - Color typeColor = Colors.grey; - IconData typeIcon = LucideIcons.arrowRightLeft; + final t = transactions[index]; + final isIncome = t.type == 'INCOME' || (t.type == 'TRANSFER' && t.fromWalletId == null); + final isInvestment = t.type == 'INVESTMENT'; + final isExpense = t.type == 'EXPENSE' || (t.type == 'TRANSFER' && t.toWalletId == null); + final isTransfer = t.type == 'TRANSFER' && t.fromWalletId != null && t.toWalletId != null; - if (isIncome) { - typeColor = NatureColors.getColor('INCOME'); - typeIcon = LucideIcons.arrowDownCircle; - } else if (isExpense) { - typeColor = NatureColors.getColor('EXPENSE'); - typeIcon = LucideIcons.arrowUpCircle; - } else if (isInvestment) { - typeColor = NatureColors.getColor('INVESTMENTS'); - typeIcon = LucideIcons.trendingUp; - } else if (isTransfer) { - typeColor = NatureColors.getColor('TRANSFER'); - if (walletsState.hasValue) { - final toW = walletsState.value!.where((w) => w.id == t.toWalletId).firstOrNull; - final fromW = walletsState.value!.where((w) => w.id == t.fromWalletId).firstOrNull; - - if (toW != null && (toW.nature == 'PAYABLES' || toW.nature == 'LOAN')) { - typeColor = NatureColors.getColor('PAYABLES'); - typeIcon = LucideIcons.alertCircle; - } else if (toW != null && (toW.nature == 'RECEIVABLES' || toW.nature == 'LENDING')) { - typeColor = NatureColors.getColor('RECEIVABLES'); - typeIcon = LucideIcons.arrowDownLeft; - } else if (fromW != null && (fromW.nature == 'PAYABLES' || fromW.nature == 'LOAN')) { - typeColor = NatureColors.getColor('PAYABLES'); - typeIcon = LucideIcons.alertCircle; - } else if (fromW != null && (fromW.nature == 'RECEIVABLES' || fromW.nature == 'LENDING')) { - typeColor = NatureColors.getColor('RECEIVABLES'); - typeIcon = LucideIcons.arrowDownLeft; + Color typeColor = Colors.grey; + IconData typeIcon = LucideIcons.arrowRightLeft; + + if (isIncome) { + typeColor = NatureColors.getColor('INCOME'); + typeIcon = LucideIcons.arrowDownCircle; + } else if (isExpense) { + typeColor = NatureColors.getColor('EXPENSE'); + typeIcon = LucideIcons.arrowUpCircle; + } else if (isInvestment) { + typeColor = NatureColors.getColor('INVESTMENTS'); + typeIcon = LucideIcons.trendingUp; + } else if (isTransfer) { + typeColor = NatureColors.getColor('TRANSFER'); + if (walletsState.hasValue) { + final toW = walletsState.value!.where((w) => w.id == t.toWalletId).firstOrNull; + final fromW = walletsState.value!.where((w) => w.id == t.fromWalletId).firstOrNull; + + if (toW != null && (toW.nature == 'PAYABLES' || toW.nature == 'LOAN')) { + typeColor = NatureColors.getColor('PAYABLES'); + typeIcon = LucideIcons.alertCircle; + } else if (toW != null && (toW.nature == 'RECEIVABLES' || toW.nature == 'LENDING')) { + typeColor = NatureColors.getColor('RECEIVABLES'); + typeIcon = LucideIcons.arrowDownLeft; + } else if (fromW != null && (fromW.nature == 'PAYABLES' || fromW.nature == 'LOAN')) { + typeColor = NatureColors.getColor('PAYABLES'); + typeIcon = LucideIcons.alertCircle; + } else if (fromW != null && (fromW.nature == 'RECEIVABLES' || fromW.nature == 'LENDING')) { + typeColor = NatureColors.getColor('RECEIVABLES'); + typeIcon = LucideIcons.arrowDownLeft; + } } } - } - String categoryName = 'Unknown'; - if (categoriesState.hasValue) { - final match = categoriesState.value!.where((c) => c.id == t.categoryId); - if (match.isNotEmpty) categoryName = match.first.name; - } - - String accountName = 'Unknown'; - if (walletsState.hasValue) { - if (isExpense && t.toWalletId != null) { - final match = walletsState.value!.where((w) => w.id == t.toWalletId); - if (match.isNotEmpty) accountName = match.first.name; - } else if (isIncome && t.toWalletId != null) { - final match = walletsState.value!.where((w) => w.id == t.toWalletId); - if (match.isNotEmpty) accountName = match.first.name; - } else if (isTransfer && t.toWalletId != null) { - final match = walletsState.value!.where((w) => w.id == t.toWalletId); - if (match.isNotEmpty) accountName = 'To ${match.first.name}'; + String categoryName = 'Unknown'; + if (categoriesState.hasValue) { + final match = categoriesState.value!.where((c) => c.id == t.categoryId); + if (match.isNotEmpty) categoryName = match.first.name; } - } - - String fromName = ''; - if (t.fromWalletId != null && walletsState.hasValue) { - final match = walletsState.value!.where((w) => w.id == t.fromWalletId); - if (match.isNotEmpty) fromName = match.first.name; - } - String toName = ''; - if (t.toWalletId != null && walletsState.hasValue) { - final match = walletsState.value!.where((w) => w.id == t.toWalletId); - if (match.isNotEmpty) toName = match.first.name; - } - - String subtitleText = ''; - if (categoryName == 'Unknown' && fromName.isNotEmpty && toName.isNotEmpty) { - subtitleText = '$fromName → $toName • ${DateFormat('MMM dd, yyyy').format(t.date)}'; - } else if (categoryName == 'Unknown' && toName.isNotEmpty) { - subtitleText = 'To $toName • ${DateFormat('MMM dd, yyyy').format(t.date)}'; - } else if (categoryName == 'Unknown' && fromName.isNotEmpty) { - subtitleText = 'From $fromName • ${DateFormat('MMM dd, yyyy').format(t.date)}'; - } else { - subtitleText = '$categoryName • ${DateFormat('MMM dd, yyyy').format(t.date)}${fromName.isNotEmpty ? ' • 💼 $fromName' : ''}'; - } + String accountName = 'Unknown'; + if (walletsState.hasValue) { + if (isExpense && t.toWalletId != null) { + final match = walletsState.value!.where((w) => w.id == t.toWalletId); + if (match.isNotEmpty) accountName = match.first.name; + } else if (isIncome && t.toWalletId != null) { + final match = walletsState.value!.where((w) => w.id == t.toWalletId); + if (match.isNotEmpty) accountName = match.first.name; + } else if (isTransfer && t.toWalletId != null) { + final match = walletsState.value!.where((w) => w.id == t.toWalletId); + if (match.isNotEmpty) accountName = 'To ${match.first.name}'; + } + } - return Dismissible( - key: Key(t.id.toString()), - direction: DismissDirection.endToStart, - background: Container( - color: Colors.red, - alignment: Alignment.centerRight, - padding: const EdgeInsets.only(right: 20), - child: const Icon(LucideIcons.trash2, color: Colors.white), - ), - confirmDismiss: (dir) async { - return await showDialog( - context: context, - builder: (ctx) => AlertDialog( - title: const Text('Delete Transaction?'), - content: const Text('Are you sure you want to delete this transaction?'), - actions: [ - TextButton(onPressed: () => Navigator.pop(ctx, false), child: const Text('Cancel')), - TextButton( - onPressed: () => Navigator.pop(ctx, true), - child: const Text('Delete', style: TextStyle(color: Colors.red)) - ), - ], - ), - ); - }, - onDismissed: (dir) { - ref.read(transactionProvider.notifier).deleteTransaction(t.id); - ref.read(paginatedTransactionProvider.notifier).removeTransactionFromState(t.id); - }, - child: Card( - margin: const EdgeInsets.only(bottom: 12), - child: Column( - children: [ - ListTile( - onTap: () { - Navigator.push(context, MaterialPageRoute(builder: (_) => AddTransactionScreen(transaction: t))); - }, - leading: CircleAvatar( - backgroundColor: typeColor.withValues(alpha: 0.1), - child: Icon(typeIcon, color: typeColor), - ), - title: Text((t.description != null && t.description!.isNotEmpty) ? t.description! : accountName, style: const TextStyle(fontWeight: FontWeight.bold)), - subtitle: Text(subtitleText), - trailing: Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - Text( - '${isIncome ? '+' : (isTransfer ? '' : '-')}Rs. ${t.amount.toStringAsFixed(0)}', - style: TextStyle(fontWeight: FontWeight.bold, color: typeColor), - ), - if (t.investmentStatus == 'OPEN' && (typeColor == NatureColors.getColor('INVESTMENTS') || typeColor == NatureColors.getColor('RECEIVABLES'))) - GestureDetector( - onTap: () { - showDialog(context: context, builder: (_) => MaturityDialog(transaction: t)); - }, - child: const Padding( - padding: EdgeInsets.only(top: 4.0), - child: Text('Close', style: TextStyle(color: Colors.blue, fontWeight: FontWeight.bold, fontSize: 12)), - ), - ), - ], - ), - ), - ], + String fromName = ''; + if (t.fromWalletId != null && walletsState.hasValue) { + final match = walletsState.value!.where((w) => w.id == t.fromWalletId); + if (match.isNotEmpty) fromName = match.first.name; + } + + String toName = ''; + if (t.toWalletId != null && walletsState.hasValue) { + final match = walletsState.value!.where((w) => w.id == t.toWalletId); + if (match.isNotEmpty) toName = match.first.name; + } + + String subtitleText = ''; + if (categoryName == 'Unknown' && fromName.isNotEmpty && toName.isNotEmpty) { + subtitleText = '$fromName → $toName • ${DateFormat('MMM dd, yyyy').format(t.date)}'; + } else if (categoryName == 'Unknown' && toName.isNotEmpty) { + subtitleText = 'To $toName • ${DateFormat('MMM dd, yyyy').format(t.date)}'; + } else if (categoryName == 'Unknown' && fromName.isNotEmpty) { + subtitleText = 'From $fromName • ${DateFormat('MMM dd, yyyy').format(t.date)}'; + } else { + subtitleText = '$categoryName • ${DateFormat('MMM dd, yyyy').format(t.date)}${fromName.isNotEmpty ? ' • 💼 $fromName' : ''}'; + } + + return Dismissible( + key: Key(t.id.toString()), + direction: DismissDirection.endToStart, + background: Container( + color: Colors.red, + alignment: Alignment.centerRight, + padding: const EdgeInsets.only(right: 20), + child: const Icon(LucideIcons.trash2, color: Colors.white), ), - ), - ); - }, - ), - ); - } + confirmDismiss: (dir) async { + return await showDialog( + context: context, + builder: (ctx) => AlertDialog( + title: const Text('Delete Transaction?'), + content: const Text('Are you sure you want to delete this transaction?'), + actions: [ + TextButton(onPressed: () => Navigator.pop(ctx, false), child: const Text('Cancel')), + TextButton( + onPressed: () => Navigator.pop(ctx, true), + child: const Text('Delete', style: TextStyle(color: Colors.red)), + ), + ], + ), + ); + }, + onDismissed: (dir) { + ref.read(transactionProvider.notifier).deleteTransaction(t.id); + ref.read(paginatedTransactionProvider.notifier).removeTransactionFromState(t.id); + }, + child: Card( + margin: const EdgeInsets.only(bottom: 12), + child: ListTile( + onTap: () { + Navigator.push(context, MaterialPageRoute(builder: (_) => AddTransactionScreen(transaction: t))); + }, + leading: CircleAvatar( + backgroundColor: typeColor.withValues(alpha: 0.1), + child: Icon(typeIcon, color: typeColor), + ), + title: Text((t.description != null && t.description!.isNotEmpty) ? t.description! : accountName, style: const TextStyle(fontWeight: FontWeight.bold)), + subtitle: Text(subtitleText), + trailing: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Text( + '${isIncome ? '+' : (isTransfer ? '' : '-')}Rs. ${t.amount.toStringAsFixed(0)}', + style: TextStyle(fontWeight: FontWeight.bold, color: typeColor), + ), + if (t.investmentStatus == 'OPEN' && (typeColor == NatureColors.getColor('INVESTMENTS') || typeColor == NatureColors.getColor('RECEIVABLES'))) + GestureDetector( + onTap: () { + showDialog(context: context, builder: (_) => MaturityDialog(transaction: t)); + }, + child: const Padding( + padding: EdgeInsets.only(top: 4.0), + child: Text('Close', style: TextStyle(color: Colors.blue, fontWeight: FontWeight.bold, fontSize: 12)), + ), + ), + ], + ), + ), + ), + ); + }, + ), + ); + }, + ), ), ); } diff --git a/kifi-app/lib/features/transactions/presentation/widgets/transaction_filter_sheet.dart b/kifi-app/lib/features/transactions/presentation/widgets/transaction_filter_sheet.dart index 86f0a17..cb0486a 100644 --- a/kifi-app/lib/features/transactions/presentation/widgets/transaction_filter_sheet.dart +++ b/kifi-app/lib/features/transactions/presentation/widgets/transaction_filter_sheet.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:lucide_icons/lucide_icons.dart'; +import 'package:lucide_icons_flutter/lucide_icons.dart'; import '../../providers/providers.dart'; import '../../providers/paginated_transaction_provider.dart'; diff --git a/kifi-app/lib/features/vendor/presentation/add_vendor_sheet.dart b/kifi-app/lib/features/vendor/presentation/add_vendor_sheet.dart index eb9e981..cd5fd4f 100644 --- a/kifi-app/lib/features/vendor/presentation/add_vendor_sheet.dart +++ b/kifi-app/lib/features/vendor/presentation/add_vendor_sheet.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:lucide_icons/lucide_icons.dart'; +import 'package:lucide_icons_flutter/lucide_icons.dart'; import '../providers/vendors_provider.dart'; import '../domain/vendor.dart'; import 'dart:async'; @@ -209,7 +210,7 @@ class _AddVendorSheetState extends ConsumerState { radius: 50, backgroundColor: Colors.blue.withOpacity(0.1), backgroundImage: _photo != null - ? FileImage(File(_photo!.path)) as ImageProvider + ? (kIsWeb ? NetworkImage(_photo!.path) : FileImage(File(_photo!.path))) as ImageProvider : (widget.vendor?.photoUrl != null ? NetworkImage(widget.vendor!.photoUrl!) : null), diff --git a/kifi-app/lib/features/vendor/presentation/pay_vendor_sheet.dart b/kifi-app/lib/features/vendor/presentation/pay_vendor_sheet.dart index 0bee0b8..3971b2c 100644 --- a/kifi-app/lib/features/vendor/presentation/pay_vendor_sheet.dart +++ b/kifi-app/lib/features/vendor/presentation/pay_vendor_sheet.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:lucide_icons/lucide_icons.dart'; +import 'package:lucide_icons_flutter/lucide_icons.dart'; import 'package:intl/intl.dart'; import '../../../core/widgets/premium_text_field.dart'; import '../../../core/widgets/smart_search_dropdown.dart'; diff --git a/kifi-app/lib/features/vendor/presentation/purchase_order_builder_screen.dart b/kifi-app/lib/features/vendor/presentation/purchase_order_builder_screen.dart index 43e2c3b..78882f3 100644 --- a/kifi-app/lib/features/vendor/presentation/purchase_order_builder_screen.dart +++ b/kifi-app/lib/features/vendor/presentation/purchase_order_builder_screen.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:lucide_icons/lucide_icons.dart'; +import 'package:lucide_icons_flutter/lucide_icons.dart'; import 'package:image_picker/image_picker.dart'; import 'dart:io'; import 'package:dio/dio.dart'; @@ -897,7 +898,7 @@ class _PurchaseOrderBuilderScreenState context, MaterialPageRoute( builder: (_) => AttachmentGalleryScreen( - images: [FileImage(File(_invoiceFile!.path))], + images: [kIsWeb ? NetworkImage(_invoiceFile!.path) : FileImage(File(_invoiceFile!.path))], initialIndex: 0, ), ), @@ -1362,7 +1363,7 @@ class _POItemRowState extends State<_POItemRow> { context, MaterialPageRoute( builder: (_) => AttachmentGalleryScreen( - images: [FileImage(File(widget.item.localPhotoPath!))], + images: [kIsWeb ? NetworkImage(widget.item.localPhotoPath!) : FileImage(File(widget.item.localPhotoPath!))], initialIndex: 0, ), ), @@ -1396,7 +1397,9 @@ class _POItemRowState extends State<_POItemRow> { child: widget.item.localPhotoPath != null ? ClipRRect( borderRadius: BorderRadius.circular(7), - child: Image.file(File(widget.item.localPhotoPath!), fit: BoxFit.cover), + child: kIsWeb + ? Image.network(widget.item.localPhotoPath!, fit: BoxFit.cover) + : Image.file(File(widget.item.localPhotoPath!), fit: BoxFit.cover), ) : widget.item.photoUrl != null && widget.item.photoUrl!.isNotEmpty ? ClipRRect( diff --git a/kifi-app/lib/features/vendor/presentation/purchase_order_details_screen.dart b/kifi-app/lib/features/vendor/presentation/purchase_order_details_screen.dart index b3f8abe..43a665e 100644 --- a/kifi-app/lib/features/vendor/presentation/purchase_order_details_screen.dart +++ b/kifi-app/lib/features/vendor/presentation/purchase_order_details_screen.dart @@ -1,7 +1,7 @@ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:lucide_icons/lucide_icons.dart'; +import 'package:lucide_icons_flutter/lucide_icons.dart'; import 'package:intl/intl.dart'; import 'package:path_provider/path_provider.dart'; import 'package:share_plus/share_plus.dart'; diff --git a/kifi-app/lib/features/vendor/presentation/purchase_orders_list_screen.dart b/kifi-app/lib/features/vendor/presentation/purchase_orders_list_screen.dart index 6aaaaf2..b8a3b42 100644 --- a/kifi-app/lib/features/vendor/presentation/purchase_orders_list_screen.dart +++ b/kifi-app/lib/features/vendor/presentation/purchase_orders_list_screen.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:lucide_icons/lucide_icons.dart'; +import 'package:lucide_icons_flutter/lucide_icons.dart'; import 'package:intl/intl.dart'; import '../providers/purchase_orders_provider.dart'; import '../providers/vendors_provider.dart'; diff --git a/kifi-app/lib/features/vendor/presentation/vendors_list_screen.dart b/kifi-app/lib/features/vendor/presentation/vendors_list_screen.dart index 38825af..3003222 100644 --- a/kifi-app/lib/features/vendor/presentation/vendors_list_screen.dart +++ b/kifi-app/lib/features/vendor/presentation/vendors_list_screen.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:lucide_icons/lucide_icons.dart'; +import 'package:lucide_icons_flutter/lucide_icons.dart'; import '../providers/vendors_provider.dart'; import 'add_vendor_sheet.dart'; diff --git a/kifi-app/lib/features/vendor/presentation/widgets/quick_add_vendor_sheet.dart b/kifi-app/lib/features/vendor/presentation/widgets/quick_add_vendor_sheet.dart index c1edb8e..3d3799c 100644 --- a/kifi-app/lib/features/vendor/presentation/widgets/quick_add_vendor_sheet.dart +++ b/kifi-app/lib/features/vendor/presentation/widgets/quick_add_vendor_sheet.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:lucide_icons/lucide_icons.dart'; +import 'package:lucide_icons_flutter/lucide_icons.dart'; import '../../../../core/widgets/premium_text_field.dart'; import '../../../../core/widgets/smart_search_dropdown.dart'; import '../../providers/vendors_provider.dart'; diff --git a/kifi-app/lib/features/vendor/providers/purchase_orders_provider.dart b/kifi-app/lib/features/vendor/providers/purchase_orders_provider.dart index 63bf82f..f6c5fa7 100644 --- a/kifi-app/lib/features/vendor/providers/purchase_orders_provider.dart +++ b/kifi-app/lib/features/vendor/providers/purchase_orders_provider.dart @@ -2,6 +2,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../../core/network/dio_client.dart'; import '../domain/purchase_order.dart'; import '../domain/purchase_payment.dart'; +import '../../inventory/providers/products_provider.dart'; class PurchaseOrdersNotifier extends AsyncNotifier> { @override @@ -84,6 +85,7 @@ class PurchaseOrdersNotifier extends AsyncNotifier> { state = AsyncValue.data( current.map((c) => c.id == updatedPo.id ? updatedPo : c).toList(), ); + ref.invalidate(productsProvider); return updatedPo; } throw Exception('Failed to receive purchase invoice'); diff --git a/kifi-app/lib/main.dart b/kifi-app/lib/main.dart index b405ce4..ae68544 100644 --- a/kifi-app/lib/main.dart +++ b/kifi-app/lib/main.dart @@ -1,3 +1,4 @@ +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; @@ -132,7 +133,7 @@ class _KifiAppState extends ConsumerState { ? Scaffold(body: Center(child: Padding(padding: const EdgeInsets.all(20), child: Text("Startup Error: $_startupError", style: const TextStyle(color: Colors.red))))) : _isLoading ? const Scaffold(body: Center(child: CircularProgressIndicator())) - : (!_hasSeenOnboarding + : ((!kIsWeb && !_hasSeenOnboarding) ? const OnboardingScreen() : (_isAuthenticated ? (_isSetupCompleted ? const DashboardScreen() : const SetupWizardScreen()) diff --git a/kifi-app/nginx.conf b/kifi-app/nginx.conf new file mode 100644 index 0000000..9c5783f --- /dev/null +++ b/kifi-app/nginx.conf @@ -0,0 +1,55 @@ +server { + listen 80; + server_name localhost; + + root /usr/share/nginx/html; + index index.html; + + # Support /kifi/ subpath if forwarded with prefix by reverse proxy + rewrite ^/kifi/(.*)$ /$1 break; + rewrite ^/kifi$ / break; + + # Gzip compression for Flutter Web assets (JS, WASM, CSS, JSON, Fonts) + gzip on; + gzip_vary on; + gzip_min_length 1024; + gzip_proxied any; + gzip_types + text/plain + text/css + text/javascript + application/javascript + application/json + application/x-javascript + application/wasm + image/svg+xml + font/woff + font/woff2; + + # Service Worker & manifest: no cache to ensure immediate updates + location ~* (flutter_service_worker\.js|manifest\.json|version\.json)$ { + add_header Cache-Control "no-cache, no-store, must-revalidate"; + add_header Pragma "no-cache"; + add_header Expires "0"; + try_files $uri =404; + } + + # Static assets (images, fonts, canvaskit, icons, js chunks): long cache + location ~* \.(?:ico|png|jpg|jpeg|gif|svg|woff|woff2|ttf|eot|wasm)$ { + expires 1y; + add_header Cache-Control "public, immutable"; + try_files $uri =404; + } + + # Main SPA routing: fall back to index.html for all subroutes + location / { + add_header Cache-Control "no-cache, no-store, must-revalidate"; + try_files $uri $uri/ /index.html; + } + + # Error pages + error_page 500 502 503 504 /50x.html; + location = /50x.html { + root /usr/share/nginx/html; + } +} diff --git a/kifi-app/pubspec.lock b/kifi-app/pubspec.lock index 430f734..f8eb4bb 100644 --- a/kifi-app/pubspec.lock +++ b/kifi-app/pubspec.lock @@ -808,14 +808,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.0" - lucide_icons: + lucide_icons_flutter: dependency: "direct main" description: - name: lucide_icons - sha256: ad24d0fd65707e48add30bebada7d90bff2a1bba0a72d6e9b19d44246b0e83c4 + name: lucide_icons_flutter + sha256: "6e2eb4a11137851be84a57e936fc864d565709ec0ae854b786b15397d1b66276" url: "https://pub.dev" source: hosted - version: "0.257.0" + version: "3.1.17" matcher: dependency: transitive description: diff --git a/kifi-app/pubspec.yaml b/kifi-app/pubspec.yaml index e2bffa9..fc17704 100644 --- a/kifi-app/pubspec.yaml +++ b/kifi-app/pubspec.yaml @@ -40,7 +40,6 @@ dependencies: fl_chart: ^1.2.0 google_fonts: ^8.2.1 intl: ^0.20.3 - lucide_icons: ^0.257.0 local_auth: ^3.0.2 path_provider: ^2.1.6 share_plus: ^13.3.0 @@ -60,6 +59,7 @@ dependencies: file_picker: ^12.0.0 intl_phone_field: ^3.2.0 url_launcher: ^6.3.2 + lucide_icons_flutter: ^3.1.17 dev_dependencies: flutter_test: diff --git a/kifi-app/web/index.html b/kifi-app/web/index.html index 3ba871a..5fe0b9c 100644 --- a/kifi-app/web/index.html +++ b/kifi-app/web/index.html @@ -1,38 +1,95 @@ - + - - - + + - - + + - kifi_app + KIFI - Financial Ledger & Jewellery ERP + +
+ +
KIFI
+
Financial Ledger & Jewellery ERP
+
+