Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions backend/models/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ def to_dict(self):
)

data["inventory"] = inventory_data
data["inventory_id"] = self.inventory.id if self.inventory else None

return data

Expand Down
5 changes: 3 additions & 2 deletions backend/routes/b2b.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,8 +71,9 @@ def delete(self, gb_id):
@b2b_ns.route("/offers")
class OfferList(Resource):
def get(self):
"""List all direct B2B stock offers."""
return {"offers": get_all_offers()}, 200
"""List direct B2B stock offers, optionally filtered by producer_id."""
producer_id = request.args.get("producer_id", type=int)
return {"offers": get_all_offers(producer_id=producer_id)}, 200

def post(self):
"""Submit a new offer from a retailer to a producer."""
Expand Down
7 changes: 3 additions & 4 deletions backend/routes/orders.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,9 @@
@orders_ns.route("")
class OrderList(Resource):
def get(self):
"""
List all inventory orders.
"""
return {"orders": get_all_orders()}, 200
"""List all inventory orders, optionally filtered by retailer_id."""
retailer_id = request.args.get("retailer_id", type=int)
return {"orders": get_all_orders(retailer_id=retailer_id)}, 200

@orders_ns.expect(order_model)
def post(self):
Expand Down
29 changes: 24 additions & 5 deletions backend/services/b2b_service.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
from database import db
from models.b2b import BuyStockOffer, GroupBuy
from models.catalog import Item
from models.user import Retailer


def get_all_group_buys():
Expand Down Expand Up @@ -55,11 +57,28 @@ def delete_group_buy(gb_id):
return True


def get_all_offers():
offers = BuyStockOffer.query.all()
return [
{"id": o.id, "retailer_id": o.retailer_id, "producer_id": o.producer_id, "status": o.status} for o in offers
]
def _serialize_offer(o):
retailer = db.session.get(Retailer, o.retailer_id)
item = db.session.get(Item, o.item_id)
return {
"id": o.id,
"retailer_id": o.retailer_id,
"retailer_name": retailer.company_name if retailer else None,
"producer_id": o.producer_id,
"item_id": o.item_id,
"item_name": item.name if item else None,
"item_unit": item.unit_type if item else None,
"offered_price": float(o.offered_price),
"requested_quantity": float(o.requested_quantity),
"status": o.status,
}


def get_all_offers(producer_id=None):
q = BuyStockOffer.query
if producer_id is not None:
q = q.filter(BuyStockOffer.producer_id == producer_id)
return [_serialize_offer(o) for o in q.all()]


def get_offer(offer_id):
Expand Down
17 changes: 14 additions & 3 deletions backend/services/order_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,21 @@
from models.orders import InventoryOrder, OrderItem


def get_all_orders():
orders = InventoryOrder.query.all()
def get_all_orders(retailer_id=None):
q = InventoryOrder.query
if retailer_id is not None:
q = q.filter(InventoryOrder.retailer_id == retailer_id)
orders = q.order_by(InventoryOrder.order_date.desc()).all()
return [
{"id": o.id, "consumer_id": o.consumer_id, "total_amount": float(o.total_amount), "status": o.status}
{
"id": o.id,
"consumer_id": o.consumer_id,
"retailer_id": o.retailer_id,
"total_amount": float(o.total_amount),
"status": o.status,
"order_date": o.order_date.isoformat() if o.order_date else None,
"item_count": len(o.order_items),
}
for o in orders
]

Expand Down
1 change: 1 addition & 0 deletions backend/services/user_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ def get_all_users(user_type=None):
"""
if user_type == "producer":
users = Producer.query.options(selectinload(Producer.items).selectinload(Item.prices)).all()
return [u.to_dict() for u in users]
elif user_type == "retailer":
users = Retailer.query.all()
elif user_type == "consumer":
Expand Down
230 changes: 43 additions & 187 deletions frontend/src/components/ProducerDashboard/ProducerDashboard.css
Original file line number Diff line number Diff line change
@@ -1,216 +1,72 @@
.producer-dashboard {
padding: 2rem;
max-width: 98vw;
width: 100%;
box-sizing: border-box;
}

.dashboard-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 2rem;
}

.dashboard-header h1 {
font-family: 'Playfair Display', serif;
color: #3e2f1c;
margin: 0 0 0.25rem;
}

.dashboard-subtitle {
color: #7a5c3e;
font-size: 0.9rem;
margin: 0;
}

.dashboard-stats {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 1rem;
margin-bottom: 2rem;
}

.stat-card {
background: #fff;
border: 1px solid #d4c4a8;
border-radius: 8px;
padding: 1.25rem 1.5rem;
display: flex;
flex-direction: column;
gap: 0.25rem;
}

.stat-card.warn {
border-color: #c1694f;
}

.stat-number {
font-size: 2rem;
font-weight: 700;
color: #3e2f1c;
font-family: 'Playfair Display', serif;
}

.stat-card.warn .stat-number {
color: #c1694f;
}

.stat-label {
font-size: 0.85rem;
color: #7a5c3e;
}

.dashboard-section {
background: #fff;
border: 1px solid #d4c4a8;
border-radius: 8px;
padding: 1.5rem;
}

.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1rem;
}

.section-header h2 {
font-family: 'Playfair Display', serif;
color: #3e2f1c;
margin: 0;
font-size: 1.2rem;
}
@import '../dashboard-shared.css';

/* ── Stock list (uses .inventory-row from Inventory.css via StockRow) ── */
.stock-list {
display: flex;
flex-direction: column;
gap: 0.2rem;
width: 100%;
}

.stock-card {
display: flex;
/* ── Offer rows ── */
.offer-row {
display: grid;
grid-template-columns: minmax(0, 2fr) minmax(0, 1.5fr) 6rem 8rem 7rem 10rem;
align-items: center;
gap: 0.6rem;
padding: 0.3rem 0.7rem;
background: #faf6ef;
border-radius: 5px;
border-left: 3px solid #4a7c59;
min-height: 2.2rem;
}

.stock-card:hover {
border-left-color: #7a5c3e;
}

.stock-card-main {
display: flex;
align-items: baseline;
gap: 0.5rem;
flex: 1;
min-width: 0;
padding: 0.3em 0.7em;
border-left: 3px solid #c4a882;
border-radius: 4px;
background-color: #faf6ef;
min-height: 2.2em;
font-size: 0.9rem;
}

.stock-name {
.offer-row-header {
font-weight: 600;
font-size: 0.88rem;
color: #3e2f1c;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
font-size: 0.8rem;
color: #6b5744;
border-left-color: transparent;
background: transparent;
padding-bottom: 0.2em;
}

.stock-sku {
.offer-status {
font-size: 0.75rem;
color: #7a5c3e;
white-space: nowrap;
flex-shrink: 0;
}

.stock-card-meta {
display: flex;
gap: 1rem;
align-items: center;
font-size: 0.82rem;
color: #7a5c3e;
flex-shrink: 0;
}

.stock-qty {
font-weight: 600;
color: #4a7c59;
white-space: nowrap;
text-transform: uppercase;
padding: 0.15em 0.5em;
border-radius: 999px;
display: inline-block;
}

.stock-qty.low {
color: #c1694f;
}

.stock-expiry {
white-space: nowrap;
}

.stock-price {
color: #3e2f1c;
white-space: nowrap;
}
.offer-status--pending { background: #fff3cd; color: #856404; }
.offer-status--accepted { background: #d1e7dd; color: #0f5132; }
.offer-status--rejected { background: #f8d7da; color: #842029; }

.empty-state {
text-align: center;
padding: 2rem;
color: #7a5c3e;
.offer-actions {
display: flex;
gap: 0.4rem;
}

.btn-primary {
.btn-accept {
padding: 0.2em 0.7em;
background: #4a7c59;
color: #fff;
border: none;
padding: 0.6rem 1.25rem;
border-radius: 6px;
font-family: 'Nunito', sans-serif;
font-size: 0.95rem;
border-radius: 4px;
cursor: pointer;
font-size: 0.8rem;
}

.btn-primary:hover {
background: #3a6347;
}

.btn-link {
background: none;
border: none;
color: #4a7c59;
.btn-reject {
padding: 0.2em 0.7em;
background: transparent;
color: #842029;
border: 1px solid #842029;
border-radius: 4px;
cursor: pointer;
font-size: 0.9rem;
padding: 0;
font-family: 'Nunito', sans-serif;
}

.dashboard-loading,
.dashboard-error {
text-align: center;
padding: 2rem;
color: #7a5c3e;
}

.dashboard-error {
color: #c1694f;
font-size: 0.8rem;
}

.modal-back-btn {
display: flex;
align-items: center;
gap: 0.3em;
background: none;
border: none;
cursor: pointer;
color: var(--farm-text-muted, #7a5c3e);
font-size: 0.9rem;
font-family: var(--brand-sans);
padding: 0;
margin-bottom: 0.8em;
}

.modal-back-btn:hover {
color: var(--text-color, #3e2f1c);
}
.btn-accept:hover { background: #3a6147; }
.btn-reject:hover { background: #f8d7da; }
Loading
Loading