Edit project screen done

This commit is contained in:
Franklin 2023-04-20 18:11:19 -04:00
parent c693287063
commit 39abe4caca
9 changed files with 363 additions and 38 deletions

View File

@ -4,4 +4,20 @@
font-family: Source Sans Pro;
font-weight: 700;
letter-spacing: 0.1rem;
}
}
.admin-dropdown {
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
min-width: 150px;
width: 100%;
min-height: 50px;
padding: 5px 10px;
border: 1px solid rgba(0, 0, 0, 0.0);
background-color: rgba(0, 0, 0, 0.04);
border-radius: 5px;
font-family: Source Sans Pro;
font-size: 16px;
}

View File

@ -0,0 +1,44 @@
.textfield-container {
display: flex;
flex-direction: column;
justify-content: center;
align-items: start;
gap: 4px;
width: 100%;
}
.textfield-label {
font-family: Space Grotesk;
font-size: 16px;
font-weight: 900;
letter-spacing: 0.1rem;
text-transform: uppercase;
}
.textfield-label-required {
font-family: Space Grotesk;
font-size: 16px;
font-weight: 900;
letter-spacing: 0.1rem;
text-transform: uppercase;
}
.textfield-label-required:after {
color: #e32;
content: '*';
display: inline;
}
.textfield {
height: 50px;
background-color: white;
border: solid 0.5px #d8d8d8;
width: 100%;
text-indent: 10px;
}
.textarea {
height: 150px;
background-color: white;
border: solid 0.5px #d8d8d8;
width: 100%;
padding: 10px;
}

View File

@ -1,11 +1,12 @@
.admin-project-fields-container {
display: flex;
flex-direction: column;
justify-content: start;
justify-content: center;
align-items: center;
gap: 20px;
width: 90%;
width: 80%;
max-width: 750px;
}
.admin-project-field-container {
@ -21,4 +22,12 @@
font-family: Source Sans Pro;
font-size: 16px;
text-align: center;
}
.admin-project-textfield {
height: 50px;
background-color: white;
border: solid 0.5px #d8d8d8;
width: 100%;
text-indent: 10px;
}

View File

@ -25,6 +25,7 @@
<link data-trunk type="text/css" href="css/components/media_slideshow.css" rel="css" />
<link data-trunk type="text/css" href="css/components/floating_widget.css" rel="css" />
<link data-trunk type="text/css" href="css/components/footer.css" rel="css" />
<link data-trunk type="text/css" href="css/components/textfield.css" rel="css" />
<link data-trunk type="text/css" href="css/components/agent_card.css" rel="css" />
<link data-trunk type="text/css" href="css/components/admin_nav_bar.css" rel="css" />
<link data-trunk type="text/css" href="css/components/admin_project.css" rel="css" />

View File

@ -0,0 +1,51 @@
use std::fmt::Display;
use jl_types::domain::{option_wrapper::OptionWrapper};
use yew::prelude::*;
use yew_utils::vdom::comp_with;
/// Give this component a list of options
#[function_component(DropDown)]
pub fn dropdown<T: Display + std::cmp::PartialEq + Clone + 'static>(props: &DropDownProps<T>) -> Html {
let selection_changed_cb = {
let onchange_cb = props.onchange.clone();
let selected = props.selected.clone();
Callback::from(move |option: OptionWrapper<T>| {
selected.set(option.option.clone());
match onchange_cb.clone() {
Some(additional_cb) => additional_cb.emit(option),
None => {},
};
})
};
let drop_down = comp_with::<yew_utils::components::drop_down::DropDown<OptionWrapper<T>>>(yew_utils::components::drop_down::DropDownProps {
initial: {
OptionWrapper::new((*props.selected).clone())},
options: {
let mut options: Vec<OptionWrapper<T>> = props.options.clone().into_iter().map(|option| OptionWrapper::new(Some(option))).collect();
if props.has_none { options.insert(0, OptionWrapper::new(None)); }
options
},
selection_changed: selection_changed_cb.clone(),
class_css: Some("admin-dropdown".into())
});
//if (*props.selected).is_none() {selection_changed_cb.emit( OptionWrapper::new((*props.selected).clone()));}
html! {
{drop_down}
}
}
#[derive(PartialEq, Properties, Clone)]
pub struct DropDownProps<T: Display + std::cmp::PartialEq + Clone> {
#[prop_or_default]
pub options: Vec<T>,
/// in case that there is no selected option, this will reflect a None variant
pub selected: UseStateHandle<Option<T>>,
#[prop_or_default]
pub unpicked_text: String,
#[prop_or_default]
pub onchange: Option<Callback<OptionWrapper<T>>>,
#[prop_or_default]
pub has_none: bool
}

View File

@ -6,4 +6,6 @@ pub mod feature;
pub mod footer;
pub mod agent_card;
pub mod admin_nav_bar;
pub mod admin_project;
pub mod admin_project;
pub mod textfield;
pub mod dropdown;

View File

@ -0,0 +1,80 @@
// COMPONENTFORLIB
use wasm_bindgen::{JsCast, UnwrapThrowExt};
use web_sys::{HtmlInputElement, HtmlTextAreaElement};
use yew::prelude::*;
/// This component is a text
#[function_component(TextField)]
pub fn textfield(props: &TextFieldProps) -> Html {
let on_input_changed = {
let handle = props.value.clone();
let fieldtype = props.fieldtype.clone();
let onchange = props.onchange.clone();
Callback::from(move |e: InputEvent| {
match fieldtype {
TextFieldType::Input => {
let value = get_value_from_input_event(e);
match onchange.clone() {
Some(onchange) => { onchange.emit(value.clone()); },
None => {}
};
handle.set(value);
},
TextFieldType::TextArea => {
let value = get_value_from_input_event(e);
match onchange.clone() {
Some(onchange) => { onchange.emit(value.clone()); },
None => {}
};
handle.set(value);
},
}
})
};
html! {
<div class={"textfield-container"}>
<div class={if props.required {"textfield-label-required"} else {"textfield-label"}}>{props.label.clone()}</div>
{
if props.fieldtype == TextFieldType::Input {
html! { <input class={"textfield"} oninput={on_input_changed} value={(*props.value).clone()}/> }
} else {
html! { <textarea class={"textarea"} oninput={on_input_changed} value={(*props.value).clone()}/> }
}
}
</div>
}
}
#[derive(Properties, PartialEq, Clone)]
pub struct TextFieldProps {
pub label: String,
pub value: UseStateHandle<String>,
#[prop_or_default]
pub fieldtype: TextFieldType,
#[prop_or_default]
pub required: bool,
pub onchange: Option<Callback<String>>
}
#[derive(PartialEq, Clone, Default)]
pub enum TextFieldType {
TextArea,
#[default]
Input,
}
pub fn get_value_from_input_event(e: InputEvent) -> String {
let event: Event = e.dyn_into().unwrap_throw();
let event_target = event.target().unwrap_throw();
let target: HtmlInputElement = event_target.dyn_into().unwrap_throw();
target.value()
}
pub fn get_value_from_textarea_event(e: InputEvent) -> String {
let event: Event = e.dyn_into().unwrap_throw();
let event_target = event.target().unwrap_throw();
let target: HtmlTextAreaElement = event_target.dyn_into().unwrap_throw();
target.value()
}

View File

@ -1,19 +1,17 @@
use std::{fmt::Display, str::FromStr};
use chrono::Utc;
use jl_types::{ domain::{agent::Agent, project_state::ProjectState, project_condition::ProjectCondition, project_type::ProjectType, media::{Media, MediaList}, unit::Unit}, dto::listing::Listing};
use jl_types::{ domain::{agent::Agent, media::{MediaList}, unit::Unit, project_state::ProjectState, project_condition::ProjectCondition, project_type::ProjectType}, dto::listing::Listing};
use uuid::Uuid;
use yew::prelude::*;
use yew_router::prelude::use_navigator;
use crate::{components::admin_nav_bar::AdminNavigationBar, api::backend::get_project_listing};
use crate::{components::{admin_nav_bar::AdminNavigationBar, textfield::{TextField, TextFieldType}, dropdown::DropDown}, api::backend::{get_project_listing, get_all_agents}};
/// All of the editing actions of the admin panel will lead to here. This should take an id of anything. A unit, a project, an agent. And its corresponding ID.
#[function_component(AdminEditPage)]
pub fn edit_page(props: &AdminEditPageProps) -> Html {
let navigator = use_navigator().unwrap();
let _navigator = use_navigator().unwrap();
let listing = use_state(|| None);
use_state(|| {
let listing = listing.clone();
match props.edit_item {
@ -31,7 +29,7 @@ pub fn edit_page(props: &AdminEditPageProps) -> Html {
listing.set(Some(listing_persisted));
},
Err(error) => log::error!("Error loading listing: {error}")
}
};
});
},
};
@ -44,18 +42,20 @@ pub fn edit_page(props: &AdminEditPageProps) -> Html {
<AdminNavigationBar/>
<div class={"admin-page-container"}>
<div class={"admin-start-container"}>
<div class={"admin-panel-page-title"}>{format!("Editar {}", match props.edit_item {
<div class={"admin-panel-page-title"}>{ format!("Editar {}", match props.edit_item {
EditItem::Agent => "Agente",
EditItem::Project => "Proyecto",
EditItem::Unit(_) => "Unidad",
})}</div>
{
match props.edit_item {
EditItem::Agent => {html! {}},
EditItem::Project => {html! { <ProjectFields listing={(*listing).clone()}/> }},
EditItem::Unit(_) => {html! {}},
<div class={"admin-project-fields-container"}>
{
match props.edit_item {
EditItem::Agent => {html! {}},
EditItem::Project => {html! { <ProjectFields listing={(*listing).clone()}/> }},
EditItem::Unit(_) => {html! {}},
}
}
}
</div>
</div>
</div>
</>
@ -84,34 +84,154 @@ pub struct AdminEditPageProps {
#[derive(Properties, PartialEq, Clone)]
pub struct ProjectFieldsProps {
pub listing: Option<Listing>
pub listing: Option<Listing>,
}
#[allow(unused)]
#[function_component(ProjectFields)]
pub fn generate_fields_for_project(props: &ProjectFieldsProps) -> Html {
let user_typed = use_state(|| false);
let listing_opt = props.listing.clone();
let location_city = use_state(|| listing_opt.clone().unwrap_or_default().location.city);
let location_district = use_state(|| listing_opt.clone().unwrap_or_default().location.district);
let agent: UseStateHandle<Option<Agent>> = use_state(|| {
match listing_opt.clone() {
let location_city = use_state_eq(|| String::new());
let location_district = use_state_eq(|| String::new());
let agent: UseStateHandle<Option<Agent>> = use_state_eq(|| None);
let project_state = use_state_eq(|| None);
let project_condition = use_state_eq(|| None);
let project_type = use_state_eq(|| None);
let project_description = use_state_eq(|| "".to_string());
let project_admin_tag: UseStateHandle<String> = use_state_eq(|| listing_opt.clone().unwrap_or_default().project.admin_tag.unwrap_or(String::new()));
let project_finish_date = use_state_eq(|| listing_opt.clone().unwrap_or_default().project.finish_date);
let project_floors = use_state_eq(|| listing_opt.clone().unwrap_or_default().project.floors.to_string());
let media: UseStateHandle<MediaList> = use_state_eq(|| listing_opt.clone().unwrap_or_default().project.media);
let units: UseStateHandle<Vec<Unit>> = use_state_eq(|| listing_opt.clone().unwrap_or_default().units);
let ontype_cb = {
let user_typed = user_typed.clone();
Callback::from(move |_| {
user_typed.set(true);
})
};
let onselect_cb = {
let user_typed = user_typed.clone();
Callback::from(move |_| {
user_typed.set(true);
})
};
let onselect_cb2 = {
let user_typed = user_typed.clone();
Callback::from(move |_| {
user_typed.set(true);
})
};
let onselect_cb3 = {
let user_typed = user_typed.clone();
Callback::from(move |_| {
user_typed.set(true);
})
};
let onselect_cb4 = {
let user_typed = user_typed.clone();
Callback::from(move |_| {
user_typed.set(true);
})
};
let all_agents = use_state(|| Vec::new());
use_state(|| {
let all_agents = all_agents.clone();
wasm_bindgen_futures::spawn_local(async move {
match get_all_agents().await {
Ok(persisted_agents) => all_agents.set(persisted_agents),
Err(error) => log::error!("Error fetching agents from admin panel edit screen: {error}"),
};
})
});
if !*user_typed {
location_city.set(listing_opt.clone().unwrap_or_default().location.city);
location_district.set(listing_opt.clone().unwrap_or_default().location.district);
agent.set(match listing_opt.clone() {
Some(listing) => Some(listing.agent),
None => None,
}
});
let project_state = use_state(|| listing_opt.clone().unwrap_or_default().project.project_state);
let project_condition = use_state(|| listing_opt.clone().unwrap_or_default().project.project_condition);
let project_type = use_state(|| listing_opt.clone().unwrap_or_default().project.project_type);
let project_description = use_state(|| listing_opt.clone().unwrap_or_default().project.description);
let project_admin_tag: UseStateHandle<Option<String>> = use_state(|| listing_opt.clone().unwrap_or_default().project.admin_tag);
let project_finish_date = use_state(|| listing_opt.clone().unwrap_or_default().project.finish_date);
let project_floors = use_state(|| listing_opt.clone().unwrap_or_default().project.floors);
let media: UseStateHandle<MediaList> = use_state(|| listing_opt.clone().unwrap_or_default().project.media);
let units: UseStateHandle<Vec<Unit>> = use_state(|| listing_opt.clone().unwrap_or_default().units);
});
project_state.set(match listing_opt.clone() {
Some(listing) => Some(listing.project.project_state),
None => None,
});
project_condition.set(match listing_opt.clone() {
Some(listing) => Some(listing.project.project_condition),
None => None,
});
project_type.set(match listing_opt.clone() {
Some(listing) => Some(listing.project.project_type),
None => None,
});
project_description.set(match listing_opt.clone() {
Some(listing) => listing.project.description,
None => "".into(),
});
project_floors.set(match listing_opt.clone() {
Some(listing) => listing.project.floors.to_string(),
None => "".into(),
});
project_admin_tag.set(match listing_opt.clone() {
Some(listing) => match listing.project.admin_tag {
Some(admin_tag) => admin_tag,
None => String::new(),
},
None => String::new(),
});
}
html! {
<>
<TextField label={"Ciudad"} value={location_city} required={true} onchange={ontype_cb.clone()}/>
<TextField label={"Distrito"} value={location_district} required={true} onchange={ontype_cb.clone()} />
{if (*agent).clone().is_none() { html! { } } else {
html! {
<div class={"textfield-container"}>
<div class={"textfield-label-required"}>{"Agente asignado"}</div>
<DropDown<Agent> options={(*all_agents).clone()} selected={agent} onchange={onselect_cb.clone()} />
</div>
}
}
}
{if (*project_state).clone().is_none() { html! { } } else {
html! {
<div class={"textfield-container"}>
<div class={"textfield-label-required"}>{"Estado del Proyecto"}</div>
<DropDown<ProjectState> options={vec![ProjectState::Finished, ProjectState::InConstruction]} selected={project_state} onchange={onselect_cb2} />
</div>
}
}
}
{if (*project_condition).clone().is_none() { html! { } } else {
html! {
<div class={"textfield-container"}>
<div class={"textfield-label-required"}>{"Condición del Proyecto"}</div>
<DropDown<ProjectCondition> options={vec![ProjectCondition::Resale, ProjectCondition::New]} selected={project_condition} onchange={onselect_cb3} />
</div>
}
}
}
{if (*project_type).clone().is_none() { html! { } } else {
html! {
<div class={"textfield-container"}>
<div class={"textfield-label-required"}>{"Tipo de Proyecto"}</div>
<DropDown<ProjectType> options={vec![ProjectType::Apartamento, ProjectType::Casa, ProjectType::Oficina, ProjectType::Local, ProjectType::Solar]} selected={project_type} onchange={onselect_cb4} />
</div>
}
}
}
<TextField label={"Pisos"} value={project_floors} required={true} />
<TextField label={"Descripción"} value={project_description} fieldtype={TextFieldType::TextArea}/>
<TextField label={"Comentario interno"} value={project_admin_tag} />
</>
}
}

View File

@ -17,7 +17,7 @@ pub fn search_page() -> Html {
let finished_loading = use_state(|| false);
let project_state_filter_handle = use_state_eq(|| ProjectState::InConstruction);
let filters = Vec::new();
let filters: Vec<Filter> = Vec::new();
// All code to execute on first render and never again
use_state(|| {
@ -34,7 +34,7 @@ pub fn search_page() -> Html {
},
Err(error) => log::error!("Error in loading cities: {error}")
};
match get_all_projects_with_filters_paged(&(*page_counter), filters).await {
match get_all_projects_with_filters_paged(&(*page_counter), vec![Filter::InConstruction]).await {
Ok(projects) => {
search_results_handle.set(projects);
finished_loading.set(true);
@ -165,6 +165,8 @@ pub fn search_page() -> Html {
let mut filters = Vec::new();
if (*project_state_filter).eq(&ProjectState::Finished) {
filters.push(Filter::Finished);
} else {
filters.push(Filter::InConstruction);
}
match &(*project_type_filter).option {
Some(project_type) => filters.push(Filter::ByProjectType(project_type.clone())),