package controllers import ( "errors" "fmt" "strconv" "strings" "time" "git.readonly.ch/bouzoure/pop-camarades/helpers" "git.readonly.ch/bouzoure/pop-camarades/models" "github.com/go-playground/validator/v10" "github.com/gofiber/fiber/v2" "gorm.io/gorm" ) func Contacts(c *fiber.Ctx) error { userid, err := helpers.GetSessionUserId(c) if err != nil { return err } allowedSections, err := helpers.PermissionsGetSections( userid, "show_contact", ) if err != nil { return err } allowedSectionsArchived, err := helpers.PermissionsGetSections( userid, "show_archived_contact", ) if err != nil { return err } permShow := (len(allowedSections) > 0) permShowArchived := (len(allowedSectionsArchived) > 0) if !permShow && !permShowArchived { return fiber.NewError(fiber.StatusForbidden, "Forbidden") } db, err := helpers.GetDatabase() if err != nil { return err } filterSection := c.Query("se") filterArchive := c.Query("a") filterSearch := c.Query("s") sqlFilterSections := allowedSections sqlFilterAppend := "IS NULL" if filterArchive == "1" { sqlFilterAppend = "IS NOT NULL" sqlFilterSections = allowedSectionsArchived } var filterSectionUint uint if len(filterSection) > 0 { filterSectionUint64, err := strconv.ParseUint(filterSection, 10, 0) if err == nil { for _, s := range sqlFilterSections { if s == uint(filterSectionUint64) { filterSectionUint = uint(filterSectionUint64) } } } } if filterSectionUint > 0 { sqlFilterAppend = fmt.Sprintf( "%s AND section_id = %d", sqlFilterAppend, filterSectionUint, ) } var sqlFilterSearch string if len(filterSearch) > 0 { sqlFilterSearch = fmt.Sprintf( "%%%s%%", filterSearch, ) } var count int64 if len(filterSearch) > 0 { db.Model(&models.Person{}).Where( fmt.Sprintf( "is_contact = ? AND section_id IN ? AND (first_name LIKE ? OR last_name LIKE ?) AND deleted_at %s", sqlFilterAppend, ), true, allowedSections, sqlFilterSearch, sqlFilterSearch, ).Count(&count) } else { db.Model(&models.Person{}).Where( fmt.Sprintf( "is_contact = ? AND section_id IN ? AND deleted_at %s", sqlFilterAppend, ), true, allowedSections, ).Count(&count) } page, _ := strconv.Atoi(c.Query("p")) pagination := helpers.Paginate(50, int(count), page) var people []models.Person if len(filterSearch) > 0 { result := db.Unscoped().Offset( pagination.Offset, ).Limit( pagination.PageSize, ).Order( "last_name collate nocase asc, first_name collate nocase asc", ).Preload( "Section", ).Find( &people, fmt.Sprintf( "is_contact = ? AND section_id IN ? AND (first_name LIKE ? OR last_name LIKE ?) AND deleted_at %s", sqlFilterAppend, ), true, sqlFilterSections, sqlFilterSearch, sqlFilterSearch, ) if result.Error != nil && !errors.Is(result.Error, gorm.ErrRecordNotFound) { return err } } else { result := db.Unscoped().Offset( pagination.Offset, ).Limit( pagination.PageSize, ).Order( "last_name collate nocase asc, first_name collate nocase asc", ).Preload( "Section", ).Find( &people, fmt.Sprintf( "is_contact = ? AND section_id IN ? AND deleted_at %s", sqlFilterAppend, ), true, sqlFilterSections, ) if result.Error != nil && !errors.Is(result.Error, gorm.ErrRecordNotFound) { return err } } var sections []models.Section db.Order( "name collate nocase asc", ).Find( §ions, "contains_contacts = ? AND id IN ?", true, sqlFilterSections, ) return c.Render("people", fiber.Map{ "PageTitle": "Contacts", "MembersPage": false, "People": people, "Pagination": pagination, "PermShow": permShow, "PermShowArchived": permShowArchived, "Sections": sections, "FilterArchive": filterArchive, "FilterSection": filterSectionUint, "FilterSearch": filterSearch, }) } func ContactsExport(c *fiber.Ctx) error { userid, err := helpers.GetSessionUserId(c) if err != nil { return err } allowedSections, err := helpers.PermissionsGetSections( userid, "show_contact", ) if err != nil { return err } allowedSectionsArchived, err := helpers.PermissionsGetSections( userid, "show_archived_contact", ) if err != nil { return err } permShow := (len(allowedSections) > 0) permShowArchived := (len(allowedSectionsArchived) > 0) if !permShow && !permShowArchived { return fiber.NewError(fiber.StatusForbidden, "Forbidden") } db, err := helpers.GetDatabase() if err != nil { return err } filterSection := c.Query("se") filterArchive := c.Query("a") filterSearch := c.Query("s") sqlFilterSections := allowedSections sqlFilterAppend := "IS NULL" if filterArchive == "1" { sqlFilterAppend = "IS NOT NULL" sqlFilterSections = allowedSectionsArchived } var filterSectionUint uint if len(filterSection) > 0 { filterSectionUint64, err := strconv.ParseUint(filterSection, 10, 0) if err == nil { for _, s := range sqlFilterSections { if s == uint(filterSectionUint64) { filterSectionUint = uint(filterSectionUint64) } } } } if filterSectionUint > 0 { sqlFilterAppend = fmt.Sprintf( "%s AND section_id = %d", sqlFilterAppend, filterSectionUint, ) } var sqlFilterSearch string if len(filterSearch) > 0 { sqlFilterSearch = fmt.Sprintf( "%%%s%%", filterSearch, ) } var people []models.Person if len(filterSearch) > 0 { result := db.Unscoped().Order( "last_name collate nocase asc, first_name collate nocase asc", ).Preload("Section").Find( &people, fmt.Sprintf( "is_contact = ? AND section_id IN ? AND (first_name LIKE ? OR last_name LIKE ?) AND deleted_at %s", sqlFilterAppend, ), true, sqlFilterSections, sqlFilterSearch, sqlFilterSearch, ) if result.Error != nil && !errors.Is(result.Error, gorm.ErrRecordNotFound) { return err } } else { result := db.Unscoped().Order( "last_name collate nocase asc, first_name collate nocase asc", ).Preload("Section").Find( &people, fmt.Sprintf( "is_contact = ? AND section_id IN ? AND deleted_at %s", sqlFilterAppend, ), true, sqlFilterSections, ) if result.Error != nil && !errors.Is(result.Error, gorm.ErrRecordNotFound) { return err } } var fields []models.Field db.Order("position asc").Preload( "List", ).Find( &fields, "person_type = ?", "contact", ) var fieldValues []models.FieldValue db.Preload("ListItem").Find(&fieldValues) c.Set("Content-Type", "text/csv") c.Set("Content-Disposition", fmt.Sprintf( "attachment;filename=contacts_%s.csv", time.Now().Format(time.RFC3339), )) csvFields := []string{ "first_name", "last_name", "email", "phone", "mobile", "address1", "address2", "postal_code", "city", "section_id", "section_name", } for _, field := range fields { csvFields = append(csvFields, field.Name) } for key, csvField := range csvFields { csvField = strings.ReplaceAll(csvField, "\r", "") csvField = strings.ReplaceAll(csvField, "\n", " ") csvField = strings.ReplaceAll(csvField, ",", "_") csvField = strings.ReplaceAll(csvField, ";", "_") if key+1 == len(csvFields) { c.Writef("\"%s\"\n", csvField) } else { c.Writef("\"%s\";", csvField) } } for _, person := range people { c.Writef("\"%s\";", person.FirstName) c.Writef("\"%s\";", person.LastName) c.Writef("\"%s\";", person.Email) c.Writef("\"%s\";", person.Phone) c.Writef("\"%s\";", person.Mobile) c.Writef("\"%s\";", person.Address1) c.Writef("\"%s\";", person.Address2) c.Writef("\"%s\";", person.PostalCode) c.Writef("\"%s\";", person.City) c.Writef("\"%d\";", person.SectionID) if len(fields) > 0 { c.Writef("\"%s\";", person.Section.Name) } else { c.Writef("\"%s\"\n", person.Section.Name) } for key, field := range fields { endLine := ";" if key+1 == len(fields) { endLine = "\n" } countMulti := 0 found := false for _, value := range fieldValues { if value.FieldID == field.ID && value.PersonID == person.ID { found = true if field.FieldType == "text" || field.FieldType == "longtext" { text := value.ValueString.String text = strings.ReplaceAll(text, "\r", "") text = strings.ReplaceAll(text, "\n", " ") text = strings.ReplaceAll(text, ",", "_") text = strings.ReplaceAll(text, ";", "_") c.Writef("\"%s\"%s", text, endLine) } else if field.FieldType == "number" { if value.ValueInt.Valid { c.Writef("\"%d\"%s", value.ValueInt.Int64, endLine) } else { c.Writef("\"\"%s", endLine) } } else if field.FieldType == "date" { if value.ValueDate.Valid { date := value.ValueDate.Time.Format("2006-01-02") c.Writef("\"%s\"%s", date, endLine) } else { c.Writef("\"\"%s", endLine) } } else if field.FieldType == "list" { if field.List.Multi { if countMulti == 0 { c.Writef("\"") } else { c.Writef(",") } c.Writef("%s", value.ListItem.Value) countMulti++ } else { c.Writef("\"%s\"%s", value.ListItem.Value, endLine) } } } } if countMulti > 0 { c.Writef("\"%s", endLine) } if !found { c.Writef("\"\"%s", endLine) } } } return nil } func ContactShow(c *fiber.Ctx) error { id := c.Params("id") db, err := helpers.GetDatabase() if err != nil { return err } var person models.Person result := db.Unscoped().Preload("Section").Find( &person, "id = ? AND is_contact", id, true, ) if result.Error != nil { return result.Error } if result.RowsAffected < 1 { return fiber.NewError(fiber.StatusNotFound, "Not found") } userid, err := helpers.GetSessionUserId(c) if err != nil { return err } persmissionName := "show_contact" if person.DeletedAt.Valid { persmissionName = "show_archived_contact" } allow, err := helpers.PermissionsCheckSection( userid, persmissionName, person.SectionID, ) if err != nil { return err } if !allow { return fiber.NewError(fiber.StatusForbidden, "Forbidden") } title := fmt.Sprintf( "%s %s | Contact", person.LastName, person.FirstName, ) var fields []models.Field db.Order("position asc").Preload( "List", ).Find( &fields, "person_type = ?", "contact", ) var fieldValues []models.FieldValue db.Preload("ListItem").Find( &fieldValues, "person_id = ?", person.ID, ) permEdit, _ := helpers.PermissionsGetSections(userid, "edit_contact") permConvert, _ := helpers.PermissionsGetSections(userid, "convert_contact_to_member") permArchive, _ := helpers.PermissionsGetSections(userid, "archive_contact") permRestore, _ := helpers.PermissionsGetSections(userid, "restore_contact") permPurge, _ := helpers.PermissionsGetSections(userid, "purge_contact") return c.Render("person", fiber.Map{ "PageTitle": title, "Person": person, "Fields": fields, "FieldValues": fieldValues, "PermEdit": permEdit, "PermConvert": permConvert, "PermArchive": permArchive, "PermRestore": permRestore, "PermPurge": permPurge, }) } func ContactAdd(c *fiber.Ctx) error { userid, err := helpers.GetSessionUserId(c) if err != nil { return err } allowedSections, err := helpers.PermissionsGetSections( userid, "create_contact", ) if err != nil { return err } if len(allowedSections) < 1 { return fiber.NewError(fiber.StatusForbidden, "Forbidden") } db, err := helpers.GetDatabase() if err != nil { return err } var sections []models.Section db.Order("name collate nocase asc").Find( §ions, "contains_contacts = ?", true, ) var fields []models.Field db.Preload("List").Preload("List.ListItems").Order( "position asc", ).Find( &fields, "person_type = ?", "contact", ) var person models.Person var errors []string if c.Method() == "POST" { data := PersonValidation{ LastName: c.FormValue("last_name"), FirstName: c.FormValue("first_name"), Email: c.FormValue("email"), Phone: c.FormValue("phone"), Mobile: c.FormValue("mobile"), Address1: c.FormValue("address1"), Address2: c.FormValue("address2"), PostalCode: c.FormValue("postal_code"), City: c.FormValue("city"), Section: c.FormValue("section"), } validate := validator.New() validErrs := validate.Struct(data) if validErrs != nil { for _, validErr := range validErrs.(validator.ValidationErrors) { switch validErr.Field() { case "LastName": errors = append(errors, "Le nom de famille est requis et ne peut pas contenir plus de 100 caractères") case "FirstName": errors = append(errors, "Le prénom est requis et ne peut pas contenir plus de 100 caractères") case "Email": if len(data.Email) > 0 { errors = append(errors, "L'adresse email doit être valide") } case "Phone": errors = append(errors, "Le numéro de téléphone fixe est trop long") case "Mobile": errors = append(errors, "Le numéro de téléphone mobile est trop long") case "Address1": errors = append(errors, "La ligne 1 de l'adresse est trop longue") case "Address2": errors = append(errors, "La ligne 2 de l'adresse est trop longue") case "PostalCode": if len(data.PostalCode) > 0 { errors = append(errors, "Le code postal n'est pas valide") } case "City": errors = append(errors, "Le lieu est trop long") case "Section": errors = append(errors, "La section n'est pas valide") } } } person.IsContact = true person.IsMember = false person.LastName = data.LastName person.FirstName = data.FirstName person.Email = data.Email person.Phone = data.Phone person.Mobile = data.Mobile person.Address1 = data.Address1 person.Address2 = data.Address2 person.PostalCode = data.PostalCode person.City = data.City sectionID, err := strconv.ParseUint(data.Section, 10, 0) if err == nil { for _, section := range sections { if section.ID == uint(sectionID) { person.SectionID = uint(sectionID) break } } } if person.SectionID == 0 { errors = append(errors, "La section est introuvable") } if len(errors) == 0 { result := db.Create(&person) if result.Error != nil { return result.Error } for _, field := range fields { if field.List.Multi { for _, listItem := range field.List.ListItems { key := fmt.Sprintf("field-%d-%d", field.ID, listItem.ID) value := c.FormValue(key) if value == "on" { var fieldValue models.FieldValue fieldValue.FieldID = field.ID fieldValue.PersonID = person.ID fieldValue.ListItemID = listItem.ID db.Create(&fieldValue) } } } else { key := fmt.Sprintf("field-%d", field.ID) value := c.FormValue(key) if (field.FieldType == "text" || field.FieldType == "longtext") && len(value) > 0 { var fieldValue models.FieldValue fieldValue.FieldID = field.ID fieldValue.PersonID = person.ID err = fieldValue.ValueString.Scan(value) if err != nil { continue } db.Create(&fieldValue) } if field.FieldType == "number" && len(value) > 0 { valueInt, err := strconv.ParseInt(value, 10, 0) if err != nil { continue } var fieldValue models.FieldValue fieldValue.FieldID = field.ID fieldValue.PersonID = person.ID err = fieldValue.ValueInt.Scan(valueInt) if err != nil { continue } db.Create(&fieldValue) } if field.FieldType == "date" && len(value) > 0 { valueDate, err := time.Parse("2006-01-02", value) if err != nil { continue } var fieldValue models.FieldValue fieldValue.FieldID = field.ID fieldValue.PersonID = person.ID err = fieldValue.ValueDate.Scan(valueDate) if err != nil { continue } db.Create(&fieldValue) } if field.FieldType == "list" && len(value) > 0 { valueInt, err := strconv.ParseUint(value, 10, 0) if err != nil { continue } found := false for _, listItem := range field.List.ListItems { if listItem.ID == uint(valueInt) { found = true } } if found { var fieldValue models.FieldValue fieldValue.FieldID = field.ID fieldValue.PersonID = person.ID fieldValue.ListItemID = uint(valueInt) db.Create(&fieldValue) } } } } c.Redirect(fmt.Sprintf( "/contacts/%d", person.ID, )) } } return c.Render("person_form", fiber.Map{ "PageTitle": "Ajouter un contact", "MembersPage": false, "Person": person, "Sections": sections, "Fields": fields, "Errors": errors, }) } func ContactEdit(c *fiber.Ctx) error { id := c.Params("id") db, err := helpers.GetDatabase() if err != nil { return err } var person models.Person result := db.Find(&person, "id = ?", id) if result.Error != nil { return result.Error } if result.RowsAffected < 1 { return fiber.NewError(fiber.StatusNotFound, "Not found") } userid, err := helpers.GetSessionUserId(c) if err != nil { return err } allow, err := helpers.PermissionsCheckSection( userid, "edit_contact", person.SectionID, ) if err != nil { return err } if !allow { return fiber.NewError(fiber.StatusForbidden, "Forbidden") } title := fmt.Sprintf( "%s %s | Modifier contact", person.LastName, person.FirstName, ) var sections []models.Section db.Order("name collate nocase asc").Find( §ions, "contains_contacts = ?", true, ) var fields []models.Field db.Preload("List").Preload("List.ListItems").Order( "position asc", ).Find( &fields, "person_type = ?", "contact", ) var fieldValues []models.FieldValue db.Preload("ListItem").Find( &fieldValues, "person_id = ?", person.ID, ) var errors []string if c.Method() == "POST" { data := PersonValidation{ LastName: c.FormValue("last_name"), FirstName: c.FormValue("first_name"), Email: c.FormValue("email"), Phone: c.FormValue("phone"), Mobile: c.FormValue("mobile"), Address1: c.FormValue("address1"), Address2: c.FormValue("address2"), PostalCode: c.FormValue("postal_code"), City: c.FormValue("city"), Section: c.FormValue("section"), } validate := validator.New() validErrs := validate.Struct(data) if validErrs != nil { for _, validErr := range validErrs.(validator.ValidationErrors) { switch validErr.Field() { case "LastName": errors = append(errors, "Le nom de famille est requis et ne peut pas contenir plus de 100 caractères") case "FirstName": errors = append(errors, "Le prénom est requis et ne peut pas contenir plus de 100 caractères") case "Email": if len(data.Email) > 0 { errors = append(errors, "L'adresse email doit être valide") } case "Phone": errors = append(errors, "Le numéro de téléphone fixe est trop long") case "Mobile": errors = append(errors, "Le numéro de téléphone mobile est trop long") case "Address1": errors = append(errors, "La ligne 1 de l'adresse est trop longue") case "Address2": errors = append(errors, "La ligne 2 de l'adresse est trop longue") case "PostalCode": if len(data.PostalCode) > 0 { errors = append(errors, "Le code postal n'est pas valide") } case "City": errors = append(errors, "Le lieu est trop long") case "Section": errors = append(errors, "La section n'est pas valide") } } } person.IsContact = true person.IsMember = false person.LastName = data.LastName person.FirstName = data.FirstName person.Email = data.Email person.Phone = data.Phone person.Mobile = data.Mobile person.Address1 = data.Address1 person.Address2 = data.Address2 person.PostalCode = data.PostalCode person.City = data.City sectionID, err := strconv.ParseUint(data.Section, 10, 0) if err == nil { for _, section := range sections { if section.ID == uint(sectionID) { person.SectionID = uint(sectionID) break } } } if person.SectionID == 0 { errors = append(errors, "La section est introuvable") } for _, field := range fields { db.Unscoped().Delete( &models.FieldValue{}, "person_id = ? AND field_id = ?", person.ID, field.ID, ) if field.List.Multi { for _, listItem := range field.List.ListItems { key := fmt.Sprintf("field-%d-%d", field.ID, listItem.ID) value := c.FormValue(key) if value == "on" { var fieldValue models.FieldValue fieldValue.FieldID = field.ID fieldValue.PersonID = person.ID fieldValue.ListItemID = listItem.ID db.Create(&fieldValue) } } } else { key := fmt.Sprintf("field-%d", field.ID) value := c.FormValue(key) if (field.FieldType == "text" || field.FieldType == "longtext") && len(value) > 0 { var fieldValue models.FieldValue fieldValue.FieldID = field.ID fieldValue.PersonID = person.ID err = fieldValue.ValueString.Scan(value) if err != nil { continue } db.Create(&fieldValue) } if field.FieldType == "number" && len(value) > 0 { valueInt, err := strconv.ParseInt(value, 10, 0) if err != nil { continue } var fieldValue models.FieldValue fieldValue.FieldID = field.ID fieldValue.PersonID = person.ID err = fieldValue.ValueInt.Scan(valueInt) if err != nil { continue } db.Create(&fieldValue) } if field.FieldType == "date" && len(value) > 0 { valueDate, err := time.Parse("2006-01-02", value) if err != nil { continue } var fieldValue models.FieldValue fieldValue.FieldID = field.ID fieldValue.PersonID = person.ID err = fieldValue.ValueDate.Scan(valueDate) if err != nil { continue } db.Create(&fieldValue) } if field.FieldType == "list" && len(value) > 0 { valueInt, err := strconv.ParseUint(value, 10, 0) if err != nil { continue } found := false for _, listItem := range field.List.ListItems { if listItem.ID == uint(valueInt) { found = true } } if found { var fieldValue models.FieldValue fieldValue.FieldID = field.ID fieldValue.PersonID = person.ID fieldValue.ListItemID = uint(valueInt) db.Create(&fieldValue) } } } } if len(errors) == 0 { result := db.Save(&person) if result.Error != nil { return result.Error } c.Redirect(fmt.Sprintf( "/contacts/%d", person.ID, )) } } return c.Render("person_form", fiber.Map{ "PageTitle": title, "Person": person, "Sections": sections, "Fields": fields, "FieldValues": fieldValues, "Errors": errors, }) } func ContactConvert(c *fiber.Ctx) error { id := c.Params("id") db, err := helpers.GetDatabase() if err != nil { return err } var person models.Person result := db.Find(&person, "id = ?", id) if result.Error != nil { return result.Error } if result.RowsAffected < 1 { return fiber.NewError(fiber.StatusNotFound, "Not found") } userid, err := helpers.GetSessionUserId(c) if err != nil { return err } allow, err := helpers.PermissionsCheckSection( userid, "convert_contact_to_member", person.SectionID, ) if err != nil { return err } if !allow { return fiber.NewError(fiber.StatusForbidden, "Forbidden") } person.IsContact = false person.IsMember = true result = db.Save(&person) if result.Error != nil { return result.Error } return c.Redirect(fmt.Sprintf( "/members/%d", person.ID, )) } func ContactArchive(c *fiber.Ctx) error { id := c.Params("id") db, err := helpers.GetDatabase() if err != nil { return err } var person models.Person result := db.Find(&person, "id = ? AND is_contact = ? AND deleted_at IS NULL", id, true) if result.Error != nil { return result.Error } if result.RowsAffected < 1 { return fiber.NewError(fiber.StatusNotFound, "Not found") } userid, err := helpers.GetSessionUserId(c) if err != nil { return err } allow, err := helpers.PermissionsCheckSection( userid, "archive_contact", person.SectionID, ) if err != nil { return err } if !allow { return fiber.NewError(fiber.StatusForbidden, "Forbidden") } result = db.Delete(&person) if result.Error != nil { return result.Error } return c.Redirect(fmt.Sprintf( "/contacts/%s", id, )) } func ContactRestore(c *fiber.Ctx) error { id := c.Params("id") db, err := helpers.GetDatabase() if err != nil { return err } var person models.Person result := db.Unscoped().Find( &person, "id = ? AND is_contact = ? AND deleted_at IS NOT NULL", id, true, ) if result.Error != nil { return result.Error } if result.RowsAffected < 1 { return fiber.NewError(fiber.StatusNotFound, "Not found") } userid, err := helpers.GetSessionUserId(c) if err != nil { return err } allow, err := helpers.PermissionsCheckSection( userid, "restore_contact", person.SectionID, ) if err != nil { return err } if !allow { return fiber.NewError(fiber.StatusForbidden, "Forbidden") } person.DeletedAt.Valid = false result = db.Save(&person) if result.Error != nil { return result.Error } return c.Redirect(fmt.Sprintf( "/contacts/%s", id, )) } func ContactPurge(c *fiber.Ctx) error { id := c.Params("id") db, err := helpers.GetDatabase() if err != nil { return err } var person models.Person result := db.Unscoped().Find( &person, "id = ? AND is_contact = ?", id, true, ) if result.Error != nil { return result.Error } if result.RowsAffected < 1 { return fiber.NewError(fiber.StatusNotFound, "Not found") } userid, err := helpers.GetSessionUserId(c) if err != nil { return err } allow, err := helpers.PermissionsCheckSection( userid, "purge_contact", person.SectionID, ) if err != nil { return err } if !allow { return fiber.NewError(fiber.StatusForbidden, "Forbidden") } result = db.Unscoped().Delete( &models.FieldValue{}, "person_id = ?", id, ) if result.Error != nil { return result.Error } result = db.Unscoped().Delete( &models.Person{}, id, ) if result.Error != nil { return result.Error } return c.Redirect("/contacts") }