Web approach - basic look and feel done
This commit is contained in:
@@ -14,6 +14,12 @@ import org.springframework.security.crypto.password.PasswordEncoder;
|
|||||||
import org.springframework.security.web.server.SecurityWebFilterChain;
|
import org.springframework.security.web.server.SecurityWebFilterChain;
|
||||||
import reactor.core.publisher.Mono;
|
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
|
@Configuration
|
||||||
@EnableWebFluxSecurity
|
@EnableWebFluxSecurity
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
@@ -27,9 +33,25 @@ public class SecurityConfig {
|
|||||||
return new BCryptPasswordEncoder();
|
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
|
@Bean
|
||||||
public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
|
public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
|
||||||
return http
|
return http
|
||||||
|
.cors(corsSpec -> corsSpec.configurationSource(corsConfigurationSource()))
|
||||||
.exceptionHandling(exceptionHandlingSpec -> exceptionHandlingSpec
|
.exceptionHandling(exceptionHandlingSpec -> exceptionHandlingSpec
|
||||||
.authenticationEntryPoint((swe, e) ->
|
.authenticationEntryPoint((swe, e) ->
|
||||||
Mono.fromRunnable(() -> swe.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED))
|
Mono.fromRunnable(() -> swe.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED))
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package com.kifi.api.controller.vendor;
|
package com.kifi.api.controller.vendor;
|
||||||
|
|
||||||
import com.kifi.api.entity.vendor.PurchaseOrder;
|
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.entity.vendor.PurchasePayment;
|
||||||
import com.kifi.api.service.vendor.PurchaseOrderService;
|
import com.kifi.api.service.vendor.PurchaseOrderService;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
@@ -67,11 +68,15 @@ public class PurchaseOrderController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@PutMapping("/{id}/receive")
|
@PutMapping("/{id}/receive")
|
||||||
public Mono<ResponseEntity<PurchaseOrder>> markAsReceived(@PathVariable Long id, Authentication authentication, @RequestBody PurchaseOrder po) {
|
public Mono<ResponseEntity<PurchaseOrder>> markAsReceived(@PathVariable Long id, Authentication authentication, @RequestBody(required = false) PurchaseOrder po) {
|
||||||
Long userId = Long.valueOf(authentication.getDetails().toString());
|
Long userId = Long.valueOf(authentication.getDetails().toString());
|
||||||
return purchaseOrderService.markAsReceived(userId, id, po.getItems())
|
List<PurchaseOrderItem> items = (po != null) ? po.getItems() : null;
|
||||||
|
return purchaseOrderService.markAsReceived(userId, id, items)
|
||||||
.map(ResponseEntity::ok)
|
.map(ResponseEntity::ok)
|
||||||
.onErrorResume(e -> Mono.just(ResponseEntity.badRequest().build()));
|
.onErrorResume(e -> {
|
||||||
|
e.printStackTrace();
|
||||||
|
return Mono.just(ResponseEntity.internalServerError().build());
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/{id}/payments")
|
@PostMapping("/{id}/payments")
|
||||||
|
|||||||
@@ -12,7 +12,10 @@ import com.kifi.api.entity.inventory.InventoryItem;
|
|||||||
import com.kifi.api.service.accounting.LedgerService;
|
import com.kifi.api.service.accounting.LedgerService;
|
||||||
import com.kifi.api.service.TransactionService;
|
import com.kifi.api.service.TransactionService;
|
||||||
import com.kifi.api.repository.vendor.VendorRepository;
|
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 lombok.RequiredArgsConstructor;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import reactor.core.publisher.Flux;
|
import reactor.core.publisher.Flux;
|
||||||
@@ -35,7 +38,8 @@ public class PurchaseOrderService {
|
|||||||
private final LedgerService ledgerService;
|
private final LedgerService ledgerService;
|
||||||
private final TransactionService transactionService;
|
private final TransactionService transactionService;
|
||||||
private final VendorRepository vendorRepository;
|
private final VendorRepository vendorRepository;
|
||||||
private final com.kifi.api.repository.inventory.InventoryBalanceRepository inventoryBalanceRepository;
|
private final InventoryBalanceRepository inventoryBalanceRepository;
|
||||||
|
private final InventoryLocationRepository inventoryLocationRepository;
|
||||||
|
|
||||||
public Flux<PurchaseOrder> getPurchaseOrders(Long userId) {
|
public Flux<PurchaseOrder> getPurchaseOrders(Long userId) {
|
||||||
return purchaseOrderRepository.findByUserId(userId)
|
return purchaseOrderRepository.findByUserId(userId)
|
||||||
@@ -134,6 +138,7 @@ public class PurchaseOrderService {
|
|||||||
@Transactional
|
@Transactional
|
||||||
public Mono<PurchaseOrder> markAsReceived(Long userId, Long id, List<PurchaseOrderItem> receivedItems) {
|
public Mono<PurchaseOrder> markAsReceived(Long userId, Long id, List<PurchaseOrderItem> receivedItems) {
|
||||||
return purchaseOrderRepository.findByUserIdAndId(userId, id)
|
return purchaseOrderRepository.findByUserIdAndId(userId, id)
|
||||||
|
.switchIfEmpty(Mono.error(new RuntimeException("PO not found")))
|
||||||
.flatMap(po -> {
|
.flatMap(po -> {
|
||||||
if ("RECEIVED".equals(po.getStatus())) {
|
if ("RECEIVED".equals(po.getStatus())) {
|
||||||
return Mono.error(new RuntimeException("PO is already marked as received"));
|
return Mono.error(new RuntimeException("PO is already marked as received"));
|
||||||
@@ -142,19 +147,41 @@ public class PurchaseOrderService {
|
|||||||
po.setStatus("RECEIVED");
|
po.setStatus("RECEIVED");
|
||||||
po.setUpdatedAt(LocalDateTime.now());
|
po.setUpdatedAt(LocalDateTime.now());
|
||||||
|
|
||||||
|
List<PurchaseOrderItem> itemsToSave = (receivedItems != null && !receivedItems.isEmpty()) ? receivedItems : po.getItems();
|
||||||
|
|
||||||
return purchaseOrderRepository.save(po)
|
return purchaseOrderRepository.save(po)
|
||||||
.flatMap(savedPo -> purchaseOrderItemRepository.deleteByPoId(id)
|
.flatMap(savedPo -> {
|
||||||
.thenMany(Flux.fromIterable(receivedItems)
|
Mono<List<PurchaseOrderItem>> itemsProcess;
|
||||||
|
if (itemsToSave != null && !itemsToSave.isEmpty()) {
|
||||||
|
itemsProcess = purchaseOrderItemRepository.deleteByPoId(id)
|
||||||
|
.thenMany(Flux.fromIterable(itemsToSave)
|
||||||
.map(item -> {
|
.map(item -> {
|
||||||
item.setId(null);
|
item.setId(null);
|
||||||
item.setPoId(id);
|
item.setPoId(id);
|
||||||
return item;
|
return item;
|
||||||
})
|
})
|
||||||
.flatMap(purchaseOrderItemRepository::save))
|
.flatMap(purchaseOrderItemRepository::save))
|
||||||
.collectList()
|
.collectList();
|
||||||
.flatMap(savedItems -> {
|
} else {
|
||||||
// Update inventory for received items
|
itemsProcess = purchaseOrderItemRepository.findByPoId(id).collectList();
|
||||||
return Flux.fromIterable(savedItems)
|
}
|
||||||
|
|
||||||
|
return itemsProcess.flatMap(savedItems -> {
|
||||||
|
// 1. Resolve or create user's default inventory location
|
||||||
|
Mono<Long> 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<Void> stockUpdates = locationIdMono.flatMap(locId ->
|
||||||
|
Flux.fromIterable(savedItems)
|
||||||
.flatMap(item -> {
|
.flatMap(item -> {
|
||||||
if (item.getProductId() != null) {
|
if (item.getProductId() != null) {
|
||||||
InventoryItem invItem = InventoryItem.builder()
|
InventoryItem invItem = InventoryItem.builder()
|
||||||
@@ -169,48 +196,72 @@ public class PurchaseOrderService {
|
|||||||
.grossWeight(item.getWeight())
|
.grossWeight(item.getWeight())
|
||||||
.netWeight(item.getWeight())
|
.netWeight(item.getWeight())
|
||||||
.build();
|
.build();
|
||||||
return inventoryItemService.createItem(userId, invItem)
|
|
||||||
.then(inventoryBalanceRepository.findByProductIdAndLocationId(item.getProductId(), 2L) // Default location 2L
|
BigDecimal itemQty = (item.getWeight() != null && item.getWeight().compareTo(BigDecimal.ZERO) > 0)
|
||||||
|
? item.getWeight()
|
||||||
|
: (item.getQuantity() != null ? item.getQuantity() : BigDecimal.ONE);
|
||||||
|
|
||||||
|
Mono<InventoryBalance> balanceUpdate = inventoryBalanceRepository.findByProductIdAndLocationId(item.getProductId(), locId)
|
||||||
.flatMap(balance -> {
|
.flatMap(balance -> {
|
||||||
balance.setQuantity(balance.getQuantity().add(item.getWeight() != null ? item.getWeight() : BigDecimal.ONE));
|
balance.setQuantity(balance.getQuantity().add(itemQty));
|
||||||
balance.setLastUpdated(LocalDateTime.now());
|
balance.setLastUpdated(LocalDateTime.now());
|
||||||
return inventoryBalanceRepository.save(balance);
|
return inventoryBalanceRepository.save(balance);
|
||||||
})
|
})
|
||||||
.switchIfEmpty(Mono.defer(() -> {
|
.switchIfEmpty(Mono.defer(() -> {
|
||||||
com.kifi.api.entity.inventory.InventoryBalance newBalance = new com.kifi.api.entity.inventory.InventoryBalance();
|
InventoryBalance newBalance = new InventoryBalance();
|
||||||
newBalance.setProductId(item.getProductId());
|
newBalance.setProductId(item.getProductId());
|
||||||
newBalance.setLocationId(2L);
|
newBalance.setLocationId(locId);
|
||||||
newBalance.setQuantity(item.getWeight() != null ? item.getWeight() : BigDecimal.ONE);
|
newBalance.setQuantity(itemQty);
|
||||||
newBalance.setLastUpdated(LocalDateTime.now());
|
newBalance.setLastUpdated(LocalDateTime.now());
|
||||||
return inventoryBalanceRepository.save(newBalance);
|
return inventoryBalanceRepository.save(newBalance);
|
||||||
})))
|
}));
|
||||||
.then(Mono.just(item));
|
|
||||||
|
return inventoryItemService.createItem(userId, invItem)
|
||||||
|
.then(balanceUpdate)
|
||||||
|
.then();
|
||||||
}
|
}
|
||||||
return Mono.just(item);
|
return Mono.empty();
|
||||||
})
|
})
|
||||||
.then(Mono.defer(() -> vendorRepository.findById(savedPo.getVendorId())))
|
.then()
|
||||||
|
);
|
||||||
|
|
||||||
|
// 3. Double-entry accounting ledger transaction
|
||||||
|
Mono<Void> 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(
|
.flatMap(vendor -> Mono.zip(
|
||||||
ledgerService.getVendorLedger(userId, vendor.getId(), vendor.getName()),
|
ledgerService.getVendorLedger(userId, vendor.getId(), vendor.getName()),
|
||||||
ledgerService.getInventoryAssetLedger(userId)
|
ledgerService.getInventoryAssetLedger(userId)
|
||||||
))
|
))
|
||||||
.flatMap(ledgers -> {
|
.flatMap(ledgers -> {
|
||||||
Transaction tx = Transaction.builder()
|
com.kifi.api.entity.Transaction tx = com.kifi.api.entity.Transaction.builder()
|
||||||
.userId(userId)
|
.userId(userId)
|
||||||
.fromWalletId(ledgers.getT1().getId()) // Vendor Payable (Liability increases via fromWallet subtraction)
|
.fromWalletId(ledgers.getT1().getId()) // Vendor Payable (Liability increases)
|
||||||
.toWalletId(ledgers.getT2().getId()) // Inventory Asset (Asset increases via toWallet addition)
|
.toWalletId(ledgers.getT2().getId()) // Inventory Asset (Asset increases)
|
||||||
.type("PURCHASE_RECEIPT")
|
.type("PURCHASE_RECEIPT")
|
||||||
.amount(savedPo.getTotalAmount())
|
.amount(savedPo.getTotalAmount())
|
||||||
.date(java.time.LocalDate.now())
|
.date(java.time.LocalDate.now())
|
||||||
.description("Receipt of PO #" + savedPo.getPoNumber())
|
.description("Receipt of PO #" + savedPo.getPoNumber())
|
||||||
.build();
|
.build();
|
||||||
return transactionService.addTransaction(userId, tx);
|
return transactionService.addTransaction(userId, tx).then();
|
||||||
})
|
})
|
||||||
|
.onErrorResume(err -> {
|
||||||
|
System.err.println("Accounting ledger update skipped or failed: " + err.getMessage());
|
||||||
|
return Mono.empty();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return stockUpdates
|
||||||
|
.then(accountingUpdate)
|
||||||
.thenReturn(savedPo)
|
.thenReturn(savedPo)
|
||||||
.map(poResult -> {
|
.map(poResult -> {
|
||||||
poResult.setItems(savedItems);
|
poResult.setItems(savedItems);
|
||||||
return poResult;
|
return poResult;
|
||||||
});
|
});
|
||||||
}));
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,12 +2,46 @@ package com.kifi.api;
|
|||||||
|
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.springframework.boot.test.context.SpringBootTest;
|
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
|
@SpringBootTest
|
||||||
public class DBTest {
|
public class DBTest {
|
||||||
|
@Autowired
|
||||||
|
private UserRepository userRepo;
|
||||||
|
@Autowired
|
||||||
|
private BusinessProfileRepository repo;
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void contextLoads() {
|
public void test() {
|
||||||
System.out.println("Spring Boot test context loaded successfully.");
|
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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
16
kifi-app/.dockerignore
Normal file
16
kifi-app/.dockerignore
Normal file
@@ -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
|
||||||
37
kifi-app/Dockerfile
Normal file
37
kifi-app/Dockerfile
Normal file
@@ -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;"]
|
||||||
29
kifi-app/build_n_push.sh
Executable file
29
kifi-app/build_n_push.sh
Executable file
@@ -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."
|
||||||
15
kifi-app/docker-compose.yml
Normal file
15
kifi-app/docker-compose.yml
Normal file
@@ -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
|
||||||
@@ -1,9 +1,22 @@
|
|||||||
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||||
import '../../main.dart';
|
import '../../main.dart';
|
||||||
import '../../features/auth/presentation/auth_screen.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 {
|
class DioClient {
|
||||||
static final DioClient _instance = DioClient._internal();
|
static final DioClient _instance = DioClient._internal();
|
||||||
final Dio dio;
|
final Dio dio;
|
||||||
@@ -26,9 +39,7 @@ class DioClient {
|
|||||||
|
|
||||||
DioClient._internal()
|
DioClient._internal()
|
||||||
: dio = Dio(BaseOptions(
|
: dio = Dio(BaseOptions(
|
||||||
baseUrl: 'https://app.technobeesolutions.in/api/kifi-v2',
|
baseUrl: _getEffectiveBaseUrl(),
|
||||||
//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)
|
|
||||||
connectTimeout: const Duration(seconds: 10),
|
connectTimeout: const Duration(seconds: 10),
|
||||||
receiveTimeout: const Duration(seconds: 10),
|
receiveTimeout: const Duration(seconds: 10),
|
||||||
)),
|
)),
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:lucide_icons/lucide_icons.dart';
|
import 'package:lucide_icons_flutter/lucide_icons.dart';
|
||||||
|
|
||||||
class SnackBarService {
|
class SnackBarService {
|
||||||
static void showSuccess(BuildContext context, String message) {
|
static void showSuccess(BuildContext context, String message) {
|
||||||
|
|||||||
454
kifi-app/lib/core/widgets/desktop_sidebar.dart
Normal file
454
kifi-app/lib/core/widgets/desktop_sidebar.dart
Normal file
@@ -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<int> 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<String>(
|
||||||
|
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),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
74
kifi-app/lib/core/widgets/responsive_layout.dart
Normal file
74
kifi-app/lib/core/widgets/responsive_layout.dart
Normal file
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -33,11 +33,10 @@ class SmartSearchDropdown<T> extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _SmartSearchDropdownState<T> extends State<SmartSearchDropdown<T>> {
|
class _SmartSearchDropdownState<T> extends State<SmartSearchDropdown<T>> {
|
||||||
|
final OverlayPortalController _overlayController = OverlayPortalController();
|
||||||
final LayerLink _layerLink = LayerLink();
|
final LayerLink _layerLink = LayerLink();
|
||||||
final FocusNode _focusNode = FocusNode();
|
final FocusNode _focusNode = FocusNode();
|
||||||
final TextEditingController _controller = TextEditingController();
|
final TextEditingController _controller = TextEditingController();
|
||||||
OverlayEntry? _overlayEntry;
|
|
||||||
bool _showAll = false;
|
|
||||||
List<T> _filteredItems = [];
|
List<T> _filteredItems = [];
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -49,15 +48,10 @@ class _SmartSearchDropdownState<T> extends State<SmartSearchDropdown<T>> {
|
|||||||
}
|
}
|
||||||
_focusNode.addListener(() {
|
_focusNode.addListener(() {
|
||||||
if (_focusNode.hasFocus) {
|
if (_focusNode.hasFocus) {
|
||||||
_showOverlay();
|
setState(() {
|
||||||
} else {
|
_filteredItems = widget.items;
|
||||||
_removeOverlay();
|
});
|
||||||
// Reset text to selected value if focus lost without selection
|
_overlayController.show();
|
||||||
if (widget.value != null) {
|
|
||||||
_controller.text = widget.itemAsString(widget.value as T);
|
|
||||||
} else {
|
|
||||||
_controller.text = '';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -81,7 +75,6 @@ class _SmartSearchDropdownState<T> extends State<SmartSearchDropdown<T>> {
|
|||||||
void dispose() {
|
void dispose() {
|
||||||
_focusNode.dispose();
|
_focusNode.dispose();
|
||||||
_controller.dispose();
|
_controller.dispose();
|
||||||
_removeOverlay();
|
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -98,58 +91,73 @@ class _SmartSearchDropdownState<T> extends State<SmartSearchDropdown<T>> {
|
|||||||
}).toList();
|
}).toList();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
_overlayEntry?.markNeedsBuild();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void _showOverlay() {
|
void _hideOverlay() {
|
||||||
_removeOverlay();
|
_overlayController.hide();
|
||||||
_showAll = true;
|
if (widget.value != null) {
|
||||||
_filteredItems = widget.items;
|
_controller.text = widget.itemAsString(widget.value as T);
|
||||||
_overlayEntry = _createOverlayEntry();
|
} else {
|
||||||
Overlay.of(context).insert(_overlayEntry!);
|
_controller.text = '';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _removeOverlay() {
|
@override
|
||||||
_overlayEntry?.remove();
|
Widget build(BuildContext context) {
|
||||||
_overlayEntry = null;
|
return CompositedTransformTarget(
|
||||||
}
|
link: _layerLink,
|
||||||
|
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;
|
||||||
|
|
||||||
OverlayEntry _createOverlayEntry() {
|
return CompositedTransformFollower(
|
||||||
RenderBox renderBox = context.findRenderObject() as RenderBox;
|
|
||||||
var size = renderBox.size;
|
|
||||||
|
|
||||||
return OverlayEntry(
|
|
||||||
builder: (context) => Positioned(
|
|
||||||
width: size.width,
|
|
||||||
child: CompositedTransformFollower(
|
|
||||||
link: _layerLink,
|
link: _layerLink,
|
||||||
showWhenUnlinked: false,
|
showWhenUnlinked: false,
|
||||||
offset: Offset(0.0, size.height + 5.0),
|
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(
|
child: Material(
|
||||||
elevation: 4.0,
|
elevation: 8.0,
|
||||||
borderRadius: BorderRadius.circular(8.0),
|
shadowColor: Colors.black26,
|
||||||
|
borderRadius: BorderRadius.circular(16.0),
|
||||||
color: Theme.of(context).cardColor,
|
color: Theme.of(context).cardColor,
|
||||||
child: StatefulBuilder(
|
clipBehavior: Clip.antiAlias,
|
||||||
builder: (context, setOverlayState) {
|
child: Container(
|
||||||
final displayItems = _showAll ? _filteredItems : (_filteredItems.isNotEmpty ? [_filteredItems.first] : <T>[]);
|
constraints: const BoxConstraints(maxHeight: 280),
|
||||||
|
decoration: BoxDecoration(
|
||||||
return Container(
|
borderRadius: BorderRadius.circular(16.0),
|
||||||
constraints: const BoxConstraints(maxHeight: 250),
|
border: Border.all(
|
||||||
|
color: Theme.of(context).dividerColor.withValues(alpha: 0.15),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: _filteredItems.isEmpty
|
||||||
|
? Padding(
|
||||||
|
padding: const EdgeInsets.all(16.0),
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
if (displayItems.isEmpty)
|
const Text('No matches found', style: TextStyle(color: Colors.grey)),
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.all(16.0),
|
|
||||||
child: Column(
|
|
||||||
children: [
|
|
||||||
const Text('No matches found'),
|
|
||||||
if (widget.emptyActionText != null && widget.onEmptyActionPressed != null)
|
if (widget.emptyActionText != null && widget.onEmptyActionPressed != null)
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.only(top: 8.0),
|
padding: const EdgeInsets.only(top: 8.0),
|
||||||
child: TextButton(
|
child: TextButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
_focusNode.unfocus();
|
_focusNode.unfocus();
|
||||||
|
_overlayController.hide();
|
||||||
widget.onEmptyActionPressed!();
|
widget.onEmptyActionPressed!();
|
||||||
},
|
},
|
||||||
child: Text(widget.emptyActionText!),
|
child: Text(widget.emptyActionText!),
|
||||||
@@ -158,88 +166,90 @@ class _SmartSearchDropdownState<T> extends State<SmartSearchDropdown<T>> {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
else
|
: ListView.separated(
|
||||||
Flexible(
|
|
||||||
child: ListView.builder(
|
|
||||||
shrinkWrap: true,
|
shrinkWrap: true,
|
||||||
padding: EdgeInsets.zero,
|
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||||
itemCount: displayItems.length,
|
itemCount: _filteredItems.length,
|
||||||
|
separatorBuilder: (context, i) => Divider(
|
||||||
|
height: 1,
|
||||||
|
color: Theme.of(context).dividerColor.withValues(alpha: 0.1),
|
||||||
|
),
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
final item = displayItems[index];
|
final item = _filteredItems[index];
|
||||||
|
final isSelected = widget.value == item;
|
||||||
|
|
||||||
return InkWell(
|
return InkWell(
|
||||||
onTap: () {
|
onTap: () {
|
||||||
widget.onChanged(item);
|
widget.onChanged(item);
|
||||||
_controller.text = widget.itemAsString(item);
|
_controller.text = widget.itemAsString(item);
|
||||||
_focusNode.unfocus();
|
_focusNode.unfocus();
|
||||||
|
_overlayController.hide();
|
||||||
},
|
},
|
||||||
|
child: Container(
|
||||||
|
color: isSelected
|
||||||
|
? Theme.of(context).colorScheme.primary.withValues(alpha: 0.1)
|
||||||
|
: null,
|
||||||
child: widget.itemBuilder != null
|
child: widget.itemBuilder != null
|
||||||
? widget.itemBuilder!(context, item)
|
? widget.itemBuilder!(context, item)
|
||||||
: Padding(
|
: Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 12.0),
|
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 14.0),
|
||||||
child: Text(widget.itemAsString(item)),
|
child: Text(
|
||||||
|
widget.itemAsString(item),
|
||||||
|
style: TextStyle(
|
||||||
|
fontWeight: isSelected ? FontWeight.bold : FontWeight.normal,
|
||||||
|
color: isSelected ? Theme.of(context).colorScheme.primary : null,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
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),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return CompositedTransformTarget(
|
|
||||||
link: _layerLink,
|
|
||||||
child: TextFormField(
|
child: TextFormField(
|
||||||
controller: _controller,
|
controller: _controller,
|
||||||
focusNode: _focusNode,
|
focusNode: _focusNode,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: widget.labelText,
|
labelText: widget.labelText,
|
||||||
labelStyle: TextStyle(color: Theme.of(context).brightness == Brightness.dark ? Colors.white70 : Colors.grey[600]),
|
|
||||||
hintText: widget.hintText,
|
hintText: widget.hintText,
|
||||||
suffixIcon: const Icon(Icons.arrow_drop_down, color: Colors.grey),
|
suffixIcon: IconButton(
|
||||||
filled: Theme.of(context).inputDecorationTheme.filled ?? true,
|
icon: Icon(
|
||||||
fillColor: widget.fillColor ?? Theme.of(context).inputDecorationTheme.fillColor ?? (Theme.of(context).brightness == Brightness.dark ? Colors.black26 : Colors.grey[100]),
|
_overlayController.isShowing ? Icons.arrow_drop_up : Icons.arrow_drop_down,
|
||||||
border: Theme.of(context).inputDecorationTheme.border ?? OutlineInputBorder(borderRadius: BorderRadius.circular(16), borderSide: BorderSide.none),
|
color: Colors.grey,
|
||||||
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)
|
|
||||||
),
|
),
|
||||||
contentPadding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16),
|
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) {
|
onChanged: (val) {
|
||||||
_showAll = true; // when user types, we want to show all matching results
|
if (!_overlayController.isShowing) {
|
||||||
|
_overlayController.show();
|
||||||
|
}
|
||||||
_filterItems(val);
|
_filterItems(val);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import 'package:flutter/material.dart';
|
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 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import '../providers/auth_provider.dart';
|
import '../providers/auth_provider.dart';
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.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 '../providers/auth_provider.dart';
|
||||||
import 'reset_password_screen.dart';
|
import 'reset_password_screen.dart';
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.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 '../providers/auth_provider.dart';
|
||||||
import '../../dashboard/presentation/dashboard_screen.dart';
|
import '../../dashboard/presentation/dashboard_screen.dart';
|
||||||
import 'setup_wizard_screen.dart';
|
import 'setup_wizard_screen.dart';
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import 'dart:ui';
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:flutter_secure_storage/flutter_secure_storage.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:path_provider/path_provider.dart';
|
||||||
import 'package:share_plus/share_plus.dart';
|
import 'package:share_plus/share_plus.dart';
|
||||||
import 'package:cross_file/cross_file.dart';
|
import 'package:cross_file/cross_file.dart';
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.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 '../providers/auth_provider.dart';
|
||||||
import 'auth_screen.dart';
|
import 'auth_screen.dart';
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import 'package:flutter/material.dart';
|
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 'package:intl_phone_field/intl_phone_field.dart';
|
||||||
import '../../../../core/network/dio_client.dart';
|
import '../../../../core/network/dio_client.dart';
|
||||||
import '../../dashboard/presentation/dashboard_screen.dart';
|
import '../../dashboard/presentation/dashboard_screen.dart';
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.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/providers/providers.dart';
|
||||||
import '../../transactions/data/models.dart';
|
import '../../transactions/data/models.dart';
|
||||||
import '../../../core/widgets/shimmer_loading.dart';
|
import '../../../core/widgets/shimmer_loading.dart';
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.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 '../../../../core/theme/nature_colors.dart';
|
||||||
import '../../../inventory/presentation/product_list_screen.dart';
|
import '../../../inventory/presentation/product_list_screen.dart';
|
||||||
import '../../../inventory/presentation/daily_rates_screen.dart';
|
import '../../../inventory/presentation/daily_rates_screen.dart';
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.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:intl/intl.dart';
|
||||||
import '../../../inventory/providers/inventory_valuation_provider.dart';
|
import '../../../inventory/providers/inventory_valuation_provider.dart';
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.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';
|
import '../../providers/business_provider.dart';
|
||||||
|
|
||||||
class BusinessSettingsScreen extends ConsumerStatefulWidget {
|
class BusinessSettingsScreen extends ConsumerStatefulWidget {
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.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:intl/intl.dart';
|
||||||
import '../../transactions/providers/providers.dart';
|
import '../../transactions/providers/providers.dart';
|
||||||
import '../../transactions/data/models.dart';
|
import '../../transactions/data/models.dart';
|
||||||
import '../../transactions/data/repository.dart';
|
import '../../transactions/data/repository.dart';
|
||||||
import 'wallet_ledger_screen.dart';
|
import 'wallet_ledger_screen.dart';
|
||||||
|
import '../../../core/widgets/responsive_layout.dart';
|
||||||
|
|
||||||
class AccountsScreen extends ConsumerStatefulWidget {
|
class AccountsScreen extends ConsumerStatefulWidget {
|
||||||
final String? initialFilterNature;
|
final String? initialFilterNature;
|
||||||
@@ -449,7 +450,9 @@ class _AccountsScreenState extends ConsumerState<AccountsScreen> {
|
|||||||
final walletsState = ref.watch(walletProvider);
|
final walletsState = ref.watch(walletProvider);
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
body: SafeArea(
|
body: MaxContentWidth(
|
||||||
|
maxWidth: 1200,
|
||||||
|
child: SafeArea(
|
||||||
bottom: false,
|
bottom: false,
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
@@ -939,6 +942,7 @@ class _AccountsScreenState extends ConsumerState<AccountsScreen> {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.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:intl/intl.dart';
|
||||||
import '../../transactions/data/models.dart';
|
import '../../transactions/data/models.dart';
|
||||||
import '../../transactions/providers/providers.dart';
|
import '../../transactions/providers/providers.dart';
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.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/providers/providers.dart';
|
||||||
|
|
||||||
class BudgetStatusCard extends ConsumerWidget {
|
class BudgetStatusCard extends ConsumerWidget {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import 'package:flutter/material.dart';
|
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 '../../../transactions/data/models.dart';
|
||||||
import '../../../../core/theme/nature_colors.dart';
|
import '../../../../core/theme/nature_colors.dart';
|
||||||
import 'day_wise_spending_chart.dart';
|
import 'day_wise_spending_chart.dart';
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:lucide_icons/lucide_icons.dart';
|
import 'package:lucide_icons_flutter/lucide_icons.dart';
|
||||||
|
|
||||||
class CarouselItemData {
|
class CarouselItemData {
|
||||||
final String title;
|
final String title;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.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:intl/intl.dart';
|
||||||
import '../../../transactions/providers/providers.dart';
|
import '../../../transactions/providers/providers.dart';
|
||||||
import '../../../transactions/data/models.dart';
|
import '../../../transactions/data/models.dart';
|
||||||
|
|||||||
@@ -85,7 +85,14 @@ class Product {
|
|||||||
0.0,
|
0.0,
|
||||||
isActive: json['isActive'] ?? json['is_active'] ?? true,
|
isActive: json['isActive'] ?? json['is_active'] ?? true,
|
||||||
imageIds: json['images'] != null
|
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:
|
currentStock:
|
||||||
(json['currentStock'] ?? json['current_stock'] as num?)?.toDouble() ??
|
(json['currentStock'] ?? json['current_stock'] as num?)?.toDouble() ??
|
||||||
|
|||||||
@@ -1,14 +1,17 @@
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.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:image_picker/image_picker.dart';
|
||||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart' as import_storage;
|
import 'package:flutter_secure_storage/flutter_secure_storage.dart' as import_storage;
|
||||||
import '../../../core/network/dio_client.dart';
|
import '../../../core/network/dio_client.dart';
|
||||||
import '../../../../core/widgets/smart_search_dropdown.dart';
|
import '../../../../core/widgets/smart_search_dropdown.dart';
|
||||||
|
import '../../../core/widgets/responsive_layout.dart';
|
||||||
import '../domain/product.dart';
|
import '../domain/product.dart';
|
||||||
import '../providers/products_provider.dart';
|
import '../providers/products_provider.dart';
|
||||||
import '../providers/product_categories_provider.dart';
|
import '../providers/product_categories_provider.dart';
|
||||||
|
import '../providers/uoms_provider.dart';
|
||||||
import '../../business/providers/business_provider.dart';
|
import '../../business/providers/business_provider.dart';
|
||||||
import '../../transactions/presentation/widgets/attachment_gallery_screen.dart';
|
import '../../transactions/presentation/widgets/attachment_gallery_screen.dart';
|
||||||
|
|
||||||
@@ -113,8 +116,12 @@ class _AddProductScreenState extends ConsumerState<AddProductScreen> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
for (final file in _images) {
|
for (final file in _images) {
|
||||||
|
if (kIsWeb) {
|
||||||
|
allImages.add(NetworkImage(file.path));
|
||||||
|
} else {
|
||||||
allImages.add(FileImage(File(file.path)));
|
allImages.add(FileImage(File(file.path)));
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Navigator.push(
|
Navigator.push(
|
||||||
context,
|
context,
|
||||||
@@ -169,6 +176,7 @@ class _AddProductScreenState extends ConsumerState<AddProductScreen> {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final categoriesState = ref.watch(productCategoriesProvider);
|
final categoriesState = ref.watch(productCategoriesProvider);
|
||||||
|
final uomsState = ref.watch(uomsProvider);
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: Theme.of(context).colorScheme.surface,
|
backgroundColor: Theme.of(context).colorScheme.surface,
|
||||||
@@ -177,7 +185,10 @@ class _AddProductScreenState extends ConsumerState<AddProductScreen> {
|
|||||||
backgroundColor: Colors.transparent,
|
backgroundColor: Colors.transparent,
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
),
|
),
|
||||||
body: GestureDetector(
|
body: MaxContentWidth(
|
||||||
|
maxWidth: 900,
|
||||||
|
child: GestureDetector(
|
||||||
|
behavior: HitTestBehavior.translucent,
|
||||||
onTap: () => FocusScope.of(context).unfocus(),
|
onTap: () => FocusScope.of(context).unfocus(),
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
@@ -209,16 +220,21 @@ class _AddProductScreenState extends ConsumerState<AddProductScreen> {
|
|||||||
const Text('Category & Unit', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600)),
|
const Text('Category & Unit', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600)),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
categoriesState.when(
|
categoriesState.when(
|
||||||
loading: () => const CircularProgressIndicator(),
|
loading: () => const Padding(
|
||||||
error: (err, stack) => Text('Error loading categories: $err'),
|
padding: EdgeInsets.symmetric(vertical: 8),
|
||||||
|
child: LinearProgressIndicator(),
|
||||||
|
),
|
||||||
|
error: (err, stack) => Text('Error loading categories: $err', style: const TextStyle(color: Colors.red)),
|
||||||
data: (categories) {
|
data: (categories) {
|
||||||
final leafCategories = categories.where((c) => !categories.any((child) => child.parentCategoryId == c.id)).toList();
|
final activeCategories = categories.where((c) => c.isActive).toList();
|
||||||
return leafCategories.isEmpty
|
final uoms = uomsState.value ?? [];
|
||||||
|
|
||||||
|
return activeCategories.isEmpty
|
||||||
? const Text('No categories found. Manage categories in Business Hub.', style: TextStyle(color: Colors.orange))
|
? const Text('No categories found. Manage categories in Business Hub.', style: TextStyle(color: Colors.orange))
|
||||||
: SmartSearchDropdown<ProductCategory>(
|
: SmartSearchDropdown<ProductCategory>(
|
||||||
hintText: 'Select Category*',
|
hintText: 'Select Category*',
|
||||||
value: _selectedCategory,
|
value: _selectedCategory,
|
||||||
items: leafCategories,
|
items: activeCategories,
|
||||||
itemAsString: (c) => c.name,
|
itemAsString: (c) => c.name,
|
||||||
itemBuilder: (context, item) {
|
itemBuilder: (context, item) {
|
||||||
List<String> path = [];
|
List<String> path = [];
|
||||||
@@ -232,11 +248,12 @@ class _AddProductScreenState extends ConsumerState<AddProductScreen> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.all(12.0),
|
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 12.0),
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text(item.name, style: const TextStyle(fontWeight: FontWeight.bold)),
|
Text(item.name, style: const TextStyle(fontWeight: FontWeight.bold)),
|
||||||
|
if (path.length > 1)
|
||||||
Text(path.join(' -> '), style: TextStyle(fontSize: 12, color: Colors.grey.shade600)),
|
Text(path.join(' -> '), style: TextStyle(fontSize: 12, color: Colors.grey.shade600)),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -258,6 +275,15 @@ class _AddProductScreenState extends ConsumerState<AddProductScreen> {
|
|||||||
if (val.makingChargeType != null && val.makingChargeType!.isNotEmpty) {
|
if (val.makingChargeType != null && val.makingChargeType!.isNotEmpty) {
|
||||||
_makingChargesType = val.makingChargeType!;
|
_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<AddProductScreen> {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
DropdownButtonFormField<int>(
|
uomsState.when(
|
||||||
decoration: const InputDecoration(labelText: 'Unit of Measure (Optional)'),
|
loading: () => const Padding(
|
||||||
value: _uomId,
|
padding: EdgeInsets.symmetric(vertical: 8),
|
||||||
items: const [
|
child: LinearProgressIndicator(),
|
||||||
DropdownMenuItem(value: 1, child: Text('Grams (g)')),
|
),
|
||||||
DropdownMenuItem(value: 2, child: Text('Kilograms (kg)')),
|
error: (err, stack) => Text('Error loading units: $err', style: const TextStyle(color: Colors.red)),
|
||||||
DropdownMenuItem(value: 3, child: Text('Pieces (pcs)')),
|
data: (uoms) {
|
||||||
],
|
final effectiveValue = uoms.any((u) => u.id == _uomId) ? _uomId : null;
|
||||||
|
return DropdownButtonFormField<int>(
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
labelText: 'Unit of Measure (Optional)',
|
||||||
|
),
|
||||||
|
isExpanded: true,
|
||||||
|
hint: const Text('None (Optional)'),
|
||||||
|
value: effectiveValue,
|
||||||
|
items: uoms.map((u) => DropdownMenuItem<int>(
|
||||||
|
value: u.id,
|
||||||
|
child: Text(u.displayName),
|
||||||
|
)).toList(),
|
||||||
onChanged: (val) => setState(() => _uomId = val),
|
onChanged: (val) => setState(() => _uomId = val),
|
||||||
|
);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
|
|
||||||
@@ -314,6 +353,7 @@ class _AddProductScreenState extends ConsumerState<AddProductScreen> {
|
|||||||
Expanded(
|
Expanded(
|
||||||
child: DropdownButtonFormField<String>(
|
child: DropdownButtonFormField<String>(
|
||||||
value: _makingChargesType,
|
value: _makingChargesType,
|
||||||
|
isExpanded: true,
|
||||||
decoration: const InputDecoration(labelText: 'Charge Type'),
|
decoration: const InputDecoration(labelText: 'Charge Type'),
|
||||||
items: const [
|
items: const [
|
||||||
DropdownMenuItem(value: 'PER_GRAM', child: Text('Per Gram')),
|
DropdownMenuItem(value: 'PER_GRAM', child: Text('Per Gram')),
|
||||||
@@ -364,7 +404,7 @@ class _AddProductScreenState extends ConsumerState<AddProductScreen> {
|
|||||||
final localIndex = index - _existingImageIds.length;
|
final localIndex = index - _existingImageIds.length;
|
||||||
return _buildImageThumbnail(
|
return _buildImageThumbnail(
|
||||||
isNetwork: false,
|
isNetwork: false,
|
||||||
file: File(_images[localIndex].path),
|
xFile: _images[localIndex],
|
||||||
onTap: () => _previewImage(index),
|
onTap: () => _previewImage(index),
|
||||||
onDelete: () => _removeImage(localIndex),
|
onDelete: () => _removeImage(localIndex),
|
||||||
);
|
);
|
||||||
@@ -379,8 +419,8 @@ class _AddProductScreenState extends ConsumerState<AddProductScreen> {
|
|||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
padding: const EdgeInsets.symmetric(vertical: 24),
|
padding: const EdgeInsets.symmetric(vertical: 24),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Theme.of(context).colorScheme.primary.withOpacity(0.05),
|
color: Theme.of(context).colorScheme.primary.withValues(alpha: 0.05),
|
||||||
border: Border.all(color: Theme.of(context).colorScheme.primary.withOpacity(0.3)),
|
border: Border.all(color: Theme.of(context).colorScheme.primary.withValues(alpha: 0.3)),
|
||||||
borderRadius: BorderRadius.circular(16),
|
borderRadius: BorderRadius.circular(16),
|
||||||
),
|
),
|
||||||
child: Column(
|
child: Column(
|
||||||
@@ -403,6 +443,7 @@ class _AddProductScreenState extends ConsumerState<AddProductScreen> {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -428,7 +469,13 @@ class _AddProductScreenState extends ConsumerState<AddProductScreen> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
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(
|
return Stack(
|
||||||
fit: StackFit.expand,
|
fit: StackFit.expand,
|
||||||
children: [
|
children: [
|
||||||
@@ -437,7 +484,7 @@ class _AddProductScreenState extends ConsumerState<AddProductScreen> {
|
|||||||
child: Container(
|
child: Container(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
borderRadius: BorderRadius.circular(16),
|
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(
|
child: ClipRRect(
|
||||||
borderRadius: BorderRadius.circular(16),
|
borderRadius: BorderRadius.circular(16),
|
||||||
@@ -452,7 +499,9 @@ class _AddProductScreenState extends ConsumerState<AddProductScreen> {
|
|||||||
headers: {'Authorization': 'Bearer $_token'},
|
headers: {'Authorization': 'Bearer $_token'},
|
||||||
errorBuilder: (ctx, err, stack) => const Icon(LucideIcons.imageOff, color: Colors.grey),
|
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)),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import '../providers/product_categories_provider.dart';
|
|||||||
import '../../../core/theme/nature_colors.dart';
|
import '../../../core/theme/nature_colors.dart';
|
||||||
import '../../business/providers/business_mode_provider.dart';
|
import '../../business/providers/business_mode_provider.dart';
|
||||||
import '../../../core/theme/app_theme.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 'dart:ui';
|
||||||
import '../../../core/utils/purity_utils.dart';
|
import '../../../core/utils/purity_utils.dart';
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.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:intl/intl.dart';
|
||||||
import '../providers/product_categories_provider.dart';
|
import '../providers/product_categories_provider.dart';
|
||||||
import '../providers/commodity_rates_provider.dart';
|
import '../providers/commodity_rates_provider.dart';
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.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/products_provider.dart';
|
import '../providers/products_provider.dart';
|
||||||
import '../providers/product_categories_provider.dart';
|
import '../providers/product_categories_provider.dart';
|
||||||
import 'add_product_screen.dart';
|
import 'add_product_screen.dart';
|
||||||
import 'product_detail_screen.dart';
|
import 'product_detail_screen.dart';
|
||||||
import '../../../core/network/dio_client.dart';
|
import '../../../core/network/dio_client.dart';
|
||||||
|
import '../../../core/widgets/responsive_layout.dart';
|
||||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||||
|
|
||||||
class ProductListScreen extends ConsumerStatefulWidget {
|
class ProductListScreen extends ConsumerStatefulWidget {
|
||||||
@@ -66,7 +67,9 @@ class _ProductListScreenState extends ConsumerState<ProductListScreen> {
|
|||||||
foregroundColor: Colors.black,
|
foregroundColor: Colors.black,
|
||||||
centerTitle: true,
|
centerTitle: true,
|
||||||
),
|
),
|
||||||
body: Column(
|
body: MaxContentWidth(
|
||||||
|
maxWidth: 1000,
|
||||||
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
Container(
|
Container(
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
@@ -138,13 +141,10 @@ class _ProductListScreenState extends ConsumerState<ProductListScreen> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
final p = products[index];
|
final p = products[index];
|
||||||
final catName =
|
final catName = categoriesState.value
|
||||||
categoriesState.value
|
?.where((c) => c.id == p.categoryId)
|
||||||
?.firstWhere(
|
.firstOrNull
|
||||||
(c) => c.id == p.categoryId,
|
?.name ??
|
||||||
orElse: () => categoriesState.value!.first,
|
|
||||||
)
|
|
||||||
.name ??
|
|
||||||
'No Category';
|
'No Category';
|
||||||
|
|
||||||
return Container(
|
return Container(
|
||||||
@@ -267,12 +267,16 @@ class _ProductListScreenState extends ConsumerState<ProductListScreen> {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
),
|
||||||
floatingActionButton: FloatingActionButton(
|
floatingActionButton: FloatingActionButton(
|
||||||
onPressed: () {
|
onPressed: () async {
|
||||||
Navigator.push(
|
await Navigator.push(
|
||||||
context,
|
context,
|
||||||
MaterialPageRoute(builder: (_) => const AddProductScreen()),
|
MaterialPageRoute(builder: (_) => const AddProductScreen()),
|
||||||
);
|
);
|
||||||
|
if (mounted) {
|
||||||
|
ref.read(productsProvider.notifier).refresh();
|
||||||
|
}
|
||||||
},
|
},
|
||||||
backgroundColor: Theme.of(context).colorScheme.primary,
|
backgroundColor: Theme.of(context).colorScheme.primary,
|
||||||
child: const Icon(LucideIcons.plus, color: Colors.white),
|
child: const Icon(LucideIcons.plus, color: Colors.white),
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.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:kifi_app/features/inventory/providers/products_provider.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/features/inventory/presentation/widgets/adjust_stock_sheet.dart';
|
||||||
import 'package:kifi_app/core/network/dio_client.dart';
|
import 'package:kifi_app/core/network/dio_client.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/features/inventory/providers/commodity_rates_provider.dart';
|
||||||
import 'package:kifi_app/core/network/dio_client.dart';
|
import 'package:kifi_app/core/network/dio_client.dart';
|
||||||
import 'package:intl/intl.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/presentation/purchase_order_details_screen.dart';
|
||||||
import 'package:kifi_app/features/vendor/providers/purchase_orders_provider.dart';
|
import 'package:kifi_app/features/vendor/providers/purchase_orders_provider.dart';
|
||||||
import 'package:kifi_app/core/utils/purity_utils.dart';
|
import 'package:kifi_app/core/utils/purity_utils.dart';
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import '../domain/inventory_item.dart';
|
|||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
import 'package:image_picker/image_picker.dart';
|
import 'package:image_picker/image_picker.dart';
|
||||||
import 'package:flutter_image_compress/flutter_image_compress.dart';
|
import 'package:flutter_image_compress/flutter_image_compress.dart';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
|
||||||
class ProductsNotifier extends AsyncNotifier<List<Product>> {
|
class ProductsNotifier extends AsyncNotifier<List<Product>> {
|
||||||
int _currentPage = 0;
|
int _currentPage = 0;
|
||||||
@@ -25,71 +26,74 @@ class ProductsNotifier extends AsyncNotifier<List<Product>> {
|
|||||||
return _fetchProducts(page: _currentPage, size: _pageSize);
|
return _fetchProducts(page: _currentPage, size: _pageSize);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<List<Product>> _fetchProducts({
|
Future<List<Product>> _fetchProducts({int page = 0, int size = 50, String? search}) async {
|
||||||
required int page,
|
try {
|
||||||
required int size,
|
final queryParams = <String, dynamic>{
|
||||||
}) async {
|
|
||||||
final response = await DioClient().dio.get(
|
|
||||||
'/inventory/products',
|
|
||||||
queryParameters: {
|
|
||||||
'page': page,
|
'page': page,
|
||||||
'size': size,
|
'size': size,
|
||||||
if (_currentSearchQuery.isNotEmpty) 'search': _currentSearchQuery,
|
};
|
||||||
},
|
if (search != null && search.isNotEmpty) {
|
||||||
);
|
queryParams['search'] = search;
|
||||||
if (response.statusCode == 200) {
|
|
||||||
final List<dynamic> 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 (_) {}
|
|
||||||
}
|
}
|
||||||
final products = data.map((e) => Product.fromJson(e)).toList();
|
|
||||||
if (products.length < size) {
|
final response = await DioClient().dio.get(
|
||||||
_hasMore = false;
|
'/inventory/products',
|
||||||
}
|
queryParameters: queryParams,
|
||||||
return products;
|
);
|
||||||
|
|
||||||
|
if (response.data is List) {
|
||||||
|
final List list = response.data;
|
||||||
|
_hasMore = list.length >= size;
|
||||||
|
return list.map((json) => Product.fromJson(json as Map<String, dynamic>)).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<String, dynamic>)).toList();
|
||||||
}
|
}
|
||||||
return [];
|
return [];
|
||||||
}
|
} catch (e) {
|
||||||
|
return [];
|
||||||
Future<void> 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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> refresh() async {
|
Future<void> refresh() async {
|
||||||
state = const AsyncValue.loading();
|
|
||||||
_currentPage = 0;
|
_currentPage = 0;
|
||||||
_hasMore = true;
|
_hasMore = true;
|
||||||
try {
|
state = const AsyncValue.loading();
|
||||||
state = AsyncValue.data(
|
state = await AsyncValue.guard(() => _fetchProducts(page: 0, size: _pageSize, search: _currentSearchQuery));
|
||||||
await _fetchProducts(page: _currentPage, size: _pageSize),
|
|
||||||
);
|
|
||||||
} catch (e, stack) {
|
|
||||||
state = AsyncValue.error(e, stack);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> search(String query) async {
|
Future<void> search(String query) async {
|
||||||
_currentSearchQuery = query;
|
_currentSearchQuery = query;
|
||||||
await refresh();
|
_currentPage = 0;
|
||||||
|
_hasMore = true;
|
||||||
|
state = const AsyncValue.loading();
|
||||||
|
state = await AsyncValue.guard(() => _fetchProducts(page: 0, size: _pageSize, search: query));
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> fetchNextPage() async {
|
||||||
|
await loadMore();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> 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<void> createProduct(Product product, {List<XFile>? images}) async {
|
Future<void> createProduct(Product product, {List<XFile>? images}) async {
|
||||||
@@ -103,15 +107,20 @@ class ProductsNotifier extends AsyncNotifier<List<Product>> {
|
|||||||
final productId = response.data['id'];
|
final productId = response.data['id'];
|
||||||
for (var image in images) {
|
for (var image in images) {
|
||||||
final bytes = await image.readAsBytes();
|
final bytes = await image.readAsBytes();
|
||||||
final compressedBytes = await FlutterImageCompress.compressWithList(
|
Uint8List uploadBytes = bytes;
|
||||||
|
if (!kIsWeb) {
|
||||||
|
try {
|
||||||
|
uploadBytes = await FlutterImageCompress.compressWithList(
|
||||||
bytes,
|
bytes,
|
||||||
minWidth: 800,
|
minWidth: 800,
|
||||||
quality: 80,
|
quality: 80,
|
||||||
);
|
);
|
||||||
|
} catch (_) {}
|
||||||
|
}
|
||||||
|
|
||||||
final formData = FormData.fromMap({
|
final formData = FormData.fromMap({
|
||||||
'file': MultipartFile.fromBytes(
|
'file': MultipartFile.fromBytes(
|
||||||
compressedBytes,
|
uploadBytes,
|
||||||
filename: image.name,
|
filename: image.name,
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
@@ -143,15 +152,20 @@ class ProductsNotifier extends AsyncNotifier<List<Product>> {
|
|||||||
if (newImages != null && newImages.isNotEmpty) {
|
if (newImages != null && newImages.isNotEmpty) {
|
||||||
for (var image in newImages) {
|
for (var image in newImages) {
|
||||||
final bytes = await image.readAsBytes();
|
final bytes = await image.readAsBytes();
|
||||||
final compressedBytes = await FlutterImageCompress.compressWithList(
|
Uint8List uploadBytes = bytes;
|
||||||
|
if (!kIsWeb) {
|
||||||
|
try {
|
||||||
|
uploadBytes = await FlutterImageCompress.compressWithList(
|
||||||
bytes,
|
bytes,
|
||||||
minWidth: 800,
|
minWidth: 800,
|
||||||
quality: 80,
|
quality: 80,
|
||||||
);
|
);
|
||||||
|
} catch (_) {}
|
||||||
|
}
|
||||||
|
|
||||||
final formData = FormData.fromMap({
|
final formData = FormData.fromMap({
|
||||||
'file': MultipartFile.fromBytes(
|
'file': MultipartFile.fromBytes(
|
||||||
compressedBytes,
|
uploadBytes,
|
||||||
filename: image.name,
|
filename: image.name,
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
|
|||||||
102
kifi-app/lib/features/inventory/providers/uoms_provider.dart
Normal file
102
kifi-app/lib/features/inventory/providers/uoms_provider.dart
Normal file
@@ -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<String, dynamic> json) {
|
||||||
|
return UnitOfMeasure(
|
||||||
|
id: json['id'],
|
||||||
|
userId: json['userId'] ?? json['user_id'],
|
||||||
|
name: json['name'] ?? '',
|
||||||
|
abbreviation: json['abbreviation'],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
return {
|
||||||
|
'id': id,
|
||||||
|
'userId': userId,
|
||||||
|
'name': name,
|
||||||
|
'abbreviation': abbreviation,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
String get displayName => abbreviation != null && abbreviation!.isNotEmpty ? '$name ($abbreviation)' : name;
|
||||||
|
}
|
||||||
|
|
||||||
|
class UomsNotifier extends AsyncNotifier<List<UnitOfMeasure>> {
|
||||||
|
@override
|
||||||
|
FutureOr<List<UnitOfMeasure>> build() async {
|
||||||
|
return _fetchUoms();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<UnitOfMeasure>> _fetchUoms() async {
|
||||||
|
try {
|
||||||
|
final response = await DioClient().dio.get('/inventory/uom');
|
||||||
|
if (response.statusCode == 200) {
|
||||||
|
final List<dynamic> 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<dynamic> data = response.data;
|
||||||
|
return data.map((e) => UnitOfMeasure.fromJson(e)).toList();
|
||||||
|
}
|
||||||
|
} catch (_) {}
|
||||||
|
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> refresh() async {
|
||||||
|
state = const AsyncValue.loading();
|
||||||
|
state = await AsyncValue.guard(() => _fetchUoms());
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<UnitOfMeasure?> 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<UomsNotifier, List<UnitOfMeasure>>(() {
|
||||||
|
return UomsNotifier();
|
||||||
|
});
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:shared_preferences/shared_preferences.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';
|
import '../../dashboard/presentation/dashboard_screen.dart';
|
||||||
|
|
||||||
class OnboardingScreen extends StatefulWidget {
|
class OnboardingScreen extends StatefulWidget {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.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 '../domain/project.dart';
|
||||||
import '../providers/project_provider.dart';
|
import '../providers/project_provider.dart';
|
||||||
import 'widgets/add_task_sheet.dart';
|
import 'widgets/add_task_sheet.dart';
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.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 '../../../../core/theme/nature_colors.dart';
|
||||||
import 'projects_screen.dart';
|
import 'projects_screen.dart';
|
||||||
import '../../sales/presentation/invoices_list_screen.dart';
|
import '../../sales/presentation/invoices_list_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/material.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import '../providers/project_provider.dart';
|
import '../providers/project_provider.dart';
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import '../../domain/project.dart';
|
import '../../domain/project.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.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 '../../../sales/presentation/add_customer_sheet.dart';
|
||||||
import '../../providers/project_provider.dart';
|
import '../../providers/project_provider.dart';
|
||||||
import '../../../sales/providers/customers_provider.dart';
|
import '../../../sales/providers/customers_provider.dart';
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
|
|||||||
|
|
||||||
import '../../../../core/widgets/premium_text_field.dart';
|
import '../../../../core/widgets/premium_text_field.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.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 '../../domain/project_task.dart';
|
||||||
import '../../providers/project_provider.dart';
|
import '../../providers/project_provider.dart';
|
||||||
import '../../providers/user_provider.dart';
|
import '../../providers/user_provider.dart';
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.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 '../../domain/project_task.dart';
|
||||||
import '../../providers/project_provider.dart';
|
import '../../providers/project_provider.dart';
|
||||||
import 'task_details_sheet.dart';
|
import 'task_details_sheet.dart';
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import 'dart:convert';
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.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:image_picker/image_picker.dart';
|
||||||
import 'package:file_picker/file_picker.dart';
|
import 'package:file_picker/file_picker.dart';
|
||||||
import 'package:path_provider/path_provider.dart';
|
import 'package:path_provider/path_provider.dart';
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.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 '../providers/customers_provider.dart';
|
||||||
import '../domain/customer.dart';
|
import '../domain/customer.dart';
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
@@ -222,7 +223,7 @@ class _AddCustomerSheetState extends ConsumerState<AddCustomerSheet> {
|
|||||||
radius: 50,
|
radius: 50,
|
||||||
backgroundColor: Colors.grey[200],
|
backgroundColor: Colors.grey[200],
|
||||||
backgroundImage: _photo != null
|
backgroundImage: _photo != null
|
||||||
? FileImage(File(_photo!.path)) as ImageProvider
|
? (kIsWeb ? NetworkImage(_photo!.path) : FileImage(File(_photo!.path))) as ImageProvider
|
||||||
: (widget.customer?.photoUrl != null &&
|
: (widget.customer?.photoUrl != null &&
|
||||||
_token != null
|
_token != null
|
||||||
? NetworkImage(
|
? NetworkImage(
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.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 '../providers/customers_provider.dart';
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import '../../../core/widgets/shimmer_loading.dart';
|
import '../../../core/widgets/shimmer_loading.dart';
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.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:image_picker/image_picker.dart';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
@@ -710,7 +711,7 @@ class _InvoiceBuilderScreenState extends ConsumerState<InvoiceBuilderScreen> {
|
|||||||
context,
|
context,
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
builder: (_) => AttachmentGalleryScreen(
|
builder: (_) => AttachmentGalleryScreen(
|
||||||
images: [FileImage(File(_invoiceFile!.path))],
|
images: [kIsWeb ? NetworkImage(_invoiceFile!.path) : FileImage(File(_invoiceFile!.path))],
|
||||||
initialIndex: 0,
|
initialIndex: 0,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import 'dart:io';
|
|||||||
import 'dart:typed_data';
|
import 'dart:typed_data';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.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:intl/intl.dart';
|
||||||
import 'package:screenshot/screenshot.dart';
|
import 'package:screenshot/screenshot.dart';
|
||||||
import 'package:path_provider/path_provider.dart';
|
import 'package:path_provider/path_provider.dart';
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.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:intl/intl.dart';
|
||||||
import '../providers/invoices_provider.dart';
|
import '../providers/invoices_provider.dart';
|
||||||
import '../domain/invoice.dart';
|
import '../domain/invoice.dart';
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.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/premium_text_field.dart';
|
||||||
import '../../../../core/widgets/smart_search_dropdown.dart';
|
import '../../../../core/widgets/smart_search_dropdown.dart';
|
||||||
import '../../providers/customers_provider.dart';
|
import '../../providers/customers_provider.dart';
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.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/premium_text_field.dart';
|
||||||
import '../../../transactions/providers/providers.dart';
|
import '../../../transactions/providers/providers.dart';
|
||||||
import '../../providers/invoices_provider.dart';
|
import '../../providers/invoices_provider.dart';
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
import 'dart:typed_data';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
import 'package:image_picker/image_picker.dart';
|
import 'package:image_picker/image_picker.dart';
|
||||||
@@ -90,15 +92,20 @@ class CustomersNotifier extends AsyncNotifier<List<Customer>> {
|
|||||||
|
|
||||||
Future<void> _uploadPhoto(int customerId, XFile photo) async {
|
Future<void> _uploadPhoto(int customerId, XFile photo) async {
|
||||||
final bytes = await photo.readAsBytes();
|
final bytes = await photo.readAsBytes();
|
||||||
final compressedBytes = await FlutterImageCompress.compressWithList(
|
Uint8List uploadBytes = bytes;
|
||||||
|
if (!kIsWeb) {
|
||||||
|
try {
|
||||||
|
uploadBytes = await FlutterImageCompress.compressWithList(
|
||||||
bytes,
|
bytes,
|
||||||
minWidth: 413,
|
minWidth: 413,
|
||||||
minHeight: 531,
|
minHeight: 531,
|
||||||
quality: 85,
|
quality: 85,
|
||||||
);
|
);
|
||||||
|
} catch (_) {}
|
||||||
|
}
|
||||||
|
|
||||||
final formData = FormData.fromMap({
|
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);
|
await DioClient().dio.post('/customers/$customerId/photo', data: formData);
|
||||||
|
|||||||
@@ -1,12 +1,16 @@
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
import 'dart:typed_data';
|
||||||
|
import 'package:flutter/foundation.dart' show kIsWeb;
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'widgets/attachment_gallery_screen.dart';
|
import 'widgets/attachment_gallery_screen.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.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:intl/intl.dart';
|
||||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart' as flutter_secure_storage;
|
import 'package:flutter_secure_storage/flutter_secure_storage.dart' as flutter_secure_storage;
|
||||||
import 'package:image_picker/image_picker.dart';
|
import 'package:image_picker/image_picker.dart';
|
||||||
import 'package:flutter_image_compress/flutter_image_compress.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 '../providers/providers.dart';
|
||||||
import '../data/models.dart';
|
import '../data/models.dart';
|
||||||
import '../../../core/utils/snackbar_service.dart';
|
import '../../../core/utils/snackbar_service.dart';
|
||||||
@@ -133,11 +137,16 @@ class _AddTransactionScreenState extends ConsumerState<AddTransactionScreen> {
|
|||||||
final XFile? image = await _picker.pickImage(source: source);
|
final XFile? image = await _picker.pickImage(source: source);
|
||||||
if (image != null) {
|
if (image != null) {
|
||||||
final bytes = await image.readAsBytes();
|
final bytes = await image.readAsBytes();
|
||||||
final compressedBytes = await FlutterImageCompress.compressWithList(
|
Uint8List compressedBytes = bytes;
|
||||||
|
if (!kIsWeb) {
|
||||||
|
try {
|
||||||
|
compressedBytes = await FlutterImageCompress.compressWithList(
|
||||||
bytes,
|
bytes,
|
||||||
minWidth: 600,
|
minWidth: 600,
|
||||||
quality: 80,
|
quality: 80,
|
||||||
);
|
);
|
||||||
|
} catch (_) {}
|
||||||
|
}
|
||||||
final base64String = base64Encode(compressedBytes);
|
final base64String = base64Encode(compressedBytes);
|
||||||
setState(() {
|
setState(() {
|
||||||
base64Attachments.add({
|
base64Attachments.add({
|
||||||
@@ -751,7 +760,9 @@ class _AddTransactionScreenState extends ConsumerState<AddTransactionScreen> {
|
|||||||
)
|
)
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
body: SafeArea(
|
body: MaxContentWidth(
|
||||||
|
maxWidth: 800,
|
||||||
|
child: SafeArea(
|
||||||
child: SingleChildScrollView(
|
child: SingleChildScrollView(
|
||||||
padding: const EdgeInsets.all(24.0),
|
padding: const EdgeInsets.all(24.0),
|
||||||
child: Column(
|
child: Column(
|
||||||
@@ -988,7 +999,7 @@ class _AddTransactionScreenState extends ConsumerState<AddTransactionScreen> {
|
|||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
image: DecorationImage(
|
image: DecorationImage(
|
||||||
image: _jwtToken != null
|
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,
|
: const AssetImage('assets/images/placeholder.png') as ImageProvider,
|
||||||
fit: BoxFit.cover,
|
fit: BoxFit.cover,
|
||||||
),
|
),
|
||||||
@@ -1096,6 +1107,7 @@ class _AddTransactionScreenState extends ConsumerState<AddTransactionScreen> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1105,7 +1117,7 @@ class _AddTransactionScreenState extends ConsumerState<AddTransactionScreen> {
|
|||||||
// Add existing attachments
|
// Add existing attachments
|
||||||
for (final att in existingAttachments) {
|
for (final att in existingAttachments) {
|
||||||
if (_jwtToken != null) {
|
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 {
|
} else {
|
||||||
allImages.add(const AssetImage('assets/images/placeholder.png'));
|
allImages.add(const AssetImage('assets/images/placeholder.png'));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.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:intl/intl.dart';
|
||||||
import '../providers/providers.dart';
|
import '../providers/providers.dart';
|
||||||
import '../../dashboard/presentation/maturity_dialog.dart';
|
import '../../dashboard/presentation/maturity_dialog.dart';
|
||||||
@@ -10,6 +10,7 @@ import '../../../core/widgets/empty_state.dart';
|
|||||||
import 'add_transaction_screen.dart';
|
import 'add_transaction_screen.dart';
|
||||||
import '../presentation/widgets/transaction_filter_sheet.dart';
|
import '../presentation/widgets/transaction_filter_sheet.dart';
|
||||||
import '../../../core/theme/nature_colors.dart';
|
import '../../../core/theme/nature_colors.dart';
|
||||||
|
import '../../../core/widgets/responsive_layout.dart';
|
||||||
|
|
||||||
class AllTransactionsScreen extends ConsumerStatefulWidget {
|
class AllTransactionsScreen extends ConsumerStatefulWidget {
|
||||||
const AllTransactionsScreen({super.key});
|
const AllTransactionsScreen({super.key});
|
||||||
@@ -40,7 +41,6 @@ class _AllTransactionsScreenState extends ConsumerState<AllTransactionsScreen> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _onSearchChanged(String value) {
|
void _onSearchChanged(String value) {
|
||||||
// Basic debounce for text search
|
|
||||||
Future.delayed(const Duration(milliseconds: 500), () {
|
Future.delayed(const Duration(milliseconds: 500), () {
|
||||||
if (_searchController.text == value) {
|
if (_searchController.text == value) {
|
||||||
ref.read(paginatedTransactionProvider.notifier).updateFilters(search: value);
|
ref.read(paginatedTransactionProvider.notifier).updateFilters(search: value);
|
||||||
@@ -88,13 +88,15 @@ class _AllTransactionsScreenState extends ConsumerState<AllTransactionsScreen> {
|
|||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
hintText: 'Search transactions...',
|
hintText: 'Search transactions...',
|
||||||
prefixIcon: const Icon(LucideIcons.search),
|
prefixIcon: const Icon(LucideIcons.search),
|
||||||
suffixIcon: _searchController.text.isNotEmpty ? IconButton(
|
suffixIcon: _searchController.text.isNotEmpty
|
||||||
|
? IconButton(
|
||||||
icon: const Icon(LucideIcons.x),
|
icon: const Icon(LucideIcons.x),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
_searchController.clear();
|
_searchController.clear();
|
||||||
_onSearchChanged('');
|
_onSearchChanged('');
|
||||||
},
|
},
|
||||||
) : null,
|
)
|
||||||
|
: null,
|
||||||
filled: true,
|
filled: true,
|
||||||
fillColor: Colors.grey.shade100,
|
fillColor: Colors.grey.shade100,
|
||||||
border: OutlineInputBorder(
|
border: OutlineInputBorder(
|
||||||
@@ -103,7 +105,7 @@ class _AllTransactionsScreenState extends ConsumerState<AllTransactionsScreen> {
|
|||||||
),
|
),
|
||||||
focusedBorder: OutlineInputBorder(
|
focusedBorder: OutlineInputBorder(
|
||||||
borderRadius: BorderRadius.circular(16),
|
borderRadius: BorderRadius.circular(16),
|
||||||
borderSide: const BorderSide(color: Color(0xFF6C63FF), width: 2)
|
borderSide: const BorderSide(color: Color(0xFF6C63FF), width: 2),
|
||||||
),
|
),
|
||||||
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||||
),
|
),
|
||||||
@@ -111,7 +113,9 @@ class _AllTransactionsScreenState extends ConsumerState<AllTransactionsScreen> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
body: Builder(
|
body: MaxContentWidth(
|
||||||
|
maxWidth: 1200,
|
||||||
|
child: Builder(
|
||||||
builder: (context) {
|
builder: (context) {
|
||||||
if (transactions.isEmpty && paginatedState.isLoading) {
|
if (transactions.isEmpty && paginatedState.isLoading) {
|
||||||
return ListView.builder(
|
return ListView.builder(
|
||||||
@@ -249,7 +253,7 @@ class _AllTransactionsScreenState extends ConsumerState<AllTransactionsScreen> {
|
|||||||
TextButton(onPressed: () => Navigator.pop(ctx, false), child: const Text('Cancel')),
|
TextButton(onPressed: () => Navigator.pop(ctx, false), child: const Text('Cancel')),
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () => Navigator.pop(ctx, true),
|
onPressed: () => Navigator.pop(ctx, true),
|
||||||
child: const Text('Delete', style: TextStyle(color: Colors.red))
|
child: const Text('Delete', style: TextStyle(color: Colors.red)),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -261,9 +265,7 @@ class _AllTransactionsScreenState extends ConsumerState<AllTransactionsScreen> {
|
|||||||
},
|
},
|
||||||
child: Card(
|
child: Card(
|
||||||
margin: const EdgeInsets.only(bottom: 12),
|
margin: const EdgeInsets.only(bottom: 12),
|
||||||
child: Column(
|
child: ListTile(
|
||||||
children: [
|
|
||||||
ListTile(
|
|
||||||
onTap: () {
|
onTap: () {
|
||||||
Navigator.push(context, MaterialPageRoute(builder: (_) => AddTransactionScreen(transaction: t)));
|
Navigator.push(context, MaterialPageRoute(builder: (_) => AddTransactionScreen(transaction: t)));
|
||||||
},
|
},
|
||||||
@@ -294,14 +296,13 @@ class _AllTransactionsScreenState extends ConsumerState<AllTransactionsScreen> {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
},
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.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/providers.dart';
|
||||||
import '../../providers/paginated_transaction_provider.dart';
|
import '../../providers/paginated_transaction_provider.dart';
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.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 '../providers/vendors_provider.dart';
|
||||||
import '../domain/vendor.dart';
|
import '../domain/vendor.dart';
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
@@ -209,7 +210,7 @@ class _AddVendorSheetState extends ConsumerState<AddVendorSheet> {
|
|||||||
radius: 50,
|
radius: 50,
|
||||||
backgroundColor: Colors.blue.withOpacity(0.1),
|
backgroundColor: Colors.blue.withOpacity(0.1),
|
||||||
backgroundImage: _photo != null
|
backgroundImage: _photo != null
|
||||||
? FileImage(File(_photo!.path)) as ImageProvider
|
? (kIsWeb ? NetworkImage(_photo!.path) : FileImage(File(_photo!.path))) as ImageProvider
|
||||||
: (widget.vendor?.photoUrl != null
|
: (widget.vendor?.photoUrl != null
|
||||||
? NetworkImage(widget.vendor!.photoUrl!)
|
? NetworkImage(widget.vendor!.photoUrl!)
|
||||||
: null),
|
: null),
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.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:intl/intl.dart';
|
||||||
import '../../../core/widgets/premium_text_field.dart';
|
import '../../../core/widgets/premium_text_field.dart';
|
||||||
import '../../../core/widgets/smart_search_dropdown.dart';
|
import '../../../core/widgets/smart_search_dropdown.dart';
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.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:image_picker/image_picker.dart';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
@@ -897,7 +898,7 @@ class _PurchaseOrderBuilderScreenState
|
|||||||
context,
|
context,
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
builder: (_) => AttachmentGalleryScreen(
|
builder: (_) => AttachmentGalleryScreen(
|
||||||
images: [FileImage(File(_invoiceFile!.path))],
|
images: [kIsWeb ? NetworkImage(_invoiceFile!.path) : FileImage(File(_invoiceFile!.path))],
|
||||||
initialIndex: 0,
|
initialIndex: 0,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -1362,7 +1363,7 @@ class _POItemRowState extends State<_POItemRow> {
|
|||||||
context,
|
context,
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
builder: (_) => AttachmentGalleryScreen(
|
builder: (_) => AttachmentGalleryScreen(
|
||||||
images: [FileImage(File(widget.item.localPhotoPath!))],
|
images: [kIsWeb ? NetworkImage(widget.item.localPhotoPath!) : FileImage(File(widget.item.localPhotoPath!))],
|
||||||
initialIndex: 0,
|
initialIndex: 0,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -1396,7 +1397,9 @@ class _POItemRowState extends State<_POItemRow> {
|
|||||||
child: widget.item.localPhotoPath != null
|
child: widget.item.localPhotoPath != null
|
||||||
? ClipRRect(
|
? ClipRRect(
|
||||||
borderRadius: BorderRadius.circular(7),
|
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
|
: widget.item.photoUrl != null && widget.item.photoUrl!.isNotEmpty
|
||||||
? ClipRRect(
|
? ClipRRect(
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.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:intl/intl.dart';
|
||||||
import 'package:path_provider/path_provider.dart';
|
import 'package:path_provider/path_provider.dart';
|
||||||
import 'package:share_plus/share_plus.dart';
|
import 'package:share_plus/share_plus.dart';
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.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:intl/intl.dart';
|
||||||
import '../providers/purchase_orders_provider.dart';
|
import '../providers/purchase_orders_provider.dart';
|
||||||
import '../providers/vendors_provider.dart';
|
import '../providers/vendors_provider.dart';
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.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 '../providers/vendors_provider.dart';
|
||||||
import 'add_vendor_sheet.dart';
|
import 'add_vendor_sheet.dart';
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.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/premium_text_field.dart';
|
||||||
import '../../../../core/widgets/smart_search_dropdown.dart';
|
import '../../../../core/widgets/smart_search_dropdown.dart';
|
||||||
import '../../providers/vendors_provider.dart';
|
import '../../providers/vendors_provider.dart';
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|||||||
import '../../../core/network/dio_client.dart';
|
import '../../../core/network/dio_client.dart';
|
||||||
import '../domain/purchase_order.dart';
|
import '../domain/purchase_order.dart';
|
||||||
import '../domain/purchase_payment.dart';
|
import '../domain/purchase_payment.dart';
|
||||||
|
import '../../inventory/providers/products_provider.dart';
|
||||||
|
|
||||||
class PurchaseOrdersNotifier extends AsyncNotifier<List<PurchaseOrder>> {
|
class PurchaseOrdersNotifier extends AsyncNotifier<List<PurchaseOrder>> {
|
||||||
@override
|
@override
|
||||||
@@ -84,6 +85,7 @@ class PurchaseOrdersNotifier extends AsyncNotifier<List<PurchaseOrder>> {
|
|||||||
state = AsyncValue.data(
|
state = AsyncValue.data(
|
||||||
current.map((c) => c.id == updatedPo.id ? updatedPo : c).toList(),
|
current.map((c) => c.id == updatedPo.id ? updatedPo : c).toList(),
|
||||||
);
|
);
|
||||||
|
ref.invalidate(productsProvider);
|
||||||
return updatedPo;
|
return updatedPo;
|
||||||
}
|
}
|
||||||
throw Exception('Failed to receive purchase invoice');
|
throw Exception('Failed to receive purchase invoice');
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||||
@@ -132,7 +133,7 @@ class _KifiAppState extends ConsumerState<KifiApp> {
|
|||||||
? Scaffold(body: Center(child: Padding(padding: const EdgeInsets.all(20), child: Text("Startup Error: $_startupError", style: const TextStyle(color: Colors.red)))))
|
? Scaffold(body: Center(child: Padding(padding: const EdgeInsets.all(20), child: Text("Startup Error: $_startupError", style: const TextStyle(color: Colors.red)))))
|
||||||
: _isLoading
|
: _isLoading
|
||||||
? const Scaffold(body: Center(child: CircularProgressIndicator()))
|
? const Scaffold(body: Center(child: CircularProgressIndicator()))
|
||||||
: (!_hasSeenOnboarding
|
: ((!kIsWeb && !_hasSeenOnboarding)
|
||||||
? const OnboardingScreen()
|
? const OnboardingScreen()
|
||||||
: (_isAuthenticated
|
: (_isAuthenticated
|
||||||
? (_isSetupCompleted ? const DashboardScreen() : const SetupWizardScreen())
|
? (_isSetupCompleted ? const DashboardScreen() : const SetupWizardScreen())
|
||||||
|
|||||||
55
kifi-app/nginx.conf
Normal file
55
kifi-app/nginx.conf
Normal file
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -808,14 +808,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.3.0"
|
version: "1.3.0"
|
||||||
lucide_icons:
|
lucide_icons_flutter:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: lucide_icons
|
name: lucide_icons_flutter
|
||||||
sha256: ad24d0fd65707e48add30bebada7d90bff2a1bba0a72d6e9b19d44246b0e83c4
|
sha256: "6e2eb4a11137851be84a57e936fc864d565709ec0ae854b786b15397d1b66276"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.257.0"
|
version: "3.1.17"
|
||||||
matcher:
|
matcher:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|||||||
@@ -40,7 +40,6 @@ dependencies:
|
|||||||
fl_chart: ^1.2.0
|
fl_chart: ^1.2.0
|
||||||
google_fonts: ^8.2.1
|
google_fonts: ^8.2.1
|
||||||
intl: ^0.20.3
|
intl: ^0.20.3
|
||||||
lucide_icons: ^0.257.0
|
|
||||||
local_auth: ^3.0.2
|
local_auth: ^3.0.2
|
||||||
path_provider: ^2.1.6
|
path_provider: ^2.1.6
|
||||||
share_plus: ^13.3.0
|
share_plus: ^13.3.0
|
||||||
@@ -60,6 +59,7 @@ dependencies:
|
|||||||
file_picker: ^12.0.0
|
file_picker: ^12.0.0
|
||||||
intl_phone_field: ^3.2.0
|
intl_phone_field: ^3.2.0
|
||||||
url_launcher: ^6.3.2
|
url_launcher: ^6.3.2
|
||||||
|
lucide_icons_flutter: ^3.1.17
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
|||||||
@@ -1,38 +1,95 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<!--
|
|
||||||
If you are serving your web app in a path other than the root, change the
|
|
||||||
href value below to reflect the base path you are serving from.
|
|
||||||
|
|
||||||
The path provided below has to start and end with a slash "/" in order for
|
|
||||||
it to work correctly.
|
|
||||||
|
|
||||||
For more details:
|
|
||||||
* https://developer.mozilla.org/en-US/docs/Web/HTML/Element/base
|
|
||||||
|
|
||||||
This is a placeholder for base href that will be replaced by the value of
|
|
||||||
the `--base-href` argument provided to `flutter build`.
|
|
||||||
-->
|
|
||||||
<base href="$FLUTTER_BASE_HREF">
|
<base href="$FLUTTER_BASE_HREF">
|
||||||
|
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta content="IE=Edge" http-equiv="X-UA-Compatible">
|
<meta content="IE=Edge" http-equiv="X-UA-Compatible">
|
||||||
<meta name="description" content="A new Flutter project.">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
||||||
|
<meta name="description" content="KIFI - Modern Financial Ledger & Jewellery Business Suite">
|
||||||
|
|
||||||
<!-- iOS meta tags & icons -->
|
<!-- iOS meta tags & icons -->
|
||||||
<meta name="mobile-web-app-capable" content="yes">
|
<meta name="mobile-web-app-capable" content="yes">
|
||||||
<meta name="apple-mobile-web-app-status-bar-style" content="black">
|
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
||||||
<meta name="apple-mobile-web-app-title" content="kifi_app">
|
<meta name="apple-mobile-web-app-title" content="KIFI">
|
||||||
<link rel="apple-touch-icon" href="icons/Icon-192.png">
|
<link rel="apple-touch-icon" href="icons/Icon-192.png">
|
||||||
|
|
||||||
<!-- Favicon -->
|
<!-- Favicon -->
|
||||||
<link rel="icon" type="image/png" href="favicon.png"/>
|
<link rel="icon" type="image/png" href="favicon.png"/>
|
||||||
|
|
||||||
<title>kifi_app</title>
|
<title>KIFI - Financial Ledger & Jewellery ERP</title>
|
||||||
<link rel="manifest" href="manifest.json">
|
<link rel="manifest" href="manifest.json">
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
background-color: #0b0e14;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
height: 100vh;
|
||||||
|
width: 100vw;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.splash-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
.splash-logo {
|
||||||
|
width: 64px;
|
||||||
|
height: 64px;
|
||||||
|
border-radius: 16px;
|
||||||
|
background: linear-gradient(135deg, #D4AF37 0%, #AA771C 100%);
|
||||||
|
box-shadow: 0 8px 32px rgba(212, 175, 55, 0.35);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: #ffffff;
|
||||||
|
font-size: 28px;
|
||||||
|
font-weight: bold;
|
||||||
|
animation: pulse 2s infinite ease-in-out;
|
||||||
|
}
|
||||||
|
.splash-title {
|
||||||
|
color: #ffffff;
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 800;
|
||||||
|
letter-spacing: 2px;
|
||||||
|
margin-top: 18px;
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
.splash-sub {
|
||||||
|
color: #8892b0;
|
||||||
|
font-size: 12px;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
margin-top: 6px;
|
||||||
|
}
|
||||||
|
.spinner {
|
||||||
|
margin-top: 28px;
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
border: 3px solid rgba(212, 175, 55, 0.2);
|
||||||
|
border-radius: 50%;
|
||||||
|
border-top-color: #D4AF37;
|
||||||
|
animation: spin 0.8s linear infinite;
|
||||||
|
}
|
||||||
|
@keyframes spin {
|
||||||
|
to { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
@keyframes pulse {
|
||||||
|
0%, 100% { transform: scale(1); }
|
||||||
|
50% { transform: scale(1.05); }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
<div class="splash-container">
|
||||||
|
<div class="splash-logo">💎</div>
|
||||||
|
<div class="splash-title">KIFI</div>
|
||||||
|
<div class="splash-sub">Financial Ledger & Jewellery ERP</div>
|
||||||
|
<div class="spinner"></div>
|
||||||
|
</div>
|
||||||
<script src="flutter_bootstrap.js" async></script>
|
<script src="flutter_bootstrap.js" async></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
Reference in New Issue
Block a user