Payments
This commit is contained in:
@@ -8,17 +8,26 @@ struct ProductDetailSheet: View {
|
||||
let currentQuantityForItemId: (String) -> Int
|
||||
let onAdd: (CartItemState) -> Void
|
||||
@Environment(\.dismiss) var dismiss
|
||||
@State var selectedAddonItemIds: Set<String> = []
|
||||
@State var selectedAddonQuantities: [String: Int] = [:]
|
||||
@State var quantity: Int = 0
|
||||
|
||||
private var selectedAddonItems: [StoreAddonItem] {
|
||||
product.addonGroups
|
||||
.flatMap(\.items)
|
||||
.filter { selectedAddonItemIds.contains($0.id) }
|
||||
private var addonItemsById: [String: StoreAddonItem] {
|
||||
Dictionary(uniqueKeysWithValues: product.addonGroups.flatMap(\.items).map { ($0.id, $0) })
|
||||
}
|
||||
|
||||
private var selectedAddonItems: [(item: StoreAddonItem, quantity: Int)] {
|
||||
selectedAddonQuantities
|
||||
.compactMap { key, qty in
|
||||
guard qty > 0, let item = addonItemsById[key] else { return nil }
|
||||
return (item, qty)
|
||||
}
|
||||
.sorted { $0.item.name < $1.item.name }
|
||||
}
|
||||
|
||||
private var addonsTotal: Double {
|
||||
selectedAddonItems.reduce(0) { $0 + ($1.price ?? 0) }
|
||||
selectedAddonItems.reduce(0) { partial, pair in
|
||||
partial + (Double(pair.quantity) * (pair.item.price ?? 0))
|
||||
}
|
||||
}
|
||||
|
||||
private var unitPrice: Double {
|
||||
@@ -30,16 +39,29 @@ struct ProductDetailSheet: View {
|
||||
}
|
||||
|
||||
private var cartItemId: String {
|
||||
let addonKey = selectedAddonItemIds.sorted().joined(separator: ",")
|
||||
let addonKey = encodedAddonKey
|
||||
return "\(storeId)::\(product.id)::\(addonKey)"
|
||||
}
|
||||
|
||||
private var selectedAddonsSummary: String? {
|
||||
let names = selectedAddonItems.map(\.name)
|
||||
let names = selectedAddonItems.map { pair in
|
||||
pair.quantity > 1 ? "\(pair.item.name) x\(pair.quantity)" : pair.item.name
|
||||
}
|
||||
if names.isEmpty { return nil }
|
||||
return names.joined(separator: ", ")
|
||||
}
|
||||
|
||||
private var selectedAddonsPayload: [CartItemAddonState] {
|
||||
selectedAddonItems.map { pair in
|
||||
CartItemAddonState(
|
||||
id: pair.item.id,
|
||||
name: pair.item.name,
|
||||
quantity: pair.quantity,
|
||||
unitPrice: pair.item.price ?? 0
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private var addButtonTitle: String {
|
||||
if quantity <= 0 {
|
||||
return "Remover do carrinho"
|
||||
@@ -72,6 +94,10 @@ struct ProductDetailSheet: View {
|
||||
Text("Inclui adicionais: \(formatCurrency(addonsTotal))")
|
||||
.font(.caption)
|
||||
.foregroundStyle(AppColors.textMuted)
|
||||
} else {
|
||||
Text("Sem adicionais")
|
||||
.font(.caption)
|
||||
.foregroundStyle(AppColors.textMuted)
|
||||
}
|
||||
|
||||
if product.addonGroups.isEmpty == false {
|
||||
@@ -86,28 +112,47 @@ struct ProductDetailSheet: View {
|
||||
.foregroundStyle(AppColors.textPrimary)
|
||||
|
||||
ForEach(group.items) { item in
|
||||
Button {
|
||||
if selectedAddonItemIds.contains(item.id) {
|
||||
selectedAddonItemIds.remove(item.id)
|
||||
} else {
|
||||
selectedAddonItemIds.insert(item.id)
|
||||
}
|
||||
} label: {
|
||||
HStack {
|
||||
Image(systemName: selectedAddonItemIds.contains(item.id) ? "checkmark.circle.fill" : "circle")
|
||||
.font(.system(size: 18, weight: .semibold))
|
||||
.foregroundStyle(selectedAddonItemIds.contains(item.id) ? AppColors.primary : AppColors.textMuted)
|
||||
|
||||
HStack(spacing: 10) {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text(item.name)
|
||||
.font(AppTypography.body)
|
||||
.foregroundStyle(AppColors.textMuted)
|
||||
Spacer()
|
||||
Text(String(format: "+ R$ %.2f", item.price ?? 0).replacingOccurrences(of: ".", with: ","))
|
||||
.font(AppTypography.body)
|
||||
.font(.caption)
|
||||
.foregroundStyle(AppColors.textMuted)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
HStack(spacing: 8) {
|
||||
Button(action: { decrementAddon(item.id) }) {
|
||||
Image(systemName: "minus")
|
||||
.font(.system(size: 12, weight: .bold))
|
||||
.foregroundStyle(AppColors.textPrimary)
|
||||
.frame(width: 26, height: 26)
|
||||
.background(AppColors.brandSoft)
|
||||
.clipShape(Circle())
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.disabled(quantity(forAddonId: item.id) <= 0 || quantity <= 0)
|
||||
|
||||
Text("\(quantity(forAddonId: item.id))")
|
||||
.font(AppTypography.heading3)
|
||||
.foregroundStyle(AppColors.textPrimary)
|
||||
.frame(minWidth: 18)
|
||||
|
||||
Button(action: { incrementAddon(item.id) }) {
|
||||
Image(systemName: "plus")
|
||||
.font(.system(size: 12, weight: .bold))
|
||||
.foregroundStyle(AppColors.textPrimary)
|
||||
.frame(width: 26, height: 26)
|
||||
.background(AppColors.tertiary)
|
||||
.clipShape(Circle())
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.disabled(quantity <= 0)
|
||||
}
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
}
|
||||
.padding(12)
|
||||
@@ -160,7 +205,9 @@ struct ProductDetailSheet: View {
|
||||
productId: product.id,
|
||||
storeId: storeId,
|
||||
name: product.name,
|
||||
imageURL: imageURL,
|
||||
details: selectedAddonsSummary,
|
||||
addons: selectedAddonsPayload,
|
||||
quantity: quantity,
|
||||
unitPrice: unitPrice
|
||||
)
|
||||
@@ -183,14 +230,50 @@ struct ProductDetailSheet: View {
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
quantity = currentQuantityForItemId(cartItemId)
|
||||
let existing = currentQuantityForItemId(cartItemId)
|
||||
quantity = existing > 0 ? existing : 1
|
||||
}
|
||||
.onChange(of: selectedAddonItemIds) { _, _ in
|
||||
quantity = currentQuantityForItemId(cartItemId)
|
||||
.onChange(of: selectedAddonQuantities) { _, _ in
|
||||
// Keep the main quantity stable when changing addon quantities.
|
||||
// Only hydrate from cart if this exact configuration already exists.
|
||||
let existingQuantity = currentQuantityForItemId(cartItemId)
|
||||
if existingQuantity > 0 {
|
||||
quantity = existingQuantity
|
||||
}
|
||||
}
|
||||
.onChange(of: quantity) { _, newValue in
|
||||
if newValue <= 0 {
|
||||
selectedAddonQuantities.removeAll()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func formatCurrency(_ value: Double) -> String {
|
||||
String(format: "R$ %.2f", value).replacingOccurrences(of: ".", with: ",")
|
||||
}
|
||||
|
||||
private var encodedAddonKey: String {
|
||||
let tokens = selectedAddonQuantities
|
||||
.filter { $0.value > 0 }
|
||||
.map { "\($0.key):\($0.value)" }
|
||||
.sorted()
|
||||
return tokens.isEmpty ? "base" : tokens.joined(separator: ",")
|
||||
}
|
||||
|
||||
private func quantity(forAddonId addonId: String) -> Int {
|
||||
selectedAddonQuantities[addonId] ?? 0
|
||||
}
|
||||
|
||||
private func incrementAddon(_ addonId: String) {
|
||||
selectedAddonQuantities[addonId, default: 0] += 1
|
||||
}
|
||||
|
||||
private func decrementAddon(_ addonId: String) {
|
||||
let current = selectedAddonQuantities[addonId] ?? 0
|
||||
if current <= 1 {
|
||||
selectedAddonQuantities.removeValue(forKey: addonId)
|
||||
} else {
|
||||
selectedAddonQuantities[addonId] = current - 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user