Add contractor management and integrate with task completions
Features: - Add full contractor CRUD functionality (Android & iOS) - Add contractor selection to task completion dialog - Display contractor info in completion cards - Add ContractorSpecialty model and API integration - Add contractors tab to bottom navigation - Replace hardcoded specialty lists with API data - Update lookup endpoints to return arrays instead of paginated responses Changes: - Add Contractor models (ContractorSummary, ContractorDetail, ContractorCreate/UpdateRequest) - Add ContractorApi with endpoints for list, detail, create, update, delete, toggle favorite - Add ContractorViewModel for state management - Add ContractorsScreen and ContractorDetailScreen for Android - Add AddContractorDialog with form validation - Add Contractor views for iOS (list, detail, form) - Update CompleteTaskDialog to include contractor selection - Update CompletionCardView to show contractor name and phone - Add contractor field to TaskCompletion model - Update LookupsApi to return List<T> instead of paginated responses - Update LookupsRepository and LookupsViewModel to handle array responses - Update LookupsManager (iOS) to handle array responses for contractor specialties 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -8,6 +8,7 @@ struct CompleteTaskView: View {
|
||||
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
@StateObject private var taskViewModel = TaskViewModel()
|
||||
@StateObject private var contractorViewModel = ContractorViewModel()
|
||||
@State private var completedByName: String = ""
|
||||
@State private var actualCost: String = ""
|
||||
@State private var notes: String = ""
|
||||
@@ -18,6 +19,8 @@ struct CompleteTaskView: View {
|
||||
@State private var showError: Bool = false
|
||||
@State private var errorMessage: String = ""
|
||||
@State private var showCamera: Bool = false
|
||||
@State private var selectedContractor: ContractorSummary? = nil
|
||||
@State private var showContractorPicker: Bool = false
|
||||
|
||||
var body: some View {
|
||||
NavigationStack {
|
||||
@@ -50,11 +53,49 @@ struct CompleteTaskView: View {
|
||||
Text("Task Details")
|
||||
}
|
||||
|
||||
// Contractor Selection Section
|
||||
Section {
|
||||
Button(action: {
|
||||
showContractorPicker = true
|
||||
}) {
|
||||
HStack {
|
||||
Label("Select Contractor", systemImage: "wrench.and.screwdriver")
|
||||
.foregroundStyle(.primary)
|
||||
|
||||
Spacer()
|
||||
|
||||
if let contractor = selectedContractor {
|
||||
VStack(alignment: .trailing) {
|
||||
Text(contractor.name)
|
||||
.foregroundStyle(.secondary)
|
||||
if let company = contractor.company {
|
||||
Text(company)
|
||||
.font(.caption)
|
||||
.foregroundStyle(.tertiary)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Text("None")
|
||||
.foregroundStyle(.tertiary)
|
||||
}
|
||||
|
||||
Image(systemName: "chevron.right")
|
||||
.font(.caption)
|
||||
.foregroundStyle(.tertiary)
|
||||
}
|
||||
}
|
||||
} header: {
|
||||
Text("Contractor (Optional)")
|
||||
} footer: {
|
||||
Text("Select a contractor if they completed this work, or leave blank for manual entry.")
|
||||
}
|
||||
|
||||
// Completion Details Section
|
||||
Section {
|
||||
LabeledContent {
|
||||
TextField("Your name", text: $completedByName)
|
||||
.multilineTextAlignment(.trailing)
|
||||
.disabled(selectedContractor != nil)
|
||||
} label: {
|
||||
Label("Completed By", systemImage: "person")
|
||||
}
|
||||
@@ -228,6 +269,15 @@ struct CompleteTaskView: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
.sheet(isPresented: $showContractorPicker) {
|
||||
ContractorPickerView(
|
||||
selectedContractor: $selectedContractor,
|
||||
contractorViewModel: contractorViewModel
|
||||
)
|
||||
}
|
||||
.onAppear {
|
||||
contractorViewModel.loadContractors()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -249,7 +299,11 @@ struct CompleteTaskView: View {
|
||||
let request = TaskCompletionCreateRequest(
|
||||
task: task.id,
|
||||
completedByUser: nil,
|
||||
contractor: selectedContractor != nil ? KotlinInt(int: selectedContractor!.id) : nil,
|
||||
completedByName: completedByName.isEmpty ? nil : completedByName,
|
||||
completedByPhone: selectedContractor?.phone ?? "",
|
||||
completedByEmail: "",
|
||||
companyName: selectedContractor?.company ?? "",
|
||||
completionDate: currentDate,
|
||||
actualCost: actualCost.isEmpty ? nil : actualCost,
|
||||
notes: notes.isEmpty ? nil : notes,
|
||||
@@ -310,3 +364,96 @@ extension KotlinByteArray {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Contractor Picker View
|
||||
struct ContractorPickerView: View {
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
@Binding var selectedContractor: ContractorSummary?
|
||||
@ObservedObject var contractorViewModel: ContractorViewModel
|
||||
|
||||
var body: some View {
|
||||
NavigationStack {
|
||||
List {
|
||||
// None option
|
||||
Button(action: {
|
||||
selectedContractor = nil
|
||||
dismiss()
|
||||
}) {
|
||||
HStack {
|
||||
VStack(alignment: .leading) {
|
||||
Text("None (Manual Entry)")
|
||||
.foregroundStyle(.primary)
|
||||
Text("Enter name manually")
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
Spacer()
|
||||
if selectedContractor == nil {
|
||||
Image(systemName: "checkmark")
|
||||
.foregroundStyle(AppColors.primary)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Contractors list
|
||||
if contractorViewModel.isLoading {
|
||||
HStack {
|
||||
Spacer()
|
||||
ProgressView()
|
||||
Spacer()
|
||||
}
|
||||
} else if let errorMessage = contractorViewModel.errorMessage {
|
||||
Text(errorMessage)
|
||||
.foregroundStyle(.red)
|
||||
.font(.caption)
|
||||
} else {
|
||||
ForEach(contractorViewModel.contractors, id: \.id) { contractor in
|
||||
Button(action: {
|
||||
selectedContractor = contractor
|
||||
dismiss()
|
||||
}) {
|
||||
HStack {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text(contractor.name)
|
||||
.foregroundStyle(.primary)
|
||||
|
||||
if let company = contractor.company {
|
||||
Text(company)
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
|
||||
if let specialty = contractor.specialty {
|
||||
HStack(spacing: 4) {
|
||||
Image(systemName: "wrench.and.screwdriver")
|
||||
.font(.caption2)
|
||||
Text(specialty)
|
||||
.font(.caption2)
|
||||
}
|
||||
.foregroundStyle(.tertiary)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
if selectedContractor?.id == contractor.id {
|
||||
Image(systemName: "checkmark")
|
||||
.foregroundStyle(AppColors.primary)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationTitle("Select Contractor")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .cancellationAction) {
|
||||
Button("Cancel") {
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -243,7 +243,11 @@ class TaskViewModel: ObservableObject {
|
||||
let request = TaskCompletionCreateRequest(
|
||||
task: taskId,
|
||||
completedByUser: nil,
|
||||
contractor: nil,
|
||||
completedByName: nil,
|
||||
completedByPhone: nil,
|
||||
completedByEmail: nil,
|
||||
companyName: nil,
|
||||
completionDate: currentDate,
|
||||
actualCost: nil,
|
||||
notes: nil,
|
||||
|
||||
Reference in New Issue
Block a user