Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- //
- // NuovoAppuntamentoView.swift
- // Harmonia Booking App
- //
- // Created by Paolo Siccardi on 23/08/25.
- //
- import SwiftUI
- import FirebaseFirestore
- struct NuovoAppuntamentoView: View {
- @StateObject private var viewModel = AppuntamentiViewModel()
- @Environment(\.presentationMode) var presentationMode
- // Stati per i campi del form
- @State private var clienteQuery = ""
- @State private var clienteSelezionato: Cliente?
- @State private var trattamento: TipoTrattamento = .gel
- @State private var zona: ZonaTrattamento = .mani
- @State private var durata = 60 // default a 60 minuti
- @State private var prezzo = 30 // default a 30 euro
- @State private var dataAppuntamento: Date
- @State private var mostraRisultatiRicerca = false
- @State private var mostraAlert = false
- @State private var messaggioAlert = ""
- @State private var mostraConferma = false
- @State private var salvataggioInCorso = false
- // Valori possibili per durata e prezzo
- let duratePossibili = stride(from: 15, through: 210, by: 15).map { $0 }
- let prezziComuni = [15, 30, 35, 40, 50 ]
- // Opzioni di durata più comuni
- let durazioniComuni = [30, 45, 60, 75, 90, 120]
- // Inizializzatore che accetta la data iniziale
- init(dataIniziale: Date = Date()) {
- _dataAppuntamento = State(initialValue: dataIniziale)
- }
- var body: some View {
- NavigationView {
- Form {
- // Sezione Cliente
- Section(header: Text("Cliente")) {
- VStack(alignment: .leading) {
- TextField("Cerca cliente...", text: $clienteQuery, onEditingChanged: { isEditing in
- if isEditing {
- mostraRisultatiRicerca = true
- viewModel.cercaClienti(query: clienteQuery)
- }
- })
- .autocapitalization(.none)
- .disableAutocorrection(true)
- .onChange(of: clienteQuery) { nuovaQuery in
- if nuovaQuery.isEmpty {
- clienteSelezionato = nil
- }
- viewModel.cercaClienti(query: nuovaQuery)
- }
- if let cliente = clienteSelezionato {
- HStack {
- Text("Cliente selezionato:")
- .font(.caption)
- .foregroundColor(.gray)
- Spacer()
- Text(cliente.nome)
- .font(.subheadline)
- .foregroundColor(.blue)
- .padding(4)
- .background(
- RoundedRectangle(cornerRadius: 4)
- .stroke(Color.blue, lineWidth: 1)
- )
- Button(action: {
- clienteSelezionato = nil
- clienteQuery = ""
- }) {
- Image(systemName: "xmark.circle.fill")
- .foregroundColor(.gray)
- }
- }
- }
- if mostraRisultatiRicerca && !clienteQuery.isEmpty && clienteSelezionato == nil {
- List {
- ForEach(viewModel.clientiTrovati) { cliente in
- Text(cliente.nome)
- .onTapGesture {
- clienteSelezionato = cliente
- clienteQuery = cliente.nome
- mostraRisultatiRicerca = false
- }
- }
- }
- .frame(height: min(CGFloat(viewModel.clientiTrovati.count * 44), 132))
- .background(Color(.systemBackground))
- .cornerRadius(8)
- .shadow(radius: 2)
- }
- }
- }
- // Sezione Trattamento
- Section(header: Text("Dettagli Trattamento")) {
- // Pulsanti per il tipo di trattamento
- VStack(spacing: 12) {
- // Tipo di trattamento
- HStack(spacing: 8) {
- ForEach(TipoTrattamento.allCases.filter { $0 == .gel || $0 == .semipermanente || $0 == .ricostruzione || $0 == .altro }, id: \.self) { tipo in
- Button(action: {
- trattamento = tipo
- }) {
- Text(tipoTrattamentoLabel(tipo))
- .frame(maxWidth: .infinity)
- .padding(.vertical, 8)
- }
- .buttonStyle(CustomTreatmentButtonStyle(isSelected: trattamento == tipo))
- }
- }
- // Zona trattamento (con selezione multipla)
- HStack(spacing: 8) {
- // Pulsante per mani
- Button(action: {
- if zona == .mani {
- zona = .altro
- } else if zona == .piedi {
- zona = .maniPiedi
- } else if zona == .maniPiedi {
- zona = .piedi
- } else {
- zona = .mani
- }
- }) {
- HStack {
- Image(systemName: zonaContainsMani(zona) ? "checkmark.circle.fill" : "circle")
- Text("Mani")
- }
- .frame(maxWidth: .infinity)
- .padding(.vertical, 8)
- }
- .buttonStyle(CustomZoneButtonStyle(isSelected: zonaContainsMani(zona)))
- // Pulsante per piedi
- Button(action: {
- if zona == .piedi {
- zona = .altro
- } else if zona == .mani {
- zona = .maniPiedi
- } else if zona == .maniPiedi {
- zona = .mani
- } else {
- zona = .piedi
- }
- }) {
- HStack {
- Image(systemName: zonaContainsPiedi(zona) ? "checkmark.circle.fill" : "circle")
- Text("Piedi")
- }
- .frame(maxWidth: .infinity)
- .padding(.vertical, 8)
- }
- .buttonStyle(CustomZoneButtonStyle(isSelected: zonaContainsPiedi(zona)))
- }
- }
- }
- // Sezione Durata - Nuovo approccio con pulsanti
- Section(header: Text("Durata")) {
- // Visualizzatore di durata e controlli
- HStack {
- // Indicatore visuale della durata (in stile orologio)
- Spacer()
- Text(formattaDurataEstesa(durata))
- .font(.title3)
- .padding(10)
- Spacer()
- // Stepper per aumentare/diminuire la durata di 15 minuti alla volta
- Stepper("",
- value: $durata,
- in: 15...240, // Min 15 minuti, max 4 ore
- step: 15, // Incrementi di 15 minuti
- onEditingChanged: { _ in
- viewModel.controllaAppuntamentoSovrapposto(data: dataAppuntamento, durata: durata)
- })
- .labelsHidden()
- }
- .padding(.vertical, 8)
- .onChange(of: durata) { newValue in
- viewModel.controllaAppuntamentoSovrapposto(data: dataAppuntamento, durata: newValue)
- }
- }
- // Sezione Prezzo - Approccio con pulsanti e stepper
- Section(header: Text("Prezzo")) {
- // Pulsanti per i prezzi più comuni
- HStack(spacing: 8) {
- ForEach(prezziComuni, id: \.self) { euro in
- Button(action: {
- prezzo = euro
- }) {
- Text("\(euro) €")
- .padding(.horizontal, 12)
- .padding(.vertical, 6)
- }
- .buttonStyle(CustomPriceButtonStyle(isSelected: prezzo == euro))
- }
- }
- .padding(.vertical, 5)
- // Stepper per aggiustamenti precisi
- HStack {
- Text("Prezzo: \(prezzo) €")
- Spacer()
- Stepper("", value: $prezzo, in: 0...200, step: 5)
- .labelsHidden()
- }
- }
- // Sezione Data e Ora
- Section(header: Text("Data e Ora")) {
- DatePicker(
- "Seleziona data e ora",
- selection: $dataAppuntamento,
- displayedComponents: [.date, .hourAndMinute]
- )
- .onChange(of: dataAppuntamento) { nuovaData in
- viewModel.controllaAppuntamentoSovrapposto(data: nuovaData, durata: durata)
- }
- if viewModel.appuntamentiSovrapposti {
- HStack {
- Image(systemName: "exclamationmark.triangle.fill")
- .foregroundColor(.yellow)
- Text("Orario potenzialmente occupato")
- .foregroundColor(.yellow)
- }
- }
- }
- // Pulsante Salva
- Section {
- Button(action: {
- if viewModel.appuntamentiSovrapposti {
- messaggioAlert = "Stai per inserire un appuntamento in un orario già occupato. Vuoi procedere comunque?"
- mostraAlert = true
- } else {
- salvaAppuntamento()
- }
- }) {
- HStack {
- Spacer()
- if salvataggioInCorso {
- ProgressView()
- .progressViewStyle(CircularProgressViewStyle(tint: viewModel.appuntamentiSovrapposti ? .black : .white))
- .padding(.trailing, 5)
- }
- if viewModel.appuntamentiSovrapposti {
- Text("Attenzione! Appuntamenti sovrapposti!")
- .fontWeight(.bold)
- .foregroundColor(.black)
- } else {
- Text("Salva Appuntamento")
- .fontWeight(.bold)
- }
- Spacer()
- }
- }
- .disabled(clienteSelezionato == nil || salvataggioInCorso)
- .listRowBackground(viewModel.appuntamentiSovrapposti ? Color.yellow : Color.blue)
- .foregroundColor(viewModel.appuntamentiSovrapposti ? .black : .white)
- }
- }
- .listSectionSpacing(.compact)
- .navigationTitle("Nuovo Appuntamento")
- .navigationBarTitleDisplayMode(.inline)
- .navigationBarItems(trailing: Button("Annulla") {
- presentationMode.wrappedValue.dismiss()
- })
- .alert(isPresented: $mostraAlert) {
- Alert(
- title: Text("Attenzione"),
- message: Text(messaggioAlert),
- primaryButton: .destructive(Text("Procedi")) {
- if viewModel.appuntamentiSovrapposti {
- salvaAppuntamento()
- }
- },
- secondaryButton: .cancel(Text("Annulla"))
- )
- }
- .alert("Appuntamento Salvato", isPresented: $mostraConferma) {
- Button("OK") {
- presentationMode.wrappedValue.dismiss()
- }
- } message: {
- Text("L'appuntamento è stato inserito con successo.")
- }
- .onAppear {
- viewModel.caricaClienti()
- viewModel.caricaAppuntamenti() {
- // Verifica sovrapposizione appena caricati gli appuntamenti
- DispatchQueue.main.async {
- self.viewModel.controllaAppuntamentoSovrapposto(data: self.dataAppuntamento, durata: self.durata)
- }
- }
- }
- }
- }
- private func formattaDurata(_ minuti: Int) -> String {
- let ore = minuti / 60
- let minutiRimanenti = minuti % 60
- if ore > 0 {
- return "\(ore)h \(minutiRimanenti)min"
- } else {
- return "\(minuti) min"
- }
- }
- // Formatta la durata come orario (ad es. "1:30")
- private func formatDurationTime(_ minuti: Int) -> String {
- let ore = minuti / 60
- let minutiRimanenti = minuti % 60
- if ore > 0 {
- return String(format: "%d:%02d", ore, minutiRimanenti)
- } else {
- return "0:\(String(format: "%02d", minuti))"
- }
- }
- // Formatta la durata in modo esteso (es. "1 ora e 30 minuti")
- private func formattaDurataEstesa(_ minuti: Int) -> String {
- let ore = minuti / 60
- let minutiRimanenti = minuti % 60
- if ore > 0 {
- if minutiRimanenti > 0 {
- return "\(ore) \(ore == 1 ? "ora" : "ore") e \(minutiRimanenti) min"
- } else {
- return "\(ore) \(ore == 1 ? "ora" : "ore")"
- }
- } else {
- return "\(minuti) minuti"
- }
- }
- private func salvaAppuntamento() {
- // Stesso codice di prima
- guard let cliente = clienteSelezionato, !salvataggioInCorso else {
- if clienteSelezionato == nil {
- messaggioAlert = "Seleziona un cliente per procedere."
- mostraAlert = true
- }
- return
- }
- // Imposta il flag per evitare salvataggi multipli
- salvataggioInCorso = true
- let nuovoAppuntamento = Appuntamento(
- clienteId: cliente.id ?? "",
- nomeCliente: cliente.nome,
- trattamento: trattamento,
- zona: zona,
- durata: durata,
- prezzo: prezzo,
- data: dataAppuntamento
- )
- // Usa l'approccio unificato per il salvataggio
- viewModel.salvaAppuntamento(appuntamento: nuovoAppuntamento) { successo in
- DispatchQueue.main.async {
- self.salvataggioInCorso = false
- if successo {
- // Mostra conferma e torna alla schermata precedente
- self.mostraConferma = true
- } else {
- self.messaggioAlert = "Si è verificato un errore durante il salvataggio dell'appuntamento."
- self.mostraAlert = true
- }
- }
- }
- }
- }
- // Stili personalizzati per i pulsanti
- struct CustomDurationButtonStyle: ButtonStyle {
- var isSelected: Bool
- func makeBody(configuration: Configuration) -> some View {
- configuration.label
- .background(
- RoundedRectangle(cornerRadius: 8)
- .fill(isSelected ? Color.blue : Color(.systemGray5))
- )
- .foregroundColor(isSelected ? .white : .primary)
- .scaleEffect(configuration.isPressed ? 0.95 : 1)
- }
- }
- struct CustomPriceButtonStyle: ButtonStyle {
- var isSelected: Bool
- func makeBody(configuration: Configuration) -> some View {
- configuration.label
- .background(
- RoundedRectangle(cornerRadius: 8)
- .fill(isSelected ? Color.green : Color(.systemGray5))
- )
- .foregroundColor(isSelected ? .white : .primary)
- .scaleEffect(configuration.isPressed ? 0.95 : 1)
- }
- }
- struct CustomTreatmentButtonStyle: ButtonStyle {
- var isSelected: Bool
- func makeBody(configuration: Configuration) -> some View {
- configuration.label
- .background(
- RoundedRectangle(cornerRadius: 8)
- .fill(isSelected ? Color.blue : Color(.systemGray5))
- )
- .foregroundColor(isSelected ? .white : .primary)
- .scaleEffect(configuration.isPressed ? 0.95 : 1)
- }
- }
- struct CustomZoneButtonStyle: ButtonStyle {
- var isSelected: Bool
- func makeBody(configuration: Configuration) -> some View {
- configuration.label
- .background(
- RoundedRectangle(cornerRadius: 8)
- .fill(isSelected ? Color.blue : Color(.systemGray5))
- )
- .foregroundColor(isSelected ? .white : .primary)
- .scaleEffect(configuration.isPressed ? 0.95 : 1)
- }
- }
- // Aggiungi queste funzioni di supporto nella vista
- // Funzione per ottenere l'etichetta abbreviata del trattamento
- private func tipoTrattamentoLabel(_ tipo: TipoTrattamento) -> String {
- switch tipo {
- case .semipermanente:
- return "Semi"
- case .ricostruzione:
- return "Ric."
- case .gel:
- return "Gel"
- case .refill:
- return "Refill"
- case .altro:
- return "Altro"
- }
- }
- // Funzioni per controllare le zone
- private func zonaContainsMani(_ zona: ZonaTrattamento) -> Bool {
- return zona == .mani || zona == .maniPiedi
- }
- private func zonaContainsPiedi(_ zona: ZonaTrattamento) -> Bool {
- return zona == .piedi || zona == .maniPiedi
- }
Advertisement
Add Comment
Please, Sign In to add comment