diff --git a/TorqueNet.xcodeproj/project.xcworkspace/xcuserdata/mac.xcuserdatad/UserInterfaceState.xcuserstate b/TorqueNet.xcodeproj/project.xcworkspace/xcuserdata/mac.xcuserdatad/UserInterfaceState.xcuserstate index c177c6e..f412e9b 100644 Binary files a/TorqueNet.xcodeproj/project.xcworkspace/xcuserdata/mac.xcuserdatad/UserInterfaceState.xcuserstate and b/TorqueNet.xcodeproj/project.xcworkspace/xcuserdata/mac.xcuserdatad/UserInterfaceState.xcuserstate differ diff --git a/TorqueNet/Core/Model/UploadError.swift b/TorqueNet/Core/Model/UploadError.swift index 7b73d9a..23ecf18 100644 --- a/TorqueNet/Core/Model/UploadError.swift +++ b/TorqueNet/Core/Model/UploadError.swift @@ -40,4 +40,5 @@ enum UploadError: LocalizedError { return "Network error. Please check your connection." } } + } diff --git a/TorqueNet/Core/Navigation/Route.swift b/TorqueNet/Core/Navigation/Route.swift index 723ba59..a29752d 100644 --- a/TorqueNet/Core/Navigation/Route.swift +++ b/TorqueNet/Core/Navigation/Route.swift @@ -30,4 +30,5 @@ enum Route: Hashable { case changePassword case notification case editProfile + case manageAuction } diff --git a/TorqueNet/Core/Navigation/Router.swift b/TorqueNet/Core/Navigation/Router.swift index 4396007..6342e77 100644 --- a/TorqueNet/Core/Navigation/Router.swift +++ b/TorqueNet/Core/Navigation/Router.swift @@ -182,5 +182,9 @@ func viewForRoute(_ route: Route, router: Router) -> some View{ EditProfileView() .navigationBarBackButtonHidden() + case .manageAuction: + ManageAuctionsView() + .navigationBarBackButtonHidden() + } } diff --git a/TorqueNet/Features/Auction/Domain/Model/AuctionUploadModel.swift b/TorqueNet/Features/Auction/Domain/Model/AuctionUploadModel.swift index e2b0abf..7bd53cb 100644 --- a/TorqueNet/Features/Auction/Domain/Model/AuctionUploadModel.swift +++ b/TorqueNet/Features/Auction/Domain/Model/AuctionUploadModel.swift @@ -14,6 +14,7 @@ import Combine struct AuctionUploadModel: Identifiable, Codable { let id: String let imageUrls: [String] + let sellerId: String let carTitle: String let subtitle: String let lotNumber: String @@ -61,6 +62,7 @@ struct AuctionUploadModel: Identifiable, Codable { enum CodingKeys: String, CodingKey { case id case imageUrls = "image_urls" + case sellerId = "seller_id" case carTitle = "car_title" case subtitle case lotNumber = "lot_number" diff --git a/TorqueNet/Features/Auction/Domain/UseCase/UploadAuctionUseCase.swift b/TorqueNet/Features/Auction/Domain/UseCase/UploadAuctionUseCase.swift index 3b87be3..62a4aca 100644 --- a/TorqueNet/Features/Auction/Domain/UseCase/UploadAuctionUseCase.swift +++ b/TorqueNet/Features/Auction/Domain/UseCase/UploadAuctionUseCase.swift @@ -22,6 +22,7 @@ class UploadAuctionUseCase { func executeCreateAuction( images: [UIImage], + sellerId: String, carTitle: String, subtitle: String, lotNumber: String, @@ -104,6 +105,7 @@ class UploadAuctionUseCase { let auction = AuctionUploadModel( id: auctionId, imageUrls: imageUrls, + sellerId: sellerId, carTitle: carTitle, subtitle: subtitle, lotNumber: lotNumber, @@ -158,6 +160,7 @@ class UploadAuctionUseCase { let updatedAuction = AuctionUploadModel( id: auctionId, imageUrls: auction.imageUrls, + sellerId: auction.sellerId, carTitle: auction.carTitle, subtitle: auction.subtitle, lotNumber: auction.lotNumber, diff --git a/TorqueNet/Features/Auction/Presentation/UploadAuction/UploadAuctionViewModel.swift b/TorqueNet/Features/Auction/Presentation/UploadAuction/UploadAuctionViewModel.swift index 58e27d9..4c49086 100644 --- a/TorqueNet/Features/Auction/Presentation/UploadAuction/UploadAuctionViewModel.swift +++ b/TorqueNet/Features/Auction/Presentation/UploadAuction/UploadAuctionViewModel.swift @@ -8,6 +8,7 @@ import Foundation import UIKit import SwiftUI +import FirebaseAuth @MainActor class AuctionUploadViewModel: ObservableObject { @@ -199,6 +200,7 @@ class AuctionUploadViewModel: ObservableObject { let result = await uploadUseCase.executeCreateAuction( images: form.selectedImages, + sellerId: Auth.auth().currentUser?.uid ?? "", carTitle: form.carTitle, subtitle: form.subtitle, lotNumber: form.lotNumber, diff --git a/TorqueNet/Features/AuctionManagement/Data/Repository/ManageAuctionRepositoryImpl.swift b/TorqueNet/Features/AuctionManagement/Data/Repository/ManageAuctionRepositoryImpl.swift new file mode 100644 index 0000000..633eda8 --- /dev/null +++ b/TorqueNet/Features/AuctionManagement/Data/Repository/ManageAuctionRepositoryImpl.swift @@ -0,0 +1,55 @@ +// +// ManageAuctionRepositoryImpl.swift +// TorqueNet +// +// Created by MAC on 20/02/2026. +// + +import Foundation +import FirebaseCore + +class ManageAuctionRepositoryImpl: ManageAuctionRepository { + static let shared = ManageAuctionRepositoryImpl() + + func fetchSellerAuctions(sellerId: String) async -> Result<[ManageAuctionItem], UploadError> { + do { + let snapshot = try await FirestoreConstants.AuctionsCollection + .whereField("seller_id", isEqualTo: sellerId) + .getDocuments() + + let auctions: [ManageAuctionItem] = snapshot.documents.compactMap { document in + guard let model = try? document.data(as: AuctionUploadModel.self) else { + return nil + } + return ManageAuctionItem(from: model) + } + + return .success(auctions) + + } catch { + return .failure( + .firestoreFetchFailed(error.localizedDescription, "seller auctions") + ) + } + } + + func updateAuctionDetails(auctionId: String, status: AuctionStatus, startDate: Date, endDate: Date) async -> Result { + do { + let updateData: [String: Any] = [ + "auction_status": status.rawValue, + "created_at": Timestamp(date: startDate), + "auction_end_date": Timestamp(date: endDate), + "updated_at": Timestamp(date: Date()) + ] + + try await FirestoreConstants.AuctionsCollection + .document(auctionId) + .updateData(updateData) + + return .success(()) + + } catch { + return .failure(.firestoreUpdateFailed(error.localizedDescription)) + } + } +} diff --git a/TorqueNet/Features/AuctionManagement/Domain/Model/ManageAuctionItem.swift b/TorqueNet/Features/AuctionManagement/Domain/Model/ManageAuctionItem.swift new file mode 100644 index 0000000..8336061 --- /dev/null +++ b/TorqueNet/Features/AuctionManagement/Domain/Model/ManageAuctionItem.swift @@ -0,0 +1,36 @@ +// +// ManageAuctionItem.swift +// TorqueNet +// +// Created by MAC on 20/02/2026. +// + +import Foundation + +struct ManageAuctionItem: Identifiable { + let id: String + let carTitle: String + let imageUrl: String + let lotNumber: String + let status: AuctionStatus + let startDate: Date + let endDate: Date + let startingBid: Double + let currentBid: Double + let bidCount: Int + let sellerId: String + + init(from model: AuctionUploadModel) { + self.id = model.id + self.carTitle = model.carTitle + self.imageUrl = model.imageUrls.first ?? "" + self.lotNumber = model.lotNumber + self.status = AuctionStatus(rawValue: model.auctionStatus) ?? .ongoing + self.startDate = model.createdAt.dateValue() + self.endDate = model.auctionEndDate.dateValue() + self.startingBid = model.startingBid + self.currentBid = model.currentBid + self.bidCount = model.bidCount + self.sellerId = model.seller + } +} diff --git a/TorqueNet/Features/AuctionManagement/Domain/Repository/ManageAuctionRepository.swift b/TorqueNet/Features/AuctionManagement/Domain/Repository/ManageAuctionRepository.swift new file mode 100644 index 0000000..8795223 --- /dev/null +++ b/TorqueNet/Features/AuctionManagement/Domain/Repository/ManageAuctionRepository.swift @@ -0,0 +1,13 @@ +// +// ManageAuctionRepository.swift +// TorqueNet +// +// Created by MAC on 20/02/2026. +// + +import Foundation + +protocol ManageAuctionRepository { + func fetchSellerAuctions(sellerId: String) async -> Result<[ManageAuctionItem], UploadError> + func updateAuctionDetails(auctionId: String, status: AuctionStatus, startDate: Date, endDate: Date) async -> Result +} diff --git a/TorqueNet/Features/AuctionManagement/Domain/UseCase/FetchSellerAuctionUseCase.swift b/TorqueNet/Features/AuctionManagement/Domain/UseCase/FetchSellerAuctionUseCase.swift new file mode 100644 index 0000000..f060095 --- /dev/null +++ b/TorqueNet/Features/AuctionManagement/Domain/UseCase/FetchSellerAuctionUseCase.swift @@ -0,0 +1,18 @@ +// +// FetchSellerAuctionUseCase.swift +// TorqueNet +// +// Created by MAC on 20/02/2026. +// + +class FetchSellerAuctionsUseCase { + private let repository: ManageAuctionRepository + + init(repository: ManageAuctionRepository) { + self.repository = repository + } + + func execute(sellerId: String) async -> Result<[ManageAuctionItem], UploadError> { + return await repository.fetchSellerAuctions(sellerId: sellerId) + } +} diff --git a/TorqueNet/Features/AuctionManagement/Domain/UseCase/UpdateAuctionUseCase.swift b/TorqueNet/Features/AuctionManagement/Domain/UseCase/UpdateAuctionUseCase.swift new file mode 100644 index 0000000..f788253 --- /dev/null +++ b/TorqueNet/Features/AuctionManagement/Domain/UseCase/UpdateAuctionUseCase.swift @@ -0,0 +1,33 @@ +// +// UpdateAuctionUseCase.swift +// TorqueNet +// +// Created by MAC on 20/02/2026. +// + +import Foundation + +class UpdateAuctionUseCase { + private let repository: ManageAuctionRepository + + init(repository: ManageAuctionRepository) { + self.repository = repository + } + + func execute(auctionId: String, status: AuctionStatus, startDate: Date, endDate: Date) async -> Result { + if endDate <= startDate { + return .failure(.invalidData("End date must be after start date")) + } + + if status == .upcoming && startDate <= Date() { + return .failure(.invalidData("Scheduled auctions must have a future start date")) + } + + return await repository.updateAuctionDetails( + auctionId: auctionId, + status: status, + startDate: startDate, + endDate: endDate + ) + } +} diff --git a/TorqueNet/Features/AuctionManagement/Presentation/ManageAuctionUiState.swift b/TorqueNet/Features/AuctionManagement/Presentation/ManageAuctionUiState.swift new file mode 100644 index 0000000..a1fc0f5 --- /dev/null +++ b/TorqueNet/Features/AuctionManagement/Presentation/ManageAuctionUiState.swift @@ -0,0 +1,20 @@ +// +// ManageAuctionUiState.swift +// TorqueNet +// +// Created by MAC on 20/02/2026. +// + +struct ManageAuctionUiState { + var auctions: [ManageAuctionItem] = [] + var filteredAuctions: [ManageAuctionItem] = [] + var selectedFilter: AuctionStatus? = nil + + // var isLoading: Bool = false + var errorMessage: String = "" + // var hasError: Bool = false + var showError: Bool = false + var auctionState: FetchState = .good + var showEditSheet: Bool = false + var selectedAuction: ManageAuctionItem? = nil +} diff --git a/TorqueNet/Features/AuctionManagement/Presentation/ManageAuctionView.swift b/TorqueNet/Features/AuctionManagement/Presentation/ManageAuctionView.swift new file mode 100644 index 0000000..faae7fd --- /dev/null +++ b/TorqueNet/Features/AuctionManagement/Presentation/ManageAuctionView.swift @@ -0,0 +1,452 @@ +// +// ManageAuctionView.swift +// TorqueNet +// +// Created by MAC on 20/02/2026. +// + +import SwiftUI +import FirebaseAuth + + +struct ManageAuctionsView: View { + @StateObject private var viewModel: ManageAuctionViewModel = ManageAuctionViewModel() + @EnvironmentObject var router: Router + + var body: some View { + VStack(spacing: 0) { + filterChipsView + + Divider() + + Group { + if viewModel.uiState.showError { + errorView + } else if viewModel.uiState.filteredAuctions.isEmpty { + emptyView + } else { + auctionListView + } + } + } + .background(Color.theme.surfaceColor) + .customTopAppBar( + title: "Manage Auctions", + leadingIcon: "chevron.left", + navbarTitleDisplayMode: .inline, + onLeadingTap: { + router.pop() + }, + trailingIcon: "", + onTrailingTap: {}, + trailingMenu: {} + ) + .fullScreenProgressOverlay(isShowing: viewModel.uiState.auctionState == .isLoading) + .sheet(isPresented: $viewModel.uiState.showEditSheet) { + if let auction = viewModel.uiState.selectedAuction { + EditAuctionSheet(auction: auction, viewModel: viewModel) + } + } + .task { + await viewModel.loadAuctions() + } + .alert("Error", isPresented: $viewModel.uiState.showError) { + Button("OK", role: .cancel) { } + } message: { + Text(viewModel.uiState.errorMessage) + } + } + + private var filterChipsView: some View { + ScrollView(.horizontal, showsIndicators: false) { + HStack(spacing: 12) { + FilterChip( + title: "All", + count: viewModel.uiState.auctions.count, + isSelected: viewModel.uiState.selectedFilter == nil + ) { + viewModel.filterByStatus(nil) + } + + ForEach(AuctionStatus.allCases, id: \.self) { status in + FilterChip( + title: status.rawValue, + count: viewModel.uiState.auctions.filter { $0.status == status }.count, + isSelected: viewModel.uiState.selectedFilter == status, + color: status.color + ) { + viewModel.filterByStatus(status) + } + } + } + .padding() + } + .background(Color.theme.surfaceColor) + } + + private var errorView: some View { + VStack(spacing: 16) { + Image(systemName: "exclamationmark.triangle.fill") + .font(.system(size: 40)) + .foregroundColor(.red) + Text(viewModel.uiState.errorMessage) + .font(.custom("Exo2-Regular", size: 14)) + .foregroundColor(.theme.onSurfaceColor) + .multilineTextAlignment(.center) + Button(action: { + Task { await viewModel.loadAuctions() } + }) { + Text("Retry") + .font(.custom("Exo2-SemiBold", size: 16)) + .foregroundColor(.white) + .padding(.horizontal, 32) + .padding(.vertical, 12) + .background(Color.theme.primaryColor) + .cornerRadius(12) + } + } + .padding() + .frame(maxWidth: .infinity, maxHeight: .infinity) + } + + private var emptyView: some View { + VStack(spacing: 16) { + Image(systemName: "hammer.fill") + .font(.system(size: 40)) + .foregroundColor(.theme.onSurfaceColor.opacity(0.4)) + Text(viewModel.uiState.selectedFilter == nil ? "No auctions yet" : "No \(viewModel.uiState.selectedFilter?.rawValue ?? "") auctions") + .font(.custom("Exo2-SemiBold", size: 16)) + .foregroundColor(.theme.onSurfaceColor) + Text("Start by uploading your first car for auction") + .font(.custom("Exo2-Regular", size: 14)) + .foregroundColor(.theme.onSurfaceColor.opacity(0.6)) + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + } + + private var auctionListView: some View { + ScrollView(.vertical, showsIndicators: false) { + LazyVStack(spacing: 16) { + ForEach(viewModel.uiState.filteredAuctions) { auction in + ManageAuctionCard(auction: auction) { + viewModel.selectAuctionForEdit(auction) + } + } + } + .padding() + } + } +} + +struct FilterChip: View { + let title: String + let count: Int + var isSelected: Bool + var color: Color = .theme.primaryColor + let action: () -> Void + + var body: some View { + Button(action: action) { + HStack(spacing: 6) { + Text(title) + .font(.custom("Exo2-SemiBold", size: 13)) + + Text("(\(count))") + .font(.custom("Exo2-Regular", size: 12)) + } + .foregroundColor(isSelected ? .white : .theme.onSurfaceColor) + .padding(.horizontal, 16) + .padding(.vertical, 8) + .background(isSelected ? color : Color.theme.onSurfaceColor.opacity(0.1)) + .cornerRadius(20) + } + } +} + +struct ManageAuctionCard: View { + let auction: ManageAuctionItem + let onEdit: () -> Void + + var body: some View { + VStack(spacing: 12) { + HStack(spacing: 12) { + // Car Image + AsyncImage(url: URL(string: auction.imageUrl)) { image in + image + .resizable() + .scaledToFill() + } placeholder: { + RoundedRectangle(cornerRadius: 10) + .fill(Color.theme.onSurfaceColor.opacity(0.1)) + .overlay( + Image(systemName: "car.fill") + .foregroundColor(.theme.onSurfaceColor.opacity(0.3)) + ) + } + .frame(width: 100, height: 80) + .clipShape(RoundedRectangle(cornerRadius: 10)) + + VStack(alignment: .leading, spacing: 6) { + // Title & Status + HStack { + Text(auction.carTitle) + .font(.custom("Exo2-SemiBold", size: 15)) + .foregroundColor(.theme.onSurfaceColor) + .lineLimit(1) + + Spacer() + + Text(auction.status.rawValue) + .font(.custom("Exo2-Regular", size: 10)) + .foregroundColor(.white) + .padding(.horizontal, 8) + .padding(.vertical, 4) + .background(auction.status.color) + .cornerRadius(6) + } + + // Lot Number + Text("Lot #\(auction.lotNumber)") + .font(.custom("Exo2-Regular", size: 12)) + .foregroundColor(.theme.onSurfaceColor.opacity(0.6)) + + // Bids + HStack(spacing: 4) { + Image(systemName: "hammer.fill") + .font(.system(size: 10)) + .foregroundColor(.theme.primaryColor) + Text("\(auction.bidCount) bids") + .font(.custom("Exo2-Regular", size: 12)) + .foregroundColor(.theme.onSurfaceColor.opacity(0.6)) + } + } + } + + Divider() + + // Details Row + HStack(spacing: 16) { + VStack(alignment: .leading, spacing: 4) { + Text("Start Date") + .font(.custom("Exo2-Regular", size: 11)) + .foregroundColor(.theme.onSurfaceColor.opacity(0.5)) + Text(formatDate(auction.startDate)) + .font(.custom("Exo2-SemiBold", size: 12)) + .foregroundColor(.theme.onSurfaceColor) + } + + VStack(alignment: .leading, spacing: 4) { + Text("End Date") + .font(.custom("Exo2-Regular", size: 11)) + .foregroundColor(.theme.onSurfaceColor.opacity(0.5)) + Text(formatDate(auction.endDate)) + .font(.custom("Exo2-SemiBold", size: 12)) + .foregroundColor(.theme.onSurfaceColor) + } + + Spacer() + + Button(action: onEdit) { + HStack(spacing: 4) { + Image(systemName: "pencil") + .font(.system(size: 12)) + Text("Edit") + .font(.custom("Exo2-SemiBold", size: 13)) + } + .foregroundColor(.theme.primaryColor) + .padding(.horizontal, 12) + .padding(.vertical, 6) + .background(Color.theme.primaryColor.opacity(0.1)) + .cornerRadius(8) + } + } + } + .padding() + .background(Color.theme.surfaceColor) + .cornerRadius(12) + .shadow(color: .theme.onSurfaceColor.opacity(0.08), radius: 8, x: 0, y: 4) + } + + private func formatDate(_ date: Date) -> String { + let formatter = DateFormatter() + formatter.dateStyle = .short + formatter.timeStyle = .short + return formatter.string(from: date) + } +} + +struct EditAuctionSheet: View { + let auction: ManageAuctionItem + @ObservedObject var viewModel: ManageAuctionViewModel + + @State private var selectedStatus: AuctionStatus + @State private var startDate: Date + @State private var endDate: Date + @State private var showStartDatePicker = false + @State private var showEndDatePicker = false + + init(auction: ManageAuctionItem, viewModel: ManageAuctionViewModel) { + self.auction = auction + self.viewModel = viewModel + _selectedStatus = State(initialValue: auction.status) + _startDate = State(initialValue: auction.startDate) + _endDate = State(initialValue: auction.endDate) + } + + var body: some View { + NavigationView { + ScrollView { + VStack(alignment: .leading, spacing: 24) { + // Car Preview + HStack(spacing: 12) { + AsyncImage(url: URL(string: auction.imageUrl)) { image in + image + .resizable() + .scaledToFill() + } placeholder: { + RoundedRectangle(cornerRadius: 10) + .fill(Color.theme.onSurfaceColor.opacity(0.1)) + } + .frame(width: 80, height: 60) + .clipShape(RoundedRectangle(cornerRadius: 10)) + + VStack(alignment: .leading, spacing: 4) { + Text(auction.carTitle) + .font(.custom("Exo2-SemiBold", size: 16)) + .foregroundColor(.theme.onSurfaceColor) + Text("Lot #\(auction.lotNumber)") + .font(.custom("Exo2-Regular", size: 13)) + .foregroundColor(.theme.onSurfaceColor.opacity(0.6)) + } + } + .padding() + .background(Color.theme.surfaceColor) + .cornerRadius(12) + + // Status Picker + VStack(alignment: .leading, spacing: 12) { + Text("Auction Status") + .font(.custom("Exo2-Medium", size: 16)) + .foregroundColor(.theme.onSurfaceColor) + + Picker("Status", selection: $selectedStatus) { + ForEach(AuctionStatus.allCases, id: \.self) { status in + Text(status.rawValue).tag(status) + } + } + .pickerStyle(.segmented) + } + + // Start Date + VStack(alignment: .leading, spacing: 12) { + Text("Start Date & Time") + .font(.custom("Exo2-Medium", size: 16)) + .foregroundColor(.theme.onSurfaceColor) + + Button(action: { showStartDatePicker.toggle() }) { + HStack { + Image(systemName: "calendar") + .foregroundColor(.theme.primaryColor) + Text(formatDateTime(startDate)) + .font(.custom("Exo2-Regular", size: 14)) + .foregroundColor(.theme.onSurfaceColor) + Spacer() + Image(systemName: "chevron.down") + .foregroundColor(.theme.onSurfaceColor.opacity(0.4)) + .rotationEffect(.degrees(showStartDatePicker ? 180 : 0)) + } + .padding() + .background(Color.theme.surfaceColor) + .cornerRadius(12) + } + + if showStartDatePicker { + DatePicker("", selection: $startDate, displayedComponents: [.date, .hourAndMinute]) + .datePickerStyle(.graphical) + .padding() + .background(Color.theme.surfaceColor) + .cornerRadius(12) + } + } + + // End Date + VStack(alignment: .leading, spacing: 12) { + Text("End Date & Time") + .font(.custom("Exo2-Medium", size: 16)) + .foregroundColor(.theme.onSurfaceColor) + + Button(action: { showEndDatePicker.toggle() }) { + HStack { + Image(systemName: "calendar") + .foregroundColor(.theme.primaryColor) + Text(formatDateTime(endDate)) + .font(.custom("Exo2-Regular", size: 14)) + .foregroundColor(.theme.onSurfaceColor) + Spacer() + Image(systemName: "chevron.down") + .foregroundColor(.theme.onSurfaceColor.opacity(0.4)) + .rotationEffect(.degrees(showEndDatePicker ? 180 : 0)) + } + .padding() + .background(Color.theme.surfaceColor) + .cornerRadius(12) + } + + if showEndDatePicker { + DatePicker("", selection: $endDate, in: startDate..., displayedComponents: [.date, .hourAndMinute]) + .datePickerStyle(.graphical) + .padding() + .background(Color.theme.surfaceColor) + .cornerRadius(12) + } + } + + // Save Button + Button(action: { + Task { + await viewModel.updateAuction( + status: selectedStatus, + startDate: startDate, + endDate: endDate + ) + } + }) { + HStack { + Text(viewModel.uiState.auctionState == .isLoading ? "Updating..." : "Update Auction") + .font(.custom("Exo2-SemiBold", size: 16)) + } + .foregroundColor(.white) + .frame(maxWidth: .infinity) + .padding() + .background(viewModel.uiState.auctionState == .isLoading ? Color.gray : Color.theme.primaryColor) + .cornerRadius(12) + } + .disabled(viewModel.uiState.auctionState == .isLoading ) + } + .padding() + } + .background(Color.theme.surfaceColor.opacity(0.5)) + .navigationTitle("Edit Auction") + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .navigationBarTrailing) { + Button("Cancel") { + viewModel.uiState.showEditSheet = false + } + .foregroundColor(.theme.primaryColor) + } + } + } + } + + private func formatDateTime(_ date: Date) -> String { + let formatter = DateFormatter() + formatter.dateStyle = .medium + formatter.timeStyle = .short + return formatter.string(from: date) + } +} + +#Preview { + ManageAuctionsView() +} diff --git a/TorqueNet/Features/AuctionManagement/Presentation/ManageAuctionViewModel.swift b/TorqueNet/Features/AuctionManagement/Presentation/ManageAuctionViewModel.swift new file mode 100644 index 0000000..b4b0ed2 --- /dev/null +++ b/TorqueNet/Features/AuctionManagement/Presentation/ManageAuctionViewModel.swift @@ -0,0 +1,75 @@ +// +// ManageAuctionViewModel.swift +// TorqueNet +// +// Created by MAC on 20/02/2026. +// + +import Foundation +import FirebaseAuth + +@MainActor +class ManageAuctionViewModel: ObservableObject { + @Published var uiState = ManageAuctionUiState() + let fetchSellerAuctionsUseCase: FetchSellerAuctionsUseCase = FetchSellerAuctionsUseCase(repository: ManageAuctionRepositoryImpl.shared) + let updateAuctionUseCase: UpdateAuctionUseCase = UpdateAuctionUseCase(repository: ManageAuctionRepositoryImpl.shared) + let currentUserId: String = Auth.auth().currentUser?.uid ?? "" + + func loadAuctions() async { + uiState.auctionState = .isLoading + + let result = await fetchSellerAuctionsUseCase.execute(sellerId: currentUserId) + switch result { + case .success(let auctions): + uiState.auctions = auctions + applyFilter() + + case .failure(let error): + uiState.showError = true + uiState.errorMessage = error.localizedDescription + } + uiState.auctionState = .good + } + + func filterByStatus(_ status: AuctionStatus?) { + uiState.selectedFilter = status + applyFilter() + } + + private func applyFilter() { + if let filter = uiState.selectedFilter { + uiState.filteredAuctions = uiState.auctions.filter { $0.status == filter } + } else { + uiState.filteredAuctions = uiState.auctions + } + } + + func selectAuctionForEdit(_ auction: ManageAuctionItem) { + uiState.selectedAuction = auction + uiState.showEditSheet = true + } + + func updateAuction(status: AuctionStatus, startDate: Date, endDate: Date) async { + guard let auctionId = uiState.selectedAuction?.id else { return } + uiState.auctionState = .isLoading + + let result = await updateAuctionUseCase.execute( + auctionId: auctionId, + status: status, + startDate: startDate, + endDate: endDate + ) + + switch result { + case .success: + uiState.showEditSheet = false + await loadAuctions() + + case .failure(let error): + uiState.showError = true + uiState.errorMessage = error.localizedDescription + } + + uiState.auctionState = .good + } +} diff --git a/TorqueNet/Features/Schedule/Domain/Model/AuctionStatus.swift b/TorqueNet/Features/Schedule/Domain/Model/AuctionStatus.swift index 648b866..784ef16 100644 --- a/TorqueNet/Features/Schedule/Domain/Model/AuctionStatus.swift +++ b/TorqueNet/Features/Schedule/Domain/Model/AuctionStatus.swift @@ -7,7 +7,7 @@ import SwiftUI -enum AuctionStatus: String { +enum AuctionStatus: String,CaseIterable { case upcoming = "Upcoming" case ongoing = "Ongoing" diff --git a/TorqueNet/Features/Settings/Presentation/SettingsView.swift b/TorqueNet/Features/Settings/Presentation/SettingsView.swift index 786e52a..abf33c9 100644 --- a/TorqueNet/Features/Settings/Presentation/SettingsView.swift +++ b/TorqueNet/Features/Settings/Presentation/SettingsView.swift @@ -54,7 +54,7 @@ struct SettingsView: View { title: "Auctions", subtitle: "Manage your auctions", action: { - //router.push(.changePassword) + router.push(.manageAuction) } )