diff --git a/core/__pycache__/admin.cpython-311.pyc b/core/__pycache__/admin.cpython-311.pyc index 5e8987a..8f6f021 100644 Binary files a/core/__pycache__/admin.cpython-311.pyc and b/core/__pycache__/admin.cpython-311.pyc differ diff --git a/core/__pycache__/context_processors.cpython-311.pyc b/core/__pycache__/context_processors.cpython-311.pyc index 75bf223..2f0013b 100644 Binary files a/core/__pycache__/context_processors.cpython-311.pyc and b/core/__pycache__/context_processors.cpython-311.pyc differ diff --git a/core/__pycache__/models.cpython-311.pyc b/core/__pycache__/models.cpython-311.pyc index a251b5f..aa04515 100644 Binary files a/core/__pycache__/models.cpython-311.pyc and b/core/__pycache__/models.cpython-311.pyc differ diff --git a/core/__pycache__/urls.cpython-311.pyc b/core/__pycache__/urls.cpython-311.pyc index f705988..c0a7488 100644 Binary files a/core/__pycache__/urls.cpython-311.pyc and b/core/__pycache__/urls.cpython-311.pyc differ diff --git a/core/__pycache__/views.cpython-311.pyc b/core/__pycache__/views.cpython-311.pyc index 2f0989c..795ffaf 100644 Binary files a/core/__pycache__/views.cpython-311.pyc and b/core/__pycache__/views.cpython-311.pyc differ diff --git a/core/admin.py b/core/admin.py index 8c38f3f..bfae149 100644 --- a/core/admin.py +++ b/core/admin.py @@ -1,3 +1,8 @@ from django.contrib import admin +from .models import UserProfile, Category, Product, Order, OrderItem -# Register your models here. +admin.site.register(UserProfile) +admin.site.register(Category) +admin.site.register(Product) +admin.site.register(Order) +admin.site.register(OrderItem) \ No newline at end of file diff --git a/core/migrations/0001_initial.py b/core/migrations/0001_initial.py new file mode 100644 index 0000000..6221d50 --- /dev/null +++ b/core/migrations/0001_initial.py @@ -0,0 +1,67 @@ +# Generated by Django 5.2.7 on 2026-03-13 07:23 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='Category', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=100)), + ('slug', models.SlugField(unique=True)), + ], + ), + migrations.CreateModel( + name='Order', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('status', models.CharField(choices=[('pending', 'Pending'), ('shipped', 'Shipped'), ('delivered', 'Delivered')], default='pending', max_length=20)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('buyer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='orders', to=settings.AUTH_USER_MODEL)), + ], + ), + migrations.CreateModel( + name='Product', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=200)), + ('description', models.TextField()), + ('price', models.DecimalField(decimal_places=2, max_digits=10)), + ('stock', models.IntegerField(default=0)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('category', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='products', to='core.category')), + ('seller', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='products', to=settings.AUTH_USER_MODEL)), + ], + ), + migrations.CreateModel( + name='OrderItem', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('quantity', models.IntegerField()), + ('price', models.DecimalField(decimal_places=2, max_digits=10)), + ('order', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='items', to='core.order')), + ('product', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='core.product')), + ], + ), + migrations.CreateModel( + name='UserProfile', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('is_seller', models.BooleanField(default=False)), + ('phone_number', models.CharField(blank=True, max_length=20)), + ('address', models.TextField(blank=True)), + ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + ), + ] diff --git a/core/migrations/__pycache__/0001_initial.cpython-311.pyc b/core/migrations/__pycache__/0001_initial.cpython-311.pyc new file mode 100644 index 0000000..bb0650e Binary files /dev/null and b/core/migrations/__pycache__/0001_initial.cpython-311.pyc differ diff --git a/core/models.py b/core/models.py index 71a8362..67804bc 100644 --- a/core/models.py +++ b/core/models.py @@ -1,3 +1,47 @@ from django.db import models +from django.contrib.auth.models import User -# Create your models here. +class UserProfile(models.Model): + user = models.OneToOneField(User, on_delete=models.CASCADE) + is_seller = models.BooleanField(default=False) + phone_number = models.CharField(max_length=20, blank=True) + address = models.TextField(blank=True) + + def __str__(self): + return self.user.username + +class Category(models.Model): + name = models.CharField(max_length=100) + slug = models.SlugField(unique=True) + + def __str__(self): + return self.name + +class Product(models.Model): + seller = models.ForeignKey(User, on_delete=models.CASCADE, related_name='products') + category = models.ForeignKey(Category, on_delete=models.SET_NULL, null=True, related_name='products') + name = models.CharField(max_length=200) + description = models.TextField() + price = models.DecimalField(max_digits=10, decimal_places=2) + stock = models.IntegerField(default=0) + created_at = models.DateTimeField(auto_now_add=True) + + def __str__(self): + return self.name + +class Order(models.Model): + buyer = models.ForeignKey(User, on_delete=models.CASCADE, related_name='orders') + status = models.CharField(max_length=20, choices=[('pending', 'Pending'), ('shipped', 'Shipped'), ('delivered', 'Delivered')], default='pending') + created_at = models.DateTimeField(auto_now_add=True) + + def __str__(self): + return f"Order {self.id} by {self.buyer.username}" + +class OrderItem(models.Model): + order = models.ForeignKey(Order, on_delete=models.CASCADE, related_name='items') + product = models.ForeignKey(Product, on_delete=models.CASCADE) + quantity = models.IntegerField() + price = models.DecimalField(max_digits=10, decimal_places=2) + + def __str__(self): + return f"{self.quantity} x {self.product.name} in Order {self.order.id}" \ No newline at end of file diff --git a/core/templates/core/buyer_dashboard.html b/core/templates/core/buyer_dashboard.html new file mode 100644 index 0000000..4d3cd16 --- /dev/null +++ b/core/templates/core/buyer_dashboard.html @@ -0,0 +1,38 @@ +{% extends "core/dashboard_base.html" %} + +{% block title %}Buyer Dashboard | Ethio-gebeya{% endblock %} + +{% block dashboard_content %} +
+
+

Total Orders

+

{{ total_orders }}

+
+
+ +
+

Recent Orders

+ {% if recent_orders %} + + + + + + + + + + {% for order in recent_orders %} + + + + + + {% endfor %} + +
OrderStatusDate
#{{ order.id }}{{ order.get_status_display }}{{ order.created_at|date:"Y-m-d" }}
+ {% else %} +

No orders yet.

+ {% endif %} +
+{% endblock %} diff --git a/core/templates/core/dashboard_base.html b/core/templates/core/dashboard_base.html new file mode 100644 index 0000000..94e0f1d --- /dev/null +++ b/core/templates/core/dashboard_base.html @@ -0,0 +1,159 @@ +{% extends "base.html" %} + +{% block head %} + +{% endblock %} + +{% block content %} +
+ +
+
+

{{ dashboard_title }}

+ Sign Out +
+ {% block dashboard_content %}{% endblock %} +
+
+{% endblock %} diff --git a/core/templates/core/index.html b/core/templates/core/index.html index faec813..5a817bd 100644 --- a/core/templates/core/index.html +++ b/core/templates/core/index.html @@ -1,18 +1,17 @@ {% extends "base.html" %} -{% block title %}{{ project_name }}{% endblock %} +{% block title %}Ethio-gebeya | Marketplace{% endblock %} {% block head %} - - - {% endblock %} {% block content %}
-
-

Analyzing your requirements and generating your app…

-
- Loading… +
+

Welcome to Ethio-gebeya

+

A modern marketplace foundation with separate dashboards for buyers and sellers.

+
+ {% if user.is_authenticated %} + Open Dashboard + Sign Out + {% else %} + Sign In + {% endif %}
-

AppWizzy AI is collecting your requirements and applying the first changes.

-

This page will refresh automatically as the plan is implemented.

-

- Runtime: Django {{ django_version }} · Python {{ python_version }} - — UTC {{ current_time|date:"Y-m-d H:i:s" }} -

-
+

Runtime: Django {{ django_version }} · Python {{ python_version }} · {{ current_time|date:"Y-m-d H:i:s" }} UTC

+
- -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/core/templates/core/login.html b/core/templates/core/login.html new file mode 100644 index 0000000..eaeab59 --- /dev/null +++ b/core/templates/core/login.html @@ -0,0 +1,67 @@ +{% extends "base.html" %} + +{% block title %}Sign In | Ethio-gebeya{% endblock %} + +{% block head %} + +{% endblock %} + +{% block content %} +
+
+

Sign in

+
+ {% csrf_token %} + {{ form.as_p }} + +
+
+
+{% endblock %} diff --git a/core/templates/core/seller_dashboard.html b/core/templates/core/seller_dashboard.html new file mode 100644 index 0000000..0dcb70e --- /dev/null +++ b/core/templates/core/seller_dashboard.html @@ -0,0 +1,48 @@ +{% extends "core/dashboard_base.html" %} + +{% block title %}Seller Dashboard | Ethio-gebeya{% endblock %} + +{% block dashboard_content %} +
+
+

Products

+

{{ product_count }}

+
+
+

Orders

+

{{ total_orders }}

+
+
+

Revenue

+

${{ total_revenue }}

+
+
+ +
+

Recent Sales

+ {% if recent_sales %} + + + + + + + + + + + {% for sale in recent_sales %} + + + + + + + {% endfor %} + +
BuyerProductQtyTotal
{{ sale.order.buyer.username }}{{ sale.product.name }}{{ sale.quantity }}${{ sale.price }}
+ {% else %} +

No sales yet.

+ {% endif %} +
+{% endblock %} diff --git a/core/urls.py b/core/urls.py index 6299e3d..dcd0911 100644 --- a/core/urls.py +++ b/core/urls.py @@ -1,7 +1,13 @@ from django.urls import path +from django.contrib.auth.views import LoginView, LogoutView -from .views import home +from .views import buyer_dashboard, dashboard_redirect, home, seller_dashboard urlpatterns = [ path("", home, name="home"), + path("login/", LoginView.as_view(template_name="core/login.html"), name="login"), + path("logout/", LogoutView.as_view(next_page="home"), name="logout"), + path("dashboard/", dashboard_redirect, name="dashboard"), + path("dashboard/buyer/", buyer_dashboard, name="buyer_dashboard"), + path("dashboard/seller/", seller_dashboard, name="seller_dashboard"), ] diff --git a/core/views.py b/core/views.py index c9aed12..4d00f97 100644 --- a/core/views.py +++ b/core/views.py @@ -2,12 +2,16 @@ import os import platform from django import get_version as django_version -from django.shortcuts import render +from django.contrib.auth.decorators import login_required +from django.db.models import Count, DecimalField, ExpressionWrapper, F, Sum +from django.shortcuts import redirect, render from django.utils import timezone +from .models import Order, OrderItem, Product + def home(request): - """Render the landing screen with loader and environment details.""" + """Render the landing screen with environment details.""" host_name = request.get_host().lower() agent_brand = "AppWizzy" if host_name == "appwizzy.com" else "Flatlogic" now = timezone.now() @@ -23,3 +27,67 @@ def home(request): "project_image_url": os.getenv("PROJECT_IMAGE_URL", ""), } return render(request, "core/index.html", context) + + +def _is_seller(user): + return hasattr(user, "userprofile") and user.userprofile.is_seller + + +@login_required +def dashboard_redirect(request): + if _is_seller(request.user): + return redirect("seller_dashboard") + return redirect("buyer_dashboard") + + +@login_required +def buyer_dashboard(request): + if _is_seller(request.user): + return redirect("seller_dashboard") + + recent_orders = ( + Order.objects.filter(buyer=request.user) + .prefetch_related("items__product") + .order_by("-created_at")[:5] + ) + total_orders = Order.objects.filter(buyer=request.user).count() + + context = { + "dashboard_title": "Buyer Dashboard", + "active_tab": "buyer", + "recent_orders": recent_orders, + "total_orders": total_orders, + } + return render(request, "core/buyer_dashboard.html", context) + + +@login_required +def seller_dashboard(request): + if not _is_seller(request.user): + return redirect("buyer_dashboard") + + seller_products = Product.objects.filter(seller=request.user) + sales_expression = ExpressionWrapper( + F("quantity") * F("price"), output_field=DecimalField(max_digits=12, decimal_places=2) + ) + sales_summary = OrderItem.objects.filter(product__seller=request.user).aggregate( + total_items=Sum("quantity"), + total_revenue=Sum(sales_expression), + total_orders=Count("order", distinct=True), + ) + recent_sales = ( + OrderItem.objects.filter(product__seller=request.user) + .select_related("product", "order__buyer") + .order_by("-order__created_at")[:8] + ) + + context = { + "dashboard_title": "Seller Dashboard", + "active_tab": "seller", + "product_count": seller_products.count(), + "total_orders": sales_summary["total_orders"] or 0, + "total_items": sales_summary["total_items"] or 0, + "total_revenue": sales_summary["total_revenue"] or 0, + "recent_sales": recent_sales, + } + return render(request, "core/seller_dashboard.html", context)