Revamp Done - Support for Individual and Jwellery module added
This commit is contained in:
@@ -34,6 +34,7 @@ public class ProductCategoryController {
|
|||||||
public Mono<ResponseEntity<ProductCategory>> createCategory(Authentication authentication, @RequestBody ProductCategory category) {
|
public Mono<ResponseEntity<ProductCategory>> createCategory(Authentication authentication, @RequestBody ProductCategory category) {
|
||||||
Long userId = Long.valueOf(authentication.getDetails().toString());
|
Long userId = Long.valueOf(authentication.getDetails().toString());
|
||||||
category.setUserId(userId);
|
category.setUserId(userId);
|
||||||
|
category.setPurityFactor(normalizePurity(category.getPurityFactor()));
|
||||||
category.setCreatedAt(LocalDateTime.now());
|
category.setCreatedAt(LocalDateTime.now());
|
||||||
return categoryRepository.save(category)
|
return categoryRepository.save(category)
|
||||||
.map(ResponseEntity::ok);
|
.map(ResponseEntity::ok);
|
||||||
@@ -47,7 +48,7 @@ public class ProductCategoryController {
|
|||||||
existing.setParentCategoryId(category.getParentCategoryId());
|
existing.setParentCategoryId(category.getParentCategoryId());
|
||||||
existing.setHasChild(category.getHasChild());
|
existing.setHasChild(category.getHasChild());
|
||||||
existing.setCommodityCode(category.getCommodityCode());
|
existing.setCommodityCode(category.getCommodityCode());
|
||||||
existing.setPurityFactor(category.getPurityFactor() != null ? category.getPurityFactor() : 1.0);
|
existing.setPurityFactor(normalizePurity(category.getPurityFactor()));
|
||||||
existing.setDefaultHsn(category.getDefaultHsn());
|
existing.setDefaultHsn(category.getDefaultHsn());
|
||||||
existing.setDefaultGst(category.getDefaultGst());
|
existing.setDefaultGst(category.getDefaultGst());
|
||||||
existing.setHuidRequired(category.getHuidRequired());
|
existing.setHuidRequired(category.getHuidRequired());
|
||||||
@@ -63,4 +64,12 @@ public class ProductCategoryController {
|
|||||||
.defaultIfEmpty(ResponseEntity.notFound().build());
|
.defaultIfEmpty(ResponseEntity.notFound().build());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private Double normalizePurity(Double rawPurity) {
|
||||||
|
if (rawPurity == null || rawPurity <= 0) return 1.0;
|
||||||
|
double p = rawPurity;
|
||||||
|
if (p > 1.0) p = p / 100.0;
|
||||||
|
if (p > 1.0) p = 1.0;
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,6 +43,15 @@ public class Invoice {
|
|||||||
@Column("tax_total")
|
@Column("tax_total")
|
||||||
private BigDecimal taxTotal;
|
private BigDecimal taxTotal;
|
||||||
|
|
||||||
|
@Column("cgst_total")
|
||||||
|
private BigDecimal cgstTotal;
|
||||||
|
|
||||||
|
@Column("sgst_total")
|
||||||
|
private BigDecimal sgstTotal;
|
||||||
|
|
||||||
|
@Column("igst_total")
|
||||||
|
private BigDecimal igstTotal;
|
||||||
|
|
||||||
@Column("discount_total")
|
@Column("discount_total")
|
||||||
private BigDecimal discountTotal;
|
private BigDecimal discountTotal;
|
||||||
|
|
||||||
|
|||||||
@@ -46,8 +46,20 @@ public class InvoiceItem {
|
|||||||
@Column("making_charge")
|
@Column("making_charge")
|
||||||
private BigDecimal makingCharge;
|
private BigDecimal makingCharge;
|
||||||
|
|
||||||
|
@Column("making_charges_type")
|
||||||
|
private String makingChargesType;
|
||||||
|
|
||||||
@Column("other_charges")
|
@Column("other_charges")
|
||||||
private BigDecimal otherCharges;
|
private BigDecimal otherCharges;
|
||||||
|
|
||||||
|
private String huid;
|
||||||
|
private BigDecimal weight;
|
||||||
|
private BigDecimal cgst;
|
||||||
|
private BigDecimal sgst;
|
||||||
|
private BigDecimal igst;
|
||||||
|
|
||||||
|
@Column("photo_url")
|
||||||
|
private String photoUrl;
|
||||||
|
|
||||||
private BigDecimal total;
|
private BigDecimal total;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,6 +28,8 @@ public class SetupService {
|
|||||||
|
|
||||||
public Mono<java.util.Map<String, String>> getSetupStatus(Long userId) {
|
public Mono<java.util.Map<String, String>> getSetupStatus(Long userId) {
|
||||||
return userRepository.findById(userId)
|
return userRepository.findById(userId)
|
||||||
|
.switchIfEmpty(Mono.error(new org.springframework.web.server.ResponseStatusException(
|
||||||
|
org.springframework.http.HttpStatus.UNAUTHORIZED, "User does not exist")))
|
||||||
.map(u -> {
|
.map(u -> {
|
||||||
java.util.Map<String, String> map = new java.util.HashMap<>();
|
java.util.Map<String, String> map = new java.util.HashMap<>();
|
||||||
map.put("status", u.getSetupStatus() != null ? u.getSetupStatus() : "NOT_STARTED");
|
map.put("status", u.getSetupStatus() != null ? u.getSetupStatus() : "NOT_STARTED");
|
||||||
@@ -35,8 +37,7 @@ public class SetupService {
|
|||||||
map.put("name", u.getName() != null ? u.getName() : "");
|
map.put("name", u.getName() != null ? u.getName() : "");
|
||||||
map.put("email", u.getEmail() != null ? u.getEmail() : "");
|
map.put("email", u.getEmail() != null ? u.getEmail() : "");
|
||||||
return map;
|
return map;
|
||||||
})
|
});
|
||||||
.defaultIfEmpty(java.util.Map.of("status", "NOT_STARTED", "profileType", "INDIVIDUAL"));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public Mono<Boolean> isUsernameAvailable(String username) {
|
public Mono<Boolean> isUsernameAvailable(String username) {
|
||||||
|
|||||||
@@ -54,10 +54,10 @@ public class CommodityRateService {
|
|||||||
.flatMap(latestRate ->
|
.flatMap(latestRate ->
|
||||||
productCategoryRepository.findByUserIdAndCommodityCode(userId, commodityCode)
|
productCategoryRepository.findByUserIdAndCommodityCode(userId, commodityCode)
|
||||||
.flatMap(category -> {
|
.flatMap(category -> {
|
||||||
double categoryPurity = category.getPurityFactor() != null ? category.getPurityFactor() : 1.0;
|
final double categoryPurity = normalizePurity(category.getPurityFactor());
|
||||||
return productRepository.findByCategoryId(category.getId())
|
return productRepository.findByCategoryId(category.getId())
|
||||||
.flatMap(product -> {
|
.flatMap(product -> {
|
||||||
double productPurity = product.getPurityFactor() != null ? product.getPurityFactor() : categoryPurity;
|
double productPurity = normalizePurity(product.getPurityFactor() != null ? product.getPurityFactor() : categoryPurity);
|
||||||
BigDecimal baseRate = latestRate.getRate().multiply(BigDecimal.valueOf(productPurity));
|
BigDecimal baseRate = latestRate.getRate().multiply(BigDecimal.valueOf(productPurity));
|
||||||
|
|
||||||
if ("WEIGHT_BASED".equalsIgnoreCase(product.getPriceCalcRule())) {
|
if ("WEIGHT_BASED".equalsIgnoreCase(product.getPriceCalcRule())) {
|
||||||
@@ -79,4 +79,12 @@ public class CommodityRateService {
|
|||||||
)
|
)
|
||||||
.defaultIfEmpty(0);
|
.defaultIfEmpty(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private Double normalizePurity(Double rawPurity) {
|
||||||
|
if (rawPurity == null || rawPurity <= 0) return 1.0;
|
||||||
|
double p = rawPurity;
|
||||||
|
if (p > 1.0) p = p / 100.0;
|
||||||
|
if (p > 1.0) p = 1.0;
|
||||||
|
return p;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -68,7 +68,9 @@ public class ProductService {
|
|||||||
product.setUpdatedAt(LocalDateTime.now());
|
product.setUpdatedAt(LocalDateTime.now());
|
||||||
if (product.getIsActive() == null) product.setIsActive(true);
|
if (product.getIsActive() == null) product.setIsActive(true);
|
||||||
if (product.getPriceCalcRule() == null) product.setPriceCalcRule("MANUAL");
|
if (product.getPriceCalcRule() == null) product.setPriceCalcRule("MANUAL");
|
||||||
if (product.getPurityFactor() == null) product.setPurityFactor(1.0);
|
product.setPurityFactor(normalizePurity(product.getPurityFactor()));
|
||||||
|
if (product.getMakingCharges() == null) product.setMakingCharges(0.0);
|
||||||
|
if (product.getMakingChargesType() == null) product.setMakingChargesType("PER_GRAM");
|
||||||
return productRepository.save(product);
|
return productRepository.save(product);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -87,7 +89,7 @@ public class ProductService {
|
|||||||
existingProduct.setSize(updatedProduct.getSize());
|
existingProduct.setSize(updatedProduct.getSize());
|
||||||
existingProduct.setDimensions(updatedProduct.getDimensions());
|
existingProduct.setDimensions(updatedProduct.getDimensions());
|
||||||
existingProduct.setPriceCalcRule(updatedProduct.getPriceCalcRule());
|
existingProduct.setPriceCalcRule(updatedProduct.getPriceCalcRule());
|
||||||
existingProduct.setPurityFactor(updatedProduct.getPurityFactor());
|
existingProduct.setPurityFactor(normalizePurity(updatedProduct.getPurityFactor()));
|
||||||
existingProduct.setMakingCharges(updatedProduct.getMakingCharges());
|
existingProduct.setMakingCharges(updatedProduct.getMakingCharges());
|
||||||
existingProduct.setMakingChargesType(updatedProduct.getMakingChargesType());
|
existingProduct.setMakingChargesType(updatedProduct.getMakingChargesType());
|
||||||
existingProduct.setWastagePercentage(updatedProduct.getWastagePercentage());
|
existingProduct.setWastagePercentage(updatedProduct.getWastagePercentage());
|
||||||
@@ -288,4 +290,13 @@ public class ProductService {
|
|||||||
.switchIfEmpty(Mono.error(new RuntimeException("Access denied")))
|
.switchIfEmpty(Mono.error(new RuntimeException("Access denied")))
|
||||||
.flatMap(p -> productBomRepository.delete(bom)));
|
.flatMap(p -> productBomRepository.delete(bom)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private Double normalizePurity(Double rawPurity) {
|
||||||
|
if (rawPurity == null) return null;
|
||||||
|
if (rawPurity <= 0) return 1.0;
|
||||||
|
double p = rawPurity;
|
||||||
|
if (p > 1.0) p = p / 100.0;
|
||||||
|
if (p > 1.0) p = 1.0;
|
||||||
|
return p;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,169 +1,43 @@
|
|||||||
|
-- ==========================================================
|
||||||
|
-- KIFI PLATFORM CONSOLIDATED DATABASE SCHEMA
|
||||||
|
-- ==========================================================
|
||||||
|
|
||||||
|
-- 1. Core Users Table
|
||||||
CREATE TABLE IF NOT EXISTS users (
|
CREATE TABLE IF NOT EXISTS users (
|
||||||
id SERIAL PRIMARY KEY,
|
id SERIAL PRIMARY KEY,
|
||||||
email VARCHAR(255) UNIQUE NOT NULL,
|
email VARCHAR(255) UNIQUE NOT NULL,
|
||||||
password VARCHAR(255),
|
password VARCHAR(255),
|
||||||
|
username VARCHAR(255) UNIQUE,
|
||||||
|
name VARCHAR(255),
|
||||||
|
mobile_number VARCHAR(255),
|
||||||
|
profile_type VARCHAR(20) DEFAULT 'INDIVIDUAL',
|
||||||
|
setup_status VARCHAR(50) DEFAULT 'NOT_STARTED',
|
||||||
|
setup_completed_at TIMESTAMP,
|
||||||
enabled BOOLEAN DEFAULT FALSE,
|
enabled BOOLEAN DEFAULT FALSE,
|
||||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS merchants (
|
-- 2. User Consents
|
||||||
id SERIAL PRIMARY KEY,
|
CREATE TABLE IF NOT EXISTS user_consents (
|
||||||
user_id INTEGER REFERENCES users(id),
|
|
||||||
name VARCHAR(255) NOT NULL,
|
|
||||||
icon_name VARCHAR(255),
|
|
||||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS categories (
|
|
||||||
id SERIAL PRIMARY KEY,
|
|
||||||
user_id INTEGER REFERENCES users(id),
|
|
||||||
name VARCHAR(255) NOT NULL,
|
|
||||||
icon_name VARCHAR(255),
|
|
||||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS transactions (
|
|
||||||
id SERIAL PRIMARY KEY,
|
|
||||||
user_id INTEGER REFERENCES users(id),
|
|
||||||
category_id INTEGER REFERENCES categories(id),
|
|
||||||
type VARCHAR(50) NOT NULL, -- 'INCOME' or 'EXPENSE' or 'INVESTMENT'
|
|
||||||
amount DECIMAL(10, 2) NOT NULL,
|
|
||||||
date DATE NOT NULL,
|
|
||||||
description TEXT,
|
|
||||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS budgets (
|
|
||||||
id SERIAL PRIMARY KEY,
|
|
||||||
user_id INTEGER REFERENCES users(id),
|
|
||||||
category_id INTEGER REFERENCES categories(id),
|
|
||||||
monthly_limit DECIMAL(10, 2) NOT NULL,
|
|
||||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
UNIQUE(user_id, category_id)
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS recurring_transactions (
|
|
||||||
id SERIAL PRIMARY KEY,
|
|
||||||
user_id INTEGER REFERENCES users(id),
|
|
||||||
category_id INTEGER REFERENCES categories(id),
|
|
||||||
type VARCHAR(50) NOT NULL,
|
|
||||||
amount DECIMAL(10, 2) NOT NULL,
|
|
||||||
frequency VARCHAR(50) NOT NULL,
|
|
||||||
next_execution_date DATE NOT NULL,
|
|
||||||
description TEXT,
|
|
||||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS wallets (
|
|
||||||
id SERIAL PRIMARY KEY,
|
|
||||||
name VARCHAR(255) NOT NULL,
|
|
||||||
owner_id INTEGER REFERENCES users(id),
|
|
||||||
nature VARCHAR(50) DEFAULT 'CASH', -- 'CASH', 'DEPOSIT', 'EXPENSE', 'INCOME', 'SAVINGS', 'LOAN', etc.
|
|
||||||
balance DECIMAL(15, 2) DEFAULT 0.00,
|
|
||||||
currency VARCHAR(10) DEFAULT 'INR',
|
|
||||||
icon VARCHAR(255),
|
|
||||||
color VARCHAR(50),
|
|
||||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS user_wallets (
|
|
||||||
id SERIAL PRIMARY KEY,
|
|
||||||
user_id INTEGER REFERENCES users(id),
|
|
||||||
wallet_id INTEGER REFERENCES wallets(id),
|
|
||||||
role VARCHAR(50) DEFAULT 'MEMBER',
|
|
||||||
joined_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
UNIQUE (user_id, wallet_id)
|
|
||||||
);
|
|
||||||
|
|
||||||
ALTER TABLE wallets ADD COLUMN IF NOT EXISTS nature VARCHAR(50) DEFAULT 'CASH';
|
|
||||||
ALTER TABLE wallets ADD COLUMN IF NOT EXISTS balance DECIMAL(15, 2) DEFAULT 0.00;
|
|
||||||
ALTER TABLE wallets ADD COLUMN IF NOT EXISTS currency VARCHAR(10) DEFAULT 'INR';
|
|
||||||
ALTER TABLE wallets ADD COLUMN IF NOT EXISTS icon VARCHAR(255);
|
|
||||||
ALTER TABLE wallets ADD COLUMN IF NOT EXISTS color VARCHAR(50);
|
|
||||||
|
|
||||||
ALTER TABLE transactions ADD COLUMN IF NOT EXISTS wallet_id INTEGER REFERENCES wallets(id);
|
|
||||||
ALTER TABLE transactions ADD COLUMN IF NOT EXISTS from_wallet_id INTEGER REFERENCES wallets(id);
|
|
||||||
ALTER TABLE transactions ADD COLUMN IF NOT EXISTS to_wallet_id INTEGER REFERENCES wallets(id);
|
|
||||||
ALTER TABLE transactions ADD COLUMN IF NOT EXISTS notes TEXT;
|
|
||||||
ALTER TABLE transactions ADD COLUMN IF NOT EXISTS due_date DATE;
|
|
||||||
ALTER TABLE transactions ADD COLUMN IF NOT EXISTS alert_schedule VARCHAR(50);
|
|
||||||
ALTER TABLE transactions ADD COLUMN IF NOT EXISTS alert_time TIME;
|
|
||||||
|
|
||||||
ALTER TABLE transactions ADD COLUMN IF NOT EXISTS investment_status VARCHAR(20) DEFAULT 'OPEN';
|
|
||||||
ALTER TABLE transactions ADD COLUMN IF NOT EXISTS maturity_amount DECIMAL(10, 2);
|
|
||||||
ALTER TABLE transactions ADD COLUMN IF NOT EXISTS profit_loss DECIMAL(10, 2);
|
|
||||||
ALTER TABLE transactions ADD COLUMN IF NOT EXISTS closing_date DATE;
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS transaction_items (
|
|
||||||
id SERIAL PRIMARY KEY,
|
|
||||||
transaction_id INTEGER REFERENCES transactions(id) ON DELETE CASCADE,
|
|
||||||
name VARCHAR(255) NOT NULL,
|
|
||||||
amount DECIMAL(10, 2) NOT NULL,
|
|
||||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS transaction_attachments (
|
|
||||||
id SERIAL PRIMARY KEY,
|
|
||||||
transaction_id INTEGER REFERENCES transactions(id) ON DELETE CASCADE,
|
|
||||||
file_name VARCHAR(255) NOT NULL,
|
|
||||||
file_path VARCHAR(1024) NOT NULL,
|
|
||||||
content_type VARCHAR(100),
|
|
||||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
||||||
);
|
|
||||||
|
|
||||||
ALTER TABLE budgets ADD COLUMN IF NOT EXISTS wallet_id INTEGER REFERENCES wallets(id);
|
|
||||||
ALTER TABLE budgets ADD COLUMN IF NOT EXISTS is_shared BOOLEAN DEFAULT FALSE;
|
|
||||||
-- Ensure budget can be either category-specific or wallet-specific
|
|
||||||
ALTER TABLE budgets ALTER COLUMN category_id DROP NOT NULL;
|
|
||||||
|
|
||||||
ALTER TABLE recurring_transactions ADD COLUMN IF NOT EXISTS from_wallet_id INTEGER REFERENCES wallets(id);
|
|
||||||
ALTER TABLE recurring_transactions ADD COLUMN IF NOT EXISTS to_wallet_id INTEGER REFERENCES wallets(id);
|
|
||||||
ALTER TABLE recurring_transactions ADD COLUMN IF NOT EXISTS status VARCHAR(20) DEFAULT 'ACTIVE';
|
|
||||||
ALTER TABLE recurring_transactions ADD COLUMN IF NOT EXISTS end_date DATE;
|
|
||||||
ALTER TABLE recurring_transactions ALTER COLUMN category_id DROP NOT NULL;
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS wallet_invitations (
|
|
||||||
id SERIAL PRIMARY KEY,
|
|
||||||
wallet_id INTEGER REFERENCES wallets(id),
|
|
||||||
inviter_id INTEGER REFERENCES users(id),
|
|
||||||
invitee_email VARCHAR(255) NOT NULL,
|
|
||||||
status VARCHAR(20) DEFAULT 'PENDING',
|
|
||||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
||||||
);
|
|
||||||
|
|
||||||
-- KIFI V2 PHASE 1 MIGRATION: INVENTORY MANAGEMENT --
|
|
||||||
|
|
||||||
ALTER TABLE users ADD COLUMN IF NOT EXISTS profile_type VARCHAR(20) DEFAULT 'INDIVIDUAL';
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS business_profiles (
|
|
||||||
id SERIAL PRIMARY KEY,
|
id SERIAL PRIMARY KEY,
|
||||||
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
|
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
|
||||||
business_name VARCHAR(255) NOT NULL,
|
consent_type VARCHAR(100) NOT NULL,
|
||||||
industry VARCHAR(100),
|
policy_version VARCHAR(50) NOT NULL,
|
||||||
tax_number VARCHAR(100),
|
accepted BOOLEAN DEFAULT TRUE,
|
||||||
currency VARCHAR(10) DEFAULT 'INR',
|
accepted_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
tax_included_in_price BOOLEAN DEFAULT FALSE,
|
ip_address VARCHAR(100),
|
||||||
address TEXT,
|
user_agent TEXT,
|
||||||
state_id INTEGER,
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
contact_person VARCHAR(100),
|
|
||||||
contact_number VARCHAR(20),
|
|
||||||
email_id VARCHAR(100),
|
|
||||||
pan_number VARCHAR(255),
|
|
||||||
gstin VARCHAR(255),
|
|
||||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
UNIQUE(user_id)
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
-- 3. Business Profiles & Features
|
||||||
CREATE TABLE IF NOT EXISTS indian_states (
|
CREATE TABLE IF NOT EXISTS indian_states (
|
||||||
id SERIAL PRIMARY KEY,
|
id SERIAL PRIMARY KEY,
|
||||||
name VARCHAR(100) NOT NULL,
|
name VARCHAR(100) NOT NULL,
|
||||||
gst_code VARCHAR(2) NOT NULL
|
gst_code VARCHAR(2) NOT NULL
|
||||||
);
|
);
|
||||||
|
|
||||||
-- Seed Indian States if empty
|
|
||||||
INSERT INTO indian_states (name, gst_code) VALUES
|
INSERT INTO indian_states (name, gst_code) VALUES
|
||||||
('Jammu & Kashmir', '01'), ('Himachal Pradesh', '02'), ('Punjab', '03'),
|
('Jammu & Kashmir', '01'), ('Himachal Pradesh', '02'), ('Punjab', '03'),
|
||||||
('Chandigarh', '04'), ('Uttarakhand', '05'), ('Haryana', '06'),
|
('Chandigarh', '04'), ('Uttarakhand', '05'), ('Haryana', '06'),
|
||||||
@@ -180,6 +54,28 @@ INSERT INTO indian_states (name, gst_code) VALUES
|
|||||||
('Telangana', '36'), ('Andhra Pradesh', '37'), ('Ladakh', '38')
|
('Telangana', '36'), ('Andhra Pradesh', '37'), ('Ladakh', '38')
|
||||||
ON CONFLICT DO NOTHING;
|
ON CONFLICT DO NOTHING;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS business_profiles (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
|
||||||
|
business_name VARCHAR(255) NOT NULL,
|
||||||
|
industry VARCHAR(100),
|
||||||
|
tax_number VARCHAR(100),
|
||||||
|
currency VARCHAR(10) DEFAULT 'INR',
|
||||||
|
tax_included_in_price BOOLEAN DEFAULT FALSE,
|
||||||
|
address TEXT,
|
||||||
|
state_id INTEGER REFERENCES indian_states(id),
|
||||||
|
contact_person VARCHAR(100),
|
||||||
|
contact_number VARCHAR(20),
|
||||||
|
email_id VARCHAR(100),
|
||||||
|
pan_number VARCHAR(255),
|
||||||
|
gstin VARCHAR(255),
|
||||||
|
nature_of_business VARCHAR(50),
|
||||||
|
msme_number VARCHAR(255),
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
UNIQUE(user_id)
|
||||||
|
);
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS business_features (
|
CREATE TABLE IF NOT EXISTS business_features (
|
||||||
id SERIAL PRIMARY KEY,
|
id SERIAL PRIMARY KEY,
|
||||||
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
|
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
|
||||||
@@ -187,20 +83,128 @@ CREATE TABLE IF NOT EXISTS business_features (
|
|||||||
sales_management BOOLEAN DEFAULT FALSE,
|
sales_management BOOLEAN DEFAULT FALSE,
|
||||||
multi_location BOOLEAN DEFAULT FALSE,
|
multi_location BOOLEAN DEFAULT FALSE,
|
||||||
bom_reduction_strategy VARCHAR(50) DEFAULT 'COMPONENTS_ONLY',
|
bom_reduction_strategy VARCHAR(50) DEFAULT 'COMPONENTS_ONLY',
|
||||||
|
stock_deduction_on_invoice BOOLEAN DEFAULT TRUE,
|
||||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
UNIQUE(user_id)
|
UNIQUE(user_id)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
-- 4. Personal Finance: Wallets, Categories, Transactions & Budgets
|
||||||
|
CREATE TABLE IF NOT EXISTS wallets (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
name VARCHAR(255) NOT NULL,
|
||||||
|
owner_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
|
||||||
|
nature VARCHAR(50) DEFAULT 'CASH', -- 'CASH', 'DEPOSIT', 'EXPENSE', 'INCOME', 'SAVINGS', 'LOAN', etc.
|
||||||
|
balance DECIMAL(15, 2) DEFAULT 0.00,
|
||||||
|
currency VARCHAR(10) DEFAULT 'INR',
|
||||||
|
icon VARCHAR(255),
|
||||||
|
color VARCHAR(50),
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS user_wallets (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
|
||||||
|
wallet_id INTEGER REFERENCES wallets(id) ON DELETE CASCADE,
|
||||||
|
role VARCHAR(50) DEFAULT 'MEMBER',
|
||||||
|
joined_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
UNIQUE (user_id, wallet_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS wallet_invitations (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
wallet_id INTEGER REFERENCES wallets(id) ON DELETE CASCADE,
|
||||||
|
inviter_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
|
||||||
|
invitee_email VARCHAR(255) NOT NULL,
|
||||||
|
status VARCHAR(20) DEFAULT 'PENDING',
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS categories (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
|
||||||
|
name VARCHAR(255) NOT NULL,
|
||||||
|
icon_name VARCHAR(255),
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS transactions (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
|
||||||
|
category_id INTEGER REFERENCES categories(id) ON DELETE SET NULL,
|
||||||
|
wallet_id INTEGER REFERENCES wallets(id) ON DELETE SET NULL,
|
||||||
|
from_wallet_id INTEGER REFERENCES wallets(id) ON DELETE SET NULL,
|
||||||
|
to_wallet_id INTEGER REFERENCES wallets(id) ON DELETE SET NULL,
|
||||||
|
type VARCHAR(50) NOT NULL, -- 'INCOME', 'EXPENSE', 'INVESTMENT', 'TRANSFER'
|
||||||
|
amount DECIMAL(15, 2) NOT NULL,
|
||||||
|
date DATE NOT NULL,
|
||||||
|
description TEXT,
|
||||||
|
notes TEXT,
|
||||||
|
due_date DATE,
|
||||||
|
alert_schedule VARCHAR(50),
|
||||||
|
alert_time TIME,
|
||||||
|
investment_status VARCHAR(20) DEFAULT 'OPEN',
|
||||||
|
maturity_amount DECIMAL(15, 2),
|
||||||
|
profit_loss DECIMAL(15, 2),
|
||||||
|
closing_date DATE,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS transaction_items (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
transaction_id INTEGER REFERENCES transactions(id) ON DELETE CASCADE,
|
||||||
|
name VARCHAR(255) NOT NULL,
|
||||||
|
amount DECIMAL(15, 2) NOT NULL,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS transaction_attachments (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
transaction_id INTEGER REFERENCES transactions(id) ON DELETE CASCADE,
|
||||||
|
file_name VARCHAR(255) NOT NULL,
|
||||||
|
file_path VARCHAR(1024) NOT NULL,
|
||||||
|
content_type VARCHAR(100),
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS budgets (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
|
||||||
|
category_id INTEGER REFERENCES categories(id) ON DELETE SET NULL,
|
||||||
|
wallet_id INTEGER REFERENCES wallets(id) ON DELETE SET NULL,
|
||||||
|
is_shared BOOLEAN DEFAULT FALSE,
|
||||||
|
monthly_limit DECIMAL(15, 2) NOT NULL,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
UNIQUE(user_id, category_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS recurring_transactions (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
|
||||||
|
category_id INTEGER REFERENCES categories(id) ON DELETE SET NULL,
|
||||||
|
from_wallet_id INTEGER REFERENCES wallets(id) ON DELETE SET NULL,
|
||||||
|
to_wallet_id INTEGER REFERENCES wallets(id) ON DELETE SET NULL,
|
||||||
|
type VARCHAR(50) NOT NULL,
|
||||||
|
amount DECIMAL(15, 2) NOT NULL,
|
||||||
|
frequency VARCHAR(50) NOT NULL,
|
||||||
|
next_execution_date DATE NOT NULL,
|
||||||
|
end_date DATE,
|
||||||
|
status VARCHAR(20) DEFAULT 'ACTIVE',
|
||||||
|
description TEXT,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
-- 5. Inventory: Categories, Rates, Products, BOM, Locations
|
||||||
CREATE TABLE IF NOT EXISTS product_categories (
|
CREATE TABLE IF NOT EXISTS product_categories (
|
||||||
id SERIAL PRIMARY KEY,
|
id SERIAL PRIMARY KEY,
|
||||||
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
|
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
|
||||||
name VARCHAR(255) NOT NULL,
|
name VARCHAR(255) NOT NULL,
|
||||||
parent_category_id INTEGER REFERENCES product_categories(id),
|
parent_category_id INTEGER REFERENCES product_categories(id) ON DELETE CASCADE,
|
||||||
has_child BOOLEAN DEFAULT FALSE,
|
has_child BOOLEAN DEFAULT FALSE,
|
||||||
default_hsn VARCHAR(50),
|
default_hsn VARCHAR(50),
|
||||||
default_gst DECIMAL(5, 2),
|
default_gst DECIMAL(5, 2),
|
||||||
huid_required BOOLEAN DEFAULT FALSE,
|
huid_required BOOLEAN DEFAULT FALSE,
|
||||||
default_making_charge DECIMAL(15, 2),
|
default_making_charge DECIMAL(15, 2),
|
||||||
|
making_charge_type VARCHAR(50),
|
||||||
commodity_code VARCHAR(10),
|
commodity_code VARCHAR(10),
|
||||||
purity_factor DECIMAL(5, 4) DEFAULT 1.0,
|
purity_factor DECIMAL(5, 4) DEFAULT 1.0,
|
||||||
base_unit VARCHAR(20) DEFAULT 'pcs',
|
base_unit VARCHAR(20) DEFAULT 'pcs',
|
||||||
@@ -217,38 +221,10 @@ CREATE TABLE IF NOT EXISTS commodity_rate_history (
|
|||||||
rate DECIMAL(15, 2) NOT NULL,
|
rate DECIMAL(15, 2) NOT NULL,
|
||||||
effective_at TIMESTAMP NOT NULL,
|
effective_at TIMESTAMP NOT NULL,
|
||||||
source VARCHAR(100),
|
source VARCHAR(100),
|
||||||
created_by INTEGER REFERENCES users(id),
|
created_by INTEGER REFERENCES users(id) ON DELETE SET NULL,
|
||||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS inventory_items (
|
|
||||||
id SERIAL PRIMARY KEY,
|
|
||||||
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
|
|
||||||
product_id INTEGER REFERENCES products(id),
|
|
||||||
tag_number VARCHAR(100),
|
|
||||||
sku VARCHAR(100),
|
|
||||||
huid VARCHAR(50),
|
|
||||||
purity VARCHAR(50),
|
|
||||||
gross_weight DECIMAL(10, 3),
|
|
||||||
net_weight DECIMAL(10, 3),
|
|
||||||
stone_weight DECIMAL(10, 3),
|
|
||||||
diamond_weight DECIMAL(10, 3),
|
|
||||||
fine_weight DECIMAL(10, 3),
|
|
||||||
making_charges DECIMAL(15, 2),
|
|
||||||
making_charge_type VARCHAR(50),
|
|
||||||
purchase_cost DECIMAL(15, 2),
|
|
||||||
metal_cost DECIMAL(15, 2),
|
|
||||||
stone_cost DECIMAL(15, 2),
|
|
||||||
certification_cost DECIMAL(15, 2),
|
|
||||||
tax DECIMAL(15, 2),
|
|
||||||
vendor_id INTEGER, -- REFERENCES vendors(id)
|
|
||||||
branch_id INTEGER,
|
|
||||||
purchase_ref VARCHAR(100),
|
|
||||||
status VARCHAR(50) DEFAULT 'AVAILABLE',
|
|
||||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS units_of_measure (
|
CREATE TABLE IF NOT EXISTS units_of_measure (
|
||||||
id SERIAL PRIMARY KEY,
|
id SERIAL PRIMARY KEY,
|
||||||
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
|
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
|
||||||
@@ -260,8 +236,8 @@ CREATE TABLE IF NOT EXISTS units_of_measure (
|
|||||||
CREATE TABLE IF NOT EXISTS products (
|
CREATE TABLE IF NOT EXISTS products (
|
||||||
id SERIAL PRIMARY KEY,
|
id SERIAL PRIMARY KEY,
|
||||||
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
|
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
|
||||||
category_id INTEGER REFERENCES product_categories(id),
|
category_id INTEGER REFERENCES product_categories(id) ON DELETE SET NULL,
|
||||||
uom_id INTEGER REFERENCES units_of_measure(id),
|
uom_id INTEGER REFERENCES units_of_measure(id) ON DELETE SET NULL,
|
||||||
name VARCHAR(255) NOT NULL,
|
name VARCHAR(255) NOT NULL,
|
||||||
sku VARCHAR(100),
|
sku VARCHAR(100),
|
||||||
barcode VARCHAR(100),
|
barcode VARCHAR(100),
|
||||||
@@ -274,9 +250,9 @@ CREATE TABLE IF NOT EXISTS products (
|
|||||||
color VARCHAR(50),
|
color VARCHAR(50),
|
||||||
size VARCHAR(50),
|
size VARCHAR(50),
|
||||||
price_calc_rule VARCHAR(50) DEFAULT 'MANUAL',
|
price_calc_rule VARCHAR(50) DEFAULT 'MANUAL',
|
||||||
purity_factor DECIMAL(5, 4) DEFAULT 1.0,
|
purity_factor DECIMAL(5, 4),
|
||||||
making_charges DECIMAL(15, 2) DEFAULT 0.0,
|
making_charges DECIMAL(15, 2) DEFAULT 0.0,
|
||||||
making_charges_type VARCHAR(50) DEFAULT 'FLAT',
|
making_charges_type VARCHAR(50) DEFAULT 'PER_GRAM',
|
||||||
wastage_percentage DECIMAL(5, 2) DEFAULT 0.0,
|
wastage_percentage DECIMAL(5, 2) DEFAULT 0.0,
|
||||||
min_stock DECIMAL(10, 2) DEFAULT 0,
|
min_stock DECIMAL(10, 2) DEFAULT 0,
|
||||||
reorder_level DECIMAL(10, 2) DEFAULT 0,
|
reorder_level DECIMAL(10, 2) DEFAULT 0,
|
||||||
@@ -312,12 +288,112 @@ CREATE TABLE IF NOT EXISTS inventory_locations (
|
|||||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
);
|
);
|
||||||
|
|
||||||
|
-- 6. Vendors & Purchase Orders
|
||||||
|
CREATE TABLE IF NOT EXISTS vendors (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
|
||||||
|
name VARCHAR(255) NOT NULL,
|
||||||
|
email VARCHAR(255),
|
||||||
|
phone VARCHAR(50),
|
||||||
|
address TEXT,
|
||||||
|
gstin VARCHAR(50),
|
||||||
|
state_id INTEGER REFERENCES indian_states(id),
|
||||||
|
id_number VARCHAR(100),
|
||||||
|
photo_url VARCHAR(1024),
|
||||||
|
contact_person VARCHAR(100),
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS purchase_orders (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
|
||||||
|
vendor_id INTEGER REFERENCES vendors(id) ON DELETE SET NULL,
|
||||||
|
po_number VARCHAR(100) NOT NULL,
|
||||||
|
issue_date DATE NOT NULL,
|
||||||
|
po_date DATE,
|
||||||
|
due_date DATE,
|
||||||
|
subtotal DECIMAL(15, 2) NOT NULL,
|
||||||
|
tax_total DECIMAL(15, 2) DEFAULT 0.0,
|
||||||
|
cgst_total DECIMAL(15, 2) DEFAULT 0.0,
|
||||||
|
sgst_total DECIMAL(15, 2) DEFAULT 0.0,
|
||||||
|
igst_total DECIMAL(15, 2) DEFAULT 0.0,
|
||||||
|
discount_total DECIMAL(15, 2) DEFAULT 0.0,
|
||||||
|
total_amount DECIMAL(15, 2) NOT NULL,
|
||||||
|
amount_paid DECIMAL(15, 2) DEFAULT 0.0,
|
||||||
|
next_payment_date DATE,
|
||||||
|
status VARCHAR(50) DEFAULT 'DRAFT',
|
||||||
|
notes TEXT,
|
||||||
|
vendor_invoice_url VARCHAR(1024),
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS purchase_order_items (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
po_id INTEGER REFERENCES purchase_orders(id) ON DELETE CASCADE,
|
||||||
|
product_id INTEGER REFERENCES products(id) ON DELETE SET NULL,
|
||||||
|
quantity DECIMAL(10, 3) NOT NULL,
|
||||||
|
unit_price DECIMAL(15, 2) NOT NULL,
|
||||||
|
tax_rate DECIMAL(5, 2) DEFAULT 0.0,
|
||||||
|
cgst_rate DECIMAL(5, 2) DEFAULT 0.0,
|
||||||
|
sgst_rate DECIMAL(5, 2) DEFAULT 0.0,
|
||||||
|
igst_rate DECIMAL(5, 2) DEFAULT 0.0,
|
||||||
|
discount DECIMAL(15, 2) DEFAULT 0.0,
|
||||||
|
making_charge DECIMAL(15, 2) DEFAULT 0.0,
|
||||||
|
other_charges DECIMAL(15, 2) DEFAULT 0.0,
|
||||||
|
sku VARCHAR(100),
|
||||||
|
huid VARCHAR(50),
|
||||||
|
weight DECIMAL(10, 3),
|
||||||
|
photo_url VARCHAR(1024),
|
||||||
|
description VARCHAR(255),
|
||||||
|
total DECIMAL(15, 2) NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS purchase_payments (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
po_id INTEGER REFERENCES purchase_orders(id) ON DELETE CASCADE,
|
||||||
|
transaction_id INTEGER REFERENCES transactions(id) ON DELETE SET NULL,
|
||||||
|
amount DECIMAL(15, 2) NOT NULL,
|
||||||
|
payment_method VARCHAR(50),
|
||||||
|
payment_date TIMESTAMP NOT NULL,
|
||||||
|
notes TEXT
|
||||||
|
);
|
||||||
|
|
||||||
|
-- 7. Individual Stock Inventory Items
|
||||||
|
CREATE TABLE IF NOT EXISTS inventory_items (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
|
||||||
|
product_id INTEGER REFERENCES products(id) ON DELETE SET NULL,
|
||||||
|
tag_number VARCHAR(100),
|
||||||
|
sku VARCHAR(100),
|
||||||
|
huid VARCHAR(50),
|
||||||
|
purity VARCHAR(50),
|
||||||
|
gross_weight DECIMAL(10, 3),
|
||||||
|
net_weight DECIMAL(10, 3),
|
||||||
|
stone_weight DECIMAL(10, 3),
|
||||||
|
diamond_weight DECIMAL(10, 3),
|
||||||
|
fine_weight DECIMAL(10, 3),
|
||||||
|
making_charges DECIMAL(15, 2),
|
||||||
|
making_charge_type VARCHAR(50),
|
||||||
|
purchase_cost DECIMAL(15, 2),
|
||||||
|
metal_cost DECIMAL(15, 2),
|
||||||
|
stone_cost DECIMAL(15, 2),
|
||||||
|
certification_cost DECIMAL(15, 2),
|
||||||
|
tax DECIMAL(15, 2),
|
||||||
|
vendor_id INTEGER REFERENCES vendors(id) ON DELETE SET NULL,
|
||||||
|
branch_id INTEGER,
|
||||||
|
purchase_ref VARCHAR(100),
|
||||||
|
status VARCHAR(50) DEFAULT 'AVAILABLE',
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS inventory_movements (
|
CREATE TABLE IF NOT EXISTS inventory_movements (
|
||||||
id SERIAL PRIMARY KEY,
|
id SERIAL PRIMARY KEY,
|
||||||
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
|
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
|
||||||
location_id INTEGER REFERENCES inventory_locations(id),
|
location_id INTEGER REFERENCES inventory_locations(id) ON DELETE SET NULL,
|
||||||
type VARCHAR(50) NOT NULL, -- ADDITION, REDUCTION, TRANSFER, ADJUSTMENT
|
type VARCHAR(50) NOT NULL,
|
||||||
reference_transaction_id INTEGER REFERENCES transactions(id),
|
reference_transaction_id INTEGER REFERENCES transactions(id) ON DELETE SET NULL,
|
||||||
notes TEXT,
|
notes TEXT,
|
||||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
);
|
);
|
||||||
@@ -325,7 +401,7 @@ CREATE TABLE IF NOT EXISTS inventory_movements (
|
|||||||
CREATE TABLE IF NOT EXISTS inventory_movement_items (
|
CREATE TABLE IF NOT EXISTS inventory_movement_items (
|
||||||
id SERIAL PRIMARY KEY,
|
id SERIAL PRIMARY KEY,
|
||||||
movement_id INTEGER REFERENCES inventory_movements(id) ON DELETE CASCADE,
|
movement_id INTEGER REFERENCES inventory_movements(id) ON DELETE CASCADE,
|
||||||
product_id INTEGER REFERENCES products(id),
|
product_id INTEGER REFERENCES products(id) ON DELETE SET NULL,
|
||||||
quantity DECIMAL(10, 2) NOT NULL,
|
quantity DECIMAL(10, 2) NOT NULL,
|
||||||
unit_price DECIMAL(10, 2),
|
unit_price DECIMAL(10, 2),
|
||||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
@@ -340,9 +416,7 @@ CREATE TABLE IF NOT EXISTS inventory_balances (
|
|||||||
UNIQUE(product_id, location_id)
|
UNIQUE(product_id, location_id)
|
||||||
);
|
);
|
||||||
|
|
||||||
-- SALES & INVOICES (Phase 2)
|
-- 8. Customers, Sales & Invoices
|
||||||
ALTER TABLE business_features ADD COLUMN IF NOT EXISTS stock_deduction_on_invoice BOOLEAN DEFAULT TRUE;
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS customers (
|
CREATE TABLE IF NOT EXISTS customers (
|
||||||
id SERIAL PRIMARY KEY,
|
id SERIAL PRIMARY KEY,
|
||||||
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
|
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
|
||||||
@@ -351,29 +425,27 @@ CREATE TABLE IF NOT EXISTS customers (
|
|||||||
phone VARCHAR(50),
|
phone VARCHAR(50),
|
||||||
address TEXT,
|
address TEXT,
|
||||||
gstin VARCHAR(50),
|
gstin VARCHAR(50),
|
||||||
|
photo_url VARCHAR(1024),
|
||||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS invoices (
|
CREATE TABLE IF NOT EXISTS invoices (
|
||||||
id SERIAL PRIMARY KEY,
|
id SERIAL PRIMARY KEY,
|
||||||
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
|
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
|
||||||
customer_id INTEGER REFERENCES customers(id),
|
customer_id INTEGER REFERENCES customers(id) ON DELETE SET NULL,
|
||||||
invoice_number VARCHAR(100) NOT NULL,
|
invoice_number VARCHAR(100) NOT NULL,
|
||||||
issue_date DATE NOT NULL,
|
issue_date DATE NOT NULL,
|
||||||
due_date DATE,
|
due_date DATE,
|
||||||
subtotal DECIMAL(15,2) NOT NULL,
|
subtotal DECIMAL(15, 2) NOT NULL,
|
||||||
tax_total DECIMAL(15,2) DEFAULT 0.0,
|
tax_total DECIMAL(15, 2) DEFAULT 0.0,
|
||||||
discount_total DECIMAL(15,2) DEFAULT 0.0,
|
discount_total DECIMAL(15, 2) DEFAULT 0.0,
|
||||||
total_amount DECIMAL(15,2) NOT NULL,
|
total_amount DECIMAL(15, 2) NOT NULL,
|
||||||
status VARCHAR(50) DEFAULT 'DRAFT', -- DRAFT, SENT, PAID, PARTIAL, OVERDUE, CANCELLED
|
status VARCHAR(50) DEFAULT 'DRAFT',
|
||||||
notes TEXT,
|
notes TEXT,
|
||||||
|
|
||||||
-- EMI tracking fields
|
|
||||||
is_emi BOOLEAN DEFAULT FALSE,
|
is_emi BOOLEAN DEFAULT FALSE,
|
||||||
emi_amount DECIMAL(15,2),
|
emi_amount DECIMAL(15, 2),
|
||||||
emi_cycle VARCHAR(20), -- MONTHLY, WEEKLY
|
emi_cycle VARCHAR(20),
|
||||||
emi_start_date DATE,
|
emi_start_date DATE,
|
||||||
|
|
||||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
);
|
);
|
||||||
@@ -381,35 +453,36 @@ CREATE TABLE IF NOT EXISTS invoices (
|
|||||||
CREATE TABLE IF NOT EXISTS invoice_items (
|
CREATE TABLE IF NOT EXISTS invoice_items (
|
||||||
id SERIAL PRIMARY KEY,
|
id SERIAL PRIMARY KEY,
|
||||||
invoice_id INTEGER REFERENCES invoices(id) ON DELETE CASCADE,
|
invoice_id INTEGER REFERENCES invoices(id) ON DELETE CASCADE,
|
||||||
product_id INTEGER REFERENCES products(id),
|
product_id INTEGER REFERENCES products(id) ON DELETE SET NULL,
|
||||||
inventory_item_id INTEGER REFERENCES inventory_items(id),
|
inventory_item_id INTEGER REFERENCES inventory_items(id) ON DELETE SET NULL,
|
||||||
hsn_code VARCHAR(50),
|
hsn_code VARCHAR(50),
|
||||||
description VARCHAR(255),
|
description VARCHAR(255),
|
||||||
quantity DECIMAL(10,3) NOT NULL,
|
quantity DECIMAL(10, 3) NOT NULL,
|
||||||
unit_price DECIMAL(15,2) NOT NULL,
|
unit_price DECIMAL(15, 2) NOT NULL,
|
||||||
tax_rate DECIMAL(5,2) DEFAULT 0.0,
|
tax_rate DECIMAL(5, 2) DEFAULT 0.0,
|
||||||
discount DECIMAL(15,2) DEFAULT 0.0,
|
discount DECIMAL(15, 2) DEFAULT 0.0,
|
||||||
making_charge DECIMAL(15,2) DEFAULT 0.0,
|
making_charge DECIMAL(15, 2) DEFAULT 0.0,
|
||||||
other_charges DECIMAL(15,2) DEFAULT 0.0,
|
other_charges DECIMAL(15, 2) DEFAULT 0.0,
|
||||||
total DECIMAL(15,2) NOT NULL
|
total DECIMAL(15, 2) NOT NULL
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS invoice_payments (
|
CREATE TABLE IF NOT EXISTS invoice_payments (
|
||||||
id SERIAL PRIMARY KEY,
|
id SERIAL PRIMARY KEY,
|
||||||
invoice_id INTEGER REFERENCES invoices(id) ON DELETE CASCADE,
|
invoice_id INTEGER REFERENCES invoices(id) ON DELETE CASCADE,
|
||||||
transaction_id INTEGER REFERENCES transactions(id) ON DELETE CASCADE,
|
transaction_id INTEGER REFERENCES transactions(id) ON DELETE SET NULL,
|
||||||
amount DECIMAL(15,2) NOT NULL,
|
amount DECIMAL(15, 2) NOT NULL,
|
||||||
payment_date DATE NOT NULL,
|
payment_date DATE NOT NULL,
|
||||||
emi_installment_number INTEGER,
|
emi_installment_number INTEGER,
|
||||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
);
|
);
|
||||||
|
|
||||||
|
-- 9. Projects & Task Management
|
||||||
CREATE TABLE IF NOT EXISTS projects (
|
CREATE TABLE IF NOT EXISTS projects (
|
||||||
id SERIAL PRIMARY KEY,
|
id SERIAL PRIMARY KEY,
|
||||||
name VARCHAR(255) NOT NULL,
|
name VARCHAR(255) NOT NULL,
|
||||||
description TEXT,
|
description TEXT,
|
||||||
customer_id INTEGER REFERENCES customers(id),
|
customer_id INTEGER REFERENCES customers(id) ON DELETE SET NULL,
|
||||||
user_id INTEGER REFERENCES users(id),
|
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
|
||||||
budget DECIMAL(15, 2) DEFAULT 0.00,
|
budget DECIMAL(15, 2) DEFAULT 0.00,
|
||||||
status VARCHAR(50) DEFAULT 'ACTIVE',
|
status VARCHAR(50) DEFAULT 'ACTIVE',
|
||||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
@@ -421,9 +494,9 @@ CREATE TABLE IF NOT EXISTS project_tasks (
|
|||||||
title VARCHAR(255) NOT NULL,
|
title VARCHAR(255) NOT NULL,
|
||||||
description TEXT,
|
description TEXT,
|
||||||
assignee_name VARCHAR(255),
|
assignee_name VARCHAR(255),
|
||||||
assignee_user_id INTEGER REFERENCES users(id),
|
assignee_user_id INTEGER REFERENCES users(id) ON DELETE SET NULL,
|
||||||
status VARCHAR(50) DEFAULT 'TODO', -- TODO, IN_PROGRESS, REVIEW, DONE
|
status VARCHAR(50) DEFAULT 'TODO',
|
||||||
is_billable BOOLEAN DEFAULT false,
|
is_billable BOOLEAN DEFAULT FALSE,
|
||||||
hourly_rate DECIMAL(15, 2) DEFAULT 0.00,
|
hourly_rate DECIMAL(15, 2) DEFAULT 0.00,
|
||||||
hours_logged DECIMAL(10, 2) DEFAULT 0.00,
|
hours_logged DECIMAL(10, 2) DEFAULT 0.00,
|
||||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
@@ -432,44 +505,8 @@ CREATE TABLE IF NOT EXISTS project_tasks (
|
|||||||
CREATE TABLE IF NOT EXISTS project_task_comments (
|
CREATE TABLE IF NOT EXISTS project_task_comments (
|
||||||
id SERIAL PRIMARY KEY,
|
id SERIAL PRIMARY KEY,
|
||||||
task_id INTEGER REFERENCES project_tasks(id) ON DELETE CASCADE,
|
task_id INTEGER REFERENCES project_tasks(id) ON DELETE CASCADE,
|
||||||
user_id INTEGER REFERENCES users(id),
|
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
|
||||||
content TEXT NOT NULL,
|
content TEXT NOT NULL,
|
||||||
images TEXT,
|
images TEXT,
|
||||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
);
|
);
|
||||||
|
|
||||||
-- ACCOUNT SETUP FLOW MIGRATIONS --
|
|
||||||
ALTER TABLE users ADD COLUMN IF NOT EXISTS username VARCHAR(255) UNIQUE;
|
|
||||||
ALTER TABLE users ADD COLUMN IF NOT EXISTS name VARCHAR(255);
|
|
||||||
ALTER TABLE users ADD COLUMN IF NOT EXISTS mobile_number VARCHAR(255);
|
|
||||||
ALTER TABLE users ADD COLUMN IF NOT EXISTS setup_status VARCHAR(50) DEFAULT 'NOT_STARTED';
|
|
||||||
ALTER TABLE users ADD COLUMN IF NOT EXISTS setup_completed_at TIMESTAMP;
|
|
||||||
|
|
||||||
ALTER TABLE business_profiles ADD COLUMN IF NOT EXISTS nature_of_business VARCHAR(50);
|
|
||||||
ALTER TABLE business_profiles ADD COLUMN IF NOT EXISTS msme_number VARCHAR(255);
|
|
||||||
|
|
||||||
ALTER TABLE business_profiles ALTER COLUMN pan_number TYPE VARCHAR(255);
|
|
||||||
ALTER TABLE business_profiles ALTER COLUMN gstin TYPE VARCHAR(255);
|
|
||||||
|
|
||||||
ALTER TABLE product_categories ADD COLUMN IF NOT EXISTS has_child BOOLEAN DEFAULT FALSE;
|
|
||||||
ALTER TABLE product_categories ADD COLUMN IF NOT EXISTS default_hsn VARCHAR(50);
|
|
||||||
ALTER TABLE product_categories ADD COLUMN IF NOT EXISTS default_gst DECIMAL(5, 2);
|
|
||||||
ALTER TABLE product_categories ADD COLUMN IF NOT EXISTS huid_required BOOLEAN DEFAULT FALSE;
|
|
||||||
ALTER TABLE product_categories ADD COLUMN IF NOT EXISTS default_making_charge DECIMAL(15, 2);
|
|
||||||
ALTER TABLE product_categories ADD COLUMN IF NOT EXISTS making_charge_type VARCHAR(50);
|
|
||||||
ALTER TABLE product_categories ADD COLUMN IF NOT EXISTS is_active BOOLEAN DEFAULT TRUE;
|
|
||||||
ALTER TABLE product_categories ADD COLUMN IF NOT EXISTS sort_order INTEGER DEFAULT 0;
|
|
||||||
ALTER TABLE product_categories DROP COLUMN IF EXISTS is_commodity;
|
|
||||||
ALTER TABLE product_categories DROP COLUMN IF EXISTS daily_rate;
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS user_consents (
|
|
||||||
id SERIAL PRIMARY KEY,
|
|
||||||
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
|
|
||||||
consent_type VARCHAR(100) NOT NULL,
|
|
||||||
policy_version VARCHAR(50) NOT NULL,
|
|
||||||
accepted BOOLEAN DEFAULT TRUE,
|
|
||||||
accepted_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
ip_address VARCHAR(100),
|
|
||||||
user_agent TEXT,
|
|
||||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
||||||
);
|
|
||||||
|
|||||||
@@ -1,26 +0,0 @@
|
|||||||
import java.sql.Connection;
|
|
||||||
import java.sql.DriverManager;
|
|
||||||
import java.sql.Statement;
|
|
||||||
|
|
||||||
public class RunSchema {
|
|
||||||
public static void main(String[] args) {
|
|
||||||
String url = "jdbc:postgresql://localhost:5432/kifi_db";
|
|
||||||
String user = "postgres";
|
|
||||||
String password = "postgres";
|
|
||||||
|
|
||||||
try (Connection conn = DriverManager.getConnection(url, user, password);
|
|
||||||
Statement stmt = conn.createStatement()) {
|
|
||||||
|
|
||||||
stmt.execute("ALTER TABLE purchase_orders ADD COLUMN IF NOT EXISTS cgst_total NUMERIC(10,2)");
|
|
||||||
stmt.execute("ALTER TABLE purchase_orders ADD COLUMN IF NOT EXISTS sgst_total NUMERIC(10,2)");
|
|
||||||
stmt.execute("ALTER TABLE purchase_orders ADD COLUMN IF NOT EXISTS igst_total NUMERIC(10,2)");
|
|
||||||
stmt.execute("ALTER TABLE purchase_order_items ADD COLUMN IF NOT EXISTS cgst_rate NUMERIC(10,2)");
|
|
||||||
stmt.execute("ALTER TABLE purchase_order_items ADD COLUMN IF NOT EXISTS sgst_rate NUMERIC(10,2)");
|
|
||||||
stmt.execute("ALTER TABLE purchase_order_items ADD COLUMN IF NOT EXISTS igst_rate NUMERIC(10,2)");
|
|
||||||
|
|
||||||
System.out.println("Schema updated successfully!");
|
|
||||||
} catch (Exception e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,26 +1,13 @@
|
|||||||
package com.kifi.api;
|
package com.kifi.api;
|
||||||
|
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
|
||||||
import org.springframework.boot.test.context.SpringBootTest;
|
import org.springframework.boot.test.context.SpringBootTest;
|
||||||
import com.kifi.api.repository.business.BusinessProfileRepository;
|
|
||||||
import com.kifi.api.entity.business.BusinessProfile;
|
|
||||||
import java.time.LocalDateTime;
|
|
||||||
@SpringBootTest
|
@SpringBootTest
|
||||||
public class DBTest {
|
public class DBTest {
|
||||||
@Autowired
|
|
||||||
private BusinessProfileRepository repo;
|
|
||||||
@Test
|
@Test
|
||||||
public void test() {
|
public void contextLoads() {
|
||||||
BusinessProfile bp = BusinessProfile.builder().userId(100L).build();
|
System.out.println("Spring Boot test context loaded successfully.");
|
||||||
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();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,9 +26,9 @@ class DioClient {
|
|||||||
|
|
||||||
DioClient._internal()
|
DioClient._internal()
|
||||||
: dio = Dio(BaseOptions(
|
: dio = Dio(BaseOptions(
|
||||||
//baseUrl: 'https://app.technobeesolutions.in/api/kifi-v2',
|
baseUrl: 'https://app.technobeesolutions.in/api/kifi-v2',
|
||||||
//baseUrl: 'http://192.168.0.104:8080/api/kifi-v2', // Local Mac IP for physical device testing
|
//baseUrl: 'http://192.168.0.104:8080/api/kifi-v2', // Local Mac IP for physical device testing
|
||||||
baseUrl: 'http://192.168.1.5:8080/api/kifi-v2', // Current Local Mac IP (192.168.1.5)
|
//baseUrl: '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),
|
||||||
)),
|
)),
|
||||||
|
|||||||
32
kifi-app/lib/core/utils/purity_utils.dart
Normal file
32
kifi-app/lib/core/utils/purity_utils.dart
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
/// Utility functions for gold/silver purity factor calculation and formatting.
|
||||||
|
/// Standard rule: Purity is represented as a fraction between 0.0 and 1.0 (e.g. 0.916 for 22KT / 91.6%).
|
||||||
|
/// If a raw value > 1.0 is supplied (such as 91.6 or 75.0), it is normalized by dividing by 100 with a maximum of 1.0.
|
||||||
|
|
||||||
|
double normalizePurity(num? rawPurity) {
|
||||||
|
if (rawPurity == null || rawPurity <= 0) return 1.0;
|
||||||
|
double p = rawPurity.toDouble();
|
||||||
|
if (p > 1.0) {
|
||||||
|
p = p / 100.0;
|
||||||
|
}
|
||||||
|
if (p > 1.0) {
|
||||||
|
p = 1.0;
|
||||||
|
}
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
|
||||||
|
String formatPurity(num? rawPurity) {
|
||||||
|
final p = normalizePurity(rawPurity);
|
||||||
|
return p.toStringAsFixed(3);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Resolves purity factor prioritizing the category (metal grade e.g. 22KT -> 0.916)
|
||||||
|
/// then falling back to product-specific purity or 1.0.
|
||||||
|
double resolvePurity({num? categoryPurity, num? productPurity}) {
|
||||||
|
if (categoryPurity != null && categoryPurity > 0) {
|
||||||
|
return normalizePurity(categoryPurity);
|
||||||
|
}
|
||||||
|
if (productPurity != null && productPurity > 0) {
|
||||||
|
return normalizePurity(productPurity);
|
||||||
|
}
|
||||||
|
return 1.0;
|
||||||
|
}
|
||||||
@@ -3,6 +3,8 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|||||||
import 'package:lucide_icons/lucide_icons.dart';
|
import 'package:lucide_icons/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 '../../../../core/network/dio_client.dart';
|
||||||
|
|
||||||
class OtpScreen extends ConsumerStatefulWidget {
|
class OtpScreen extends ConsumerStatefulWidget {
|
||||||
final String email;
|
final String email;
|
||||||
@@ -23,11 +25,32 @@ class _OtpScreenState extends ConsumerState<OtpScreen> {
|
|||||||
.read(authControllerProvider.notifier)
|
.read(authControllerProvider.notifier)
|
||||||
.verifyOtp(widget.email, otp);
|
.verifyOtp(widget.email, otp);
|
||||||
if (success && mounted) {
|
if (success && mounted) {
|
||||||
|
try {
|
||||||
|
await Future.delayed(const Duration(milliseconds: 300));
|
||||||
|
final res = await DioClient().dio.get('/account/setup/status');
|
||||||
|
final status = res.data['status'];
|
||||||
|
if (status == 'COMPLETED' && mounted) {
|
||||||
Navigator.pushAndRemoveUntil(
|
Navigator.pushAndRemoveUntil(
|
||||||
context,
|
context,
|
||||||
MaterialPageRoute(builder: (_) => const DashboardScreen()),
|
MaterialPageRoute(builder: (_) => const DashboardScreen()),
|
||||||
(route) => false,
|
(route) => false,
|
||||||
);
|
);
|
||||||
|
} else if (mounted) {
|
||||||
|
Navigator.pushAndRemoveUntil(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(builder: (_) => const SetupWizardScreen()),
|
||||||
|
(route) => false,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (_) {
|
||||||
|
if (mounted) {
|
||||||
|
Navigator.pushAndRemoveUntil(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(builder: (_) => const SetupWizardScreen()),
|
||||||
|
(route) => false,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,10 +11,23 @@ import 'auth_screen.dart';
|
|||||||
import '../../../core/network/dio_client.dart';
|
import '../../../core/network/dio_client.dart';
|
||||||
import '../providers/auth_provider.dart';
|
import '../providers/auth_provider.dart';
|
||||||
import '../../transactions/providers/providers.dart';
|
import '../../transactions/providers/providers.dart';
|
||||||
|
import '../../transactions/providers/paginated_transaction_provider.dart';
|
||||||
import '../../../core/theme/theme_provider.dart';
|
import '../../../core/theme/theme_provider.dart';
|
||||||
import '../../business/providers/business_mode_provider.dart';
|
import '../../business/providers/business_mode_provider.dart';
|
||||||
|
import '../../business/providers/business_provider.dart';
|
||||||
import '../../projects/providers/project_mode_provider.dart';
|
import '../../projects/providers/project_mode_provider.dart';
|
||||||
|
import '../../projects/providers/project_provider.dart';
|
||||||
|
import '../../inventory/providers/products_provider.dart';
|
||||||
|
import '../../inventory/providers/product_categories_provider.dart';
|
||||||
|
import '../../inventory/providers/inventory_items_provider.dart';
|
||||||
|
import '../../inventory/providers/inventory_valuation_provider.dart';
|
||||||
|
import '../../inventory/providers/commodity_rates_provider.dart';
|
||||||
|
import '../../vendor/providers/vendors_provider.dart';
|
||||||
|
import '../../vendor/providers/purchase_orders_provider.dart';
|
||||||
|
import '../../sales/providers/customers_provider.dart';
|
||||||
|
import '../../sales/providers/invoices_provider.dart';
|
||||||
import '../../business/presentation/settings/business_settings_screen.dart';
|
import '../../business/presentation/settings/business_settings_screen.dart';
|
||||||
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
|
||||||
class ProfileScreen extends ConsumerStatefulWidget {
|
class ProfileScreen extends ConsumerStatefulWidget {
|
||||||
const ProfileScreen({super.key});
|
const ProfileScreen({super.key});
|
||||||
@@ -86,6 +99,35 @@ class _ProfileScreenState extends ConsumerState<ProfileScreen>
|
|||||||
|
|
||||||
Future<void> _logout() async {
|
Future<void> _logout() async {
|
||||||
await DioClient().clearToken();
|
await DioClient().clearToken();
|
||||||
|
try {
|
||||||
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
await prefs.remove('is_business_mode');
|
||||||
|
} catch (_) {}
|
||||||
|
|
||||||
|
try {
|
||||||
|
ref.invalidate(categoryProvider);
|
||||||
|
ref.invalidate(transactionProvider);
|
||||||
|
ref.invalidate(paginatedTransactionProvider);
|
||||||
|
ref.invalidate(budgetProvider);
|
||||||
|
ref.invalidate(recurringTransactionProvider);
|
||||||
|
ref.invalidate(walletProvider);
|
||||||
|
ref.invalidate(invitationProvider);
|
||||||
|
ref.invalidate(businessProfileProvider);
|
||||||
|
ref.invalidate(businessFeatureProvider);
|
||||||
|
ref.invalidate(businessModeProvider);
|
||||||
|
ref.invalidate(productsProvider);
|
||||||
|
ref.invalidate(productCategoriesProvider);
|
||||||
|
ref.invalidate(inventoryItemsProvider);
|
||||||
|
ref.invalidate(inventoryValuationProvider);
|
||||||
|
ref.invalidate(commodityRatesProvider);
|
||||||
|
ref.invalidate(vendorsProvider);
|
||||||
|
ref.invalidate(purchaseOrdersProvider);
|
||||||
|
ref.invalidate(customersProvider);
|
||||||
|
ref.invalidate(invoicesProvider);
|
||||||
|
ref.invalidate(projectsProvider);
|
||||||
|
ref.invalidate(authControllerProvider);
|
||||||
|
} catch (_) {}
|
||||||
|
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
Navigator.pushAndRemoveUntil(
|
Navigator.pushAndRemoveUntil(
|
||||||
context,
|
context,
|
||||||
|
|||||||
@@ -78,6 +78,12 @@ class AuthController extends AsyncNotifier<void> {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> logout() async {
|
||||||
|
try {
|
||||||
|
await DioClient().clearToken();
|
||||||
|
} catch (_) {}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final authControllerProvider = AsyncNotifierProvider<AuthController, void>(() {
|
final authControllerProvider = AsyncNotifierProvider<AuthController, void>(() {
|
||||||
|
|||||||
@@ -1,19 +1,37 @@
|
|||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
import 'business_provider.dart';
|
||||||
|
|
||||||
class BusinessModeNotifier extends Notifier<bool> {
|
class BusinessModeNotifier extends Notifier<bool> {
|
||||||
@override
|
@override
|
||||||
bool build() {
|
bool build() {
|
||||||
_loadState();
|
final businessProfile = ref.watch(businessProfileProvider).asData?.value;
|
||||||
return false; // Default until loaded
|
if (businessProfile == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
_loadPreference();
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _loadState() async {
|
Future<void> _loadPreference() async {
|
||||||
|
final businessProfile = ref.read(businessProfileProvider).asData?.value;
|
||||||
|
if (businessProfile == null) {
|
||||||
|
state = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
final prefs = await SharedPreferences.getInstance();
|
final prefs = await SharedPreferences.getInstance();
|
||||||
state = prefs.getBool('is_business_mode') ?? false;
|
final saved = prefs.getBool('is_business_mode');
|
||||||
|
if (saved != null) {
|
||||||
|
state = saved;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> toggleMode() async {
|
Future<void> toggleMode() async {
|
||||||
|
final businessProfile = ref.read(businessProfileProvider).asData?.value;
|
||||||
|
if (businessProfile == null) {
|
||||||
|
state = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
final prefs = await SharedPreferences.getInstance();
|
final prefs = await SharedPreferences.getInstance();
|
||||||
state = !state;
|
state = !state;
|
||||||
await prefs.setBool('is_business_mode', state);
|
await prefs.setBool('is_business_mode', state);
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ class Product {
|
|||||||
this.size,
|
this.size,
|
||||||
this.priceCalcRule = 'MANUAL',
|
this.priceCalcRule = 'MANUAL',
|
||||||
this.autoCalculatePrice = false,
|
this.autoCalculatePrice = false,
|
||||||
this.purityFactor = 1.0,
|
this.purityFactor,
|
||||||
this.makingCharges = 0.0,
|
this.makingCharges = 0.0,
|
||||||
this.makingChargesType = 'FLAT',
|
this.makingChargesType = 'FLAT',
|
||||||
this.wastagePercentage = 0.0,
|
this.wastagePercentage = 0.0,
|
||||||
@@ -72,8 +72,7 @@ class Product {
|
|||||||
autoCalculatePrice:
|
autoCalculatePrice:
|
||||||
json['autoCalculatePrice'] ?? json['auto_calculate_price'] ?? false,
|
json['autoCalculatePrice'] ?? json['auto_calculate_price'] ?? false,
|
||||||
purityFactor:
|
purityFactor:
|
||||||
(json['purityFactor'] ?? json['purity_factor'] as num?)?.toDouble() ??
|
(json['purityFactor'] ?? json['purity_factor'] as num?)?.toDouble(),
|
||||||
1.0,
|
|
||||||
makingCharges:
|
makingCharges:
|
||||||
(json['makingCharges'] ?? json['making_charges'] as num?)
|
(json['makingCharges'] ?? json['making_charges'] as num?)
|
||||||
?.toDouble() ??
|
?.toDouble() ??
|
||||||
|
|||||||
@@ -24,6 +24,8 @@ class _AddProductScreenState extends ConsumerState<AddProductScreen> {
|
|||||||
final _formKey = GlobalKey<FormState>();
|
final _formKey = GlobalKey<FormState>();
|
||||||
final _hsnController = TextEditingController();
|
final _hsnController = TextEditingController();
|
||||||
final _gstController = TextEditingController();
|
final _gstController = TextEditingController();
|
||||||
|
final _makingChargesController = TextEditingController();
|
||||||
|
String _makingChargesType = 'PER_GRAM';
|
||||||
|
|
||||||
// Basic
|
// Basic
|
||||||
String _name = '';
|
String _name = '';
|
||||||
@@ -55,6 +57,8 @@ class _AddProductScreenState extends ConsumerState<AddProductScreen> {
|
|||||||
_uomId = p.uomId;
|
_uomId = p.uomId;
|
||||||
_color = p.color ?? '';
|
_color = p.color ?? '';
|
||||||
_gstController.text = p.gstRate != null && p.gstRate! > 0 ? p.gstRate.toString() : '';
|
_gstController.text = p.gstRate != null && p.gstRate! > 0 ? p.gstRate.toString() : '';
|
||||||
|
_makingChargesController.text = p.makingCharges != null && p.makingCharges! > 0 ? p.makingCharges!.toString() : '';
|
||||||
|
_makingChargesType = p.makingChargesType ?? 'PER_GRAM';
|
||||||
|
|
||||||
if (p.imageIds.isNotEmpty) {
|
if (p.imageIds.isNotEmpty) {
|
||||||
_existingImageIds.addAll(p.imageIds);
|
_existingImageIds.addAll(p.imageIds);
|
||||||
@@ -75,6 +79,7 @@ class _AddProductScreenState extends ConsumerState<AddProductScreen> {
|
|||||||
void dispose() {
|
void dispose() {
|
||||||
_hsnController.dispose();
|
_hsnController.dispose();
|
||||||
_gstController.dispose();
|
_gstController.dispose();
|
||||||
|
_makingChargesController.dispose();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -142,6 +147,8 @@ class _AddProductScreenState extends ConsumerState<AddProductScreen> {
|
|||||||
uomId: _uomId,
|
uomId: _uomId,
|
||||||
color: _color.isNotEmpty ? _color : null,
|
color: _color.isNotEmpty ? _color : null,
|
||||||
gstRate: double.tryParse(_gstController.text) ?? 0,
|
gstRate: double.tryParse(_gstController.text) ?? 0,
|
||||||
|
makingCharges: double.tryParse(_makingChargesController.text) ?? 0.0,
|
||||||
|
makingChargesType: _makingChargesType,
|
||||||
priceCalcRule: 'MANUAL',
|
priceCalcRule: 'MANUAL',
|
||||||
autoCalculatePrice: false,
|
autoCalculatePrice: false,
|
||||||
);
|
);
|
||||||
@@ -245,6 +252,12 @@ class _AddProductScreenState extends ConsumerState<AddProductScreen> {
|
|||||||
if (val.defaultGst != null && val.defaultGst! > 0) {
|
if (val.defaultGst != null && val.defaultGst! > 0) {
|
||||||
_gstController.text = val.defaultGst.toString();
|
_gstController.text = val.defaultGst.toString();
|
||||||
}
|
}
|
||||||
|
if (_makingChargesController.text.isEmpty && val.defaultMakingCharge != null && val.defaultMakingCharge! > 0) {
|
||||||
|
_makingChargesController.text = val.defaultMakingCharge!.toString();
|
||||||
|
}
|
||||||
|
if (val.makingChargeType != null && val.makingChargeType!.isNotEmpty) {
|
||||||
|
_makingChargesType = val.makingChargeType!;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@@ -284,6 +297,36 @@ class _AddProductScreenState extends ConsumerState<AddProductScreen> {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: TextFormField(
|
||||||
|
controller: _makingChargesController,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
labelText: 'Making Charges',
|
||||||
|
prefixIcon: Icon(LucideIcons.hammer),
|
||||||
|
),
|
||||||
|
keyboardType: const TextInputType.numberWithOptions(decimal: true),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 16),
|
||||||
|
Expanded(
|
||||||
|
child: DropdownButtonFormField<String>(
|
||||||
|
value: _makingChargesType,
|
||||||
|
decoration: const InputDecoration(labelText: 'Charge Type'),
|
||||||
|
items: const [
|
||||||
|
DropdownMenuItem(value: 'PER_GRAM', child: Text('Per Gram')),
|
||||||
|
DropdownMenuItem(value: 'PER_PIECE', child: Text('Per Piece')),
|
||||||
|
DropdownMenuItem(value: 'PERCENTAGE', child: Text('Percentage %')),
|
||||||
|
],
|
||||||
|
onChanged: (val) {
|
||||||
|
if (val != null) setState(() => _makingChargesType = val);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
|
|
||||||
const Text('Product Images', style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold)),
|
const Text('Product Images', style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold)),
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ 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/lucide_icons.dart';
|
||||||
import 'dart:ui';
|
import 'dart:ui';
|
||||||
|
import '../../../core/utils/purity_utils.dart';
|
||||||
|
|
||||||
class CategoryManagementScreen extends ConsumerStatefulWidget {
|
class CategoryManagementScreen extends ConsumerStatefulWidget {
|
||||||
const CategoryManagementScreen({super.key});
|
const CategoryManagementScreen({super.key});
|
||||||
@@ -272,7 +273,7 @@ class _CategoryFormSheetState extends ConsumerState<CategoryFormSheet> {
|
|||||||
_hsnController.text = c.defaultHsn ?? '';
|
_hsnController.text = c.defaultHsn ?? '';
|
||||||
_gstController.text = c.defaultGst?.toString() ?? '';
|
_gstController.text = c.defaultGst?.toString() ?? '';
|
||||||
_makingChargeController.text = c.defaultMakingCharge?.toString() ?? '';
|
_makingChargeController.text = c.defaultMakingCharge?.toString() ?? '';
|
||||||
_purityFactorController.text = (c.purityFactor ?? 1.0).toString();
|
_purityFactorController.text = formatPurity(c.purityFactor);
|
||||||
_huidRequired = c.huidRequired;
|
_huidRequired = c.huidRequired;
|
||||||
_commodityCode = c.commodityCode ?? 'XAU';
|
_commodityCode = c.commodityCode ?? 'XAU';
|
||||||
|
|
||||||
@@ -307,7 +308,7 @@ class _CategoryFormSheetState extends ConsumerState<CategoryFormSheet> {
|
|||||||
parentCategoryId: _selectedParentId,
|
parentCategoryId: _selectedParentId,
|
||||||
hasChild: !_isLeaf,
|
hasChild: !_isLeaf,
|
||||||
commodityCode: _isLeaf ? _commodityCode : null,
|
commodityCode: _isLeaf ? _commodityCode : null,
|
||||||
purityFactor: _isLeaf ? (double.tryParse(_purityFactorController.text) ?? 1.0) : 1.0,
|
purityFactor: _isLeaf ? normalizePurity(double.tryParse(_purityFactorController.text)) : 1.0,
|
||||||
defaultHsn: _isLeaf ? _hsnController.text.trim() : null,
|
defaultHsn: _isLeaf ? _hsnController.text.trim() : null,
|
||||||
defaultGst: _isLeaf ? double.tryParse(_gstController.text) : null,
|
defaultGst: _isLeaf ? double.tryParse(_gstController.text) : null,
|
||||||
huidRequired: _isLeaf ? _huidRequired : false,
|
huidRequired: _isLeaf ? _huidRequired : false,
|
||||||
@@ -483,7 +484,7 @@ class _CategoryFormSheetState extends ConsumerState<CategoryFormSheet> {
|
|||||||
flex: 2,
|
flex: 2,
|
||||||
child: _buildTextField(
|
child: _buildTextField(
|
||||||
controller: _purityFactorController,
|
controller: _purityFactorController,
|
||||||
label: 'Purity (e.g. 0.916)',
|
label: 'Purity (0 to 1, e.g. 0.916)',
|
||||||
icon: LucideIcons.gem,
|
icon: LucideIcons.gem,
|
||||||
keyboardType: const TextInputType.numberWithOptions(decimal: true),
|
keyboardType: const TextInputType.numberWithOptions(decimal: true),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import '../providers/commodity_rates_provider.dart';
|
|||||||
import '../domain/commodity_rate.dart';
|
import '../domain/commodity_rate.dart';
|
||||||
import '../../../core/theme/app_theme.dart';
|
import '../../../core/theme/app_theme.dart';
|
||||||
import '../../../core/utils/snackbar_service.dart';
|
import '../../../core/utils/snackbar_service.dart';
|
||||||
|
import '../../../core/utils/purity_utils.dart';
|
||||||
|
|
||||||
class DailyRatesScreen extends ConsumerStatefulWidget {
|
class DailyRatesScreen extends ConsumerStatefulWidget {
|
||||||
const DailyRatesScreen({super.key});
|
const DailyRatesScreen({super.key});
|
||||||
@@ -699,7 +700,7 @@ class _DailyRatesScreenState extends ConsumerState<DailyRatesScreen> {
|
|||||||
spacing: 8,
|
spacing: 8,
|
||||||
runSpacing: 8,
|
runSpacing: 8,
|
||||||
children: linkedCategories.map((cat) {
|
children: linkedCategories.map((cat) {
|
||||||
final purity = cat.purityFactor ?? 1.0;
|
final purity = normalizePurity(cat.purityFactor);
|
||||||
final effectiveUnitRate = currentRate * purity;
|
final effectiveUnitRate = currentRate * purity;
|
||||||
return Container(
|
return Container(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
|
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
|
||||||
@@ -725,7 +726,7 @@ class _DailyRatesScreenState extends ConsumerState<DailyRatesScreen> {
|
|||||||
borderRadius: BorderRadius.circular(4),
|
borderRadius: BorderRadius.circular(4),
|
||||||
),
|
),
|
||||||
child: Text(
|
child: Text(
|
||||||
'${(purity * 100).toStringAsFixed(1)}% (₹${effectiveUnitRate.toStringAsFixed(0)}/g)',
|
'${formatPurity(purity)} (₹${effectiveUnitRate.toStringAsFixed(0)}/g)',
|
||||||
style: TextStyle(fontSize: 10, fontWeight: FontWeight.bold, color: color),
|
style: TextStyle(fontSize: 10, fontWeight: FontWeight.bold, color: color),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -133,6 +133,15 @@ class ProductDetailScreen extends ConsumerWidget {
|
|||||||
"GST Rate",
|
"GST Rate",
|
||||||
"${p.gstRate?.toStringAsFixed(1) ?? '0'}%",
|
"${p.gstRate?.toStringAsFixed(1) ?? '0'}%",
|
||||||
),
|
),
|
||||||
|
if (p.makingCharges != null && p.makingCharges! > 0)
|
||||||
|
_buildDetailRow(
|
||||||
|
"Making Charges",
|
||||||
|
p.makingChargesType == 'PERCENTAGE'
|
||||||
|
? "${p.makingCharges}%"
|
||||||
|
: (p.makingChargesType == 'PER_PIECE'
|
||||||
|
? "₹${p.makingCharges!.toStringAsFixed(2)} / pc"
|
||||||
|
: "₹${p.makingCharges!.toStringAsFixed(2)} / g"),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import 'package:intl/intl.dart';
|
|||||||
import 'package:lucide_icons/lucide_icons.dart';
|
import 'package:lucide_icons/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';
|
||||||
|
|
||||||
class StockLedgerTab extends ConsumerStatefulWidget {
|
class StockLedgerTab extends ConsumerStatefulWidget {
|
||||||
final Product product;
|
final Product product;
|
||||||
@@ -71,7 +72,7 @@ class _StockLedgerTabState extends ConsumerState<StockLedgerTab> {
|
|||||||
).firstOrNull;
|
).firstOrNull;
|
||||||
|
|
||||||
final String unit = category?.baseUnit ?? 'g';
|
final String unit = category?.baseUnit ?? 'g';
|
||||||
final double purityFactor = category?.purityFactor ?? (widget.product.purityFactor ?? 1.0);
|
final double purityFactor = resolvePurity(categoryPurity: category?.purityFactor, productPurity: widget.product.purityFactor);
|
||||||
|
|
||||||
double commodityRate = 0.0;
|
double commodityRate = 0.0;
|
||||||
if (category?.commodityCode != null && commodityRatesState.value != null) {
|
if (category?.commodityCode != null && commodityRatesState.value != null) {
|
||||||
@@ -221,6 +222,23 @@ class _StockLedgerTabState extends ConsumerState<StockLedgerTab> {
|
|||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
const SizedBox(height: 3),
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 1.5),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.amber.withValues(alpha: 0.12),
|
||||||
|
borderRadius: BorderRadius.circular(4),
|
||||||
|
border: Border.all(color: Colors.amber.withValues(alpha: 0.3)),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
'Purity: ${formatPurity(double.tryParse(item.purity ?? '') ?? purityFactor)}',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 10,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Colors.amber.shade900,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -3,16 +3,26 @@ class InvoiceItem {
|
|||||||
final int? invoiceId;
|
final int? invoiceId;
|
||||||
final int? productId;
|
final int? productId;
|
||||||
final int? inventoryItemId;
|
final int? inventoryItemId;
|
||||||
|
final String? productName;
|
||||||
|
final String? categoryName;
|
||||||
|
final String? commodityCode;
|
||||||
final String? sku;
|
final String? sku;
|
||||||
|
final String? huid;
|
||||||
final String? hsnCode;
|
final String? hsnCode;
|
||||||
|
|
||||||
final String? description;
|
final String? description;
|
||||||
final double quantity;
|
final double quantity;
|
||||||
final double unitPrice;
|
final double? weight;
|
||||||
final double taxRate;
|
final double unitPrice; // metal rate
|
||||||
final double discount;
|
|
||||||
final double makingCharge;
|
final double makingCharge;
|
||||||
|
final String makingChargesType; // PER_GRAM, PER_PIECE, PERCENTAGE
|
||||||
|
final double taxRate;
|
||||||
|
final double cgst;
|
||||||
|
final double sgst;
|
||||||
|
final double igst;
|
||||||
|
final double discount;
|
||||||
final double otherCharges;
|
final double otherCharges;
|
||||||
|
final String? photoUrl;
|
||||||
|
final String? localPhotoPath;
|
||||||
final double total;
|
final double total;
|
||||||
|
|
||||||
InvoiceItem({
|
InvoiceItem({
|
||||||
@@ -20,15 +30,26 @@ class InvoiceItem {
|
|||||||
this.invoiceId,
|
this.invoiceId,
|
||||||
this.productId,
|
this.productId,
|
||||||
this.inventoryItemId,
|
this.inventoryItemId,
|
||||||
|
this.productName,
|
||||||
|
this.categoryName,
|
||||||
|
this.commodityCode,
|
||||||
this.hsnCode,
|
this.hsnCode,
|
||||||
this.sku,
|
this.sku,
|
||||||
|
this.huid,
|
||||||
this.description,
|
this.description,
|
||||||
required this.quantity,
|
required this.quantity,
|
||||||
|
this.weight,
|
||||||
required this.unitPrice,
|
required this.unitPrice,
|
||||||
this.taxRate = 0.0,
|
|
||||||
this.discount = 0.0,
|
|
||||||
this.makingCharge = 0.0,
|
this.makingCharge = 0.0,
|
||||||
|
this.makingChargesType = 'PER_GRAM',
|
||||||
|
this.taxRate = 0.0,
|
||||||
|
this.cgst = 0.0,
|
||||||
|
this.sgst = 0.0,
|
||||||
|
this.igst = 0.0,
|
||||||
|
this.discount = 0.0,
|
||||||
this.otherCharges = 0.0,
|
this.otherCharges = 0.0,
|
||||||
|
this.photoUrl,
|
||||||
|
this.localPhotoPath,
|
||||||
required this.total,
|
required this.total,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -37,15 +58,26 @@ class InvoiceItem {
|
|||||||
int? invoiceId,
|
int? invoiceId,
|
||||||
int? productId,
|
int? productId,
|
||||||
int? inventoryItemId,
|
int? inventoryItemId,
|
||||||
|
String? productName,
|
||||||
|
String? categoryName,
|
||||||
|
String? commodityCode,
|
||||||
String? hsnCode,
|
String? hsnCode,
|
||||||
String? sku,
|
String? sku,
|
||||||
|
String? huid,
|
||||||
String? description,
|
String? description,
|
||||||
double? quantity,
|
double? quantity,
|
||||||
|
double? weight,
|
||||||
double? unitPrice,
|
double? unitPrice,
|
||||||
double? taxRate,
|
|
||||||
double? discount,
|
|
||||||
double? makingCharge,
|
double? makingCharge,
|
||||||
|
String? makingChargesType,
|
||||||
|
double? taxRate,
|
||||||
|
double? cgst,
|
||||||
|
double? sgst,
|
||||||
|
double? igst,
|
||||||
|
double? discount,
|
||||||
double? otherCharges,
|
double? otherCharges,
|
||||||
|
String? photoUrl,
|
||||||
|
String? localPhotoPath,
|
||||||
double? total,
|
double? total,
|
||||||
}) {
|
}) {
|
||||||
return InvoiceItem(
|
return InvoiceItem(
|
||||||
@@ -53,15 +85,26 @@ class InvoiceItem {
|
|||||||
invoiceId: invoiceId ?? this.invoiceId,
|
invoiceId: invoiceId ?? this.invoiceId,
|
||||||
productId: productId ?? this.productId,
|
productId: productId ?? this.productId,
|
||||||
inventoryItemId: inventoryItemId ?? this.inventoryItemId,
|
inventoryItemId: inventoryItemId ?? this.inventoryItemId,
|
||||||
|
productName: productName ?? this.productName,
|
||||||
|
categoryName: categoryName ?? this.categoryName,
|
||||||
|
commodityCode: commodityCode ?? this.commodityCode,
|
||||||
hsnCode: hsnCode ?? this.hsnCode,
|
hsnCode: hsnCode ?? this.hsnCode,
|
||||||
sku: sku ?? this.sku,
|
sku: sku ?? this.sku,
|
||||||
|
huid: huid ?? this.huid,
|
||||||
description: description ?? this.description,
|
description: description ?? this.description,
|
||||||
quantity: quantity ?? this.quantity,
|
quantity: quantity ?? this.quantity,
|
||||||
|
weight: weight ?? this.weight,
|
||||||
unitPrice: unitPrice ?? this.unitPrice,
|
unitPrice: unitPrice ?? this.unitPrice,
|
||||||
taxRate: taxRate ?? this.taxRate,
|
|
||||||
discount: discount ?? this.discount,
|
|
||||||
makingCharge: makingCharge ?? this.makingCharge,
|
makingCharge: makingCharge ?? this.makingCharge,
|
||||||
|
makingChargesType: makingChargesType ?? this.makingChargesType,
|
||||||
|
taxRate: taxRate ?? this.taxRate,
|
||||||
|
cgst: cgst ?? this.cgst,
|
||||||
|
sgst: sgst ?? this.sgst,
|
||||||
|
igst: igst ?? this.igst,
|
||||||
|
discount: discount ?? this.discount,
|
||||||
otherCharges: otherCharges ?? this.otherCharges,
|
otherCharges: otherCharges ?? this.otherCharges,
|
||||||
|
photoUrl: photoUrl ?? this.photoUrl,
|
||||||
|
localPhotoPath: localPhotoPath ?? this.localPhotoPath,
|
||||||
total: total ?? this.total,
|
total: total ?? this.total,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -69,19 +112,29 @@ class InvoiceItem {
|
|||||||
factory InvoiceItem.fromJson(Map<String, dynamic> json) {
|
factory InvoiceItem.fromJson(Map<String, dynamic> json) {
|
||||||
return InvoiceItem(
|
return InvoiceItem(
|
||||||
id: json['id'],
|
id: json['id'],
|
||||||
invoiceId: json['invoiceId'],
|
invoiceId: json['invoiceId'] ?? json['invoice_id'],
|
||||||
productId: json['productId'],
|
productId: json['productId'] ?? json['product_id'],
|
||||||
inventoryItemId: json['inventoryItemId'],
|
inventoryItemId: json['inventoryItemId'] ?? json['inventory_item_id'],
|
||||||
|
productName: json['productName'] ?? json['product_name'],
|
||||||
|
categoryName: json['categoryName'] ?? json['category_name'],
|
||||||
|
commodityCode: json['commodityCode'] ?? json['commodity_code'],
|
||||||
hsnCode: json['hsnCode'] ?? json['hsn_code'],
|
hsnCode: json['hsnCode'] ?? json['hsn_code'],
|
||||||
sku: json['sku'],
|
sku: json['sku'],
|
||||||
|
huid: json['huid'],
|
||||||
description: json['description'],
|
description: json['description'],
|
||||||
quantity: json['quantity'].toDouble(),
|
quantity: (json['quantity'] as num?)?.toDouble() ?? 1.0,
|
||||||
unitPrice: json['unitPrice'].toDouble(),
|
weight: (json['weight'] as num?)?.toDouble(),
|
||||||
taxRate: json['taxRate']?.toDouble() ?? 0.0,
|
unitPrice: (json['unitPrice'] ?? json['unit_price'] as num?)?.toDouble() ?? 0.0,
|
||||||
discount: json['discount']?.toDouble() ?? 0.0,
|
makingCharge: (json['makingCharge'] ?? json['making_charge'] as num?)?.toDouble() ?? 0.0,
|
||||||
makingCharge: json['makingCharge']?.toDouble() ?? 0.0,
|
makingChargesType: json['makingChargesType'] ?? json['making_charges_type'] ?? 'PER_GRAM',
|
||||||
otherCharges: json['otherCharges']?.toDouble() ?? 0.0,
|
taxRate: (json['taxRate'] ?? json['tax_rate'] as num?)?.toDouble() ?? 0.0,
|
||||||
total: json['total'].toDouble(),
|
cgst: (json['cgst'] as num?)?.toDouble() ?? 0.0,
|
||||||
|
sgst: (json['sgst'] as num?)?.toDouble() ?? 0.0,
|
||||||
|
igst: (json['igst'] as num?)?.toDouble() ?? 0.0,
|
||||||
|
discount: (json['discount'] as num?)?.toDouble() ?? 0.0,
|
||||||
|
otherCharges: (json['otherCharges'] ?? json['other_charges'] as num?)?.toDouble() ?? 0.0,
|
||||||
|
photoUrl: json['photoUrl'] ?? json['photo_url'],
|
||||||
|
total: (json['total'] as num?)?.toDouble() ?? 0.0,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -91,15 +144,25 @@ class InvoiceItem {
|
|||||||
if (invoiceId != null) data['invoiceId'] = invoiceId;
|
if (invoiceId != null) data['invoiceId'] = invoiceId;
|
||||||
if (productId != null) data['productId'] = productId;
|
if (productId != null) data['productId'] = productId;
|
||||||
if (inventoryItemId != null) data['inventoryItemId'] = inventoryItemId;
|
if (inventoryItemId != null) data['inventoryItemId'] = inventoryItemId;
|
||||||
|
if (productName != null) data['productName'] = productName;
|
||||||
|
if (categoryName != null) data['categoryName'] = categoryName;
|
||||||
|
if (commodityCode != null) data['commodityCode'] = commodityCode;
|
||||||
if (hsnCode != null) data['hsnCode'] = hsnCode;
|
if (hsnCode != null) data['hsnCode'] = hsnCode;
|
||||||
if (sku != null) data['sku'] = sku;
|
if (sku != null) data['sku'] = sku;
|
||||||
|
if (huid != null) data['huid'] = huid;
|
||||||
if (description != null) data['description'] = description;
|
if (description != null) data['description'] = description;
|
||||||
data['quantity'] = quantity;
|
data['quantity'] = quantity;
|
||||||
|
if (weight != null) data['weight'] = weight;
|
||||||
data['unitPrice'] = unitPrice;
|
data['unitPrice'] = unitPrice;
|
||||||
data['taxRate'] = taxRate;
|
|
||||||
data['discount'] = discount;
|
|
||||||
data['makingCharge'] = makingCharge;
|
data['makingCharge'] = makingCharge;
|
||||||
|
data['makingChargesType'] = makingChargesType;
|
||||||
|
data['taxRate'] = taxRate;
|
||||||
|
data['cgst'] = cgst;
|
||||||
|
data['sgst'] = sgst;
|
||||||
|
data['igst'] = igst;
|
||||||
|
data['discount'] = discount;
|
||||||
data['otherCharges'] = otherCharges;
|
data['otherCharges'] = otherCharges;
|
||||||
|
if (photoUrl != null) data['photoUrl'] = photoUrl;
|
||||||
data['total'] = total;
|
data['total'] = total;
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
@@ -113,6 +176,9 @@ class Invoice {
|
|||||||
final DateTime? dueDate;
|
final DateTime? dueDate;
|
||||||
final double subtotal;
|
final double subtotal;
|
||||||
final double taxTotal;
|
final double taxTotal;
|
||||||
|
final double cgstTotal;
|
||||||
|
final double sgstTotal;
|
||||||
|
final double igstTotal;
|
||||||
final double discountTotal;
|
final double discountTotal;
|
||||||
final double totalAmount;
|
final double totalAmount;
|
||||||
final double amountPaid;
|
final double amountPaid;
|
||||||
@@ -121,6 +187,7 @@ class Invoice {
|
|||||||
final int? paymentWalletId;
|
final int? paymentWalletId;
|
||||||
final String status;
|
final String status;
|
||||||
final String? notes;
|
final String? notes;
|
||||||
|
final String? invoiceUrl;
|
||||||
final bool isEmi;
|
final bool isEmi;
|
||||||
final double? emiAmount;
|
final double? emiAmount;
|
||||||
final String? emiCycle;
|
final String? emiCycle;
|
||||||
@@ -136,6 +203,9 @@ class Invoice {
|
|||||||
this.dueDate,
|
this.dueDate,
|
||||||
required this.subtotal,
|
required this.subtotal,
|
||||||
this.taxTotal = 0.0,
|
this.taxTotal = 0.0,
|
||||||
|
this.cgstTotal = 0.0,
|
||||||
|
this.sgstTotal = 0.0,
|
||||||
|
this.igstTotal = 0.0,
|
||||||
this.discountTotal = 0.0,
|
this.discountTotal = 0.0,
|
||||||
required this.totalAmount,
|
required this.totalAmount,
|
||||||
this.amountPaid = 0.0,
|
this.amountPaid = 0.0,
|
||||||
@@ -144,6 +214,7 @@ class Invoice {
|
|||||||
this.paymentWalletId,
|
this.paymentWalletId,
|
||||||
this.status = 'DRAFT',
|
this.status = 'DRAFT',
|
||||||
this.notes,
|
this.notes,
|
||||||
|
this.invoiceUrl,
|
||||||
this.isEmi = false,
|
this.isEmi = false,
|
||||||
this.emiAmount,
|
this.emiAmount,
|
||||||
this.emiCycle,
|
this.emiCycle,
|
||||||
@@ -155,28 +226,34 @@ class Invoice {
|
|||||||
factory Invoice.fromJson(Map<String, dynamic> json) {
|
factory Invoice.fromJson(Map<String, dynamic> json) {
|
||||||
return Invoice(
|
return Invoice(
|
||||||
id: json['id'],
|
id: json['id'],
|
||||||
customerId: json['customerId'],
|
customerId: json['customerId'] ?? json['customer_id'],
|
||||||
invoiceNumber: json['invoiceNumber'],
|
invoiceNumber: json['invoiceNumber'] ?? json['invoice_number'] ?? '',
|
||||||
issueDate: DateTime.parse(json['issueDate']),
|
issueDate: json['issueDate'] != null
|
||||||
dueDate: json['dueDate'] != null ? DateTime.parse(json['dueDate']) : null,
|
? DateTime.parse(json['issueDate'])
|
||||||
subtotal: json['subtotal'].toDouble(),
|
: (json['issue_date'] != null ? DateTime.parse(json['issue_date']) : DateTime.now()),
|
||||||
taxTotal: json['taxTotal']?.toDouble() ?? 0.0,
|
dueDate: json['dueDate'] != null ? DateTime.parse(json['dueDate']) : (json['due_date'] != null ? DateTime.parse(json['due_date']) : null),
|
||||||
discountTotal: json['discountTotal']?.toDouble() ?? 0.0,
|
subtotal: (json['subtotal'] as num?)?.toDouble() ?? 0.0,
|
||||||
totalAmount: json['totalAmount'].toDouble(),
|
taxTotal: (json['taxTotal'] ?? json['tax_total'] as num?)?.toDouble() ?? 0.0,
|
||||||
amountPaid: json['amountPaid']?.toDouble() ?? 0.0,
|
cgstTotal: (json['cgstTotal'] ?? json['cgst_total'] as num?)?.toDouble() ?? 0.0,
|
||||||
|
sgstTotal: (json['sgstTotal'] ?? json['sgst_total'] as num?)?.toDouble() ?? 0.0,
|
||||||
|
igstTotal: (json['igstTotal'] ?? json['igst_total'] as num?)?.toDouble() ?? 0.0,
|
||||||
|
discountTotal: (json['discountTotal'] ?? json['discount_total'] as num?)?.toDouble() ?? 0.0,
|
||||||
|
totalAmount: (json['totalAmount'] ?? json['total_amount'] as num?)?.toDouble() ?? 0.0,
|
||||||
|
amountPaid: (json['amountPaid'] ?? json['amount_paid'] as num?)?.toDouble() ?? 0.0,
|
||||||
nextPaymentDate: json['nextPaymentDate'] != null
|
nextPaymentDate: json['nextPaymentDate'] != null
|
||||||
? DateTime.parse(json['nextPaymentDate'])
|
? DateTime.parse(json['nextPaymentDate'])
|
||||||
: null,
|
: (json['next_payment_date'] != null ? DateTime.parse(json['next_payment_date']) : null),
|
||||||
paymentMethod: json['paymentMethod'],
|
paymentMethod: json['paymentMethod'] ?? json['payment_method'],
|
||||||
paymentWalletId: json['paymentWalletId'],
|
paymentWalletId: json['paymentWalletId'] ?? json['payment_wallet_id'],
|
||||||
status: json['status'] ?? 'DRAFT',
|
status: json['status'] ?? 'DRAFT',
|
||||||
notes: json['notes'],
|
notes: json['notes'],
|
||||||
isEmi: json['isEmi'] ?? false,
|
invoiceUrl: json['invoiceUrl'] ?? json['invoice_url'],
|
||||||
emiAmount: json['emiAmount']?.toDouble(),
|
isEmi: json['isEmi'] ?? json['is_emi'] ?? false,
|
||||||
emiCycle: json['emiCycle'],
|
emiAmount: (json['emiAmount'] ?? json['emi_amount'] as num?)?.toDouble(),
|
||||||
|
emiCycle: json['emiCycle'] ?? json['emi_cycle'],
|
||||||
emiStartDate: json['emiStartDate'] != null
|
emiStartDate: json['emiStartDate'] != null
|
||||||
? DateTime.parse(json['emiStartDate'])
|
? DateTime.parse(json['emiStartDate'])
|
||||||
: null,
|
: (json['emi_start_date'] != null ? DateTime.parse(json['emi_start_date']) : null),
|
||||||
items: json['items'] != null
|
items: json['items'] != null
|
||||||
? (json['items'] as List).map((i) => InvoiceItem.fromJson(i)).toList()
|
? (json['items'] as List).map((i) => InvoiceItem.fromJson(i)).toList()
|
||||||
: [],
|
: [],
|
||||||
@@ -194,29 +271,35 @@ class Invoice {
|
|||||||
if (customerId != null) data['customerId'] = customerId;
|
if (customerId != null) data['customerId'] = customerId;
|
||||||
data['invoiceNumber'] = invoiceNumber;
|
data['invoiceNumber'] = invoiceNumber;
|
||||||
data['issueDate'] = issueDate.toIso8601String().split('T')[0];
|
data['issueDate'] = issueDate.toIso8601String().split('T')[0];
|
||||||
if (dueDate != null)
|
if (dueDate != null) {
|
||||||
data['dueDate'] = dueDate!.toIso8601String().split('T')[0];
|
data['dueDate'] = dueDate!.toIso8601String().split('T')[0];
|
||||||
|
}
|
||||||
data['subtotal'] = subtotal;
|
data['subtotal'] = subtotal;
|
||||||
data['taxTotal'] = taxTotal;
|
data['taxTotal'] = taxTotal;
|
||||||
|
data['cgstTotal'] = cgstTotal;
|
||||||
|
data['sgstTotal'] = sgstTotal;
|
||||||
|
data['igstTotal'] = igstTotal;
|
||||||
data['discountTotal'] = discountTotal;
|
data['discountTotal'] = discountTotal;
|
||||||
data['totalAmount'] = totalAmount;
|
data['totalAmount'] = totalAmount;
|
||||||
if (amountPaid > 0) data['amountPaid'] = amountPaid;
|
if (amountPaid > 0) data['amountPaid'] = amountPaid;
|
||||||
if (nextPaymentDate != null)
|
if (nextPaymentDate != null) {
|
||||||
data['nextPaymentDate'] = nextPaymentDate!.toIso8601String().split(
|
data['nextPaymentDate'] = nextPaymentDate!.toIso8601String().split('T')[0];
|
||||||
'T',
|
}
|
||||||
)[0];
|
|
||||||
if (paymentMethod != null) data['paymentMethod'] = paymentMethod;
|
if (paymentMethod != null) data['paymentMethod'] = paymentMethod;
|
||||||
if (paymentWalletId != null) data['paymentWalletId'] = paymentWalletId;
|
if (paymentWalletId != null) data['paymentWalletId'] = paymentWalletId;
|
||||||
data['status'] = status;
|
data['status'] = status;
|
||||||
if (notes != null) data['notes'] = notes;
|
if (notes != null) data['notes'] = notes;
|
||||||
|
if (invoiceUrl != null) data['invoiceUrl'] = invoiceUrl;
|
||||||
data['isEmi'] = isEmi;
|
data['isEmi'] = isEmi;
|
||||||
if (emiAmount != null) data['emiAmount'] = emiAmount;
|
if (emiAmount != null) data['emiAmount'] = emiAmount;
|
||||||
if (emiCycle != null) data['emiCycle'] = emiCycle;
|
if (emiCycle != null) data['emiCycle'] = emiCycle;
|
||||||
if (emiStartDate != null)
|
if (emiStartDate != null) {
|
||||||
data['emiStartDate'] = emiStartDate!.toIso8601String().split('T')[0];
|
data['emiStartDate'] = emiStartDate!.toIso8601String().split('T')[0];
|
||||||
|
}
|
||||||
data['items'] = items.map((i) => i.toJson()).toList();
|
data['items'] = items.map((i) => i.toJson()).toList();
|
||||||
if (payments != null)
|
if (payments != null) {
|
||||||
data['payments'] = payments!.map((i) => i.toJson()).toList();
|
data['payments'] = payments!.map((i) => i.toJson()).toList();
|
||||||
|
}
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -243,14 +326,14 @@ class InvoicePayment {
|
|||||||
factory InvoicePayment.fromJson(Map<String, dynamic> json) {
|
factory InvoicePayment.fromJson(Map<String, dynamic> json) {
|
||||||
return InvoicePayment(
|
return InvoicePayment(
|
||||||
id: json['id'],
|
id: json['id'],
|
||||||
invoiceId: json['invoiceId'],
|
invoiceId: json['invoiceId'] ?? json['invoice_id'],
|
||||||
amount: json['amount'].toDouble(),
|
amount: (json['amount'] as num?)?.toDouble() ?? 0.0,
|
||||||
paymentDate: json['paymentDate'] != null
|
paymentDate: json['paymentDate'] != null
|
||||||
? DateTime.parse(json['paymentDate'])
|
? DateTime.parse(json['paymentDate'])
|
||||||
: null,
|
: (json['payment_date'] != null ? DateTime.parse(json['payment_date']) : null),
|
||||||
paymentMethod: json['paymentMethod'] ?? 'Cash',
|
paymentMethod: json['paymentMethod'] ?? json['payment_method'] ?? 'Cash',
|
||||||
emiInstallmentNumber: json['emiInstallmentNumber'],
|
emiInstallmentNumber: json['emiInstallmentNumber'] ?? json['emi_installment_number'],
|
||||||
walletId: json['walletId'],
|
walletId: json['walletId'] ?? json['wallet_id'],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -259,11 +342,13 @@ class InvoicePayment {
|
|||||||
if (id != null) data['id'] = id;
|
if (id != null) data['id'] = id;
|
||||||
if (invoiceId != null) data['invoiceId'] = invoiceId;
|
if (invoiceId != null) data['invoiceId'] = invoiceId;
|
||||||
data['amount'] = amount;
|
data['amount'] = amount;
|
||||||
if (paymentDate != null)
|
if (paymentDate != null) {
|
||||||
data['paymentDate'] = paymentDate!.toIso8601String().split('T')[0];
|
data['paymentDate'] = paymentDate!.toIso8601String().split('T')[0];
|
||||||
|
}
|
||||||
data['paymentMethod'] = paymentMethod;
|
data['paymentMethod'] = paymentMethod;
|
||||||
if (emiInstallmentNumber != null)
|
if (emiInstallmentNumber != null) {
|
||||||
data['emiInstallmentNumber'] = emiInstallmentNumber;
|
data['emiInstallmentNumber'] = emiInstallmentNumber;
|
||||||
|
}
|
||||||
if (walletId != null) data['walletId'] = walletId;
|
if (walletId != null) data['walletId'] = walletId;
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -2,14 +2,12 @@ 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/lucide_icons.dart';
|
||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
import '../../../core/widgets/shimmer_loading.dart';
|
|
||||||
import '../providers/invoices_provider.dart';
|
import '../providers/invoices_provider.dart';
|
||||||
import '../domain/invoice.dart';
|
import '../domain/invoice.dart';
|
||||||
|
import '../providers/customers_provider.dart';
|
||||||
|
import '../domain/customer.dart';
|
||||||
import 'invoice_builder_screen.dart';
|
import 'invoice_builder_screen.dart';
|
||||||
import 'invoice_details_screen.dart';
|
import 'invoice_details_screen.dart';
|
||||||
import '../../transactions/providers/providers.dart';
|
|
||||||
import 'widgets/receive_payment_sheet.dart';
|
|
||||||
import '../providers/customers_provider.dart';
|
|
||||||
|
|
||||||
class InvoicesListScreen extends ConsumerStatefulWidget {
|
class InvoicesListScreen extends ConsumerStatefulWidget {
|
||||||
const InvoicesListScreen({super.key});
|
const InvoicesListScreen({super.key});
|
||||||
@@ -24,277 +22,115 @@ class _InvoicesListScreenState extends ConsumerState<InvoicesListScreen> {
|
|||||||
|
|
||||||
String _getStatusLabel(Invoice invoice) {
|
String _getStatusLabel(Invoice invoice) {
|
||||||
if (invoice.isEmi && invoice.status != 'PAID') return 'EMI';
|
if (invoice.isEmi && invoice.status != 'PAID') return 'EMI';
|
||||||
if (invoice.status == 'PAID') return 'Fully Paid';
|
if (invoice.status == 'PAID' || invoice.amountPaid >= invoice.totalAmount && invoice.totalAmount > 0) return 'PAID';
|
||||||
if (invoice.status == 'PARTIAL') return 'Partially Paid';
|
if (invoice.amountPaid > 0) return 'PARTIAL';
|
||||||
return invoice.status;
|
return invoice.status;
|
||||||
}
|
}
|
||||||
|
|
||||||
Color _getStatusColor(Invoice invoice) {
|
Color _getStatusColor(Invoice invoice) {
|
||||||
if (invoice.isEmi && invoice.status != 'PAID') return Colors.purple;
|
if (invoice.isEmi && invoice.status != 'PAID') return Colors.purple;
|
||||||
switch (invoice.status) {
|
final label = _getStatusLabel(invoice);
|
||||||
case 'DRAFT':
|
switch (label) {
|
||||||
return Colors.grey;
|
|
||||||
case 'FINALIZED':
|
|
||||||
return Colors.orange;
|
|
||||||
case 'PAID':
|
case 'PAID':
|
||||||
return Colors.green;
|
return Colors.green;
|
||||||
case 'PARTIAL':
|
case 'PARTIAL':
|
||||||
return Colors.blue;
|
return Colors.blue;
|
||||||
|
case 'FINALIZED':
|
||||||
|
return Colors.orange;
|
||||||
case 'OVERDUE':
|
case 'OVERDUE':
|
||||||
return Colors.red;
|
return Colors.red;
|
||||||
case 'CANCELLED':
|
case 'DRAFT':
|
||||||
return Colors.black;
|
|
||||||
default:
|
default:
|
||||||
return Colors.grey;
|
return Colors.grey;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _showPaymentHistory(
|
@override
|
||||||
BuildContext context,
|
void dispose() {
|
||||||
WidgetRef ref,
|
_searchController.dispose();
|
||||||
Invoice invoice,
|
super.dispose();
|
||||||
) {
|
|
||||||
showModalBottomSheet(
|
|
||||||
context: context,
|
|
||||||
isScrollControlled: true,
|
|
||||||
shape: const RoundedRectangleBorder(
|
|
||||||
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
|
|
||||||
),
|
|
||||||
builder: (ctx) {
|
|
||||||
return Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(vertical: 24, horizontal: 16),
|
|
||||||
child: Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
'Payment History: ${invoice.invoiceNumber}',
|
|
||||||
style: const TextStyle(
|
|
||||||
fontSize: 18,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
FutureBuilder<List<InvoicePayment>>(
|
|
||||||
future: ref
|
|
||||||
.read(invoicesProvider.notifier)
|
|
||||||
.fetchPaymentsForInvoice(invoice.id!),
|
|
||||||
builder: (context, snapshot) {
|
|
||||||
if (snapshot.connectionState == ConnectionState.waiting) {
|
|
||||||
return const Center(child: CircularProgressIndicator());
|
|
||||||
}
|
|
||||||
if (snapshot.hasError) {
|
|
||||||
return Center(child: Text('Error: ${snapshot.error}'));
|
|
||||||
}
|
|
||||||
final payments = snapshot.data;
|
|
||||||
if (payments == null || payments.isEmpty) {
|
|
||||||
return const Padding(
|
|
||||||
padding: EdgeInsets.all(24.0),
|
|
||||||
child: Text('No payments recorded yet.'),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
final wallets = ref.read(walletProvider).value ?? [];
|
|
||||||
|
|
||||||
return SingleChildScrollView(
|
|
||||||
scrollDirection: Axis.horizontal,
|
|
||||||
child: DataTable(
|
|
||||||
headingRowColor: WidgetStateProperty.resolveWith(
|
|
||||||
(states) => Colors.grey.shade100,
|
|
||||||
),
|
|
||||||
columnSpacing: 24,
|
|
||||||
columns: const [
|
|
||||||
DataColumn(
|
|
||||||
label: Text(
|
|
||||||
'Date',
|
|
||||||
style: TextStyle(fontWeight: FontWeight.bold),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
DataColumn(
|
|
||||||
label: Text(
|
|
||||||
'Wallet',
|
|
||||||
style: TextStyle(fontWeight: FontWeight.bold),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
DataColumn(
|
|
||||||
label: Text(
|
|
||||||
'Method',
|
|
||||||
style: TextStyle(fontWeight: FontWeight.bold),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
DataColumn(
|
|
||||||
label: Text(
|
|
||||||
'Amount',
|
|
||||||
style: TextStyle(fontWeight: FontWeight.bold),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
rows: payments.map((p) {
|
|
||||||
final walletName = wallets
|
|
||||||
.firstWhere(
|
|
||||||
(w) => w.id == p.walletId,
|
|
||||||
orElse: () => wallets.first,
|
|
||||||
)
|
|
||||||
.name;
|
|
||||||
return DataRow(
|
|
||||||
cells: [
|
|
||||||
DataCell(
|
|
||||||
Text(
|
|
||||||
p.paymentDate != null
|
|
||||||
? DateFormat(
|
|
||||||
'dd MMM yyyy',
|
|
||||||
).format(p.paymentDate!)
|
|
||||||
: '-',
|
|
||||||
),
|
|
||||||
),
|
|
||||||
DataCell(
|
|
||||||
Text(p.walletId != null ? walletName : '-'),
|
|
||||||
),
|
|
||||||
DataCell(Text(p.paymentMethod ?? '-')),
|
|
||||||
DataCell(
|
|
||||||
Text(
|
|
||||||
'₹${p.amount.toStringAsFixed(2)}',
|
|
||||||
style: TextStyle(
|
|
||||||
color: Colors.green.shade700,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}).toList(),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
SizedBox(
|
|
||||||
width: double.infinity,
|
|
||||||
child: TextButton(
|
|
||||||
onPressed: () => Navigator.pop(ctx),
|
|
||||||
child: const Text('Close'),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _showReceivePaymentSheet(BuildContext context, Invoice invoice) async {
|
|
||||||
final result = await showModalBottomSheet(
|
|
||||||
context: context,
|
|
||||||
isScrollControlled: true,
|
|
||||||
backgroundColor: Colors.transparent,
|
|
||||||
builder: (context) => ReceivePaymentSheet(invoice: invoice),
|
|
||||||
);
|
|
||||||
if (result == true) {
|
|
||||||
ref.read(invoicesProvider.notifier).refresh();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final invoicesState = ref.watch(invoicesProvider);
|
final invoicesState = ref.watch(invoicesProvider);
|
||||||
final customersState = ref.watch(customersProvider);
|
final customersState = ref.watch(customersProvider);
|
||||||
final customers = customersState.value ?? [];
|
final isDark = Theme.of(context).brightness == Brightness.dark;
|
||||||
|
|
||||||
final formatCurrency = NumberFormat.currency(symbol: '₹');
|
final formatCurrency = NumberFormat.currency(symbol: '₹');
|
||||||
final formatDate = DateFormat('MMM dd, yyyy');
|
final formatDate = DateFormat('MMM dd, yyyy');
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: Colors.grey[100],
|
backgroundColor: isDark ? const Color(0xFF0F172A) : const Color(0xFFF8FAFC),
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: const Text(
|
title: const Text(
|
||||||
'Invoices',
|
'Sales Invoices',
|
||||||
style: TextStyle(fontWeight: FontWeight.bold),
|
style: TextStyle(fontWeight: FontWeight.bold),
|
||||||
),
|
),
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
backgroundColor: Colors.white,
|
backgroundColor: isDark ? const Color(0xFF1E293B) : Colors.white,
|
||||||
foregroundColor: Colors.black,
|
foregroundColor: isDark ? Colors.white : Colors.black,
|
||||||
centerTitle: true,
|
centerTitle: true,
|
||||||
),
|
),
|
||||||
body: Column(
|
body: Column(
|
||||||
children: [
|
children: [
|
||||||
|
// Unified Search Header
|
||||||
Container(
|
Container(
|
||||||
color: Colors.white,
|
color: isDark ? const Color(0xFF1E293B) : Colors.white,
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||||
child: TextField(
|
child: TextField(
|
||||||
controller: _searchController,
|
controller: _searchController,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
hintText: 'Search by Invoice # or Customer',
|
hintText: 'Search Invoice # or Customer...',
|
||||||
prefixIcon: const Icon(LucideIcons.search, color: Colors.grey),
|
prefixIcon: const Icon(LucideIcons.search, color: Colors.grey),
|
||||||
contentPadding: const EdgeInsets.symmetric(vertical: 14),
|
contentPadding: const EdgeInsets.symmetric(vertical: 14),
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
borderSide: BorderSide(color: isDark ? Colors.white12 : Colors.grey.shade300),
|
||||||
|
),
|
||||||
|
enabledBorder: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
borderSide: BorderSide(color: isDark ? Colors.white12 : Colors.grey.shade300),
|
||||||
|
),
|
||||||
suffixIcon: _searchQuery.isNotEmpty
|
suffixIcon: _searchQuery.isNotEmpty
|
||||||
? IconButton(
|
? IconButton(
|
||||||
icon: const Icon(LucideIcons.x, size: 20),
|
icon: const Icon(LucideIcons.xCircle, size: 20),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
_searchController.clear();
|
_searchController.clear();
|
||||||
setState(() {
|
setState(() => _searchQuery = '');
|
||||||
_searchQuery = '';
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
: null,
|
: null,
|
||||||
),
|
),
|
||||||
onChanged: (value) {
|
onChanged: (val) => setState(() => _searchQuery = val.trim()),
|
||||||
setState(() {
|
|
||||||
_searchQuery = value.toLowerCase();
|
|
||||||
});
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: invoicesState.when(
|
child: invoicesState.when(
|
||||||
loading: () => ListView.builder(
|
data: (invoices) {
|
||||||
padding: const EdgeInsets.all(16),
|
final customers = customersState.value ?? [];
|
||||||
itemCount: 5,
|
final filtered = invoices.where((inv) {
|
||||||
itemBuilder: (context, index) => const Padding(
|
final matchesInv = inv.invoiceNumber.toLowerCase().contains(_searchQuery.toLowerCase());
|
||||||
padding: EdgeInsets.only(bottom: 12),
|
final customer = customers.where((c) => c.id == inv.customerId).firstOrNull;
|
||||||
child: ShimmerCard(),
|
final matchesCust = customer != null && customer.name.toLowerCase().contains(_searchQuery.toLowerCase());
|
||||||
),
|
return matchesInv || matchesCust;
|
||||||
),
|
|
||||||
error: (err, stack) => Center(child: Text('Error: $err')),
|
|
||||||
data: (allInvoices) {
|
|
||||||
final invoices = allInvoices.where((invoice) {
|
|
||||||
final matchesInvoice = invoice.invoiceNumber
|
|
||||||
.toLowerCase()
|
|
||||||
.contains(_searchQuery);
|
|
||||||
final customer = customers.firstWhere(
|
|
||||||
(c) => c.id == invoice.customerId,
|
|
||||||
orElse: () => customers.first,
|
|
||||||
);
|
|
||||||
final customerName = invoice.customerId != null
|
|
||||||
? customer.name.toLowerCase()
|
|
||||||
: '';
|
|
||||||
final matchesCustomer = customerName.contains(_searchQuery);
|
|
||||||
return matchesInvoice || matchesCustomer;
|
|
||||||
}).toList();
|
}).toList();
|
||||||
|
|
||||||
if (invoices.isEmpty) {
|
if (filtered.isEmpty) {
|
||||||
return RefreshIndicator(
|
return RefreshIndicator(
|
||||||
onRefresh: () async {
|
onRefresh: () => ref.refresh(invoicesProvider.future),
|
||||||
await ref.read(invoicesProvider.notifier).refresh();
|
|
||||||
},
|
|
||||||
child: ListView(
|
child: ListView(
|
||||||
physics: const AlwaysScrollableScrollPhysics(),
|
physics: const AlwaysScrollableScrollPhysics(),
|
||||||
children: const [
|
children: const [
|
||||||
SizedBox(height: 100),
|
SizedBox(height: 120),
|
||||||
Center(
|
Center(
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
Icon(
|
Icon(LucideIcons.fileText, size: 64, color: Colors.grey),
|
||||||
LucideIcons.receipt,
|
|
||||||
size: 64,
|
|
||||||
color: Colors.grey,
|
|
||||||
),
|
|
||||||
SizedBox(height: 16),
|
SizedBox(height: 16),
|
||||||
Text(
|
Text(
|
||||||
'No invoices found.',
|
'No sales invoices found.',
|
||||||
style: TextStyle(
|
style: TextStyle(color: Colors.grey, fontSize: 16),
|
||||||
color: Colors.grey,
|
|
||||||
fontSize: 16,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -305,32 +141,25 @@ class _InvoicesListScreenState extends ConsumerState<InvoicesListScreen> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return RefreshIndicator(
|
return RefreshIndicator(
|
||||||
onRefresh: () async {
|
onRefresh: () => ref.refresh(invoicesProvider.future),
|
||||||
await ref.read(invoicesProvider.notifier).refresh();
|
|
||||||
},
|
|
||||||
child: ListView.builder(
|
child: ListView.builder(
|
||||||
physics: const AlwaysScrollableScrollPhysics(),
|
|
||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.all(16),
|
||||||
itemCount: invoices.length,
|
itemCount: filtered.length,
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
final invoice = invoices[index];
|
final invoice = filtered[index];
|
||||||
double remaining =
|
final customer = customers.where((c) => c.id == invoice.customerId).firstOrNull;
|
||||||
invoice.totalAmount - invoice.amountPaid;
|
final statusColor = _getStatusColor(invoice);
|
||||||
final customer = invoice.customerId != null
|
final statusLabel = _getStatusLabel(invoice);
|
||||||
? customers.firstWhere(
|
|
||||||
(c) => c.id == invoice.customerId,
|
|
||||||
orElse: () => customers.first,
|
|
||||||
)
|
|
||||||
: null;
|
|
||||||
|
|
||||||
return Container(
|
return Container(
|
||||||
margin: const EdgeInsets.only(bottom: 16),
|
margin: const EdgeInsets.only(bottom: 16),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.white,
|
color: isDark ? const Color(0xFF1E293B) : Colors.white,
|
||||||
borderRadius: BorderRadius.circular(20),
|
borderRadius: BorderRadius.circular(20),
|
||||||
|
border: Border.all(color: isDark ? Colors.white10 : Colors.grey.shade200),
|
||||||
boxShadow: [
|
boxShadow: [
|
||||||
BoxShadow(
|
BoxShadow(
|
||||||
color: Colors.grey.withOpacity(0.08),
|
color: Colors.black.withValues(alpha: isDark ? 0.2 : 0.04),
|
||||||
blurRadius: 10,
|
blurRadius: 10,
|
||||||
offset: const Offset(0, 4),
|
offset: const Offset(0, 4),
|
||||||
),
|
),
|
||||||
@@ -341,13 +170,21 @@ class _InvoicesListScreenState extends ConsumerState<InvoicesListScreen> {
|
|||||||
borderRadius: BorderRadius.circular(20),
|
borderRadius: BorderRadius.circular(20),
|
||||||
child: InkWell(
|
child: InkWell(
|
||||||
onTap: () {
|
onTap: () {
|
||||||
|
if (invoice.status == 'DRAFT') {
|
||||||
Navigator.push(
|
Navigator.push(
|
||||||
context,
|
context,
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
builder: (_) =>
|
builder: (_) => InvoiceBuilderScreen(existingInvoice: invoice),
|
||||||
InvoiceDetailsScreen(invoice: invoice),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
} else {
|
||||||
|
Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (_) => InvoiceDetailsScreen(invoice: invoice),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
borderRadius: BorderRadius.circular(20),
|
borderRadius: BorderRadius.circular(20),
|
||||||
child: Padding(
|
child: Padding(
|
||||||
@@ -355,13 +192,9 @@ class _InvoicesListScreenState extends ConsumerState<InvoicesListScreen> {
|
|||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
|
// Row 1: Invoice Number + Status Pill
|
||||||
Row(
|
Row(
|
||||||
mainAxisAlignment:
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
MainAxisAlignment.spaceBetween,
|
|
||||||
children: [
|
|
||||||
Column(
|
|
||||||
crossAxisAlignment:
|
|
||||||
CrossAxisAlignment.start,
|
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
invoice.invoiceNumber,
|
invoice.invoiceNumber,
|
||||||
@@ -370,34 +203,17 @@ class _InvoicesListScreenState extends ConsumerState<InvoicesListScreen> {
|
|||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (customer != null)
|
|
||||||
Text(
|
|
||||||
customer.name,
|
|
||||||
style: TextStyle(
|
|
||||||
color: Colors.grey[700],
|
|
||||||
fontSize: 13,
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
Container(
|
Container(
|
||||||
padding: const EdgeInsets.symmetric(
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 5),
|
||||||
horizontal: 8,
|
|
||||||
vertical: 4,
|
|
||||||
),
|
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: _getStatusColor(
|
color: statusColor.withValues(alpha: 0.12),
|
||||||
invoice,
|
borderRadius: BorderRadius.circular(20),
|
||||||
).withOpacity(0.1),
|
border: Border.all(color: statusColor.withValues(alpha: 0.3), width: 0.8),
|
||||||
borderRadius: BorderRadius.circular(
|
|
||||||
12,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
child: Text(
|
child: Text(
|
||||||
_getStatusLabel(invoice),
|
statusLabel,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: _getStatusColor(invoice),
|
color: statusColor,
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
),
|
),
|
||||||
@@ -406,172 +222,48 @@ class _InvoicesListScreenState extends ConsumerState<InvoicesListScreen> {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
Row(
|
|
||||||
mainAxisAlignment:
|
// Row 2: Customer Name
|
||||||
MainAxisAlignment.spaceBetween,
|
|
||||||
children: [
|
|
||||||
Column(
|
|
||||||
crossAxisAlignment:
|
|
||||||
CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
'Issued: ${formatDate.format(invoice.issueDate)}',
|
|
||||||
style: TextStyle(
|
|
||||||
color: Colors.grey[600],
|
|
||||||
fontSize: 13,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
if (invoice.dueDate != null)
|
|
||||||
Text(
|
|
||||||
'Due: ${formatDate.format(invoice.dueDate!)}',
|
|
||||||
style: TextStyle(
|
|
||||||
color: Colors.grey[600],
|
|
||||||
fontSize: 13,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
if (invoice.nextPaymentDate != null &&
|
|
||||||
remaining > 0)
|
|
||||||
Text(
|
|
||||||
'Next Pmt: ${formatDate.format(invoice.nextPaymentDate!)}',
|
|
||||||
style: TextStyle(
|
|
||||||
color: Colors.orange.shade700,
|
|
||||||
fontSize: 13,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
Column(
|
|
||||||
crossAxisAlignment:
|
|
||||||
CrossAxisAlignment.end,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
formatCurrency.format(
|
|
||||||
invoice.totalAmount,
|
|
||||||
),
|
|
||||||
style: const TextStyle(
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
fontSize: 18,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
if (invoice.amountPaid > 0)
|
|
||||||
Text(
|
|
||||||
'Paid: ${formatCurrency.format(invoice.amountPaid)}',
|
|
||||||
style: TextStyle(
|
|
||||||
color: Colors.green.shade700,
|
|
||||||
fontSize: 12,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
if (remaining > 0 &&
|
|
||||||
invoice.status != 'DRAFT')
|
|
||||||
Text(
|
|
||||||
'Bal: ${formatCurrency.format(remaining)}',
|
|
||||||
style: TextStyle(
|
|
||||||
color: Colors.red.shade700,
|
|
||||||
fontSize: 12,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
if (invoice.isEmi &&
|
|
||||||
invoice.emiAmount != null) ...[
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
Container(
|
|
||||||
padding: const EdgeInsets.all(8),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Colors.purple.withOpacity(0.05),
|
|
||||||
borderRadius: BorderRadius.circular(8),
|
|
||||||
border: Border.all(
|
|
||||||
color: Colors.purple.withOpacity(0.2),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
const Icon(
|
|
||||||
LucideIcons.calendarClock,
|
|
||||||
size: 16,
|
|
||||||
color: Colors.purple,
|
|
||||||
),
|
|
||||||
const SizedBox(width: 8),
|
|
||||||
Text(
|
|
||||||
'EMI: ${formatCurrency.format(invoice.emiAmount)} / ${invoice.emiCycle}',
|
|
||||||
style: const TextStyle(
|
|
||||||
color: Colors.purple,
|
|
||||||
fontSize: 12,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
const Divider(height: 24),
|
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
if (invoice.amountPaid > 0)
|
const Icon(LucideIcons.user, size: 16, color: Colors.grey),
|
||||||
Expanded(
|
|
||||||
child: OutlinedButton.icon(
|
|
||||||
onPressed: () =>
|
|
||||||
_showPaymentHistory(
|
|
||||||
context,
|
|
||||||
ref,
|
|
||||||
invoice,
|
|
||||||
),
|
|
||||||
icon: const Icon(
|
|
||||||
LucideIcons.history,
|
|
||||||
size: 14,
|
|
||||||
),
|
|
||||||
label: const Text(
|
|
||||||
'History',
|
|
||||||
style: TextStyle(fontSize: 13),
|
|
||||||
),
|
|
||||||
style: OutlinedButton.styleFrom(
|
|
||||||
foregroundColor:
|
|
||||||
Colors.blue.shade700,
|
|
||||||
side: BorderSide(
|
|
||||||
color: Colors.blue.shade200,
|
|
||||||
),
|
|
||||||
padding:
|
|
||||||
const EdgeInsets.symmetric(
|
|
||||||
horizontal: 8,
|
|
||||||
vertical: 8,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
if (invoice.amountPaid > 0 &&
|
|
||||||
remaining > 0)
|
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
if (remaining > 0)
|
|
||||||
Expanded(
|
Expanded(
|
||||||
child: ElevatedButton.icon(
|
child: Text(
|
||||||
onPressed: () =>
|
customer?.name ?? 'Walk-in Customer',
|
||||||
_showReceivePaymentSheet(
|
style: const TextStyle(fontWeight: FontWeight.w600, fontSize: 14),
|
||||||
context,
|
maxLines: 1,
|
||||||
invoice,
|
overflow: TextOverflow.ellipsis,
|
||||||
),
|
|
||||||
icon: const Icon(
|
|
||||||
LucideIcons.indianRupee,
|
|
||||||
size: 14,
|
|
||||||
),
|
|
||||||
label: const Text(
|
|
||||||
'Receive',
|
|
||||||
style: TextStyle(fontSize: 13),
|
|
||||||
),
|
|
||||||
style: ElevatedButton.styleFrom(
|
|
||||||
backgroundColor:
|
|
||||||
Colors.blue.shade50,
|
|
||||||
foregroundColor:
|
|
||||||
Colors.blue.shade700,
|
|
||||||
elevation: 0,
|
|
||||||
padding:
|
|
||||||
const EdgeInsets.symmetric(
|
|
||||||
horizontal: 8,
|
|
||||||
vertical: 8,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
if (invoice.items.isNotEmpty)
|
||||||
|
Text(
|
||||||
|
'${invoice.items.length} ${invoice.items.length == 1 ? "item" : "items"}',
|
||||||
|
style: TextStyle(fontSize: 12, color: Colors.grey.shade500),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
|
||||||
|
// Row 3: Date & Total Amount
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
const Icon(LucideIcons.calendar, size: 16, color: Colors.grey),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Text(
|
||||||
|
formatDate.format(invoice.issueDate),
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.grey.shade600,
|
||||||
|
fontSize: 13,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Spacer(),
|
||||||
|
Text(
|
||||||
|
formatCurrency.format(invoice.totalAmount),
|
||||||
|
style: const TextStyle(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
fontSize: 16,
|
||||||
|
color: Colors.green,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -586,6 +278,8 @@ class _InvoicesListScreenState extends ConsumerState<InvoicesListScreen> {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
loading: () => const Center(child: CircularProgressIndicator()),
|
||||||
|
error: (e, stack) => Center(child: Text('Error: $e')),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -598,8 +292,9 @@ class _InvoicesListScreenState extends ConsumerState<InvoicesListScreen> {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
icon: const Icon(LucideIcons.plus),
|
icon: const Icon(LucideIcons.plus),
|
||||||
label: const Text('Create Invoice'),
|
label: const Text('Create Invoice', style: TextStyle(fontWeight: FontWeight.bold)),
|
||||||
backgroundColor: Colors.blue,
|
backgroundColor: Colors.blue,
|
||||||
|
foregroundColor: Colors.white,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,171 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
import 'package:lucide_icons/lucide_icons.dart';
|
||||||
|
import '../../../../core/widgets/premium_text_field.dart';
|
||||||
|
import '../../../../core/widgets/smart_search_dropdown.dart';
|
||||||
|
import '../../providers/customers_provider.dart';
|
||||||
|
import '../../domain/customer.dart';
|
||||||
|
import '../../../business/providers/indian_states_provider.dart';
|
||||||
|
|
||||||
|
class QuickAddCustomerSheet extends ConsumerStatefulWidget {
|
||||||
|
const QuickAddCustomerSheet({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
ConsumerState<QuickAddCustomerSheet> createState() => _QuickAddCustomerSheetState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _QuickAddCustomerSheetState extends ConsumerState<QuickAddCustomerSheet> {
|
||||||
|
final _formKey = GlobalKey<FormState>();
|
||||||
|
final _nameCtrl = TextEditingController();
|
||||||
|
final _phoneCtrl = TextEditingController();
|
||||||
|
final _addressCtrl = TextEditingController();
|
||||||
|
final _gstinCtrl = TextEditingController();
|
||||||
|
int? _selectedStateId;
|
||||||
|
bool _isLoading = false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_nameCtrl.dispose();
|
||||||
|
_phoneCtrl.dispose();
|
||||||
|
_addressCtrl.dispose();
|
||||||
|
_gstinCtrl.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _save() async {
|
||||||
|
if (!_formKey.currentState!.validate()) return;
|
||||||
|
|
||||||
|
setState(() => _isLoading = true);
|
||||||
|
try {
|
||||||
|
final customer = Customer(
|
||||||
|
name: _nameCtrl.text.trim(),
|
||||||
|
phone: _phoneCtrl.text.trim().isEmpty ? null : _phoneCtrl.text.trim(),
|
||||||
|
address: _addressCtrl.text.trim().isEmpty ? null : _addressCtrl.text.trim(),
|
||||||
|
gstin: _gstinCtrl.text.trim().isEmpty ? null : _gstinCtrl.text.trim(),
|
||||||
|
stateId: _selectedStateId,
|
||||||
|
);
|
||||||
|
|
||||||
|
final created = await ref.read(customersProvider.notifier).addCustomer(customer);
|
||||||
|
if (mounted) Navigator.pop(context, created ?? true);
|
||||||
|
} catch (e) {
|
||||||
|
if (mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Error: $e')));
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
if (mounted) setState(() => _isLoading = false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Theme.of(context).scaffoldBackgroundColor,
|
||||||
|
borderRadius: const BorderRadius.vertical(top: Radius.circular(20)),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.fromLTRB(8, 8, 8, 0),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(LucideIcons.chevronLeft),
|
||||||
|
onPressed: () => Navigator.pop(context),
|
||||||
|
),
|
||||||
|
const Expanded(
|
||||||
|
child: Text(
|
||||||
|
'Quick Add Customer',
|
||||||
|
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Divider(),
|
||||||
|
Padding(
|
||||||
|
padding: EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom),
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
padding: const EdgeInsets.all(24),
|
||||||
|
child: Form(
|
||||||
|
key: _formKey,
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: [
|
||||||
|
PremiumTextField(
|
||||||
|
controller: _nameCtrl,
|
||||||
|
labelText: 'Customer Name *',
|
||||||
|
prefixIcon: const Icon(LucideIcons.user),
|
||||||
|
textCapitalization: TextCapitalization.words,
|
||||||
|
validator: (val) => val == null || val.isEmpty ? 'Required' : null,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
PremiumTextField(
|
||||||
|
controller: _phoneCtrl,
|
||||||
|
labelText: 'Phone Number',
|
||||||
|
prefixIcon: const Icon(LucideIcons.phone),
|
||||||
|
keyboardType: TextInputType.phone,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Consumer(
|
||||||
|
builder: (context, ref, _) {
|
||||||
|
final statesState = ref.watch(indianStatesProvider);
|
||||||
|
return statesState.when(
|
||||||
|
data: (states) => SmartSearchDropdown<int>(
|
||||||
|
labelText: 'State',
|
||||||
|
hintText: 'Select State',
|
||||||
|
value: _selectedStateId,
|
||||||
|
items: states.map((s) => s.id).toList(),
|
||||||
|
itemAsString: (id) => states.firstWhere((s) => s.id == id).name,
|
||||||
|
onChanged: (val) {
|
||||||
|
setState(() {
|
||||||
|
_selectedStateId = val;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
loading: () => const Center(child: CircularProgressIndicator()),
|
||||||
|
error: (e, _) => Text('Error loading states: $e'),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
PremiumTextField(
|
||||||
|
controller: _gstinCtrl,
|
||||||
|
labelText: 'GSTIN (Optional)',
|
||||||
|
prefixIcon: const Icon(LucideIcons.fileText),
|
||||||
|
textCapitalization: TextCapitalization.characters,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
PremiumTextField(
|
||||||
|
controller: _addressCtrl,
|
||||||
|
labelText: 'Address',
|
||||||
|
prefixIcon: const Icon(LucideIcons.home),
|
||||||
|
maxLines: 2,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed: _isLoading ? null : _save,
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||||
|
backgroundColor: Colors.blue,
|
||||||
|
foregroundColor: Colors.white,
|
||||||
|
),
|
||||||
|
child: _isLoading
|
||||||
|
? const SizedBox(
|
||||||
|
width: 20,
|
||||||
|
height: 20,
|
||||||
|
child: CircularProgressIndicator(strokeWidth: 2, color: Colors.white),
|
||||||
|
)
|
||||||
|
: const Text('Save Customer', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -42,7 +42,7 @@ class CustomersNotifier extends AsyncNotifier<List<Customer>> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> addCustomer(Customer customer, {XFile? photo}) async {
|
Future<Customer?> addCustomer(Customer customer, {XFile? photo}) async {
|
||||||
try {
|
try {
|
||||||
final response = await DioClient().dio.post(
|
final response = await DioClient().dio.post(
|
||||||
'/customers',
|
'/customers',
|
||||||
@@ -55,14 +55,18 @@ class CustomersNotifier extends AsyncNotifier<List<Customer>> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
await refresh();
|
await refresh();
|
||||||
|
if (response.data != null) {
|
||||||
|
return Customer.fromJson(response.data);
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e is DioException) {
|
if (e is DioException) {
|
||||||
throw Exception(
|
throw Exception(
|
||||||
'Failed to add customer: ${e.response?.statusCode} - ${e.response?.data}',
|
e.response?.data?['message'] ?? 'Failed to add customer',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
throw Exception('Failed to add customer: $e');
|
throw Exception('Error adding customer: $e');
|
||||||
}
|
}
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> updateCustomer(int id, Customer customer, {XFile? photo}) async {
|
Future<void> updateCustomer(int id, Customer customer, {XFile? photo}) async {
|
||||||
|
|||||||
@@ -34,10 +34,14 @@ class InvoicesNotifier extends AsyncNotifier<List<Invoice>> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> createInvoice(Invoice invoice) async {
|
Future<Invoice?> createInvoice(Invoice invoice) async {
|
||||||
try {
|
try {
|
||||||
await DioClient().dio.post('/invoices', data: invoice.toJson());
|
final response = await DioClient().dio.post('/invoices', data: invoice.toJson());
|
||||||
await refresh();
|
await refresh();
|
||||||
|
if (response.data != null) {
|
||||||
|
return Invoice.fromJson(response.data);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e is DioException) {
|
if (e is DioException) {
|
||||||
throw Exception(
|
throw Exception(
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import '../data/repository.dart';
|
|
||||||
import '../data/models.dart';
|
import '../data/models.dart';
|
||||||
import 'providers.dart';
|
import 'providers.dart';
|
||||||
|
|
||||||
@@ -98,9 +97,8 @@ class PaginatedTransactionNotifier extends Notifier<PaginatedTransactionState> {
|
|||||||
isLoading: false,
|
isLoading: false,
|
||||||
hasMore: _currentPage < totalPages,
|
hasMore: _currentPage < totalPages,
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (_) {
|
||||||
state = state.copyWith(isLoading: false);
|
state = state.copyWith(isLoading: false);
|
||||||
print("Error loading paginated transactions: $e");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,8 @@ import '../domain/vendor.dart';
|
|||||||
import '../../inventory/domain/product.dart';
|
import '../../inventory/domain/product.dart';
|
||||||
import 'pay_vendor_sheet.dart' as import_pay;
|
import 'pay_vendor_sheet.dart' as import_pay;
|
||||||
import 'widgets/quick_add_vendor_sheet.dart';
|
import 'widgets/quick_add_vendor_sheet.dart';
|
||||||
|
import '../../../core/widgets/barcode_scanner_screen.dart';
|
||||||
|
import '../../../core/utils/purity_utils.dart';
|
||||||
|
|
||||||
String _resolveImageUrl(String path) {
|
String _resolveImageUrl(String path) {
|
||||||
if (path.startsWith('http://') || path.startsWith('https://')) {
|
if (path.startsWith('http://') || path.startsWith('https://')) {
|
||||||
@@ -47,6 +49,7 @@ class _PurchaseOrderBuilderScreenState
|
|||||||
DateTime? _dueDate;
|
DateTime? _dueDate;
|
||||||
final TextEditingController _poNumberCtrl = TextEditingController();
|
final TextEditingController _poNumberCtrl = TextEditingController();
|
||||||
final TextEditingController _notesCtrl = TextEditingController();
|
final TextEditingController _notesCtrl = TextEditingController();
|
||||||
|
final TextEditingController _discountCtrl = TextEditingController(text: '0.0');
|
||||||
|
|
||||||
List<PurchaseOrderItem> _items = [];
|
List<PurchaseOrderItem> _items = [];
|
||||||
bool _isLoading = false;
|
bool _isLoading = false;
|
||||||
@@ -64,6 +67,9 @@ class _PurchaseOrderBuilderScreenState
|
|||||||
_notesCtrl.text = widget.existingPo!.notes ?? '';
|
_notesCtrl.text = widget.existingPo!.notes ?? '';
|
||||||
_items = List.from(widget.existingPo!.items);
|
_items = List.from(widget.existingPo!.items);
|
||||||
_invoiceUrl = widget.existingPo!.vendorInvoiceUrl;
|
_invoiceUrl = widget.existingPo!.vendorInvoiceUrl;
|
||||||
|
if (widget.existingPo!.discountTotal > 0) {
|
||||||
|
_discountCtrl.text = widget.existingPo!.discountTotal.toStringAsFixed(2);
|
||||||
|
}
|
||||||
if (_items.isEmpty && widget.existingPo!.id != null) {
|
if (_items.isEmpty && widget.existingPo!.id != null) {
|
||||||
_loadExistingItems(widget.existingPo!.id!);
|
_loadExistingItems(widget.existingPo!.id!);
|
||||||
}
|
}
|
||||||
@@ -90,22 +96,10 @@ class _PurchaseOrderBuilderScreenState
|
|||||||
void dispose() {
|
void dispose() {
|
||||||
_poNumberCtrl.dispose();
|
_poNumberCtrl.dispose();
|
||||||
_notesCtrl.dispose();
|
_notesCtrl.dispose();
|
||||||
|
_discountCtrl.dispose();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
double get _subtotal {
|
|
||||||
return _items.fold(
|
|
||||||
0,
|
|
||||||
(sum, item) => sum + (item.total),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
double get _totalAmount {
|
|
||||||
return _subtotal;
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _pickInvoiceFile() async {
|
Future<void> _pickInvoiceFile() async {
|
||||||
final picker = ImagePicker();
|
final picker = ImagePicker();
|
||||||
final picked = await picker.pickImage(
|
final picked = await picker.pickImage(
|
||||||
@@ -230,6 +224,9 @@ class _PurchaseOrderBuilderScreenState
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final discountAmount = double.tryParse(_discountCtrl.text) ?? 0.0;
|
||||||
|
final grandTotal = (totalSubtotal - discountAmount).clamp(0.0, double.infinity) + totalTax;
|
||||||
|
|
||||||
final po = PurchaseOrder(
|
final po = PurchaseOrder(
|
||||||
id: widget.existingPo?.id,
|
id: widget.existingPo?.id,
|
||||||
vendorId: _selectedVendorId,
|
vendorId: _selectedVendorId,
|
||||||
@@ -237,18 +234,17 @@ class _PurchaseOrderBuilderScreenState
|
|||||||
issueDate: _issueDate,
|
issueDate: _issueDate,
|
||||||
dueDate: _dueDate,
|
dueDate: _dueDate,
|
||||||
subtotal: totalSubtotal,
|
subtotal: totalSubtotal,
|
||||||
|
discountTotal: discountAmount,
|
||||||
taxTotal: totalTax,
|
taxTotal: totalTax,
|
||||||
cgstTotal: totalCgst,
|
cgstTotal: totalCgst,
|
||||||
sgstTotal: totalSgst,
|
sgstTotal: totalSgst,
|
||||||
igstTotal: totalIgst,
|
igstTotal: totalIgst,
|
||||||
totalAmount: totalSubtotal + totalTax,
|
totalAmount: grandTotal,
|
||||||
notes: _notesCtrl.text,
|
notes: _notesCtrl.text,
|
||||||
vendorInvoiceUrl: finalInvoiceUrl,
|
vendorInvoiceUrl: finalInvoiceUrl,
|
||||||
items: processedItems,
|
items: processedItems,
|
||||||
);
|
);
|
||||||
|
|
||||||
print('--- SAVING PO ---');
|
|
||||||
print(po.toJson());
|
|
||||||
if (widget.existingPo == null) {
|
if (widget.existingPo == null) {
|
||||||
await ref.read(purchaseOrdersProvider.notifier).createPurchaseOrder(po);
|
await ref.read(purchaseOrdersProvider.notifier).createPurchaseOrder(po);
|
||||||
} else {
|
} else {
|
||||||
@@ -319,18 +315,72 @@ class _PurchaseOrderBuilderScreenState
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _scanBarcode() async {
|
||||||
|
final scannedCode = await Navigator.push<String>(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(builder: (_) => const BarcodeScannerScreen()),
|
||||||
|
);
|
||||||
|
if (scannedCode != null && scannedCode.isNotEmpty) {
|
||||||
|
final products = ref.read(productsProvider).value ?? [];
|
||||||
|
final categories = ref.read(productCategoriesProvider).value ?? [];
|
||||||
|
final commodityRates = ref.read(commodityRatesProvider).value ?? [];
|
||||||
|
|
||||||
|
final matchedProduct = products.where((p) =>
|
||||||
|
(p.barcode != null && p.barcode == scannedCode) ||
|
||||||
|
(p.sku != null && p.sku!.toUpperCase() == scannedCode.toUpperCase())
|
||||||
|
).firstOrNull;
|
||||||
|
|
||||||
|
if (matchedProduct != null) {
|
||||||
|
final cat = categories.where((c) => c.id == matchedProduct.categoryId).firstOrNull;
|
||||||
|
double commodityRate = 0.0;
|
||||||
|
if (cat?.commodityCode != null) {
|
||||||
|
final match = commodityRates.where((r) => r.commodityCode.toUpperCase() == cat!.commodityCode!.toUpperCase()).firstOrNull;
|
||||||
|
if (match != null) commodityRate = match.rate;
|
||||||
|
}
|
||||||
|
final purity = resolvePurity(categoryPurity: cat?.purityFactor, productPurity: matchedProduct.purityFactor);
|
||||||
|
final initialRate = commodityRate > 0 ? (commodityRate * purity) : (matchedProduct.sellingPrice ?? 0.0);
|
||||||
|
|
||||||
|
final newItem = PurchaseOrderItem(
|
||||||
|
productId: matchedProduct.id,
|
||||||
|
quantity: 1.0,
|
||||||
|
unitPrice: initialRate,
|
||||||
|
total: initialRate,
|
||||||
|
);
|
||||||
|
|
||||||
|
setState(() => _items.add(newItem));
|
||||||
|
if (mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(content: Text('Added ${matchedProduct.name} via barcode scan!'), backgroundColor: Colors.green),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(content: Text('No product found for barcode: $scannedCode'), backgroundColor: Colors.orange),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final vendorsState = ref.watch(vendorsProvider);
|
final vendorsState = ref.watch(vendorsProvider);
|
||||||
|
final isDark = Theme.of(context).brightness == Brightness.dark;
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: Text(
|
title: Text(
|
||||||
widget.existingPo == null
|
widget.existingPo == null
|
||||||
? 'New Invoice'
|
? 'New Purchase Invoice'
|
||||||
: 'Edit Invoice ${_poNumberCtrl.text}',
|
: 'Edit Invoice ${_poNumberCtrl.text}',
|
||||||
),
|
),
|
||||||
actions: [
|
actions: [
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(LucideIcons.qrCode),
|
||||||
|
tooltip: 'Scan Barcode/SKU',
|
||||||
|
onPressed: _scanBarcode,
|
||||||
|
),
|
||||||
if (_isLoading)
|
if (_isLoading)
|
||||||
const Padding(
|
const Padding(
|
||||||
padding: EdgeInsets.all(16.0),
|
padding: EdgeInsets.all(16.0),
|
||||||
@@ -384,44 +434,112 @@ class _PurchaseOrderBuilderScreenState
|
|||||||
],
|
],
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
body: SingleChildScrollView(
|
body: GestureDetector(
|
||||||
|
onTap: () => FocusScope.of(context).unfocus(),
|
||||||
|
behavior: HitTestBehavior.translucent,
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
keyboardDismissBehavior: ScrollViewKeyboardDismissBehavior.onDrag,
|
||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.all(16),
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
vendorsState.when(
|
// Vendor Selection Row + Quick Add Vendor
|
||||||
data: (vendors) => SmartSearchDropdown<int>(
|
Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: vendorsState.when(
|
||||||
|
data: (vendors) => SmartSearchDropdown<Vendor>(
|
||||||
labelText: 'Vendor *',
|
labelText: 'Vendor *',
|
||||||
hintText: 'Select Vendor',
|
hintText: 'Search Vendor by Name / Phone / Company / GSTIN',
|
||||||
value: _selectedVendorId,
|
value: vendors.where((v) => v.id == _selectedVendorId).firstOrNull,
|
||||||
items: vendors.map((v) => v.id!).toList(),
|
items: vendors,
|
||||||
itemAsString: (id) =>
|
itemAsString: (v) => v.name,
|
||||||
vendors.where((v) => v.id == id).firstOrNull?.name ?? 'Unknown',
|
filterFn: (v, query) {
|
||||||
onChanged: (val) => setState(() => _selectedVendorId = val),
|
final q = query.toLowerCase();
|
||||||
emptyActionText: '+ Quick Add Vendor',
|
return v.name.toLowerCase().contains(q) ||
|
||||||
onEmptyActionPressed: () {
|
(v.phone != null && v.phone!.contains(q)) ||
|
||||||
|
(v.contactPerson != null && v.contactPerson!.toLowerCase().contains(q)) ||
|
||||||
|
(v.gstin != null && v.gstin!.toLowerCase().contains(q)) ||
|
||||||
|
(v.address != null && v.address!.toLowerCase().contains(q));
|
||||||
|
},
|
||||||
|
itemBuilder: (context, v) {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 14.0, vertical: 10.0),
|
||||||
|
child: Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
CircleAvatar(
|
||||||
|
radius: 18,
|
||||||
|
backgroundColor: Colors.amber.withValues(alpha: 0.15),
|
||||||
|
child: Text(
|
||||||
|
v.name.isNotEmpty ? v.name[0].toUpperCase() : 'V',
|
||||||
|
style: const TextStyle(fontWeight: FontWeight.bold, color: Colors.amber, fontSize: 14),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
v.name,
|
||||||
|
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 14),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 3),
|
||||||
|
Wrap(
|
||||||
|
spacing: 6,
|
||||||
|
runSpacing: 3,
|
||||||
|
children: [
|
||||||
|
if (v.contactPerson != null && v.contactPerson!.isNotEmpty)
|
||||||
|
_buildPOBadge(v.contactPerson!, const Color(0xFFD4AF37)),
|
||||||
|
if (v.phone != null && v.phone!.isNotEmpty)
|
||||||
|
_buildPOBadge(v.phone!, Colors.teal),
|
||||||
|
if (v.address != null && v.address!.isNotEmpty)
|
||||||
|
_buildPOBadge(v.address!, Colors.purple),
|
||||||
|
if (v.gstin != null && v.gstin!.isNotEmpty)
|
||||||
|
_buildPOBadge('GST: ${v.gstin}', Colors.indigo),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
onChanged: (val) => setState(() => _selectedVendorId = val?.id),
|
||||||
|
),
|
||||||
|
loading: () => const Center(child: CircularProgressIndicator()),
|
||||||
|
error: (e, stack) => Text('Error: $e'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(top: 4.0),
|
||||||
|
child: IconButton.filledTonal(
|
||||||
|
onPressed: () {
|
||||||
showModalBottomSheet(
|
showModalBottomSheet(
|
||||||
context: context,
|
context: context,
|
||||||
isScrollControlled: true,
|
isScrollControlled: true,
|
||||||
|
backgroundColor: Colors.transparent,
|
||||||
builder: (ctx) => const QuickAddVendorSheet(),
|
builder: (ctx) => const QuickAddVendorSheet(),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
icon: const Icon(LucideIcons.userPlus, size: 20),
|
||||||
|
tooltip: 'Quick Add Vendor',
|
||||||
),
|
),
|
||||||
loading: () => const CircularProgressIndicator(),
|
),
|
||||||
error: (e, stack) => Text('Error: $e'),
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
Row(
|
PremiumTextField(
|
||||||
children: [
|
|
||||||
Expanded(
|
|
||||||
child: PremiumTextField(
|
|
||||||
controller: _poNumberCtrl,
|
controller: _poNumberCtrl,
|
||||||
labelText: 'Invoice Number *',
|
labelText: 'Invoice Number *',
|
||||||
|
prefixIcon: const Icon(LucideIcons.fileText),
|
||||||
),
|
),
|
||||||
),
|
const SizedBox(height: 16),
|
||||||
const SizedBox(width: 16),
|
InkWell(
|
||||||
Expanded(
|
|
||||||
child: InkWell(
|
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
final d = await showDatePicker(
|
final d = await showDatePicker(
|
||||||
context: context,
|
context: context,
|
||||||
@@ -432,41 +550,65 @@ class _PurchaseOrderBuilderScreenState
|
|||||||
if (d != null) setState(() => _issueDate = d);
|
if (d != null) setState(() => _issueDate = d);
|
||||||
},
|
},
|
||||||
child: InputDecorator(
|
child: InputDecorator(
|
||||||
decoration: const InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: 'Issue Date',
|
labelText: 'Issue Date',
|
||||||
|
prefixIcon: const Icon(LucideIcons.calendar),
|
||||||
|
border: OutlineInputBorder(borderRadius: BorderRadius.circular(12)),
|
||||||
),
|
),
|
||||||
child: Text(DateFormat('dd MMM yyyy').format(_issueDate)),
|
child: Text(
|
||||||
|
DateFormat('dd MMM yyyy').format(_issueDate),
|
||||||
|
style: const TextStyle(fontWeight: FontWeight.w600, fontSize: 14),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
|
||||||
),
|
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
Row(
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
'Items',
|
'Invoice Items (${_items.length})',
|
||||||
style: Theme.of(
|
style: Theme.of(context).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.bold),
|
||||||
context,
|
|
||||||
).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold),
|
|
||||||
),
|
),
|
||||||
TextButton.icon(
|
Row(
|
||||||
|
children: [
|
||||||
|
IconButton.filledTonal(
|
||||||
|
onPressed: _scanBarcode,
|
||||||
|
icon: const Icon(LucideIcons.qrCode, size: 18),
|
||||||
|
tooltip: 'Scan Barcode',
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
FilledButton.tonalIcon(
|
||||||
onPressed: _showAddItemSheet,
|
onPressed: _showAddItemSheet,
|
||||||
icon: const Icon(LucideIcons.plus),
|
icon: const Icon(LucideIcons.plus, size: 16),
|
||||||
label: const Text('Add Item'),
|
label: const Text('Add Item'),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const Divider(),
|
],
|
||||||
if (_items.isEmpty)
|
|
||||||
const Padding(
|
|
||||||
padding: EdgeInsets.all(24.0),
|
|
||||||
child: Center(
|
|
||||||
child: Text(
|
|
||||||
'No items added yet',
|
|
||||||
style: TextStyle(color: Colors.grey),
|
|
||||||
),
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
if (_items.isEmpty)
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.all(32),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: isDark ? const Color(0xFF1E293B) : Colors.grey.shade100,
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
border: Border.all(color: Colors.grey.withValues(alpha: 0.2)),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Icon(LucideIcons.packageOpen, size: 48, color: Colors.grey.shade400),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
Text(
|
||||||
|
'No items added yet',
|
||||||
|
style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: Colors.grey.shade600),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Text(
|
||||||
|
'Tap "Add Item" or scan a barcode/SKU to add purchase items',
|
||||||
|
style: TextStyle(fontSize: 12, color: Colors.grey.shade500),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
else
|
else
|
||||||
@@ -544,14 +686,14 @@ class _PurchaseOrderBuilderScreenState
|
|||||||
},
|
},
|
||||||
onDelete: () => setState(() => _items.removeAt(itemIndex)),
|
onDelete: () => setState(() => _items.removeAt(itemIndex)),
|
||||||
);
|
);
|
||||||
}).toList(),
|
}),
|
||||||
Container(
|
Container(
|
||||||
margin: const EdgeInsets.only(bottom: 16),
|
margin: const EdgeInsets.only(bottom: 16),
|
||||||
padding: const EdgeInsets.all(12),
|
padding: const EdgeInsets.all(12),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.blue.withOpacity(0.05),
|
color: Colors.blue.withValues(alpha: 0.05),
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
border: Border.all(color: Colors.blue.withOpacity(0.2)),
|
border: Border.all(color: Colors.blue.withValues(alpha: 0.2)),
|
||||||
),
|
),
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
@@ -612,45 +754,124 @@ class _PurchaseOrderBuilderScreenState
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
const SizedBox(height: 24),
|
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
|
||||||
|
// Invoice Discount Input Field before Summary
|
||||||
|
PremiumTextField(
|
||||||
|
controller: _discountCtrl,
|
||||||
|
labelText: 'Invoice Discount (₹)',
|
||||||
|
prefixIcon: const Icon(LucideIcons.tag, color: Colors.red),
|
||||||
|
keyboardType: const TextInputType.numberWithOptions(decimal: true),
|
||||||
|
onChanged: (_) => setState(() {}),
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
|
||||||
|
// INVOICE SUMMARY CARD
|
||||||
|
Builder(
|
||||||
|
builder: (context) {
|
||||||
|
final products = ref.watch(productsProvider).value ?? [];
|
||||||
|
final categories = ref.watch(productCategoriesProvider).value ?? [];
|
||||||
|
final businessState = ref.watch(businessProfileProvider).value?.stateId;
|
||||||
|
final vendorState = _selectedVendorId == null
|
||||||
|
? null
|
||||||
|
: vendorsState.value?.where((v) => v.id == _selectedVendorId).firstOrNull?.stateId;
|
||||||
|
final isSameState = businessState != null && vendorState != null && businessState == vendorState;
|
||||||
|
final discountAmount = double.tryParse(_discountCtrl.text) ?? 0.0;
|
||||||
|
|
||||||
|
double rawSubtotal = 0;
|
||||||
|
double taxTotal = 0;
|
||||||
|
|
||||||
|
for (var item in _items) {
|
||||||
|
final p = products.where((pr) => pr.id == item.productId).firstOrNull;
|
||||||
|
final cat = categories.where((c) => c.id == p?.categoryId).firstOrNull;
|
||||||
|
final gstRate = item.taxRate > 0 ? item.taxRate : (p?.gstRate ?? (cat?.defaultGst ?? 3.0));
|
||||||
|
|
||||||
|
rawSubtotal += item.total;
|
||||||
|
taxTotal += (item.total * gstRate) / 100.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
final effectiveTaxable = (rawSubtotal - discountAmount).clamp(0.0, double.infinity);
|
||||||
|
final grandTotal = effectiveTaxable + taxTotal;
|
||||||
|
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.all(20),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: isDark ? const Color(0xFF1E293B) : Colors.white,
|
||||||
|
borderRadius: BorderRadius.circular(20),
|
||||||
|
border: Border.all(color: isDark ? Colors.white12 : Colors.grey.shade200),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.black.withValues(alpha: isDark ? 0.2 : 0.05),
|
||||||
|
blurRadius: 15,
|
||||||
|
offset: const Offset(0, 5),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
const Text('INVOICE SUMMARY', style: TextStyle(fontSize: 12, fontWeight: FontWeight.bold, letterSpacing: 0.8, color: Colors.grey)),
|
||||||
|
const SizedBox(height: 14),
|
||||||
Row(
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
const Text(
|
const Text('Items Subtotal:'),
|
||||||
'Total Amount',
|
Text('₹${rawSubtotal.toStringAsFixed(2)}', style: const TextStyle(fontWeight: FontWeight.w600)),
|
||||||
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
],
|
||||||
),
|
),
|
||||||
Builder(
|
if (discountAmount > 0) ...[
|
||||||
builder: (context) {
|
const SizedBox(height: 8),
|
||||||
final productsState = ref.watch(productsProvider);
|
Row(
|
||||||
final products = productsState.value ?? [];
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
double grandTotal = 0;
|
children: [
|
||||||
|
const Text('Discount:', style: TextStyle(color: Colors.red, fontWeight: FontWeight.w600)),
|
||||||
final Map<int, double> groupSubtotals = {};
|
Text('- ₹${discountAmount.toStringAsFixed(2)}', style: const TextStyle(fontWeight: FontWeight.bold, color: Colors.red)),
|
||||||
for (final item in _items) {
|
],
|
||||||
if (item.productId != null) groupSubtotals[item.productId!] = (groupSubtotals[item.productId!] ?? 0) + item.total;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (final entry in groupSubtotals.entries) {
|
|
||||||
final productId = entry.key;
|
|
||||||
final sub = entry.value;
|
|
||||||
final product = products.firstWhere((p) => p.id == productId, orElse: () => products.first);
|
|
||||||
final gstRate = product.gstRate ?? 0.0;
|
|
||||||
final taxAmount = (sub * gstRate) / 100.0;
|
|
||||||
grandTotal += (sub + taxAmount);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Text(
|
|
||||||
'₹${grandTotal.toStringAsFixed(2)}',
|
|
||||||
style: const TextStyle(
|
|
||||||
fontSize: 24,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
color: Colors.green,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
if (isSameState) ...[
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Text('CGST (${(taxTotal > 0 ? '1.5%' : '0%')}):', style: TextStyle(color: Colors.grey.shade600)),
|
||||||
|
Text('₹${(taxTotal / 2.0).toStringAsFixed(2)}'),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 6),
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Text('SGST (${(taxTotal > 0 ? '1.5%' : '0%')}):', style: TextStyle(color: Colors.grey.shade600)),
|
||||||
|
Text('₹${(taxTotal / 2.0).toStringAsFixed(2)}'),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
] else ...[
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Text('IGST (${(taxTotal > 0 ? '3.0%' : '0%')}):', style: TextStyle(color: Colors.grey.shade600)),
|
||||||
|
Text('₹${taxTotal.toStringAsFixed(2)}'),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
const Divider(height: 24),
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
const Text('Grand Total:', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
|
||||||
|
Text(
|
||||||
|
'₹${grandTotal.toStringAsFixed(2)}',
|
||||||
|
style: const TextStyle(fontSize: 22, fontWeight: FontWeight.bold, color: Colors.green),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
PremiumTextField(
|
PremiumTextField(
|
||||||
@@ -718,6 +939,22 @@ class _PurchaseOrderBuilderScreenState
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildPOBadge(String text, Color color) {
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: color.withValues(alpha: 0.12),
|
||||||
|
borderRadius: BorderRadius.circular(6),
|
||||||
|
border: Border.all(color: color.withValues(alpha: 0.3), width: 0.5),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
text,
|
||||||
|
style: TextStyle(fontSize: 10, fontWeight: FontWeight.bold, color: color),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -730,40 +967,47 @@ class _AddItemSheet extends ConsumerStatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _AddItemSheetState extends ConsumerState<_AddItemSheet> {
|
class _AddItemSheetState extends ConsumerState<_AddItemSheet> {
|
||||||
int? _selectedProductId;
|
final _searchCtrl = TextEditingController();
|
||||||
final TextEditingController _qtyCtrl = TextEditingController(text: '1');
|
final Map<int, TextEditingController> _qtyControllers = {};
|
||||||
void _save() {
|
|
||||||
if (_selectedProductId == null) return;
|
|
||||||
final qty = int.tryParse(_qtyCtrl.text) ?? 1;
|
|
||||||
|
|
||||||
final products = ref.read(productsProvider).value ?? [];
|
@override
|
||||||
final categories = ref.read(productCategoriesProvider).value ?? [];
|
void dispose() {
|
||||||
final commodityRates = ref.read(commodityRatesProvider).value ?? [];
|
_searchCtrl.dispose();
|
||||||
|
for (final ctrl in _qtyControllers.values) {
|
||||||
|
ctrl.dispose();
|
||||||
|
}
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
final selectedProduct = products.where((p) => p.id == _selectedProductId).firstOrNull;
|
TextEditingController _getQtyController(int productId) {
|
||||||
final selectedCat = categories.where((c) => c.id == selectedProduct?.categoryId).firstOrNull;
|
return _qtyControllers.putIfAbsent(productId, () => TextEditingController(text: '1'));
|
||||||
|
}
|
||||||
|
|
||||||
double initialRate = 0.0;
|
Future<void> _scanBarcodeInSheet() async {
|
||||||
if (selectedCat?.commodityCode != null) {
|
final scanned = await Navigator.push<String>(
|
||||||
final matchRate = commodityRates.where(
|
context,
|
||||||
(r) => r.commodityCode.toUpperCase() == selectedCat!.commodityCode!.toUpperCase()
|
MaterialPageRoute(builder: (_) => const BarcodeScannerScreen()),
|
||||||
).firstOrNull;
|
);
|
||||||
if (matchRate != null && matchRate.rate > 0) {
|
if (scanned != null && scanned.isNotEmpty) {
|
||||||
final purity = selectedCat?.purityFactor ?? (selectedProduct?.purityFactor ?? 1.0);
|
_searchCtrl.text = scanned;
|
||||||
initialRate = matchRate.rate * purity;
|
setState(() {});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (initialRate == 0.0) {
|
|
||||||
initialRate = selectedProduct?.sellingPrice ?? 0.0;
|
void _addItemsFromCard(Product product, ProductCategory? category, int qty) {
|
||||||
}
|
if (qty <= 0) return;
|
||||||
|
final gstRate = product.gstRate ?? (category?.defaultGst ?? 3.0);
|
||||||
|
|
||||||
final List<PurchaseOrderItem> items = [];
|
final List<PurchaseOrderItem> items = [];
|
||||||
for (int i = 0; i < qty; i++) {
|
for (int i = 0; i < qty; i++) {
|
||||||
items.add(PurchaseOrderItem(
|
items.add(PurchaseOrderItem(
|
||||||
productId: _selectedProductId,
|
productId: product.id,
|
||||||
quantity: 1.0,
|
quantity: 1.0,
|
||||||
unitPrice: initialRate,
|
weight: 0.0,
|
||||||
total: initialRate > 0 ? initialRate : 0.0,
|
unitPrice: 0.0,
|
||||||
|
taxRate: gstRate,
|
||||||
|
makingCharge: 0.0,
|
||||||
|
total: 0.0,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
Navigator.pop(context, items);
|
Navigator.pop(context, items);
|
||||||
@@ -771,62 +1015,252 @@ class _AddItemSheetState extends ConsumerState<_AddItemSheet> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final productsState = ref.watch(productsProvider);
|
final products = ref.watch(productsProvider).value ?? [];
|
||||||
|
final categories = ref.watch(productCategoriesProvider).value ?? [];
|
||||||
|
final commodityRates = ref.watch(commodityRatesProvider).value ?? [];
|
||||||
|
final isDark = Theme.of(context).brightness == Brightness.dark;
|
||||||
|
|
||||||
return Padding(
|
final query = _searchCtrl.text.trim().toLowerCase();
|
||||||
padding: EdgeInsets.only(
|
final filteredProducts = query.isEmpty
|
||||||
bottom: MediaQuery.of(context).viewInsets.bottom,
|
? products
|
||||||
left: 24,
|
: products.where((p) =>
|
||||||
right: 24,
|
p.name.toLowerCase().contains(query) ||
|
||||||
top: 24,
|
(p.sku != null && p.sku!.toLowerCase().contains(query)) ||
|
||||||
|
(p.barcode != null && p.barcode!.toLowerCase().contains(query))).toList();
|
||||||
|
|
||||||
|
final keyboardHeight = MediaQuery.of(context).viewInsets.bottom;
|
||||||
|
|
||||||
|
return AnimatedPadding(
|
||||||
|
padding: EdgeInsets.only(bottom: keyboardHeight),
|
||||||
|
duration: const Duration(milliseconds: 150),
|
||||||
|
curve: Curves.easeOut,
|
||||||
|
child: GestureDetector(
|
||||||
|
onTap: () => FocusScope.of(context).unfocus(),
|
||||||
|
behavior: HitTestBehavior.translucent,
|
||||||
|
child: Container(
|
||||||
|
height: MediaQuery.of(context).size.height * 0.85,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Theme.of(context).scaffoldBackgroundColor,
|
||||||
|
borderRadius: const BorderRadius.vertical(top: Radius.circular(28)),
|
||||||
),
|
),
|
||||||
child: Column(
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Center(
|
||||||
|
child: Container(
|
||||||
|
margin: const EdgeInsets.only(top: 12, bottom: 8),
|
||||||
|
width: 40,
|
||||||
|
height: 4,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.grey.withValues(alpha: 0.4),
|
||||||
|
borderRadius: BorderRadius.circular(2),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 8),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
const Text('Add Items to Invoice', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(LucideIcons.x, size: 20),
|
||||||
|
onPressed: () => Navigator.pop(context),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Divider(height: 1),
|
||||||
|
|
||||||
|
Expanded(
|
||||||
|
child: ListView(
|
||||||
|
keyboardDismissBehavior: ScrollViewKeyboardDismissBehavior.onDrag,
|
||||||
|
padding: const EdgeInsets.fromLTRB(20, 16, 20, 40),
|
||||||
|
children: [
|
||||||
|
TextField(
|
||||||
|
controller: _searchCtrl,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: 'Search Product by Name / SKU / Barcode',
|
||||||
|
prefixIcon: const Icon(LucideIcons.search),
|
||||||
|
suffixIcon: Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
if (_searchCtrl.text.isNotEmpty)
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(LucideIcons.x),
|
||||||
|
onPressed: () => setState(() => _searchCtrl.clear()),
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(LucideIcons.qrCode, color: Colors.blue),
|
||||||
|
tooltip: 'Scan Barcode',
|
||||||
|
onPressed: _scanBarcodeInSheet,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
border: OutlineInputBorder(borderRadius: BorderRadius.circular(12)),
|
||||||
|
),
|
||||||
|
onChanged: (val) => setState(() {}),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
|
const Text('Products & Catalogue:', style: TextStyle(fontSize: 12, fontWeight: FontWeight.bold, color: Colors.grey)),
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
|
||||||
|
if (filteredProducts.isEmpty)
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 32.0),
|
||||||
|
child: Center(
|
||||||
|
child: Text(
|
||||||
|
'No matching products found',
|
||||||
|
style: TextStyle(color: Colors.grey.shade500),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
else
|
||||||
|
...filteredProducts.map((p) {
|
||||||
|
final cat = categories.where((c) => c.id == p.categoryId).firstOrNull;
|
||||||
|
double commRate = 0.0;
|
||||||
|
if (cat?.commodityCode != null) {
|
||||||
|
final m = commodityRates.where((r) => r.commodityCode.toUpperCase() == cat!.commodityCode!.toUpperCase()).firstOrNull;
|
||||||
|
if (m != null) commRate = m.rate;
|
||||||
|
}
|
||||||
|
return _buildPOSearchCard(
|
||||||
|
product: p,
|
||||||
|
category: cat,
|
||||||
|
commodityRate: commRate,
|
||||||
|
isDark: isDark,
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildPOSheetBadge(String text, Color color) {
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: color.withValues(alpha: 0.12),
|
||||||
|
borderRadius: BorderRadius.circular(6),
|
||||||
|
border: Border.all(color: color.withValues(alpha: 0.4)),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
text,
|
||||||
|
style: TextStyle(fontSize: 11, fontWeight: FontWeight.bold, color: color),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildPOSearchCard({
|
||||||
|
required Product product,
|
||||||
|
ProductCategory? category,
|
||||||
|
required double commodityRate,
|
||||||
|
required bool isDark,
|
||||||
|
}) {
|
||||||
|
final sku = product.sku;
|
||||||
|
final qtyCtrl = _getQtyController(product.id ?? 0);
|
||||||
|
|
||||||
|
return Container(
|
||||||
|
margin: const EdgeInsets.only(bottom: 12),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: isDark ? const Color(0xFF1E293B) : Colors.white,
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
border: Border.all(color: isDark ? Colors.white12 : Colors.grey.shade200),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.black.withValues(alpha: isDark ? 0.2 : 0.04),
|
||||||
|
blurRadius: 8,
|
||||||
|
offset: const Offset(0, 3),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(14),
|
||||||
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
const Text(
|
Row(
|
||||||
'Add PO Item',
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
|
children: [
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.all(8),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.blue.withValues(alpha: 0.12),
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
child: const Icon(
|
||||||
productsState.when(
|
LucideIcons.tag,
|
||||||
data: (products) => SmartSearchDropdown<int>(
|
size: 20,
|
||||||
labelText: 'Product',
|
color: Colors.blue,
|
||||||
hintText: 'Select Product',
|
|
||||||
value: _selectedProductId,
|
|
||||||
items: products.map((p) => p.id!).toList(),
|
|
||||||
itemAsString: (id) => products.where((p) => p.id == id).firstOrNull?.name ?? 'Unknown',
|
|
||||||
onChanged: (val) {
|
|
||||||
setState(() {
|
|
||||||
_selectedProductId = val;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
loading: () => const CircularProgressIndicator(),
|
|
||||||
error: (e, stack) => Text('Error: $e'),
|
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(width: 10),
|
||||||
PremiumTextField(
|
Expanded(
|
||||||
controller: _qtyCtrl,
|
child: Column(
|
||||||
labelText: 'Number of Items',
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
product.name,
|
||||||
|
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 15),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Wrap(
|
||||||
|
spacing: 6,
|
||||||
|
runSpacing: 4,
|
||||||
|
children: [
|
||||||
|
if (category?.name != null)
|
||||||
|
_buildPOSheetBadge(category!.name, const Color(0xFFD4AF37)),
|
||||||
|
if (sku != null && sku.isNotEmpty)
|
||||||
|
_buildPOSheetBadge('SKU: $sku', Colors.blue),
|
||||||
|
_buildPOSheetBadge('Purity: ${formatPurity(resolvePurity(categoryPurity: category?.purityFactor, productPurity: product.purityFactor))}', Colors.teal),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
// Small box to enter number of items
|
||||||
|
SizedBox(
|
||||||
|
width: 75,
|
||||||
|
child: TextFormField(
|
||||||
|
controller: qtyCtrl,
|
||||||
keyboardType: TextInputType.number,
|
keyboardType: TextInputType.number,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: 'Qty',
|
||||||
|
isDense: true,
|
||||||
|
contentPadding: const EdgeInsets.symmetric(horizontal: 8, vertical: 10),
|
||||||
|
border: OutlineInputBorder(borderRadius: BorderRadius.circular(10)),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 24),
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const Divider(height: 20),
|
||||||
SizedBox(
|
SizedBox(
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
child: ElevatedButton(
|
child: ElevatedButton.icon(
|
||||||
onPressed: _save,
|
onPressed: () {
|
||||||
|
final qty = int.tryParse(qtyCtrl.text.trim()) ?? 1;
|
||||||
|
_addItemsFromCard(product, category, qty);
|
||||||
|
},
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
|
||||||
backgroundColor: Colors.blue,
|
backgroundColor: Colors.blue,
|
||||||
foregroundColor: Colors.white,
|
foregroundColor: Colors.white,
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 10),
|
||||||
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
|
||||||
),
|
),
|
||||||
child: const Text('Add Item'),
|
icon: const Icon(LucideIcons.plus, size: 16),
|
||||||
|
label: const Text('Add to Invoice', style: TextStyle(fontWeight: FontWeight.bold)),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 24),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -866,10 +1300,10 @@ class _POItemRowState extends State<_POItemRow> {
|
|||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_weightCtrl = TextEditingController(text: widget.item.weight?.toString() ?? '');
|
_weightCtrl = TextEditingController(text: widget.item.weight != null && widget.item.weight! > 0 ? widget.item.weight.toString() : '0');
|
||||||
_rateCtrl = TextEditingController(text: widget.item.unitPrice > 0 ? widget.item.unitPrice.toString() : '');
|
_rateCtrl = TextEditingController(text: widget.item.unitPrice > 0 ? widget.item.unitPrice.toString() : '0');
|
||||||
_huidCtrl = TextEditingController(text: widget.item.huid ?? '');
|
_huidCtrl = TextEditingController(text: widget.item.huid ?? '');
|
||||||
_totalCtrl = TextEditingController(text: widget.item.total > 0 ? widget.item.total.toString() : '');
|
_totalCtrl = TextEditingController(text: widget.item.total > 0 ? widget.item.total.toStringAsFixed(2) : '0.00');
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -882,12 +1316,13 @@ class _POItemRowState extends State<_POItemRow> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _updateItem() {
|
void _updateItem() {
|
||||||
final weight = double.tryParse(_weightCtrl.text);
|
final weight = double.tryParse(_weightCtrl.text) ?? 0.0;
|
||||||
final rate = double.tryParse(_rateCtrl.text) ?? 0.0;
|
final rate = double.tryParse(_rateCtrl.text) ?? 0.0;
|
||||||
|
|
||||||
final total = (weight ?? 0.0) * rate;
|
final total = weight * rate;
|
||||||
if (_totalCtrl.text != total.toStringAsFixed(2)) {
|
final totalFormatted = total.toStringAsFixed(2);
|
||||||
_totalCtrl.text = total.toStringAsFixed(2);
|
if (_totalCtrl.text != totalFormatted) {
|
||||||
|
_totalCtrl.text = totalFormatted;
|
||||||
}
|
}
|
||||||
|
|
||||||
final updated = widget.item.copyWith(
|
final updated = widget.item.copyWith(
|
||||||
@@ -1029,6 +1464,18 @@ class _POItemRowState extends State<_POItemRow> {
|
|||||||
style: TextStyle(fontSize: 10, color: Colors.grey[700]),
|
style: TextStyle(fontSize: 10, color: Colors.grey[700]),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.amber.withValues(alpha: 0.15),
|
||||||
|
borderRadius: BorderRadius.circular(4),
|
||||||
|
border: Border.all(color: Colors.amber.withValues(alpha: 0.4)),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
'Purity: ${formatPurity(resolvePurity(categoryPurity: widget.category?.purityFactor, productPurity: widget.product?.purityFactor))}',
|
||||||
|
style: TextStyle(fontSize: 10, fontWeight: FontWeight.bold, color: Colors.amber.shade900),
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -1060,7 +1507,7 @@ class _POItemRowState extends State<_POItemRow> {
|
|||||||
labelText: 'Weight/Pcs',
|
labelText: 'Weight/Pcs',
|
||||||
keyboardType: const TextInputType.numberWithOptions(decimal: true),
|
keyboardType: const TextInputType.numberWithOptions(decimal: true),
|
||||||
onChanged: (_) => _updateItem(),
|
onChanged: (_) => _updateItem(),
|
||||||
suffixIcon: widget.category?.baseUnit != null && widget.category!.baseUnit!.isNotEmpty
|
suffixIcon: (widget.category?.baseUnit != null && widget.category!.baseUnit.isNotEmpty)
|
||||||
? Container(
|
? Container(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
@@ -1071,7 +1518,7 @@ class _POItemRowState extends State<_POItemRow> {
|
|||||||
child: Column(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [Text(widget.category!.baseUnit!, style: TextStyle(color: Colors.grey[700]))],
|
children: [Text(widget.category?.baseUnit ?? '', style: TextStyle(color: Colors.grey[700]))],
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
: null,
|
: null,
|
||||||
@@ -1089,7 +1536,7 @@ class _POItemRowState extends State<_POItemRow> {
|
|||||||
labelText: 'Rate',
|
labelText: 'Rate',
|
||||||
keyboardType: const TextInputType.numberWithOptions(decimal: true),
|
keyboardType: const TextInputType.numberWithOptions(decimal: true),
|
||||||
onChanged: (_) => _updateItem(),
|
onChanged: (_) => _updateItem(),
|
||||||
suffixIcon: widget.category?.baseUnit != null && widget.category!.baseUnit!.isNotEmpty
|
suffixIcon: (widget.category?.baseUnit != null && widget.category!.baseUnit.isNotEmpty)
|
||||||
? Container(
|
? Container(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
@@ -1100,7 +1547,7 @@ class _POItemRowState extends State<_POItemRow> {
|
|||||||
child: Column(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [Text('per ${widget.category!.baseUnit}', style: TextStyle(color: Colors.grey[700]))],
|
children: [Text('per ${widget.category?.baseUnit ?? ''}', style: TextStyle(color: Colors.grey[700]))],
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
: null,
|
: null,
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
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/lucide_icons.dart';
|
||||||
@@ -1143,11 +1142,11 @@ class _PurchaseOrderDetailsScreenState
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}).toList(),
|
}),
|
||||||
Container(
|
Container(
|
||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.all(16),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.blue.shade50.withOpacity(0.5),
|
color: Colors.blue.shade50.withValues(alpha: 0.5),
|
||||||
borderRadius: const BorderRadius.only(
|
borderRadius: const BorderRadius.only(
|
||||||
bottomLeft: Radius.circular(16),
|
bottomLeft: Radius.circular(16),
|
||||||
bottomRight: Radius.circular(16),
|
bottomRight: Radius.circular(16),
|
||||||
|
|||||||
@@ -56,34 +56,44 @@ class _PurchaseOrdersListScreenState
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final poState = ref.watch(purchaseOrdersProvider);
|
final poState = ref.watch(purchaseOrdersProvider);
|
||||||
final vendorsState = ref.watch(vendorsProvider);
|
final vendorsState = ref.watch(vendorsProvider);
|
||||||
final darkTheme = Theme.of(context).brightness == Brightness.dark;
|
final isDark = Theme.of(context).brightness == Brightness.dark;
|
||||||
|
final formatCurrency = NumberFormat.currency(symbol: '₹');
|
||||||
|
final formatDate = DateFormat('MMM dd, yyyy');
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: Colors.grey[100],
|
backgroundColor: isDark ? const Color(0xFF0F172A) : const Color(0xFFF8FAFC),
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: const Text(
|
title: const Text(
|
||||||
'Purchase Invoices',
|
'Purchase Invoices',
|
||||||
style: TextStyle(fontWeight: FontWeight.bold),
|
style: TextStyle(fontWeight: FontWeight.bold),
|
||||||
),
|
),
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
backgroundColor: Colors.white,
|
backgroundColor: isDark ? const Color(0xFF1E293B) : Colors.white,
|
||||||
foregroundColor: Colors.black,
|
foregroundColor: isDark ? Colors.white : Colors.black,
|
||||||
centerTitle: true,
|
centerTitle: true,
|
||||||
),
|
),
|
||||||
body: Column(
|
body: Column(
|
||||||
children: [
|
children: [
|
||||||
Container(
|
Container(
|
||||||
color: Colors.white,
|
color: isDark ? const Color(0xFF1E293B) : Colors.white,
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||||
child: TextField(
|
child: TextField(
|
||||||
controller: _searchController,
|
controller: _searchController,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
hintText: 'Search PO Number...',
|
hintText: 'Search PO Number or Vendor...',
|
||||||
prefixIcon: const Icon(LucideIcons.search, color: Colors.grey),
|
prefixIcon: const Icon(LucideIcons.search, color: Colors.grey),
|
||||||
contentPadding: const EdgeInsets.symmetric(vertical: 14),
|
contentPadding: const EdgeInsets.symmetric(vertical: 14),
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
borderSide: BorderSide(color: isDark ? Colors.white12 : Colors.grey.shade300),
|
||||||
|
),
|
||||||
|
enabledBorder: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
borderSide: BorderSide(color: isDark ? Colors.white12 : Colors.grey.shade300),
|
||||||
|
),
|
||||||
suffixIcon: _searchQuery.isNotEmpty
|
suffixIcon: _searchQuery.isNotEmpty
|
||||||
? IconButton(
|
? IconButton(
|
||||||
icon: const Icon(LucideIcons.xCircle),
|
icon: const Icon(LucideIcons.xCircle, size: 20),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
_searchController.clear();
|
_searchController.clear();
|
||||||
setState(() => _searchQuery = '');
|
setState(() => _searchQuery = '');
|
||||||
@@ -91,20 +101,28 @@ class _PurchaseOrdersListScreenState
|
|||||||
)
|
)
|
||||||
: null,
|
: null,
|
||||||
),
|
),
|
||||||
onChanged: (val) => setState(() => _searchQuery = val),
|
onChanged: (val) => setState(() => _searchQuery = val.trim()),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: poState.when(
|
child: poState.when(
|
||||||
data: (pos) {
|
data: (pos) {
|
||||||
|
final vendors = vendorsState.value ?? [];
|
||||||
final filtered = pos.where((p) {
|
final filtered = pos.where((p) {
|
||||||
return p.poNumber.toLowerCase().contains(
|
final matchesPo = p.poNumber.toLowerCase().contains(_searchQuery.toLowerCase());
|
||||||
_searchQuery.toLowerCase(),
|
final vendor = vendors.where((v) => v.id == p.vendorId).firstOrNull;
|
||||||
);
|
final matchesVendor = vendor != null && vendor.name.toLowerCase().contains(_searchQuery.toLowerCase());
|
||||||
|
return matchesPo || matchesVendor;
|
||||||
}).toList();
|
}).toList();
|
||||||
|
|
||||||
if (filtered.isEmpty) {
|
if (filtered.isEmpty) {
|
||||||
return const Center(
|
return RefreshIndicator(
|
||||||
|
onRefresh: () => ref.refresh(purchaseOrdersProvider.future),
|
||||||
|
child: ListView(
|
||||||
|
physics: const AlwaysScrollableScrollPhysics(),
|
||||||
|
children: const [
|
||||||
|
SizedBox(height: 120),
|
||||||
|
Center(
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
@@ -120,6 +138,9 @@ class _PurchaseOrdersListScreenState
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -130,20 +151,19 @@ class _PurchaseOrdersListScreenState
|
|||||||
itemCount: filtered.length,
|
itemCount: filtered.length,
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
final po = filtered[index];
|
final po = filtered[index];
|
||||||
final vendors = vendorsState.value ?? [];
|
final vendor = vendors.where((v) => v.id == po.vendorId).firstOrNull;
|
||||||
final vendor = vendors.firstWhere(
|
final statusColor = _getStatusColor(po);
|
||||||
(v) => v.id == po.vendorId,
|
final statusLabel = _getStatusLabel(po);
|
||||||
orElse: () => po.vendor!,
|
|
||||||
);
|
|
||||||
|
|
||||||
return Container(
|
return Container(
|
||||||
margin: const EdgeInsets.only(bottom: 16),
|
margin: const EdgeInsets.only(bottom: 16),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.white,
|
color: isDark ? const Color(0xFF1E293B) : Colors.white,
|
||||||
borderRadius: BorderRadius.circular(20),
|
borderRadius: BorderRadius.circular(20),
|
||||||
|
border: Border.all(color: isDark ? Colors.white10 : Colors.grey.shade200),
|
||||||
boxShadow: [
|
boxShadow: [
|
||||||
BoxShadow(
|
BoxShadow(
|
||||||
color: Colors.grey.withOpacity(0.08),
|
color: Colors.black.withValues(alpha: isDark ? 0.2 : 0.04),
|
||||||
blurRadius: 10,
|
blurRadius: 10,
|
||||||
offset: const Offset(0, 4),
|
offset: const Offset(0, 4),
|
||||||
),
|
),
|
||||||
@@ -193,20 +213,17 @@ class _PurchaseOrdersListScreenState
|
|||||||
Container(
|
Container(
|
||||||
padding: const EdgeInsets.symmetric(
|
padding: const EdgeInsets.symmetric(
|
||||||
horizontal: 12,
|
horizontal: 12,
|
||||||
vertical: 6,
|
vertical: 5,
|
||||||
),
|
),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: _getStatusColor(
|
color: statusColor.withValues(alpha: 0.12),
|
||||||
po,
|
borderRadius: BorderRadius.circular(20),
|
||||||
).withOpacity(0.1),
|
border: Border.all(color: statusColor.withValues(alpha: 0.3), width: 0.8),
|
||||||
borderRadius: BorderRadius.circular(
|
|
||||||
20,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
child: Text(
|
child: Text(
|
||||||
_getStatusLabel(po),
|
statusLabel,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: _getStatusColor(po),
|
color: statusColor,
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
),
|
),
|
||||||
@@ -218,24 +235,30 @@ class _PurchaseOrdersListScreenState
|
|||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
const Icon(
|
const Icon(
|
||||||
LucideIcons.user,
|
LucideIcons.truck,
|
||||||
size: 16,
|
size: 16,
|
||||||
color: Colors.grey,
|
color: Colors.grey,
|
||||||
),
|
),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Text(
|
child: Text(
|
||||||
vendor.name,
|
vendor?.name ?? po.vendor?.name ?? 'Vendor',
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontWeight: FontWeight.w500,
|
fontWeight: FontWeight.w600,
|
||||||
|
fontSize: 14,
|
||||||
),
|
),
|
||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
if (po.items.isNotEmpty)
|
||||||
|
Text(
|
||||||
|
'${po.items.length} ${po.items.length == 1 ? "item" : "items"}',
|
||||||
|
style: TextStyle(fontSize: 12, color: Colors.grey.shade500),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 10),
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
const Icon(
|
const Icon(
|
||||||
@@ -245,20 +268,19 @@ class _PurchaseOrdersListScreenState
|
|||||||
),
|
),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
Text(
|
Text(
|
||||||
DateFormat(
|
formatDate.format(po.issueDate),
|
||||||
'MMM dd, yyyy',
|
style: TextStyle(
|
||||||
).format(po.issueDate),
|
color: Colors.grey.shade600,
|
||||||
style: const TextStyle(
|
fontSize: 13,
|
||||||
color: Colors.grey,
|
|
||||||
fontSize: 14,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const Spacer(),
|
const Spacer(),
|
||||||
Text(
|
Text(
|
||||||
'₹${po.totalAmount.toStringAsFixed(2)}',
|
formatCurrency.format(po.totalAmount),
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
|
color: Colors.green,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -289,7 +311,9 @@ class _PurchaseOrdersListScreenState
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
icon: const Icon(LucideIcons.plus),
|
icon: const Icon(LucideIcons.plus),
|
||||||
label: const Text('New PO'),
|
label: const Text('Create PO', style: TextStyle(fontWeight: FontWeight.bold)),
|
||||||
|
backgroundColor: Colors.blue,
|
||||||
|
foregroundColor: Colors.white,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ class _VendorsListScreenState extends ConsumerState<VendorsListScreen> {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final vendorsState = ref.watch(vendorsProvider);
|
final vendorsState = ref.watch(vendorsProvider);
|
||||||
final darkTheme = Theme.of(context).brightness == Brightness.dark;
|
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: Colors.grey[100],
|
backgroundColor: Colors.grey[100],
|
||||||
@@ -82,7 +81,7 @@ class _VendorsListScreenState extends ConsumerState<VendorsListScreen> {
|
|||||||
borderRadius: BorderRadius.circular(20),
|
borderRadius: BorderRadius.circular(20),
|
||||||
boxShadow: [
|
boxShadow: [
|
||||||
BoxShadow(
|
BoxShadow(
|
||||||
color: Colors.grey.withOpacity(0.08),
|
color: Colors.grey.withValues(alpha: 0.08),
|
||||||
blurRadius: 10,
|
blurRadius: 10,
|
||||||
offset: const Offset(0, 4),
|
offset: const Offset(0, 4),
|
||||||
),
|
),
|
||||||
@@ -107,8 +106,8 @@ class _VendorsListScreenState extends ConsumerState<VendorsListScreen> {
|
|||||||
children: [
|
children: [
|
||||||
CircleAvatar(
|
CircleAvatar(
|
||||||
radius: 24,
|
radius: 24,
|
||||||
backgroundColor: Colors.orange.withOpacity(
|
backgroundColor: Colors.orange.withValues(
|
||||||
0.1,
|
alpha: 0.1,
|
||||||
),
|
),
|
||||||
child: vendor.photoUrl != null
|
child: vendor.photoUrl != null
|
||||||
? ClipOval(
|
? ClipOval(
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import 'package:dio/dio.dart';
|
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
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';
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import 'package:dio/dio.dart';
|
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import '../../../core/network/dio_client.dart';
|
import '../../../core/network/dio_client.dart';
|
||||||
import '../domain/vendor.dart';
|
import '../domain/vendor.dart';
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import 'package:local_auth/local_auth.dart';
|
|||||||
import 'core/theme/app_theme.dart';
|
import 'core/theme/app_theme.dart';
|
||||||
import 'core/theme/theme_provider.dart';
|
import 'core/theme/theme_provider.dart';
|
||||||
import 'features/auth/presentation/auth_screen.dart';
|
import 'features/auth/presentation/auth_screen.dart';
|
||||||
|
import 'features/auth/presentation/setup_wizard_screen.dart';
|
||||||
import 'features/dashboard/presentation/dashboard_screen.dart';
|
import 'features/dashboard/presentation/dashboard_screen.dart';
|
||||||
import 'features/onboarding/presentation/onboarding_screen.dart';
|
import 'features/onboarding/presentation/onboarding_screen.dart';
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
@@ -31,6 +32,7 @@ class KifiApp extends ConsumerStatefulWidget {
|
|||||||
class _KifiAppState extends ConsumerState<KifiApp> {
|
class _KifiAppState extends ConsumerState<KifiApp> {
|
||||||
bool _isLoading = true;
|
bool _isLoading = true;
|
||||||
bool _isAuthenticated = false;
|
bool _isAuthenticated = false;
|
||||||
|
bool _isSetupCompleted = true;
|
||||||
bool _hasSeenOnboarding = false;
|
bool _hasSeenOnboarding = false;
|
||||||
final LocalAuthentication _localAuth = LocalAuthentication();
|
final LocalAuthentication _localAuth = LocalAuthentication();
|
||||||
String? _startupError;
|
String? _startupError;
|
||||||
@@ -43,11 +45,11 @@ class _KifiAppState extends ConsumerState<KifiApp> {
|
|||||||
|
|
||||||
Future<bool> _authenticateWithBiometrics() async {
|
Future<bool> _authenticateWithBiometrics() async {
|
||||||
try {
|
try {
|
||||||
final canCheckBiometrics = await _localAuth.canCheckBiometrics;
|
final isAvailable = await _localAuth.canCheckBiometrics;
|
||||||
final isDeviceSupported = await _localAuth.isDeviceSupported();
|
final isDeviceSupported = await _localAuth.isDeviceSupported();
|
||||||
|
|
||||||
if (!canCheckBiometrics && !isDeviceSupported) {
|
if (!isAvailable || !isDeviceSupported) {
|
||||||
return true; // Pass if device doesn't support biometrics to avoid locking users out
|
return true; // If device does not support biometrics, allow access via stored token
|
||||||
}
|
}
|
||||||
|
|
||||||
return await _localAuth.authenticate(
|
return await _localAuth.authenticate(
|
||||||
@@ -79,11 +81,27 @@ class _KifiAppState extends ConsumerState<KifiApp> {
|
|||||||
return; // User failed biometrics, leave them on login screen
|
return; // User failed biometrics, leave them on login screen
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Validate token and check account setup status with backend
|
||||||
|
try {
|
||||||
|
final res = await DioClient().dio.get('/account/setup/status');
|
||||||
|
final status = res.data != null ? res.data['status'] : null;
|
||||||
|
|
||||||
setState(() {
|
setState(() {
|
||||||
_isAuthenticated = true;
|
_isAuthenticated = true;
|
||||||
|
_isSetupCompleted = status == 'COMPLETED';
|
||||||
_hasSeenOnboarding = hasSeenOnboarding;
|
_hasSeenOnboarding = hasSeenOnboarding;
|
||||||
_isLoading = false;
|
_isLoading = false;
|
||||||
});
|
});
|
||||||
|
} catch (_) {
|
||||||
|
// Token is stale / invalid / user was wiped from DB -> clear token and go to AuthScreen
|
||||||
|
await storage.delete(key: 'jwt_token');
|
||||||
|
await DioClient().clearToken();
|
||||||
|
setState(() {
|
||||||
|
_isAuthenticated = false;
|
||||||
|
_hasSeenOnboarding = hasSeenOnboarding;
|
||||||
|
_isLoading = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
setState(() {
|
setState(() {
|
||||||
_isAuthenticated = false;
|
_isAuthenticated = false;
|
||||||
@@ -91,7 +109,7 @@ class _KifiAppState extends ConsumerState<KifiApp> {
|
|||||||
_isLoading = false;
|
_isLoading = false;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (e, stacktrace) {
|
} catch (e) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_startupError = e.toString();
|
_startupError = e.toString();
|
||||||
_isLoading = false;
|
_isLoading = false;
|
||||||
@@ -116,7 +134,9 @@ class _KifiAppState extends ConsumerState<KifiApp> {
|
|||||||
? const Scaffold(body: Center(child: CircularProgressIndicator()))
|
? const Scaffold(body: Center(child: CircularProgressIndicator()))
|
||||||
: (!_hasSeenOnboarding
|
: (!_hasSeenOnboarding
|
||||||
? const OnboardingScreen()
|
? const OnboardingScreen()
|
||||||
: (_isAuthenticated ? const DashboardScreen() : const AuthScreen())),
|
: (_isAuthenticated
|
||||||
|
? (_isSetupCompleted ? const DashboardScreen() : const SetupWizardScreen())
|
||||||
|
: const AuthScreen())),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +0,0 @@
|
|||||||
import 'package:dio/dio.dart';
|
|
||||||
void main() {
|
|
||||||
final dio = Dio(BaseOptions(baseUrl: 'https://app.technobeesolutions.in/api/kifi-v2'));
|
|
||||||
final req = RequestOptions(path: '/account/setup/status', baseUrl: dio.options.baseUrl);
|
|
||||||
print(req.uri);
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user