Edit project screen done
This commit is contained in:
parent
c693287063
commit
39abe4caca
@ -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;
|
||||
}
|
||||
|
44
css/components/textfield.css
Normal file
44
css/components/textfield.css
Normal 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;
|
||||
}
|
13
css/edit.css
13
css/edit.css
@ -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;
|
||||
}
|
@ -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" />
|
||||
|
51
src/components/dropdown.rs
Normal file
51
src/components/dropdown.rs
Normal 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
|
||||
}
|
@ -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;
|
80
src/components/textfield.rs
Normal file
80
src/components/textfield.rs
Normal 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()
|
||||
}
|
@ -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} />
|
||||
|
||||
</>
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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())),
|
||||
|
Loading…
Reference in New Issue
Block a user