Add residence picker to contractor create/edit screens
Kotlin/KMM: - Update Contractor model with optional residenceId and specialties array - Rename averageRating to rating, update address field names - Add ContractorMinimal model for task references - Add residence picker and multi-select specialty chips to AddContractorDialog - Fix ContractorsScreen and ContractorDetailScreen field references iOS: - Rewrite ContractorFormSheet with residence and specialty pickers - Update ContractorDetailView with FlowLayout for specialties - Add FlowLayout component for wrapping badge layouts - Fix ContractorCard and CompleteTaskView field references - Update ContractorFormState with residence/specialty selection 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
55
iosApp/iosApp/Components/FlowLayout.swift
Normal file
55
iosApp/iosApp/Components/FlowLayout.swift
Normal file
@@ -0,0 +1,55 @@
|
||||
import SwiftUI
|
||||
|
||||
/// A simple wrapping layout that arranges views horizontally and wraps to new rows when needed
|
||||
struct FlowLayout: Layout {
|
||||
var spacing: CGFloat = 8
|
||||
|
||||
func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) -> CGSize {
|
||||
let result = FlowResult(
|
||||
in: proposal.replacingUnspecifiedDimensions().width,
|
||||
subviews: subviews,
|
||||
spacing: spacing
|
||||
)
|
||||
return result.size
|
||||
}
|
||||
|
||||
func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) {
|
||||
let result = FlowResult(
|
||||
in: bounds.width,
|
||||
subviews: subviews,
|
||||
spacing: spacing
|
||||
)
|
||||
for (index, subview) in subviews.enumerated() {
|
||||
subview.place(at: CGPoint(x: bounds.minX + result.positions[index].x,
|
||||
y: bounds.minY + result.positions[index].y),
|
||||
proposal: .unspecified)
|
||||
}
|
||||
}
|
||||
|
||||
struct FlowResult {
|
||||
var size: CGSize = .zero
|
||||
var positions: [CGPoint] = []
|
||||
|
||||
init(in maxWidth: CGFloat, subviews: Subviews, spacing: CGFloat) {
|
||||
var currentX: CGFloat = 0
|
||||
var currentY: CGFloat = 0
|
||||
var lineHeight: CGFloat = 0
|
||||
|
||||
for subview in subviews {
|
||||
let viewSize = subview.sizeThatFits(.unspecified)
|
||||
|
||||
if currentX + viewSize.width > maxWidth && currentX > 0 {
|
||||
currentX = 0
|
||||
currentY += lineHeight + spacing
|
||||
lineHeight = 0
|
||||
}
|
||||
|
||||
positions.append(CGPoint(x: currentX, y: currentY))
|
||||
lineHeight = max(lineHeight, viewSize.height)
|
||||
currentX += viewSize.width + spacing
|
||||
}
|
||||
|
||||
size = CGSize(width: maxWidth, height: currentY + lineHeight)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -44,15 +44,15 @@ struct ContractorCard: View {
|
||||
|
||||
// Info row
|
||||
HStack(spacing: AppSpacing.sm) {
|
||||
// Specialty
|
||||
if let specialty = contractor.specialty {
|
||||
Label(specialty, systemImage: "wrench.and.screwdriver")
|
||||
// Specialties (show first one if available)
|
||||
if let firstSpecialty = contractor.specialties.first {
|
||||
Label(firstSpecialty.name, systemImage: "wrench.and.screwdriver")
|
||||
.font(.caption.weight(.medium))
|
||||
.foregroundColor(Color.appTextSecondary)
|
||||
}
|
||||
|
||||
// Rating
|
||||
if let rating = contractor.averageRating, rating.doubleValue > 0 {
|
||||
if let rating = contractor.rating, rating.doubleValue > 0 {
|
||||
Label(String(format: "%.1f", rating.doubleValue), systemImage: "star.fill")
|
||||
.font(.caption.weight(.medium))
|
||||
.foregroundColor(Color.appAccent)
|
||||
|
||||
@@ -49,23 +49,27 @@ struct ContractorDetailView: View {
|
||||
.foregroundColor(Color.appTextSecondary)
|
||||
}
|
||||
|
||||
// Specialty Badge
|
||||
if let specialty = contractor.specialty {
|
||||
HStack(spacing: AppSpacing.xxs) {
|
||||
Image(systemName: "wrench.and.screwdriver")
|
||||
.font(.caption)
|
||||
Text(specialty)
|
||||
.font(.body)
|
||||
// Specialties Badges
|
||||
if !contractor.specialties.isEmpty {
|
||||
FlowLayout(spacing: AppSpacing.xs) {
|
||||
ForEach(contractor.specialties, id: \.id) { specialty in
|
||||
HStack(spacing: AppSpacing.xxs) {
|
||||
Image(systemName: "wrench.and.screwdriver")
|
||||
.font(.caption)
|
||||
Text(specialty.name)
|
||||
.font(.body)
|
||||
}
|
||||
.padding(.horizontal, AppSpacing.sm)
|
||||
.padding(.vertical, AppSpacing.xxs)
|
||||
.background(Color.appPrimary.opacity(0.1))
|
||||
.foregroundColor(Color.appPrimary)
|
||||
.cornerRadius(AppRadius.full)
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, AppSpacing.sm)
|
||||
.padding(.vertical, AppSpacing.xxs)
|
||||
.background(Color.appPrimary.opacity(0.1))
|
||||
.foregroundColor(Color.appPrimary)
|
||||
.cornerRadius(AppRadius.full)
|
||||
}
|
||||
|
||||
// Rating
|
||||
if let rating = contractor.averageRating, rating.doubleValue > 0 {
|
||||
if let rating = contractor.rating, rating.doubleValue > 0 {
|
||||
HStack(spacing: AppSpacing.xxs) {
|
||||
ForEach(0..<5) { index in
|
||||
Image(systemName: index < Int(rating.doubleValue) ? "star.fill" : "star")
|
||||
@@ -100,31 +104,18 @@ struct ContractorDetailView: View {
|
||||
DetailRow(icon: "envelope", label: "Email", value: email, iconColor: Color.appPrimary)
|
||||
}
|
||||
|
||||
if let secondaryPhone = contractor.secondaryPhone {
|
||||
DetailRow(icon: "phone", label: "Secondary Phone", value: secondaryPhone, iconColor: Color.appAccent)
|
||||
}
|
||||
|
||||
if let website = contractor.website {
|
||||
DetailRow(icon: "globe", label: "Website", value: website, iconColor: Color.appAccent)
|
||||
}
|
||||
}
|
||||
|
||||
// Business Details
|
||||
if contractor.licenseNumber != nil {
|
||||
DetailSection(title: "Business Details") {
|
||||
if let licenseNumber = contractor.licenseNumber {
|
||||
DetailRow(icon: "doc.badge", label: "License Number", value: licenseNumber, iconColor: Color.appPrimary)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Address
|
||||
if contractor.address != nil || contractor.city != nil {
|
||||
if contractor.streetAddress != nil || contractor.city != nil {
|
||||
DetailSection(title: "Address") {
|
||||
let addressComponents = [
|
||||
contractor.address,
|
||||
[contractor.city, contractor.state].compactMap { $0 }.joined(separator: ", "),
|
||||
contractor.zipCode
|
||||
contractor.streetAddress,
|
||||
[contractor.city, contractor.stateProvince].compactMap { $0 }.joined(separator: ", "),
|
||||
contractor.postalCode
|
||||
].compactMap { $0 }.filter { !$0.isEmpty }
|
||||
|
||||
if !addressComponents.isEmpty {
|
||||
|
||||
@@ -3,14 +3,15 @@ import ComposeApp
|
||||
|
||||
// MARK: - Field Focus Enum
|
||||
enum ContractorFormField: Hashable {
|
||||
case name, company, phone, email, secondaryPhone, specialty, licenseNumber, website
|
||||
case address, city, state, zipCode, notes
|
||||
case name, company, phone, email, website
|
||||
case streetAddress, city, stateProvince, postalCode, notes
|
||||
}
|
||||
|
||||
// MARK: - Contractor Form Sheet
|
||||
struct ContractorFormSheet: View {
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
@StateObject private var viewModel = ContractorViewModel()
|
||||
@StateObject private var residenceViewModel = ResidenceViewModel()
|
||||
|
||||
let contractor: Contractor?
|
||||
let onSave: () -> Void
|
||||
@@ -20,25 +21,27 @@ struct ContractorFormSheet: View {
|
||||
@State private var company = ""
|
||||
@State private var phone = ""
|
||||
@State private var email = ""
|
||||
@State private var secondaryPhone = ""
|
||||
@State private var specialty = ""
|
||||
@State private var licenseNumber = ""
|
||||
@State private var website = ""
|
||||
@State private var address = ""
|
||||
@State private var streetAddress = ""
|
||||
@State private var city = ""
|
||||
@State private var state = ""
|
||||
@State private var zipCode = ""
|
||||
@State private var stateProvince = ""
|
||||
@State private var postalCode = ""
|
||||
@State private var notes = ""
|
||||
@State private var isFavorite = false
|
||||
|
||||
// Residence selection (optional)
|
||||
@State private var selectedResidenceId: Int32?
|
||||
@State private var selectedResidenceName: String?
|
||||
@State private var showingResidencePicker = false
|
||||
|
||||
// Specialty selection (multiple)
|
||||
@State private var selectedSpecialtyIds: Set<Int32> = []
|
||||
@State private var showingSpecialtyPicker = false
|
||||
|
||||
@FocusState private var focusedField: ContractorFormField?
|
||||
|
||||
// Lookups from DataCache
|
||||
@State private var contractorSpecialties: [ContractorSpecialty] = []
|
||||
|
||||
private var specialties: [String] {
|
||||
return DataCache.shared.contractorSpecialties.value.map { $0.name }
|
||||
private var specialties: [ContractorSpecialty] {
|
||||
return DataCache.shared.contractorSpecialties.value as? [ContractorSpecialty] ?? []
|
||||
}
|
||||
|
||||
private var canSave: Bool {
|
||||
@@ -74,6 +77,31 @@ struct ContractorFormSheet: View {
|
||||
}
|
||||
.listRowBackground(Color.appBackgroundSecondary)
|
||||
|
||||
// Residence (Optional)
|
||||
Section {
|
||||
Button(action: { showingResidencePicker = true }) {
|
||||
HStack {
|
||||
Image(systemName: "house")
|
||||
.foregroundColor(Color.appPrimary)
|
||||
.frame(width: 24)
|
||||
Text(selectedResidenceName ?? "Personal (No Residence)")
|
||||
.foregroundColor(selectedResidenceName == nil ? Color.appTextSecondary.opacity(0.7) : Color.appTextPrimary)
|
||||
Spacer()
|
||||
Image(systemName: "chevron.down")
|
||||
.font(.caption)
|
||||
.foregroundColor(Color.appTextSecondary.opacity(0.7))
|
||||
}
|
||||
}
|
||||
} header: {
|
||||
Text("Residence (Optional)")
|
||||
} footer: {
|
||||
Text(selectedResidenceId == nil
|
||||
? "Only you will see this contractor"
|
||||
: "All users of \(selectedResidenceName ?? "") will see this contractor")
|
||||
.font(.caption)
|
||||
}
|
||||
.listRowBackground(Color.appBackgroundSecondary)
|
||||
|
||||
// Contact Information
|
||||
Section {
|
||||
HStack {
|
||||
@@ -96,45 +124,6 @@ struct ContractorFormSheet: View {
|
||||
.focused($focusedField, equals: .email)
|
||||
}
|
||||
|
||||
HStack {
|
||||
Image(systemName: "phone.badge.plus")
|
||||
.foregroundColor(Color.appPrimary)
|
||||
.frame(width: 24)
|
||||
TextField("Secondary Phone", text: $secondaryPhone)
|
||||
.keyboardType(.phonePad)
|
||||
.focused($focusedField, equals: .secondaryPhone)
|
||||
}
|
||||
} header: {
|
||||
Text("Contact Information")
|
||||
} footer: {
|
||||
|
||||
}
|
||||
.listRowBackground(Color.appBackgroundSecondary)
|
||||
|
||||
// Business Details
|
||||
Section {
|
||||
Button(action: { showingSpecialtyPicker = true }) {
|
||||
HStack {
|
||||
Image(systemName: "wrench.and.screwdriver")
|
||||
.foregroundColor(Color.appPrimary)
|
||||
.frame(width: 24)
|
||||
Text(specialty.isEmpty ? "Specialty" : specialty)
|
||||
.foregroundColor(specialty.isEmpty ? Color.appTextSecondary.opacity(0.5) : Color.appTextPrimary)
|
||||
Spacer()
|
||||
Image(systemName: "chevron.down")
|
||||
.font(.caption)
|
||||
.foregroundColor(Color.appTextSecondary.opacity(0.7))
|
||||
}
|
||||
}
|
||||
|
||||
HStack {
|
||||
Image(systemName: "doc.badge")
|
||||
.foregroundColor(Color.appPrimary)
|
||||
.frame(width: 24)
|
||||
TextField("License Number", text: $licenseNumber)
|
||||
.focused($focusedField, equals: .licenseNumber)
|
||||
}
|
||||
|
||||
HStack {
|
||||
Image(systemName: "globe")
|
||||
.foregroundColor(Color.appAccent)
|
||||
@@ -146,7 +135,36 @@ struct ContractorFormSheet: View {
|
||||
.focused($focusedField, equals: .website)
|
||||
}
|
||||
} header: {
|
||||
Text("Business Details")
|
||||
Text("Contact Information")
|
||||
}
|
||||
.listRowBackground(Color.appBackgroundSecondary)
|
||||
|
||||
// Specialties (Multi-select)
|
||||
Section {
|
||||
Button(action: { showingSpecialtyPicker = true }) {
|
||||
HStack {
|
||||
Image(systemName: "wrench.and.screwdriver")
|
||||
.foregroundColor(Color.appPrimary)
|
||||
.frame(width: 24)
|
||||
if selectedSpecialtyIds.isEmpty {
|
||||
Text("Select Specialties")
|
||||
.foregroundColor(Color.appTextSecondary.opacity(0.5))
|
||||
} else {
|
||||
let selectedNames = specialties
|
||||
.filter { selectedSpecialtyIds.contains($0.id) }
|
||||
.map { $0.name }
|
||||
Text(selectedNames.joined(separator: ", "))
|
||||
.foregroundColor(Color.appTextPrimary)
|
||||
.lineLimit(2)
|
||||
}
|
||||
Spacer()
|
||||
Image(systemName: "chevron.down")
|
||||
.font(.caption)
|
||||
.foregroundColor(Color.appTextSecondary.opacity(0.7))
|
||||
}
|
||||
}
|
||||
} header: {
|
||||
Text("Specialties")
|
||||
}
|
||||
.listRowBackground(Color.appBackgroundSecondary)
|
||||
|
||||
@@ -156,8 +174,8 @@ struct ContractorFormSheet: View {
|
||||
Image(systemName: "location.fill")
|
||||
.foregroundColor(Color.appError)
|
||||
.frame(width: 24)
|
||||
TextField("Street Address", text: $address)
|
||||
.focused($focusedField, equals: .address)
|
||||
TextField("Street Address", text: $streetAddress)
|
||||
.focused($focusedField, equals: .streetAddress)
|
||||
}
|
||||
|
||||
HStack {
|
||||
@@ -173,16 +191,16 @@ struct ContractorFormSheet: View {
|
||||
Image(systemName: "map")
|
||||
.foregroundColor(Color.appAccent)
|
||||
.frame(width: 24)
|
||||
TextField("State", text: $state)
|
||||
.focused($focusedField, equals: .state)
|
||||
TextField("State", text: $stateProvince)
|
||||
.focused($focusedField, equals: .stateProvince)
|
||||
}
|
||||
|
||||
Divider()
|
||||
.frame(height: 24)
|
||||
|
||||
TextField("ZIP", text: $zipCode)
|
||||
TextField("ZIP", text: $postalCode)
|
||||
.keyboardType(.numberPad)
|
||||
.focused($focusedField, equals: .zipCode)
|
||||
.focused($focusedField, equals: .postalCode)
|
||||
.frame(maxWidth: 100)
|
||||
}
|
||||
} header: {
|
||||
@@ -258,41 +276,14 @@ struct ContractorFormSheet: View {
|
||||
.disabled(!canSave || viewModel.isCreating || viewModel.isUpdating)
|
||||
}
|
||||
}
|
||||
.sheet(isPresented: $showingResidencePicker) {
|
||||
residencePickerSheet
|
||||
}
|
||||
.sheet(isPresented: $showingSpecialtyPicker) {
|
||||
NavigationStack {
|
||||
List {
|
||||
ForEach(specialties, id: \.self) { spec in
|
||||
Button(action: {
|
||||
specialty = spec
|
||||
showingSpecialtyPicker = false
|
||||
}) {
|
||||
HStack {
|
||||
Text(spec)
|
||||
.foregroundColor(Color.appTextPrimary)
|
||||
Spacer()
|
||||
if specialty == spec {
|
||||
Image(systemName: "checkmark")
|
||||
.foregroundColor(Color.appPrimary)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.scrollContentBackground(.hidden)
|
||||
.background(Color.appBackgroundPrimary)
|
||||
.navigationTitle("Select Specialty")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .confirmationAction) {
|
||||
Button("Done") {
|
||||
showingSpecialtyPicker = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.presentationDetents([.large])
|
||||
specialtyPickerSheet
|
||||
}
|
||||
.onAppear {
|
||||
residenceViewModel.loadMyResidences()
|
||||
loadContractorData()
|
||||
}
|
||||
.handleErrors(
|
||||
@@ -302,6 +293,121 @@ struct ContractorFormSheet: View {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Residence Picker Sheet
|
||||
|
||||
private var residencePickerSheet: some View {
|
||||
NavigationStack {
|
||||
List {
|
||||
// Personal (no residence) option
|
||||
Button(action: {
|
||||
selectedResidenceId = nil
|
||||
selectedResidenceName = nil
|
||||
showingResidencePicker = false
|
||||
}) {
|
||||
HStack {
|
||||
Text("Personal (No Residence)")
|
||||
.foregroundColor(Color.appTextPrimary)
|
||||
Spacer()
|
||||
if selectedResidenceId == nil {
|
||||
Image(systemName: "checkmark")
|
||||
.foregroundColor(Color.appPrimary)
|
||||
}
|
||||
}
|
||||
}
|
||||
.listRowBackground(Color.appBackgroundSecondary)
|
||||
|
||||
// Residences
|
||||
if let residences = residenceViewModel.myResidences?.residences {
|
||||
ForEach(residences, id: \.id) { residence in
|
||||
Button(action: {
|
||||
selectedResidenceId = residence.id
|
||||
selectedResidenceName = residence.name
|
||||
showingResidencePicker = false
|
||||
}) {
|
||||
HStack {
|
||||
Text(residence.name)
|
||||
.foregroundColor(Color.appTextPrimary)
|
||||
Spacer()
|
||||
if selectedResidenceId == residence.id {
|
||||
Image(systemName: "checkmark")
|
||||
.foregroundColor(Color.appPrimary)
|
||||
}
|
||||
}
|
||||
}
|
||||
.listRowBackground(Color.appBackgroundSecondary)
|
||||
}
|
||||
} else if residenceViewModel.isLoading {
|
||||
HStack {
|
||||
Spacer()
|
||||
ProgressView()
|
||||
Spacer()
|
||||
}
|
||||
.listRowBackground(Color.appBackgroundSecondary)
|
||||
}
|
||||
}
|
||||
.listStyle(.plain)
|
||||
.scrollContentBackground(.hidden)
|
||||
.background(Color.appBackgroundPrimary)
|
||||
.navigationTitle("Select Residence")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .confirmationAction) {
|
||||
Button("Done") {
|
||||
showingResidencePicker = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.presentationDetents([.medium, .large])
|
||||
}
|
||||
|
||||
// MARK: - Specialty Picker Sheet (Multi-select)
|
||||
|
||||
private var specialtyPickerSheet: some View {
|
||||
NavigationStack {
|
||||
List {
|
||||
ForEach(specialties, id: \.id) { specialty in
|
||||
Button(action: {
|
||||
if selectedSpecialtyIds.contains(specialty.id) {
|
||||
selectedSpecialtyIds.remove(specialty.id)
|
||||
} else {
|
||||
selectedSpecialtyIds.insert(specialty.id)
|
||||
}
|
||||
}) {
|
||||
HStack {
|
||||
Text(specialty.name)
|
||||
.foregroundColor(Color.appTextPrimary)
|
||||
Spacer()
|
||||
if selectedSpecialtyIds.contains(specialty.id) {
|
||||
Image(systemName: "checkmark")
|
||||
.foregroundColor(Color.appPrimary)
|
||||
}
|
||||
}
|
||||
}
|
||||
.listRowBackground(Color.appBackgroundSecondary)
|
||||
}
|
||||
}
|
||||
.listStyle(.plain)
|
||||
.scrollContentBackground(.hidden)
|
||||
.background(Color.appBackgroundPrimary)
|
||||
.navigationTitle("Select Specialties")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .cancellationAction) {
|
||||
Button("Clear") {
|
||||
selectedSpecialtyIds.removeAll()
|
||||
}
|
||||
}
|
||||
ToolbarItem(placement: .confirmationAction) {
|
||||
Button("Done") {
|
||||
showingSpecialtyPicker = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.presentationDetents([.large])
|
||||
}
|
||||
|
||||
// MARK: - Data Loading
|
||||
|
||||
private func loadContractorData() {
|
||||
@@ -311,39 +417,50 @@ struct ContractorFormSheet: View {
|
||||
company = contractor.company ?? ""
|
||||
phone = contractor.phone ?? ""
|
||||
email = contractor.email ?? ""
|
||||
secondaryPhone = contractor.secondaryPhone ?? ""
|
||||
specialty = contractor.specialty ?? ""
|
||||
licenseNumber = contractor.licenseNumber ?? ""
|
||||
website = contractor.website ?? ""
|
||||
address = contractor.address ?? ""
|
||||
streetAddress = contractor.streetAddress ?? ""
|
||||
city = contractor.city ?? ""
|
||||
state = contractor.state ?? ""
|
||||
zipCode = contractor.zipCode ?? ""
|
||||
stateProvince = contractor.stateProvince ?? ""
|
||||
postalCode = contractor.postalCode ?? ""
|
||||
notes = contractor.notes ?? ""
|
||||
isFavorite = contractor.isFavorite
|
||||
|
||||
// Set residence if contractor has one
|
||||
if let residenceId = contractor.residenceId {
|
||||
selectedResidenceId = residenceId.int32Value
|
||||
// Try to find residence name from loaded residences
|
||||
if let residences = residenceViewModel.myResidences?.residences,
|
||||
let residence = residences.first(where: { $0.id == residenceId.int32Value }) {
|
||||
selectedResidenceName = residence.name
|
||||
}
|
||||
}
|
||||
|
||||
// Set specialties
|
||||
selectedSpecialtyIds = Set(contractor.specialties.map { $0.id })
|
||||
}
|
||||
|
||||
// MARK: - Save Action
|
||||
|
||||
private func saveContractor() {
|
||||
let specialtyIdsArray = Array(selectedSpecialtyIds).map { KotlinInt(int: $0) }
|
||||
|
||||
if let contractor = contractor {
|
||||
// Update existing contractor
|
||||
let request = ContractorUpdateRequest(
|
||||
name: name.isEmpty ? nil : name,
|
||||
residenceId: selectedResidenceId.map { KotlinInt(int: $0) },
|
||||
company: company.isEmpty ? nil : company,
|
||||
phone: phone.isEmpty ? nil : phone,
|
||||
email: email.isEmpty ? nil : email,
|
||||
secondaryPhone: secondaryPhone.isEmpty ? nil : secondaryPhone,
|
||||
specialty: specialty.isEmpty ? nil : specialty,
|
||||
licenseNumber: licenseNumber.isEmpty ? nil : licenseNumber,
|
||||
website: website.isEmpty ? nil : website,
|
||||
address: address.isEmpty ? nil : address,
|
||||
streetAddress: streetAddress.isEmpty ? nil : streetAddress,
|
||||
city: city.isEmpty ? nil : city,
|
||||
state: state.isEmpty ? nil : state,
|
||||
zipCode: zipCode.isEmpty ? nil : zipCode,
|
||||
stateProvince: stateProvince.isEmpty ? nil : stateProvince,
|
||||
postalCode: postalCode.isEmpty ? nil : postalCode,
|
||||
rating: nil,
|
||||
isFavorite: isFavorite.asKotlin,
|
||||
isActive: nil,
|
||||
notes: notes.isEmpty ? nil : notes
|
||||
notes: notes.isEmpty ? nil : notes,
|
||||
specialtyIds: specialtyIdsArray.isEmpty ? nil : specialtyIdsArray
|
||||
)
|
||||
|
||||
viewModel.updateContractor(id: contractor.id, request: request) { success in
|
||||
@@ -356,20 +473,19 @@ struct ContractorFormSheet: View {
|
||||
// Create new contractor
|
||||
let request = ContractorCreateRequest(
|
||||
name: name,
|
||||
residenceId: selectedResidenceId.map { KotlinInt(int: $0) },
|
||||
company: company.isEmpty ? nil : company,
|
||||
phone: phone.isEmpty ? nil : phone,
|
||||
email: email.isEmpty ? nil : email,
|
||||
secondaryPhone: secondaryPhone.isEmpty ? nil : secondaryPhone,
|
||||
specialty: specialty.isEmpty ? nil : specialty,
|
||||
licenseNumber: licenseNumber.isEmpty ? nil : licenseNumber,
|
||||
website: website.isEmpty ? nil : website,
|
||||
address: address.isEmpty ? nil : address,
|
||||
streetAddress: streetAddress.isEmpty ? nil : streetAddress,
|
||||
city: city.isEmpty ? nil : city,
|
||||
state: state.isEmpty ? nil : state,
|
||||
zipCode: zipCode.isEmpty ? nil : zipCode,
|
||||
stateProvince: stateProvince.isEmpty ? nil : stateProvince,
|
||||
postalCode: postalCode.isEmpty ? nil : postalCode,
|
||||
rating: nil,
|
||||
isFavorite: isFavorite,
|
||||
isActive: true,
|
||||
notes: notes.isEmpty ? nil : notes
|
||||
notes: notes.isEmpty ? nil : notes,
|
||||
specialtyIds: specialtyIdsArray.isEmpty ? nil : specialtyIdsArray
|
||||
)
|
||||
|
||||
viewModel.createContractor(request: request) { success in
|
||||
|
||||
@@ -10,17 +10,21 @@ struct ContractorFormState: FormState {
|
||||
var company = FormField<String>()
|
||||
var phone = FormField<String>()
|
||||
var email = FormField<String>()
|
||||
var secondaryPhone = FormField<String>()
|
||||
var specialty = FormField<String>()
|
||||
var licenseNumber = FormField<String>()
|
||||
var website = FormField<String>()
|
||||
var address = FormField<String>()
|
||||
var streetAddress = FormField<String>()
|
||||
var city = FormField<String>()
|
||||
var state = FormField<String>()
|
||||
var zipCode = FormField<String>()
|
||||
var stateProvince = FormField<String>()
|
||||
var postalCode = FormField<String>()
|
||||
var notes = FormField<String>()
|
||||
var isFavorite: Bool = false
|
||||
|
||||
// Residence selection (optional - nil means personal contractor)
|
||||
var selectedResidenceId: Int32?
|
||||
var selectedResidenceName: String?
|
||||
|
||||
// Specialty IDs (multiple selection)
|
||||
var selectedSpecialtyIds: [Int32] = []
|
||||
|
||||
// For edit mode
|
||||
var existingContractorId: Int32?
|
||||
|
||||
@@ -48,16 +52,16 @@ struct ContractorFormState: FormState {
|
||||
company = FormField<String>()
|
||||
phone = FormField<String>()
|
||||
email = FormField<String>()
|
||||
secondaryPhone = FormField<String>()
|
||||
specialty = FormField<String>()
|
||||
licenseNumber = FormField<String>()
|
||||
website = FormField<String>()
|
||||
address = FormField<String>()
|
||||
streetAddress = FormField<String>()
|
||||
city = FormField<String>()
|
||||
state = FormField<String>()
|
||||
zipCode = FormField<String>()
|
||||
stateProvince = FormField<String>()
|
||||
postalCode = FormField<String>()
|
||||
notes = FormField<String>()
|
||||
isFavorite = false
|
||||
selectedResidenceId = nil
|
||||
selectedResidenceName = nil
|
||||
selectedSpecialtyIds = []
|
||||
existingContractorId = nil
|
||||
}
|
||||
|
||||
@@ -65,20 +69,19 @@ struct ContractorFormState: FormState {
|
||||
func toCreateRequest() -> ContractorCreateRequest {
|
||||
ContractorCreateRequest(
|
||||
name: name.trimmedValue,
|
||||
residenceId: selectedResidenceId.map { KotlinInt(int: $0) },
|
||||
company: company.isEmpty ? nil : company.trimmedValue,
|
||||
phone: phone.isEmpty ? nil : phone.trimmedValue,
|
||||
email: email.isEmpty ? nil : email.trimmedValue,
|
||||
secondaryPhone: secondaryPhone.isEmpty ? nil : secondaryPhone.trimmedValue,
|
||||
specialty: specialty.isEmpty ? nil : specialty.trimmedValue,
|
||||
licenseNumber: licenseNumber.isEmpty ? nil : licenseNumber.trimmedValue,
|
||||
website: website.isEmpty ? nil : website.trimmedValue,
|
||||
address: address.isEmpty ? nil : address.trimmedValue,
|
||||
streetAddress: streetAddress.isEmpty ? nil : streetAddress.trimmedValue,
|
||||
city: city.isEmpty ? nil : city.trimmedValue,
|
||||
state: state.isEmpty ? nil : state.trimmedValue,
|
||||
zipCode: zipCode.isEmpty ? nil : zipCode.trimmedValue,
|
||||
stateProvince: stateProvince.isEmpty ? nil : stateProvince.trimmedValue,
|
||||
postalCode: postalCode.isEmpty ? nil : postalCode.trimmedValue,
|
||||
rating: nil,
|
||||
isFavorite: isFavorite,
|
||||
isActive: true,
|
||||
notes: notes.isEmpty ? nil : notes.trimmedValue
|
||||
notes: notes.isEmpty ? nil : notes.trimmedValue,
|
||||
specialtyIds: selectedSpecialtyIds.isEmpty ? nil : selectedSpecialtyIds.map { KotlinInt(int: $0) }
|
||||
)
|
||||
}
|
||||
|
||||
@@ -86,20 +89,19 @@ struct ContractorFormState: FormState {
|
||||
func toUpdateRequest() -> ContractorUpdateRequest {
|
||||
ContractorUpdateRequest(
|
||||
name: name.isEmpty ? nil : name.trimmedValue,
|
||||
residenceId: selectedResidenceId.map { KotlinInt(int: $0) },
|
||||
company: company.isEmpty ? nil : company.trimmedValue,
|
||||
phone: phone.isEmpty ? nil : phone.trimmedValue,
|
||||
email: email.isEmpty ? nil : email.trimmedValue,
|
||||
secondaryPhone: secondaryPhone.isEmpty ? nil : secondaryPhone.trimmedValue,
|
||||
specialty: specialty.isEmpty ? nil : specialty.trimmedValue,
|
||||
licenseNumber: licenseNumber.isEmpty ? nil : licenseNumber.trimmedValue,
|
||||
website: website.isEmpty ? nil : website.trimmedValue,
|
||||
address: address.isEmpty ? nil : address.trimmedValue,
|
||||
streetAddress: streetAddress.isEmpty ? nil : streetAddress.trimmedValue,
|
||||
city: city.isEmpty ? nil : city.trimmedValue,
|
||||
state: state.isEmpty ? nil : state.trimmedValue,
|
||||
zipCode: zipCode.isEmpty ? nil : zipCode.trimmedValue,
|
||||
stateProvince: stateProvince.isEmpty ? nil : stateProvince.trimmedValue,
|
||||
postalCode: postalCode.isEmpty ? nil : postalCode.trimmedValue,
|
||||
rating: nil,
|
||||
isFavorite: isFavorite.asKotlin,
|
||||
isActive: nil,
|
||||
notes: notes.isEmpty ? nil : notes.trimmedValue
|
||||
notes: notes.isEmpty ? nil : notes.trimmedValue,
|
||||
specialtyIds: selectedSpecialtyIds.isEmpty ? nil : selectedSpecialtyIds.map { KotlinInt(int: $0) }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -428,11 +428,11 @@ struct ContractorPickerView: View {
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
|
||||
if let specialty = contractor.specialty {
|
||||
if let firstSpecialty = contractor.specialties.first {
|
||||
HStack(spacing: 4) {
|
||||
Image(systemName: "wrench.and.screwdriver")
|
||||
.font(.caption2)
|
||||
Text(specialty)
|
||||
Text(firstSpecialty.name)
|
||||
.font(.caption2)
|
||||
}
|
||||
.foregroundStyle(.tertiary)
|
||||
|
||||
Reference in New Issue
Block a user