Skin_1980

jshdjhsdjh

Aug 27th, 2025
66
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Swift 20.07 KB | None | 0 0
  1. //
  2. //  NuovoAppuntamentoView.swift
  3. //  Harmonia Booking App
  4. //
  5. //  Created by Paolo Siccardi on 23/08/25.
  6. //
  7.  
  8.  
  9. import SwiftUI
  10. import FirebaseFirestore
  11.  
  12. struct NuovoAppuntamentoView: View {
  13.     @StateObject private var viewModel = AppuntamentiViewModel()
  14.     @Environment(\.presentationMode) var presentationMode
  15.    
  16.     // Stati per i campi del form
  17.     @State private var clienteQuery = ""
  18.     @State private var clienteSelezionato: Cliente?
  19.     @State private var trattamento: TipoTrattamento = .gel
  20.     @State private var zona: ZonaTrattamento = .mani
  21.     @State private var durata = 60 // default a 60 minuti
  22.     @State private var prezzo = 30 // default a 30 euro
  23.     @State private var dataAppuntamento: Date
  24.    
  25.     @State private var mostraRisultatiRicerca = false
  26.     @State private var mostraAlert = false
  27.     @State private var messaggioAlert = ""
  28.     @State private var mostraConferma = false
  29.     @State private var salvataggioInCorso = false
  30.    
  31.     // Valori possibili per durata e prezzo
  32.     let duratePossibili = stride(from: 15, through: 210, by: 15).map { $0 }
  33.     let prezziComuni = [15, 30, 35, 40, 50 ]
  34.    
  35.     // Opzioni di durata più comuni
  36.     let durazioniComuni = [30, 45, 60, 75, 90, 120]
  37.    
  38.     // Inizializzatore che accetta la data iniziale
  39.     init(dataIniziale: Date = Date()) {
  40.         _dataAppuntamento = State(initialValue: dataIniziale)
  41.     }
  42.    
  43.     var body: some View {
  44.         NavigationView {
  45.             Form {
  46.                 // Sezione Cliente
  47.                 Section(header: Text("Cliente")) {
  48.                     VStack(alignment: .leading) {
  49.                         TextField("Cerca cliente...", text: $clienteQuery, onEditingChanged: { isEditing in
  50.                             if isEditing {
  51.                                 mostraRisultatiRicerca = true
  52.                                 viewModel.cercaClienti(query: clienteQuery)
  53.                             }
  54.                         })
  55.                         .autocapitalization(.none)
  56.                         .disableAutocorrection(true)
  57.                         .onChange(of: clienteQuery) { nuovaQuery in
  58.                             if nuovaQuery.isEmpty {
  59.                                 clienteSelezionato = nil
  60.                             }
  61.                             viewModel.cercaClienti(query: nuovaQuery)
  62.                         }
  63.                        
  64.                         if let cliente = clienteSelezionato {
  65.                             HStack {
  66.                                 Text("Cliente selezionato:")
  67.                                     .font(.caption)
  68.                                     .foregroundColor(.gray)
  69.                                 Spacer()
  70.                                 Text(cliente.nome)
  71.                                     .font(.subheadline)
  72.                                     .foregroundColor(.blue)
  73.                                     .padding(4)
  74.                                     .background(
  75.                                         RoundedRectangle(cornerRadius: 4)
  76.                                             .stroke(Color.blue, lineWidth: 1)
  77.                                     )
  78.                                
  79.                                 Button(action: {
  80.                                     clienteSelezionato = nil
  81.                                     clienteQuery = ""
  82.                                 }) {
  83.                                     Image(systemName: "xmark.circle.fill")
  84.                                         .foregroundColor(.gray)
  85.                                 }
  86.                             }
  87.                         }
  88.                        
  89.                         if mostraRisultatiRicerca && !clienteQuery.isEmpty && clienteSelezionato == nil {
  90.                             List {
  91.                                 ForEach(viewModel.clientiTrovati) { cliente in
  92.                                     Text(cliente.nome)
  93.                                         .onTapGesture {
  94.                                             clienteSelezionato = cliente
  95.                                             clienteQuery = cliente.nome
  96.                                             mostraRisultatiRicerca = false
  97.                                         }
  98.                                 }
  99.                             }
  100.                             .frame(height: min(CGFloat(viewModel.clientiTrovati.count * 44), 132))
  101.                             .background(Color(.systemBackground))
  102.                             .cornerRadius(8)
  103.                             .shadow(radius: 2)
  104.                         }
  105.                     }
  106.                 }
  107.                
  108.                 // Sezione Trattamento
  109.                 Section(header: Text("Dettagli Trattamento")) {
  110.                     // Pulsanti per il tipo di trattamento
  111.                     VStack(spacing: 12) {
  112.                         // Tipo di trattamento
  113.                         HStack(spacing: 8) {
  114.                             ForEach(TipoTrattamento.allCases.filter { $0 == .gel || $0 == .semipermanente || $0 == .ricostruzione || $0 == .altro }, id: \.self) { tipo in
  115.                                 Button(action: {
  116.                                     trattamento = tipo
  117.                                 }) {
  118.                                     Text(tipoTrattamentoLabel(tipo))
  119.                                         .frame(maxWidth: .infinity)
  120.                                         .padding(.vertical, 8)
  121.                                 }
  122.                                 .buttonStyle(CustomTreatmentButtonStyle(isSelected: trattamento == tipo))
  123.                             }
  124.                         }
  125.                        
  126.                         // Zona trattamento (con selezione multipla)
  127.                         HStack(spacing: 8) {
  128.                             // Pulsante per mani
  129.                             Button(action: {
  130.                                 if zona == .mani {
  131.                                     zona = .altro
  132.                                 } else if zona == .piedi {
  133.                                     zona = .maniPiedi
  134.                                 } else if zona == .maniPiedi {
  135.                                     zona = .piedi
  136.                                 } else {
  137.                                     zona = .mani
  138.                                 }
  139.                             }) {
  140.                                 HStack {
  141.                                     Image(systemName: zonaContainsMani(zona) ? "checkmark.circle.fill" : "circle")
  142.                                     Text("Mani")
  143.                                 }
  144.                                 .frame(maxWidth: .infinity)
  145.                                 .padding(.vertical, 8)
  146.                             }
  147.                             .buttonStyle(CustomZoneButtonStyle(isSelected: zonaContainsMani(zona)))
  148.                            
  149.                             // Pulsante per piedi
  150.                             Button(action: {
  151.                                 if zona == .piedi {
  152.                                     zona = .altro
  153.                                 } else if zona == .mani {
  154.                                     zona = .maniPiedi
  155.                                 } else if zona == .maniPiedi {
  156.                                     zona = .mani
  157.                                 } else {
  158.                                     zona = .piedi
  159.                                 }
  160.                             }) {
  161.                                 HStack {
  162.                                     Image(systemName: zonaContainsPiedi(zona) ? "checkmark.circle.fill" : "circle")
  163.                                     Text("Piedi")
  164.                                 }
  165.                                 .frame(maxWidth: .infinity)
  166.                                 .padding(.vertical, 8)
  167.                             }
  168.                             .buttonStyle(CustomZoneButtonStyle(isSelected: zonaContainsPiedi(zona)))
  169.                         }
  170.                     }
  171.                 }
  172.                
  173.                 // Sezione Durata - Nuovo approccio con pulsanti
  174.                 Section(header: Text("Durata")) {
  175.                     // Visualizzatore di durata e controlli
  176.                     HStack {
  177.                         // Indicatore visuale della durata (in stile orologio)
  178.                        
  179.                         Spacer()
  180.  
  181.                         Text(formattaDurataEstesa(durata))
  182.                             .font(.title3)
  183.                             .padding(10)
  184.                        
  185.                         Spacer()
  186.                        
  187.                         // Stepper per aumentare/diminuire la durata di 15 minuti alla volta
  188.                         Stepper("",
  189.                                 value: $durata,
  190.                                 in: 15...240, // Min 15 minuti, max 4 ore
  191.                                 step: 15,     // Incrementi di 15 minuti
  192.                                 onEditingChanged: { _ in
  193.                                     viewModel.controllaAppuntamentoSovrapposto(data: dataAppuntamento, durata: durata)
  194.                                 })
  195.                             .labelsHidden()
  196.                     }
  197.                     .padding(.vertical, 8)
  198.                    
  199.                  
  200.                     .onChange(of: durata) { newValue in
  201.                         viewModel.controllaAppuntamentoSovrapposto(data: dataAppuntamento, durata: newValue)
  202.                     }
  203.                 }
  204.                
  205.                 // Sezione Prezzo - Approccio con pulsanti e stepper
  206.                 Section(header: Text("Prezzo")) {
  207.                     // Pulsanti per i prezzi più comuni
  208.  
  209.                         HStack(spacing: 8) {
  210.                             ForEach(prezziComuni, id: \.self) { euro in
  211.                                 Button(action: {
  212.                                     prezzo = euro
  213.                                 }) {
  214.                                     Text("\(euro) €")
  215.                                         .padding(.horizontal, 12)
  216.                                         .padding(.vertical, 6)
  217.                                 }
  218.                                 .buttonStyle(CustomPriceButtonStyle(isSelected: prezzo == euro))
  219.                             }
  220.                         }
  221.                         .padding(.vertical, 5)
  222.                    
  223.                    
  224.                     // Stepper per aggiustamenti precisi
  225.                     HStack {
  226.                         Text("Prezzo: \(prezzo) €")
  227.                         Spacer()
  228.                         Stepper("", value: $prezzo, in: 0...200, step: 5)
  229.                             .labelsHidden()
  230.                     }
  231.                 }
  232.                
  233.                 // Sezione Data e Ora
  234.                 Section(header: Text("Data e Ora")) {
  235.                     DatePicker(
  236.                         "Seleziona data e ora",
  237.                         selection: $dataAppuntamento,
  238.                         displayedComponents: [.date, .hourAndMinute]
  239.                     )
  240.                     .onChange(of: dataAppuntamento) { nuovaData in
  241.                         viewModel.controllaAppuntamentoSovrapposto(data: nuovaData, durata: durata)
  242.                     }
  243.                    
  244.                     if viewModel.appuntamentiSovrapposti {
  245.                         HStack {
  246.                             Image(systemName: "exclamationmark.triangle.fill")
  247.                                 .foregroundColor(.yellow)
  248.                             Text("Orario potenzialmente occupato")
  249.                                 .foregroundColor(.yellow)
  250.                         }
  251.                     }
  252.                 }
  253.                
  254.                 // Pulsante Salva
  255.                 Section {
  256.                     Button(action: {
  257.                         if viewModel.appuntamentiSovrapposti {
  258.                             messaggioAlert = "Stai per inserire un appuntamento in un orario già occupato. Vuoi procedere comunque?"
  259.                             mostraAlert = true
  260.                         } else {
  261.                             salvaAppuntamento()
  262.                         }
  263.                     }) {
  264.                         HStack {
  265.                             Spacer()
  266.                             if salvataggioInCorso {
  267.                                 ProgressView()
  268.                                     .progressViewStyle(CircularProgressViewStyle(tint: viewModel.appuntamentiSovrapposti ? .black : .white))
  269.                                     .padding(.trailing, 5)
  270.                             }
  271.                            
  272.                             if viewModel.appuntamentiSovrapposti {
  273.                                 Text("Attenzione! Appuntamenti sovrapposti!")
  274.                                     .fontWeight(.bold)
  275.                                     .foregroundColor(.black)
  276.                             } else {
  277.                                 Text("Salva Appuntamento")
  278.                                     .fontWeight(.bold)
  279.                             }
  280.                             Spacer()
  281.                         }
  282.                     }
  283.                     .disabled(clienteSelezionato == nil || salvataggioInCorso)
  284.                     .listRowBackground(viewModel.appuntamentiSovrapposti ? Color.yellow : Color.blue)
  285.                     .foregroundColor(viewModel.appuntamentiSovrapposti ? .black : .white)
  286.                 }
  287.             }
  288.             .listSectionSpacing(.compact)
  289.             .navigationTitle("Nuovo Appuntamento")
  290.             .navigationBarTitleDisplayMode(.inline)
  291.             .navigationBarItems(trailing: Button("Annulla") {
  292.                 presentationMode.wrappedValue.dismiss()
  293.             })
  294.             .alert(isPresented: $mostraAlert) {
  295.                 Alert(
  296.                     title: Text("Attenzione"),
  297.                     message: Text(messaggioAlert),
  298.                     primaryButton: .destructive(Text("Procedi")) {
  299.                         if viewModel.appuntamentiSovrapposti {
  300.                             salvaAppuntamento()
  301.                         }
  302.                     },
  303.                     secondaryButton: .cancel(Text("Annulla"))
  304.                 )
  305.             }
  306.             .alert("Appuntamento Salvato", isPresented: $mostraConferma) {
  307.                 Button("OK") {
  308.                     presentationMode.wrappedValue.dismiss()
  309.                 }
  310.             } message: {
  311.                 Text("L'appuntamento è stato inserito con successo.")
  312.             }
  313.             .onAppear {
  314.                 viewModel.caricaClienti()
  315.                 viewModel.caricaAppuntamenti() {
  316.                     // Verifica sovrapposizione appena caricati gli appuntamenti
  317.                     DispatchQueue.main.async {
  318.                         self.viewModel.controllaAppuntamentoSovrapposto(data: self.dataAppuntamento, durata: self.durata)
  319.                     }
  320.                 }
  321.             }
  322.         }
  323.     }
  324.    
  325.     private func formattaDurata(_ minuti: Int) -> String {
  326.         let ore = minuti / 60
  327.         let minutiRimanenti = minuti % 60
  328.        
  329.         if ore > 0 {
  330.             return "\(ore)h \(minutiRimanenti)min"
  331.         } else {
  332.             return "\(minuti) min"
  333.         }
  334.     }
  335.    
  336.     // Formatta la durata come orario (ad es. "1:30")
  337.     private func formatDurationTime(_ minuti: Int) -> String {
  338.         let ore = minuti / 60
  339.         let minutiRimanenti = minuti % 60
  340.        
  341.         if ore > 0 {
  342.             return String(format: "%d:%02d", ore, minutiRimanenti)
  343.         } else {
  344.             return "0:\(String(format: "%02d", minuti))"
  345.         }
  346.     }
  347.    
  348.     // Formatta la durata in modo esteso (es. "1 ora e 30 minuti")
  349.     private func formattaDurataEstesa(_ minuti: Int) -> String {
  350.         let ore = minuti / 60
  351.         let minutiRimanenti = minuti % 60
  352.        
  353.         if ore > 0 {
  354.             if minutiRimanenti > 0 {
  355.                 return "\(ore) \(ore == 1 ? "ora" : "ore") e \(minutiRimanenti) min"
  356.             } else {
  357.                 return "\(ore) \(ore == 1 ? "ora" : "ore")"
  358.             }
  359.         } else {
  360.             return "\(minuti) minuti"
  361.         }
  362.     }
  363.    
  364.     private func salvaAppuntamento() {
  365.         // Stesso codice di prima
  366.         guard let cliente = clienteSelezionato, !salvataggioInCorso else {
  367.             if clienteSelezionato == nil {
  368.                 messaggioAlert = "Seleziona un cliente per procedere."
  369.                 mostraAlert = true
  370.             }
  371.             return
  372.         }
  373.        
  374.         // Imposta il flag per evitare salvataggi multipli
  375.         salvataggioInCorso = true
  376.        
  377.         let nuovoAppuntamento = Appuntamento(
  378.             clienteId: cliente.id ?? "",
  379.             nomeCliente: cliente.nome,
  380.             trattamento: trattamento,
  381.             zona: zona,
  382.             durata: durata,
  383.             prezzo: prezzo,
  384.             data: dataAppuntamento
  385.         )
  386.        
  387.         // Usa l'approccio unificato per il salvataggio
  388.         viewModel.salvaAppuntamento(appuntamento: nuovoAppuntamento) { successo in
  389.             DispatchQueue.main.async {
  390.                 self.salvataggioInCorso = false
  391.                
  392.                 if successo {
  393.                     // Mostra conferma e torna alla schermata precedente
  394.                     self.mostraConferma = true
  395.                 } else {
  396.                     self.messaggioAlert = "Si è verificato un errore durante il salvataggio dell'appuntamento."
  397.                     self.mostraAlert = true
  398.                 }
  399.             }
  400.         }
  401.     }
  402. }
  403.  
  404. // Stili personalizzati per i pulsanti
  405. struct CustomDurationButtonStyle: ButtonStyle {
  406.     var isSelected: Bool
  407.    
  408.     func makeBody(configuration: Configuration) -> some View {
  409.         configuration.label
  410.             .background(
  411.                 RoundedRectangle(cornerRadius: 8)
  412.                     .fill(isSelected ? Color.blue : Color(.systemGray5))
  413.             )
  414.             .foregroundColor(isSelected ? .white : .primary)
  415.             .scaleEffect(configuration.isPressed ? 0.95 : 1)
  416.     }
  417. }
  418.  
  419. struct CustomPriceButtonStyle: ButtonStyle {
  420.     var isSelected: Bool
  421.    
  422.     func makeBody(configuration: Configuration) -> some View {
  423.         configuration.label
  424.             .background(
  425.                 RoundedRectangle(cornerRadius: 8)
  426.                     .fill(isSelected ? Color.green : Color(.systemGray5))
  427.             )
  428.             .foregroundColor(isSelected ? .white : .primary)
  429.             .scaleEffect(configuration.isPressed ? 0.95 : 1)
  430.     }
  431. }
  432.  
  433. struct CustomTreatmentButtonStyle: ButtonStyle {
  434.     var isSelected: Bool
  435.    
  436.     func makeBody(configuration: Configuration) -> some View {
  437.         configuration.label
  438.             .background(
  439.                 RoundedRectangle(cornerRadius: 8)
  440.                     .fill(isSelected ? Color.blue : Color(.systemGray5))
  441.             )
  442.             .foregroundColor(isSelected ? .white : .primary)
  443.             .scaleEffect(configuration.isPressed ? 0.95 : 1)
  444.     }
  445. }
  446.  
  447. struct CustomZoneButtonStyle: ButtonStyle {
  448.     var isSelected: Bool
  449.    
  450.     func makeBody(configuration: Configuration) -> some View {
  451.         configuration.label
  452.             .background(
  453.                 RoundedRectangle(cornerRadius: 8)
  454.                     .fill(isSelected ? Color.blue : Color(.systemGray5))
  455.             )
  456.             .foregroundColor(isSelected ? .white : .primary)
  457.             .scaleEffect(configuration.isPressed ? 0.95 : 1)
  458.     }
  459. }
  460.  
  461. // Aggiungi queste funzioni di supporto nella vista
  462.  
  463. // Funzione per ottenere l'etichetta abbreviata del trattamento
  464. private func tipoTrattamentoLabel(_ tipo: TipoTrattamento) -> String {
  465.     switch tipo {
  466.     case .semipermanente:
  467.         return "Semi"
  468.     case .ricostruzione:
  469.         return "Ric."
  470.     case .gel:
  471.         return "Gel"
  472.     case .refill:
  473.         return "Refill"
  474.     case .altro:
  475.         return "Altro"
  476.     }
  477. }
  478.  
  479. // Funzioni per controllare le zone
  480. private func zonaContainsMani(_ zona: ZonaTrattamento) -> Bool {
  481.     return zona == .mani || zona == .maniPiedi
  482. }
  483.  
  484. private func zonaContainsPiedi(_ zona: ZonaTrattamento) -> Bool {
  485.     return zona == .piedi || zona == .maniPiedi
  486. }
Advertisement
Add Comment
Please, Sign In to add comment