refactor: modularize app into config, UI, and server components
Co-authored-by: aider (deepseek/deepseek-chat) <aider@aider.chat>
This commit is contained in:
371
R/app_server.R
Normal file
371
R/app_server.R
Normal file
@@ -0,0 +1,371 @@
|
||||
# Server logic for St. Andrews Shiny App
|
||||
|
||||
create_server <- function(input, output, session) {
|
||||
# Load data
|
||||
owners <- readRDS(app_config$data_paths$owners)
|
||||
listings <- readRDS(app_config$data_paths$listings) |>
|
||||
arrange(price_per_sqft) |>
|
||||
mutate(
|
||||
price_fmt = scales::dollar(price),
|
||||
ppsf_fmt = scales::dollar(price_per_sqft)
|
||||
)
|
||||
sales <- readRDS(app_config$data_paths$sales) |>
|
||||
arrange(desc(listed_date)) |>
|
||||
mutate(
|
||||
price_fmt = scales::dollar(price),
|
||||
ppsf_fmt = scales::dollar(price_per_sqft)
|
||||
)
|
||||
last_sale_date <- format(attr(owners, "last_sale_date"), "%Y-%m-%d")
|
||||
sbdvn <- sf::st_read(app_config$data_paths$plats)
|
||||
beaches <- readRDS(app_config$data_paths$beaches)
|
||||
|
||||
# update tabs depending on side panel
|
||||
observeEvent(input$menu, {
|
||||
updateF7Tabs(id = "tabs",
|
||||
selected = input$menu)
|
||||
})
|
||||
|
||||
# resources sub-section toggle
|
||||
res_section <- reactiveVal("beaches")
|
||||
observeEvent(input$res_beaches, { res_section("beaches") })
|
||||
observeEvent(input$res_links, { res_section("links") })
|
||||
observeEvent(input$res_restart, { res_section("restart") })
|
||||
|
||||
output$resources_content <- renderUI({
|
||||
if (res_section() == "beaches") {
|
||||
tagList(
|
||||
f7Card(
|
||||
title = "Beaches",
|
||||
divider = TRUE,
|
||||
leafletOutput("beach_map"),
|
||||
footer = p("Source: Environmental Protection Agency")
|
||||
),
|
||||
f7Block(
|
||||
h3("Helpful Links:"),
|
||||
f7List(
|
||||
inset = TRUE, dividers = TRUE, strong = TRUE, outline = FALSE,
|
||||
f7ListItem(title = "EPA Beaches", href = "https://www.epa.gov/beaches", external = TRUE),
|
||||
f7ListItem(title = "Red Tide Forecast", href = "https://habforecast.gcoos.org/", external = TRUE),
|
||||
f7ListItem(title = "Healthy Beaches Program", href = "https://fdoh.maps.arcgis.com/apps/instant/nearby/index.html?appid=7106a20597de4bff98cc5ebc7f932047&findSource=0&find=1600%2520Harbor%2520Dr%2520S%252C%2520Venice%252C%2520Florida%252C%252034285&sliderDistance=2", external = TRUE),
|
||||
f7ListItem(title = "MOTE Beach Conditions", href = "https://visitbeaches.org/beach/6/report/53033", external = TRUE),
|
||||
f7ListItem(title = "National Hurricane Center", href = "https://www.nhc.noaa.gov/", external = TRUE)
|
||||
)
|
||||
)
|
||||
)
|
||||
} else {
|
||||
tagList(
|
||||
f7BlockTitle(title = "Services:", size = "medium"),
|
||||
f7Block(
|
||||
f7List(
|
||||
mode = "links", inset = TRUE, outline = TRUE, dividers = TRUE, strong = TRUE,
|
||||
f7Link(label = "City of Venice", href = "https://www.venicegov.com/"),
|
||||
f7Link(label = "Florida Power & Light", href = "https://www.fpl.com/"),
|
||||
f7Link(label = "Sarasota County", href = "https://www.scgov.net/"),
|
||||
f7Link(label = "Property Appraiser", href = "https://www.sc-pa.com/"),
|
||||
f7Link(label = "Open GIS Portal", href = "https://data-sarco.opendata.arcgis.com/"),
|
||||
f7Link(label = "Waste & Recycling", href = "https://www.venicegov.com/government/public-works/waste-and-recycling"),
|
||||
f7Link(label = "Condo Regulation", href = "https://condos.myfloridalicense.com/"),
|
||||
f7Link(label = "Property Records Search", href = "https://www.sarasotaclerk.com/records/official-records/search-land-records")
|
||||
)
|
||||
),
|
||||
f7BlockTitle(title = "Documents:", size = "medium"),
|
||||
f7Block(
|
||||
f7List(
|
||||
mode = "links", inset = TRUE, outline = TRUE, dividers = TRUE, strong = TRUE,
|
||||
f7Link(label = "St. Andrews Covenants", href = "docs/2000_01_01_st_andrews_covenants.pdf"),
|
||||
f7Link(label = "St. Andrews (unrecorded)", href = "docs/2004_06_23_sap_map.pdf"),
|
||||
f7Link(label = "Patios 2", href = "docs/1997_08_12_patios_2_plat.pdf"),
|
||||
f7Link(label = "Patios 3", href = "docs/1998_11_17_patios_3_plat.pdf"),
|
||||
f7Link(label = "Villas 2", href = "docs/1998_09_14_villas_2_plat.pdf")
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
# Initialize filteredOwners with all owners
|
||||
filteredOwners <- reactiveVal(owners)
|
||||
|
||||
# Function to filter owners based on inputs
|
||||
filterOwners <- function() {
|
||||
filtered <- owners
|
||||
|
||||
# Filter by subdivision if specified
|
||||
if (!is.null(input$sub_name) && input$sub_name != "All") {
|
||||
# Get the subdivision IDs for the selected subdivision name
|
||||
sub_ids <- sbdvn %>%
|
||||
filter(sub_name == input$sub_name) %>%
|
||||
pull(max_sub_id)
|
||||
filtered <- filtered %>%
|
||||
filter(subdivision %in% sub_ids)
|
||||
}
|
||||
|
||||
# Filter by name if specified
|
||||
if (!is.null(input$name) && input$name != "") {
|
||||
filtered <- filtered %>%
|
||||
filter(
|
||||
grepl(input$name, owner_1, ignore.case = TRUE) |
|
||||
grepl(input$name, owner_2, ignore.case = TRUE)
|
||||
)
|
||||
}
|
||||
|
||||
# Filter by location if specified
|
||||
if (!is.null(input$location) && input$location != "") {
|
||||
filtered <- filtered %>%
|
||||
filter(grepl(input$location, location, ignore.case = TRUE))
|
||||
}
|
||||
|
||||
return(filtered)
|
||||
}
|
||||
|
||||
# Update filtered owners when filter button is clicked
|
||||
observeEvent(input$filterButton, {
|
||||
filteredOwners(filterOwners())
|
||||
})
|
||||
|
||||
# Also update when any of the filter inputs change
|
||||
observe({
|
||||
# Track dependencies
|
||||
input$name
|
||||
input$location
|
||||
input$sub_name
|
||||
|
||||
# Update filtered owners, but only if the filter button has been clicked at least once
|
||||
# To prevent immediate filtering on app start
|
||||
if (!is.null(input$filterButton)) {
|
||||
if (input$filterButton > 0) {
|
||||
filteredOwners(filterOwners())
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
mean_lat <- reactive({
|
||||
if (!is.null(filteredOwners()) && nrow(filteredOwners()) > 0) {
|
||||
filteredOwners() %>%
|
||||
st_coordinates() %>%
|
||||
.[, "Y"] %>%
|
||||
mean()
|
||||
} else {
|
||||
app_config$map_config$default_lat
|
||||
}
|
||||
})
|
||||
|
||||
mean_lng <- reactive({
|
||||
if (!is.null(filteredOwners()) && nrow(filteredOwners()) > 0) {
|
||||
filteredOwners() %>%
|
||||
st_coordinates() %>%
|
||||
.[, "X"] %>%
|
||||
mean()
|
||||
} else {
|
||||
app_config$map_config$default_lng
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
output$map <- renderLeaflet({
|
||||
leaflet() %>%
|
||||
addProviderTiles("CartoDB.Voyager") %>%
|
||||
addPolygons(
|
||||
data = sbdvn,
|
||||
color = "red",
|
||||
weight = 2,
|
||||
opacity = 0.5,
|
||||
fillOpacity = 0.2,
|
||||
label = ~sub_name,
|
||||
group = "Subdivisions"
|
||||
)
|
||||
})
|
||||
|
||||
# Update map markers when filteredOwners changes
|
||||
observe({
|
||||
leafletProxy("map") %>%
|
||||
clearGroup("Owners") %>%
|
||||
addMarkers(
|
||||
data = filteredOwners(),
|
||||
popup = popupTable(
|
||||
filteredOwners(),
|
||||
row.numbers = FALSE,
|
||||
feature.id = FALSE,
|
||||
zcol = c(
|
||||
"label",
|
||||
"owner_1",
|
||||
"owner_2"
|
||||
)
|
||||
),
|
||||
group = "Owners"
|
||||
) %>%
|
||||
addLayersControl(
|
||||
overlayGroups = c("Subdivisions", "Owners"),
|
||||
options = layersControlOptions(collapsed = FALSE)
|
||||
) %>%
|
||||
setView(lng = mean_lng(), lat = mean_lat(), zoom = app_config$map_config$default_zoom)
|
||||
})
|
||||
|
||||
output$table <- renderDT({
|
||||
if (!is.null(filteredOwners()) && nrow(filteredOwners()) > 0) {
|
||||
my_table <-
|
||||
filteredOwners() %>%
|
||||
st_drop_geometry() %>%
|
||||
select(label, owner_1, owner_2, homestead)
|
||||
datatable(my_table,
|
||||
colnames = c("Address", "Owner 1", "Owner 2", "Homestead"),
|
||||
rownames = FALSE,
|
||||
options = list(
|
||||
pageLength = 10,
|
||||
scrollX = TRUE,
|
||||
searching = FALSE,
|
||||
lengthMenu = c(5, 10, 25, 50),
|
||||
dom = 'tpi'
|
||||
)
|
||||
)
|
||||
} else {
|
||||
# Return empty table with same structure
|
||||
my_table <- data.frame(
|
||||
label = character(0),
|
||||
owner_1 = character(0),
|
||||
owner_2 = character(0),
|
||||
homestead = numeric(0)
|
||||
)
|
||||
datatable(my_table,
|
||||
colnames = c("Address", "Owner 1", "Owner 2", "Homestead"),
|
||||
rownames = FALSE,
|
||||
options = list(
|
||||
pageLength = 10,
|
||||
scrollX = TRUE,
|
||||
searching = FALSE,
|
||||
lengthMenu = c(5, 10, 25, 50),
|
||||
dom = 'tpi'
|
||||
)
|
||||
)
|
||||
}
|
||||
})
|
||||
prep_mailing <- function(data) {
|
||||
data |>
|
||||
sf::st_drop_geometry() |>
|
||||
dplyr::select(owner_1, owner_2, mailing_address_1, mailing_address_2,
|
||||
mailing_city, mailing_state, mailing_zip_code) |>
|
||||
dplyr::mutate(
|
||||
owner_2 = ifelse(is.na(owner_2), "", owner_2),
|
||||
mailing_address_2 = ifelse(is.na(mailing_address_2), "", mailing_address_2)
|
||||
) |>
|
||||
dplyr::rename(
|
||||
address_1 = mailing_address_1,
|
||||
address_2 = mailing_address_2,
|
||||
city = mailing_city,
|
||||
state = mailing_state,
|
||||
zip = mailing_zip_code
|
||||
)
|
||||
}
|
||||
|
||||
output$download_filtered <- downloadHandler(
|
||||
filename = "st_andrews_owners_filtered.csv",
|
||||
content = function(file) {
|
||||
write.csv(prep_mailing(filteredOwners()), file, row.names = FALSE)
|
||||
}
|
||||
)
|
||||
|
||||
output$download_all <- downloadHandler(
|
||||
filename = "st_andrews_owners_all.csv",
|
||||
content = function(file) {
|
||||
write.csv(prep_mailing(owners), file, row.names = FALSE)
|
||||
}
|
||||
)
|
||||
|
||||
# listings map ----
|
||||
output$listings_map <- renderLeaflet({
|
||||
leaflet(listings) |>
|
||||
addProviderTiles("CartoDB.Voyager") |>
|
||||
addPolygons(
|
||||
data = sbdvn,
|
||||
color = "red",
|
||||
weight = 2,
|
||||
opacity = 0.5,
|
||||
fillOpacity = 0.2,
|
||||
label = ~sub_name
|
||||
) |>
|
||||
addMarkers(
|
||||
lat = ~latitude,
|
||||
lng = ~longitude,
|
||||
popup = ~paste0(
|
||||
"<b>", address, "</b><br>",
|
||||
"Price: ", price_fmt, "<br>",
|
||||
"Sq Ft: ", sqft, "<br>",
|
||||
"$/Sq Ft: ", ppsf_fmt
|
||||
)
|
||||
) |>
|
||||
setView(lng = app_config$map_config$default_lng,
|
||||
lat = app_config$map_config$default_lat,
|
||||
zoom = app_config$map_config$default_zoom)
|
||||
})
|
||||
|
||||
# listings table ----
|
||||
output$listings_table <- renderDT({
|
||||
datatable(
|
||||
listings |> select(listed_date, address, sqft, price_fmt, ppsf_fmt),
|
||||
colnames = c("Listed", "Address", "Sq Ft", "Price", "$/Sq Ft"),
|
||||
rownames = FALSE,
|
||||
options = list(
|
||||
pageLength = 25,
|
||||
searching = FALSE,
|
||||
dom = 't'
|
||||
)
|
||||
)
|
||||
})
|
||||
|
||||
# sales map ----
|
||||
output$sales_map <- renderLeaflet({
|
||||
leaflet(sales) |>
|
||||
addProviderTiles("CartoDB.Voyager") |>
|
||||
addPolygons(
|
||||
data = sbdvn,
|
||||
color = "red",
|
||||
weight = 2,
|
||||
opacity = 0.5,
|
||||
fillOpacity = 0.2,
|
||||
label = ~sub_name
|
||||
) |>
|
||||
addMarkers(
|
||||
lat = ~latitude,
|
||||
lng = ~longitude,
|
||||
popup = ~paste0(
|
||||
"<b>", address, "</b><br>",
|
||||
"Sale Date: ", listed_date, "<br>",
|
||||
"Price: ", price_fmt, "<br>",
|
||||
"Sq Ft: ", sqft, "<br>",
|
||||
"$/Sq Ft: ", ppsf_fmt
|
||||
)
|
||||
) |>
|
||||
setView(lng = app_config$map_config$default_lng,
|
||||
lat = app_config$map_config$default_lat,
|
||||
zoom = app_config$map_config$default_zoom)
|
||||
})
|
||||
|
||||
# sales table ----
|
||||
output$sales_table <- renderDT({
|
||||
datatable(
|
||||
sales |> select(listed_date, address, sqft, price_fmt, ppsf_fmt),
|
||||
colnames = c("Date", "Address", "Sq Ft", "Price", "$/Sq Ft"),
|
||||
rownames = FALSE,
|
||||
options = list(
|
||||
pageLength = 10,
|
||||
searching = FALSE,
|
||||
dom = 't'
|
||||
)
|
||||
)
|
||||
})
|
||||
|
||||
# beach map ----
|
||||
output$beach_map <- renderLeaflet({
|
||||
leaflet() %>%
|
||||
addProviderTiles("CartoDB.Voyager") %>%
|
||||
setView(lng = app_config$map_config$beach_lng,
|
||||
lat = app_config$map_config$beach_lat,
|
||||
zoom = app_config$map_config$beach_zoom) %>%
|
||||
addMarkers(
|
||||
data = beaches,
|
||||
lat = ~lat,
|
||||
lng = ~lng,
|
||||
popup = ~beach_name
|
||||
)
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user