-
Notifications
You must be signed in to change notification settings - Fork 342
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #235 from reflex-dev/carlos/improve-examples
update customer data app
- Loading branch information
Showing
37 changed files
with
2,114 additions
and
897 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
"""empty message | ||
Revision ID: 277cad49d2b0 | ||
Revises: 7aaec6b87d88 | ||
Create Date: 2024-05-30 10:58:18.235598 | ||
""" | ||
from typing import Sequence, Union | ||
|
||
from alembic import op | ||
import sqlalchemy as sa | ||
import sqlmodel | ||
|
||
# revision identifiers, used by Alembic. | ||
revision: str = '277cad49d2b0' | ||
down_revision: Union[str, None] = '7aaec6b87d88' | ||
branch_labels: Union[str, Sequence[str], None] = None | ||
depends_on: Union[str, Sequence[str], None] = None | ||
|
||
|
||
def upgrade() -> None: | ||
# ### commands auto generated by Alembic - please adjust! ### | ||
with op.batch_alter_table('customer', schema=None) as batch_op: | ||
batch_op.add_column(sa.Column('date', sqlmodel.sql.sqltypes.AutoString(), nullable=False)) | ||
|
||
# ### end Alembic commands ### | ||
|
||
|
||
def downgrade() -> None: | ||
# ### commands auto generated by Alembic - please adjust! ### | ||
with op.batch_alter_table('customer', schema=None) as batch_op: | ||
batch_op.drop_column('date') | ||
|
||
# ### end Alembic commands ### |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
"""empty message | ||
Revision ID: 7aaec6b87d88 | ||
Revises: e565fdc23e6c | ||
Create Date: 2024-05-30 09:50:05.149862 | ||
""" | ||
from typing import Sequence, Union | ||
|
||
from alembic import op | ||
import sqlalchemy as sa | ||
import sqlmodel | ||
|
||
# revision identifiers, used by Alembic. | ||
revision: str = '7aaec6b87d88' | ||
down_revision: Union[str, None] = 'e565fdc23e6c' | ||
branch_labels: Union[str, Sequence[str], None] = None | ||
depends_on: Union[str, Sequence[str], None] = None | ||
|
||
|
||
def upgrade() -> None: | ||
# ### commands auto generated by Alembic - please adjust! ### | ||
with op.batch_alter_table('customer', schema=None) as batch_op: | ||
batch_op.add_column(sa.Column('payments', sa.Float(), nullable=False)) | ||
batch_op.add_column(sa.Column('status', sqlmodel.sql.sqltypes.AutoString(), nullable=False)) | ||
|
||
# ### end Alembic commands ### | ||
|
||
|
||
def downgrade() -> None: | ||
# ### commands auto generated by Alembic - please adjust! ### | ||
with op.batch_alter_table('customer', schema=None) as batch_op: | ||
batch_op.drop_column('status') | ||
batch_op.drop_column('payments') | ||
|
||
# ### end Alembic commands ### |
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,183 @@ | ||
import reflex as rx | ||
from typing import Literal, Union | ||
from sqlmodel import select, asc, desc, or_, func | ||
from datetime import datetime, timedelta | ||
|
||
#LiteralStatus = Literal["Delivered", "Pending", "Cancelled"] | ||
|
||
|
||
def _get_percentage_change(value: Union[int, float], prev_value: Union[int, float]) -> float: | ||
percentage_change = ( | ||
round(((value - prev_value) / prev_value) * 100, 2) | ||
if prev_value != 0 | ||
else 0 | ||
if value == 0 | ||
else | ||
float("inf") | ||
) | ||
return percentage_change | ||
|
||
class Customer(rx.Model, table=True): | ||
"""The customer model.""" | ||
|
||
name: str | ||
email: str | ||
phone: str | ||
address: str | ||
date: str | ||
payments: float | ||
status: str | ||
|
||
|
||
class MonthValues(rx.Base): | ||
"""Values for a month.""" | ||
|
||
num_customers: int = 0 | ||
total_payments: float = 0.0 | ||
num_delivers: int = 0 | ||
|
||
|
||
|
||
class State(rx.State): | ||
"""The app state.""" | ||
|
||
users: list[Customer] = [] | ||
sort_value: str = "" | ||
sort_reverse: bool = False | ||
search_value: str = "" | ||
current_user: Customer = Customer() | ||
# Values for current and previous month | ||
current_month_values: MonthValues = MonthValues() | ||
previous_month_values: MonthValues = MonthValues() | ||
|
||
|
||
def load_entries(self) -> list[Customer]: | ||
"""Get all users from the database.""" | ||
with rx.session() as session: | ||
query = select(Customer) | ||
if self.search_value: | ||
search_value = f"%{str(self.search_value).lower()}%" | ||
query = query.where( | ||
or_( | ||
*[ | ||
getattr(Customer, field).ilike(search_value) | ||
for field in Customer.get_fields() | ||
], | ||
) | ||
) | ||
|
||
if self.sort_value: | ||
sort_column = getattr(Customer, self.sort_value) | ||
if self.sort_value == "payments": | ||
order = desc(sort_column) if self.sort_reverse else asc(sort_column) | ||
else: | ||
order = desc(func.lower(sort_column)) if self.sort_reverse else asc(func.lower(sort_column)) | ||
query = query.order_by(order) | ||
|
||
self.users = session.exec(query).all() | ||
|
||
self.get_current_month_values() | ||
self.get_previous_month_values() | ||
|
||
|
||
def get_current_month_values(self): | ||
"""Calculate current month's values.""" | ||
now = datetime.now() | ||
start_of_month = datetime(now.year, now.month, 1) | ||
|
||
current_month_users = [ | ||
user for user in self.users if datetime.strptime(user.date, '%Y-%m-%d %H:%M:%S') >= start_of_month | ||
] | ||
num_customers = len(current_month_users) | ||
total_payments = sum(user.payments for user in current_month_users) | ||
num_delivers = len([user for user in current_month_users if user.status == "Delivered"]) | ||
self.current_month_values = MonthValues(num_customers=num_customers, total_payments=total_payments, num_delivers=num_delivers) | ||
|
||
|
||
def get_previous_month_values(self): | ||
"""Calculate previous month's values.""" | ||
now = datetime.now() | ||
first_day_of_current_month = datetime(now.year, now.month, 1) | ||
last_day_of_last_month = first_day_of_current_month - timedelta(days=1) | ||
start_of_last_month = datetime(last_day_of_last_month.year, last_day_of_last_month.month, 1) | ||
|
||
previous_month_users = [ | ||
user for user in self.users | ||
if start_of_last_month <= datetime.strptime(user.date, '%Y-%m-%d %H:%M:%S') <= last_day_of_last_month | ||
] | ||
# We add some dummy values to simulate growth/decline. Remove them in production. | ||
num_customers = len(previous_month_users) + 3 | ||
total_payments = sum(user.payments for user in previous_month_users) + 240 | ||
num_delivers = len([user for user in previous_month_users if user.status == "Delivered"]) + 5 | ||
|
||
self.previous_month_values = MonthValues(num_customers=num_customers, total_payments=total_payments, num_delivers=num_delivers) | ||
|
||
|
||
def sort_values(self, sort_value: str): | ||
self.sort_value = sort_value | ||
self.load_entries() | ||
|
||
|
||
def toggle_sort(self): | ||
self.sort_reverse = not self.sort_reverse | ||
self.load_entries() | ||
|
||
def filter_values(self, search_value): | ||
self.search_value = search_value | ||
self.load_entries() | ||
|
||
def get_user(self, user: Customer): | ||
self.current_user = user | ||
|
||
|
||
def add_customer_to_db(self, form_data: dict): | ||
self.current_user = form_data | ||
self.current_user["date"] = datetime.now().strftime("%Y-%m-%d %H:%M:%S") | ||
|
||
with rx.session() as session: | ||
if session.exec( | ||
select(Customer).where(Customer.email == self.current_user["email"]) | ||
).first(): | ||
return rx.window_alert("User with this email already exists") | ||
session.add(Customer(**self.current_user)) | ||
session.commit() | ||
self.load_entries() | ||
return rx._x.toast.info(f"User {self.current_user["name"]} has been added.", variant="outline", position="bottom-right") | ||
|
||
|
||
def update_customer_to_db(self, form_data: dict): | ||
self.current_user.update(form_data) | ||
with rx.session() as session: | ||
customer = session.exec( | ||
select(Customer).where(Customer.id == self.current_user["id"]) | ||
).first() | ||
for field in Customer.get_fields(): | ||
if field != "id": | ||
setattr(customer, field, self.current_user[field]) | ||
session.add(customer) | ||
session.commit() | ||
self.load_entries() | ||
return rx._x.toast.info(f"User {self.current_user["name"]} has been modified.", variant="outline", position="bottom-right") | ||
|
||
|
||
def delete_customer(self, id: int): | ||
"""Delete a customer from the database.""" | ||
with rx.session() as session: | ||
customer = session.exec(select(Customer).where(Customer.id == id)).first() | ||
session.delete(customer) | ||
session.commit() | ||
self.load_entries() | ||
return rx._x.toast.info(f"User {customer.name} has been deleted.", variant="outline", position="bottom-right") | ||
|
||
|
||
@rx.var | ||
def payments_change(self) -> float: | ||
return _get_percentage_change(self.current_month_values.total_payments, self.previous_month_values.total_payments) | ||
|
||
@rx.var | ||
def customers_change(self) -> float: | ||
return _get_percentage_change(self.current_month_values.num_customers, self.previous_month_values.num_customers) | ||
|
||
@rx.var | ||
def delivers_change(self) -> float: | ||
return _get_percentage_change(self.current_month_values.num_delivers, self.previous_month_values.num_delivers) |
Empty file.
26 changes: 26 additions & 0 deletions
26
customer_data_app/customer_data_app/components/form_field.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
import reflex as rx | ||
|
||
|
||
def form_field( | ||
label: str, placeholder: str, type: str, name: str, icon: str, default_value: str = "" | ||
) -> rx.Component: | ||
return rx.form.field( | ||
rx.flex( | ||
rx.hstack( | ||
rx.icon(icon, size=16, stroke_width=1.5), | ||
rx.form.label(label), | ||
align="center", | ||
spacing="2", | ||
), | ||
rx.form.control( | ||
rx.input( | ||
placeholder=placeholder, type=type, default_value=default_value | ||
), | ||
as_child=True, | ||
), | ||
direction="column", | ||
spacing="1", | ||
), | ||
name=name, | ||
width="100%", | ||
) |
110 changes: 110 additions & 0 deletions
110
customer_data_app/customer_data_app/components/stats_cards.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,110 @@ | ||
import reflex as rx | ||
from reflex.components.radix.themes.base import ( | ||
LiteralAccentColor, | ||
) | ||
|
||
from ..backend.backend import State | ||
|
||
|
||
def _arrow_badge(arrow_icon: str, percentage_change: float, arrow_color: str): | ||
return rx.badge( | ||
rx.icon( | ||
tag=arrow_icon, | ||
color=rx.color(arrow_color, 9), | ||
), | ||
rx.text( | ||
f"{percentage_change}%", | ||
size="2", | ||
color=rx.color(arrow_color, 9), | ||
weight="medium", | ||
), | ||
color_scheme=arrow_color, | ||
radius="large", | ||
align_items="center", | ||
) | ||
|
||
def stats_card(stat_name: str, | ||
value: int, | ||
prev_value: int, | ||
percentage_change: float, | ||
icon: str, | ||
icon_color: LiteralAccentColor, | ||
extra_char: str = "") -> rx.Component: | ||
return rx.card( | ||
rx.hstack( | ||
rx.vstack( | ||
rx.hstack( | ||
rx.hstack( | ||
rx.icon( | ||
tag=icon, | ||
size=22, | ||
color=rx.color(icon_color, 11), | ||
), | ||
rx.text( | ||
stat_name, | ||
size="4", | ||
weight="medium", | ||
color=rx.color("gray", 11), | ||
), | ||
spacing="2", | ||
align="center", | ||
), | ||
rx.cond( | ||
value > prev_value, | ||
_arrow_badge("trending-up", percentage_change, "grass"), | ||
_arrow_badge("trending-down", percentage_change, "tomato"), | ||
), | ||
justify="between", | ||
width="100%", | ||
), | ||
rx.hstack( | ||
rx.heading( | ||
f"{extra_char}{value:,}", | ||
size="7", | ||
weight="bold", | ||
), | ||
rx.text( | ||
f"from {extra_char}{prev_value:,}", | ||
size="3", | ||
color=rx.color("gray", 10), | ||
), | ||
spacing="2", | ||
align_items="end", | ||
), | ||
align_items="start", | ||
justify="between", | ||
width="100%", | ||
), | ||
align_items="start", | ||
width="100%", | ||
justify="between", | ||
), | ||
size="3", | ||
width="100%", | ||
max_width="22rem", | ||
) | ||
|
||
|
||
def stats_cards_group() -> rx.Component: | ||
return rx.flex( | ||
stats_card("Total Customers", | ||
State.current_month_values.num_customers, | ||
State.previous_month_values.num_customers, | ||
State.customers_change, | ||
"users", "blue"), | ||
stats_card("Total Payments", | ||
State.current_month_values.total_payments, | ||
State.previous_month_values.total_payments, | ||
State.payments_change, | ||
"dollar-sign", "orange", | ||
"$"), | ||
stats_card("Total Delivers", | ||
State.current_month_values.num_delivers, | ||
State.previous_month_values.num_delivers, | ||
State.delivers_change, | ||
"truck", "ruby"), | ||
spacing="5", | ||
width="100%", | ||
wrap="wrap", | ||
display=["none", "none", "flex"], | ||
) |
Oops, something went wrong.