This commit is contained in:
Daniel Arantes Loverde
2026-02-15 08:18:06 -03:00
parent d7b8b1864a
commit 0f5ad4c268
35 changed files with 4090 additions and 309 deletions

View File

@@ -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
}
}
}