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:
Trey t
2025-11-10 19:39:41 -06:00
parent 764a90cb41
commit d3caffa792
25 changed files with 3506 additions and 29 deletions

View File

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