Add per-user notification time preferences
Allow users to customize when they receive notification reminders: - Add hour fields to NotificationPreference model - Add timezone conversion utilities (localHourToUtc, utcHourToLocal) - Add time picker UI for iOS (wheel picker in sheet) - Add time picker UI for Android (hour chip selector dialog) - Times stored in UTC, displayed in user's local timezone - Add localized strings for time picker UI 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -12,6 +12,19 @@ struct ResidenceFormView: View {
|
||||
// Lookups from DataManagerObservable
|
||||
private var residenceTypes: [ResidenceType] { dataManager.residenceTypes }
|
||||
|
||||
// User management state
|
||||
@State private var users: [ResidenceUserResponse] = []
|
||||
@State private var isLoadingUsers = false
|
||||
@State private var userToRemove: ResidenceUserResponse?
|
||||
@State private var showRemoveUserConfirmation = false
|
||||
|
||||
// Check if current user is the owner
|
||||
private var isCurrentUserOwner: Bool {
|
||||
guard let residence = existingResidence,
|
||||
let currentUser = dataManager.currentUser else { return false }
|
||||
return Int(residence.ownerId) == Int(currentUser.id)
|
||||
}
|
||||
|
||||
// Form fields
|
||||
@State private var name: String = ""
|
||||
@State private var selectedPropertyType: ResidenceType?
|
||||
@@ -154,6 +167,38 @@ struct ResidenceFormView: View {
|
||||
}
|
||||
.listRowBackground(Color.appBackgroundSecondary)
|
||||
|
||||
// Users section (edit mode only, owner only)
|
||||
if isEditMode && isCurrentUserOwner {
|
||||
Section {
|
||||
if isLoadingUsers {
|
||||
HStack {
|
||||
Spacer()
|
||||
ProgressView()
|
||||
Spacer()
|
||||
}
|
||||
} else if users.isEmpty {
|
||||
Text("No shared users")
|
||||
.foregroundColor(.secondary)
|
||||
} else {
|
||||
ForEach(users, id: \.id) { user in
|
||||
UserRow(
|
||||
user: user,
|
||||
isOwner: user.id == existingResidence?.ownerId,
|
||||
onRemove: {
|
||||
userToRemove = user
|
||||
showRemoveUserConfirmation = true
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
} header: {
|
||||
Text("Shared Users (\(users.count))")
|
||||
} footer: {
|
||||
Text("Users with access to this residence. Use the share button to invite others.")
|
||||
}
|
||||
.listRowBackground(Color.appBackgroundSecondary)
|
||||
}
|
||||
|
||||
if let errorMessage = viewModel.errorMessage {
|
||||
Section {
|
||||
Text(errorMessage)
|
||||
@@ -187,6 +232,23 @@ struct ResidenceFormView: View {
|
||||
.onAppear {
|
||||
loadResidenceTypes()
|
||||
initializeForm()
|
||||
if isEditMode && isCurrentUserOwner {
|
||||
loadUsers()
|
||||
}
|
||||
}
|
||||
.alert("Remove User", isPresented: $showRemoveUserConfirmation) {
|
||||
Button("Cancel", role: .cancel) {
|
||||
userToRemove = nil
|
||||
}
|
||||
Button("Remove", role: .destructive) {
|
||||
if let user = userToRemove {
|
||||
removeUser(user)
|
||||
}
|
||||
}
|
||||
} message: {
|
||||
if let user = userToRemove {
|
||||
Text("Are you sure you want to remove \(user.username) from this residence?")
|
||||
}
|
||||
}
|
||||
.handleErrors(
|
||||
error: viewModel.errorMessage,
|
||||
@@ -312,6 +374,107 @@ struct ResidenceFormView: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func loadUsers() {
|
||||
guard let residence = existingResidence,
|
||||
TokenStorage.shared.getToken() != nil else { return }
|
||||
|
||||
isLoadingUsers = true
|
||||
|
||||
Task {
|
||||
do {
|
||||
let result = try await APILayer.shared.getResidenceUsers(residenceId: residence.id)
|
||||
|
||||
await MainActor.run {
|
||||
if let successResult = result as? ApiResultSuccess<NSArray>,
|
||||
let responseData = successResult.data as? [ResidenceUserResponse] {
|
||||
// Filter out the owner from the list
|
||||
self.users = responseData.filter { $0.id != residence.ownerId }
|
||||
}
|
||||
self.isLoadingUsers = false
|
||||
}
|
||||
} catch {
|
||||
await MainActor.run {
|
||||
self.isLoadingUsers = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func removeUser(_ user: ResidenceUserResponse) {
|
||||
guard let residence = existingResidence,
|
||||
TokenStorage.shared.getToken() != nil else { return }
|
||||
|
||||
Task {
|
||||
do {
|
||||
let result = try await APILayer.shared.removeUser(residenceId: residence.id, userId: user.id)
|
||||
|
||||
await MainActor.run {
|
||||
if result is ApiResultSuccess<RemoveUserResponse> {
|
||||
self.users.removeAll { $0.id == user.id }
|
||||
}
|
||||
self.userToRemove = nil
|
||||
}
|
||||
} catch {
|
||||
await MainActor.run {
|
||||
self.userToRemove = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - User Row Component
|
||||
|
||||
private struct UserRow: View {
|
||||
let user: ResidenceUserResponse
|
||||
let isOwner: Bool
|
||||
let onRemove: () -> Void
|
||||
|
||||
var body: some View {
|
||||
HStack {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
HStack {
|
||||
Text(user.username)
|
||||
.font(.body)
|
||||
if isOwner {
|
||||
Text("Owner")
|
||||
.font(.caption)
|
||||
.foregroundColor(.white)
|
||||
.padding(.horizontal, 6)
|
||||
.padding(.vertical, 2)
|
||||
.background(Color.appPrimary)
|
||||
.clipShape(Capsule())
|
||||
}
|
||||
}
|
||||
if !user.email.isEmpty {
|
||||
Text(user.email)
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
let fullName = [user.firstName, user.lastName]
|
||||
.compactMap { $0 }
|
||||
.filter { !$0.isEmpty }
|
||||
.joined(separator: " ")
|
||||
if !fullName.isEmpty {
|
||||
Text(fullName)
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
if !isOwner {
|
||||
Button(action: onRemove) {
|
||||
Image(systemName: "trash")
|
||||
.foregroundColor(Color.appError)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
}
|
||||
.padding(.vertical, 4)
|
||||
}
|
||||
}
|
||||
|
||||
#Preview("Add Mode") {
|
||||
|
||||
Reference in New Issue
Block a user